cloudfoundry/cloud_controller_ng

View on GitHub
spec/request/service_instances_spec.rb

Summary

Maintainability
A
0 mins
Test Coverage
require 'spec_helper'
require 'request_spec_shared_examples'

RSpec.describe 'V3 service instances' do
  let(:user) { VCAP::CloudController::User.make }
  let(:org) { VCAP::CloudController::Organization.make }
  let!(:org_annotation) { VCAP::CloudController::OrganizationAnnotationModel.make(key_prefix: 'pre.fix', key_name: 'foo', value: 'bar', resource_guid: org.guid) }
  let(:space) { VCAP::CloudController::Space.make(organization: org) }
  let!(:space_annotation) { VCAP::CloudController::SpaceAnnotationModel.make(key_prefix: 'pre.fix', key_name: 'baz', value: 'wow', space: space) }
  let(:another_space) { VCAP::CloudController::Space.make }

  describe 'GET /v3/service_instances/:guid' do
    let(:api_call) { ->(user_headers) { get "/v3/service_instances/#{guid}", nil, user_headers } }

    context 'no such instance' do
      let(:guid) { 'no-such-guid' }

      let(:expected_codes_and_responses) do
        Hash.new(code: 404)
      end

      it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS
    end

    context 'managed service instance' do
      let(:instance) { VCAP::CloudController::ManagedServiceInstance.make(space:) }
      let(:guid) { instance.guid }

      let(:expected_codes_and_responses) do
        responses_for_space_restricted_single_endpoint(create_managed_json(instance))
      end

      it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS
    end

    context 'user-provided service instance' do
      let(:instance) { VCAP::CloudController::UserProvidedServiceInstance.make(space:) }
      let(:guid) { instance.guid }

      let(:expected_codes_and_responses) do
        responses_for_space_restricted_single_endpoint(create_user_provided_json(instance))
      end

      it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS
    end

    context 'shared service instance' do
      let(:another_space) { VCAP::CloudController::Space.make }
      let(:instance) { VCAP::CloudController::ManagedServiceInstance.make(space: another_space) }
      let(:guid) { instance.guid }

      before do
        instance.add_shared_space(space)
      end

      let(:expected_codes_and_responses) do
        responses_for_space_restricted_single_endpoint(create_managed_json(instance))
      end

      it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS
    end

    context 'fields' do
      let(:instance) { VCAP::CloudController::ManagedServiceInstance.make(space:) }
      let(:guid) { instance.guid }

      it 'can include the organization name and guid fields' do
        get "/v3/service_instances/#{guid}?fields[space.organization]=name,guid", nil, admin_headers
        expect(last_response).to have_status_code(200)

        included = {
          organizations: [
            {
              name: space.organization.name,
              guid: space.organization.guid
            }
          ]
        }

        expect({ included: parsed_response['included'] }).to match_json_response({ included: })
      end

      it 'can include the space name and guid fields' do
        get "/v3/service_instances/#{guid}?fields[space]=name,guid", nil, admin_headers
        expect(last_response).to have_status_code(200)

        included = {
          spaces: [
            {
              name: space.name,
              guid: space.guid
            }
          ]
        }

        expect({ included: parsed_response['included'] }).to match_json_response({ included: })
      end

      it 'can include service plan guid and name fields' do
        get "/v3/service_instances/#{guid}?fields[service_plan]=guid,name", nil, admin_headers

        expect(last_response).to have_status_code(200)

        included = {
          service_plans: [
            {
              guid: instance.service_plan.guid,
              name: instance.service_plan.name
            }
          ]
        }

        expect({ included: parsed_response['included'] }).to match_json_response({ included: })
      end

      it 'can include service offering and broker fields' do
        get "/v3/service_instances/#{guid}?fields[service_plan.service_offering]=name,guid,description,documentation_url&" \
            'fields[service_plan.service_offering.service_broker]=name,guid', nil, admin_headers
        expect(last_response).to have_status_code(200)

        included = {
          service_offerings: [
            {
              name: instance.service_plan.service.name,
              guid: instance.service_plan.service.guid,
              description: instance.service_plan.service.description,
              documentation_url: 'https://some.url.for.docs/'
            }
          ],
          service_brokers: [
            {
              name: instance.service_plan.service.service_broker.name,
              guid: instance.service_plan.service.service_broker.guid
            }
          ]
        }

        expect({ included: parsed_response['included'] }).to match_json_response({ included: })
      end
    end
  end

  describe 'GET /v3/service_instances' do
    let(:api_call) { ->(user_headers) { get '/v3/service_instances', nil, user_headers } }

    it_behaves_like 'list query endpoint' do
      let(:user_header) { admin_headers }
      let(:request) { 'v3/service_instances' }
      let(:message) { VCAP::CloudController::ServiceInstancesListMessage }

      let(:params) do
        {
          names: %w[foo bar],
          space_guids: %w[foo bar],
          organization_guids: %w[org-1 org-2],
          per_page: '10',
          page: 2,
          order_by: 'updated_at',
          label_selector: 'foo,bar',
          type: 'managed',
          service_plan_guids: %w[guid-1 guid-2],
          service_plan_names: %w[plan-1 plan-2],
          fields: { 'space.organization' => 'name' },
          guids: 'foo,bar',
          created_ats: "#{Time.now.utc.iso8601},#{Time.now.utc.iso8601}",
          updated_ats: { gt: Time.now.utc.iso8601 }
        }
      end
    end

    describe 'pagination' do
      let!(:resources) { Array.new(2) { VCAP::CloudController::ServiceInstance.make } }

      it_behaves_like 'paginated response', '/v3/service_instances'

      it_behaves_like 'paginated fields response', '/v3/service_instances', 'space', 'guid,name,relationships.organization'

      it_behaves_like 'paginated fields response', '/v3/service_instances', 'space.organization', 'name,guid'
    end

    describe 'order_by' do
      it_behaves_like 'list endpoint order_by name', '/v3/service_instances' do
        let(:resource_klass) { VCAP::CloudController::ServiceInstance }
      end

      it_behaves_like 'list endpoint order_by timestamps', '/v3/service_instances' do
        let(:resource_klass) { VCAP::CloudController::ServiceInstance }
      end
    end

    context 'given a mixture of managed, user-provided and shared service instances' do
      let!(:msi_1) { VCAP::CloudController::ManagedServiceInstance.make(space:) }
      let!(:msi_2) { VCAP::CloudController::ManagedServiceInstance.make(space: another_space) }
      let!(:upsi_1) { VCAP::CloudController::UserProvidedServiceInstance.make(space:) }
      let!(:upsi_2) { VCAP::CloudController::UserProvidedServiceInstance.make(space: another_space) }
      let!(:ssi) { VCAP::CloudController::ManagedServiceInstance.make(space: another_space) }

      before do
        ssi.add_shared_space(space)
      end

      describe 'permissions' do
        let(:all_instances) do
          {
            code: 200,
            response_objects: [
              create_managed_json(msi_1),
              create_managed_json(msi_2),
              create_user_provided_json(upsi_1),
              create_user_provided_json(upsi_2),
              create_managed_json(ssi)
            ]
          }
        end

        let(:space_instances) do
          {
            code: 200,
            response_objects: [
              create_managed_json(msi_1),
              create_user_provided_json(upsi_1),
              create_managed_json(ssi)
            ]
          }
        end

        let(:expected_codes_and_responses) do
          h = Hash.new(
            code: 200,
            response_objects: []
          )

          h['admin'] = all_instances
          h['admin_read_only'] = all_instances
          h['global_auditor'] = all_instances
          h['space_supporter'] = space_instances
          h['space_developer'] = space_instances
          h['space_manager'] = space_instances
          h['space_auditor'] = space_instances
          h['org_manager'] = space_instances

          h
        end

        it_behaves_like 'permissions for list endpoint', ALL_PERMISSIONS
      end

      describe 'filters' do
        it 'filters by name' do
          get "/v3/service_instances?names=#{msi_1.name}", nil, admin_headers
          check_filtered_instances(create_managed_json(msi_1))
        end

        it 'filters by space guid' do
          get "/v3/service_instances?space_guids=#{another_space.guid}", nil, admin_headers
          check_filtered_instances(
            create_managed_json(msi_2),
            create_user_provided_json(upsi_2),
            create_managed_json(ssi)
          )
        end

        it 'filters by organization guids' do
          get "/v3/service_instances?organization_guids=#{another_space.organization.guid}", nil, admin_headers
          check_filtered_instances(
            create_managed_json(msi_2),
            create_user_provided_json(upsi_2),
            create_managed_json(ssi)
          )
        end

        it 'filters by label' do
          VCAP::CloudController::ServiceInstanceLabelModel.make(key_name: 'fruit', value: 'strawberry', service_instance: msi_1)
          VCAP::CloudController::ServiceInstanceLabelModel.make(key_name: 'fruit', value: 'raspberry', service_instance: msi_2)
          VCAP::CloudController::ServiceInstanceLabelModel.make(key_name: 'fruit', value: 'strawberry', service_instance: ssi)
          VCAP::CloudController::ServiceInstanceLabelModel.make(key_name: 'fruit', value: 'strawberry', service_instance: upsi_2)

          get '/v3/service_instances?label_selector=fruit=strawberry', nil, admin_headers

          check_filtered_instances(
            create_managed_json(msi_1, labels: { fruit: 'strawberry' }),
            create_user_provided_json(upsi_2, labels: { fruit: 'strawberry' }),
            create_managed_json(ssi, labels: { fruit: 'strawberry' })
          )
        end

        it 'filters by type' do
          get '/v3/service_instances?type=managed', nil, admin_headers
          check_filtered_instances(
            create_managed_json(msi_1),
            create_managed_json(msi_2),
            create_managed_json(ssi)
          )
        end

        it 'filters by service_plan_guids' do
          get "/v3/service_instances?service_plan_guids=#{msi_1.service_plan.guid},#{msi_2.service_plan.guid}", nil, admin_headers
          check_filtered_instances(
            create_managed_json(msi_1),
            create_managed_json(msi_2)
          )
        end

        it 'filters by service_plan_names' do
          get "/v3/service_instances?service_plan_names=#{msi_1.service_plan.name},#{msi_2.service_plan.name}", nil, admin_headers
          check_filtered_instances(
            create_managed_json(msi_1),
            create_managed_json(msi_2)
          )
        end

        def check_filtered_instances(*instances)
          expect(last_response).to have_status_code(200)
          expect(parsed_response['resources'].length).to be(instances.length)
          expect({ resources: parsed_response['resources'] }).to match_json_response(
            { resources: instances }
          )
        end
      end

      context 'fields' do
        it 'can include the space and organization name and guid fields' do
          get '/v3/service_instances?fields[space]=guid,name,relationships.organization&fields[space.organization]=name,guid', nil, admin_headers
          expect(last_response).to have_status_code(200)

          included = {
            spaces: [
              {
                guid: space.guid,
                name: space.name,
                relationships: {
                  organization: {
                    data: {
                      guid: space.organization.guid
                    }
                  }
                }
              },
              {
                guid: another_space.guid,
                name: another_space.name,
                relationships: {
                  organization: {
                    data: {
                      guid: another_space.organization.guid
                    }
                  }
                }
              }
            ],
            organizations: [
              {
                name: space.organization.name,
                guid: space.organization.guid
              },
              {
                name: another_space.organization.name,
                guid: another_space.organization.guid
              }
            ]
          }

          expect({ included: parsed_response['included'] }).to match_json_response({ included: })
        end

        it 'can include the service plan, offering and broker fields' do
          get '/v3/service_instances?fields[service_plan]=guid,name,relationships.service_offering&' \
              'fields[service_plan.service_offering]=name,guid,description,documentation_url,relationships.service_broker&' \
              'fields[service_plan.service_offering.service_broker]=name,guid', nil, admin_headers

          expect(last_response).to have_status_code(200)

          included = {
            service_plans: [
              {
                guid: msi_1.service_plan.guid,
                name: msi_1.service_plan.name,
                relationships: {
                  service_offering: {
                    data: {
                      guid: msi_1.service_plan.service.guid
                    }
                  }
                }
              },
              {
                guid: msi_2.service_plan.guid,
                name: msi_2.service_plan.name,
                relationships: {
                  service_offering: {
                    data: {
                      guid: msi_2.service_plan.service.guid
                    }
                  }
                }
              },
              {
                guid: ssi.service_plan.guid,
                name: ssi.service_plan.name,
                relationships: {
                  service_offering: {
                    data: {
                      guid: ssi.service_plan.service.guid
                    }
                  }
                }
              }
            ],
            service_offerings: [
              {
                name: msi_1.service_plan.service.name,
                guid: msi_1.service_plan.service.guid,
                description: msi_1.service_plan.service.description,
                documentation_url: 'https://some.url.for.docs/',
                relationships: {
                  service_broker: {
                    data: {
                      guid: msi_1.service_plan.service.service_broker.guid
                    }
                  }
                }
              },
              {
                name: msi_2.service_plan.service.name,
                guid: msi_2.service_plan.service.guid,
                description: msi_2.service_plan.service.description,
                documentation_url: 'https://some.url.for.docs/',
                relationships: {
                  service_broker: {
                    data: {
                      guid: msi_2.service_plan.service.service_broker.guid
                    }
                  }
                }
              },
              {
                name: ssi.service_plan.service.name,
                guid: ssi.service_plan.service.guid,
                description: ssi.service_plan.service.description,
                documentation_url: 'https://some.url.for.docs/',
                relationships: {
                  service_broker: {
                    data: {
                      guid: ssi.service_plan.service.service_broker.guid
                    }
                  }
                }
              }
            ],
            service_brokers: [
              {
                name: msi_1.service_plan.service.service_broker.name,
                guid: msi_1.service_plan.service.service_broker.guid
              },
              {
                name: msi_2.service_plan.service.service_broker.name,
                guid: msi_2.service_plan.service.service_broker.guid
              },
              {
                name: ssi.service_plan.service.service_broker.name,
                guid: ssi.service_plan.service.service_broker.guid
              }

            ]
          }

          expect({ included: parsed_response['included'] }).to match_json_response({ included: })
        end
      end
    end

    describe 'eager loading' do
      it 'eager loads associated resources that the presenter specifies' do
        expect(VCAP::CloudController::ServiceInstanceListFetcher).to receive(:fetch).with(
          an_instance_of(VCAP::CloudController::ServiceInstancesListMessage),
          hash_including(eager_loaded_associations: %i[labels annotations space service_instance_operation service_plan_sti_eager_load])
        ).and_call_original

        get '/v3/service_instances', nil, admin_headers
        expect(last_response).to have_status_code(200)
      end
    end

    it_behaves_like 'list_endpoint_with_common_filters' do
      let(:resource_klass) { VCAP::CloudController::ServiceInstance }
      let(:api_call) do
        ->(headers, filters) { get "/v3/service_instances?#{filters}", nil, headers }
      end
      let(:headers) { admin_headers }
    end
  end

  describe 'GET /v3/service_instances/:guid/credentials' do
    let(:space_dev_headers) do
      org.add_user(user)
      space.add_developer(user)
      headers_for(user)
    end

    it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS do
      let(:api_call) { ->(user_headers) { get "/v3/service_instances/#{guid}/credentials", nil, user_headers } }
      let(:credentials) { { 'fake-key' => 'fake-value' } }
      let(:instance) { VCAP::CloudController::UserProvidedServiceInstance.make(space:, credentials:) }
      let(:guid) { instance.guid }

      let(:expected_codes_and_responses) do
        h = Hash.new(
          code: 200,
          response_object: credentials
        )

        h['global_auditor'] = h['space_supporter'] = h['space_manager'] = h['space_auditor'] = h['org_manager'] = { code: 403 }
        h['org_auditor'] = h['org_billing_manager'] = h['no_role'] = { code: 404 }
        h
      end
    end

    it 'responds with an empty obect when no credentials were set' do
      upsi = VCAP::CloudController::UserProvidedServiceInstance.make(space: space, credentials: nil)
      get "/v3/service_instances/#{upsi.guid}/credentials", nil, admin_headers
      expect(last_response).to have_status_code(200)
      expect(parsed_response).to match_json_response({})
    end

    it 'responds with 404 when the instance does not exist' do
      get '/v3/service_instances/does-not-exist/credentials', nil, admin_headers
      expect(last_response).to have_status_code(404)
    end

    it 'responds with 404 for a managed service instance' do
      msi = VCAP::CloudController::ManagedServiceInstance.make(space:)
      get "/v3/service_instances/#{msi.guid}/credentials", nil, admin_headers
      expect(last_response).to have_status_code(404)
    end

    it 'records an audit event' do
      upsi = VCAP::CloudController::UserProvidedServiceInstance.make(space: space, credentials: {})
      get "/v3/service_instances/#{upsi.guid}/credentials", nil, space_dev_headers
      expect(last_response).to have_status_code(200)

      event = VCAP::CloudController::Event.last
      expect(event.values).to include({
                                        type: 'audit.user_provided_service_instance.show',
                                        actor: user.guid,
                                        actee: upsi.guid,
                                        actee_type: 'user_provided_service_instance',
                                        actee_name: upsi.name,
                                        space_guid: space.guid,
                                        organization_guid: space.organization.guid
                                      })
    end
  end

  describe 'GET /v3/service_instances/:guid/parameters' do
    let(:service) { VCAP::CloudController::Service.make(instances_retrievable: true) }
    let(:service_plan) { VCAP::CloudController::ServicePlan.make(service:) }
    let(:instance) { VCAP::CloudController::ManagedServiceInstance.make(space:, service_plan:) }
    let(:body) { {}.to_json }
    let(:response_code) { 200 }

    before do
      stub_request(:get, %r{#{instance.service.service_broker.broker_url}/v2/service_instances/#{guid_pattern}}).
        with(basic_auth: basic_auth(service_broker: instance.service.service_broker)).
        to_return(status: response_code, body: body)
    end

    it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS do
      let(:api_call) { ->(user_headers) { get "/v3/service_instances/#{guid}/parameters", nil, user_headers } }
      let(:parameters) { { 'some-key' => 'some-value' } }
      let(:body) { { 'parameters' => parameters }.to_json }
      let(:guid) { instance.guid }

      let(:expected_codes_and_responses) do
        h = Hash.new(
          code: 200,
          response_object: parameters
        )

        h['org_auditor'] = { code: 404 }
        h['org_billing_manager'] = { code: 404 }
        h['no_role'] = { code: 404 }
        h
      end
    end

    it 'sends the correct request to the service broker' do
      get "/v3/service_instances/#{instance.guid}/parameters", nil, headers_for(user, scopes: %w[cloud_controller.admin])

      encoded_user_guid = Base64.strict_encode64("{\"user_id\":\"#{user.guid}\"}")
      expect(a_request(:get, "#{instance.service_broker.broker_url}/v2/service_instances/#{instance.guid}").
        with(
          headers: { 'X-Broker-Api-Originating-Identity' => "cloudfoundry #{encoded_user_guid}" }
        )).to have_been_made.once
    end

    context 'when the instance does not support retrievable instances' do
      let(:service) { VCAP::CloudController::Service.make(instances_retrievable: false) }

      it 'fails with an explanatory error' do
        get "/v3/service_instances/#{instance.guid}/parameters", nil, admin_headers
        expect(last_response).to have_status_code(400)
        expect(parsed_response['errors']).to include(include({
                                                               'detail' => 'This service does not support fetching service instance parameters.',
                                                               'title' => 'CF-ServiceFetchInstanceParametersNotSupported',
                                                               'code' => 120_004
                                                             }))
      end
    end

    context 'when the broker returns no parameters' do
      it 'returns an empty object' do
        get "/v3/service_instances/#{instance.guid}/parameters", nil, admin_headers
        expect(last_response).to have_status_code(200)
        expect(parsed_response).to match_json_response({})
      end
    end

    context 'when the broker returns invalid parameters' do
      let(:body) { { 'parameters' => 'not valid' }.to_json }

      it 'fails with an explanatory error' do
        get "/v3/service_instances/#{instance.guid}/parameters", nil, admin_headers
        expect(last_response).to have_status_code(502)
        expect(parsed_response['errors']).to include(include({
                                                               'title' => 'CF-ServiceBrokerResponseMalformed',
                                                               'code' => 10_001
                                                             }))
      end
    end

    context 'when the broker returns invalid JSON' do
      let(:body) { 'this is not json' }

      it 'fails with an explanatory error' do
        get "/v3/service_instances/#{instance.guid}/parameters", nil, admin_headers
        expect(last_response).to have_status_code(502)
        expect(parsed_response['errors']).to include(include({
                                                               'title' => 'CF-ServiceBrokerResponseMalformed',
                                                               'code' => 10_001
                                                             }))
      end
    end

    context 'when the broker returns a non-200 response code' do
      let(:response_code) { 500 }

      it 'fails with an explanatory error' do
        get "/v3/service_instances/#{instance.guid}/parameters", nil, admin_headers
        expect(last_response).to have_status_code(502)
        expect(parsed_response['errors']).to include(include({
                                                               'title' => 'CF-ServiceBrokerBadResponse',
                                                               'code' => 10_001
                                                             }))
      end
    end

    context 'when the broker returns a 422 (update in progress) response code' do
      let(:response_code) { 422 }

      it 'fails with an explanatory error' do
        get "/v3/service_instances/#{instance.guid}/parameters", nil, admin_headers
        expect(last_response).to have_status_code(502)
        expect(parsed_response['errors']).to include(include({
                                                               'title' => 'CF-ServiceBrokerBadResponse',
                                                               'code' => 10_001
                                                             }))
      end
    end

    context 'when the instance is shared' do
      it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS do
        let(:api_call) { ->(user_headers) { get "/v3/service_instances/#{guid}/parameters", nil, user_headers } }
        let(:instance) { VCAP::CloudController::ManagedServiceInstance.make(space: another_space, service_plan: service_plan) }
        let(:parameters) { { 'some-key' => 'some-value' } }
        let(:body) { { 'parameters' => parameters }.to_json }
        let(:guid) { instance.guid }

        before do
          instance.add_shared_space(space)
        end

        let(:expected_codes_and_responses) do
          h = Hash.new(
            code: 200,
            response_object: parameters
          )

          h['space_supporter'] = h['space_developer'] = h['space_manager'] = h['space_auditor'] = h['org_manager'] = { code: 403 }
          h['org_auditor'] = h['org_billing_manager'] = h['no_role'] = { code: 404 }
          h
        end
      end
    end

    context 'when the instance does not exist' do
      it 'responds with 404' do
        get '/v3/service_instances/does-not-exist/parameters', nil, admin_headers
        expect(last_response).to have_status_code(404)
      end
    end

    context 'when the last operation state of the service instance is create in progress' do
      before do
        instance.save_with_new_operation({}, { type: 'create', state: 'in progress' })
      end

      it 'fails with an explanatory error' do
        get "/v3/service_instances/#{instance.guid}/parameters", nil, admin_headers
        expect(last_response).to have_status_code(409)
        expect(parsed_response['errors']).to include(include({
                                                               'title' => 'CF-AsyncServiceInstanceOperationInProgress',
                                                               'code' => 60_016
                                                             }))
      end
    end

    context 'when the last operation state of the service instance is create succeeded' do
      before do
        instance.save_with_new_operation({}, { type: 'create', state: 'succeeded' })
      end

      it 'returns the parameters' do
        get "/v3/service_instances/#{instance.guid}/parameters", nil, admin_headers
        expect(last_response).to have_status_code(200)
      end
    end

    context 'when the last operation state of the service instance is create failed' do
      before do
        instance.save_with_new_operation({}, { type: 'create', state: 'failed' })
      end

      it 'fails with an explanatory error' do
        get "/v3/service_instances/#{instance.guid}/parameters", nil, admin_headers
        expect(last_response).to have_status_code(404)
        expect(parsed_response['errors']).to include(include({
                                                               'title' => 'CF-ResourceNotFound',
                                                               'code' => 10_010
                                                             }))
      end
    end

    context 'when the last operation state of the service instance is update in progress' do
      before do
        instance.save_with_new_operation({}, { type: 'update', state: 'in progress' })
      end

      it 'fails with an explanatory error' do
        get "/v3/service_instances/#{instance.guid}/parameters", nil, admin_headers
        expect(last_response).to have_status_code(409)
        expect(parsed_response['errors']).to include(include({
                                                               'title' => 'CF-AsyncServiceInstanceOperationInProgress',
                                                               'code' => 60_016
                                                             }))
      end
    end

    context 'when the last operation state of the service instance is update succeeded' do
      before do
        instance.save_with_new_operation({}, { type: 'update', state: 'succeeded' })
      end

      it 'returns the parameters' do
        get "/v3/service_instances/#{instance.guid}/parameters", nil, admin_headers
        expect(last_response).to have_status_code(200)
      end
    end

    context 'when the last operation state of the service instance is update failed' do
      before do
        instance.save_with_new_operation({}, { type: 'update', state: 'failed' })
      end

      it 'returns the parameters' do
        get "/v3/service_instances/#{instance.guid}/parameters", nil, admin_headers
        expect(last_response).to have_status_code(200)
      end
    end

    context 'when the last operation state of the service instance is delete in progress' do
      before do
        instance.save_with_new_operation({}, { type: 'delete', state: 'in progress' })
      end

      it 'fails with an explanatory error' do
        get "/v3/service_instances/#{instance.guid}/parameters", nil, admin_headers
        expect(last_response).to have_status_code(409)
        expect(parsed_response['errors']).to include(include({
                                                               'title' => 'CF-AsyncServiceInstanceOperationInProgress',
                                                               'code' => 60_016
                                                             }))
      end
    end

    context 'when the last operation state of the service instance is delete failed' do
      before do
        instance.save_with_new_operation({}, { type: 'delete', state: 'failed' })
      end

      it 'returns the parameters' do
        get "/v3/service_instances/#{instance.guid}/parameters", nil, admin_headers
        expect(last_response).to have_status_code(200)
      end
    end

    context 'when the instance is user-provided' do
      it 'responds with 404' do
        upsi = VCAP::CloudController::UserProvidedServiceInstance.make(space:)

        get "/v3/service_instances/#{upsi.guid}/parameters", nil, admin_headers

        expect(last_response).to have_status_code(400)
        expect(parsed_response['errors']).to include(include({
                                                               'detail' => 'This service does not support fetching service instance parameters.',
                                                               'title' => 'CF-ServiceFetchInstanceParametersNotSupported',
                                                               'code' => 120_004
                                                             }))
      end
    end
  end

  describe 'POST /v3/service_instances' do
    let(:api_call) { ->(user_headers) { post '/v3/service_instances', request_body.to_json, user_headers } }
    let(:space_guid) { space.guid }

    let(:name) { Sham.name }
    let(:type) { 'user-provided' }
    let(:request_body_additions) { {} }
    let(:request_body) do
      {
        type: type,
        name: name,
        relationships: {
          space: {
            data: {
              guid: space_guid
            }
          }
        }
      }.merge(request_body_additions)
    end

    let(:space_dev_headers) do
      org.add_user(user)
      space.add_developer(user)
      headers_for(user)
    end

    context 'permissions' do
      it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS do
        let(:expected_codes_and_responses) { responses_for_space_restricted_create_endpoint(success_code: 201) }
      end

      it_behaves_like 'permissions for create endpoint when organization is suspended', 201
    end

    context 'when service_instance_creation flag is disabled' do
      before do
        VCAP::CloudController::FeatureFlag.create(name: 'service_instance_creation', enabled: false)
      end

      it 'makes non_admins unable to create any type of service' do
        api_call.call(space_dev_headers)
        expect(last_response).to have_status_code(403)
        expect(parsed_response['errors']).to include(
          include({
                    'detail' => 'Feature Disabled: service_instance_creation',
                    'title' => 'CF-FeatureDisabled',
                    'code' => 330_002
                  })
        )
      end

      it 'does not impact admins ability create services' do
        api_call.call(admin_headers)
        expect(last_response).to have_status_code(201)
      end
    end

    context 'when the request body is invalid' do
      let(:request_body) { { type: 'foo' } }

      it 'says the message is unprocessable' do
        api_call.call(admin_headers)
        expect(last_response).to have_status_code(422)
        expect(parsed_response['errors']).to include(
          include({
                    'detail' => "Relationships 'relationships' is not an object, Type must be one of 'managed', 'user-provided', Name must be a string, Name can't be blank",
                    'title' => 'CF-UnprocessableEntity',
                    'code' => 10_008
                  })
        )
      end
    end

    context 'when the space is not readable' do
      it 'fails saying the space cannot be found' do
        request_body[:relationships][:space][:data][:guid] = VCAP::CloudController::Space.make.guid

        api_call.call(space_dev_headers)
        expect(last_response).to have_status_code(422)
        expect(parsed_response['errors']).to include(
          include({
                    'detail' => 'Invalid space. Ensure that the space exists and you have access to it.',
                    'title' => 'CF-UnprocessableEntity',
                    'code' => 10_008
                  })
        )
      end
    end

    context 'user-provided service instance' do
      let(:request_body) do
        {
          type: type,
          name: name,
          relationships: {
            space: {
              data: {
                guid: space_guid
              }
            }
          },
          credentials: {
            foo: 'bar',
            baz: 'qux'
          },
          tags: %w[foo bar baz],
          syslog_drain_url: 'https://syslog.com/drain',
          route_service_url: 'https://route.com/service',
          metadata: {
            annotations: {
              foo: 'bar'
            },
            labels: {
              baz: 'qux'
            }
          }
        }
      end

      it 'responds with the created object' do
        api_call.call(space_dev_headers)
        expect(last_response).to have_status_code(201)
        expect(parsed_response).to match_json_response(
          create_user_provided_json(
            VCAP::CloudController::ServiceInstance.last,
            labels: { baz: 'qux' },
            annotations: { foo: 'bar' },
            last_operation: {
              type: 'create',
              state: 'succeeded',
              description: 'Operation succeeded',
              created_at: iso8601,
              updated_at: iso8601
            }
          )
        )
      end

      it 'creates a service instance in the database' do
        api_call.call(space_dev_headers)

        instance = VCAP::CloudController::ServiceInstance.last

        expect(instance.name).to eq(name)
        expect(instance.syslog_drain_url).to eq('https://syslog.com/drain')
        expect(instance.route_service_url).to eq('https://route.com/service')
        expect(instance.tags).to contain_exactly('foo', 'bar', 'baz')
        expect(instance.credentials).to match({ 'foo' => 'bar', 'baz' => 'qux' })
        expect(instance.space).to eq(space)
        expect(instance.last_operation.type).to eq('create')
        expect(instance.last_operation.state).to eq('succeeded')
        expect(instance).to have_annotations({ prefix: nil, key_name: 'foo', value: 'bar' })
        expect(instance).to have_labels({ prefix: nil, key_name: 'baz', value: 'qux' })
      end

      context 'when the name has already been taken' do
        it 'fails when the same name is already used in this space' do
          VCAP::CloudController::ServiceInstance.make(name:, space:)

          api_call.call(admin_headers)
          expect(last_response).to have_status_code(422)
          expect(parsed_response['errors']).to include(
            include({
                      'detail' => "The service instance name is taken: #{name}.",
                      'title' => 'CF-UnprocessableEntity',
                      'code' => 10_008
                    })
          )
        end

        it 'succeeds when the same name is used in another space' do
          VCAP::CloudController::ServiceInstance.make(name: name, space: another_space)

          api_call.call(admin_headers)
          expect(last_response).to have_status_code(201)
        end
      end

      context 'when the route is not https' do
        it 'returns an error' do
          request_body[:route_service_url] = 'http://banana.example.com'
          api_call.call(space_dev_headers)
          expect(last_response).to have_status_code(422)
          expect(parsed_response['errors']).to include(
            include({
                      'detail' => 'Route service url must be https',
                      'title' => 'CF-UnprocessableEntity',
                      'code' => 10_008
                    })
          )
        end
      end
    end

    context 'managed service instance' do
      let(:type) { 'managed' }
      let(:maintenance_info) do
        {
          version: '1.2.3',
          description: 'amazing version'
        }
      end
      let(:service_plan) { VCAP::CloudController::ServicePlan.make(public: true, active: true, maintenance_info: maintenance_info) }
      let(:service_plan_guid) { service_plan.guid }
      let(:request_body) do
        {
          type: type,
          name: name,
          relationships: {
            space: {
              data: {
                guid: space_guid
              }
            },
            service_plan: {
              data: {
                guid: service_plan_guid
              }
            }
          },
          parameters: {
            foo: 'bar',
            baz: 'qux'
          },
          tags: %w[foo bar baz],
          metadata: {
            annotations: {
              foo: 'bar',
              'pre.fix/wow': 'baz'
            },
            labels: {
              baz: 'qux'
            }
          }
        }
      end
      let(:instance) { VCAP::CloudController::ServiceInstance.last }
      let(:job) { VCAP::CloudController::PollableJobModel.last }

      it 'creates a service instance in the database' do
        api_call.call(space_dev_headers)

        expect(instance.name).to eq(name)
        expect(instance.tags).to contain_exactly('foo', 'bar', 'baz')
        expect(instance.space).to eq(space)
        expect(instance.service_plan).to eq(service_plan)

        expect(instance).to have_annotations({ prefix: nil, key_name: 'foo', value: 'bar' }, { prefix: 'pre.fix', key_name: 'wow', value: 'baz' })
        expect(instance).to have_labels({ prefix: nil, key_name: 'baz', value: 'qux' })

        expect(instance.last_operation.type).to eq('create')
        expect(instance.last_operation.state).to eq('initial')
      end

      it 'responds with job resource' do
        api_call.call(space_dev_headers)
        expect(last_response).to have_status_code(202)
        expect(last_response.headers['Location']).to end_with("/v3/jobs/#{job.guid}")

        expect(job.state).to eq(VCAP::CloudController::PollableJobModel::PROCESSING_STATE)
        expect(job.operation).to eq('service_instance.create')
        expect(job.resource_guid).to eq(instance.guid)
        expect(job.resource_type).to eq('service_instances')
      end

      context 'when the name has already been taken' do
        it 'fails when the same name is already used in this space' do
          VCAP::CloudController::ServiceInstance.make(name:, space:)

          api_call.call(admin_headers)
          expect(last_response).to have_status_code(422)
          expect(parsed_response['errors']).to include(
            include({
                      'detail' => "The service instance name is taken: #{name}.",
                      'title' => 'CF-UnprocessableEntity',
                      'code' => 10_008
                    })
          )
        end

        it 'succeeds when the same name is used in another space' do
          VCAP::CloudController::ServiceInstance.make(name: name, space: another_space)

          api_call.call(admin_headers)
          expect(last_response).to have_status_code(202)
        end
      end

      context 'when the plan is org-restricted' do
        let(:service_plan) { VCAP::CloudController::ServicePlan.make(public: false, active: true) }

        before do
          VCAP::CloudController::ServicePlanVisibility.make(service_plan: service_plan, organization: org)
        end

        it 'can be created in a space in that org' do
          api_call.call(space_dev_headers)
          expect(last_response).to have_status_code(202)
          expect(instance.name).to eq(name)
        end
      end

      describe 'unavailable broker' do
        context 'when the service broker does not have state (v2 brokers)' do
          let(:service_broker) { service_plan.service_broker }

          it 'creates a service instance' do
            service_broker.update(state: '')
            api_call.call(space_dev_headers)
            expect(last_response).to have_status_code(202)
          end
        end

        context 'when there is an operation in progress for the service broker' do
          let(:service_broker) { service_plan.service_broker }

          before do
            service_broker.update(state: broker_state)
          end

          context 'when the service broker is being deleted' do
            let(:broker_state) { VCAP::CloudController::ServiceBrokerStateEnum::DELETE_IN_PROGRESS }

            it 'fails to create a service instance' do
              api_call.call(space_dev_headers)
              expect(last_response).to have_status_code(422)
              expect(parsed_response['errors']).to include(
                include({
                          'detail' => 'The service instance cannot be created because there is an operation in progress for the service broker.',
                          'title' => 'CF-UnprocessableEntity',
                          'code' => 10_008
                        })
              )
            end
          end

          context 'when the service broker is synchronising the catalog' do
            let(:broker_state) { VCAP::CloudController::ServiceBrokerStateEnum::SYNCHRONIZING }

            it 'fails to create a service instance' do
              api_call.call(space_dev_headers)
              expect(last_response).to have_status_code(422)
              expect(parsed_response['errors']).to include(
                include({
                          'detail' => 'The service instance cannot be created because there is an operation in progress for the service broker.',
                          'title' => 'CF-UnprocessableEntity',
                          'code' => 10_008
                        })
              )
            end
          end
        end
      end

      describe 'when db is unavailable' do
        before do
          allow_any_instance_of(VCAP::CloudController::Jobs::Enqueuer).to receive(:enqueue_pollable).and_raise(Sequel::DatabaseDisconnectError)
        end

        it 'raises the appropriate error' do
          api_call.call(admin_headers)

          expect(last_response).to have_status_code(503)
          expect(parsed_response['errors']).to include(include({
                                                                 'detail' => include('Database connection failure'),
                                                                 'title' => 'CF-ServiceUnavailable',
                                                                 'code' => 10_015
                                                               }))
        end

        it 'does not create a service instance in the database' do
          api_call.call(admin_headers)

          expect(instance).to be_nil
        end
      end

      describe 'service plan checks' do
        context 'does not exist' do
          let(:service_plan_guid) { 'does-not-exist' }

          it 'fails saying the plan is invalid' do
            api_call.call(space_dev_headers)
            expect(last_response).to have_status_code(422)
            expect(parsed_response['errors']).to include(
              include({
                        'detail' => 'Invalid service plan. Ensure that the service plan exists, is available, and you have access to it.',
                        'title' => 'CF-UnprocessableEntity',
                        'code' => 10_008
                      })
            )
          end
        end

        context 'not readable by the user' do
          let(:service_plan) { VCAP::CloudController::ServicePlan.make(public: false, active: true) }

          it 'fails saying the plan is invalid' do
            api_call.call(space_dev_headers)
            expect(last_response).to have_status_code(422)
            expect(parsed_response['errors']).to include(
              include({
                        'detail' => 'Invalid service plan. Ensure that the service plan exists, is available, and you have access to it.',
                        'title' => 'CF-UnprocessableEntity',
                        'code' => 10_008
                      })
            )
          end
        end

        context 'not enabled in that org' do
          let(:service_plan) { VCAP::CloudController::ServicePlan.make(public: false, active: true) }

          it 'fails saying the plan is invalid' do
            api_call.call(admin_headers)
            expect(last_response).to have_status_code(422)
            expect(parsed_response['errors']).to include(
              include({
                        'detail' => 'Invalid service plan. Ensure that the service plan exists, is available, and you have access to it.',
                        'title' => 'CF-UnprocessableEntity',
                        'code' => 10_008
                      })
            )
          end
        end

        context 'not active' do
          let(:service_plan) { VCAP::CloudController::ServicePlan.make(public: true, active: false) }

          it 'fails saying the plan is invalid' do
            api_call.call(admin_headers)
            expect(last_response).to have_status_code(422)
            expect(parsed_response['errors']).to include(
              include({
                        'detail' => 'Invalid service plan. Ensure that the service plan exists, is available, and you have access to it.',
                        'title' => 'CF-UnprocessableEntity',
                        'code' => 10_008
                      })
            )
          end
        end

        context 'space-scoped plan from a different space' do
          let(:service_broker) { VCAP::CloudController::ServiceBroker.make(space: another_space) }
          let(:service_offering) { VCAP::CloudController::Service.make(service_broker:) }
          let(:service_plan) { VCAP::CloudController::ServicePlan.make(service: service_offering, active: true, public: false) }

          it 'fails saying the plan is invalid' do
            api_call.call(space_dev_headers)
            expect(last_response).to have_status_code(422)
            expect(parsed_response['errors']).to include(
              include({
                        'detail' => 'Invalid service plan. Ensure that the service plan exists, is available, and you have access to it.',
                        'title' => 'CF-UnprocessableEntity',
                        'code' => 10_008
                      })
            )
          end
        end
      end

      describe 'the pollable job' do
        let(:request_body_additions) { { parameters: { foo: 'bar', baz: 'qux' } } }
        let(:broker_response) { { dashboard_url: 'http://dashboard.url' } }
        let(:broker_status_code) { 201 }
        let(:last_operation_status_code) { 200 }
        let(:last_operation_response) { { state: 'in progress' } }

        before do
          api_call.call(space_dev_headers)
          instance = VCAP::CloudController::ServiceInstance.last
          stub_request(:put, "#{instance.service_broker.broker_url}/v2/service_instances/#{instance.guid}").
            with(query: { 'accepts_incomplete' => true }).
            to_return(status: broker_status_code, body: broker_response.to_json, headers: {})

          stub_request(:get, "#{instance.service_broker.broker_url}/v2/service_instances/#{instance.guid}/last_operation").
            with(
              query: {
                operation: 'task12',
                service_id: service_plan.service.unique_id,
                plan_id: service_plan.unique_id
              }
            ).
            to_return(status: last_operation_status_code, body: last_operation_response.to_json, headers: {})
        end

        it 'sends a provision request with the right arguments to the service broker' do
          execute_all_jobs(expected_successes: 1, expected_failures: 0)

          encoded_user_guid = Base64.strict_encode64("{\"user_id\":\"#{user.guid}\"}")
          expect(a_request(:put, "#{instance.service_broker.broker_url}/v2/service_instances/#{instance.guid}").
            with(
              query: { accepts_incomplete: true },
              body: {
                service_id: service_plan.service.unique_id,
                plan_id: service_plan.unique_id,
                context: {
                  platform: 'cloudfoundry',
                  organization_guid: org.guid,
                  organization_name: org.name,
                  organization_annotations: { 'pre.fix/foo': 'bar' },
                  space_guid: space.guid,
                  space_name: space.name,
                  space_annotations: { 'pre.fix/baz': 'wow' },
                  instance_name: instance.name,
                  instance_annotations: { 'pre.fix/wow': 'baz' }
                },
                organization_guid: org.guid,
                space_guid: space.guid,
                parameters: {
                  foo: 'bar',
                  baz: 'qux'
                },
                maintenance_info: maintenance_info
              },
              headers: { 'X-Broker-Api-Originating-Identity' => "cloudfoundry #{encoded_user_guid}" }
            )).to have_been_made.once
        end

        context 'when the provision completes synchronously' do
          it 'marks the service instance as created' do
            execute_all_jobs(expected_successes: 1, expected_failures: 0)

            expect(instance.dashboard_url).to eq('http://dashboard.url')
            expect(instance.last_operation.type).to eq('create')
            expect(instance.last_operation.state).to eq('succeeded')
          end

          it 'completes' do
            execute_all_jobs(expected_successes: 1, expected_failures: 0)

            expect(job.state).to eq(VCAP::CloudController::PollableJobModel::COMPLETE_STATE)
          end

          context 'when the broker responds with an error' do
            let(:broker_status_code) { 400 }

            it 'marks the service instance as failed' do
              execute_all_jobs(expected_successes: 0, expected_failures: 1)

              expect(instance.last_operation.type).to eq('create')
              expect(instance.last_operation.state).to eq('failed')
              expect(instance.last_operation.description).to include('Status Code: 400 Bad Request')
            end

            it 'completes with failure' do
              execute_all_jobs(expected_successes: 0, expected_failures: 1)

              expect(job.state).to eq(VCAP::CloudController::PollableJobModel::FAILED_STATE)
            end
          end
        end

        context 'when the provision is asynchronous' do
          let(:broker_status_code) { 202 }
          let(:broker_response) { { operation: 'task12' } }

          it 'marks the job state as polling' do
            execute_all_jobs(expected_successes: 1, expected_failures: 0)
            expect(job.state).to eq(VCAP::CloudController::PollableJobModel::POLLING_STATE)
          end

          it 'calls last operation immediately' do
            encoded_user_guid = Base64.strict_encode64("{\"user_id\":\"#{user.guid}\"}")
            execute_all_jobs(expected_successes: 1, expected_failures: 0)
            expect(
              a_request(:get, "#{instance.service_broker.broker_url}/v2/service_instances/#{instance.guid}/last_operation").
                with(
                  query: {
                    operation: 'task12',
                    service_id: service_plan.service.unique_id,
                    plan_id: service_plan.unique_id
                  },
                  headers: { 'X-Broker-Api-Originating-Identity' => "cloudfoundry #{encoded_user_guid}" }
                )
            ).to have_been_made.once
          end

          it 'enqueues the next fetch last operation job' do
            execute_all_jobs(expected_successes: 1, expected_failures: 0)
            expect(Delayed::Job.count).to eq(1)
          end

          context 'when last operation eventually returns `create succeeded`' do
            let(:dashboard_url) { '' }

            before do
              stub_request(:get, "#{instance.service_broker.broker_url}/v2/service_instances/#{instance.guid}/last_operation").
                with(
                  query: {
                    operation: 'task12',
                    service_id: service_plan.service.unique_id,
                    plan_id: service_plan.unique_id
                  }
                ).
                to_return(status: last_operation_status_code, body: last_operation_response.to_json, headers: {}).times(1).then.
                to_return(status: 200, body: { state: 'succeeded' }.to_json, headers: {})

              stub_request(:get, "#{instance.service.service_broker.broker_url}/v2/service_instances/#{instance.guid}").
                to_return(status: 200, body: { dashboard_url: }.to_json)

              execute_all_jobs(expected_successes: 1, expected_failures: 0)
              expect(job.state).to eq(VCAP::CloudController::PollableJobModel::POLLING_STATE)

              Timecop.freeze(Time.now + 1.hour) do
                execute_all_jobs(expected_successes: 1, expected_failures: 0)
              end
            end

            it 'completes the job' do
              updated_job = VCAP::CloudController::PollableJobModel.find(guid: job.guid)
              expect(updated_job.state).to eq(VCAP::CloudController::PollableJobModel::COMPLETE_STATE)
            end

            it 'sets the service instance last operation to create succeeded' do
              expect(instance.last_operation.type).to eq('create')
              expect(instance.last_operation.state).to eq('succeeded')
            end

            context 'it fetches dashboard url' do
              let(:service) { VCAP::CloudController::Service.make(instances_retrievable: true) }
              let(:service_plan) { VCAP::CloudController::ServicePlan.make(public: true, active: true, service: service) }
              let(:dashboard_url) { 'http:/some-new-dashboard-url.com' }

              it 'sets the service instance dashboard url' do
                instance.reload

                expect(instance.dashboard_url).to eq(dashboard_url)
              end
            end
          end

          context 'when last operation eventually returns `create failed`' do
            before do
              stub_request(:get, "#{instance.service_broker.broker_url}/v2/service_instances/#{instance.guid}/last_operation").
                with(
                  query: {
                    operation: 'task12',
                    service_id: service_plan.service.unique_id,
                    plan_id: service_plan.unique_id
                  }
                ).
                to_return(status: last_operation_status_code, body: last_operation_response.to_json, headers: {}).times(1).then.
                to_return(status: 200, body: { state: 'failed' }.to_json, headers: {})

              execute_all_jobs(expected_successes: 1, expected_failures: 0)
              expect(job.state).to eq(VCAP::CloudController::PollableJobModel::POLLING_STATE)

              Timecop.freeze(Time.now + 1.hour) do
                execute_all_jobs(expected_successes: 0, expected_failures: 1, jobs_to_execute: 1)
              end
            end

            it 'completes the job' do
              updated_job = VCAP::CloudController::PollableJobModel.find(guid: job.guid)
              expect(updated_job.state).to eq(VCAP::CloudController::PollableJobModel::FAILED_STATE)
            end

            it 'sets the service instance last operation to create failed' do
              expect(instance.last_operation.type).to eq('create')
              expect(instance.last_operation.state).to eq('failed')
            end
          end
        end

        describe 'volume mount and route service checks' do
          context 'when volume mount required' do
            let(:service_offering) { VCAP::CloudController::Service.make(requires: %w[volume_mount]) }
            let(:service_plan) { VCAP::CloudController::ServicePlan.make(service: service_offering) }

            context 'volume mount disabled' do
              before do
                TestConfig.config[:volume_services_enabled] = false
              end

              it 'warns' do
                execute_all_jobs(expected_successes: 1, expected_failures: 0)

                job = VCAP::CloudController::PollableJobModel.last
                expect(job.warnings.to_json).to include(VCAP::CloudController::ServiceInstance::VOLUME_SERVICE_WARNING)
              end
            end

            context 'volume mount enabled' do
              before do
                TestConfig.config[:volume_services_enabled] = true
              end

              it 'does not warn' do
                execute_all_jobs(expected_successes: 1, expected_failures: 0)

                job = VCAP::CloudController::PollableJobModel.last
                expect(job.warnings).to be_empty
              end
            end
          end

          context 'when route forwarding required' do
            let(:service_offering) { VCAP::CloudController::Service.make(requires: %w[route_forwarding]) }
            let(:service_plan) { VCAP::CloudController::ServicePlan.make(service: service_offering) }

            context 'route forwarding disabled' do
              before do
                TestConfig.config[:route_services_enabled] = false
              end

              it 'warns' do
                execute_all_jobs(expected_successes: 1, expected_failures: 0)

                job = VCAP::CloudController::PollableJobModel.last
                expect(job.warnings.to_json).to include(VCAP::CloudController::ServiceInstance::ROUTE_SERVICE_WARNING)
              end
            end

            context 'route forwarding enabled' do
              before do
                TestConfig.config[:route_services_enabled] = true
              end

              it 'does not warn' do
                execute_all_jobs(expected_successes: 1, expected_failures: 0)

                job = VCAP::CloudController::PollableJobModel.last
                expect(job.warnings).to be_empty
              end
            end
          end
        end
      end

      describe 'quotas restrictions' do
        describe 'space quotas' do
          context 'when the total services quota has been reached' do
            before do
              quota = VCAP::CloudController::SpaceQuotaDefinition.make(total_services: 1, organization: org)
              quota.add_space(space)

              VCAP::CloudController::ManagedServiceInstance.make(space:)
            end

            it 'returns an error' do
              api_call.call(space_dev_headers)
              expect(last_response).to have_status_code(422)
              expect(parsed_response['errors']).to include(
                include({
                          'detail' => "You have exceeded your space's services limit.",
                          'title' => 'CF-UnprocessableEntity',
                          'code' => 10_008
                        })
              )
            end
          end

          context 'when the paid services quota has been reached' do
            let!(:service_plan) { VCAP::CloudController::ServicePlan.make(free: false, public: true, active: true) }

            before do
              quota = VCAP::CloudController::SpaceQuotaDefinition.make(non_basic_services_allowed: false, organization: org)
              quota.add_space(space)
            end

            it 'returns an error' do
              api_call.call(space_dev_headers)
              expect(last_response).to have_status_code(422)
              expect(parsed_response['errors']).to include(
                include({
                          'detail' => 'The service instance cannot be created because paid service plans are not allowed for your space.',
                          'title' => 'CF-UnprocessableEntity',
                          'code' => 10_008
                        })
              )
            end
          end
        end

        describe 'organization quotas' do
          context 'when the total services quota has been reached' do
            before do
              quota = VCAP::CloudController::QuotaDefinition.make(total_services: 1)
              quota.add_organization(org)
              VCAP::CloudController::ManagedServiceInstance.make(space:)
            end

            it 'returns an error' do
              api_call.call(space_dev_headers)
              expect(last_response).to have_status_code(422)
              expect(parsed_response['errors']).to include(
                include({
                          'detail' => "You have exceeded your organization's services limit.",
                          'title' => 'CF-UnprocessableEntity',
                          'code' => 10_008
                        })
              )
            end
          end

          context 'when the paid services quota has been reached' do
            let!(:service_plan) { VCAP::CloudController::ServicePlan.make(free: false, public: true, active: true) }

            before do
              quota = VCAP::CloudController::QuotaDefinition.make(non_basic_services_allowed: false)
              quota.add_organization(org)
            end

            it 'returns an error' do
              api_call.call(space_dev_headers)
              expect(last_response).to have_status_code(422)
              expect(parsed_response['errors']).to include(
                include({
                          'detail' => 'The service instance cannot be created because paid service plans are not allowed.',
                          'title' => 'CF-UnprocessableEntity',
                          'code' => 10_008
                        })
              )
            end
          end
        end
      end
    end
  end

  describe 'PATCH /v3/service_instances/:guid' do
    let(:api_call) { ->(user_headers) { patch "/v3/service_instances/#{guid}", request_body.to_json, user_headers } }
    let(:space_dev_headers) do
      org.add_user(user)
      space.add_developer(user)
      headers_for(user)
    end
    let(:request_body) do
      {}
    end

    context 'permissions' do
      let(:guid) { VCAP::CloudController::ServiceInstance.make(space:).guid }

      it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS do
        let(:expected_codes_and_responses) { responses_for_space_restricted_update_endpoint(success_code: 200) }
      end

      it_behaves_like 'permissions for update endpoint when organization is suspended', 200
    end

    context 'service instance does not exist' do
      let(:guid) { 'no-such-instance' }

      it 'fails saying the service instance is not found (404)' do
        api_call.call(space_dev_headers)
        expect(last_response).to have_status_code(404)
        expect(parsed_response['errors']).to include(
          include({
                    'detail' => 'Service instance not found',
                    'title' => 'CF-ResourceNotFound',
                    'code' => 10_010
                  })
        )
      end
    end

    context 'managed service instance' do
      describe 'updates that do not require broker communication' do
        let!(:service_instance) do
          si = VCAP::CloudController::ManagedServiceInstance.make(
            guid: 'bommel',
            tags: %w[foo bar],
            space: space
          )
          VCAP::CloudController::ServiceInstanceAnnotationModel.make(service_instance: si, key_prefix: 'pre.fix', key_name: 'to_delete', value: 'value')
          VCAP::CloudController::ServiceInstanceAnnotationModel.make(service_instance: si, key_prefix: 'pre.fix', key_name: 'fox', value: 'bushy')
          VCAP::CloudController::ServiceInstanceLabelModel.make(service_instance: si, key_prefix: 'pre.fix', key_name: 'to_delete', value: 'value')
          VCAP::CloudController::ServiceInstanceLabelModel.make(service_instance: si, key_prefix: 'pre.fix', key_name: 'tail', value: 'fluffy')
          si
        end

        let(:guid) { service_instance.guid }

        let(:request_body) do
          {
            tags: %w[baz quz],
            metadata: {
              labels: {
                potato: 'yam',
                style: 'baked',
                'pre.fix/to_delete': nil
              },
              annotations: {
                potato: 'idaho',
                style: 'mashed',
                'pre.fix/to_delete': nil
              }
            }
          }
        end

        it 'responds synchronously' do
          api_call.call(space_dev_headers)

          expect(last_response).to have_status_code(200)
          expect(parsed_response).to match_json_response(
            create_managed_json(
              service_instance,
              labels: {
                potato: 'yam',
                style: 'baked',
                'pre.fix/tail': 'fluffy'
              },
              annotations: {
                potato: 'idaho',
                style: 'mashed',
                'pre.fix/fox': 'bushy'
              },
              last_operation: {
                created_at: iso8601,
                updated_at: iso8601,
                description: nil,
                state: 'succeeded',
                type: 'update'
              },
              tags: %w[baz quz]
            )
          )
        end

        it 'updates the service instance' do
          api_call.call(space_dev_headers)

          service_instance.reload
          expect(service_instance.tags).to eq(%w[baz quz])

          expect(service_instance).to have_annotations(
            { prefix: 'pre.fix', key_name: 'fox', value: 'bushy' },
            { prefix: nil, key_name: 'potato', value: 'idaho' },
            { prefix: nil, key_name: 'style', value: 'mashed' }
          )
          expect(service_instance).to have_labels(
            { prefix: 'pre.fix', key_name: 'tail', value: 'fluffy' },
            { prefix: nil, key_name: 'potato', value: 'yam' },
            { prefix: nil, key_name: 'style', value: 'baked' }
          )

          expect(service_instance.last_operation.type).to eq('update')
          expect(service_instance.last_operation.state).to eq('succeeded')
        end
      end

      describe 'updates that require broker communication' do
        let(:service_offering) { VCAP::CloudController::Service.make }
        let(:original_service_plan) do
          VCAP::CloudController::ServicePlan.make(
            service: service_offering,
            plan_updateable: true,
            maintenance_info: { version: '1.1.1' }
          )
        end
        let(:new_service_plan) { VCAP::CloudController::ServicePlan.make(service: service_offering) }
        let(:original_maintenance_info) { { version: '1.1.0' } }
        let!(:service_instance) do
          si = VCAP::CloudController::ManagedServiceInstance.make(
            guid: 'bommel',
            tags: %w[foo bar],
            space: space,
            service_plan: original_service_plan,
            maintenance_info: original_maintenance_info
          )
          VCAP::CloudController::ServiceInstanceAnnotationModel.make(service_instance: si, key_prefix: 'pre.fix', key_name: 'to_delete', value: 'value')
          VCAP::CloudController::ServiceInstanceAnnotationModel.make(service_instance: si, key_prefix: 'pre.fix', key_name: 'fox', value: 'bushy')
          VCAP::CloudController::ServiceInstanceLabelModel.make(service_instance: si, key_prefix: 'pre.fix', key_name: 'to_delete', value: 'value')
          VCAP::CloudController::ServiceInstanceLabelModel.make(service_instance: si, key_prefix: 'pre.fix', key_name: 'tail', value: 'fluffy')
          si
        end
        let(:guid) { service_instance.guid }
        let(:request_body) do
          {
            name: 'new-name',
            relationships: {
              service_plan: {
                data: {
                  guid: new_service_plan.guid
                }
              }
            },
            parameters: {
              foo: 'bar',
              baz: 'qux'
            },
            tags: %w[baz quz],
            metadata: {
              labels: {
                potato: 'yam',
                style: 'baked',
                'pre.fix/to_delete': nil
              },
              annotations: {
                potato: 'idaho',
                style: 'mashed',
                'pre.fix/to_delete': nil
              }
            }
          }
        end
        let(:job) { VCAP::CloudController::PollableJobModel.last }

        it 'responds with a pollable job' do
          api_call.call(space_dev_headers)

          expect(last_response).to have_status_code(202)
          expect(last_response.headers['Location']).to end_with("/v3/jobs/#{job.guid}")

          expect(job.state).to eq(VCAP::CloudController::PollableJobModel::PROCESSING_STATE)
          expect(job.operation).to eq('service_instance.update')
          expect(job.resource_guid).to eq(service_instance.guid)
          expect(job.resource_type).to eq('service_instances')
        end

        it 'updates the last operation' do
          api_call.call(space_dev_headers)

          expect(service_instance.last_operation.type).to eq('update')
          expect(service_instance.last_operation.state).to eq('in progress')
        end

        it 'does not immediately update the service instance' do
          api_call.call(space_dev_headers)

          service_instance.reload
          expect(service_instance.reload.tags).to eq(%w[foo bar])

          expect(service_instance).to have_annotations(
            { prefix: 'pre.fix', key_name: 'to_delete', value: 'value' },
            { prefix: 'pre.fix', key_name: 'fox', value: 'bushy' }
          )
          expect(service_instance).to have_labels(
            { prefix: 'pre.fix', key_name: 'to_delete', value: 'value' },
            { prefix: 'pre.fix', key_name: 'tail', value: 'fluffy' }
          )
        end

        describe 'the pollable job' do
          let(:broker_response) { { dashboard_url: 'http://new-dashboard.url' } }
          let(:broker_status_code) { 200 }

          before do
            api_call.call(space_dev_headers)

            instance = VCAP::CloudController::ServiceInstance.last

            stub_request(:patch, "#{instance.service_broker.broker_url}/v2/service_instances/#{instance.guid}").
              with(query: { 'accepts_incomplete' => true }).
              to_return(status: broker_status_code, body: broker_response.to_json, headers: {})
          end

          it 'sends a UPDATE request with the right arguments to the service broker' do
            execute_all_jobs(expected_successes: 1, expected_failures: 0)

            encoded_user_guid = Base64.strict_encode64("{\"user_id\":\"#{user.guid}\"}")
            expect(
              a_request(:patch, "#{service_instance.service_broker.broker_url}/v2/service_instances/#{service_instance.guid}").
                with(
                  query: { accepts_incomplete: true },
                  body: {
                    service_id: new_service_plan.service.unique_id,
                    plan_id: new_service_plan.unique_id,
                    previous_values: {
                      plan_id: original_service_plan.unique_id,
                      service_id: original_service_plan.service.unique_id,
                      organization_id: org.guid,
                      space_id: space.guid,
                      maintenance_info: { version: '1.1.0' }
                    },
                    context: {
                      platform: 'cloudfoundry',
                      organization_guid: org.guid,
                      organization_name: org.name,
                      organization_annotations: { 'pre.fix/foo': 'bar' },
                      space_guid: space.guid,
                      space_name: space.name,
                      space_annotations: { 'pre.fix/baz': 'wow' },
                      instance_name: 'new-name'
                    },
                    parameters: {
                      foo: 'bar',
                      baz: 'qux'
                    }
                  },
                  headers: { 'X-Broker-Api-Originating-Identity' => "cloudfoundry #{encoded_user_guid}" }
                )
            ).to have_been_made.once
          end

          context 'when the update completes synchronously' do
            it 'marks the service instance as updated' do
              execute_all_jobs(expected_successes: 1, expected_failures: 0)

              service_instance.reload
              expect(service_instance.dashboard_url).to eq('http://new-dashboard.url')
              expect(service_instance.last_operation.type).to eq('update')
              expect(service_instance.last_operation.state).to eq('succeeded')
              expect(service_instance.maintenance_info).to eq(new_service_plan.maintenance_info)
            end

            it 'marks the job as complete' do
              execute_all_jobs(expected_successes: 1, expected_failures: 0)

              expect(job.state).to eq(VCAP::CloudController::PollableJobModel::COMPLETE_STATE)
            end

            context 'when the broker responds with an error' do
              let(:broker_status_code) { 400 }

              it 'marks the service instance as failed' do
                execute_all_jobs(expected_successes: 0, expected_failures: 1)

                expect(service_instance.last_operation.type).to eq('update')
                expect(service_instance.last_operation.state).to eq('failed')
                expect(service_instance.last_operation.description).to include('Status Code: 400 Bad Request')
              end

              it 'marks the job as failed' do
                execute_all_jobs(expected_successes: 0, expected_failures: 1)

                expect(job.state).to eq(VCAP::CloudController::PollableJobModel::FAILED_STATE)
              end
            end
          end

          context 'when the update is asynchronous' do
            let(:broker_status_code) { 202 }
            let(:broker_response) { { operation: 'task12' } }
            let(:last_operation_status_code) { 200 }
            let(:last_operation_response) { { state: 'in progress' } }
            let(:dashboard_url) {}

            before do
              stub_request(:get, "#{service_instance.service_broker.broker_url}/v2/service_instances/#{service_instance.guid}/last_operation").
                with(
                  query: {
                    operation: 'task12',
                    service_id: service_instance.service_plan.service.unique_id,
                    plan_id: service_instance.service_plan.unique_id
                  }
                ).
                to_return(status: last_operation_status_code, body: last_operation_response.to_json, headers: {})

              stub_request(:get, "#{service_instance.service_broker.broker_url}/v2/service_instances/#{service_instance.guid}").
                to_return(status: 200, body: { dashboard_url: }.to_json)
            end

            it 'marks the job state as polling' do
              execute_all_jobs(expected_successes: 1, expected_failures: 0)
              expect(job.state).to eq(VCAP::CloudController::PollableJobModel::POLLING_STATE)
            end

            it 'calls last operation immediately' do
              encoded_user_guid = Base64.strict_encode64("{\"user_id\":\"#{user.guid}\"}")
              execute_all_jobs(expected_successes: 1, expected_failures: 0)
              expect(
                a_request(:get, "#{service_instance.service_broker.broker_url}/v2/service_instances/#{service_instance.guid}/last_operation").
                  with(
                    query: {
                      operation: 'task12',
                      service_id: service_instance.service_plan.service.unique_id,
                      plan_id: service_instance.service_plan.unique_id
                    },
                    headers: { 'X-Broker-Api-Originating-Identity' => "cloudfoundry #{encoded_user_guid}" }
                  )
              ).to have_been_made.once
            end

            it 'enqueues the next fetch last operation job' do
              execute_all_jobs(expected_successes: 1, expected_failures: 0)
              expect(Delayed::Job.count).to eq(1)
            end

            context 'when last operation eventually returns `update succeeded`' do
              let(:last_operation_status_code) { 200 }
              let(:last_operation_response) { { state: 'in progress' } }

              before do
                stub_request(:get, "#{service_instance.service_broker.broker_url}/v2/service_instances/#{service_instance.guid}/last_operation").
                  with(
                    query: {
                      operation: 'task12',
                      service_id: service_instance.service_plan.service.unique_id,
                      plan_id: service_instance.service_plan.unique_id
                    }
                  ).
                  to_return(status: last_operation_status_code, body: last_operation_response.to_json, headers: {}).times(1).then.
                  to_return(status: 200, body: { state: 'succeeded' }.to_json, headers: {})

                execute_all_jobs(expected_successes: 1, expected_failures: 0)
                expect(job.state).to eq(VCAP::CloudController::PollableJobModel::POLLING_STATE)

                Timecop.freeze(Time.now + 1.hour) do
                  execute_all_jobs(expected_successes: 1, expected_failures: 0)
                end
              end

              it 'completes the job' do
                updated_job = VCAP::CloudController::PollableJobModel.find(guid: job.guid)
                expect(updated_job.state).to eq(VCAP::CloudController::PollableJobModel::COMPLETE_STATE)
              end

              it 'sets the service instance last operation to create succeeded' do
                expect(service_instance.last_operation.type).to eq('update')
                expect(service_instance.last_operation.state).to eq('succeeded')
              end

              context 'it fetches dashboard url' do
                let(:service_offering) { VCAP::CloudController::Service.make(instances_retrievable: true) }
                let(:dashboard_url) { 'http:/some-new-dashboard-url.com' }

                it 'sets the service instance dashboard url' do
                  service_instance.reload
                  expect(service_instance.dashboard_url).to eq(dashboard_url)
                end
              end
            end

            context 'when last operation eventually returns `update failed`' do
              before do
                stub_request(:get, "#{service_instance.service_broker.broker_url}/v2/service_instances/#{service_instance.guid}/last_operation").
                  with(
                    query: {
                      operation: 'task12',
                      service_id: service_instance.service_plan.service.unique_id,
                      plan_id: service_instance.service_plan.unique_id
                    }
                  ).
                  to_return(status: last_operation_status_code, body: last_operation_response.to_json, headers: {}).times(1).then.
                  to_return(status: 200, body: { state: 'failed' }.to_json, headers: {})

                execute_all_jobs(expected_successes: 1, expected_failures: 0)
                expect(job.state).to eq(VCAP::CloudController::PollableJobModel::POLLING_STATE)

                Timecop.freeze(Time.now + 1.hour) do
                  execute_all_jobs(expected_successes: 0, expected_failures: 1, jobs_to_execute: 1)
                end
              end

              it 'completes the job' do
                updated_job = VCAP::CloudController::PollableJobModel.find(guid: job.guid)
                expect(updated_job.state).to eq(VCAP::CloudController::PollableJobModel::FAILED_STATE)
              end

              it 'sets the service instance last operation to update failed' do
                expect(service_instance.last_operation.type).to eq('update')
                expect(service_instance.last_operation.state).to eq('failed')
              end
            end

            context 'when last operation eventually returns error 400' do
              before do
                stub_request(:get, "#{service_instance.service_broker.broker_url}/v2/service_instances/#{service_instance.guid}/last_operation").
                  with(
                    query: {
                      operation: 'task12',
                      service_id: service_instance.service_plan.service.unique_id,
                      plan_id: service_instance.service_plan.unique_id
                    }
                  ).
                  to_return(status: last_operation_status_code, body: last_operation_response.to_json, headers: {}).times(1).then.
                  to_return(status: 400, body: {}.to_json, headers: {})

                execute_all_jobs(expected_successes: 1, expected_failures: 0)
                expect(job.state).to eq(VCAP::CloudController::PollableJobModel::POLLING_STATE)

                Timecop.freeze(Time.now + 1.hour) do
                  execute_all_jobs(expected_successes: 0, expected_failures: 1, jobs_to_execute: 1)
                end
              end

              it 'completes the job' do
                updated_job = VCAP::CloudController::PollableJobModel.find(guid: job.guid)
                expect(updated_job.state).to eq(VCAP::CloudController::PollableJobModel::FAILED_STATE)
              end

              it 'sets the service instance last operation to update failed' do
                expect(service_instance.last_operation.type).to eq('update')
                expect(service_instance.last_operation.state).to eq('failed')
              end

              it 'does not update the instance' do
                #  TODO maybe look in the client to add this test and make sure what it returns? so we can test at a unit level in the job as well
                service_instance.reload
                expect(service_instance.reload.tags).to eq(%w[foo bar])
                expect(service_instance.service_plan).to eq(original_service_plan)
                expect(service_instance).to have_annotations(
                  { prefix: 'pre.fix', key_name: 'to_delete', value: 'value' },
                  { prefix: 'pre.fix', key_name: 'fox', value: 'bushy' }
                )
                expect(service_instance).to have_labels(
                  { prefix: 'pre.fix', key_name: 'to_delete', value: 'value' },
                  { prefix: 'pre.fix', key_name: 'tail', value: 'fluffy' }
                )
              end

              context 'when changing maintenance_info' do
                let(:request_body) do
                  {
                    maintenance_info: { version: '1.1.1' }
                  }
                end

                it 'does not update the instance' do
                  service_instance.reload
                  expect(service_instance.maintenance_info.symbolize_keys).to eq(original_maintenance_info)
                end
              end
            end
          end

          context 'changing maintenance_info alongside other parameters' do
            let(:new_maintenance_info) { { version: '1.1.1' } }
            let(:request_body) do
              {
                name: 'new-name',
                maintenance_info: new_maintenance_info,
                tags: %w[baz quz]
              }
            end

            it 'modifies the instance' do
              execute_all_jobs(expected_successes: 1, expected_failures: 0)

              service_instance.reload
              expect(service_instance.maintenance_info.symbolize_keys).to eq(new_maintenance_info)
              expect(service_instance.last_operation.type).to eq('update')
              expect(service_instance.last_operation.state).to eq('succeeded')
              expect(service_instance.name).to eq('new-name')
              expect(service_instance.tags).to include('baz', 'quz')
            end
          end
        end

        context 'database disconnect error during creation of pollable job' do
          before do
            allow(VCAP::CloudController::PollableJobModel).to receive(:create).and_raise(Sequel::DatabaseDisconnectError)
          end

          it 'sets the last operation to failed' do
            api_call.call(space_dev_headers)

            service_instance.reload
            expect(service_instance.last_operation.type).to eq('update')
            expect(service_instance.last_operation.state).to eq('failed')
          end
        end
      end

      describe 'no changes requested' do
        let!(:service_instance) do
          si = VCAP::CloudController::ManagedServiceInstance.make(
            tags: %w[foo bar],
            space: space
          )
          si
        end

        let(:guid) { service_instance.guid }

        it 'updates the instance synchronously' do
          api_call.call(space_dev_headers)

          expect(last_response).to have_status_code(200)
          expect(parsed_response).to match_json_response(
            create_managed_json(
              service_instance,
              last_operation: {
                created_at: iso8601,
                updated_at: iso8601,
                description: nil,
                state: 'succeeded',
                type: 'update'
              },
              tags: %w[foo bar]
            )
          )
        end
      end

      describe 'maintenance_info checks' do
        let!(:service_instance) do
          VCAP::CloudController::ManagedServiceInstance.make(
            space:,
            service_plan:
          )
        end
        let(:guid) { service_instance.guid }

        context 'changing maintenance_info when the plan does not support it' do
          let(:service_offering) { VCAP::CloudController::Service.make(plan_updateable: true) }
          let(:service_plan) { VCAP::CloudController::ServicePlan.make(public: true, active: true, service: service_offering) }
          let(:service_plan_guid) { service_plan.guid }

          let(:request_body) do
            {
              maintenance_info: {
                version: '3.1.0'
              }
            }
          end

          it 'fails with a descriptive message' do
            api_call.call(space_dev_headers)

            expect(last_response).to have_status_code(422)
            expect(parsed_response['errors']).to include(
              include(
                {
                  'title' => 'CF-UnprocessableEntity',
                  'detail' => 'The service broker does not support upgrades for service instances created from this plan.',
                  'code' => 10_008
                }
              )
            )
          end
        end

        context 'maintenance_info conflict' do
          let(:service_offering) { VCAP::CloudController::Service.make(plan_updateable: true) }
          let(:service_plan) do
            VCAP::CloudController::ServicePlan.make(
              public: true,
              active: true,
              service: service_offering,
              maintenance_info: { version: '2.1.0' }
            )
          end
          let(:service_plan_guid) { service_plan.guid }

          let(:request_body) do
            {
              maintenance_info: {
                version: '2.2.0'
              }
            }
          end

          it 'fails with a descriptive message' do
            api_call.call(space_dev_headers)

            expect(last_response).to have_status_code(422)
            expect(parsed_response['errors']).to include(
              include(
                {
                  'title' => 'CF-UnprocessableEntity',
                  'detail' => include('maintenance_info.version requested is invalid'),
                  'code' => 10_008
                }
              )
            )
          end
        end

        context 'changing maintenance_info alongside plan' do
          let(:service_offering) { VCAP::CloudController::Service.make(plan_updateable: true) }
          let(:service_plan) do
            VCAP::CloudController::ServicePlan.make(
              public: true,
              active: true,
              service: service_offering,
              maintenance_info: { version: '2.2.0' }
            )
          end

          let(:new_service_plan) do
            VCAP::CloudController::ServicePlan.make(
              public: true,
              active: true,
              service: service_offering,
              maintenance_info: { version: '2.1.0' }
            )
          end

          let(:new_service_plan_guid) { new_service_plan.guid }

          let(:request_body) do
            {
              maintenance_info: {
                version: '2.2.0'
              },
              relationships: {
                service_plan: {
                  data: {
                    guid: new_service_plan_guid
                  }
                }
              }
            }
          end

          it 'fails with a descriptive message' do
            api_call.call(space_dev_headers)

            expect(last_response).to have_status_code(422)
            expect(parsed_response['errors']).to include(
              include(
                {
                  'title' => 'CF-UnprocessableEntity',
                  'detail' => include('maintenance_info should not be changed when switching to different plan.'),
                  'code' => 10_008
                }
              )
            )
          end
        end
      end

      describe 'service plan checks' do
        let!(:service_instance) do
          VCAP::CloudController::ManagedServiceInstance.make(
            tags: %w[foo bar],
            space: space
          )
        end
        let(:guid) { service_instance.guid }

        let(:request_body) do
          {
            relationships: {
              service_plan: {
                data: {
                  guid: service_plan_guid
                }
              }
            }
          }
        end

        context 'does not exist' do
          let(:service_plan_guid) { 'does-not-exist' }

          it 'fails saying the plan is invalid' do
            api_call.call(space_dev_headers)

            expect(last_response).to have_status_code(422)
            expect(parsed_response['errors']).to include(
              include({
                        'detail' => 'Invalid service plan. Ensure that the service plan exists, is available, and you have access to it.',
                        'title' => 'CF-UnprocessableEntity',
                        'code' => 10_008
                      })
            )
          end
        end

        context 'not readable by the user' do
          let(:service_plan) { VCAP::CloudController::ServicePlan.make(public: false, active: true) }
          let(:service_plan_guid) { service_plan.guid }

          it 'fails saying the plan is invalid' do
            api_call.call(space_dev_headers)
            expect(last_response).to have_status_code(422)
            expect(parsed_response['errors']).to include(
              include({
                        'detail' => 'Invalid service plan. Ensure that the service plan exists, is available, and you have access to it.',
                        'title' => 'CF-UnprocessableEntity',
                        'code' => 10_008
                      })
            )
          end
        end

        context 'not available' do
          let(:service_plan) { VCAP::CloudController::ServicePlan.make(public: true, active: false) }
          let(:service_plan_guid) { service_plan.guid }

          it 'fails saying the plan is invalid' do
            api_call.call(admin_headers)
            expect(last_response).to have_status_code(422)
            expect(parsed_response['errors']).to include(
              include({
                        'detail' => 'Invalid service plan. Ensure that the service plan exists, is available, and you have access to it.',
                        'title' => 'CF-UnprocessableEntity',
                        'code' => 10_008
                      })
            )
          end
        end

        context 'space-scoped plan from a different space' do
          let(:service_broker) { VCAP::CloudController::ServiceBroker.make(space: another_space) }
          let(:service_offering) { VCAP::CloudController::Service.make(service_broker:) }
          let(:service_plan) { VCAP::CloudController::ServicePlan.make(service: service_offering, active: true, public: false) }
          let(:service_plan_guid) { service_plan.guid }

          it 'fails saying the plan is invalid' do
            api_call.call(space_dev_headers)
            expect(last_response).to have_status_code(422)
            expect(parsed_response['errors']).to include(
              include({
                        'detail' => 'Invalid service plan. Ensure that the service plan exists, is available, and you have access to it.',
                        'title' => 'CF-UnprocessableEntity',
                        'code' => 10_008
                      })
            )
          end
        end

        context 'relates to a different service offering' do
          let(:service_plan_guid) { VCAP::CloudController::ServicePlan.make.guid }

          it 'fails saying the plan relates to a different service offering' do
            api_call.call(admin_headers)

            expect(last_response).to have_status_code(400)
            expect(parsed_response['errors']).to include(
              include({
                        'detail' => 'service plan relates to a different service offering',
                        'title' => 'CF-InvalidRelation',
                        'code' => 1002
                      })
            )
          end
        end
      end

      describe 'name checks' do
        context 'name is already used in this space' do
          let(:guid) { service_instance.guid }
          let!(:service_instance) do
            VCAP::CloudController::ManagedServiceInstance.make(
              tags: %w[foo bar],
              space: space
            )
          end

          let!(:name) { 'test' }
          let!(:other_si) { VCAP::CloudController::ServiceInstance.make(name:, space:) }
          let(:request_body) { { name: } }

          it 'fails' do
            api_call.call(admin_headers)

            expect(last_response).to have_status_code(422)
            expect(parsed_response['errors']).to include(
              include({
                        'detail' => "The service instance name is taken: #{name}",
                        'title' => 'CF-UnprocessableEntity',
                        'code' => 10_008
                      })
            )
          end
        end
      end

      describe 'invalid request' do
        let!(:service_instance) do
          si = VCAP::CloudController::ManagedServiceInstance.make(
            tags: %w[foo bar],
            space: space
          )
          si
        end

        let(:guid) { service_instance.guid }
        let(:request_body) do
          {
            relationships: {
              space: {
                data: {
                  guid: 'some-space'
                }
              }
            }
          }
        end

        it 'fails' do
          api_call.call(space_dev_headers)

          expect(last_response).to have_status_code(422)
          expect(parsed_response['errors']).to include(
            include({
                      'detail' => include("Relationships Unknown field(s): 'space'"),
                      'title' => 'CF-UnprocessableEntity',
                      'code' => 10_008
                    })
          )
        end
      end

      describe 'when the SI plan is no longer active' do
        let(:version) { { version: '2.0.0' } }
        let(:service_offering) { VCAP::CloudController::Service.make }
        let(:service_plan) do
          VCAP::CloudController::ServicePlan.make(
            public: true,
            active: false,
            maintenance_info: version,
            service: service_offering
          )
        end
        let!(:service_instance) do
          VCAP::CloudController::ManagedServiceInstance.make(space:, service_plan:)
        end
        let(:guid) { service_instance.guid }

        context 'and the request is updating parameters' do
          let(:request_body) { { parameters: { foo: 'bar', baz: 'qux' } } }

          it 'fails with a plan inaccessible message' do
            api_call.call(admin_headers)
            expect(last_response).to have_status_code(422)
            expect(parsed_response['errors']).to include(
              include({
                        'detail' => 'Cannot update parameters of a service instance that belongs to inaccessible plan',
                        'title' => 'CF-UnprocessableEntity',
                        'code' => 10_008
                      })
            )
          end
        end

        context 'and the request is updating maintenance_info' do
          let(:request_body) { { maintenance_info: { version: '2.0.0' } } }

          it 'fails with a plan inaccessible message' do
            api_call.call(admin_headers)
            expect(last_response).to have_status_code(422)
            expect(parsed_response['errors']).to include(
              include({
                        'detail' => 'Cannot update maintenance_info of a service instance that belongs to inaccessible plan',
                        'title' => 'CF-UnprocessableEntity',
                        'code' => 10_008
                      })
            )
          end
        end

        context 'and the request is updating the SI name' do
          let(:request_body) { { name: 'new-name' } }

          context 'and the service offering allows contextual updates' do
            let(:service_offering) { VCAP::CloudController::Service.make(allow_context_updates: true) }

            it 'fails with a plan inaccessible message' do
              api_call.call(admin_headers)
              expect(last_response).to have_status_code(422)
              expect(parsed_response['errors']).to include(
                include({
                          'detail' => 'Cannot update name of a service instance that belongs to inaccessible plan',
                          'title' => 'CF-UnprocessableEntity',
                          'code' => 10_008
                        })
              )
            end
          end

          context 'but the service offering does not allow contextual updates' do
            let(:service_offering) { VCAP::CloudController::Service.make(allow_context_updates: false) }

            it 'succeeds' do
              api_call.call(admin_headers)
              expect(last_response).to have_status_code(200)
            end
          end
        end
      end
    end

    context 'user-provided service instance' do
      let!(:service_instance) do
        si = VCAP::CloudController::UserProvidedServiceInstance.make(
          guid: 'bommel',
          space: space,
          name: 'foo',
          credentials: {
            foo: 'bar',
            baz: 'qux'
          },
          syslog_drain_url: 'https://foo.com',
          route_service_url: 'https://bar.com',
          tags: %w[accounting mongodb]
        )
        VCAP::CloudController::ServiceInstanceAnnotationModel.make(service_instance: si, key_prefix: 'pre.fix', key_name: 'to_delete', value: 'value')
        VCAP::CloudController::ServiceInstanceAnnotationModel.make(service_instance: si, key_prefix: 'pre.fix', key_name: 'fox', value: 'bushy')
        VCAP::CloudController::ServiceInstanceLabelModel.make(service_instance: si, key_prefix: 'pre.fix', key_name: 'to_delete', value: 'value')
        VCAP::CloudController::ServiceInstanceLabelModel.make(service_instance: si, key_prefix: 'pre.fix', key_name: 'tail', value: 'fluffy')
        si
      end

      let(:guid) { service_instance.guid }
      let(:new_name) { 'my_service_instance' }

      let(:request_body) do
        {
          name: new_name,
          credentials: {
            used_in: 'bindings',
            foo: 'bar'
          },
          syslog_drain_url: 'https://foo2.com',
          route_service_url: 'https://bar2.com',
          tags: %w[accounting couchbase nosql],
          metadata: {
            labels: {
              foo: 'bar',
              'pre.fix/to_delete': nil
            },
            annotations: {
              alpha: 'beta',
              'pre.fix/to_delete': nil
            }
          }
        }
      end

      it 'allows updates' do
        api_call.call(space_dev_headers)
        expect(last_response).to have_status_code(200)

        expect(parsed_response).to match_json_response(
          create_user_provided_json(
            service_instance.reload,
            labels: {
              foo: 'bar',
              'pre.fix/tail': 'fluffy'
            },
            annotations: {
              alpha: 'beta',
              'pre.fix/fox': 'bushy'
            },
            last_operation: {
              type: 'update',
              state: 'succeeded',
              description: 'Operation succeeded',
              created_at: iso8601,
              updated_at: iso8601
            }
          )
        )
      end

      it 'updates the a service instance in the database' do
        api_call.call(space_dev_headers)

        instance = VCAP::CloudController::ServiceInstance.last

        expect(instance.name).to eq(new_name)
        expect(instance.syslog_drain_url).to eq('https://foo2.com')
        expect(instance.route_service_url).to eq('https://bar2.com')
        expect(instance.tags).to contain_exactly('accounting', 'couchbase', 'nosql')
        expect(instance.space).to eq(space)
        expect(instance.last_operation.type).to eq('update')
        expect(instance.last_operation.state).to eq('succeeded')
        expect(instance).to have_labels({ prefix: 'pre.fix', key_name: 'tail', value: 'fluffy' }, { prefix: nil, key_name: 'foo', value: 'bar' })
        expect(instance).to have_annotations({ prefix: 'pre.fix', key_name: 'fox', value: 'bushy' }, { prefix: nil, key_name: 'alpha', value: 'beta' })
      end

      context 'when the request is invalid' do
        let(:request_body) do
          {
            guid: Sham.guid
          }
        end

        it 'is rejected' do
          api_call.call(space_dev_headers)
          expect(last_response).to have_status_code(422)
          expect(parsed_response['errors']).to include(
            include({
                      'detail' => include("Unknown field(s): 'guid'"),
                      'title' => 'CF-UnprocessableEntity',
                      'code' => 10_008
                    })
          )
        end
      end

      context 'when the name is already taken' do
        let!(:duplicate_name) { VCAP::CloudController::UserProvidedServiceInstance.make(space: space, name: new_name) }

        let(:request_body) do
          {
            name: new_name
          }
        end

        it 'is rejected' do
          api_call.call(space_dev_headers)
          expect(last_response).to have_status_code(422)
          expect(parsed_response['errors']).to include(
            include({
                      'detail' => "The service instance name is taken: #{new_name}.",
                      'title' => 'CF-UnprocessableEntity',
                      'code' => 10_008
                    })
          )
        end
      end
    end

    context 'when an operation is in progress' do
      let(:service_instance) do
        si = VCAP::CloudController::ManagedServiceInstance.make(
          space:
        )
        si
      end
      let(:guid) { service_instance.guid }
      let(:request_body) do
        {
          metadata: {
            labels: { unit: 'metre', distance: '1003' },
            annotations: { location: 'london' }
          }
        }
      end

      context 'and it is a create operation' do
        before do
          service_instance.save_with_new_operation({}, { type: 'create', state: 'in progress', description: 'almost there, I promise' })
        end

        context 'and the update contains metadata only' do
          it 'updates the metadata' do
            api_call.call(admin_headers)
            expect(last_response).to have_status_code(200)
            expect(parsed_response.dig('metadata', 'labels')).to eq({ 'unit' => 'metre', 'distance' => '1003' })
            expect(parsed_response.dig('metadata', 'annotations')).to eq({ 'location' => 'london' })
          end

          it 'does not update the service instance last operation' do
            api_call.call(admin_headers)
            expect(last_response).to have_status_code(200)
            expect(parsed_response['last_operation']).to include({
                                                                   'type' => 'create',
                                                                   'state' => 'in progress',
                                                                   'description' => 'almost there, I promise'
                                                                 })
          end
        end

        context 'and the update contains more than just metadata' do
          it 'returns an error' do
            request_body[:name] = 'new-name'
            api_call.call(admin_headers)
            expect(last_response).to have_status_code(409)
            response = parsed_response['errors'].first
            expect(response).to include('title' => 'CF-AsyncServiceInstanceOperationInProgress')
            expect(response).to include('detail' => include("An operation for service instance #{service_instance.name} is in progress"))
          end
        end
      end

      context 'and it is an update operation' do
        before do
          service_instance.save_with_new_operation({}, { type: 'update', state: 'in progress', description: 'almost there, I promise' })
        end

        context 'and the update contains metadata only' do
          it 'updates the metadata' do
            api_call.call(admin_headers)
            expect(last_response).to have_status_code(200)
            expect(parsed_response.dig('metadata', 'labels')).to eq({ 'unit' => 'metre', 'distance' => '1003' })
            expect(parsed_response.dig('metadata', 'annotations')).to eq({ 'location' => 'london' })
          end

          it 'does not update the service instance last operation' do
            api_call.call(admin_headers)
            expect(last_response).to have_status_code(200)
            expect(parsed_response['last_operation']).to include({
                                                                   'type' => 'update',
                                                                   'state' => 'in progress',
                                                                   'description' => 'almost there, I promise'
                                                                 })
          end
        end

        context 'and the update contains more than just metadata' do
          it 'returns an error' do
            request_body[:name] = 'new-name'
            api_call.call(admin_headers)
            expect(last_response).to have_status_code(409)
            response = parsed_response['errors'].first
            expect(response).to include('title' => 'CF-AsyncServiceInstanceOperationInProgress')
            expect(response).to include('detail' => include("An operation for service instance #{service_instance.name} is in progress"))
          end
        end
      end

      context 'and it is a delete operation' do
        before do
          service_instance.save_with_new_operation({}, { type: 'delete', state: 'in progress', description: 'almost there, I promise' })
        end

        context 'and the update contains metadata only' do
          it 'returns an error' do
            api_call.call(admin_headers)
            expect(last_response).to have_status_code(409)
            response = parsed_response['errors'].first
            expect(response).to include('title' => 'CF-AsyncServiceInstanceOperationInProgress')
            expect(response).to include('detail' => include("An operation for service instance #{service_instance.name} is in progress"))
          end
        end

        context 'and the update contains more than just metadata' do
          it 'returns an error' do
            request_body[:name] = 'new-name'
            api_call.call(admin_headers)
            expect(last_response).to have_status_code(409)
            response = parsed_response['errors'].first
            expect(response).to include('title' => 'CF-AsyncServiceInstanceOperationInProgress')
            expect(response).to include('detail' => include("An operation for service instance #{service_instance.name} is in progress"))
          end
        end
      end
    end
  end

  describe 'DELETE /v3/service_instances/:guid' do
    let(:query_params) { '' }
    let(:api_call) { ->(user_headers) { delete "/v3/service_instances/#{instance.guid}?#{query_params}", '{}', user_headers } }

    context 'permissions' do
      let!(:instance) { VCAP::CloudController::UserProvidedServiceInstance.make(space:) }
      let(:db_check) do
        lambda {
          expect(VCAP::CloudController::ServiceInstance.all).to be_empty
        }
      end

      it_behaves_like 'permissions for delete endpoint', ALL_PERMISSIONS do
        let(:expected_codes_and_responses) { responses_for_space_restricted_delete_endpoint }
      end

      it_behaves_like 'permissions for delete endpoint when organization is suspended', 204
    end

    context 'user provided service instances' do
      let!(:instance) do
        si = VCAP::CloudController::UserProvidedServiceInstance.make(space: space, route_service_url: 'https://banana.example.com/')
        si.service_instance_operation = VCAP::CloudController::ServiceInstanceOperation.make(type: 'create', state: 'succeeded')
        si
      end
      let(:instance_labels) { VCAP::CloudController::ServiceInstanceLabelModel.where(service_instance: instance) }
      let(:instance_annotations) { VCAP::CloudController::ServiceInstanceAnnotationModel.where(service_instance: instance) }

      before do
        VCAP::CloudController::ServiceInstanceLabelModel.make(key_name: 'fruit', value: 'banana', service_instance: instance)
        VCAP::CloudController::ServiceInstanceLabelModel.make(key_name: 'spice', value: 'cinnamon', service_instance: instance)
        VCAP::CloudController::ServiceInstanceAnnotationModel.make(key_name: 'contact', value: 'marie', service_instance: instance)
        VCAP::CloudController::ServiceInstanceAnnotationModel.make(key_name: 'email', value: 'some@example.com', service_instance: instance)
      end

      it 'deletes the instance and removes any labels or annotations' do
        api_call.call(admin_headers)
        expect(last_response).to have_status_code(204)

        get "/v3/service_instances/#{instance.guid}", {}, admin_headers
        expect(last_response.status).to eq(404)
        expect(VCAP::CloudController::ServiceInstanceLabelModel.where(service_instance: instance).all).to be_empty
        expect(VCAP::CloudController::ServiceInstanceAnnotationModel.where(service_instance: instance).all).to be_empty
      end

      it 'deletes any related bindings' do
        VCAP::CloudController::RouteBinding.make(service_instance: instance)
        VCAP::CloudController::ServiceBinding.make(service_instance: instance)

        api_call.call(admin_headers)
        expect(last_response).to have_status_code(204)

        expect(VCAP::CloudController::ServiceInstance.all).to be_empty
        expect(VCAP::CloudController::RouteBinding.all).to be_empty
        expect(VCAP::CloudController::ServiceBinding.all).to be_empty
      end

      context 'with purge' do
        let(:query_params) { 'purge=true' }

        before do
          @binding = VCAP::CloudController::ServiceBinding.make(service_instance: instance)
          @route = VCAP::CloudController::RouteBinding.make(service_instance: instance)
        end

        it 'deletes the instance and the related resources' do
          api_call.call(admin_headers)
          expect(last_response).to have_status_code(204)

          expect { instance.reload }.to raise_error Sequel::NoExistingObject
          expect { @binding.reload }.to raise_error Sequel::NoExistingObject
          expect { @route.reload }.to raise_error Sequel::NoExistingObject
          expect(instance_labels.count).to eq(0)
          expect(instance_annotations.count).to eq(0)
        end
      end
    end

    context 'managed service instance' do
      let!(:instance) { VCAP::CloudController::ManagedServiceInstance.make(space:) }
      let(:broker_status_code) { 200 }
      let(:broker_response) { {} }
      let!(:stub_delete) do
        stub_request(:delete, "#{instance.service_broker.broker_url}/v2/service_instances/#{instance.guid}").
          with(query: {
                 'accepts_incomplete' => true,
                 'service_id' => instance.service.broker_provided_id,
                 'plan_id' => instance.service_plan.broker_provided_id
               }).
          to_return(status: broker_status_code, body: broker_response.to_json, headers: {})
      end

      it 'responds with job resource' do
        api_call.call(admin_headers)
        expect(last_response).to have_status_code(202)

        job = VCAP::CloudController::PollableJobModel.last
        expect(last_response.headers['Location']).to end_with("/v3/jobs/#{job.guid}")

        expect(job.state).to eq(VCAP::CloudController::PollableJobModel::PROCESSING_STATE)
        expect(job.operation).to eq('service_instance.delete')
        expect(job.resource_guid).to eq(instance.guid)
        expect(job.resource_type).to eq('service_instance')
      end

      describe 'the pollable job' do
        it 'sends a delete request with the right arguments to the service broker' do
          api_call.call(headers_for(user, scopes: %w[cloud_controller.admin]))

          execute_all_jobs(expected_successes: 1, expected_failures: 0)

          encoded_user_guid = Base64.strict_encode64("{\"user_id\":\"#{user.guid}\"}")
          expect(
            a_request(:delete, "#{instance.service_broker.broker_url}/v2/service_instances/#{instance.guid}").
              with(
                query: {
                  accepts_incomplete: true,
                  service_id: instance.service.broker_provided_id,
                  plan_id: instance.service_plan.broker_provided_id
                },
                headers: { 'X-Broker-Api-Originating-Identity' => "cloudfoundry #{encoded_user_guid}" }
              )
          ).to have_been_made.once
        end

        context 'when the service broker responds synchronously' do
          context 'with success' do
            it 'removes the service instance' do
              api_call.call(admin_headers)
              execute_all_jobs(expected_successes: 1, expected_failures: 0)

              expect(VCAP::CloudController::ServiceInstance.first(guid: instance.guid)).to be_nil
            end

            it 'completes the job' do
              api_call.call(admin_headers)
              execute_all_jobs(expected_successes: 1, expected_failures: 0)

              job = VCAP::CloudController::PollableJobModel.last
              expect(job.state).to eq(VCAP::CloudController::PollableJobModel::COMPLETE_STATE)
            end
          end

          context 'with an error' do
            let(:broker_status_code) { 404 }

            it 'marks the service instance as delete failed' do
              api_call.call(admin_headers)
              execute_all_jobs(expected_successes: 0, expected_failures: 1)
              instance.reload

              expect(instance.last_operation).not_to be_nil
              expect(instance.last_operation.type).to eq('delete')
              expect(instance.last_operation.state).to eq('failed')
            end

            it 'completes with failure' do
              api_call.call(admin_headers)
              execute_all_jobs(expected_successes: 0, expected_failures: 1)

              job = VCAP::CloudController::PollableJobModel.last
              expect(job.state).to eq(VCAP::CloudController::PollableJobModel::FAILED_STATE)
            end
          end
        end

        context 'when the service broker responds asynchronously' do
          let(:broker_status_code) { 202 }
          let(:broker_response) { { operation: 'some delete operation' } }
          let(:last_operation_response) { { state: 'in progress', description: 'deleting si' } }
          let(:last_operation_status_code) { 200 }
          let(:job) { VCAP::CloudController::PollableJobModel.last }

          before do
            stub_request(:get, "#{instance.service_broker.broker_url}/v2/service_instances/#{instance.guid}/last_operation").
              with(
                query: {
                  operation: 'some delete operation',
                  service_id: instance.service.broker_provided_id,
                  plan_id: instance.service_plan.broker_provided_id
                }
              ).
              to_return(status: last_operation_status_code, body: last_operation_response.to_json, headers: {})
          end

          it 'marks the job state as polling' do
            api_call.call(admin_headers)
            execute_all_jobs(expected_successes: 1, expected_failures: 0)

            expect(job.state).to eq(VCAP::CloudController::PollableJobModel::POLLING_STATE)
          end

          it 'calls last operation immediately' do
            api_call.call(headers_for(user, scopes: %w[cloud_controller.admin]))
            execute_all_jobs(expected_successes: 1, expected_failures: 0)

            encoded_user_guid = Base64.strict_encode64("{\"user_id\":\"#{user.guid}\"}")
            expect(
              a_request(:get, "#{instance.service_broker.broker_url}/v2/service_instances/#{instance.guid}/last_operation").
                with(
                  query: {
                    operation: 'some delete operation',
                    service_id: instance.service.broker_provided_id,
                    plan_id: instance.service_plan.broker_provided_id
                  },
                  headers: { 'X-Broker-Api-Originating-Identity' => "cloudfoundry #{encoded_user_guid}" }
                )
            ).to have_been_made.once
          end

          it 'enqueues the next fetch last operation job' do
            api_call.call(admin_headers)

            execute_all_jobs(expected_successes: 1, expected_failures: 0)
            expect(Delayed::Job.count).to eq(1)
          end

          it 'sets the service instance last operation to delete in progress' do
            api_call.call(admin_headers)
            execute_all_jobs(expected_successes: 1, expected_failures: 0)

            instance.reload

            expect(instance.last_operation).not_to be_nil
            expect(instance.last_operation.type).to eq('delete')
            expect(instance.last_operation.state).to eq('in progress')
          end

          context 'when last operation eventually returns `delete succeeded`' do
            before do
              stub_request(:get, "#{instance.service_broker.broker_url}/v2/service_instances/#{instance.guid}/last_operation").
                with(
                  query: {
                    operation: 'some delete operation',
                    service_id: instance.service.broker_provided_id,
                    plan_id: instance.service_plan.broker_provided_id
                  }
                ).
                to_return(status: last_operation_status_code, body: last_operation_response.to_json, headers: {}).times(1).then.
                to_return(status: 200, body: { state: 'succeeded' }.to_json, headers: {})

              api_call.call(admin_headers)

              execute_all_jobs(expected_successes: 1, expected_failures: 0)
              expect(job.state).to eq(VCAP::CloudController::PollableJobModel::POLLING_STATE)

              Timecop.freeze(Time.now + 1.hour) do
                execute_all_jobs(expected_successes: 1, expected_failures: 0)
              end
            end

            it 'completes the job' do
              updated_job = VCAP::CloudController::PollableJobModel.find(guid: job.guid)
              expect(updated_job.state).to eq(VCAP::CloudController::PollableJobModel::COMPLETE_STATE)
            end

            it 'removes the service instance last from the db' do
              expect(VCAP::CloudController::ServiceInstance.first(guid: instance.guid)).to be_nil
            end
          end

          context 'when last operation eventually returns `delete failed`' do
            before do
              stub_request(:get, "#{instance.service_broker.broker_url}/v2/service_instances/#{instance.guid}/last_operation").
                with(
                  query: {
                    operation: 'some delete operation',
                    service_id: instance.service.broker_provided_id,
                    plan_id: instance.service_plan.broker_provided_id
                  }
                ).
                to_return(status: last_operation_status_code, body: last_operation_response.to_json, headers: {}).times(3).then.
                to_return(status: 200, body: { state: 'failed', description: 'oh no failed' }.to_json, headers: {})

              api_call.call(admin_headers)

              execute_all_jobs(expected_successes: 1, expected_failures: 0)
              expect(job.state).to eq(VCAP::CloudController::PollableJobModel::POLLING_STATE)

              (1..2).each do |attempt|
                Timecop.freeze(Time.now + attempt.hour) do
                  execute_all_jobs(expected_successes: 1, expected_failures: 0, jobs_to_execute: 1)
                end
              end
              Timecop.freeze(Time.now + 3.hours) do
                execute_all_jobs(expected_successes: 0, expected_failures: 1, jobs_to_execute: 1)
              end
            end

            it 'completes the job' do
              updated_job = VCAP::CloudController::PollableJobModel.find(guid: job.guid)
              expect(updated_job.state).to eq(VCAP::CloudController::PollableJobModel::FAILED_STATE)
            end

            it 'sets the service instance last operation to delete failed' do
              expect(instance.last_operation.type).to eq('delete')
              expect(instance.last_operation.state).to eq('failed')
              expect(instance.last_operation.description).to eq('oh no failed')
            end
          end

          context 'when last operation eventually returns 410 Gone' do
            before do
              stub_request(:get, "#{instance.service_broker.broker_url}/v2/service_instances/#{instance.guid}/last_operation").
                with(
                  query: {
                    operation: 'some delete operation',
                    service_id: instance.service.broker_provided_id,
                    plan_id: instance.service_plan.broker_provided_id
                  }
                ).
                to_return(status: last_operation_status_code, body: last_operation_response.to_json, headers: {}).times(1).then.
                to_return(status: 410, headers: {})

              api_call.call(admin_headers)

              execute_all_jobs(expected_successes: 1, expected_failures: 0)
              expect(job.state).to eq(VCAP::CloudController::PollableJobModel::POLLING_STATE)

              Timecop.freeze(Time.now + 1.hour) do
                execute_all_jobs(expected_successes: 1, expected_failures: 0)
              end
            end

            it 'completes the job' do
              updated_job = VCAP::CloudController::PollableJobModel.find(guid: job.guid)
              expect(updated_job.state).to eq(VCAP::CloudController::PollableJobModel::COMPLETE_STATE)
            end

            it 'removes the service instance last from the db' do
              expect(VCAP::CloudController::ServiceInstance.first(guid: instance.guid)).to be_nil
            end
          end

          context 'when last operation eventually returns 400 Bad Request' do
            before do
              stub_request(:get, "#{instance.service_broker.broker_url}/v2/service_instances/#{instance.guid}/last_operation").
                with(
                  query: {
                    operation: 'some delete operation',
                    service_id: instance.service.broker_provided_id,
                    plan_id: instance.service_plan.broker_provided_id
                  }
                ).
                to_return(status: last_operation_status_code, body: last_operation_response.to_json, headers: {}).times(1).then.
                to_return(status: 400, headers: {})

              api_call.call(admin_headers)

              execute_all_jobs(expected_successes: 1, expected_failures: 0)
              expect(job.state).to eq(VCAP::CloudController::PollableJobModel::POLLING_STATE)

              Timecop.freeze(Time.now + 1.hour) do
                execute_all_jobs(expected_successes: 0, expected_failures: 1)
              end
            end

            it 'fails the job' do
              updated_job = VCAP::CloudController::PollableJobModel.find(guid: job.guid)
              expect(updated_job.state).to eq(VCAP::CloudController::PollableJobModel::FAILED_STATE)
            end

            it 'sets the service instance last operation to delete failed' do
              expect(instance.last_operation.type).to eq('delete')
              expect(instance.last_operation.state).to eq('failed')
            end
          end

          context 'when last operation returns with an unknown status code' do
            before do
              stub_request(:get, "#{instance.service_broker.broker_url}/v2/service_instances/#{instance.guid}/last_operation").
                with(
                  query: {
                    operation: 'some delete operation',
                    service_id: instance.service.broker_provided_id,
                    plan_id: instance.service_plan.broker_provided_id
                  }
                ).
                to_return(status: last_operation_status_code, body: last_operation_response.to_json, headers: {}).times(1).then.
                to_return(status: 404, headers: {})

              api_call.call(admin_headers)

              execute_all_jobs(expected_successes: 1, expected_failures: 0)
              expect(job.state).to eq(VCAP::CloudController::PollableJobModel::POLLING_STATE)

              Timecop.freeze(Time.now + 1.hour) do
                execute_all_jobs(expected_successes: 1, expected_failures: 0)
              end
            end

            it 'continues to poll' do
              updated_job = VCAP::CloudController::PollableJobModel.find(guid: job.guid)
              expect(updated_job.state).to eq(VCAP::CloudController::PollableJobModel::POLLING_STATE)
            end
          end
        end

        context 'when the service instance is shared' do
          let!(:shared_space) do
            VCAP::CloudController::Space.make.tap do |s|
              instance.add_shared_space(s)
            end
          end

          it 'removes the service instance' do
            api_call.call(admin_headers)
            execute_all_jobs(expected_successes: 1, expected_failures: 0)

            expect(VCAP::CloudController::ServiceInstance.all).to be_empty
          end

          context 'when there is a binding in the shared space' do
            let!(:application) { VCAP::CloudController::AppModel.make(space: shared_space) }
            let!(:service_binding) { VCAP::CloudController::ServiceBinding.make(service_instance: instance, app: application) }

            before do
              stub_request(:delete, "#{instance.service_broker.broker_url}/v2/service_instances/#{instance.guid}/service_bindings/#{service_binding.guid}").
                with(query: {
                       'accepts_incomplete' => true,
                       'service_id' => instance.service.broker_provided_id,
                       'plan_id' => instance.service_plan.broker_provided_id
                     }).
                to_return(status: 202, body: '{}', headers: {})
            end

            it 'fails when the unbind is async' do
              api_call.call(admin_headers)
              execute_all_jobs(expected_successes: 0, expected_failures: 1, jobs_to_execute: 1)

              lo = instance.last_operation
              expect(lo.type).to eq('delete')
              expect(lo.state).to eq('failed')
              expect(lo.description).to eq("An operation for the service binding between app #{application.name} and service instance #{instance.name} is in progress.")

              expect(
                stub_request(:delete, "#{instance.service_broker.broker_url}/v2/service_instances/#{instance.guid}/service_bindings/#{service_binding.guid}").
                  with(query: {
                         'accepts_incomplete' => true,
                         service_id: instance.service.broker_provided_id,
                         plan_id: instance.service_plan.broker_provided_id
                       })
              ).to have_been_made.once
            end
          end
        end

        context 'when there are bindings' do
          let(:service_offering) { VCAP::CloudController::Service.make(requires: %w[route_forwarding]) }
          let(:service_plan) { VCAP::CloudController::ServicePlan.make(service: service_offering) }
          let(:instance) { VCAP::CloudController::ManagedServiceInstance.make(space:, service_plan:) }
          let!(:route_binding) { VCAP::CloudController::RouteBinding.make(service_instance: instance) }
          let!(:service_binding) { VCAP::CloudController::ServiceBinding.make(service_instance: instance) }
          let!(:service_key) { VCAP::CloudController::ServiceKey.make(service_instance: instance) }

          context 'and the broker responds synchronously to the bindings being deleted' do
            before do
              [route_binding, service_binding, service_key].each do |binding|
                stub_request(:delete, "#{instance.service_broker.broker_url}/v2/service_instances/#{instance.guid}/service_bindings/#{binding.guid}").
                  with(query: {
                         'accepts_incomplete' => true,
                         'service_id' => instance.service.broker_provided_id,
                         'plan_id' => instance.service_plan.broker_provided_id
                       }).
                  to_return(status: 200, body: '{}', headers: {})
              end
            end

            it 'removes the service instance' do
              api_call.call(admin_headers)
              execute_all_jobs(expected_successes: 1, expected_failures: 0)

              expect(VCAP::CloudController::ServiceInstance.all).to be_empty
              expect(VCAP::CloudController::RouteBinding.all).to be_empty
              expect(VCAP::CloudController::ServiceBinding.all).to be_empty
              expect(VCAP::CloudController::ServiceKey.all).to be_empty

              [route_binding, service_binding, service_key].each do |binding|
                expect(
                  a_request(:delete, "#{instance.service_broker.broker_url}/v2/service_instances/#{instance.guid}/service_bindings/#{binding.guid}").
                    with(query: {
                           accepts_incomplete: true,
                           service_id: instance.service.broker_provided_id,
                           plan_id: instance.service_plan.broker_provided_id
                         })
                ).to have_been_made.once
              end
            end
          end

          context 'and the broker responds asynchronously to the bindings being deleted' do
            before do
              [route_binding, service_binding, service_key].each do |binding|
                stub_request(:delete, "#{instance.service_broker.broker_url}/v2/service_instances/#{instance.guid}/service_bindings/#{binding.guid}").
                  with(query: {
                         'accepts_incomplete' => true,
                         'service_id' => instance.service.broker_provided_id,
                         'plan_id' => instance.service_plan.broker_provided_id
                       }).
                  to_return(status: 202, body: '{}', headers: {})

                stub_request(:get, "#{instance.service_broker.broker_url}/v2/service_instances/#{instance.guid}/service_bindings/#{binding.guid}/last_operation").
                  with(query: {
                         'service_id' => instance.service.broker_provided_id,
                         'plan_id' => instance.service_plan.broker_provided_id
                       }).
                  to_return(status: 200, body: '{"state":"succeeded"}', headers: {})
              end
            end

            it 'fails and starts the delete operation on the bindings' do
              api_call.call(admin_headers)
              execute_all_jobs(expected_successes: 0, expected_failures: 1, jobs_to_execute: 1)

              lo = VCAP::CloudController::ServiceInstance.first.last_operation
              expect(lo.type).to eq('delete')
              expect(lo.state).to eq('failed')
              expect(lo.description).to eq("An operation for a service binding of service instance #{instance.name} is in progress.")

              lo = VCAP::CloudController::RouteBinding.first.last_operation
              expect(lo.type).to eq('delete')
              expect(lo.state).to eq('in progress')

              lo = VCAP::CloudController::ServiceBinding.first.last_operation
              expect(lo.type).to eq('delete')
              expect(lo.state).to eq('in progress')

              lo = VCAP::CloudController::ServiceKey.first.last_operation
              expect(lo.type).to eq('delete')
              expect(lo.state).to eq('in progress')
            end

            it 'continues to poll the last operation for the bindings' do
              api_call.call(admin_headers)
              execute_all_jobs(expected_successes: 3, expected_failures: 1)

              [route_binding, service_binding, service_key].each do |binding|
                expect(
                  stub_request(:get, "#{instance.service_broker.broker_url}/v2/service_instances/#{instance.guid}/service_bindings/#{binding.guid}/last_operation").
                    with(query: {
                           service_id: instance.service.broker_provided_id,
                           plan_id: instance.service_plan.broker_provided_id
                         })
                ).to have_been_made.once
              end
            end

            it 'eventually removes the bindings' do
              api_call.call(admin_headers)
              execute_all_jobs(expected_successes: 3, expected_failures: 1)

              expect(VCAP::CloudController::RouteBinding.all).to be_empty
              expect(VCAP::CloudController::ServiceBinding.all).to be_empty
              expect(VCAP::CloudController::ServiceKey.all).to be_empty
            end
          end
        end
      end

      context 'when purge is true' do
        let(:query_params) { 'purge=true' }
        let(:service_plan) { VCAP::CloudController::ServicePlan.make(service: service_offering) }
        let(:instance) { VCAP::CloudController::ManagedServiceInstance.make(space:, service_plan:) }

        context 'when broker is space scoped' do
          let(:service_broker) { VCAP::CloudController::ServiceBroker.make(space:) }
          let(:service_offering) { VCAP::CloudController::Service.make(requires: %w[route_forwarding], service_broker: service_broker) }

          context 'as developer' do
            let(:space_dev_headers) do
              org.add_user(user)
              space.add_developer(user)
              headers_for(user)
            end

            it 'deletes the service instance' do
              api_call.call(space_dev_headers)

              expect(last_response).to have_status_code(204)
              expect { instance.reload }.to raise_error Sequel::NoExistingObject
            end
          end

          context 'as admin' do
            it 'deletes the service instance' do
              api_call.call(admin_headers)

              expect(last_response).to have_status_code(204)
              expect { instance.reload }.to raise_error Sequel::NoExistingObject
            end
          end
        end

        context 'when broker is global' do
          let(:service_offering) { VCAP::CloudController::Service.make(requires: %w[route_forwarding]) }

          before do
            @binding = VCAP::CloudController::ServiceBinding.make(service_instance: instance)
            @key = VCAP::CloudController::ServiceKey.make(service_instance: instance)
            @route = VCAP::CloudController::RouteBinding.make(service_instance: instance)
          end

          context 'as developer' do
            let(:space_dev_headers) do
              org.add_user(user)
              space.add_developer(user)
              headers_for(user)
            end

            it 'responds with 403' do
              api_call.call(space_dev_headers)

              expect(last_response).to have_status_code(403)
            end
          end

          context 'as admin' do
            before do
              api_call.call(admin_headers)
            end

            it 'removes all associations' do
              expect { @binding.reload }.to raise_error Sequel::NoExistingObject
              expect { @key.reload }.to raise_error Sequel::NoExistingObject
              expect { @route.reload }.to raise_error Sequel::NoExistingObject
            end

            it 'deletes the service instance' do
              expect { instance.reload }.to raise_error Sequel::NoExistingObject
            end

            it 'responds with 204' do
              expect(last_response).to have_status_code(204)
            end
          end
        end
      end

      context 'when delete is already in progress' do
        before do
          instance.save_with_new_operation({}, { type: 'delete', state: 'in progress' })
        end

        it 'responds with 422' do
          api_call.call(admin_headers)

          expect(last_response).to have_status_code(422)
          expect(parsed_response['errors']).to include(include({
                                                                 'detail' => include('There is an operation in progress for the service instance.'),
                                                                 'title' => 'CF-UnprocessableEntity',
                                                                 'code' => 10_008
                                                               }))
        end
      end

      context 'when the service instance creation request has not been responded to be the broker' do
        before do
          instance.save_with_new_operation({}, { type: 'create', state: 'initial' })
        end

        it 'responds with 422' do
          api_call.call(admin_headers)

          expect(last_response).to have_status_code(422)
          expect(parsed_response['errors']).to include(include({
                                                                 'detail' => include('There is an operation in progress for the service instance.'),
                                                                 'title' => 'CF-UnprocessableEntity',
                                                                 'code' => 10_008
                                                               }))
        end
      end

      context 'when the creation is still in progress' do
        before do
          instance.save_with_new_operation({}, {
                                             type: 'create',
                                             state: 'in progress',
                                             broker_provided_operation: 'some create operation'
                                           })
        end

        context 'and the broker confirms the deletion' do
          it 'deletes the service instance' do
            api_call.call(admin_headers)
            execute_all_jobs(expected_successes: 1, expected_failures: 0)

            expect(VCAP::CloudController::ServiceInstance.first(guid: instance.guid)).to be_nil
          end
        end

        context 'and the broker accepts the delete' do
          let(:broker_status_code) { 202 }
          let(:broker_response) { { operation: 'some delete operation' } }
          let(:last_operation_response) { { state: 'in progress', description: 'deleting si' } }
          let(:last_operation_status_code) { 200 }

          before do
            stub_request(:get, "#{instance.service_broker.broker_url}/v2/service_instances/#{instance.guid}/last_operation").
              with(
                query: {
                  operation: 'some delete operation',
                  service_id: instance.service.broker_provided_id,
                  plan_id: instance.service_plan.broker_provided_id
                }
              ).
              to_return(status: last_operation_status_code, body: last_operation_response.to_json, headers: {})
          end

          it 'triggers the delete process' do
            api_call.call(admin_headers)
            expect(last_response).to have_status_code(HTTP::Status::ACCEPTED)
            execute_all_jobs(expected_successes: 1, expected_failures: 0)

            instance.reload

            expect(instance.last_operation).not_to be_nil
            expect(instance.last_operation.type).to eq('delete')
            expect(instance.last_operation.state).to eq('in progress')
            expect(instance.last_operation.broker_provided_operation).to eq('some delete operation')
          end
        end

        context 'but the broker rejects the delete' do
          let(:broker_status_code) { 422 }
          let(:broker_response) { { error: 'ConcurrencyError', description: 'Cannot delete right now' } }

          it 'responds with an error' do
            api_call.call(admin_headers)
            expect(last_response).to have_status_code(HTTP::Status::ACCEPTED)
            execute_all_jobs(expected_successes: 0, expected_failures: 1)

            job = VCAP::CloudController::PollableJobModel.last
            expect(job.state).to eq(VCAP::CloudController::PollableJobModel::FAILED_STATE)

            expect(job.cf_api_error).not_to be_nil
            api_error = YAML.safe_load(job.cf_api_error)['errors'].first
            expect(api_error['title']).to eql('CF-AsyncServiceInstanceOperationInProgress')
            expect(api_error['detail']).to eql("An operation for service instance #{instance.name} is in progress.")
          end

          it 'does not change the operation in progress' do
            api_call.call(admin_headers)
            expect(last_response).to have_status_code(HTTP::Status::ACCEPTED)
            execute_all_jobs(expected_successes: 0, expected_failures: 1)

            instance.reload

            expect("#{instance.last_operation.type} #{instance.last_operation.state}").to eq('create in progress')
            expect(instance.last_operation.broker_provided_operation).to eq('some create operation')
          end
        end
      end
    end

    context 'when the service instance does not exist' do
      let(:instance) { Struct.new(:guid).new('some-fake-guid') }

      it 'returns a 404' do
        api_call.call(admin_headers)
        expect(last_response).to have_status_code(404)
      end
    end
  end

  describe 'POST /v3/service_instances/:guid/relationships/shared_spaces' do
    let(:api_call) { ->(user_headers) { post "/v3/service_instances/#{guid}/relationships/shared_spaces", request_body.to_json, user_headers } }
    let(:target_space_1) { VCAP::CloudController::Space.make(organization: org) }
    let(:target_space_2) { VCAP::CloudController::Space.make(organization: org) }
    let(:request_body) do
      {
        'data' => [
          { 'guid' => target_space_1.guid },
          { 'guid' => target_space_2.guid }
        ]
      }
    end
    let(:service_instance) { VCAP::CloudController::ManagedServiceInstance.make(space:) }
    let(:guid) { service_instance.guid }
    let(:space_dev_headers) do
      org.add_user(user)
      space.add_developer(user)
      headers_for(user)
    end
    let!(:feature_flag) do
      VCAP::CloudController::FeatureFlag.make(name: 'service_instance_sharing', enabled: true, error_message: nil)
    end

    before do
      org.add_user(user)
      target_space_1.add_developer(user)
      target_space_2.add_developer(user)
    end

    context 'permissions' do
      it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS do
        let(:expected_codes_and_responses) { responses_for_space_restricted_update_endpoint(success_code: 200) }
      end

      it_behaves_like 'permissions for update endpoint when organization is suspended', 200

      context 'when target organization is suspended' do
        let(:target_space_1) do
          space = VCAP::CloudController::Space.make
          space.organization.add_user(user)
          space.organization.update(status: VCAP::CloudController::Organization::SUSPENDED)
          space
        end

        it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS do
          let(:expected_codes_and_responses) do
            responses_for_org_suspended_space_restricted_update_endpoint(success_code: 200).merge({ 'space_developer' => { code: 422 } })
          end
        end
      end
    end

    it 'shares the service instance to the target space and logs audit event' do
      api_call.call(space_dev_headers)

      expect(last_response.status).to eq(200)

      event = VCAP::CloudController::Event.last
      expect(event.values).to include({
                                        type: 'audit.service_instance.share',
                                        actor: user.guid,
                                        actee_type: 'service_instance',
                                        actee_name: service_instance.name,
                                        space_guid: space.guid,
                                        organization_guid: space.organization.guid
                                      })
      expect(event.metadata['target_space_guids']).to include(target_space_1.guid, target_space_2.guid)

      service_instance.reload
      expect(service_instance.shared_spaces).to include(target_space_1, target_space_2)
    end

    describe 'when service_instance_sharing flag is disabled' do
      before do
        feature_flag.enabled = false
        feature_flag.save
      end

      it 'makes users unable to share services' do
        api_call.call(space_dev_headers)

        expect(last_response).to have_status_code(403)
        expect(parsed_response['errors']).to include(
          include(
            {
              'detail' => 'Feature Disabled: service_instance_sharing',
              'title' => 'CF-FeatureDisabled',
              'code' => 330_002
            }
          )
        )
      end
    end

    it 'responds with 404 when the instance does not exist' do
      post '/v3/service_instances/some-fake-guid/relationships/shared_spaces', request_body.to_json, space_dev_headers

      expect(last_response).to have_status_code(404)
      expect(parsed_response['errors']).to include(
        include(
          {
            'detail' => 'Service instance not found',
            'title' => 'CF-ResourceNotFound'
          }
        )
      )
    end

    describe 'when the request body is invalid' do
      context 'when it is not a valid relationship' do
        let(:request_body) do
          {
            'data' => { 'guid' => target_space_1.guid }
          }
        end

        it 'responds with 422' do
          api_call.call(space_dev_headers)

          expect(last_response.status).to eq(422)
          expect(parsed_response['errors']).to include(
            include(
              {
                'detail' => 'Data must be an array',
                'title' => 'CF-UnprocessableEntity'
              }
            )
          )
        end
      end

      context 'when there are additional keys' do
        let(:request_body) do
          {
            'data' => [
              { 'guid' => target_space_1.guid }
            ],
            'fake-key' => 'foo'
          }
        end

        it 'responds with 422' do
          api_call.call(space_dev_headers)

          expect(last_response.status).to eq(422)
          expect(parsed_response['errors']).to include(
            include(
              {
                'detail' => "Unknown field(s): 'fake-key'",
                'title' => 'CF-UnprocessableEntity'
              }
            )
          )
        end
      end
    end

    describe 'target space to share to' do
      context 'does not exist' do
        let(:target_space_guid) { 'fake-target' }
        let(:request_body) do
          {
            'data' => [
              { 'guid' => target_space_guid }
            ]
          }
        end

        it 'responds with 422' do
          api_call.call(space_dev_headers)

          expect(last_response.status).to eq(422)
          expect(parsed_response['errors']).to include(
            include(
              {
                'detail' => "Unable to share service instance #{service_instance.name} with spaces ['#{target_space_guid}']. " \
                            'Ensure the spaces exist and that you have access to them.',
                'title' => 'CF-UnprocessableEntity'
              }
            )
          )
        end
      end

      context 'user does not have access to one of the target spaces' do
        let(:no_access_target_space) { VCAP::CloudController::Space.make(organization: org) }
        let(:request_body) do
          {
            'data' => [
              { 'guid' => no_access_target_space.guid },
              { 'guid' => target_space_1.guid }
            ]
          }
        end

        it 'responds with 422 and does not share the instance' do
          api_call.call(space_dev_headers)

          expect(last_response.status).to eq(422)
          expect(parsed_response['errors']).to include(
            include(
              {
                'detail' => "Unable to share service instance #{service_instance.name} with spaces ['#{no_access_target_space.guid}']. " \
                            'Ensure the spaces exist and that you have access to them.',
                'title' => 'CF-UnprocessableEntity'
              }
            )
          )

          service_instance.reload
          expect(service_instance).not_to be_shared
        end
      end

      context 'owns the space' do
        let(:no_access_target_space) { VCAP::CloudController::Space.make(organization: org) }
        let(:request_body) do
          {
            'data' => [
              { 'guid' => space.guid },
              { 'guid' => target_space_1.guid }
            ]
          }
        end

        it 'responds with 422 and does not share the instance' do
          api_call.call(space_dev_headers)

          expect(last_response.status).to eq(422)
          expect(parsed_response['errors']).to include(
            include(
              {
                'detail' => "Unable to share service instance '#{service_instance.name}' with space '#{space.guid}'. " \
                            'Service instances cannot be shared into the space where they were created.',
                'title' => 'CF-UnprocessableEntity'
              }
            )
          )

          service_instance.reload
          expect(service_instance).not_to be_shared
        end
      end
    end

    describe 'errors while sharing' do
      context 'service instance is user provided' do
        let(:service_instance) { VCAP::CloudController::UserProvidedServiceInstance.make(space:) }

        it 'responds with 422 and the error' do
          api_call.call(space_dev_headers)

          expect(last_response.status).to eq(422)
          expect(parsed_response['errors']).to include(
            include(
              {
                'detail' => 'User-provided services cannot be shared.',
                'title' => 'CF-UnprocessableEntity'
              }
            )
          )
        end
      end
    end
  end

  describe 'DELETE /v3/service_instances/:guid/relationships/shared_spaces/:space_guid' do
    let(:api_call) { ->(user_headers) { delete "/v3/service_instances/#{guid}/relationships/shared_spaces/#{space_guid}", nil, user_headers } }
    let(:target_space) { VCAP::CloudController::Space.make(organization: org) }
    let(:service_instance) { VCAP::CloudController::ManagedServiceInstance.make(space:) }
    let(:guid) { service_instance.guid }
    let(:space_guid) { target_space.guid }
    let(:space_dev_headers) do
      org.add_user(user)
      space.add_developer(user)
      headers_for(user)
    end
    let!(:feature_flag) do
      VCAP::CloudController::FeatureFlag.make(name: 'service_instance_sharing', enabled: true, error_message: nil)
    end

    before do
      share_service_instance(service_instance, target_space)
    end

    context 'permissions' do
      let(:db_check) do
        lambda {
          si = VCAP::CloudController::ServiceInstance.first(guid:)
          expect(si).not_to be_shared
        }
      end

      it_behaves_like 'permissions for delete endpoint', ALL_PERMISSIONS do
        let(:expected_codes_and_responses) { responses_for_space_restricted_delete_endpoint }
      end

      it_behaves_like 'permissions for delete endpoint when organization is suspended', 204
    end

    it 'unshares the service instance from the target space and logs audit event' do
      api_call.call(space_dev_headers)

      expect(last_response.status).to eq(204)

      event = VCAP::CloudController::Event.last
      expect(event.values).to include({
                                        type: 'audit.service_instance.unshare',
                                        actor: user.guid,
                                        actee_type: 'service_instance',
                                        actee_name: service_instance.name,
                                        space_guid: space.guid,
                                        organization_guid: space.organization.guid
                                      })
      expect(event.metadata['target_space_guid']).to eq(target_space.guid)
    end

    describe 'when there are bindings in the shared space' do
      let(:app_1) { VCAP::CloudController::AppModel.make(space: target_space) }
      let(:app_2) { VCAP::CloudController::AppModel.make(space: target_space) }

      let(:binding_1) { VCAP::CloudController::ServiceBinding.make(service_instance: service_instance, app: app_1) }
      let(:binding_2) { VCAP::CloudController::ServiceBinding.make(service_instance: service_instance, app: app_2) }

      context 'and the bindings can be deleted synchronously' do
        before do
          stub_unbind(binding_1, accepts_incomplete: true, status: 200, body: {}.to_json)
          stub_unbind(binding_2, accepts_incomplete: true, status: 200, body: {}.to_json)
        end

        it 'deletes all bindings and successfully unshares' do
          api_call.call(space_dev_headers)

          expect(last_response.status).to eq(204)

          service_instance.reload
          expect(service_instance).not_to be_shared
          expect(service_instance).not_to have_bindings
        end
      end

      context 'but the bindings can only be deleted asynchronously' do
        before do
          stub_unbind(binding_1, accepts_incomplete: true, status: 202, body: {}.to_json)
          stub_unbind(binding_2, accepts_incomplete: true, status: 200, body: {}.to_json)
        end

        it 'responds with 502 and does not unshare' do
          api_call.call(space_dev_headers)

          expect(last_response.status).to eq(502)
          expect(parsed_response['errors']).to include(
            include(
              {
                'detail' => "Unshare of service instance failed: \n\nUnshare of service instance failed because one or more bindings could not be deleted.\n\n " \
                            "\tThe binding between an application and service instance #{service_instance.name} in space #{target_space.name} is being deleted asynchronously.",
                'title' => 'CF-ServiceInstanceUnshareFailed'
              }
            )
          )

          expect(service_instance).to be_shared
        end
      end
    end

    it 'responds with 404 when the instance does not exist' do
      delete "/v3/service_instances/some-fake-guid/relationships/shared_spaces/#{space_guid}",
             nil,
             space_dev_headers

      expect(last_response).to have_status_code(404)
      expect(parsed_response['errors']).to include(
        include(
          {
            'detail' => 'Service instance not found',
            'title' => 'CF-ResourceNotFound'
          }
        )
      )
    end

    describe 'target space to unshare from' do
      context 'when it does not exist' do
        let(:space_guid) { 'fake-target' }

        it 'responds with 422' do
          api_call.call(space_dev_headers)

          expect(last_response.status).to eq(422)
          expect(parsed_response['errors']).to include(
            include(
              {
                'detail' => "Unable to unshare service instance from space #{space_guid}. " \
                            'Ensure the space exists.',
                'title' => 'CF-UnprocessableEntity'
              }
            )
          )
        end
      end

      context 'when instance was not shared to the space' do
        let(:space_guid) { VCAP::CloudController::Space.make(organization: org).guid }

        it 'responds with 204' do
          api_call.call(space_dev_headers)

          expect(last_response.status).to eq(204)
        end
      end
    end
  end

  describe 'GET /v3/service_instances/:guid/relationships/shared_spaces' do
    let(:user_header) { headers_for(user) }
    let(:instance) { VCAP::CloudController::ManagedServiceInstance.make(space:) }
    let(:other_space) { VCAP::CloudController::Space.make }

    before do
      share_service_instance(instance, other_space)
    end

    describe 'permissions in originating space' do
      let(:api_call) { ->(user_headers) { get "/v3/service_instances/#{instance.guid}/relationships/shared_spaces", nil, user_headers } }

      it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS do
        let(:expected_response) do
          {
            data: [{ guid: other_space.guid }],
            links: {
              self: { href: "#{link_prefix}/v3/service_instances/#{instance.guid}/relationships/shared_spaces" }
            }
          }
        end

        let(:expected_codes_and_responses) do
          responses_for_space_restricted_single_endpoint(expected_response)
        end
      end
    end

    it 'respond with 404 when the user cannot read the originating space' do
      set_current_user_as_role(role: 'space_developer', org: other_space.organization, space: other_space, user: user)
      get "/v3/service_instances/#{instance.guid}/relationships/shared_spaces", nil, user_header
      expect(last_response.status).to eq(404)
    end

    describe 'fields' do
      it 'can include the space name, guid and organization relationship fields' do
        get "/v3/service_instances/#{instance.guid}/relationships/shared_spaces?fields[space]=name,guid,relationships.organization", nil, admin_headers
        expect(last_response).to have_status_code(200)

        r = { organization: { data: { guid: other_space.organization.guid } } }
        included = {
          spaces: [
            { name: other_space.name, guid: other_space.guid, relationships: r }
          ]
        }

        expect({ included: parsed_response['included'] }).to match_json_response({ included: })
      end

      it 'can include the organization name and guid fields through space' do
        get "/v3/service_instances/#{instance.guid}/relationships/shared_spaces?fields[space.organization]=name,guid", nil, admin_headers
        expect(last_response).to have_status_code(200)

        included = {
          organizations: [
            {
              name: other_space.organization.name,
              guid: other_space.organization.guid
            }
          ]
        }

        expect({ included: parsed_response['included'] }).to match_json_response({ included: })
      end

      it 'fails for invalid resources' do
        get "/v3/service_instances/#{instance.guid}/relationships/shared_spaces?fields[fruit]=name", nil, admin_headers
        expect(last_response).to have_status_code(400)
        expect(parsed_response['errors']).to include(
          include(
            'detail' => "The query parameter is invalid: Fields [fruit] valid resources are: 'space', 'space.organization'"
          )
        )
      end

      it 'fails for not allowed space fields' do
        get "/v3/service_instances/#{instance.guid}/relationships/shared_spaces?fields[space]=metadata", nil, admin_headers
        expect(last_response).to have_status_code(400)
        expect(parsed_response['errors']).to include(
          include(
            'detail' => "The query parameter is invalid: Fields valid keys for 'space' are: 'name', 'guid', 'relationships.organization'"
          )
        )
      end

      it 'fails for not allowed space.organization fields' do
        get "/v3/service_instances/#{instance.guid}/relationships/shared_spaces?fields[space.organization]=metadata", nil, admin_headers
        expect(last_response).to have_status_code(400)
        expect(parsed_response['errors']).to include(
          include(
            'detail' => "The query parameter is invalid: Fields valid keys for 'space.organization' are: 'name', 'guid'"
          )
        )
      end
    end
  end

  describe 'GET /v3/service_instances/:guid/relationships/shared_spaces/usage_summary' do
    let(:guid) { instance.guid }
    let(:instance) { VCAP::CloudController::ManagedServiceInstance.make(space:) }
    let(:space_1) { VCAP::CloudController::Space.make }
    let(:space_2) { VCAP::CloudController::Space.make }
    let(:space_3) { VCAP::CloudController::Space.make }
    let(:url) { "/v3/service_instances/#{guid}/relationships/shared_spaces/usage_summary" }
    let(:api_call) { ->(user_headers) { get url, nil, user_headers } }
    let(:bindings_on_space_1) { 1 }
    let(:bindings_on_space_2) { 3 }

    def create_bindings(instance, space:, count:)
      (1..count).each do
        VCAP::CloudController::ServiceBinding.make(
          app: VCAP::CloudController::AppModel.make(space:),
          service_instance: instance
        )
      end
    end

    before do
      share_service_instance(instance, space_1)
      share_service_instance(instance, space_2)
      share_service_instance(instance, space_3)

      create_bindings(instance, space: space_1, count: bindings_on_space_1)
      create_bindings(instance, space: space_2, count: bindings_on_space_2)
    end

    context 'permissions' do
      let(:response_object) do
        {
          usage_summary: [
            { space: { guid: space_1.guid }, bound_app_count: bindings_on_space_1 },
            { space: { guid: space_2.guid }, bound_app_count: bindings_on_space_2 },
            { space: { guid: space_3.guid }, bound_app_count: 0 }
          ],
          links: {
            self: { href: "#{link_prefix}/v3/service_instances/#{instance.guid}/relationships/shared_spaces/usage_summary" },
            shared_spaces: { href: "#{link_prefix}/v3/service_instances/#{instance.guid}/relationships/shared_spaces" },
            service_instance: { href: "#{link_prefix}/v3/service_instances/#{instance.guid}" }
          }
        }
      end

      let(:expected_codes_and_responses) do
        responses_for_space_restricted_single_endpoint(response_object)
      end

      it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS
    end

    context 'when the instance does not exist' do
      let(:guid) { 'a-fake-guid' }

      it 'responds with 404 Not Found' do
        api_call.call(admin_headers)
        expect(last_response).to have_status_code(404)
      end
    end

    context 'when the user cannot read from the originating space' do
      it 'responds with 404 Not Found' do
        user = VCAP::CloudController::User.make
        set_current_user_as_role(role: 'space_developer', org: space_2.organization, space: space_2, user: user)

        api_call.call(headers_for(user))

        expect(last_response).to have_status_code(404)
      end
    end
  end

  describe 'GET /v3/service_instances/:guid/permissions' do
    # For this endpoint we also want to test the 'cloud_controller_service_permissions.read' scope as well as unauthenticated calls.
    ADDITIONAL_PERMISSIONS_TO_TEST = %w[service_permissions_reader unauthenticated].freeze

    READ_AND_WRITE = { code: 200, response_object: { manage: true, read: true } }.freeze
    READ_ONLY = { code: 200, response_object: { manage: false, read: true } }.freeze
    NO_PERMISSIONS = { code: 200, response_object: { manage: false, read: false } }.freeze

    let(:api_call) { ->(user_headers) { get "/v3/service_instances/#{guid}/permissions", nil, user_headers } }

    context 'when the service instance does not exist' do
      let(:guid) { 'no-such-guid' }

      let(:expected_codes_and_responses) do
        h = Hash.new(code: 404)
        h['unauthenticated'] = { code: 401 }
        h
      end

      it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS + ADDITIONAL_PERMISSIONS_TO_TEST
    end

    context 'when the user is a member of the org or space the service instance exists in' do
      let(:instance) { VCAP::CloudController::ManagedServiceInstance.make(space:) }
      let(:guid) { instance.guid }

      let(:expected_codes_and_responses) do
        h = Hash.new(code: 404)
        %w[admin space_developer].each { |r| h[r] = READ_AND_WRITE }
        %w[admin_read_only global_auditor org_manager space_manager space_auditor space_supporter].each { |r| h[r] = READ_ONLY }
        %w[org_billing_manager org_auditor no_role service_permissions_reader].each { |r| h[r] = NO_PERMISSIONS }
        h['unauthenticated'] = { code: 401 }
        h
      end

      it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS + ADDITIONAL_PERMISSIONS_TO_TEST

      context 'when organization is suspended' do
        let(:expected_codes_and_responses) do
          h = super()
          h['space_developer'] = READ_ONLY
          h
        end

        before do
          org.update(status: VCAP::CloudController::Organization::SUSPENDED)
        end

        it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS + ADDITIONAL_PERMISSIONS_TO_TEST
      end

      context 'request with only the cloud_controller_service_permissions.read scope' do
        it_behaves_like 'permissions for single object endpoint', LOCAL_ROLES do
          let(:after_request_check) do
            lambda do
              # Store the HTTP status and response from the original call.
              expected_status_code = last_response.status
              expected_json_response = parsed_response

              # Repeat the same call for the same user, but now with only the 'cloud_controller_service_permissions.read' scope.
              api_call.call(set_user_with_header_as_service_permissions_reader(user:))

              # Both the HTTP status and response should be the same.
              expect(last_response).to have_status_code(expected_status_code)
              expect(parsed_response).to match_json_response(expected_json_response)
            end
          end
        end
      end
    end

    context 'when the user is not a member of the org or space the service instance exists in' do
      let(:instance) { VCAP::CloudController::ManagedServiceInstance.make }
      let(:guid) { instance.guid }

      let(:expected_codes_and_responses) do
        h = Hash.new(code: 404)
        h['admin'] = READ_AND_WRITE
        %w[admin_read_only global_auditor].each { |r| h[r] = READ_ONLY }
        %w[org_billing_manager org_auditor org_manager space_manager space_auditor space_developer space_supporter no_role service_permissions_reader].each do |r|
          h[r] = NO_PERMISSIONS
        end
        h['unauthenticated'] = { code: 401 }
        h
      end

      it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS + ADDITIONAL_PERMISSIONS_TO_TEST
    end
  end

  def create_managed_json(instance, labels: {}, annotations: {}, last_operation: {}, tags: [])
    {
      guid: instance.guid,
      name: instance.name,
      created_at: iso8601,
      updated_at: iso8601,
      type: 'managed',
      dashboard_url: nil,
      last_operation: last_operation,
      maintenance_info: {},
      upgrade_available: false,
      tags: tags,
      metadata: {
        labels:,
        annotations:
      },
      relationships: {
        space: {
          data: {
            guid: instance.space.guid
          }
        },
        service_plan: {
          data: {
            guid: instance.service_plan.guid
          }
        }
      },
      links: {
        self: {
          href: "#{link_prefix}/v3/service_instances/#{instance.guid}"
        },
        space: {
          href: "#{link_prefix}/v3/spaces/#{instance.space.guid}"
        },
        service_plan: {
          href: "#{link_prefix}/v3/service_plans/#{instance.service_plan.guid}"
        },
        parameters: {
          href: "#{link_prefix}/v3/service_instances/#{instance.guid}/parameters"
        },
        service_credential_bindings: {
          href: "#{link_prefix}/v3/service_credential_bindings?service_instance_guids=#{instance.guid}"
        },
        service_route_bindings: {
          href: "#{link_prefix}/v3/service_route_bindings?service_instance_guids=#{instance.guid}"
        },
        shared_spaces: {
          href: "#{link_prefix}/v3/service_instances/#{instance.guid}/relationships/shared_spaces"
        }
      }
    }
  end

  def create_user_provided_json(instance, labels: {}, annotations: {}, last_operation: {})
    {
      guid: instance.guid,
      name: instance.name,
      created_at: iso8601,
      updated_at: iso8601,
      type: 'user-provided',
      last_operation: last_operation,
      syslog_drain_url: instance.syslog_drain_url,
      route_service_url: instance.route_service_url,
      tags: instance.tags,
      metadata: {
        labels:,
        annotations:
      },
      relationships: {
        space: {
          data: {
            guid: instance.space.guid
          }
        }
      },
      links: {
        self: {
          href: "#{link_prefix}/v3/service_instances/#{instance.guid}"
        },
        space: {
          href: "#{link_prefix}/v3/spaces/#{instance.space.guid}"
        },
        credentials: {
          href: "#{link_prefix}/v3/service_instances/#{instance.guid}/credentials"
        },
        service_credential_bindings: {
          href: "#{link_prefix}/v3/service_credential_bindings?service_instance_guids=#{instance.guid}"
        },
        service_route_bindings: {
          href: "#{link_prefix}/v3/service_route_bindings?service_instance_guids=#{instance.guid}"
        }
      }
    }
  end

  def share_service_instance(instance, target_space)
    enable_sharing!

    share_request = {
      'data' => [
        { 'guid' => target_space.guid }
      ]
    }

    post "/v3/service_instances/#{instance.guid}/relationships/shared_spaces", share_request.to_json, admin_headers
    expect(last_response.status).to eq(200)
  end

  def enable_sharing!
    VCAP::CloudController::FeatureFlag.
      find_or_create(name: 'service_instance_sharing') { |ff| ff.enabled = true }.
      update(enabled: true)
  end
end