diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-12-01 21:07:03 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-12-01 21:07:03 +0300 |
commit | 4e3a998b8ec1351d8345863f6cad4b9bd497bd6a (patch) | |
tree | 9bab8c1089ef4bcc11bd8acdffd1f0f6f62c3e56 /spec | |
parent | 08489a6db8ddff0794f9beaf770930803dc7bdca (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
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 |