cloudfoundry/cloud_controller_ng

View on GitHub
spec/request/domains_spec.rb

Summary

Maintainability
F
5 days
Test Coverage
require 'spec_helper'
require 'request_spec_shared_examples'

RSpec.describe 'Domains Request' do
  let(:user) { VCAP::CloudController::User.make }
  let(:space) { VCAP::CloudController::Space.make }
  let(:org) { space.organization }
  let(:admin_header) { headers_for(user, scopes: %w[cloud_controller.admin]) }
  let(:user_header) { headers_for(user, scopes: []) }
  let(:routing_api_client) { instance_double(VCAP::CloudController::RoutingApi::Client) }
  let(:router_group) { instance_double(VCAP::CloudController::RoutingApi::RouterGroup, type: 'http') }

  before do
    VCAP::CloudController::Domain.dataset.destroy # this will clean up the seeded test domains
    allow(VCAP::CloudController::RoutingApi::Client).to receive(:new).and_return(routing_api_client)
    allow(routing_api_client).to receive(:router_group).with('some-router-guid').and_return router_group
    allow(routing_api_client).to receive(:router_group).with('some-other-router-guid').and_return nil
    allow(routing_api_client).to receive(:enabled?).and_return true
  end

  describe 'GET /v3/domains' do
    it_behaves_like 'list query endpoint' do
      let(:request) { 'v3/domains' }
      let(:message) { VCAP::CloudController::DomainsListMessage }
      let(:user_header) { admin_header }
      let(:params) do
        {
          page: '2',
          per_page: '10',
          order_by: 'updated_at',
          names: 'foo,bar',
          guids: 'foo,bar',
          organization_guids: 'foo,bar',
          label_selector: 'foo,bar',
          created_ats: "#{Time.now.utc.iso8601},#{Time.now.utc.iso8601}",
          updated_ats: { gt: Time.now.utc.iso8601 }
        }
      end
    end

    describe 'when the user is not logged in' do
      it 'returns 401 for Unauthenticated requests' do
        get '/v3/domains'
        expect(last_response.status).to eq(401)
      end
    end

    describe 'when the user is logged in' do
      let!(:non_visible_org) { VCAP::CloudController::Organization.make(guid: 'non-visible') }
      let!(:user_visible_org) { VCAP::CloudController::Organization.make(guid: 'visible') }

      # (domain)                        | (owning org)       | (visible orgs shared to)
      # (visible_owned_private_domain)  | (org)              | (non_visible_org, user_visible_org)
      # (visible_shared_private_domain) | (non_visible_org)  | (org)
      # (not_visible_private_domain)    | (non_visible_org)  | ()
      # (shared_domain)                 | ()                 | ()
      let!(:visible_owned_private_domain) do
        VCAP::CloudController::PrivateDomain.make(guid: 'domain1', name: 'domain1.com', owning_organization: org)
      end
      let!(:visible_shared_private_domain) do
        VCAP::CloudController::PrivateDomain.make(guid: 'domain2', name: 'domain2.com', owning_organization: non_visible_org)
      end
      let!(:not_visible_private_domain) do
        VCAP::CloudController::PrivateDomain.make(guid: 'domain3', name: 'domain3.com', owning_organization: non_visible_org)
      end
      let!(:shared_domain) do
        VCAP::CloudController::SharedDomain.make(guid: 'domain4', name: 'domain4.com')
      end

      let(:visible_owned_private_domain_json) do
        {
          guid: visible_owned_private_domain.guid,
          created_at: iso8601,
          updated_at: iso8601,
          name: visible_owned_private_domain.name,
          internal: false,
          router_group: nil,
          supported_protocols: ['http'],
          metadata: {
            labels: {},
            annotations: {}
          },
          relationships: {
            organization: {
              data: { guid: org.guid }
            },
            shared_organizations: {
              data: match_array(shared_visible_orgs)
            }
          },
          links: {
            self: { href: "#{link_prefix}/v3/domains/#{visible_owned_private_domain.guid}" },
            organization: { href: %r{#{Regexp.escape(link_prefix)}/v3/organizations/#{org.guid}} },
            route_reservations: { href: %r{#{Regexp.escape(link_prefix)}/v3/domains/#{visible_owned_private_domain.guid}/route_reservations} },
            shared_organizations: { href: %r{#{Regexp.escape(link_prefix)}/v3/domains/#{visible_owned_private_domain.guid}/relationships/shared_organizations} }
          }
        }
      end

      let(:visible_shared_private_domain_json) do
        {
          guid: visible_shared_private_domain.guid,
          created_at: iso8601,
          updated_at: iso8601,
          name: visible_shared_private_domain.name,
          internal: false,
          router_group: nil,
          supported_protocols: ['http'],
          metadata: {
            labels: {},
            annotations: {}
          },
          relationships: {
            organization: {
              data: { guid: non_visible_org.guid }
            },
            shared_organizations: {
              data: [{ guid: org.guid }]
            }
          },
          links: {
            self: { href: "#{link_prefix}/v3/domains/#{visible_shared_private_domain.guid}" },
            organization: { href: %r{#{Regexp.escape(link_prefix)}/v3/organizations/#{non_visible_org.guid}} },
            route_reservations: { href: %r{#{Regexp.escape(link_prefix)}/v3/domains/#{visible_shared_private_domain.guid}/route_reservations} },
            shared_organizations: { href: %r{#{Regexp.escape(link_prefix)}/v3/domains/#{visible_shared_private_domain.guid}/relationships/shared_organizations} }
          }
        }
      end

      let(:not_visible_private_domain_json) do
        {
          guid: not_visible_private_domain.guid,
          created_at: iso8601,
          updated_at: iso8601,
          name: not_visible_private_domain.name,
          internal: false,
          router_group: nil,
          supported_protocols: ['http'],
          metadata: {
            labels: {},
            annotations: {}
          },
          relationships: {
            organization: {
              data: { guid: non_visible_org.guid }
            },
            shared_organizations: {
              data: []
            }
          },
          links: {
            self: { href: "#{link_prefix}/v3/domains/#{not_visible_private_domain.guid}" },
            organization: { href: %r{#{Regexp.escape(link_prefix)}/v3/organizations/#{non_visible_org.guid}} },
            route_reservations: { href: %r{#{Regexp.escape(link_prefix)}/v3/domains/#{not_visible_private_domain.guid}/route_reservations} },
            shared_organizations: { href: %r{#{Regexp.escape(link_prefix)}/v3/domains/#{not_visible_private_domain.guid}/relationships/shared_organizations} }
          }
        }
      end

      let(:shared_domain_json) do
        {
          guid: shared_domain.guid,
          created_at: iso8601,
          updated_at: iso8601,
          name: shared_domain.name,
          internal: false,
          router_group: nil,
          supported_protocols: ['http'],
          metadata: {
            labels: {},
            annotations: {}
          },
          relationships: {
            organization: {
              data: nil
            },
            shared_organizations: {
              data: []
            }
          },
          links: {
            self: { href: "#{link_prefix}/v3/domains/#{shared_domain.guid}" },
            route_reservations: { href: %r{#{Regexp.escape(link_prefix)}/v3/domains/#{shared_domain.guid}/route_reservations} }
          }
        }
      end

      before do
        non_visible_org.add_private_domain(visible_owned_private_domain)
        org.add_private_domain(visible_shared_private_domain)
        user_visible_org.add_private_domain(visible_owned_private_domain)
      end

      describe 'scope level permissions' do
        let(:shared_visible_orgs) { [{ guid: non_visible_org.guid }, { guid: user_visible_org.guid }] }

        context 'when the user does not have the required scopes' do
          let(:user_header) { headers_for(user, scopes: []) }

          it 'returns a 403' do
            get '/v3/domains', nil, user_header
            expect(last_response.status).to eq(403)
          end
        end

        context 'when the user has the required scopes' do
          let(:api_call) { ->(user_headers) { get '/v3/domains', nil, user_headers } }
          let(:expected_codes_and_responses) do
            Hash.new(
              code: 200,
              response_objects: [
                visible_owned_private_domain_json,
                visible_shared_private_domain_json,
                not_visible_private_domain_json,
                shared_domain_json
              ]
            )
          end

          it_behaves_like 'permissions for list endpoint', GLOBAL_SCOPES
        end
      end

      describe 'org/space roles' do
        context 'when the domain is shared with an org that user is a billing manager' do
          before do
            user_visible_org.add_billing_manager(user)
          end

          let(:shared_visible_orgs) { [{ guid: user_visible_org.guid }] }

          let(:api_call) { ->(user_headers) { get '/v3/domains', nil, user_headers } }

          let(:expected_codes_and_responses) do
            h = Hash.new(
              code: 200,
              response_objects: [
                visible_owned_private_domain_json,
                visible_shared_private_domain_json,
                shared_domain_json
              ]
            )
            h['org_billing_manager'] = {
              code: 200,
              response_objects: [
                shared_domain_json
              ]
            }
            h['no_role'] = {
              code: 200,
              response_objects: [
                shared_domain_json
              ]
            }
            h
          end

          it_behaves_like 'permissions for list endpoint', LOCAL_ROLES
        end

        context 'when the domain is shared with an org that user is an org manager' do
          before do
            user_visible_org.add_manager(user)
          end

          let(:shared_visible_orgs) { [{ guid: user_visible_org.guid }] }

          let(:api_call) { ->(user_headers) { get '/v3/domains', nil, user_headers } }

          let(:expected_codes_and_responses) do
            h = Hash.new(
              code: 200,
              response_objects: [
                visible_owned_private_domain_json,
                visible_shared_private_domain_json,
                shared_domain_json
              ]
            )
            # because the user is a manager in the shared org, they have access to see the domain
            h['org_billing_manager'] = {
              code: 200,
              response_objects: [
                visible_owned_private_domain_json,
                shared_domain_json
              ]
            }
            h['no_role'] = {
              code: 200,
              response_objects: [
                visible_owned_private_domain_json,
                shared_domain_json
              ]
            }
            h
          end

          it_behaves_like 'permissions for list endpoint', LOCAL_ROLES
        end
      end

      describe 'when filtering by name' do
        let(:shared_visible_orgs) { [{ guid: user_visible_org.guid }] }
        let(:endpoint) { "/v3/domains?names=#{visible_shared_private_domain.name}" }
        let(:api_call) { ->(user_headers) { get endpoint, nil, user_headers } }

        let(:expected_codes_and_responses) do
          h = Hash.new(
            code: 200,
            response_objects: [
              visible_shared_private_domain_json
            ]
          )
          # because the user is a manager in the shared org, they have access to see the domain
          h['org_billing_manager'] = {
            code: 200,
            response_objects: []
          }
          h['no_role'] = {
            code: 200,
            response_objects: []
          }
          h
        end

        it_behaves_like 'permissions for list endpoint', ALL_PERMISSIONS

        context 'pagination' do
          let(:pagination_hsh) do
            {
              'total_results' => 1,
              'total_pages' => 1,
              'first' => { 'href' => "#{link_prefix}#{endpoint}&page=1&per_page=50" },
              'last' => { 'href' => "#{link_prefix}#{endpoint}&page=1&per_page=50" },
              'next' => nil,
              'previous' => nil
            }
          end

          it 'paginates the results' do
            get endpoint, nil, admin_header
            expect(pagination_hsh).to eq(parsed_response['pagination'])
          end
        end
      end

      describe 'when filtering by guid' do
        let(:shared_visible_orgs) { [{ guid: user_visible_org.guid }] }
        let(:endpoint) { "/v3/domains?guids=#{visible_shared_private_domain.guid}" }
        let(:api_call) { ->(user_headers) { get endpoint, nil, user_headers } }

        let(:expected_codes_and_responses) do
          h = Hash.new(
            code: 200,
            response_objects: [
              visible_shared_private_domain_json
            ]
          )
          # because the user is a manager in the shared org, they have access to see the domain
          h['org_billing_manager'] = {
            code: 200,
            response_objects: []
          }
          h['no_role'] = {
            code: 200,
            response_objects: []
          }
          h
        end

        it_behaves_like 'permissions for list endpoint', ALL_PERMISSIONS

        context 'pagination' do
          let(:pagination_hsh) do
            {
              'total_results' => 1,
              'total_pages' => 1,
              'first' => { 'href' => "#{link_prefix}#{endpoint}&page=1&per_page=50" },
              'last' => { 'href' => "#{link_prefix}#{endpoint}&page=1&per_page=50" },
              'next' => nil,
              'previous' => nil
            }
          end

          it 'paginates the results' do
            get endpoint, nil, admin_header
            expect(pagination_hsh).to eq(parsed_response['pagination'])
          end
        end
      end

      describe 'when filtering by owning organization guid' do
        let(:endpoint) { "/v3/domains?organization_guids=#{visible_shared_private_domain.owning_organization_guid}" }
        let(:api_call) { ->(user_headers) { get endpoint, nil, user_headers } }

        context 'when the user can read globally' do
          let(:expected_codes_and_responses) do
            Hash.new(
              code: 200,
              response_objects: [
                visible_shared_private_domain_json,
                not_visible_private_domain_json
              ]
            )
          end

          it_behaves_like 'permissions for list endpoint', GLOBAL_SCOPES
        end

        context 'when the user cannot read globally' do
          let(:expected_codes_and_responses) do
            h = Hash.new(
              code: 200,
              response_objects: [
                visible_shared_private_domain_json
              ]
            )
            # because the user is a manager in the shared org, they have access to see the domain
            h['org_billing_manager'] = {
              code: 200,
              response_objects: []
            }
            h['no_role'] = {
              code: 200,
              response_objects: []
            }
            h
          end

          it_behaves_like 'permissions for list endpoint', LOCAL_ROLES
        end

        context 'pagination' do
          let(:pagination_hsh) do
            {
              'total_results' => 2,
              'total_pages' => 1,
              'first' => { 'href' => "#{link_prefix}#{endpoint}&page=1&per_page=50" },
              'last' => { 'href' => "#{link_prefix}#{endpoint}&page=1&per_page=50" },
              'next' => nil,
              'previous' => nil
            }
          end

          it 'paginates the results' do
            get endpoint, nil, admin_header
            expect(pagination_hsh).to eq(parsed_response['pagination'])
          end
        end
      end
    end

    describe 'labels' do
      let!(:domain1) { VCAP::CloudController::PrivateDomain.make(name: 'dom1.com', owning_organization: org) }
      let!(:domain1_label) { VCAP::CloudController::DomainLabelModel.make(resource_guid: domain1.guid, key_name: 'animal', value: 'dog') }

      let!(:domain2) { VCAP::CloudController::PrivateDomain.make(name: 'dom2.com', owning_organization: org) }
      let!(:domain2_label) { VCAP::CloudController::DomainLabelModel.make(resource_guid: domain2.guid, key_name: 'animal', value: 'cow') }
      let!(:domain2__exclusive_label) { VCAP::CloudController::DomainLabelModel.make(resource_guid: domain2.guid, key_name: 'santa', value: 'claus') }

      it 'returns a 200 and the filtered domains for "in" label selector' do
        get '/v3/domains?label_selector=animal in (dog)', nil, admin_header

        parsed_response = MultiJson.load(last_response.body)

        expected_pagination = {
          'total_results' => 1,
          'total_pages' => 1,
          'first' => { 'href' => "#{link_prefix}/v3/domains?label_selector=animal+in+%28dog%29&page=1&per_page=50" },
          'last' => { 'href' => "#{link_prefix}/v3/domains?label_selector=animal+in+%28dog%29&page=1&per_page=50" },
          'next' => nil,
          'previous' => nil
        }

        expect(last_response.status).to eq(200)
        expect(parsed_response['resources'].pluck('guid')).to contain_exactly(domain1.guid)
        expect(parsed_response['pagination']).to eq(expected_pagination)
      end

      it 'returns a 200 and the filtered domains for "notin" label selector' do
        get '/v3/domains?label_selector=animal notin (dog)', nil, admin_header

        parsed_response = MultiJson.load(last_response.body)

        expected_pagination = {
          'total_results' => 1,
          'total_pages' => 1,
          'first' => { 'href' => "#{link_prefix}/v3/domains?label_selector=animal+notin+%28dog%29&page=1&per_page=50" },
          'last' => { 'href' => "#{link_prefix}/v3/domains?label_selector=animal+notin+%28dog%29&page=1&per_page=50" },
          'next' => nil,
          'previous' => nil
        }

        expect(last_response.status).to eq(200)
        expect(parsed_response['resources'].pluck('guid')).to contain_exactly(domain2.guid)
        expect(parsed_response['pagination']).to eq(expected_pagination)
      end

      it 'returns a 200 and the filtered domains for "=" label selector' do
        get '/v3/domains?label_selector=animal=dog', nil, admin_header

        parsed_response = MultiJson.load(last_response.body)

        expected_pagination = {
          'total_results' => 1,
          'total_pages' => 1,
          'first' => { 'href' => "#{link_prefix}/v3/domains?label_selector=animal%3Ddog&page=1&per_page=50" },
          'last' => { 'href' => "#{link_prefix}/v3/domains?label_selector=animal%3Ddog&page=1&per_page=50" },
          'next' => nil,
          'previous' => nil
        }

        expect(last_response.status).to eq(200)
        expect(parsed_response['resources'].pluck('guid')).to contain_exactly(domain1.guid)
        expect(parsed_response['pagination']).to eq(expected_pagination)
      end

      it 'returns a 200 and the filtered domains for "==" label selector' do
        get '/v3/domains?label_selector=animal==dog', nil, admin_header

        parsed_response = MultiJson.load(last_response.body)

        expected_pagination = {
          'total_results' => 1,
          'total_pages' => 1,
          'first' => { 'href' => "#{link_prefix}/v3/domains?label_selector=animal%3D%3Ddog&page=1&per_page=50" },
          'last' => { 'href' => "#{link_prefix}/v3/domains?label_selector=animal%3D%3Ddog&page=1&per_page=50" },
          'next' => nil,
          'previous' => nil
        }

        expect(last_response.status).to eq(200)
        expect(parsed_response['resources'].pluck('guid')).to contain_exactly(domain1.guid)
        expect(parsed_response['pagination']).to eq(expected_pagination)
      end

      it 'returns a 200 and the filtered domains for "!=" label selector' do
        get '/v3/domains?label_selector=animal!=dog', nil, admin_header

        parsed_response = MultiJson.load(last_response.body)

        expected_pagination = {
          'total_results' => 1,
          'total_pages' => 1,
          'first' => { 'href' => "#{link_prefix}/v3/domains?label_selector=animal%21%3Ddog&page=1&per_page=50" },
          'last' => { 'href' => "#{link_prefix}/v3/domains?label_selector=animal%21%3Ddog&page=1&per_page=50" },
          'next' => nil,
          'previous' => nil
        }

        expect(last_response.status).to eq(200)
        expect(parsed_response['resources'].pluck('guid')).to contain_exactly(domain2.guid)
        expect(parsed_response['pagination']).to eq(expected_pagination)
      end

      it 'returns a 200 and the filtered domains for "=" label selector' do
        get '/v3/domains?label_selector=animal=cow,santa=claus', nil, admin_header

        parsed_response = MultiJson.load(last_response.body)

        expected_pagination = {
          'total_results' => 1,
          'total_pages' => 1,
          'first' => { 'href' => "#{link_prefix}/v3/domains?label_selector=animal%3Dcow%2Csanta%3Dclaus&page=1&per_page=50" },
          'last' => { 'href' => "#{link_prefix}/v3/domains?label_selector=animal%3Dcow%2Csanta%3Dclaus&page=1&per_page=50" },
          'next' => nil,
          'previous' => nil
        }

        expect(last_response.status).to eq(200)
        expect(parsed_response['resources'].pluck('guid')).to contain_exactly(domain2.guid)
        expect(parsed_response['pagination']).to eq(expected_pagination)
      end

      it 'returns a 200 and the filtered domains for existence label selector' do
        get '/v3/domains?label_selector=santa', nil, admin_header

        parsed_response = MultiJson.load(last_response.body)

        expected_pagination = {
          'total_results' => 1,
          'total_pages' => 1,
          'first' => { 'href' => "#{link_prefix}/v3/domains?label_selector=santa&page=1&per_page=50" },
          'last' => { 'href' => "#{link_prefix}/v3/domains?label_selector=santa&page=1&per_page=50" },
          'next' => nil,
          'previous' => nil
        }

        expect(last_response.status).to eq(200)
        expect(parsed_response['resources'].pluck('guid')).to contain_exactly(domain2.guid)
        expect(parsed_response['pagination']).to eq(expected_pagination)
      end

      it 'returns a 200 and the filtered domains for non-existence label selector' do
        get '/v3/domains?label_selector=!santa', nil, admin_header

        parsed_response = MultiJson.load(last_response.body)

        expected_pagination = {
          'total_results' => 1,
          'total_pages' => 1,
          'first' => { 'href' => "#{link_prefix}/v3/domains?label_selector=%21santa&page=1&per_page=50" },
          'last' => { 'href' => "#{link_prefix}/v3/domains?label_selector=%21santa&page=1&per_page=50" },
          'next' => nil,
          'previous' => nil
        }

        expect(last_response.status).to eq(200)
        expect(parsed_response['resources'].pluck('guid')).to contain_exactly(domain1.guid)
        expect(parsed_response['pagination']).to eq(expected_pagination)
      end
    end

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

  describe 'GET /v3/domains/:domain_guid/route_reservations' do
    let!(:non_visible_org) { VCAP::CloudController::Organization.make(guid: 'non-visible') }
    let!(:non_visible_domain) do
      VCAP::CloudController::PrivateDomain.make(guid: 'non-visible', name: 'non-visible-domain.com', owning_organization: non_visible_org)
    end
    let!(:domain) do
      VCAP::CloudController::PrivateDomain.make(guid: 'visible', name: 'visibledomain.com', owning_organization: org)
    end

    context 'no route matches' do
      let(:api_call) { ->(user_headers) { get "/v3/domains/#{domain.guid}/route_reservations?host=my-host&path=/somepath", nil, user_headers } }

      let(:matching_route_json) do
        {
          matching_route: false
        }
      end

      let(:expected_codes_and_responses) do
        h = Hash.new(
          code: 200,
          response_object: matching_route_json
        )
        h['org_billing_manager'] = {
          code: 404
        }
        h['no_role'] = {
          code: 404
        }
        h
      end

      it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS
    end

    context 'there are route matches' do
      context 'when querying with both host and path' do
        let!(:matching_route) { VCAP::CloudController::Route.make(space: space, domain: domain, host: 'my-host', path: '/somepath') }
        let(:api_call) { ->(user_headers) { get "/v3/domains/#{domain.guid}/route_reservations?host=my-host&path=/somepath", nil, user_headers } }

        let(:matching_route_json) do
          {
            matching_route: true
          }
        end

        let(:expected_codes_and_responses) do
          h = Hash.new(
            code: 200,
            response_object: matching_route_json
          )
          h['org_billing_manager'] = {
            code: 404
          }
          h['no_role'] = {
            code: 404
          }
          h
        end

        it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS
      end

      context 'when querying with only host' do
        let!(:other_route) { VCAP::CloudController::Route.make(space: space, domain: domain, host: 'my-host', path: '/path/to/something') }
        let(:api_call) { ->(user_headers) { get "/v3/domains/#{domain.guid}/route_reservations?host=my-host", nil, user_headers } }

        let(:matching_route_json) do
          {
            matching_route: false
          }
        end

        let(:expected_codes_and_responses) do
          h = Hash.new(
            code: 200,
            response_object: matching_route_json
          )
          h['org_billing_manager'] = {
            code: 404
          }
          h['no_role'] = {
            code: 404
          }
          h
        end

        it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS
      end

      context 'when querying with only port' do
        let(:router_group) { VCAP::CloudController::RoutingApi::RouterGroup.new({ 'type' => 'tcp', 'reservable_ports' => '123' }) }
        let(:domain) { VCAP::CloudController::SharedDomain.make(router_group_guid: 'some-router-group', name: 'my.domain') }
        let(:routing_api_client) { instance_double(VCAP::CloudController::RoutingApi::Client) }

        before do
          TestConfig.override(
            kubernetes: { host_url: nil },
            external_domain: 'api2.vcap.me',
            external_protocol: 'https'
          )
          allow_any_instance_of(CloudController::DependencyLocator).to receive(:routing_api_client).and_return(routing_api_client)
          allow(routing_api_client).to receive_messages(enabled?: true, router_group: router_group)
        end

        let!(:other_route) { VCAP::CloudController::Route.make(host: '', space: space, domain: domain, port: 123) }
        let(:api_call) { ->(user_headers) { get "/v3/domains/#{domain.guid}/route_reservations?port=123", nil, user_headers } }

        let(:matching_route_json) do
          {
            matching_route: true
          }
        end

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

        it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS

        context 'when querying a TCP route without filtering the port' do
          it 'returns no matching routes' do
            get "/v3/domains/#{domain.guid}/route_reservations", nil, admin_headers

            expect(parsed_response).to eq({ 'matching_route' => false })
          end
        end
      end
    end

    context 'when the domain cannot be found' do
      it 'returns a 404 with a helpful error message' do
        get '/v3/domains/nonexistent-domain-guid/route_reservations', nil, admin_header

        expect(last_response.status).to eq(404)
        expect(last_response).to have_error_message('Domain not found')
      end
    end

    context 'when the user does not have read visibility for the domain' do
      let(:user_header) { set_user_with_header_as_role(role: 'org_auditor', org: org) }

      it 'returns a 404 with a helpful error message' do
        get "/v3/domains/#{domain.guid}/route_reservations", nil, user_header
      end
    end
  end

  describe 'POST /v3/domains' do
    let(:params) do
      {
        name: 'my-domain.com',
        metadata: {
          labels: { 'key' => 'value' },
          annotations: { 'key2' => 'value2' }
        }
      }
    end

    context 'when metadata is invalid' do
      let(:user_header) { admin_headers_for(user) }

      it 'returns a 422' do
        post '/v3/domains', {
          metadata: {
            labels: { '': 'invalid' },
            annotations: { "#{'a' * 1001}": 'value2' }
          }
        }.to_json, user_header

        expect(last_response.status).to eq(422)
        expect(parsed_response['errors'][0]['detail']).to match(/label [\w\s]+ error/)
        expect(parsed_response['errors'][0]['detail']).to match(/annotation [\w\s]+ error/)
      end
    end

    describe 'when creating a shared domain' do
      let(:api_call) { ->(user_headers) { post '/v3/domains', domain_params.to_json, user_headers } }

      let(:domain_params) do
        {
          router_group: { guid: 'some-router-guid' }
        }.merge(params)
      end

      let(:domain_json) do
        {
          guid: UUID_REGEX,
          created_at: iso8601,
          updated_at: iso8601,
          name: params[:name],
          internal: false,
          router_group: { guid: 'some-router-guid' },
          supported_protocols: ['http'],
          metadata: {
            labels: { key: 'value' },
            annotations: { key2: 'value2' }
          },
          relationships: {
            organization: {
              data: nil
            },
            shared_organizations: {
              data: []
            }
          },
          links: {
            self: { href: %r{#{Regexp.escape(link_prefix)}/v3/domains/#{UUID_REGEX}} },
            route_reservations: { href: %r{#{Regexp.escape(link_prefix)}/v3/domains/#{UUID_REGEX}/route_reservations} },
            router_group: { href: %r{#{Regexp.escape(link_prefix)}/routing/v1/router_groups/some-router-guid} }
          }
        }
      end

      let(:expected_codes_and_responses) do
        h = Hash.new(
          code: 403
        )
        h['admin'] = {
          code: 201,
          response_object: domain_json
        }
        h
      end

      it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS

      context 'when the Routing API is unavailable' do
        let(:user_header) { admin_headers_for(user) }

        before do
          allow(routing_api_client).to receive(:router_group).and_raise VCAP::CloudController::RoutingApi::RoutingApiUnavailable
        end

        it 'returns a 503 and helpful error message' do
          post '/v3/domains', domain_params.to_json, user_header

          expect(last_response.status).to eq(503)
          expect(parsed_response['errors'][0]['detail']).to eq 'The Routing API is currently unavailable. Please try again later.'
        end
      end

      context 'when the Routing API is disabled' do
        let(:user_header) { admin_headers_for(user) }

        before do
          allow(routing_api_client).to receive(:enabled?).and_return false
        end

        it 'returns a 503 with a helpful message' do
          post '/v3/domains', domain_params.to_json, user_header

          expect(last_response.status).to eq(503)
          expect(parsed_response['errors'][0]['detail']).to eq 'The Routing API is disabled.'
        end
      end

      context 'when UAA is unavailable' do
        let(:user_header) { admin_headers_for(user) }

        before do
          allow(routing_api_client).to receive(:router_group).and_raise VCAP::CloudController::UaaUnavailable
        end

        it 'returns a 503 with a helpful message' do
          post '/v3/domains', domain_params.to_json, user_header

          expect(last_response.status).to eq(503)
          expect(parsed_response['errors'][0]['detail']).to eq 'Communicating with the Routing API failed because UAA is currently unavailable. Please try again later.'
        end
      end
    end

    describe 'when creating a private domain' do
      let(:shared_org1) { VCAP::CloudController::Organization.make(guid: 'shared-org1') }
      let(:shared_org2) { VCAP::CloudController::Organization.make(guid: 'shared-org2') }

      let(:domain_json) do
        {
          guid: UUID_REGEX,
          created_at: iso8601,
          updated_at: iso8601,
          name: params[:name],
          internal: false,
          router_group: nil,
          supported_protocols: ['http'],
          metadata: {
            labels: { key: 'value' },
            annotations: { key2: 'value2' }
          },
          relationships: {
            organization: {
              data: {
                guid: org.guid
              }
            },
            shared_organizations: {
              data: contain_exactly(
                { guid: shared_org1.guid },
                { guid: shared_org2.guid }
              )
            }
          },
          links: {
            self: { href: %r{#{Regexp.escape(link_prefix)}/v3/domains/#{UUID_REGEX}} },
            organization: { href: %r{#{Regexp.escape(link_prefix)}/v3/organizations/#{org.guid}} },
            route_reservations: { href: %r{#{Regexp.escape(link_prefix)}/v3/domains/#{UUID_REGEX}/route_reservations} },
            shared_organizations: { href: %r{#{Regexp.escape(link_prefix)}/v3/domains/#{UUID_REGEX}/relationships/shared_organizations} }
          }
        }
      end

      let(:private_domain_params) do
        {
          name: 'my-domain.com',
          relationships: {
            organization: {
              data: {
                guid: org.guid
              }
            },
            shared_organizations: {
              data: [
                { guid: shared_org1.guid },
                { guid: shared_org2.guid }
              ]

            }
          },
          metadata: {
            labels: { 'key' => 'value' },
            annotations: { 'key2' => 'value2' }
          }
        }
      end

      before do
        shared_org1.add_manager(user)
        shared_org2.add_manager(user)
      end

      describe 'valid private domains' do
        let(:api_call) { ->(user_headers) { post '/v3/domains', private_domain_params.to_json, user_headers } }

        let(:expected_codes_and_responses) do
          h = Hash.new(code: 403, errors: CF_NOT_AUTHORIZED)
          h['admin'] = {
            code: 201,
            response_object: domain_json
          }
          h['org_manager'] = {
            code: 201,
            response_object: domain_json
          }
          h
        end

        it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS

        context 'when organization is suspended' do
          let(:expected_codes_and_responses) do
            h = super()
            h['org_manager'] = { code: 403, errors: CF_ORG_SUSPENDED }
            h
          end

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

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

      describe 'invalid private domains' do
        let(:headers) { set_user_with_header_as_role(user: user, role: 'org_manager', org: org) }

        context 'when the feature flag is disabled' do
          let!(:feature_flag) { VCAP::CloudController::FeatureFlag.make(name: 'private_domain_creation', enabled: false, error_message: 'my name is bob') }

          context 'when the user is not an admin' do
            it 'returns a 403' do
              post '/v3/domains', private_domain_params.to_json, headers

              expect(last_response.status).to eq(403)
              expect(parsed_response['errors'][0]['detail']).to eq('Feature Disabled: my name is bob')
            end
          end

          context 'when the user is an admin' do
            let(:headers) { set_user_with_header_as_role(role: 'admin') }

            it 'allows creation' do
              post '/v3/domains', private_domain_params.to_json, headers

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

        context 'when the org doesnt exist' do
          let(:params) do
            {
              name: 'my-domain.biz',
              relationships: {
                organization: {
                  data: {
                    guid: 'non-existent-guid'
                  }
                }
              }
            }
          end

          it 'returns a 422 and a helpful error message' do
            post '/v3/domains', params.to_json, headers

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

            expect(parsed_response['errors'][0]['detail']).to eq 'Organization with guid \'non-existent-guid\' does not exist or you do not have access to it.'
          end
        end

        context 'when the org has exceeded its private domains quota' do
          it 'returns a 422 and a helpful error message' do
            org.update(quota_definition: VCAP::CloudController::QuotaDefinition.make(total_private_domains: 0))

            post '/v3/domains', private_domain_params.to_json, headers

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

            expect(parsed_response['errors'][0]['detail']).to eq "The number of private domains exceeds the quota for organization \"#{org.name}\""
          end
        end

        context 'when the domain is in the list of reserved private domains' do
          before do
            TestConfig.override(reserved_private_domains: File.join(Paths::FIXTURES, 'config/reserved_private_domains.dat'))
          end

          it 'returns a 422 with a error message about reserved domains' do
            post '/v3/domains', private_domain_params.merge({ name: 'com.ac' }).to_json, headers

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

            expect(parsed_response['errors'][0]['detail']).to eq 'The "com.ac" domain is reserved and cannot be used for org-scoped domains.'
          end
        end

        context 'when one of the shared orgs does not exist' do
          let(:missing_shared_org_relationship) do
            {
              relationships: {
                organization: {
                  data: {
                    guid: org.guid
                  }
                },
                shared_organizations: {
                  data: [
                    { guid: 'doesnt-exist' }
                  ]
                }
              }
            }.merge(params)
          end

          it 'returns a 422 with a helpful error message' do
            post '/v3/domains', missing_shared_org_relationship.to_json, headers

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

            expect(parsed_response['errors'][0]['detail']).to eq "Organization with guid 'doesnt-exist' does not exist, or you do not have access to it."
          end
        end

        context 'when the user does not have proper permissions in one of the shared orgs' do
          let(:shared_org3) { VCAP::CloudController::Organization.make(guid: 'shared-org3') }

          let(:unwriteable_shared_org) do
            {
              relationships: {
                organization: {
                  data: {
                    guid: org.guid
                  }
                },
                shared_organizations: {
                  data: [
                    { guid: shared_org3.guid },
                    { guid: shared_org1.guid }
                  ]
                }
              }
            }.merge(params)
          end

          before do
            shared_org3.add_user(user)
          end

          it 'returns a 422 with a helpful error message' do
            post '/v3/domains', unwriteable_shared_org.to_json, headers

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

            expect(parsed_response['errors'][0]['detail']).to eq "You do not have sufficient permissions for organization '#{shared_org3.name}' to share domain."
          end
        end

        context 'when one of the shared orgs is suspended' do
          let(:shared_org3) do
            VCAP::CloudController::Organization.make(
              guid: 'shared-org3',
              status: VCAP::CloudController::Organization::SUSPENDED
            )
          end

          let(:suspended_shared_org) do
            {
              relationships: {
                organization: {
                  data: {
                    guid: org.guid
                  }
                },
                shared_organizations: {
                  data: [
                    { guid: shared_org3.guid },
                    { guid: shared_org1.guid }
                  ]
                }
              }
            }.merge(params)
          end

          before do
            shared_org3.add_manager(user)
          end

          it 'returns a 422 with a helpful error message' do
            post '/v3/domains', suspended_shared_org.to_json, headers

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

            expect(parsed_response['errors'][0]['detail']).to eq "Organization '#{shared_org3.name}' is suspended."
          end
        end

        context 'when the owning org is listed as a shared org' do
          let(:sharing_to_owning_org_relationship) do
            {
              relationships: {
                organization: {
                  data: {
                    guid: org.guid
                  }
                },
                shared_organizations: {
                  data: [
                    { guid: org.guid }
                  ]
                }
              }
            }.merge(params)
          end

          it 'returns a 422 with a helpful error message' do
            post '/v3/domains', sharing_to_owning_org_relationship.to_json, headers

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

            expect(parsed_response['errors'][0]['detail']).to eq 'Domain cannot be shared with owning organization.'
          end
        end

        context 'when creating without an owning org' do
          let(:sharing_without_owning_org_relationship) do
            {
              relationships: {
                shared_organizations: {
                  data: [
                    { guid: org.guid }
                  ]
                }
              }
            }.merge(params)
          end

          it 'returns a 422 with a helpful error message' do
            post '/v3/domains', sharing_without_owning_org_relationship.to_json, headers

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

            expect(parsed_response['errors'][0]['detail']).to eq 'Relationships cannot contain shared_organizations without an owning organization.'
          end
        end

        describe 'when a router group is provided' do
          let(:params) do
            {
              name: 'my-domain.biz',
              router_group: { guid: 'some-router-guid' },
              relationships: {
                organization: {
                  data: {
                    guid: org.guid
                  }
                }
              }
            }
          end

          it 'returns a 422 and a helpful error message' do
            post '/v3/domains', params.to_json, headers

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

            expect(parsed_response['errors'][0]['detail']).to eq 'Domains scoped to an organization cannot be associated to a router group.'
          end
        end
      end
    end

    describe 'when the user is not logged in' do
      it 'returns 401 for Unauthenticated requests' do
        post '/v3/domains', params.to_json, base_json_headers
        expect(last_response.status).to eq(401)
      end
    end

    context 'when the user does not have the required scopes' do
      let(:user_header) { headers_for(user, scopes: ['cloud_controller.read']) }

      it 'returns a 403' do
        post '/v3/domains', params.to_json, user_header
        expect(last_response.status).to eq(403)
      end
    end

    context 'when the params are invalid' do
      let(:headers) { set_user_with_header_as_role(role: 'admin') }

      context 'creating a sub domain of a domain owned by another organization' do
        let(:organization_to_scope_to) { VCAP::CloudController::Organization.make }
        let(:existing_private_domain) { VCAP::CloudController::PrivateDomain.make }

        let(:params) do
          {
            name: "foo.#{existing_private_domain.name}",
            relationships: {
              organization: {
                data: {
                  guid: organization_to_scope_to.guid
                }
              }
            }
          }
        end

        it 'returns a 422 and an error' do
          post '/v3/domains', params.to_json, headers

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

          expect(parsed_response['errors'][0]['detail']).to eq "The domain name \"#{params[:name]}\" " \
                                                               "cannot be created because \"#{existing_private_domain.name}\" is already reserved by another domain"
        end
      end

      context 'when provided invalid arguments' do
        let(:params) do
          {
            name: "#{'f' * 63}$"
          }
        end

        it 'returns 422' do
          post '/v3/domains', params.to_json, headers

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

          expected_err = [
            'Name does not comply with RFC 1035 standards',
            'Name must contain at least one "."',
            'Name subdomains must each be at most 63 characters',
            'Name must consist of alphanumeric characters and hyphens'
          ]
          expect(parsed_response['errors'][0]['detail']).to eq expected_err.join(', ')
        end
      end

      describe 'collisions' do
        context 'with an existing domain' do
          let!(:existing_domain) { VCAP::CloudController::SharedDomain.make }

          let(:params) do
            {
              name: existing_domain.name
            }
          end

          it 'returns 422' do
            post '/v3/domains', params.to_json, headers

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

            expect(parsed_response['errors'][0]['detail']).to eq "The domain name \"#{existing_domain.name}\" is already in use"
          end
        end

        context 'with an existing route' do
          let(:existing_domain) { VCAP::CloudController::SharedDomain.make }
          let(:existing_route) { VCAP::CloudController::Route.make(domain: existing_domain) }
          let(:domain_name) { existing_route.fqdn }

          let(:params) do
            {
              name: domain_name
            }
          end

          it 'returns 422' do
            post '/v3/domains', params.to_json, headers

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

            expect(parsed_response['errors'][0]['detail']).to match(
              /The domain name "#{domain_name}" cannot be created because "#{existing_route.fqdn}" is already reserved by a route/
            )
          end
        end

        context 'with an existing route as a subdomain' do
          let(:existing_route) { VCAP::CloudController::Route.make }
          let(:domain) { "sub.#{existing_route.fqdn}" }

          let(:params) do
            {
              name: domain
            }
          end

          it 'returns 422' do
            post '/v3/domains', params.to_json, headers

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

            expect(parsed_response['errors'][0]['detail']).to match(
              /The domain name "#{domain}" cannot be created because "#{existing_route.fqdn}" is already reserved by a route/
            )
          end
        end
      end
    end

    describe 'when specifying internal: false with an organization' do
      let(:user_header) { admin_headers_for(user) }
      let(:domain_params) do
        {
          name: 'my-domain.com',
          internal: false,
          relationships: {
            organization: {
              data: {
                guid: org.guid
              }
            }
          }
        }
      end

      it 'succeeds' do
        post '/v3/domains', domain_params.to_json, user_header
        expect(last_response.status).to eq 201
      end
    end

    describe 'when specifying a router group that does not exist' do
      let(:user_header) { admin_headers_for(user) }
      let(:domain_params) do
        {
          name: 'my-domain.com',
          router_group: { guid: 'some-other-router-guid' }
        }
      end

      it 'returns a 422 and a helpful error message' do
        post '/v3/domains', domain_params.to_json, user_header

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

        expect(parsed_response['errors'][0]['detail']).to eq "Router group with guid 'some-other-router-guid' not found."
      end
    end

    describe 'when specifying a router group with internal: true' do
      let(:user_header) { admin_headers_for(user) }
      let(:domain_params) do
        {
          name: 'my-domain.com',
          internal: true,
          router_group: { guid: 'some-router-guid' }
        }
      end

      it 'returns a 422 and a helpful error message' do
        post '/v3/domains', domain_params.to_json, user_header

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

        expect(parsed_response['errors'][0]['detail']).to eq 'Internal domains cannot be associated to a router group.'
      end
    end
  end

  describe 'POST /v3/domains/:guid/relationships/shared_organizations' do
    let(:params) { { data: [] } }
    let(:private_domain) { VCAP::CloudController::PrivateDomain.make(owning_organization: org) }
    let(:user_header) { admin_headers_for(user) }

    describe 'when updating shared orgs for a shared domain' do
      let(:params) { { data: [{ guid: org.guid }] } }
      let(:shared_domain) { VCAP::CloudController::SharedDomain.make }

      it 'returns a 422' do
        post "/v3/domains/#{shared_domain.guid}/relationships/shared_organizations", params.to_json, user_header
        expect(last_response.status).to eq(422)
        expect(parsed_response['errors'][0]['detail']).to eq('Domains cannot be shared with other organizations unless they are scoped to an organization.')
      end
    end

    describe 'when the user is not logged in' do
      it 'returns 401 for Unauthenticated requests' do
        post "/v3/domains/#{private_domain.guid}/relationships/shared_organizations", params.to_json, base_json_headers
        expect(last_response.status).to eq(401)
      end
    end

    context 'when the user does not have the required scopes' do
      let(:user_header) { headers_for(user, scopes: ['cloud_controller.read']) }

      it 'returns a 403' do
        post "/v3/domains/#{private_domain.guid}/relationships/shared_organizations", params.to_json, user_header
        expect(last_response.status).to eq(403)
      end
    end

    context 'when the domain with specified guid does not exist' do
      it 'returns a 404' do
        post '/v3/domains/domain-does-not-exist/relationships/shared_organizations', params.to_json, user_header
        expect(last_response.status).to eq(404)
      end
    end

    context 'when sharing with owning org' do
      let(:params) { { data: [{ guid: private_domain.owning_organization_guid }] } }

      it 'returns a 422' do
        post "/v3/domains/#{private_domain.guid}/relationships/shared_organizations", params.to_json, user_header
        expect(last_response.status).to eq(422)
      end
    end

    context 'when sharing with invalid org' do
      let(:params) { { data: [{ guid: 'not-an-org' }] } }

      it 'returns a 422' do
        post "/v3/domains/#{private_domain.guid}/relationships/shared_organizations", params.to_json, user_header
        expect(last_response.status).to eq(422)
      end
    end

    describe 'when sharing orgs with a private domain' do
      let(:shared_org1) { VCAP::CloudController::Organization.make(guid: 'shared-org1') }

      let(:domain_shared_orgs) do
        {
          data: [{ guid: shared_org1.guid }]
        }
      end

      let(:private_domain_params) do
        {
          data: [{ guid: shared_org1.guid }]
        }
      end

      before do
        shared_org1.add_private_domain(private_domain)
        shared_org1.add_manager(user)
      end

      describe 'valid private domains' do
        let(:api_call) { ->(user_headers) { post "/v3/domains/#{private_domain.guid}/relationships/shared_organizations", private_domain_params.to_json, user_headers } }

        let(:expected_codes_and_responses) do
          h = Hash.new(code: 403, errors: CF_NOT_AUTHORIZED)
          h['admin'] = {
            code: 200,
            response_object: domain_shared_orgs
          }
          h['org_manager'] = {
            code: 200,
            response_object: domain_shared_orgs
          }
          h
        end

        it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS

        context 'when organization is suspended' do
          let(:expected_codes_and_responses) do
            h = super()
            h['org_manager'] = { code: 403, errors: CF_ORG_SUSPENDED }
            h
          end

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

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

    describe 'when the user does not have read permissions for the domain' do
      let(:org1) { VCAP::CloudController::Organization.make(guid: 'org1') }
      let(:org2) { VCAP::CloudController::Organization.make(guid: 'org2') }
      let!(:shared_domain) { VCAP::CloudController::SharedDomain.make }
      let(:unreadable_domain) { VCAP::CloudController::PrivateDomain.make(owning_organization: org1) }

      let(:domain_shared_orgs) do
        {
          data: [{ guid: org2.guid }]
        }
      end

      let(:unreadable_domain_params) do
        {
          data: [{ guid: org2.guid }]
        }
      end

      before do
        org2.add_manager(user)
      end

      let(:api_call) { ->(user_headers) { post "/v3/domains/#{unreadable_domain.guid}/relationships/shared_organizations", unreadable_domain_params.to_json, user_headers } }

      let(:expected_codes_and_responses) do
        h = Hash.new(
          code: 404
        )
        h['admin'] = {
          code: 200,
          response_object: domain_shared_orgs
        }
        h['admin_read_only'] = {
          code: 403
        }
        h['global_auditor'] = {
          code: 403
        }
        h
      end

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

  describe 'DELETE /v3/domains/:guid' do
    describe 'when deleting a shared domain' do
      let(:shared_domain) { VCAP::CloudController::SharedDomain.make }
      let(:api_call) { ->(user_headers) { delete "/v3/domains/#{shared_domain.guid}", nil, user_headers } }
      let(:db_check) do
        lambda do
          expect(last_response.headers['Location']).to match(%r{http.+/v3/jobs/[a-fA-F0-9-]+})

          execute_all_jobs(expected_successes: 1, expected_failures: 0)
          get "/v3/domains/#{shared_domain.guid}", {}, admin_headers
          expect(last_response.status).to eq(404)
        end
      end

      let(:expected_codes_and_responses) do
        h = Hash.new(
          code: 403
        )

        h['admin'] = {
          code: 202
        }

        h
      end

      it_behaves_like 'permissions for delete endpoint', ALL_PERMISSIONS

      context 'deleting metadata' do
        it_behaves_like 'resource with metadata' do
          let(:resource) { shared_domain }
          let(:api_call) do
            -> { delete "/v3/domains/#{resource.guid}", nil, admin_headers }
          end
        end
      end
    end

    describe 'when deleting a private domain' do
      let(:private_domain) { VCAP::CloudController::PrivateDomain.make(owning_organization: org) }
      let(:api_call) { ->(user_headers) { delete "/v3/domains/#{private_domain.guid}", nil, user_headers } }

      let(:db_check) do
        lambda do
          expect(last_response.headers['Location']).to match(%r{http.+/v3/jobs/[a-fA-F0-9-]+})

          execute_all_jobs(expected_successes: 1, expected_failures: 0)
          get "/v3/domains/#{private_domain.guid}", {}, admin_headers
          expect(last_response.status).to eq(404)
        end
      end

      let(:expected_codes_and_responses) do
        h = Hash.new(code: 403, errors: CF_NOT_AUTHORIZED)
        h['admin'] = { code: 202 }
        h['org_manager'] = { code: 202 }
        h['org_billing_manager'] = { code: 404 }
        h['no_role'] = { code: 404 }
        h
      end

      it_behaves_like 'permissions for delete endpoint', ALL_PERMISSIONS

      context 'when organization is suspended' do
        let(:expected_codes_and_responses) do
          h = super()
          h['org_manager'] = { code: 403, errors: CF_ORG_SUSPENDED }
          h
        end

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

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

    describe 'when deleting a shared private domain as an org manager of the shared organization' do
      let(:shared_org1) { VCAP::CloudController::Organization.make(guid: 'shared-org1') }
      let(:private_domain) { VCAP::CloudController::PrivateDomain.make(owning_organization: org) }
      let(:user_header) { headers_for(user) }

      before do
        private_domain.add_shared_organization(shared_org1)
        shared_org1.add_manager(user)
      end

      it 'returns a 403' do
        delete "/v3/domains/#{private_domain.guid}", nil, user_header
        expect(last_response.status).to eq(403)
      end
    end

    describe 'when deleting a shared private domain' do
      let(:shared_org1) { VCAP::CloudController::Organization.make(guid: 'shared-org1') }
      let(:private_domain) { VCAP::CloudController::PrivateDomain.make(owning_organization: org) }
      let(:user_header) { admin_headers_for(user) }

      before do
        private_domain.add_shared_organization(shared_org1)
      end

      it 'returns a 422' do
        delete "/v3/domains/#{private_domain.guid}", nil, user_header
        expect(last_response.status).to eq(422)
        expect(parsed_response['errors'][0]['detail']).to eq(
          'This domain is shared with other organizations. Unshare before deleting.'
        )
      end
    end
  end

  describe 'DELETE /v3/domains/:guid/relationships/shared_organizations/:org_guid' do
    let(:owning_org) { org }
    let(:shared_org1) { VCAP::CloudController::Organization.make(guid: 'shared-org1') }
    let(:private_domain) { VCAP::CloudController::PrivateDomain.make(owning_organization: owning_org) }
    let(:user_header) { admin_headers_for(user) }

    context 'when there are non role related permissions issues' do
      context 'when the user is not logged in' do
        it 'returns 401 for Unauthenticated requests' do
          delete "/v3/domains/#{private_domain.guid}/relationships/shared_organizations/#{shared_org1.guid}", nil, base_json_headers
          expect(last_response.status).to eq(401)
        end
      end

      context 'when the user does not have the required scopes' do
        let(:user_header) { headers_for(user, scopes: ['cloud_controller.read']) }

        it 'returns a 403' do
          delete "/v3/domains/#{private_domain.guid}/relationships/shared_organizations/#{shared_org1.guid}", nil, user_header
          expect(last_response.status).to eq(403)
        end
      end
    end

    context 'when the org is invalid' do
      context 'when unsharing from invalid org' do
        it 'returns a 422' do
          delete "/v3/domains/#{private_domain.guid}/relationships/shared_organizations/invalid_org", nil, user_header
          expect(last_response.status).to eq(422)
          expect(parsed_response['errors'][0]['detail']).to eq("Organization with guid 'invalid_org' does not exist or you do not have access to it.")
        end
      end

      context 'when unsharing from non-shared org' do
        let(:org2) { VCAP::CloudController::Organization.make }

        it 'returns a 422' do
          delete "/v3/domains/#{private_domain.guid}/relationships/shared_organizations/#{org2.guid}", nil, user_header
          expect(last_response.status).to eq(422)
          expect(parsed_response['errors'][0]['detail']).to eq(
            "Unable to unshare domain from organization with name '#{org2.name}'. Ensure the domain is shared to this organization."
          )
        end
      end

      context 'when unsharing from owning org' do
        it 'returns a 422' do
          delete "/v3/domains/#{private_domain.guid}/relationships/shared_organizations/#{private_domain.owning_organization_guid}", nil, user_header
          expect(last_response.status).to eq(422)
          expect(parsed_response['errors'][0]['detail']).to eq(
            "Unable to unshare domain from organization with name '#{org.name}'. Ensure the domain is shared to this organization."
          )
        end
      end
    end

    context 'when the domain is invalid' do
      context 'when the domain with specified guid does not exist' do
        it 'returns a 404' do
          delete "/v3/domains/domain-does-not-exist/relationships/shared_organizations/#{shared_org1.guid}", nil, user_header
          expect(last_response.status).to eq(404)
        end
      end

      context "when domain exists but user doesn't have read permissions for it" do
        let(:user_headers) { set_user_with_header_as_role(role: 'org_billing_manager', org: org) }

        it 'returns a 404' do
          delete "/v3/domains/#{private_domain.guid}/relationships/shared_organizations/#{shared_org1.guid}", nil, user_headers
          expect(last_response.status).to eq(404)
        end
      end

      context 'when unsharing a shared domain' do
        let(:shared_domain) { VCAP::CloudController::SharedDomain.make }

        it 'returns a 422' do
          delete "/v3/domains/#{shared_domain.guid}/relationships/shared_organizations/#{shared_org1.guid}", nil, user_header
          expect(last_response.status).to eq(422)
          expect(parsed_response['errors'][0]['detail']).to eq(
            "Unable to unshare domain from organization with name '#{shared_org1.name}'. Ensure the domain is shared to this organization."
          )
        end
      end
    end

    context 'when the org has routes using the domain' do
      let(:route_space) { VCAP::CloudController::Space.make(organization: shared_org1) }
      let(:route) { VCAP::CloudController::Route.make(domain: private_domain, space: route_space) }

      before do
        private_domain.add_shared_organization(shared_org1)
      end

      it 'returns a 422' do
        delete "/v3/domains/#{private_domain.guid}/relationships/shared_organizations/#{route.space.organization_guid}", nil, user_header
        expect(last_response.status).to eq(422)
        expect(parsed_response['errors'][0]['detail']).to eq('This domain has associated routes in this organization. Delete the routes before unsharing.')
      end
    end

    context 'when user can write to source org but has no permissions in shared org' do
      before do
        org.add_manager(user)
        shared_org1.add_private_domain(private_domain)
      end

      it 'returns a 422' do
        delete "/v3/domains/#{private_domain.guid}/relationships/shared_organizations/#{shared_org1.guid}", nil, headers_for(user)
        expect(last_response.status).to eq(422)
        expect(parsed_response['errors'][0]['detail']).to eq("Organization with guid '#{shared_org1.guid}' does not exist or you do not have access to it.")
      end
    end

    context 'when the owning org is suspended' do
      let(:api_call) { ->(user_headers) { delete "/v3/domains/#{private_domain.guid}/relationships/shared_organizations/#{shared_org1.guid}", nil, user_headers } }

      let(:expected_codes_and_responses) do
        h = Hash.new(code: 403, errors: CF_NOT_AUTHORIZED)
        %w[org_billing_manager no_role].each { |r| h[r] = { code: 404 } }
        h['org_manager'] = { code: 403, errors: CF_ORG_SUSPENDED }
        h['admin'] = { code: 204 }
        h
      end

      before do
        private_domain.add_shared_organization(shared_org1)
        owning_org.update(status: VCAP::CloudController::Organization::SUSPENDED)
      end

      it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS
    end

    context 'when the shared org is suspended' do
      let(:owning_org) { VCAP::CloudController::Organization.make(guid: 'owning-org') }
      let(:shared_org1) { org } # permissions apply to shared org
      let(:api_call) { ->(user_headers) { delete "/v3/domains/#{private_domain.guid}/relationships/shared_organizations/#{shared_org1.guid}", nil, user_headers } }

      let(:expected_codes_and_responses) do
        h = Hash.new(code: 403, errors: CF_NOT_AUTHORIZED)
        %w[org_billing_manager no_role].each { |r| h[r] = { code: 404 } }
        h['org_manager'] = { code: 403, errors: CF_ORG_SUSPENDED }
        h['admin'] = { code: 204 }
        h
      end

      before do
        private_domain.add_shared_organization(shared_org1)
        shared_org1.update(status: VCAP::CloudController::Organization::SUSPENDED)
      end

      it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS
    end

    describe 'when unsharing orgs for a private domain' do
      let(:api_call) { ->(user_headers) { delete "/v3/domains/#{private_domain.guid}/relationships/shared_organizations/#{shared_org1.guid}", nil, user_headers } }
      let(:db_check) do
        lambda do
          domain = VCAP::CloudController::Domain.find(guid: private_domain.guid)
          expect(domain.shared_organizations).not_to include(shared_org1)
        end
      end

      before do
        private_domain.add_shared_organization(shared_org1)
      end

      context 'when the user is an org manager in the shared org' do
        before do
          shared_org1.add_manager(user)
        end

        let(:expected_codes_and_responses) do
          h = Hash.new(
            code: 204
          )

          h['admin_read_only'] = {
            code: 204
          }

          h['global_auditor'] = {
            code: 204
          }

          h
        end

        it_behaves_like 'permissions for delete endpoint', ALL_PERMISSIONS
      end

      context 'when the user is a billing manager is the shared org' do
        before do
          shared_org1.add_billing_manager(user)
        end

        let(:expected_codes_and_responses) do
          h = Hash.new(code: 403, errors: CF_NOT_AUTHORIZED)
          h['admin'] = {
            code: 204
          }
          h['org_manager'] = {
            code: 204
          }
          h['org_billing_manager'] = {
            code: 404
          }
          h['no_role'] = {
            code: 404
          }
          h
        end

        it_behaves_like 'permissions for delete endpoint', ALL_PERMISSIONS
      end
    end
  end

  describe 'GET /v3/domains/:guid' do
    context 'when the domain does not exist' do
      let(:user_header) { headers_for(user) }

      it 'returns not found' do
        get '/v3/domains/does-not-exist', nil, user_header

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

    context 'when getting a shared domain' do
      let(:shared_domain) { VCAP::CloudController::SharedDomain.make }

      let(:shared_domain_json) do
        {
          guid: shared_domain.guid,
          created_at: iso8601,
          updated_at: iso8601,
          name: shared_domain.name,
          internal: false,
          router_group: nil,
          supported_protocols: ['http'],
          metadata: {
            labels: {},
            annotations: {}
          },
          relationships: {
            organization: {
              data: nil
            },
            shared_organizations: {
              data: []
            }
          },
          links: {
            self: { href: "#{link_prefix}/v3/domains/#{shared_domain.guid}" },
            route_reservations: { href: %r{#{Regexp.escape(link_prefix)}/v3/domains/#{shared_domain.guid}/route_reservations} }
          }
        }
      end

      let(:api_call) { ->(user_headers) { get "/v3/domains/#{shared_domain.guid}", nil, user_headers } }

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

      it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS
    end

    context 'when getting a private domain' do
      context 'when the domain has not been shared' do
        let(:private_domain) { VCAP::CloudController::PrivateDomain.make(owning_organization: org) }

        let(:private_domain_json) do
          {
            guid: private_domain.guid,
            created_at: iso8601,
            updated_at: iso8601,
            name: private_domain.name,
            internal: false,
            router_group: nil,
            supported_protocols: ['http'],
            metadata: {
              labels: {},
              annotations: {}
            },
            relationships: {
              organization: {
                data: {
                  guid: org.guid
                }
              },
              shared_organizations: {
                data: []
              }
            },
            links: {
              self: { href: "#{link_prefix}/v3/domains/#{private_domain.guid}" },
              organization: { href: %r{#{Regexp.escape(link_prefix)}/v3/organizations/#{org.guid}} },
              route_reservations: { href: %r{#{Regexp.escape(link_prefix)}/v3/domains/#{private_domain.guid}/route_reservations} },
              shared_organizations: { href: %r{#{Regexp.escape(link_prefix)}/v3/domains/#{private_domain.guid}/relationships/shared_organizations} }
            }
          }
        end

        let(:expected_codes_and_responses) do
          h = Hash.new(
            code: 200,
            response_object: private_domain_json
          )
          h['org_billing_manager'] = {
            code: 404
          }
          h['no_role'] = {
            code: 404
          }
          h
        end

        let(:api_call) { ->(user_headers) { get "/v3/domains/#{private_domain.guid}", nil, user_headers } }

        it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS
      end

      context 'when the domain has been shared with another organization' do
        let!(:non_visible_org) { VCAP::CloudController::Organization.make }
        let!(:user_visible_org) { VCAP::CloudController::Organization.make }

        let(:private_domain) { VCAP::CloudController::PrivateDomain.make(owning_organization: org) }

        before do
          non_visible_org.add_private_domain(private_domain)
          user_visible_org.add_private_domain(private_domain)
        end

        let(:private_domain_json) do
          {
            guid: private_domain.guid,
            created_at: iso8601,
            updated_at: iso8601,
            name: private_domain.name,
            internal: false,
            router_group: nil,
            supported_protocols: ['http'],
            metadata: {
              labels: {},
              annotations: {}
            },
            relationships: {
              organization: {
                data: {
                  guid: org.guid
                }
              },
              shared_organizations: {
                data: match_array(shared_organizations)
              }
            },
            links: {
              self: { href: "#{link_prefix}/v3/domains/#{private_domain.guid}" },
              organization: { href: %r{#{Regexp.escape(link_prefix)}/v3/organizations/#{org.guid}} },
              route_reservations: { href: %r{#{Regexp.escape(link_prefix)}/v3/domains/#{private_domain.guid}/route_reservations} },
              shared_organizations: { href: %r{#{Regexp.escape(link_prefix)}/v3/domains/#{private_domain.guid}/relationships/shared_organizations} }
            }
          }
        end

        let(:api_call) { ->(user_headers) { get "/v3/domains/#{private_domain.guid}", nil, user_headers } }

        context 'when the user can read in the shared organization' do
          let(:shared_organizations) { [{ guid: user_visible_org.guid }] }

          before do
            user_visible_org.add_manager(user)
          end

          let(:expected_codes_and_responses) do
            Hash.new(
              code: 200,
              response_object: private_domain_json
            )
          end

          it_behaves_like 'permissions for single object endpoint', LOCAL_ROLES
        end

        context 'when the user can read globally' do
          let(:shared_organizations) { [{ guid: non_visible_org.guid }, { guid: user_visible_org.guid }] }

          let(:expected_codes_and_responses) do
            Hash.new(
              code: 200,
              response_object: private_domain_json
            )
          end

          it_behaves_like 'permissions for single object endpoint', GLOBAL_SCOPES
        end
      end
    end
  end

  describe 'PATCH /v3/domains/:guid' do
    context 'when the domain does not exist' do
      let(:user_header) { headers_for(user) }

      it 'returns not found' do
        patch '/v3/domains/does-not-exist', nil, user_header

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

    context 'when metadata is invalid' do
      let(:domain) { VCAP::CloudController::SharedDomain.make }
      let(:user_header) { admin_headers_for(user) }

      it 'returns a 422' do
        patch "/v3/domains/#{domain.guid}", {
          metadata: {
            labels: { '': 'invalid' },
            annotations: { "#{'a' * 1001}": 'value2' }
          }
        }.to_json, user_header

        expect(last_response.status).to eq(422)
        expect(parsed_response['errors'][0]['detail']).to match(/label [\w\s]+ error/)
      end
    end

    context 'updating an existing shared domain' do
      let(:domain) { VCAP::CloudController::SharedDomain.make }

      let(:domain_json) do
        {
          guid: domain.guid,
          created_at: iso8601,
          updated_at: iso8601,
          name: domain.name,
          internal: false,
          router_group: nil,
          supported_protocols: ['http'],
          relationships: {
            organization: {
              data: nil
            },
            shared_organizations: {
              data: []
            }
          },
          metadata: {
            labels: { key: 'value' },
            annotations: { key2: 'value2' }
          },
          links: {
            self: { href: "#{link_prefix}/v3/domains/#{domain.guid}" },
            route_reservations: { href: %r{#{Regexp.escape(link_prefix)}/v3/domains/#{domain.guid}/route_reservations} }
          }
        }
      end

      let(:api_call) do
        lambda do |user_headers|
          patch "/v3/domains/#{domain.guid}", {
            metadata: {
              labels: { key: 'value' },
              annotations: { key2: 'value2' }
            }
          }.to_json, user_headers
        end
      end

      let(:expected_codes_and_responses) do
        h = Hash.new(code: 403)

        h['admin'] = { code: 200, response_object: domain_json }

        h
      end

      it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS
    end

    context 'updating an existing private domain' do
      let(:domain) { VCAP::CloudController::PrivateDomain.make(owning_organization: org) }

      let(:domain_json) do
        {
          guid: domain.guid,
          created_at: iso8601,
          updated_at: iso8601,
          name: domain.name,
          internal: false,
          router_group: nil,
          supported_protocols: ['http'],
          relationships: {
            organization: {
              data: { guid: org.guid }
            },
            shared_organizations: {
              data: []
            }
          },
          metadata: {
            labels: { key: 'value' },
            annotations: { key2: 'value2' }
          },
          links: {
            self: { href: "#{link_prefix}/v3/domains/#{domain.guid}" },
            organization: { href: %r{#{Regexp.escape(link_prefix)}/v3/organizations/#{org.guid}} },
            route_reservations: { href: %r{#{Regexp.escape(link_prefix)}/v3/domains/#{domain.guid}/route_reservations} },
            shared_organizations: { href: %r{#{Regexp.escape(link_prefix)}/v3/domains/#{domain.guid}/relationships/shared_organizations} }
          }
        }
      end

      let(:api_call) do
        lambda do |user_headers|
          patch "/v3/domains/#{domain.guid}", {
            metadata: {
              labels: { key: 'value' },
              annotations: { key2: 'value2' }
            }
          }.to_json, user_headers
        end
      end

      let(:expected_codes_and_responses) do
        h = Hash.new(code: 403, errors: CF_NOT_AUTHORIZED)

        h['admin'] = { code: 200, response_object: domain_json }
        h['org_manager'] = { code: 200, response_object: domain_json }
        h['org_billing_manager'] = { code: 404 }
        h['no_role'] = { code: 404 }

        h
      end

      it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS

      context 'when organization is suspended' do
        let(:expected_codes_and_responses) do
          h = super()
          h['org_manager'] = { code: 403, errors: CF_ORG_SUSPENDED }
          h
        end

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

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

    context 'updating an existing, shared private domain' do
      let(:domain) { VCAP::CloudController::PrivateDomain.make }

      let(:domain_json) do
        {
          guid: domain.guid,
          created_at: iso8601,
          updated_at: iso8601,
          name: domain.name,
          internal: false,
          router_group: nil,
          supported_protocols: ['http'],
          relationships: {
            organization: {
              data: { guid: domain.owning_organization_guid }
            },
            shared_organizations: {
              data: [{ guid: org.guid }]
            }
          },
          metadata: {
            labels: { key: 'value' },
            annotations: { key2: 'value2' }
          },
          links: {
            self: { href: "#{link_prefix}/v3/domains/#{domain.guid}" },
            organization: { href: %r{#{Regexp.escape(link_prefix)}/v3/organizations/#{domain.owning_organization_guid}} },
            route_reservations: { href: %r{#{Regexp.escape(link_prefix)}/v3/domains/#{domain.guid}/route_reservations} },
            shared_organizations: { href: %r{#{Regexp.escape(link_prefix)}/v3/domains/#{domain.guid}/relationships/shared_organizations} }
          }
        }
      end

      let(:api_call) do
        lambda do |user_headers|
          patch "/v3/domains/#{domain.guid}", {
            metadata: {
              labels: { key: 'value' },
              annotations: { key2: 'value2' }
            }
          }.to_json, user_headers
        end
      end

      let(:expected_codes_and_responses) do
        h = Hash.new(code: 403)

        h['admin'] = { code: 200, response_object: domain_json }
        h['org_billing_manager'] = { code: 404 }
        h['no_role'] = { code: 404 }

        h
      end

      before do
        domain.add_shared_organization(org)
      end

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