cloudfoundry/cloud_controller_ng

View on GitHub
spec/acceptance/service_broker_spec.rb

Summary

Maintainability
F
4 days
Test Coverage
require 'spec_helper'

RSpec.describe 'Service Broker' do
  include VCAP::CloudController::BrokerApiHelper

  let(:small_plan) do
    {
      id: 'plan-1',
      name: 'small',
      description: 'A small shared database with 100mb storage quota and 10 connections',
      schemas: {
        service_instance: {
          create: {
            parameters: { '$schema': 'http://json-schema.org/draft-04/schema#', properties: {} }
          }
        }
      }
    }
  end
  let(:catalog_with_no_plans) do
    {
      services: [
        {
          id: 'service-guid-here',
          name: service_name,
          description: 'A MySQL-compatible relational database',
          bindable: true,
          plans: [{}]
        }
      ]
    }
  end

  let(:catalog_with_small_plan) do
    {
      services: [
        {
          id: 'service-guid-here',
          name: service_name,
          description: 'A MySQL-compatible relational database',
          bindable: true,
          plans: [
            {
              id: 'plan1-guid-here',
              name: 'small',
              description: 'A small shared database with 100mb storage quota and 10 connections'
            }
          ]
        }
      ]
    }
  end

  let(:catalog_with_large_plan) do
    {
      services: [
        {
          id: 'service-guid-here',
          name: service_name,
          description: 'A MySQL-compatible relational database',
          bindable: true,
          plans: [
            {
              id: 'plan2-guid-here',
              name: 'large',
              description: 'A large dedicated database with 10GB storage quota, 512MB of RAM, and 100 connections'
            }
          ]
        }
      ]
    }
  end

  let(:catalog_with_two_plans) do
    {
      services: [
        {
          id: 'service-guid-here',
          name: service_name,
          description: 'A MySQL-compatible relational database',
          bindable: true,
          plans: [
            {
              id: 'plan1-guid-here',
              name: 'small',
              description: 'A small shared database with 100mb storage quota and 10 connections'
            }, {
              id: 'plan2-guid-here',
              name: 'large',
              description: 'A large dedicated database with 10GB storage quota, 512MB of RAM, and 100 connections'
            }
          ]
        }
      ]
    }
  end

  before { setup_cc }

  def build_service(attrs={})
    @index ||= 0
    @index += 1
    {
      id: SecureRandom.uuid,
      name: "service-#{@index}",
      description: 'A service, duh!',
      bindable: true,
      plans: [
        {
          id: "plan-#{@index}",
          name: "plan-#{@index}",
          description: 'A plan, duh!'
        }
      ]
    }.merge(attrs)
  end

  describe 'on registration' do
    context 'when a service has no plans' do
      before do
        stub_catalog_fetch(200, {
                             services: [
                               {
                                 id: '12345',
                                 name: 'MySQL',
                                 description: 'A MySQL service, duh!',
                                 bindable: true,
                                 plans: []
                               }
                             ]
                           })
      end

      it 'notifies the operator of the problem' do
        post('/v2/service_brokers', {
          name: 'some-guid',
          broker_url: 'http://broker-url',
          auth_username: 'username',
          auth_password: 'password'
        }.to_json, admin_headers)

        expect(last_response.status).to be(502)
        expect(decoded_response['code']).to be(270_012)
        expect(decoded_response['description']).to eql("Service broker catalog is invalid: \nService MySQL\n  At least one plan is required\n")
      end
    end

    context 'when there are multiple validation problems in the catalog' do
      before do
        stub_catalog_fetch(200, {
                             services: [
                               {
                                 id: 12_345,
                                 name: 'service-1',
                                 description: 'A' * 10_001,
                                 bindable: true,
                                 bindings_retrievable: 'not-a-bool',
                                 instances_retrievable: 'not-a-bool',
                                 allow_context_updates: 'not-a-bool',
                                 plans: [
                                   {
                                     id: 'plan-1',
                                     name: 'small',
                                     description: 'B' * 10_001,
                                     schemas: {
                                       service_instance: {
                                         create: {
                                           parameters: { '$schema': 'http://json-schema.org/draft-04/schema#', properties: true }
                                         }
                                       }
                                     }
                                   }, {
                                     id: 'plan-2',
                                     name: 'large',
                                     description: 'A large dedicated database with 10GB storage quota, 512MB of RAM, and 100 connections'
                                   }
                                 ]
                               },
                               {
                                 id: '67890',
                                 name: 'service-2',
                                 description: 'Another service, duh!',
                                 bindable: true,
                                 plans: [
                                   {
                                     id: 'plan-b',
                                     name: 'small',
                                     description: 'A small shared database with 100mb storage quota and 10 connections'
                                   }, {
                                     id: 'plan-b',
                                     name: 'large',
                                     description: ''
                                   }
                                 ]
                               },
                               {
                                 id: '67890',
                                 name: 'service-3',
                                 description: 'Yet another service, duh!',
                                 bindable: true,
                                 dashboard_client: {
                                   id: 'client-1'
                                 },
                                 plans: [
                                   {
                                     id: 123,
                                     name: 'tiny',
                                     description: 'A small shared database with 100mb storage quota and 10 connections'
                                   }, {
                                     id: '456',
                                     name: 'tiny',
                                     description: 'A large dedicated database with 10GB storage quota, 512MB of RAM, and 100 connections'
                                   }
                                 ]
                               },
                               {
                                 id: '987654',
                                 name: 'service-4',
                                 description: 'Yet another service, duh!',
                                 bindable: true,
                                 dashboard_client: {
                                   id: 'client-1',
                                   secret: 'no-one-knows',
                                   redirect_uri: 'http://example.com/client-1'
                                 },
                                 plans: []
                               },
                               {
                                 id: '888444',
                                 name: 'service-4',
                                 description: 'Yet another service, duh!',
                                 bindable: true,
                                 dashboard_client: {
                                   id: 'client-9',
                                   secret: 'some-secret',
                                   redirect_uri: 'http://example.com/client-1'
                                 },
                                 plans: [
                                   {
                                     id: '999',
                                     name: 'micro',
                                     description: 'The smallest plan in the world'
                                   }
                                 ]
                               }
                             ]
                           })
      end

      it 'notifies the operator of the problem' do
        post('/v2/service_brokers', {
          name: 'some-guid',
          broker_url: 'http://broker-url',
          auth_username: 'username',
          auth_password: 'password'
        }.to_json, admin_headers)

        expect(last_response.status).to be(502)
        expect(decoded_response['code']).to be(270_012)
        expect(decoded_response['description']).to eql(
          "Service broker catalog is invalid: \n" \
          "Service ids must be unique\n" \
          "Service names must be unique within a broker\n" \
          "Plan ids must be unique. Unable to register plan with id 'plan-b' (plan name 'large', " \
          "service name 'service-2') because it uses the same id as another plan in the catalog " \
          "(plan name 'small', service name 'service-2')\n" \
          "Service dashboard_client id must be unique\n" \
          "Service service-1\n  " \
          "Service id must be a string, but has value 12345\n  " \
          "Service description may not have more than 10000 characters\n  " \
          "Service \"bindings_retrievable\" field must be a boolean, but has value \"not-a-bool\"\n  " \
          "Service \"instances_retrievable\" field must be a boolean, but has value \"not-a-bool\"\n  " \
          "Service \"allow_context_updates\" field must be a boolean, but has value \"not-a-bool\"\n  " \
          "Plan small\n    " \
          "Plan description may not have more than 10000 characters\n    " \
          "Schemas\n      " \
          'Schema service_instance.create.parameters is not valid. Must conform to JSON Schema Draft 04 (experimental support for later versions): ' \
          "The property '#/properties' of type boolean did not match the following type: object in schema " \
          "http://json-schema.org/draft-04/schema#\n" \
          "Service service-2\n  " \
          "Plan ids must be unique. Service service-2 already has a plan with id 'plan-b'\n  " \
          "Plan large\n    " \
          "Plan description is required\n" \
          "Service service-3\n  " \
          "Service dashboard client secret is required\n  " \
          "Service dashboard client redirect_uri is required\n  " \
          "Plan names must be unique within a service. Service service-3 already has a plan named tiny\n  " \
          "Plan tiny\n    " \
          "Plan id must be a string, but has value 123\n" \
          "Service service-4\n  " \
          "At least one plan is required\n"
        )
      end
    end

    context 'when a plan has a free field in the catalog' do
      before do
        stub_catalog_fetch(200, {
                             services: [
                               {
                                 id: '12345',
                                 name: 'service-1',
                                 description: 'A service, duh!',
                                 bindable: true,
                                 plans: [
                                   {
                                     id: 'plan-1',
                                     name: 'not-free-plan',
                                     description: 'A not free plan',
                                     free: false
                                   }, {
                                     id: 'plan-2',
                                     name: 'free-plan',
                                     description: 'A free plan',
                                     free: true
                                   }
                                 ]
                               }
                             ]
                           })

        post('/v2/service_brokers', {
          name: 'some-guid',
          broker_url: 'http://broker-url',
          auth_username: 'username',
          auth_password: 'password'
        }.to_json, admin_headers)
      end

      it 'sets the cc plan free field' do
        get('/v2/service_plans', {}.to_json, admin_headers)

        resources = Oj.load(last_response.body)['resources']
        not_free_plan = resources.find { |plan| plan['entity']['name'] == 'not-free-plan' }
        free_plan = resources.find { |plan| plan['entity']['name'] == 'free-plan' }

        expect(free_plan['entity']['free']).to be true
        expect(not_free_plan['entity']['free']).to be false
      end
    end

    context 'when the CC dashboard_client feature is disabled and the catalog requests a client' do
      let(:service) { build_service(dashboard_client: { id: 'client-id', secret: 'shhhhh', redirect_uri: 'http://example.com/client-id' }) }

      before do
        TestConfig.override(uaa_client_name: nil, uaa_client_secret: nil)

        stub_catalog_fetch(200, services: [service])
      end

      it 'returns a warning' do
        post('/v2/service_brokers', {
          name: 'some-guid',
          broker_url: 'http://broker-url',
          auth_username: 'username',
          auth_password: 'password'
        }.to_json, admin_headers)

        warning = CGI.unescape(last_response.headers['X-Cf-Warnings'])
        expect(warning).to eq(VCAP::Services::SSO::DashboardClientManager::REQUESTED_FEATURE_DISABLED_WARNING)
      end

      it 'does not create any dashboard clients' do
        post('/v2/service_brokers', {
          name: 'some-guid',
          broker_url: 'http://broker-url',
          auth_username: 'username',
          auth_password: 'password'
        }.to_json, admin_headers)

        expect(VCAP::CloudController::ServiceDashboardClient.count).to eq(0)
      end
    end

    context 'when a schema' do
      [
        { type: 'service_instance', actions: %w[create update] },
        { type: 'service_binding', actions: ['create'] }
      ].each do |test|
        test[:actions].each do |schema_action|
          context "of type #{test[:type]} and action #{schema_action} is not present" do
            {
              "#{schema_action} is nil": { test[:type] => { schema_action => nil } },
              "#{schema_action} is nil": { test[:type] => { schema_action => { 'parameters' => nil } } },
              "#{schema_action} is empty object": { test[:type] => { schema_action => {} } }
            }.each do |desc, schema|
              context "#{desc} #{schema}" do
                before do
                  stub_catalog_fetch(200, default_catalog(plan_schemas: schema))
                end

                it 'succeeds' do
                  post('/v2/service_brokers', {
                    name: 'some-guid',
                    broker_url: 'http://broker-url',
                    auth_username: 'username',
                    auth_password: 'password'
                  }.to_json, admin_headers)

                  expect(last_response.status).to be(201)
                end
              end
            end
          end

          context "of type #{test[:type]} and action #{schema_action} has a valid schema" do
            let(:schema) { { (test[:type]).to_s => { schema_action => { 'parameters' => { '$schema': 'http://json-schema.org/draft-04/schema#', type: 'object' } } } } }

            before do
              stub_catalog_fetch(200, default_catalog(plan_schemas: schema))
            end

            it 'succeeds' do
              post('/v2/service_brokers', {
                name: 'some-guid',
                broker_url: 'http://broker-url',
                auth_username: 'username',
                auth_password: 'password'
              }.to_json, admin_headers)

              expect(last_response.status).to be(201)
            end
          end

          context "of type #{test[:type]} and action #{schema_action} is not a JSON object" do
            {
              "#{test[:type]}.#{schema_action}": { (test[:type]).to_s => { schema_action => true } },
              "#{test[:type]}.#{schema_action}.parameters": { (test[:type]).to_s => { schema_action => { 'parameters' => true } } }
            }.each do |path, schema|
              context "operator receives an error about #{path} #{schema}" do
                before do
                  stub_catalog_fetch(200, default_catalog(plan_schemas: schema))
                end

                it 'rejects the request' do
                  post('/v2/service_brokers', {
                    name: 'some-guid',
                    broker_url: 'http://broker-url',
                    auth_username: 'username',
                    auth_password: 'password'
                  }.to_json, admin_headers)

                  expect(last_response.status).to be(502)
                  expect(decoded_response['code']).to be(270_012)
                  expect(decoded_response['description']).to eql(
                    "Service broker catalog is invalid: \n" \
                    "Service MySQL\n  " \
                    "Plan small\n    " \
                    "Schemas\n      " \
                    "Schemas #{path} must be a hash, but has value true\n"
                  )
                end
              end
            end
          end

          context "of type #{test[:type]} and action #{schema_action} does not conform to JSON Schema Draft 04 (experimental support for later versions)" do
            let(:path) { "#{test[:type]}.#{schema_action}.parameters" }
            let(:schema) { { (test[:type]).to_s => { schema_action => { 'parameters' => { '$schema': 'http://json-schema.org/draft-04/schema#', properties: true } } } } }

            before do
              stub_catalog_fetch(200, default_catalog(plan_schemas: schema))
            end

            it 'rejects the request' do
              post('/v2/service_brokers', {
                name: 'some-guid',
                broker_url: 'http://broker-url',
                auth_username: 'username',
                auth_password: 'password'
              }.to_json, admin_headers)

              expect(last_response.status).to be(502)
              expect(decoded_response['code']).to be(270_012)
              expect(decoded_response['description']).to eql(
                "Service broker catalog is invalid: \n" \
                "Service MySQL\n  " \
                "Plan small\n    " \
                "Schemas\n      " \
                "Schema #{path} is not valid. Must conform to JSON Schema Draft 04 (experimental support for later versions): The property '#/properties' " \
                "of type boolean did not match the following type: object in schema http://json-schema.org/draft-04/schema#\n"
              )
            end
          end

          context "of type #{test[:type]} and action #{schema_action} does not conform to JSON Schema Draft 04 (experimental support for later versions) with multiple problems" do
            let(:path) { "#{test[:type]}.#{schema_action}.parameters" }
            let(:schema) do
              {
                (test[:type]).to_s => {
                  schema_action => {
                    'parameters' => {
                      '$schema': 'http://json-schema.org/draft-04/schema#',
                      properties: true,
                      anyOf: true
                    }
                  }
                }
              }
            end

            before do
              stub_catalog_fetch(200, default_catalog(plan_schemas: schema))
            end

            it 'responds with invalid' do
              post('/v2/service_brokers', {
                name: 'some-guid',
                broker_url: 'http://broker-url',
                auth_username: 'username',
                auth_password: 'password'
              }.to_json, admin_headers)

              expect(last_response.status).to be(502)
              expect(decoded_response['code']).to be(270_012)
              expect(decoded_response['description']).to eql(
                "Service broker catalog is invalid: \n" \
                "Service MySQL\n  " \
                "Plan small\n    " \
                "Schemas\n      " \
                "Schema #{path} is not valid. Must conform to JSON Schema Draft 04 (experimental support for later versions): The property '#/properties' " \
                "of type boolean did not match the following type: object in schema http://json-schema.org/draft-04/schema#\n      " \
                "Schema #{path} is not valid. Must conform to JSON Schema Draft 04 (experimental support for later versions): The property '#/anyOf' " \
                "of type boolean did not match the following type: array in schema http://json-schema.org/draft-04/schema#\n"
              )
            end
          end

          context "of type #{test[:type]} and action #{schema_action} has an external schema" do
            let(:path) { "#{test[:type]}.#{schema_action}.parameters" }
            let(:schema) { { (test[:type]).to_s => { schema_action => { 'parameters' => { '$schema': 'http://example.com/schema', type: 'object' } } } } }

            before do
              stub_catalog_fetch(200, default_catalog(plan_schemas: schema))
            end

            it 'responds with invalid' do
              post('/v2/service_brokers', {
                name: 'some-guid',
                broker_url: 'http://broker-url',
                auth_username: 'username',
                auth_password: 'password'
              }.to_json, admin_headers)

              expect(last_response.status).to be(502)
              expect(decoded_response['code']).to be(270_012)
              expect(decoded_response['description']).to eql(
                "Service broker catalog is invalid: \n" \
                "Service MySQL\n  " \
                "Plan small\n    " \
                "Schemas\n      " \
                "Schema #{path} is not valid. Custom meta schemas are not supported.\n"
              )
            end
          end

          context "of type #{test[:type]} and action #{schema_action} has an external uri reference" do
            let(:path) { "#{test[:type]}.#{schema_action}.parameters" }
            let(:schema) do
              {
                (test[:type]).to_s => {
                  schema_action => {
                    'parameters' => {
                      '$schema': 'http://json-schema.org/draft-04/schema#',
                      '$ref': 'http://example.com/ref'
                    }
                  }
                }
              }
            end

            before do
              stub_catalog_fetch(200, default_catalog(plan_schemas: schema))
            end

            it 'responds with invalid' do
              post('/v2/service_brokers', {
                name: 'some-guid',
                broker_url: 'http://broker-url',
                auth_username: 'username',
                auth_password: 'password'
              }.to_json, admin_headers)

              expect(last_response.status).to be(502)
              expect(decoded_response['code']).to be(270_012)
              expect(decoded_response['description']).to eql(
                "Service broker catalog is invalid: \n" \
                "Service MySQL\n  " \
                "Plan small\n    " \
                "Schemas\n      " \
                "Schema #{path} is not valid. No external references are allowed: Read of URI at http://example.com/ref refused\n"
              )
            end
          end

          context "of type #{test[:type]} and action #{schema_action} has no $schema" do
            let(:path) { "#{test[:type]}.#{schema_action}.parameters" }
            let(:schema) { { (test[:type]).to_s => { schema_action => { 'parameters' => { type: 'object' } } } } }

            before do
              stub_catalog_fetch(200, default_catalog(plan_schemas: schema))
            end

            it 'responds with invalid' do
              post('/v2/service_brokers', {
                name: 'some-guid',
                broker_url: 'http://broker-url',
                auth_username: 'username',
                auth_password: 'password'
              }.to_json, admin_headers)

              expect(last_response.status).to be(502)
              expect(decoded_response['code']).to be(270_012)
              expect(decoded_response['description']).to eql(
                "Service broker catalog is invalid: \n" \
                "Service MySQL\n  " \
                "Plan small\n    " \
                "Schemas\n      " \
                "Schema #{path} is not valid. Schema must have $schema key but was not present\n"
              )
            end
          end
        end
      end
    end

    context 'when multiple schemas have validation issues' do
      let(:schema) do
        {
          'service_instance' => {
            'create' => { 'parameters' => { '$schema': 'http://json-schema.org/draft-04/schema#', '$ref': 'http://example.com/create' } },
            'update' => { 'parameters' => { '$schema': 'http://json-schema.org/draft-04/schema#', '$ref': 'http://example.com/update' } }
          },
          'service_binding' => {
            'create' => { 'parameters' => { '$schema': 'http://json-schema.org/draft-04/schema#', '$ref': 'http://example.com/binding' } }
          }
        }
      end

      before do
        stub_catalog_fetch(200, default_catalog(plan_schemas: schema))
      end

      it 'reports all issues' do
        post('/v2/service_brokers', {
          name: 'some-guid',
          broker_url: 'http://broker-url',
          auth_username: 'username',
          auth_password: 'password'
        }.to_json, admin_headers)

        expect(last_response.status).to be(502)
        expect(decoded_response['code']).to be(270_012)
        expect(decoded_response['description']).to eql(
          "Service broker catalog is invalid: \n" \
          "Service MySQL\n  " \
          "Plan small\n    " \
          "Schemas\n      " \
          "Schema service_instance.create.parameters is not valid. No external references are allowed: Read of URI at http://example.com/create refused\n      " \
          "Schema service_instance.update.parameters is not valid. No external references are allowed: Read of URI at http://example.com/update refused\n      " \
          "Schema service_binding.create.parameters is not valid. No external references are allowed: Read of URI at http://example.com/binding refused\n"
        )
      end
    end

    context 'when multiple schemas appear in multiple plans for multiple services' do
      let(:schema) do
        {
          'service_instance' => {
            'create' => { 'parameters' => { '$schema' => 'http://json-schema.org/draft-04/schema#' } },
            'update' => { 'parameters' => { '$schema' => 'http://json-schema.org/draft-04/schema#' } }
          },
          'service_binding' => {
            'create' => { 'parameters' => { '$schema' => 'http://json-schema.org/draft-04/schema#' } }
          }
        }
      end

      let(:catalog_with_two_services_two_plans_schemas) do
        {
          services:
            [
              {
                id: 'service-guid-here',
                name: service_name,
                description: 'A MySQL-compatible relational database',
                bindable: true,
                plans: [
                  {
                    id: 'plan1-guid-here',
                    name: 'plan1',
                    description: 'A small shared database with 100mb storage quota and 10 connections',
                    schemas: schema
                  }, {
                    id: 'plan2-guid-here',
                    name: 'plan2',
                    description: 'A large dedicated database with 10GB storage quota, 512MB of RAM, and 100 connections',
                    schemas: schema
                  }
                ]
              },
              {
                id: 'service-guid-here-2',
                name: "#{service_name}-2",
                description: 'A MySQL-compatible relational database',
                bindable: true,
                plans: [
                  {
                    id: 'plan3-guid-here',
                    name: 'plan3',
                    description: 'A small shared database with 100mb storage quota and 10 connections',
                    schemas: schema
                  }, {
                    id: 'plan4-guid-here',
                    name: 'plan4',
                    description: 'A large dedicated database with 10GB storage quota, 512MB of RAM, and 100 connections',
                    schemas: schema
                  }
                ]
              }
            ]
        }
      end

      before do
        stub_catalog_fetch(200, catalog_with_two_services_two_plans_schemas)
        post('/v2/service_brokers',
             { name: 'broker-name', broker_url: 'http://broker-url', auth_username: 'username', auth_password: 'password' }.to_json,
             admin_headers)
      end

      it 'registers all schemas successfully' do
        expect(last_response.status).to eq(201)
        get('/v2/service_plans', {}.to_json, admin_headers)
        resources = Oj.load(last_response.body)['resources']

        expect(resources.length).to eq(4)

        resources.each do |plan|
          expect(plan['entity']['schemas']).to eq(schema)
        end
      end
    end

    context 'when a service broker already exists with the same URL' do
      before do
        stub_catalog_fetch(200, catalog_with_small_plan)

        post('/v2/service_brokers', {
          name: 'some-broker',
          broker_url: 'http://broker-url',
          auth_username: 'username',
          auth_password: 'password'
        }.to_json, admin_headers)
      end

      it 'fails when the provided broker name is the same' do
        post('/v2/service_brokers', {
          name: 'some-broker',
          broker_url: 'http://broker-url',
          auth_username: 'username',
          auth_password: 'password'
        }.to_json, admin_headers)
        expect(last_response.status).to be(400)
        expect(decoded_response['code']).to be(270_002)
        expect(decoded_response['description']).to eql('The service broker name is taken')
      end

      it 'succeeds when the provided broker name is different' do
        post('/v2/service_brokers', {
          name: 'some-other-broker',
          broker_url: 'http://broker-url',
          auth_username: 'username',
          auth_password: 'password'
        }.to_json, admin_headers)
        expect(last_response.status).to be(201)
      end
    end

    context 'when a service broker already exists with a different URL but the same catalog' do
      before do
        stub_catalog_fetch(200, catalog_with_small_plan, 'broker-url')
        stub_catalog_fetch(200, catalog_with_small_plan, 'different-broker-url')

        post('/v2/service_brokers', {
          name: 'some-broker',
          broker_url: 'http://broker-url',
          auth_username: 'username',
          auth_password: 'password'
        }.to_json, admin_headers)
        expect(last_response).to have_status_code(201)
      end

      it 'succeeds when the provided broker name is different' do
        post('/v2/service_brokers', {
          name: 'some-other-broker',
          broker_url: 'http://different-broker-url',
          auth_username: 'username',
          auth_password: 'password'
        }.to_json, admin_headers)
        expect(last_response).to have_status_code(201)
      end

      it 'fails when the provided broker name is the same' do
        post('/v2/service_brokers', {
          name: 'some-broker',
          broker_url: 'http://different-broker-url',
          auth_username: 'username',
          auth_password: 'password'
        }.to_json, admin_headers)
        expect(last_response).to have_status_code(400)
        expect(decoded_response['code']).to be(270_002)
        expect(decoded_response['description']).to eql('The service broker name is taken')
      end
    end

    context 'when a service broker already exists with the same dashboard_client client_id' do
      before do
        UAARequests.stub_all
        stub_request(:get, %r{https://uaa.service.cf.internal/oauth/clients/client-1}).to_return(status: 404)
        stub_catalog_fetch(200, catalog_with_small_plan)

        post('/v2/service_brokers', {
          name: 'some-broker',
          broker_url: 'http://broker-url',
          auth_username: 'username',
          auth_password: 'password'
        }.to_json, admin_headers)
        expect(last_response).to have_status_code(201)
      end

      it 'fails with a uniqueness error' do
        stub_request(:get, %r{https://uaa.service.cf.internal/oauth/clients/client-1}).to_return(
          body: { client_id: 'client-1' }.to_json,
          status: 200,
          headers: { 'content-type' => 'application/json' }
        )

        stub_catalog_fetch(200, {
                             services: [
                               {
                                 id: '12345',
                                 name: 'MySQL',
                                 description: 'A MySQL service, duh!',
                                 bindable: true,
                                 plans: [small_plan],
                                 dashboard_client: { id: 'client-1', secret: 'shhhhh', redirect_uri: 'http://example.com/client-id' }
                               }
                             ]
                           }, 'some-other-broker-url')

        post('/v2/service_brokers', {
          name: 'some-other-broker',
          broker_url: 'http://some-other-broker-url',
          auth_username: 'username',
          auth_password: 'password'
        }.to_json, admin_headers)

        expect(last_response).to have_status_code(502)
        expect(decoded_response['code']).to be(270_012)
        expect(decoded_response['description']).to match('Service dashboard client id must be unique')
      end
    end
  end

  describe 'updating a service broker' do
    context 'when the dashboard_client values for a service have changed' do
      let(:service_1) { build_service(dashboard_client: { id: 'client-1', secret: 'shhhhh', redirect_uri: 'http://example.com/client-1' }) }
      let(:service_2) { build_service(dashboard_client: { id: 'client-2', secret: 'sekret', redirect_uri: 'http://example.com/client-2' }) }
      let(:service_3) { build_service(dashboard_client: { id: 'client-3', secret: 'unguessable', redirect_uri: 'http://example.com/client-3' }) }
      let(:service_4) { build_service }
      let(:service_5) { build_service(dashboard_client: { id: 'client-5', secret: 'secret5', redirect_uri: 'http://example.com/client-5' }) }
      let(:service_6) { build_service(dashboard_client: { id: 'client-6', secret: 'secret6', redirect_uri: 'http://example.com/client-6' }) }

      before do
        # set up a fake broker catalog that includes dashboard_client for services
        stub_catalog_fetch(200, services: [service_1, service_2, service_3, service_4, service_5, service_6])
        UAARequests.stub_all
        stub_request(:get, %r{https://uaa.service.cf.internal/oauth/clients/.*}).to_return(status: 404)

        # add that broker to the CC
        post('/v2/service_brokers',
             {
               name: 'broker_name',
               broker_url: stubbed_broker_url,
               auth_username: stubbed_broker_username,
               auth_password: stubbed_broker_password
             }.to_json,
             admin_headers)
        expect(last_response).to have_status_code(201)
        @service_broker_guid = decoded_response.fetch('metadata').fetch('guid')

        WebMock.reset!

        UAARequests.stub_all
        stub_request(:get, %r{https://uaa.service.cf.internal/oauth/clients/.*}).to_return(status: 404)
        stub_request(:get, %r{https://uaa.service.cf.internal/oauth/clients/client-1}).to_return(
          body: { client_id: 'client-1' }.to_json,
          status: 200,
          headers: { 'content-type' => 'application/json' }
        )
        stub_request(:get, %r{https://uaa.service.cf.internal/oauth/clients/client-2}).to_return(
          body: { client_id: 'client-2' }.to_json,
          status: 200,
          headers: { 'content-type' => 'application/json' }
        )
        stub_request(:get, %r{https://uaa.service.cf.internal/oauth/clients/client-3}).to_return(
          body: { client_id: 'client-3' }.to_json,
          status: 200,
          headers: { 'content-type' => 'application/json' }
        )
        stub_request(:get, %r{https://uaa.service.cf.internal/oauth/clients/client-5}).to_return(
          body: { client_id: 'client-5' }.to_json,
          status: 200,
          headers: { 'content-type' => 'application/json' }
        )
        stub_request(:get, %r{https://uaa.service.cf.internal/oauth/clients/client-6}).to_return(
          body: { client_id: 'client-6' }.to_json,
          status: 200,
          headers: { 'content-type' => 'application/json' }
        )

        # delete client
        service_1.delete(:dashboard_client)
        # change client id - should result in a delete and a create
        service_2[:dashboard_client][:id] = 'different-client'
        # change client secret - should post to /clients/<client-id>/secret
        service_3[:dashboard_client][:secret] = 'SUPERsecret'
        # add client
        service_4[:dashboard_client] = { id: 'client-4', secret: '1337', redirect_uri: 'http://example.com/client-4' }
        # change property other than ID or secret
        service_5[:dashboard_client][:redirect_uri] = 'http://nowhere.net'

        stub_catalog_fetch(200, services: [service_1, service_2, service_3, service_4, service_5, service_6])

        stub_request(:post, %r{https://uaa.service.cf.internal/oauth/clients/tx/modify}).
          to_return(
            status: 200,
            headers: { 'content-type' => 'application/json' },
            body: ''
          )
      end

      it 'sends the correct batch request to create/update/delete clients' do
        put("/v2/service_brokers/#{@service_broker_guid}", '{}', admin_headers)

        expect(last_response).to have_status_code(200)

        expected_client_modifications = [
          { # client deleted
            'client_id' => 'client-1',
            'client_secret' => nil,
            'redirect_uri' => nil,
            'scope' => ['openid', 'cloud_controller_service_permissions.read'],
            'authorities' => ['uaa.resource'],
            'authorized_grant_types' => ['authorization_code'],
            'action' => 'delete'
          },
          { # client id renamed to 'different-client'
            'client_id' => 'client-2',
            'client_secret' => nil,
            'redirect_uri' => nil,
            'scope' => ['openid', 'cloud_controller_service_permissions.read'],
            'authorities' => ['uaa.resource'],
            'authorized_grant_types' => ['authorization_code'],
            'action' => 'delete'
          },
          { # client id renamed from 'client-2'
            'client_id' => 'different-client',
            'client_secret' => service_2[:dashboard_client][:secret],
            'redirect_uri' => service_2[:dashboard_client][:redirect_uri],
            'scope' => ['openid', 'cloud_controller_service_permissions.read'],
            'authorities' => ['uaa.resource'],
            'authorized_grant_types' => ['authorization_code'],
            'action' => 'add'
          },
          { # client secret updated
            'client_id' => service_3[:dashboard_client][:id],
            'client_secret' => 'SUPERsecret',
            'redirect_uri' => service_3[:dashboard_client][:redirect_uri],
            'scope' => ['openid', 'cloud_controller_service_permissions.read'],
            'authorities' => ['uaa.resource'],
            'authorized_grant_types' => ['authorization_code'],
            'action' => 'update,secret'
          },
          { # newly added client
            'client_id' => service_4[:dashboard_client][:id],
            'client_secret' => service_4[:dashboard_client][:secret],
            'redirect_uri' => service_4[:dashboard_client][:redirect_uri],
            'scope' => ['openid', 'cloud_controller_service_permissions.read'],
            'authorities' => ['uaa.resource'],
            'authorized_grant_types' => ['authorization_code'],
            'action' => 'add'
          },
          { # client redirect_uri updated
            'client_id' => service_5[:dashboard_client][:id],
            'client_secret' => service_5[:dashboard_client][:secret],
            'redirect_uri' => 'http://nowhere.net',
            'scope' => ['openid', 'cloud_controller_service_permissions.read'],
            'authorities' => ['uaa.resource'],
            'authorized_grant_types' => ['authorization_code'],
            'action' => 'update,secret'
          },
          { # no change
            'client_id' => service_6[:dashboard_client][:id],
            'client_secret' => service_6[:dashboard_client][:secret],
            'redirect_uri' => service_6[:dashboard_client][:redirect_uri],
            'scope' => ['openid', 'cloud_controller_service_permissions.read'],
            'authorities' => ['uaa.resource'],
            'authorized_grant_types' => ['authorization_code'],
            'action' => 'update,secret'
          }
        ]

        expect(a_request(:post, 'https://uaa.service.cf.internal/oauth/clients/tx/modify').with do |req|
          client_modifications = Oj.load(req.body)
          expect(client_modifications).to match_array(expected_client_modifications)
        end).to have_been_made
      end

      it 'can update the service broker name' do
        put("/v2/service_brokers/#{@service_broker_guid}", '{"name":"new_broker_name"}',
            admin_headers)

        expect(last_response).to have_status_code(200)

        parsed_body = Oj.load(last_response.body)
        expect(parsed_body['entity']['name']).to eq('new_broker_name')
      end
    end

    context 'when the free field for a plan has changed' do
      before do
        stub_catalog_fetch(200, {
                             services: [
                               {
                                 id: '12345',
                                 name: 'service-1',
                                 description: 'A service, duh!',
                                 bindable: true,
                                 plans: [
                                   {
                                     id: 'plan-1',
                                     name: 'not-free-plan',
                                     description: 'A not free plan',
                                     free: false
                                   }, {
                                     id: 'plan-2',
                                     name: 'free-plan',
                                     description: 'A free plan',
                                     free: true
                                   }
                                 ]
                               }
                             ]
                           })

        post('/v2/service_brokers', {
          name: 'some-guid',
          broker_url: 'http://broker-url',
          auth_username: 'username',
          auth_password: 'password'
        }.to_json, admin_headers)

        guid = VCAP::CloudController::ServiceBroker.first.guid

        stub_catalog_fetch(200, {
                             services: [
                               {
                                 id: '12345',
                                 name: 'service-1',
                                 description: 'A service, duh!',
                                 bindable: true,
                                 plans: [
                                   {
                                     id: 'plan-1',
                                     name: 'not-free-plan',
                                     description: 'A not free plan',
                                     free: true
                                   }, {
                                     id: 'plan-2',
                                     name: 'free-plan',
                                     description: 'A free plan',
                                     free: false
                                   }
                                 ]
                               }
                             ]
                           })

        put("/v2/service_brokers/#{guid}", {
          name: 'some-guid',
          broker_url: 'http://broker-url',
          auth_username: 'username',
          auth_password: 'password'
        }.to_json, admin_headers)
      end

      it 'sets the cc plan free field' do
        get('/v2/service_plans', {}.to_json, admin_headers)

        resources = Oj.load(last_response.body)['resources']
        no_longer_not_free_plan = resources.find { |plan| plan['entity']['name'] == 'not-free-plan' }
        no_longer_free_plan = resources.find { |plan| plan['entity']['name'] == 'free-plan' }

        expect(no_longer_free_plan['entity']['free']).to be false
        expect(no_longer_not_free_plan['entity']['free']).to be true
      end
    end

    context 'when the allow_context_updates field for a service has changed' do
      before do
        stub_catalog_fetch(200, {
                             services: [
                               {
                                 id: '12345',
                                 name: 'allow-context-updates-service',
                                 description: 'A service, duh!',
                                 bindable: true,
                                 bindings_retrievable: true,
                                 allow_context_updates: true,
                                 plans: [
                                   {
                                     id: 'plan-1',
                                     name: 'random-name-1',
                                     description: 'A not free plan'
                                   }
                                 ]
                               }, {
                                 id: '123456',
                                 name: 'not-allow-context-updates-service',
                                 description: 'A service, duh!',
                                 bindable: true,
                                 bindings_retrievable: false,
                                 allow_context_updates: false,
                                 plans: [
                                   {
                                     id: 'plan-2',
                                     name: 'random-name-2',
                                     description: 'A not free plan'
                                   }
                                 ]
                               }, {
                                 id: '1234567',
                                 name: 'allow-context-updates-service-will-be-unset',
                                 description: 'A service, duh!',
                                 bindable: true,
                                 bindings_retrievable: true,
                                 allow_context_updates: true,
                                 plans: [
                                   {
                                     id: 'plan-3',
                                     name: 'random-name-2',
                                     description: 'A not free plan'
                                   }
                                 ]
                               }
                             ]
                           })

        post('/v2/service_brokers', {
          name: 'some-guid',
          broker_url: 'http://broker-url',
          auth_username: 'username',
          auth_password: 'password'
        }.to_json, admin_headers)

        guid = VCAP::CloudController::ServiceBroker.first.guid

        stub_catalog_fetch(200, {
                             services: [
                               {
                                 id: '12345',
                                 name: 'allow-context-updates-service',
                                 description: 'A service, duh!',
                                 bindable: true,
                                 bindings_retrievable: false,
                                 allow_context_updates: false,
                                 plans: [
                                   {
                                     id: 'plan-1',
                                     name: 'random-name-1',
                                     description: 'A not free plan'
                                   }
                                 ]
                               },
                               {
                                 id: '123456',
                                 name: 'not-allow-context-updates-service',
                                 description: 'a service, duh!',
                                 bindable: true,
                                 bindings_retrievable: true,
                                 allow_context_updates: true,
                                 plans: [
                                   {
                                     id: 'plan-2',
                                     name: 'random-name-2',
                                     description: 'a not free plan'
                                   }
                                 ]
                               }, {
                                 id: '1234567',
                                 name: 'allow-context-updates-service-will-be-unset',
                                 description: 'A service, duh!',
                                 bindable: true,
                                 plans: [
                                   {
                                     id: 'plan-3',
                                     name: 'random-name-2',
                                     description: 'A not free plan'
                                   }
                                 ]
                               }
                             ]
                           })

        put("/v2/service_brokers/#{guid}", {
          name: 'some-guid',
          broker_url: 'http://broker-url',
          auth_username: 'username',
          auth_password: 'password'
        }.to_json, admin_headers)
      end

      it 'sets the cc service allow_context_updates field' do
        get('/v2/services', {}.to_json, admin_headers)
        expect(last_response).to have_status_code(200)

        resources = Oj.load(last_response.body)['resources']

        no_longer_allow_context_updates_service = resources.find { |service| service['entity']['label'] == 'allow-context-updates-service' }
        now_allow_context_updates_service = resources.find { |service| service['entity']['label'] == 'not-allow-context-updates-service' }
        unset_allow_context_updates_service = resources.find { |service| service['entity']['label'] == 'allow-context-updates-service-will-be-unset' }

        expect(no_longer_allow_context_updates_service['entity']['allow_context_updates']).to be false
        expect(now_allow_context_updates_service['entity']['allow_context_updates']).to be true
        expect(unset_allow_context_updates_service['entity']['allow_context_updates']).to be false
      end
    end

    context 'when the bindings_retrievable field for a service has changed' do
      before do
        stub_catalog_fetch(200, {
                             services: [
                               {
                                 id: '12345',
                                 name: 'bindings-retrievable-service',
                                 description: 'A service, duh!',
                                 bindable: true,
                                 bindings_retrievable: true,
                                 plans: [
                                   {
                                     id: 'plan-1',
                                     name: 'random-name-1',
                                     description: 'A not free plan'
                                   }
                                 ]
                               }, {
                                 id: '123456',
                                 name: 'bindings-not-retrievable-service',
                                 description: 'A service, duh!',
                                 bindable: true,
                                 bindings_retrievable: false,
                                 plans: [
                                   {
                                     id: 'plan-2',
                                     name: 'random-name-2',
                                     description: 'A not free plan'
                                   }
                                 ]
                               }, {
                                 id: '1234567',
                                 name: 'bindings-retrievable-service-will-be-unset',
                                 description: 'A service, duh!',
                                 bindable: true,
                                 bindings_retrievable: true,
                                 plans: [
                                   {
                                     id: 'plan-3',
                                     name: 'random-name-2',
                                     description: 'A not free plan'
                                   }
                                 ]
                               }
                             ]
                           })

        post('/v2/service_brokers', {
          name: 'some-guid',
          broker_url: 'http://broker-url',
          auth_username: 'username',
          auth_password: 'password'
        }.to_json, admin_headers)

        guid = VCAP::CloudController::ServiceBroker.first.guid

        stub_catalog_fetch(200, {
                             services: [
                               {
                                 id: '12345',
                                 name: 'bindings-retrievable-service',
                                 description: 'A service, duh!',
                                 bindable: true,
                                 bindings_retrievable: false,
                                 plans: [
                                   {
                                     id: 'plan-1',
                                     name: 'random-name-1',
                                     description: 'A not free plan'
                                   }
                                 ]
                               },
                               {
                                 id: '123456',
                                 name: 'bindings-not-retrievable-service',
                                 description: 'a service, duh!',
                                 bindable: true,
                                 bindings_retrievable: true,
                                 plans: [
                                   {
                                     id: 'plan-2',
                                     name: 'random-name-2',
                                     description: 'a not free plan'
                                   }
                                 ]
                               }, {
                                 id: '1234567',
                                 name: 'bindings-retrievable-service-will-be-unset',
                                 description: 'A service, duh!',
                                 bindable: true,
                                 plans: [
                                   {
                                     id: 'plan-3',
                                     name: 'random-name-2',
                                     description: 'A not free plan'
                                   }
                                 ]
                               }
                             ]
                           })

        put("/v2/service_brokers/#{guid}", {
          name: 'some-guid',
          broker_url: 'http://broker-url',
          auth_username: 'username',
          auth_password: 'password'
        }.to_json, admin_headers)
      end

      it 'sets the cc service bindings_retrievable field' do
        get('/v2/services', {}.to_json, admin_headers)

        resources = Oj.load(last_response.body)['resources']

        no_longer_bindings_retrievable_service = resources.find { |service| service['entity']['label'] == 'bindings-retrievable-service' }
        now_bindings_retrievable_service = resources.find { |service| service['entity']['label'] == 'bindings-not-retrievable-service' }
        unset_bindings_retrievable_service = resources.find { |service| service['entity']['label'] == 'bindings-retrievable-service-will-be-unset' }

        expect(no_longer_bindings_retrievable_service['entity']['bindings_retrievable']).to be false
        expect(now_bindings_retrievable_service['entity']['bindings_retrievable']).to be true
        expect(unset_bindings_retrievable_service['entity']['bindings_retrievable']).to be false
      end
    end

    context 'when the instances_retrievable field for a service has changed' do
      before do
        stub_catalog_fetch(200, {
                             services: [
                               {
                                 id: '12345',
                                 name: 'instances-retrievable-service',
                                 description: 'A service, duh!',
                                 bindable: true,
                                 instances_retrievable: true,
                                 plans: [
                                   {
                                     id: 'plan-1',
                                     name: 'random-name-1',
                                     description: 'A not free plan'
                                   }
                                 ]
                               }, {
                                 id: '123456',
                                 name: 'instances-not-retrievable-service',
                                 description: 'A service, duh!',
                                 bindable: true,
                                 instances_retrievable: false,
                                 plans: [
                                   {
                                     id: 'plan-2',
                                     name: 'random-name-2',
                                     description: 'A not free plan'
                                   }
                                 ]
                               }, {
                                 id: '1234567',
                                 name: 'instances-retrievable-service-will-be-unset',
                                 description: 'A service, duh!',
                                 bindable: true,
                                 instances_retrievable: true,
                                 plans: [
                                   {
                                     id: 'plan-3',
                                     name: 'random-name-2',
                                     description: 'A not free plan'
                                   }
                                 ]
                               }
                             ]
                           })

        post('/v2/service_brokers', {
          name: 'some-guid',
          broker_url: 'http://broker-url',
          auth_username: 'username',
          auth_password: 'password'
        }.to_json, admin_headers)

        guid = VCAP::CloudController::ServiceBroker.first.guid

        stub_catalog_fetch(200, {
                             services: [
                               {
                                 id: '12345',
                                 name: 'instances-retrievable-service',
                                 description: 'A service, duh!',
                                 bindable: true,
                                 instances_retrievable: false,
                                 plans: [
                                   {
                                     id: 'plan-1',
                                     name: 'random-name-1',
                                     description: 'A not free plan'
                                   }
                                 ]
                               },
                               {
                                 id: '123456',
                                 name: 'instances-not-retrievable-service',
                                 description: 'a service, duh!',
                                 bindable: true,
                                 instances_retrievable: true,
                                 plans: [
                                   {
                                     id: 'plan-2',
                                     name: 'random-name-2',
                                     description: 'a not free plan'
                                   }
                                 ]
                               }, {
                                 id: '1234567',
                                 name: 'instances-retrievable-service-will-be-unset',
                                 description: 'A service, duh!',
                                 bindable: true,
                                 plans: [
                                   {
                                     id: 'plan-3',
                                     name: 'random-name-2',
                                     description: 'A not free plan'
                                   }
                                 ]
                               }
                             ]
                           })

        put("/v2/service_brokers/#{guid}", {
          name: 'some-guid',
          broker_url: 'http://broker-url',
          auth_username: 'username',
          auth_password: 'password'
        }.to_json, admin_headers)
      end

      it 'sets the cc service instances_retrievable field' do
        get('/v2/services', {}.to_json, admin_headers)

        resources = Oj.load(last_response.body)['resources']

        no_longer_instances_retrievable_service = resources.find { |service| service['entity']['label'] == 'instances-retrievable-service' }
        now_instances_retrievable_service = resources.find { |service| service['entity']['label'] == 'instances-not-retrievable-service' }
        unset_instances_retrievable_service = resources.find { |service| service['entity']['label'] == 'instances-retrievable-service-will-be-unset' }

        expect(no_longer_instances_retrievable_service['entity']['instances_retrievable']).to be false
        expect(now_instances_retrievable_service['entity']['instances_retrievable']).to be true
        expect(unset_instances_retrievable_service['entity']['instances_retrievable']).to be false
      end
    end

    context 'when a plan is re-named, and another plan is added to the front of the list of plans with the old name' do
      let(:catalog1) do
        {
          services: [
            {
              id: 'service-guid-here',
              name: service_name,
              description: 'A MySQL-compatible relational database',
              bindable: true,
              plans: [
                {
                  id: 'plan1-guid-here',
                  name: 'small',
                  description: 'A small shared database with 100mb storage quota and 10 connections'
                }
              ]
            }
          ]
        }
      end

      let(:catalog2) do
        {
          services: [
            {
              id: 'service-guid-here',
              name: service_name,
              description: 'A MySQL-compatible relational database',
              bindable: true,
              plans: [
                {
                  id: 'plan2-guid-here',
                  name: 'small',
                  description: 'A small shared database with 100mb storage quota and 10 connections'
                }, {
                  id: 'plan1-guid-here',
                  name: 'small-legacy',
                  description: '(Legacy) A small shared database with 100mb storage quota and 10 connections'
                }
              ]
            }
          ]
        }
      end

      it 'renames the plan and adds a new plan with the old name' do
        setup_broker(catalog1)
        update_broker(catalog2)
        expect(last_response).to have_status_code(200)

        expect(VCAP::CloudController::ServicePlan.find(unique_id: 'plan2-guid-here')[:name]).to eq('small')
        expect(VCAP::CloudController::ServicePlan.find(unique_id: 'plan1-guid-here')[:name]).to eq('small-legacy')
      end
    end

    context 'when a service is re-named and another service is added to the front of the list with the old name' do
      let(:catalog1) do
        {
          services: [
            {
              id: 'service-guid-here',
              name: 'mysql',
              description: 'A MySQL-compatible relational database',
              bindable: true,
              plans: [
                {
                  id: 'plan-guid-here',
                  name: 'small',
                  description: 'A small shared database with 100mb storage quota and 10 connections'
                }
              ]
            }
          ]
        }
      end

      let(:catalog2) do
        {
          services: [
            {
              id: 'new-service-guid-here',
              name: 'mysql',
              description: 'A MySQL-compatible relational database',
              bindable: true,
              plans: [
                {
                  id: 'new-plan-guid-here',
                  name: 'small',
                  description: 'A small shared database with 100mb storage quota and 10 connections'
                }
              ]
            },
            {
              id: 'service-guid-here',
              name: 'legacy-mysql',
              description: 'A MySQL-compatible relational database',
              bindable: true,
              plans: [
                {
                  id: 'plan-guid-here',
                  name: 'small',
                  description: 'A small shared database with 100mb storage quota and 10 connections'
                }
              ]
            }
          ]
        }
      end

      it 'renames the plan and adds a new plan with the old name' do
        setup_broker(catalog1)
        update_broker(catalog2)
        expect(last_response).to have_status_code(200)

        expect(VCAP::CloudController::Service.find(unique_id: 'service-guid-here')[:label]).to eq('legacy-mysql')
        expect(VCAP::CloudController::Service.find(unique_id: 'new-service-guid-here')[:label]).to eq('mysql')
      end
    end

    context 'when a service plan disappears from the catalog' do
      before do
        setup_broker(catalog_with_two_plans)
      end

      context 'when it has an existing instance' do
        before do
          provision_service
        end

        it 'the plan should become inactive' do
          update_broker(catalog_with_large_plan)
          expect(last_response).to have_status_code(200)

          expect(VCAP::CloudController::ServicePlan.find(unique_id: 'plan1-guid-here')[:active]).to be false
        end

        it 'returns a warning to the operator' do
          update_broker(catalog_with_large_plan)
          expect(last_response).to have_status_code(200)

          warning = CGI.unescape(last_response.headers['X-Cf-Warnings'])

          expect(warning).to eq(<<~HEREDOC)
            Warning: Service plans are missing from the broker's catalog (http://#{stubbed_broker_host}/v2/catalog) but can not be removed from Cloud Foundry while instances exist. The plans have been deactivated to prevent users from attempting to provision new instances of these plans. The broker should continue to support bind, unbind, and delete for existing instances; if these operations fail contact your broker provider.

            Service Offering: #{service_name}
            Plans deactivated: small
          HEREDOC
        end
      end

      context 'when it has no existing instance' do
        it 'the plan should become inactive' do
          update_broker(catalog_with_large_plan)
          expect(last_response).to have_status_code(200)

          get('/v2/services?inline-relations-depth=1', '{}', admin_headers)
          expect(last_response).to have_status_code(200)

          parsed_body = Oj.load(last_response.body)
          expect(parsed_body['resources'].first['entity']['service_plans'].length).to eq(1)
        end
      end

      context 'when the service is updated to have no plans' do
        it 'returns an error and does not update the broker' do
          update_broker(catalog_with_no_plans)
          expect(last_response).to have_status_code(502)

          get('/v2/services?inline-relations-depth=1', '{}', admin_headers)
          expect(last_response).to have_status_code(200)

          parsed_body = Oj.load(last_response.body)
          expect(parsed_body['resources'].first['entity']['service_plans'].length).to eq(2)
        end
      end
    end
  end

  describe 'deleting a service broker' do
    context 'when broker has dashboard clients' do
      let(:service_1) { build_service(dashboard_client: { id: 'client-1', secret: 'shhhhh', redirect_uri: 'http://example.com/client-1' }) }
      let(:service_2) { build_service(dashboard_client: { id: 'client-2', secret: 'sekret', redirect_uri: 'http://example.com/client-2' }) }
      let(:service_3) { build_service }

      before do
        # set up a fake broker catalog that includes dashboard_client for services
        stub_catalog_fetch(200, services: [service_1, service_2, service_3])
        UAARequests.stub_all
        stub_request(:get, %r{https://uaa.service.cf.internal/oauth/clients/.*}).to_return(status: 404)

        # add that broker to the CC
        post('/v2/service_brokers',
             {
               name: 'broker_name',
               broker_url: stubbed_broker_url,
               auth_username: stubbed_broker_username,
               auth_password: stubbed_broker_password
             }.to_json,
             admin_headers)
        expect(last_response).to have_status_code(201)
        @service_broker_guid = decoded_response.fetch('metadata').fetch('guid')

        stub_request(:get, %r{https://uaa.service.cf.internal/oauth/clients/client-1}).to_return(
          body: { client_id: 'client-1' }.to_json,
          status: 200,
          headers: { 'content-type' => 'application/json' }
        )
        stub_request(:get, %r{https://uaa.service.cf.internal/oauth/clients/client-2}).to_return(
          body: { client_id: 'client-2' }.to_json,
          status: 200,
          headers: { 'content-type' => 'application/json' }
        )

        stub_request(:post, %r{https://uaa.service.cf.internal/oauth/clients/tx/modify}).
          to_return(
            status: 200,
            headers: { 'content-type' => 'application/json' },
            body: ''
          )
      end

      it 'deletes the dashboard clients from UAA' do
        delete("/v2/service_brokers/#{@service_broker_guid}", '', admin_headers)
        expect(last_response).to have_status_code(204)

        expected_json_body = [
          {
            client_id: service_1[:dashboard_client][:id],
            client_secret: nil,
            redirect_uri: nil,
            scope: ['openid', 'cloud_controller_service_permissions.read'],
            authorities: ['uaa.resource'],
            authorized_grant_types: ['authorization_code'],
            action: 'delete'
          },
          {
            client_id: service_2[:dashboard_client][:id],
            client_secret: nil,
            redirect_uri: nil,
            scope: ['openid', 'cloud_controller_service_permissions.read'],
            authorities: ['uaa.resource'],
            authorized_grant_types: ['authorization_code'],
            action: 'delete'
          }
        ].to_json

        expect(
          a_request(:post, 'https://uaa.service.cf.internal/oauth/clients/tx/modify').
            with(
              body: expected_json_body
            )
        ).to have_been_made
      end
    end

    context 'when a service instance exists' do
      before do
        setup_broker(catalog_with_small_plan)
        provision_service
      end

      after do
        deprovision_service
        delete_broker
      end

      it 'does not delete the broker', isolation: :truncation do # Can't use transactions for isolation because we're
        # testing a rollback
        delete_broker
        expect(last_response).to have_status_code(400)

        get('/v2/services?inline-relations-depth=1', '{}', admin_headers)
        expect(last_response).to have_status_code(200)

        parsed_body = Oj.load(last_response.body)
        expect(parsed_body['resources'].first['entity']['label']).to eq(service_name)
        expect(parsed_body['resources'].first['entity']['service_plans'].length).to eq(1)
      end
    end
  end
end