diff --git a/app/access/service_instance_access.rb b/app/access/service_instance_access.rb
index cd8ab240003..dce57e8faa5 100644
--- a/app/access/service_instance_access.rb
+++ b/app/access/service_instance_access.rb
@@ -4,13 +4,13 @@ def create?(service_instance, params=nil)
return true if admin_user?
FeatureFlag.raise_unless_enabled!(:service_instance_creation)
return false if service_instance.in_suspended_org?
- service_instance.space.has_developer?(context.user) && allowed?(service_instance)
+ service_instance.space&.has_developer?(context.user) && allowed?(service_instance)
end
def read_for_update?(service_instance, params=nil)
return true if admin_user?
return false if service_instance.in_suspended_org?
- service_instance.space.has_developer?(context.user)
+ service_instance.space&.has_developer?(context.user)
end
def update?(service_instance, params=nil)
@@ -20,12 +20,12 @@ def update?(service_instance, params=nil)
def delete?(service_instance)
return true if admin_user?
return false if service_instance.in_suspended_org?
- service_instance.space.has_developer?(context.user)
+ service_instance.space&.has_developer?(context.user)
end
def manage_permissions?(service_instance)
return true if admin_user?
- service_instance.space.has_developer?(context.user)
+ service_instance.space&.has_developer?(context.user)
end
def manage_permissions_with_token?(service_instance)
@@ -33,8 +33,7 @@ def manage_permissions_with_token?(service_instance)
end
def read_permissions?(service_instance)
- return true if admin_user? || admin_read_only_user?
- service_instance.space.has_member?(context.user) || service_instance.space.organization.managers.include?(context.user)
+ admin_user? || admin_read_only_user? || object_is_visible_to_user?(service_instance, context.user)
end
def read_permissions_with_token?(service_instance)
@@ -43,7 +42,7 @@ def read_permissions_with_token?(service_instance)
def read_env?(service_instance)
return true if admin_user? || admin_read_only_user?
- service_instance.space.has_developer?(context.user)
+ service_instance.space&.has_developer?(context.user)
end
def read_env_with_token?(service_instance)
@@ -64,7 +63,7 @@ def allowed?(service_instance)
end
def purge?(service_instance)
- admin_user? || (service_instance.space.has_developer?(context.user) && service_instance.service_broker.private?)
+ admin_user? || (service_instance.space&.has_developer?(context.user) && service_instance.service_broker.private?)
end
def purge_with_token?(instance)
diff --git a/app/controllers/runtime/spaces_controller.rb b/app/controllers/runtime/spaces_controller.rb
index 7132a96abb6..d7e8780036c 100644
--- a/app/controllers/runtime/spaces_controller.rb
+++ b/app/controllers/runtime/spaces_controller.rb
@@ -138,20 +138,18 @@ def enumerate_services(guid)
def enumerate_service_instances(guid)
space = find_guid_and_validate_access(:read, guid)
- if params['return_user_provided_service_instances'] == 'true'
- model_class = ServiceInstance
- relation_name = :service_instances
- else
- model_class = ManagedServiceInstance
- relation_name = :managed_service_instances
- end
+ model_class = params['return_user_provided_service_instances'] == 'true' ? ServiceInstance : ManagedServiceInstance
service_instances = Query.filtered_dataset_from_query_params(
model_class,
- space.user_visible_relationship_dataset(relation_name, @access_context.user, @access_context.admin_override),
+ model_class.user_visible(@access_context.user, @access_context.admin_override),
ServiceInstancesController.query_parameters,
@opts)
- service_instances.filter(space: space)
+
+ service_instances = service_instances.filter(Sequel.or([
+ [:space, space],
+ [:shared_spaces, space]
+ ]))
collection_renderer.render_json(
ServiceInstancesController,
diff --git a/app/controllers/services/service_instances_controller.rb b/app/controllers/services/service_instances_controller.rb
index 44cc9ebf2ea..40786a6e6fe 100644
--- a/app/controllers/services/service_instances_controller.rb
+++ b/app/controllers/services/service_instances_controller.rb
@@ -5,6 +5,9 @@
require 'controllers/services/lifecycle/service_instance_deprovisioner'
require 'controllers/services/lifecycle/service_instance_purger'
require 'fetchers/service_instance_fetcher'
+require 'fetchers/service_binding_list_fetcher'
+require 'presenters/v2/service_instance_shared_to_presenter'
+require 'presenters/v2/service_instance_shared_from_presenter'
module VCAP::CloudController
class ServiceInstancesController < RestController::ModelController
@@ -205,6 +208,46 @@ def permissions(guid)
end
end
+ get '/v2/service_instances/:guid/shared_from', :shared_from_information
+ def shared_from_information(guid)
+ service_instance = find_guid_and_validate_access(:read, guid, ManagedServiceInstance)
+
+ if service_instance.shared?
+ [HTTP::OK, {}, JSON.generate(CloudController::Presenters::V2::ServiceInstanceSharedFromPresenter.new.to_hash(service_instance.space))]
+ else
+ [HTTP::NO_CONTENT, {}, '']
+ end
+ rescue CloudController::Errors::ApiError => e
+ if e.name == 'NotAuthorized'
+ HTTP::NOT_FOUND
+ else
+ raise e
+ end
+ end
+
+ get '/v2/service_instances/:guid/shared_to', :enumerate_shared_to_information
+ def enumerate_shared_to_information(guid)
+ service_instance = find_guid_and_validate_access(:read, guid, ManagedServiceInstance)
+ validate_access(:read, service_instance.space)
+
+ associated_controller = VCAP::CloudController::SpacesController
+ associated_path = "#{self.class.url_for_guid(guid)}/shared_to"
+
+ create_paginated_collection_renderer(service_instance).render_json(
+ associated_controller,
+ service_instance.shared_spaces_dataset,
+ associated_path,
+ @opts,
+ {},
+ )
+ rescue CloudController::Errors::ApiError => e
+ if e.name == 'NotAuthorized'
+ HTTP::NOT_FOUND
+ else
+ raise e
+ end
+ end
+
def self.url_for_guid(guid)
object = ServiceInstance.where(guid: guid).first
@@ -303,6 +346,28 @@ def unbind_route(route_guid, instance_guid)
private
+ class ServiceInstanceSharedToSerializer
+ def initialize(service_instance)
+ @service_instance = service_instance
+ end
+
+ def serialize(controller, space, opts, orphans=nil)
+ bound_app_count = ServiceBindingListFetcher.fetch_service_instance_bindings_in_space(@service_instance.guid, space.guid).count
+ CloudController::Presenters::V2::ServiceInstanceSharedToPresenter.new.to_hash(space, bound_app_count)
+ end
+ end
+
+ def create_paginated_collection_renderer(service_instance)
+ VCAP::CloudController::RestController::PaginatedCollectionRenderer.new(
+ VCAP::CloudController::RestController::SecureEagerLoader.new,
+ ServiceInstanceSharedToSerializer.new(service_instance),
+ {
+ max_results_per_page: config.get(:renderer, :max_results_per_page),
+ default_results_per_page: config.get(:renderer, :default_results_per_page),
+ max_inline_relations_depth: config.get(:renderer, :max_inline_relations_depth),
+ })
+ end
+
def route_services_enabled?
@config.get(:route_services_enabled)
end
diff --git a/app/controllers/v3/service_instances_controller.rb b/app/controllers/v3/service_instances_controller.rb
index 3f1141b7ae4..1aef6c27de5 100644
--- a/app/controllers/v3/service_instances_controller.rb
+++ b/app/controllers/v3/service_instances_controller.rb
@@ -1,11 +1,30 @@
require 'messages/to_many_relationship_message'
+require 'messages/service_instances/service_instances_list_message'
require 'presenters/v3/relationship_presenter'
require 'presenters/v3/to_many_relationship_presenter'
+require 'presenters/v3/paginated_list_presenter'
require 'actions/service_instance_share'
require 'actions/service_instance_unshare'
+require 'fetchers/service_instance_list_fetcher'
class ServiceInstancesV3Controller < ApplicationController
+ def index
+ message = ServiceInstancesListMessage.from_params(query_params)
+ invalid_param!(message.errors.full_messages) unless message.valid?
+
+ dataset = if can_read_globally?
+ ServiceInstanceListFetcher.new.fetch_all(message: message)
+ else
+ ServiceInstanceListFetcher.new.fetch(message: message, space_guids: readable_space_guids)
+ end
+
+ render status: :ok, json: Presenters::V3::PaginatedListPresenter.new(
+ dataset: dataset,
+ path: '/v3/service_instances',
+ message: message)
+ end
+
def share_service_instance
FeatureFlag.raise_unless_enabled!(:service_instance_sharing)
diff --git a/app/fetchers/service_binding_list_fetcher.rb b/app/fetchers/service_binding_list_fetcher.rb
index 2939ccd5eac..fd49728ba31 100644
--- a/app/fetchers/service_binding_list_fetcher.rb
+++ b/app/fetchers/service_binding_list_fetcher.rb
@@ -16,6 +16,13 @@ def fetch_all
filter(dataset)
end
+ def self.fetch_service_instance_bindings_in_space(service_instance_guid, space_guid)
+ ServiceBinding.select_all(ServiceBinding.table_name).
+ join(:apps, guid: :app_guid).
+ where(apps__space_guid: space_guid).
+ where(service_bindings__service_instance_guid: service_instance_guid)
+ end
+
private
def filter(dataset)
diff --git a/app/fetchers/service_instance_list_fetcher.rb b/app/fetchers/service_instance_list_fetcher.rb
new file mode 100644
index 00000000000..01212a3f3b1
--- /dev/null
+++ b/app/fetchers/service_instance_list_fetcher.rb
@@ -0,0 +1,30 @@
+module VCAP::CloudController
+ class ServiceInstanceListFetcher
+ def fetch(message:, space_guids:)
+ source_space_instance_dataset = ServiceInstance.select_all(ServiceInstance.table_name).
+ join(Space.table_name, id: :space_id, guid: space_guids)
+
+ shared_instance_dataset = ServiceInstance.select_all(ServiceInstance.table_name).
+ join(:service_instance_shares, service_instance_guid: :guid, target_space_guid: space_guids)
+
+ dataset = source_space_instance_dataset.union(shared_instance_dataset, alias: :service_instances)
+
+ filter(dataset, message)
+ end
+
+ def fetch_all(message:)
+ dataset = ServiceInstance.dataset
+ filter(dataset, message)
+ end
+
+ private
+
+ def filter(dataset, message)
+ if message.requested?(:names)
+ dataset = dataset.where(service_instances__name: message.names)
+ end
+
+ dataset
+ end
+ end
+end
diff --git a/app/messages/service_instances/service_instances_list_message.rb b/app/messages/service_instances/service_instances_list_message.rb
new file mode 100644
index 00000000000..f368e921fe4
--- /dev/null
+++ b/app/messages/service_instances/service_instances_list_message.rb
@@ -0,0 +1,32 @@
+require 'messages/list_message'
+
+module VCAP::CloudController
+ class ServiceInstancesListMessage < ListMessage
+ ALLOWED_KEYS = [:page, :per_page, :order_by, :names].freeze
+
+ attr_accessor(*ALLOWED_KEYS)
+
+ validates_with NoAdditionalParamsValidator
+ validates :names, array: true, allow_nil: true
+
+ def initialize(params={})
+ super(params.symbolize_keys)
+ end
+
+ def self.from_params(params)
+ opts = params.dup
+ to_array! opts, 'names'
+ new(opts.symbolize_keys)
+ end
+
+ def valid_order_by_values
+ super << :name
+ end
+
+ private
+
+ def allowed_keys
+ ALLOWED_KEYS
+ end
+ end
+end
diff --git a/app/models/services/service_instance.rb b/app/models/services/service_instance.rb
index b2aea99e2af..02159e1d975 100644
--- a/app/models/services/service_instance.rb
+++ b/app/models/services/service_instance.rb
@@ -68,6 +68,10 @@ def self.user_visibility_filter(user)
[:space, user.spaces_dataset],
[:space, user.audited_spaces_dataset],
[:space, user.managed_spaces_dataset],
+ [:shared_spaces, user.spaces_dataset],
+ [:shared_spaces, user.managed_spaces_dataset],
+ [:shared_spaces, user.audited_spaces_dataset],
+ [:shared_spaces, managed_organizations_spaces_dataset(user.managed_organizations_dataset)],
])
end
@@ -134,7 +138,7 @@ def credentials_with_serialization
alias_method_chain :credentials, 'serialization'
def in_suspended_org?
- space.in_suspended_org?
+ space&.in_suspended_org?
end
def after_create
@@ -171,6 +175,10 @@ def volume_service?
false
end
+ def shared?
+ shared_spaces.any?
+ end
+
def self.managed_organizations_spaces_dataset(managed_organizations_dataset)
VCAP::CloudController::Space.dataset.filter({ organization_id: managed_organizations_dataset.select(:organization_id) })
end
diff --git a/app/presenters/v2/service_instance_presenter.rb b/app/presenters/v2/service_instance_presenter.rb
index a020fdae8f2..8f688d6a781 100644
--- a/app/presenters/v2/service_instance_presenter.rb
+++ b/app/presenters/v2/service_instance_presenter.rb
@@ -28,6 +28,8 @@ def entity_hash(controller, obj, opts, depth, parents, orphans=nil)
obj_hash['service_plan_guid'] = service_plan.guid
obj_hash['service_guid'] = service_plan.service.guid
rel_hash['service_url'] = "/v2/services/#{service_plan.service.guid}"
+ rel_hash['shared_from_url'] = "/v2/service_instances/#{obj.guid}/shared_from"
+ rel_hash['shared_to_url'] = "/v2/service_instances/#{obj.guid}/shared_to"
end
obj_hash.merge!(rel_hash)
diff --git a/app/presenters/v2/service_instance_shared_from_presenter.rb b/app/presenters/v2/service_instance_shared_from_presenter.rb
new file mode 100644
index 00000000000..405ac03c3bd
--- /dev/null
+++ b/app/presenters/v2/service_instance_shared_from_presenter.rb
@@ -0,0 +1,14 @@
+module CloudController
+ module Presenters
+ module V2
+ class ServiceInstanceSharedFromPresenter
+ def to_hash(space)
+ {
+ 'space_name' => space.name,
+ 'organization_name' => space.organization.name
+ }
+ end
+ end
+ end
+ end
+end
diff --git a/app/presenters/v2/service_instance_shared_to_presenter.rb b/app/presenters/v2/service_instance_shared_to_presenter.rb
new file mode 100644
index 00000000000..0f3229579bf
--- /dev/null
+++ b/app/presenters/v2/service_instance_shared_to_presenter.rb
@@ -0,0 +1,13 @@
+require 'presenters/v2/service_instance_shared_from_presenter'
+
+module CloudController
+ module Presenters
+ module V2
+ class ServiceInstanceSharedToPresenter < ServiceInstanceSharedFromPresenter
+ def to_hash(space, bound_app_count)
+ super(space).merge({ 'bound_app_count' => bound_app_count })
+ end
+ end
+ end
+ end
+end
diff --git a/app/presenters/v3/paginated_list_presenter.rb b/app/presenters/v3/paginated_list_presenter.rb
index 48ad1f07a3f..238e18a7729 100644
--- a/app/presenters/v3/paginated_list_presenter.rb
+++ b/app/presenters/v3/paginated_list_presenter.rb
@@ -6,6 +6,7 @@
require 'presenters/v3/process_presenter'
require 'presenters/v3/route_mapping_presenter'
require 'presenters/v3/service_binding_presenter'
+require 'presenters/v3/service_instance_presenter'
require 'presenters/v3/task_presenter'
require 'presenters/v3/organization_presenter'
require 'presenters/v3/space_presenter'
@@ -24,7 +25,8 @@ class PaginatedListPresenter
'PackageModel' => VCAP::CloudController::Presenters::V3::PackagePresenter,
'RouteMappingModel' => VCAP::CloudController::Presenters::V3::RouteMappingPresenter,
'ServiceBinding' => VCAP::CloudController::Presenters::V3::ServiceBindingPresenter,
- 'TaskModel' => VCAP::CloudController::Presenters::V3::TaskPresenter,
+ 'ManagedServiceInstance' => VCAP::CloudController::Presenters::V3::ServiceInstancePresenter,
+ 'TaskModel' => VCAP::CloudController::Presenters::V3::TaskPresenter,
}.freeze
def initialize(dataset:, path:, message: nil, show_secrets: false)
diff --git a/app/presenters/v3/service_instance_presenter.rb b/app/presenters/v3/service_instance_presenter.rb
new file mode 100644
index 00000000000..537d1969e8a
--- /dev/null
+++ b/app/presenters/v3/service_instance_presenter.rb
@@ -0,0 +1,24 @@
+require 'presenters/v3/base_presenter'
+
+module VCAP::CloudController
+ module Presenters
+ module V3
+ class ServiceInstancePresenter < BasePresenter
+ def to_hash
+ {
+ guid: service_instance.guid,
+ created_at: service_instance.created_at,
+ updated_at: service_instance.updated_at,
+ name: service_instance.name
+ }
+ end
+
+ private
+
+ def service_instance
+ @resource
+ end
+ end
+ end
+ end
+end
diff --git a/config/routes.rb b/config/routes.rb
index 729606e0a8a..34661d0e55b 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -119,6 +119,7 @@
get '/apps/:app_guid/tasks', to: 'tasks#index'
# service_instances
+ get '/service_instances', to: 'service_instances_v3#index'
post '/service_instances/:service_instance_guid/relationships/shared_spaces', to: 'service_instances_v3#share_service_instance'
delete '/service_instances/:service_instance_guid/relationships/shared_spaces/:space_guid', to: 'service_instances_v3#unshare_service_instance'
end
diff --git a/docs/v2/index.html b/docs/v2/index.html
index b2e77eb7adc..6896378863e 100644
--- a/docs/v2/index.html
+++ b/docs/v2/index.html
@@ -875,6 +875,12 @@
tags
@@ -549,7 +581,9 @@ Body
"service_plan_url": "/v2/service_plans/4ec73bf4-9f3a-44c7-bbac-61ee9cb5a511",
"service_bindings_url": "/v2/service_instances/a34f1423-4b84-4727-ab49-3f1522c4cb16/service_bindings",
"service_keys_url": "/v2/service_instances/a34f1423-4b84-4727-ab49-3f1522c4cb16/service_keys",
- "routes_url": "/v2/service_instances/a34f1423-4b84-4727-ab49-3f1522c4cb16/routes"
+ "routes_url": "/v2/service_instances/a34f1423-4b84-4727-ab49-3f1522c4cb16/routes",
+ "shared_from_url": "/v2/service_instances/0d632575-bb06-4ea5-bb19-a451a9644d92/shared_from",
+ "shared_to_url": "/v2/service_instances/0d632575-bb06-4ea5-bb19-a451a9644d92/shared_to"
}
}
diff --git a/docs/v2/service_plans/list_all_service_instances_for_the_service_plan.html b/docs/v2/service_plans/list_all_service_instances_for_the_service_plan.html
index 5a7aab99fdb..770bcaf779f 100644
--- a/docs/v2/service_plans/list_all_service_instances_for_the_service_plan.html
+++ b/docs/v2/service_plans/list_all_service_instances_for_the_service_plan.html
@@ -303,7 +303,9 @@ Body
"service_plan_url": "/v2/service_plans/85615ea0-9d23-4de8-aabd-89bffcce39d5",
"service_bindings_url": "/v2/service_instances/0fac6687-69fd-4567-afb0-dd39503523ff/service_bindings",
"service_keys_url": "/v2/service_instances/0fac6687-69fd-4567-afb0-dd39503523ff/service_keys",
- "routes_url": "/v2/service_instances/0fac6687-69fd-4567-afb0-dd39503523ff/routes"
+ "routes_url": "/v2/service_instances/0fac6687-69fd-4567-afb0-dd39503523ff/routes",
+ "shared_from_url": "/v2/service_instances/9547e9ed-e460-4abe-bda3-7070b9835917/shared_from",
+ "shared_to_url": "/v2/service_instances/9547e9ed-e460-4abe-bda3-7070b9835917/shared_to"
}
}
]
diff --git a/docs/v2/spaces/list_all_service_instances_for_the_space.html b/docs/v2/spaces/list_all_service_instances_for_the_space.html
index 19145598b74..e3cd4cae7f9 100644
--- a/docs/v2/spaces/list_all_service_instances_for_the_space.html
+++ b/docs/v2/spaces/list_all_service_instances_for_the_space.html
@@ -302,7 +302,9 @@ Body
"service_plan_url": "/v2/service_plans/fcf57f7f-3c51-49b2-b252-dc24e0f7dcab",
"service_bindings_url": "/v2/service_instances/9547e9ed-e460-4abe-bda3-7070b9835917/service_bindings",
"service_keys_url": "/v2/service_instances/9547e9ed-e460-4abe-bda3-7070b9835917/service_keys",
- "routes_url": "/v2/service_instances/9547e9ed-e460-4abe-bda3-7070b9835917/routes"
+ "routes_url": "/v2/service_instances/9547e9ed-e460-4abe-bda3-7070b9835917/routes",
+ "shared_from_url": "/v2/service_instances/9547e9ed-e460-4abe-bda3-7070b9835917/shared_from",
+ "shared_to_url": "/v2/service_instances/9547e9ed-e460-4abe-bda3-7070b9835917/shared_to"
}
}
]
diff --git a/docs/v3/source/includes/api_resources/_service_instances.erb b/docs/v3/source/includes/api_resources/_service_instances.erb
index 3868b3df80f..a9a15f49183 100644
--- a/docs/v3/source/includes/api_resources/_service_instances.erb
+++ b/docs/v3/source/includes/api_resources/_service_instances.erb
@@ -18,3 +18,28 @@
}
}
<% end %>
+
+<% content_for :paginated_list_of_service_instances do %>
+{
+ "pagination": {
+ "total_results": 1,
+ "total_pages": 1,
+ "first": {
+ "href": "https://api.example.org/v3/service_instances?page=1&per_page=50"
+ },
+ "last": {
+ "href": "https://api.example.org/v3/service_instances?page=1&per_page=50"
+ },
+ "next": null,
+ "previous": null
+ },
+ "resources": [
+ {
+ "guid": "d4c91047-7b29-4fda-b7f9-04033e5c9c9f",
+ "created_at": "2017-02-02T00:14:30Z",
+ "updated_at": "2017-02-02T00:14:30Z",
+ "name": "my_service_instance"
+ }
+ ]
+}
+<% end %>
diff --git a/docs/v3/source/includes/experimental_resources/service_instances/_list.md.erb b/docs/v3/source/includes/experimental_resources/service_instances/_list.md.erb
new file mode 100644
index 00000000000..de938427740
--- /dev/null
+++ b/docs/v3/source/includes/experimental_resources/service_instances/_list.md.erb
@@ -0,0 +1,36 @@
+### List service instances
+
+```
+Example Request
+```
+
+```shell
+curl "https://api.example.org/v3/service_instances" \
+ -X GET \
+ -H "Authorization: bearer [token]"
+```
+
+```
+Example Response
+```
+
+```http
+HTTP/1.1 200 OK
+Content-Type: application/json
+
+<%= yield_content :paginated_list_of_service_instances, '/v3/service_instances' %>
+```
+This endpoint retrieves the service instances the user has access to. At the moment, this endpoint only returns managed service instances. This may change in the future.
+
+This includes access granted by service instance sharing.
+
+#### Definition
+`GET /v3/service_instances`
+
+#### Query Parameters
+
+Name | Type | Description
+---- | ---- | ------------
+**name** | _list of strings_ | Comma-delimited list of service instance names to filter by.
+**page** | _integer_ | Page to display. Valid values are integers >= 1.
+**per_page** | _integer_ | Number of results per page. Valid values are 1 through 5000.
diff --git a/docs/v3/source/index.md b/docs/v3/source/index.md
index 75e2112c34b..b035577261e 100644
--- a/docs/v3/source/index.md
+++ b/docs/v3/source/index.md
@@ -137,6 +137,7 @@ includes:
- experimental_resources/service_bindings/delete
- experimental_resources/service_bindings/list
- experimental_resources/service_instances/header
+ - experimental_resources/service_instances/list
- experimental_resources/service_instances/share_to_space
- experimental_resources/service_instances/unshare_from_space
search: true
diff --git a/spec/request/service_instances_spec.rb b/spec/request/service_instances_spec.rb
index c9e0a68bb80..b0376067818 100644
--- a/spec/request/service_instances_spec.rb
+++ b/spec/request/service_instances_spec.rb
@@ -2,11 +2,127 @@
RSpec.describe 'Service Instances' do
let(:user_email) { 'user@email.example.com' }
- let(:user_name) { 'sharer_username' }
+ let(:user_name) { 'username' }
let(:user) { VCAP::CloudController::User.make }
+ let(:user_header) { headers_for(user) }
let(:admin_header) { admin_headers_for(user, email: user_email, user_name: user_name) }
+ let(:space) { VCAP::CloudController::Space.make }
let(:target_space) { VCAP::CloudController::Space.make }
- let(:service_instance) { VCAP::CloudController::ManagedServiceInstance.make }
+ let!(:service_instance1) { VCAP::CloudController::ManagedServiceInstance.make(space: space, name: 'rabbitmq') }
+ let!(:service_instance2) { VCAP::CloudController::ManagedServiceInstance.make(space: space, name: 'redis') }
+ let!(:service_instance3) { VCAP::CloudController::ManagedServiceInstance.make(space: space, name: 'mysql') }
+
+ describe 'GET /v3/service_instances' do
+ it 'returns a paginated list of service instances the user has access to' do
+ set_current_user_as_role(role: 'space_developer', org: space.organization, space: space, user: user)
+ get '/v3/service_instances?per_page=2&order_by=name', nil, user_header
+ expect(last_response.status).to eq(200)
+
+ parsed_response = MultiJson.load(last_response.body)
+ expect(parsed_response).to be_a_response_like(
+ {
+ 'pagination' => {
+ 'total_results' => 3,
+ 'total_pages' => 2,
+ 'first' => {
+ 'href' => "#{link_prefix}/v3/service_instances?order_by=name&page=1&per_page=2"
+ },
+ 'last' => {
+ 'href' => "#{link_prefix}/v3/service_instances?order_by=name&page=2&per_page=2"
+ },
+ 'next' => {
+ 'href' => "#{link_prefix}/v3/service_instances?order_by=name&page=2&per_page=2"
+ },
+ 'previous' => nil
+ },
+ 'resources' => [
+ {
+ 'guid' => service_instance3.guid,
+ 'name' => service_instance3.name,
+ 'created_at' => iso8601,
+ 'updated_at' => iso8601,
+ },
+ {
+ 'guid' => service_instance1.guid,
+ 'name' => service_instance1.name,
+ 'created_at' => iso8601,
+ 'updated_at' => iso8601,
+ }
+ ]
+ }
+ )
+ end
+
+ it 'returns a paginated list of service instances filtered by name' do
+ set_current_user_as_role(role: 'space_developer', org: space.organization, space: space, user: user)
+ get '/v3/service_instances?per_page=2&names=redis', nil, user_header
+ expect(last_response.status).to eq(200)
+
+ parsed_response = MultiJson.load(last_response.body)
+ expect(parsed_response).to be_a_response_like(
+ {
+ 'pagination' => {
+ 'total_results' => 1,
+ 'total_pages' => 1,
+ 'first' => {
+ 'href' => "#{link_prefix}/v3/service_instances?names=redis&page=1&per_page=2"
+ },
+ 'last' => {
+ 'href' => "#{link_prefix}/v3/service_instances?names=redis&page=1&per_page=2"
+ },
+ 'next' => nil,
+ 'previous' => nil
+ },
+ 'resources' => [
+ {
+ 'guid' => service_instance2.guid,
+ 'name' => service_instance2.name,
+ 'created_at' => iso8601,
+ 'updated_at' => iso8601,
+ }
+ ]
+ }
+ )
+ end
+
+ context 'when a user has access to a shared service instance' do
+ before do
+ service_instance1.add_shared_space(target_space)
+ end
+
+ it 'returns a paginated list of service instances the user has access to' do
+ set_current_user_as_role(role: 'space_developer', org: target_space.organization, space: target_space, user: user)
+ get '/v3/service_instances?per_page=2&order_by=name', nil, user_header
+ expect(last_response.status).to eq(200)
+
+ parsed_response = MultiJson.load(last_response.body)
+ expect(parsed_response).to be_a_response_like(
+ {
+ 'pagination' => {
+ 'total_results' => 1,
+ 'total_pages' => 1,
+ 'first' => {
+ 'href' => "#{link_prefix}/v3/service_instances?order_by=name&page=1&per_page=2"
+ },
+ 'last' => {
+ 'href' => "#{link_prefix}/v3/service_instances?order_by=name&page=1&per_page=2"
+ },
+ 'next' => nil,
+ 'previous' => nil
+ },
+ 'resources' => [
+ {
+ 'guid' => service_instance1.guid,
+ 'name' => service_instance1.name,
+ 'created_at' => iso8601,
+ 'updated_at' => iso8601,
+ }
+ ]
+ }
+ )
+ end
+ end
+ end
describe 'POST /v3/service_instances/:guid/relationships/shared_spaces' do
before do
@@ -20,7 +136,7 @@
]
}
- post "/v3/service_instances/#{service_instance.guid}/relationships/shared_spaces", share_request.to_json, admin_header
+ post "/v3/service_instances/#{service_instance1.guid}/relationships/shared_spaces", share_request.to_json, admin_header
parsed_response = MultiJson.load(last_response.body)
expect(last_response.status).to eq(200)
@@ -30,8 +146,8 @@
{ 'guid' => target_space.guid }
],
'links' => {
- 'self' => { 'href' => "#{link_prefix}/v3/service_instances/#{service_instance.guid}/relationships/shared_spaces" },
- 'related' => { 'href' => "#{link_prefix}/v3/service_instances/#{service_instance.guid}/shared_spaces" },
+ 'self' => { 'href' => "#{link_prefix}/v3/service_instances/#{service_instance1.guid}/relationships/shared_spaces" },
+ 'related' => { 'href' => "#{link_prefix}/v3/service_instances/#{service_instance1.guid}/shared_spaces" },
}
}
@@ -44,11 +160,11 @@
actor_type: 'user',
actor_name: user_email,
actor_username: user_name,
- actee: service_instance.guid,
+ actee: service_instance1.guid,
actee_type: 'service_instance',
- actee_name: service_instance.name,
- space_guid: service_instance.space.guid,
- organization_guid: service_instance.space.organization.guid
+ actee_name: service_instance1.name,
+ space_guid: space.guid,
+ organization_guid: space.organization.guid
})
expect(event.metadata['target_space_guids']).to eq([target_space.guid])
end
@@ -68,12 +184,12 @@
]
}
- post "/v3/service_instances/#{service_instance.guid}/relationships/shared_spaces", share_request.to_json, admin_header
+ post "/v3/service_instances/#{service_instance1.guid}/relationships/shared_spaces", share_request.to_json, admin_header
expect(last_response.status).to eq(200)
end
it 'unshares the service instance from the target space' do
- delete "/v3/service_instances/#{service_instance.guid}/relationships/shared_spaces/#{target_space.guid}", nil, admin_header
+ delete "/v3/service_instances/#{service_instance1.guid}/relationships/shared_spaces/#{target_space.guid}", nil, admin_header
expect(last_response.status).to eq(204)
event = VCAP::CloudController::Event.last
@@ -83,23 +199,23 @@
actor_type: 'user',
actor_name: user_email,
actor_username: user_name,
- actee: service_instance.guid,
+ actee: service_instance1.guid,
actee_type: 'service_instance',
- actee_name: service_instance.name,
- space_guid: service_instance.space.guid,
- organization_guid: service_instance.space.organization.guid
+ actee_name: service_instance1.name,
+ space_guid: space.guid,
+ organization_guid: space.organization.guid
})
expect(event.metadata['target_space_guid']).to eq(target_space.guid)
end
it 'deletes associated bindings in target space when service instance is unshared' do
process = VCAP::CloudController::ProcessModelFactory.make(diego: false, space: target_space)
- service_binding = VCAP::CloudController::ServiceBinding.make(service_instance: service_instance, app: process.app, credentials: { secret: 'key' })
+ service_binding = VCAP::CloudController::ServiceBinding.make(service_instance: service_instance1, app: process.app, credentials: { secret: 'key' })
get "/v2/service_bindings/#{service_binding.guid}", nil, admin_header
expect(last_response.status).to eq(200)
- delete "/v3/service_instances/#{service_instance.guid}/relationships/shared_spaces/#{target_space.guid}", nil, admin_header
+ delete "/v3/service_instances/#{service_instance1.guid}/relationships/shared_spaces/#{target_space.guid}", nil, admin_header
expect(last_response.status).to eq(204)
get "/v2/service_bindings/#{service_binding.guid}", nil, admin_header
diff --git a/spec/request/v2/service_bindings_spec.rb b/spec/request/v2/service_bindings_spec.rb
index 11aea28425a..ce50ba51bb5 100644
--- a/spec/request/v2/service_bindings_spec.rb
+++ b/spec/request/v2/service_bindings_spec.rb
@@ -197,7 +197,9 @@
'service_plan_url' => "/v2/service_plans/#{service_instance.service_plan.guid}",
'service_bindings_url' => "/v2/service_instances/#{service_instance.guid}/service_bindings",
'service_keys_url' => "/v2/service_instances/#{service_instance.guid}/service_keys",
- 'routes_url' => "/v2/service_instances/#{service_instance.guid}/routes"
+ 'routes_url' => "/v2/service_instances/#{service_instance.guid}/routes",
+ 'shared_from_url' => "/v2/service_instances/#{service_instance.guid}/shared_from",
+ 'shared_to_url' => "/v2/service_instances/#{service_instance.guid}/shared_to",
}
}
}
diff --git a/spec/request/v2/service_instances_spec.rb b/spec/request/v2/service_instances_spec.rb
index 8708ea9e454..2446c5b6414 100644
--- a/spec/request/v2/service_instances_spec.rb
+++ b/spec/request/v2/service_instances_spec.rb
@@ -54,7 +54,9 @@
'service_plan_url' => "/v2/service_plans/#{service_plan.guid}",
'service_bindings_url' => "/v2/service_instances/#{service_instance.guid}/service_bindings",
'service_keys_url' => "/v2/service_instances/#{service_instance.guid}/service_keys",
- 'routes_url' => "/v2/service_instances/#{service_instance.guid}/routes"
+ 'routes_url' => "/v2/service_instances/#{service_instance.guid}/routes",
+ 'shared_from_url' => "/v2/service_instances/#{service_instance.guid}/shared_from",
+ 'shared_to_url' => "/v2/service_instances/#{service_instance.guid}/shared_to",
}
}
)
@@ -96,7 +98,9 @@
'service_url' => "/v2/services/#{service_instance.service.guid}",
'service_bindings_url' => "/v2/service_instances/#{service_instance.guid}/service_bindings",
'service_keys_url' => "/v2/service_instances/#{service_instance.guid}/service_keys",
- 'routes_url' => "/v2/service_instances/#{service_instance.guid}/routes"
+ 'routes_url' => "/v2/service_instances/#{service_instance.guid}/routes",
+ 'shared_from_url' => "/v2/service_instances/#{service_instance.guid}/shared_from",
+ 'shared_to_url' => "/v2/service_instances/#{service_instance.guid}/shared_to",
}
}
)
@@ -137,7 +141,9 @@
'service_url' => "/v2/services/#{service_instance.service.guid}",
'service_bindings_url' => "/v2/service_instances/#{service_instance.guid}/service_bindings",
'service_keys_url' => "/v2/service_instances/#{service_instance.guid}/service_keys",
- 'routes_url' => "/v2/service_instances/#{service_instance.guid}/routes"
+ 'routes_url' => "/v2/service_instances/#{service_instance.guid}/routes",
+ 'shared_from_url' => "/v2/service_instances/#{service_instance.guid}/shared_from",
+ 'shared_to_url' => "/v2/service_instances/#{service_instance.guid}/shared_to",
}
}
)
@@ -145,4 +151,97 @@
end
end
end
+
+ describe 'GET /v2/service_instances/:service_instance_guid/shared_from' do
+ let(:service_instance) { VCAP::CloudController::ManagedServiceInstance.make(space: space) }
+
+ before do
+ service_instance.add_shared_space(VCAP::CloudController::Space.make)
+ end
+
+ it 'returns data about the source space and org' do
+ get "v2/service_instances/#{service_instance.guid}/shared_from", nil, admin_headers
+
+ expect(last_response.status).to eq(200), last_response.body
+
+ parsed_response = MultiJson.load(last_response.body)
+ expect(parsed_response).to be_a_response_like({
+ 'space_name' => space.name,
+ 'organization_name' => space.organization.name
+ })
+ end
+
+ context 'when the user is a member of the space where a service instance has been shared to' do
+ let(:other_space) { VCAP::CloudController::Space.make }
+ let(:other_user) { make_developer_for_space(other_space) }
+ let(:req_body) do
+ {
+ data: [
+ { guid: other_space.guid }
+ ]
+ }.to_json
+ end
+
+ before do
+ VCAP::CloudController::FeatureFlag.make(name: 'service_instance_sharing', enabled: true, error_message: nil)
+
+ other_space.organization.add_user(user)
+ other_space.add_developer(user)
+
+ post "v3/service_instances/#{service_instance.guid}/relationships/shared_spaces", req_body, headers_for(user)
+ expect(last_response.status).to eq(200)
+ end
+
+ it 'returns data about the source space and org' do
+ get "v2/service_instances/#{service_instance.guid}/shared_from", nil, headers_for(other_user)
+
+ expect(last_response.status).to eq(200)
+
+ parsed_response = MultiJson.load(last_response.body)
+ expect(parsed_response).to be_a_response_like({
+ 'space_name' => space.name,
+ 'organization_name' => space.organization.name
+ })
+ end
+ end
+ end
+
+ describe 'GET /v2/service_instances/:service_instance_guid/shared_to' do
+ let(:service_instance) { VCAP::CloudController::ManagedServiceInstance.make(space: space) }
+ let(:space1) { VCAP::CloudController::Space.make }
+ let(:space2) { VCAP::CloudController::Space.make }
+
+ before do
+ service_instance.add_shared_space(space1)
+ service_instance.add_shared_space(space2)
+ end
+
+ it 'returns data about the source space, org, and bound_app_count' do
+ get "v2/service_instances/#{service_instance.guid}/shared_to", nil, admin_headers
+
+ expect(last_response.status).to eq(200)
+
+ parsed_response = MultiJson.load(last_response.body)
+ expect(parsed_response).to be_a_response_like(
+ {
+ 'total_results' => 2,
+ 'total_pages' => 1,
+ 'prev_url' => nil,
+ 'next_url' => nil,
+ 'resources' => [
+ {
+ 'space_name' => space1.name,
+ 'organization_name' => space1.organization.name,
+ 'bound_app_count' => 0
+ },
+ {
+ 'space_name' => space2.name,
+ 'organization_name' => space2.organization.name,
+ 'bound_app_count' => 0
+ }
+ ]
+ }
+ )
+ end
+ end
end
diff --git a/spec/request/v2/spaces_spec.rb b/spec/request/v2/spaces_spec.rb
index bdd08d3d8ed..f163cf306fa 100644
--- a/spec/request/v2/spaces_spec.rb
+++ b/spec/request/v2/spaces_spec.rb
@@ -132,7 +132,7 @@
space.add_developer(user)
end
- it 'lists the isolation segment for SpaceDvelopers' do
+ it 'lists the isolation segment for SpaceDevelopers' do
get "/v2/spaces/#{space.guid}", {}, headers_for(user)
expect(last_response.status).to eq(200)
@@ -170,6 +170,63 @@
end
end
+ describe 'GET /v2/spaces/:guid/service_instances' do
+ let(:originating_space) { VCAP::CloudController::Space.make }
+ let(:shared_service_instance) { VCAP::CloudController::ManagedServiceInstance.make(space: originating_space) }
+ let(:space) { VCAP::CloudController::Space.make }
+
+ before do
+ originating_space.organization.add_user(user)
+ originating_space.add_developer(user)
+ space.organization.add_user(user)
+ space.add_developer(user)
+
+ shared_service_instance.add_shared_space(space)
+ end
+
+ it 'shows the shared service instances associated with the space' do
+ get "/v2/spaces/#{space.guid}/service_instances", {}, headers_for(user)
+
+ expect(last_response.status).to eq(200)
+ parsed_response = MultiJson.load(last_response.body)
+
+ expect(parsed_response).to be_a_response_like({
+ 'total_results' => 1,
+ 'total_pages' => 1,
+ 'prev_url' => nil,
+ 'next_url' => nil,
+ 'resources' => [{
+ 'metadata' => {
+ 'guid' => shared_service_instance.guid,
+ 'url' => "/v2/service_instances/#{shared_service_instance.guid}",
+ 'created_at' => iso8601,
+ 'updated_at' => iso8601,
+ },
+ 'entity' => {
+ 'name' => shared_service_instance.name,
+ 'credentials' => shared_service_instance.credentials,
+ 'service_plan_guid' => shared_service_instance.service_plan_guid,
+ 'space_guid' => originating_space.guid,
+ 'gateway_data' => nil,
+ 'dashboard_url' => nil,
+ 'type' => 'managed_service_instance',
+ 'last_operation' => nil,
+ 'tags' => [],
+ 'service_guid' => shared_service_instance.service_plan.service_guid,
+ 'space_url' => "/v2/spaces/#{originating_space.guid}",
+ 'service_plan_url' => "/v2/service_plans/#{shared_service_instance.service_plan_guid}",
+ 'service_bindings_url' => "/v2/service_instances/#{shared_service_instance.guid}/service_bindings",
+ 'service_keys_url' => "/v2/service_instances/#{shared_service_instance.guid}/service_keys",
+ 'routes_url' => "/v2/service_instances/#{shared_service_instance.guid}/routes",
+ 'service_url' => "/v2/services/#{shared_service_instance.service_plan.service_guid}",
+ 'shared_from_url' => "/v2/service_instances/#{shared_service_instance.guid}/shared_from",
+ 'shared_to_url' => "/v2/service_instances/#{shared_service_instance.guid}/shared_to",
+ }
+ }]
+ })
+ end
+ end
+
describe 'DELETE /v2/spaces/:guid/unmapped_routes' do
let(:space) { VCAP::CloudController::Space.make(organization: org) }
let(:process) { VCAP::CloudController::ProcessModelFactory.make(state: 'STARTED') }
diff --git a/spec/unit/access/service_instance_access_spec.rb b/spec/unit/access/service_instance_access_spec.rb
index 1361e7236d4..7ed67887254 100644
--- a/spec/unit/access/service_instance_access_spec.rb
+++ b/spec/unit/access/service_instance_access_spec.rb
@@ -149,6 +149,63 @@ module VCAP::CloudController
end
end
+ context 'space developer in a space that the service instance has been shared into' do
+ before do
+ org.add_user(user)
+ target_space = VCAP::CloudController::Space.make(organization: org)
+ target_space.add_developer(user)
+ service_instance.add_shared_space(target_space)
+ end
+
+ context 'when the space of the service instance is visible' do
+ it_behaves_like :read_only_access do
+ let(:object) { service_instance }
+ end
+
+ it 'does NOT allow the user to have manage permissions of the service instance' do
+ expect(subject).to_not allow_op_on_object(:manage_permissions, service_instance)
+ end
+
+ it 'allows the user to have read permissions of the service instance' do
+ expect(subject).to allow_op_on_object(:read_permissions, service_instance)
+ end
+
+ it 'does NOT allow the user to read default credentials of the service instance' do
+ expect(subject).not_to allow_op_on_object(:read_env, service_instance)
+ end
+
+ it 'returns false for purge' do
+ expect(subject).not_to allow_op_on_object(:purge, service_instance)
+ end
+ end
+
+ context 'when the space of the service instance is not visible' do
+ before do
+ service_instance.space = nil
+ end
+
+ it_behaves_like :read_only_access do
+ let(:object) { service_instance }
+ end
+
+ it 'does NOT allow the user to have manage permissions of the service instance' do
+ expect(subject).to_not allow_op_on_object(:manage_permissions, service_instance)
+ end
+
+ it 'allows the user to have read permissions of the service instance' do
+ expect(subject).to allow_op_on_object(:read_permissions, service_instance)
+ end
+
+ it 'does NOT allow the user to read default credentials of the service instance' do
+ expect(subject).not_to allow_op_on_object(:read_env, service_instance)
+ end
+
+ it 'returns false for purge' do
+ expect(subject).not_to allow_op_on_object(:purge, service_instance)
+ end
+ end
+ end
+
context 'organization manager (defensive)' do
before { org.add_manager(user) }
diff --git a/spec/unit/controllers/runtime/spaces_controller_spec.rb b/spec/unit/controllers/runtime/spaces_controller_spec.rb
index 7477158ad49..de2de367020 100644
--- a/spec/unit/controllers/runtime/spaces_controller_spec.rb
+++ b/spec/unit/controllers/runtime/spaces_controller_spec.rb
@@ -223,6 +223,14 @@ def decoded_guids
before { set_current_user(developer) }
+ it 'returns the shared from url' do
+ space_instance = ManagedServiceInstance.make(space: space)
+
+ get "/v2/spaces/#{space.guid}/service_instances"
+ service_instance_response = decoded_response.fetch('resources').first
+ expect(service_instance_response.fetch('entity').fetch('shared_from_url')).to eq("/v2/service_instances/#{space_instance.guid}/shared_from")
+ end
+
context 'when filtering results' do
it 'returns only matching results' do
user_provided_service_instance_1 = UserProvidedServiceInstance.make(space: space, name: 'provided service 1')
@@ -240,6 +248,39 @@ def decoded_guids
end
end
+ describe 'shared service instances' do
+ context 'when a service instance has been shared from another space' do
+ let(:shared_service_instance) { ManagedServiceInstance.make(space: Space.make) }
+
+ before do
+ shared_service_instance.add_shared_space(space)
+ end
+
+ it 'returns the shared service instance' do
+ get "v2/spaces/#{space.guid}/service_instances"
+
+ guids = decoded_response.fetch('resources').map { |service| service.fetch('metadata').fetch('guid') }
+ expect(guids).to include(shared_service_instance.guid)
+ end
+ end
+
+ context 'when a service instance has been shared between two spaces that are not the queried space' do
+ let(:other_space) { make_space_for_user(developer) }
+ let(:irrelevant_shared_service_instance) { ManagedServiceInstance.make(space: Space.make) }
+
+ before do
+ irrelevant_shared_service_instance.add_shared_space(other_space)
+ end
+
+ it 'does not return the irrelevant shared service instance' do
+ get "v2/spaces/#{space.guid}/service_instances"
+
+ guids = decoded_response.fetch('resources').map { |service| service.fetch('metadata').fetch('guid') }
+ expect(guids).not_to include(irrelevant_shared_service_instance.guid)
+ end
+ end
+ end
+
context 'when there are provided service instances' do
let!(:user_provided_service_instance) { UserProvidedServiceInstance.make(space: space) }
let!(:managed_service_instance) { ManagedServiceInstance.make(space: space) }
diff --git a/spec/unit/controllers/services/service_instances_controller_spec.rb b/spec/unit/controllers/services/service_instances_controller_spec.rb
index 6f6607fa9e2..93f3a78af3e 100644
--- a/spec/unit/controllers/services/service_instances_controller_spec.rb
+++ b/spec/unit/controllers/services/service_instances_controller_spec.rb
@@ -3496,6 +3496,253 @@ def verify_forbidden(user)
end
end
+ describe 'GET /v2/service_instances/:service_instance_guid/shared_from' do
+ let(:org) { Organization.make }
+ let(:space) { Space.make(organization: org) }
+ let(:instance) { ManagedServiceInstance.make(space: space) }
+
+ context 'when the service instance is not shared' do
+ it 'returns no content' do
+ set_current_user_as_admin
+ get "/v2/service_instances/#{instance.guid}/shared_from"
+ expect(last_response.status).to eql(204)
+ expect(JSON.parse(last_response.body)).to be nil
+ end
+ end
+
+ context 'when the service instance is shared' do
+ let(:other_org) { Organization.make }
+ let(:other_space) { Space.make(organization: other_org) }
+
+ before do
+ instance.add_shared_space(other_space)
+ end
+
+ it 'returns the correct body' do
+ set_current_user_as_admin
+ get "/v2/service_instances/#{instance.guid}/shared_from"
+ expect(last_response.status).to eql(200), last_response.body
+ parsed_response = JSON.parse(last_response.body)
+ expect(parsed_response['space_name']).to eq(space.name)
+ expect(parsed_response['organization_name']).to eq(space.organization.name)
+ expect(parsed_response.keys).to match_array(['space_name', 'organization_name'])
+ end
+
+ describe 'permissions' do
+ let(:user) { User.make }
+
+ context 'when the user is a member of the org/space this instance exists in' do
+ {
+ 'admin' => 200,
+ 'space_developer' => 200,
+ 'admin_read_only' => 200,
+ 'global_auditor' => 200,
+ 'space_manager' => 200,
+ 'space_auditor' => 200,
+ 'org_manager' => 200,
+ 'org_auditor' => 404,
+ 'org_billing_manager' => 404,
+ }.each do |role, expected_status|
+ context "as an #{role}" do
+ before do
+ set_current_user_as_role(
+ role: role,
+ org: org,
+ space: space,
+ user: user,
+ scopes: ['cloud_controller.read']
+ )
+ end
+
+ it "has a #{expected_status} http status code" do
+ get "/v2/service_instances/#{instance.guid}/shared_from"
+ expect(last_response.status).to eq(expected_status), "Expected #{expected_status}, got: #{last_response.status}, role: #{role}"
+ end
+ end
+ end
+ end
+
+ context 'when the user is a member of the org/space where the service instance was shared to' do
+ {
+ 'space_developer' => 200,
+ 'space_manager' => 200,
+ 'space_auditor' => 200,
+ 'org_manager' => 200,
+ 'org_auditor' => 404,
+ 'org_billing_manager' => 404,
+ }.each do |role, expected_status|
+ context "as an #{role}" do
+ before do
+ set_current_user_as_role(
+ role: role,
+ org: other_org,
+ space: other_space,
+ user: user,
+ scopes: ['cloud_controller.read']
+ )
+ end
+
+ it "has a #{expected_status} http status code" do
+ get "/v2/service_instances/#{instance.guid}/shared_from"
+ expect(last_response.status).to eq(expected_status), "Expected #{expected_status}, got: #{last_response.status}, role: #{role}"
+ end
+ end
+ end
+ end
+
+ context 'when the user is NOT a member of the space this instance exists in' do
+ let(:instance) { ManagedServiceInstance.make }
+
+ it 'returns a JSON payload indicating the user does not have permission to manage this instance' do
+ set_current_user(user)
+ get "/v2/service_instances/#{instance.guid}/shared_from"
+ expect(last_response.status).to eql(404)
+ end
+ end
+ end
+ end
+ end
+
+ describe 'GET /v2/service_instances/:service_instance_guid/shared_to' do
+ let(:org) { Organization.make }
+ let(:space) { Space.make(organization: org) }
+ let(:instance) { ManagedServiceInstance.make(space: space) }
+
+ it 'returns the correct body' do
+ set_current_user_as_admin
+ get "/v2/service_instances/#{instance.guid}/shared_to"
+ expect(last_response.status).to eql(200)
+ expect(JSON.parse(last_response.body)['resources']).to eq([])
+ end
+
+ context 'when the service instance is shared into multiple spaces' do
+ let(:space1) { Space.make }
+ let(:space2) { Space.make }
+
+ before do
+ FeatureFlag.make(name: 'service_instance_sharing', enabled: true, error_message: nil)
+ instance.add_shared_space(space1)
+ instance.add_shared_space(space2)
+ end
+
+ it 'returns the correct body' do
+ set_current_user_as_admin
+ get "/v2/service_instances/#{instance.guid}/shared_to"
+ decoded_response = JSON.parse(last_response.body)
+ expect(last_response.status).to eql(200), last_response.body
+ expect(decoded_response.fetch('total_results')).to eq(2)
+ resources = decoded_response.fetch('resources')
+
+ space1_resource = resources.find { |resource| resource['space_name'] == space1.name }
+ space2_resource = resources.find { |resource| resource['space_name'] == space2.name }
+
+ expect(space1_resource.keys).to match_array(['space_name', 'organization_name', 'bound_app_count'])
+ expect(space2_resource.keys).to match_array(['space_name', 'organization_name', 'bound_app_count'])
+
+ expect(space1_resource.fetch('space_name')).to eq(space1.name)
+ expect(space2_resource.fetch('space_name')).to eq(space2.name)
+
+ expect(space1_resource.fetch('organization_name')).to eq(space1.organization.name)
+ expect(space2_resource.fetch('organization_name')).to eq(space2.organization.name)
+
+ expect(space1_resource.fetch('bound_app_count')).to eq(0)
+ expect(space2_resource.fetch('bound_app_count')).to eq(0)
+ end
+
+ context 'when there are apps bound to the shared service instance' do
+ before do
+ ServiceBinding.make(service_instance: instance, app: AppModel.make(space: space1))
+ ServiceBinding.make(service_instance: instance, app: AppModel.make(space: space1))
+ ServiceBinding.make(service_instance: ServiceInstance.make(space: space1), app: AppModel.make(space: space1))
+
+ ServiceBinding.make(service_instance: instance, app: AppModel.make(space: space2))
+ end
+
+ it 'returns the correct bound_app_count' do
+ set_current_user_as_admin
+ get "/v2/service_instances/#{instance.guid}/shared_to"
+ decoded_response = JSON.parse(last_response.body)
+ expect(last_response.status).to eql(200), last_response.body
+ resources = decoded_response.fetch('resources')
+
+ space1_resource = resources.find { |resource| resource['space_name'] == space1.name }
+ space2_resource = resources.find { |resource| resource['space_name'] == space2.name }
+
+ expect(space1_resource.fetch('bound_app_count')).to eq(2)
+ expect(space2_resource.fetch('bound_app_count')).to eq(1)
+ end
+ end
+ end
+
+ describe 'permissions' do
+ let(:user) { User.make }
+
+ context 'when the user is a member of the org/space this instance exists in' do
+ {
+ 'admin' => 200,
+ 'space_developer' => 200,
+ 'admin_read_only' => 200,
+ 'global_auditor' => 200,
+ 'space_manager' => 200,
+ 'space_auditor' => 200,
+ 'org_manager' => 200,
+ 'org_auditor' => 404,
+ 'org_billing_manager' => 404,
+ }.each do |role, expected_status|
+ context "as an #{role}" do
+ before do
+ set_current_user_as_role(
+ role: role,
+ org: org,
+ space: space,
+ user: user,
+ )
+ end
+
+ it "has a #{expected_status} http status code" do
+ get "/v2/service_instances/#{instance.guid}/shared_to"
+ expect(last_response.status).to eq(expected_status), "Expected #{expected_status}, got: #{last_response.status}, role: #{role}"
+ end
+ end
+ end
+ end
+
+ context 'when the user is a member of the org/space where the service instance was shared to' do
+ let(:other_org) { Organization.make }
+ let(:other_space) { Space.make(organization: other_org) }
+
+ before do
+ instance.add_shared_space(other_space)
+ end
+
+ {
+ 'space_developer' => 404,
+ 'space_manager' => 404,
+ 'space_auditor' => 404,
+ 'org_manager' => 404,
+ 'org_auditor' => 404,
+ 'org_billing_manager' => 404,
+ }.each do |role, expected_status|
+ context "as an #{role}" do
+ before do
+ set_current_user_as_role(
+ role: role,
+ org: other_org,
+ space: other_space,
+ user: user,
+ )
+ end
+
+ it "has a #{expected_status} http status code" do
+ get "/v2/service_instances/#{instance.guid}/shared_to"
+ expect(last_response.status).to eq(expected_status), "Expected #{expected_status}, got: #{last_response.status}, role: #{role}"
+ end
+ end
+ end
+ end
+ end
+ end
+
describe 'GET /v2/service_instances/:service_instance_guid/service_keys' do
let(:space) { Space.make }
let(:manager) { make_manager_for_space(space) }
diff --git a/spec/unit/controllers/v3/service_instance_controller_spec.rb b/spec/unit/controllers/v3/service_instance_controller_spec.rb
index 16dad33ef32..b2025bc5e7f 100644
--- a/spec/unit/controllers/v3/service_instance_controller_spec.rb
+++ b/spec/unit/controllers/v3/service_instance_controller_spec.rb
@@ -2,6 +2,113 @@
RSpec.describe ServiceInstancesV3Controller, type: :controller do
let(:user) { set_current_user(VCAP::CloudController::User.make) }
+ let(:space) { VCAP::CloudController::Space.make }
+ let!(:service_instance) { VCAP::CloudController::ManagedServiceInstance.make(space: space) }
+
+ describe '#index' do
+ context 'when there are multiple service instances' do
+ let!(:service_instance2) { VCAP::CloudController::ManagedServiceInstance.make }
+ let!(:service_instance3) { VCAP::CloudController::ManagedServiceInstance.make }
+
+ context 'as an admin' do
+ before do
+ set_current_user_as_admin
+ end
+
+ it 'returns all service instances' do
+ get :index
+ expect(response.status).to eq(200), response.body
+ expect(parsed_body['resources'].length).to eq 3
+
+ response_names = parsed_body['resources'].map { |resource| resource['name'] }
+ expect(response_names).to include(service_instance.name, service_instance2.name, service_instance3.name)
+ end
+ end
+
+ context 'as a user who only has limited access' do
+ before do
+ set_current_user_as_role(role: 'space_developer', org: space.organization, space: space, user: user)
+ end
+
+ it 'returns a subset of service instances' do
+ get :index
+ expect(response.status).to eq(200), response.body
+ expect(parsed_body['resources'].length).to eq 1
+
+ response_names = parsed_body['resources'].map { |resource| resource['name'] }
+ expect(response_names).to include(service_instance.name)
+ end
+ end
+ end
+
+ describe 'permissions by role' do
+ role_to_expected_http_response = {
+ 'admin' => true,
+ 'admin_read_only' => true,
+ 'global_auditor' => true,
+ 'org_manager' => true,
+ 'org_auditor' => false,
+ 'org_billing_manager' => false,
+ 'space_manager' => true,
+ 'space_auditor' => true,
+ 'space_developer' => true,
+ }.freeze
+
+ role_to_expected_http_response.each do |role, can_see_service_instance|
+ context "as an #{role}" do
+ it "#{can_see_service_instance ? 'can' : 'cannot'} see the service instance" do
+ set_current_user_as_role(role: role, org: space.organization, space: space, user: user)
+
+ expected_service_instance_names = can_see_service_instance ? [service_instance.name] : []
+
+ get :index
+ expect(response.status).to eq(200), response.body
+ expect(parsed_body['resources'].map { |h| h['name'] }).to match_array(expected_service_instance_names)
+ end
+ end
+ end
+ end
+
+ describe 'permissions by role for shared services' do
+ let(:target_space) { VCAP::CloudController::Space.make }
+ before do
+ service_instance.add_shared_space(target_space)
+ end
+ role_to_expected_http_response = {
+ 'org_manager' => true,
+ 'org_auditor' => false,
+ 'org_billing_manager' => false,
+ 'space_manager' => true,
+ 'space_auditor' => true,
+ 'space_developer' => true,
+ }.freeze
+
+ role_to_expected_http_response.each do |role, can_see_service_instance|
+ context "as an #{role}" do
+ it "#{can_see_service_instance ? 'can' : 'cannot'} see the service instance" do
+ set_current_user_as_role(role: role, org: target_space.organization, space: target_space, user: user)
+
+ expected_service_instance_names = can_see_service_instance ? [service_instance.name] : []
+
+ get :index
+ expect(response.status).to eq(200), response.body
+ expect(parsed_body['resources'].map { |h| h['name'] }).to match_array(expected_service_instance_names)
+ end
+ end
+ end
+ end
+
+ context 'when a non-supported value is specified' do
+ it 'a bad query parameter error is returned' do
+ set_current_user_as_admin
+ get :index, { order_by: 'banana' }
+
+ expect(response.status).to eq(400)
+ expect(response.body).to include 'BadQueryParameter'
+ expect(response.body).to include("Order by can only be: 'created_at', 'updated_at', 'name'")
+ end
+ end
+ end
describe '#share_service_instance' do
let(:service_instance) { VCAP::CloudController::ServiceInstance.make }
diff --git a/spec/unit/messages/service_instances_list_message_spec.rb b/spec/unit/messages/service_instances_list_message_spec.rb
new file mode 100644
index 00000000000..1d935b1d602
--- /dev/null
+++ b/spec/unit/messages/service_instances_list_message_spec.rb
@@ -0,0 +1,60 @@
+require 'spec_helper'
+require 'messages/service_instances/service_instances_list_message'
+
+module VCAP::CloudController
+ RSpec.describe ServiceInstancesListMessage do
+ describe '.from_params' do
+ let(:params) do
+ {
+ 'page' => 1,
+ 'per_page' => 5,
+ 'order_by' => 'name',
+ 'names' => 'rabbitmq, redis,mysql'
+ }
+ end
+
+ it 'returns the correct ServiceInstancesListMessage' do
+ message = ServiceInstancesListMessage.from_params(params)
+
+ expect(message).to be_a(ServiceInstancesListMessage)
+ expect(message.page).to eq(1)
+ expect(message.per_page).to eq(5)
+ expect(message.order_by).to eq('name')
+ expect(message.names).to match_array(['mysql', 'rabbitmq', 'redis'])
+ end
+
+ it 'converts requested keys to symbols' do
+ message = ServiceInstancesListMessage.from_params(params)
+
+ expect(message.requested?(:page)).to be_truthy
+ expect(message.requested?(:per_page)).to be_truthy
+ expect(message.requested?(:order_by)).to be_truthy
+ expect(message.requested?(:names)).to be_truthy
+ end
+ end
+
+ describe 'fields' do
+ it 'accepts a set of fields' do
+ message = ServiceInstancesListMessage.new({
+ page: 1,
+ per_page: 5,
+ order_by: 'created_at',
+ names: ['rabbitmq', 'redis']
+ })
+ expect(message).to be_valid
+ end
+
+ it 'accepts an empty set' do
+ message = ServiceInstancesListMessage.new
+ expect(message).to be_valid
+ end
+
+ it 'does not accept a field not in this set' do
+ message = ServiceInstancesListMessage.new({ foobar: 'pants' })
+
+ expect(message).not_to be_valid
+ expect(message.errors[:base]).to include("Unknown query parameter(s): 'foobar'")
+ end
+ end
+ end
+end
diff --git a/spec/unit/models/services/service_instance_spec.rb b/spec/unit/models/services/service_instance_spec.rb
index 671bcc5b92f..52bb0ff5662 100644
--- a/spec/unit/models/services/service_instance_spec.rb
+++ b/spec/unit/models/services/service_instance_spec.rb
@@ -298,6 +298,14 @@ module VCAP::CloudController
expect(service_instance).not_to be_in_suspended_org
end
end
+
+ context 'when the service instance space is not visible' do
+ let(:space) { nil }
+
+ it 'is false' do
+ expect(service_instance).not_to be_in_suspended_org
+ end
+ end
end
describe '#to_hash' do
@@ -326,5 +334,121 @@ module VCAP::CloudController
expect(service_instance.to_hash(opts)['credentials']).to eq({ redacted_message: '[PRIVATE DATA HIDDEN]' })
end
end
+
+ describe '#user_visibility_filter' do
+ let(:developer) { make_developer_for_space(service_instance.space) }
+ let(:auditor) { make_auditor_for_space(service_instance.space) }
+ let(:user) { make_user_for_space(service_instance.space) }
+ let(:org_manager) { make_manager_for_org(service_instance.space.organization) }
+ let(:space_manager) { make_manager_for_space(service_instance.space) }
+
+ context 'when a user is an org manager where the instance was created' do
+ it 'the service instance is visible' do
+ filter = ServiceInstance.user_visibility_filter(org_manager)
+ expect(ServiceInstance.filter(filter).all.length).to eq(1)
+ end
+ end
+
+ context 'when a user is a space developer in the space the instance was created' do
+ it 'the service instance is visible' do
+ filter = ServiceInstance.user_visibility_filter(developer)
+ expect(ServiceInstance.filter(filter).all.length).to eq(1)
+ end
+ end
+
+ context 'when a user is a space auditor in the space the instance was created' do
+ it 'the service instance is visible' do
+ filter = ServiceInstance.user_visibility_filter(auditor)
+ expect(ServiceInstance.filter(filter).all.length).to eq(1)
+ end
+ end
+
+ context 'when a user is a space manager in the space the instance was created' do
+ it 'the service instance is visible' do
+ filter = ServiceInstance.user_visibility_filter(space_manager)
+ expect(ServiceInstance.filter(filter).all.length).to eq(1)
+ end
+ end
+
+ context 'when a user does not have access to the originating space' do
+ it 'the service instance is not visible' do
+ filter = ServiceInstance.user_visibility_filter(user)
+ expect(ServiceInstance.filter(filter).all.length).to eq(0)
+ end
+ end
+
+ context 'when the service instance is shared' do
+ let(:target_space) { VCAP::CloudController::Space.make }
+ let(:target_space_dev) { make_developer_for_space(target_space) }
+ let(:target_org_user) { make_user_for_org(target_space.organization) }
+ let(:target_space_auditor) { make_auditor_for_space(target_space) }
+ let(:target_space_manager) { make_manager_for_space(target_space) }
+ let(:target_space_org_manager) { make_manager_for_org(target_space.organization) }
+
+ before do
+ service_instance.add_shared_space(target_space)
+ end
+
+ context 'when a user is a space developer in the target space' do
+ it 'the service instance is visible' do
+ filter = ServiceInstance.user_visibility_filter(target_space_dev)
+ expect(ServiceInstance.filter(filter).all.length).to eq(1)
+ end
+ end
+
+ context 'when a user is a space developer in the source space' do
+ it 'the service instance is visible' do
+ filter = ServiceInstance.user_visibility_filter(developer)
+ expect(ServiceInstance.filter(filter).all.length).to eq(1)
+ end
+ end
+
+ context 'when a user is a space auditor in the target space' do
+ it 'the service instance is visible' do
+ filter = ServiceInstance.user_visibility_filter(target_space_auditor)
+ expect(ServiceInstance.filter(filter).all.length).to eq(1)
+ end
+ end
+
+ context 'when a user is a space manager in the target space' do
+ it 'the service instance is visible' do
+ filter = ServiceInstance.user_visibility_filter(target_space_manager)
+ expect(ServiceInstance.filter(filter).all.length).to eq(1)
+ end
+ end
+
+ context 'when a user is a org manager in the target space' do
+ it 'the service instance is visible' do
+ filter = ServiceInstance.user_visibility_filter(target_space_org_manager)
+ expect(ServiceInstance.filter(filter).all.length).to eq(1)
+ end
+ end
+
+ context 'when a user does not have access to the target space' do
+ it 'the service instance is not visible' do
+ filter = ServiceInstance.user_visibility_filter(target_org_user)
+ expect(ServiceInstance.filter(filter).all.length).to eq(0)
+ end
+ end
+ end
+ end
+
+ describe '#shared?' do
+ context 'when the service instance has shared spaces' do
+ before do
+ service_instance.add_shared_space(Space.make)
+ end
+
+ it 'returns true' do
+ expect(service_instance.shared?).to be true
+ end
+ end
+
+ context 'when the service instance does not have shared spaces' do
+ it 'returns false' do
+ expect(service_instance.shared?).to be false
+ end
+ end
+ end
end
end
diff --git a/spec/unit/presenters/v2/service_instance_presenter_spec.rb b/spec/unit/presenters/v2/service_instance_presenter_spec.rb
index 804e909e196..811a2d87a33 100644
--- a/spec/unit/presenters/v2/service_instance_presenter_spec.rb
+++ b/spec/unit/presenters/v2/service_instance_presenter_spec.rb
@@ -11,35 +11,49 @@ module CloudController::Presenters::V2
let(:relations_hash) { { 'relationship_url' => 'http://relationship.example.com' } }
subject { ServiceInstancePresenter.new }
- describe '#entity_hash' do
- before do
- set_current_user_as_admin
- end
+ before do
+ set_current_user_as_admin
+ allow(RelationsPresenter).to receive(:new).and_return(relations_presenter)
+ end
- let(:service_instance) do
- VCAP::CloudController::ServiceInstance.make(
- name: 'things',
- )
- end
- let(:service_plan) { VCAP::CloudController::ServicePlan.make }
+ describe 'ManagedServiceInstance' do
+ describe '#entity_hash' do
+ let(:service_instance) { VCAP::CloudController::ManagedServiceInstance.make }
+ let(:service_plan) { VCAP::CloudController::ServicePlan.make }
- before do
- service_instance.service_plan_id = service_plan.id
- service_instance.save
+ before do
+ service_instance.service_plan_id = service_plan.id
+ service_instance.save
+ end
- allow(RelationsPresenter).to receive(:new).and_return(relations_presenter)
+ it 'returns the service instance entity' do
+ expect(subject.entity_hash(controller, service_instance, opts, depth, parents, orphans)).to eq(
+ {
+ 'name' => service_instance.name,
+ 'service_plan_guid' => service_plan.guid,
+ 'service_guid' => service_plan.service.guid,
+ 'relationship_url' => 'http://relationship.example.com',
+ 'service_url' => "/v2/services/#{service_plan.service.guid}",
+ 'shared_from_url' => "/v2/service_instances/#{service_instance.guid}/shared_from",
+ 'shared_to_url' => "/v2/service_instances/#{service_instance.guid}/shared_to",
+ }
+ )
+ end
end
+ end
+
+ describe 'UserProvidedServiceInstance' do
+ describe '#entity_hash' do
+ let(:service_instance) { VCAP::CloudController::UserProvidedServiceInstance.make }
- it 'returns the service instance entity' do
- expect(subject.entity_hash(controller, service_instance, opts, depth, parents, orphans)).to eq(
- {
- 'name' => service_instance.name,
- 'service_plan_guid' => service_plan.guid,
- 'service_guid' => service_plan.service.guid,
- 'relationship_url' => 'http://relationship.example.com',
- 'service_url' => "/v2/services/#{service_plan.service.guid}"
- }
- )
+ it 'returns the service instance entity' do
+ expect(subject.entity_hash(controller, service_instance, opts, depth, parents, orphans)).to eq(
+ {
+ 'name' => service_instance.name,
+ 'relationship_url' => 'http://relationship.example.com',
+ }
+ )
+ end
end
end
end
diff --git a/spec/unit/presenters/v2/service_instance_shared_from_presenter_spec.rb b/spec/unit/presenters/v2/service_instance_shared_from_presenter_spec.rb
new file mode 100644
index 00000000000..8c18d57602e
--- /dev/null
+++ b/spec/unit/presenters/v2/service_instance_shared_from_presenter_spec.rb
@@ -0,0 +1,18 @@
+require 'spec_helper'
+
+module CloudController::Presenters::V2
+ RSpec.describe ServiceInstanceSharedFromPresenter do
+ describe '#to_hash' do
+ it 'returns the space and org name' do
+ space = VCAP::CloudController::Space.make
+ presenter = ServiceInstanceSharedFromPresenter.new
+ expect(presenter.to_hash(space)).to eq(
+ {
+ 'space_name' => space.name,
+ 'organization_name' => space.organization.name,
+ }
+ )
+ end
+ end
+ end
+end
diff --git a/spec/unit/presenters/v2/service_instance_shared_to_presenter_spec.rb b/spec/unit/presenters/v2/service_instance_shared_to_presenter_spec.rb
new file mode 100644
index 00000000000..8119cfd3c69
--- /dev/null
+++ b/spec/unit/presenters/v2/service_instance_shared_to_presenter_spec.rb
@@ -0,0 +1,19 @@
+require 'spec_helper'
+
+module CloudController::Presenters::V2
+ RSpec.describe ServiceInstanceSharedToPresenter do
+ describe '#to_hash' do
+ it 'returns the space name, org name, and bound app count' do
+ space = VCAP::CloudController::Space.make
+ presenter = ServiceInstanceSharedToPresenter.new
+ expect(presenter.to_hash(space, 42)).to eq(
+ {
+ 'space_name' => space.name,
+ 'organization_name' => space.organization.name,
+ 'bound_app_count' => 42
+ }
+ )
+ end
+ end
+ end
+end
diff --git a/spec/unit/presenters/v3/service_instance_presenter_spec.rb b/spec/unit/presenters/v3/service_instance_presenter_spec.rb
new file mode 100644
index 00000000000..58c595f90a4
--- /dev/null
+++ b/spec/unit/presenters/v3/service_instance_presenter_spec.rb
@@ -0,0 +1,20 @@
+require 'spec_helper'
+require 'presenters/v3/service_instance_presenter'
+
+module VCAP::CloudController::Presenters::V3
+ RSpec.describe ServiceInstancePresenter do
+ let(:presenter) { ServiceInstancePresenter.new(service_instance) }
+ let(:service_instance) { VCAP::CloudController::ManagedServiceInstance.make(name: 'denise-db') }
+
+ describe '#to_hash' do
+ let(:result) { presenter.to_hash }
+
+ it 'presents the model as a hash' do
+ expect(result[:guid]).to eq(service_instance.guid)
+ expect(result[:created_at]).to eq(service_instance.created_at)
+ expect(result[:updated_at]).to eq(service_instance.updated_at)
+ expect(result[:name]).to eq('denise-db')
+ end
+ end
+ end
+end
diff --git a/spec/unit/queries/service_binding_list_fetcher_spec.rb b/spec/unit/queries/service_binding_list_fetcher_spec.rb
index 1e6e825dfaf..3e45c6eb02a 100644
--- a/spec/unit/queries/service_binding_list_fetcher_spec.rb
+++ b/spec/unit/queries/service_binding_list_fetcher_spec.rb
@@ -90,5 +90,53 @@ module VCAP::CloudController
end
end
end
+
+ describe '#fetch_service_instance_bindings_in_space' do
+ let(:space) { Space.make }
+ let(:service_instance) { ServiceInstance.make(space: space) }
+
+ it 'returns a Sequel::Dataset' do
+ results = ServiceBindingListFetcher.fetch_service_instance_bindings_in_space(service_instance.guid, space.guid)
+ expect(results).to be_a(Sequel::Dataset)
+ end
+
+ context 'when there are no bindings' do
+ it 'returns an empty dataset' do
+ results = ServiceBindingListFetcher.fetch_service_instance_bindings_in_space(service_instance.guid, space.guid)
+ expect(results.count).to eql(0)
+ end
+ end
+
+ context 'when a binding exists in a space' do
+ let!(:service_binding) { ServiceBinding.make(app: AppModel.make(space: space), service_instance: service_instance) }
+ let!(:other_service_binding) { ServiceBinding.make }
+
+ it 'returns the binding for the correct space' do
+ results = ServiceBindingListFetcher.fetch_service_instance_bindings_in_space(service_instance.guid, space.guid)
+ expect(results.count).to eql(1)
+ end
+ end
+
+ context 'when multiple bindings exist in a space' do
+ let!(:service_binding1) { ServiceBinding.make(app: AppModel.make(space: space), service_instance: service_instance) }
+ let!(:service_binding2) { ServiceBinding.make(app: AppModel.make(space: space), service_instance: service_instance) }
+ let!(:other_service_binding) { ServiceBinding.make }
+
+ it 'returns the bindings for the correct space' do
+ results = ServiceBindingListFetcher.fetch_service_instance_bindings_in_space(service_instance.guid, space.guid)
+ expect(results.count).to eql(2)
+ end
+ end
+
+ context 'when multiple service instances exist' do
+ let!(:service_binding) { ServiceBinding.make(app: AppModel.make(space: space), service_instance: service_instance) }
+ let!(:other_service_binding) { ServiceBinding.make(service_instance: ServiceInstance.make(space: space)) }
+
+ it 'returns the binding for the correct service instance' do
+ results = ServiceBindingListFetcher.fetch_service_instance_bindings_in_space(service_instance.guid, space.guid)
+ expect(results.count).to eql(1)
+ end
+ end
+ end
end
end
diff --git a/spec/unit/queries/service_instance_list_fetcher_spec.rb b/spec/unit/queries/service_instance_list_fetcher_spec.rb
new file mode 100644
index 00000000000..fa07743ce8d
--- /dev/null
+++ b/spec/unit/queries/service_instance_list_fetcher_spec.rb
@@ -0,0 +1,103 @@
+require 'spec_helper'
+require 'fetchers/service_instance_list_fetcher'
+require 'messages/service_instances/service_instances_list_message'
+
+module VCAP::CloudController
+ RSpec.describe ServiceInstanceListFetcher do
+ let(:filters) { {} }
+ let(:message) { ServiceInstancesListMessage.new(filters) }
+ let(:fetcher) { ServiceInstanceListFetcher.new }
+
+ describe '#fetch_all' do
+ let!(:service_instance_1) { ManagedServiceInstance.make(name: 'rabbitmq') }
+ let!(:service_instance_2) { ManagedServiceInstance.make(name: 'redis') }
+
+ it 'returns a Sequel::Dataset' do
+ results = fetcher.fetch_all(message: message)
+ expect(results).to be_a(Sequel::Dataset)
+ end
+
+ it 'includes all the V3 Service Instances' do
+ results = fetcher.fetch_all(message: message).all
+ expect(results.length).to eq 2
+ expect(results).to include(service_instance_1, service_instance_2)
+ end
+
+ context 'filter' do
+ context 'by service instance name' do
+ let(:filters) { { names: ['rabbitmq'] } }
+
+ it 'only returns matching service instances' do
+ results = fetcher.fetch_all(message: message).all
+ expect(results).to match_array([service_instance_1])
+ expect(results).not_to include(service_instance_2)
+ end
+ end
+ end
+ end
+
+ describe '#fetch' do
+ let!(:service_instance_1) { ManagedServiceInstance.make(name: 'rabbitmq', space: space_1) }
+ let!(:service_instance_2) { ManagedServiceInstance.make(name: 'redis', space: space_1) }
+ let!(:service_instance_3) { ManagedServiceInstance.make(name: 'mysql', space: space_2) }
+
+ let(:space_1) { Space.make }
+ let(:space_2) { Space.make }
+
+ it 'returns all of the service instances in the specified space' do
+ results = fetcher.fetch(message: message, space_guids: [space_1.guid]).all
+
+ expect(results).to match_array([service_instance_1, service_instance_2])
+ end
+
+ context 'filter' do
+ context 'by service instance name' do
+ let(:filters) { { names: ['rabbitmq'] } }
+
+ it 'only returns matching service instances' do
+ results = fetcher.fetch(message: message, space_guids: [space_1.guid]).all
+ expect(results).to match_array([service_instance_1])
+ end
+ end
+
+ context 'by non-existent service instance name' do
+ let(:filters) { { names: ['made-up-name'] } }
+
+ it 'returns no matching service instances' do
+ results = fetcher.fetch(message: message, space_guids: [space_1.guid]).all
+ expect(results).to be_empty
+ end
+ end
+ end
+
+ context 'when service instances are shared' do
+ let(:shared_to_space) { Space.make }
+
+ before do
+ service_instance_2.add_shared_space(shared_to_space)
+ service_instance_1.add_shared_space(shared_to_space)
+ end
+
+ it 'returns all of the service instances shared into the specified space' do
+ results = fetcher.fetch(message: message, space_guids: [shared_to_space.guid]).all
+ expect(results).to match_array([service_instance_1, service_instance_2])
+ end
+ end
+
+ context 'when a space contains both shared and non-shared service instances' do
+ let(:shared_to_space) { Space.make }
+ let!(:service_instance_4) { ManagedServiceInstance.make(space: shared_to_space) }
+
+ before do
+ service_instance_2.add_shared_space(shared_to_space)
+ service_instance_1.add_shared_space(shared_to_space)
+ end
+
+ it 'returns all of the service instances shared into the specified space' do
+ results = fetcher.fetch(message: message, space_guids: [shared_to_space.guid]).all
+ expect(results).to match_array([service_instance_1, service_instance_2, service_instance_4])
+ end
+ end
+ end
+ end
+end
|