diff --git a/ee/lib/api/gitlab_subscriptions/subscriptions.rb b/ee/lib/api/gitlab_subscriptions/subscriptions.rb index d83090a465be613d6a068da798a3095a82085d29..88409981706f64e75e22e34d9a4a15feef9fe6ae 100644 --- a/ee/lib/api/gitlab_subscriptions/subscriptions.rb +++ b/ee/lib/api/gitlab_subscriptions/subscriptions.rb @@ -18,7 +18,6 @@ class Subscriptions < ::API::Base end params do requires :start_date, type: Date, desc: 'The date when subscription was started' - optional :end_date, type: Date, desc: 'End date of subscription' optional :plan_code, type: String, desc: 'Subscription tier code' @@ -44,6 +43,39 @@ class Subscriptions < ::API::Base render_validation_error!(subscription) end end + + desc 'Update the subscription for the namespace' do + success ::API::Entities::GitlabSubscription + end + params do + optional :start_date, type: Date, desc: 'Start date of subscription' + optional :end_date, type: Date, desc: 'End date of subscription' + optional :plan_code, type: String, desc: 'Subscription tier code' + + optional :seats, type: Integer, desc: 'Number of seats in subscription' + optional :max_seats_used, type: Integer, desc: 'Highest number of active users in the last month' + optional :auto_renew, type: Grape::API::Boolean, desc: 'Whether subscription will auto renew on end date' + + optional :trial, type: Grape::API::Boolean, desc: 'Whether the subscription is a trial' + optional :trial_ends_on, type: Date, desc: 'End date of trial' + optional :trial_starts_on, type: Date, desc: 'Start date of trial' + optional :trial_extension_type, type: Integer, desc: 'Whether the trial was extended or reactivated' + end + put ":id/gitlab_subscription" do + subscription = @namespace.gitlab_subscription + + not_found!('GitlabSubscription') unless subscription + + subscription_params = declared_params(include_missing: false) + subscription_params[:trial_starts_on] ||= subscription_params[:start_date] if subscription_params[:trial] + subscription_params[:updated_at] = Time.current + + if subscription.update(subscription_params) + present subscription, with: ::API::Entities::GitlabSubscription + else + render_validation_error!(subscription) + end + end end end end diff --git a/ee/lib/ee/api/namespaces.rb b/ee/lib/ee/api/namespaces.rb index a95682b79b65a23cbe131546fa3da4d956a71bba..883031b05cba4321449ab7d871a49045c7eed72e 100644 --- a/ee/lib/ee/api/namespaces.rb +++ b/ee/lib/ee/api/namespaces.rb @@ -97,31 +97,6 @@ def update_namespace(namespace) present namespace.gitlab_subscription || {}, with: ::API::Entities::GitlabSubscription end - desc 'Update the subscription for the namespace' do - success ::API::Entities::GitlabSubscription - end - params do - use :gitlab_subscription_optional_attributes - end - put ":id/gitlab_subscription", urgency: :low, feature_category: :subscription_management do - authenticated_as_admin! - - namespace = find_namespace!(params[:id]) - subscription = namespace.gitlab_subscription - - not_found!('GitlabSubscription') unless subscription - - subscription_params = declared_params(include_missing: false) - subscription_params[:trial_starts_on] ||= subscription_params[:start_date] if subscription_params[:trial] - subscription_params[:updated_at] = Time.current - - if subscription.update(subscription_params) - present subscription, with: ::API::Entities::GitlabSubscription - else - render_validation_error!(subscription) - end - end - desc 'Creates a storage limit exclusion for a Namespace' do detail 'Creates a Namespaces::Storage::LimitExclusion' success code: 201, model: ::API::Entities::Namespaces::Storage::LimitExclusion diff --git a/ee/spec/requests/api/gitlab_subscriptions/subscriptions_spec.rb b/ee/spec/requests/api/gitlab_subscriptions/subscriptions_spec.rb index 7efe743935673df486ce36ae9ce6b6a620f198cc..92836c962a4dab1fb972b7a7871af09fff3f25af 100644 --- a/ee/spec/requests/api/gitlab_subscriptions/subscriptions_spec.rb +++ b/ee/spec/requests/api/gitlab_subscriptions/subscriptions_spec.rb @@ -3,8 +3,9 @@ require 'spec_helper' RSpec.describe API::GitlabSubscriptions::Subscriptions, :aggregate_failures, feature_category: :plan_provisioning do + let_it_be(:admin) { create(:admin) } + describe 'POST :id/gitlab_subscription', :saas do - let_it_be(:admin) { create(:admin) } let_it_be(:namespace) { create(:namespace) } it_behaves_like 'POST request permissions for admin mode' do @@ -93,4 +94,148 @@ end end end + + describe 'PUT :id/gitlab_subscription', :saas do + let_it_be(:premium_plan) { create(:premium_plan) } + let_it_be(:namespace) { create(:group, name: 'test.test-group.22') } + + let_it_be(:gitlab_subscription) do + create(:gitlab_subscription, namespace: namespace, start_date: '2018-01-01', end_date: '2019-01-01') + end + + it_behaves_like 'PUT request permissions for admin mode' do + let(:path) { "/namespaces/#{namespace.id}/gitlab_subscription" } + let(:current_user) { admin } + let(:params) { { start_date: '2018-01-01', end_date: '2019-01-01', seats: 10, plan_code: 'ultimate' } } + end + + context 'when authenticated as a regular user' do + it 'returns an unauthorized error' do + user = create(:user) + + put api("/namespaces/#{namespace.id}/gitlab_subscription", user, admin_mode: false), params: { seats: 150 } + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + + context 'when authenticated as an admin' do + context 'when namespace is not found' do + it 'returns a 404 error' do + put api("/namespaces/#{non_existing_record_id}/gitlab_subscription", admin, admin_mode: true), + params: { seats: 150 } + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'when namespace does not have a subscription' do + let_it_be(:namespace_2) { create(:group) } + + it 'returns a 404 error' do + put api("/namespaces/#{namespace_2.id}/gitlab_subscription", admin, admin_mode: true), params: { seats: 150 } + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'when namespace is a project namespace' do + it 'returns a 404 error' do + project_namespace = create(:project, namespace: namespace) + + put api("/namespaces/#{project_namespace.id}/gitlab_subscription", admin, admin_mode: true), + params: { seats: 150 } + + expect(response).to have_gitlab_http_status(:not_found) + expect(json_response).to eq('message' => '404 Namespace Not Found') + end + end + + context 'when params are invalid' do + it 'returns a 400 error' do + put api("/namespaces/#{namespace.id}/gitlab_subscription", admin, admin_mode: true), params: { seats: nil } + + expect(response).to have_gitlab_http_status(:bad_request) + end + end + + context 'when params are valid' do + it 'updates the subscription for the group' do + params = { seats: 150, plan_code: 'premium', start_date: '2018-01-01', end_date: '2019-01-01' } + + put api("/namespaces/#{namespace.id}/gitlab_subscription", admin, admin_mode: true), params: params + + expect(response).to have_gitlab_http_status(:ok) + expect(gitlab_subscription.reload.seats).to eq(150) + expect(gitlab_subscription.max_seats_used).to eq(0) + expect(gitlab_subscription.plan_name).to eq('premium') + expect(gitlab_subscription.plan_title).to eq('Premium') + end + + it 'is successful when using full_path routing' do + params = { seats: 150, plan_code: 'premium', start_date: '2018-01-01', end_date: '2019-01-01' } + + put api("/namespaces/#{namespace.full_path}/gitlab_subscription", admin, admin_mode: true), params: params + + expect(response).to have_gitlab_http_status(:ok) + end + + it 'does not clear out existing data because of defaults' do + gitlab_subscription.update!(seats: 20, max_seats_used: 42) + + params = { plan_code: 'premium', start_date: '2018-01-01', end_date: '2019-01-01' } + + put api("/namespaces/#{namespace.id}/gitlab_subscription", admin, admin_mode: true), params: params + + expect(response).to have_gitlab_http_status(:ok) + expect(gitlab_subscription.reload).to have_attributes( + seats: 20, + max_seats_used: 42 + ) + end + + it 'updates the timestamp when the attributes are the same' do + expect do + put api("/namespaces/#{namespace.full_path}/gitlab_subscription", admin, admin_mode: true), + params: namespace.gitlab_subscription.attributes + end.to change { gitlab_subscription.reload.updated_at } + end + + context 'when starting a new term' do + it 'resets the seat attributes for the subscription' do + params = { seats: 150, plan_code: 'premium', start_date: '2018-01-01', end_date: '2019-01-01' } + + gitlab_subscription.update!(seats: 20, max_seats_used: 42, seats_owed: 22) + + new_start = gitlab_subscription.end_date + 1.year + new_end = new_start + 1.year + new_term_params = params.merge(start_date: new_start, end_date: new_end) + + expect(gitlab_subscription.seats_in_use).to eq 0 + + put api("/namespaces/#{namespace.id}/gitlab_subscription", admin, admin_mode: true), params: new_term_params + + expect(response).to have_gitlab_http_status(:ok) + expect(gitlab_subscription.reload).to have_attributes( + max_seats_used: 0, + seats_owed: 0 + ) + end + end + + context 'when updating the trial expiration date' do + it 'updates the trial expiration date' do + date = 30.days.from_now.to_date + + params = { seats: 150, plan_code: 'ultimate', trial_ends_on: date.iso8601 } + + put api("/namespaces/#{namespace.id}/gitlab_subscription", admin, admin_mode: true), params: params + + expect(response).to have_gitlab_http_status(:ok) + expect(gitlab_subscription.reload.trial_ends_on).to eq(date) + end + end + end + end + end end diff --git a/ee/spec/requests/api/namespaces_spec.rb b/ee/spec/requests/api/namespaces_spec.rb index 5501af8d37dd17811f5086e50b6029380c8eb968..65cd7e4652be008a6d819dee6920dc121c2f64fc 100644 --- a/ee/spec/requests/api/namespaces_spec.rb +++ b/ee/spec/requests/api/namespaces_spec.rb @@ -614,143 +614,6 @@ def do_get_subscription(current_user, namespace_id = namespace.id) end end - describe 'PUT :id/gitlab_subscription', :saas do - def do_put(namespace_id, current_user, payload, admin_mode: false) - put api("/namespaces/#{namespace_id}/gitlab_subscription", current_user, admin_mode: admin_mode), params: payload - end - - let_it_be(:premium_plan) { create(:premium_plan) } - let_it_be(:namespace) { create(:group, name: 'test.test-group.22') } - let_it_be(:gitlab_subscription) { create(:gitlab_subscription, namespace: namespace, start_date: '2018/01/01', end_date: '2019/01/01') } - - let(:params) do - { - seats: 150, - plan_code: 'premium', - start_date: '2018/01/01', - end_date: '2019/01/01' - } - end - - it_behaves_like 'PUT request permissions for admin mode' do - let(:path) { "/namespaces/#{namespace.id}/gitlab_subscription" } - end - - context 'when authenticated as a regular user' do - it 'returns an unauthorized error' do - do_put(namespace.id, user, { seats: 150 }) - - expect(response).to have_gitlab_http_status(:forbidden) - end - end - - context 'when authenticated as an admin' do - context 'when namespace is not found' do - it 'returns a 404 error' do - do_put(non_existing_record_id, admin, params, admin_mode: true) - - expect(response).to have_gitlab_http_status(:not_found) - end - end - - context 'when namespace does not have a subscription' do - let_it_be(:namespace_2) { create(:group) } - - it 'returns a 404 error' do - do_put(namespace_2.id, admin, params, admin_mode: true) - - expect(response).to have_gitlab_http_status(:not_found) - end - end - - context 'when namespace is a project namespace' do - it 'returns a 404 error' do - do_put(project_namespace.id, admin, params, admin_mode: true) - - expect(response).to have_gitlab_http_status(:not_found) - expect(json_response).to eq('message' => '404 Namespace Not Found') - end - end - - context 'when params are invalid' do - it 'returns a 400 error' do - do_put(namespace.id, admin, params.merge(seats: nil), admin_mode: true) - - expect(response).to have_gitlab_http_status(:bad_request) - end - end - - context 'when params are valid' do - it 'updates the subscription for the Group' do - do_put(namespace.id, admin, params, admin_mode: true) - - expect(response).to have_gitlab_http_status(:ok) - expect(gitlab_subscription.reload.seats).to eq(150) - expect(gitlab_subscription.max_seats_used).to eq(0) - expect(gitlab_subscription.plan_name).to eq('premium') - expect(gitlab_subscription.plan_title).to eq('Premium') - end - - it 'is successful using full_path when namespace path contains dots' do - do_put(namespace.id, admin, params, admin_mode: true) - - expect(response).to have_gitlab_http_status(:ok) - end - - it 'does not clear out existing data because of defaults' do - gitlab_subscription.update!(seats: 20, max_seats_used: 42) - - do_put(namespace.id, admin, params.except(:seats), admin_mode: true) - - expect(response).to have_gitlab_http_status(:ok) - expect(gitlab_subscription.reload).to have_attributes( - seats: 20, - max_seats_used: 42 - ) - end - - it 'updates the timestamp when the attributes are the same' do - expect do - do_put(namespace.id, admin, gitlab_subscription.attributes, admin_mode: true) - end.to change { gitlab_subscription.reload.updated_at } - end - - context 'when starting a new term' do - it 'resets the seat attributes for the subscription' do - gitlab_subscription.update!(seats: 20, max_seats_used: 42, seats_owed: 22) - - new_start = gitlab_subscription.end_date + 1.year - new_end = new_start + 1.year - new_term_params = params.merge(start_date: new_start, end_date: new_end) - - expect(gitlab_subscription.seats_in_use).to eq 0 - - do_put(namespace.id, admin, new_term_params, admin_mode: true) - - expect(response).to have_gitlab_http_status(:ok) - expect(gitlab_subscription.reload).to have_attributes( - max_seats_used: 0, - seats_owed: 0 - ) - end - end - end - end - - context 'setting the trial expiration date' do - context 'when the attr has a future date' do - it 'updates the trial expiration date' do - date = 30.days.from_now.to_date - - do_put(namespace.id, admin, params.merge(trial_ends_on: date), admin_mode: true) - - expect(response).to have_gitlab_http_status(:ok) - expect(gitlab_subscription.reload.trial_ends_on).to eq(date) - end - end - end - end - describe 'POST :id/storage/limit_exclusion' do def do_post(namespace_id, current_user, payload, admin_mode: false) post api("/namespaces/#{namespace_id}/storage/limit_exclusion", current_user, admin_mode: admin_mode), params: payload