cloudfoundry/cloud_controller_ng

View on GitHub
spec/request/roles_spec.rb

Summary

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

RSpec.describe 'Roles Request' do
  let(:user) { VCAP::CloudController::User.make(guid: 'user_guid') }
  let(:admin_header) { admin_headers_for(user) }
  let(:org) { VCAP::CloudController::Organization.make(guid: 'big-org', created_at: Time.now.utc - 1.second) }
  let(:space) { VCAP::CloudController::Space.make(guid: 'big-space', organization: org, created_at: Time.now.utc - 1.second) }
  let(:user_with_role) { VCAP::CloudController::User.make(guid: 'user_with_role') }
  let(:user_guid) { user.guid }
  let(:space_guid) { space.guid }
  let(:user_unaffiliated) { VCAP::CloudController::User.make(guid: 'user_no_role') }
  let(:uaa_client) { instance_double(VCAP::CloudController::UaaClient) }

  before do
    allow(CloudController::DependencyLocator.instance).to receive(:uaa_username_lookup_client).and_return(uaa_client)
    allow(uaa_client).to receive(:usernames_for_ids).with([user_with_role.guid]).and_return(
      { user_with_role.guid => 'mona' }
    )
    allow(uaa_client).to receive(:usernames_for_ids).with([user_unaffiliated.guid]).and_return(
      { user_with_role.guid => 'bob_unaffiliated' }
    )
  end

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

    context 'creating a space role' do
      let(:params) do
        {
          type: 'space_auditor',
          relationships: {
            user: {
              data: { guid: user_with_role.guid }
            },
            space: {
              data: { guid: space.guid }
            }
          }
        }
      end

      let(:expected_response) do
        {
          guid: UUID_REGEX,
          created_at: iso8601,
          updated_at: iso8601,
          type: 'space_auditor',
          relationships: {
            user: {
              data: { guid: user_with_role.guid }
            },
            space: {
              data: { guid: space.guid }
            },
            organization: {
              data: nil
            }
          },
          links: {
            self: { href: %r{#{Regexp.escape(link_prefix)}/v3/roles/#{UUID_REGEX}} },
            user: { href: %r{#{Regexp.escape(link_prefix)}/v3/users/#{user_with_role.guid}} },
            space: { href: %r{#{Regexp.escape(link_prefix)}/v3/spaces/#{space.guid}} }
          }
        }
      end

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

      before do
        org.add_user(user_with_role)
      end

      it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS

      context 'when user is invalid' do
        let(:params) do
          {
            type: 'space_auditor',
            relationships: {
              user: {
                data: { guid: 'not-a-real-user' }
              },
              space: {
                data: { guid: space.guid }
              }
            }
          }
        end

        it 'returns a 422 with a helpful message' do
          post '/v3/roles', params.to_json, admin_header
          expect(last_response).to have_status_code(422)
          expect(last_response).to have_error_message("Users cannot be assigned roles in a space if they do not have a role in that space's organization.")
        end
      end

      context 'when space is invalid' do
        let(:params) do
          {
            type: 'space_auditor',
            relationships: {
              user: {
                data: { guid: user_with_role.guid }
              },
              space: {
                data: { guid: 'not-a-real-space' }
              }
            }
          }
        end

        it 'returns a 422 with a helpful message' do
          post '/v3/roles', params.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 organization is suspended' do
        let(:expected_codes_and_responses) do
          h = super()
          %w[space_manager org_manager].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 role already exists' do
        before do
          org.add_user(user_with_role)
          post '/v3/roles', params.to_json, admin_header
        end

        it 'returns a 422 with a helpful message' do
          post '/v3/roles', params.to_json, admin_header

          expect(last_response).to have_status_code(422)
          expect(last_response).to have_error_message(
            "User 'mona' already has 'space_auditor' role in space '#{space.name}'."
          )
        end
      end
    end

    context 'creating a organization role' do
      let(:params) do
        {
          type: 'organization_auditor',
          relationships: {
            user: {
              data: { guid: user_with_role.guid }
            },
            organization: {
              data: { guid: org.guid }
            }
          }
        }
      end
      let(:expected_response) do
        {
          guid: UUID_REGEX,
          created_at: iso8601,
          updated_at: iso8601,
          type: 'organization_auditor',
          relationships: {
            user: {
              data: { guid: user_with_role.guid }
            },
            space: {
              data: nil
            },
            organization: {
              data: { guid: org.guid }
            }
          },
          links: {
            self: { href: %r{#{Regexp.escape(link_prefix)}/v3/roles/#{UUID_REGEX}} },
            user: { href: %r{#{Regexp.escape(link_prefix)}/v3/users/#{user_with_role.guid}} },
            organization: { href: %r{#{Regexp.escape(link_prefix)}/v3/organizations/#{org.guid}} }
          }
        }
      end

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

      before do
        org.add_user(user_with_role)
      end

      it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS

      context 'when organization is invalid' do
        let(:params) do
          {
            type: 'organization_auditor',
            relationships: {
              user: {
                data: { guid: user_with_role.guid }
              },
              organization: {
                data: { guid: 'not-a-real-organization' }
              }
            }
          }
        end

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

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

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

        it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS
      end

      context 'when role already exists' do
        before do
          post '/v3/roles', params.to_json, admin_header
        end

        it 'returns a 422 with a helpful message' do
          post '/v3/roles', params.to_json, admin_header

          expect(last_response).to have_status_code(422)
          expect(last_response).to have_error_message(
            "User 'mona' already has 'organization_auditor' role in organization '#{org.name}'."
          )
        end
      end

      context 'creating role by user GUID for unaffiliated user' do
        let(:params) do
          {
            type: 'organization_auditor',
            relationships: {
              user: {
                data: { guid: user_unaffiliated.guid }
              },
              organization: {
                data: { guid: org.guid }
              }
            }
          }
        end

        let(:expected_response) do
          {
            guid: UUID_REGEX,
            created_at: iso8601,
            updated_at: iso8601,
            type: 'organization_auditor',
            relationships: {
              user: {
                data: { guid: user_unaffiliated.guid }
              },
              space: {
                data: nil
              },
              organization: {
                data: { guid: org.guid }
              }
            },
            links: {
              self: { href: %r{#{Regexp.escape(link_prefix)}/v3/roles/#{UUID_REGEX}} },
              user: { href: %r{#{Regexp.escape(link_prefix)}/v3/users/#{user_unaffiliated.guid}} },
              organization: { href: %r{#{Regexp.escape(link_prefix)}/v3/organizations/#{org.guid}} }
            }
          }
        end

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

        before do
          allow(uaa_client).to receive(:users_for_ids).with([user_unaffiliated.guid]).and_return({ user_unaffiliated.guid => { 'username' => user_unaffiliated.username } })
          allow(uaa_client).to receive(:usernames_for_ids).with([user_unaffiliated.guid]).and_return({ user_unaffiliated.guid => 'bob_unaffiliated' })
        end

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

    context 'creating a role by username' do
      let(:params) do
        {
          type: 'space_auditor',
          relationships: {
            user: {
              data: {
                username: 'uuu'
              }
            },
            space: {
              data: { guid: space.guid }
            }
          }
        }
      end

      context 'when the user exists in a single origin' do
        let(:expected_response) do
          {
            guid: UUID_REGEX,
            created_at: iso8601,
            updated_at: iso8601,
            type: 'space_auditor',
            relationships: {
              user: {
                data: { guid: user_with_role.guid }
              },
              space: {
                data: { guid: space.guid }
              },
              organization: {
                data: nil
              }
            },
            links: {
              self: { href: %r{#{Regexp.escape(link_prefix)}/v3/roles/#{UUID_REGEX}} },
              user: { href: %r{#{Regexp.escape(link_prefix)}/v3/users/#{user_with_role.guid}} },
              space: { href: %r{#{Regexp.escape(link_prefix)}/v3/spaces/#{space.guid}} }
            }
          }
        end

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

        before do
          allow(uaa_client).to receive(:origins_for_username).with('uuu').and_return(['uaa'])
          allow(uaa_client).to receive(:id_for_username).with('uuu', origin: 'uaa').and_return(user_with_role.guid)
          org.add_user(user_with_role)
        end

        it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS
      end

      context 'when there are multiple users with the same username' do
        before do
          allow(uaa_client).to receive(:origins_for_username).with('uuu').and_return(%w[uaa ldap okta])
          allow(uaa_client).to receive(:id_for_username).with('uuu', origin: 'uaa').and_return(user_with_role.guid)
        end

        it 'returns a 422 with a helpful message' do
          post '/v3/roles', params.to_json, admin_header

          expect(last_response).to have_status_code(422)
          expect(last_response).to have_error_message(
            "User with username 'uuu' exists in the following origins: ldap, okta, uaa. Specify an origin to disambiguate."
          )
        end
      end

      context 'when there is no user with the given username' do
        before do
          allow(uaa_client).to receive(:origins_for_username).with('uuu').and_return([])
          allow(uaa_client).to receive(:id_for_username).with('uuu', origin: nil).and_return(nil)
        end

        context 'for a space role' do
          it 'returns a 422 with a helpful message' do
            post '/v3/roles', params.to_json, admin_header

            expect(last_response).to have_status_code(422)
            expect(last_response).to have_error_message(
              "Users cannot be assigned roles in a space if they do not have a role in that space's organization."
            )
          end
        end

        context 'for an org role' do
          let(:params) do
            {
              type: 'organization_auditor',
              relationships: {
                user: {
                  data: {
                    username: 'uuu'
                  }
                },
                organization: {
                  data: { guid: org.guid }
                }
              }
            }
          end

          it 'returns a 422 with a helpful message' do
            post '/v3/roles', params.to_json, admin_header

            expect(last_response).to have_status_code(422)
            expect(last_response).to have_error_message(
              "No user exists with the username 'uuu'."
            )
          end
        end
      end
    end

    context 'creating a role by username and origin' do
      let(:params) do
        {
          type: 'space_auditor',
          relationships: {
            user: {
              data: {
                username: 'uuu',
                origin: 'okta'
              }
            },
            space: {
              data: { guid: space.guid }
            }
          }
        }
      end

      let(:expected_response) do
        {
          guid: UUID_REGEX,
          created_at: iso8601,
          updated_at: iso8601,
          type: 'space_auditor',
          relationships: {
            user: {
              data: { guid: user_with_role.guid }
            },
            space: {
              data: { guid: space.guid }
            },
            organization: {
              data: nil
            }
          },
          links: {
            self: { href: %r{#{Regexp.escape(link_prefix)}/v3/roles/#{UUID_REGEX}} },
            user: { href: %r{#{Regexp.escape(link_prefix)}/v3/users/#{user_with_role.guid}} },
            space: { href: %r{#{Regexp.escape(link_prefix)}/v3/spaces/#{space.guid}} }
          }
        }
      end

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

      before do
        allow(uaa_client).to receive(:id_for_username).with('uuu', origin: 'okta').and_return(user_with_role.guid)

        org.add_user(user_with_role)
      end

      it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS

      context 'when the flag to set roles by username is disabled' do
        before do
          VCAP::CloudController::FeatureFlag.make(name: 'set_roles_by_username')
        end

        let(:params) do
          {
            type: 'space_auditor',
            relationships: {
              user: {
                data: {
                  username: 'uuu',
                  origin: 'okta'
                }
              },
              space: {
                data: { guid: space.guid }
              }
            }
          }
        end

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

          h
        end

        it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS
      end

      context 'when there is no user with the given username and origin' do
        before do
          allow(uaa_client).to receive(:origins_for_username).with('uuu').and_return(['something-else'])
          allow(uaa_client).to receive(:id_for_username).with('uuu', origin: 'okta').and_return(nil)
        end

        context 'for a space role' do
          it 'returns a 422 with a helpful message' do
            post '/v3/roles', params.to_json, admin_header

            expect(last_response).to have_status_code(422)
            expect(last_response).to have_error_message(
              "Users cannot be assigned roles in a space if they do not have a role in that space's organization."
            )
          end
        end

        context 'for an org role' do
          let(:params) do
            {
              type: 'organization_auditor',
              relationships: {
                user: {
                  data: {
                    username: 'uuu',
                    origin: 'okta'
                  }
                },
                organization: {
                  data: { guid: org.guid }
                }
              }
            }
          end

          it 'returns a 422 with a helpful message' do
            post '/v3/roles', params.to_json, admin_header

            expect(last_response).to have_status_code(422)
            expect(last_response).to have_error_message(
              "No user exists with the username 'uuu' and origin 'okta'."
            )
          end
        end
      end

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

        it 'raises a 502 with a helpful message' do
          post '/v3/roles', params.to_json, admin_header

          expect(last_response).to have_status_code(503)
          expect(last_response).to have_error_message(
            'UAA service is currently unavailable'
          )
        end
      end

      # This case only applies for creating org roles, any user that would be able to have
      # a space role must also have at least an org user role
      context 'when the user is unaffiliated' do
        before do
          allow(uaa_client).to receive(:origins_for_username).with('bob_unaffiliated').and_return(['uaa'])
          allow(uaa_client).to receive(:id_for_username).with('bob_unaffiliated', origin: 'uaa').and_return(user_unaffiliated.guid)
          allow(uaa_client).to receive(:usernames_for_ids).with([user_unaffiliated.guid]).and_return({ user_unaffiliated.guid => 'bob_unaffiliated' })
        end

        let(:params) do
          {
            type: 'organization_auditor',
            relationships: {
              user: {
                data: {
                  username: 'bob_unaffiliated'
                }
              },
              organization: {
                data: { guid: org.guid }
              }
            }
          }
        end

        let(:expected_response) do
          {
            guid: UUID_REGEX,
            created_at: iso8601,
            updated_at: iso8601,
            type: 'organization_auditor',
            relationships: {
              user: {
                data: { guid: user_unaffiliated.guid }
              },
              space: {
                data: nil
              },
              organization: {
                data: { guid: org.guid }
              }
            },
            links: {
              self: { href: %r{#{Regexp.escape(link_prefix)}/v3/roles/#{UUID_REGEX}} },
              user: { href: %r{#{Regexp.escape(link_prefix)}/v3/users/#{user_unaffiliated.guid}} },
              organization: { href: %r{#{Regexp.escape(link_prefix)}/v3/organizations/#{org.guid}} }
            }
          }
        end

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

        it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS

        context 'when the flag to set roles by username is disabled' do
          before do
            VCAP::CloudController::FeatureFlag.make(name: 'set_roles_by_username')
          end

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

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

    context 'creating a role for a user that does not exist' do
      let(:expected_response) do
        {
          guid: UUID_REGEX,
          created_at: iso8601,
          updated_at: iso8601,
          type: 'organization_auditor',
          relationships: {
            user: {
              data: { guid: 'a-new-user-guid' }
            },
            space: {
              data: nil
            },
            organization: {
              data: { guid: org.guid }
            }
          },
          links: {
            self: { href: %r{#{Regexp.escape(link_prefix)}/v3/roles/#{UUID_REGEX}} },
            user: { href: %r{#{Regexp.escape(link_prefix)}/v3/users/a-new-user-guid} },
            organization: { href: %r{#{Regexp.escape(link_prefix)}/v3/organizations/#{org.guid}} }
          }
        }
      end

      before do
        allow(uaa_client).to receive(:usernames_for_ids).with(['a-new-user-guid']).and_return({ 'a-new-user-guid' => 'a-new-user-name' })
        allow(uaa_client).to receive(:id_for_username).with('a-new-user-name', origin: 'uaa').and_return('a-new-user-guid')
        allow(uaa_client).to receive(:users_for_ids).with(['a-new-user-guid']).and_return({ 'a-new-user-guid' => { 'username' => 'a-new-user-name' } })
      end

      context 'by user guid' do
        let(:params) do
          {
            type: 'organization_auditor',
            relationships: {
              user: {
                data: { guid: 'a-new-user-guid' }
              },
              organization: {
                data: { guid: org.guid }
              }
            }
          }
        end

        it 'creates the user and the role' do
          expect(VCAP::CloudController::User.where(guid: 'a-new-user-guid').empty?).to be true
          post '/v3/roles', params.to_json, admin_headers

          expect(last_response).to have_status_code(201)
          expect(parsed_response).to match_json_response(expected_response)
          expect(VCAP::CloudController::User.where(guid: 'a-new-user-guid').empty?).to be false
        end
      end

      context 'by user name' do
        let(:params) do
          {
            type: 'organization_auditor',
            relationships: {
              user: {
                data: { username: 'a-new-user-name', origin: 'uaa' }
              },
              organization: {
                data: { guid: org.guid }
              }
            }
          }
        end

        it 'creates the user and the role' do
          expect(VCAP::CloudController::User.where(guid: 'a-new-user-guid').empty?).to be true
          post '/v3/roles', params.to_json, admin_headers

          expect(last_response).to have_status_code(201)
          expect(parsed_response).to match_json_response(expected_response)
          expect(VCAP::CloudController::User.where(guid: 'a-new-user-guid').empty?).to be false
        end
      end

      context 'when the request is for a space role' do
        let(:params) do
          {
            type: 'space_auditor',
            relationships: {
              user: {
                data: { guid: 'a-new-user-guid' }
              },
              space: {
                data: { guid: space.guid }
              }
            }
          }
        end

        it 'raises the same error as a user that does not exist at all, without creating a new user' do
          expect(VCAP::CloudController::User.where(guid: 'a-new-user-guid').empty?).to be true
          post '/v3/roles', params.to_json, admin_headers

          expect(last_response).to have_status_code(422)
          expect(last_response).to have_error_message("Users cannot be assigned roles in a space if they do not have a role in that space's organization.")
          expect(VCAP::CloudController::User.where(guid: 'a-new-user-guid').empty?).to be true
        end
      end
    end
  end

  describe 'GET /v3/roles' do
    let(:api_call) { ->(user_headers) { get '/v3/roles', nil, user_headers } }
    let(:other_user) { VCAP::CloudController::User.make(guid: 'other-user-guid', created_at: Time.now.utc - 1.second) }

    let!(:space_auditor) do
      VCAP::CloudController::SpaceAuditor.make(
        space: space,
        user: other_user,
        created_at: Time.now - 5.minutes
      )
    end

    let!(:organization_auditor) do
      VCAP::CloudController::OrganizationAuditor.make(
        organization: org,
        user: other_user,
        created_at: Time.now
      )
    end

    let(:space_auditor_response_object) do
      {
        guid: space_auditor.guid,
        created_at: iso8601,
        updated_at: iso8601,
        type: 'space_auditor',
        relationships: {
          user: {
            data: { guid: other_user.guid }
          },
          organization: {
            data: nil
          },
          space: {
            data: { guid: space.guid }
          }
        },
        links: {
          self: { href: %r{#{Regexp.escape(link_prefix)}/v3/roles/#{UUID_REGEX}} },
          user: { href: %r{#{Regexp.escape(link_prefix)}/v3/users/#{other_user.guid}} },
          space: { href: %r{#{Regexp.escape(link_prefix)}/v3/spaces/#{space.guid}} }
        }
      }
    end

    let(:org_auditor_response_object) do
      {
        guid: organization_auditor.guid,
        created_at: iso8601,
        updated_at: iso8601,
        type: 'organization_auditor',
        relationships: {
          user: {
            data: { guid: other_user.guid }
          },
          organization: {
            data: { guid: org.guid }
          },
          space: {
            data: nil
          }
        },
        links: {
          self: { href: %r{#{Regexp.escape(link_prefix)}/v3/roles/#{UUID_REGEX}} },
          user: { href: %r{#{Regexp.escape(link_prefix)}/v3/users/#{other_user.guid}} },
          organization: { href: %r{#{Regexp.escape(link_prefix)}/v3/organizations/#{org.guid}} }
        }
      }
    end

    def make_org_role_for_current_user(type)
      {
        guid: UUID_REGEX,
        created_at: iso8601,
        updated_at: iso8601,
        type: type,
        relationships: {
          user: {
            data: { guid: user.guid }
          },
          organization: {
            data: { guid: org.guid }
          },
          space: {
            data: nil
          }
        },
        links: {
          self: { href: %r{#{Regexp.escape(link_prefix)}/v3/roles/#{UUID_REGEX}} },
          user: { href: %r{#{Regexp.escape(link_prefix)}/v3/users/#{user.guid}} },
          organization: { href: %r{#{Regexp.escape(link_prefix)}/v3/organizations/#{org.guid}} }
        }
      }
    end

    def make_space_role_for_current_user(type)
      {
        guid: UUID_REGEX,
        created_at: iso8601,
        updated_at: iso8601,
        type: type,
        relationships: {
          user: {
            data: { guid: user.guid }
          },
          organization: {
            data: nil
          },
          space: {
            data: { guid: space.guid }
          }
        },
        links: {
          self: { href: %r{#{Regexp.escape(link_prefix)}/v3/roles/#{UUID_REGEX}} },
          user: { href: %r{#{Regexp.escape(link_prefix)}/v3/users/#{user.guid}} },
          space: { href: %r{#{Regexp.escape(link_prefix)}/v3/spaces/#{space.guid}} }
        }
      }
    end

    describe 'list query parameters' do
      before do
        allow(uaa_client).to receive(:users_for_ids).and_return([])
      end

      it_behaves_like 'list query endpoint' do
        let(:request) { 'v3/roles' }
        let(:message) { VCAP::CloudController::RolesListMessage }
        let(:user_header) { headers_for(user) }
        let(:params) do
          {
            guids: %w[foo bar],
            organization_guids: %w[foo bar],
            space_guids: %w[foo bar],
            user_guids: %w[foo bar],
            types: %w[foo bar],
            per_page: '10',
            page: 2,
            order_by: 'updated_at',
            include: 'user, space',
            created_ats: "#{Time.now.utc.iso8601},#{Time.now.utc.iso8601}",
            updated_ats: { gt: Time.now.utc.iso8601 }
          }
        end
      end
    end

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

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

    context 'listing all roles' do
      let(:expected_codes_and_responses) do
        h = Hash.new(code: 200, response_objects: [space_auditor_response_object, org_auditor_response_object])

        h['org_auditor'] = {
          code: 200,
          response_objects: contain_exactly(
            org_auditor_response_object,
            make_org_role_for_current_user('organization_user'),
            make_org_role_for_current_user('organization_auditor')
          )
        }

        h['org_manager'] = {
          code: 200,
          response_objects: contain_exactly(
            space_auditor_response_object,
            org_auditor_response_object,
            make_org_role_for_current_user('organization_user'),
            make_org_role_for_current_user('organization_manager')
          )
        }

        h['org_billing_manager'] = {
          code: 200,
          response_objects: contain_exactly(
            org_auditor_response_object,
            make_org_role_for_current_user('organization_user'),
            make_org_role_for_current_user('organization_billing_manager')
          )
        }

        h['space_manager'] = {
          code: 200,
          response_objects: contain_exactly(
            space_auditor_response_object,
            org_auditor_response_object,
            make_org_role_for_current_user('organization_user'),
            make_space_role_for_current_user('space_manager')
          )
        }

        h['space_auditor'] = {
          code: 200,
          response_objects: contain_exactly(
            space_auditor_response_object,
            org_auditor_response_object,
            make_org_role_for_current_user('organization_user'),
            make_space_role_for_current_user('space_auditor')
          )
        }

        h['space_developer'] = {
          code: 200,
          response_objects: contain_exactly(
            space_auditor_response_object,
            org_auditor_response_object,
            make_org_role_for_current_user('organization_user'),
            make_space_role_for_current_user('space_developer')
          )
        }

        h['space_supporter'] = {
          code: 200,
          response_objects: contain_exactly(
            space_auditor_response_object,
            org_auditor_response_object,
            make_org_role_for_current_user('organization_user'),
            make_space_role_for_current_user('space_supporter')
          )
        }

        h['no_role'] = { code: 200, response_objects: [] }
        h
      end

      it_behaves_like 'permissions for list endpoint', ALL_PERMISSIONS

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

    context 'listing roles with filters' do
      let(:too_late_org_role) { OrganizationAuditor.make(user: other_user, organization: org, created_at: '2028-05-26T18:47:01Z') }
      let(:api_call) do
        lambda { |user_headers|
          get "/v3/roles?user_guids=#{other_user.guid}&
order_by=-created_at&created_ats[lt]=2028-05-26T18:47:01Z&guids=#{organization_auditor.guid},#{space_auditor.guid}",
              nil,
              user_headers
        }
      end

      let(:expected_codes_and_responses) do
        h = Hash.new(code: 200, response_objects: [org_auditor_response_object, space_auditor_response_object])
        h['org_auditor'] = {
          code: 200,
          response_objects: contain_exactly(
            org_auditor_response_object
          )
        }
        h['org_billing_manager'] = {
          code: 200,
          response_objects: contain_exactly(
            org_auditor_response_object
          )
        }
        h['no_role'] = { code: 200, response_objects: [] }
        h
      end

      it_behaves_like 'permissions for list endpoint', ALL_PERMISSIONS
    end

    context 'listing roles with overlapping timestamps' do
      let!(:user_jeff) { VCAP::CloudController::User.make(guid: 'jeff-guid') }
      let!(:role_one) { VCAP::CloudController::OrganizationAuditor.make(guid: '1', user: user_jeff, organization: org, created_at: '2019-12-25T13:00:00Z') }
      let!(:role_two) { VCAP::CloudController::SpaceAuditor.make(guid: '2', user: user_jeff, space: space, created_at: '2019-12-25T13:00:00Z') }
      let!(:role_three) { VCAP::CloudController::SpaceManager.make(guid: '3', user: user_jeff, space: space, created_at: '2019-12-25T13:00:00Z') }

      it 'sorts the the roles on a secondary key and keeps the same order between calls' do
        get('/v3/roles', nil, admin_header)
        expect(last_response).to have_status_code(200)

        parsed_response = Oj.load(last_response.body)
        expect(parsed_response['resources'][0]['guid']).to match('1')
        expect(parsed_response['resources'][1]['guid']).to match('2')
        expect(parsed_response['resources'][2]['guid']).to match('3')
      end
    end

    context 'listing roles with include' do
      let(:other_user_response) do
        {
          guid: other_user.guid,
          created_at: iso8601,
          updated_at: iso8601,
          username: 'other_user_name',
          presentation_name: 'other_user_name',
          origin: 'uaa',
          metadata: {
            labels: {},
            annotations: {}
          },
          links: {
            self: { href: %r{#{Regexp.escape(link_prefix)}/v3/users/#{other_user.guid}} }
          }
        }
      end

      let(:org_response_object) do
        {
          guid: org.guid,
          created_at: iso8601,
          updated_at: iso8601,
          name: org.name,
          suspended: false,
          relationships: {
            quota: {
              data: { guid: org.quota_definition.guid }
            }
          },
          metadata: {
            labels: {},
            annotations: {}
          },
          links: {
            self: { href: %r{#{Regexp.escape(link_prefix)}/v3/organizations/#{org.guid}} },
            domains: { href: %r{#{Regexp.escape(link_prefix)}/v3/organizations/#{org.guid}/domains} },
            default_domain: { href: %r{#{Regexp.escape(link_prefix)}/v3/organizations/#{org.guid}/domains/default} },
            quota: { href: %r{#{Regexp.escape(link_prefix)}/v3/organization_quotas/#{org.quota_definition.guid}} }
          }
        }
      end

      let(:space_response_object) do
        {
          guid: space.guid,
          created_at: iso8601,
          updated_at: iso8601,
          name: space.name,
          relationships: {
            organization: {
              data: { guid: org.guid }
            },
            quota: {
              data: nil
            }
          },
          metadata: {
            labels: {},
            annotations: {}
          },
          links: {
            self: { href: %r{#{Regexp.escape(link_prefix)}/v3/spaces/#{space.guid}} },
            organization: { href: %r{#{Regexp.escape(link_prefix)}/v3/organizations/#{org.guid}} },
            features: { href: %r{#{Regexp.escape(link_prefix)}/v3/spaces/#{space.guid}/features} },
            apply_manifest: { href: %r{#{Regexp.escape(link_prefix)}/v3/spaces/#{space.guid}/actions/apply_manifest}, method: 'POST' }
          }
        }
      end

      before do
        allow(uaa_client).to receive(:users_for_ids).with([other_user.guid]).and_return(
          { other_user.guid => { 'username' => 'other_user_name', 'origin' => 'uaa' } }
        )
      end

      it 'includes the requested users' do
        get('/v3/roles?include=user,organization,space', nil, admin_header)
        expect(last_response).to have_status_code(200)

        parsed_response = Oj.load(last_response.body)
        expect(parsed_response['included']['users'][0]).to match_json_response(other_user_response)
        expect(parsed_response['included']['organizations'][0]).to match_json_response(org_response_object)
        expect(parsed_response['included']['spaces'][0]).to match_json_response(space_response_object)
      end

      it 'eagerly loads users to efficiently access user_guid' do
        expect(VCAP::CloudController::IncludeRoleUserDecorator).to receive(:decorate) do |_, roles|
          expect(roles).not_to be_empty
          roles.each { |r| expect(r.associations).to include(:user) }
        end

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

      context 'when there are multiple users with multiple roles' do
        let(:another_user) { VCAP::CloudController::User.make(guid: 'another-user-guid') }
        let(:another_org) { VCAP::CloudController::Organization.make }
        let(:another_space) { VCAP::CloudController::Space.make }

        let(:another_user_response) do
          {
            guid: another_user.guid,
            created_at: iso8601,
            updated_at: iso8601,
            username: 'another_user_name',
            presentation_name: 'another_user_name', # username is nil, so presenter defaults to guid
            origin: 'uaa',
            metadata: {
              labels: {},
              annotations: {}
            },
            links: {
              self: { href: %r{#{Regexp.escape(link_prefix)}/v3/users/#{another_user.guid}} }
            }
          }
        end

        let(:another_space_response) do
          {
            guid: another_space.guid,
            created_at: iso8601,
            updated_at: iso8601,
            name: another_space.name,
            relationships: {
              organization: {
                data: { guid: another_space.organization.guid }
              },
              quota: {
                data: nil
              }
            },
            metadata: {
              labels: {},
              annotations: {}
            },
            links: {
              self: { href: %r{#{Regexp.escape(link_prefix)}/v3/spaces/#{another_space.guid}} },
              organization: { href: %r{#{Regexp.escape(link_prefix)}/v3/organizations/#{another_space.organization.guid}} },
              features: { href: %r{#{Regexp.escape(link_prefix)}/v3/spaces/#{another_space.guid}/features} },
              apply_manifest: { href: %r{#{Regexp.escape(link_prefix)}/v3/spaces/#{another_space.guid}/actions/apply_manifest}, method: 'POST' }
            }
          }
        end

        let(:another_org_response) do
          {
            guid: another_org.guid,
            created_at: iso8601,
            updated_at: iso8601,
            name: another_org.name,
            suspended: false,
            relationships: {
              quota: {
                data: { guid: another_org.quota_definition.guid }
              }
            },
            metadata: {
              labels: {},
              annotations: {}
            },
            links: {
              self: { href: %r{#{Regexp.escape(link_prefix)}/v3/organizations/#{another_org.guid}} },
              domains: { href: %r{#{Regexp.escape(link_prefix)}/v3/organizations/#{another_org.guid}/domains} },
              default_domain: { href: %r{#{Regexp.escape(link_prefix)}/v3/organizations/#{another_org.guid}/domains/default} },
              quota: { href: %r{#{Regexp.escape(link_prefix)}/v3/organization_quotas/#{another_org.quota_definition.guid}} }
            }
          }
        end

        let!(:organization_billing_manager) do
          VCAP::CloudController::OrganizationBillingManager.make(
            guid: 'organization_billing_manager-guid',
            organization: org,
            user: another_user,
            created_at: Time.now - 3.minutes
          )
        end

        let!(:space_auditor) do
          VCAP::CloudController::SpaceAuditor.make(
            guid: 'space_auditor-guid',
            space: space,
            user: another_user
          )
        end

        let!(:another_space_auditor) do
          VCAP::CloudController::SpaceAuditor.make(
            guid: 'another-space_auditor-guid',
            space: another_space,
            user: another_user
          )
        end

        let!(:org_manager) do
          VCAP::CloudController::OrganizationManager.make(
            guid: 'organization_manager-guid',
            organization: another_org,
            user: another_user
          )
        end

        before do
          allow(uaa_client).to receive(:users_for_ids).with(contain_exactly(other_user.guid, another_user.guid)).and_return(
            {
              another_user.guid => { 'username' => 'another_user_name', 'origin' => 'uaa' },
              other_user.guid => { 'username' => 'other_user_name', 'origin' => 'uaa' }
            }
          )
        end

        it 'returns all of the relevant users' do
          get('/v3/roles?include=user,space,organization', nil, admin_header)
          expect(last_response).to have_status_code(200)

          parsed_response = Oj.load(last_response.body)
          expect(parsed_response['included']['users']).to contain_exactly(
            match_json_response(other_user_response),
            match_json_response(another_user_response)
          )
          expect(parsed_response['included']['spaces']).to contain_exactly(
            match_json_response(space_response_object),
            match_json_response(another_space_response)
          )
          expect(parsed_response['included']['organizations']).to contain_exactly(
            match_json_response(org_response_object),
            match_json_response(another_org_response)
          )
        end
      end
    end
  end

  describe 'GET /v3/roles/:guid' do
    let(:api_call) { ->(user_headers) { get "/v3/roles/#{role.guid}", nil, user_headers } }

    context 'when getting a space role' do
      let(:role) { VCAP::CloudController::SpaceAuditor.make(user: user_with_role, space: space) }

      let(:expected_response) do
        {
          guid: role.guid,
          created_at: iso8601,
          updated_at: iso8601,
          type: 'space_auditor',
          relationships: {
            user: {
              data: { guid: user_with_role.guid }
            },
            space: {
              data: { guid: space.guid }
            },
            organization: {
              data: nil
            }
          },
          links: {
            self: { href: %r{#{Regexp.escape(link_prefix)}/v3/roles/#{role.guid}} },
            user: { href: %r{#{Regexp.escape(link_prefix)}/v3/users/#{user_with_role.guid}} },
            space: { href: %r{#{Regexp.escape(link_prefix)}/v3/spaces/#{space.guid}} }
          }
        }
      end

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

      before do
        org.add_user(user_with_role)
      end

      it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS
    end

    context 'when getting a org role' do
      let(:role) { VCAP::CloudController::OrganizationAuditor.make(user: user_with_role, organization: org) }

      let(:expected_response) do
        {
          guid: role.guid,
          created_at: iso8601,
          updated_at: iso8601,
          type: 'organization_auditor',
          relationships: {
            user: {
              data: { guid: user_with_role.guid }
            },
            space: {
              data: nil
            },
            organization: {
              data: { guid: org.guid }
            }
          },
          links: {
            self: { href: %r{#{Regexp.escape(link_prefix)}/v3/roles/#{role.guid}} },
            user: { href: %r{#{Regexp.escape(link_prefix)}/v3/users/#{user_with_role.guid}} },
            organization: { href: %r{#{Regexp.escape(link_prefix)}/v3/organizations/#{org.guid}} }
          }
        }
      end

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

      it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS
    end

    context 'when the role does not exist' do
      it 'returns a 404 with a helpful message' do
        get '/v3/roles/not-exist', nil, admin_header

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

    context 'when not logged in' do
      it 'returns a 401 with a helpful message' do
        get '/v3/roles/not-exist', nil, {}

        expect(last_response).to have_status_code(401)
        expect(last_response).to have_error_message('Authentication error')
      end
    end

    context 'getting a role with included resources' do
      let(:org_role) { VCAP::CloudController::OrganizationAuditor.make(user: user_with_role, organization: org) }
      let(:space_role) { VCAP::CloudController::SpaceAuditor.make(user: user_with_role, space: space) }

      let(:user_with_role_response) do
        {
          guid: user_with_role.guid,
          created_at: iso8601,
          updated_at: iso8601,
          username: 'user_name',
          presentation_name: 'user_name',
          origin: 'uaa',
          metadata: {
            labels: {},
            annotations: {}
          },
          links: {
            self: { href: %r{#{Regexp.escape(link_prefix)}/v3/users/#{user_with_role.guid}} }
          }
        }
      end

      let(:org_response_object) do
        {
          guid: org.guid,
          created_at: iso8601,
          updated_at: iso8601,
          name: org.name,
          suspended: false,
          relationships: {
            quota: {
              data: { guid: org.quota_definition.guid }
            }
          },
          metadata: {
            labels: {},
            annotations: {}
          },
          links: {
            self: { href: %r{#{Regexp.escape(link_prefix)}/v3/organizations/#{org.guid}} },
            domains: { href: %r{#{Regexp.escape(link_prefix)}/v3/organizations/#{org.guid}/domains} },
            default_domain: { href: %r{#{Regexp.escape(link_prefix)}/v3/organizations/#{org.guid}/domains/default} },
            quota: { href: %r{#{Regexp.escape(link_prefix)}/v3/organization_quotas/#{org.quota_definition.guid}} }
          }
        }
      end

      let(:space_response_object) do
        {
          guid: space.guid,
          created_at: iso8601,
          updated_at: iso8601,
          name: space.name,
          relationships: {
            organization: {
              data: { guid: org.guid }
            },
            quota: {
              data: nil
            }
          },
          metadata: {
            labels: {},
            annotations: {}
          },
          links: {
            self: { href: %r{#{Regexp.escape(link_prefix)}/v3/spaces/#{space.guid}} },
            organization: { href: %r{#{Regexp.escape(link_prefix)}/v3/organizations/#{org.guid}} },
            features: { href: %r{#{Regexp.escape(link_prefix)}/v3/spaces/#{space.guid}/features} },
            apply_manifest: { href: %r{#{Regexp.escape(link_prefix)}/v3/spaces/#{space.guid}/actions/apply_manifest}, method: 'POST' }
          }
        }
      end

      before do
        allow(uaa_client).to receive(:users_for_ids).with([user_with_role.guid]).and_return(
          { user_with_role.guid => { 'username' => 'user_name', 'origin' => 'uaa' } }
        )
      end

      context 'for an org role' do
        it 'includes the requested users and organization' do
          get("/v3/roles/#{org_role.guid}?include=user,space,organization", nil, admin_header)
          expect(last_response).to have_status_code(200)

          parsed_response = Oj.load(last_response.body)
          expect(parsed_response['included']['users'][0]).to match_json_response(user_with_role_response)
          expect(parsed_response['included']['organizations'][0]).to match_json_response(org_response_object)
          expect(parsed_response['included']['spaces']).to eq([])
        end
      end

      context 'for a space role' do
        it 'includes the requested users and organization' do
          get("/v3/roles/#{space_role.guid}?include=user,space,organization", nil, admin_header)
          expect(last_response).to have_status_code(200)

          parsed_response = Oj.load(last_response.body)
          expect(parsed_response['included']['users'][0]).to match_json_response(user_with_role_response)
          expect(parsed_response['included']['organizations']).to eq([])
          expect(parsed_response['included']['spaces'][0]).to match_json_response(space_response_object)
        end
      end
    end
  end

  describe 'DELETE /v3/roles/:guid' do
    let(:api_call) { ->(headers) { delete "/v3/roles/#{role.guid}", nil, headers } }
    let(:db_check) do
      lambda do
        expect(last_response.headers['Location']).to match(%r{http.+/v3/jobs/[a-fA-F0-9-]+})

        job_guid = last_response.headers['Location'].gsub("#{link_prefix}/v3/jobs/", '')
        get "/v3/jobs/#{job_guid}", {}, admin_headers
        expect(last_response).to have_status_code(200)

        execute_all_jobs(expected_successes: 1, expected_failures: 0)
        get "/v3/roles/#{role.guid}", {}, admin_headers
        expect(last_response).to have_status_code(404)
      end
    end

    before do
      org.add_user(user_with_role)
    end

    context 'when deleting a space role' do
      let(:role) { VCAP::CloudController::SpaceAuditor.make(user: user_with_role, space: space) }

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

      it_behaves_like 'permissions for delete endpoint', ALL_PERMISSIONS

      context 'when organization is suspended' do
        let(:expected_codes_and_responses) do
          h = super()
          %w[space_manager org_manager].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 deleting an org role' do
      let(:role) { VCAP::CloudController::OrganizationAuditor.make(user: user_with_role, organization: org) }

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

      it_behaves_like 'permissions for delete endpoint', ALL_PERMISSIONS

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

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

        it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS
      end

      context 'and the user still has a role in a space within that org' do
        let(:org_user_role) { VCAP::CloudController::OrganizationUser.find(user_id: user_with_role.id) }

        before do
          space.add_manager(user_with_role)
        end

        it 'returns a 422 when trying to delete the organization_user role' do
          delete "/v3/roles/#{org_user_role.guid}", nil, admin_headers
          expect(last_response).to have_status_code(422)
        end

        it 'successfullies delete any other org role' do
          delete "/v3/roles/#{role.guid}", nil, admin_headers
          expect(last_response).to have_status_code(202)
        end
      end
    end

    context 'when the user is not logged in' do
      let(:role) { VCAP::CloudController::SpaceAuditor.make(user: user_with_role, space: space) }

      it 'returns a 401' do
        delete "/v3/roles/#{role.guid}", nil, base_json_headers
        expect(last_response).to have_status_code(401)
      end
    end

    context 'when the requested role does not exist' do
      let(:headers) { headers_for(user, scopes: %w[cloud_controller.write]) }

      before do
        set_current_user_as_role(role: 'org_manager', org: org, space: space, user: user)
      end

      it 'returns a 404 not found' do
        delete('/v3/roles/does-not-exist', nil, headers)
        expect(last_response).to have_status_code(404)
      end
    end
  end
end