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-01-06 21:13:45 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2022-01-06 21:13:45 +0300
commitc38fb401d2e0348c0dbfd415c9818444dbe4951c (patch)
treeaee27d8fd8758356c73b6654011effa270eb0f3a /spec
parentf20be8802a40405885e9421417809438e5772bf5 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
-rw-r--r--spec/controllers/admin/runner_projects_controller_spec.rb59
-rw-r--r--spec/controllers/admin/runners_controller_spec.rb29
-rw-r--r--spec/controllers/projects/packages/infrastructure_registry_controller_spec.rb24
-rw-r--r--spec/factories/packages/package_files.rb6
-rw-r--r--spec/features/admin/admin_runners_spec.rb18
-rw-r--r--spec/finders/packages/conan/package_file_finder_spec.rb30
-rw-r--r--spec/finders/packages/package_file_finder_spec.rb28
-rw-r--r--spec/finders/user_group_notification_settings_finder_spec.rb238
-rw-r--r--spec/frontend/crm/contact_form_spec.js4
-rw-r--r--spec/frontend/crm/form_spec.js278
-rw-r--r--spec/frontend/crm/mock_data.js8
-rw-r--r--spec/frontend/crm/new_organization_form_spec.js2
-rw-r--r--spec/lib/api/entities/ci/pipeline_spec.rb21
-rw-r--r--spec/lib/backup/files_spec.rb4
-rw-r--r--spec/lib/backup/repository_backup_error_spec.rb42
-rw-r--r--spec/models/packages/package_file_spec.rb69
-rw-r--r--spec/presenters/packages/conan/package_presenter_spec.rb34
-rw-r--r--spec/presenters/packages/detail/package_presenter_spec.rb22
-rw-r--r--spec/presenters/packages/npm/package_presenter_spec.rb20
-rw-r--r--spec/presenters/packages/nuget/package_metadata_presenter_spec.rb14
-rw-r--r--spec/presenters/packages/pypi/package_presenter_spec.rb16
-rw-r--r--spec/requests/api/graphql/packages/package_spec.rb25
-rw-r--r--spec/requests/api/package_files_spec.rb50
-rw-r--r--spec/requests/api/rubygem_packages_spec.rb28
-rw-r--r--spec/requests/api/terraform/modules/v1/packages_spec.rb37
-rw-r--r--spec/services/packages/maven/metadata/sync_service_spec.rb18
-rw-r--r--spec/support/shared_examples/models/packages/debian/distribution_shared_examples.rb21
-rw-r--r--spec/support/shared_examples/requests/api/graphql/packages/package_details_shared_examples.rb24
-rw-r--r--spec/support/shared_examples/requests/api/nuget_endpoints_shared_examples.rb1
-rw-r--r--spec/tasks/gitlab/backup_rake_spec.rb31
30 files changed, 989 insertions, 212 deletions
diff --git a/spec/controllers/admin/runner_projects_controller_spec.rb b/spec/controllers/admin/runner_projects_controller_spec.rb
new file mode 100644
index 00000000000..e5f63025cf7
--- /dev/null
+++ b/spec/controllers/admin/runner_projects_controller_spec.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Admin::RunnerProjectsController do
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, group: group) }
+
+ before do
+ sign_in(create(:admin))
+ end
+
+ describe '#create' do
+ let(:project_id) { project.path }
+
+ subject do
+ post :create, params: {
+ namespace_id: group.path,
+ project_id: project_id,
+ runner_project: { runner_id: project_runner.id }
+ }
+ end
+
+ context 'assigning runner to same project' do
+ let(:project_runner) { create(:ci_runner, :project, projects: [project]) }
+
+ it 'redirects to the admin runner edit page' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:redirect)
+ expect(response).to redirect_to edit_admin_runner_url(project_runner)
+ end
+ end
+
+ context 'assigning runner to another project' do
+ let(:project_runner) { create(:ci_runner, :project, projects: [source_project]) }
+ let(:source_project) { create(:project) }
+
+ it 'redirects to the admin runner edit page' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:redirect)
+ expect(response).to redirect_to edit_admin_runner_url(project_runner)
+ end
+ end
+
+ context 'for unknown project' do
+ let_it_be(:project_runner) { create(:ci_runner, :project, projects: [project]) }
+
+ let(:project_id) { 0 }
+
+ it 'shows 404 for unknown project' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+end
diff --git a/spec/controllers/admin/runners_controller_spec.rb b/spec/controllers/admin/runners_controller_spec.rb
index b9a59e9ae5f..e970e5c0916 100644
--- a/spec/controllers/admin/runners_controller_spec.rb
+++ b/spec/controllers/admin/runners_controller_spec.rb
@@ -26,6 +26,23 @@ RSpec.describe Admin::RunnersController do
render_views
let_it_be(:project) { create(:project) }
+
+ before_all do
+ create(:ci_build, runner: runner, project: project)
+ end
+
+ it 'redirects to the runner edit page' do
+ get :show, params: { id: runner.id }
+
+ expect(response).to have_gitlab_http_status(:redirect)
+ expect(response).to redirect_to edit_admin_runner_path(runner)
+ end
+ end
+
+ describe '#edit' do
+ render_views
+
+ let_it_be(:project) { create(:project) }
let_it_be(:project_two) { create(:project) }
before_all do
@@ -33,29 +50,29 @@ RSpec.describe Admin::RunnersController do
create(:ci_build, runner: runner, project: project_two)
end
- it 'shows a particular runner' do
- get :show, params: { id: runner.id }
+ it 'shows a runner edit page' do
+ get :edit, params: { id: runner.id }
expect(response).to have_gitlab_http_status(:ok)
end
it 'shows 404 for unknown runner' do
- get :show, params: { id: 0 }
+ get :edit, params: { id: 0 }
expect(response).to have_gitlab_http_status(:not_found)
end
it 'avoids N+1 queries', :request_store do
- get :show, params: { id: runner.id }
+ get :edit, params: { id: runner.id }
- control_count = ActiveRecord::QueryRecorder.new { get :show, params: { id: runner.id } }.count
+ control_count = ActiveRecord::QueryRecorder.new { get :edit, params: { id: runner.id } }.count
new_project = create(:project)
create(:ci_build, runner: runner, project: new_project)
# There is one additional query looking up subject.group in ProjectPolicy for the
# needs_new_sso_session permission
- expect { get :show, params: { id: runner.id } }.not_to exceed_query_limit(control_count + 1)
+ expect { get :edit, params: { id: runner.id } }.not_to exceed_query_limit(control_count + 1)
expect(response).to have_gitlab_http_status(:ok)
end
diff --git a/spec/controllers/projects/packages/infrastructure_registry_controller_spec.rb b/spec/controllers/projects/packages/infrastructure_registry_controller_spec.rb
index fc741d0f3f6..707edeaeee3 100644
--- a/spec/controllers/projects/packages/infrastructure_registry_controller_spec.rb
+++ b/spec/controllers/projects/packages/infrastructure_registry_controller_spec.rb
@@ -41,5 +41,29 @@ RSpec.describe Projects::Packages::InfrastructureRegistryController do
it_behaves_like 'returning response status', :not_found
end
+
+ context 'with package file pending destruction' do
+ let_it_be(:package_file_pending_destruction) { create(:package_file, :pending_destruction, package: terraform_module) }
+
+ let(:terraform_module_package_file) { terraform_module.package_files.first }
+
+ it 'does not return them' do
+ subject
+
+ expect(assigns(:package_files)).to contain_exactly(terraform_module_package_file)
+ end
+
+ context 'with packages_installable_package_files disabled' do
+ before do
+ stub_feature_flags(packages_installable_package_files: false)
+ end
+
+ it 'returns them' do
+ subject
+
+ expect(assigns(:package_files)).to contain_exactly(package_file_pending_destruction, terraform_module_package_file)
+ end
+ end
+ end
end
end
diff --git a/spec/factories/packages/package_files.rb b/spec/factories/packages/package_files.rb
index 845fd882beb..5eac0036b91 100644
--- a/spec/factories/packages/package_files.rb
+++ b/spec/factories/packages/package_files.rb
@@ -6,6 +6,8 @@ FactoryBot.define do
file_name { 'somefile.txt' }
+ status { :default }
+
transient do
file_fixture { 'spec/fixtures/packages/conan/recipe_files/conanfile.py' }
end
@@ -14,6 +16,10 @@ FactoryBot.define do
package_file.file = fixture_file_upload(evaluator.file_fixture)
end
+ trait :pending_destruction do
+ status { :pending_destruction }
+ end
+
factory :conan_package_file do
package { association(:conan_package, without_package_files: true) }
diff --git a/spec/features/admin/admin_runners_spec.rb b/spec/features/admin/admin_runners_spec.rb
index 79b3f63155a..e0c6ac21bfb 100644
--- a/spec/features/admin/admin_runners_spec.rb
+++ b/spec/features/admin/admin_runners_spec.rb
@@ -449,19 +449,21 @@ RSpec.describe "Admin Runners" do
end
end
- describe "Runner show page" do
+ describe "Runner edit page" do
let(:runner) { create(:ci_runner) }
before do
@project1 = create(:project)
@project2 = create(:project)
- visit admin_runner_path(runner)
+ visit edit_admin_runner_path(runner)
+
+ wait_for_requests
end
describe 'runner page breadcrumbs' do
- it 'contains the current runner token' do
+ it 'contains the current runner id and token' do
page.within '[data-testid="breadcrumb-links"]' do
- expect(page.find('h2')).to have_content(runner.short_sha)
+ expect(page.find('h2')).to have_content("##{runner.id} (#{runner.short_sha})")
end
end
end
@@ -510,7 +512,7 @@ RSpec.describe "Admin Runners" do
let(:runner) { create(:ci_runner, :project, projects: [@project1]) }
before do
- visit admin_runner_path(runner)
+ visit edit_admin_runner_path(runner)
end
it_behaves_like 'assignable runner'
@@ -520,7 +522,7 @@ RSpec.describe "Admin Runners" do
let(:runner) { create(:ci_runner, :project, projects: [@project1], locked: true) }
before do
- visit admin_runner_path(runner)
+ visit edit_admin_runner_path(runner)
end
it_behaves_like 'assignable runner'
@@ -531,7 +533,7 @@ RSpec.describe "Admin Runners" do
before do
@project1.destroy!
- visit admin_runner_path(runner)
+ visit edit_admin_runner_path(runner)
end
it_behaves_like 'assignable runner'
@@ -542,7 +544,7 @@ RSpec.describe "Admin Runners" do
let(:runner) { create(:ci_runner, :project, projects: [@project1]) }
before do
- visit admin_runner_path(runner)
+ visit edit_admin_runner_path(runner)
end
it 'removed specific runner from project' do
diff --git a/spec/finders/packages/conan/package_file_finder_spec.rb b/spec/finders/packages/conan/package_file_finder_spec.rb
index c2f445c58f7..3da7da456c2 100644
--- a/spec/finders/packages/conan/package_file_finder_spec.rb
+++ b/spec/finders/packages/conan/package_file_finder_spec.rb
@@ -8,7 +8,7 @@ RSpec.describe ::Packages::Conan::PackageFileFinder do
let(:package_file_name) { package_file.file_name }
let(:params) { {} }
- RSpec.shared_examples 'package file finder examples' do
+ shared_examples 'package file finder examples' do
it { is_expected.to eq(package_file) }
context 'with conan_file_type' do
@@ -39,11 +39,37 @@ RSpec.describe ::Packages::Conan::PackageFileFinder do
end
end
+ shared_examples 'not returning pending_destruction package files' do
+ let_it_be(:recent_package_file_pending_destruction) do
+ create(:package_file, :pending_destruction, package: package, file_name: package_file.file_name)
+ end
+
+ it 'returns the correct package file' do
+ expect(package.package_files.last).to eq(recent_package_file_pending_destruction)
+
+ expect(subject).to eq(package_file)
+ end
+
+ context 'with packages_installable_package_files disabled' do
+ before do
+ stub_feature_flags(packages_installable_package_files: false)
+ end
+
+ it 'returns the correct package file' do
+ expect(package.package_files.last).to eq(recent_package_file_pending_destruction)
+
+ expect(subject).to eq(recent_package_file_pending_destruction)
+ end
+ end
+ end
+
describe '#execute' do
subject { described_class.new(package, package_file_name, params).execute }
it_behaves_like 'package file finder examples'
+ it_behaves_like 'not returning pending_destruction package files'
+
context 'with unknown file_name' do
let(:package_file_name) { 'unknown.jpg' }
@@ -56,6 +82,8 @@ RSpec.describe ::Packages::Conan::PackageFileFinder do
it_behaves_like 'package file finder examples'
+ it_behaves_like 'not returning pending_destruction package files'
+
context 'with unknown file_name' do
let(:package_file_name) { 'unknown.jpg' }
diff --git a/spec/finders/packages/package_file_finder_spec.rb b/spec/finders/packages/package_file_finder_spec.rb
index 8014f04d917..8b21c9cd3ec 100644
--- a/spec/finders/packages/package_file_finder_spec.rb
+++ b/spec/finders/packages/package_file_finder_spec.rb
@@ -8,7 +8,7 @@ RSpec.describe Packages::PackageFileFinder do
let(:package_file_name) { package_file.file_name }
let(:params) { {} }
- RSpec.shared_examples 'package file finder examples' do
+ shared_examples 'package file finder examples' do
it { is_expected.to eq(package_file) }
context 'with file_name_like' do
@@ -19,11 +19,35 @@ RSpec.describe Packages::PackageFileFinder do
end
end
+ shared_examples 'not returning pending_destruction package files' do
+ let_it_be(:recent_package_file_pending_destruction) do
+ create(:package_file, :pending_destruction, package: package, file_name: package_file.file_name)
+ end
+
+ it 'returns the correct package file' do
+ expect(package.package_files.last).to eq(recent_package_file_pending_destruction)
+
+ expect(subject).to eq(package_file)
+ end
+
+ context 'with packages_installable_package_files disabled' do
+ before do
+ stub_feature_flags(packages_installable_package_files: false)
+ end
+
+ it 'returns them' do
+ expect(subject).to eq(recent_package_file_pending_destruction)
+ end
+ end
+ end
+
describe '#execute' do
subject { described_class.new(package, package_file_name, params).execute }
it_behaves_like 'package file finder examples'
+ it_behaves_like 'not returning pending_destruction package files'
+
context 'with unknown file_name' do
let(:package_file_name) { 'unknown.jpg' }
@@ -36,6 +60,8 @@ RSpec.describe Packages::PackageFileFinder do
it_behaves_like 'package file finder examples'
+ it_behaves_like 'not returning pending_destruction package files'
+
context 'with unknown file_name' do
let(:package_file_name) { 'unknown.jpg' }
diff --git a/spec/finders/user_group_notification_settings_finder_spec.rb b/spec/finders/user_group_notification_settings_finder_spec.rb
index ea44688bc8d..ac59a42d813 100644
--- a/spec/finders/user_group_notification_settings_finder_spec.rb
+++ b/spec/finders/user_group_notification_settings_finder_spec.rb
@@ -11,167 +11,155 @@ RSpec.describe UserGroupNotificationSettingsFinder do
subject.map(&proc).uniq
end
- shared_examples 'user group notifications settings tests' do
- context 'when the groups have no existing notification settings' do
- context 'when the groups have no ancestors' do
- let_it_be(:groups) { create_list(:group, 3) }
-
- it 'will be a default Global notification setting', :aggregate_failures do
- expect(subject.count).to eq(3)
- expect(attributes(&:notification_email)).to match_array([nil])
- expect(attributes(&:level)).to match_array(['global'])
- end
+ context 'when the groups have no existing notification settings' do
+ context 'when the groups have no ancestors' do
+ let_it_be(:groups) { create_list(:group, 3) }
+
+ it 'will be a default Global notification setting', :aggregate_failures do
+ expect(subject.count).to eq(3)
+ expect(attributes(&:notification_email)).to match_array([nil])
+ expect(attributes(&:level)).to match_array(['global'])
end
+ end
- context 'when the groups have ancestors' do
- context 'when an ancestor has a level other than Global' do
- let_it_be(:ancestor_a) { create(:group) }
- let_it_be(:group_a) { create(:group, parent: ancestor_a) }
- let_it_be(:ancestor_b) { create(:group) }
- let_it_be(:group_b) { create(:group, parent: ancestor_b) }
- let_it_be(:email) { create(:email, :confirmed, email: 'ancestor@example.com', user: user) }
-
- let_it_be(:groups) { [group_a, group_b] }
+ context 'when the groups have ancestors' do
+ context 'when an ancestor has a level other than Global' do
+ let_it_be(:ancestor_a) { create(:group) }
+ let_it_be(:group_a) { create(:group, parent: ancestor_a) }
+ let_it_be(:ancestor_b) { create(:group) }
+ let_it_be(:group_b) { create(:group, parent: ancestor_b) }
+ let_it_be(:email) { create(:email, :confirmed, email: 'ancestor@example.com', user: user) }
- before do
- create(:notification_setting, user: user, source: ancestor_a, level: 'participating', notification_email: email.email)
- create(:notification_setting, user: user, source: ancestor_b, level: 'participating', notification_email: email.email)
- end
+ let_it_be(:groups) { [group_a, group_b] }
- it 'has the same level set' do
- expect(attributes(&:level)).to match_array(['participating'])
- end
+ before do
+ create(:notification_setting, user: user, source: ancestor_a, level: 'participating', notification_email: email.email)
+ create(:notification_setting, user: user, source: ancestor_b, level: 'participating', notification_email: email.email)
+ end
- it 'has the same email set' do
- expect(attributes(&:notification_email)).to match_array(['ancestor@example.com'])
- end
+ it 'has the same level set' do
+ expect(attributes(&:level)).to match_array(['participating'])
+ end
- it 'only returns the two queried groups' do
- expect(subject.count).to eq(2)
- end
+ it 'has the same email set' do
+ expect(attributes(&:notification_email)).to match_array(['ancestor@example.com'])
end
- context 'when an ancestor has a Global level but has an email set' do
- let_it_be(:grand_ancestor) { create(:group) }
- let_it_be(:ancestor) { create(:group, parent: grand_ancestor) }
- let_it_be(:group) { create(:group, parent: ancestor) }
- let_it_be(:ancestor_email) { create(:email, :confirmed, email: 'ancestor@example.com', user: user) }
- let_it_be(:grand_email) { create(:email, :confirmed, email: 'grand@example.com', user: user) }
-
- let_it_be(:groups) { [group] }
-
- before do
- create(:notification_setting, user: user, source: grand_ancestor, level: 'participating', notification_email: grand_email.email)
- create(:notification_setting, user: user, source: ancestor, level: 'global', notification_email: ancestor_email.email)
- end
-
- it 'has the same email and level set', :aggregate_failures do
- expect(subject.count).to eq(1)
- expect(attributes(&:level)).to match_array(['global'])
- expect(attributes(&:notification_email)).to match_array(['ancestor@example.com'])
- end
+ it 'only returns the two queried groups' do
+ expect(subject.count).to eq(2)
end
+ end
- context 'when the group has parent_id set but that does not belong to any group' do
- let_it_be(:group) { create(:group) }
- let_it_be(:groups) { [group] }
+ context 'when an ancestor has a Global level but has an email set' do
+ let_it_be(:grand_ancestor) { create(:group) }
+ let_it_be(:ancestor) { create(:group, parent: grand_ancestor) }
+ let_it_be(:group) { create(:group, parent: ancestor) }
+ let_it_be(:ancestor_email) { create(:email, :confirmed, email: 'ancestor@example.com', user: user) }
+ let_it_be(:grand_email) { create(:email, :confirmed, email: 'grand@example.com', user: user) }
- before do
- # Let's set a parent_id for a group that definitely doesn't exist
- group.update_columns(parent_id: 19283746)
- end
+ let_it_be(:groups) { [group] }
- it 'returns a default Global notification setting' do
- expect(subject.count).to eq(1)
- expect(attributes(&:level)).to match_array(['global'])
- expect(attributes(&:notification_email)).to match_array([nil])
- end
+ before do
+ create(:notification_setting, user: user, source: grand_ancestor, level: 'participating', notification_email: grand_email.email)
+ create(:notification_setting, user: user, source: ancestor, level: 'global', notification_email: ancestor_email.email)
end
- context 'when the group has a private parent' do
- let_it_be(:ancestor) { create(:group, :private) }
- let_it_be(:group) { create(:group, :private, parent: ancestor) }
- let_it_be(:ancestor_email) { create(:email, :confirmed, email: 'ancestor@example.com', user: user) }
- let_it_be(:groups) { [group] }
-
- before do
- group.add_reporter(user)
- # Adding the user creates a NotificationSetting, so we remove it here
- user.notification_settings.where(source: group).delete_all
-
- create(:notification_setting, user: user, source: ancestor, level: 'participating', notification_email: ancestor_email.email)
- end
-
- it 'still inherits the notification settings' do
- expect(subject.count).to eq(1)
- expect(attributes(&:level)).to match_array(['participating'])
- expect(attributes(&:notification_email)).to match_array([ancestor_email.email])
- end
+ it 'has the same email and level set', :aggregate_failures do
+ expect(subject.count).to eq(1)
+ expect(attributes(&:level)).to match_array(['global'])
+ expect(attributes(&:notification_email)).to match_array(['ancestor@example.com'])
end
+ end
- it 'does not cause an N+1', :aggregate_failures do
- parent = create(:group)
- child = create(:group, parent: parent)
-
- control = ActiveRecord::QueryRecorder.new do
- described_class.new(user, Group.where(id: child.id)).execute
- end
-
- other_parent = create(:group)
- other_children = create_list(:group, 2, parent: other_parent)
-
- result = nil
+ context 'when the group has parent_id set but that does not belong to any group' do
+ let_it_be(:group) { create(:group) }
+ let_it_be(:groups) { [group] }
- expect do
- result = described_class.new(user, Group.where(id: other_children.append(child).map(&:id))).execute
- end.not_to exceed_query_limit(control)
+ before do
+ # Let's set a parent_id for a group that definitely doesn't exist
+ group.update_columns(parent_id: 19283746)
+ end
- expect(result.count).to eq(3)
+ it 'returns a default Global notification setting' do
+ expect(subject.count).to eq(1)
+ expect(attributes(&:level)).to match_array(['global'])
+ expect(attributes(&:notification_email)).to match_array([nil])
end
end
- end
- context 'preloading `emails_disabled`' do
- let_it_be(:root_group) { create(:group) }
- let_it_be(:sub_group) { create(:group, parent: root_group) }
- let_it_be(:sub_sub_group) { create(:group, parent: sub_group) }
+ context 'when the group has a private parent' do
+ let_it_be(:ancestor) { create(:group, :private) }
+ let_it_be(:group) { create(:group, :private, parent: ancestor) }
+ let_it_be(:ancestor_email) { create(:email, :confirmed, email: 'ancestor@example.com', user: user) }
+ let_it_be(:groups) { [group] }
- let_it_be(:another_root_group) { create(:group) }
- let_it_be(:sub_group_with_emails_disabled) { create(:group, emails_disabled: true, parent: another_root_group) }
- let_it_be(:another_sub_sub_group) { create(:group, parent: sub_group_with_emails_disabled) }
+ before do
+ group.add_reporter(user)
+ # Adding the user creates a NotificationSetting, so we remove it here
+ user.notification_settings.where(source: group).delete_all
- let_it_be(:root_group_with_emails_disabled) { create(:group, emails_disabled: true) }
- let_it_be(:group) { create(:group, parent: root_group_with_emails_disabled) }
-
- let(:groups) { Group.where(id: [sub_sub_group, another_sub_sub_group, group]) }
+ create(:notification_setting, user: user, source: ancestor, level: 'participating', notification_email: ancestor_email.email)
+ end
- before do
- described_class.new(user, groups).execute
+ it 'still inherits the notification settings' do
+ expect(subject.count).to eq(1)
+ expect(attributes(&:level)).to match_array(['participating'])
+ expect(attributes(&:notification_email)).to match_array([ancestor_email.email])
+ end
end
- it 'preloads the `group.emails_disabled` method' do
- recorder = ActiveRecord::QueryRecorder.new do
- groups.each(&:emails_disabled?)
+ it 'does not cause an N+1', :aggregate_failures do
+ parent = create(:group)
+ child = create(:group, parent: parent)
+
+ control = ActiveRecord::QueryRecorder.new do
+ described_class.new(user, Group.where(id: child.id)).execute
end
- expect(recorder.count).to eq(0)
- end
+ other_parent = create(:group)
+ other_children = create_list(:group, 2, parent: other_parent)
- it 'preloads the `group.emails_disabled` method correctly' do
- groups.each do |group|
- expect(group.emails_disabled?).to eq(Group.find(group.id).emails_disabled?) # compare the memoized and the freshly loaded value
- end
+ result = nil
+
+ expect do
+ result = described_class.new(user, Group.where(id: other_children.append(child).map(&:id))).execute
+ end.not_to exceed_query_limit(control)
+
+ expect(result.count).to eq(3)
end
end
end
- it_behaves_like 'user group notifications settings tests'
+ context 'preloading `emails_disabled`' do
+ let_it_be(:root_group) { create(:group) }
+ let_it_be(:sub_group) { create(:group, parent: root_group) }
+ let_it_be(:sub_sub_group) { create(:group, parent: sub_group) }
+
+ let_it_be(:another_root_group) { create(:group) }
+ let_it_be(:sub_group_with_emails_disabled) { create(:group, emails_disabled: true, parent: another_root_group) }
+ let_it_be(:another_sub_sub_group) { create(:group, parent: sub_group_with_emails_disabled) }
+
+ let_it_be(:root_group_with_emails_disabled) { create(:group, emails_disabled: true) }
+ let_it_be(:group) { create(:group, parent: root_group_with_emails_disabled) }
+
+ let(:groups) { Group.where(id: [sub_sub_group, another_sub_sub_group, group]) }
- context 'when feature flag :linear_user_group_notification_settings_finder_ancestors_scopes is disabled' do
before do
- stub_feature_flags(linear_user_group_notification_settings_finder_ancestors_scopes: false)
+ described_class.new(user, groups).execute
+ end
+
+ it 'preloads the `group.emails_disabled` method' do
+ recorder = ActiveRecord::QueryRecorder.new do
+ groups.each(&:emails_disabled?)
+ end
+
+ expect(recorder.count).to eq(0)
end
- it_behaves_like 'user group notifications settings tests'
+ it 'preloads the `group.emails_disabled` method correctly' do
+ groups.each do |group|
+ expect(group.emails_disabled?).to eq(Group.find(group.id).emails_disabled?) # compare the memoized and the freshly loaded value
+ end
+ end
end
end
diff --git a/spec/frontend/crm/contact_form_spec.js b/spec/frontend/crm/contact_form_spec.js
index b2753ad8cf5..0edab4f5ec5 100644
--- a/spec/frontend/crm/contact_form_spec.js
+++ b/spec/frontend/crm/contact_form_spec.js
@@ -112,7 +112,7 @@ describe('Customer relations contact form component', () => {
await waitForPromises();
expect(findError().exists()).toBe(true);
- expect(findError().text()).toBe('Phone is invalid.');
+ expect(findError().text()).toBe('create contact is invalid.');
});
});
@@ -151,7 +151,7 @@ describe('Customer relations contact form component', () => {
await waitForPromises();
expect(findError().exists()).toBe(true);
- expect(findError().text()).toBe('Email is invalid.');
+ expect(findError().text()).toBe('update contact is invalid.');
});
});
});
diff --git a/spec/frontend/crm/form_spec.js b/spec/frontend/crm/form_spec.js
new file mode 100644
index 00000000000..0e3abc05c37
--- /dev/null
+++ b/spec/frontend/crm/form_spec.js
@@ -0,0 +1,278 @@
+import { GlAlert } from '@gitlab/ui';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import VueRouter from 'vue-router';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import Form from '~/crm/components/form.vue';
+import routes from '~/crm/routes';
+import createContactMutation from '~/crm/components/queries/create_contact.mutation.graphql';
+import updateContactMutation from '~/crm/components/queries/update_contact.mutation.graphql';
+import getGroupContactsQuery from '~/crm/components/queries/get_group_contacts.query.graphql';
+import createOrganizationMutation from '~/crm/components/queries/create_organization.mutation.graphql';
+import getGroupOrganizationsQuery from '~/crm/components/queries/get_group_organizations.query.graphql';
+import {
+ createContactMutationErrorResponse,
+ createContactMutationResponse,
+ getGroupContactsQueryResponse,
+ updateContactMutationErrorResponse,
+ updateContactMutationResponse,
+ createOrganizationMutationErrorResponse,
+ createOrganizationMutationResponse,
+ getGroupOrganizationsQueryResponse,
+} from './mock_data';
+
+const FORM_CREATE_CONTACT = 'create contact';
+const FORM_UPDATE_CONTACT = 'update contact';
+const FORM_CREATE_ORG = 'create organization';
+
+describe('Reusable form component', () => {
+ Vue.use(VueApollo);
+ Vue.use(VueRouter);
+
+ const DEFAULT_RESPONSES = {
+ createContact: Promise.resolve(createContactMutationResponse),
+ updateContact: Promise.resolve(updateContactMutationResponse),
+ createOrg: Promise.resolve(createOrganizationMutationResponse),
+ };
+
+ let wrapper;
+ let handler;
+ let fakeApollo;
+ let router;
+
+ beforeEach(() => {
+ router = new VueRouter({
+ base: '',
+ mode: 'history',
+ routes,
+ });
+ router.push('/test');
+
+ handler = jest.fn().mockImplementation((key) => DEFAULT_RESPONSES[key]);
+
+ const hanlderWithKey = (key) => (...args) => handler(key, ...args);
+
+ fakeApollo = createMockApollo([
+ [createContactMutation, hanlderWithKey('createContact')],
+ [updateContactMutation, hanlderWithKey('updateContact')],
+ [createOrganizationMutation, hanlderWithKey('createOrg')],
+ ]);
+
+ fakeApollo.clients.defaultClient.cache.writeQuery({
+ query: getGroupContactsQuery,
+ variables: { groupFullPath: 'flightjs' },
+ data: getGroupContactsQueryResponse.data,
+ });
+
+ fakeApollo.clients.defaultClient.cache.writeQuery({
+ query: getGroupOrganizationsQuery,
+ variables: { groupFullPath: 'flightjs' },
+ data: getGroupOrganizationsQueryResponse.data,
+ });
+ });
+
+ const mockToastShow = jest.fn();
+
+ const findSaveButton = () => wrapper.findByTestId('save-button');
+ const findForm = () => wrapper.find('form');
+ const findError = () => wrapper.findComponent(GlAlert);
+
+ const mountComponent = (propsData) => {
+ wrapper = shallowMountExtended(Form, {
+ router,
+ apolloProvider: fakeApollo,
+ propsData: { drawerOpen: true, ...propsData },
+ mocks: {
+ $toast: {
+ show: mockToastShow,
+ },
+ },
+ });
+ };
+
+ const mountContact = ({ propsData } = {}) => {
+ mountComponent({
+ fields: [
+ { name: 'firstName', label: 'First name', required: true },
+ { name: 'lastName', label: 'Last name', required: true },
+ { name: 'email', label: 'Email', required: true },
+ { name: 'phone', label: 'Phone' },
+ { name: 'description', label: 'Description' },
+ ],
+ ...propsData,
+ });
+ };
+
+ const mountContactCreate = () => {
+ const propsData = {
+ title: 'New contact',
+ successMessage: 'Contact has been added',
+ buttonLabel: 'Create contact',
+ getQuery: {
+ query: getGroupContactsQuery,
+ variables: { groupFullPath: 'flightjs' },
+ },
+ getQueryNodePath: 'group.contacts',
+ mutation: createContactMutation,
+ additionalCreateParams: { groupId: 'gid://gitlab/Group/26' },
+ };
+ mountContact({ propsData });
+ };
+
+ const mountContactUpdate = () => {
+ const propsData = {
+ title: 'Edit contact',
+ successMessage: 'Contact has been updated',
+ mutation: updateContactMutation,
+ existingModel: {
+ id: 'gid://gitlab/CustomerRelations::Contact/12',
+ firstName: 'First',
+ lastName: 'Last',
+ email: 'email@example.com',
+ },
+ };
+ mountContact({ propsData });
+ };
+
+ const mountOrganization = ({ propsData } = {}) => {
+ mountComponent({
+ fields: [
+ { name: 'name', label: 'Name', required: true },
+ { name: 'defaultRate', label: 'Default rate', input: { type: 'number', step: '0.01' } },
+ { name: 'description', label: 'Description' },
+ ],
+ ...propsData,
+ });
+ };
+
+ const mountOrganizationCreate = () => {
+ const propsData = {
+ title: 'New organization',
+ successMessage: 'Organization has been added',
+ buttonLabel: 'Create organization',
+ getQuery: {
+ query: getGroupOrganizationsQuery,
+ variables: { groupFullPath: 'flightjs' },
+ },
+ getQueryNodePath: 'group.organizations',
+ mutation: createOrganizationMutation,
+ additionalCreateParams: { groupId: 'gid://gitlab/Group/26' },
+ };
+ mountOrganization({ propsData });
+ };
+
+ const forms = {
+ [FORM_CREATE_CONTACT]: {
+ mountFunction: mountContactCreate,
+ mutationErrorResponse: createContactMutationErrorResponse,
+ toastMessage: 'Contact has been added',
+ },
+ [FORM_UPDATE_CONTACT]: {
+ mountFunction: mountContactUpdate,
+ mutationErrorResponse: updateContactMutationErrorResponse,
+ toastMessage: 'Contact has been updated',
+ },
+ [FORM_CREATE_ORG]: {
+ mountFunction: mountOrganizationCreate,
+ mutationErrorResponse: createOrganizationMutationErrorResponse,
+ toastMessage: 'Organization has been added',
+ },
+ };
+ const asTestParams = (...keys) => keys.map((name) => [name, forms[name]]);
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe.each(asTestParams(FORM_CREATE_CONTACT, FORM_UPDATE_CONTACT))(
+ '%s form save button',
+ (name, { mountFunction }) => {
+ beforeEach(() => {
+ mountFunction();
+ });
+
+ it('should be disabled when required fields are empty', async () => {
+ wrapper.find('#firstName').vm.$emit('input', '');
+ await waitForPromises();
+
+ expect(findSaveButton().props('disabled')).toBe(true);
+ });
+
+ it('should not be disabled when required fields have values', async () => {
+ wrapper.find('#firstName').vm.$emit('input', 'A');
+ wrapper.find('#lastName').vm.$emit('input', 'B');
+ wrapper.find('#email').vm.$emit('input', 'C');
+ await waitForPromises();
+
+ expect(findSaveButton().props('disabled')).toBe(false);
+ });
+ },
+ );
+
+ describe.each(asTestParams(FORM_CREATE_ORG))('%s form save button', (name, { mountFunction }) => {
+ beforeEach(() => {
+ mountFunction();
+ });
+
+ it('should be disabled when required field is empty', async () => {
+ wrapper.find('#name').vm.$emit('input', '');
+ await waitForPromises();
+
+ expect(findSaveButton().props('disabled')).toBe(true);
+ });
+
+ it('should not be disabled when required field has a value', async () => {
+ wrapper.find('#name').vm.$emit('input', 'A');
+ await waitForPromises();
+
+ expect(findSaveButton().props('disabled')).toBe(false);
+ });
+ });
+
+ describe.each(asTestParams(FORM_CREATE_CONTACT, FORM_UPDATE_CONTACT, FORM_CREATE_ORG))(
+ 'when %s mutation is successful',
+ (name, { mountFunction, toastMessage }) => {
+ it('form should display correct toast message', async () => {
+ mountFunction();
+
+ findForm().trigger('submit');
+ await waitForPromises();
+
+ expect(mockToastShow).toHaveBeenCalledWith(toastMessage);
+ });
+ },
+ );
+
+ describe.each(asTestParams(FORM_CREATE_CONTACT, FORM_UPDATE_CONTACT, FORM_CREATE_ORG))(
+ 'when %s mutation fails',
+ (formName, { mutationErrorResponse, mountFunction }) => {
+ beforeEach(() => {
+ jest.spyOn(console, 'error').mockImplementation();
+ });
+
+ it('should show error on reject', async () => {
+ handler.mockRejectedValue('ERROR');
+
+ mountFunction();
+
+ findForm().trigger('submit');
+ await waitForPromises();
+
+ expect(findError().text()).toBe('Something went wrong. Please try again.');
+ });
+
+ it('should show error on error response', async () => {
+ handler.mockResolvedValue(mutationErrorResponse);
+
+ mountFunction();
+
+ findForm().trigger('submit');
+ await waitForPromises();
+
+ expect(findError().text()).toBe(`${formName} is invalid.`);
+ });
+ },
+ );
+});
diff --git a/spec/frontend/crm/mock_data.js b/spec/frontend/crm/mock_data.js
index f7af2ccdb72..e351e101b29 100644
--- a/spec/frontend/crm/mock_data.js
+++ b/spec/frontend/crm/mock_data.js
@@ -82,7 +82,6 @@ export const getGroupOrganizationsQueryResponse = {
export const createContactMutationResponse = {
data: {
customerRelationsContactCreate: {
- __typeName: 'CustomerRelationsContactCreatePayload',
contact: {
__typename: 'CustomerRelationsContact',
id: 'gid://gitlab/CustomerRelations::Contact/1',
@@ -102,7 +101,7 @@ export const createContactMutationErrorResponse = {
data: {
customerRelationsContactCreate: {
contact: null,
- errors: ['Phone is invalid.'],
+ errors: ['create contact is invalid.'],
},
},
};
@@ -130,7 +129,7 @@ export const updateContactMutationErrorResponse = {
data: {
customerRelationsContactUpdate: {
contact: null,
- errors: ['Email is invalid.'],
+ errors: ['update contact is invalid.'],
},
},
};
@@ -138,7 +137,6 @@ export const updateContactMutationErrorResponse = {
export const createOrganizationMutationResponse = {
data: {
customerRelationsOrganizationCreate: {
- __typeName: 'CustomerRelationsOrganizationCreatePayload',
organization: {
__typename: 'CustomerRelationsOrganization',
id: 'gid://gitlab/CustomerRelations::Organization/2',
@@ -155,7 +153,7 @@ export const createOrganizationMutationErrorResponse = {
data: {
customerRelationsOrganizationCreate: {
organization: null,
- errors: ['Name cannot be blank.'],
+ errors: ['create organization is invalid.'],
},
},
};
diff --git a/spec/frontend/crm/new_organization_form_spec.js b/spec/frontend/crm/new_organization_form_spec.js
index 976b626f35f..0a7909774c9 100644
--- a/spec/frontend/crm/new_organization_form_spec.js
+++ b/spec/frontend/crm/new_organization_form_spec.js
@@ -103,7 +103,7 @@ describe('Customer relations organizations root app', () => {
await waitForPromises();
expect(findError().exists()).toBe(true);
- expect(findError().text()).toBe('Name cannot be blank.');
+ expect(findError().text()).toBe('create organization is invalid.');
});
});
});
diff --git a/spec/lib/api/entities/ci/pipeline_spec.rb b/spec/lib/api/entities/ci/pipeline_spec.rb
index 6a658cc3e18..2b8e59b68c6 100644
--- a/spec/lib/api/entities/ci/pipeline_spec.rb
+++ b/spec/lib/api/entities/ci/pipeline_spec.rb
@@ -3,14 +3,31 @@
require 'spec_helper'
RSpec.describe API::Entities::Ci::Pipeline do
- let_it_be(:pipeline) { create(:ci_empty_pipeline) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:pipeline) { create(:ci_empty_pipeline, user: user) }
let_it_be(:job) { create(:ci_build, name: "rspec", coverage: 30.212, pipeline: pipeline) }
let(:entity) { described_class.new(pipeline) }
subject { entity.as_json }
- it 'returns the coverage as a string' do
+ exposed_fields = %i[before_sha tag yaml_errors created_at updated_at started_at finished_at committed_at duration queued_duration]
+
+ exposed_fields.each do |field|
+ it "exposes pipeline #{field}" do
+ expect(subject[field]).to eq(pipeline.public_send(field))
+ end
+ end
+
+ it 'exposes pipeline user basic information' do
+ expect(subject[:user].keys).to include(:avatar_url, :web_url)
+ end
+
+ it 'exposes pipeline detailed status' do
+ expect(subject[:detailed_status].keys).to include(:icon, :favicon)
+ end
+
+ it 'exposes pipeline coverage as a string' do
expect(subject[:coverage]).to eq '30.21'
end
end
diff --git a/spec/lib/backup/files_spec.rb b/spec/lib/backup/files_spec.rb
index 92de191da2d..6bff0919293 100644
--- a/spec/lib/backup/files_spec.rb
+++ b/spec/lib/backup/files_spec.rb
@@ -134,7 +134,7 @@ RSpec.describe Backup::Files do
expect do
subject.dump
- end.to raise_error(/Backup operation failed:/)
+ end.to raise_error(/Failed to create compressed file/)
end
describe 'with STRATEGY=copy' do
@@ -170,7 +170,7 @@ RSpec.describe Backup::Files do
expect do
subject.dump
end.to output(/rsync failed/).to_stdout
- .and raise_error(/Backup failed/)
+ .and raise_error(/Failed to create compressed file/)
end
end
end
diff --git a/spec/lib/backup/repository_backup_error_spec.rb b/spec/lib/backup/repository_backup_error_spec.rb
deleted file mode 100644
index 44c75c1cf77..00000000000
--- a/spec/lib/backup/repository_backup_error_spec.rb
+++ /dev/null
@@ -1,42 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Backup::RepositoryBackupError do
- let_it_be(:snippet) { create(:snippet, content: 'foo', file_name: 'foo') }
- let_it_be(:project) { create(:project, :repository) }
- let_it_be(:wiki) { ProjectWiki.new(project, nil ) }
-
- let(:backup_repos_path) { '/tmp/backup/repositories' }
-
- shared_examples 'includes backup path' do
- it { is_expected.to respond_to :container }
- it { is_expected.to respond_to :backup_repos_path }
-
- it 'expects exception message to include repo backup path location' do
- expect(subject.message).to include("#{subject.backup_repos_path}")
- end
-
- it 'expects exception message to include container being back-up' do
- expect(subject.message).to include("#{subject.container.disk_path}")
- end
- end
-
- context 'with snippet repository' do
- subject { described_class.new(snippet, backup_repos_path) }
-
- it_behaves_like 'includes backup path'
- end
-
- context 'with project repository' do
- subject { described_class.new(project, backup_repos_path) }
-
- it_behaves_like 'includes backup path'
- end
-
- context 'with wiki repository' do
- subject { described_class.new(wiki, backup_repos_path) }
-
- it_behaves_like 'includes backup path'
- end
-end
diff --git a/spec/models/packages/package_file_spec.rb b/spec/models/packages/package_file_spec.rb
index 8617793f41d..5b5cb0827a8 100644
--- a/spec/models/packages/package_file_spec.rb
+++ b/spec/models/packages/package_file_spec.rb
@@ -10,6 +10,8 @@ RSpec.describe Packages::PackageFile, type: :model do
let_it_be(:package_file3) { create(:package_file, :xml, file_name: 'formatted.zip') }
let_it_be(:debian_package) { create(:debian_package, project: project) }
+ it_behaves_like 'having unique enum values'
+
describe 'relationships' do
it { is_expected.to belong_to(:package) }
it { is_expected.to have_one(:conan_file_metadatum) }
@@ -138,6 +140,24 @@ RSpec.describe Packages::PackageFile, type: :model do
it 'returns the matching file only for Helm packages' do
expect(described_class.for_helm_with_channel(project, channel)).to contain_exactly(helm_file2)
end
+
+ context 'with package files pending destruction' do
+ let_it_be(:package_file_pending_destruction) { create(:helm_package_file, :pending_destruction, package: helm_package2, channel: channel) }
+
+ it 'does not return them' do
+ expect(described_class.for_helm_with_channel(project, channel)).to contain_exactly(helm_file2)
+ end
+
+ context 'with packages_installable_package_files disabled' do
+ before do
+ stub_feature_flags(packages_installable_package_files: false)
+ end
+
+ it 'returns them' do
+ expect(described_class.for_helm_with_channel(project, channel)).to contain_exactly(helm_file2, package_file_pending_destruction)
+ end
+ end
+ end
end
describe '.most_recent!' do
@@ -154,15 +174,17 @@ RSpec.describe Packages::PackageFile, type: :model do
let_it_be(:package_file3_2) { create(:package_file, :npm, package: package3) }
let_it_be(:package_file3_3) { create(:package_file, :npm, package: package3) }
+ let_it_be(:package_file3_4) { create(:package_file, :npm, :pending_destruction, package: package3) }
let_it_be(:package_file4_2) { create(:package_file, :npm, package: package2) }
let_it_be(:package_file4_3) { create(:package_file, :npm, package: package2) }
let_it_be(:package_file4_4) { create(:package_file, :npm, package: package2) }
+ let_it_be(:package_file4_4) { create(:package_file, :npm, :pending_destruction, package: package2) }
- let(:most_recent_package_file1) { package1.package_files.recent.first }
- let(:most_recent_package_file2) { package2.package_files.recent.first }
- let(:most_recent_package_file3) { package3.package_files.recent.first }
- let(:most_recent_package_file4) { package4.package_files.recent.first }
+ let(:most_recent_package_file1) { package1.installable_package_files.recent.first }
+ let(:most_recent_package_file2) { package2.installable_package_files.recent.first }
+ let(:most_recent_package_file3) { package3.installable_package_files.recent.first }
+ let(:most_recent_package_file4) { package4.installable_package_files.recent.first }
subject { described_class.most_recent_for(packages) }
@@ -202,6 +224,24 @@ RSpec.describe Packages::PackageFile, type: :model do
it 'returns the most recent package for the selected channel' do
expect(subject).to contain_exactly(helm_package_file2)
end
+
+ context 'with package files pending destruction' do
+ let_it_be(:package_file_pending_destruction) { create(:helm_package_file, :pending_destruction, package: helm_package, channel: 'alpha') }
+
+ it 'does not return them' do
+ expect(subject).to contain_exactly(helm_package_file2)
+ end
+
+ context 'with packages_installable_package_files disabled' do
+ before do
+ stub_feature_flags(packages_installable_package_files: false)
+ end
+
+ it 'returns them' do
+ expect(subject).to contain_exactly(package_file_pending_destruction)
+ end
+ end
+ end
end
end
@@ -314,4 +354,25 @@ RSpec.describe Packages::PackageFile, type: :model do
end
end
end
+
+ context 'status scopes' do
+ let_it_be(:package) { create(:package) }
+ let_it_be(:default_package_file) { create(:package_file, package: package) }
+ let_it_be(:pending_destruction_package_file) { create(:package_file, :pending_destruction, package: package) }
+
+ describe '.installable' do
+ subject { package.installable_package_files }
+
+ it 'does not include non-displayable packages', :aggregate_failures do
+ is_expected.to include(default_package_file)
+ is_expected.not_to include(pending_destruction_package_file)
+ end
+ end
+
+ describe '.with_status' do
+ subject { described_class.with_status(:pending_destruction) }
+
+ it { is_expected.to contain_exactly(pending_destruction_package_file) }
+ end
+ end
end
diff --git a/spec/presenters/packages/conan/package_presenter_spec.rb b/spec/presenters/packages/conan/package_presenter_spec.rb
index 6d82c5ef547..27ecf32b6f2 100644
--- a/spec/presenters/packages/conan/package_presenter_spec.rb
+++ b/spec/presenters/packages/conan/package_presenter_spec.rb
@@ -9,6 +9,7 @@ RSpec.describe ::Packages::Conan::PackagePresenter do
let_it_be(:conan_package_reference) { '123456789'}
let(:params) { { package_scope: :instance } }
+ let(:presenter) { described_class.new(package, user, project, params) }
shared_examples 'no existing package' do
context 'when package does not exist' do
@@ -21,7 +22,7 @@ RSpec.describe ::Packages::Conan::PackagePresenter do
shared_examples 'conan_file_metadatum is not found' do
context 'when no conan_file_metadatum exists' do
before do
- package.package_files.each do |file|
+ package.installable_package_files.each do |file|
file.conan_file_metadatum.delete
file.reload
end
@@ -32,7 +33,7 @@ RSpec.describe ::Packages::Conan::PackagePresenter do
end
describe '#recipe_urls' do
- subject { described_class.new(package, user, project, params).recipe_urls }
+ subject { presenter.recipe_urls }
it_behaves_like 'no existing package'
it_behaves_like 'conan_file_metadatum is not found'
@@ -71,7 +72,9 @@ RSpec.describe ::Packages::Conan::PackagePresenter do
end
describe '#recipe_snapshot' do
- subject { described_class.new(package, user, project).recipe_snapshot }
+ let(:params) { {} }
+
+ subject { presenter.recipe_snapshot }
it_behaves_like 'no existing package'
it_behaves_like 'conan_file_metadatum is not found'
@@ -180,12 +183,9 @@ RSpec.describe ::Packages::Conan::PackagePresenter do
describe '#package_snapshot' do
let(:reference) { conan_package_reference }
+ let(:params) { { conan_package_reference: reference } }
- subject do
- described_class.new(
- package, user, project, conan_package_reference: reference
- ).package_snapshot
- end
+ subject { presenter.package_snapshot }
it_behaves_like 'no existing package'
it_behaves_like 'conan_file_metadatum is not found'
@@ -208,4 +208,22 @@ RSpec.describe ::Packages::Conan::PackagePresenter do
end
end
end
+
+ # TODO when cleaning up packages_installable_package_files, consider removing this context and
+ # add a dummy package file pending destruction on L8
+ context 'with package files pending destruction' do
+ let_it_be(:package_file_pending_destruction) { create(:package_file, :pending_destruction, package: package) }
+
+ subject { presenter.send(:package_files).to_a }
+
+ it { is_expected.not_to include(package_file_pending_destruction) }
+
+ context 'with packages_installable_package_files disabled' do
+ before do
+ stub_feature_flags(packages_installable_package_files: false)
+ end
+
+ it { is_expected.to include(package_file_pending_destruction) }
+ end
+ end
end
diff --git a/spec/presenters/packages/detail/package_presenter_spec.rb b/spec/presenters/packages/detail/package_presenter_spec.rb
index 3009f2bd56d..4e2645b27ff 100644
--- a/spec/presenters/packages/detail/package_presenter_spec.rb
+++ b/spec/presenters/packages/detail/package_presenter_spec.rb
@@ -6,12 +6,12 @@ RSpec.describe ::Packages::Detail::PackagePresenter do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, creator: user) }
let_it_be(:package) { create(:npm_package, :with_build, project: project) }
- let(:presenter) { described_class.new(package) }
-
let_it_be(:user_info) { { name: user.name, avatar_url: user.avatar_url } }
+ let(:presenter) { described_class.new(package) }
+
let!(:expected_package_files) do
- package.package_files.map do |file|
+ package.installable_package_files.map do |file|
{
created_at: file.created_at,
download_path: file.download_path,
@@ -154,5 +154,21 @@ RSpec.describe ::Packages::Detail::PackagePresenter do
expect(presenter.detail_view).to eq expected_package_details
end
end
+
+ context 'with package files pending destruction' do
+ let_it_be(:package_file_pending_destruction) { create(:package_file, :pending_destruction, package: package) }
+
+ subject { presenter.detail_view[:package_files].map { |e| e[:id] } }
+
+ it { is_expected.not_to include(package_file_pending_destruction.id) }
+
+ context 'with packages_installable_package_files disabled' do
+ before do
+ stub_feature_flags(packages_installable_package_files: false)
+ end
+
+ it { is_expected.to include(package_file_pending_destruction.id) }
+ end
+ end
end
end
diff --git a/spec/presenters/packages/npm/package_presenter_spec.rb b/spec/presenters/packages/npm/package_presenter_spec.rb
index 3b6dfcd20b8..7d54d017b26 100644
--- a/spec/presenters/packages/npm/package_presenter_spec.rb
+++ b/spec/presenters/packages/npm/package_presenter_spec.rb
@@ -95,6 +95,26 @@ RSpec.describe ::Packages::Npm::PackagePresenter do
end
end
end
+
+ context 'with package files pending destruction' do
+ let_it_be(:package_file_pending_destruction) { create(:package_file, :pending_destruction, package: package2, file_sha1: 'pending_destruction_sha1') }
+
+ let(:shasums) { subject.values.map { |v| v.dig(:dist, :shasum) } }
+
+ it 'does not return them' do
+ expect(shasums).not_to include(package_file_pending_destruction.file_sha1)
+ end
+
+ context 'with packages_installable_package_files disabled' do
+ before do
+ stub_feature_flags(packages_installable_package_files: false)
+ end
+
+ it 'returns them' do
+ expect(shasums).to include(package_file_pending_destruction.file_sha1)
+ end
+ end
+ end
end
describe '#dist_tags' do
diff --git a/spec/presenters/packages/nuget/package_metadata_presenter_spec.rb b/spec/presenters/packages/nuget/package_metadata_presenter_spec.rb
index 8bb0694f39c..6e99b6bafec 100644
--- a/spec/presenters/packages/nuget/package_metadata_presenter_spec.rb
+++ b/spec/presenters/packages/nuget/package_metadata_presenter_spec.rb
@@ -24,6 +24,20 @@ RSpec.describe Packages::Nuget::PackageMetadataPresenter do
subject { presenter.archive_url }
it { is_expected.to end_with(expected_suffix) }
+
+ context 'with package files pending destruction' do
+ let_it_be(:package_file_pending_destruction) { create(:package_file, :pending_destruction, package: package, file_name: 'pending_destruction.nupkg') }
+
+ it { is_expected.not_to include('pending_destruction.nupkg') }
+
+ context 'with packages_installable_package_files disabled' do
+ before do
+ stub_feature_flags(packages_installable_package_files: false)
+ end
+
+ it { is_expected.to include('pending_destruction.nupkg') }
+ end
+ end
end
describe '#catalog_entry' do
diff --git a/spec/presenters/packages/pypi/package_presenter_spec.rb b/spec/presenters/packages/pypi/package_presenter_spec.rb
index 25aa5c31034..8a23c0ec3cb 100644
--- a/spec/presenters/packages/pypi/package_presenter_spec.rb
+++ b/spec/presenters/packages/pypi/package_presenter_spec.rb
@@ -52,5 +52,21 @@ RSpec.describe ::Packages::Pypi::PackagePresenter do
it_behaves_like 'pypi package presenter'
end
+
+ context 'with package files pending destruction' do
+ let_it_be(:package_file_pending_destruction) { create(:package_file, :pending_destruction, package: package1, file_name: "package_file_pending_destruction") }
+
+ let(:project_or_group) { project }
+
+ it { is_expected.not_to include(package_file_pending_destruction.file_name)}
+
+ context 'with packages_installable_package_files disabled' do
+ before do
+ stub_feature_flags(packages_installable_package_files: false)
+ end
+
+ it { is_expected.to include(package_file_pending_destruction.file_name)}
+ end
+ end
end
end
diff --git a/spec/requests/api/graphql/packages/package_spec.rb b/spec/requests/api/graphql/packages/package_spec.rb
index a9019a7611a..03dd310cf0e 100644
--- a/spec/requests/api/graphql/packages/package_spec.rb
+++ b/spec/requests/api/graphql/packages/package_spec.rb
@@ -73,6 +73,31 @@ RSpec.describe 'package details' do
end
end
+ context 'with package files pending destruction' do
+ let_it_be(:package_file) { create(:package_file, package: composer_package) }
+ let_it_be(:package_file_pending_destruction) { create(:package_file, :pending_destruction, package: composer_package) }
+
+ let(:package_file_ids) { graphql_data_at(:package, :package_files, :nodes).map { |node| node["id"] } }
+
+ it 'does not return them' do
+ subject
+
+ expect(package_file_ids).to contain_exactly(package_file.to_global_id.to_s)
+ end
+
+ context 'with packages_installable_package_files disabled' do
+ before do
+ stub_feature_flags(packages_installable_package_files: false)
+ end
+
+ it 'returns them' do
+ subject
+
+ expect(package_file_ids).to contain_exactly(package_file_pending_destruction.to_global_id.to_s, package_file.to_global_id.to_s)
+ end
+ end
+ end
+
context 'with a batched query' do
let_it_be(:conan_package) { create(:conan_package, project: project) }
diff --git a/spec/requests/api/package_files_spec.rb b/spec/requests/api/package_files_spec.rb
index eb1f04d193e..7a6b1599154 100644
--- a/spec/requests/api/package_files_spec.rb
+++ b/spec/requests/api/package_files_spec.rb
@@ -76,6 +76,30 @@ RSpec.describe API::PackageFiles do
end
end
end
+
+ context 'with package files pending destruction' do
+ let!(:package_file_pending_destruction) { create(:package_file, :pending_destruction, package: package) }
+
+ let(:package_file_ids) { json_response.map { |e| e['id'] } }
+
+ it 'does not return them' do
+ get api(url, user)
+
+ expect(package_file_ids).not_to include(package_file_pending_destruction.id)
+ end
+
+ context 'with packages_installable_package_files disabled' do
+ before do
+ stub_feature_flags(packages_installable_package_files: false)
+ end
+
+ it 'returns them' do
+ get api(url, user)
+
+ expect(package_file_ids).to include(package_file_pending_destruction.id)
+ end
+ end
+ end
end
end
@@ -149,6 +173,32 @@ RSpec.describe API::PackageFiles do
expect(response).to have_gitlab_http_status(:not_found)
end
end
+
+ context 'with package file pending destruction' do
+ let!(:package_file_id) { create(:package_file, :pending_destruction, package: package).id }
+
+ before do
+ project.add_maintainer(user)
+ end
+
+ it 'can not be accessed', :aggregate_failures do
+ expect { api_request }.not_to change { package.package_files.count }
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+
+ context 'with packages_installable_package_files disabled' do
+ before do
+ stub_feature_flags(packages_installable_package_files: false)
+ end
+
+ it 'can be accessed', :aggregate_failures do
+ expect { api_request }.to change { package.package_files.count }.by(-1)
+
+ expect(response).to have_gitlab_http_status(:no_content)
+ end
+ end
+ end
end
end
end
diff --git a/spec/requests/api/rubygem_packages_spec.rb b/spec/requests/api/rubygem_packages_spec.rb
index 9b104520b52..0e63a7269e7 100644
--- a/spec/requests/api/rubygem_packages_spec.rb
+++ b/spec/requests/api/rubygem_packages_spec.rb
@@ -173,6 +173,34 @@ RSpec.describe API::RubygemPackages do
it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member]
end
end
+
+ context 'with package files pending destruction' do
+ let_it_be(:package_file_pending_destruction) { create(:package_file, :pending_destruction, :xml, package: package, file_name: file_name) }
+
+ before do
+ project.update_column(:visibility_level, Gitlab::VisibilityLevel::PUBLIC)
+ end
+
+ it 'does not return them' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.body).not_to eq(package_file_pending_destruction.file.file.read)
+ end
+
+ context 'with packages_installable_package_files disabled' do
+ before do
+ stub_feature_flags(packages_installable_package_files: false)
+ end
+
+ it 'returns them' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.body).to eq(package_file_pending_destruction.file.file.read)
+ end
+ end
+ end
end
describe 'POST /api/v4/projects/:project_id/packages/rubygems/api/v1/gems/authorize' do
diff --git a/spec/requests/api/terraform/modules/v1/packages_spec.rb b/spec/requests/api/terraform/modules/v1/packages_spec.rb
index b17bc11a451..c0f04ba09be 100644
--- a/spec/requests/api/terraform/modules/v1/packages_spec.rb
+++ b/spec/requests/api/terraform/modules/v1/packages_spec.rb
@@ -154,6 +154,7 @@ RSpec.describe API::Terraform::Modules::V1::Packages do
end
describe 'GET /api/v4/packages/terraform/modules/v1/:module_namespace/:module_name/:module_system/:module_version/file' do
+ let(:url) { api("/packages/terraform/modules/v1/#{group.path}/#{package.name}/#{package.version}/file?token=#{token}") }
let(:tokens) do
{
personal_access_token: ::Gitlab::JWTToken.new.tap { |jwt| jwt['token'] = personal_access_token.id }.encoded,
@@ -202,7 +203,6 @@ RSpec.describe API::Terraform::Modules::V1::Packages do
with_them do
let(:token) { valid_token ? tokens[token_type] : 'invalid-token123' }
- let(:url) { api("/packages/terraform/modules/v1/#{group.path}/#{package.name}/#{package.version}/file?token=#{token}") }
let(:snowplow_gitlab_standard_context) { { project: project, user: user, namespace: project.namespace } }
before do
@@ -212,6 +212,41 @@ RSpec.describe API::Terraform::Modules::V1::Packages do
it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member]
end
end
+
+ context 'with package file pending destruction' do
+ let_it_be(:package) { create(:package, package_type: :terraform_module, project: project, name: "module-555/pending-destruction", version: '1.0.0') }
+ let_it_be(:package_file_pending_destruction) { create(:package_file, :pending_destruction, :xml, package: package) }
+ let_it_be(:package_file) { create(:package_file, :terraform_module, package: package) }
+
+ let(:token) { tokens[:personal_access_token] }
+ let(:headers) { { 'Authorization' => "Bearer #{token}" } }
+
+ before do
+ project.add_maintainer(user)
+ end
+
+ it 'does not return them' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.body).not_to eq(package_file_pending_destruction.file.file.read)
+ expect(response.body).to eq(package_file.file.file.read)
+ end
+
+ context 'with packages_installable_package_files disabled' do
+ before do
+ stub_feature_flags(packages_installable_package_files: false)
+ end
+
+ it 'returns them' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.body).to eq(package_file_pending_destruction.file.file.read)
+ expect(response.body).not_to eq(package_file.file.file.read)
+ end
+ end
+ end
end
describe 'PUT /api/v4/projects/:project_id/packages/terraform/modules/:module_name/:module_system/:module_version/file/authorize' do
diff --git a/spec/services/packages/maven/metadata/sync_service_spec.rb b/spec/services/packages/maven/metadata/sync_service_spec.rb
index 30ddb48207a..a736ed281f0 100644
--- a/spec/services/packages/maven/metadata/sync_service_spec.rb
+++ b/spec/services/packages/maven/metadata/sync_service_spec.rb
@@ -265,4 +265,22 @@ RSpec.describe ::Packages::Maven::Metadata::SyncService do
end
end
end
+
+ # TODO When cleaning up packages_installable_package_files, consider adding a
+ # dummy package file pending for destruction on L10/11 and remove this context
+ context 'with package files pending destruction' do
+ let_it_be(:package_file_pending_destruction) { create(:package_file, :pending_destruction, package: versionless_package_for_versions, file_name: Packages::Maven::Metadata.filename) }
+
+ subject { service.send(:metadata_package_file_for, versionless_package_for_versions) }
+
+ it { is_expected.not_to eq(package_file_pending_destruction) }
+
+ context 'with packages_installable_package_files disabled' do
+ before do
+ stub_feature_flags(packages_installable_package_files: false)
+ end
+
+ it { is_expected.to eq(package_file_pending_destruction) }
+ end
+ end
end
diff --git a/spec/support/shared_examples/models/packages/debian/distribution_shared_examples.rb b/spec/support/shared_examples/models/packages/debian/distribution_shared_examples.rb
index 750d3dd11e3..3f8c3b8960b 100644
--- a/spec/support/shared_examples/models/packages/debian/distribution_shared_examples.rb
+++ b/spec/support/shared_examples/models/packages/debian/distribution_shared_examples.rb
@@ -198,7 +198,6 @@ RSpec.shared_examples 'Debian Distribution' do |factory, container, can_freeze|
describe 'relationships' do
it { is_expected.to have_many(:publications).class_name('Packages::Debian::Publication').inverse_of(:distribution).with_foreign_key(:distribution_id) }
it { is_expected.to have_many(:packages).class_name('Packages::Package').through(:publications) }
- it { is_expected.to have_many(:package_files).class_name('Packages::PackageFile').through(:packages) }
end
end
else
@@ -229,6 +228,26 @@ RSpec.shared_examples 'Debian Distribution' do |factory, container, can_freeze|
it 'returns only files from public packages with same codename' do
expect(subject.to_a).to contain_exactly(*public_package_with_same_codename.package_files)
end
+
+ context 'with pending destruction package files' do
+ let_it_be(:package_file_pending_destruction) { create(:package_file, :pending_destruction, package: public_package_with_same_codename) }
+
+ it 'does not return them' do
+ expect(subject.to_a).not_to include(package_file_pending_destruction)
+ end
+
+ context 'with packages_installable_package_files disabled' do
+ before do
+ stub_feature_flags(packages_installable_package_files: false)
+ end
+
+ it 'returns them' do
+ subject
+
+ expect(subject.to_a).to include(package_file_pending_destruction)
+ end
+ end
+ end
end
end
end
diff --git a/spec/support/shared_examples/requests/api/graphql/packages/package_details_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/packages/package_details_shared_examples.rb
index d576a5874fd..9385706d991 100644
--- a/spec/support/shared_examples/requests/api/graphql/packages/package_details_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/graphql/packages/package_details_shared_examples.rb
@@ -38,4 +38,28 @@ RSpec.shared_examples 'a package with files' do
'fileSha256' => first_file.file_sha256
)
end
+
+ context 'with package files pending destruction' do
+ let_it_be(:package_file_pending_destruction) { create(:package_file, :pending_destruction, package: package) }
+
+ let(:response_package_file_ids) { package_files_response.map { |pf| pf['id'] } }
+
+ it 'does not return them' do
+ expect(package.reload.package_files).to include(package_file_pending_destruction)
+
+ expect(response_package_file_ids).not_to include(package_file_pending_destruction.to_global_id.to_s)
+ end
+
+ context 'with packages_installable_package_files disabled' do
+ before(:context) do
+ stub_feature_flags(packages_installable_package_files: false)
+ end
+
+ it 'returns them' do
+ expect(package.reload.package_files).to include(package_file_pending_destruction)
+
+ expect(response_package_file_ids).to include(package_file_pending_destruction.to_global_id.to_s)
+ end
+ end
+ end
end
diff --git a/spec/support/shared_examples/requests/api/nuget_endpoints_shared_examples.rb b/spec/support/shared_examples/requests/api/nuget_endpoints_shared_examples.rb
index db70bc75c63..290bf58fb6b 100644
--- a/spec/support/shared_examples/requests/api/nuget_endpoints_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/nuget_endpoints_shared_examples.rb
@@ -221,6 +221,7 @@ RSpec.shared_examples 'handling nuget search requests' do |anonymous_requests_ex
let_it_be(:packages_c) { create_list(:nuget_package, 5, name: 'Dummy.PackageC', project: project) }
let_it_be(:package_d) { create(:nuget_package, name: 'Dummy.PackageD', version: '5.0.5-alpha', project: project) }
let_it_be(:package_e) { create(:nuget_package, name: 'Foo.BarE', project: project) }
+
let(:search_term) { 'uMmy' }
let(:take) { 26 }
let(:skip) { 0 }
diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb
index 5a7fbb42536..c6833531f62 100644
--- a/spec/tasks/gitlab/backup_rake_spec.rb
+++ b/spec/tasks/gitlab/backup_rake_spec.rb
@@ -222,6 +222,37 @@ RSpec.describe 'gitlab:app namespace rake task', :delete do
end
end
+ describe 'backup create fails' do
+ using RSpec::Parameterized::TableSyntax
+
+ file_backup_error = Backup::FileBackupError.new('/tmp', '/tmp/backup/uploads')
+ config = ActiveRecord::Base.configurations.find_db_config(Rails.env).configuration_hash
+ db_file_name = File.join(Gitlab.config.backup.path, 'db', 'database.sql.gz')
+ db_backup_error = Backup::DatabaseBackupError.new(config, db_file_name)
+
+ where(:backup_class, :rake_task, :error) do
+ Backup::Database | 'gitlab:backup:db:create' | db_backup_error
+ Backup::Builds | 'gitlab:backup:builds:create' | file_backup_error
+ Backup::Uploads | 'gitlab:backup:uploads:create' | file_backup_error
+ Backup::Artifacts | 'gitlab:backup:artifacts:create' | file_backup_error
+ Backup::Pages | 'gitlab:backup:pages:create' | file_backup_error
+ Backup::Lfs | 'gitlab:backup:lfs:create' | file_backup_error
+ Backup::Registry | 'gitlab:backup:registry:create' | file_backup_error
+ end
+
+ with_them do
+ before do
+ expect_next_instance_of(backup_class) do |instance|
+ expect(instance).to receive(:dump).and_raise(error)
+ end
+ end
+
+ it "raises an error with message" do
+ expect { run_rake_task(rake_task) }.to output(Regexp.new(error.message)).to_stdout_from_any_process
+ end
+ end
+ end
+
context 'tar creation' do
context 'archive file permissions' do
it 'sets correct permissions on the tar file' do