cloudfoundry/cloud_controller_ng

View on GitHub
spec/request/organization_quotas_spec.rb

Summary

Maintainability
D
1 day
Test Coverage
require 'spec_helper'
require 'request_spec_shared_examples'

module VCAP::CloudController
  RSpec.describe 'organization_quotas' do
    let(:user) { VCAP::CloudController::User.make(guid: 'user-guid') }
    let(:organization_quota) { VCAP::CloudController::QuotaDefinition.make(guid: 'org-quota-guid') }
    let!(:org) { VCAP::CloudController::Organization.make(guid: 'organization-guid', quota_definition: organization_quota) }
    let(:space) { VCAP::CloudController::Space.make(guid: 'space-guid', organization: org) }
    let(:admin_header) { headers_for(user, scopes: %w[cloud_controller.admin]) }

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

      let(:params) do
        {
          name: 'quota1',
          relationships: {
            organizations: {
              data: [
                { guid: org.guid }
              ]
            }
          }
        }
      end

      let(:organization_quota_json) do
        {
          guid: UUID_REGEX,
          created_at: iso8601,
          updated_at: iso8601,
          name: params[:name],
          apps: {
            total_memory_in_mb: nil,
            per_process_memory_in_mb: nil,
            total_instances: nil,
            per_app_tasks: nil,
            log_rate_limit_in_bytes_per_second: nil
          },
          services: {
            paid_services_allowed: true,
            total_service_instances: nil,
            total_service_keys: nil
          },
          routes: {
            total_routes: nil,
            total_reserved_ports: nil
          },
          domains: {
            total_domains: nil
          },
          relationships: {
            organizations: {
              data: [{ guid: 'organization-guid' }]
            }
          },
          links: {
            self: { href: %r{#{Regexp.escape(link_prefix)}/v3/organization_quotas/#{params[:guid]}} }
          }
        }
      end

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

      context 'using the default params' do
        it 'creates a organization_quota' do
          expect do
            api_call.call(admin_header)
          end.to change(QuotaDefinition, :count).by 1
        end

        it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS
      end

      context 'using provided params' do
        let(:params) do
          {
            name: 'org1',
            apps: {
              total_memory_in_mb: 5120,
              per_process_memory_in_mb: 1024,
              total_instances: 10,
              per_app_tasks: 5,
              log_rate_limit_in_bytes_per_second: 2000
            },
            services: {
              paid_services_allowed: false,
              total_service_instances: 10,
              total_service_keys: 20
            },
            routes: {
              total_routes: 8,
              total_reserved_ports: 4
            },
            domains: {
              total_domains: 7
            }
          }
        end

        let(:expected_response) do
          {
            guid: UUID_REGEX,
            created_at: iso8601,
            updated_at: iso8601,
            name: 'org1',
            apps: {
              total_memory_in_mb: 5120,
              per_process_memory_in_mb: 1024,
              total_instances: 10,
              per_app_tasks: 5,
              log_rate_limit_in_bytes_per_second: 2000
            },
            services: {
              paid_services_allowed: false,
              total_service_instances: 10,
              total_service_keys: 20
            },
            routes: {
              total_routes: 8,
              total_reserved_ports: 4
            },
            domains: {
              total_domains: 7
            },
            relationships: {
              organizations: {
                data: []
              }
            },
            links: {
              self: { href: %r{#{Regexp.escape(link_prefix)}/v3/organization_quotas/#{params[:guid]}} }
            }
          }
        end

        it 'responds with the expected code and response' do
          api_call.call(admin_header)
          expect(last_response).to have_status_code(201)
          expect(parsed_response).to match_json_response(expected_response)
        end
      end

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

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

            expect(last_response).to have_status_code(422)
            expect(last_response).to include_error_message('Name must be a string')
          end
        end

        context 'with a pre-existing name' do
          let(:params) do
            {
              name: 'double-trouble'
            }
          end

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

            expect(last_response).to have_status_code(422)
            expect(last_response).to include_error_message("Organization Quota 'double-trouble' already exists.")
          end
        end
      end
    end

    describe 'GET /v3/organization_quotas' do
      let(:api_call) { ->(user_headers) { get '/v3/organization_quotas', nil, user_headers } }

      context 'when listing organization_quotas' do
        let!(:other_org) { VCAP::CloudController::Organization.make(guid: 'other-organization-guid', quota_definition: organization_quota) }
        let(:other_org_response) { { guid: 'other-organization-guid' } }
        let(:org_response) { { guid: 'organization-guid' } }

        let(:expected_codes_and_responses) do
          h = Hash.new(code: 200, response_objects: generate_org_quota_list_response([org_response], false))
          h['admin'] = { code: 200, response_objects: generate_org_quota_list_response([org_response, other_org_response], true) }
          h['admin_read_only'] = { code: 200, response_objects: generate_org_quota_list_response([org_response, other_org_response], true) }
          h['global_auditor'] = { code: 200, response_objects: generate_org_quota_list_response([org_response, other_org_response], true) }
          h['no_role'] = { code: 200, response_objects: generate_org_quota_list_response([], false) }
          h
        end

        it_behaves_like 'permissions for list endpoint', ALL_PERMISSIONS

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

        context 'with filters' do
          let!(:organization_quota_2) { VCAP::CloudController::QuotaDefinition.make(guid: 'second-guid', name: 'second-name') }
          let!(:organization_quota_3) { VCAP::CloudController::QuotaDefinition.make(guid: 'third-guid', name: 'third-name') }

          before do
            org.quota_definition = organization_quota
            org.save

            other_org.quota_definition = organization_quota
            other_org.save
          end

          it 'returns the list of quotas filtered by names and guids' do
            get "/v3/organization_quotas?guids=#{organization_quota.guid},second-guid&names=#{organization_quota.name},third-name", nil, admin_header

            expect(last_response).to have_status_code(200)
            expect(parsed_response['resources'].length).to eq(1)
            expect(parsed_response['resources'][0]['guid']).to eq(organization_quota.guid)
          end

          it 'returns the list of quotas filtered by organization guids' do
            get "/v3/organization_quotas?organization_guids=#{org.guid},#{other_org.guid}", nil, admin_header

            expect(last_response).to have_status_code(200)
            expect(
              parsed_response['resources'].pluck('guid')
            ).to eq([organization_quota.guid])
          end
        end
      end

      context 'when not logged in' do
        it 'returns a 401 with a helpful message' do
          get '/v3/organization_quotas', nil, {}
          expect(last_response).to have_status_code(401)
          expect(last_response).to have_error_message('Authentication error')
        end
      end
    end

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

      context 'when getting an organization_quota' do
        let!(:other_org) { VCAP::CloudController::Organization.make(guid: 'other-organization-guid', quota_definition: organization_quota) }
        let(:other_org_response) { { guid: 'other-organization-guid' } }
        let(:org_response) { { guid: 'organization-guid' } }

        let(:expected_codes_and_responses) do
          h = Hash.new(code: 200, response_object: generate_org_quota_single_response([org_response]))
          h['admin'] = { code: 200, response_object: generate_org_quota_single_response([org_response, other_org_response]) }
          h['admin_read_only'] = { code: 200, response_object: generate_org_quota_single_response([org_response, other_org_response]) }
          h['global_auditor'] = { code: 200, response_object: generate_org_quota_single_response([org_response, other_org_response]) }
          h['no_role'] = { code: 200, response_object: generate_org_quota_single_response([]) }
          h
        end

        it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS
      end

      context 'when the organization_quota had no associated organizations' do
        let(:unused_organization_quota) { VCAP::CloudController::QuotaDefinition.make }

        it 'returns a quota with an empty array of org guids' do
          get "/v3/organization_quotas/#{unused_organization_quota.guid}", nil, admin_header

          expect(last_response).to have_status_code(200)
          expect(parsed_response['relationships']['organizations']['data']).to eq([])
        end
      end

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

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

      context 'when not logged in' do
        it 'returns a 401 with a helpful message' do
          get '/v3/organization_quotas/not-exist', nil, {}
          expect(last_response).to have_status_code(401)
          expect(last_response).to have_error_message('Authentication error')
        end
      end
    end

    describe 'PATCH /v3/organization_quotas/:guid' do
      let(:api_call) { ->(user_headers) { patch "/v3/organization_quotas/#{organization_quota.guid}", params.to_json, user_headers } }

      let(:params) do
        {
          name: 'don-quixote',
          apps: {
            total_memory_in_mb: 5120,
            per_process_memory_in_mb: 1024,
            total_instances: nil,
            per_app_tasks: 5,
            log_rate_limit_in_bytes_per_second: 2000
          },
          services: {
            paid_services_allowed: false,
            total_service_instances: 10,
            total_service_keys: 20
          },
          routes: {
            total_routes: 8,
            total_reserved_ports: 4
          },
          domains: {
            total_domains: 7
          }
        }
      end

      let(:organization_quota_json) do
        {
          guid: organization_quota.guid,
          created_at: iso8601,
          updated_at: iso8601,
          name: params[:name],
          apps: {
            total_memory_in_mb: 5120,
            per_process_memory_in_mb: 1024,
            total_instances: nil,
            per_app_tasks: 5,
            log_rate_limit_in_bytes_per_second: 2000
          },
          services: {
            paid_services_allowed: false,
            total_service_instances: 10,
            total_service_keys: 20
          },
          routes: {
            total_routes: 8,
            total_reserved_ports: 4
          },
          domains: {
            total_domains: 7
          },
          relationships: {
            organizations: {
              data: [{ guid: 'organization-guid' }]
            }
          },
          links: {
            self: { href: %r{#{Regexp.escape(link_prefix)}/v3/organization_quotas/#{params[:guid]}} }
          }
        }
      end

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

      it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS

      context 'when the organization_quota does not exist' do
        it 'returns a 404 with a helpful message' do
          patch '/v3/organization_quotas/not-exist', params.to_json, admin_header

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

      context 'update partial values' do
        let(:org_quota_to_update) do
          VCAP::CloudController::QuotaDefinition.make(
            guid: 'org_quota_to_update_guid',
            name: 'update-me',
            memory_limit: 8,
            non_basic_services_allowed: true
          )
        end
        let(:partial_params) do
          {
            name: 'don-quixote',
            apps: {
              per_app_tasks: 9,
              total_memory_in_mb: nil
            },
            services: {
              total_service_instances: 14,
              paid_services_allowed: false
            }
          }
        end

        before do
          patch "/v3/organization_quotas/#{org_quota_to_update.guid}", partial_params.to_json, admin_header
        end

        it 'only updates the requested fields' do
          expect(last_response).to have_status_code(200)
          expect(org_quota_to_update.reload.app_task_limit).to eq(9)
          expect(org_quota_to_update.reload.memory_limit).to eq(-1)
          expect(org_quota_to_update.reload.total_services).to eq(14)
          expect(org_quota_to_update.reload.log_rate_limit).to eq(-1)
          expect(org_quota_to_update.reload.non_basic_services_allowed).to be_falsey
        end

        context 'patching with empty params' do
          it 'succeeds without changing the quota' do
            patch "/v3/organization_quotas/#{org_quota_to_update.guid}", {}, admin_header

            expect(last_response).to have_status_code(200)
            expect(org_quota_to_update.reload.app_task_limit).to eq(9)
            expect(org_quota_to_update.reload.memory_limit).to eq(-1)
            expect(org_quota_to_update.reload.total_services).to eq(14)
            expect(org_quota_to_update.reload.log_rate_limit).to eq(-1)
            expect(org_quota_to_update.reload.non_basic_services_allowed).to be_falsey
          end
        end
      end

      context 'when trying to update name to a pre-existing name' do
        let(:new_org_quota) { QuotaDefinition.make }

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

        it 'returns 422' do
          patch "/v3/organization_quotas/#{new_org_quota.guid}", params.to_json, admin_header

          expect(last_response).to have_status_code(422)
          expect(last_response).to include_error_message("Organization Quota '#{organization_quota.name}' already exists.")
        end
      end

      context 'when trying to set a log rate limit and there are apps with unlimited log rates' do
        let!(:app_model) { VCAP::CloudController::AppModel.make(name: 'name1', space: space) }
        let!(:process_model) { VCAP::CloudController::ProcessModel.make(app: app_model, log_rate_limit: -1) }

        it 'returns 422' do
          patch "/v3/organization_quotas/#{organization_quota.guid}", params.to_json, admin_header

          expect(last_response).to have_status_code(422)
          expect(last_response).to include_error_message(
            'Current usage exceeds new quota values. ' \
            "This quota is applied to org '#{org.name}' which contains apps running with an unlimited log rate limit."
          )
        end
      end
    end

    describe 'POST /v3/organization_quotas/:guid/relationships/organizations' do
      let(:api_call) { ->(user_headers) { post "/v3/organization_quotas/#{org_quota.guid}/relationships/organizations", params.to_json, user_headers } }

      let(:org) { VCAP::CloudController::Organization.make }
      let(:org_quota) { VCAP::CloudController::QuotaDefinition.make }

      let(:params) do
        {
          data: [{ guid: org.guid }]
        }
      end

      context 'when applying quota to an organization' do
        let(:data_json) do
          {
            data: [
              { guid: org.guid }
            ],
            links: {
              self: { href: "#{link_prefix}/v3/organization_quotas/#{org_quota.guid}/relationships/organizations" }
            }
          }
        end

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

        it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS
      end

      context 'when an org guid does not exist' do
        let(:params) do
          {
            data: [{ guid: 'not a real guid' }]
          }
        end

        it 'returns a 422 with a helpful message' do
          post "/v3/organization_quotas/#{org_quota.guid}/relationships/organizations", params.to_json, admin_header
          expect(last_response).to have_status_code(422)
          expect(last_response).to have_error_message('Organizations with guids ["not a real guid"] do not exist')
        end
      end

      context 'when an org guid is the wrong type' do
        let(:params) do
          {
            data: [{ guid: 8 }]
          }
        end

        it 'returns a 422 with a helpful message' do
          post "/v3/organization_quotas/#{org_quota.guid}/relationships/organizations", params.to_json, admin_header
          expect(last_response).to have_status_code(422)
          expect(parsed_response['errors'][0]['detail']).to eq('Invalid data type: Data[0] guid should be a string.')
        end
      end

      context 'when the quota has a finite log rate limit and there are apps with unlimited log rates' do
        let(:org_quota) { VCAP::CloudController::QuotaDefinition.make(log_rate_limit: 100) }
        let!(:app_model) { VCAP::CloudController::AppModel.make(name: 'name1', space: space) }
        let!(:process_model) { VCAP::CloudController::ProcessModel.make(app: app_model, log_rate_limit: -1) }

        it 'returns 422' do
          post "/v3/organization_quotas/#{org_quota.guid}/relationships/organizations", params.to_json, admin_header
          expect(last_response).to have_status_code(422)
          expect(last_response).to include_error_message(
            'Current usage exceeds new quota values. The org(s) being assigned this quota contain apps running with an unlimited log rate limit.'
          )
        end
      end
    end

    describe 'DELETE /v3/organization_quotas/:guid/' do
      let(:org_quota) { VCAP::CloudController::QuotaDefinition.make }
      let(:api_call) { ->(user_headers) { delete "/v3/organization_quotas/#{org_quota.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)

          last_job = VCAP::CloudController::PollableJobModel.last
          expect(last_response.headers['Location']).to match(%r{/v3/jobs/#{last_job.guid}})
          expect(last_job.resource_type).to eq('organization_quota')
        end
      end

      context 'when deleting an organization quota' do
        let(:expected_codes_and_responses) do
          h = Hash.new(code: 403)
          h['admin'] = { code: 202 }
          h
        end

        it_behaves_like 'permissions for delete endpoint', ALL_PERMISSIONS
      end

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

      context 'when an organization quota is applied to an organization' do
        let(:org) { VCAP::CloudController::Organization.make }

        let(:params) do
          {
            data: [{ guid: org.guid }]
          }
        end

        it 'the org quota is not deleted and returns a 422' do
          post "/v3/organization_quotas/#{org_quota.guid}/relationships/organizations", params.to_json, admin_headers

          delete "/v3/organization_quotas/#{org_quota.guid}", nil, admin_headers
          expect(last_response).to have_status_code(422)

          get "/v3/organization_quotas/#{org_quota.guid}", {}, admin_headers
          expect(last_response.status).to eq(200)
        end
      end

      context 'when an organization_quota guid is invalid' do
        it 'returns a 404 with a helpful message' do
          delete '/v3/organization_quotas/fake_org_quota', nil, admin_headers
          expect(last_response).to have_status_code(404)
        end
      end
    end
  end
end

def generate_org_quota_single_response(list_of_orgs)
  {
    guid: organization_quota.guid,
    created_at: iso8601,
    updated_at: iso8601,
    name: organization_quota.name,
    apps: {
      total_memory_in_mb: 20_480,
      per_process_memory_in_mb: nil,
      total_instances: nil,
      per_app_tasks: nil,
      log_rate_limit_in_bytes_per_second: nil
    },
    services: {
      paid_services_allowed: true,
      total_service_instances: 60,
      total_service_keys: nil
    },
    routes: {
      total_routes: 1000,
      total_reserved_ports: 5
    },
    domains: {
      total_domains: nil
    },
    relationships: {
      organizations: {
        data: match_array(list_of_orgs)
      }
    },
    links: {
      self: { href: %r{#{Regexp.escape(link_prefix)}/v3/organization_quotas/#{organization_quota.guid}} }
    }
  }
end

def generate_org_quota_list_response(list_of_orgs, global_read)
  [
    generate_default_org_quota_response(global_read),
    generate_org_quota_single_response(list_of_orgs)
  ]
end

def generate_default_org_quota_response(global_read)
  # our request specs are seeded with an org that uses the default org quota
  # the visibility of this org depends on the user's permissions
  seeded_org_guid = VCAP::CloudController::Organization.where(name: 'the-system_domain-org-name').first.guid
  seeded_org = global_read ? [{ guid: seeded_org_guid }] : []

  default_quota = VCAP::CloudController::QuotaDefinition.default
  {
    guid: default_quota.guid,
    created_at: iso8601,
    updated_at: iso8601,
    name: default_quota.name,
    apps: {
      total_memory_in_mb: 10_240,
      per_process_memory_in_mb: nil,
      total_instances: nil,
      per_app_tasks: nil,
      log_rate_limit_in_bytes_per_second: nil
    },
    services: {
      paid_services_allowed: true,
      total_service_instances: 100,
      total_service_keys: nil
    },
    routes: {
      total_routes: 1000,
      total_reserved_ports: 0
    },
    domains: {
      total_domains: nil
    },
    relationships: {
      organizations: {
        data: seeded_org
      }
    },
    links: {
      self: { href: %r{#{Regexp.escape(link_prefix)}/v3/organization_quotas/#{default_quota.guid}} }
    }
  }
end