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