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-12-01 21:07:03 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2022-12-01 21:07:03 +0300
commit4e3a998b8ec1351d8345863f6cad4b9bd497bd6a (patch)
tree9bab8c1089ef4bcc11bd8acdffd1f0f6f62c3e56 /spec
parent08489a6db8ddff0794f9beaf770930803dc7bdca (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
-rw-r--r--spec/features/admin/admin_hooks_spec.rb2
-rw-r--r--spec/features/broadcast_messages_spec.rb3
-rw-r--r--spec/features/projects/settings/repository_settings_spec.rb24
-rw-r--r--spec/features/projects/settings/webhooks_settings_spec.rb4
-rw-r--r--spec/finders/ci/jobs_finder_spec.rb61
-rw-r--r--spec/finders/ci/runners_finder_spec.rb298
-rw-r--r--spec/frontend/boards/components/board_content_sidebar_spec.js2
-rw-r--r--spec/frontend/ci/ci_lint/components/ci_lint_spec.js (renamed from spec/frontend/ci_lint/components/ci_lint_spec.js)2
-rw-r--r--spec/frontend/ci/ci_lint/mock_data.js (renamed from spec/frontend/ci_lint/mock_data.js)0
-rw-r--r--spec/frontend/clusters_list/components/agent_token_spec.js20
-rw-r--r--spec/frontend/content_editor/components/content_editor_spec.js19
-rw-r--r--spec/frontend/content_editor/components/formatting_toolbar_spec.js (renamed from spec/frontend/content_editor/components/top_toolbar_spec.js)4
-rw-r--r--spec/frontend/editor/components/source_editor_toolbar_button_spec.js16
-rw-r--r--spec/frontend/issues/dashboard/components/issues_dashboard_app_spec.js122
-rw-r--r--spec/frontend/issues/dashboard/mock_data.js88
-rw-r--r--spec/frontend/pipeline_editor/components/validate/ci_validate_spec.js2
-rw-r--r--spec/frontend/projects/settings/repository/branch_rules/app_spec.js7
-rw-r--r--spec/frontend/projects/settings/repository/branch_rules/components/branch_rule_spec.js15
-rw-r--r--spec/frontend/projects/settings/repository/branch_rules/mock_data.js33
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_contents_create_view_spec.js (renamed from spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view_spec.js)4
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_contents_labels_view_spec.js (renamed from spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view_spec.js)8
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_contents_spec.js (renamed from spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_spec.js)10
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_footer_spec.js (renamed from spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_footer_spec.js)2
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_header_spec.js (renamed from spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_header_spec.js)2
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_value_spec.js (renamed from spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_value_spec.js)2
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_widget/label_item_spec.js (renamed from spec/frontend/vue_shared/components/sidebar/labels_select_widget/label_item_spec.js)2
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_widget/labels_select_root_spec.js (renamed from spec/frontend/vue_shared/components/sidebar/labels_select_widget/labels_select_root_spec.js)10
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_widget/mock_data.js (renamed from spec/frontend/vue_shared/components/sidebar/labels_select_widget/mock_data.js)0
-rw-r--r--spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js1
-rw-r--r--spec/frontend/work_items/components/work_item_labels_spec.js2
-rw-r--r--spec/graphql/resolvers/ci/all_jobs_resolver_spec.rb43
-rw-r--r--spec/graphql/resolvers/ci/runners_resolver_spec.rb20
-rw-r--r--spec/helpers/issues_helper_spec.rb20
-rw-r--r--spec/lib/api/entities/ssh_key_spec.rb10
-rw-r--r--spec/lib/gitlab/background_migration/delete_orphans_approval_merge_request_rules_spec.rb75
-rw-r--r--spec/lib/gitlab/background_migration/delete_orphans_approval_project_rules_spec.rb71
-rw-r--r--spec/migrations/20221110152133_delete_orphans_approval_rules_spec.rb22
-rw-r--r--spec/requests/api/graphql/issues_spec.rb37
-rw-r--r--spec/requests/api/graphql/project/issues_spec.rb334
-rw-r--r--spec/support/helpers/cookie_helper.rb6
-rw-r--r--spec/support/shared_examples/requests/api/graphql/issue_list_shared_examples.rb311
-rw-r--r--spec/views/profiles/keys/_form.html.haml_spec.rb13
-rw-r--r--spec/views/profiles/keys/_key.html.haml_spec.rb12
-rw-r--r--spec/views/profiles/keys/_key_details.html.haml_spec.rb12
44 files changed, 1062 insertions, 689 deletions
diff --git a/spec/features/admin/admin_hooks_spec.rb b/spec/features/admin/admin_hooks_spec.rb
index c4cd88817bc..94c26bcc8a6 100644
--- a/spec/features/admin/admin_hooks_spec.rb
+++ b/spec/features/admin/admin_hooks_spec.rb
@@ -145,7 +145,7 @@ RSpec.describe 'Admin::Hooks', feature_category: :integrations do
visit admin_hooks_path
find('.hook-test-button.dropdown').click
- click_link 'Merge requests events'
+ click_link 'Merge request events'
expect(page).to have_content 'Hook executed successfully'
end
diff --git a/spec/features/broadcast_messages_spec.rb b/spec/features/broadcast_messages_spec.rb
index 1fec68a1d98..039493ca8dd 100644
--- a/spec/features/broadcast_messages_spec.rb
+++ b/spec/features/broadcast_messages_spec.rb
@@ -36,6 +36,9 @@ RSpec.describe 'Broadcast Messages' do
visit root_path
find('.js-dismiss-current-broadcast-notification').click
+
+ wait_for_cookie_set("hide_broadcast_message_#{broadcast_message.id}")
+
visit root_path
expect(page).not_to have_content 'SampleMessage'
diff --git a/spec/features/projects/settings/repository_settings_spec.rb b/spec/features/projects/settings/repository_settings_spec.rb
index cb11c6fdbb4..e6cdb436f6f 100644
--- a/spec/features/projects/settings/repository_settings_spec.rb
+++ b/spec/features/projects/settings/repository_settings_spec.rb
@@ -164,13 +164,7 @@ RSpec.describe 'Projects > Settings > Repository settings' do
end
project.reload
-
- # TODO: The following line is skipped because a toast with
- # "An error occurred while loading branch rules. Please try again."
- # shows up right after which hides the below message. It is causing flakiness.
- # https://gitlab.com/gitlab-org/gitlab/-/issues/383717#note_1185091998
-
- # expect(page).to have_content('Mirroring settings were successfully updated')
+ expect(page).to have_content('Mirroring settings were successfully updated')
expect(project.remote_mirrors.first.only_protected_branches).to eq(false)
end
@@ -190,13 +184,7 @@ RSpec.describe 'Projects > Settings > Repository settings' do
end
project.reload
-
- # TODO: The following line is skipped because a toast with
- # "An error occurred while loading branch rules. Please try again."
- # shows up right after which hides the below message. It is causing flakiness.
- # https://gitlab.com/gitlab-org/gitlab/-/issues/383717#note_1185091998
-
- # expect(page).to have_content('Mirroring settings were successfully updated')
+ expect(page).to have_content('Mirroring settings were successfully updated')
expect(project.remote_mirrors.first.only_protected_branches).to eq(true)
end
@@ -272,13 +260,7 @@ RSpec.describe 'Projects > Settings > Repository settings' do
click_button 'Start cleanup'
end
end
-
- # TODO: The following line is skipped because a toast with
- # "An error occurred while loading branch rules. Please try again."
- # shows up right after which hides the below message. It is causing flakiness.
- # https://gitlab.com/gitlab-org/gitlab/-/issues/383717#note_1185091998
-
- # expect(page).to have_content('Repository cleanup has started')
+ expect(page).to have_content('Repository cleanup has started')
expect(RepositoryCleanupWorker.jobs.count).to eq(1)
end
end
diff --git a/spec/features/projects/settings/webhooks_settings_spec.rb b/spec/features/projects/settings/webhooks_settings_spec.rb
index 0b6c4144340..aafc6ec71c9 100644
--- a/spec/features/projects/settings/webhooks_settings_spec.rb
+++ b/spec/features/projects/settings/webhooks_settings_spec.rb
@@ -41,8 +41,8 @@ RSpec.describe 'Projects > Settings > Webhook Settings' do
expect(page).to have_content('Tag push events')
expect(page).to have_content('Issues events')
expect(page).to have_content('Confidential issues events')
- expect(page).to have_content('Note events')
- expect(page).to have_content('Merge requests events')
+ expect(page).to have_content('Comment')
+ expect(page).to have_content('Merge request events')
expect(page).to have_content('Pipeline events')
expect(page).to have_content('Wiki page events')
expect(page).to have_content('Releases events')
diff --git a/spec/finders/ci/jobs_finder_spec.rb b/spec/finders/ci/jobs_finder_spec.rb
index dd3ba9721e4..4c94aa4049b 100644
--- a/spec/finders/ci/jobs_finder_spec.rb
+++ b/spec/finders/ci/jobs_finder_spec.rb
@@ -14,52 +14,55 @@ RSpec.describe Ci::JobsFinder, '#execute' do
let(:params) { {} }
context 'no project' do
- subject { described_class.new(current_user: admin, params: params).execute }
+ subject { described_class.new(current_user: current_user, params: params).execute }
- it 'returns all jobs' do
- expect(subject).to match_array([pending_job, running_job, successful_job])
- end
+ context 'with admin' do
+ let(:current_user) { admin }
- context 'non admin user' do
- let(:admin) { user }
+ context 'when admin mode setting is disabled', :do_not_mock_admin_mode_setting do
+ it { is_expected.to match_array([pending_job, running_job, successful_job]) }
+ end
- it 'returns no jobs' do
- expect(subject).to be_empty
+ context 'when admin mode setting is enabled' do
+ context 'when in admin mode', :enable_admin_mode do
+ it { is_expected.to match_array([pending_job, running_job, successful_job]) }
+ end
+
+ context 'when not in admin mode' do
+ it { is_expected.to be_empty }
+ end
end
end
+ context 'with normal user' do
+ let(:current_user) { user }
+
+ it { is_expected.to be_empty }
+ end
+
context 'without user' do
- let(:admin) { nil }
+ let(:current_user) { nil }
- it 'returns no jobs' do
- expect(subject).to be_empty
- end
+ it { is_expected.to be_empty }
end
- context 'scope is present' do
+ context 'with scope', :enable_admin_mode do
+ let(:current_user) { admin }
let(:jobs) { [pending_job, running_job, successful_job] }
- where(:scope, :index) do
- [
- ['pending', 0],
- ['running', 1],
- ['finished', 2]
- ]
+ using RSpec::Parameterized::TableSyntax
+
+ where(:scope, :expected_jobs) do
+ 'pending' | lazy { [pending_job] }
+ 'running' | lazy { [running_job] }
+ 'finished' | lazy { [successful_job] }
+ %w[running success] | lazy { [running_job, successful_job] }
end
with_them do
let(:params) { { scope: scope } }
- it { expect(subject).to match_array([jobs[index]]) }
- end
- end
-
- context 'scope is an array' do
- let(:jobs) { [pending_job, running_job, successful_job, canceled_job] }
- let(:params) { { scope: %w'running success' } }
-
- it 'filters by the job statuses in the scope' do
- expect(subject).to contain_exactly(running_job, successful_job)
+ it { is_expected.to match_array(expected_jobs) }
end
end
end
diff --git a/spec/finders/ci/runners_finder_spec.rb b/spec/finders/ci/runners_finder_spec.rb
index 372e6a3ff7e..a8ef99eeaec 100644
--- a/spec/finders/ci/runners_finder_spec.rb
+++ b/spec/finders/ci/runners_finder_spec.rb
@@ -7,219 +7,221 @@ RSpec.describe Ci::RunnersFinder do
let_it_be(:admin) { create(:user, :admin) }
describe '#execute' do
- context 'with 2 runners' do
- let_it_be(:runner1) { create(:ci_runner, active: true) }
- let_it_be(:runner2) { create(:ci_runner, active: false) }
-
- context 'with empty params' do
- it 'returns all runners' do
- expect(Ci::Runner).to receive(:with_tags).and_call_original
- expect(described_class.new(current_user: admin, params: {}).execute).to match_array [runner1, runner2]
+ shared_examples 'executes as admin' do
+ context 'with 2 runners' do
+ let_it_be(:runner1) { create(:ci_runner, active: true) }
+ let_it_be(:runner2) { create(:ci_runner, active: false) }
+
+ context 'with empty params' do
+ it 'returns all runners' do
+ expect(Ci::Runner).to receive(:with_tags).and_call_original
+ expect(described_class.new(current_user: admin, params: {}).execute).to match_array [runner1, runner2]
+ end
end
- end
- context 'with nil group' do
- it 'returns all runners' do
- expect(Ci::Runner).to receive(:with_tags).and_call_original
- expect(described_class.new(current_user: admin, params: { group: nil }).execute).to match_array [runner1, runner2]
+ context 'with nil group' do
+ it 'returns all runners' do
+ expect(Ci::Runner).to receive(:with_tags).and_call_original
+ expect(described_class.new(current_user: admin, params: { group: nil }).execute).to match_array [runner1, runner2]
+ end
end
- end
- context 'with preload param set to :tag_name true' do
- it 'requests tags' do
- expect(Ci::Runner).to receive(:with_tags).and_call_original
- expect(described_class.new(current_user: admin, params: { preload: { tag_name: true } }).execute).to match_array [runner1, runner2]
+ context 'with preload param set to :tag_name true' do
+ it 'requests tags' do
+ expect(Ci::Runner).to receive(:with_tags).and_call_original
+ expect(described_class.new(current_user: admin, params: { preload: { tag_name: true } }).execute).to match_array [runner1, runner2]
+ end
end
- end
- context 'with preload param set to :tag_name false' do
- it 'does not request tags' do
- expect(Ci::Runner).not_to receive(:with_tags)
- expect(described_class.new(current_user: admin, params: { preload: { tag_name: false } }).execute).to match_array [runner1, runner2]
+ context 'with preload param set to :tag_name false' do
+ it 'does not request tags' do
+ expect(Ci::Runner).not_to receive(:with_tags)
+ expect(described_class.new(current_user: admin, params: { preload: { tag_name: false } }).execute).to match_array [runner1, runner2]
+ end
end
end
- end
- context 'filtering' do
- context 'by search term' do
- it 'calls Ci::Runner.search' do
- expect(Ci::Runner).to receive(:search).with('term').and_call_original
+ context 'filtering' do
+ context 'by search term' do
+ it 'calls Ci::Runner.search' do
+ expect(Ci::Runner).to receive(:search).with('term').and_call_original
- described_class.new(current_user: admin, params: { search: 'term' }).execute
+ described_class.new(current_user: admin, params: { search: 'term' }).execute
+ end
end
- end
- context 'by upgrade status' do
- let(:upgrade_status) {}
+ context 'by upgrade status' do
+ let(:upgrade_status) {}
- let_it_be(:runner1) { create(:ci_runner, version: 'a') }
- let_it_be(:runner2) { create(:ci_runner, version: 'b') }
- let_it_be(:runner3) { create(:ci_runner, version: 'c') }
- let_it_be(:runner_version_recommended) do
- create(:ci_runner_version, version: 'a', status: :recommended)
- end
+ let_it_be(:runner1) { create(:ci_runner, version: 'a') }
+ let_it_be(:runner2) { create(:ci_runner, version: 'b') }
+ let_it_be(:runner3) { create(:ci_runner, version: 'c') }
+ let_it_be(:runner_version_recommended) do
+ create(:ci_runner_version, version: 'a', status: :recommended)
+ end
- let_it_be(:runner_version_not_available) do
- create(:ci_runner_version, version: 'b', status: :not_available)
- end
+ let_it_be(:runner_version_not_available) do
+ create(:ci_runner_version, version: 'b', status: :not_available)
+ end
- let_it_be(:runner_version_available) do
- create(:ci_runner_version, version: 'c', status: :available)
- end
+ let_it_be(:runner_version_available) do
+ create(:ci_runner_version, version: 'c', status: :available)
+ end
- def execute
- described_class.new(current_user: admin, params: { upgrade_status: upgrade_status }).execute
- end
+ def execute
+ described_class.new(current_user: admin, params: { upgrade_status: upgrade_status }).execute
+ end
- Ci::RunnerVersion.statuses.keys.map(&:to_sym).each do |status|
- context "set to :#{status}" do
- let(:upgrade_status) { status }
+ Ci::RunnerVersion.statuses.keys.map(&:to_sym).each do |status|
+ context "set to :#{status}" do
+ let(:upgrade_status) { status }
- it "calls with_upgrade_status scope with corresponding :#{status} status" do
- if [:available, :not_available, :recommended].include?(status)
- expected_result = Ci::Runner.with_upgrade_status(status)
- end
+ it "calls with_upgrade_status scope with corresponding :#{status} status" do
+ if [:available, :not_available, :recommended].include?(status)
+ expected_result = Ci::Runner.with_upgrade_status(status)
+ end
- expect(Ci::Runner).to receive(:with_upgrade_status).with(status).and_call_original
+ expect(Ci::Runner).to receive(:with_upgrade_status).with(status).and_call_original
- result = execute
+ result = execute
- expect(result).to match_array(expected_result) if expected_result
+ expect(result).to match_array(expected_result) if expected_result
+ end
end
end
- end
- context 'set to an invalid value' do
- let(:upgrade_status) { :some_invalid_status }
+ context 'set to an invalid value' do
+ let(:upgrade_status) { :some_invalid_status }
- it 'raises ArgumentError' do
- expect { execute }.to raise_error(ArgumentError)
+ it 'raises ArgumentError' do
+ expect { execute }.to raise_error(ArgumentError)
+ end
end
- end
- context 'set to nil' do
- let(:upgrade_status) { nil }
+ context 'set to nil' do
+ let(:upgrade_status) { nil }
- it 'does not call with_upgrade_status' do
- expect(Ci::Runner).not_to receive(:with_upgrade_status)
+ it 'does not call with_upgrade_status' do
+ expect(Ci::Runner).not_to receive(:with_upgrade_status)
- expect(execute).to match_array(Ci::Runner.all)
+ expect(execute).to match_array(Ci::Runner.all)
+ end
end
end
- end
- context 'by status' do
- Ci::Runner::AVAILABLE_STATUSES.each do |status|
- it "calls the corresponding :#{status} scope on Ci::Runner" do
- expect(Ci::Runner).to receive(status.to_sym).and_call_original
+ context 'by status' do
+ Ci::Runner::AVAILABLE_STATUSES.each do |status|
+ it "calls the corresponding :#{status} scope on Ci::Runner" do
+ expect(Ci::Runner).to receive(status.to_sym).and_call_original
- described_class.new(current_user: admin, params: { status_status: status }).execute
+ described_class.new(current_user: admin, params: { status_status: status }).execute
+ end
end
end
- end
- context 'by active status' do
- it 'with active set as false calls the corresponding scope on Ci::Runner with false' do
- expect(Ci::Runner).to receive(:active).with(false).and_call_original
+ context 'by active status' do
+ it 'with active set as false calls the corresponding scope on Ci::Runner with false' do
+ expect(Ci::Runner).to receive(:active).with(false).and_call_original
- described_class.new(current_user: admin, params: { active: false }).execute
- end
+ described_class.new(current_user: admin, params: { active: false }).execute
+ end
- it 'with active set as true calls the corresponding scope on Ci::Runner with true' do
- expect(Ci::Runner).to receive(:active).with(true).and_call_original
+ it 'with active set as true calls the corresponding scope on Ci::Runner with true' do
+ expect(Ci::Runner).to receive(:active).with(true).and_call_original
- described_class.new(current_user: admin, params: { active: true }).execute
+ described_class.new(current_user: admin, params: { active: true }).execute
+ end
end
- end
- context 'by runner type' do
- it 'calls the corresponding scope on Ci::Runner' do
- expect(Ci::Runner).to receive(:project_type).and_call_original
+ context 'by runner type' do
+ it 'calls the corresponding scope on Ci::Runner' do
+ expect(Ci::Runner).to receive(:project_type).and_call_original
- described_class.new(current_user: admin, params: { type_type: 'project_type' }).execute
+ described_class.new(current_user: admin, params: { type_type: 'project_type' }).execute
+ end
end
- end
- context 'by tag_name' do
- it 'calls the corresponding scope on Ci::Runner' do
- expect(Ci::Runner).to receive(:tagged_with).with(%w[tag1 tag2]).and_call_original
+ context 'by tag_name' do
+ it 'calls the corresponding scope on Ci::Runner' do
+ expect(Ci::Runner).to receive(:tagged_with).with(%w[tag1 tag2]).and_call_original
- described_class.new(current_user: admin, params: { tag_name: %w[tag1 tag2] }).execute
+ described_class.new(current_user: admin, params: { tag_name: %w[tag1 tag2] }).execute
+ end
end
end
- end
- context 'sorting' do
- let_it_be(:runner1) { create :ci_runner, created_at: '2018-07-12 07:00', contacted_at: 1.minute.ago, token_expires_at: '2022-02-15 07:00' }
- let_it_be(:runner2) { create :ci_runner, created_at: '2018-07-12 08:00', contacted_at: 3.minutes.ago, token_expires_at: '2022-02-15 06:00' }
- let_it_be(:runner3) { create :ci_runner, created_at: '2018-07-12 09:00', contacted_at: 2.minutes.ago }
+ context 'sorting' do
+ let_it_be(:runner1) { create :ci_runner, created_at: '2018-07-12 07:00', contacted_at: 1.minute.ago, token_expires_at: '2022-02-15 07:00' }
+ let_it_be(:runner2) { create :ci_runner, created_at: '2018-07-12 08:00', contacted_at: 3.minutes.ago, token_expires_at: '2022-02-15 06:00' }
+ let_it_be(:runner3) { create :ci_runner, created_at: '2018-07-12 09:00', contacted_at: 2.minutes.ago }
- subject do
- described_class.new(current_user: admin, params: params).execute
- end
+ subject do
+ described_class.new(current_user: admin, params: params).execute
+ end
- shared_examples 'sorts by created_at descending' do
- it 'sorts by created_at descending' do
- is_expected.to eq [runner3, runner2, runner1]
+ shared_examples 'sorts by created_at descending' do
+ it 'sorts by created_at descending' do
+ is_expected.to eq [runner3, runner2, runner1]
+ end
end
- end
- context 'without sort param' do
- let(:params) { {} }
+ context 'without sort param' do
+ let(:params) { {} }
- it_behaves_like 'sorts by created_at descending'
- end
+ it_behaves_like 'sorts by created_at descending'
+ end
- %w(created_date created_at_desc).each do |sort|
- context "with sort param equal to #{sort}" do
- let(:params) { { sort: sort } }
+ %w(created_date created_at_desc).each do |sort|
+ context "with sort param equal to #{sort}" do
+ let(:params) { { sort: sort } }
- it_behaves_like 'sorts by created_at descending'
+ it_behaves_like 'sorts by created_at descending'
+ end
end
- end
- context 'with sort param equal to created_at_asc' do
- let(:params) { { sort: 'created_at_asc' } }
+ context 'with sort param equal to created_at_asc' do
+ let(:params) { { sort: 'created_at_asc' } }
- it 'sorts by created_at ascending' do
- is_expected.to eq [runner1, runner2, runner3]
+ it 'sorts by created_at ascending' do
+ is_expected.to eq [runner1, runner2, runner3]
+ end
end
- end
- context 'with sort param equal to contacted_asc' do
- let(:params) { { sort: 'contacted_asc' } }
+ context 'with sort param equal to contacted_asc' do
+ let(:params) { { sort: 'contacted_asc' } }
- it 'sorts by contacted_at ascending' do
- is_expected.to eq [runner2, runner3, runner1]
+ it 'sorts by contacted_at ascending' do
+ is_expected.to eq [runner2, runner3, runner1]
+ end
end
- end
- context 'with sort param equal to contacted_desc' do
- let(:params) { { sort: 'contacted_desc' } }
+ context 'with sort param equal to contacted_desc' do
+ let(:params) { { sort: 'contacted_desc' } }
- it 'sorts by contacted_at descending' do
- is_expected.to eq [runner1, runner3, runner2]
+ it 'sorts by contacted_at descending' do
+ is_expected.to eq [runner1, runner3, runner2]
+ end
end
- end
- context 'with sort param equal to token_expires_at_asc' do
- let(:params) { { sort: 'token_expires_at_asc' } }
+ context 'with sort param equal to token_expires_at_asc' do
+ let(:params) { { sort: 'token_expires_at_asc' } }
- it 'sorts by contacted_at ascending' do
- is_expected.to eq [runner2, runner1, runner3]
+ it 'sorts by contacted_at ascending' do
+ is_expected.to eq [runner2, runner1, runner3]
+ end
end
- end
- context 'with sort param equal to token_expires_at_desc' do
- let(:params) { { sort: 'token_expires_at_desc' } }
+ context 'with sort param equal to token_expires_at_desc' do
+ let(:params) { { sort: 'token_expires_at_desc' } }
- it 'sorts by contacted_at descending' do
- is_expected.to eq [runner3, runner1, runner2]
+ it 'sorts by contacted_at descending' do
+ is_expected.to eq [runner3, runner1, runner2]
+ end
end
end
end
- context 'by non admin user' do
+ shared_examples 'executes as normal user' do
it 'returns no runners' do
user = create :user
create :ci_runner, active: true
@@ -229,6 +231,24 @@ RSpec.describe Ci::RunnersFinder do
end
end
+ context 'when admin mode setting is disabled', :do_not_mock_admin_mode_setting do
+ it_behaves_like 'executes as admin'
+ end
+
+ context 'when admin mode setting is enabled' do
+ context 'when in admin mode', :enable_admin_mode do
+ it_behaves_like 'executes as admin'
+ end
+
+ context 'when not in admin mode' do
+ it_behaves_like 'executes as normal user'
+ end
+ end
+
+ context 'by non admin user' do
+ it_behaves_like 'executes as normal user'
+ end
+
context 'when user is nil' do
it 'returns no runners' do
user = nil
diff --git a/spec/frontend/boards/components/board_content_sidebar_spec.js b/spec/frontend/boards/components/board_content_sidebar_spec.js
index 69f5992a80e..0d5b1d16e30 100644
--- a/spec/frontend/boards/components/board_content_sidebar_spec.js
+++ b/spec/frontend/boards/components/board_content_sidebar_spec.js
@@ -12,7 +12,7 @@ import SidebarDateWidget from '~/sidebar/components/date/sidebar_date_widget.vue
import SidebarSeverity from '~/sidebar/components/severity/sidebar_severity.vue';
import SidebarSubscriptionsWidget from '~/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue';
import SidebarTodoWidget from '~/sidebar/components/todo_toggle/sidebar_todo_widget.vue';
-import SidebarLabelsWidget from '~/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue';
+import SidebarLabelsWidget from '~/sidebar/components/labels/labels_select_widget/labels_select_root.vue';
import { mockActiveIssue, mockIssue, mockIssueGroupPath, mockIssueProjectPath } from '../mock_data';
Vue.use(Vuex);
diff --git a/spec/frontend/ci_lint/components/ci_lint_spec.js b/spec/frontend/ci/ci_lint/components/ci_lint_spec.js
index ea69a80274e..518375cb831 100644
--- a/spec/frontend/ci_lint/components/ci_lint_spec.js
+++ b/spec/frontend/ci/ci_lint/components/ci_lint_spec.js
@@ -2,7 +2,7 @@ import { GlAlert } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import waitForPromises from 'helpers/wait_for_promises';
-import CiLint from '~/ci_lint/components/ci_lint.vue';
+import CiLint from '~/ci/ci_lint/components/ci_lint.vue';
import CiLintResults from '~/pipeline_editor/components/lint/ci_lint_results.vue';
import lintCIMutation from '~/pipeline_editor/graphql/mutations/client/lint_ci.mutation.graphql';
import SourceEditor from '~/vue_shared/components/source_editor.vue';
diff --git a/spec/frontend/ci_lint/mock_data.js b/spec/frontend/ci/ci_lint/mock_data.js
index 660b2ad6e8b..660b2ad6e8b 100644
--- a/spec/frontend/ci_lint/mock_data.js
+++ b/spec/frontend/ci/ci_lint/mock_data.js
diff --git a/spec/frontend/clusters_list/components/agent_token_spec.js b/spec/frontend/clusters_list/components/agent_token_spec.js
index 8d3130b45a6..e656a601699 100644
--- a/spec/frontend/clusters_list/components/agent_token_spec.js
+++ b/spec/frontend/clusters_list/components/agent_token_spec.js
@@ -1,7 +1,11 @@
import { GlAlert, GlFormInputGroup } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import AgentToken from '~/clusters_list/components/agent_token.vue';
-import { I18N_AGENT_TOKEN, INSTALL_AGENT_MODAL_ID } from '~/clusters_list/constants';
+import {
+ I18N_AGENT_TOKEN,
+ INSTALL_AGENT_MODAL_ID,
+ NAME_MAX_LENGTH,
+} from '~/clusters_list/constants';
import { generateAgentRegistrationCommand } from '~/clusters_list/clusters_util';
import CodeBlock from '~/vue_shared/components/code_block.vue';
import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
@@ -20,14 +24,14 @@ describe('InstallAgentModal', () => {
const findCopyButton = () => wrapper.findComponent(ModalCopyButton);
const findInput = () => wrapper.findComponent(GlFormInputGroup);
- const createWrapper = () => {
+ const createWrapper = (newAgentName = agentName) => {
const provide = {
kasAddress,
kasVersion,
};
const propsData = {
- agentName,
+ agentName: newAgentName,
agentToken,
modalId,
};
@@ -79,9 +83,19 @@ describe('InstallAgentModal', () => {
it('shows code block with agent installation command', () => {
expect(findCodeBlock().props('code')).toContain(`helm upgrade --install ${agentName}`);
+ expect(findCodeBlock().props('code')).toContain(`--namespace gitlab-agent-${agentName}`);
expect(findCodeBlock().props('code')).toContain(`--set config.token=${agentToken}`);
expect(findCodeBlock().props('code')).toContain(`--set config.kasAddress=${kasAddress}`);
expect(findCodeBlock().props('code')).toContain(`--set image.tag=v${kasVersion}`);
});
+
+ it('truncates the namespace name if it exceeds the maximum length', () => {
+ const newAgentName = 'agent-name-that-is-too-long-and-needs-to-be-truncated-to-use';
+ createWrapper(newAgentName);
+
+ expect(findCodeBlock().props('code')).toContain(
+ `--namespace gitlab-agent-${newAgentName.substring(0, NAME_MAX_LENGTH)}`,
+ );
+ });
});
});
diff --git a/spec/frontend/content_editor/components/content_editor_spec.js b/spec/frontend/content_editor/components/content_editor_spec.js
index c1c2a125515..1a3cd36a8bb 100644
--- a/spec/frontend/content_editor/components/content_editor_spec.js
+++ b/spec/frontend/content_editor/components/content_editor_spec.js
@@ -10,7 +10,7 @@ import FormattingBubbleMenu from '~/content_editor/components/bubble_menus/forma
import CodeBlockBubbleMenu from '~/content_editor/components/bubble_menus/code_block_bubble_menu.vue';
import LinkBubbleMenu from '~/content_editor/components/bubble_menus/link_bubble_menu.vue';
import MediaBubbleMenu from '~/content_editor/components/bubble_menus/media_bubble_menu.vue';
-import TopToolbar from '~/content_editor/components/top_toolbar.vue';
+import FormattingToolbar from '~/content_editor/components/formatting_toolbar.vue';
import LoadingIndicator from '~/content_editor/components/loading_indicator.vue';
import waitForPromises from 'helpers/wait_for_promises';
import { KEYDOWN_EVENT } from '~/content_editor/constants';
@@ -27,13 +27,14 @@ describe('ContentEditor', () => {
const findEditorStateObserver = () => wrapper.findComponent(EditorStateObserver);
const findLoadingIndicator = () => wrapper.findComponent(LoadingIndicator);
const findContentEditorAlert = () => wrapper.findComponent(ContentEditorAlert);
- const createWrapper = ({ markdown, autofocus } = {}) => {
+ const createWrapper = ({ markdown, autofocus, useBottomToolbar } = {}) => {
wrapper = shallowMountExtended(ContentEditor, {
propsData: {
renderMarkdown,
uploadsPath,
markdown,
autofocus,
+ useBottomToolbar,
},
stubs: {
EditorStateObserver,
@@ -89,7 +90,19 @@ describe('ContentEditor', () => {
it('renders top toolbar component', () => {
createWrapper();
- expect(wrapper.findComponent(TopToolbar).exists()).toBe(true);
+ expect(wrapper.findComponent(FormattingToolbar).exists()).toBe(true);
+ expect(wrapper.findComponent(FormattingToolbar).classes('gl-border-t')).toBe(false);
+ expect(wrapper.findComponent(FormattingToolbar).classes('gl-border-b')).toBe(true);
+ });
+
+ it('renders bottom toolbar component', () => {
+ createWrapper({
+ useBottomToolbar: true,
+ });
+
+ expect(wrapper.findComponent(FormattingToolbar).exists()).toBe(true);
+ expect(wrapper.findComponent(FormattingToolbar).classes('gl-border-t')).toBe(true);
+ expect(wrapper.findComponent(FormattingToolbar).classes('gl-border-b')).toBe(false);
});
describe('when setting initial content', () => {
diff --git a/spec/frontend/content_editor/components/top_toolbar_spec.js b/spec/frontend/content_editor/components/formatting_toolbar_spec.js
index 8f194ff32e2..c4bf21ba813 100644
--- a/spec/frontend/content_editor/components/top_toolbar_spec.js
+++ b/spec/frontend/content_editor/components/formatting_toolbar_spec.js
@@ -1,6 +1,6 @@
import { mockTracking } from 'helpers/tracking_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import TopToolbar from '~/content_editor/components/top_toolbar.vue';
+import FormattingToolbar from '~/content_editor/components/formatting_toolbar.vue';
import {
TOOLBAR_CONTROL_TRACKING_ACTION,
CONTENT_EDITOR_TRACKING_LABEL,
@@ -11,7 +11,7 @@ describe('content_editor/components/top_toolbar', () => {
let trackingSpy;
const buildWrapper = () => {
- wrapper = shallowMountExtended(TopToolbar);
+ wrapper = shallowMountExtended(FormattingToolbar);
};
beforeEach(() => {
diff --git a/spec/frontend/editor/components/source_editor_toolbar_button_spec.js b/spec/frontend/editor/components/source_editor_toolbar_button_spec.js
index 1475d451ab3..ded31bb62dc 100644
--- a/spec/frontend/editor/components/source_editor_toolbar_button_spec.js
+++ b/spec/frontend/editor/components/source_editor_toolbar_button_spec.js
@@ -55,6 +55,12 @@ describe('Source Editor Toolbar button', () => {
});
describe('click handler', () => {
+ let clickEvent;
+
+ beforeEach(() => {
+ clickEvent = new Event('click');
+ });
+
it('fires the click handler on the button when available', async () => {
const spy = jest.fn();
createComponent({
@@ -63,20 +69,20 @@ describe('Source Editor Toolbar button', () => {
},
});
expect(spy).not.toHaveBeenCalled();
- findButton().vm.$emit('click');
+ findButton().vm.$emit('click', clickEvent);
await nextTick();
- expect(spy).toHaveBeenCalled();
+ expect(spy).toHaveBeenCalledWith(clickEvent);
});
- it('emits the "click" event', async () => {
+ it('emits the "click" event, passing the event itself', async () => {
createComponent();
jest.spyOn(wrapper.vm, '$emit');
expect(wrapper.vm.$emit).not.toHaveBeenCalled();
- findButton().vm.$emit('click');
+ findButton().vm.$emit('click', clickEvent);
await nextTick();
- expect(wrapper.vm.$emit).toHaveBeenCalledWith('click');
+ expect(wrapper.vm.$emit).toHaveBeenCalledWith('click', clickEvent);
});
});
});
diff --git a/spec/frontend/issues/dashboard/components/issues_dashboard_app_spec.js b/spec/frontend/issues/dashboard/components/issues_dashboard_app_spec.js
index 3f72396cce6..3195d5ff0a1 100644
--- a/spec/frontend/issues/dashboard/components/issues_dashboard_app_spec.js
+++ b/spec/frontend/issues/dashboard/components/issues_dashboard_app_spec.js
@@ -1,58 +1,168 @@
import { GlEmptyState } from '@gitlab/ui';
+import * as Sentry from '@sentry/browser';
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
+import { cloneDeep } from 'lodash';
+import getIssuesQuery from 'ee_else_ce/issues/dashboard/queries/get_issues.query.graphql';
+import IssueCardStatistics from 'ee_else_ce/issues/list/components/issue_card_statistics.vue';
+import IssueCardTimeInfo from 'ee_else_ce/issues/list/components/issue_card_time_info.vue';
+import createMockApollo from 'helpers/mock_apollo_helper';
import { mountExtended } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
import IssuesDashboardApp from '~/issues/dashboard/components/issues_dashboard_app.vue';
+import { i18n } from '~/issues/list/constants';
+import { scrollUp } from '~/lib/utils/scroll_utils';
import IssuableList from '~/vue_shared/issuable/list/components/issuable_list_root.vue';
import { IssuableStates } from '~/vue_shared/issuable/list/constants';
+import { emptyIssuesQueryResponse, issuesQueryResponse } from '../mock_data';
+
+jest.mock('@sentry/browser');
+jest.mock('~/lib/utils/scroll_utils', () => ({ scrollUp: jest.fn() }));
describe('IssuesDashboardApp component', () => {
let wrapper;
+ Vue.use(VueApollo);
+
const defaultProvide = {
calendarPath: 'calendar/path',
emptyStateSvgPath: 'empty-state.svg',
+ hasBlockedIssuesFeature: true,
+ hasIssuableHealthStatusFeature: true,
+ hasIssueWeightsFeature: true,
+ hasScopedLabelsFeature: true,
+ isPublicVisibilityRestricted: false,
isSignedIn: true,
rssPath: 'rss/path',
};
+ let defaultQueryResponse = issuesQueryResponse;
+ if (IS_EE) {
+ defaultQueryResponse = cloneDeep(issuesQueryResponse);
+ defaultQueryResponse.data.issues.nodes[0].blockingCount = 1;
+ defaultQueryResponse.data.issues.nodes[0].healthStatus = null;
+ defaultQueryResponse.data.issues.nodes[0].weight = 5;
+ }
+
const findCalendarButton = () =>
wrapper.findByRole('link', { name: IssuesDashboardApp.i18n.calendarButtonText });
const findEmptyState = () => wrapper.findComponent(GlEmptyState);
const findIssuableList = () => wrapper.findComponent(IssuableList);
+ const findIssueCardStatistics = () => wrapper.findComponent(IssueCardStatistics);
+ const findIssueCardTimeInfo = () => wrapper.findComponent(IssueCardTimeInfo);
const findRssButton = () =>
wrapper.findByRole('link', { name: IssuesDashboardApp.i18n.rssButtonText });
- const mountComponent = () => {
- wrapper = mountExtended(IssuesDashboardApp, { provide: defaultProvide });
+ const mountComponent = ({
+ issuesQueryHandler = jest.fn().mockResolvedValue(defaultQueryResponse),
+ } = {}) => {
+ wrapper = mountExtended(IssuesDashboardApp, {
+ apolloProvider: createMockApollo([[getIssuesQuery, issuesQueryHandler]]),
+ provide: defaultProvide,
+ });
};
- beforeEach(() => {
+ it('renders IssuableList component', async () => {
mountComponent();
- });
+ await waitForPromises();
- it('renders IssuableList component', () => {
expect(findIssuableList().props()).toMatchObject({
currentTab: IssuableStates.Opened,
+ hasNextPage: true,
+ hasPreviousPage: false,
+ hasScopedLabelsFeature: defaultProvide.hasScopedLabelsFeature,
namespace: 'dashboard',
recentSearchesStorageKey: 'issues',
searchInputPlaceholder: IssuesDashboardApp.i18n.searchInputPlaceholder,
+ showPaginationControls: true,
tabs: IssuesDashboardApp.IssuableListTabs,
+ useKeysetPagination: true,
});
});
it('renders RSS button link', () => {
+ mountComponent();
+
expect(findRssButton().attributes('href')).toBe(defaultProvide.rssPath);
expect(findRssButton().props('icon')).toBe('rss');
});
it('renders calendar button link', () => {
+ mountComponent();
+
expect(findCalendarButton().attributes('href')).toBe(defaultProvide.calendarPath);
expect(findCalendarButton().props('icon')).toBe('calendar');
});
- it('renders empty state', () => {
+ it('renders issue time information', async () => {
+ mountComponent();
+ await waitForPromises();
+
+ expect(findIssueCardTimeInfo().exists()).toBe(true);
+ });
+
+ it('renders issue statistics', async () => {
+ mountComponent();
+ await waitForPromises();
+
+ expect(findIssueCardStatistics().exists()).toBe(true);
+ });
+
+ it('renders empty state', async () => {
+ mountComponent({ issuesQueryHandler: jest.fn().mockResolvedValue(emptyIssuesQueryResponse) });
+ await waitForPromises();
+
expect(findEmptyState().props()).toMatchObject({
svgPath: defaultProvide.emptyStateSvgPath,
title: IssuesDashboardApp.i18n.emptyStateTitle,
});
});
+
+ describe('when there is an error fetching issues', () => {
+ beforeEach(() => {
+ mountComponent({ issuesQueryHandler: jest.fn().mockRejectedValue(new Error('ERROR')) });
+ return waitForPromises();
+ });
+
+ it('shows an error message', () => {
+ expect(findIssuableList().props('error')).toBe(i18n.errorFetchingIssues);
+ expect(Sentry.captureException).toHaveBeenCalledWith(new Error('ERROR'));
+ });
+
+ it('clears error message when "dismiss-alert" event is emitted from IssuableList', async () => {
+ findIssuableList().vm.$emit('dismiss-alert');
+ await nextTick();
+
+ expect(findIssuableList().props('error')).toBeNull();
+ });
+ });
+
+ describe('events', () => {
+ describe('when "click-tab" event is emitted by IssuableList', () => {
+ beforeEach(() => {
+ mountComponent();
+
+ findIssuableList().vm.$emit('click-tab', IssuableStates.Closed);
+ });
+
+ it('updates ui to the new tab', () => {
+ expect(findIssuableList().props('currentTab')).toBe(IssuableStates.Closed);
+ });
+ });
+
+ describe.each(['next-page', 'previous-page'])(
+ 'when "%s" event is emitted by IssuableList',
+ (event) => {
+ beforeEach(() => {
+ mountComponent();
+
+ findIssuableList().vm.$emit(event);
+ });
+
+ it('scrolls to the top', () => {
+ expect(scrollUp).toHaveBeenCalled();
+ });
+ },
+ );
+ });
});
diff --git a/spec/frontend/issues/dashboard/mock_data.js b/spec/frontend/issues/dashboard/mock_data.js
new file mode 100644
index 00000000000..feb4cb80bd8
--- /dev/null
+++ b/spec/frontend/issues/dashboard/mock_data.js
@@ -0,0 +1,88 @@
+export const issuesQueryResponse = {
+ data: {
+ issues: {
+ nodes: [
+ {
+ __typename: 'Issue',
+ id: 'gid://gitlab/Issue/123456',
+ iid: '789',
+ closedAt: null,
+ confidential: false,
+ createdAt: '2021-05-22T04:08:01Z',
+ downvotes: 2,
+ dueDate: '2021-05-29',
+ hidden: false,
+ humanTimeEstimate: null,
+ mergeRequestsCount: false,
+ moved: false,
+ reference: 'group/project#123456',
+ state: 'opened',
+ title: 'Issue title',
+ type: 'issue',
+ updatedAt: '2021-05-22T04:08:01Z',
+ upvotes: 3,
+ userDiscussionsCount: 4,
+ webPath: 'project/-/issues/789',
+ webUrl: 'project/-/issues/789',
+ assignees: {
+ nodes: [
+ {
+ __typename: 'UserCore',
+ id: 'gid://gitlab/User/234',
+ avatarUrl: 'avatar/url',
+ name: 'Marge Simpson',
+ username: 'msimpson',
+ webUrl: 'url/msimpson',
+ },
+ ],
+ },
+ author: {
+ __typename: 'UserCore',
+ id: 'gid://gitlab/User/456',
+ avatarUrl: 'avatar/url',
+ name: 'Homer Simpson',
+ username: 'hsimpson',
+ webUrl: 'url/hsimpson',
+ },
+ labels: {
+ nodes: [
+ {
+ id: 'gid://gitlab/ProjectLabel/456',
+ color: '#333',
+ title: 'Label title',
+ description: 'Label description',
+ },
+ ],
+ },
+ milestone: null,
+ taskCompletionStatus: {
+ completedCount: 1,
+ count: 2,
+ },
+ },
+ ],
+ pageInfo: {
+ __typename: 'PageInfo',
+ hasNextPage: true,
+ hasPreviousPage: false,
+ startCursor: 'startcursor',
+ endCursor: 'endcursor',
+ },
+ },
+ },
+};
+
+export const emptyIssuesQueryResponse = {
+ data: {
+ issues: {
+ nodes: [],
+ pageInfo: {
+ __typename: 'PageInfo',
+ hasNextPage: false,
+ hasPreviousPage: false,
+ startCursor: '',
+ endCursor: '',
+ },
+ },
+ },
+};
diff --git a/spec/frontend/pipeline_editor/components/validate/ci_validate_spec.js b/spec/frontend/pipeline_editor/components/validate/ci_validate_spec.js
index 09d4f9736ad..6e5b3125e75 100644
--- a/spec/frontend/pipeline_editor/components/validate/ci_validate_spec.js
+++ b/spec/frontend/pipeline_editor/components/validate/ci_validate_spec.js
@@ -17,7 +17,7 @@ import {
mockCiYml,
mockSimulatePipelineHelpPagePath,
} from '../../mock_data';
-import { mockLintDataError, mockLintDataValid } from '../../../ci_lint/mock_data';
+import { mockLintDataError, mockLintDataValid } from '../../../ci/ci_lint/mock_data';
const localVue = createLocalVue();
localVue.use(VueApollo);
diff --git a/spec/frontend/projects/settings/repository/branch_rules/app_spec.js b/spec/frontend/projects/settings/repository/branch_rules/app_spec.js
index 6369f04781f..447d7e86ceb 100644
--- a/spec/frontend/projects/settings/repository/branch_rules/app_spec.js
+++ b/spec/frontend/projects/settings/repository/branch_rules/app_spec.js
@@ -5,9 +5,12 @@ import waitForPromises from 'helpers/wait_for_promises';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import BranchRules, { i18n } from '~/projects/settings/repository/branch_rules/app.vue';
import BranchRule from '~/projects/settings/repository/branch_rules/components/branch_rule.vue';
-import branchRulesQuery from '~/projects/settings/repository/branch_rules/graphql/queries/branch_rules.query.graphql';
+import branchRulesQuery from 'ee_else_ce/projects/settings/repository/branch_rules/graphql/queries/branch_rules.query.graphql';
import { createAlert } from '~/flash';
-import { branchRulesMockResponse, appProvideMock } from './mock_data';
+import {
+ branchRulesMockResponse,
+ appProvideMock,
+} from 'ee_else_ce_jest/projects/settings/repository/branch_rules/mock_data';
jest.mock('~/flash');
diff --git a/spec/frontend/projects/settings/repository/branch_rules/components/branch_rule_spec.js b/spec/frontend/projects/settings/repository/branch_rules/components/branch_rule_spec.js
index ee12fd4ee42..49c45c080b4 100644
--- a/spec/frontend/projects/settings/repository/branch_rules/components/branch_rule_spec.js
+++ b/spec/frontend/projects/settings/repository/branch_rules/components/branch_rule_spec.js
@@ -50,20 +50,7 @@ describe('Branch rule', () => {
it('renders the protection details list items', () => {
expect(findProtectionDetailsListItems()).toHaveLength(wrapper.vm.approvalDetails.length);
expect(findProtectionDetailsListItems().at(0).text()).toBe(i18n.allowForcePush);
- expect(findProtectionDetailsListItems().at(1).text()).toBe(i18n.codeOwnerApprovalRequired);
- expect(findProtectionDetailsListItems().at(2).text()).toMatchInterpolatedText(
- sprintf(i18n.statusChecks, {
- total: branchRulePropsMock.statusChecksTotal,
- subject: n__('check', 'checks', branchRulePropsMock.statusChecksTotal),
- }),
- );
- expect(findProtectionDetailsListItems().at(3).text()).toMatchInterpolatedText(
- sprintf(i18n.approvalRules, {
- total: branchRulePropsMock.approvalRulesTotal,
- subject: n__('rule', 'rules', branchRulePropsMock.approvalRulesTotal),
- }),
- );
- expect(findProtectionDetailsListItems().at(4).text()).toBe(wrapper.vm.pushAccessLevelsText);
+ expect(findProtectionDetailsListItems().at(1).text()).toBe(wrapper.vm.pushAccessLevelsText);
});
it('renders branches count for wildcards', () => {
diff --git a/spec/frontend/projects/settings/repository/branch_rules/mock_data.js b/spec/frontend/projects/settings/repository/branch_rules/mock_data.js
index c105999dce6..6f506882c36 100644
--- a/spec/frontend/projects/settings/repository/branch_rules/mock_data.js
+++ b/spec/frontend/projects/settings/repository/branch_rules/mock_data.js
@@ -4,12 +4,7 @@ export const accessLevelsMockResponse = [
node: {
__typename: 'PushAccessLevel',
accessLevel: 40,
- accessLevelDescription: 'Jona Langworth',
- group: null,
- user: {
- __typename: 'UserCore',
- id: '123',
- },
+ accessLevelDescription: 'Developers',
},
},
{
@@ -18,8 +13,6 @@ export const accessLevelsMockResponse = [
__typename: 'PushAccessLevel',
accessLevel: 40,
accessLevelDescription: 'Maintainers',
- group: null,
- user: null,
},
},
];
@@ -38,7 +31,6 @@ export const branchRulesMockResponse = {
matchingBranchesCount: 1,
branchProtection: {
allowForcePush: true,
- codeOwnerApprovalRequired: true,
mergeAccessLevels: {
edges: [],
__typename: 'MergeAccessLevelConnection',
@@ -48,14 +40,6 @@ export const branchRulesMockResponse = {
__typename: 'PushAccessLevelConnection',
},
},
- approvalRules: {
- nodes: [{ id: 1 }],
- __typename: 'ApprovalProjectRuleConnection',
- },
- externalStatusChecks: {
- nodes: [{ id: 1 }, { id: 2 }],
- __typename: 'ExternalStatusCheckConnection',
- },
__typename: 'BranchRule',
},
{
@@ -64,7 +48,6 @@ export const branchRulesMockResponse = {
matchingBranchesCount: 2,
branchProtection: {
allowForcePush: false,
- codeOwnerApprovalRequired: false,
mergeAccessLevels: {
edges: [],
__typename: 'MergeAccessLevelConnection',
@@ -74,14 +57,6 @@ export const branchRulesMockResponse = {
__typename: 'PushAccessLevelConnection',
},
},
- approvalRules: {
- nodes: [],
- __typename: 'ApprovalProjectRuleConnection',
- },
- externalStatusChecks: {
- nodes: [],
- __typename: 'ExternalStatusCheckConnection',
- },
__typename: 'BranchRule',
},
],
@@ -104,13 +79,13 @@ export const branchRulePropsMock = {
matchingBranchesCount: 1,
branchProtection: {
allowForcePush: true,
- codeOwnerApprovalRequired: true,
+ codeOwnerApprovalRequired: false,
pushAccessLevels: {
edges: accessLevelsMockResponse,
},
},
- approvalRulesTotal: 1,
- statusChecksTotal: 2,
+ approvalRulesTotal: 0,
+ statusChecksTotal: 0,
};
export const branchRuleWithoutDetailsPropsMock = {
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view_spec.js b/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_contents_create_view_spec.js
index 237f174e048..79b164b0ea7 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view_spec.js
+++ b/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_contents_create_view_spec.js
@@ -6,8 +6,8 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/flash';
import { workspaceLabelsQueries } from '~/sidebar/constants';
-import DropdownContentsCreateView from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue';
-import createLabelMutation from '~/vue_shared/components/sidebar/labels_select_widget/graphql/create_label.mutation.graphql';
+import DropdownContentsCreateView from '~/sidebar/components/labels/labels_select_widget/dropdown_contents_create_view.vue';
+import createLabelMutation from '~/sidebar/components/labels/labels_select_widget/graphql/create_label.mutation.graphql';
import {
mockRegularLabel,
mockSuggestedColors,
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view_spec.js b/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_contents_labels_view_spec.js
index 5d8ad5ddee5..913badccbe4 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view_spec.js
+++ b/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_contents_labels_view_spec.js
@@ -11,10 +11,10 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/flash';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
-import { DropdownVariant } from '~/vue_shared/components/sidebar/labels_select_widget/constants';
-import DropdownContentsLabelsView from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue';
-import projectLabelsQuery from '~/vue_shared/components/sidebar/labels_select_widget/graphql/project_labels.query.graphql';
-import LabelItem from '~/vue_shared/components/sidebar/labels_select_widget/label_item.vue';
+import { DropdownVariant } from '~/sidebar/components/labels/labels_select_widget/constants';
+import DropdownContentsLabelsView from '~/sidebar/components/labels/labels_select_widget/dropdown_contents_labels_view.vue';
+import projectLabelsQuery from '~/sidebar/components/labels/labels_select_widget/graphql/project_labels.query.graphql';
+import LabelItem from '~/sidebar/components/labels/labels_select_widget/label_item.vue';
import { mockConfig, workspaceLabelsQueryResponse } from './mock_data';
jest.mock('~/flash');
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_spec.js b/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_contents_spec.js
index 00da9b74957..9bbb1413ee9 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_spec.js
+++ b/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_contents_spec.js
@@ -1,10 +1,10 @@
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
-import { DropdownVariant } from '~/vue_shared/components/sidebar/labels_select_widget/constants';
-import DropdownContents from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue';
-import DropdownContentsCreateView from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue';
-import DropdownContentsLabelsView from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue';
-import DropdownFooter from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_footer.vue';
+import { DropdownVariant } from '~/sidebar/components/labels/labels_select_widget/constants';
+import DropdownContents from '~/sidebar/components/labels/labels_select_widget/dropdown_contents.vue';
+import DropdownContentsCreateView from '~/sidebar/components/labels/labels_select_widget/dropdown_contents_create_view.vue';
+import DropdownContentsLabelsView from '~/sidebar/components/labels/labels_select_widget/dropdown_contents_labels_view.vue';
+import DropdownFooter from '~/sidebar/components/labels/labels_select_widget/dropdown_footer.vue';
import { mockLabels } from './mock_data';
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_footer_spec.js b/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_footer_spec.js
index 0508a059195..9a6e0ca3ccd 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_footer_spec.js
+++ b/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_footer_spec.js
@@ -1,6 +1,6 @@
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
-import DropdownFooter from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_footer.vue';
+import DropdownFooter from '~/sidebar/components/labels/labels_select_widget/dropdown_footer.vue';
describe('DropdownFooter', () => {
let wrapper;
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_header_spec.js b/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_header_spec.js
index c4faef8ccdd..d9001dface4 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_header_spec.js
+++ b/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_header_spec.js
@@ -1,7 +1,7 @@
import { GlSearchBoxByType } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
-import DropdownHeader from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_header.vue';
+import DropdownHeader from '~/sidebar/components/labels/labels_select_widget/dropdown_header.vue';
describe('DropdownHeader', () => {
let wrapper;
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_value_spec.js b/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_value_spec.js
index 0c4f4b7d504..585048983c9 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_value_spec.js
+++ b/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_value_spec.js
@@ -1,7 +1,7 @@
import { GlLabel } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import DropdownValue from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_value.vue';
+import DropdownValue from '~/sidebar/components/labels/labels_select_widget/dropdown_value.vue';
import { mockRegularLabel, mockScopedLabel } from './mock_data';
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/label_item_spec.js b/spec/frontend/sidebar/components/labels/labels_select_widget/label_item_spec.js
index 6e8841411a2..74188a77994 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/label_item_spec.js
+++ b/spec/frontend/sidebar/components/labels/labels_select_widget/label_item_spec.js
@@ -1,6 +1,6 @@
import { shallowMount } from '@vue/test-utils';
-import LabelItem from '~/vue_shared/components/sidebar/labels_select_widget/label_item.vue';
+import LabelItem from '~/sidebar/components/labels/labels_select_widget/label_item.vue';
import { mockRegularLabel } from './mock_data';
const mockLabel = { ...mockRegularLabel, set: true };
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/labels_select_root_spec.js b/spec/frontend/sidebar/components/labels/labels_select_widget/labels_select_root_spec.js
index 74ddd07d041..ff8bbf7a1e9 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/labels_select_root_spec.js
+++ b/spec/frontend/sidebar/components/labels/labels_select_widget/labels_select_root_spec.js
@@ -6,14 +6,14 @@ import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/flash';
import { IssuableType } from '~/issues/constants';
import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
-import DropdownContents from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue';
-import DropdownValue from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_value.vue';
-import issueLabelsQuery from '~/vue_shared/components/sidebar/labels_select_widget/graphql/issue_labels.query.graphql';
+import DropdownContents from '~/sidebar/components/labels/labels_select_widget/dropdown_contents.vue';
+import DropdownValue from '~/sidebar/components/labels/labels_select_widget/dropdown_value.vue';
+import issueLabelsQuery from '~/sidebar/components/labels/labels_select_widget/graphql/issue_labels.query.graphql';
import updateIssueLabelsMutation from '~/boards/graphql/issue_set_labels.mutation.graphql';
import updateMergeRequestLabelsMutation from '~/sidebar/queries/update_merge_request_labels.mutation.graphql';
import issuableLabelsSubscription from 'ee_else_ce/sidebar/queries/issuable_labels.subscription.graphql';
-import updateEpicLabelsMutation from '~/vue_shared/components/sidebar/labels_select_widget/graphql/epic_update_labels.mutation.graphql';
-import LabelsSelectRoot from '~/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue';
+import updateEpicLabelsMutation from '~/sidebar/components/labels/labels_select_widget/graphql/epic_update_labels.mutation.graphql';
+import LabelsSelectRoot from '~/sidebar/components/labels/labels_select_widget/labels_select_root.vue';
import {
mockConfig,
issuableLabelsQueryResponse,
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/mock_data.js b/spec/frontend/sidebar/components/labels/labels_select_widget/mock_data.js
index 48530a0261f..48530a0261f 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/mock_data.js
+++ b/spec/frontend/sidebar/components/labels/labels_select_widget/mock_data.js
diff --git a/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js b/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js
index 625e67c7cc1..5f416db2676 100644
--- a/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js
@@ -171,6 +171,7 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
expect.objectContaining({
renderMarkdown: expect.any(Function),
uploadsPath: window.uploads_path,
+ useBottomToolbar: false,
markdown: value,
}),
);
diff --git a/spec/frontend/work_items/components/work_item_labels_spec.js b/spec/frontend/work_items/components/work_item_labels_spec.js
index 22ac709a7ff..083bb5bc4a4 100644
--- a/spec/frontend/work_items/components/work_item_labels_spec.js
+++ b/spec/frontend/work_items/components/work_item_labels_spec.js
@@ -5,7 +5,7 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
-import labelSearchQuery from '~/vue_shared/components/sidebar/labels_select_widget/graphql/project_labels.query.graphql';
+import labelSearchQuery from '~/sidebar/components/labels/labels_select_widget/graphql/project_labels.query.graphql';
import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
import workItemLabelsSubscription from 'ee_else_ce/work_items/graphql/work_item_labels.subscription.graphql';
import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
diff --git a/spec/graphql/resolvers/ci/all_jobs_resolver_spec.rb b/spec/graphql/resolvers/ci/all_jobs_resolver_spec.rb
index 2a7d0a8171b..5c632ed3443 100644
--- a/spec/graphql/resolvers/ci/all_jobs_resolver_spec.rb
+++ b/spec/graphql/resolvers/ci/all_jobs_resolver_spec.rb
@@ -11,29 +11,46 @@ RSpec.describe Resolvers::Ci::AllJobsResolver do
let_it_be(:pending_job) { create(:ci_build, :pending, name: 'Job Three') }
let(:args) { {} }
- let(:current_user) { create(:admin) }
subject { resolve_jobs(args) }
describe '#resolve' do
- context 'with authorized user' do
- context 'with statuses argument' do
- let(:args) { { statuses: [Types::Ci::JobStatusEnum.coerce_isolated_input('SUCCESS')] } }
+ context 'with admin' do
+ let(:current_user) { create(:admin) }
- it { is_expected.to contain_exactly(successful_job, successful_job_two) }
- end
+ shared_examples 'executes as admin' do
+ context 'with statuses argument' do
+ let(:args) { { statuses: [Types::Ci::JobStatusEnum.coerce_isolated_input('SUCCESS')] } }
+
+ it { is_expected.to contain_exactly(successful_job, successful_job_two) }
+ end
+
+ context 'with multiple statuses' do
+ let(:args) do
+ { statuses: [Types::Ci::JobStatusEnum.coerce_isolated_input('SUCCESS'),
+ Types::Ci::JobStatusEnum.coerce_isolated_input('FAILED')] }
+ end
+
+ it { is_expected.to contain_exactly(successful_job, successful_job_two, failed_job) }
+ end
- context 'with multiple statuses' do
- let(:args) do
- { statuses: [Types::Ci::JobStatusEnum.coerce_isolated_input('SUCCESS'),
- Types::Ci::JobStatusEnum.coerce_isolated_input('FAILED')] }
+ context 'without statuses argument' do
+ it { is_expected.to contain_exactly(successful_job, successful_job_two, failed_job, pending_job) }
end
+ end
- it { is_expected.to contain_exactly(successful_job, successful_job_two, failed_job) }
+ context 'when admin mode setting is disabled', :do_not_mock_admin_mode_setting do
+ it_behaves_like 'executes as admin'
end
- context 'without statuses argument' do
- it { is_expected.to contain_exactly(successful_job, successful_job_two, failed_job, pending_job) }
+ context 'when admin mode setting is enabled' do
+ context 'when in admin mode', :enable_admin_mode do
+ it_behaves_like 'executes as admin'
+ end
+
+ context 'when not in admin mode' do
+ it { is_expected.to be_empty }
+ end
end
end
diff --git a/spec/graphql/resolvers/ci/runners_resolver_spec.rb b/spec/graphql/resolvers/ci/runners_resolver_spec.rb
index a7eb93da297..9e3793ba1e2 100644
--- a/spec/graphql/resolvers/ci/runners_resolver_spec.rb
+++ b/spec/graphql/resolvers/ci/runners_resolver_spec.rb
@@ -28,8 +28,24 @@ RSpec.describe Resolvers::Ci::RunnersResolver do
context 'when user can see runners' do
let(:obj) { nil }
- it 'returns all the runners' do
- expect(subject.items.to_a).to contain_exactly(inactive_project_runner, offline_project_runner, group_runner, subgroup_runner, instance_runner)
+ context 'when admin mode setting is disabled', :do_not_mock_admin_mode_setting do
+ it 'returns all the runners' do
+ expect(subject.items.to_a).to contain_exactly(inactive_project_runner, offline_project_runner, group_runner, subgroup_runner, instance_runner)
+ end
+ end
+
+ context 'when admin mode setting is enabled' do
+ context 'when in admin mode', :enable_admin_mode do
+ it 'returns all the runners' do
+ expect(subject.items.to_a).to contain_exactly(inactive_project_runner, offline_project_runner, group_runner, subgroup_runner, instance_runner)
+ end
+ end
+
+ context 'when not in admin mode' do
+ it 'returns no runners' do
+ expect(subject.items.to_a).to eq([])
+ end
+ end
end
end
diff --git a/spec/helpers/issues_helper_spec.rb b/spec/helpers/issues_helper_spec.rb
index e5bd8e6532f..83b863eb7e3 100644
--- a/spec/helpers/issues_helper_spec.rb
+++ b/spec/helpers/issues_helper_spec.rb
@@ -381,6 +381,26 @@ RSpec.describe IssuesHelper do
end
end
+ describe '#dashboard_issues_list_data' do
+ let(:current_user) { double.as_null_object }
+
+ it 'returns expected result' do
+ allow(helper).to receive(:current_user).and_return(current_user)
+ allow(helper).to receive(:image_path).and_return('#')
+ allow(helper).to receive(:url_for).and_return('#')
+
+ expected = {
+ calendar_path: '#',
+ empty_state_svg_path: '#',
+ is_public_visibility_restricted: Gitlab::CurrentSettings.restricted_visibility_levels ? 'false' : '',
+ is_signed_in: current_user.present?.to_s,
+ rss_path: '#'
+ }
+
+ expect(helper.dashboard_issues_list_data(current_user)).to include(expected)
+ end
+ end
+
describe '#issues_form_data' do
it 'returns expected result' do
expected = {
diff --git a/spec/lib/api/entities/ssh_key_spec.rb b/spec/lib/api/entities/ssh_key_spec.rb
index 9d0acf69274..b4310035a66 100644
--- a/spec/lib/api/entities/ssh_key_spec.rb
+++ b/spec/lib/api/entities/ssh_key_spec.rb
@@ -19,15 +19,5 @@ RSpec.describe API::Entities::SSHKey, feature_category: :authentication_and_auth
usage_type: 'auth_and_signing'
)
end
-
- context 'when ssh_key_usage_types is disabled' do
- before do
- stub_feature_flags(ssh_key_usage_types: false)
- end
-
- it 'does not include usage type field' do
- expect(subject.keys).not_to include(:usage_type)
- end
- end
end
end
diff --git a/spec/lib/gitlab/background_migration/delete_orphans_approval_merge_request_rules_spec.rb b/spec/lib/gitlab/background_migration/delete_orphans_approval_merge_request_rules_spec.rb
new file mode 100644
index 00000000000..c5b46d3f57c
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/delete_orphans_approval_merge_request_rules_spec.rb
@@ -0,0 +1,75 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::BackgroundMigration::DeleteOrphansApprovalMergeRequestRules do
+ describe '#perform' do
+ let(:batch_table) { :approval_merge_request_rules }
+ let(:batch_column) { :id }
+ let(:sub_batch_size) { 1 }
+ let(:pause_ms) { 0 }
+ let(:connection) { ApplicationRecord.connection }
+
+ let(:namespaces) { table(:namespaces) }
+ let(:projects) { table(:projects) }
+ let(:approval_merge_request_rules) { table(:approval_merge_request_rules) }
+ let(:security_orchestration_policy_configurations) { table(:security_orchestration_policy_configurations) }
+ let(:namespace) { namespaces.create!(name: 'name', path: 'path') }
+ let(:project) do
+ projects
+ .create!(name: "project", path: "project", namespace_id: namespace.id, project_namespace_id: namespace.id)
+ end
+
+ let(:namespace_2) { namespaces.create!(name: 'name_2', path: 'path_2') }
+ let(:security_project) do
+ projects
+ .create!(name: "security_project", path: "security_project", namespace_id: namespace_2.id,
+ project_namespace_id: namespace_2.id)
+ end
+
+ let!(:security_orchestration_policy_configuration) do
+ security_orchestration_policy_configurations
+ .create!(project_id: project.id, security_policy_management_project_id: security_project.id)
+ end
+
+ let(:merge_request) do
+ table(:merge_requests).create!(target_project_id: project.id, target_branch: 'main', source_branch: 'feature')
+ end
+
+ let!(:approval_rule) do
+ approval_merge_request_rules.create!(
+ name: 'rule',
+ merge_request_id: merge_request.id,
+ report_type: 4,
+ security_orchestration_policy_configuration_id: security_orchestration_policy_configuration.id)
+ end
+
+ let!(:approval_rule_other_report_type) do
+ approval_merge_request_rules.create!(
+ name: 'rule 2',
+ merge_request_id: merge_request.id,
+ report_type: 1,
+ security_orchestration_policy_configuration_id: security_orchestration_policy_configuration.id)
+ end
+
+ let!(:approval_rule_last) do
+ approval_merge_request_rules.create!(name: 'rule 3', merge_request_id: merge_request.id, report_type: 4)
+ end
+
+ subject do
+ described_class.new(
+ start_id: approval_rule.id,
+ end_id: approval_rule_last.id,
+ batch_table: batch_table,
+ batch_column: batch_column,
+ sub_batch_size: sub_batch_size,
+ pause_ms: pause_ms,
+ connection: connection
+ ).perform
+ end
+
+ it 'delete only approval rules without association with the security project and report_type equals to 4' do
+ expect { subject }.to change { approval_merge_request_rules.count }.from(3).to(2)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/background_migration/delete_orphans_approval_project_rules_spec.rb b/spec/lib/gitlab/background_migration/delete_orphans_approval_project_rules_spec.rb
new file mode 100644
index 00000000000..16253255764
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/delete_orphans_approval_project_rules_spec.rb
@@ -0,0 +1,71 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::BackgroundMigration::DeleteOrphansApprovalProjectRules do
+ describe '#perform' do
+ let(:batch_table) { :approval_project_rules }
+ let(:batch_column) { :id }
+ let(:sub_batch_size) { 1 }
+ let(:pause_ms) { 0 }
+ let(:connection) { ApplicationRecord.connection }
+
+ let(:namespaces) { table(:namespaces) }
+ let(:projects) { table(:projects) }
+ let(:approval_project_rules) { table(:approval_project_rules) }
+ let(:security_orchestration_policy_configurations) { table(:security_orchestration_policy_configurations) }
+ let(:namespace) { namespaces.create!(name: 'name', path: 'path') }
+ let(:project) do
+ projects
+ .create!(name: "project", path: "project", namespace_id: namespace.id, project_namespace_id: namespace.id)
+ end
+
+ let(:namespace_2) { namespaces.create!(name: 'name_2', path: 'path_2') }
+ let(:security_project) do
+ projects
+ .create!(name: "security_project", path: "security_project", namespace_id: namespace_2.id,
+ project_namespace_id: namespace_2.id)
+ end
+
+ let!(:security_orchestration_policy_configuration) do
+ security_orchestration_policy_configurations
+ .create!(project_id: project.id, security_policy_management_project_id: security_project.id)
+ end
+
+ let!(:project_rule) do
+ approval_project_rules.create!(
+ name: 'rule',
+ project_id: project.id,
+ report_type: 4,
+ security_orchestration_policy_configuration_id: security_orchestration_policy_configuration.id)
+ end
+
+ let!(:project_rule_other_report_type) do
+ approval_project_rules.create!(
+ name: 'rule 2',
+ project_id: project.id,
+ report_type: 1,
+ security_orchestration_policy_configuration_id: security_orchestration_policy_configuration.id)
+ end
+
+ let!(:project_rule_last) do
+ approval_project_rules.create!(name: 'rule 3', project_id: project.id, report_type: 4)
+ end
+
+ subject do
+ described_class.new(
+ start_id: project_rule.id,
+ end_id: project_rule_last.id,
+ batch_table: batch_table,
+ batch_column: batch_column,
+ sub_batch_size: sub_batch_size,
+ pause_ms: pause_ms,
+ connection: connection
+ ).perform
+ end
+
+ it 'delete only approval rules without association with the security project and report_type equals to 4' do
+ expect { subject }.to change { approval_project_rules.count }.from(3).to(2)
+ end
+ end
+end
diff --git a/spec/migrations/20221110152133_delete_orphans_approval_rules_spec.rb b/spec/migrations/20221110152133_delete_orphans_approval_rules_spec.rb
new file mode 100644
index 00000000000..2d7e22d9b72
--- /dev/null
+++ b/spec/migrations/20221110152133_delete_orphans_approval_rules_spec.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe DeleteOrphansApprovalRules do
+ describe '#up' do
+ it 'schedules background migration for both levels of approval rules' do
+ migrate!
+
+ expect(described_class::MERGE_REQUEST_MIGRATION).to have_scheduled_batched_migration(
+ table_name: :approval_merge_request_rules,
+ column_name: :id,
+ interval: described_class::INTERVAL)
+
+ expect(described_class::PROJECT_MIGRATION).to have_scheduled_batched_migration(
+ table_name: :approval_project_rules,
+ column_name: :id,
+ interval: described_class::INTERVAL)
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/issues_spec.rb b/spec/requests/api/graphql/issues_spec.rb
index 8c8690a5a3e..cb575428eb8 100644
--- a/spec/requests/api/graphql/issues_spec.rb
+++ b/spec/requests/api/graphql/issues_spec.rb
@@ -81,14 +81,13 @@ RSpec.describe 'getting an issue list at root level' do
)
end
- let(:issues) { [issue_a, issue_b, issue_c, issue_d, issue_e] }
- let(:issue_filter_params) { {} }
+ let_it_be(:issues, reload: true) { [issue_a, issue_b, issue_c, issue_d, issue_e] }
+ let(:issue_filter_params) { {} }
+ let(:current_user) { developer }
let(:fields) do
<<~QUERY
- nodes {
- #{all_graphql_fields_for('issues'.classify)}
- }
+ nodes { id }
QUERY
end
@@ -108,15 +107,16 @@ RSpec.describe 'getting an issue list at root level' do
end
end
+ # All new specs should be added to the shared example if the change also
+ # affects the `issues` query at the root level of the API.
+ # Shared example also used in spec/requests/api/graphql/project/issues_spec.rb
it_behaves_like 'graphql issue list request spec' do
let_it_be(:external_user) { create(:user) }
let(:public_projects) { [project_a, project_c] }
- let(:current_user) { developer }
let(:another_user) { reporter }
- let(:issues_data) { graphql_data['issues']['nodes'] }
- let(:issue_ids) { graphql_dig_at(issues_data, :id) }
+ let(:issue_nodes_path) { %w[issues nodes] }
# filters
let(:expected_negated_assignee_issues) { [issue_b, issue_c, issue_d, issue_e] }
@@ -133,7 +133,6 @@ RSpec.describe 'getting an issue list at root level' do
# sorting
let(:data_path) { [:issues] }
- let(:expected_severity_sorted_asc) { [issue_c, issue_a, issue_b, issue_e, issue_d] }
let(:expected_priority_sorted_asc) { [issue_c, issue_e, issue_d, issue_a, issue_b] }
let(:expected_priority_sorted_desc) { [issue_a, issue_d, issue_e, issue_c, issue_b] }
let(:expected_due_date_sorted_desc) { [issue_c, issue_b, issue_a, issue_e, issue_d] }
@@ -144,18 +143,16 @@ RSpec.describe 'getting an issue list at root level' do
let(:expected_milestone_sorted_asc) { [issue_c, issue_e, issue_d, issue_a, issue_b] }
let(:expected_milestone_sorted_desc) { [issue_a, issue_d, issue_e, issue_c, issue_b] }
+ # N+1 queries
+ let(:same_project_issue1) { issue_d }
+ let(:same_project_issue2) { issue_e }
+
before_all do
issue_a.assignee_ids = developer.id
issue_c.assignee_ids = reporter.id
create(:award_emoji, :upvote, user: developer, awardable: issue_a)
create(:award_emoji, :upvote, user: developer, awardable: issue_c)
-
- # severity sorting
- create(:issuable_severity, issue: issue_a, severity: :unknown)
- create(:issuable_severity, issue: issue_b, severity: :low)
- create(:issuable_severity, issue: issue_d, severity: :critical)
- create(:issuable_severity, issue: issue_e, severity: :high)
end
def pagination_query(params)
@@ -165,10 +162,14 @@ RSpec.describe 'getting an issue list at root level' do
"#{page_info} nodes { id }"
)
end
+ end
- def post_query(request_user = current_user)
- post_graphql(query, current_user: request_user)
- end
+ def execute_query
+ post_query
+ end
+
+ def post_query(request_user = current_user)
+ post_graphql(query, current_user: request_user)
end
def query(params = issue_filter_params)
diff --git a/spec/requests/api/graphql/project/issues_spec.rb b/spec/requests/api/graphql/project/issues_spec.rb
index 57f7a410d4b..563ddb62bcd 100644
--- a/spec/requests/api/graphql/project/issues_spec.rb
+++ b/spec/requests/api/graphql/project/issues_spec.rb
@@ -17,7 +17,7 @@ RSpec.describe 'getting an issue list for a project' do
let_it_be(:priority2) { create(:label, project: project, priority: 5) }
let_it_be(:priority3) { create(:label, project: project, priority: 10) }
- let_it_be(:issue_a, reload: true) do
+ let_it_be(:issue_a) do
create(
:issue,
project: project,
@@ -28,7 +28,7 @@ RSpec.describe 'getting an issue list for a project' do
)
end
- let_it_be(:issue_b, reload: true) do
+ let_it_be(:issue_b) do
create(
:issue,
:with_alert,
@@ -74,19 +74,9 @@ RSpec.describe 'getting an issue list for a project' do
let_it_be(:issues, reload: true) { [issue_a, issue_b, issue_c, issue_d, issue_e] }
- let(:issue_a_gid) { issue_a.to_global_id.to_s }
- let(:issue_b_gid) { issue_b.to_global_id.to_s }
- let(:issues_data) { graphql_data['project']['issues']['nodes'] }
+ let(:issue_nodes_path) { %w[project issues nodes] }
let(:issue_filter_params) { {} }
- let(:fields) do
- <<~QUERY
- nodes {
- #{all_graphql_fields_for('issues'.classify)}
- }
- QUERY
- end
-
# All new specs should be added to the shared example if the change also
# affects the `issues` query at the root level of the API.
# Shared example also used in spec/requests/api/graphql/issues_spec.rb
@@ -114,7 +104,6 @@ RSpec.describe 'getting an issue list for a project' do
# sorting
let(:data_path) { [:project, :issues] }
- let(:expected_severity_sorted_asc) { [issue_c, issue_a, issue_b, issue_e, issue_d] }
let(:expected_priority_sorted_asc) { [issue_b, issue_c, issue_d, issue_a, issue_e] }
let(:expected_priority_sorted_desc) { [issue_a, issue_d, issue_c, issue_b, issue_e] }
let(:expected_due_date_sorted_desc) { [issue_d, issue_e, issue_c, issue_b, issue_a] }
@@ -125,17 +114,15 @@ RSpec.describe 'getting an issue list for a project' do
let(:expected_milestone_sorted_asc) { [issue_b, issue_c, issue_d, issue_a, issue_e] }
let(:expected_milestone_sorted_desc) { [issue_a, issue_d, issue_c, issue_b, issue_e] }
+ # N+1 queries
+ let(:same_project_issue1) { issue_a }
+ let(:same_project_issue2) { issue_b }
+
before_all do
issue_a.assignee_ids = current_user.id
issue_b.assignee_ids = another_user.id
create(:award_emoji, :upvote, user: current_user, awardable: issue_a)
-
- # severity sorting
- create(:issuable_severity, issue: issue_a, severity: :unknown)
- create(:issuable_severity, issue: issue_b, severity: :low)
- create(:issuable_severity, issue: issue_d, severity: :critical)
- create(:issuable_severity, issue: issue_e, severity: :high)
end
def pagination_query(params)
@@ -151,313 +138,6 @@ RSpec.describe 'getting an issue list for a project' do
end
end
- context 'when fetching alert management alert' do
- let(:fields) do
- <<~QUERY
- nodes {
- iid
- alertManagementAlert {
- title
- }
- alertManagementAlerts {
- nodes {
- title
- }
- }
- }
- QUERY
- end
-
- # Alerts need to have developer permission and above
- before do
- project.add_developer(current_user)
- end
-
- it 'avoids N+1 queries' do
- control = ActiveRecord::QueryRecorder.new { post_graphql(query, current_user: current_user) }
-
- create(:alert_management_alert, :with_incident, project: project)
-
- expect { post_graphql(query, current_user: current_user) }.not_to exceed_query_limit(control)
- end
-
- it 'returns the alert data' do
- post_graphql(query, current_user: current_user)
-
- alert_titles = issues_data.map { |issue| issue.dig('alertManagementAlert', 'title') }
- expected_titles = issues.map { |issue| issue.alert_management_alerts.first&.title }
-
- expect(alert_titles).to contain_exactly(*expected_titles)
- end
-
- it 'returns the alerts data' do
- post_graphql(query, current_user: current_user)
-
- alert_titles = issues_data.map { |issue| issue.dig('alertManagementAlerts', 'nodes') }
- expected_titles = issues.map do |issue|
- issue.alert_management_alerts.map { |alert| { 'title' => alert.title } }
- end
-
- expect(alert_titles).to contain_exactly(*expected_titles)
- end
- end
-
- context 'when fetching customer_relations_contacts' do
- let(:fields) do
- <<~QUERY
- nodes {
- id
- customerRelationsContacts {
- nodes {
- firstName
- }
- }
- }
- QUERY
- end
-
- def clean_state_query
- run_with_clean_state(query, context: { current_user: current_user })
- end
-
- it 'avoids N+1 queries' do
- create(:issue_customer_relations_contact, :for_issue, issue: issue_a)
-
- control = ActiveRecord::QueryRecorder.new(skip_cached: false) { clean_state_query }
-
- create(:issue_customer_relations_contact, :for_issue, issue: issue_a)
-
- expect { clean_state_query }.not_to exceed_all_query_limit(control)
- end
- end
-
- context 'when fetching labels' do
- let(:fields) do
- <<~QUERY
- nodes {
- id
- labels {
- nodes {
- id
- }
- }
- }
- QUERY
- end
-
- before do
- project.add_developer(current_user)
- issues.each do |issue|
- # create a label for each issue we have to properly test N+1
- label = create(:label, project: project)
- issue.update!(labels: [label])
- end
- end
-
- def response_label_ids(response_data)
- response_data.map do |node|
- node['labels']['nodes'].map { |u| u['id'] }
- end.flatten
- end
-
- def labels_as_global_ids(issues)
- issues.map(&:labels).flatten.map(&:to_global_id).map(&:to_s)
- end
-
- it 'avoids N+1 queries', :aggregate_failures do
- control = ActiveRecord::QueryRecorder.new { post_graphql(query, current_user: current_user) }
- expect(issues_data.count).to eq(5)
- expect(response_label_ids(issues_data)).to match_array(labels_as_global_ids(issues))
-
- new_issues = issues + [create(:issue, project: project, labels: [create(:label, project: project)])]
-
- expect { post_graphql(query, current_user: current_user) }.not_to exceed_query_limit(control)
- # graphql_data is memoized (see spec/support/helpers/graphql_helpers.rb)
- # so we have to parse the body ourselves the second time
- issues_data = Gitlab::Json.parse(response.body)['data']['project']['issues']['nodes']
- expect(issues_data.count).to eq(6)
- expect(response_label_ids(issues_data)).to match_array(labels_as_global_ids(new_issues))
- end
- end
-
- context 'when fetching assignees' do
- let(:fields) do
- <<~QUERY
- nodes {
- id
- assignees {
- nodes {
- id
- }
- }
- }
- QUERY
- end
-
- before do
- project.add_developer(current_user)
- issues.each do |issue|
- # create an assignee for each issue we have to properly test N+1
- assignee = create(:user)
- issue.update!(assignees: [assignee])
- end
- end
-
- def response_assignee_ids(response_data)
- response_data.map do |node|
- node['assignees']['nodes'].map { |node| node['id'] }
- end.flatten
- end
-
- def assignees_as_global_ids(issues)
- issues.map(&:assignees).flatten.map(&:to_global_id).map(&:to_s)
- end
-
- it 'avoids N+1 queries', :aggregate_failures do
- control = ActiveRecord::QueryRecorder.new { post_graphql(query, current_user: current_user) }
- expect(issues_data.count).to eq(5)
- expect(response_assignee_ids(issues_data)).to match_array(assignees_as_global_ids(issues))
-
- new_issues = issues + [create(:issue, project: project, assignees: [create(:user)])]
-
- expect { post_graphql(query, current_user: current_user) }.not_to exceed_query_limit(control)
- # graphql_data is memoized (see spec/support/helpers/graphql_helpers.rb)
- # so we have to parse the body ourselves the second time
- issues_data = Gitlab::Json.parse(response.body)['data']['project']['issues']['nodes']
- expect(issues_data.count).to eq(6)
- expect(response_assignee_ids(issues_data)).to match_array(assignees_as_global_ids(new_issues))
- end
- end
-
- describe 'N+1 query checks' do
- let(:extra_iid_for_second_query) { issue_b.iid.to_s }
- let(:search_params) { { iids: [issue_a.iid.to_s] } }
-
- def execute_query
- query = graphql_query_for(
- :project,
- { full_path: project.full_path },
- query_graphql_field(
- :issues, search_params,
- query_graphql_field(:nodes, nil, requested_fields)
- )
- )
- post_graphql(query, current_user: current_user)
- end
-
- context 'when requesting `user_notes_count`' do
- let(:requested_fields) { [:user_notes_count] }
-
- before do
- create_list(:note_on_issue, 2, noteable: issue_a, project: project)
- create(:note_on_issue, noteable: issue_b, project: project)
- end
-
- include_examples 'N+1 query check'
- end
-
- context 'when requesting `user_discussions_count`' do
- let(:requested_fields) { [:user_discussions_count] }
-
- before do
- create_list(:note_on_issue, 2, noteable: issue_a, project: project)
- create(:note_on_issue, noteable: issue_b, project: project)
- end
-
- include_examples 'N+1 query check'
- end
-
- context 'when requesting `merge_requests_count`' do
- let(:requested_fields) { [:merge_requests_count] }
-
- before do
- create_list(:merge_requests_closing_issues, 2, issue: issue_a)
- create_list(:merge_requests_closing_issues, 3, issue: issue_b)
- end
-
- include_examples 'N+1 query check'
- end
-
- context 'when requesting `timelogs`' do
- let(:requested_fields) { 'timelogs { nodes { timeSpent } }' }
-
- before do
- create_list(:issue_timelog, 2, issue: issue_a)
- create(:issue_timelog, issue: issue_b)
- end
-
- include_examples 'N+1 query check'
- end
-
- # rubocop:disable RSpec/MultipleMemoizedHelpers
- context 'when requesting `closed_as_duplicate_of`' do
- let(:requested_fields) { 'closedAsDuplicateOf { id }' }
- let(:issue_a_dup) { create(:issue, project: project) }
- let(:issue_b_dup) { create(:issue, project: project) }
-
- before do
- issue_a.update!(duplicated_to_id: issue_a_dup)
- issue_b.update!(duplicated_to_id: issue_a_dup)
- end
-
- include_examples 'N+1 query check'
- end
- # rubocop:enable RSpec/MultipleMemoizedHelpers
-
- context 'when award emoji votes' do
- let(:requested_fields) { [:upvotes, :downvotes] }
-
- before do
- create_list(:award_emoji, 2, name: 'thumbsup', awardable: issue_a)
- create_list(:award_emoji, 2, name: 'thumbsdown', awardable: issue_b)
- end
-
- include_examples 'N+1 query check'
- end
-
- context 'when requesting participants' do
- let_it_be(:issue_c) { create(:issue, project: project) }
-
- let(:search_params) { { iids: [issue_a.iid.to_s, issue_c.iid.to_s] } }
- let(:requested_fields) { 'participants { nodes { name } }' }
-
- before do
- create(:award_emoji, :upvote, awardable: issue_a)
- create(:award_emoji, :upvote, awardable: issue_b)
- create(:award_emoji, :upvote, awardable: issue_c)
-
- note_with_emoji_a = create(:note_on_issue, noteable: issue_a, project: project)
- note_with_emoji_b = create(:note_on_issue, noteable: issue_b, project: project)
- note_with_emoji_c = create(:note_on_issue, noteable: issue_c, project: project)
-
- create(:award_emoji, :upvote, awardable: note_with_emoji_a)
- create(:award_emoji, :upvote, awardable: note_with_emoji_b)
- create(:award_emoji, :upvote, awardable: note_with_emoji_c)
- end
-
- # Executes 3 extra queries to fetch participant_attrs
- include_examples 'N+1 query check', threshold: 3
- end
-
- context 'when requesting labels' do
- let(:requested_fields) { ['labels { nodes { id } }'] }
-
- before do
- project_labels = create_list(:label, 2, project: project)
- group_labels = create_list(:group_label, 2, group: group)
-
- issue_a.update!(labels: [project_labels.first, group_labels.first].flatten)
- issue_b.update!(labels: [project_labels, group_labels].flatten)
- end
-
- include_examples 'N+1 query check', skip_cached: false
- end
- end
-
- def issue_ids
- graphql_dig_at(issues_data, :id)
- end
-
def query(params = issue_filter_params)
graphql_query_for(
'project',
diff --git a/spec/support/helpers/cookie_helper.rb b/spec/support/helpers/cookie_helper.rb
index ea4be12355b..8971c03a5cc 100644
--- a/spec/support/helpers/cookie_helper.rb
+++ b/spec/support/helpers/cookie_helper.rb
@@ -27,6 +27,12 @@ module CookieHelper
page.driver.browser.manage.cookie_named(name)
end
+ def wait_for_cookie_set(name)
+ wait_for("Complete setting cookie") do
+ get_cookie(name)
+ end
+ end
+
private
def on_a_page?
diff --git a/spec/support/shared_examples/requests/api/graphql/issue_list_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/issue_list_shared_examples.rb
index 9de741ec529..d4479e462af 100644
--- a/spec/support/shared_examples/requests/api/graphql/issue_list_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/graphql/issue_list_shared_examples.rb
@@ -1,6 +1,15 @@
# frozen_string_literal: true
RSpec.shared_examples 'graphql issue list request spec' do
+ let(:issue_ids) { graphql_dig_at(issues_data, :id) }
+ let(:fields) do
+ <<~QUERY
+ nodes {
+ #{all_graphql_fields_for('issues'.classify)}
+ }
+ QUERY
+ end
+
it_behaves_like 'a working graphql query' do
before do
post_query
@@ -151,6 +160,15 @@ RSpec.shared_examples 'graphql issue list request spec' do
describe 'sorting and pagination' do
context 'when sorting by severity' do
+ let(:expected_severity_sorted_asc) { [issue_c, issue_a, issue_b, issue_e, issue_d] }
+
+ before_all do
+ create(:issuable_severity, issue: issue_a, severity: :unknown)
+ create(:issuable_severity, issue: issue_b, severity: :low)
+ create(:issuable_severity, issue: issue_d, severity: :critical)
+ create(:issuable_severity, issue: issue_e, severity: :high)
+ end
+
context 'when ascending' do
it_behaves_like 'sorted paginated query' do
let(:sort_param) { :SEVERITY_ASC }
@@ -251,6 +269,120 @@ RSpec.shared_examples 'graphql issue list request spec' do
end
end
+ describe 'N+1 query checks' do
+ let(:extra_iid_for_second_query) { issue_b.iid.to_s }
+ let(:search_params) { { iids: [issue_a.iid.to_s] } }
+ let(:issue_filter_params) { search_params }
+ let(:fields) do
+ <<~QUERY
+ nodes {
+ id
+ #{requested_fields}
+ }
+ QUERY
+ end
+
+ def execute_query
+ post_query
+ end
+
+ context 'when requesting `user_notes_count` and `user_discussions_count`' do
+ let(:requested_fields) { 'userNotesCount userDiscussionsCount' }
+
+ before do
+ create_list(:note_on_issue, 2, noteable: issue_a, project: issue_a.project)
+ create(:note_on_issue, noteable: issue_b, project: issue_b.project)
+ end
+
+ include_examples 'N+1 query check'
+ end
+
+ context 'when requesting `merge_requests_count`' do
+ let(:requested_fields) { 'mergeRequestsCount' }
+
+ before do
+ create_list(:merge_requests_closing_issues, 2, issue: issue_a)
+ create_list(:merge_requests_closing_issues, 3, issue: issue_b)
+ end
+
+ include_examples 'N+1 query check'
+ end
+
+ context 'when requesting `timelogs`' do
+ let(:requested_fields) { 'timelogs { nodes { timeSpent } }' }
+
+ before do
+ create_list(:issue_timelog, 2, issue: issue_a)
+ create(:issue_timelog, issue: issue_b)
+ end
+
+ include_examples 'N+1 query check'
+ end
+
+ context 'when requesting `closed_as_duplicate_of`' do
+ let(:requested_fields) { 'closedAsDuplicateOf { id }' }
+ let(:issue_a_dup) { create(:issue, project: issue_a.project) }
+ let(:issue_b_dup) { create(:issue, project: issue_b.project) }
+
+ before do
+ issue_a.update!(duplicated_to_id: issue_a_dup)
+ issue_b.update!(duplicated_to_id: issue_a_dup)
+ end
+
+ include_examples 'N+1 query check'
+ end
+
+ context 'when award emoji votes' do
+ let(:requested_fields) { 'upvotes downvotes' }
+
+ before do
+ create_list(:award_emoji, 2, name: 'thumbsup', awardable: issue_a)
+ create_list(:award_emoji, 2, name: 'thumbsdown', awardable: issue_b)
+ end
+
+ include_examples 'N+1 query check'
+ end
+
+ context 'when requesting participants' do
+ let(:search_params) { { iids: [issue_a.iid.to_s, issue_c.iid.to_s] } }
+ let(:requested_fields) { 'participants { nodes { name } }' }
+
+ before do
+ create(:award_emoji, :upvote, awardable: issue_a)
+ create(:award_emoji, :upvote, awardable: issue_b)
+ create(:award_emoji, :upvote, awardable: issue_c)
+
+ note_with_emoji_a = create(:note_on_issue, noteable: issue_a, project: issue_a.project)
+ note_with_emoji_b = create(:note_on_issue, noteable: issue_b, project: issue_b.project)
+ note_with_emoji_c = create(:note_on_issue, noteable: issue_c, project: issue_c.project)
+
+ create(:award_emoji, :upvote, awardable: note_with_emoji_a)
+ create(:award_emoji, :upvote, awardable: note_with_emoji_b)
+ create(:award_emoji, :upvote, awardable: note_with_emoji_c)
+ end
+
+ # Executes 3 extra queries to fetch participant_attrs
+ include_examples 'N+1 query check', threshold: 3
+ end
+
+ context 'when requesting labels', :use_sql_query_cache do
+ let(:requested_fields) { 'labels { nodes { id } }' }
+ let(:extra_iid_for_second_query) { same_project_issue2.iid.to_s }
+ let(:search_params) { { iids: [same_project_issue1.iid.to_s] } }
+
+ before do
+ current_project = same_project_issue1.project
+ project_labels = create_list(:label, 2, project: current_project)
+ group_labels = create_list(:group_label, 2, group: current_project.group)
+
+ same_project_issue1.update!(labels: [project_labels.first, group_labels.first].flatten)
+ same_project_issue2.update!(labels: [project_labels, group_labels].flatten)
+ end
+
+ include_examples 'N+1 query check', skip_cached: false
+ end
+ end
+
context 'when confidential issues exist' do
context 'when user can see confidential issues' do
it 'includes confidential issues' do
@@ -259,7 +391,7 @@ RSpec.shared_examples 'graphql issue list request spec' do
all_issues = confidential_issues + non_confidential_issues
expect(issue_ids).to match_array(to_gid_list(all_issues))
- expect(issues_data.map { |i| i['confidential'] }).to match_array(all_issues.map(&:confidential))
+ expect(issues_data.pluck('confidential')).to match_array(all_issues.map(&:confidential))
end
end
@@ -340,7 +472,7 @@ RSpec.shared_examples 'graphql issue list request spec' do
it 'returns the escalation status values' do
post_query
- statuses = issues_data.map { |issue| issue['escalationStatus'] }
+ statuses = issues_data.pluck('escalationStatus')
expect(statuses).to contain_exactly(escalation_status.status_name.upcase.to_s, nil, nil, nil, nil)
end
@@ -355,6 +487,177 @@ RSpec.shared_examples 'graphql issue list request spec' do
end
end
+ context 'when fetching alert management alert' do
+ let(:fields) do
+ <<~QUERY
+ nodes {
+ iid
+ alertManagementAlert {
+ title
+ }
+ alertManagementAlerts {
+ nodes {
+ title
+ }
+ }
+ }
+ QUERY
+ end
+
+ it 'avoids N+1 queries' do
+ control = ActiveRecord::QueryRecorder.new { post_query }
+
+ create(:alert_management_alert, :with_incident, project: public_projects.first)
+
+ expect { post_query }.not_to exceed_query_limit(control)
+ end
+
+ it 'returns the alert data' do
+ post_query
+
+ alert_titles = issues_data.map { |issue| issue.dig('alertManagementAlert', 'title') }
+ expected_titles = issues.map { |issue| issue.alert_management_alerts.first&.title }
+
+ expect(alert_titles).to contain_exactly(*expected_titles)
+ end
+
+ it 'returns the alerts data' do
+ post_query
+
+ alert_titles = issues_data.map { |issue| issue.dig('alertManagementAlerts', 'nodes') }
+ expected_titles = issues.map do |issue|
+ issue.alert_management_alerts.map { |alert| { 'title' => alert.title } }
+ end
+
+ expect(alert_titles).to contain_exactly(*expected_titles)
+ end
+ end
+
+ context 'when fetching customer_relations_contacts' do
+ let(:fields) do
+ <<~QUERY
+ nodes {
+ id
+ customerRelationsContacts {
+ nodes {
+ firstName
+ }
+ }
+ }
+ QUERY
+ end
+
+ def clean_state_query
+ run_with_clean_state(query, context: { current_user: current_user })
+ end
+
+ it 'avoids N+1 queries' do
+ create(:issue_customer_relations_contact, :for_issue, issue: issue_a)
+
+ control = ActiveRecord::QueryRecorder.new(skip_cached: false) { clean_state_query }
+
+ create(:issue_customer_relations_contact, :for_issue, issue: issue_a)
+
+ expect { clean_state_query }.not_to exceed_all_query_limit(control)
+ end
+ end
+
+ context 'when fetching labels' do
+ let(:fields) do
+ <<~QUERY
+ nodes {
+ id
+ labels {
+ nodes {
+ id
+ }
+ }
+ }
+ QUERY
+ end
+
+ before do
+ issues.each do |issue|
+ # create a label for each issue we have to properly test N+1
+ label = create(:label, project: issue.project)
+ issue.update!(labels: [label])
+ end
+ end
+
+ def response_label_ids(response_data)
+ response_data.map do |node|
+ node['labels']['nodes'].pluck('id')
+ end.flatten
+ end
+
+ def labels_as_global_ids(issues)
+ issues.map(&:labels).flatten.map(&:to_global_id).map(&:to_s)
+ end
+
+ it 'avoids N+1 queries', :aggregate_failures do
+ control = ActiveRecord::QueryRecorder.new { post_query }
+ expect(issues_data.count).to eq(5)
+ expect(response_label_ids(issues_data)).to match_array(labels_as_global_ids(issues))
+
+ public_project = public_projects.first
+ new_issues = issues + [
+ create(:issue, project: public_project, labels: [create(:label, project: public_project)])
+ ]
+
+ expect { post_query }.not_to exceed_query_limit(control)
+
+ expect(issues_data.count).to eq(6)
+ expect(response_label_ids(issues_data)).to match_array(labels_as_global_ids(new_issues))
+ end
+ end
+
+ context 'when fetching assignees' do
+ let(:fields) do
+ <<~QUERY
+ nodes {
+ id
+ assignees {
+ nodes {
+ id
+ }
+ }
+ }
+ QUERY
+ end
+
+ before do
+ issues.each do |issue|
+ # create an assignee for each issue we have to properly test N+1
+ assignee = create(:user)
+ issue.update!(assignees: [assignee])
+ end
+ end
+
+ def response_assignee_ids(response_data)
+ response_data.map do |node|
+ node['assignees']['nodes'].pluck('id')
+ end.flatten
+ end
+
+ def assignees_as_global_ids(issues)
+ issues.map(&:assignees).flatten.map(&:to_global_id).map(&:to_s)
+ end
+
+ it 'avoids N+1 queries', :aggregate_failures do
+ control = ActiveRecord::QueryRecorder.new { post_query }
+ expect(issues_data.count).to eq(5)
+ expect(response_assignee_ids(issues_data)).to match_array(assignees_as_global_ids(issues))
+
+ public_project = public_projects.first
+ new_issues = issues + [create(:issue, project: public_project, assignees: [create(:user)])]
+
+ expect { post_query }.not_to exceed_query_limit(control)
+
+ expect(issues_data.count).to eq(6)
+ expect(response_assignee_ids(issues_data)).to match_array(assignees_as_global_ids(new_issues))
+ end
+ end
+
it 'includes a web_url' do
post_query
@@ -373,4 +676,8 @@ RSpec.shared_examples 'graphql issue list request spec' do
def to_gid_list(instance_list)
instance_list.map { |instance| instance.to_gid.to_s }
end
+
+ def issues_data
+ graphql_data.dig(*issue_nodes_path)
+ end
end
diff --git a/spec/views/profiles/keys/_form.html.haml_spec.rb b/spec/views/profiles/keys/_form.html.haml_spec.rb
index 2a1bb5334b6..dd8af14100a 100644
--- a/spec/views/profiles/keys/_form.html.haml_spec.rb
+++ b/spec/views/profiles/keys/_form.html.haml_spec.rb
@@ -52,17 +52,4 @@ RSpec.describe 'profiles/keys/_form.html.haml' do
expect(rendered).to have_button('Add key')
end
end
-
- context 'when ssh_key_usage_types is disabled' do
- before do
- stub_feature_flags(ssh_key_usage_types: false)
- end
-
- it 'has the usage type field', :aggregate_failures do
- render
-
- expect(rendered).not_to have_field('Usage type', type: 'text')
- expect(rendered).not_to have_text('Authentication & Signing')
- end
- end
end
diff --git a/spec/views/profiles/keys/_key.html.haml_spec.rb b/spec/views/profiles/keys/_key.html.haml_spec.rb
index 821e7ea794d..d2e27bd2ee0 100644
--- a/spec/views/profiles/keys/_key.html.haml_spec.rb
+++ b/spec/views/profiles/keys/_key.html.haml_spec.rb
@@ -47,18 +47,6 @@ RSpec.describe 'profiles/keys/_key.html.haml' do
expect(rendered).to have_text(usage_type_text)
end
-
- context 'when ssh_key_usage_types is disabled' do
- before do
- stub_feature_flags(ssh_key_usage_types: false)
- end
-
- it 'does not render usage type text' do
- render
-
- expect(rendered).not_to have_text(usage_type_text)
- end
- end
end
end
diff --git a/spec/views/profiles/keys/_key_details.html.haml_spec.rb b/spec/views/profiles/keys/_key_details.html.haml_spec.rb
index acb22b5657e..c223d6702c5 100644
--- a/spec/views/profiles/keys/_key_details.html.haml_spec.rb
+++ b/spec/views/profiles/keys/_key_details.html.haml_spec.rb
@@ -27,18 +27,6 @@ RSpec.describe 'profiles/keys/_key_details.html.haml' do
expect(rendered).to have_text(usage_type_text)
end
-
- context 'when ssh_key_usage_types is disabled' do
- before do
- stub_feature_flags(ssh_key_usage_types: false)
- end
-
- it 'does not render usage type text' do
- render
-
- expect(rendered).not_to have_text(usage_type_text)
- end
- end
end
end
end