cloudfoundry/cloud_controller_ng

View on GitHub
spec/support/shared_examples/request/bindings_create.rb

Summary

Maintainability
A
0 mins
Test Coverage
RSpec.shared_examples 'service credential binding create endpoint' do |klass, check_app, audit_event_type, display_name|
  describe 'managed instance' do
    let(:api_call) { ->(user_headers) { post '/v3/service_credential_bindings', create_body.to_json, user_headers } }
    let(:job) { VCAP::CloudController::PollableJobModel.last }
    let(:binding) { klass.last }

    describe 'a successful creation' do
      before do
        api_call.call(admin_headers)
      end

      it 'creates a credential binding in the database' do
        expect(binding.app).to eq(app_to_bind_to) if check_app
        expect(binding.service_instance).to eq(service_instance)

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

        expect(binding).to have_labels({ prefix: nil, key_name: 'foo', value: 'bar' })
        expect(binding).to have_annotations({ prefix: nil, key_name: 'foz', value: 'baz' })
      end

      it 'responds with a job resource' do
        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("#{display_name}.create")
        expect(job.resource_guid).to eq(binding.guid)
        expect(job.resource_type).to eq('service_credential_binding')

        get "/v3/jobs/#{job.guid}", nil, admin_headers
        expect(last_response).to have_status_code(200)
        expect(parsed_response['guid']).to eq(job.guid)
        binding_link = parsed_response.dig('links', 'service_credential_binding', 'href')
        expect(binding_link).to end_with("/v3/service_credential_bindings/#{binding.guid}")
      end
    end

    describe 'the pollable job' do
      let(:credentials) { { 'password' => 'special sauce' } }
      let(:broker_base_url) { service_instance.service_broker.broker_url }
      let(:broker_bind_url) { "#{broker_base_url}/v2/service_instances/#{service_instance.guid}/service_bindings/#{binding.guid}" }
      let(:broker_status_code) { 201 }
      let(:broker_response) { { credentials: } }
      let(:app_binding_attributes) do
        if check_app
          {
            app_guid: app_to_bind_to.guid,
            bind_resource: {
              app_guid: app_to_bind_to.guid,
              space_guid: service_instance.space.guid,
              app_annotations: { 'pre.fix/foo': 'bar' }
            }
          }
        else
          {}
        end
      end
      let(:client_body) do
        {
          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' }
          },
          service_id: service_instance.service_plan.service.unique_id,
          plan_id: service_instance.service_plan.unique_id,
          bind_resource: {
            credential_client_id: 'cc_service_key_client'
          }
        }.merge(app_binding_attributes)
      end

      before do
        api_call.call(headers_for(user, scopes: %w[cloud_controller.admin]))
        expect(last_response).to have_status_code(202)

        stub_request(:put, broker_bind_url).
          with(query: { accepts_incomplete: true }).
          to_return(status: broker_status_code, body: broker_response.to_json, headers: {})
      end

      it 'sends a bind 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, broker_bind_url).
            with(
              body: client_body,
              query: { accepts_incomplete: true },
              headers: { 'X-Broker-Api-Originating-Identity' => "cloudfoundry #{encoded_user_guid}" }
            )
        ).to have_been_made.once
      end

      context 'parameters are specified' do
        let(:request_extra) { { parameters: { foo: 'bar' } } }

        it 'sends the parameters to the broker' do
          execute_all_jobs(expected_successes: 1, expected_failures: 0)

          expect(
            a_request(:put, broker_bind_url).
              with(
                body: client_body.deep_merge(request_extra),
                query: { accepts_incomplete: true }
              )
          ).to have_been_made.once
        end
      end

      context 'when the bind completes synchronously' do
        it 'updates the the binding' do
          execute_all_jobs(expected_successes: 1, expected_failures: 0)

          binding.reload
          expect(binding.credentials).to eq(credentials)
          expect(binding.last_operation.type).to eq('create')
          expect(binding.last_operation.state).to eq('succeeded')
        end

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

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

        it 'logs an audit event' do
          execute_all_jobs(expected_successes: 1, expected_failures: 0)

          event = VCAP::CloudController::Event.find(type: "audit.#{audit_event_type}.create")
          expect(event).to be
          expect(event.actee).to eq(binding.guid)
          expect(event.actee_name).to eq(binding.name)
          expect(event.data).to include({
                                          'request' => create_body.with_indifferent_access
                                        })
        end
      end

      context 'when the broker fails to bind' do
        let(:broker_status_code) { 422 }
        let(:broker_response) { { error: 'RequiresApp' } }

        it 'updates the the binding with a failure' do
          execute_all_jobs(expected_successes: 0, expected_failures: 1)

          binding.reload
          expect(binding.last_operation.type).to eq('create')
          expect(binding.last_operation.state).to eq('failed')
        end

        it 'fails the job' do
          execute_all_jobs(expected_successes: 0, expected_failures: 1)

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

      context 'when the binding completes asynchronously' do
        let(:broker_status_code) { 202 }
        let(:operation) { Sham.guid }
        let(:broker_response) { { operation: } }
        let(:broker_binding_last_operation_url) { "#{broker_base_url}/v2/service_instances/#{service_instance.guid}/service_bindings/#{binding.guid}/last_operation" }
        let(:last_operation_status_code) { 200 }
        let(:description) { Sham.description }
        let(:state) { 'in progress' }
        let(:last_operation_body) do
          {
            description:,
            state:
          }
        end

        before do
          stub_request(:get, broker_binding_last_operation_url).
            with(query: hash_including({
                                         operation:
                                       })).
            to_return(status: last_operation_status_code, body: last_operation_body.to_json, headers: {})
        end

        it 'polls the last operation endpoint' do
          execute_all_jobs(expected_successes: 1, expected_failures: 0)

          encoded_user_guid = Base64.strict_encode64("{\"user_id\":\"#{user.guid}\"}")
          expect(
            a_request(:get, broker_binding_last_operation_url).
              with(
                query: {
                  operation: operation,
                  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 'updates the binding and job' do
          execute_all_jobs(expected_successes: 1, expected_failures: 0)

          expect(binding.last_operation.type).to eq('create')
          expect(binding.last_operation.state).to eq(state)
          expect(binding.last_operation.description).to eq(description)

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

        it 'logs an audit event' do
          execute_all_jobs(expected_successes: 1, expected_failures: 0)

          event = VCAP::CloudController::Event.find(type: "audit.#{audit_event_type}.start_create")
          expect(event).to be
          expect(event.actee).to eq(binding.guid)
          expect(event.data).to include({
                                          'request' => create_body.with_indifferent_access
                                        })
        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

        it 'keeps track of the broker operation' do
          execute_all_jobs(expected_successes: 1, expected_failures: 0)
          expect(Delayed::Job.count).to eq(1)

          Timecop.travel(Time.now + 1.minute)
          execute_all_jobs(expected_successes: 1, expected_failures: 0)

          encoded_user_guid = Base64.strict_encode64("{\"user_id\":\"#{user.guid}\"}")
          expect(
            a_request(:get, broker_binding_last_operation_url).
              with(
                query: {
                  operation: operation,
                  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.twice
        end

        context 'last operation response is 200 OK and indicates success' do
          let(:state) { 'succeeded' }
          let(:fetch_binding_status_code) { 200 }
          let(:credentials) { { password: 'foo' } }
          let(:parameters) { { foo: 'bar', another_foo: 'another_bar' } }

          let(:app_binding_attributes) do
            if check_app
              {
                syslog_drain_url:
              }
            else
              {}
            end
          end

          let(:fetch_binding_body) do
            {
              credentials: credentials,
              parameters: parameters,
              service_id: 'extra-field-service_id-should-ignore',
              name: 'extra-field-name-should-not-update'
            }.merge(app_binding_attributes)
          end

          before do
            stub_request(:get, broker_bind_url).
              to_return(status: fetch_binding_status_code, body: fetch_binding_body.to_json, headers: {})
          end

          it 'fetches the binding' do
            execute_all_jobs(expected_successes: 1, expected_failures: 0)

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

          it 'updates the binding and job' do
            execute_all_jobs(expected_successes: 1, expected_failures: 0)

            expect(binding.last_operation.type).to eq('create')
            expect(binding.last_operation.state).to eq(state)
            expect(binding.last_operation.description).to eq(description)

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

          it 'updates the binding details with the fetch binding response ignoring extra fields' do
            execute_all_jobs(expected_successes: 1, expected_failures: 0)

            expect(binding.reload.credentials).to eq(credentials.with_indifferent_access)
            expect(binding.syslog_drain_url).to eq(syslog_drain_url) if check_app
            expect(binding.name).to eq(binding_name)
          end

          it 'logs an audit event' do
            execute_all_jobs(expected_successes: 1, expected_failures: 0)

            event = VCAP::CloudController::Event.find(type: "audit.#{audit_event_type}.create")
            expect(event).to be
            expect(event.actee).to eq(binding.guid)
            expect(event.data).to include({
                                            'request' => create_body.with_indifferent_access
                                          })
          end

          context 'fetching binding fails' do
            let(:fetch_binding_status_code) { 404 }
            let(:fetch_binding_body) {}

            it 'fails the job' do
              execute_all_jobs(expected_successes: 0, expected_failures: 1)

              expect(binding.last_operation.type).to eq('create')
              expect(binding.last_operation.state).to eq('failed')
              expect(binding.last_operation.description).to include('The service broker rejected the request. Status Code: 404 Not Found')

              expect(job.state).to eq(VCAP::CloudController::PollableJobModel::FAILED_STATE)
              expect(job.cf_api_error).not_to be_nil
              error = YAML.safe_load(job.cf_api_error)
              expect(error['errors'].first).to include({
                                                         'code' => 10_009,
                                                         'title' => 'CF-UnableToPerform',
                                                         'detail' => 'bind could not be completed: The service broker rejected the request. Status Code: 404 Not Found, Body: null'
                                                       })
            end
          end
        end

        it_behaves_like 'binding last operation response handling', 'create'

        context 'binding not retrievable' do
          let(:offering) { VCAP::CloudController::Service.make(bindings_retrievable: false) }

          it 'fails the job with an appropriate error' do
            execute_all_jobs(expected_successes: 0, expected_failures: 1)

            expect(binding.last_operation.type).to eq('create')
            expect(binding.last_operation.state).to eq('failed')
            expect(binding.last_operation.description).to eq('The broker responded asynchronously but does not support fetching binding data')

            expect(job.state).to eq(VCAP::CloudController::PollableJobModel::FAILED_STATE)
            expect(job.cf_api_error).not_to be_nil
            error = YAML.safe_load(job.cf_api_error)
            # TODO: check error message
            expect(error['errors'].first).to include({
                                                       'code' => 90_001,
                                                       'title' => 'CF-ServiceBindingInvalid',
                                                       'detail' => 'The service binding is invalid: The broker responded asynchronously but does not support fetching binding data'
                                                     })
          end
        end
      end

      context 'orphan mitigation' do
        it_behaves_like 'create binding orphan mitigation' do
          let(:bind_url) { broker_bind_url }
          let(:plan_id) { plan.unique_id }
          let(:offering_id) { offering.unique_id }
        end
      end
    end
  end
end