diff --git a/ee/lib/gitlab_subscriptions/api/internal/api.rb b/ee/lib/gitlab_subscriptions/api/internal/api.rb index bdd70aa715ed08f6717c3fc59d008d5553806270..4b451cec58018ebd0f9bf56132b076dea6911ee4 100644 --- a/ee/lib/gitlab_subscriptions/api/internal/api.rb +++ b/ee/lib/gitlab_subscriptions/api/internal/api.rb @@ -4,8 +4,14 @@ module GitlabSubscriptions module API module Internal class API < ::API::Base + helpers GitlabSubscriptions::API::Internal::Helpers + before do - authenticated_as_admin! + if jwt_request? + authenticate_from_jwt! + else + authenticated_as_admin! + end end mount ::GitlabSubscriptions::API::Internal::Members diff --git a/ee/lib/gitlab_subscriptions/api/internal/auth.rb b/ee/lib/gitlab_subscriptions/api/internal/auth.rb new file mode 100644 index 0000000000000000000000000000000000000000..f22e2a458b21e051e78a1601f78d1993f094aeb1 --- /dev/null +++ b/ee/lib/gitlab_subscriptions/api/internal/auth.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +module GitlabSubscriptions + module API + module Internal + class Auth + include Gitlab::Utils::StrongMemoize + + INTERNAL_API_REQUEST_HEADER = 'X-Customers-Dot-Internal-Token' + AUDIENCE = 'gitlab-subscriptions' + SUBJECT = 'customers-dot-internal-api' + + def self.verify_api_request(headers) + token = headers[INTERNAL_API_REQUEST_HEADER] + + new(token: token).decode if token.present? + end + + def initialize(token:) + @token = token + end + + def decode + return unless openid_configuration.present? + return unless jwks.present? + + JWT.decode(token, nil, true, options) + rescue JWT::DecodeError, JWT::ExpiredSignature + nil + end + + private + + attr_reader :token + + def options + { + algorithms: openid_configuration['id_token_signing_alg_values_supported'], + jwks: jwks, + iss: openid_configuration['issuer'], + verify_iss: true, + sub: SUBJECT, + verify_sub: true, + aud: AUDIENCE, + verify_aud: true + } + end + + def jwks + response = Gitlab::HTTP.get(openid_configuration['jwks_uri']) + + return unless response.ok? + + response.parsed_response + end + strong_memoize_attr :jwks + + def openid_configuration + response = Gitlab::HTTP.get( + "#{Gitlab::Routing.url_helpers.subscription_portal_url}/.well-known/openid-configuration" + ) + + return {} unless response.ok? + + response.parsed_response + end + strong_memoize_attr :openid_configuration + end + end + end +end diff --git a/ee/lib/gitlab_subscriptions/api/internal/helpers.rb b/ee/lib/gitlab_subscriptions/api/internal/helpers.rb new file mode 100644 index 0000000000000000000000000000000000000000..9885adf67fcecc15e9025e8809f8a5af32b71369 --- /dev/null +++ b/ee/lib/gitlab_subscriptions/api/internal/helpers.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module GitlabSubscriptions + module API + module Internal + module Helpers + def jwt_request? + headers[Auth::INTERNAL_API_REQUEST_HEADER].present? + end + + def authenticate_from_jwt! + unauthorized! unless Auth.verify_api_request(headers) + end + end + end + end +end diff --git a/ee/lib/gitlab_subscriptions/api/internal/namespaces.rb b/ee/lib/gitlab_subscriptions/api/internal/namespaces.rb index 283d7059264babca462d5f3b1b7bd1ae092ada64..457ff6206a277c8db790a3f3e9a7897bbb0ce304 100644 --- a/ee/lib/gitlab_subscriptions/api/internal/namespaces.rb +++ b/ee/lib/gitlab_subscriptions/api/internal/namespaces.rb @@ -4,7 +4,7 @@ module GitlabSubscriptions module API module Internal class Namespaces < ::API::Base - feature_category :subscription_management + feature_category :plan_provisioning urgency :low namespace :internal do diff --git a/ee/spec/requests/gitlab_subscriptions/api/internal/auth_spec.rb b/ee/spec/requests/gitlab_subscriptions/api/internal/auth_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..d27347ccc67305aa15f19e977dc81835dafa4a27 --- /dev/null +++ b/ee/spec/requests/gitlab_subscriptions/api/internal/auth_spec.rb @@ -0,0 +1,153 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe GitlabSubscriptions::API::Internal::Auth, :aggregate_failures, :api, feature_category: :plan_provisioning do + describe '.verify_api_request' do + let_it_be(:internal_api_jwk) { ::JWT::JWK.new(OpenSSL::PKey.generate_key('RSA')) } + let_it_be(:unrelated_jwk) { ::JWT::JWK.new(OpenSSL::PKey.generate_key('RSA')) } + + context 'when the request does not have the internal token header' do + it 'returns nil' do + headers = { 'Other-Header' => 'test-token' } + + expect(described_class.verify_api_request(headers)).to be_nil + end + end + + context 'when the open ID configuration cannot be fetched' do + it 'returns nil' do + stub_open_id_configuration(success: false, json: {}) + + token = generate_token(jwk: internal_api_jwk, payload: jwt_payload) + + expect(described_class.verify_api_request({ 'X-Customers-Dot-Internal-Token' => token })).to be_nil + end + end + + context 'when the JWKS cannot be fetched' do + it 'returns nil' do + stub_open_id_configuration + stub_keys_discovery(success: false) + + token = generate_token(jwk: internal_api_jwk, payload: jwt_payload) + + expect(described_class.verify_api_request({ 'X-Customers-Dot-Internal-Token' => token })).to be_nil + end + end + + context 'when the JWKs can be fetched from the subscription portal', :freeze_time do + before do + stub_open_id_configuration + stub_keys_discovery(jwks: [unrelated_jwk, internal_api_jwk]) + end + + context 'when the token has the wrong issuer' do + it 'returns nil' do + token = generate_token( + jwk: internal_api_jwk, + payload: jwt_payload(iss: 'some-other-issuer') + ) + + expect(described_class.verify_api_request({ 'X-Customers-Dot-Internal-Token' => token })).to be_nil + end + end + + context 'when the token has the wrong subject' do + it 'returns nil' do + token = generate_token( + jwk: internal_api_jwk, + payload: jwt_payload(sub: 'some-other-subject') + ) + + expect(described_class.verify_api_request({ 'X-Customers-Dot-Internal-Token' => token })).to be_nil + end + end + + context 'when the token has the wrong audience' do + it 'returns nil' do + token = generate_token( + jwk: internal_api_jwk, + payload: jwt_payload(aud: 'some-other-audience') + ) + + expect(described_class.verify_api_request({ 'X-Customers-Dot-Internal-Token' => token })).to be_nil + end + end + + context 'when the token has expired' do + it 'returns nil' do + token = generate_token( + jwk: internal_api_jwk, + payload: jwt_payload(iat: 10.minutes.ago.to_i, exp: 5.minutes.ago.to_i) + ) + + expect(described_class.verify_api_request({ 'X-Customers-Dot-Internal-Token' => token })).to be_nil + end + end + + context 'when the token cannot be decoded using the CustomersDot JWKs' do + it 'returns nil' do + token = generate_token( + jwk: ::JWT::JWK.new(OpenSSL::PKey.generate_key('RSA')), + payload: jwt_payload + ) + + expect(described_class.verify_api_request({ 'X-Customers-Dot-Internal-Token' => token })).to be_nil + end + end + + context 'when the token can be decoded using CustomersDot JWKs' do + it 'returns the decoded JWT' do + token = generate_token(jwk: internal_api_jwk, payload: jwt_payload) + + expect(described_class.verify_api_request({ 'X-Customers-Dot-Internal-Token' => token })).to match_array( + [jwt_payload.stringify_keys, { 'typ' => 'JWT', 'kid' => internal_api_jwk.kid, 'alg' => 'RS256' }] + ) + end + end + end + + def generate_token(jwk:, payload:) + JWT.encode(payload, jwk.keypair, 'RS256', { typ: 'JWT', kid: jwk.kid }) + end + + def jwt_payload(**options) + { + aud: 'gitlab-subscriptions', + sub: 'customers-dot-internal-api', + iss: "#{Gitlab::Routing.url_helpers.subscription_portal_url}/", + exp: (Time.current.to_i + 5.minutes.to_i) + }.merge(options) + end + + def stub_open_id_configuration(success: true, json: nil) + subscriptions_host = Gitlab::Routing.url_helpers.subscription_portal_url + response_json = json || { + 'issuer' => "#{subscriptions_host}/", + 'jwks_uri' => "#{subscriptions_host}/oauth/discovery/keys", + 'id_token_signing_alg_values_supported' => ['RS256'] + } + + gitlab_http_response = instance_double(HTTParty::Response, ok?: success, parsed_response: response_json) + + allow(Gitlab::HTTP) + .to receive(:get) + .with("#{subscriptions_host}/.well-known/openid-configuration") + .and_return(gitlab_http_response) + end + + def stub_keys_discovery(success: true, jwks: []) + response_json = { + 'keys' => jwks.map { |jwk| jwk.export.merge('use' => 'sig', 'alg' => 'RS256') } + } + + gitlab_http_response = instance_double(HTTParty::Response, ok?: success, parsed_response: response_json) + + allow(Gitlab::HTTP) + .to receive(:get) + .with("#{Gitlab::Routing.url_helpers.subscription_portal_url}/oauth/discovery/keys") + .and_return(gitlab_http_response) + end + end +end diff --git a/ee/spec/requests/gitlab_subscriptions/api/internal/helpers_spec.rb b/ee/spec/requests/gitlab_subscriptions/api/internal/helpers_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..cea8ec650202b26a57dba3ff3dbaddf7ac1958b3 --- /dev/null +++ b/ee/spec/requests/gitlab_subscriptions/api/internal/helpers_spec.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe GitlabSubscriptions::API::Internal::Helpers, feature_category: :plan_provisioning do + using RSpec::Parameterized::TableSyntax + + subject(:helper) { Class.new.include(described_class).new } + + describe '#jwt_request?' do + context 'when the headers do not contain the subscription portal JWT token' do + it 'returns false' do + allow(helper).to receive(:headers).and_return({ 'Authorization' => 'Bearer test' }) + + expect(helper.jwt_request?).to eq(false) + end + end + + context 'when the headers contain the subscription portal JWT token' do + it 'returns true' do + allow(helper).to receive(:headers).and_return({ 'X-Customers-Dot-Internal-Token' => 'test-token' }) + + expect(helper.jwt_request?).to eq(true) + end + end + end + + describe '#authenticate_from_jwt!' do + let(:jwt_token_headers) { { 'X-Customers-Dot-Internal-Token' => 'test-token' } } + + before do + allow(helper).to receive(:headers).and_return(jwt_token_headers) + end + + context 'when the request cannot be verified with the subscription portal JWT token' do + it 'returns an unauthorised error' do + allow(helper).to receive(:unauthorized!).and_raise('unauthorized') + + allow(GitlabSubscriptions::API::Internal::Auth) + .to receive(:verify_api_request) + .with(jwt_token_headers) + .and_return(nil) + + expect { helper.authenticate_from_jwt! }.to raise_error('unauthorized') + end + end + + context 'when the request can be verified with the subscription portal JWT token' do + it 'does not return an error' do + allow(GitlabSubscriptions::API::Internal::Auth) + .to receive(:verify_api_request) + .with(jwt_token_headers) + .and_return(['decoded-token']) + + expect(helper.authenticate_from_jwt!).to eq(nil) + end + end + end +end diff --git a/ee/spec/requests/gitlab_subscriptions/api/internal/members_spec.rb b/ee/spec/requests/gitlab_subscriptions/api/internal/members_spec.rb index 28bc4673f9fe0a4514a5231d6ab50cfed54160c6..b963ccc1d6e898c6dd7fab70650fe8d23ee6437b 100644 --- a/ee/spec/requests/gitlab_subscriptions/api/internal/members_spec.rb +++ b/ee/spec/requests/gitlab_subscriptions/api/internal/members_spec.rb @@ -4,38 +4,30 @@ RSpec.describe GitlabSubscriptions::API::Internal::Members, :aggregate_failures, :api, feature_category: :subscription_management do describe 'GET /internal/gitlab_subscriptions/namespaces/:id/owners', :saas do - let_it_be(:namespace) { create(:group) } - let(:namespace_id) { namespace.id } - let(:owners_path) { "/internal/gitlab_subscriptions/namespaces/#{namespace_id}/owners" } + include GitlabSubscriptions::InternalApiHelpers - context 'when unauthenticated' do - it 'returns authentication error' do - get api(owners_path) + let_it_be(:namespace) { create(:group) } - expect(response).to have_gitlab_http_status(:unauthorized) - end + def owners_path(namespace_id) + internal_api("namespaces/#{namespace_id}/owners") end - context 'when authenticated as user' do + context 'when unauthenticated' do it 'returns authentication error' do - get api(owners_path, create(:user)) + get owners_path(namespace.id) - expect(response).to have_gitlab_http_status(:forbidden) + expect(response).to have_gitlab_http_status(:unauthorized) end end - context 'when authenticated as admin' do - let_it_be(:admin) { create(:admin) } - - subject(:get_owners) do - get api(owners_path, admin, admin_mode: true) + context 'when authenticated as the subscription portal' do + before do + stub_internal_api_authentication end context 'when the namespace cannot be found' do - let(:namespace_id) { -1 } - it 'returns an error response' do - get_owners + get owners_path(non_existing_record_id), headers: internal_api_headers expect(response).to have_gitlab_http_status(:not_found) expect(json_response['message']).to eq('404 Group Namespace Not Found') @@ -44,7 +36,7 @@ context 'when the namespace does not have any owners' do it 'returns an empty response' do - get_owners + get owners_path(namespace.id), headers: internal_api_headers expect(response).to have_gitlab_http_status(:ok) expect(json_response).to be_empty @@ -93,7 +85,7 @@ 'X-Total-Pages' => '1' } - get_owners + get owners_path(namespace.id), headers: internal_api_headers expect(response).to have_gitlab_http_status(:ok) expect(response.headers).to match(hash_including(expected_pagination_headers)) @@ -108,7 +100,7 @@ end it 'does not return inactive users' do - get_owners + get owners_path(namespace.id), headers: internal_api_headers expect(response).to have_gitlab_http_status(:ok) expect(json_response.count).to eq(1) @@ -117,5 +109,109 @@ end end end + + # this method of authentication is deprecated and will be removed in + # https://gitlab.com/gitlab-org/gitlab/-/issues/473625 + context 'when authenticating with a personal access token' do + def owners_path(namespace_id) + "/internal/gitlab_subscriptions/namespaces/#{namespace_id}/owners" + end + + context 'when authenticated as user' do + it 'returns authentication error' do + get api(owners_path(namespace.id), create(:user)) + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + + context 'when authenticated as admin' do + let_it_be(:admin) { create(:admin) } + + context 'when the namespace cannot be found' do + it 'returns an error response' do + get api(owners_path(non_existing_record_id), admin, admin_mode: true) + + expect(response).to have_gitlab_http_status(:not_found) + expect(json_response['message']).to eq('404 Group Namespace Not Found') + end + end + + context 'when the namespace does not have any owners' do + it 'returns an empty response' do + get api(owners_path(namespace.id), admin, admin_mode: true) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to be_empty + end + end + + context 'when the namespace has owners and other members' do + let_it_be(:owner_1) { create(:user) } + let_it_be(:owner_2) { create(:user) } + let_it_be(:maintainer) { create(:user) } + let_it_be(:guest) { create(:user) } + + let_it_be(:sub_group_owner) { create(:user) } + let_it_be(:sub_group) { create(:group, parent: namespace) } + + before_all do + namespace.add_owner(owner_1) + namespace.add_owner(owner_2) + + namespace.add_maintainer(maintainer) + namespace.add_guest(guest) + + sub_group.add_owner(sub_group_owner) + end + + it 'returns only direct owners of the namespace' do + expected_response = [ + { + 'user' => { 'id' => owner_1.id, 'username' => a_kind_of(String), 'name' => a_kind_of(String) }, + 'access_level' => 50, + 'notification_email' => a_kind_of(String) + }, + { + 'user' => { 'id' => owner_2.id, 'username' => a_kind_of(String), 'name' => a_kind_of(String) }, + 'access_level' => 50, + 'notification_email' => a_kind_of(String) + } + ] + + expected_pagination_headers = { + 'X-Per-Page' => '20', + 'X-Page' => '1', + 'X-Next-Page' => '', + 'X-Prev-Page' => '', + 'X-Total' => '2', + 'X-Total-Pages' => '1' + } + + get api(owners_path(namespace.id), admin, admin_mode: true) + + expect(response).to have_gitlab_http_status(:ok) + expect(response.headers).to match(hash_including(expected_pagination_headers)) + + expect(json_response.count).to eq(2) + expect(json_response).to match_array(expected_response) + end + + context 'when the owner is inactive' do + before do + owner_2.block! + end + + it 'does not return inactive users' do + get api(owners_path(namespace.id), admin, admin_mode: true) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response.count).to eq(1) + expect(json_response.first['user']['id']).to eq(owner_1.id) + end + end + end + end + end end end diff --git a/ee/spec/requests/gitlab_subscriptions/api/internal/namespaces_spec.rb b/ee/spec/requests/gitlab_subscriptions/api/internal/namespaces_spec.rb index 5559c3f414f19e73d0007f58f8e477ba8705f912..44e01ebf9cba0eb448706bc5f1257c6f87cc9061 100644 --- a/ee/spec/requests/gitlab_subscriptions/api/internal/namespaces_spec.rb +++ b/ee/spec/requests/gitlab_subscriptions/api/internal/namespaces_spec.rb @@ -2,369 +2,618 @@ require 'spec_helper' -RSpec.describe GitlabSubscriptions::API::Internal::Namespaces, :aggregate_failures, :api, feature_category: :subscription_management do +RSpec.describe GitlabSubscriptions::API::Internal::Namespaces, :saas, :aggregate_failures, :api, feature_category: :plan_provisioning do include AfterNextHelpers + include GitlabSubscriptions::InternalApiHelpers def namespace_path(namespace_id) - "/internal/gitlab_subscriptions/namespaces/#{namespace_id}" + internal_api("namespaces/#{namespace_id}") end describe 'GET /internal/gitlab_subscriptions/namespaces/:id' do - let_it_be(:admin) { create(:admin) } let_it_be(:namespace) { create(:group) } context 'when unauthenticated' do it 'returns an error response' do - get api(namespace_path(namespace.id)) + get namespace_path(namespace.id) expect(response).to have_gitlab_http_status(:unauthorized) end end - context 'when the user is not an admin' do - it 'returns an error response' do - user = create(:user) + context 'when authenticated as the subscription portal' do + before do + stub_internal_api_authentication + end - get api(namespace_path(namespace.id), user) + context 'when the namespace cannot be found' do + it 'returns an error response' do + get namespace_path(non_existing_record_id), headers: internal_api_headers - expect(response).to have_gitlab_http_status(:forbidden) + expect(response).to have_gitlab_http_status(:not_found) + end end - end - context 'when the admin is not in admin mode' do - it 'returns an error response' do - get api(namespace_path(namespace.id), admin, admin_mode: false) + context 'when fetching a group namespace' do + it 'successfully returns the namespace attributes' do + get namespace_path(namespace.id), headers: internal_api_headers - expect(response).to have_gitlab_http_status(:forbidden) + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to eq({ + 'id' => namespace.id, + 'kind' => 'group', + 'name' => namespace.name, + 'parent_id' => nil, + 'path' => namespace.path, + 'full_path' => namespace.full_path, + 'avatar_url' => nil, + 'plan' => 'free', + 'projects_count' => 0, + 'root_repository_size' => nil, + 'shared_runners_minutes_limit' => nil, + 'trial' => false, + 'trial_ends_on' => nil, + 'web_url' => namespace.web_url, + 'additional_purchased_storage_size' => 0, + 'additional_purchased_storage_ends_on' => nil, + 'billable_members_count' => 0, + 'extra_shared_runners_minutes_limit' => nil, + 'members_count_with_descendants' => 0 + }) + end end - end - context 'when the namespace cannot be found' do - it 'returns an error response' do - get api(namespace_path('0'), admin, admin_mode: true) + context 'when fetching a user namespace' do + it 'successfully returns the namespace attributes' do + user_namespace = create(:user, :with_namespace).namespace - expect(response).to have_gitlab_http_status(:not_found) + get namespace_path(user_namespace.id), headers: internal_api_headers + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to match( + 'id' => user_namespace.id, + 'kind' => 'user', + 'name' => user_namespace.name, + 'parent_id' => nil, + 'path' => user_namespace.path, + 'full_path' => user_namespace.full_path, + 'avatar_url' => user_namespace.avatar_url, + 'plan' => 'free', + 'shared_runners_minutes_limit' => nil, + 'trial' => false, + 'trial_ends_on' => nil, + 'web_url' => a_string_including(user_namespace.path), + 'additional_purchased_storage_size' => 0, + 'additional_purchased_storage_ends_on' => nil, + 'billable_members_count' => 1, + 'extra_shared_runners_minutes_limit' => nil + ) + end end end - context 'when the namespace is of a group' do - it 'returns OK status and contains some set of keys' do - get api(namespace_path(namespace.id), admin, admin_mode: true) - - expect(response).to have_gitlab_http_status(:ok) - expect(json_response.keys).to contain_exactly('id', 'kind', 'name', 'path', 'full_path', - 'parent_id', 'members_count_with_descendants', - 'plan', 'shared_runners_minutes_limit', - 'avatar_url', 'web_url', 'trial_ends_on', 'trial', - 'extra_shared_runners_minutes_limit', 'billable_members_count', - 'root_repository_size', 'projects_count', - 'additional_purchased_storage_size', 'additional_purchased_storage_ends_on') + # this method of authentication is deprecated and will be removed in + # https://gitlab.com/gitlab-org/gitlab/-/issues/473625 + context 'when authenticating with an admin personal access token' do + let_it_be(:admin) { create(:admin) } + + def namespace_path(namespace_id) + "/internal/gitlab_subscriptions/namespaces/#{namespace_id}" end - end - context 'when the namespace is of a user' do - let_it_be(:user) { create(:user, :with_namespace) } - let_it_be(:namespace) { user.namespace } + context 'when the user is not an admin' do + it 'returns an error response' do + user = create(:user) - it 'returns OK status and contains some set of keys' do - get api(namespace_path(namespace.id), admin, admin_mode: true) + get api(namespace_path(namespace.id), user) - expect(response).to have_gitlab_http_status(:ok) - expect(json_response.keys).to contain_exactly('id', 'kind', 'name', 'path', 'full_path', - 'parent_id', 'plan', 'shared_runners_minutes_limit', - 'avatar_url', 'web_url', 'trial_ends_on', 'trial', - 'extra_shared_runners_minutes_limit', 'billable_members_count', - 'additional_purchased_storage_size', 'additional_purchased_storage_ends_on') + expect(response).to have_gitlab_http_status(:forbidden) + end end - end - end - describe 'PUT /internal/gitlab_subscriptions/namespaces/:id' do - let(:admin) { create(:admin) } - let(:user) { create(:user) } + context 'when the admin is not in admin mode' do + it 'returns an error response' do + get api(namespace_path(namespace.id), admin, admin_mode: false) - let(:group1) { create(:group, :with_ci_minutes, ci_minutes_used: 1600) } - let_it_be(:group2) { create(:group, :nested) } - let_it_be(:ultimate_plan) { create(:ultimate_plan) } - let_it_be(:project) { create(:project, namespace: group2, name: group2.name, path: group2.path) } - let_it_be(:project_namespace) { project.project_namespace } + expect(response).to have_gitlab_http_status(:forbidden) + end + end - let(:usage) do - ::Ci::Minutes::NamespaceMonthlyUsage.current_month.find_by(namespace_id: group1) - end + context 'when the namespace cannot be found' do + it 'returns an error response' do + get api(namespace_path(non_existing_record_id), admin, admin_mode: true) - let(:params) do - { - shared_runners_minutes_limit: 9001, - additional_purchased_storage_size: 10_000, - additional_purchased_storage_ends_on: Date.today.to_s - } - end + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'when the namespace is of a group' do + it 'returns OK status and contains some set of keys' do + get api(namespace_path(namespace.id), admin, admin_mode: true) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response.keys).to contain_exactly('id', 'kind', 'name', 'path', 'full_path', + 'parent_id', 'members_count_with_descendants', + 'plan', 'shared_runners_minutes_limit', + 'avatar_url', 'web_url', 'trial_ends_on', 'trial', + 'extra_shared_runners_minutes_limit', 'billable_members_count', + 'root_repository_size', 'projects_count', + 'additional_purchased_storage_size', 'additional_purchased_storage_ends_on') + end + end - before do - usage.update!(notification_level: 30) - group1.update!(shared_runners_minutes_limit: 1000, extra_shared_runners_minutes_limit: 500) + context 'when the namespace is of a user' do + it 'returns OK status and contains some set of keys' do + user = create(:user, :with_namespace) + + get api(namespace_path(user.namespace.id), admin, admin_mode: true) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response.keys).to contain_exactly('id', 'kind', 'name', 'path', 'full_path', + 'parent_id', 'plan', 'shared_runners_minutes_limit', + 'avatar_url', 'web_url', 'trial_ends_on', 'trial', + 'extra_shared_runners_minutes_limit', 'billable_members_count', + 'additional_purchased_storage_size', 'additional_purchased_storage_ends_on') + end + end end + end + describe 'PUT /internal/gitlab_subscriptions/namespaces/:id' do context 'when unauthenticated' do it 'returns an error response' do - put api(namespace_path(group1.id)) + put namespace_path(non_existing_record_id) expect(response).to have_gitlab_http_status(:unauthorized) end end - context 'when the user is not an admin' do - it 'returns an error response' do - user = create(:user) + context 'when authenticated as the subscription portal' do + before do + stub_internal_api_authentication + end - put api(namespace_path(group1.id), user) + context 'when the namespace cannot be found' do + it 'returns an error response' do + put namespace_path(non_existing_record_id), headers: internal_api_headers - expect(response).to have_gitlab_http_status(:forbidden) + expect(response).to have_gitlab_http_status(:not_found) + end end - end - context 'when the admin is not in admin mode' do - it 'returns an error response' do - put api(namespace_path(group1.id), admin, admin_mode: false) + context 'when a project namespace ID is passed' do + it 'returns 404' do + project = create(:project) - expect(response).to have_gitlab_http_status(:forbidden) + put namespace_path(project.project_namespace.id), headers: internal_api_headers + + expect(response).to have_gitlab_http_status(:not_found) + expect(json_response).to eq('message' => '404 Namespace Not Found') + end end - end - context 'when the namespace cannot be found' do - it 'returns an error response' do - put api(namespace_path('0'), admin, admin_mode: true) + context 'when updating gitlab subscription data' do + let_it_be(:root_namespace) { create(:namespace_with_plan) } + + it "updates the gitlab_subscription record" do + existing_subscription = root_namespace.gitlab_subscription + + params = { + gitlab_subscription_attributes: { + start_date: '2019-06-01', + end_date: '2020-06-01', + plan_code: 'ultimate', + seats: 20, + max_seats_used: 10, + auto_renew: true, + trial: true, + trial_ends_on: '2019-05-01', + trial_starts_on: '2019-06-01', + trial_extension_type: GitlabSubscription.trial_extension_types[:reactivated] + } + } - expect(response).to have_gitlab_http_status(:not_found) + put namespace_path(root_namespace.id), headers: internal_api_headers, params: params + + expect(root_namespace.reload.gitlab_subscription.reload.seats).to eq 20 + expect(root_namespace.gitlab_subscription).to eq existing_subscription + end + + it 'returns a 400 error with invalid data' do + params = { + gitlab_subscription_attributes: { + start_date: nil, + end_date: '2020-06-01', + plan_code: 'ultimate', + seats: nil, + max_seats_used: 10, + auto_renew: true + } + } + + put namespace_path(root_namespace.id), headers: internal_api_headers, params: params + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['message']).to eq( + "gitlab_subscription.seats" => ["can't be blank"], + "gitlab_subscription.start_date" => ["can't be blank"] + ) + end end - end - context 'when authenticated as admin' do - subject(:request) { put api(namespace_path(group1.id), admin, admin_mode: true), params: params } + describe 'runners minutes limits' do + let_it_be(:root_namespace) do + create( + :group, + :with_ci_minutes, + ci_minutes_used: 1600, + shared_runners_minutes_limit: 1000, + extra_shared_runners_minutes_limit: 500 + ) + end + + context 'when updating the extra_shared_runners_minutes_limit' do + let(:params) { { extra_shared_runners_minutes_limit: 1000 } } + + it 'updates the extra shared runners minutes limit' do + put namespace_path(root_namespace.id), headers: internal_api_headers, params: params + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['extra_shared_runners_minutes_limit']) + .to eq(params[:extra_shared_runners_minutes_limit]) + end + + it 'expires the compute minutes CachedQuota' do + expect_next(Gitlab::Ci::Minutes::CachedQuota).to receive(:expire!) + + put namespace_path(root_namespace.id), headers: internal_api_headers, params: params + end + + it 'resets the current compute minutes notification level' do + usage = ::Ci::Minutes::NamespaceMonthlyUsage.current_month.find_by(namespace_id: root_namespace.id) + usage.update!(notification_level: 30) - let(:group1) { create(:group, :with_ci_minutes, ci_minutes_used: 1600, name: 'Hello.World') } + expect { put namespace_path(root_namespace.id), headers: internal_api_headers, params: params } + .to change { usage.reload.notification_level } + .to(Ci::Minutes::Notification::PERCENTAGES.fetch(:not_set)) + end - it 'updates namespace using full_path when full_path contains dots' do - put api(namespace_path(group1.full_path), admin, admin_mode: true), params: params + it 'refreshes cached data' do + expect(::Ci::Minutes::RefreshCachedDataService) + .to receive(:new) + .with(root_namespace) + .and_call_original - expect(response).to have_gitlab_http_status(:ok) - expect(json_response['shared_runners_minutes_limit']).to eq(params[:shared_runners_minutes_limit]) - expect(json_response['additional_purchased_storage_size']).to eq(params[:additional_purchased_storage_size]) - expect( - json_response['additional_purchased_storage_ends_on'] - ).to eq(params[:additional_purchased_storage_ends_on]) + put namespace_path(root_namespace.id), headers: internal_api_headers, params: params + end + end + + context 'when updating the shared_runners_minutes_limit' do + let(:params) { { shared_runners_minutes_limit: 9000 } } + + it 'expires the compute minutes CachedQuota' do + expect_next(Gitlab::Ci::Minutes::CachedQuota).to receive(:expire!) + + put namespace_path(root_namespace.id), headers: internal_api_headers, params: params + end + + it 'resets the current compute minutes notification level' do + usage = ::Ci::Minutes::NamespaceMonthlyUsage.current_month.find_by(namespace_id: root_namespace.id) + usage.update!(notification_level: 30) + + expect do + put namespace_path(root_namespace.id), headers: internal_api_headers, params: params + end.to change { usage.reload.notification_level } + .to(Ci::Minutes::Notification::PERCENTAGES.fetch(:not_set)) + end + end + + context 'when neither minutes_limit params is provided' do + let(:params) { { plan_code: 'free' } } + + it 'does not expire the compute minutes CachedQuota' do + expect(Gitlab::Ci::Minutes::CachedQuota).not_to receive(:new) + + put namespace_path(root_namespace.id), headers: internal_api_headers, params: params + end + + it 'does not reset the current compute minutes notification level' do + usage = ::Ci::Minutes::NamespaceMonthlyUsage.current_month.find_by(namespace_id: root_namespace.id) + usage.update!(notification_level: 30) + + expect { put namespace_path(root_namespace.id), headers: internal_api_headers, params: params } + .not_to change { usage.reload.notification_level } + end + end end + end - it 'updates namespace using id' do - request + # this method of authentication is deprecated and will be removed in + # https://gitlab.com/gitlab-org/gitlab/-/issues/473625 + context 'when authenticating with an admin personal access token' do + let_it_be(:admin) { create(:admin) } + let(:user) { create(:user) } - expect(response).to have_gitlab_http_status(:ok) - expect(json_response['shared_runners_minutes_limit']).to eq(params[:shared_runners_minutes_limit]) - expect(json_response['additional_purchased_storage_size']).to eq(params[:additional_purchased_storage_size]) - expect( - json_response['additional_purchased_storage_ends_on'] - ).to eq(params[:additional_purchased_storage_ends_on]) + let(:group1) { create(:group, :with_ci_minutes, ci_minutes_used: 1600) } + let_it_be(:group2) { create(:group, :nested) } + let_it_be(:ultimate_plan) { create(:ultimate_plan) } + let_it_be(:project) { create(:project, namespace: group2, name: group2.name, path: group2.path) } + let_it_be(:project_namespace) { project.project_namespace } + + let(:usage) do + ::Ci::Minutes::NamespaceMonthlyUsage.current_month.find_by(namespace_id: group1) end - it 'expires the compute minutes CachedQuota' do - expect_next(Gitlab::Ci::Minutes::CachedQuota).to receive(:expire!) + let(:params) do + { + shared_runners_minutes_limit: 9001, + additional_purchased_storage_size: 10_000, + additional_purchased_storage_ends_on: Date.today.to_s + } + end - request + before do + usage.update!(notification_level: 30) + group1.update!(shared_runners_minutes_limit: 1000, extra_shared_runners_minutes_limit: 500) end - context 'when current compute minutes notification level is set' do - it 'resets the current compute minutes notification level' do - expect do - request - end.to change { usage.reload.notification_level } - .to(Ci::Minutes::Notification::PERCENTAGES.fetch(:not_set)) + def namespace_path(namespace_id) + "/internal/gitlab_subscriptions/namespaces/#{namespace_id}" + end + + context 'when the user is not an admin' do + it 'returns an error response' do + user = create(:user) + + put api(namespace_path(group1.id), user) + + expect(response).to have_gitlab_http_status(:forbidden) end end - shared_examples 'handles monthly usage' do - it 'expires the compute minutes CachedQuota' do - expect_next(Gitlab::Ci::Minutes::CachedQuota).to receive(:expire!) + context 'when the admin is not in admin mode' do + it 'returns an error response' do + put api(namespace_path(group1.id), admin, admin_mode: false) - request + expect(response).to have_gitlab_http_status(:forbidden) end + end - it 'resets the current compute minutes notification level' do - expect do - request - end.to change { usage.reload.notification_level } - .to(Ci::Minutes::Notification::PERCENTAGES.fetch(:not_set)) + context 'when the namespace cannot be found' do + it 'returns an error response' do + put api(namespace_path(non_existing_record_id), admin, admin_mode: true) + + expect(response).to have_gitlab_http_status(:not_found) end end - context 'when request has extra_shared_runners_minutes_limit param' do - before do - params[:extra_shared_runners_minutes_limit] = 1000 - params.delete(:shared_runners_minutes_limit) + context 'when authenticated as admin' do + subject(:request) do + put api(namespace_path(group1.id), admin, admin_mode: true), params: params end - it 'updates the extra shared runners minutes limit' do - request + let(:group1) { create(:group, :with_ci_minutes, ci_minutes_used: 1600, name: 'Hello.World') } + + it 'updates namespace using full_path when full_path contains dots' do + put api(namespace_path(group1.full_path), admin, admin_mode: true), params: params expect(response).to have_gitlab_http_status(:ok) - expect(json_response['extra_shared_runners_minutes_limit']) - .to eq(params[:extra_shared_runners_minutes_limit]) + expect(json_response['shared_runners_minutes_limit']).to eq(params[:shared_runners_minutes_limit]) + expect(json_response['additional_purchased_storage_size']).to eq(params[:additional_purchased_storage_size]) + expect( + json_response['additional_purchased_storage_ends_on'] + ).to eq(params[:additional_purchased_storage_ends_on]) end - it 'updates pending builds data since adding extra minutes the quota is not used up anymore' do - minutes_exceeded = group1.ci_minutes_usage.minutes_used_up? - expect(minutes_exceeded).to eq(true) - - pending_build = create(:ci_pending_build, namespace: group1, minutes_exceeded: minutes_exceeded) - + it 'updates namespace using id' do request - expect(pending_build.reload.minutes_exceeded).to eq(false) + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['shared_runners_minutes_limit']).to eq(params[:shared_runners_minutes_limit]) + expect(json_response['additional_purchased_storage_size']).to eq(params[:additional_purchased_storage_size]) + expect( + json_response['additional_purchased_storage_ends_on'] + ).to eq(params[:additional_purchased_storage_ends_on]) end - it_behaves_like 'handles monthly usage' - end + it 'expires the compute minutes CachedQuota' do + expect_next(Gitlab::Ci::Minutes::CachedQuota).to receive(:expire!) - context 'when shared_runners_minutes_limit param is present' do - before do - params[:shared_runners_minutes_limit] = nil + request end - it_behaves_like 'handles monthly usage' - end + context 'when current compute minutes notification level is set' do + it 'resets the current compute minutes notification level' do + expect do + request + end.to change { usage.reload.notification_level } + .to(Ci::Minutes::Notification::PERCENTAGES.fetch(:not_set)) + end + end - context 'when neither minutes limit params is provided' do - it 'does not expire the compute minutes CachedQuota' do - params.delete(:shared_runners_minutes_limit) - expect(Gitlab::Ci::Minutes::CachedQuota).not_to receive(:new) + shared_examples 'handles monthly usage' do + it 'expires the compute minutes CachedQuota' do + expect_next(Gitlab::Ci::Minutes::CachedQuota).to receive(:expire!) - request + request + end + + it 'resets the current compute minutes notification level' do + expect do + request + end.to change { usage.reload.notification_level } + .to(Ci::Minutes::Notification::PERCENTAGES.fetch(:not_set)) + end end - context 'when current compute minutes notification level is set' do - it 'does not reset the current compute minutes notification level' do + context 'when request has extra_shared_runners_minutes_limit param' do + before do + params[:extra_shared_runners_minutes_limit] = 1000 params.delete(:shared_runners_minutes_limit) + end - expect do - put api(namespace_path(group1.id), admin), params: params - end.not_to change { usage.reload.notification_level } + it 'updates the extra shared runners minutes limit' do + request + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['extra_shared_runners_minutes_limit']) + .to eq(params[:extra_shared_runners_minutes_limit]) end + + it 'updates pending builds data since adding extra minutes the quota is not used up anymore' do + minutes_exceeded = group1.ci_minutes_usage.minutes_used_up? + expect(minutes_exceeded).to eq(true) + + pending_build = create(:ci_pending_build, namespace: group1, minutes_exceeded: minutes_exceeded) + + request + + expect(pending_build.reload.minutes_exceeded).to eq(false) + end + + it_behaves_like 'handles monthly usage' end - end - end - context 'when project namespace is passed' do - it 'returns 404' do - put api(namespace_path(project_namespace.id), admin, admin_mode: true), params: params + context 'when shared_runners_minutes_limit param is present' do + before do + params[:shared_runners_minutes_limit] = nil + end - expect(response).to have_gitlab_http_status(:not_found) - expect(json_response).to eq('message' => '404 Namespace Not Found') - end - end + it_behaves_like 'handles monthly usage' + end - context 'when invalid params' do - where(:attr) do - [ - :shared_runners_minutes_limit, - :additional_purchased_storage_size, - :additional_purchased_storage_ends_on - ] - end + context 'when neither minutes limit params is provided' do + it 'does not expire the compute minutes CachedQuota' do + params.delete(:shared_runners_minutes_limit) + expect(Gitlab::Ci::Minutes::CachedQuota).not_to receive(:new) + + request + end - with_them do - it "returns validation error for #{attr}" do - put api(namespace_path(group1.id), admin, admin_mode: true), params: Hash[attr, 'unknown'] + context 'when current compute minutes notification level is set' do + it 'does not reset the current compute minutes notification level' do + params.delete(:shared_runners_minutes_limit) - expect(response).to have_gitlab_http_status(:bad_request) + expect { put api(namespace_path(group1.id), admin), params: params } + .not_to change { usage.reload.notification_level } + end + end end end - end - [:last_ci_minutes_notification_at, :last_ci_minutes_usage_notification_level].each do |attr| - context "when namespace has a value for #{attr}" do - before do - group1.update_attribute(attr, Time.now) - end + context 'when project namespace is passed' do + it 'returns 404' do + put api(namespace_path(project_namespace.id), admin, admin_mode: true), params: params - it 'resets that value when assigning extra compute minutes' do - expect do - put api(namespace_path(group1.full_path), admin, admin_mode: true), - params: { extra_shared_runners_minutes_limit: 1000 } - end.to change { group1.reload.send(attr) }.to(nil) + expect(response).to have_gitlab_http_status(:not_found) + expect(json_response).to eq('message' => '404 Namespace Not Found') end end - end - context "when customer purchases extra compute minutes" do - it "ticks instance runners" do - runners = Ci::Runner.instance_type + context 'when invalid params' do + where(:attr) do + [ + :shared_runners_minutes_limit, + :additional_purchased_storage_size, + :additional_purchased_storage_ends_on + ] + end - put api(namespace_path(group1.id), admin), params: { extra_shared_runners_minutes_limit: 1000 } + with_them do + it "returns validation error for #{attr}" do + put api(namespace_path(group1.id), admin, admin_mode: true), params: Hash[attr, 'unknown'] - expect(runners).to all(receive(:tick_runner_queue)) + expect(response).to have_gitlab_http_status(:bad_request) + end + end end - end - context "when passing attributes for gitlab_subscription", :saas do - let(:gitlab_subscription) do - { - start_date: '2019-06-01', - end_date: '2020-06-01', - plan_code: 'ultimate', - seats: 20, - max_seats_used: 10, - auto_renew: true, - trial: true, - trial_ends_on: '2019-05-01', - trial_starts_on: '2019-06-01', - trial_extension_type: GitlabSubscription.trial_extension_types[:reactivated] - } + [:last_ci_minutes_notification_at, :last_ci_minutes_usage_notification_level].each do |attr| + context "when namespace has a value for #{attr}" do + before do + group1.update_attribute(attr, Time.now) + end + + it 'resets that value when assigning extra compute minutes' do + expect do + put api(namespace_path(group1.id), admin, admin_mode: true), + params: { extra_shared_runners_minutes_limit: 1000 } + end.to change { group1.reload.send(attr) }.to(nil) + end + end end - it "creates the gitlab_subscription record" do - expect(group1.gitlab_subscription).to be_nil + context "when customer purchases extra compute minutes" do + it "ticks instance runners" do + runners = Ci::Runner.instance_type - put api(namespace_path(group1.id), admin, admin_mode: true), params: { - gitlab_subscription_attributes: gitlab_subscription - } + put api(namespace_path(group1.id), admin), params: { extra_shared_runners_minutes_limit: 1000 } - expect(group1.reload.gitlab_subscription).to have_attributes( - start_date: Date.parse(gitlab_subscription[:start_date]), - end_date: Date.parse(gitlab_subscription[:end_date]), - hosted_plan: instance_of(Plan), - seats: 20, - max_seats_used: 10, - auto_renew: true, - trial: true, - trial_starts_on: Date.parse(gitlab_subscription[:trial_starts_on]), - trial_ends_on: Date.parse(gitlab_subscription[:trial_ends_on]), - trial_extension_type: 'reactivated' - ) + expect(runners).to all(receive(:tick_runner_queue)) + end end - it "updates the gitlab_subscription record" do - existing_subscription = group1.create_gitlab_subscription! - - put api(namespace_path(group1.id), admin, admin_mode: true), params: { - gitlab_subscription_attributes: gitlab_subscription - } + context "when passing attributes for gitlab_subscription", :saas do + let(:gitlab_subscription) do + { + start_date: '2019-06-01', + end_date: '2020-06-01', + plan_code: 'ultimate', + seats: 20, + max_seats_used: 10, + auto_renew: true, + trial: true, + trial_ends_on: '2019-05-01', + trial_starts_on: '2019-06-01', + trial_extension_type: GitlabSubscription.trial_extension_types[:reactivated] + } + end - expect(group1.reload.gitlab_subscription.reload.seats).to eq 20 - expect(group1.gitlab_subscription).to eq existing_subscription - end + it "creates the gitlab_subscription record" do + expect(group1.gitlab_subscription).to be_nil - context 'when params are invalid' do - it 'returns a 400 error' do put api(namespace_path(group1.id), admin, admin_mode: true), params: { - gitlab_subscription_attributes: { start_date: nil, seats: nil } + gitlab_subscription_attributes: gitlab_subscription } - expect(response).to have_gitlab_http_status(:bad_request) - expect(json_response['message']).to eq( - "gitlab_subscription.seats" => ["can't be blank"], - "gitlab_subscription.start_date" => ["can't be blank"] + expect(group1.reload.gitlab_subscription).to have_attributes( + start_date: Date.parse(gitlab_subscription[:start_date]), + end_date: Date.parse(gitlab_subscription[:end_date]), + hosted_plan: instance_of(Plan), + seats: 20, + max_seats_used: 10, + auto_renew: true, + trial: true, + trial_starts_on: Date.parse(gitlab_subscription[:trial_starts_on]), + trial_ends_on: Date.parse(gitlab_subscription[:trial_ends_on]), + trial_extension_type: 'reactivated' ) end + + it "updates the gitlab_subscription record" do + existing_subscription = group1.create_gitlab_subscription! + + put api(namespace_path(group1.id), admin, admin_mode: true), params: { + gitlab_subscription_attributes: gitlab_subscription + } + + expect(group1.reload.gitlab_subscription.reload.seats).to eq 20 + expect(group1.gitlab_subscription).to eq existing_subscription + end + + context 'when params are invalid' do + it 'returns a 400 error' do + put api(namespace_path(group1.id), admin, admin_mode: true), params: { + gitlab_subscription_attributes: { start_date: nil, seats: nil } + } + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['message']).to eq( + "gitlab_subscription.seats" => ["can't be blank"], + "gitlab_subscription.start_date" => ["can't be blank"] + ) + end + end end end end diff --git a/ee/spec/requests/gitlab_subscriptions/api/internal/subscriptions_spec.rb b/ee/spec/requests/gitlab_subscriptions/api/internal/subscriptions_spec.rb index a3d4c3dee4054d6932a7d507b76c362ee3625abc..e333dba3c8380266f75912aafa9054d8944f1127 100644 --- a/ee/spec/requests/gitlab_subscriptions/api/internal/subscriptions_spec.rb +++ b/ee/spec/requests/gitlab_subscriptions/api/internal/subscriptions_spec.rb @@ -4,114 +4,209 @@ RSpec.describe GitlabSubscriptions::API::Internal::Subscriptions, :aggregate_failures, :api, feature_category: :plan_provisioning do describe 'GET /internal/gitlab_subscriptions/namespaces/:id/gitlab_subscription', :saas do - let_it_be(:admin) { create(:admin) } + include GitlabSubscriptions::InternalApiHelpers + let_it_be(:namespace) { create(:group) } def subscription_path(namespace_id) - "/internal/gitlab_subscriptions/namespaces/#{namespace_id}/gitlab_subscription" + internal_api("namespaces/#{namespace_id}/gitlab_subscription") end context 'when unauthenticated' do it 'returns an error response' do - get api(subscription_path(namespace.id)) + get subscription_path(namespace.id) expect(response).to have_gitlab_http_status(:unauthorized) end end - context 'when the user is not an admin' do - it 'returns an error response' do - user = create(:user) + context 'when authenticated as the subscription portal' do + before do + stub_internal_api_authentication + end - get api(subscription_path(namespace.id), user) + context 'when the namespace cannot be found' do + it 'returns an error response' do + get subscription_path(non_existing_record_id), headers: internal_api_headers - expect(response).to have_gitlab_http_status(:forbidden) + expect(response).to have_gitlab_http_status(:not_found) + end end - end - context 'when the admin is not in admin mode' do - it 'returns an error response' do - get api(subscription_path(namespace.id), admin, admin_mode: false) + context 'when the namespace does not have a subscription' do + it 'returns an empty response' do + get subscription_path(namespace.id), headers: internal_api_headers + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response.keys).to match_array(%w[plan usage billing]) + + expect(json_response['plan']).to eq( + 'name' => nil, + 'code' => nil, + 'auto_renew' => nil, + 'trial' => nil, + 'upgradable' => nil, + 'exclude_guests' => nil + ) + + expect(json_response['usage']).to eq( + 'max_seats_used' => nil, + 'seats_in_subscription' => nil, + 'seats_in_use' => nil, + 'seats_owed' => nil + ) + + expect(json_response['billing']).to eq( + 'subscription_start_date' => nil, + 'subscription_end_date' => nil, + 'trial_ends_on' => nil + ) + end + end - expect(response).to have_gitlab_http_status(:forbidden) + context 'when the request is authenticated for a namespace with a subscription' do + it 'returns the subscription data' do + subscription = create( + :gitlab_subscription, + :ultimate, + namespace: namespace, + auto_renew: true, + max_seats_used: 5 + ) + + get subscription_path(namespace.id), headers: internal_api_headers + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response.keys).to match_array(%w[plan usage billing]) + + expect(json_response['plan']).to eq( + 'name' => 'Ultimate', + 'code' => 'ultimate', + 'auto_renew' => true, + 'trial' => false, + 'upgradable' => false, + 'exclude_guests' => true + ) + + expect(json_response['usage']).to eq( + 'max_seats_used' => 5, + 'seats_in_subscription' => 10, + 'seats_in_use' => 0, + 'seats_owed' => 0 + ) + + expect(json_response['billing']).to eq( + 'subscription_start_date' => subscription.start_date.iso8601, + 'subscription_end_date' => subscription.end_date.iso8601, + 'trial_ends_on' => nil + ) + end end end - context 'when the namespace cannot be found' do - it 'returns an error response' do - get api(subscription_path(non_existing_record_id), admin, admin_mode: true) + # this method of authentication is deprecated and will be removed in + # https://gitlab.com/gitlab-org/gitlab/-/issues/473625 + context 'when authenticating with an admin personal access token' do + let_it_be(:admin) { create(:admin) } - expect(response).to have_gitlab_http_status(:not_found) + def subscription_path(namespace_id) + "/internal/gitlab_subscriptions/namespaces/#{namespace_id}/gitlab_subscription" end - end - context 'when the namespace does not have a subscription' do - it 'returns an empty response' do - get api(subscription_path(namespace.id), admin, admin_mode: true) - - expect(response).to have_gitlab_http_status(:ok) - expect(json_response.keys).to match_array(%w[plan usage billing]) - - expect(json_response['plan']).to eq( - 'name' => nil, - 'code' => nil, - 'auto_renew' => nil, - 'trial' => nil, - 'upgradable' => nil, - 'exclude_guests' => nil - ) - - expect(json_response['usage']).to eq( - 'max_seats_used' => nil, - 'seats_in_subscription' => nil, - 'seats_in_use' => nil, - 'seats_owed' => nil - ) - - expect(json_response['billing']).to eq( - 'subscription_start_date' => nil, - 'subscription_end_date' => nil, - 'trial_ends_on' => nil - ) + context 'when the user is not an admin' do + it 'returns an error response' do + user = create(:user) + + get api(subscription_path(namespace.id), user) + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + + context 'when the admin is not in admin mode' do + it 'returns an error response' do + get api(subscription_path(namespace.id), admin, admin_mode: false) + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + + context 'when the namespace cannot be found' do + it 'returns an error response' do + get api(subscription_path(non_existing_record_id), admin, admin_mode: true) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'when the namespace does not have a subscription' do + it 'returns an empty response' do + get api(subscription_path(namespace.id), admin, admin_mode: true) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response.keys).to match_array(%w[plan usage billing]) + + expect(json_response['plan']).to eq( + 'name' => nil, + 'code' => nil, + 'auto_renew' => nil, + 'trial' => nil, + 'upgradable' => nil, + 'exclude_guests' => nil + ) + + expect(json_response['usage']).to eq( + 'max_seats_used' => nil, + 'seats_in_subscription' => nil, + 'seats_in_use' => nil, + 'seats_owed' => nil + ) + + expect(json_response['billing']).to eq( + 'subscription_start_date' => nil, + 'subscription_end_date' => nil, + 'trial_ends_on' => nil + ) + end end - end - context 'when the request is authenticated for a namespace with a subscription' do - it 'returns the subscription data' do - subscription = create( - :gitlab_subscription, - :ultimate, - namespace: namespace, - auto_renew: true, - max_seats_used: 5 - ) - - get api(subscription_path(namespace.id), admin, admin_mode: true) - - expect(response).to have_gitlab_http_status(:ok) - expect(json_response.keys).to match_array(%w[plan usage billing]) - - expect(json_response['plan']).to eq( - 'name' => 'Ultimate', - 'code' => 'ultimate', - 'auto_renew' => true, - 'trial' => false, - 'upgradable' => false, - 'exclude_guests' => true - ) - - expect(json_response['usage']).to eq( - 'max_seats_used' => 5, - 'seats_in_subscription' => 10, - 'seats_in_use' => 0, - 'seats_owed' => 0 - ) - - expect(json_response['billing']).to eq( - 'subscription_start_date' => subscription.start_date.iso8601, - 'subscription_end_date' => subscription.end_date.iso8601, - 'trial_ends_on' => nil - ) + context 'when the request is authenticated for a namespace with a subscription' do + it 'returns the subscription data' do + subscription = create( + :gitlab_subscription, + :ultimate, + namespace: namespace, + auto_renew: true, + max_seats_used: 5 + ) + + get api(subscription_path(namespace.id), admin, admin_mode: true) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response.keys).to match_array(%w[plan usage billing]) + + expect(json_response['plan']).to eq( + 'name' => 'Ultimate', + 'code' => 'ultimate', + 'auto_renew' => true, + 'trial' => false, + 'upgradable' => false, + 'exclude_guests' => true + ) + + expect(json_response['usage']).to eq( + 'max_seats_used' => 5, + 'seats_in_subscription' => 10, + 'seats_in_use' => 0, + 'seats_owed' => 0 + ) + + expect(json_response['billing']).to eq( + 'subscription_start_date' => subscription.start_date.iso8601, + 'subscription_end_date' => subscription.end_date.iso8601, + 'trial_ends_on' => nil + ) + end end end end diff --git a/ee/spec/requests/gitlab_subscriptions/api/internal/upcoming_reconciliations_spec.rb b/ee/spec/requests/gitlab_subscriptions/api/internal/upcoming_reconciliations_spec.rb index a5013cd1df6085c9972e47eb3155135e908db853..fada96d84f4e7b8412653f54e51a1dfe899e64e1 100644 --- a/ee/spec/requests/gitlab_subscriptions/api/internal/upcoming_reconciliations_spec.rb +++ b/ee/spec/requests/gitlab_subscriptions/api/internal/upcoming_reconciliations_spec.rb @@ -3,90 +3,137 @@ require 'spec_helper' RSpec.describe GitlabSubscriptions::API::Internal::UpcomingReconciliations, :aggregate_failures, :api, feature_category: :subscription_management do + include GitlabSubscriptions::InternalApiHelpers + before do stub_saas_features(gitlab_com_subscriptions: true) stub_application_setting(check_namespace_plan: true) end + def upcoming_reconciliations_path(namespace_id) + internal_api("namespaces/#{namespace_id}/upcoming_reconciliations") + end + describe 'PUT /internal/gitlab_subscriptions/namespaces/:namespace_id/upcoming_reconciliations' do + let_it_be(:namespace) { create(:group) } + context 'when unauthenticated' do it 'returns authentication error' do - put api('/internal/gitlab_subscriptions/namespaces/1/upcoming_reconciliations') + put upcoming_reconciliations_path(namespace.id) expect(response).to have_gitlab_http_status(:unauthorized) end end - context 'when authenticated as user' do - let_it_be(:user) { create(:user) } + context 'when authenticated as the subscription portal' do + before do + stub_internal_api_authentication + end - it 'returns authentication error' do - put api('/internal/gitlab_subscriptions/namespaces/1/upcoming_reconciliations', user) + context 'when supplied valid params' do + it 'updates the upcoming reconciliation' do + params = { + next_reconciliation_date: Date.today + 5.days, + display_alert_from: Date.today - 2.days + } - expect(response).to have_gitlab_http_status(:forbidden) + expect { put upcoming_reconciliations_path(namespace.id), headers: internal_api_headers, params: params } + .to change { namespace.reload.upcoming_reconciliation } + .to be_present + + expect(response).to have_gitlab_http_status(:ok) + end + end + + context 'when supplied invalid params' do + it 'returns an error' do + params = { + next_reconciliation_date: nil, + display_alert_from: Date.today - 2.days + } + + put upcoming_reconciliations_path(namespace.id), headers: internal_api_headers, params: params + + expect(response).to have_gitlab_http_status(:internal_server_error) + expect(json_response['message']['error']).to include "Next reconciliation date can't be blank" + end end end - context 'when authenticated as admin' do - let_it_be(:default_organization) { create(:organization, :default) } - let_it_be(:admin) { create(:admin) } - let_it_be(:namespace) { create(:namespace) } - let(:namespace_id) { namespace.id } + # this method of authentication is deprecated and will be removed in + # https://gitlab.com/gitlab-org/gitlab/-/issues/473625 + context 'when authenticating with an admin personal access token' do let(:path) { "/internal/gitlab_subscriptions/namespaces/#{namespace_id}/upcoming_reconciliations" } + let(:namespace_id) { namespace.id } + + context 'when authenticated as user' do + let_it_be(:user) { create(:user) } + + it 'returns authentication error' do + put api(path, user) - let(:params) do - { - next_reconciliation_date: Date.today + 5.days, - display_alert_from: Date.today - 2.days - } + expect(response).to have_gitlab_http_status(:forbidden) + end end - it_behaves_like 'PUT request permissions for admin mode' do + context 'when authenticated as admin' do + let_it_be(:default_organization) { create(:organization, :default) } + let_it_be(:admin) { create(:admin) } + let(:params) do { next_reconciliation_date: Date.today + 5.days, display_alert_from: Date.today - 2.days } end - end - subject(:put_upcoming_reconciliations) do - put api(path, admin, admin_mode: true), params: params - end + it_behaves_like 'PUT request permissions for admin mode' do + let(:params) do + { + next_reconciliation_date: Date.today + 5.days, + display_alert_from: Date.today - 2.days + } + end + end - it 'returns success' do - put_upcoming_reconciliations + subject(:put_upcoming_reconciliations) do + put api(path, admin, admin_mode: true), params: params + end - expect(response).to have_gitlab_http_status(:ok) - end + it 'returns success' do + put_upcoming_reconciliations - context 'when update service failed' do - let(:error_message) { 'update_service_error' } + expect(response).to have_gitlab_http_status(:ok) + end - before do - allow_next_instance_of(::UpcomingReconciliations::UpdateService) do |service| - allow(service).to receive(:execute).and_return(ServiceResponse.error(message: error_message)) + context 'when update service failed' do + let(:error_message) { 'update_service_error' } + + before do + allow_next_instance_of(::UpcomingReconciliations::UpdateService) do |service| + allow(service).to receive(:execute).and_return(ServiceResponse.error(message: error_message)) + end end - end - it 'returns error' do - put_upcoming_reconciliations + it 'returns error' do + put_upcoming_reconciliations - expect(response).to have_gitlab_http_status(:internal_server_error) - expect(json_response.dig('message', 'error')).to eq(error_message) + expect(response).to have_gitlab_http_status(:internal_server_error) + expect(json_response.dig('message', 'error')).to eq(error_message) + end end - end - context 'when not gitlab.com' do - before do - stub_saas_features(gitlab_com_subscriptions: false) - end + context 'when not gitlab.com' do + before do + stub_saas_features(gitlab_com_subscriptions: false) + end - it 'returns 403 error' do - put_upcoming_reconciliations + it 'returns 403 error' do + put_upcoming_reconciliations - expect(response).to have_gitlab_http_status(:forbidden) - expect(json_response['message']).to eq('403 Forbidden - This API is gitlab.com only!') + expect(response).to have_gitlab_http_status(:forbidden) + expect(json_response['message']).to eq('403 Forbidden - This API is gitlab.com only!') + end end end end @@ -94,68 +141,103 @@ describe 'DELETE /internal/gitlab_subscriptions/namespaces/:namespace_id/upcoming_reconciliations' do let_it_be(:namespace) { create(:namespace) } - let(:path) { "/internal/gitlab_subscriptions/namespaces/#{namespace.id}/upcoming_reconciliations" } - it_behaves_like 'DELETE request permissions for admin mode' do - before do - create(:upcoming_reconciliation, namespace_id: namespace.id) - end - end - - context 'when the request is not authenticated' do + context 'when unauthenticated' do it 'returns authentication error' do - delete api(path) + delete upcoming_reconciliations_path(namespace.id) expect(response).to have_gitlab_http_status(:unauthorized) end end - context 'when authenticated as user' do - it 'returns authentication error' do - user = create(:user) + context 'when authenticated as the subscription portal' do + subject(:delete_upcoming_reconciliation) do + delete upcoming_reconciliations_path(namespace.id), headers: internal_api_headers + end - expect { delete api(path, user) } - .not_to change { GitlabSubscriptions::UpcomingReconciliation.count } + before do + stub_internal_api_authentication + end + + context 'when there is an upcoming reconciliation for the namespace' do + it 'destroys the reconciliation and returns success' do + create(:upcoming_reconciliation, namespace_id: namespace.id) - expect(response).to have_gitlab_http_status(:forbidden) + expect { delete_upcoming_reconciliation } + .to change { ::GitlabSubscriptions::UpcomingReconciliation.where(namespace_id: namespace.id).count } + .by(-1) + + expect(response).to have_gitlab_http_status(:no_content) + end + end + + context 'when the namespace_id does not have an upcoming reconciliation' do + it 'returns a not found error' do + expect { delete_upcoming_reconciliation }.not_to change { GitlabSubscriptions::UpcomingReconciliation.count } + + expect(response).to have_gitlab_http_status(:not_found) + end end end - context 'when authenticated as an admin' do - let_it_be(:admin) { create(:admin) } + # this method of authentication is deprecated and will be removed in + # https://gitlab.com/gitlab-org/gitlab/-/issues/473625 + context 'when authenticating with an admin personal access token' do + let(:path) { "/internal/gitlab_subscriptions/namespaces/#{namespace.id}/upcoming_reconciliations" } - context 'when the request is not for .com' do + it_behaves_like 'DELETE request permissions for admin mode' do before do - stub_saas_features(gitlab_com_subscriptions: false) + create(:upcoming_reconciliation, namespace_id: namespace.id) end + end - it 'returns an error' do - expect { delete api(path, admin, admin_mode: true) } + context 'when authenticated as user' do + it 'returns authentication error' do + user = create(:user) + + expect { delete api(path, user) } .not_to change { GitlabSubscriptions::UpcomingReconciliation.count } expect(response).to have_gitlab_http_status(:forbidden) - expect(response.body).to include('403 Forbidden - This API is gitlab.com only!') end end - context 'when there is an upcoming reconciliation for the namespace' do - it 'destroys the reconciliation and returns success' do - create(:upcoming_reconciliation, namespace_id: namespace.id) + context 'when authenticated as an admin' do + let_it_be(:admin) { create(:admin) } - expect { delete api(path, admin, admin_mode: true) } - .to change { ::GitlabSubscriptions::UpcomingReconciliation.where(namespace_id: namespace.id).count } - .by(-1) + context 'when the request is not for .com' do + before do + stub_saas_features(gitlab_com_subscriptions: false) + end - expect(response).to have_gitlab_http_status(:no_content) + it 'returns an error' do + expect { delete api(path, admin, admin_mode: true) } + .not_to change { GitlabSubscriptions::UpcomingReconciliation.count } + + expect(response).to have_gitlab_http_status(:forbidden) + expect(response.body).to include('403 Forbidden - This API is gitlab.com only!') + end end - end - context 'when the namespace_id does not have an upcoming reconciliation' do - it 'returns a not found error' do - expect { delete api(path, admin, admin_mode: true) } - .not_to change { GitlabSubscriptions::UpcomingReconciliation.count } + context 'when there is an upcoming reconciliation for the namespace' do + it 'destroys the reconciliation and returns success' do + create(:upcoming_reconciliation, namespace_id: namespace.id) - expect(response).to have_gitlab_http_status(:not_found) + expect { delete api(path, admin, admin_mode: true) } + .to change { ::GitlabSubscriptions::UpcomingReconciliation.where(namespace_id: namespace.id).count } + .by(-1) + + expect(response).to have_gitlab_http_status(:no_content) + end + end + + context 'when the namespace_id does not have an upcoming reconciliation' do + it 'returns a not found error' do + expect { delete api(path, admin, admin_mode: true) } + .not_to change { GitlabSubscriptions::UpcomingReconciliation.count } + + expect(response).to have_gitlab_http_status(:not_found) + end end end end diff --git a/ee/spec/requests/gitlab_subscriptions/api/internal/users_spec.rb b/ee/spec/requests/gitlab_subscriptions/api/internal/users_spec.rb index e5529a88d554921cb4ada9bbe2c4bf665fdb86c0..2fe00ff1fc59bf1fba01b96271a233397b996d52 100644 --- a/ee/spec/requests/gitlab_subscriptions/api/internal/users_spec.rb +++ b/ee/spec/requests/gitlab_subscriptions/api/internal/users_spec.rb @@ -3,53 +3,85 @@ require 'spec_helper' RSpec.describe GitlabSubscriptions::API::Internal::Users, :aggregate_failures, :api, feature_category: :subscription_management do + include GitlabSubscriptions::InternalApiHelpers + describe 'GET /internal/gitlab_subscriptions/users/:id' do let_it_be(:user) { create(:user) } - let(:user_id) { user.id } - let(:user_path) { "/internal/gitlab_subscriptions/users/#{user_id}" } + + def users_path(user_id) + internal_api("users/#{user_id}") + end context 'when unauthenticated' do it 'returns authentication error' do - get api(user_path) + get users_path(user.id) expect(response).to have_gitlab_http_status(:unauthorized) end end - context 'when authenticated as user' do - it 'returns authentication error' do - get api(user_path, create(:user)) + context 'when authenticated as the subscription portal' do + before do + stub_internal_api_authentication + end + + context 'when the user exists' do + it 'returns success' do + get users_path(user.id), headers: internal_api_headers + + expect(response).to have_gitlab_http_status(:ok) + + expect(json_response["id"]).to eq(user.id) + expect(json_response.keys).to eq(%w[id username name web_url]) + end + end + + context 'when user does not exists' do + it 'returns not found' do + get users_path(non_existing_record_id), headers: internal_api_headers - expect(response).to have_gitlab_http_status(:forbidden) + expect(response).to have_gitlab_http_status(:not_found) + expect(json_response['message']).to eq("404 User Not Found") + end end end - context 'when authenticated as admin' do - let_it_be(:admin) { create(:admin) } + # this method of authentication is deprecated and will be removed in + # https://gitlab.com/gitlab-org/gitlab/-/issues/473625 + context 'when authenticating with an admin personal access token' do + def users_path(user_id) + "/internal/gitlab_subscriptions/users/#{user_id}" + end - subject(:get_user) do - get api(user_path, admin, admin_mode: true) + context 'when authenticated as user' do + it 'returns authentication error' do + get api(users_path(user.id), create(:user)) + + expect(response).to have_gitlab_http_status(:forbidden) + end end - it 'returns success' do - get_user + context 'when authenticated as admin' do + let_it_be(:admin) { create(:admin) } - expected_attributes = %w[id username name web_url] + it 'returns success' do + get api(users_path(user.id), admin, admin_mode: true) - expect(response).to have_gitlab_http_status(:ok) + expected_attributes = %w[id username name web_url] - expect(json_response["id"]).to eq(user_id) - expect(json_response.keys).to eq(expected_attributes) - end + expect(response).to have_gitlab_http_status(:ok) - context 'when user does not exists' do - let(:user_id) { -1 } + expect(json_response["id"]).to eq(user.id) + expect(json_response.keys).to eq(expected_attributes) + end - it 'returns not found' do - get_user + context 'when user does not exists' do + it 'returns not found' do + get api(users_path(non_existing_record_id), admin, admin_mode: true) - expect(response).to have_gitlab_http_status(:not_found) - expect(json_response['message']).to eq("404 User Not Found") + expect(response).to have_gitlab_http_status(:not_found) + expect(json_response['message']).to eq("404 User Not Found") + end end end end @@ -60,35 +92,27 @@ let_it_be(:user) { create(:user) } def user_permissions_path(namespace_id, user_id) - "/internal/gitlab_subscriptions/namespaces/#{namespace_id}/user_permissions/#{user_id}" + internal_api("namespaces/#{namespace_id}/user_permissions/#{user_id}") end context 'when unauthenticated' do it 'returns an authentication error' do - get api(user_permissions_path(namespace.id, user.id)) + get user_permissions_path(namespace.id, user.id) expect(response).to have_gitlab_http_status(:unauthorized) end end - context 'when authenticated as a non-admin user' do - it 'returns an authentication error' do - non_admin = create(:user) - - get api(user_permissions_path(namespace.id, user.id), non_admin) - - expect(response).to have_gitlab_http_status(:forbidden) + context 'when authenticated as the subscription portal' do + before do + stub_internal_api_authentication end - end - - context 'when authenticated as an admin' do - let_it_be(:admin) { create(:admin) } context 'when the user can manage the namespace billing' do it 'returns true for edit_billing' do namespace.add_owner(user) - get api(user_permissions_path(namespace.id, user.id), admin, admin_mode: true) + get user_permissions_path(namespace.id, user.id), headers: internal_api_headers expect(response).to have_gitlab_http_status(:ok) expect(json_response['edit_billing']).to be true @@ -97,7 +121,7 @@ def user_permissions_path(namespace_id, user_id) context 'when the user cannot manage the namespace billing' do it 'returns false for edit_billing' do - get api(user_permissions_path(namespace.id, user.id), admin, admin_mode: true) + get user_permissions_path(namespace.id, user.id), headers: internal_api_headers expect(response).to have_gitlab_http_status(:ok) expect(json_response['edit_billing']).to be false @@ -106,7 +130,7 @@ def user_permissions_path(namespace_id, user_id) context 'when the namespace does not exist' do it 'returns a not found response' do - get api(user_permissions_path(non_existing_record_id, user.id), admin, admin_mode: true) + get user_permissions_path(non_existing_record_id, user.id), headers: internal_api_headers expect(response).to have_gitlab_http_status(:not_found) end @@ -114,11 +138,69 @@ def user_permissions_path(namespace_id, user_id) context 'when the user does not exist' do it 'returns a not found response' do - get api(user_permissions_path(namespace.id, non_existing_record_id), admin, admin_mode: true) + get user_permissions_path(namespace.id, non_existing_record_id), headers: internal_api_headers expect(response).to have_gitlab_http_status(:not_found) end end end + + # this method of authentication is deprecated and will be removed in + # https://gitlab.com/gitlab-org/gitlab/-/issues/473625 + context 'when authenticating with an admin personal access token' do + def user_permissions_path(namespace_id, user_id) + "/internal/gitlab_subscriptions/namespaces/#{namespace_id}/user_permissions/#{user_id}" + end + + context 'when authenticated as a non-admin user' do + it 'returns an authentication error' do + non_admin = create(:user) + + get api(user_permissions_path(namespace.id, user.id), non_admin) + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + + context 'when authenticated as an admin' do + let_it_be(:admin) { create(:admin) } + + context 'when the user can manage the namespace billing' do + it 'returns true for edit_billing' do + namespace.add_owner(user) + + get api(user_permissions_path(namespace.id, user.id), admin, admin_mode: true) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['edit_billing']).to be true + end + end + + context 'when the user cannot manage the namespace billing' do + it 'returns false for edit_billing' do + get api(user_permissions_path(namespace.id, user.id), admin, admin_mode: true) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['edit_billing']).to be false + end + end + + context 'when the namespace does not exist' do + it 'returns a not found response' do + get api(user_permissions_path(non_existing_record_id, user.id), admin, admin_mode: true) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'when the user does not exist' do + it 'returns a not found response' do + get api(user_permissions_path(namespace.id, non_existing_record_id), admin, admin_mode: true) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end + end end end diff --git a/spec/support/helpers/gitlab_subscriptions/internal_api_helpers.rb b/spec/support/helpers/gitlab_subscriptions/internal_api_helpers.rb new file mode 100644 index 0000000000000000000000000000000000000000..c8625c2416956ed7ee2940afb4f524546fe7bbe1 --- /dev/null +++ b/spec/support/helpers/gitlab_subscriptions/internal_api_helpers.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module GitlabSubscriptions + module InternalApiHelpers + def internal_api(path) + "/api/#{::API::API.version}/internal/gitlab_subscriptions/#{path}" + end + + def internal_api_headers + { 'X-Customers-Dot-Internal-Token' => 'internal-api-token' } + end + + def stub_internal_api_authentication + allow(GitlabSubscriptions::API::Internal::Auth) + .to receive(:verify_api_request) + .with(hash_including(**internal_api_headers)) + .and_return(['decoded-token']) + end + end +end