Commit c6c54806 authored by Etienne Baqué's avatar Etienne Baqué

Merge branch 'vij-add-create-ci-minutes-pack-endpoint' into 'master'

Add Ci::Minutes::AdditionalPack create API

See merge request gitlab-org/gitlab!63853
parents 4317bf91 332b1511
# frozen_string_literal: true
module Ci
module Minutes
module AdditionalPacks
class CreateService
include BaseServiceUtility
def initialize(current_user, namespace, params = {})
@current_user = current_user
@namespace = namespace
@purchase_xid = params[:purchase_xid]
@expires_at = params[:expires_at]
@number_of_minutes = params[:number_of_minutes]
end
def execute
authorize_current_user!
return successful_response if additional_pack.persisted?
save_additional_pack ? successful_response : error_response
end
private
attr_reader :current_user, :namespace, :purchase_xid, :expires_at, :number_of_minutes
# 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
# rubocop: disable CodeReuse/ActiveRecord
def additional_pack
@additional_pack ||= Ci::Minutes::AdditionalPack.find_or_initialize_by(
namespace: namespace,
purchase_xid: purchase_xid
)
end
# rubocop: enable CodeReuse/ActiveRecord
def save_additional_pack
additional_pack.assign_attributes(
expires_at: expires_at,
number_of_minutes: number_of_minutes
)
additional_pack.save
end
def successful_response
success({ additional_pack: additional_pack })
end
def error_response
error('Unable to save additional pack')
end
end
end
end
end
# frozen_string_literal: true
module API
module Ci
class Minutes < ::API::Base
feature_category :utilization
before { authenticated_as_admin! }
resource :namespaces, requirements: ::API::API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc 'Create a CI Minutes purchase record for the namespace' do
success ::EE::API::Entities::Ci::Minutes::AdditionalPack
end
params do
requires :id, type: String, desc: 'The ID of a namespace'
requires :number_of_minutes, type: Integer, desc: 'Number of additional minutes purchased'
requires :expires_at, type: Date, desc: 'The expiry date for the purchase'
requires :purchase_xid, type: String, desc: 'Purchase ID for the additional minutes'
end
post ':id/minutes' do
namespace = find_namespace(params[:id])
not_found!('Namespace') unless namespace
result = ::Ci::Minutes::AdditionalPacks::CreateService.new(current_user, namespace, params).execute
if result[:status] == :success
present result[:additional_pack], with: ::EE::API::Entities::Ci::Minutes::AdditionalPack
else
bad_request!(result[:message])
end
end
end
end
end
end
......@@ -51,6 +51,7 @@ module EE
mount ::API::ResourceIterationEvents
mount ::API::Iterations
mount ::API::GroupRepositoryStorageMoves
mount ::API::Ci::Minutes
mount ::API::Internal::AppSec::Dast::SiteValidations
end
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe API::Ci::Minutes do
describe 'POST /namespaces/:id/minutes' do
let_it_be(:namespace) { create(:namespace) }
let_it_be(:user) { create(:user) }
let(:namespace_id) { namespace.id }
let(:payload) do
{
number_of_minutes: 10_000,
expires_at: (Date.current + 1.year).to_s,
purchase_xid: SecureRandom.hex(16)
}
end
subject(:post_minutes) { post api("/namespaces/#{namespace_id}/minutes", user), params: payload }
context 'with insufficient access' do
it 'returns an error' do
post_minutes
expect(response).to have_gitlab_http_status(:forbidden)
end
end
context 'with admin user' do
let_it_be(:user) { create(:admin) }
context 'when the namespace cannot be found' do
let(:namespace_id) { non_existing_record_id }
it 'returns an error' do
post_minutes
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'when the additional pack does not exist' do
it 'creates a new additional pack', :aggregate_failures do
expect { post_minutes }.to change(Ci::Minutes::AdditionalPack, :count).by(1)
expect(response).to have_gitlab_http_status(:success)
expect(json_response['number_of_minutes']).to eq payload[:number_of_minutes]
expect(json_response['expires_at']).to eq payload[:expires_at]
expect(json_response['purchase_xid']).to eq payload[:purchase_xid]
expect(json_response['namespace_id']).to eq namespace_id
end
end
context 'when the additional pack already exists' do
before do
create(:ci_minutes_additional_pack, purchase_xid: payload[:purchase_xid], namespace: namespace, number_of_minutes: 20_000)
end
it 'does not create a new additional pack and does not update the existing pack' do
expect { post_minutes }.not_to change(Ci::Minutes::AdditionalPack, :count)
expect(response).to have_gitlab_http_status(:success)
expect(json_response['number_of_minutes']).not_to eq 10_0000
end
end
context 'when the additional pack cannot be saved' do
it 'returns an error' do
allow_next_instance_of(Ci::Minutes::AdditionalPack) do |instance|
allow(instance).to receive(:save).and_return false
end
post_minutes
expect(response).to have_gitlab_http_status(:bad_request)
expect(response.body).to include('Unable to save additional pack')
end
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Ci::Minutes::AdditionalPacks::CreateService do
describe '#execute' do
let_it_be(:namespace) { create(:namespace) }
let_it_be(:admin) { build(:user, :admin) }
let_it_be(:non_admin) { build(:user) }
let(:params) { {} }
subject(:result) { described_class.new(user, namespace, params).execute }
context 'with a non-admin user' do
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(:existing_pack) { create(:ci_minutes_additional_pack, namespace: namespace) }
let(:user) { admin }
context 'when a record exists' do
let(:params) do
{
expires_at: Date.today + 1.year,
purchase_xid: existing_pack.purchase_xid,
number_of_minutes: 10_000
}
end
it 'returns success' do
expect(result[:status]).to eq :success
end
it 'returns the existing record' do
expect(result[:additional_pack]).to eq existing_pack
end
end
context 'when no record exists' do
let(:params) do
{
expires_at: Date.today + 1.year,
purchase_xid: 'new-purchase-xid',
number_of_minutes: 10_000
}
end
it 'creates a new record', :aggregate_failures do
expect { result }.to change(Ci::Minutes::AdditionalPack, :count).by(1)
pack = result[:additional_pack]
expect(pack).to be_persisted
expect(pack.expires_at).to eq params[:expires_at]
expect(pack.purchase_xid).to eq params[:purchase_xid]
expect(pack.number_of_minutes).to eq params[:number_of_minutes]
end
it 'returns success' do
expect(result[:status]).to eq :success
end
context 'with invalid params' do
let(:params) { { purchase_xid: 'missing-minutes' } }
it 'returns an error' do
response = result
expect(response[:status]).to eq :error
expect(response[:message]).to eq 'Unable to save additional pack'
end
end
end
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment