diff --git a/app/graphql/mutations/packages/cleanup/policy/update.rb b/app/graphql/mutations/packages/cleanup/policy/update.rb new file mode 100644 index 0000000000000000000000000000000000000000..e7ab743994993f7b52c4f4b2b0f59a10b6c94c9a --- /dev/null +++ b/app/graphql/mutations/packages/cleanup/policy/update.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +module Mutations + module Packages + module Cleanup + module Policy + class Update < Mutations::BaseMutation + graphql_name 'UpdatePackagesCleanupPolicy' + + include FindsProject + + authorize :admin_package + + argument :project_path, + GraphQL::Types::ID, + required: true, + description: 'Project path where the packages cleanup policy is located.' + + argument :keep_n_duplicated_package_files, + Types::Packages::Cleanup::KeepDuplicatedPackageFilesEnum, + required: false, + description: copy_field_description( + Types::Packages::Cleanup::PolicyType, + :keep_n_duplicated_package_files + ) + + field :packages_cleanup_policy, + Types::Packages::Cleanup::PolicyType, + null: true, + description: 'Packages cleanup policy after mutation.' + + def resolve(project_path:, **args) + project = authorized_find!(project_path) + + result = ::Packages::Cleanup::UpdatePolicyService + .new(project: project, current_user: current_user, params: args) + .execute + + { + packages_cleanup_policy: result.payload[:packages_cleanup_policy], + errors: result.errors + } + end + end + end + end + end +end diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb index 224ed73bacda0ec9c992955b45169d69f7e554e0..084b56c015e62988f9df1d8efa579bab8382948a 100644 --- a/app/graphql/types/mutation_type.rb +++ b/app/graphql/types/mutation_type.rb @@ -136,6 +136,7 @@ class MutationType < BaseObject mount_mutation Mutations::UserPreferences::Update mount_mutation Mutations::Packages::Destroy mount_mutation Mutations::Packages::DestroyFile + mount_mutation Mutations::Packages::Cleanup::Policy::Update mount_mutation Mutations::Echo mount_mutation Mutations::WorkItems::Create, deprecated: { milestone: '15.1', reason: :alpha } mount_mutation Mutations::WorkItems::CreateFromTask, deprecated: { milestone: '15.1', reason: :alpha } diff --git a/app/graphql/types/packages/cleanup/keep_duplicated_package_files_enum.rb b/app/graphql/types/packages/cleanup/keep_duplicated_package_files_enum.rb new file mode 100644 index 0000000000000000000000000000000000000000..bf8d625a334b8702bb4c2114d5c03fb4687881c1 --- /dev/null +++ b/app/graphql/types/packages/cleanup/keep_duplicated_package_files_enum.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Types + module Packages + module Cleanup + class KeepDuplicatedPackageFilesEnum < BaseEnum + graphql_name 'PackagesCleanupKeepDuplicatedPackageFilesEnum' + + OPTIONS_MAPPING = { + 'all' => 'ALL_PACKAGE_FILES', + '1' => 'ONE_PACKAGE_FILE', + '10' => 'TEN_PACKAGE_FILES', + '20' => 'TWENTY_PACKAGE_FILES', + '30' => 'THIRTY_PACKAGE_FILES', + '40' => 'FORTY_PACKAGE_FILES', + '50' => 'FIFTY_PACKAGE_FILES' + }.freeze + + ::Packages::Cleanup::Policy::KEEP_N_DUPLICATED_PACKAGE_FILES_VALUES.each do |keep_value| + value OPTIONS_MAPPING[keep_value], value: keep_value, description: "Value to keep #{keep_value} package files" + end + end + end + end +end diff --git a/app/graphql/types/packages/cleanup/policy_type.rb b/app/graphql/types/packages/cleanup/policy_type.rb new file mode 100644 index 0000000000000000000000000000000000000000..f08aace7df9f1ab226cc2172d87cbe631caf9745 --- /dev/null +++ b/app/graphql/types/packages/cleanup/policy_type.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Types + module Packages + module Cleanup + class PolicyType < ::Types::BaseObject + graphql_name 'PackagesCleanupPolicy' + description 'A packages cleanup policy designed to keep only packages and packages assets that matter most' + + authorize :admin_package + + field :keep_n_duplicated_package_files, + Types::Packages::Cleanup::KeepDuplicatedPackageFilesEnum, + null: false, + description: 'Number of duplicated package files to retain.' + field :next_run_at, + Types::TimeType, + null: true, + description: 'Next time that this packages cleanup policy will be executed.' + end + end + end +end diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb index 9a33e773f16f69bd150475e13a46c04de9f44631..a6cb451e614e35455539aa0fcb830d90a6f9eb21 100644 --- a/app/graphql/types/project_type.rb +++ b/app/graphql/types/project_type.rb @@ -181,6 +181,11 @@ class ProjectType < BaseObject description: 'Packages of the project.', resolver: Resolvers::ProjectPackagesResolver + field :packages_cleanup_policy, + Types::Packages::Cleanup::PolicyType, + null: true, + description: 'Packages cleanup policy for the project.' + field :jobs, type: Types::Ci::JobType.connection_type, null: true, diff --git a/app/models/packages/cleanup/policy.rb b/app/models/packages/cleanup/policy.rb index 87c101cfb8ce293743ffd0be79e9ad78df42221c..d7df90a4ce00c1714913764c0a1be3f4f4b82112 100644 --- a/app/models/packages/cleanup/policy.rb +++ b/app/models/packages/cleanup/policy.rb @@ -15,7 +15,7 @@ class Policy < ApplicationRecord validates :keep_n_duplicated_package_files, inclusion: { in: KEEP_N_DUPLICATED_PACKAGE_FILES_VALUES, - message: 'keep_n_duplicated_package_files is invalid' + message: 'is invalid' } # used by Schedulable diff --git a/app/policies/packages/cleanup/policy_policy.rb b/app/policies/packages/cleanup/policy_policy.rb new file mode 100644 index 0000000000000000000000000000000000000000..6c2aacef1744ab6f13508e9663f56efb0c09a38d --- /dev/null +++ b/app/policies/packages/cleanup/policy_policy.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module Packages + module Cleanup + class PolicyPolicy < BasePolicy + delegate { @subject.project } + end + end +end diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index dac7fe161ed9be4fafb9751666a33b355a5cb5a1..fb391485ada687211b96dade8bd26bba277d4c43 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -433,6 +433,7 @@ class ProjectPolicy < BasePolicy rule { can?(:maintainer_access) }.policy do enable :destroy_package + enable :admin_package enable :admin_issue_board enable :push_to_delete_protected_branch enable :update_snippet diff --git a/app/services/packages/cleanup/update_policy_service.rb b/app/services/packages/cleanup/update_policy_service.rb new file mode 100644 index 0000000000000000000000000000000000000000..6744accc00796ee4693e1c3e074edca588cae15f --- /dev/null +++ b/app/services/packages/cleanup/update_policy_service.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +module Packages + module Cleanup + class UpdatePolicyService < BaseProjectService + ALLOWED_ATTRIBUTES = %i[keep_n_duplicated_package_files].freeze + + def execute + return ServiceResponse.error(message: 'Access denied') unless allowed? + + if policy.update(policy_params) + ServiceResponse.success(payload: { packages_cleanup_policy: policy }) + else + ServiceResponse.error(message: policy.errors.full_messages.to_sentence || 'Bad request') + end + end + + private + + def policy + strong_memoize(:policy) do + project.packages_cleanup_policy + end + end + + def allowed? + can?(current_user, :admin_package, project) + end + + def policy_params + params.slice(*ALLOWED_ATTRIBUTES) + end + end + end +end diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 801bd90634ba6ec2a7b01ea347bf9d64e2ef1822..a138232c8fc94cbcb5b862d070813249b398767e 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -5112,6 +5112,26 @@ Input type: `UpdateNoteInput` | `errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. | | `note` | [`Note`](#note) | Note after mutation. | +### `Mutation.updatePackagesCleanupPolicy` + +Input type: `UpdatePackagesCleanupPolicyInput` + +#### Arguments + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | +| `keepNDuplicatedPackageFiles` | [`PackagesCleanupKeepDuplicatedPackageFilesEnum`](#packagescleanupkeepduplicatedpackagefilesenum) | Number of duplicated package files to retain. | +| `projectPath` | [`ID!`](#id) | Project path where the packages cleanup policy is located. | + +#### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | +| `errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. | +| `packagesCleanupPolicy` | [`PackagesCleanupPolicy`](#packagescleanuppolicy) | Packages cleanup policy after mutation. | + ### `Mutation.updateRequirement` Input type: `UpdateRequirementInput` @@ -14382,6 +14402,17 @@ Represents a package tag. | `name` | [`String!`](#string) | Name of the tag. | | `updatedAt` | [`Time!`](#time) | Updated date. | +### `PackagesCleanupPolicy` + +A packages cleanup policy designed to keep only packages and packages assets that matter most. + +#### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `keepNDuplicatedPackageFiles` | [`PackagesCleanupKeepDuplicatedPackageFilesEnum!`](#packagescleanupkeepduplicatedpackagefilesenum) | Number of duplicated package files to retain. | +| `nextRunAt` | [`Time`](#time) | Next time that this packages cleanup policy will be executed. | + ### `PageInfo` Information about pagination in a connection. @@ -14690,6 +14721,7 @@ Represents vulnerability finding of a security report on the pipeline. | `onlyAllowMergeIfAllDiscussionsAreResolved` | [`Boolean`](#boolean) | Indicates if merge requests of the project can only be merged when all the discussions are resolved. | | `onlyAllowMergeIfPipelineSucceeds` | [`Boolean`](#boolean) | Indicates if merge requests of the project can only be merged with successful jobs. | | `openIssuesCount` | [`Int`](#int) | Number of open issues for the project. | +| `packagesCleanupPolicy` | [`PackagesCleanupPolicy`](#packagescleanuppolicy) | Packages cleanup policy for the project. | | `path` | [`String!`](#string) | Path of the project. | | `pathLocks` | [`PathLockConnection`](#pathlockconnection) | The project's path locks. (see [Connections](#connections)) | | `pipelineAnalytics` | [`PipelineAnalytics`](#pipelineanalytics) | Pipeline analytics. | @@ -19177,6 +19209,18 @@ Values for sorting package. | `RUBYGEMS` | Packages from the Rubygems package manager. | | `TERRAFORM_MODULE` | Packages from the Terraform Module package manager. | +### `PackagesCleanupKeepDuplicatedPackageFilesEnum` + +| Value | Description | +| ----- | ----------- | +| `ALL_PACKAGE_FILES` | Value to keep all package files. | +| `FIFTY_PACKAGE_FILES` | Value to keep 50 package files. | +| `FORTY_PACKAGE_FILES` | Value to keep 40 package files. | +| `ONE_PACKAGE_FILE` | Value to keep 1 package files. | +| `TEN_PACKAGE_FILES` | Value to keep 10 package files. | +| `THIRTY_PACKAGE_FILES` | Value to keep 30 package files. | +| `TWENTY_PACKAGE_FILES` | Value to keep 20 package files. | + ### `PipelineConfigSourceEnum` | Value | Description | diff --git a/spec/graphql/types/packages/cleanup/keep_duplicated_package_files_enum_spec.rb b/spec/graphql/types/packages/cleanup/keep_duplicated_package_files_enum_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..d7f24a9edfd92df242dbd53bb7e7afcfcf337825 --- /dev/null +++ b/spec/graphql/types/packages/cleanup/keep_duplicated_package_files_enum_spec.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe GitlabSchema.types['PackagesCleanupKeepDuplicatedPackageFilesEnum'] do + it 'exposes all options' do + expect(described_class.values.keys) + .to contain_exactly(*Types::Packages::Cleanup::KeepDuplicatedPackageFilesEnum::OPTIONS_MAPPING.values) + end + + it 'uses all possible options from model' do + all_options = Packages::Cleanup::Policy::KEEP_N_DUPLICATED_PACKAGE_FILES_VALUES + expect(described_class::OPTIONS_MAPPING.keys).to contain_exactly(*all_options) + end +end diff --git a/spec/graphql/types/packages/cleanup/policy_type_spec.rb b/spec/graphql/types/packages/cleanup/policy_type_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..f48651ed832060a4c6f70ba9a1fe4cf9d74a111a --- /dev/null +++ b/spec/graphql/types/packages/cleanup/policy_type_spec.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe GitlabSchema.types['PackagesCleanupPolicy'] do + specify { expect(described_class.graphql_name).to eq('PackagesCleanupPolicy') } + + specify do + expect(described_class.description) + .to eq('A packages cleanup policy designed to keep only packages and packages assets that matter most') + end + + specify { expect(described_class).to require_graphql_authorizations(:admin_package) } + + describe 'keep_n_duplicated_package_files' do + subject { described_class.fields['keepNDuplicatedPackageFiles'] } + + it { is_expected.to have_non_null_graphql_type(Types::Packages::Cleanup::KeepDuplicatedPackageFilesEnum) } + end + + describe 'next_run_at' do + subject { described_class.fields['nextRunAt'] } + + it { is_expected.to have_nullable_graphql_type(Types::TimeType) } + end +end diff --git a/spec/graphql/types/project_type_spec.rb b/spec/graphql/types/project_type_spec.rb index 23deef7373431c15d2c4323580e754a213a54996..2e994bf78201edcde779cd1a49f851d72f5db786 100644 --- a/spec/graphql/types/project_type_spec.rb +++ b/spec/graphql/types/project_type_spec.rb @@ -36,7 +36,7 @@ pipeline_analytics squash_read_only sast_ci_configuration cluster_agent cluster_agents agent_configurations ci_template timelogs merge_commit_template squash_commit_template work_item_types - recent_issue_boards ci_config_path_or_default + recent_issue_boards ci_config_path_or_default packages_cleanup_policy ] expect(described_class).to include_graphql_fields(*expected_fields) @@ -421,6 +421,12 @@ it { is_expected.to have_graphql_type(Types::ContainerExpirationPolicyType) } end + describe 'packages cleanup policy field' do + subject { described_class.fields['packagesCleanupPolicy'] } + + it { is_expected.to have_graphql_type(Types::Packages::Cleanup::PolicyType) } + end + describe 'terraform state field' do subject { described_class.fields['terraformState'] } diff --git a/spec/models/packages/cleanup/policy_spec.rb b/spec/models/packages/cleanup/policy_spec.rb index 972071aa0ad64babdc5584e0097a9d52941c150a..c08ae4aa7e7309232f8a2b96d11dd346550743a8 100644 --- a/spec/models/packages/cleanup/policy_spec.rb +++ b/spec/models/packages/cleanup/policy_spec.rb @@ -13,7 +13,7 @@ is_expected .to validate_inclusion_of(:keep_n_duplicated_package_files) .in_array(described_class::KEEP_N_DUPLICATED_PACKAGE_FILES_VALUES) - .with_message('keep_n_duplicated_package_files is invalid') + .with_message('is invalid') end end diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb index d9316344474e7b12f69234949b24255d6841749b..23e4641e0d5ed27eff249cb4de3bc5b7580643bf 100644 --- a/spec/policies/project_policy_spec.rb +++ b/spec/policies/project_policy_spec.rb @@ -1356,6 +1356,36 @@ def set_access_level(access_level) end end + describe 'admin_package' do + context 'with admin' do + let(:current_user) { admin } + + context 'when admin mode enabled', :enable_admin_mode do + it { is_expected.to be_allowed(:admin_package) } + end + + context 'when admin mode disabled' do + it { is_expected.to be_disallowed(:admin_package) } + end + end + + %i[owner maintainer].each do |role| + context "with #{role}" do + let(:current_user) { public_send(role) } + + it { is_expected.to be_allowed(:admin_package) } + end + end + + %i[developer reporter guest non_member anonymous].each do |role| + context "with #{role}" do + let(:current_user) { public_send(role) } + + it { is_expected.to be_disallowed(:admin_package) } + end + end + end + describe 'read_feature_flag' do subject { described_class.new(current_user, project) } diff --git a/spec/requests/api/graphql/mutations/packages/cleanup/policy/update_spec.rb b/spec/requests/api/graphql/mutations/packages/cleanup/policy/update_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..7e00f3ca53ad0b2065cc87c555efb1db30df2f30 --- /dev/null +++ b/spec/requests/api/graphql/mutations/packages/cleanup/policy/update_spec.rb @@ -0,0 +1,109 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Updating the packages cleanup policy' do + include GraphqlHelpers + using RSpec::Parameterized::TableSyntax + + let_it_be(:project, reload: true) { create(:project) } + let_it_be(:user) { create(:user) } + + let(:params) do + { + project_path: project.full_path, + keep_n_duplicated_package_files: 'TWENTY_PACKAGE_FILES' + } + end + + let(:mutation) do + graphql_mutation(:update_packages_cleanup_policy, params, + <<~QUERY + packagesCleanupPolicy { + keepNDuplicatedPackageFiles + nextRunAt + } + errors + QUERY + ) + end + + let(:mutation_response) { graphql_mutation_response(:update_packages_cleanup_policy) } + let(:packages_cleanup_policy_response) { mutation_response['packagesCleanupPolicy'] } + + shared_examples 'accepting the mutation request and updates the existing policy' do + it 'returns the updated packages cleanup policy' do + expect { subject }.not_to change { ::Packages::Cleanup::Policy.count } + + expect(project.packages_cleanup_policy.keep_n_duplicated_package_files).to eq('20') + expect_graphql_errors_to_be_empty + expect(packages_cleanup_policy_response['keepNDuplicatedPackageFiles']) + .to eq(params[:keep_n_duplicated_package_files]) + expect(packages_cleanup_policy_response['nextRunAt']).not_to eq(nil) + end + end + + shared_examples 'accepting the mutation request and creates a policy' do + it 'returns the created packages cleanup policy' do + expect { subject }.to change { ::Packages::Cleanup::Policy.count }.by(1) + + expect(project.packages_cleanup_policy.keep_n_duplicated_package_files).to eq('20') + expect_graphql_errors_to_be_empty + expect(packages_cleanup_policy_response['keepNDuplicatedPackageFiles']) + .to eq(params[:keep_n_duplicated_package_files]) + expect(packages_cleanup_policy_response['nextRunAt']).not_to eq(nil) + end + end + + shared_examples 'denying the mutation request' do + it 'returns an error' do + expect { subject }.not_to change { ::Packages::Cleanup::Policy.count } + + expect(project.packages_cleanup_policy.keep_n_duplicated_package_files).not_to eq('20') + expect(mutation_response).to be_nil + expect_graphql_errors_to_include(/you don't have permission to perform this action/) + end + end + + describe 'post graphql mutation' do + subject { post_graphql_mutation(mutation, current_user: user) } + + context 'with existing packages cleanup policy' do + let_it_be(:project_packages_cleanup_policy) { create(:packages_cleanup_policy, project: project) } + + where(:user_role, :shared_examples_name) do + :maintainer | 'accepting the mutation request and updates the existing policy' + :developer | 'denying the mutation request' + :reporter | 'denying the mutation request' + :guest | 'denying the mutation request' + :anonymous | 'denying the mutation request' + end + + with_them do + before do + project.send("add_#{user_role}", user) unless user_role == :anonymous + end + + it_behaves_like params[:shared_examples_name] + end + end + + context 'without existing packages cleanup policy' do + where(:user_role, :shared_examples_name) do + :maintainer | 'accepting the mutation request and creates a policy' + :developer | 'denying the mutation request' + :reporter | 'denying the mutation request' + :guest | 'denying the mutation request' + :anonymous | 'denying the mutation request' + end + + with_them do + before do + project.send("add_#{user_role}", user) unless user_role == :anonymous + end + + it_behaves_like params[:shared_examples_name] + end + end + end +end diff --git a/spec/requests/api/graphql/project/packages_cleanup_policy_spec.rb b/spec/requests/api/graphql/project/packages_cleanup_policy_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..a025c57d4b883d3a1d8d29069370fd710f38ac9e --- /dev/null +++ b/spec/requests/api/graphql/project/packages_cleanup_policy_spec.rb @@ -0,0 +1,79 @@ +# frozen_string_literal: true +require 'spec_helper' + +RSpec.describe 'getting the packages cleanup policy linked to a project' do + using RSpec::Parameterized::TableSyntax + include GraphqlHelpers + + let_it_be_with_reload(:project) { create(:project) } + let_it_be(:current_user) { project.first_owner } + + let(:fields) do + <<~QUERY + #{all_graphql_fields_for('packages_cleanup_policy'.classify)} + QUERY + end + + let(:query) do + graphql_query_for( + 'project', + { 'fullPath' => project.full_path }, + query_graphql_field('packagesCleanupPolicy', {}, fields) + ) + end + + subject { post_graphql(query, current_user: current_user) } + + it_behaves_like 'a working graphql query' do + before do + subject + end + end + + context 'with an existing policy' do + let_it_be(:policy) { create(:packages_cleanup_policy, project: project) } + + it_behaves_like 'a working graphql query' do + before do + subject + end + end + end + + context 'with different permissions' do + let_it_be(:current_user) { create(:user) } + + let(:packages_cleanup_policy_response) { graphql_data_at('project', 'packagesCleanupPolicy') } + + where(:visibility, :role, :policy_visible) do + :private | :maintainer | true + :private | :developer | false + :private | :reporter | false + :private | :guest | false + :private | :anonymous | false + :public | :maintainer | true + :public | :developer | false + :public | :reporter | false + :public | :guest | false + :public | :anonymous | false + end + + with_them do + before do + project.update!(visibility: visibility.to_s) + project.add_user(current_user, role) unless role == :anonymous + end + + it 'return the proper response' do + subject + + if policy_visible + expect(packages_cleanup_policy_response) + .to eq('keepNDuplicatedPackageFiles' => 'ALL_PACKAGE_FILES', 'nextRunAt' => nil) + else + expect(packages_cleanup_policy_response).to be_blank + end + end + end + end +end diff --git a/spec/services/packages/cleanup/update_policy_service_spec.rb b/spec/services/packages/cleanup/update_policy_service_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..a11fbb766f5fbde32953a56a6cff7dc98f2350b2 --- /dev/null +++ b/spec/services/packages/cleanup/update_policy_service_spec.rb @@ -0,0 +1,105 @@ +# frozen_string_literal: true +require 'spec_helper' + +RSpec.describe Packages::Cleanup::UpdatePolicyService do + using RSpec::Parameterized::TableSyntax + + let_it_be_with_reload(:project) { create(:project) } + let_it_be(:current_user) { create(:user) } + + let(:params) { { keep_n_duplicated_package_files: 50 } } + + describe '#execute' do + subject { described_class.new(project: project, current_user: current_user, params: params).execute } + + shared_examples 'creating the policy' do + it 'creates a new one' do + expect { subject }.to change { ::Packages::Cleanup::Policy.count }.from(0).to(1) + + expect(subject.payload[:packages_cleanup_policy]).to be_present + expect(subject.success?).to be_truthy + expect(project.packages_cleanup_policy).to be_persisted + expect(project.packages_cleanup_policy.keep_n_duplicated_package_files).to eq('50') + end + + context 'with invalid parameters' do + let(:params) { { keep_n_duplicated_package_files: 100 } } + + it 'does not create one' do + expect { subject }.not_to change { ::Packages::Cleanup::Policy.count } + + expect(subject.status).to eq(:error) + expect(subject.message).to eq('Keep n duplicated package files is invalid') + end + end + end + + shared_examples 'updating the policy' do + it 'updates the existing one' do + expect { subject }.not_to change { ::Packages::Cleanup::Policy.count } + + expect(subject.payload[:packages_cleanup_policy]).to be_present + expect(subject.success?).to be_truthy + expect(project.packages_cleanup_policy.keep_n_duplicated_package_files).to eq('50') + end + + context 'with invalid parameters' do + let(:params) { { keep_n_duplicated_package_files: 100 } } + + it 'does not update one' do + expect { subject }.not_to change { policy.keep_n_duplicated_package_files } + + expect(subject.status).to eq(:error) + expect(subject.message).to eq('Keep n duplicated package files is invalid') + end + end + end + + shared_examples 'denying access' do + it 'returns an error' do + subject + + expect(subject.message).to eq('Access denied') + expect(subject.status).to eq(:error) + end + end + + context 'with existing container expiration policy' do + let_it_be(:policy) { create(:packages_cleanup_policy, project: project) } + + where(:user_role, :shared_examples_name) do + :maintainer | 'updating the policy' + :developer | 'denying access' + :reporter | 'denying access' + :guest | 'denying access' + :anonymous | 'denying access' + end + + with_them do + before do + project.send("add_#{user_role}", current_user) unless user_role == :anonymous + end + + it_behaves_like params[:shared_examples_name] + end + end + + context 'without existing container expiration policy' do + where(:user_role, :shared_examples_name) do + :maintainer | 'creating the policy' + :developer | 'denying access' + :reporter | 'denying access' + :guest | 'denying access' + :anonymous | 'denying access' + end + + with_them do + before do + project.send("add_#{user_role}", current_user) unless user_role == :anonymous + end + + it_behaves_like params[:shared_examples_name] + end + end + end +end