spec/acceptance/broker_api_compatibility/broker_api_v2.15_spec.rb
require 'spec_helper'
RSpec.describe 'Service Broker API integration' do
describe 'v2.15' do
include VCAP::CloudController::BrokerApiHelper
before { setup_cc }
describe 'updates service instances based on the plan object of the catalog' do
context 'when the broker supports plan_updateable on plan level' do
let(:catalog) do
catalog = default_catalog
catalog[:services].first[:plans].first[:plan_updateable] = true
catalog
end
before do
setup_broker(catalog)
end
it 'successfully updates the service instance plan' do
provision_service
expect(VCAP::CloudController::ServiceInstance.find(guid: @service_instance_guid).service_plan_guid).to eq @plan_guid
update_service_instance(200)
expect(last_response).to have_status_code(201)
expect(VCAP::CloudController::ServiceInstance.find(guid: @service_instance_guid).service_plan_guid).to eq @large_plan_guid
end
end
end
describe 'platform delays polling to last_operation based on Retry-After header' do
let(:default_poll_interval) { VCAP::CloudController::Config.config.get(:broker_client_default_async_poll_interval_seconds) }
let(:retry_after_interval) { default_poll_interval * 4 }
before do
setup_broker(default_catalog(plan_updateable: true, bindings_retrievable: true))
@broker = VCAP::CloudController::ServiceBroker.find guid: @broker_guid
stub_async_last_operation(body: { state: 'in progress' }, headers: { 'Retry-After': retry_after_interval })
end
describe 'service instances' do
context 'when provisioning a service instance' do
it 'polls the broker at the given retry interval' do
expect(async_provision_service).to have_status_code(202)
expect(
a_request(:put, provision_url_for_broker(@broker, accepts_incomplete: true))
).to have_been_made
service_instance = VCAP::CloudController::ManagedServiceInstance.find(guid: @service_instance_guid)
assert_cc_polls_service_instance_last_operation(
service_instance,
default_poll_interval,
retry_after_interval
)
end
end
context 'when deprovisioning a service instance' do
let(:service_instance) { VCAP::CloudController::ManagedServiceInstance.make(space_guid: @space_guid, service_plan_guid: @plan_guid) }
before do
@service_instance_guid = service_instance.guid
end
it 'polls the broker at the given retry interval' do
expect(async_delete_service).to have_status_code(202)
expect(
a_request(:delete, deprovision_url(service_instance, accepts_incomplete: true))
).to have_been_made
assert_cc_polls_service_instance_last_operation(
service_instance,
default_poll_interval,
retry_after_interval
)
end
end
context 'when updating a service instance' do
let(:service_instance) { VCAP::CloudController::ManagedServiceInstance.make(space_guid: @space_guid, service_plan_guid: @plan_guid) }
before do
@service_instance_guid = service_instance.guid
end
it 'polls the broker at the given retry interval' do
expect(async_update_service).to have_status_code(202)
expect(
a_request(:patch, update_url_for_broker(@broker, accepts_incomplete: true))
).to have_been_made
assert_cc_polls_service_instance_last_operation(
service_instance,
default_poll_interval,
retry_after_interval
)
end
end
end
describe 'service bindings' do
context 'when creating a service binding' do
let(:service_instance) { VCAP::CloudController::ManagedServiceInstance.make(space_guid: @space_guid, service_plan_guid: @plan_guid) }
before do
@service_instance_guid = service_instance.guid
create_app
end
it 'polls the broker at the given retry interval' do
expect(async_bind_service(status: 202)).to have_status_code(202)
expect(
a_request(:put, bind_url(service_instance, accepts_incomplete: true))
).to have_been_made
assert_cc_polls_service_binding_last_operation(
service_instance,
default_poll_interval,
retry_after_interval
)
end
end
context 'when removing a service binding' do
let(:service_instance) { VCAP::CloudController::ManagedServiceInstance.make(space_guid: @space_guid, service_plan_guid: @plan_guid) }
before do
@service_instance_guid = service_instance.guid
create_app
bind_service
end
it 'polls the broker at the given retry interval' do
expect(async_unbind_service(status: 202)).to have_status_code(202)
service_binding = VCAP::CloudController::ServiceBinding.last
expect(
a_request(:delete, unbind_url(service_binding, accepts_incomplete: true))
).to have_been_made
assert_cc_polls_service_binding_last_operation(
service_instance,
default_poll_interval,
retry_after_interval
)
end
end
end
end
describe 'platform limits polling duration to last_operation based on plan maximum_polling_duration value' do
let(:default_max_poll_duration) { VCAP::CloudController::Config.config.get(:broker_client_max_async_poll_duration_minutes) }
let(:broker_max_poll_duration_in_seconds) { 60 }
before do
setup_broker(default_catalog(maximum_polling_duration: broker_max_poll_duration_in_seconds, plan_updateable: true, bindings_retrievable: true))
@broker = VCAP::CloudController::ServiceBroker.find guid: @broker_guid
stub_async_last_operation(body: { state: 'in progress' })
end
describe 'service instances' do
context 'when creating a service instance' do
it 'stops polling the broker after the given maximum_polling_duration' do
expect(async_provision_service).to have_status_code(202)
expect(
a_request(:put, provision_url_for_broker(@broker, accepts_incomplete: true))
).to have_been_made
service_instance = VCAP::CloudController::ManagedServiceInstance.find(guid: @service_instance_guid)
last_operation_url = %r{#{service_instance_url(service_instance)}/last_operation}
assert_cc_polls_last_operation_with_provided_max_duration(last_operation_url, broker_max_poll_duration_in_seconds, default_max_poll_duration)
end
end
context 'when updating a service instance' do
let(:service_instance) { VCAP::CloudController::ManagedServiceInstance.make(space_guid: @space_guid, service_plan_guid: @plan_guid) }
before do
@service_instance_guid = service_instance.guid
end
it 'stops polling the broker after the given maximum_polling_duration' do
expect(async_update_service).to have_status_code(202)
expect(a_request(:patch, update_url_for_broker(@broker, accepts_incomplete: true))).to have_been_made
service_instance = VCAP::CloudController::ManagedServiceInstance.find(guid: @service_instance_guid)
last_operation_url = %r{#{service_instance_url(service_instance)}/last_operation}
assert_cc_polls_last_operation_with_provided_max_duration(last_operation_url, broker_max_poll_duration_in_seconds, default_max_poll_duration)
end
end
context 'when deleting a service instance' do
let(:service_instance) { VCAP::CloudController::ManagedServiceInstance.make(space_guid: @space_guid, service_plan_guid: @plan_guid) }
before do
@service_instance_guid = service_instance.guid
end
it 'stops polling the broker after the given maximum_polling_duration' do
expect(async_delete_service).to have_status_code(202)
expect(a_request(:delete, deprovision_url(service_instance, accepts_incomplete: true))).to have_been_made
service_instance = VCAP::CloudController::ManagedServiceInstance.find(guid: @service_instance_guid)
last_operation_url = %r{#{service_instance_url(service_instance)}/last_operation}
assert_cc_polls_last_operation_with_provided_max_duration(last_operation_url, broker_max_poll_duration_in_seconds, default_max_poll_duration)
end
end
end
describe 'service bindings' do
context 'when creating a service binding' do
let(:service_instance) { VCAP::CloudController::ManagedServiceInstance.make(space_guid: @space_guid, service_plan_guid: @plan_guid) }
before do
@service_instance_guid = service_instance.guid
create_app
end
it 'polls the broker at the given retry interval' do
expect(async_bind_service(status: 202)).to have_status_code(202)
expect(a_request(:put, bind_url(service_instance, accepts_incomplete: true))).to have_been_made
last_operation_url = %r{#{bind_url(service_instance)}/last_operation}
assert_cc_polls_last_operation_with_provided_max_duration(last_operation_url, broker_max_poll_duration_in_seconds, default_max_poll_duration)
end
end
context 'when removing a service binding' do
let(:service_instance) { VCAP::CloudController::ManagedServiceInstance.make(space_guid: @space_guid, service_plan_guid: @plan_guid) }
before do
@service_instance_guid = service_instance.guid
create_app
bind_service
end
it 'polls the broker at the given retry interval' do
expect(async_unbind_service(status: 202)).to have_status_code(202)
service_binding = VCAP::CloudController::ServiceBinding.last
expect(a_request(:delete, unbind_url(service_binding, accepts_incomplete: true))).to have_been_made
last_operation_url = %r{#{service_binding_url(service_binding)}/last_operation}
assert_cc_polls_last_operation_with_provided_max_duration(last_operation_url, broker_max_poll_duration_in_seconds, default_max_poll_duration)
end
end
end
end
context 'service instance context hash' do
let(:catalog) { default_catalog(plan_updateable: true) }
before do
setup_broker(catalog)
@broker = VCAP::CloudController::ServiceBroker.find guid: @broker_guid
provision_service(name: 'instance-007')
end
context 'service provision request' do
it 'receives the correct attributes in the context' do
expected_context_attributes = {
'platform' => 'cloudfoundry',
'organization_guid' => @org_guid,
'space_guid' => @space_guid,
'instance_name' => 'instance-007',
'organization_name' => @space.organization.name,
'space_name' => @space.name
}
expect(
a_request(:put, %r{/v2/service_instances/#{@service_instance_guid}}).with do |req|
context = Oj.load(req.body)['context']
context >= expected_context_attributes
end
).to have_been_made
end
end
context 'service update request' do
before do
update_service_instance(200)
end
it 'receives the correct attributes in the context' do
expected_context_attributes = {
'platform' => 'cloudfoundry',
'organization_guid' => @org_guid,
'space_guid' => @space_guid,
'instance_name' => 'instance-007',
'organization_name' => @space.organization.name,
'space_name' => @space.name
}
expect(
a_request(:patch, %r{/v2/service_instances/#{@service_instance_guid}}).with do |req|
context = Oj.load(req.body)['context']
context >= expected_context_attributes
end
).to have_been_made
end
end
context 'service rename request' do
before do
rename_service_instance(200, { name: 'instance-014' })
end
context 'when broker has allow_context_updates enabled in catalog' do
let(:catalog) { default_catalog(allow_context_updates: true) }
it 'receives the correct attributes in the context' do
expected_context_attributes = {
'platform' => 'cloudfoundry',
'organization_guid' => @org_guid,
'space_guid' => @space_guid,
'instance_name' => 'instance-014',
'organization_name' => @space.organization.name,
'space_name' => @space.name
}
expect(
a_request(:patch, %r{/v2/service_instances/#{@service_instance_guid}}).with do |req|
context = Oj.load(req.body)['context']
context >= expected_context_attributes
end
).to have_been_made
end
end
context 'when broker has allow_context_updates disabled in catalog' do
let(:catalog) { default_catalog(allow_context_updates: false) }
it 'does not receive a patch update request' do
expect(
a_request(:patch, %r{/v2/service_instances/#{@service_instance_guid}})
).not_to have_been_made
end
end
end
end
context 'service bindings context hash' do
let(:catalog) { default_catalog(requires: ['route_forwarding']) }
let(:expected_context_attributes) do
{
'platform' => 'cloudfoundry',
'organization_guid' => @org_guid,
'space_guid' => @space_guid,
'organization_name' => @space.organization.name,
'space_name' => @space.name
}
end
before do
setup_broker(catalog)
@broker = VCAP::CloudController::ServiceBroker.find guid: @broker_guid
provision_service(name: 'instance-007')
end
context 'for binding to an application' do
before do
create_app
bind_service
end
it 'receives the correct attributes in the context' do
expect(
a_request(:put, %r{/v2/service_instances/#{@service_instance_guid}/service_bindings/#{@binding_guid}}).with do |req|
context = Oj.load(req.body)['context']
context >= expected_context_attributes
end
).to have_been_made
end
end
context 'for create service key' do
before do
create_service_key
end
it 'receives the correct attributes in the context' do
expect(
a_request(:put, %r{/v2/service_instances/#{@service_instance_guid}/service_bindings/#{@binding_guid}}).with do |req|
context = Oj.load(req.body)['context']
context >= expected_context_attributes
end
).to have_been_made
end
end
context 'for bind route service' do
let(:route) { VCAP::CloudController::Route.make(space: @space) }
before do
create_route_binding(route)
end
it 'receives the correct attributes in the context' do
expect(
a_request(:put, %r{/v2/service_instances/#{@service_instance_guid}/service_bindings/#{@binding_guid}}).with do |req|
context = Oj.load(req.body)['context']
context >= expected_context_attributes
end
).to have_been_made
end
end
end
describe 'cancel service instance create async operation' do
before do
setup_broker(default_catalog)
@broker = VCAP::CloudController::ServiceBroker.find guid: @broker_guid
end
context 'when provisioning a service instance' do
it 'delete request should cancel the creation and delete the instance synchronously' do
async_provision_service
expect(
a_request(:put, provision_url_for_broker(@broker, accepts_incomplete: true))
).to have_been_made
service_instance = VCAP::CloudController::ManagedServiceInstance.find(guid: @service_instance_guid)
expect(delete_service).to have_status_code(204)
expect(
a_request(:delete, deprovision_url(service_instance))
).to have_been_made
expect(VCAP::CloudController::Event.order(:id).all.map(&:type)).to end_with(
'audit.service_instance.start_create',
'audit.service_instance.delete'
)
expect { service_instance.reload }.to raise_error(Sequel::Error)
end
it 'does not delete the instance if the broker rejects the request' do
async_provision_service
expect(
a_request(:put, provision_url_for_broker(@broker, accepts_incomplete: true))
).to have_been_made
service_instance = VCAP::CloudController::ManagedServiceInstance.find(guid: @service_instance_guid)
expect(delete_service(status: 422, broker_response_body: %({"error": "ConcurrencyError"}))).to have_status_code(409)
expect { service_instance.reload }.not_to raise_error
end
it 'delete request should cancel the creation and delete the instance asynchronously' do
async_provision_service
expect(
a_request(:put, provision_url_for_broker(@broker, accepts_incomplete: true))
).to have_been_made
service_instance = VCAP::CloudController::ManagedServiceInstance.find(guid: @service_instance_guid)
expect(async_delete_service).to have_status_code(202)
expect(
a_request(:delete, deprovision_url(service_instance, accepts_incomplete: true))
).to have_been_made
expect { service_instance.reload }.not_to raise_error
Timecop.freeze(Time.now) do
stub_async_last_operation
Delayed::Worker.new.work_off
expect(a_request(:get, %r{#{service_instance_url(service_instance)}/last_operation})).to have_been_made
end
expect(VCAP::CloudController::Event.order(:id).all.map(&:type)).to end_with(
'audit.service_instance.start_create',
'audit.service_instance.start_delete',
'audit.service_instance.delete'
)
expect { service_instance.reload }.to raise_error(Sequel::Error)
end
end
end
context 'when the broker provides maintenance_info' do
let(:catalog) do
catalog = default_catalog
catalog[:services].first[:plans].first[:maintenance_info] = { 'version' => '2.0.0', 'description' => 'Test description' }
catalog
end
before do
setup_broker(catalog)
@broker = VCAP::CloudController::ServiceBroker.find guid: @broker_guid
end
it 'is saved with the service plan' do
get("/v2/service_plans/#{@plan_guid}",
{}.to_json,
json_headers(admin_headers))
parsed_body = Oj.load(last_response.body)
maintenance_info = parsed_body['entity']['maintenance_info']
expect(maintenance_info).to eq({ 'version' => '2.0.0', 'description' => 'Test description' })
end
context 'when updating the service with the provided maintenance_info' do
let(:service_instance) do
VCAP::CloudController::ManagedServiceInstance.make(
space_guid: @space_guid,
service_plan_guid: @plan_guid,
maintenance_info: { 'version' => '1.0.0' }
)
end
before do
@service_instance_guid = service_instance.guid
end
it 'forwards the maintanance info to the broker (only version)' do
response = async_update_service(maintenance_info: { 'version' => '2.0.0', 'description' => 'Test description' })
expect(response).to have_status_code(202)
expect(
a_request(:patch, update_url_for_broker(@broker, accepts_incomplete: true)).with do |req|
expect(Oj.load(req.body)).to include('maintenance_info' => { 'version' => '2.0.0' })
end
).to have_been_made
end
end
end
describe 'cancel service binding create async operation' do
before do
setup_broker(default_catalog(bindings_retrievable: true))
@broker = VCAP::CloudController::ServiceBroker.find guid: @broker_guid
provision_service
end
context 'when binding is in progress' do
let(:service_instance) { VCAP::CloudController::ManagedServiceInstance.find(guid: @service_instance_guid) }
before do
create_app
async_bind_service
expect(a_request(:put, bind_url(service_instance, accepts_incomplete: true))).to have_been_made
end
context 'broker responds synchronously to the unbind request' do
it 'deletes the binding' do
service_binding = VCAP::CloudController::ServiceBinding.find(guid: @binding_guid)
expect(unbind_service).to have_status_code(204)
expect(a_request(:delete, unbind_url(service_binding))).to have_been_made
expect(VCAP::CloudController::Event.order(:id).all.map(&:type)).to end_with(
'audit.service_binding.start_create',
'audit.service_binding.delete'
)
expect { service_binding.reload }.to raise_error(Sequel::Error)
end
end
context 'broker rejects the unbind request' do
it 'raise concurrency error' do
service_binding = VCAP::CloudController::ServiceBinding.find(guid: @binding_guid)
expect(unbind_service(status: 422, response_body: { error: 'ConcurrencyError' })).to have_status_code(409)
expect(a_request(:delete, unbind_url(service_binding))).to have_been_made
expect(VCAP::CloudController::Event.order(:id).all.map(&:type)).to end_with(
'audit.service_binding.start_create'
)
expect(service_binding.reload).not_to be_nil
end
end
it 'unbind request should cancel the bind and delete the binding asynchronously' do
stub_async_last_operation
service_binding = VCAP::CloudController::ServiceBinding.find(guid: @binding_guid)
expect(async_unbind_service).to have_status_code(202)
expect(a_request(:delete, unbind_url(service_binding, accepts_incomplete: true))).to have_been_made
expect(service_binding.reload).not_to be_nil
Timecop.freeze(Time.now) do
Delayed::Worker.new.work_off
expect(a_request(:get, %r{#{service_binding_url(service_binding)}/last_operation})).to have_been_made
end
expect(VCAP::CloudController::Event.order(:id).all.map(&:type)).to end_with(
'audit.service_binding.start_create',
'audit.service_binding.start_delete',
'audit.service_binding.delete'
)
expect { service_binding.reload }.to raise_error(Sequel::Error)
end
end
end
end
end
def assert_cc_polls_last_operation_with_provided_max_duration(last_operation_url, broker_max_poll_duration, default_max_poll_duration)
Timecop.freeze(Time.now) do
Delayed::Worker.new.work_off
expect(a_request(:get, last_operation_url)).to have_been_made
# between the broker max. poll duration and the platform max. poll duration
# we expect no further requests to last_operation to have been made
Timecop.travel(broker_max_poll_duration.seconds)
Delayed::Worker.new.work_off
expect(a_request(:get, last_operation_url)).to have_been_made
WebMock.reset_executed_requests!
Timecop.travel(default_max_poll_duration.minutes)
Delayed::Worker.new.work_off
expect(a_request(:get, last_operation_url)).not_to have_been_made
end
end
def assert_cc_polls_service_instance_last_operation(service_instance, default_poll_interval, retry_after_interval)
Timecop.freeze(Time.now) do
Delayed::Worker.new.work_off
expect(a_request(:get, %r{#{service_instance_url(service_instance)}/last_operation})).to have_been_made
Timecop.travel(default_poll_interval.seconds)
Delayed::Worker.new.work_off
expect(a_request(:get, %r{#{service_instance_url(service_instance)}/last_operation})).to have_been_made.once
Timecop.travel(retry_after_interval.seconds)
Delayed::Worker.new.work_off
expect(a_request(:get, %r{#{service_instance_url(service_instance)}/last_operation})).to have_been_made.twice
end
end
def assert_cc_polls_service_binding_last_operation(service_instance, default_poll_interval, retry_after_interval)
Timecop.freeze(Time.now) do
Delayed::Worker.new.work_off
expect(a_request(:get, %r{#{bind_url(service_instance)}/last_operation})).to have_been_made
Timecop.travel(default_poll_interval.seconds)
Delayed::Worker.new.work_off
expect(a_request(:get, %r{#{bind_url(service_instance)}/last_operation})).to have_been_made.once
Timecop.travel(retry_after_interval.seconds)
Delayed::Worker.new.work_off
expect(a_request(:get, %r{#{bind_url(service_instance)}/last_operation})).to have_been_made.twice
end
end