cloudfoundry/cloud_controller_ng

View on GitHub
spec/request/users_spec.rb

Summary

Maintainability
D
2 days
Test Coverage
require 'spec_helper'
require 'request_spec_shared_examples'

RSpec.describe 'Users Request' do
  let(:actee) { VCAP::CloudController::User.make(guid: actee_guid) }
  let(:user) { VCAP::CloudController::User.make(guid: 'user') }
  let(:client) { VCAP::CloudController::User.make(guid: 'client-user') }
  let(:space) { VCAP::CloudController::Space.make }
  let(:org) { space.organization }
  let(:admin_header) { headers_for(user, scopes: %w[cloud_controller.admin]) }
  let(:uaa_client) { instance_double(VCAP::CloudController::UaaClient) }
  let(:actee_guid) { 'actee-guid' }

  before do
    VCAP::CloudController::User.dataset.destroy # this will clean up the seeded test users
    allow(VCAP::CloudController::UaaClient).to receive(:new).and_return(uaa_client)
    allow(uaa_client).to receive(:users_for_ids).with(contain_exactly(actee.guid, client.guid, user.guid)).and_return(
      {
        user.guid => { 'username' => 'bob-mcjames', 'origin' => 'Okta' },
        actee.guid => { 'username' => 'lola', 'origin' => 'uaa' }
      }
    )

    allow(uaa_client).to receive(:users_for_ids).with(contain_exactly(actee.guid, user.guid)).and_return(
      {
        user.guid => { 'username' => 'bob-mcjames', 'origin' => 'Okta' },
        actee.guid => { 'username' => 'lola', 'origin' => 'uaa' }
      }
    )

    allow(uaa_client).to receive(:users_for_ids).with([user.guid]).and_return(
      {
        user.guid => { 'username' => 'bob-mcjames', 'origin' => 'Okta' }
      }
    )
    allow(uaa_client).to receive(:users_for_ids).with([client.guid]).and_return({})
    allow(uaa_client).to receive(:users_for_ids).with([actee.guid]).and_return(
      {
        actee.guid => { 'username' => 'lola', 'origin' => 'uaa' }
      }
    )
    allow(uaa_client).to receive(:users_for_ids).with([]).and_return({})
  end

  describe 'GET /v3/users' do
    let(:current_user_json) do
      {
        guid: user.guid,
        created_at: iso8601,
        updated_at: iso8601,
        username: 'bob-mcjames',
        presentation_name: 'bob-mcjames',
        origin: 'Okta',
        metadata: {
          labels: {},
          annotations: {}
        },
        links: {
          self: { href: %r{#{Regexp.escape(link_prefix)}/v3/users/#{user.guid}} }
        }
      }
    end

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

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

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

      it_behaves_like 'list query endpoint' do
        let(:excluded_params) { [:partial_usernames] }
        let(:request) { 'v3/users' }
        let(:message) { VCAP::CloudController::UsersListMessage }
        let(:user_header) { admin_header }

        let(:params) do
          {
            guids: %w[foo bar],
            usernames: %w[foo bar],
            origins: %w[foo bar],
            page: '2',
            per_page: '10',
            order_by: 'updated_at',
            label_selector: 'foo,bar',
            created_ats: "#{Time.now.utc.iso8601},#{Time.now.utc.iso8601}",
            updated_ats: { gt: Time.now.utc.iso8601 }
          }
        end
      end

      context 'uses partial_usernames' do
        it_behaves_like 'list query endpoint' do
          before do
            allow(uaa_client).to receive(:ids_for_usernames_and_origins).and_return([])
          end

          let(:excluded_params) { [:usernames] }
          let(:request) { '/v3/users' }
          let(:message) { VCAP::CloudController::UsersListMessage }
          let(:user_header) { admin_header }

          let(:params) do
            {
              guids: %w[foo bar],
              partial_usernames: %w[foo bar],
              origins: %w[foo bar],
              page: '2',
              per_page: '10',
              order_by: 'updated_at',
              label_selector: 'foo,bar',
              created_ats: "#{Time.now.utc.iso8601},#{Time.now.utc.iso8601}",
              updated_ats: { gt: Time.now.utc.iso8601 }
            }
          end
        end
      end
    end

    describe 'without filters' do
      let(:api_call) { ->(user_headers) { get '/v3/users', nil, user_headers } }

      context 'when there are no other users in your space or org' do
        let(:expected_codes_and_responses) do
          h = Hash.new(
            code: 200,
            response_objects: [
              current_user_json
            ]
          )
          h['admin'] = {
            code: 200,
            response_objects: [
              actee_json,
              client_json,
              current_user_json
            ]
          }
          h['admin_read_only'] = {
            code: 200,
            response_objects: [
              actee_json,
              client_json,
              current_user_json
            ]
          }
          h['global_auditor'] = {
            code: 200,
            response_objects: [
              actee_json,
              client_json,
              current_user_json
            ]
          }
          h
        end

        it_behaves_like 'permissions for list endpoint', ALL_PERMISSIONS
      end

      context 'when the actee has an org or space role' do
        before do
          org.add_user(actee)
          space.add_developer(actee)
        end

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

        let(:expected_codes_and_responses) do
          h = Hash.new(
            code: 200,
            response_objects: [
              actee_json,
              current_user_json
            ]
          )
          h['admin'] = {
            code: 200,
            response_objects: [
              actee_json,
              client_json,
              current_user_json
            ]
          }
          h['admin_read_only'] = {
            code: 200,
            response_objects: [
              actee_json,
              client_json,
              current_user_json
            ]
          }
          h['global_auditor'] = {
            code: 200,
            response_objects: [
              actee_json,
              client_json,
              current_user_json
            ]
          }
          h['no_role'] = {
            code: 200,
            response_objects: [
              current_user_json
            ]
          }
          h
        end

        it_behaves_like 'permissions for list endpoint', ALL_PERMISSIONS
      end
    end

    describe 'with filters' do
      context 'when filtering by guid' do
        let(:endpoint) { "/v3/users?guids=#{user.guid}" }
        let(:api_call) { ->(user_headers) { get endpoint, nil, user_headers } }

        let(:expected_codes_and_responses) do
          h = Hash.new(
            code: 200,
            response_objects: [
              current_user_json
            ]
          )
          h
        end

        it_behaves_like 'permissions for list endpoint', ALL_PERMISSIONS
      end

      context 'when filtering by usernames and origins' do
        let(:user_in_different_origin) { VCAP::CloudController::User.make(guid: 'user_in_different_origin') }
        let(:user_with_different_username) { VCAP::CloudController::User.make(guid: 'user_in_different_origin') }
        let(:endpoint) { '/v3/users?usernames=bob-mcjames&origins=Okta' }
        let(:api_call) { ->(user_headers) { get endpoint, nil, user_headers } }
        let(:user_in_different_origin_json) do
          {
            guid: user_in_different_origin.guid,
            created_at: iso8601,
            updated_at: iso8601,
            username: 'bob-mcjames',
            presentation_name: 'bob-mcjames',
            origin: 'uaa',
            metadata: {
              labels: {},
              annotations: {}
            },
            links: {
              self: { href: %r{#{Regexp.escape(link_prefix)}/v3/users/#{user.guid}} }
            }
          }
        end

        let(:expected_codes_and_responses) do
          h = Hash.new(
            code: 200,
            response_objects: [
              current_user_json
            ]
          )
          h
        end

        before do
          org.add_user(user_in_different_origin)
          space.add_developer(user_in_different_origin)
          allow(uaa_client).to receive(:ids_for_usernames_and_origins).with(['bob-mcjames'], ['Okta']).and_return([user.guid])
          allow(uaa_client).to receive(:users_for_ids).with(contain_exactly('user', 'user_in_different_origin')).and_return(
            {
              user.guid => { 'username' => 'bob-mcjames', 'origin' => 'Okta' },
              user_in_different_origin.guid => { 'username' => 'bob-mcjames', 'origin' => 'uaa' }
            }
          )
        end

        it_behaves_like 'permissions for list endpoint', ALL_PERMISSIONS
      end

      context 'when filtering by usernames' do
        let(:endpoint) { '/v3/users?usernames=bob-mcjames' }
        let(:api_call) { ->(user_headers) { get endpoint, nil, user_headers } }
        let(:expected_codes_and_responses) do
          h = Hash.new(
            code: 200,
            response_objects: [
              current_user_json
            ]
          )
          h
        end

        before do
          allow(uaa_client).to receive(:ids_for_usernames_and_origins).with(['bob-mcjames'], nil).and_return([user.guid])
          allow(uaa_client).to receive(:users_for_ids).with(contain_exactly('user')).and_return(
            {
              user.guid => { 'username' => 'bob-mcjames', 'origin' => 'Okta' }
            }
          )
        end

        it_behaves_like 'permissions for list endpoint', ALL_PERMISSIONS

        context 'when UAA is unavailable' do
          before do
            allow(uaa_client).to receive(:ids_for_usernames_and_origins).with(['bob-mcjames'], nil).
              and_raise(VCAP::CloudController::UaaUnavailable)
          end

          it 'returns an error indicating UAA is unavailable' do
            get endpoint, nil, admin_header
            expect(last_response).to have_status_code(503)
            expect(parsed_response['errors'].first['detail']).to eq('The UAA service is currently unavailable')
          end
        end
      end

      context 'when filtering by origins' do
        it 'returns 422 for origin queries without usernames' do
          get '/v3/users', 'origins=uaa', admin_header
          expect(last_response).to have_status_code(422)
          expect(parsed_response['errors'].first['detail']).to eq(
            'Origins filter cannot be provided without usernames or partial_usernames filter.'
          )
        end
      end

      context 'when filtering by labels' do
        let!(:user_label) { VCAP::CloudController::UserLabelModel.make(resource_guid: user.guid, key_name: 'animal', value: 'dog') }

        let!(:actee_label) { VCAP::CloudController::UserLabelModel.make(resource_guid: actee.guid, key_name: 'animal', value: 'cow') }

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

          parsed_response = MultiJson.load(last_response.body)

          expected_pagination = {
            'total_results' => 1,
            'total_pages' => 1,
            'first' => { 'href' => "#{link_prefix}/v3/users?label_selector=animal+in+%28dog%29&page=1&per_page=50" },
            'last' => { 'href' => "#{link_prefix}/v3/users?label_selector=animal+in+%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(user.guid)
          expect(parsed_response['pagination']).to eq(expected_pagination)
        end
      end

      # normally this would be under request_spec_shared_examples; we copy it here because this test brings up issues with UAA
      context 'filtering timestamps on creation' do
        before do
          allow(uaa_client).to receive(:users_for_ids).and_return({})
        end

        let!(:resource_1) { VCAP::CloudController::User.make(guid: '1', created_at: '2020-05-26T18:47:01Z') }
        let!(:resource_2) { VCAP::CloudController::User.make(guid: '2', created_at: '2020-05-26T18:47:02Z') }
        let!(:resource_3) { VCAP::CloudController::User.make(guid: '3', created_at: '2020-05-26T18:47:03Z') }
        let!(:resource_4) { VCAP::CloudController::User.make(guid: '4', created_at: '2020-05-26T18:47:04Z') }

        it 'filters' do
          get "/v3/users?created_ats[lt]=#{resource_3.created_at.iso8601}", nil, admin_headers

          expect(last_response).to have_status_code(200)
          expect(parsed_response['resources'].pluck('guid')).to include(resource_1.guid, resource_2.guid)
          expect(parsed_response['resources'].pluck('guid')).not_to include(resource_3.guid, resource_4.guid)
        end
      end

      # normally this would be under request_spec_shared_examples; we copy it here because this test brings up issues with UAA
      context 'filtering timestamps on update' do
        # before must occur before the let! otherwise the resources will be created with
        # update_on_create: true
        before do
          VCAP::CloudController::User.plugin :timestamps, update_on_create: false
          allow(uaa_client).to receive(:users_for_ids).and_return({})
        end

        let!(:resource_1) { VCAP::CloudController::User.make(guid: '1', updated_at: '2020-05-26T18:47:01Z') }
        let!(:resource_2) { VCAP::CloudController::User.make(guid: '2', updated_at: '2020-05-26T18:47:02Z') }
        let!(:resource_3) { VCAP::CloudController::User.make(guid: '3', updated_at: '2020-05-26T18:47:03Z') }
        let!(:resource_4) { VCAP::CloudController::User.make(guid: '4', updated_at: '2020-05-26T18:47:04Z') }

        after do
          VCAP::CloudController::User.plugin :timestamps, update_on_create: true
        end

        it 'filters' do
          get "/v3/users?updated_ats[lt]=#{resource_3.updated_at.iso8601}", nil, admin_headers

          expect(last_response).to have_status_code(200)
          expect(parsed_response['resources'].pluck('guid')).to include(resource_1.guid, resource_2.guid)
          expect(parsed_response['resources'].pluck('guid')).not_to include(resource_3.guid, resource_4.guid)
        end
      end
    end

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

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

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

    context 'when the actee is not in an org or space' do
      let(:expected_codes_and_responses) do
        h = Hash.new(
          code: 404
        )
        h['admin'] = {
          code: 200,
          response_object: client_json
        }
        h['admin_read_only'] = {
          code: 200,
          response_object: client_json
        }
        h['global_auditor'] = {
          code: 200,
          response_object: client_json
        }
        h
      end

      it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS
    end

    context 'when the actee has an org or space role' do
      let(:expected_codes_and_responses) do
        h = Hash.new(
          code: 200,
          response_object: client_json
        )
        h['no_role'] = {
          code: 404,
          response_object: []
        }
        h
      end

      before do
        org.add_user(actee)
      end

      it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS
    end

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

    context 'when the user is logged in' do
      let(:user_header) { headers_for(user, scopes: %w[cloud_controller.read]) }

      it 'returns 200 when showing current user' do
        get "/v3/users/#{user.guid}", nil, user_header
        expect(last_response).to have_status_code(200)
        expect(parsed_response).to include('guid' => user.guid)
      end

      context 'when the user is not found' do
        it 'returns 404' do
          get '/v3/users/unknown-user', nil, admin_headers
          expect(last_response).to have_status_code(404)
          expect(last_response).to have_error_message('User not found')
        end
      end
    end

    context 'when the user has a guid with strange characters' do
      let(:weird_user) { VCAP::CloudController::User.make(guid: 'weird-/(%)') }

      before do
        allow(uaa_client).to receive(:users_for_ids).with([weird_user.guid]).and_return({})
      end

      it 'returns the user successfully' do
        get "/v3/users/#{CGI.escape(weird_user.guid)}", nil, admin_headers
        expect(last_response).to have_status_code(200)
        expect(parsed_response['guid']).to eq('weird-/(%)')
        expect(parsed_response['links']['self']['href']).to eq(link_prefix + '/v3/users/' + CGI.escape(weird_user.guid))
      end
    end
  end

  describe 'POST /v3/users' do
    let(:params) do
      {
        guid: 'new-user-guid'
      }
    end

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

      let(:user_json) do
        {
          guid: params[:guid],
          created_at: iso8601,
          updated_at: iso8601,
          username: 'my-new-user',
          presentation_name: 'my-new-user',
          origin: 'uaa',
          metadata: {
            labels: {
              potato: 'yam',
              style: 'casserole'
            },
            annotations: {
              potato: 'russet',
              style: 'french'
            }
          },
          links: {
            self: { href: %r{#{Regexp.escape(link_prefix)}/v3/users/#{params[:guid]}} }
          }
        }
      end

      context 'when metadata is valid' do
        it 'returns a 201' do
          post '/v3/users', {
            guid: params[:guid],
            metadata: {
              labels: {
                potato: 'yam',
                style: 'casserole'
              },
              annotations: {
                potato: 'russet',
                style: 'french'
              }
            }
          }.to_json, admin_header

          expect(parsed_response).to match_json_response(user_json)
          expect(last_response).to have_status_code(201)
        end
      end

      context 'when metadata is invalid' do
        it 'returns a 422' do
          post '/v3/users', {
            metadata: {
              labels: { '': 'invalid' },
              annotations: { "#{'a' * 1001}": 'value2' }
            }
          }.to_json, admin_header

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

    describe 'when creating a user that does not exist in uaa' do
      before do
        allow(uaa_client).to receive(:users_for_ids).and_return({})
      end

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

      let(:user_json) do
        {
          guid: params[:guid],
          created_at: iso8601,
          updated_at: iso8601,
          username: nil,
          presentation_name: params[:guid],
          origin: nil,
          metadata: {
            labels: {},
            annotations: {}
          },
          links: {
            self: { href: %r{#{Regexp.escape(link_prefix)}/v3/users/#{params[:guid]}} }
          }
        }
      end

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

      it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS
    end

    describe 'when creating a user that exists in uaa' do
      context "it's a UAA user" do
        before do
          allow(uaa_client).to receive(:users_for_ids).with(['new-user-guid']).and_return(
            {
              'new-user-guid' => {
                'username' => 'my-new-user',
                'origin' => 'uaa'
              }
            }
          )
        end

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

        let(:user_json) do
          {
            guid: params[:guid],
            created_at: iso8601,
            updated_at: iso8601,
            username: 'my-new-user',
            presentation_name: 'my-new-user',
            origin: 'uaa',
            metadata: {
              labels: {},
              annotations: {}
            },
            links: {
              self: { href: %r{#{Regexp.escape(link_prefix)}/v3/users/#{params[:guid]}} }
            }
          }
        end

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

        it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS
      end

      context "it's a UAA client" do
        let(:params) do
          {
            guid: uaa_client_id
          }
        end
        let(:uaa_client_id) { 'cc_routing' }

        before do
          allow(uaa_client).to receive_messages(users_for_ids: {}, get_clients: [{ client_id: uaa_client_id }])
        end

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

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

        let(:expected_codes_and_responses) do
          h = Hash.new(
            code: 403
          )
          h['admin'] = {
            code: 201,
            response_object: user_json
          }
          h
        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
        post '/v3/users', params.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/users', params.to_json, user_header
        expect(last_response).to have_status_code(403)
      end
    end

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

      context 'when provided invalid arguments' do
        let(:params) do
          {
            guid: 555
          }
        end

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

          expect(last_response).to have_status_code(422)

          expected_err = [
            'Guid must be a string, Guid must be between 1 and 200 characters'
          ]
          expect(parsed_response['errors'][0]['detail']).to eq expected_err.join(', ')
        end
      end

      context 'with an existing user' do
        let!(:existing_user) { VCAP::CloudController::User.make }

        let(:params) do
          {
            guid: existing_user.guid
          }
        end

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

          expect(last_response).to have_status_code(422)

          expect(parsed_response['errors'][0]['detail']).to eq "User with guid '#{existing_user.guid}' already exists."
        end
      end
    end
  end

  describe 'PATCH /v3/users/:guid' do
    let(:api_call) do
      lambda { |user_headers|
        patch "/v3/users/#{actee.guid}",
              {
                metadata: {
                  labels: {
                    potato: 'yam',
                    style: 'casserole'
                  },
                  annotations: {
                    potato: 'russet',
                    style: 'french'
                  }
                }
              }.to_json,
              user_headers
      }
    end

    describe 'metadata' do
      let(:client_json) do
        {
          guid: actee.guid,
          created_at: iso8601,
          updated_at: iso8601,
          username: 'lola',
          presentation_name: 'lola',
          origin: 'uaa',
          metadata: {
            labels: {
              potato: 'yam',
              style: 'casserole'
            },
            annotations: {
              potato: 'russet',
              style: 'french'
            }
          },
          links: {
            self: { href: %r{#{Regexp.escape(link_prefix)}/v3/users/#{actee.guid}} }
          }
        }
      end

      context 'when the actee is not associated with any org or space' do
        let(:expected_codes_and_responses) do
          h = Hash.new(
            code: 404
          )
          h['admin'] = {
            code: 200,
            response_object: client_json
          }
          h['admin_read_only'] = {
            code: 403,
            response_object: client_json
          }
          h['global_auditor'] = {
            code: 403
          }
          h
        end

        it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS
      end

      context 'when the actee has an org or space role' do
        let(:expected_codes_and_responses) do
          h = Hash.new(
            code: 403
          )
          h['admin'] = {
            code: 200,
            response_object: client_json
          }
          h['no_role'] = {
            code: 404
          }
          h
        end

        before do
          org.add_user(actee)
        end

        it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS
      end

      context 'when metadata is invalid' do
        it 'returns a 422' do
          patch "/v3/users/#{actee.guid}", {
            metadata: {
              labels: { '': 'invalid' },
              annotations: { "#{'a' * 1001}": 'value2' }
            }
          }.to_json, admin_header

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

  describe 'DELETE /v3/users/:guid' do
    let(:user_to_delete) { VCAP::CloudController::User.make }
    let(:api_call) { ->(user_headers) { delete "/v3/users/#{user_to_delete.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/users/#{user_to_delete.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) { user_to_delete }
        let(:api_call) do
          -> { delete "/v3/users/#{user_to_delete.guid}", nil, admin_headers }
        end
      end
    end

    context 'when the actee is not associated with any org or space' do
      let(:expected_codes_and_responses) do
        h = Hash.new(code: 404)

        h['admin_read_only'] = { code: 403 }
        h['global_auditor'] = { code: 403 }
        h['admin'] = { code: 202 }
        h
      end

      it_behaves_like 'permissions for delete endpoint', ALL_PERMISSIONS
    end

    context 'when the actee has an org or space role' do
      before do
        org.add_user(user_to_delete)
      end

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

      it_behaves_like 'permissions for delete endpoint', ALL_PERMISSIONS
    end

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

    context 'when the user is logged in' do
      context 'when the current non-admin user tries to delete themselves' do
        let(:user_header) { headers_for(user_to_delete, scopes: %w[cloud_controller.write]) }

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

        it 'returns 403' do
          delete "/v3/users/#{user_to_delete.guid}", nil, user_header
          expect(last_response).to have_status_code(403)
        end
      end

      context 'when the user is admin_read_only and has cloud_controller.write scope' do
        let(:user_header) { headers_for(user, scopes: %w[cloud_controller.admin_read_only cloud_controller.write]) }

        it 'returns 403' do
          delete "/v3/users/#{user_to_delete.guid}", nil, user_header
          expect(last_response).to have_status_code(403)
        end
      end

      context 'when the user is not found' do
        let(:user_header) { headers_for(user_to_delete, scopes: %w[cloud_controller.write]) }

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

        it 'returns 404' do
          delete '/v3/users/unknown-user', nil, user_header
          expect(last_response).to have_status_code(404)
          expect(last_response).to have_error_message('User not found')
        end
      end
    end

    context 'permissions' do
      it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS do
        let(:expected_codes_and_responses) do
          h = Hash.new(code: 404)
          h['admin'] = { code: 202 }
          h['admin_read_only'] = { code: 403 }
          h['global_auditor'] = { code: 403 }
          h
        end
      end
    end
  end
end