Skip to content
Snippets Groups Projects
Commit e363f833 authored by Etienne Baqué's avatar Etienne Baqué :red_circle:
Browse files

Merge branch '408494_add_add_on_purchase_api_services' into 'master'

Add Add-on purchase API services

See merge request !123387



Merged-by: default avatarEtienne Baqué <ebaque@gitlab.com>
Approved-by: default avatarPaulo Barros <pbarros@gitlab.com>
Approved-by: default avatarEtienne Baqué <ebaque@gitlab.com>
Reviewed-by: default avatarEtienne Baqué <ebaque@gitlab.com>
Reviewed-by: default avatarCorinna Gogolok <cgogolok@gitlab.com>
Reviewed-by: default avatarPaulo Barros <pbarros@gitlab.com>
Co-authored-by: default avatarCorinna Gogolok <cgogolok@gitlab.com>
parents a701a04c d7691ae5
No related branches found
No related tags found
2 merge requests!123387Add Add-on purchase API services,!119439Draft: Prevent file variable content expansion in downstream pipeline
Pipeline #899975735 canceled
Showing
with 410 additions and 3 deletions
......@@ -8,6 +8,7 @@ class AddOnPurchase < ApplicationRecord
belongs_to :namespace
validates :add_on, :namespace, :expires_on, presence: true
validates :subscription_add_on_id, uniqueness: { scope: :namespace_id }
validates :quantity,
presence: true,
numericality: { only_integer: true, greater_than_or_equal_to: 1 }
......
# frozen_string_literal: true
module GitlabSubscriptions
module AddOnPurchases
class BaseService
ImplementationMissingError = Class.new(RuntimeError)
def initialize(current_user, namespace, add_on, params = {})
@current_user = current_user
@namespace = namespace
@add_on = add_on
@quantity = params[:quantity]
@expires_on = params[:expires_on]
@purchase_xid = params[:purchase_xid]
end
def execute
authorize_current_user!
end
private
attr_reader :current_user, :namespace, :add_on, :quantity, :expires_on, :purchase_xid
# rubocop: disable Cop/UserAdmin
def authorize_current_user!
# Using #admin? is discouraged as it will bypass admin mode authorisation checks,
# however those checks are not in place in our REST API yet, and this service is only
# going to be used by the API for admin-only actions
raise Gitlab::Access::AccessDeniedError unless current_user&.admin?
end
# rubocop: enable Cop/UserAdmin
# Override in derived class
def add_on_purchase
raise ImplementationMissingError, 'Override in derived class'
end
def successful_response
ServiceResponse.success(payload: { add_on_purchase: add_on_purchase })
end
def error_response
ServiceResponse.error(
message: 'Add-on purchase could not be saved',
payload: { add_on_purchase: add_on_purchase }
)
end
end
end
end
# frozen_string_literal: true
module GitlabSubscriptions
module AddOnPurchases
class CreateService < ::GitlabSubscriptions::AddOnPurchases::BaseService
def execute
super
add_on_purchase.save ? successful_response : error_response
end
private
def add_on_purchase
@add_on_purchase ||= GitlabSubscriptions::AddOnPurchase.new(
namespace: namespace,
add_on: add_on,
quantity: quantity,
expires_on: expires_on,
purchase_xid: purchase_xid
)
end
def error_response
if add_on_purchase.errors.of_kind?(:subscription_add_on_id, :taken)
ServiceResponse.error(
message: "Add-on purchase for namespace #{namespace.id} and add-on #{add_on.name.titleize} " \
"already exists, use the update endpoint instead"
)
else
super
end
end
end
end
end
# frozen_string_literal: true
module GitlabSubscriptions
module AddOnPurchases
class UpdateService < ::GitlabSubscriptions::AddOnPurchases::BaseService
def execute
super
return error_response unless add_on_purchase
update_add_on_purchase ? successful_response : error_response
end
private
# rubocop: disable CodeReuse/ActiveRecord
def add_on_purchase
@add_on_purchase ||= GitlabSubscriptions::AddOnPurchase.find_by(
namespace: namespace,
add_on: add_on
)
end
# rubocop: enable CodeReuse/ActiveRecord
def update_add_on_purchase
add_on_purchase.update(
quantity: quantity,
expires_on: expires_on,
purchase_xid: purchase_xid
)
end
def error_response
if add_on_purchase.nil?
ServiceResponse.error(
message: 'Add-on purchase for namespace and add-on does not exist, use the create endpoint instead'
)
else
super
end
end
end
end
end
# frozen_string_literal: true
FactoryBot.define do
factory :add_on, class: 'GitlabSubscriptions::AddOn' do
factory :gitlab_subscription_add_on, class: 'GitlabSubscriptions::AddOn' do
name { GitlabSubscriptions::AddOn.names[:code_suggestions] }
description { 'AddOn for code suggestion features' }
end
......
# frozen_string_literal: true
FactoryBot.define do
factory :add_on_purchase, class: 'GitlabSubscriptions::AddOnPurchase' do
add_on
factory :gitlab_subscription_add_on_purchase, class: 'GitlabSubscriptions::AddOnPurchase' do
add_on { association(:gitlab_subscription_add_on) }
namespace { association(:group) }
quantity { 1 }
expires_on { 1.year.from_now.to_date }
purchase_xid { 'S-A00000001' }
purchase_xid { SecureRandom.hex(16) }
end
end
......@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe GitlabSubscriptions::AddOnPurchase, feature_category: :subscription_management do
subject { build(:add_on_purchase) }
subject { build(:gitlab_subscription_add_on_purchase) }
describe 'associations' do
it { is_expected.to belong_to(:add_on).with_foreign_key(:subscription_add_on_id).inverse_of(:add_on_purchases) }
......@@ -15,6 +15,8 @@
it { is_expected.to validate_presence_of(:namespace) }
it { is_expected.to validate_presence_of(:expires_on) }
it { is_expected.to validate_uniqueness_of(:subscription_add_on_id).scoped_to(:namespace_id) }
it { is_expected.to validate_presence_of(:quantity) }
it { is_expected.to validate_numericality_of(:quantity).only_integer.is_greater_than_or_equal_to(1) }
......
......@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe GitlabSubscriptions::AddOn, feature_category: :subscription_management do
subject { build(:add_on) }
subject { build(:gitlab_subscription_add_on) }
describe 'associations' do
it { is_expected.to have_many(:add_on_purchases).with_foreign_key(:subscription_add_on_id).inverse_of(:add_on) }
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe GitlabSubscriptions::AddOnPurchases::BaseService, feature_category: :purchase do
describe '#execute' do
let_it_be(:admin) { build(:user, :admin) }
let_it_be(:namespace) { create(:namespace) }
let_it_be(:add_on) { create(:gitlab_subscription_add_on) }
let(:params) do
{
quantity: 10,
expires_on: (Date.current + 1.year).to_s,
purchase_xid: 'S-A00000001'
}
end
let(:test_class) do
Class.new(described_class) do
def execute
super
add_on_purchase
end
end
end
subject(:result) { test_class.new(user, namespace, add_on, params).execute }
context 'with a non-admin user' do
let(:non_admin) { build(:user) }
let(:user) { non_admin }
it 'raises an error' do
expect { result }.to raise_error(Gitlab::Access::AccessDeniedError)
end
end
context 'with an admin user' do
let(:user) { admin }
context 'when add_on_purchase method was not overridden' do
it 'raises an error' do
expect { result }.to raise_error(described_class::ImplementationMissingError)
end
end
context 'when add_on_purchase method was overridden' do
let(:test_class) do
Class.new(described_class) do
include Gitlab::Utils::StrongMemoize
def execute
super
add_on_purchase.save ? successful_response : error_response
end
private
def add_on_purchase
@add_on_purchase ||= GitlabSubscriptions::AddOnPurchase.new(
namespace: namespace,
add_on: add_on,
quantity: quantity,
expires_on: expires_on,
purchase_xid: purchase_xid
)
end
end
end
context 'with success response' do
it 'returns a success' do
expect(result[:status]).to eq(:success)
expect(result[:add_on_purchase]).to be_present
end
end
context 'with error response' do
let(:params) { super().merge(quantity: 0) }
it 'returns an error' do
expect(result[:status]).to eq(:error)
expect(result[:message]).to eq('Add-on purchase could not be saved')
expect(result[:add_on_purchase]).to be_present
end
end
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe GitlabSubscriptions::AddOnPurchases::CreateService, :aggregate_failures, feature_category: :purchase do
describe '#execute' do
let_it_be(:admin) { build(:user, :admin) }
let_it_be(:namespace) { create(:namespace) }
let_it_be(:add_on) { create(:gitlab_subscription_add_on) }
let(:params) do
{
quantity: 10,
expires_on: (Date.current + 1.year).to_s,
purchase_xid: 'S-A00000001'
}
end
subject(:result) { described_class.new(user, namespace, add_on, params).execute }
context 'with a non-admin user' do
let(:non_admin) { build(:user) }
let(:user) { non_admin }
it 'raises an error' do
expect { result }.to raise_error(Gitlab::Access::AccessDeniedError)
end
end
context 'with an admin user' do
let(:user) { admin }
context 'when a record exists' do
let!(:existing_add_on_purchase) do
create(
:gitlab_subscription_add_on_purchase,
namespace: namespace,
add_on: add_on,
purchase_xid: params[:purchase_xid]
)
end
it 'returns an error' do
expect(result[:status]).to eq(:error)
expect(result[:message]).to eq(
"Add-on purchase for namespace #{namespace.id} and add-on #{add_on.name.titleize} already exists, " \
"use the update endpoint instead"
)
end
end
context 'when no record exists' do
it 'returns a success' do
expect(result[:status]).to eq(:success)
end
it 'creates a new record' do
expect { result }.to change { GitlabSubscriptions::AddOnPurchase.count }.by(1)
expect(result[:add_on_purchase]).to be_persisted
expect(result[:add_on_purchase]).to have_attributes(
namespace: namespace,
add_on: add_on,
quantity: params[:quantity],
expires_on: params[:expires_on].to_date,
purchase_xid: params[:purchase_xid]
)
end
context 'when creating the record failed' do
let(:params) { super().merge(quantity: 0) }
it 'returns an error' do
expect(result[:status]).to eq(:error)
expect(result[:message]).to eq('Add-on purchase could not be saved')
expect(result[:add_on_purchase]).to be_an_instance_of(GitlabSubscriptions::AddOnPurchase)
expect(result[:add_on_purchase]).not_to be_persisted
expect(GitlabSubscriptions::AddOnPurchase.count).to eq(0)
end
end
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe GitlabSubscriptions::AddOnPurchases::UpdateService, :aggregate_failures, feature_category: :purchase do
describe '#execute' do
let_it_be(:admin) { build(:user, :admin) }
let_it_be(:namespace) { create(:namespace) }
let_it_be(:add_on) { create(:gitlab_subscription_add_on) }
let_it_be(:purchase_xid) { 'S-A00000001' }
let(:params) do
{
quantity: 10,
expires_on: (Date.current + 1.year).to_s,
purchase_xid: purchase_xid
}
end
subject(:result) { described_class.new(user, namespace, add_on, params).execute }
context 'with a non-admin user' do
let(:non_admin) { build(:user) }
let(:user) { non_admin }
it 'raises an error' do
expect { result }.to raise_error(Gitlab::Access::AccessDeniedError)
end
end
context 'with an admin user' do
let(:user) { admin }
context 'when no record exists' do
it 'returns an error' do
expect(result[:status]).to eq(:error)
expect(result[:message]).to eq(
'Add-on purchase for namespace and add-on does not exist, use the create endpoint instead'
)
end
end
context 'when a record exists' do
let_it_be(:expires_on) { Date.current + 6.months }
let_it_be(:add_on_purchase) do
create(
:gitlab_subscription_add_on_purchase,
namespace: namespace,
add_on: add_on,
quantity: 5,
expires_on: expires_on,
purchase_xid: purchase_xid
)
end
it 'returns a success' do
expect(result[:status]).to eq(:success)
end
it 'updates the found record' do
expect(result[:add_on_purchase]).to be_persisted
expect(result[:add_on_purchase]).to eq(add_on_purchase)
expect do
result
add_on_purchase.reload
end.to change { add_on_purchase.quantity }.from(5).to(10)
.and change { add_on_purchase.expires_on }.from(expires_on).to(params[:expires_on].to_date)
end
context 'when creating the record failed' do
let(:params) { super().merge(quantity: 0) }
it 'returns an error' do
expect { result }.not_to change { add_on_purchase.quantity }
expect(result[:status]).to eq(:error)
expect(result[:message]).to eq('Add-on purchase could not be saved')
expect(result[:add_on_purchase]).to be_an_instance_of(GitlabSubscriptions::AddOnPurchase)
expect(result[:add_on_purchase]).to eq(add_on_purchase)
end
end
end
end
end
end
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment