Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/spec
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-09-09 12:14:13 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2022-09-09 12:14:13 +0300
commit6021fa2fc624b7d6902273bae55c5b8b2b2b3fff (patch)
tree54d044a94c33f737d46ecb4829930868fd79105e /spec
parent377c02f959f45d07a6d1f3c4d7f5afc6ad5162ff (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
-rw-r--r--spec/factories/packages/package_files.rb8
-rw-r--r--spec/factories/packages/packages.rb6
-rw-r--r--spec/factories/packages/rpm/metadata.rb11
-rw-r--r--spec/features/projects/settings/registry_settings_spec.rb38
-rw-r--r--spec/features/projects/show/user_sees_setup_shortcut_buttons_spec.rb11
-rw-r--r--spec/fixtures/packages/rpm/hello-0.0.1-1.fc29.x86_64.rpmbin0 -> 6604 bytes
-rw-r--r--spec/frontend/packages_and_registries/settings/project/settings/components/container_expiration_policy_spec.js58
-rw-r--r--spec/graphql/types/packages/package_type_enum_spec.rb2
-rw-r--r--spec/lib/container_registry/tag_spec.rb25
-rw-r--r--spec/models/incident_management/timeline_event_spec.rb16
-rw-r--r--spec/models/packages/package_spec.rb1
-rw-r--r--spec/models/packages/rpm/metadatum_spec.rb32
-rw-r--r--spec/presenters/project_presenter_spec.rb6
-rw-r--r--spec/requests/api/commits_spec.rb85
-rw-r--r--spec/requests/api/graphql/ci/runners_spec.rb9
-rw-r--r--spec/services/ci/job_artifacts/destroy_batch_service_spec.rb11
-rw-r--r--spec/services/projects/container_repository/cleanup_tags_service_spec.rb409
-rw-r--r--spec/services/projects/container_repository/gitlab/cleanup_tags_service_spec.rb183
-rw-r--r--spec/support/shared_contexts/projects/container_repository/cleanup_tags_service_shared_context.rb28
-rw-r--r--spec/support/shared_examples/projects/container_repository/cleanup_tags_service_shared_examples.rb263
-rw-r--r--spec/support/shared_examples/services/packages_shared_examples.rb1
-rw-r--r--spec/views/groups/new.html.haml_spec.rb11
22 files changed, 823 insertions, 391 deletions
diff --git a/spec/factories/packages/package_files.rb b/spec/factories/packages/package_files.rb
index 5eac0036b91..7d3dd274777 100644
--- a/spec/factories/packages/package_files.rb
+++ b/spec/factories/packages/package_files.rb
@@ -337,6 +337,14 @@ FactoryBot.define do
size { 3989.bytes }
end
+ trait(:rpm) do
+ package
+ file_fixture { 'spec/fixtures/packages/rpm/hello-0.0.1-1.fc29.x86_64.rpm' }
+ file_name { 'hello-0.0.1-1.fc29.x86_64.rpm' }
+ file_sha1 { '5fe852b2a6abd96c22c11fa1ff2fb19d9ce58b57' }
+ size { 115.kilobytes }
+ end
+
trait(:object_storage) do
file_store { Packages::PackageFileUploader::Store::REMOTE }
end
diff --git a/spec/factories/packages/packages.rb b/spec/factories/packages/packages.rb
index 3d5bdc95722..8a3fe4bd948 100644
--- a/spec/factories/packages/packages.rb
+++ b/spec/factories/packages/packages.rb
@@ -55,6 +55,12 @@ FactoryBot.define do
end
end
+ factory :rpm_package do
+ sequence(:name) { |n| "package-#{n}" }
+ sequence(:version) { |n| "v1.0.#{n}" }
+ package_type { :rpm }
+ end
+
factory :debian_package do
sequence(:name) { |n| "package-#{n}" }
sequence(:version) { |n| "1.0-#{n}" }
diff --git a/spec/factories/packages/rpm/metadata.rb b/spec/factories/packages/rpm/metadata.rb
new file mode 100644
index 00000000000..96c785bd1ae
--- /dev/null
+++ b/spec/factories/packages/rpm/metadata.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :rpm_metadatum, class: 'Packages::Rpm::Metadatum' do
+ package { association(:rpm_package) }
+ release { "#{rand(10)}.#{rand(10)}" }
+ summary { FFaker::Lorem.sentences(2).join }
+ description { FFaker::Lorem.sentences(4).join }
+ arch { FFaker::Lorem.word }
+ end
+end
diff --git a/spec/features/projects/settings/registry_settings_spec.rb b/spec/features/projects/settings/registry_settings_spec.rb
index 5d252a86de7..1ed9ff83cdd 100644
--- a/spec/features/projects/settings/registry_settings_spec.rb
+++ b/spec/features/projects/settings/registry_settings_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Project > Settings > Packages and registries > Container registry tag expiration policy', :js do
+RSpec.describe 'Project > Settings > Packages and registries > Container registry tag expiration policy' do
let_it_be(:user) { create(:user) }
let_it_be(:project, reload: true) { create(:project, namespace: user.namespace) }
@@ -19,7 +19,7 @@ RSpec.describe 'Project > Settings > Packages and registries > Container registr
stub_container_registry_config(enabled: container_registry_enabled)
end
- context 'as owner' do
+ context 'as owner', :js do
it 'shows available section' do
subject
@@ -27,40 +27,14 @@ RSpec.describe 'Project > Settings > Packages and registries > Container registr
expect(settings_block).to have_text 'Clean up image tags'
end
- it 'saves cleanup policy submit the form' do
+ it 'contains link to clean up image tags page' do
subject
- within '[data-testid="container-expiration-policy-project-settings"]' do
- select('Every day', from: 'Run cleanup')
- select('50 tags per image name', from: 'Keep the most recent:')
- fill_in('Keep tags matching:', with: 'stable')
- select('7 days', from: 'Remove tags older than:')
- fill_in('Remove tags matching:', with: '.*-production')
-
- submit_button = find('[data-testid="save-button"')
- expect(submit_button).not_to be_disabled
- submit_button.click
- end
-
- expect(find('.gl-toast')).to have_content('Cleanup policy successfully saved.')
- end
-
- it 'does not save cleanup policy submit form with invalid regex' do
- subject
-
- within '[data-testid="container-expiration-policy-project-settings"]' do
- fill_in('Remove tags matching:', with: '*-production')
-
- submit_button = find('[data-testid="save-button"')
- expect(submit_button).not_to be_disabled
- submit_button.click
- end
-
- expect(find('.gl-toast')).to have_content('Something went wrong while updating the cleanup policy.')
+ expect(page).to have_link('Edit cleanup rules', href: cleanup_image_tags_project_settings_packages_and_registries_path(project))
end
end
- context 'with a project without expiration policy' do
+ context 'with a project without expiration policy', :js do
before do
project.container_expiration_policy.destroy!
end
@@ -74,7 +48,7 @@ RSpec.describe 'Project > Settings > Packages and registries > Container registr
subject
within '[data-testid="container-expiration-policy-project-settings"]' do
- expect(find('[data-testid="enable-toggle"]')).to have_content('Disabled - Tags will not be automatically deleted.')
+ expect(page).to have_link('Set cleanup rules', href: cleanup_image_tags_project_settings_packages_and_registries_path(project))
end
end
end
diff --git a/spec/features/projects/show/user_sees_setup_shortcut_buttons_spec.rb b/spec/features/projects/show/user_sees_setup_shortcut_buttons_spec.rb
index 89f6b4237a4..5056e245fed 100644
--- a/spec/features/projects/show/user_sees_setup_shortcut_buttons_spec.rb
+++ b/spec/features/projects/show/user_sees_setup_shortcut_buttons_spec.rb
@@ -288,6 +288,17 @@ RSpec.describe 'Projects > Show > User sees setup shortcut buttons' do
end
end
+ it 'no Auto DevOps button if builds feature is disabled' do
+ project.project_feature.update_attribute(:builds_access_level, ProjectFeature::DISABLED)
+
+ visit project_path(project)
+
+ page.within('.project-buttons') do
+ expect(page).not_to have_link('Enable Auto DevOps')
+ expect(page).not_to have_link('Auto DevOps enabled')
+ end
+ end
+
it 'no "Enable Auto DevOps" button when .gitlab-ci.yml already exists' do
Files::CreateService.new(
project,
diff --git a/spec/fixtures/packages/rpm/hello-0.0.1-1.fc29.x86_64.rpm b/spec/fixtures/packages/rpm/hello-0.0.1-1.fc29.x86_64.rpm
new file mode 100644
index 00000000000..bff3193a9e8
--- /dev/null
+++ b/spec/fixtures/packages/rpm/hello-0.0.1-1.fc29.x86_64.rpm
Binary files differ
diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/container_expiration_policy_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/container_expiration_policy_spec.js
index d83c717da6a..827cd05390a 100644
--- a/spec/frontend/packages_and_registries/settings/project/settings/components/container_expiration_policy_spec.js
+++ b/spec/frontend/packages_and_registries/settings/project/settings/components/container_expiration_policy_spec.js
@@ -1,12 +1,14 @@
-import { GlAlert, GlSprintf, GlLink } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
+import { GlAlert, GlSprintf, GlLink, GlCard } from '@gitlab/ui';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import component from '~/packages_and_registries/settings/project/components/container_expiration_policy.vue';
-import ContainerExpirationPolicyForm from '~/packages_and_registries/settings/project/components/container_expiration_policy_form.vue';
import {
+ CONTAINER_CLEANUP_POLICY_EDIT_RULES,
+ CONTAINER_CLEANUP_POLICY_SET_RULES,
+ CONTAINER_CLEANUP_POLICY_RULES_DESCRIPTION,
FETCH_SETTINGS_ERROR_MESSAGE,
UNAVAILABLE_FEATURE_INTRO_TEXT,
UNAVAILABLE_USER_FEATURE_TEXT,
@@ -14,11 +16,7 @@ import {
import expirationPolicyQuery from '~/packages_and_registries/settings/project/graphql/queries/get_expiration_policy.query.graphql';
import SettingsBlock from '~/vue_shared/components/settings/settings_block.vue';
-import {
- expirationPolicyPayload,
- emptyExpirationPolicyPayload,
- containerExpirationPolicyData,
-} from '../mock_data';
+import { expirationPolicyPayload, emptyExpirationPolicyPayload } from '../mock_data';
describe('Container expiration policy project settings', () => {
let wrapper;
@@ -28,17 +26,19 @@ describe('Container expiration policy project settings', () => {
projectPath: 'path',
isAdmin: false,
adminSettingsPath: 'settingsPath',
+ cleanupSettingsPath: 'cleanupSettingsPath',
enableHistoricEntries: false,
helpPagePath: 'helpPagePath',
- showCleanupPolicyLink: false,
};
- const findFormComponent = () => wrapper.find(ContainerExpirationPolicyForm);
- const findAlert = () => wrapper.find(GlAlert);
+ const findFormComponent = () => wrapper.findComponent(GlCard);
+ const findDescription = () => wrapper.findByTestId('description');
+ const findButton = () => wrapper.findByTestId('rules-button');
+ const findAlert = () => wrapper.findComponent(GlAlert);
const findSettingsBlock = () => wrapper.find(SettingsBlock);
const mountComponent = (provide = defaultProvidedValues, config) => {
- wrapper = shallowMount(component, {
+ wrapper = shallowMountExtended(component, {
stubs: {
GlSprintf,
SettingsBlock,
@@ -63,37 +63,19 @@ describe('Container expiration policy project settings', () => {
wrapper.destroy();
});
- describe('isEdited status', () => {
- it.each`
- description | apiResponse | workingCopy | result
- ${'empty response and no changes from user'} | ${emptyExpirationPolicyPayload()} | ${{}} | ${false}
- ${'empty response and changes from user'} | ${emptyExpirationPolicyPayload()} | ${{ enabled: true }} | ${true}
- ${'response and no changes'} | ${expirationPolicyPayload()} | ${containerExpirationPolicyData()} | ${false}
- ${'response and changes'} | ${expirationPolicyPayload()} | ${{ ...containerExpirationPolicyData(), nameRegex: '12345' }} | ${true}
- ${'response and empty'} | ${expirationPolicyPayload()} | ${{}} | ${true}
- `('$description', async ({ apiResponse, workingCopy, result }) => {
- mountComponentWithApollo({
- provide: { ...defaultProvidedValues, enableHistoricEntries: true },
- resolver: jest.fn().mockResolvedValue(apiResponse),
- });
- await waitForPromises();
-
- findFormComponent().vm.$emit('input', workingCopy);
-
- await waitForPromises();
-
- expect(findFormComponent().props('isEdited')).toBe(result);
- });
- });
-
it('renders the setting form', async () => {
mountComponentWithApollo({
resolver: jest.fn().mockResolvedValue(expirationPolicyPayload()),
});
await waitForPromises();
- expect(findFormComponent().exists()).toBe(true);
expect(findSettingsBlock().exists()).toBe(true);
+ expect(findFormComponent().exists()).toBe(true);
+ expect(findDescription().text()).toMatchInterpolatedText(
+ CONTAINER_CLEANUP_POLICY_RULES_DESCRIPTION,
+ );
+ expect(findButton().text()).toMatchInterpolatedText(CONTAINER_CLEANUP_POLICY_EDIT_RULES);
+ expect(findButton().attributes('href')).toBe(defaultProvidedValues.cleanupSettingsPath);
});
describe('the form is disabled', () => {
@@ -157,6 +139,10 @@ describe('Container expiration policy project settings', () => {
await waitForPromises();
expect(findFormComponent().exists()).toBe(isShown);
+ if (isShown) {
+ expect(findButton().text()).toMatchInterpolatedText(CONTAINER_CLEANUP_POLICY_SET_RULES);
+ expect(findButton().attributes('href')).toBe(defaultProvidedValues.cleanupSettingsPath);
+ }
});
});
});
diff --git a/spec/graphql/types/packages/package_type_enum_spec.rb b/spec/graphql/types/packages/package_type_enum_spec.rb
index 9d5a7716a61..fb93b1c8c8a 100644
--- a/spec/graphql/types/packages/package_type_enum_spec.rb
+++ b/spec/graphql/types/packages/package_type_enum_spec.rb
@@ -4,6 +4,6 @@ require 'spec_helper'
RSpec.describe GitlabSchema.types['PackageTypeEnum'] do
it 'exposes all package types' do
- expect(described_class.values.keys).to contain_exactly(*%w[MAVEN NPM CONAN NUGET PYPI COMPOSER GENERIC GOLANG DEBIAN RUBYGEMS HELM TERRAFORM_MODULE])
+ expect(described_class.values.keys).to contain_exactly(*%w[MAVEN NPM CONAN NUGET PYPI COMPOSER GENERIC GOLANG DEBIAN RUBYGEMS HELM TERRAFORM_MODULE RPM])
end
end
diff --git a/spec/lib/container_registry/tag_spec.rb b/spec/lib/container_registry/tag_spec.rb
index 190ddef0cd5..cb5c6a60e1d 100644
--- a/spec/lib/container_registry/tag_spec.rb
+++ b/spec/lib/container_registry/tag_spec.rb
@@ -240,6 +240,31 @@ RSpec.describe ContainerRegistry::Tag do
it_behaves_like 'setting and caching the created_at value'
end
end
+
+ describe 'updated_at=' do
+ subject do
+ tag.updated_at = input
+ tag.updated_at
+ end
+
+ context 'with a valid input' do
+ let(:input) { 2.days.ago.iso8601 }
+
+ it { is_expected.to eq(DateTime.iso8601(input)) }
+ end
+
+ context 'with a nil input' do
+ let(:input) { nil }
+
+ it { is_expected.to eq(nil) }
+ end
+
+ context 'with an invalid input' do
+ let(:input) { 'not a timestamp' }
+
+ it { is_expected.to eq(nil) }
+ end
+ end
end
end
end
diff --git a/spec/models/incident_management/timeline_event_spec.rb b/spec/models/incident_management/timeline_event_spec.rb
index 9f4011fe6a7..fea391acda3 100644
--- a/spec/models/incident_management/timeline_event_spec.rb
+++ b/spec/models/incident_management/timeline_event_spec.rb
@@ -29,7 +29,7 @@ RSpec.describe IncidentManagement::TimelineEvent do
it { is_expected.to validate_length_of(:action).is_at_most(128) }
end
- describe '.order_occurred_at_asc' do
+ describe '.order_occurred_at_asc_id_asc' do
let_it_be(:occurred_3mins_ago) do
create(:incident_management_timeline_event, project: project, occurred_at: 3.minutes.ago)
end
@@ -38,11 +38,23 @@ RSpec.describe IncidentManagement::TimelineEvent do
create(:incident_management_timeline_event, project: project, occurred_at: 2.minutes.ago)
end
- subject(:order) { described_class.order_occurred_at_asc }
+ subject(:order) { described_class.order_occurred_at_asc_id_asc }
it 'sorts timeline events by occurred_at' do
is_expected.to eq([occurred_3mins_ago, occurred_2mins_ago, timeline_event])
end
+
+ context 'when two events occured at the same time' do
+ let_it_be(:also_occurred_2mins_ago) do
+ create(:incident_management_timeline_event, project: project, occurred_at: occurred_2mins_ago.occurred_at)
+ end
+
+ it 'sorts timeline events by occurred_at then sorts by id' do
+ occurred_2mins_ago.touch # Interact with record of earlier id to switch default DB ordering
+
+ is_expected.to eq([occurred_3mins_ago, occurred_2mins_ago, also_occurred_2mins_ago, timeline_event])
+ end
+ end
end
describe '#cache_markdown_field' do
diff --git a/spec/models/packages/package_spec.rb b/spec/models/packages/package_spec.rb
index 526c57d08b0..e44883e7b0e 100644
--- a/spec/models/packages/package_spec.rb
+++ b/spec/models/packages/package_spec.rb
@@ -21,6 +21,7 @@ RSpec.describe Packages::Package, type: :model do
it { is_expected.to have_one(:nuget_metadatum).inverse_of(:package) }
it { is_expected.to have_one(:rubygems_metadatum).inverse_of(:package) }
it { is_expected.to have_one(:npm_metadatum).inverse_of(:package) }
+ it { is_expected.to have_one(:rpm_metadatum).inverse_of(:package) }
end
describe '.with_debian_codename' do
diff --git a/spec/models/packages/rpm/metadatum_spec.rb b/spec/models/packages/rpm/metadatum_spec.rb
new file mode 100644
index 00000000000..53a40b4a4b3
--- /dev/null
+++ b/spec/models/packages/rpm/metadatum_spec.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe Packages::Rpm::Metadatum, type: :model do
+ describe 'relationships' do
+ it { is_expected.to belong_to(:package) }
+ end
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:package) }
+ it { is_expected.to validate_presence_of(:release) }
+ it { is_expected.to validate_presence_of(:summary) }
+ it { is_expected.to validate_presence_of(:description) }
+ it { is_expected.to validate_presence_of(:arch) }
+ it { is_expected.to validate_length_of(:release).is_at_most(128) }
+ it { is_expected.to validate_length_of(:summary).is_at_most(1000) }
+ it { is_expected.to validate_length_of(:description).is_at_most(5000) }
+ it { is_expected.to validate_length_of(:arch).is_at_most(255) }
+ it { is_expected.to validate_length_of(:license).is_at_most(1000) }
+ it { is_expected.to validate_length_of(:url).is_at_most(1000) }
+
+ describe '#rpm_package_type' do
+ it 'will not allow a package with a different package_type' do
+ package = build('conan_package')
+ rpm_metadatum = build('rpm_metadatum', package: package)
+
+ expect(rpm_metadatum).not_to be_valid
+ expect(rpm_metadatum.errors.to_a).to include('Package type must be RPM')
+ end
+ end
+ end
+end
diff --git a/spec/presenters/project_presenter_spec.rb b/spec/presenters/project_presenter_spec.rb
index f6bc3048092..7ff19b1b770 100644
--- a/spec/presenters/project_presenter_spec.rb
+++ b/spec/presenters/project_presenter_spec.rb
@@ -484,6 +484,12 @@ RSpec.describe ProjectPresenter do
end
describe '#autodevops_anchor_data' do
+ it 'returns nil if builds feature is not available' do
+ allow(project).to receive(:feature_available?).with(:builds, user).and_return(false)
+
+ expect(presenter.autodevops_anchor_data).to be_nil
+ end
+
context 'when Auto Devops is enabled' do
it 'returns anchor data' do
allow(project).to receive(:auto_devops_enabled?).and_return(true)
diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb
index 362826eb10a..8a08d5203fd 100644
--- a/spec/requests/api/commits_spec.rb
+++ b/spec/requests/api/commits_spec.rb
@@ -7,10 +7,11 @@ RSpec.describe API::Commits do
include ProjectForksHelper
include SessionHelpers
- let(:user) { create(:user) }
- let(:guest) { create(:user).tap { |u| project.add_guest(u) } }
- let(:developer) { create(:user).tap { |u| project.add_developer(u) } }
- let(:project) { create(:project, :repository, creator: user, path: 'my.project') }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project, :repository, creator: user, path: 'my.project') }
+ let_it_be(:guest) { create(:user).tap { |u| project.add_guest(u) } }
+ let_it_be(:developer) { create(:user).tap { |u| project.add_developer(u) } }
+
let(:branch_with_dot) { project.repository.find_branch('ends-with.json') }
let(:branch_with_slash) { project.repository.find_branch('improve/awesome') }
let(:project_id) { project.id }
@@ -46,7 +47,7 @@ RSpec.describe API::Commits do
end
context 'when unauthenticated', 'and project is public' do
- let(:project) { create(:project, :public, :repository) }
+ let_it_be(:project) { create(:project, :public, :repository) }
it_behaves_like 'project commits'
end
@@ -413,6 +414,9 @@ RSpec.describe API::Commits do
end
describe 'create' do
+ let_it_be(:sequencer) { FactoryBot::Sequence.new(:new_file_path) { |n| "files/test/#{n}.rb" } }
+
+ let(:new_file_path) { sequencer.next }
let(:message) { 'Created a new file with a very very looooooooooooooooooooooooooooooooooooooooooooooong commit message' }
let(:invalid_c_params) do
{
@@ -435,7 +439,7 @@ RSpec.describe API::Commits do
actions: [
{
action: 'create',
- file_path: 'foo/bar/baz.txt',
+ file_path: new_file_path,
content: 'puts 8'
}
]
@@ -449,7 +453,7 @@ RSpec.describe API::Commits do
actions: [
{
action: 'create',
- file_path: 'foo/bar/baz.txt',
+ file_path: new_file_path,
content: 'puts 🦊'
}
]
@@ -959,6 +963,7 @@ RSpec.describe API::Commits do
end
describe 'multiple operations' do
+ let(:project) { create(:project, :repository, creator: user, path: 'my.project') }
let(:message) { 'Multiple actions' }
let(:invalid_mo_params) do
{
@@ -1028,17 +1033,11 @@ RSpec.describe API::Commits do
}
end
- it 'are committed as one in project repo' do
+ it 'is committed as one in project repo and includes stats' do
post api(url, user), params: valid_mo_params
expect(response).to have_gitlab_http_status(:created)
expect(json_response['title']).to eq(message)
- end
-
- it 'includes the commit stats' do
- post api(url, user), params: valid_mo_params
-
- expect(response).to have_gitlab_http_status(:created)
expect(json_response).to include 'stats'
end
@@ -1124,7 +1123,8 @@ RSpec.describe API::Commits do
end
describe 'GET /projects/:id/repository/commits/:sha/refs' do
- let(:project) { create(:project, :public, :repository) }
+ let_it_be(:project) { create(:project, :public, :repository) }
+
let(:tag) { project.repository.find_tag('v1.1.0') }
let(:commit_id) { tag.dereferenced_target.id }
let(:route) { "/projects/#{project_id}/repository/commits/#{commit_id}/refs" }
@@ -1139,6 +1139,8 @@ RSpec.describe API::Commits do
end
context 'when repository is disabled' do
+ let(:project) { create(:project, :repository, creator: user, path: 'my.project') }
+
include_context 'disabled repository'
it_behaves_like '404 response' do
@@ -1228,6 +1230,8 @@ RSpec.describe API::Commits do
end
context 'when repository is disabled' do
+ let(:project) { create(:project, :repository, creator: user, path: 'my.project') }
+
include_context 'disabled repository'
it_behaves_like '404 response' do
@@ -1269,8 +1273,14 @@ RSpec.describe API::Commits do
end
shared_examples_for 'ref with unaccessible pipeline' do
- let!(:pipeline) do
- create(:ci_empty_pipeline, project: project, status: :created, source: :push, ref: 'master', sha: commit.sha, protected: false)
+ let(:pipeline) do
+ create(:ci_empty_pipeline,
+ project: project,
+ status: :created,
+ source: :push,
+ ref: 'master',
+ sha: commit.sha,
+ protected: false)
end
it 'does not include last_pipeline' do
@@ -1308,7 +1318,7 @@ RSpec.describe API::Commits do
end
context 'when unauthenticated', 'and project is public' do
- let(:project) { create(:project, :public, :repository) }
+ let_it_be_with_reload(:project) { create(:project, :public, :repository) }
it_behaves_like 'ref commit'
it_behaves_like 'ref with pipeline'
@@ -1338,6 +1348,7 @@ RSpec.describe API::Commits do
context 'when builds are disabled' do
before do
project
+ .reload
.project_feature
.update!(builds_access_level: ProjectFeature::DISABLED)
end
@@ -1389,7 +1400,7 @@ RSpec.describe API::Commits do
context 'with private builds' do
before do
- project.project_feature.update!(builds_access_level: ProjectFeature::PRIVATE)
+ project.reload.project_feature.update!(builds_access_level: ProjectFeature::PRIVATE)
end
it_behaves_like 'ref with pipeline'
@@ -1415,8 +1426,8 @@ RSpec.describe API::Commits do
end
context 'when authenticated', 'as non_member and project is public' do
- let(:current_user) { create(:user) }
- let(:project) { create(:project, :public, :repository) }
+ let_it_be(:current_user) { create(:user) }
+ let_it_be_with_reload(:project) { create(:project, :public, :repository) }
it_behaves_like 'ref with pipeline'
@@ -1469,6 +1480,8 @@ RSpec.describe API::Commits do
end
context 'when repository is disabled' do
+ let(:project) { create(:project, :repository, creator: user, path: 'my.project') }
+
include_context 'disabled repository'
it_behaves_like '404 response' do
@@ -1478,7 +1491,7 @@ RSpec.describe API::Commits do
end
context 'when unauthenticated', 'and project is public' do
- let(:project) { create(:project, :public, :repository) }
+ let_it_be(:project) { create(:project, :public, :repository) }
it_behaves_like 'ref diff'
end
@@ -1568,6 +1581,8 @@ RSpec.describe API::Commits do
end
context 'when repository is disabled' do
+ let(:project) { create(:project, :repository, creator: user, path: 'my.project') }
+
include_context 'disabled repository'
it_behaves_like '404 response' do
@@ -1577,7 +1592,7 @@ RSpec.describe API::Commits do
end
context 'when unauthenticated', 'and project is public' do
- let(:project) { create(:project, :public, :repository) }
+ let_it_be(:project) { create(:project, :public, :repository) }
it_behaves_like 'ref comments'
end
@@ -1666,6 +1681,7 @@ RSpec.describe API::Commits do
end
describe 'POST :id/repository/commits/:sha/cherry_pick' do
+ let(:project) { create(:project, :repository, creator: user, path: 'my.project') }
let(:commit) { project.commit('7d3b0f7cff5f37573aea97cebfd5692ea1689924') }
let(:commit_id) { commit.id }
let(:branch) { 'master' }
@@ -1703,6 +1719,8 @@ RSpec.describe API::Commits do
end
context 'when repository is disabled' do
+ let(:project) { create(:project, :repository, creator: user, path: 'my.project') }
+
include_context 'disabled repository'
it_behaves_like '404 response' do
@@ -1712,7 +1730,7 @@ RSpec.describe API::Commits do
end
context 'when unauthenticated', 'and project is public' do
- let(:project) { create(:project, :public, :repository) }
+ let_it_be(:project) { create(:project, :public, :repository) }
it_behaves_like '403 response' do
let(:request) { post api(route), params: { branch: 'master' } }
@@ -1851,6 +1869,7 @@ RSpec.describe API::Commits do
end
describe 'POST :id/repository/commits/:sha/revert' do
+ let(:project) { create(:project, :repository, creator: user, path: 'my.project') }
let(:commit_id) { 'b83d6e391c22777fca1ed3012fce84f633d7fed0' }
let(:commit) { project.commit(commit_id) }
let(:branch) { 'master' }
@@ -1891,7 +1910,7 @@ RSpec.describe API::Commits do
end
context 'when unauthenticated', 'and project is public' do
- let(:project) { create(:project, :public, :repository) }
+ let_it_be(:project) { create(:project, :public, :repository) }
it_behaves_like '403 response' do
let(:request) { post api(route), params: { branch: branch } }
@@ -1998,6 +2017,7 @@ RSpec.describe API::Commits do
end
describe 'POST /projects/:id/repository/commits/:sha/comments' do
+ let(:project) { create(:project, :repository, :private) }
let(:commit) { project.repository.commit }
let(:commit_id) { commit.id }
let(:note) { 'My comment' }
@@ -2018,6 +2038,8 @@ RSpec.describe API::Commits do
end
context 'when repository is disabled' do
+ let(:project) { create(:project, :repository, creator: user, path: 'my.project') }
+
include_context 'disabled repository'
it_behaves_like '404 response' do
@@ -2027,7 +2049,7 @@ RSpec.describe API::Commits do
end
context 'when unauthenticated', 'and project is public' do
- let(:project) { create(:project, :public, :repository) }
+ let_it_be(:project) { create(:project, :public, :repository) }
it_behaves_like '400 response' do
let(:request) { post api(route), params: { note: 'My comment' } }
@@ -2047,12 +2069,13 @@ RSpec.describe API::Commits do
it_behaves_like 'ref new comment'
it 'returns the inline comment' do
- post api(route, current_user), params: { note: 'My comment', path: project.repository.commit.raw_diffs.first.new_path, line: 1, line_type: 'new' }
+ path = project.repository.commit.raw_diffs.first.new_path
+ post api(route, current_user), params: { note: 'My comment', path: path, line: 1, line_type: 'new' }
expect(response).to have_gitlab_http_status(:created)
expect(response).to match_response_schema('public_api/v4/commit_note')
expect(json_response['note']).to eq('My comment')
- expect(json_response['path']).to eq(project.repository.commit.raw_diffs.first.new_path)
+ expect(json_response['path']).to eq(path)
expect(json_response['line']).to eq(1)
expect(json_response['line_type']).to eq('new')
end
@@ -2127,7 +2150,8 @@ RSpec.describe API::Commits do
end
describe 'GET /projects/:id/repository/commits/:sha/merge_requests' do
- let(:project) { create(:project, :repository, :private) }
+ let_it_be(:project) { create(:project, :repository, :private) }
+
let(:merged_mr) { create(:merge_request, source_project: project, source_branch: 'master', target_branch: 'feature') }
let(:commit) { merged_mr.merge_request_diff.commits.last }
@@ -2159,7 +2183,8 @@ RSpec.describe API::Commits do
end
context 'public project' do
- let(:project) { create(:project, :repository, :public, :merge_requests_private) }
+ let_it_be(:project) { create(:project, :repository, :public, :merge_requests_private) }
+
let(:non_member) { create(:user) }
it 'responds 403 when only members are allowed to read merge requests' do
diff --git a/spec/requests/api/graphql/ci/runners_spec.rb b/spec/requests/api/graphql/ci/runners_spec.rb
index 2aab402a1a0..3054b866812 100644
--- a/spec/requests/api/graphql/ci/runners_spec.rb
+++ b/spec/requests/api/graphql/ci/runners_spec.rb
@@ -69,15 +69,6 @@ RSpec.describe 'Query.runners' do
it_behaves_like 'a working graphql query returning expected runner'
end
-
- context 'runner_type is PROJECT_TYPE and status is NEVER_CONTACTED' do
- let(:runner_type) { 'PROJECT_TYPE' }
- let(:status) { 'NEVER_CONTACTED' }
-
- let!(:expected_runner) { project_runner }
-
- it_behaves_like 'a working graphql query returning expected runner'
- end
end
describe 'pagination' do
diff --git a/spec/services/ci/job_artifacts/destroy_batch_service_spec.rb b/spec/services/ci/job_artifacts/destroy_batch_service_spec.rb
index 9ca39d4d32e..54d1cacc068 100644
--- a/spec/services/ci/job_artifacts/destroy_batch_service_spec.rb
+++ b/spec/services/ci/job_artifacts/destroy_batch_service_spec.rb
@@ -221,6 +221,15 @@ RSpec.describe Ci::JobArtifacts::DestroyBatchService do
end
context 'with update_stats: false' do
+ let_it_be(:extra_artifact_with_file) do
+ create(:ci_job_artifact, :zip, project: artifact_with_file.project)
+ end
+
+ let(:artifacts) do
+ Ci::JobArtifact.where(id: [artifact_with_file.id, extra_artifact_with_file.id,
+ artifact_without_file.id, trace_artifact.id])
+ end
+
it 'does not update project statistics' do
expect(ProjectStatistics).not_to receive(:increment_statistic)
@@ -230,7 +239,7 @@ RSpec.describe Ci::JobArtifacts::DestroyBatchService do
it 'returns size statistics' do
expected_updates = {
statistics_updates: {
- artifact_with_file.project => -artifact_with_file.file.size,
+ artifact_with_file.project => -(artifact_with_file.file.size + extra_artifact_with_file.file.size),
artifact_without_file.project => 0
}
}
diff --git a/spec/services/projects/container_repository/cleanup_tags_service_spec.rb b/spec/services/projects/container_repository/cleanup_tags_service_spec.rb
index 102c5e72c2c..4b753315bf7 100644
--- a/spec/services/projects/container_repository/cleanup_tags_service_spec.rb
+++ b/spec/services/projects/container_repository/cleanup_tags_service_spec.rb
@@ -5,6 +5,8 @@ require 'spec_helper'
RSpec.describe Projects::ContainerRepository::CleanupTagsService, :clean_gitlab_redis_cache do
using RSpec::Parameterized::TableSyntax
+ include_context 'for a cleanup tags service'
+
let_it_be(:user) { create(:user) }
let_it_be(:project, reload: true) { create(:project, :private) }
@@ -39,268 +41,141 @@ RSpec.describe Projects::ContainerRepository::CleanupTagsService, :clean_gitlab_
describe '#execute' do
subject { service.execute }
- context 'when no params are specified' do
- let(:params) { {} }
-
- it 'does not remove anything' do
- expect_any_instance_of(Projects::ContainerRepository::DeleteTagsService)
- .not_to receive(:execute)
- expect_no_caching
-
- is_expected.to eq(expected_service_response(before_truncate_size: 0, after_truncate_size: 0, before_delete_size: 0))
- end
- end
-
- context 'when regex matching everything is specified' do
- shared_examples 'removes all matches' do
- it 'does remove all tags except latest' do
- expect_no_caching
-
- expect_delete(%w(A Ba Bb C D E))
-
- is_expected.to eq(expected_service_response(deleted: %w(A Ba Bb C D E)))
- end
- end
-
- let(:params) do
- { 'name_regex_delete' => '.*' }
- end
-
- it_behaves_like 'removes all matches'
-
- context 'with deprecated name_regex param' do
- let(:params) do
- { 'name_regex' => '.*' }
- end
-
- it_behaves_like 'removes all matches'
- end
- end
-
- context 'with invalid regular expressions' do
- shared_examples 'handling an invalid regex' do
- it 'keeps all tags' do
- expect_no_caching
-
- expect(Projects::ContainerRepository::DeleteTagsService)
- .not_to receive(:new)
-
- subject
- end
-
- it { is_expected.to eq(status: :error, message: 'invalid regex') }
-
- it 'calls error tracking service' do
- expect(Gitlab::ErrorTracking).to receive(:log_exception).and_call_original
-
- subject
- end
- end
-
- context 'when name_regex_delete is invalid' do
- let(:params) { { 'name_regex_delete' => '*test*' } }
-
- it_behaves_like 'handling an invalid regex'
- end
-
- context 'when name_regex is invalid' do
- let(:params) { { 'name_regex' => '*test*' } }
-
- it_behaves_like 'handling an invalid regex'
- end
-
- context 'when name_regex_keep is invalid' do
- let(:params) { { 'name_regex_keep' => '*test*' } }
-
- it_behaves_like 'handling an invalid regex'
- end
- end
-
- context 'when delete regex matching specific tags is used' do
- let(:params) do
- { 'name_regex_delete' => 'C|D' }
- end
-
- it 'does remove C and D' do
- expect_delete(%w(C D))
-
- expect_no_caching
-
- is_expected.to eq(expected_service_response(deleted: %w(C D), before_truncate_size: 2, after_truncate_size: 2, before_delete_size: 2))
- end
-
- context 'with overriding allow regex' do
- let(:params) do
- { 'name_regex_delete' => 'C|D',
- 'name_regex_keep' => 'C' }
- end
-
- it 'does not remove C' do
- expect_delete(%w(D))
-
- expect_no_caching
-
- is_expected.to eq(expected_service_response(deleted: %w(D), before_truncate_size: 1, after_truncate_size: 1, before_delete_size: 1))
- end
- end
-
- context 'with name_regex_delete overriding deprecated name_regex' do
- let(:params) do
- { 'name_regex' => 'C|D',
- 'name_regex_delete' => 'D' }
- end
-
- it 'does not remove C' do
- expect_delete(%w(D))
-
- expect_no_caching
-
- is_expected.to eq(expected_service_response(deleted: %w(D), before_truncate_size: 1, after_truncate_size: 1, before_delete_size: 1))
- end
- end
- end
-
- context 'with allow regex value' do
- let(:params) do
- { 'name_regex_delete' => '.*',
- 'name_regex_keep' => 'B.*' }
- end
-
- it 'does not remove B*' do
- expect_delete(%w(A C D E))
-
- expect_no_caching
-
- is_expected.to eq(expected_service_response(deleted: %w(A C D E), before_truncate_size: 4, after_truncate_size: 4, before_delete_size: 4))
- end
- end
-
- context 'when keeping only N tags' do
- let(:params) do
- { 'name_regex' => 'A|B.*|C',
- 'keep_n' => 1 }
- end
-
- it 'sorts tags by date' do
- expect_delete(%w(Bb Ba C))
-
- expect_no_caching
-
- expect(service).to receive(:order_by_date).and_call_original
-
- is_expected.to eq(expected_service_response(deleted: %w(Bb Ba C), before_truncate_size: 4, after_truncate_size: 4, before_delete_size: 3))
- end
- end
-
- context 'when not keeping N tags' do
- let(:params) do
- { 'name_regex' => 'A|B.*|C' }
- end
-
- it 'does not sort tags by date' do
- expect_delete(%w(A Ba Bb C))
-
- expect_no_caching
-
- expect(service).not_to receive(:order_by_date)
-
- is_expected.to eq(expected_service_response(deleted: %w(A Ba Bb C), before_truncate_size: 4, after_truncate_size: 4, before_delete_size: 4))
- end
- end
-
- context 'when removing keeping only 3' do
- let(:params) do
- { 'name_regex_delete' => '.*',
- 'keep_n' => 3 }
- end
-
- it 'does remove B* and C as they are the oldest' do
- expect_delete(%w(Bb Ba C))
-
- expect_no_caching
-
- is_expected.to eq(expected_service_response(deleted: %w(Bb Ba C), before_delete_size: 3))
- end
- end
-
- context 'when removing older than 1 day' do
- let(:params) do
- { 'name_regex_delete' => '.*',
- 'older_than' => '1 day' }
- end
-
- it 'does remove B* and C as they are older than 1 day' do
- expect_delete(%w(Ba Bb C))
-
- expect_no_caching
-
- is_expected.to eq(expected_service_response(deleted: %w(Ba Bb C), before_delete_size: 3))
- end
- end
-
- context 'when combining all parameters' do
+ it_behaves_like 'handling invalid params',
+ service_response_extra: {
+ before_truncate_size: 0,
+ after_truncate_size: 0,
+ before_delete_size: 0,
+ cached_tags_count: 0
+ },
+ supports_caching: true
+
+ it_behaves_like 'when regex matching everything is specified',
+ delete_expectations: [%w(A Ba Bb C D E)],
+ service_response_extra: {
+ before_truncate_size: 6,
+ after_truncate_size: 6,
+ before_delete_size: 6,
+ cached_tags_count: 0
+ },
+ supports_caching: true
+
+ it_behaves_like 'when delete regex matching specific tags is used',
+ service_response_extra: {
+ before_truncate_size: 2,
+ after_truncate_size: 2,
+ before_delete_size: 2,
+ cached_tags_count: 0
+ },
+ supports_caching: true
+
+ it_behaves_like 'when delete regex matching specific tags is used with overriding allow regex',
+ service_response_extra: {
+ before_truncate_size: 1,
+ after_truncate_size: 1,
+ before_delete_size: 1,
+ cached_tags_count: 0
+ },
+ supports_caching: true
+
+ it_behaves_like 'with allow regex value',
+ delete_expectations: [%w(A C D E)],
+ service_response_extra: {
+ before_truncate_size: 4,
+ after_truncate_size: 4,
+ before_delete_size: 4,
+ cached_tags_count: 0
+ },
+ supports_caching: true
+
+ it_behaves_like 'when keeping only N tags',
+ delete_expectations: [%w(Bb Ba C)],
+ service_response_extra: {
+ before_truncate_size: 4,
+ after_truncate_size: 4,
+ before_delete_size: 3,
+ cached_tags_count: 0
+ },
+ supports_caching: true
+
+ it_behaves_like 'when not keeping N tags',
+ delete_expectations: [%w(A Ba Bb C)],
+ service_response_extra: {
+ before_truncate_size: 4,
+ after_truncate_size: 4,
+ before_delete_size: 4,
+ cached_tags_count: 0
+ },
+ supports_caching: true
+
+ it_behaves_like 'when removing keeping only 3',
+ delete_expectations: [%w(Bb Ba C)],
+ service_response_extra: {
+ before_truncate_size: 6,
+ after_truncate_size: 6,
+ before_delete_size: 3,
+ cached_tags_count: 0
+ },
+ supports_caching: true
+
+ it_behaves_like 'when removing older than 1 day',
+ delete_expectations: [%w(Ba Bb C)],
+ service_response_extra: {
+ before_truncate_size: 6,
+ after_truncate_size: 6,
+ before_delete_size: 3,
+ cached_tags_count: 0
+ },
+ supports_caching: true
+
+ it_behaves_like 'when combining all parameters',
+ delete_expectations: [%w(Bb Ba C)],
+ service_response_extra: {
+ before_truncate_size: 6,
+ after_truncate_size: 6,
+ before_delete_size: 3,
+ cached_tags_count: 0
+ },
+ supports_caching: true
+
+ it_behaves_like 'when running a container_expiration_policy',
+ delete_expectations: [%w(Bb Ba C)],
+ service_response_extra: {
+ before_truncate_size: 6,
+ after_truncate_size: 6,
+ before_delete_size: 3,
+ cached_tags_count: 0
+ },
+ supports_caching: true
+
+ context 'when running a container_expiration_policy with caching' do
+ let(:user) { nil }
let(:params) do
- { 'name_regex_delete' => '.*',
+ {
+ 'name_regex_delete' => '.*',
'keep_n' => 1,
- 'older_than' => '1 day' }
+ 'older_than' => '1 day',
+ 'container_expiration_policy' => true
+ }
end
- it 'does remove B* and C' do
- expect_delete(%w(Bb Ba C))
-
- expect_no_caching
+ it 'expects caching to be used' do
+ expect_delete(%w(Bb Ba C), container_expiration_policy: true)
+ expect_caching
- is_expected.to eq(expected_service_response(deleted: %w(Bb Ba C), before_delete_size: 3))
+ subject
end
- end
-
- context 'when running a container_expiration_policy' do
- let(:user) { nil }
-
- context 'with valid container_expiration_policy param' do
- let(:params) do
- { 'name_regex_delete' => '.*',
- 'keep_n' => 1,
- 'older_than' => '1 day',
- 'container_expiration_policy' => true }
- end
+ context 'when setting set to false' do
before do
- expect_delete(%w(Bb Ba C), container_expiration_policy: true)
+ stub_application_setting(container_registry_expiration_policies_caching: false)
end
- it { is_expected.to eq(expected_service_response(deleted: %w(Bb Ba C), before_delete_size: 3)) }
-
- context 'caching' do
- it 'expects caching to be used' do
- expect_caching
-
- subject
- end
-
- context 'when setting set to false' do
- before do
- stub_application_setting(container_registry_expiration_policies_caching: false)
- end
-
- it 'does not use caching' do
- expect_no_caching
-
- subject
- end
- end
- end
- end
-
- context 'without container_expiration_policy param' do
- let(:params) do
- { 'name_regex_delete' => '.*',
- 'keep_n' => 1,
- 'older_than' => '1 day' }
- end
+ it 'does not use caching' do
+ expect_delete(%w(Bb Ba C), container_expiration_policy: true)
+ expect_no_caching
- it 'fails' do
- is_expected.to eq(status: :error, message: 'access denied')
+ subject
end
end
end
@@ -322,10 +197,12 @@ RSpec.describe Projects::ContainerRepository::CleanupTagsService, :clean_gitlab_
service_response = expected_service_response(
status: status,
original_size: original_size,
+ deleted: nil
+ ).merge(
before_truncate_size: before_truncate_size,
after_truncate_size: after_truncate_size,
before_delete_size: before_delete_size,
- deleted: nil
+ cached_tags_count: 0
)
expect(result).to eq(service_response)
@@ -483,34 +360,6 @@ RSpec.describe Projects::ContainerRepository::CleanupTagsService, :clean_gitlab_
end
end
- def expect_delete(tags, container_expiration_policy: nil)
- expect(Projects::ContainerRepository::DeleteTagsService)
- .to receive(:new)
- .with(repository.project, user, tags: tags, container_expiration_policy: container_expiration_policy)
- .and_call_original
-
- expect_any_instance_of(Projects::ContainerRepository::DeleteTagsService)
- .to receive(:execute)
- .with(repository) { { status: :success, deleted: tags } }
- end
-
- # all those -1 because the default tags on L13 have a "latest" that will be filtered out
- def expected_service_response(status: :success, deleted: [], original_size: tags.size, before_truncate_size: tags.size - 1, after_truncate_size: tags.size - 1, before_delete_size: tags.size - 1)
- {
- status: status,
- deleted: deleted,
- original_size: original_size,
- before_truncate_size: before_truncate_size,
- after_truncate_size: after_truncate_size,
- before_delete_size: before_delete_size,
- cached_tags_count: 0
- }.compact.merge(deleted_size: deleted&.size)
- end
-
- def expect_no_caching
- expect(::Gitlab::Redis::Cache).not_to receive(:with)
- end
-
def expect_caching
::Gitlab::Redis::Cache.with do |redis|
expect(redis).to receive(:mget).and_call_original
diff --git a/spec/services/projects/container_repository/gitlab/cleanup_tags_service_spec.rb b/spec/services/projects/container_repository/gitlab/cleanup_tags_service_spec.rb
new file mode 100644
index 00000000000..609257ee779
--- /dev/null
+++ b/spec/services/projects/container_repository/gitlab/cleanup_tags_service_spec.rb
@@ -0,0 +1,183 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Projects::ContainerRepository::Gitlab::CleanupTagsService do
+ using RSpec::Parameterized::TableSyntax
+
+ include_context 'for a cleanup tags service'
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project, reload: true) { create(:project, :private) }
+
+ let(:repository) { create(:container_repository, :root, :import_done, project: project) }
+ let(:service) { described_class.new(repository, user, params) }
+ let(:tags) { %w[latest A Ba Bb C D E] }
+
+ before do
+ project.add_maintainer(user) if user
+
+ stub_container_registry_config(enabled: true)
+
+ stub_const("#{described_class}::TAGS_PAGE_SIZE", tags_page_size)
+
+ one_hour_ago = 1.hour.ago
+ five_days_ago = 5.days.ago
+ six_days_ago = 6.days.ago
+ one_month_ago = 1.month.ago
+
+ stub_tags(
+ {
+ 'latest' => one_hour_ago,
+ 'A' => one_hour_ago,
+ 'Ba' => five_days_ago,
+ 'Bb' => six_days_ago,
+ 'C' => one_month_ago,
+ 'D' => nil,
+ 'E' => nil
+ }
+ )
+ end
+
+ describe '#execute' do
+ subject { service.execute }
+
+ context 'with several tags pages' do
+ let(:tags_page_size) { 2 }
+
+ it_behaves_like 'handling invalid params'
+
+ it_behaves_like 'when regex matching everything is specified',
+ delete_expectations: [%w[A], %w[Ba Bb], %w[C D], %w[E]]
+
+ it_behaves_like 'when delete regex matching specific tags is used'
+
+ it_behaves_like 'when delete regex matching specific tags is used with overriding allow regex'
+
+ it_behaves_like 'with allow regex value',
+ delete_expectations: [%w[A], %w[C D], %w[E]]
+
+ it_behaves_like 'when keeping only N tags',
+ delete_expectations: [%w[Bb]]
+
+ it_behaves_like 'when not keeping N tags',
+ delete_expectations: [%w[A], %w[Ba Bb], %w[C]]
+
+ context 'when removing keeping only 3' do
+ let(:params) do
+ {
+ 'name_regex_delete' => '.*',
+ 'keep_n' => 3
+ }
+ end
+
+ it_behaves_like 'not removing anything'
+ end
+
+ it_behaves_like 'when removing older than 1 day',
+ delete_expectations: [%w[Ba Bb], %w[C]]
+
+ it_behaves_like 'when combining all parameters',
+ delete_expectations: [%w[Bb], %w[C]]
+
+ it_behaves_like 'when running a container_expiration_policy',
+ delete_expectations: [%w[Bb], %w[C]]
+
+ context 'with a timeout' do
+ let(:params) do
+ { 'name_regex_delete' => '.*' }
+ end
+
+ it 'removes the first few pages' do
+ expect(service).to receive(:timeout?).and_return(false, true)
+
+ expect_delete(%w[A])
+ expect_delete(%w[Ba Bb])
+
+ response = expected_service_response(status: :error, deleted: %w[A Ba Bb], original_size: 4)
+
+ is_expected.to eq(response)
+ end
+ end
+ end
+
+ context 'with a single tags page' do
+ let(:tags_page_size) { 1000 }
+
+ it_behaves_like 'handling invalid params'
+
+ it_behaves_like 'when regex matching everything is specified',
+ delete_expectations: [%w[A Ba Bb C D E]]
+
+ it_behaves_like 'when delete regex matching specific tags is used'
+
+ it_behaves_like 'when delete regex matching specific tags is used with overriding allow regex'
+
+ it_behaves_like 'with allow regex value',
+ delete_expectations: [%w[A C D E]]
+
+ it_behaves_like 'when keeping only N tags',
+ delete_expectations: [%w[Ba Bb C]]
+
+ it_behaves_like 'when not keeping N tags',
+ delete_expectations: [%w[A Ba Bb C]]
+
+ it_behaves_like 'when removing keeping only 3',
+ delete_expectations: [%w[Ba Bb C]]
+
+ it_behaves_like 'when removing older than 1 day',
+ delete_expectations: [%w[Ba Bb C]]
+
+ it_behaves_like 'when combining all parameters',
+ delete_expectations: [%w[Ba Bb C]]
+
+ it_behaves_like 'when running a container_expiration_policy',
+ delete_expectations: [%w[Ba Bb C]]
+ end
+ end
+
+ private
+
+ def stub_tags(tags)
+ chunked = tags_page_size < tags.size
+ previous_last = nil
+ max_chunk_index = tags.size / tags_page_size
+
+ tags.keys.in_groups_of(tags_page_size, false).each_with_index do |chunked_tag_names, index|
+ last = index == max_chunk_index
+ pagination_needed = chunked && !last
+
+ response = {
+ pagination: pagination_needed ? pagination_with(last: chunked_tag_names.last) : {},
+ response_body: chunked_tag_names.map do |name|
+ tag_raw_response(name, tags[name])
+ end
+ }
+
+ allow(repository.gitlab_api_client)
+ .to receive(:tags)
+ .with(repository.path, page_size: described_class::TAGS_PAGE_SIZE, last: previous_last)
+ .and_return(response)
+ previous_last = chunked_tag_names.last
+ end
+ end
+
+ def pagination_with(last:)
+ {
+ next: {
+ uri: URI("http://test.org?last=#{last}")
+ }
+ }
+ end
+
+ def tag_raw_response(name, timestamp)
+ timestamp_field = name.start_with?('B') ? 'updated_at' : 'created_at'
+ {
+ 'name' => name,
+ 'digest' => 'sha256:1234567890',
+ 'media_type' => 'application/vnd.oci.image.manifest.v1+json',
+ timestamp_field => timestamp&.iso8601
+ }
+ end
+end
diff --git a/spec/support/shared_contexts/projects/container_repository/cleanup_tags_service_shared_context.rb b/spec/support/shared_contexts/projects/container_repository/cleanup_tags_service_shared_context.rb
new file mode 100644
index 00000000000..c976bbe9212
--- /dev/null
+++ b/spec/support/shared_contexts/projects/container_repository/cleanup_tags_service_shared_context.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+RSpec.shared_context 'for a cleanup tags service' do
+ def expected_service_response(status: :success, deleted: [], original_size: tags.size)
+ {
+ status: status,
+ deleted: deleted,
+ original_size: original_size,
+ before_delete_size: deleted&.size
+ }.compact.merge(deleted_size: deleted&.size)
+ end
+
+ def expect_delete(tags, container_expiration_policy: nil)
+ service = instance_double('Projects::ContainerRepository::DeleteTagsService')
+
+ expect(Projects::ContainerRepository::DeleteTagsService)
+ .to receive(:new)
+ .with(repository.project, user, tags: tags, container_expiration_policy: container_expiration_policy)
+ .and_return(service)
+
+ expect(service).to receive(:execute)
+ .with(repository) { { status: :success, deleted: tags } }
+ end
+
+ def expect_no_caching
+ expect(::Gitlab::Redis::Cache).not_to receive(:with)
+ end
+end
diff --git a/spec/support/shared_examples/projects/container_repository/cleanup_tags_service_shared_examples.rb b/spec/support/shared_examples/projects/container_repository/cleanup_tags_service_shared_examples.rb
new file mode 100644
index 00000000000..9c2d30a9c8c
--- /dev/null
+++ b/spec/support/shared_examples/projects/container_repository/cleanup_tags_service_shared_examples.rb
@@ -0,0 +1,263 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'handling invalid params' do |service_response_extra: {}, supports_caching: false|
+ context 'when no params are specified' do
+ let(:params) { {} }
+
+ it_behaves_like 'not removing anything',
+ service_response_extra: service_response_extra,
+ supports_caching: supports_caching
+ end
+
+ context 'with invalid regular expressions' do
+ shared_examples 'handling an invalid regex' do
+ it 'keeps all tags' do
+ expect(Projects::ContainerRepository::DeleteTagsService)
+ .not_to receive(:new)
+ expect_no_caching unless supports_caching
+
+ subject
+ end
+
+ it { is_expected.to eq(status: :error, message: 'invalid regex') }
+
+ it 'calls error tracking service' do
+ expect(Gitlab::ErrorTracking).to receive(:log_exception).and_call_original
+
+ subject
+ end
+ end
+
+ context 'when name_regex_delete is invalid' do
+ let(:params) { { 'name_regex_delete' => '*test*' } }
+
+ it_behaves_like 'handling an invalid regex'
+ end
+
+ context 'when name_regex is invalid' do
+ let(:params) { { 'name_regex' => '*test*' } }
+
+ it_behaves_like 'handling an invalid regex'
+ end
+
+ context 'when name_regex_keep is invalid' do
+ let(:params) { { 'name_regex_keep' => '*test*' } }
+
+ it_behaves_like 'handling an invalid regex'
+ end
+ end
+end
+
+RSpec.shared_examples 'when regex matching everything is specified' do
+ |service_response_extra: {}, supports_caching: false, delete_expectations:|
+ let(:params) do
+ { 'name_regex_delete' => '.*' }
+ end
+
+ it_behaves_like 'removing the expected tags',
+ service_response_extra: service_response_extra,
+ supports_caching: supports_caching,
+ delete_expectations: delete_expectations
+
+ context 'with deprecated name_regex param' do
+ let(:params) do
+ { 'name_regex' => '.*' }
+ end
+
+ it_behaves_like 'removing the expected tags',
+ service_response_extra: service_response_extra,
+ supports_caching: supports_caching,
+ delete_expectations: delete_expectations
+ end
+end
+
+RSpec.shared_examples 'when delete regex matching specific tags is used' do
+ |service_response_extra: {}, supports_caching: false|
+ let(:params) do
+ { 'name_regex_delete' => 'C|D' }
+ end
+
+ it_behaves_like 'removing the expected tags',
+ service_response_extra: service_response_extra,
+ supports_caching: supports_caching,
+ delete_expectations: [%w[C D]]
+end
+
+RSpec.shared_examples 'when delete regex matching specific tags is used with overriding allow regex' do
+ |service_response_extra: {}, supports_caching: false|
+ let(:params) do
+ {
+ 'name_regex_delete' => 'C|D',
+ 'name_regex_keep' => 'C'
+ }
+ end
+
+ it_behaves_like 'removing the expected tags',
+ service_response_extra: service_response_extra,
+ supports_caching: supports_caching,
+ delete_expectations: [%w[D]]
+
+ context 'with name_regex_delete overriding deprecated name_regex' do
+ let(:params) do
+ {
+ 'name_regex' => 'C|D',
+ 'name_regex_delete' => 'D'
+ }
+ end
+
+ it_behaves_like 'removing the expected tags',
+ service_response_extra: service_response_extra,
+ supports_caching: supports_caching,
+ delete_expectations: [%w[D]]
+ end
+end
+
+RSpec.shared_examples 'with allow regex value' do
+ |service_response_extra: {}, supports_caching: false, delete_expectations:|
+ let(:params) do
+ {
+ 'name_regex_delete' => '.*',
+ 'name_regex_keep' => 'B.*'
+ }
+ end
+
+ it_behaves_like 'removing the expected tags',
+ service_response_extra: service_response_extra,
+ supports_caching: supports_caching,
+ delete_expectations: delete_expectations
+end
+
+RSpec.shared_examples 'when keeping only N tags' do
+ |service_response_extra: {}, supports_caching: false, delete_expectations:|
+ let(:params) do
+ {
+ 'name_regex' => 'A|B.*|C',
+ 'keep_n' => 1
+ }
+ end
+
+ it 'sorts tags by date' do
+ delete_expectations.each { |expectation| expect_delete(expectation) }
+ expect_no_caching unless supports_caching
+
+ expect(service).to receive(:order_by_date_desc).at_least(:once).and_call_original
+
+ is_expected.to eq(expected_service_response(deleted: delete_expectations.flatten).merge(service_response_extra))
+ end
+end
+
+RSpec.shared_examples 'when not keeping N tags' do
+ |service_response_extra: {}, supports_caching: false, delete_expectations:|
+ let(:params) do
+ { 'name_regex' => 'A|B.*|C' }
+ end
+
+ it 'does not sort tags by date' do
+ delete_expectations.each { |expectation| expect_delete(expectation) }
+ expect_no_caching unless supports_caching
+
+ expect(service).not_to receive(:order_by_date_desc)
+
+ is_expected.to eq(expected_service_response(deleted: delete_expectations.flatten).merge(service_response_extra))
+ end
+end
+
+RSpec.shared_examples 'when removing keeping only 3' do
+ |service_response_extra: {}, supports_caching: false, delete_expectations:|
+ let(:params) do
+ { 'name_regex_delete' => '.*',
+ 'keep_n' => 3 }
+ end
+
+ it_behaves_like 'removing the expected tags',
+ service_response_extra: service_response_extra,
+ supports_caching: supports_caching,
+ delete_expectations: delete_expectations
+end
+
+RSpec.shared_examples 'when removing older than 1 day' do
+ |service_response_extra: {}, supports_caching: false, delete_expectations:|
+ let(:params) do
+ {
+ 'name_regex_delete' => '.*',
+ 'older_than' => '1 day'
+ }
+ end
+
+ it_behaves_like 'removing the expected tags',
+ service_response_extra: service_response_extra,
+ supports_caching: supports_caching,
+ delete_expectations: delete_expectations
+end
+
+RSpec.shared_examples 'when combining all parameters' do
+ |service_response_extra: {}, supports_caching: false, delete_expectations:|
+ let(:params) do
+ {
+ 'name_regex_delete' => '.*',
+ 'keep_n' => 1,
+ 'older_than' => '1 day'
+ }
+ end
+
+ it_behaves_like 'removing the expected tags',
+ service_response_extra: service_response_extra,
+ supports_caching: supports_caching,
+ delete_expectations: delete_expectations
+end
+
+RSpec.shared_examples 'when running a container_expiration_policy' do
+ |service_response_extra: {}, supports_caching: false, delete_expectations:|
+ let(:user) { nil }
+
+ context 'with valid container_expiration_policy param' do
+ let(:params) do
+ {
+ 'name_regex_delete' => '.*',
+ 'keep_n' => 1,
+ 'older_than' => '1 day',
+ 'container_expiration_policy' => true
+ }
+ end
+
+ it 'removes the expected tags' do
+ delete_expectations.each { |expectation| expect_delete(expectation, container_expiration_policy: true) }
+ expect_no_caching unless supports_caching
+
+ is_expected.to eq(expected_service_response(deleted: delete_expectations.flatten).merge(service_response_extra))
+ end
+ end
+
+ context 'without container_expiration_policy param' do
+ let(:params) do
+ {
+ 'name_regex_delete' => '.*',
+ 'keep_n' => 1,
+ 'older_than' => '1 day'
+ }
+ end
+
+ it 'fails' do
+ is_expected.to eq(status: :error, message: 'access denied')
+ end
+ end
+end
+
+RSpec.shared_examples 'not removing anything' do |service_response_extra: {}, supports_caching: false|
+ it 'does not remove anything' do
+ expect(Projects::ContainerRepository::DeleteTagsService).not_to receive(:new)
+ expect_no_caching unless supports_caching
+
+ is_expected.to eq(expected_service_response(deleted: []).merge(service_response_extra))
+ end
+end
+
+RSpec.shared_examples 'removing the expected tags' do
+ |service_response_extra: {}, supports_caching: false, delete_expectations:|
+ it 'removes the expected tags' do
+ delete_expectations.each { |expectation| expect_delete(expectation) }
+ expect_no_caching unless supports_caching
+
+ is_expected.to eq(expected_service_response(deleted: delete_expectations.flatten).merge(service_response_extra))
+ end
+end
diff --git a/spec/support/shared_examples/services/packages_shared_examples.rb b/spec/support/shared_examples/services/packages_shared_examples.rb
index 704a4bbe0b8..ca4dea90c55 100644
--- a/spec/support/shared_examples/services/packages_shared_examples.rb
+++ b/spec/support/shared_examples/services/packages_shared_examples.rb
@@ -227,6 +227,7 @@ RSpec.shared_examples 'filters on each package_type' do |is_project: false|
let_it_be(:package10) { create(:rubygems_package, project: project) }
let_it_be(:package11) { create(:helm_package, project: project) }
let_it_be(:package12) { create(:terraform_module_package, project: project) }
+ let_it_be(:package13) { create(:rpm_package, project: project) }
Packages::Package.package_types.keys.each do |package_type|
context "for package type #{package_type}" do
diff --git a/spec/views/groups/new.html.haml_spec.rb b/spec/views/groups/new.html.haml_spec.rb
index 8b12cc42a88..5c7378e8dc7 100644
--- a/spec/views/groups/new.html.haml_spec.rb
+++ b/spec/views/groups/new.html.haml_spec.rb
@@ -25,4 +25,15 @@ RSpec.describe 'groups/new.html.haml' do
expect(rendered).not_to have_checked_field('Just me')
end
end
+
+ context 'when a subgroup' do
+ let_it_be(:group) { create(:group, :nested) }
+
+ it 'renders the visibility level section' do
+ expect(rendered).to have_content('Visibility level')
+ expect(rendered).to have_field('Private')
+ expect(rendered).to have_field('Internal')
+ expect(rendered).to have_field('Public')
+ end
+ end
end