diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-01-06 21:13:45 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-01-06 21:13:45 +0300 |
commit | c38fb401d2e0348c0dbfd415c9818444dbe4951c (patch) | |
tree | aee27d8fd8758356c73b6654011effa270eb0f3a /spec | |
parent | f20be8802a40405885e9421417809438e5772bf5 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
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 |