cloudfoundry/cloud_controller_ng

View on GitHub
spec/request/routes_spec.rb

Summary

Maintainability
A
0 mins
Test Coverage
require 'spec_helper'
require 'request_spec_shared_examples'
require 'presenters/v3/space_presenter'
require 'presenters/v3/organization_presenter'

RSpec.describe 'Routes Request' do
  let(:user) { VCAP::CloudController::User.make }
  let(:admin_header) { admin_headers_for(user) }
  let!(:org) { VCAP::CloudController::Organization.make(created_at: 1.hour.ago) }
  let!(:space) { VCAP::CloudController::Space.make(name: 'a-space', created_at: 1.hour.ago, organization: org) }

  let(:space_json_generator) do
    lambda { |s|
      presented_space = VCAP::CloudController::Presenters::V3::SpacePresenter.new(s).to_hash
      presented_space[:created_at] = iso8601
      presented_space[:updated_at] = iso8601
      presented_space
    }
  end

  let(:org_json_generator) do
    lambda { |o|
      presented_space = VCAP::CloudController::Presenters::V3::OrganizationPresenter.new(o).to_hash
      presented_space[:created_at] = iso8601
      presented_space[:updated_at] = iso8601
      presented_space
    }
  end

  before do
    TestConfig.override(kubernetes: {})
  end

  describe 'GET /v3/routes' do
    let(:other_space) { VCAP::CloudController::Space.make(name: 'b-space') }
    let(:app_model) { VCAP::CloudController::AppModel.make(space:) }
    let(:domain) { VCAP::CloudController::PrivateDomain.make(owning_organization: space.organization) }
    let!(:route_in_org) do
      VCAP::CloudController::Route.make(space: space, domain: domain, host: 'host-1', path: '/path1', guid: 'route-in-org-guid')
    end
    let!(:route_in_other_org) do
      VCAP::CloudController::Route.make(space: other_space, host: 'host-2', path: '/path2', guid: 'route-in-other-org-guid')
    end
    let!(:route_in_org_dest_web) { VCAP::CloudController::RouteMappingModel.make(app: app_model, route: route_in_org, process_type: 'web') }
    let!(:route_in_org_dest_worker) { VCAP::CloudController::RouteMappingModel.make(app: app_model, route: route_in_org, process_type: 'worker') }
    let(:api_call) { ->(user_headers) { get '/v3/routes', nil, user_headers } }
    let(:route_in_org_json) do
      {
        guid: route_in_org.guid,
        protocol: route_in_org.domain.protocols[0],
        host: route_in_org.host,
        path: route_in_org.path,
        port: nil,
        url: "#{route_in_org.host}.#{route_in_org.domain.name}#{route_in_org.path}",
        created_at: iso8601,
        updated_at: iso8601,
        destinations: contain_exactly({
                                        guid: route_in_org_dest_web.guid,
                                        app: {
                                          guid: app_model.guid,
                                          process: {
                                            type: route_in_org_dest_web.process_type
                                          }
                                        },
                                        weight: route_in_org_dest_web.weight,
                                        port: route_in_org_dest_web.presented_port,
                                        protocol: 'http1'
                                      }, {
                                        guid: route_in_org_dest_worker.guid,
                                        app: {
                                          guid: app_model.guid,
                                          process: {
                                            type: route_in_org_dest_worker.process_type
                                          }
                                        },
                                        weight: route_in_org_dest_worker.weight,
                                        port: route_in_org_dest_worker.presented_port,
                                        protocol: 'http1'
                                      }),
        relationships: {
          space: {
            data: { guid: route_in_org.space.guid }
          },
          domain: {
            data: { guid: route_in_org.domain.guid }
          }
        },
        metadata: {
          labels: {},
          annotations: {}
        },
        links: {
          self: { href: %r{#{Regexp.escape(link_prefix)}/v3/routes/#{route_in_org.guid}} },
          space: { href: %r{#{Regexp.escape(link_prefix)}/v3/spaces/#{route_in_org.space.guid}} },
          destinations: { href: %r{#{Regexp.escape(link_prefix)}/v3/routes/#{route_in_org.guid}/destinations} },
          domain: { href: %r{#{Regexp.escape(link_prefix)}/v3/domains/#{route_in_org.domain.guid}} }
        }
      }
    end

    let(:route_in_other_org_json) do
      {
        guid: route_in_other_org.guid,
        protocol: route_in_other_org.domain.protocols[0],
        host: route_in_other_org.host,
        path: route_in_other_org.path,
        port: nil,
        url: "#{route_in_other_org.host}.#{route_in_other_org.domain.name}#{route_in_other_org.path}",
        created_at: iso8601,
        updated_at: iso8601,
        destinations: [],
        relationships: {
          space: {
            data: { guid: route_in_other_org.space.guid }
          },
          domain: {
            data: { guid: route_in_other_org.domain.guid }
          }
        },
        metadata: {
          labels: {},
          annotations: {}
        },
        links: {
          self: { href: %r{#{Regexp.escape(link_prefix)}/v3/routes/#{route_in_other_org.guid}} },
          space: { href: %r{#{Regexp.escape(link_prefix)}/v3/spaces/#{route_in_other_org.space.guid}} },
          destinations: { href: %r{#{Regexp.escape(link_prefix)}/v3/routes/#{route_in_other_org.guid}/destinations} },
          domain: { href: %r{#{Regexp.escape(link_prefix)}/v3/domains/#{route_in_other_org.domain.guid}} }
        }
      }
    end

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

    describe 'query list parameters' do
      it_behaves_like 'list query endpoint' do
        let(:request) { 'v3/routes' }
        let(:message) { VCAP::CloudController::RoutesListMessage }
        let(:user_header) { admin_header }

        let(:params) do
          {
            page: '2',
            per_page: '10',
            order_by: 'updated_at',
            space_guids: %w[foo bar],
            service_instance_guids: %w[baz qux],
            organization_guids: %w[foo bar],
            domain_guids: %w[foo bar],
            app_guids: %w[foo bar],
            guids: %w[foo bar],
            paths: %w[foo bar],
            hosts: 'foo',
            ports: 636,
            include: 'domain',
            label_selector: 'foo,bar',
            created_ats: "#{Time.now.utc.iso8601},#{Time.now.utc.iso8601}",
            updated_ats: { gt: Time.now.utc.iso8601 }
          }
        end
      end
    end

    context 'when the user is a member in the routes org' do
      let(:expected_codes_and_responses) do
        h = Hash.new(
          code: 200,
          response_objects: [route_in_org_json]
        )

        h['admin'] = { code: 200, response_objects: [route_in_org_json, route_in_other_org_json] }
        h['admin_read_only'] = { code: 200, response_objects: [route_in_org_json, route_in_other_org_json] }
        h['global_auditor'] = { code: 200, response_objects: [route_in_org_json, route_in_other_org_json] }

        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
    end

    describe 'includes' do
      context 'when including domains' do
        let(:domain1) { VCAP::CloudController::SharedDomain.make(name: 'first-domain.example.com') }
        let(:domain1_json) do
          {
            guid: domain1.guid,
            created_at: iso8601,
            updated_at: iso8601,
            name: domain1.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/#{domain1.guid}" },
              route_reservations: { href: %r{#{Regexp.escape(link_prefix)}/v3/domains/#{domain1.guid}/route_reservations} }
            }
          }
        end

        let(:domain2) { VCAP::CloudController::SharedDomain.make(name: 'second-domain.example.com') }
        let(:domain2_json) do
          {
            guid: domain2.guid,
            created_at: iso8601,
            updated_at: iso8601,
            name: domain2.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/#{domain1.guid}" },
              route_reservations: { href: %r{#{Regexp.escape(link_prefix)}/v3/domains/#{domain1.guid}/route_reservations} }
            }
          }
        end

        let(:domain2) { VCAP::CloudController::SharedDomain.make(name: 'second-domain.example.com') }
        let(:domain2_json) do
          {
            guid: domain2.guid,
            created_at: iso8601,
            updated_at: iso8601,
            name: domain2.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/#{domain2.guid}" },
              route_reservations: { href: %r{#{Regexp.escape(link_prefix)}/v3/domains/#{domain2.guid}/route_reservations} }
            }
          }
        end

        let!(:route1_domain1) do
          VCAP::CloudController::Route.make(space: space, host: 'route1', domain: domain1, path: '/path1', guid: 'route1-guid')
        end
        let(:route1_domain1_json) do
          {
            guid: route1_domain1.guid,
            protocol: route1_domain1.domain.protocols[0],
            created_at: iso8601,
            updated_at: iso8601,
            host: route1_domain1.host,
            path: route1_domain1.path,
            port: nil,
            url: "#{route1_domain1.host}.#{domain1.name}#{route1_domain1.path}",
            destinations: [],
            metadata: {
              labels: {},
              annotations: {}
            },
            relationships: {
              space: {
                data: {
                  guid: space.guid
                }
              },
              domain: {
                data: {
                  guid: domain1.guid
                }
              }
            },
            links: {
              self: { href: "http://api2.vcap.me/v3/routes/#{route1_domain1.guid}" },
              space: { href: "http://api2.vcap.me/v3/spaces/#{space.guid}" },
              destinations: { href: %r{#{Regexp.escape(link_prefix)}/v3/routes/#{route1_domain1.guid}/destinations} },
              domain: { href: "http://api2.vcap.me/v3/domains/#{domain1.guid}" }
            }
          }
        end

        let!(:route_in_org) do
          VCAP::CloudController::Route.make(space: space, domain: domain1, host: 'host-1', path: '/path1', guid: 'route-in-org-guid')
        end
        let!(:route_in_other_org) do
          VCAP::CloudController::Route.make(space: other_space, domain: domain2, host: 'host-2', path: '/path2', guid: 'route-in-other-org-guid')
        end

        it 'includes the unique domains for the routes' do
          get '/v3/routes?include=domain', nil, admin_header
          expect(last_response).to have_status_code(200)
          expect({
                   resources: parsed_response['resources'],
                   included: parsed_response['included']
                 }).to match_json_response({
                                             resources: [route_in_org_json, route_in_other_org_json, route1_domain1_json],
                                             included: { 'domains' => [domain1_json, domain2_json] }
                                           })
        end
      end

      context 'when including spaces and orgs' do
        it 'includes the unique spaces and organizations for the routes' do
          get '/v3/routes?include=space,space.organization', nil, admin_header
          expect(last_response).to have_status_code(200)
          expect({
                   resources: parsed_response['resources'],
                   included: parsed_response['included']
                 }).to match_json_response({
                                             resources: [route_in_org_json, route_in_other_org_json],
                                             included: {
                                               'spaces' => [
                                                 space_json_generator.call(space),
                                                 space_json_generator.call(other_space)
                                               ],
                                               'organizations' => [
                                                 org_json_generator.call(org),
                                                 org_json_generator.call(other_space.organization)
                                               ]
                                             }
                                           })
        end
      end

      context 'when including spaces' do
        it 'eagerly loads spaces to efficiently access space_guid' do
          expect(VCAP::CloudController::IncludeSpaceDecorator).to receive(:decorate) do |_, resources|
            expect(resources).not_to be_empty
            resources.each { |r| expect(r.associations).to include(:space) }
          end

          get '/v3/routes?include=space', nil, admin_header
          expect(last_response).to have_status_code(200)
        end
      end

      context 'when including orgs' do
        it 'eagerly loads spaces to efficiently access space.organization_id' do
          expect(VCAP::CloudController::IncludeOrganizationDecorator).to receive(:decorate) do |_, resources|
            expect(resources).not_to be_empty
            resources.each { |r| expect(r.associations).to include(:space) }
          end

          get '/v3/routes?include=space.organization', nil, admin_header
          expect(last_response).to have_status_code(200)
        end
      end
    end

    describe 'filters' do
      let!(:route_without_host_and_with_path) do
        VCAP::CloudController::Route.make(space: space, host: '', domain: domain, path: '/path1', guid: 'route-without-host')
      end
      let!(:route_without_host_and_with_path2) do
        VCAP::CloudController::Route.make(space: space, host: '', domain: domain, path: '/path2', guid: 'route-without-host2')
      end
      let(:route_without_host_and_with_path_json) do
        {
          guid: 'route-without-host',
          protocol: domain.protocols[0],
          created_at: iso8601,
          updated_at: iso8601,
          destinations: [],
          host: '',
          path: '/path1',
          port: nil,
          url: "#{domain.name}/path1",
          metadata: {
            labels: {},
            annotations: {}
          },
          relationships: {
            space: {
              data: {
                guid: space.guid
              }
            },
            domain: {
              data: {
                guid: domain.guid
              }
            }
          },
          links: {
            self: { href: 'http://api2.vcap.me/v3/routes/route-without-host' },
            space: { href: "http://api2.vcap.me/v3/spaces/#{space.guid}" },
            destinations: { href: %r{#{Regexp.escape(link_prefix)}/v3/routes/route-without-host/destinations} },
            domain: { href: "http://api2.vcap.me/v3/domains/#{domain.guid}" }
          }
        }
      end
      let(:route_without_host_and_with_path2_json) do
        {
          guid: 'route-without-host2',
          protocol: domain.protocols[0],
          created_at: iso8601,
          updated_at: iso8601,
          destinations: [],
          host: '',
          path: '/path2',
          port: nil,
          url: "#{domain.name}/path2",
          metadata: {
            labels: {},
            annotations: {}
          },
          relationships: {
            space: {
              data: {
                guid: space.guid
              }
            },
            domain: {
              data: {
                guid: domain.guid
              }
            }
          },
          links: {
            self: { href: 'http://api2.vcap.me/v3/routes/route-without-host2' },
            space: { href: "http://api2.vcap.me/v3/spaces/#{space.guid}" },
            destinations: { href: %r{#{Regexp.escape(link_prefix)}/v3/routes/route-without-host2/destinations} },
            domain: { href: "http://api2.vcap.me/v3/domains/#{domain.guid}" }
          }
        }
      end
      let!(:route_without_path_and_with_host) do
        VCAP::CloudController::Route.make(space: space, host: 'host-1', domain: domain, path: '', guid: 'route-without-path')
      end
      let(:route_without_path_and_with_host_json) do
        {
          guid: 'route-without-path',
          protocol: domain.protocols[0],
          created_at: iso8601,
          updated_at: iso8601,
          destinations: [],
          host: 'host-1',
          path: '',
          port: nil,
          url: "host-1.#{domain.name}",
          metadata: {
            labels: {},
            annotations: {}
          },
          relationships: {
            space: {
              data: {
                guid: space.guid
              }
            },
            domain: {
              data: {
                guid: domain.guid
              }
            }
          },
          links: {
            self: { href: 'http://api2.vcap.me/v3/routes/route-without-path' },
            space: { href: "http://api2.vcap.me/v3/spaces/#{space.guid}" },
            destinations: { href: %r{#{Regexp.escape(link_prefix)}/v3/routes/route-without-path/destinations} },
            domain: { href: "http://api2.vcap.me/v3/domains/#{domain.guid}" }
          }
        }
      end

      context 'hosts filter' do
        it 'returns routes filtered by host' do
          get '/v3/routes?hosts=host-1', nil, admin_header
          expect(last_response).to have_status_code(200)
          expect({
                   resources: parsed_response['resources']
                 }).to match_json_response({
                                             resources: [route_in_org_json, route_without_path_and_with_host_json]
                                           })
        end

        it 'returns route with no host if one exists when filtering by empty host' do
          get '/v3/routes?hosts=', nil, admin_header
          expect(last_response).to have_status_code(200)
          expect({
                   resources: parsed_response['resources']
                 }).to match_json_response({
                                             resources: [route_without_host_and_with_path_json, route_without_host_and_with_path2_json]
                                           })
        end
      end

      context 'paths filter' do
        it 'returns routes filtered by path' do
          get '/v3/routes?paths=%2Fpath1', nil, admin_header
          expect(last_response).to have_status_code(200)
          expect({
                   resources: parsed_response['resources']
                 }).to match_json_response({
                                             resources: [route_in_org_json, route_without_host_and_with_path_json]
                                           })
        end

        it 'returns route with no path when filtering by empty path' do
          get '/v3/routes?paths=', nil, admin_header
          expect(last_response).to have_status_code(200)
          expect({
                   resources: parsed_response['resources']
                 }).to match_json_response({
                                             resources: [route_without_path_and_with_host_json]
                                           })
        end
      end

      context 'hosts and paths filter' do
        it 'returns routes with no host and the provided path when host is empty' do
          get '/v3/routes?paths=%2Fpath1&hosts=', nil, admin_header
          expect(last_response).to have_status_code(200)
          expect({
                   resources: parsed_response['resources']
                 }).to match_json_response({
                                             resources: [route_without_host_and_with_path_json]
                                           })
        end
      end

      context 'organization_guids filter' do
        it 'returns routes filtered by organization_guid' do
          get "/v3/routes?organization_guids=#{other_space.organization.guid}", nil, admin_header
          expect(last_response).to have_status_code(200)
          expect({
                   resources: parsed_response['resources']
                 }).to match_json_response({
                                             resources: [route_in_other_org_json]
                                           })
        end
      end

      context 'space_guids filter' do
        it 'returns routes filtered by space_guid' do
          get "/v3/routes?space_guids=#{other_space.guid}", nil, admin_header
          expect(last_response).to have_status_code(200)
          expect({
                   resources: parsed_response['resources']
                 }).to match_json_response({
                                             resources: [route_in_other_org_json]
                                           })
        end
      end

      context 'domain_guids filter' do
        it 'returns routes filtered by domain_guid' do
          get "/v3/routes?domain_guids=#{route_in_other_org.domain.guid}", nil, admin_header
          expect(last_response).to have_status_code(200)
          expect({
                   resources: parsed_response['resources']
                 }).to match_json_response({
                                             resources: [route_in_other_org_json]
                                           })
        end
      end

      context 'app_guids filter' do
        it 'returns routes filtered by app_guid' do
          get "/v3/routes?app_guids=#{app_model.guid}", nil, admin_header
          expect(last_response).to have_status_code(200)
          expect(parsed_response['resources'].size).to eq(1)
          expect(parsed_response['resources'].first['destinations'].size).to eq(2)
          expect(
            parsed_response['resources'].first['destinations'].map { |destination| destination['app']['guid'] }.uniq
          ).to eq([app_model.guid])
        end
      end

      context 'ports filter' do
        # Don't even think of converting the following hash to symbols ('type' => 'tcp' NOT type: 'tcp'), and you need to set the GUID
        let(:router_group) { VCAP::CloudController::RoutingApi::RouterGroup.new({ 'type' => 'tcp', 'reservable_ports' => '7777,8888,9999', 'guid' => 'some-guid' }) }
        let(:routing_api_client) { instance_double(VCAP::CloudController::RoutingApi::Client) }

        before do
          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

        context 'when there are multiple TCP routes with different ports' do
          # The following `let`s depend on the above `before do`
          let(:domain_tcp) { VCAP::CloudController::SharedDomain.make(router_group_guid: router_group.guid, name: 'my.domain') }
          let!(:route_with_ports_0) do
            VCAP::CloudController::Route.make(host: '', space: space, domain: domain_tcp, guid: 'route-with-port-0', port: 7777)
          end
          let!(:route_with_ports_1) do
            VCAP::CloudController::Route.make(host: '', space: space, domain: domain_tcp, guid: 'route-with-port-1', port: 8888)
          end
          let!(:route_with_ports_2) do
            VCAP::CloudController::Route.make(host: '', space: space, domain: domain_tcp, guid: 'route-with-port-2', port: 9999)
          end

          it 'returns routes filtered by ports' do
            get '/v3/routes?ports=7777,8888', nil, admin_header
            expect(last_response).to have_status_code(200)
            expect(parsed_response['resources'].size).to eq(2)
            expect(parsed_response['resources'].pluck('port')).to contain_exactly(route_with_ports_0.port, route_with_ports_1.port)
          end
        end
      end

      context 'service instance guids filter' do
        let(:service_instance_one) do
          VCAP::CloudController::ManagedServiceInstance.make(:routing, space: space, name: 'si-name-1')
        end
        let(:service_instance_two) do
          VCAP::CloudController::ManagedServiceInstance.make(:routing, space: space, name: 'si-name-2')
        end

        let!(:route_with_service_instance_one) do
          VCAP::CloudController::Route.make(space: space, host: 'host-with-service-instance-one', domain: domain, path: '/path1', guid: 'route-with-service-instance-one')
        end
        let!(:route_with_service_instance_two) do
          VCAP::CloudController::Route.make(space: space, host: 'host-with-service-instance-two', domain: domain, path: '/path2', guid: 'route-with-service-instance-two')
        end

        let!(:route_mapping_one) { VCAP::CloudController::RouteBinding.make(route: route_with_service_instance_one, service_instance: service_instance_one) }
        let!(:route_mapping_two) { VCAP::CloudController::RouteBinding.make(route: route_with_service_instance_two, service_instance: service_instance_two) }

        it 'returns routes filtered by service instance guid' do
          get "/v3/routes?service_instance_guids=#{service_instance_one.guid},#{service_instance_two.guid}", nil, admin_header
          expect(last_response).to have_status_code(200)
          expect(parsed_response['resources'].size).to eq(2)
          expect(parsed_response['resources'].pluck('guid')).to contain_exactly('route-with-service-instance-one', 'route-with-service-instance-two')
        end
      end
    end

    describe 'labels' do
      let!(:domain1) { VCAP::CloudController::PrivateDomain.make(name: 'dom1.com', owning_organization: org) }
      let!(:route1) { VCAP::CloudController::Route.make(space: space, domain: domain1, host: 'hall', path: '/oates', guid: 'guid-1') }
      let!(:route1_label) { VCAP::CloudController::RouteLabelModel.make(resource_guid: route1.guid, key_name: 'animal', value: 'dog') }

      let!(:domain2) { VCAP::CloudController::PrivateDomain.make(name: 'dom2.com', owning_organization: org) }
      let!(:route2) { VCAP::CloudController::Route.make(space: space, domain: domain2, guid: 'guid-2') }
      let!(:route2_label) { VCAP::CloudController::RouteLabelModel.make(resource_guid: route2.guid, key_name: 'animal', value: 'cow') }
      let!(:route2__exclusive_label) { VCAP::CloudController::RouteLabelModel.make(resource_guid: route2.guid, key_name: 'santa', value: 'claus') }

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

          expect(last_response).to have_status_code(200), last_response.body
          parsed_response = Oj.load(last_response.body)

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

          expect(parsed_response['resources'].pluck('guid')).to contain_exactly(route1.guid)
          expect(parsed_response['pagination']).to eq(expected_pagination)
        end

        it 'returns a 200 and the filtered routes for "in" label selector with space guids' do
          get "/v3/routes?label_selector=animal in (dog)&space_guids=#{space.guid}", nil, admin_header

          expect(last_response).to have_status_code(200)
          parsed_response = Oj.load(last_response.body)

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

          expect(parsed_response['resources'].pluck('guid')).to contain_exactly(route1.guid)
          expect(parsed_response['pagination']).to eq(expected_pagination)
        end

        it 'returns a 200 and the filtered routes for "in" label selector with org filters' do
          get "/v3/routes?label_selector=animal in (dog)&organization_guids=#{org.guid}", nil, admin_header

          expect(last_response).to have_status_code(200), last_response.body
          parsed_response = Oj.load(last_response.body)

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

          expect(parsed_response['resources'].pluck('guid')).to contain_exactly(route1.guid)
          expect(parsed_response['pagination']).to eq(expected_pagination)
        end

        it 'returns a 200 and the filtered routes for "in" label selector with domain filters' do
          get "/v3/routes?label_selector=animal in (dog)&domain_guids=#{domain1.guid}", nil, admin_header

          expect(last_response).to have_status_code(200), last_response.body
          parsed_response = Oj.load(last_response.body)

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

          expect(parsed_response['resources'].pluck('guid')).to contain_exactly(route1.guid)
          expect(parsed_response['pagination']).to eq(expected_pagination)
        end

        it 'returns a 200 and the filtered routes for "in" label selector with host filters' do
          get '/v3/routes?label_selector=animal in (dog)&hosts=hall', nil, admin_header

          expect(last_response).to have_status_code(200), last_response.body
          parsed_response = Oj.load(last_response.body)

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

          expect(parsed_response['resources'].pluck('guid')).to contain_exactly(route1.guid)
          expect(parsed_response['pagination']).to eq(expected_pagination)
        end

        it 'returns a 200 and the filtered routes for "in" label selector with path filters' do
          get '/v3/routes?label_selector=animal in (dog)&paths=/oates', nil, admin_header

          expect(last_response).to have_status_code(200)
          parsed_response = Oj.load(last_response.body)

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

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

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

        parsed_response = Oj.load(last_response.body)

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

        expect(last_response).to have_status_code(200)
        expect(parsed_response['resources'].pluck('guid')).to contain_exactly(route2.guid, route_in_org.guid, route_in_other_org.guid)
        expect(parsed_response['pagination']).to eq(expected_pagination)
      end

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

        parsed_response = Oj.load(last_response.body)

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

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

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

        parsed_response = Oj.load(last_response.body)

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

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

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

        parsed_response = Oj.load(last_response.body)

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

        expect(last_response).to have_status_code(200)
        expect(parsed_response['resources'].pluck('guid')).to contain_exactly(route2.guid, route_in_org.guid, route_in_other_org.guid)
        expect(parsed_response['pagination']).to eq(expected_pagination)
      end

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

        parsed_response = Oj.load(last_response.body)

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

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

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

        parsed_response = Oj.load(last_response.body)

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

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

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

        parsed_response = Oj.load(last_response.body)

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

        expect(last_response).to have_status_code(200)
        expect(parsed_response['resources'].pluck('guid')).to contain_exactly(route1.guid, route_in_org.guid, route_in_other_org.guid)
        expect(parsed_response['pagination']).to eq(expected_pagination)
      end
    end

    describe 'eager loading' do
      it 'eager loads associated resources that the presenter specifies' do
        expect(VCAP::CloudController::RouteFetcher).to receive(:fetch).with(
          anything,
          hash_including(eager_loaded_associations: %i[domain space route_mappings labels annotations])
        ).and_call_original

        get '/v3/routes', nil, admin_header
        expect(last_response).to have_status_code(200)
      end
    end

    context 'when the request is invalid' do
      it 'returns 400 with a meaningful error' do
        get '/v3/routes?page=potato', nil, admin_header
        expect(last_response).to have_status_code(400)
        expect(last_response).to have_error_message('The query parameter is invalid: Page must be a positive integer')
      end
    end

    context 'when the user is not logged in' do
      it 'returns 401 for Unauthenticated requests' do
        get '/v3/routes', nil, base_json_headers
        expect(last_response).to have_status_code(401)
      end
    end
  end

  describe 'GET /v3/routes/:guid' do
    let(:domain) { VCAP::CloudController::PrivateDomain.make(owning_organization: space.organization) }
    let(:route) { VCAP::CloudController::Route.make(space:, domain:) }
    let(:api_call) { ->(user_headers) { get "/v3/routes/#{route.guid}", nil, user_headers } }
    let(:route_json) do
      {
        guid: route.guid,
        protocol: route.domain.protocols[0],
        host: route.host,
        path: route.path,
        port: nil,
        url: "#{route.host}.#{route.domain.name}#{route.path}",
        created_at: iso8601,
        updated_at: iso8601,
        destinations: [],
        relationships: {
          space: {
            data: { guid: route.space.guid }
          },
          domain: {
            data: { guid: route.domain.guid }
          }
        },
        metadata: {
          labels: {},
          annotations: {}
        },
        links: {
          self: { href: %r{#{Regexp.escape(link_prefix)}/v3/routes/#{UUID_REGEX}} },
          space: { href: %r{#{Regexp.escape(link_prefix)}/v3/spaces/#{route.space.guid}} },
          destinations: { href: %r{#{Regexp.escape(link_prefix)}/v3/routes/#{UUID_REGEX}/destinations} },
          domain: { href: %r{#{Regexp.escape(link_prefix)}/v3/domains/#{route.domain.guid}} }
        }
      }
    end

    context 'when the user is a member in the routes org' do
      let(:expected_codes_and_responses) do
        h = Hash.new(
          code: 200,
          response_object: 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

    describe 'when the user is not logged in' do
      it 'returns 401 for Unauthenticated requests' do
        get "/v3/routes/#{route.guid}", nil, base_json_headers
        expect(last_response).to have_status_code(401)
      end
    end

    describe 'includes' do
      context 'when including domains' do
        let(:domain_json) do
          {
            guid: domain.guid,
            created_at: iso8601,
            updated_at: iso8601,
            name: domain.name,
            internal: false,
            router_group: nil,
            supported_protocols: ['http'],
            metadata: {
              labels: {},
              annotations: {}
            },
            relationships: {
              organization: {
                data: { guid: domain.owning_organization.guid }
              },
              shared_organizations: {
                data: []
              }
            },
            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(:route_json) do
          {
            guid: route.guid,
            protocol: route.domain.protocols[0],
            host: route.host,
            path: route.path,
            port: nil,
            url: "#{route.host}.#{route.domain.name}#{route.path}",
            created_at: iso8601,
            updated_at: iso8601,
            destinations: [],
            relationships: {
              space: {
                data: { guid: route.space.guid }
              },
              domain: {
                data: { guid: route.domain.guid }
              }
            },
            metadata: {
              labels: {},
              annotations: {}
            },
            links: {
              self: { href: %r{#{Regexp.escape(link_prefix)}/v3/routes/#{UUID_REGEX}} },
              space: { href: %r{#{Regexp.escape(link_prefix)}/v3/spaces/#{route.space.guid}} },
              destinations: { href: %r{#{Regexp.escape(link_prefix)}/v3/routes/#{UUID_REGEX}/destinations} },
              domain: { href: %r{#{Regexp.escape(link_prefix)}/v3/domains/#{route.domain.guid}} }
            },
            included: { domains: [domain_json] }
          }
        end

        it 'includes the domain for the route' do
          get "/v3/routes/#{route.guid}?include=domain", nil, admin_header
          expect(last_response).to have_status_code(200), last_response.body
          expect(parsed_response).to match_json_response(route_json)
        end
      end

      context 'when including spaces and orgs' do
        it 'includes the unique spaces and organizations for the routes' do
          get "/v3/routes/#{route.guid}?include=space,space.organization", nil, admin_header
          expect(last_response).to have_status_code(200)
          expect(parsed_response['included']).to match_json_response(
            'spaces' => [
              space_json_generator.call(space)
            ],
            'organizations' => [
              org_json_generator.call(org)
            ]
          )
        end

        context 'user is org_auditor' do
          let(:user_header) { set_user_with_header_as_role(user: user, role: 'org_auditor', org: org) }

          it 'includes the unique organizations for the routes, but no spaces' do
            get "/v3/routes/#{route.guid}?include=space,space.organization", nil, user_header
            expect(last_response).to have_status_code(200)
            expect(parsed_response['included']).to match_json_response(
              'spaces' => [],
              'organizations' => [
                org_json_generator.call(org)
              ]
            )
          end
        end
      end
    end
  end

  describe 'POST /v3/routes' do
    context 'when creating a route in a tcp domain' do
      let(:domain) { VCAP::CloudController::SharedDomain.make(router_group_guid: 'some-router-group', router_group_type: 'tcp') }

      before do
        token = { token_type: 'Bearer', access_token: 'my-favourite-access-token' }
        stub_request(:post, 'https://uaa.service.cf.internal/oauth/token').
          to_return(status: 200, body: token.to_json, headers: { 'Content-Type' => 'application/json' })
        stub_request(:get, 'http://localhost:3000/routing/v1/router_groups').
          to_return(status: 200, body: '[{"guid":"some-router-group","name":"Robby Router","type":"tcp","reservable_ports":"25555"}]', headers: {})
      end

      context 'and the route has a host' do
        let(:params) do
          {
            host: 'my-host',
            relationships: {
              space: {
                data: { guid: space.guid }
              },
              domain: {
                data: { guid: domain.guid }
              }
            }
          }
        end

        it 'returns a 422 and a helpful error message' do
          post '/v3/routes', params.to_json, admin_header
          expect(last_response).to have_status_code(422)
          expect(last_response).to have_error_message('Hosts are not supported for TCP routes.')
        end
      end

      context 'and the route has a path' do
        let(:params) do
          {
            path: '/cgi-bin',
            relationships: {
              space: {
                data: { guid: space.guid }
              },
              domain: {
                data: { guid: domain.guid }
              }
            }
          }
        end

        it 'returns a 422 and a helpful error message' do
          post '/v3/routes', params.to_json, admin_header
          expect(last_response).to have_status_code(422)
          expect(last_response).to have_error_message('Paths are not supported for TCP routes.')
        end
      end
    end

    context 'when creating a route in a scoped domain' do
      let(:domain) { VCAP::CloudController::PrivateDomain.make(owning_organization: space.organization) }

      describe 'when creating a route without a host' do
        let(:params) do
          {
            relationships: {
              space: {
                data: { guid: space.guid }
              },
              domain: {
                data: { guid: domain.guid }
              }
            }
          }
        end

        let(:route_json) do
          {
            guid: UUID_REGEX,
            protocol: domain.protocols[0],
            host: '',
            path: '',
            port: nil,
            url: domain.name,
            created_at: iso8601,
            updated_at: iso8601,
            destinations: [],
            relationships: {
              space: {
                data: { guid: space.guid }
              },
              domain: {
                data: { guid: domain.guid }
              }
            },
            links: {
              self: { href: %r{#{Regexp.escape(link_prefix)}/v3/routes/#{UUID_REGEX}} },
              space: { href: %r{#{Regexp.escape(link_prefix)}/v3/spaces/#{space.guid}} },
              destinations: { href: %r{#{Regexp.escape(link_prefix)}/v3/routes/#{UUID_REGEX}/destinations} },
              domain: { href: %r{#{Regexp.escape(link_prefix)}/v3/domains/#{domain.guid}} }
            },
            metadata: {
              labels: {},
              annotations: {}
            }
          }
        end

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

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

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

      describe 'when creating a route with a host' do
        let(:params) do
          {
            host: 'some-host',
            path: '/some-path',
            relationships: {
              space: {
                data: { guid: space.guid }
              },
              domain: {
                data: { guid: domain.guid }
              }
            },
            metadata: {
              labels: { potato: 'yam' },
              annotations: { style: 'mashed' }
            }
          }
        end

        let(:route_json) do
          {
            guid: UUID_REGEX,
            protocol: domain.protocols[0],
            host: 'some-host',
            path: '/some-path',
            port: nil,
            url: "some-host.#{domain.name}/some-path",
            created_at: iso8601,
            updated_at: iso8601,
            destinations: [],
            relationships: {
              space: {
                data: { guid: space.guid }
              },
              domain: {
                data: { guid: domain.guid }
              }
            },
            links: {
              self: { href: %r{#{Regexp.escape(link_prefix)}/v3/routes/#{UUID_REGEX}} },
              space: { href: %r{#{Regexp.escape(link_prefix)}/v3/spaces/#{space.guid}} },
              destinations: { href: %r{#{Regexp.escape(link_prefix)}/v3/routes/#{UUID_REGEX}/destinations} },
              domain: { href: %r{#{Regexp.escape(link_prefix)}/v3/domains/#{domain.guid}} }
            },
            metadata: {
              labels: { potato: 'yam' },
              annotations: { style: 'mashed' }
            }
          }
        end

        describe 'valid routes' do
          it_behaves_like 'permissions for single object endpoint', ['admin'] do
            let(:api_call) { ->(user_headers) { post '/v3/routes', params.to_json, user_headers } }

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

            let(:expected_event_hash) do
              {
                type: 'audit.route.create',
                actee: parsed_response['guid'],
                actee_type: 'route',
                actee_name: 'some-host',
                metadata: { request: params }.to_json,
                space_guid: space.guid,
                organization_guid: org.guid
              }
            end
          end
        end
      end

      describe 'when creating a route with a wildcard host' do
        let(:params) do
          {
            host: '*',
            relationships: {
              space: {
                data: { guid: space.guid }
              },
              domain: {
                data: { guid: domain.guid }
              }
            }
          }
        end

        let(:route_json) do
          {
            guid: UUID_REGEX,
            protocol: domain.protocols[0],
            host: '*',
            path: '',
            port: nil,
            url: "*.#{domain.name}",
            created_at: iso8601,
            updated_at: iso8601,
            destinations: [],
            relationships: {
              space: {
                data: { guid: space.guid }
              },
              domain: {
                data: { guid: domain.guid }
              }
            },
            links: {
              self: { href: %r{#{Regexp.escape(link_prefix)}/v3/routes/#{UUID_REGEX}} },
              space: { href: %r{#{Regexp.escape(link_prefix)}/v3/spaces/#{space.guid}} },
              destinations: { href: %r{#{Regexp.escape(link_prefix)}/v3/routes/#{UUID_REGEX}/destinations} },
              domain: { href: %r{#{Regexp.escape(link_prefix)}/v3/domains/#{domain.guid}} }
            },
            metadata: {
              labels: {},
              annotations: {}
            }
          }
        end

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

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

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

    context 'when creating a route in an unscoped domain' do
      let(:domain) { VCAP::CloudController::SharedDomain.make }

      describe 'when creating a route without a host' do
        let(:params) do
          {
            relationships: {
              space: {
                data: { guid: space.guid }
              },
              domain: {
                data: { guid: domain.guid }
              }
            }
          }
        end

        it 'fails with a helpful message' do
          post '/v3/routes', params.to_json, admin_header
          expect(last_response).to have_status_code(422)
          expect(last_response).to have_error_message('Missing host. Routes in shared domains must have a host defined.')
        end
      end

      describe 'when creating a route with a host' do
        let(:params) do
          {
            host: 'some-host',
            relationships: {
              space: {
                data: { guid: space.guid }
              },
              domain: {
                data: { guid: domain.guid }
              }
            }
          }
        end

        let(:route_json) do
          {
            guid: UUID_REGEX,
            protocol: domain.protocols[0],
            host: 'some-host',
            path: '',
            port: nil,
            url: "some-host.#{domain.name}",
            created_at: iso8601,
            updated_at: iso8601,
            destinations: [],
            relationships: {
              space: {
                data: { guid: space.guid }
              },
              domain: {
                data: { guid: domain.guid }
              }
            },
            links: {
              self: { href: %r{#{Regexp.escape(link_prefix)}/v3/routes/#{UUID_REGEX}} },
              space: { href: %r{#{Regexp.escape(link_prefix)}/v3/spaces/#{space.guid}} },
              destinations: { href: %r{#{Regexp.escape(link_prefix)}/v3/routes/#{UUID_REGEX}/destinations} },
              domain: { href: %r{#{Regexp.escape(link_prefix)}/v3/domains/#{domain.guid}} }
            },
            metadata: {
              labels: {},
              annotations: {}
            }
          }
        end

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

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

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

      describe 'when creating a route with a wildcard host' do
        let(:params) do
          {
            host: '*',
            relationships: {
              space: {
                data: { guid: space.guid }
              },
              domain: {
                data: { guid: domain.guid }
              }
            }
          }
        end

        let(:route_json) do
          {
            guid: UUID_REGEX,
            protocol: domain.protocols[0],
            host: '*',
            path: '',
            port: nil,
            url: "*.#{domain.name}",
            created_at: iso8601,
            updated_at: iso8601,
            destinations: [],
            relationships: {
              space: {
                data: { guid: space.guid }
              },
              domain: {
                data: { guid: domain.guid }
              }
            },
            links: {
              self: { href: %r{#{Regexp.escape(link_prefix)}/v3/routes/#{UUID_REGEX}} },
              space: { href: %r{#{Regexp.escape(link_prefix)}/v3/spaces/#{space.guid}} },
              destinations: { href: %r{#{Regexp.escape(link_prefix)}/v3/routes/#{UUID_REGEX}/destinations} },
              domain: { href: %r{#{Regexp.escape(link_prefix)}/v3/domains/#{domain.guid}} }
            },
            metadata: {
              labels: {},
              annotations: {}
            }
          }
        end

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

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

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

      describe 'the domain supports tcp routes' 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(:params) do
          {
            port: 123,
            relationships: {
              space: {
                data: { guid: space.guid }
              },
              domain: {
                data: { guid: domain.guid }
              }
            }
          }
        end

        let(:api_call) { ->(user_headers) { post '/v3/routes', params.to_json, user_headers } }

        let(:route_json) do
          {
            guid: UUID_REGEX,
            port: 123,
            host: '',
            path: '',
            protocol: 'tcp',
            url: "#{domain.name}:123",
            created_at: iso8601,
            updated_at: iso8601,
            destinations: [],
            relationships: {
              space: {
                data: { guid: space.guid }
              },
              domain: {
                data: { guid: domain.guid }
              }
            },
            links: {
              self: { href: %r{#{Regexp.escape(link_prefix)}/v3/routes/#{UUID_REGEX}} },
              space: { href: %r{#{Regexp.escape(link_prefix)}/v3/spaces/#{space.guid}} },
              destinations: { href: %r{#{Regexp.escape(link_prefix)}/v3/routes/#{UUID_REGEX}/destinations} },
              domain: { href: %r{#{Regexp.escape(link_prefix)}/v3/domains/#{domain.guid}} }
            },
            metadata: {
              labels: {},
              annotations: {}
            }
          }
        end

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

        context 'and the user provides a valid port' do
          it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS

          context 'and a route with the domain and port already exist' do
            let!(:duplicate_route) { VCAP::CloudController::Route.make(host: '', space: space, domain: domain, port: 123) }

            it 'fails with a helpful error message' do
              post '/v3/routes', params.to_json, admin_headers
              expect(last_response).to have_status_code(422)
              expect(last_response).to have_error_message("Route already exists with port '123' for domain 'my.domain'.")
            end
          end

          context 'and the port is already in use for the router group' do
            let!(:other_domain) { VCAP::CloudController::SharedDomain.make(router_group_guid: 'some-router-group', name: 'my.domain2') }
            let!(:route_with_port) { VCAP::CloudController::Route.make(host: '', space: space, domain: other_domain, port: 123) }

            it 'fails with a helpful error message' do
              post '/v3/routes', params.to_json, admin_headers
              expect(last_response).to have_status_code(422)
              expect(last_response).to have_error_message("Port '123' is not available. Try a different port or use a different domain.")
            end
          end
        end

        context 'and the user does not provide a port' do
          let(:params) do
            {
              relationships: {
                space: {
                  data: { guid: space.guid }
                },
                domain: {
                  data: { guid: domain.guid }
                }
              }
            }
          end

          it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS

          context 'and randomly selected port is already in use' do
            let(:existing_route) { VCAP::CloudController::Route.make(host: '', space: space, domain: domain, port: 123) }

            let(:params) do
              {
                port: existing_route.port,
                relationships: {
                  space: {
                    data: { guid: space.guid }
                  },
                  domain: {
                    data: { guid: domain.guid }
                  }
                }
              }
            end

            it 'fails with a helpful error message' do
              post '/v3/routes', params.to_json, admin_headers
              expect(last_response).to have_status_code(422)
              expect(last_response).to have_error_message("Route already exists with port '123' for domain 'my.domain'.")
            end
          end
        end
      end
    end

    context 'when creating a route in a suspended org' do
      before do
        org.update(status: VCAP::CloudController::Organization::SUSPENDED)
      end

      let(:domain) { VCAP::CloudController::SharedDomain.make }

      let(:params) do
        {
          host: 'some-host',
          relationships: {
            space: {
              data: { guid: space.guid }
            },
            domain: {
              data: { guid: domain.guid }
            }
          }
        }
      end

      let(:route_json) do
        {
          guid: UUID_REGEX,
          protocol: domain.protocols[0],
          host: 'some-host',
          path: '',
          port: nil,
          url: "some-host.#{domain.name}",
          created_at: iso8601,
          updated_at: iso8601,
          destinations: [],
          relationships: {
            space: {
              data: { guid: space.guid }
            },
            domain: {
              data: { guid: domain.guid }
            }
          },
          metadata: {
            labels: {},
            annotations: {}
          },
          links: {
            self: { href: %r{#{Regexp.escape(link_prefix)}/v3/routes/#{UUID_REGEX}} },
            space: { href: %r{#{Regexp.escape(link_prefix)}/v3/spaces/#{space.guid}} },
            destinations: { href: %r{#{Regexp.escape(link_prefix)}/v3/routes/#{UUID_REGEX}/destinations} },
            domain: { href: %r{#{Regexp.escape(link_prefix)}/v3/domains/#{domain.guid}} }
          }
        }
      end

      describe 'valid routes' do
        let(:api_call) { ->(user_headers) { post '/v3/routes', 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: route_json
          }
          %w[space_developer space_supporter].each { |r| h[r] = { code: 403, errors: CF_ORG_SUSPENDED } }
          h
        end

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

    context 'when creating a route in an internal domain' do
      let(:domain) { VCAP::CloudController::SharedDomain.make(internal: true) }

      describe 'when creating a route with a wildcard host' do
        let(:params) do
          {
            host: '*',
            relationships: {
              space: {
                data: { guid: space.guid }
              },
              domain: {
                data: { guid: domain.guid }
              }
            }
          }
        end

        it 'fails with a helpful message' do
          post '/v3/routes', params.to_json, admin_header
          expect(last_response).to have_status_code(422)
          expect(last_response).to have_error_message('Wildcard hosts are not supported for internal domains.')
        end
      end

      describe 'when creating a route with a path' do
        let(:params) do
          {
            host: 'host',
            path: '/apath',
            relationships: {
              space: {
                data: { guid: space.guid }
              },
              domain: {
                data: { guid: domain.guid }
              }
            }
          }
        end

        it 'fails with a helpful message' do
          post '/v3/routes', params.to_json, admin_header
          expect(last_response).to have_status_code(422)
          expect(last_response).to have_error_message('Paths are not supported for internal domains.')
        end
      end

      describe 'when creating a route with a host' do
        let(:params) do
          {
            host: 'some-host',
            relationships: {
              space: {
                data: { guid: space.guid }
              },
              domain: {
                data: { guid: domain.guid }
              }
            }
          }
        end

        let(:route_json) do
          {
            guid: UUID_REGEX,
            protocol: domain.protocols[0],
            host: 'some-host',
            path: '',
            port: nil,
            url: "some-host.#{domain.name}",
            created_at: iso8601,
            updated_at: iso8601,
            destinations: [],
            relationships: {
              space: {
                data: { guid: space.guid }
              },
              domain: {
                data: { guid: domain.guid }
              }
            },
            links: {
              self: { href: %r{#{Regexp.escape(link_prefix)}/v3/routes/#{UUID_REGEX}} },
              space: { href: %r{#{Regexp.escape(link_prefix)}/v3/spaces/#{space.guid}} },
              destinations: { href: %r{#{Regexp.escape(link_prefix)}/v3/routes/#{UUID_REGEX}/destinations} },
              domain: { href: %r{#{Regexp.escape(link_prefix)}/v3/domains/#{domain.guid}} }
            },
            metadata: {
              labels: {},
              annotations: {}
            }
          }
        end

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

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

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

    context 'when the domain has an owning org that is different from the space\'s parent org' do
      let(:other_org) { VCAP::CloudController::Organization.make }
      let(:inaccessible_domain) { VCAP::CloudController::PrivateDomain.make(owning_organization: other_org) }

      let(:params_with_inaccessible_domain) do
        {
          relationships: {
            space: {
              data: { guid: space.guid }
            },
            domain: {
              data: { guid: inaccessible_domain.guid }
            }
          }
        }
      end

      it 'returns a 422 with a helpful error message' do
        post '/v3/routes', params_with_inaccessible_domain.to_json, admin_header
        expect(last_response).to have_status_code(422)
        expect(last_response).to have_error_message("Invalid domain. Domain '#{inaccessible_domain.name}' is not available in organization '#{org.name}'.")
      end
    end

    context 'when the host-less route has already been created for this domain' do
      let(:domain) { VCAP::CloudController::PrivateDomain.make(owning_organization: space.organization) }
      let!(:existing_route) { VCAP::CloudController::Route.make(host: '', space: space, domain: domain) }

      let(:params_for_duplicate_route) do
        {
          relationships: {
            space: {
              data: { guid: space.guid }
            },
            domain: {
              data: { guid: domain.guid }
            }
          }
        }
      end

      it 'returns a 422 with a helpful error message' do
        post '/v3/routes', params_for_duplicate_route.to_json, admin_header
        expect(last_response).to have_status_code(422)
        expect(last_response).to have_error_message("Route already exists for domain '#{domain.name}'.")
      end
    end

    context 'when there is already a route' do
      context 'with the host/domain/path combination' do
        let(:domain) { VCAP::CloudController::PrivateDomain.make(owning_organization: space.organization) }
        let!(:existing_route) { VCAP::CloudController::Route.make(host: 'my-host', path: '/existing', space: space, domain: domain) }

        let(:params_for_duplicate_route) do
          {
            host: existing_route.host,
            path: existing_route.path,
            relationships: {
              space: {
                data: { guid: space.guid }
              },
              domain: {
                data: { guid: domain.guid }
              }
            }
          }
        end

        it 'returns a 422 with a helpful error message' do
          post '/v3/routes', params_for_duplicate_route.to_json, admin_header
          expect(last_response).to have_status_code(422)
          expect(last_response).to have_error_message("Route already exists with host '#{existing_route.host}' and path '#{existing_route.path}' for domain '#{domain.name}'.")
        end
      end

      context 'with the host/domain combination' do
        let(:domain) { VCAP::CloudController::PrivateDomain.make(owning_organization: space.organization) }
        let!(:existing_route) { VCAP::CloudController::Route.make(host: 'my-host', space: space, domain: domain) }

        let(:params_for_duplicate_route) do
          {
            host: existing_route.host,
            path: existing_route.path,
            relationships: {
              space: {
                data: { guid: space.guid }
              },
              domain: {
                data: { guid: domain.guid }
              }
            }
          }
        end

        it 'returns a 422 with a helpful error message' do
          post '/v3/routes', params_for_duplicate_route.to_json, admin_header
          expect(last_response).to have_status_code(422)
          expect(last_response).to have_error_message("Route already exists with host '#{existing_route.host}' for domain '#{domain.name}'.")
        end
      end
    end

    context 'when there is already a domain matching the host/domain combination' do
      let(:domain) { VCAP::CloudController::PrivateDomain.make(owning_organization: space.organization) }
      let!(:existing_domain) { VCAP::CloudController::PrivateDomain.make(owning_organization: space.organization, name: "#{params[:host]}.#{domain.name}") }

      let(:params) do
        {
          host: 'some-host',
          relationships: {
            space: {
              data: { guid: space.guid }
            },
            domain: {
              data: { guid: domain.guid }
            }
          }
        }
      end

      it 'returns a 422 with a helpful error message' do
        post '/v3/routes', params.to_json, admin_header
        expect(last_response).to have_status_code(422)
        expect(last_response).to have_error_message("Route conflicts with domain '#{existing_domain.name}'.")
      end
    end

    context 'when using a reserved system hostname with the system domain' do
      let(:domain) { VCAP::CloudController::PrivateDomain.make(owning_organization: space.organization) }

      let(:params) do
        {
          host: 'host',
          relationships: {
            space: {
              data: { guid: space.guid }
            },
            domain: {
              data: { guid: domain.guid }
            }
          }
        }
      end

      before do
        VCAP::CloudController::Config.config.set(:system_domain, domain.name)
        VCAP::CloudController::Config.config.set(:system_hostnames, [params[:host]])
      end

      it 'returns a 422 with a helpful error message' do
        post '/v3/routes', params.to_json, admin_header
        expect(last_response).to have_status_code(422)
        expect(last_response).to have_error_message('Route conflicts with a reserved system route.')
      end
    end

    context 'when using a non-reserved hostname with the system domain' do
      let(:domain) { VCAP::CloudController::PrivateDomain.make(owning_organization: space.organization) }
      let(:api_call) { ->(user_headers) { post '/v3/routes', params.to_json, user_headers } }

      let(:params) do
        {
          host: 'host',
          relationships: {
            space: {
              data: { guid: space.guid }
            },
            domain: {
              data: { guid: domain.guid }
            }
          }
        }
      end

      let(:route_json) do
        {
          guid: UUID_REGEX,
          protocol: domain.protocols[0],
          host: params[:host],
          path: '',
          port: nil,
          url: "#{params[:host]}.#{domain.name}",
          created_at: iso8601,
          updated_at: iso8601,
          destinations: [],
          relationships: {
            space: {
              data: { guid: space.guid }
            },
            domain: {
              data: { guid: domain.guid }
            }
          },
          metadata: {
            labels: {},
            annotations: {}
          },
          links: {
            self: { href: %r{#{Regexp.escape(link_prefix)}/v3/routes/#{UUID_REGEX}} },
            space: { href: %r{#{Regexp.escape(link_prefix)}/v3/spaces/#{space.guid}} },
            destinations: { href: %r{#{Regexp.escape(link_prefix)}/v3/routes/#{UUID_REGEX}/destinations} },
            domain: { href: %r{#{Regexp.escape(link_prefix)}/v3/domains/#{domain.guid}} }
          }
        }
      end

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

      before do
        VCAP::CloudController::Config.config.set(:system_domain, domain.name)
      end

      it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS
    end

    describe 'quotas' do
      context 'when the space quota for routes is maxed out' do
        let(:domain) { VCAP::CloudController::PrivateDomain.make(owning_organization: space.organization) }
        let!(:space_quota_definition) { VCAP::CloudController::SpaceQuotaDefinition.make(total_routes: 0, organization: org) }
        let!(:space_with_quota) { VCAP::CloudController::Space.make(space_quota_definition: space_quota_definition, organization: org) }

        let(:params_for_space_with_quota) do
          {
            relationships: {
              space: {
                data: { guid: space_with_quota.guid }
              },
              domain: {
                data: { guid: domain.guid }
              }
            }
          }
        end

        it 'returns a 422 with a helpful error message' do
          post '/v3/routes', params_for_space_with_quota.to_json, admin_header
          expect(last_response).to have_status_code(422)
          expect(last_response).to have_error_message("Routes quota exceeded for space '#{space_with_quota.name}'.")
        end
      end

      context 'when the org quota for routes is maxed out' do
        let!(:org_quota_definition) { VCAP::CloudController::QuotaDefinition.make(total_routes: 0, total_reserved_route_ports: 0) }
        let!(:org_with_quota) { VCAP::CloudController::Organization.make(quota_definition: org_quota_definition) }
        let!(:space_in_org_with_quota) do
          VCAP::CloudController::Space.make(organization: org_with_quota)
        end
        let(:domain_in_org_with_quota) { VCAP::CloudController::Domain.make(owning_organization: org_with_quota) }

        let(:params_for_org_with_quota) do
          {
            relationships: {
              space: {
                data: { guid: space_in_org_with_quota.guid }
              },
              domain: {
                data: { guid: domain_in_org_with_quota.guid }
              }
            }
          }
        end

        it 'returns a 422 with a helpful error message' do
          post '/v3/routes', params_for_org_with_quota.to_json, admin_header
          expect(last_response).to have_status_code(422)
          expect(last_response).to have_error_message("Routes quota exceeded for organization '#{org_with_quota.name}'.")
        end
      end
    end

    context 'when the feature flag is disabled' do
      let(:headers) { set_user_with_header_as_role(user: user, role: 'space_developer', org: org, space: space) }
      let!(:feature_flag) { VCAP::CloudController::FeatureFlag.make(name: 'route_creation', enabled: false, error_message: 'my name is bob') }
      let(:domain) { VCAP::CloudController::PrivateDomain.make(owning_organization: space.organization) }
      let(:params) do
        {
          host: 'some-host',
          relationships: {
            space: {
              data: { guid: space.guid }
            },
            domain: {
              data: { guid: domain.guid }
            }
          }
        }
      end

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

          expect(last_response).to have_status_code(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/routes', params.to_json, headers

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

    context 'when the user is not logged in' do
      it 'returns 401 for Unauthenticated requests' do
        post '/v3/routes', {}.to_json, base_json_headers
        expect(last_response).to have_status_code(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/routes', {}.to_json, user_header
        expect(last_response).to have_status_code(403)
      end
    end

    context 'when the space does not exist' do
      let(:domain) { VCAP::CloudController::PrivateDomain.make(owning_organization: space.organization) }

      let(:params_with_invalid_space) do
        {
          relationships: {
            space: {
              data: { guid: 'invalid-space' }
            },
            domain: {
              data: { guid: domain.guid }
            }
          }
        }
      end

      it 'returns a 422 with a helpful error message' do
        post '/v3/routes', params_with_invalid_space.to_json, admin_header
        expect(last_response).to have_status_code(422)
        expect(last_response).to have_error_message('Invalid space. Ensure that the space exists and you have access to it.')
      end
    end

    context 'when the domain does not exist' do
      let(:params_with_invalid_domain) do
        {
          relationships: {
            space: {
              data: { guid: space.guid }
            },
            domain: {
              data: { guid: 'invalid-domain' }
            }
          }
        }
      end

      it 'returns a 422 with a helpful error message' do
        post '/v3/routes', params_with_invalid_domain.to_json, admin_header
        expect(last_response).to have_status_code(422)
        expect(last_response).to have_error_message('Invalid domain. Ensure that the domain exists and you have access to it.')
      end
    end

    context 'when communicating with the routing API' do
      let(:routing_api_client) { instance_double(VCAP::CloudController::RoutingApi::Client) }
      let(:router_group) { VCAP::CloudController::RoutingApi::RouterGroup.new({ 'type' => 'tcp', 'guid' => 'some-guid' }) }
      let(:headers) { set_user_with_header_as_role(role: 'admin') }
      let(:domain_tcp) { VCAP::CloudController::SharedDomain.make(router_group_guid: router_group.guid, name: 'my.domain') }
      let(:params) do
        {
          relationships: {
            space: {
              data: { guid: space.guid }
            },
            domain: {
              data: { guid: domain_tcp.guid }
            }
          }
        }
      end

      before do
        allow_any_instance_of(CloudController::DependencyLocator).to receive(:routing_api_client).and_return(routing_api_client)
      end

      context 'when UAA is unavailable' do
        before do
          allow(routing_api_client).to receive(:router_group).and_raise VCAP::CloudController::RoutingApi::UaaUnavailable
        end

        it 'returns a 503 with a helpful error message' do
          post '/v3/routes', params.to_json, headers

          expect(last_response).to have_status_code(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

      context 'when the routing API is unavailable' do
        before do
          allow(routing_api_client).to receive(:enabled?).and_return true
          allow(routing_api_client).to receive(:router_group).and_raise VCAP::CloudController::RoutingApi::RoutingApiUnavailable
        end

        it 'returns a 503 with a helpful error message' do
          post '/v3/routes', params.to_json, headers

          expect(last_response).to have_status_code(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
        before do
          allow(routing_api_client).to receive(:enabled?).and_return false
          allow(routing_api_client).to receive(:router_group).and_raise VCAP::CloudController::RoutingApi::RoutingApiDisabled
        end

        it 'returns a 503 with a helpful error message' do
          post '/v3/routes', params.to_json, headers

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

      context 'when the router group is unavailable' do
        let(:domain_tcp) { VCAP::CloudController::SharedDomain.make(router_group_guid: 'not a guid', name: 'my.domain') }

        before do
          allow(routing_api_client).to receive_messages(enabled?: true, router_group: nil)
        end

        it 'returns a 503 with a helpful error message' do
          post '/v3/routes', params.to_json, headers

          expect(last_response.status).to eq(422)
          expect(parsed_response['errors'][0]['detail']).to eq 'Route could not be created because the specified domain does not have a valid router group.'
        end
      end
    end
  end

  describe 'PATCH /v3/routes/:guid' do
    let(:domain) { VCAP::CloudController::PrivateDomain.make(owning_organization: space.organization) }
    let(:route) { VCAP::CloudController::Route.make(space: space, domain: domain, host: '') }
    let(:api_call) { ->(user_headers) { patch "/v3/routes/#{route.guid}", params.to_json, user_headers } }
    let(:params) do
      {
        metadata: {
          labels: {
            potato: 'fingerling',
            style: 'roasted'
          },
          annotations: {
            potato: 'russet',
            style: 'fried'
          }
        }
      }
    end

    let(:route_json) do
      {
        guid: UUID_REGEX,
        protocol: domain.protocols[0],
        host: '',
        path: '',
        port: nil,
        url: domain.name,
        created_at: iso8601,
        updated_at: iso8601,
        destinations: [],
        relationships: {
          space: {
            data: { guid: space.guid }
          },
          domain: {
            data: { guid: domain.guid }
          }
        },
        links: {
          self: { href: %r{#{Regexp.escape(link_prefix)}/v3/routes/#{UUID_REGEX}} },
          space: { href: %r{#{Regexp.escape(link_prefix)}/v3/spaces/#{space.guid}} },
          destinations: { href: %r{#{Regexp.escape(link_prefix)}/v3/routes/#{UUID_REGEX}/destinations} },
          domain: { href: %r{#{Regexp.escape(link_prefix)}/v3/domains/#{domain.guid}} }
        },
        metadata: {
          labels: {
            potato: 'fingerling',
            style: 'roasted'
          },
          annotations: {
            potato: 'russet',
            style: 'fried'
          }
        }
      }
    end

    context 'when the user logged in' do
      let(:expected_codes_and_responses) do
        h = Hash.new(code: 403, errors: CF_NOT_AUTHORIZED)
        h['admin'] = { code: 200, response_object: route_json }
        h['no_role'] = { code: 404 }
        h['org_billing_manager'] = { code: 404 }
        h['space_developer'] = { code: 200, response_object: route_json }
        h['space_supporter'] = { code: 200, response_object: route_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()
          %w[space_developer space_supporter].each { |r| h[r] = { 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 'when the user is not a member in the routes org' do
      let(:other_space) { VCAP::CloudController::Space.make }
      let(:domain) { VCAP::CloudController::PrivateDomain.make(owning_organization: other_space.organization) }
      let(:route) { VCAP::CloudController::Route.make(space: other_space, domain: domain, host: '') }

      let(:route_json) do
        {
          guid: UUID_REGEX,
          protocol: domain.protocols[0],
          host: '',
          path: '',
          port: nil,
          url: domain.name,
          created_at: iso8601,
          updated_at: iso8601,
          destinations: [],
          relationships: {
            space: {
              data: { guid: other_space.guid }
            },
            domain: {
              data: { guid: domain.guid }
            }
          },
          links: {
            self: { href: %r{#{Regexp.escape(link_prefix)}/v3/routes/#{UUID_REGEX}} },
            space: { href: %r{#{Regexp.escape(link_prefix)}/v3/spaces/#{other_space.guid}} },
            destinations: { href: %r{#{Regexp.escape(link_prefix)}/v3/routes/#{UUID_REGEX}/destinations} },
            domain: { href: %r{#{Regexp.escape(link_prefix)}/v3/domains/#{domain.guid}} }
          },
          metadata: {
            labels: {
              potato: 'fingerling',
              style: 'roasted'
            },
            annotations: {
              potato: 'russet',
              style: 'fried'
            }
          }
        }
      end
      let(:expected_codes_and_responses) do
        h = Hash.new(code: 404)
        h['admin'] = {
          code: 200,
          response_object: route_json
        }
        h['admin_read_only'] = {
          code: 403
        }
        h['global_auditor'] = {
          code: 403
        }
        h
      end

      it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS
    end

    context 'when route does not exist' do
      it 'returns a 404 with a helpful error message' do
        patch "/v3/routes/#{user.guid}", params.to_json, admin_header

        expect(last_response).to have_status_code(404)
        expect(last_response).to have_error_message('Route not found')
      end
    end

    context 'when request input message is invalid' do
      let(:params_with_invalid_input) do
        {
          disallowed_key: 'val'
        }
      end

      it 'returns a 422' do
        patch "/v3/routes/#{route.guid}", params_with_invalid_input.to_json, admin_header

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

    context 'when metadata is given with invalid format' do
      let(:params_with_invalid_metadata_format) do
        {
          metadata: {
            labels: {
              "": 'mashed',
              '/potato': '.value.'
            }
          }
        }
      end

      it 'returns a 422' do
        patch "/v3/routes/#{route.guid}", params_with_invalid_metadata_format.to_json, admin_header

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

    context 'when the user is not logged in' do
      it 'returns 401 for Unauthenticated requests' do
        patch "/v3/routes/#{route.guid}", nil, base_json_headers
        expect(last_response).to have_status_code(401)
      end
    end
  end

  describe 'DELETE /v3/routes/:guid' do
    let(:domain) { VCAP::CloudController::PrivateDomain.make(owning_organization: space.organization) }
    let(:route) { VCAP::CloudController::Route.make(space:, domain:) }
    let(:api_call) { ->(user_headers) { delete "/v3/routes/#{route.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/routes/#{route.guid}", {}, admin_headers
        expect(last_response).to have_status_code(404)
      end
    end

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

    context 'when the user is a member in the routes org' do
      let(:expected_codes_and_responses) do
        h = Hash.new(code: 403, errors: CF_NOT_AUTHORIZED)
        h['org_billing_manager'] = { code: 404 }
        h['no_role'] = { code: 404 }
        h['admin'] = { code: 202 }
        h['space_developer'] = { code: 202 }
        h['space_supporter'] = { code: 202 }
        h
      end

      it_behaves_like 'permissions for delete endpoint', ALL_PERMISSIONS do
        let(:expected_event_hash) do
          {
            type: 'audit.route.delete-request',
            actee: route.guid,
            actee_type: 'route',
            actee_name: route.host,
            metadata: { request: { recursive: true } }.to_json,
            space_guid: space.guid,
            organization_guid: org.guid
          }
        end
      end

      context 'when organization is suspended' do
        let(:expected_codes_and_responses) do
          h = super()
          %w[space_developer space_supporter].each { |r| h[r] = { 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 the user is not logged in' do
      it 'returns 401 for Unauthenticated requests' do
        delete "/v3/routes/#{route.guid}", nil, base_json_headers
        expect(last_response).to have_status_code(401)
      end
    end
  end

  describe 'GET /v3/routes/:guid/relationships/shared_spaces' do
    let(:api_call) { ->(user_headers) { get "/v3/routes/#{guid}/relationships/shared_spaces", nil, user_headers } }
    let(:target_space_1) { VCAP::CloudController::Space.make(organization: org) }
    let(:route) do
      route = VCAP::CloudController::Route.make(space:)
      route.add_shared_space(target_space_1)
      route
    end
    let(:guid) { route.guid }
    let(:space_dev_headers) do
      org.add_user(user)
      space.add_developer(user)
      headers_for(user)
    end
    let!(:feature_flag) do
      VCAP::CloudController::FeatureFlag.make(name: 'route_sharing', enabled: true, error_message: nil)
    end

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

    describe 'permissions' do
      it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS do
        let(:expected_codes_and_responses) do
          h = Hash.new(code: 200, response_object: {
                         data: [
                           {
                             guid: target_space_1.guid
                           }
                         ],
                         links: {
                           self: { href: %r{#{Regexp.escape(link_prefix)}/v3/routes/#{route.guid}/relationships/shared_spaces} }
                         }
                       })

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

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

      it 'makes users unable to unshare routes' do
        api_call.call(space_dev_headers)

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

    it 'responds with 404 when the route does not exist' do
      get '/v3/routes/some-fake-guid/relationships/shared_spaces', nil, space_dev_headers

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

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

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

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

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

        h['admin'] = { code: 200 }
        h['space_developer'] = { code: 200 }
        h['space_supporter'] = { code: 200 }
        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()
          %w[space_developer space_supporter].each { |r| h[r] = { 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

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

        let(:expected_codes_and_responses) do
          h = super()
          %w[space_developer space_supporter].each { |r| h[r] = { code: 422 } }
          h
        end

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

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

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

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

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

    it 'reports that the route is now shared' do
      api_call.call(space_dev_headers)

      expect(last_response.status).to eq(200)
      route.reload
      expect(route.shared_spaces).to include(target_space_1, target_space_2)
      expect(route).to be_shared
    end

    it 'reports that the route is not shared when it has not been shared' do
      route.reload
      expect(route.shared_spaces).to be_empty
      expect(route).not_to be_shared
    end

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

      context 'already owns the route' do
        let(:request_body) do
          {
            'data' => [
              { 'guid' => space.guid },
              { 'guid' => target_space_1.guid }
            ]
          }
        end

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

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

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

    describe 'errors while sharing' do
      # isolation segments?
    end
  end

  describe 'DELETE /v3/routes/:guid/relationships/shared_spaces/:space_guid' do
    let(:api_call) { ->(user_headers) { delete "/v3/routes/#{guid}/relationships/shared_spaces/#{unshared_space_guid}", request_body.to_json, user_headers } }
    let(:target_space_1) { VCAP::CloudController::Space.make(organization: org) }
    let(:target_space_2) { VCAP::CloudController::Space.make(organization: org) }
    let(:target_space_3) { VCAP::CloudController::Space.make(organization: org) }
    let(:target_space_not_shared_with_route) { VCAP::CloudController::Space.make(organization: org) }
    let(:space_to_unshare) { target_space_2 }
    let(:unshared_space_guid) { space_to_unshare.guid }
    let(:request_body) { {} }
    let(:route) do
      route = VCAP::CloudController::Route.make(space:)
      route.add_shared_space(target_space_1)
      route.add_shared_space(target_space_2)
      route.add_shared_space(target_space_3)
      route
    end
    let(:guid) { route.guid }
    let(:space_dev_headers) do
      org.add_user(user)
      space.add_developer(user)
      headers_for(user)
    end
    let!(:feature_flag) do
      VCAP::CloudController::FeatureFlag.make(name: 'route_sharing', enabled: true, error_message: nil)
    end

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

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

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

        h['admin'] = { code: 204 }
        h['space_developer'] = { code: 204 }
        h['space_supporter'] = { code: 204 }
        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()
          %w[space_developer space_supporter].each { |r| h[r] = { 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

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

        let(:expected_codes_and_responses) do
          h = super()
          %w[space_developer space_supporter].each do |r|
            h[r] = {
              code: 422,
              errors: [{
                detail: "Unable to unshare route '#{route.uri}' from space '#{space_to_unshare.guid}'. The target organization is suspended.",
                title: 'CF-UnprocessableEntity',
                code: 10_008
              }]
            }
          end
          h
        end

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

    it 'unshares the specified route from the target space and logs audit event' do
      expect(route.shared_spaces).to include(target_space_1, space_to_unshare, target_space_3)

      api_call.call(space_dev_headers)

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

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

      route.reload
      expect(route.shared_spaces).to include(target_space_1, target_space_3)
    end

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

      it 'makes users unable to unshare routes' do
        api_call.call(space_dev_headers)

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

    it 'responds with 204 when the route is not shared with the specified space' do
      delete "/v3/routes/#{route.guid}/relationships/shared_spaces/#{target_space_not_shared_with_route.guid}", request_body.to_json, space_dev_headers

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

    it "responds with 404 when the route doesn't exist" do
      delete "/v3/routes/some-fake-guid/relationships/shared_spaces/#{target_space_1.guid}", request_body.to_json, space_dev_headers

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

    context 'attempting to unshare from space that owns us' do
      let(:space_to_unshare) { space }

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

        expect(last_response.status).to eq(422)
        expect(parsed_response['errors']).to include(
          include(
            {
              'detail' => "Unable to unshare route '#{route.uri}' from space " \
                          "'#{space.guid}'. Routes cannot be removed from the space that owns them.",
              'title' => 'CF-UnprocessableEntity'
            }
          )
        )

        route.reload
        expect(route.shared_spaces).to contain_exactly(target_space_1, target_space_2, target_space_3)
      end
    end

    describe 'target space to unshare with' do
      context 'does not exist' do
        let(:unshared_space_guid) { 'fake-target' }

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

          expect(last_response.status).to eq(422)
          expect(parsed_response['errors']).to include(
            include(
              {
                'detail' => "Unable to unshare route '#{route.uri}' from space '#{unshared_space_guid}'. " \
                            'Ensure the space exists and that you have access to it.',
                'title' => 'CF-UnprocessableEntity'
              }
            )
          )
        end
      end

      context 'user does not have read access to the target space' do
        let(:unshared_space_guid) { VCAP::CloudController::Space.make(organization: org).guid }

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

          expect(last_response.status).to eq(422)
          expect(parsed_response['errors']).to include(
            include(
              {
                'detail' => "Unable to unshare route '#{route.uri}' from space '#{unshared_space_guid}'. " \
                            'Ensure the space exists and that you have access to it.',
                'title' => 'CF-UnprocessableEntity'
              }
            )
          )
        end
      end

      context 'user does not have write access to the target space' do
        let(:no_write_access_target_space) { VCAP::CloudController::Space.make(organization: org) }
        let(:unshared_space_guid) { no_write_access_target_space.guid }

        before do
          no_write_access_target_space.add_auditor(user)
        end

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

          expect(last_response.status).to eq(422)
          expect(parsed_response['errors']).to include(
            include(
              {
                'detail' => "Unable to unshare route '#{route.uri}' from space '#{no_write_access_target_space.guid}'. " \
                            "You don't have write permission for the target space.",
                'title' => 'CF-UnprocessableEntity'
              }
            )
          )
        end
      end
    end
  end

  describe 'PATCH /v3/routes/:guid/relationships/space' do
    let(:shared_domain) { VCAP::CloudController::SharedDomain.make }
    let(:route) { VCAP::CloudController::Route.make(space: space, domain: shared_domain) }
    let(:api_call) { ->(user_headers) { patch "/v3/routes/#{route.guid}/relationships/space", request_body.to_json, user_headers } }
    let(:target_space) { VCAP::CloudController::Space.make(organization: org) }
    let(:request_body) do
      {
        data: { 'guid' => target_space.guid }
      }
    end
    let(:space_dev_headers) do
      org.add_user(user)
      space.add_developer(user)
      headers_for(user)
    end
    let!(:feature_flag) do
      VCAP::CloudController::FeatureFlag.make(name: 'route_sharing', enabled: true, error_message: nil)
    end

    before do
      org.add_user(user)
      target_space.add_developer(user)
    end

    context 'when the user logged in' do
      let(:expected_codes_and_responses) do
        h = Hash.new(code: 403, errors: CF_NOT_AUTHORIZED)
        h['admin'] = { code: 200 }
        h['no_role'] = { code: 404 }
        h['org_billing_manager'] = { code: 404 }
        h['space_developer'] = { code: 200 }
        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()
          %w[space_developer].each { |r| h[r] = { 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

      context 'when target organization is suspended' do
        let(:suspended_space) { VCAP::CloudController::Space.make }
        let(:request_body) do
          {
            data: { 'guid' => suspended_space.guid }
          }
        end

        let(:expected_codes_and_responses) do
          h = super()
          %w[space_developer].each do |r|
            h[r] = {
              code: 422,
              errors: [{
                detail: "Unable to transfer owner of route '#{route.uri}' to space '#{suspended_space.guid}'. The target organization is suspended.",
                title: 'CF-UnprocessableEntity',
                code: 10_008
              }]
            }
          end
          h
        end

        before do
          suspended_space.organization.add_user(user)
          suspended_space.add_developer(user)
          suspended_space.organization.update(status: VCAP::CloudController::Organization::SUSPENDED)
        end

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

    it 'changes the route owner to the given space and logs an event', isolation: :truncation do
      api_call.call(space_dev_headers)

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

      event = VCAP::CloudController::Event.last
      expect(event.values).to include({
                                        type: 'audit.route.transfer-owner',
                                        actor: user.guid,
                                        actee_type: 'route',
                                        actee_name: route.host,
                                        space_guid: space.guid,
                                        organization_guid: space.organization.guid
                                      })
      expect(event.metadata['target_space_guid']).to eq(target_space.guid)

      route.reload
      expect(route.space).to eq target_space
    end

    describe 'when using a private domain' do
      let(:private_domain) { VCAP::CloudController::PrivateDomain.make(owning_organization: org) }
      let(:route) { VCAP::CloudController::Route.make(space: space, domain: private_domain) }
      let(:second_org) { VCAP::CloudController::Organization.make }
      let(:another_space) { VCAP::CloudController::Space.make(organization: second_org) }
      let(:request_body) do
        {
          data: { 'guid' => another_space.guid }
        }
      end
      let(:space_dev_headers) do
        org.add_user(user)
        space.add_developer(user)
        second_org.add_user(user)
        another_space.add_developer(user)
        headers_for(user)
      end

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

        expect(last_response.status).to eq(422)
        expect(parsed_response['errors']).to include(
          include(
            {
              'detail' => "Unable to transfer owner of route '#{route.uri}' to space '#{another_space.guid}'. " \
                          "Target space does not have access to route's domain",
              'title' => 'CF-UnprocessableEntity'
            }
          )
        )
      end
    end

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

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

          expect(last_response.status).to eq(422)
          expect(parsed_response['errors']).to include(
            include(
              {
                'detail' => "Unable to transfer owner of route '#{route.uri}' to space '#{target_space_guid}'. " \
                            'Ensure the space exists and that you have access to it.',
                'title' => 'CF-UnprocessableEntity'
              }
            )
          )
        end
      end

      context 'user does not have read access to the target space' do
        let(:no_access_target_space) { VCAP::CloudController::Space.make(organization: org) }
        let(:request_body) do
          {
            data: { 'guid' => no_access_target_space.guid }
          }
        end

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

          expect(last_response.status).to eq(422)
          expect(parsed_response['errors']).to include(
            include(
              {
                'detail' => "Unable to transfer owner of route '#{route.uri}' to space '#{no_access_target_space.guid}'. " \
                            'Ensure the space exists and that you have access to it.',
                'title' => 'CF-UnprocessableEntity'
              }
            )
          )
        end
      end

      context 'user does not have write access to the target space' do
        let(:no_write_access_target_space) { VCAP::CloudController::Space.make(organization: org) }
        let(:request_body) do
          {
            data: { 'guid' => no_write_access_target_space.guid }
          }
        end

        before do
          no_write_access_target_space.add_auditor(user)
        end

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

          expect(last_response.status).to eq(422)
          expect(parsed_response['errors']).to include(
            include(
              {
                'detail' => "Unable to transfer owner of route '#{route.uri}' to space '#{no_write_access_target_space.guid}'. " \
                            "You don't have write permission for the target space.",
                'title' => 'CF-UnprocessableEntity'
              }
            )
          )
        end
      end
    end

    it 'responds with 404 when the route does not exist' do
      patch '/v3/routes/some-fake-guid/relationships/space', request_body.to_json, space_dev_headers

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

    describe 'when the request body is invalid' do
      context 'when there are additional keys' do
        let(:request_body) do
          {
            data: { 'guid' => target_space.guid },
            'fake-key' => 'foo'
          }
        end

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

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

      context 'when data is not a hash' do
        let(:request_body) do
          {
            data: [{ 'guid' => target_space.guid }]
          }
        end

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

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

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

      it 'makes users unable to transfer-owner' do
        api_call.call(space_dev_headers)

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

  describe 'GET /v3/apps/:app_guid/routes' do
    let(:app_model) { VCAP::CloudController::AppModel.make(space:) }
    let(:route1) { VCAP::CloudController::Route.make(space:) }
    let(:route2) { VCAP::CloudController::Route.make(space:) }
    let!(:route3) { VCAP::CloudController::Route.make(space:) }
    let!(:route_mapping1) { VCAP::CloudController::RouteMappingModel.make(app: app_model, route: route1, process_type: 'web') }
    let!(:route_mapping2) { VCAP::CloudController::RouteMappingModel.make(app: app_model, route: route2, process_type: 'admin') }
    let(:api_call) { ->(user_headers) { get "/v3/apps/#{app_model.guid}/routes", nil, user_headers } }

    let(:route1_json) do
      {
        guid: route1.guid,
        protocol: route1.domain.protocols[0],
        host: route1.host,
        path: route1.path,
        port: nil,
        url: "#{route1.host}.#{route1.domain.name}#{route1.path}",
        created_at: iso8601,
        updated_at: iso8601,
        destinations: contain_exactly({
                                        guid: route_mapping1.guid,
                                        app: {
                                          guid: app_model.guid,
                                          process: {
                                            type: route_mapping1.process_type
                                          }
                                        },
                                        weight: route_mapping1.weight,
                                        port: route_mapping1.presented_port,
                                        protocol: 'http1'
                                      }),
        relationships: {
          space: {
            data: { guid: route1.space.guid }
          },
          domain: {
            data: { guid: route1.domain.guid }
          }
        },
        metadata: {
          labels: {},
          annotations: {}
        },
        links: {
          self: { href: %r{#{Regexp.escape(link_prefix)}/v3/routes/#{route1.guid}} },
          space: { href: %r{#{Regexp.escape(link_prefix)}/v3/spaces/#{route1.space.guid}} },
          destinations: { href: %r{#{Regexp.escape(link_prefix)}/v3/routes/#{route1.guid}/destinations} },
          domain: { href: %r{#{Regexp.escape(link_prefix)}/v3/domains/#{route1.domain.guid}} }
        }
      }
    end

    let(:route2_json) do
      {
        guid: route2.guid,
        protocol: route2.domain.protocols[0],
        host: route2.host,
        path: route2.path,
        port: nil,
        url: "#{route2.host}.#{route2.domain.name}#{route2.path}",
        created_at: iso8601,
        updated_at: iso8601,
        destinations: contain_exactly({
                                        guid: route_mapping2.guid,
                                        app: {
                                          guid: app_model.guid,
                                          process: {
                                            type: route_mapping2.process_type
                                          }
                                        },
                                        weight: route_mapping2.weight,
                                        port: route_mapping2.presented_port,
                                        protocol: 'http1'
                                      }),
        relationships: {
          space: {
            data: { guid: route2.space.guid }
          },
          domain: {
            data: { guid: route2.domain.guid }
          }
        },
        metadata: {
          labels: {},
          annotations: {}
        },
        links: {
          self: { href: %r{#{Regexp.escape(link_prefix)}/v3/routes/#{route2.guid}} },
          space: { href: %r{#{Regexp.escape(link_prefix)}/v3/spaces/#{route2.space.guid}} },
          destinations: { href: %r{#{Regexp.escape(link_prefix)}/v3/routes/#{route2.guid}/destinations} },
          domain: { href: %r{#{Regexp.escape(link_prefix)}/v3/domains/#{route2.domain.guid}} }
        }
      }
    end

    context 'when the user is a member in the app space' do
      let(:expected_codes_and_responses) do
        h = Hash.new(
          code: 200,
          response_objects: [route1_json, route2_json]
        )

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

      it_behaves_like 'permissions for list endpoint', ALL_PERMISSIONS
    end

    context 'ports filter' do
      # Don't even think of converting the following hash to symbols ('type' => 'tcp' NOT type: 'tcp'), and you need to set the GUID
      let(:router_group) { VCAP::CloudController::RoutingApi::RouterGroup.new({ 'type' => 'tcp', 'reservable_ports' => '7777,8888,9999', 'guid' => 'some-guid' }) }
      let(:routing_api_client) { instance_double(VCAP::CloudController::RoutingApi::Client) }

      before do
        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

      context 'when there are multiple TCP routes with different ports' do
        # The following `let`s depend on the above `before do`
        let(:domain_tcp) { VCAP::CloudController::SharedDomain.make(router_group_guid: router_group.guid, name: 'my.domain') }
        let!(:route_with_ports_0) do
          VCAP::CloudController::Route.make(host: '', space: space, domain: domain_tcp, guid: 'route-with-port-0', port: 7777)
        end
        let!(:route_with_ports_1) do
          VCAP::CloudController::Route.make(host: '', space: space, domain: domain_tcp, guid: 'route-with-port-1', port: 8888)
        end
        let!(:route_with_ports_2) do
          VCAP::CloudController::Route.make(host: '', space: space, domain: domain_tcp, guid: 'route-with-port-2', port: 9999)
        end
        let!(:route_mapping_1) { VCAP::CloudController::RouteMappingModel.make(app: app_model, route: route_with_ports_1, process_type: 'web') }
        let!(:route_mapping_2) { VCAP::CloudController::RouteMappingModel.make(app: app_model, route: route_with_ports_2, process_type: 'web') }

        it 'returns routes filtered by ports' do
          get "/v3/apps/#{app_model.guid}/routes?ports=7777,8888", nil, admin_header
          expect(last_response).to have_status_code(200)
          expect(parsed_response['resources'].size).to eq(1)
          expect(parsed_response['resources'].first['port']).to eq(route_with_ports_1.port)
        end
      end
    end

    describe 'eager loading' do
      it 'eager loads associated resources that the presenter specifies' do
        expect(VCAP::CloudController::RouteFetcher).to receive(:fetch).with(
          anything,
          hash_including(eager_loaded_associations: %i[domain space route_mappings labels annotations])
        ).and_call_original

        get "/v3/apps/#{app_model.guid}/routes", nil, admin_header
        expect(last_response).to have_status_code(200)
      end
    end
  end
end