diff --git a/.rubocop_todo/style/inline_disable_annotation.yml b/.rubocop_todo/style/inline_disable_annotation.yml index 09522a41e4af4ae3517434ba537d6c542c6e3fbe..03a466bbf704c0ad7d03dd596e51f9cfdc910265 100644 --- a/.rubocop_todo/style/inline_disable_annotation.yml +++ b/.rubocop_todo/style/inline_disable_annotation.yml @@ -1462,7 +1462,6 @@ Style/InlineDisableAnnotation: - 'ee/app/services/epics/update_dates_service.rb' - 'ee/app/services/epics/update_service.rb' - 'ee/app/services/geo/container_repository_sync_service.rb' - - 'ee/app/services/gitlab_subscriptions/add_on_purchases/self_managed/provision_code_suggestions_service.rb' - 'ee/app/services/gitlab_subscriptions/add_on_purchases/update_service.rb' - 'ee/app/services/gitlab_subscriptions/notify_seats_exceeded_batch_service.rb' - 'ee/app/services/gitlab_subscriptions/preview_billable_user_change_service.rb' @@ -1909,7 +1908,6 @@ Style/InlineDisableAnnotation: - 'ee/spec/services/geo/framework_repository_sync_service_spec.rb' - 'ee/spec/services/geo/registry_consistency_service_spec.rb' - 'ee/spec/services/geo/registry_update_service_spec.rb' - - 'ee/spec/services/gitlab_subscriptions/add_on_purchases/self_managed/provision_code_suggestions_service_spec.rb' - 'ee/spec/services/gitlab_subscriptions/preview_billable_user_change_service_spec.rb' - 'ee/spec/services/merge_requests/update_blocks_service_spec.rb' - 'ee/spec/services/package_metadata/sync_service_spec.rb' diff --git a/ee/app/models/gitlab_subscriptions/add_on_purchase.rb b/ee/app/models/gitlab_subscriptions/add_on_purchase.rb index d4bea1fc3486cb10ddbe3c2288ef3e34e45a302f..7075970fece186d8f95026a2838b514f517c5377 100644 --- a/ee/app/models/gitlab_subscriptions/add_on_purchase.rb +++ b/ee/app/models/gitlab_subscriptions/add_on_purchase.rb @@ -46,6 +46,10 @@ class AddOnPurchase < ApplicationRecord .limit(limit) end + def self.find_by_namespace_and_add_on(namespace, add_on) + find_by(namespace: namespace, add_on: add_on) + end + def self.next_candidate_requiring_assigned_users_refresh requiring_assigned_users_refresh(1) .order('last_assigned_users_refreshed_at ASC NULLS FIRST') diff --git a/ee/app/services/gitlab_subscriptions/activate_service.rb b/ee/app/services/gitlab_subscriptions/activate_service.rb index b578f9a3b8e8cf4784e2e5b24ed8b7d0d3852ebd..0c4c703bcdac5be7470ac192d6e3b379418a7811 100644 --- a/ee/app/services/gitlab_subscriptions/activate_service.rb +++ b/ee/app/services/gitlab_subscriptions/activate_service.rb @@ -72,7 +72,7 @@ def application_settings end def update_code_suggestions_add_on_purchase - ::GitlabSubscriptions::AddOnPurchases::SelfManaged::ProvisionCodeSuggestionsService.new.execute + ::GitlabSubscriptions::AddOnPurchases::SelfManaged::ProvisionServices::CodeSuggestions.new.execute end end end diff --git a/ee/app/services/gitlab_subscriptions/add_on_purchases/self_managed/base_provision_service.rb b/ee/app/services/gitlab_subscriptions/add_on_purchases/self_managed/base_provision_service.rb new file mode 100644 index 0000000000000000000000000000000000000000..2c25da0f5865f18b339f60f1f3401b599a8a4998 --- /dev/null +++ b/ee/app/services/gitlab_subscriptions/add_on_purchases/self_managed/base_provision_service.rb @@ -0,0 +1,107 @@ +# frozen_string_literal: true + +module GitlabSubscriptions + module AddOnPurchases + module SelfManaged + class BaseProvisionService + include ::Gitlab::Utils::StrongMemoize + + AddOnPurchaseSyncError = Class.new(StandardError) + MethodNotImplementedError = Class.new(StandardError) + + def execute + result = license_has_add_on? ? create_or_update_add_on_purchase : expire_prior_add_on_purchase + + unless result.success? + raise AddOnPurchaseSyncError, "Error syncing subscription add-on purchases. Message: #{result[:message]}" + end + + result + rescue AddOnPurchaseSyncError => e + Gitlab::ErrorTracking.track_and_raise_for_dev_exception(e) + + ServiceResponse.error(message: e.message) + end + + private + + def license_has_add_on? + current_license&.online_cloud_license? && quantity > 0 + end + + def current_license + License.current + end + strong_memoize_attr :current_license + + def license_restrictions + current_license&.license&.restrictions + end + + def empty_success_response + ServiceResponse.success(payload: { add_on_purchase: nil }) + end + + def create_or_update_add_on_purchase + service_class = if add_on_purchase + GitlabSubscriptions::AddOnPurchases::UpdateService + else + GitlabSubscriptions::AddOnPurchases::CreateService + end + + service_class.new(namespace, add_on, attributes).execute + end + + def add_on_purchase + GitlabSubscriptions::AddOnPurchase.find_by_namespace_and_add_on(namespace, add_on) + end + strong_memoize_attr :add_on_purchase + + def add_on + GitlabSubscriptions::AddOn.find_or_create_by_name(name) + end + strong_memoize_attr :add_on + + def namespace + nil # self-managed is unrelated to namespaces + end + + def attributes + { + add_on_purchase: add_on_purchase, + expires_on: expires_on, + purchase_xid: purchase_xid, + quantity: quantity + } + end + + def expire_prior_add_on_purchase + return empty_success_response unless add_on_purchase + + GitlabSubscriptions::AddOnPurchases::SelfManaged::ExpireService.new(add_on_purchase).execute + end + + def purchase_xid + license_restrictions&.dig(:subscription_name) + end + + def expires_on + current_license&.block_changes_at || current_license&.expires_at + end + + def quantity + quantity_from_restrictions(license_restrictions) if license_restrictions + end + strong_memoize_attr :quantity + + def name + raise MethodNotImplementedError + end + + def quantity_from_restrictions(_) + raise MethodNotImplementedError + end + end + end + end +end diff --git a/ee/app/services/gitlab_subscriptions/add_on_purchases/self_managed/provision_code_suggestions_service.rb b/ee/app/services/gitlab_subscriptions/add_on_purchases/self_managed/provision_code_suggestions_service.rb deleted file mode 100644 index 434743f523a5da313866555d529d606b567fd876..0000000000000000000000000000000000000000 --- a/ee/app/services/gitlab_subscriptions/add_on_purchases/self_managed/provision_code_suggestions_service.rb +++ /dev/null @@ -1,87 +0,0 @@ -# frozen_string_literal: true - -module GitlabSubscriptions - module AddOnPurchases - module SelfManaged - class ProvisionCodeSuggestionsService - include ::Gitlab::Utils::StrongMemoize - - AddOnPurchaseSyncError = Class.new(StandardError) - - def execute - result = license_has_duo_pro? ? create_or_update_add_on_purchase : expire_prior_add_on_purchase - - unless result.success? - raise AddOnPurchaseSyncError, "Error syncing subscription add-on purchases. Message: #{result[:message]}" - end - - result - rescue AddOnPurchaseSyncError => e - Gitlab::ErrorTracking.track_and_raise_for_dev_exception(e) - - ServiceResponse.error(message: e.message) - end - - private - - def license_has_duo_pro? - current_license&.online_cloud_license? && license_restrictions[:code_suggestions_seat_count].to_i > 0 - end - - def current_license - License.current - end - strong_memoize_attr :current_license - - def license_restrictions - current_license.license.restrictions - end - strong_memoize_attr :license_restrictions - - def empty_success_response - ServiceResponse.success(payload: { add_on_purchase: nil }) - end - - def create_or_update_add_on_purchase - service_class = if gitlab_duo_pro_add_on_purchase - GitlabSubscriptions::AddOnPurchases::UpdateService - else - GitlabSubscriptions::AddOnPurchases::CreateService - end - - service_class.new(namespace, gitlab_duo_pro_add_on, add_on_purchase_attributes).execute - end - - # rubocop: disable CodeReuse/ActiveRecord - def gitlab_duo_pro_add_on_purchase - GitlabSubscriptions::AddOnPurchase.find_by(namespace: namespace, add_on: gitlab_duo_pro_add_on) - end - strong_memoize_attr :gitlab_duo_pro_add_on_purchase - # rubocop: enable CodeReuse/ActiveRecord - - def gitlab_duo_pro_add_on - GitlabSubscriptions::AddOn.find_or_create_by_name(:code_suggestions) - end - strong_memoize_attr :gitlab_duo_pro_add_on - - def namespace - nil - end - - def add_on_purchase_attributes - { - quantity: license_restrictions[:code_suggestions_seat_count], - expires_on: current_license.block_changes_at || current_license.expires_at, - purchase_xid: license_restrictions[:subscription_name] - }.merge({ add_on_purchase: gitlab_duo_pro_add_on_purchase }.compact) - end - - def expire_prior_add_on_purchase - return empty_success_response unless gitlab_duo_pro_add_on_purchase - - GitlabSubscriptions::AddOnPurchases::SelfManaged::ExpireService.new(gitlab_duo_pro_add_on_purchase).execute - end - end - end - end -end diff --git a/ee/app/services/gitlab_subscriptions/add_on_purchases/self_managed/provision_services/code_suggestions.rb b/ee/app/services/gitlab_subscriptions/add_on_purchases/self_managed/provision_services/code_suggestions.rb new file mode 100644 index 0000000000000000000000000000000000000000..91e1157d4d633ccd68aab5f32bd38ac68c7521b6 --- /dev/null +++ b/ee/app/services/gitlab_subscriptions/add_on_purchases/self_managed/provision_services/code_suggestions.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module GitlabSubscriptions + module AddOnPurchases + module SelfManaged + module ProvisionServices + class CodeSuggestions < BaseProvisionService + private + + def quantity_from_restrictions(restrictions) + restrictions[:code_suggestions_seat_count].to_i + end + + def name + :code_suggestions + end + end + end + end + end +end diff --git a/ee/app/services/licenses/destroy_service.rb b/ee/app/services/licenses/destroy_service.rb index 356d1f431ad9ddf2dce0ebf2b7f1b13be1a2b72e..9f8d2c0add9937df3eb22eefb3222fe9b7fcd16f 100644 --- a/ee/app/services/licenses/destroy_service.rb +++ b/ee/app/services/licenses/destroy_service.rb @@ -23,7 +23,7 @@ def clear_future_subscriptions end def update_code_suggestions_add_on_purchase - ::GitlabSubscriptions::AddOnPurchases::SelfManaged::ProvisionCodeSuggestionsService.new.execute + ::GitlabSubscriptions::AddOnPurchases::SelfManaged::ProvisionServices::CodeSuggestions.new.execute end end end diff --git a/ee/app/workers/sync_seat_link_request_worker.rb b/ee/app/workers/sync_seat_link_request_worker.rb index d0ebecba5e9066b9602ae3cd9617f107b59fafbb..f88b71a88f5f609f80e5245cecb0ddf449ef0ddf 100644 --- a/ee/app/workers/sync_seat_link_request_worker.rb +++ b/ee/app/workers/sync_seat_link_request_worker.rb @@ -72,7 +72,7 @@ def request_error_message(response) end def update_code_suggestions_add_on_purchase - ::GitlabSubscriptions::AddOnPurchases::SelfManaged::ProvisionCodeSuggestionsService.new.execute + ::GitlabSubscriptions::AddOnPurchases::SelfManaged::ProvisionServices::CodeSuggestions.new.execute end def update_reconciliation!(response) diff --git a/ee/spec/factories/gitlab_subscriptions/add_ons.rb b/ee/spec/factories/gitlab_subscriptions/add_ons.rb index f079a92d7483d007107188b458cd5c656049b262..07c616743f286ae0388728ea440f0253a82f9f97 100644 --- a/ee/spec/factories/gitlab_subscriptions/add_ons.rb +++ b/ee/spec/factories/gitlab_subscriptions/add_ons.rb @@ -5,6 +5,11 @@ name { GitlabSubscriptions::AddOn.names[:code_suggestions] } description { GitlabSubscriptions::AddOn.descriptions[:code_suggestions] } + trait :code_suggestions do + name { GitlabSubscriptions::AddOn.names[:code_suggestions] } + description { GitlabSubscriptions::AddOn.descriptions[:code_suggestions] } + end + trait :gitlab_duo_pro do name { GitlabSubscriptions::AddOn.names[:code_suggestions] } end diff --git a/ee/spec/models/gitlab_subscriptions/add_on_purchase_spec.rb b/ee/spec/models/gitlab_subscriptions/add_on_purchase_spec.rb index f647897b2765fee9c3574e911575574204939c2c..97a0eeb09a9b58e369f7787a25a5a30f3cba8072 100644 --- a/ee/spec/models/gitlab_subscriptions/add_on_purchase_spec.rb +++ b/ee/spec/models/gitlab_subscriptions/add_on_purchase_spec.rb @@ -396,6 +396,26 @@ expect(described_class.requiring_assigned_users_refresh(1).size).to eq 1 end end + + describe '.find_by_namespace_and_add_on' do + subject(:find_by_namespace_and_add_on) { described_class.find_by_namespace_and_add_on } + + let(:namespace) { create(:group) } + + let(:add_on_1) { create(:gitlab_subscription_add_on, :code_suggestions) } + let(:add_on_2) { create(:gitlab_subscription_add_on, :product_analytics) } + + let!(:add_on_purchase_1) { create(:gitlab_subscription_add_on_purchase, namespace: nil, add_on: add_on_1) } + let!(:add_on_purchase_2) { create(:gitlab_subscription_add_on_purchase, namespace: namespace, add_on: add_on_1) } + + let!(:add_on_purchase_3) { create(:gitlab_subscription_add_on_purchase, namespace: nil, add_on: add_on_2) } + let!(:add_on_purchase_4) { create(:gitlab_subscription_add_on_purchase, namespace: namespace, add_on: add_on_2) } + + it 'filters by namespace and add-on' do + expect(described_class.find_by_namespace_and_add_on(nil, add_on_1)).to eq add_on_purchase_1 + expect(described_class.find_by_namespace_and_add_on(namespace, add_on_1)).to eq add_on_purchase_2 + end + end end describe '.uniq_add_on_names' do diff --git a/ee/spec/services/gitlab_subscriptions/add_on_purchases/self_managed/base_provision_service_spec.rb b/ee/spec/services/gitlab_subscriptions/add_on_purchases/self_managed/base_provision_service_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..a9a226931468676d48e86f635a880c8d96ec4662 --- /dev/null +++ b/ee/spec/services/gitlab_subscriptions/add_on_purchases/self_managed/base_provision_service_spec.rb @@ -0,0 +1,155 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe GitlabSubscriptions::AddOnPurchases::SelfManaged::BaseProvisionService, + :aggregate_failures, feature_category: :plan_provisioning do + describe '#execute' do + it { expect { described_class.new.execute }.to raise_error described_class::MethodNotImplementedError } + + context 'with child class insufficient implemented' do + let!(:current_license) { create_current_license(cloud_licensing_enabled: true) } + + let(:provision_dummy_add_on_service_class) do + Class.new(described_class) do + def name + # One of the enums for name of GitlabSubscriptions::AddOn + :code_suggestions + end + end + end + + specify do + expect { provision_dummy_add_on_service_class.new.execute } + .to raise_error described_class::MethodNotImplementedError + end + end + + context 'with child class' do + subject(:result) { provision_dummy_add_on_service_class.new.execute } + + let_it_be(:add_on) { create(:gitlab_subscription_add_on) } + let_it_be(:default_organization) { create(:organization, :default) } + let_it_be(:namespace) { nil } + let_it_be(:quantity) { 5 } + let_it_be(:subscription_name) { 'A-S00000002' } + + let!(:current_license) do + create_current_license( + cloud_licensing_enabled: true, + restrictions: { + subscription_name: subscription_name + } + ) + end + + let(:provision_dummy_add_on_service_class) do + quantity_from_restrictions = quantity + + Class.new(described_class) do + define_method :quantity_from_restrictions do |_| + quantity_from_restrictions + end + + def name + # One of the enums for name of GitlabSubscriptions::AddOn + :code_suggestions + end + end + end + + context 'without a current license', :without_license do + let!(:current_license) { nil } + + it_behaves_like 'provision service expires add-on purchase' + end + + context 'when current license is not a cloud license' do + let!(:current_license) do + create_current_license( + cloud_licensing_enabled: true, + offline_cloud_licensing_enabled: true + ) + end + + it_behaves_like 'provision service expires add-on purchase' + end + + context 'when current license does not contain a code suggestions add-on purchase' do + let_it_be(:quantity) { 0 } + + it_behaves_like 'provision service expires add-on purchase' + end + + context 'when add-on record does not exist' do + before do + GitlabSubscriptions::AddOn.destroy_all # rubocop: disable Cop/DestroyAll -- clean-up + end + + it 'creates the add-on record' do + expect { result }.to change { GitlabSubscriptions::AddOn.count }.by(1) + end + end + + context 'when add-on purchase exists' do + let(:expiration_date) { Date.current + 3.months } + let!(:existing_add_on_purchase) do + create( + :gitlab_subscription_add_on_purchase, + namespace: namespace, + add_on: add_on, + expires_on: expiration_date + ) + end + + context 'when the update fails' do + it_behaves_like 'provision service handles error', GitlabSubscriptions::AddOnPurchases::UpdateService + end + + context 'when existing add-on purchase is expired' do + let(:expiration_date) { Date.current - 3.months } + + it_behaves_like 'provision service updates the existing add-on purchase' + end + + it_behaves_like 'provision service updates the existing add-on purchase' + end + + context 'when the creation fails' do + it_behaves_like 'provision service handles error', GitlabSubscriptions::AddOnPurchases::CreateService + end + + context 'when the license has no block_changes_at set' do + let!(:current_license) do + create_current_license( + block_changes_at: nil, + cloud_licensing_enabled: true, + restrictions: { + code_suggestions_seat_count: quantity, + subscription_name: subscription_name + } + ) + end + + it 'uses expires_at from license' do + expect(GitlabSubscriptions::AddOnPurchases::CreateService).to receive(:new).with( + namespace, + add_on, + { + add_on_purchase: nil, + quantity: quantity, + expires_on: current_license.expires_at, + purchase_xid: subscription_name + } + ).and_call_original + + expect(result[:add_on_purchase]).to have_attributes( + expires_on: current_license.expires_at + ) + end + end + + it_behaves_like 'provision service creates add-on purchase' + end + end +end diff --git a/ee/spec/services/gitlab_subscriptions/add_on_purchases/self_managed/provision_code_suggestions_service_spec.rb b/ee/spec/services/gitlab_subscriptions/add_on_purchases/self_managed/provision_code_suggestions_service_spec.rb deleted file mode 100644 index 6b1c03181480c4d33d0abb1c76ee63cb139cc137..0000000000000000000000000000000000000000 --- a/ee/spec/services/gitlab_subscriptions/add_on_purchases/self_managed/provision_code_suggestions_service_spec.rb +++ /dev/null @@ -1,233 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe GitlabSubscriptions::AddOnPurchases::SelfManaged::ProvisionCodeSuggestionsService, - :aggregate_failures, feature_category: :plan_provisioning do - describe '#execute' do - let_it_be(:add_on) { create(:gitlab_subscription_add_on) } - let_it_be(:namespace) { nil } - - let!(:current_license) do - create_current_license( - cloud_licensing_enabled: true, - restrictions: { code_suggestions_seat_count: purchased_add_on_quantity, subscription_name: subscription_name } - ) - end - - let_it_be(:default_organization) { create(:organization, :default) } - - let(:purchased_add_on_quantity) { 5 } - let(:subscription_name) { 'A-S00000002' } - - subject(:result) { described_class.new.execute } - - shared_examples 'empty success response' do - it 'returns a success' do - expect(result[:status]).to eq(:success) - expect(result[:add_on_purchase]).to eq(nil) - end - end - - shared_examples 'handle error' do |service_class| - it 'logs and returns an error' do - allow_next_instance_of(service_class) do |service| - allow(service).to receive(:execute).and_return(ServiceResponse.error(message: 'Something went wrong')) - end - - expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception) - expect(result[:status]).to eq(:error) - expect(result[:message]).to eq('Error syncing subscription add-on purchases. Message: Something went wrong') - end - end - - shared_examples 'expire add-on purchase' do - context 'with existing add-on purchase' do - let_it_be(:expiration_date) { Date.current + 3.months } - let_it_be(:existing_add_on_purchase) do - create( - :gitlab_subscription_add_on_purchase, - namespace: namespace, - add_on: add_on, - expires_on: expiration_date - ) - end - - it 'does not call any service to create or update an add-on purchase' do - expect(GitlabSubscriptions::AddOnPurchases::CreateService).not_to receive(:new) - expect(GitlabSubscriptions::AddOnPurchases::UpdateService).not_to receive(:new) - - result - end - - context 'when the expiration fails' do - it_behaves_like 'handle error', GitlabSubscriptions::AddOnPurchases::SelfManaged::ExpireService - end - - it 'expires the existing add-on purchase' do - expect do - result - existing_add_on_purchase.reload - end.to change { existing_add_on_purchase.expires_on }.from(expiration_date).to(Date.yesterday) - end - - it_behaves_like 'empty success response' - end - - context 'without existing add-on purchase' do - it 'does not call any of the services to update an add-on purchase' do - expect(GitlabSubscriptions::AddOnPurchases::CreateService).not_to receive(:new) - expect(GitlabSubscriptions::AddOnPurchases::UpdateService).not_to receive(:new) - expect(GitlabSubscriptions::AddOnPurchases::SelfManaged::ExpireService).not_to receive(:new) - - result - end - - it_behaves_like 'empty success response' - end - end - - context 'without a current license' do - let!(:current_license) { nil } - - it_behaves_like 'expire add-on purchase' - end - - context 'when current license is not a cloud license' do - let!(:current_license) do - create_current_license(cloud_licensing_enabled: true, offline_cloud_licensing_enabled: true) - end - - it_behaves_like 'expire add-on purchase' - end - - context 'when current license does not contain a code suggestions add-on purchase' do - let!(:current_license) do - create_current_license(cloud_licensing_enabled: true, restrictions: { subscription_name: subscription_name }) - end - - it_behaves_like 'expire add-on purchase' - end - - context 'when add-on record does not exist' do - before do - GitlabSubscriptions::AddOn.destroy_all # rubocop: disable Cop/DestroyAll - end - - it 'creates the add-on record' do - expect { result }.to change { GitlabSubscriptions::AddOn.count }.by(1) - end - end - - context 'when add-on purchase exists' do - let(:expiration_date) { Date.current + 3.months } - let!(:existing_add_on_purchase) do - create( - :gitlab_subscription_add_on_purchase, - namespace: namespace, - add_on: add_on, - expires_on: expiration_date - ) - end - - shared_examples 'updates the existing add-on purchase' do - it 'updates the existing add-on purchase' do - expect(GitlabSubscriptions::AddOnPurchases::UpdateService).to receive(:new) - .with( - namespace, - add_on, - { - add_on_purchase: existing_add_on_purchase, - quantity: purchased_add_on_quantity, - expires_on: current_license.block_changes_at, - purchase_xid: subscription_name - } - ).and_call_original - - expect { result }.not_to change { GitlabSubscriptions::AddOnPurchase.count } - - expect(current_license.block_changes_at).to eq(current_license.expires_at + 14.days) - expect(result[:status]).to eq(:success) - expect(result[:add_on_purchase]).to have_attributes( - id: existing_add_on_purchase.id, - expires_on: current_license.block_changes_at, - quantity: purchased_add_on_quantity, - purchase_xid: subscription_name - ) - end - end - - context 'when the update fails' do - it_behaves_like 'handle error', GitlabSubscriptions::AddOnPurchases::UpdateService - end - - context 'when existing add-on purchase is expired' do - let(:expiration_date) { Date.current - 3.months } - - it_behaves_like 'updates the existing add-on purchase' - end - - it_behaves_like 'updates the existing add-on purchase' - end - - context 'when the creation fails' do - it_behaves_like 'handle error', GitlabSubscriptions::AddOnPurchases::CreateService - end - - context 'when the license has no block_changes_at set' do - let!(:current_license) do - create_current_license( - block_changes_at: nil, - cloud_licensing_enabled: true, - restrictions: { code_suggestions_seat_count: purchased_add_on_quantity, subscription_name: subscription_name } - ) - end - - it 'creates a new add-on purchase' do - expect(GitlabSubscriptions::AddOnPurchases::CreateService).to receive(:new) - .with( - namespace, - add_on, - { - quantity: purchased_add_on_quantity, - expires_on: current_license.expires_at, - purchase_xid: subscription_name - } - ).and_call_original - - expect { result }.to change { GitlabSubscriptions::AddOnPurchase.count }.by(1) - - expect(current_license.block_changes_at).to eq(nil) - expect(result[:status]).to eq(:success) - expect(result[:add_on_purchase]).to have_attributes( - expires_on: current_license.expires_at, - quantity: purchased_add_on_quantity, - purchase_xid: subscription_name - ) - end - end - - it 'creates a new add-on purchase' do - expect(GitlabSubscriptions::AddOnPurchases::CreateService).to receive(:new) - .with( - namespace, - add_on, - { - quantity: purchased_add_on_quantity, - expires_on: current_license.block_changes_at, - purchase_xid: subscription_name - } - ).and_call_original - - expect { result }.to change { GitlabSubscriptions::AddOnPurchase.count }.by(1) - - expect(current_license.block_changes_at).to eq(current_license.expires_at + 14.days) - expect(result[:status]).to eq(:success) - expect(result[:add_on_purchase]).to have_attributes( - expires_on: current_license.block_changes_at, - quantity: purchased_add_on_quantity, - purchase_xid: subscription_name - ) - end - end -end diff --git a/ee/spec/services/gitlab_subscriptions/add_on_purchases/self_managed/provision_services/code_suggestions_spec.rb b/ee/spec/services/gitlab_subscriptions/add_on_purchases/self_managed/provision_services/code_suggestions_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..563c8fa828f1412c2dbafc2d4701f2997f699482 --- /dev/null +++ b/ee/spec/services/gitlab_subscriptions/add_on_purchases/self_managed/provision_services/code_suggestions_spec.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe GitlabSubscriptions::AddOnPurchases::SelfManaged::ProvisionServices::CodeSuggestions, + :aggregate_failures, feature_category: :plan_provisioning do + subject(:result) { described_class.new.execute } + + describe '#execute', :freeze_time do + let_it_be(:add_on) { create(:gitlab_subscription_add_on, :code_suggestions) } + let_it_be(:default_organization) { create(:organization, :default) } + let_it_be(:namespace) { nil } + let_it_be(:subscription_name) { 'A-S00000002' } + + let!(:current_license) do + create_current_license( + cloud_licensing_enabled: true, + restrictions: { + code_suggestions_seat_count: quantity, + subscription_name: subscription_name + } + ) + end + + context 'when current license has no code suggestions information' do + let!(:current_license) { create_current_license(cloud_licensing_enabled: true) } + + it_behaves_like 'provision service expires add-on purchase' + end + + context 'when current license has zero code suggestions seats purchased' do + let_it_be(:quantity) { 0 } + + it_behaves_like 'provision service expires add-on purchase' + end + + context 'when current license has code suggestions seats purchased' do + let_it_be(:quantity) { 1 } + + it_behaves_like 'provision service creates add-on purchase' + end + end +end diff --git a/ee/spec/support/shared_examples/services/gitlab_subscriptions/add_on_purchases/self_managed/base_provision_service_shared_examples.rb b/ee/spec/support/shared_examples/services/gitlab_subscriptions/add_on_purchases/self_managed/base_provision_service_shared_examples.rb new file mode 100644 index 0000000000000000000000000000000000000000..c4bc12794bba0f9480e6e74a3af23b01ecc593ad --- /dev/null +++ b/ee/spec/support/shared_examples/services/gitlab_subscriptions/add_on_purchases/self_managed/base_provision_service_shared_examples.rb @@ -0,0 +1,129 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'call service to handle the provision of code suggestions' do + it 'calls the service to handle the provision of code suggestions' do + expect_next_instance_of( + GitlabSubscriptions::AddOnPurchases::SelfManaged::ProvisionServices::CodeSuggestions + ) do |service| + expect(service).to receive(:execute).and_call_original + end + + subject + end +end + +RSpec.shared_examples 'provision service have empty success response' do + it 'returns a success' do + expect(result[:status]).to eq(:success) + expect(result[:add_on_purchase]).to eq(nil) + end +end + +RSpec.shared_examples 'provision service handles error' do |service_class| + it 'logs and returns an error' do + allow_next_instance_of(service_class) do |service| + allow(service).to receive(:execute).and_return(ServiceResponse.error(message: 'Something went wrong')) + end + + expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception) + expect(result[:status]).to eq(:error) + expect(result[:message]).to eq('Error syncing subscription add-on purchases. Message: Something went wrong') + end +end + +RSpec.shared_examples 'provision service expires add-on purchase' do + context 'with existing add-on purchase' do + let_it_be(:expiration_date) { Date.current + 3.months } + let_it_be(:existing_add_on_purchase) do + create( + :gitlab_subscription_add_on_purchase, + namespace: namespace, + add_on: add_on, + expires_on: expiration_date + ) + end + + it 'does not call any service to create or update an add-on purchase' do + expect(GitlabSubscriptions::AddOnPurchases::CreateService).not_to receive(:new) + expect(GitlabSubscriptions::AddOnPurchases::UpdateService).not_to receive(:new) + + result + end + + context 'when the expiration fails' do + it_behaves_like 'provision service handles error', GitlabSubscriptions::AddOnPurchases::SelfManaged::ExpireService + end + + it 'expires the existing add-on purchase' do + expect do + result + existing_add_on_purchase.reload + end.to change { existing_add_on_purchase.expires_on }.from(expiration_date).to(Date.yesterday) + end + + it_behaves_like 'provision service have empty success response' + end + + context 'without existing add-on purchase' do + it 'does not call any of the services to update an add-on purchase' do + expect(GitlabSubscriptions::AddOnPurchases::CreateService).not_to receive(:new) + expect(GitlabSubscriptions::AddOnPurchases::UpdateService).not_to receive(:new) + expect(GitlabSubscriptions::AddOnPurchases::SelfManaged::ExpireService).not_to receive(:new) + + result + end + + it_behaves_like 'provision service have empty success response' + end +end + +RSpec.shared_examples 'provision service updates the existing add-on purchase' do + it 'updates the existing add-on purchase' do + expect(GitlabSubscriptions::AddOnPurchases::UpdateService).to receive(:new) + .with( + namespace, + add_on, + { + add_on_purchase: existing_add_on_purchase, + quantity: quantity, + expires_on: current_license.block_changes_at, + purchase_xid: subscription_name + } + ).and_call_original + + expect { result }.not_to change { GitlabSubscriptions::AddOnPurchase.count } + + expect(current_license.block_changes_at).to eq(current_license.expires_at + 14.days) + expect(result[:status]).to eq(:success) + expect(result[:add_on_purchase]).to have_attributes( + id: existing_add_on_purchase.id, + expires_on: current_license.block_changes_at, + quantity: quantity, + purchase_xid: subscription_name + ) + end +end + +RSpec.shared_examples 'provision service creates add-on purchase' do + it 'creates a new add-on purchase' do + expect(GitlabSubscriptions::AddOnPurchases::CreateService).to receive(:new).with( + namespace, + add_on, + { + add_on_purchase: nil, + quantity: quantity, + expires_on: current_license.expires_at + 14.days, + purchase_xid: subscription_name + } + ).and_call_original + + expect { result }.to change { GitlabSubscriptions::AddOnPurchase.count }.by(1) + + expect(result[:status]).to eq(:success) + expect(result[:add_on_purchase]).to have_attributes( + expires_on: current_license.expires_at + 14.days, + quantity: quantity, + purchase_xid: subscription_name + ) + end +end diff --git a/ee/spec/support/shared_examples/services/gitlab_subscriptions/add_on_purchases/self_managed/provision_code_suggestion_service_shared_examples.rb b/ee/spec/support/shared_examples/services/gitlab_subscriptions/add_on_purchases/self_managed/provision_code_suggestion_service_shared_examples.rb deleted file mode 100644 index c30be87518796e932ff63cc0169d5efbc5896b50..0000000000000000000000000000000000000000 --- a/ee/spec/support/shared_examples/services/gitlab_subscriptions/add_on_purchases/self_managed/provision_code_suggestion_service_shared_examples.rb +++ /dev/null @@ -1,13 +0,0 @@ -# frozen_string_literal: true - -RSpec.shared_examples 'call service to handle the provision of code suggestions' do - it 'calls the service to handle the provision of code suggestions' do - expect_next_instance_of( - GitlabSubscriptions::AddOnPurchases::SelfManaged::ProvisionCodeSuggestionsService - ) do |service| - expect(service).to receive(:execute).and_call_original - end - - subject - end -end