From 9dc93a4519d9d5d7be48ff274127136236a3adb3 Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Tue, 20 Apr 2021 23:50:22 +0000 Subject: Add latest changes from gitlab-org/gitlab@13-11-stable-ee --- .../boards/destroy_service_shared_examples.rb | 30 ++++ .../boards/lists/update_service_shared_examples.rb | 43 ++++++ .../controllers/snippet_blob_shared_examples.rb | 10 -- .../controllers/snippet_shared_examples.rb | 28 ++++ .../controllers/trackable_shared_examples.rb | 39 ----- .../controllers/unique_visits_shared_examples.rb | 11 +- .../features/cascading_settings_shared_examples.rb | 41 +++++ .../creatable_merge_request_shared_examples.rb | 2 +- .../features/discussion_comments_shared_example.rb | 4 +- .../features/error_tracking_shared_example.rb | 2 +- .../issuable_invite_members_shared_examples.rb | 6 +- .../project_upload_files_shared_examples.rb | 73 +++++++-- ...olving_discussions_in_issues_shared_examples.rb | 6 +- .../features/search_settings_shared_examples.rb | 27 ++-- .../features/sidebar_shared_examples.rb | 165 +++++++++++++++++++++ .../wiki/user_creates_wiki_page_shared_examples.rb | 2 +- .../user_git_access_wiki_page_shared_examples.rb | 2 +- .../user_previews_wiki_changes_shared_examples.rb | 6 +- .../wiki/user_updates_wiki_page_shared_examples.rb | 6 +- .../wiki/user_views_wiki_empty_shared_examples.rb | 6 +- .../mutations/can_mutate_spammable_examples.rb | 6 +- .../mutations/set_assignees_shared_examples.rb | 28 +++- .../sorted_paginated_query_shared_examples.rb | 12 +- .../graphql/spam_protection_shared_examples.rb | 85 +++++++++++ .../gitlab_style_deprecations_shared_examples.rb | 24 ++- .../helpers/groups_shared_examples.rb | 53 +++++++ .../lib/api/ci/runner_shared_examples.rb | 2 +- .../lib/api/internal_base_shared_examples.rb | 11 -- .../lib/gitlab/ci/ci_trace_shared_examples.rb | 11 ++ .../database/cte_materialized_shared_examples.rb | 44 ++++++ ...ware_with_worker_attribution_shared_examples.rb | 132 +++++++++++++++++ .../lib/gitlab/sql/set_operator_shared_examples.rb | 29 ++++ .../issuable_activity_shared_examples.rb | 4 - .../mailers/notify_shared_examples.rb | 2 +- .../active_record_subscriber_shared_examples.rb | 14 +- .../models/boards/listable_shared_examples.rb | 25 ++-- .../cluster_application_status_shared_examples.rb | 7 +- .../cluster_application_version_shared_examples.rb | 11 ++ .../models/clusters/prometheus_client_shared.rb | 86 +++++++++++ .../debian/architecture_shared_examples.rb | 4 +- .../packages/debian/component_shared_examples.rb | 4 +- .../debian/distribution_shared_examples.rb | 4 +- .../shared_examples/models/wiki_shared_examples.rb | 95 +++++++----- .../namespaces/namespace_traversal_examples.rb | 124 ++++++++++++++++ .../namespaces/recursive_traversal_examples.rb | 78 ---------- .../shared_examples/nav_sidebar_shared_examples.rb | 10 ++ .../resource_access_token_shared_examples.rb | 76 +++++++++- .../shared_examples/querying_shared_examples.rb | 23 +++ .../issuable/close_quick_action_shared_examples.rb | 2 +- .../requests/api/conan_packages_shared_examples.rb | 29 ++-- .../integrations_shared_examples.rb | 49 ++++++ .../logging_application_context_shared_examples.rb | 10 +- .../api/nuget_endpoints_shared_examples.rb | 4 +- .../requests/api/packages_shared_examples.rb | 13 +- .../api/rubygems_packages_shared_examples.rb | 13 ++ .../integrations_controller_shared_examples.rb | 46 ++++++ .../environment_serializer_shared_examples.rb | 29 ++++ .../lists_destroy_service_shared_examples.rb | 4 +- .../boards/lists_list_service_shared_examples.rb | 38 +++-- ...luster_applications_artifact_shared_examples.rb | 4 +- .../groups_count_service_shared_examples.rb | 55 +++++++ .../issuable/destroy_service_shared_examples.rb | 31 ++++ .../services/merge_request_shared_examples.rb | 90 +++++++++++ .../notification_service_shared_examples.rb | 2 +- .../services/snippets_shared_examples.rb | 41 ++++- .../in_product_marketing_email_shared_example.rb | 15 ++ .../worker_with_data_consistency_shared_example.rb | 27 ++++ 67 files changed, 1698 insertions(+), 317 deletions(-) create mode 100644 spec/support/shared_examples/boards/destroy_service_shared_examples.rb create mode 100644 spec/support/shared_examples/boards/lists/update_service_shared_examples.rb create mode 100644 spec/support/shared_examples/controllers/snippet_shared_examples.rb delete mode 100644 spec/support/shared_examples/controllers/trackable_shared_examples.rb create mode 100644 spec/support/shared_examples/features/cascading_settings_shared_examples.rb create mode 100644 spec/support/shared_examples/features/sidebar_shared_examples.rb create mode 100644 spec/support/shared_examples/graphql/spam_protection_shared_examples.rb create mode 100644 spec/support/shared_examples/helpers/groups_shared_examples.rb create mode 100644 spec/support/shared_examples/lib/gitlab/database/cte_materialized_shared_examples.rb create mode 100644 spec/support/shared_examples/lib/gitlab/sidekiq_middleware/metrics_middleware_with_worker_attribution_shared_examples.rb create mode 100644 spec/support/shared_examples/models/clusters/prometheus_client_shared.rb create mode 100644 spec/support/shared_examples/namespaces/namespace_traversal_examples.rb delete mode 100644 spec/support/shared_examples/namespaces/recursive_traversal_examples.rb create mode 100644 spec/support/shared_examples/querying_shared_examples.rb create mode 100644 spec/support/shared_examples/requests/api/graphql/projects/alert_management/integrations_shared_examples.rb create mode 100644 spec/support/shared_examples/requests/clusters/integrations_controller_shared_examples.rb create mode 100644 spec/support/shared_examples/serializers/environment_serializer_shared_examples.rb create mode 100644 spec/support/shared_examples/services/groups_count_service_shared_examples.rb create mode 100644 spec/support/shared_examples/services/issuable/destroy_service_shared_examples.rb create mode 100644 spec/support/shared_examples/workers/in_product_marketing_email_shared_example.rb create mode 100644 spec/support/shared_examples/workers/worker_with_data_consistency_shared_example.rb (limited to 'spec/support/shared_examples') diff --git a/spec/support/shared_examples/boards/destroy_service_shared_examples.rb b/spec/support/shared_examples/boards/destroy_service_shared_examples.rb new file mode 100644 index 00000000000..33bae3da44b --- /dev/null +++ b/spec/support/shared_examples/boards/destroy_service_shared_examples.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'board destroy service' do + describe '#execute' do + let(:parent_type) { parent.is_a?(Project) ? :project : :group } + let!(:board) { create(board_factory, parent_type => parent) } + + subject(:service) { described_class.new(parent, double) } + + context 'when there is more than one board' do + let!(:board2) { create(board_factory, parent_type => parent) } + + it 'destroys the board' do + create(board_factory, parent_type => parent) + + expect do + expect(service.execute(board)).to be_success + end.to change(boards, :count).by(-1) + end + end + + context 'when there is only one board' do + it 'does not remove board' do + expect do + expect(service.execute(board)).to be_error + end.not_to change(boards, :count) + end + end + end +end diff --git a/spec/support/shared_examples/boards/lists/update_service_shared_examples.rb b/spec/support/shared_examples/boards/lists/update_service_shared_examples.rb new file mode 100644 index 00000000000..d8a74f2582d --- /dev/null +++ b/spec/support/shared_examples/boards/lists/update_service_shared_examples.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'moving list' do + context 'when user can admin list' do + it 'calls Lists::MoveService to update list position' do + board.resource_parent.add_developer(user) + + expect_next_instance_of(Boards::Lists::MoveService, board.resource_parent, user, params) do |move_service| + expect(move_service).to receive(:execute).with(list).and_call_original + end + + service.execute(list) + end + end + + context 'when user cannot admin list' do + it 'does not call Lists::MoveService to update list position' do + expect(Boards::Lists::MoveService).not_to receive(:new) + + service.execute(list) + end + end +end + +RSpec.shared_examples 'updating list preferences' do + context 'when user can read list' do + it 'updates list preference for user' do + board.resource_parent.add_guest(user) + + service.execute(list) + + expect(list.preferences_for(user).collapsed).to eq(true) + end + end + + context 'when user cannot read list' do + it 'does not update list preference for user' do + service.execute(list) + + expect(list.preferences_for(user).collapsed).to be_falsy + end + end +end diff --git a/spec/support/shared_examples/controllers/snippet_blob_shared_examples.rb b/spec/support/shared_examples/controllers/snippet_blob_shared_examples.rb index 62aaec85162..c939c306d93 100644 --- a/spec/support/shared_examples/controllers/snippet_blob_shared_examples.rb +++ b/spec/support/shared_examples/controllers/snippet_blob_shared_examples.rb @@ -36,16 +36,6 @@ RSpec.shared_examples 'raw snippet blob' do expect(response.header['Content-Disposition']).to match "attachment; filename=\"#{filepath}\"" end - - context 'when the feature flag attachment_with_filename is disabled' do - it 'returns just attachment in the disposition header' do - stub_feature_flags(attachment_with_filename: false) - - subject - - expect(response.header['Content-Disposition']).to eq 'attachment' - end - end end end diff --git a/spec/support/shared_examples/controllers/snippet_shared_examples.rb b/spec/support/shared_examples/controllers/snippet_shared_examples.rb new file mode 100644 index 00000000000..f49cc979368 --- /dev/null +++ b/spec/support/shared_examples/controllers/snippet_shared_examples.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'snippets views' do + let(:params) { {} } + + before do + sign_in(user) + end + + context 'when rendered' do + render_views + + it 'avoids N+1 database queries' do + # Warming call to load everything non snippet related + get(:index, params: params) + + project = create(:project, namespace: user.namespace) + create(:project_snippet, project: project, author: user) + + control_count = ActiveRecord::QueryRecorder.new { get(:index, params: params) }.count + + project = create(:project, namespace: user.namespace) + create(:project_snippet, project: project, author: user) + + expect { get(:index, params: params) }.not_to exceed_query_limit(control_count) + end + end +end diff --git a/spec/support/shared_examples/controllers/trackable_shared_examples.rb b/spec/support/shared_examples/controllers/trackable_shared_examples.rb deleted file mode 100644 index dac7d8c94ff..00000000000 --- a/spec/support/shared_examples/controllers/trackable_shared_examples.rb +++ /dev/null @@ -1,39 +0,0 @@ -# frozen_string_literal: true - -RSpec.shared_examples 'a Trackable Controller' do - describe '#track_event', :snowplow do - before do - sign_in user - end - - context 'with no params' do - controller(described_class) do - def index - track_event - head :ok - end - end - - it 'tracks the action name', :snowplow do - get :index - - expect_snowplow_event(category: 'AnonymousController', action: 'index') - end - end - - context 'with params' do - controller(described_class) do - def index - track_event('some_event', category: 'SomeCategory', label: 'errorlabel') - head :ok - end - end - - it 'tracks with the specified param' do - get :index - - expect_snowplow_event(category: 'SomeCategory', action: 'some_event', label: 'errorlabel') - end - end - end -end diff --git a/spec/support/shared_examples/controllers/unique_visits_shared_examples.rb b/spec/support/shared_examples/controllers/unique_visits_shared_examples.rb index 428389a9a01..3f97c031e27 100644 --- a/spec/support/shared_examples/controllers/unique_visits_shared_examples.rb +++ b/spec/support/shared_examples/controllers/unique_visits_shared_examples.rb @@ -4,27 +4,30 @@ RSpec.shared_examples 'tracking unique visits' do |method| let(:request_params) { {} } it 'tracks unique visit if the format is HTML' do - expect_any_instance_of(Gitlab::Analytics::UniqueVisits).to receive(:track_visit).with(instance_of(String), target_id) + expect(Gitlab::UsageDataCounters::HLLRedisCounter) + .to receive(:track_event).with(target_id, values: kind_of(String)) get method, params: request_params, format: :html end it 'tracks unique visit if DNT is not enabled' do - expect_any_instance_of(Gitlab::Analytics::UniqueVisits).to receive(:track_visit).with(instance_of(String), target_id) + expect(Gitlab::UsageDataCounters::HLLRedisCounter) + .to receive(:track_event).with(target_id, values: kind_of(String)) + request.headers['DNT'] = '0' get method, params: request_params, format: :html end it 'does not track unique visit if DNT is enabled' do - expect_any_instance_of(Gitlab::Analytics::UniqueVisits).not_to receive(:track_visit) + expect(Gitlab::UsageDataCounters::HLLRedisCounter).not_to receive(:track_event) request.headers['DNT'] = '1' get method, params: request_params, format: :html end it 'does not track unique visit if the format is JSON' do - expect_any_instance_of(Gitlab::Analytics::UniqueVisits).not_to receive(:track_visit) + expect(Gitlab::UsageDataCounters::HLLRedisCounter).not_to receive(:track_event) get method, params: request_params, format: :json end diff --git a/spec/support/shared_examples/features/cascading_settings_shared_examples.rb b/spec/support/shared_examples/features/cascading_settings_shared_examples.rb new file mode 100644 index 00000000000..29ef3da9a85 --- /dev/null +++ b/spec/support/shared_examples/features/cascading_settings_shared_examples.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'a cascading setting' do + context 'when setting is enforced by an ancestor group' do + before do + visit group_path + + page.within form_group_selector do + find(setting_field_selector).check + find('[data-testid="enforce-for-all-subgroups-checkbox"]').check + end + + click_save_button + end + + it 'disables setting in subgroups' do + visit subgroup_path + + expect(find("#{setting_field_selector}[disabled]")).to be_checked + end + + it 'does not show enforcement checkbox in subgroups' do + visit subgroup_path + + expect(page).not_to have_selector '[data-testid="enforce-for-all-subgroups-checkbox"]' + end + + it 'displays lock icon with popover', :js do + visit subgroup_path + + page.within form_group_selector do + find('[data-testid="cascading-settings-lock-icon"]').click + end + + page.within '[data-testid="cascading-settings-lock-popover"]' do + expect(page).to have_text 'This setting has been enforced by an owner of Foo bar.' + expect(page).to have_link 'Foo bar', href: setting_path + end + end + end +end diff --git a/spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb b/spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb index da966fd2200..1c816ee4b0a 100644 --- a/spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb +++ b/spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb @@ -43,7 +43,7 @@ RSpec.shared_examples 'a creatable merge request' do expect(page.all('input[name="merge_request[label_ids][]"]', visible: false)[1].value).to match(label.id.to_s) expect(page.all('input[name="merge_request[label_ids][]"]', visible: false)[2].value).to match(label2.id.to_s) - click_button 'Submit merge request' + click_button 'Create merge request' page.within '.issuable-sidebar' do page.within '.assignee' do diff --git a/spec/support/shared_examples/features/discussion_comments_shared_example.rb b/spec/support/shared_examples/features/discussion_comments_shared_example.rb index 86ba2821c78..808e0be6be2 100644 --- a/spec/support/shared_examples/features/discussion_comments_shared_example.rb +++ b/spec/support/shared_examples/features/discussion_comments_shared_example.rb @@ -304,7 +304,7 @@ RSpec.shared_examples 'thread comments for issue, epic and merge request' do |re let(:reply_id) { find("#{comments_selector} .note:last-of-type", match: :first)['data-note-id'] } it 'can be replied to after resolving' do - click_button "Resolve thread" + find('button[data-qa-selector="resolve_discussion_button"]').click wait_for_requests refresh @@ -316,7 +316,7 @@ RSpec.shared_examples 'thread comments for issue, epic and merge request' do |re it 'shows resolved thread when toggled' do submit_reply('a') - click_button "Resolve thread" + find('button[data-qa-selector="resolve_discussion_button"]').click wait_for_requests expect(page).to have_selector(".note-row-#{note_id}", visible: true) diff --git a/spec/support/shared_examples/features/error_tracking_shared_example.rb b/spec/support/shared_examples/features/error_tracking_shared_example.rb index 92fc54ce0b0..1bdc5355408 100644 --- a/spec/support/shared_examples/features/error_tracking_shared_example.rb +++ b/spec/support/shared_examples/features/error_tracking_shared_example.rb @@ -2,7 +2,7 @@ RSpec.shared_examples 'error tracking index page' do it 'renders the error index page', quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/217810' } do - within('div.js-title-container') do + within('[data-testid="breadcrumb-links"]') do expect(page).to have_content(project.namespace.name) expect(page).to have_content(project.name) end diff --git a/spec/support/shared_examples/features/issuable_invite_members_shared_examples.rb b/spec/support/shared_examples/features/issuable_invite_members_shared_examples.rb index 7a32f61d4fa..49c3674277d 100644 --- a/spec/support/shared_examples/features/issuable_invite_members_shared_examples.rb +++ b/spec/support/shared_examples/features/issuable_invite_members_shared_examples.rb @@ -2,7 +2,7 @@ RSpec.shared_examples 'issuable invite members experiments' do context 'when a privileged user can invite' do - it 'shows a link for inviting members and follows through to the members page' do + it 'shows a link for inviting members and launches invite modal' do project.add_maintainer(user) visit issuable_path @@ -11,14 +11,14 @@ RSpec.shared_examples 'issuable invite members experiments' do wait_for_requests page.within '.dropdown-menu-user' do - expect(page).to have_link('Invite Members', href: project_project_members_path(project)) + expect(page).to have_link('Invite Members') expect(page).to have_selector('[data-track-event="click_invite_members"]') expect(page).to have_selector('[data-track-label="edit_assignee"]') end click_link 'Invite Members' - expect(current_path).to eq project_project_members_path(project) + expect(page).to have_content("You're inviting members to the") end end diff --git a/spec/support/shared_examples/features/project_upload_files_shared_examples.rb b/spec/support/shared_examples/features/project_upload_files_shared_examples.rb index 00d3bd08218..7adf303bde4 100644 --- a/spec/support/shared_examples/features/project_upload_files_shared_examples.rb +++ b/spec/support/shared_examples/features/project_upload_files_shared_examples.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true -RSpec.shared_examples 'it uploads and commit a new text file' do - it 'uploads and commit a new text file', :js do +RSpec.shared_examples 'it uploads and commits a new text file' do + it 'uploads and commits a new text file', :js do find('.add-to-tree').click page.within('.dropdown-menu') do @@ -10,7 +10,7 @@ RSpec.shared_examples 'it uploads and commit a new text file' do wait_for_requests end - drop_in_dropzone(File.join(Rails.root, 'spec', 'fixtures', 'doc_sample.txt')) + attach_file('upload_file', File.join(Rails.root, 'spec', 'fixtures', 'doc_sample.txt'), make_visible: true) page.within('#modal-upload-blob') do fill_in(:commit_message, with: 'New commit message') @@ -32,8 +32,8 @@ RSpec.shared_examples 'it uploads and commit a new text file' do end end -RSpec.shared_examples 'it uploads and commit a new image file' do - it 'uploads and commit a new image file', :js do +RSpec.shared_examples 'it uploads and commits a new image file' do + it 'uploads and commits a new image file', :js do find('.add-to-tree').click page.within('.dropdown-menu') do @@ -42,7 +42,7 @@ RSpec.shared_examples 'it uploads and commit a new image file' do wait_for_requests end - drop_in_dropzone(File.join(Rails.root, 'spec', 'fixtures', 'logo_sample.svg')) + attach_file('upload_file', File.join(Rails.root, 'spec', 'fixtures', 'logo_sample.svg'), make_visible: true) page.within('#modal-upload-blob') do fill_in(:commit_message, with: 'New commit message') @@ -58,21 +58,49 @@ RSpec.shared_examples 'it uploads and commit a new image file' do end end -RSpec.shared_examples 'it uploads and commit a new file to a forked project' do +RSpec.shared_examples 'it uploads and commits a new pdf file' do + it 'uploads and commits a new pdf file', :js do + find('.add-to-tree').click + + page.within('.dropdown-menu') do + click_link('Upload file') + + wait_for_requests + end + + attach_file('upload_file', File.join(Rails.root, 'spec', 'fixtures', 'git-cheat-sheet.pdf'), make_visible: true) + + page.within('#modal-upload-blob') do + fill_in(:commit_message, with: 'New commit message') + fill_in(:branch_name, with: 'upload_image', visible: true) + click_button('Upload file') + end + + wait_for_all_requests + + visit(project_blob_path(project, 'upload_image/git-cheat-sheet.pdf')) + + expect(page).to have_css('.js-pdf-viewer') + end +end + +RSpec.shared_examples 'it uploads and commits a new file to a forked project' do let(:fork_message) do "You're not allowed to make changes to this project directly. "\ "A fork of this project has been created that you can make changes in, so you can submit a merge request." end - it 'uploads and commit a new file to a forked project', :js, :sidekiq_might_not_need_inline do + it 'uploads and commits a new file to a forked project', :js, :sidekiq_might_not_need_inline do find('.add-to-tree').click click_link('Upload file') expect(page).to have_content(fork_message) + wait_for_all_requests + find('.add-to-tree').click click_link('Upload file') - drop_in_dropzone(File.join(Rails.root, 'spec', 'fixtures', 'doc_sample.txt')) + attach_file('upload_file', File.join(Rails.root, 'spec', 'fixtures', 'doc_sample.txt'), make_visible: true) page.within('#modal-upload-blob') do fill_in(:commit_message, with: 'New commit message') @@ -95,6 +123,33 @@ RSpec.shared_examples 'it uploads and commit a new file to a forked project' do end end +RSpec.shared_examples 'it uploads a file to a sub-directory' do + it 'uploads a file to a sub-directory', :js do + click_link 'files' + + page.within('.repo-breadcrumb') do + expect(page).to have_content('files') + end + + find('.add-to-tree').click + click_link('Upload file') + attach_file('upload_file', File.join(Rails.root, 'spec', 'fixtures', 'doc_sample.txt'), make_visible: true) + + page.within('#modal-upload-blob') do + fill_in(:commit_message, with: 'New commit message') + end + + click_button('Upload file') + + expect(page).to have_content('New commit message') + + page.within('.repo-breadcrumb') do + expect(page).to have_content('files') + expect(page).to have_content('doc_sample.txt') + end + end +end + RSpec.shared_examples 'uploads and commits a new text file via "upload file" button' do it 'uploads and commits a new text file via "upload file" button', :js do find('[data-testid="upload-file-button"]').click diff --git a/spec/support/shared_examples/features/resolving_discussions_in_issues_shared_examples.rb b/spec/support/shared_examples/features/resolving_discussions_in_issues_shared_examples.rb index 06127f2ed8c..6d44a6fde85 100644 --- a/spec/support/shared_examples/features/resolving_discussions_in_issues_shared_examples.rb +++ b/spec/support/shared_examples/features/resolving_discussions_in_issues_shared_examples.rb @@ -14,11 +14,11 @@ RSpec.shared_examples 'creating an issue for a thread' do end it 'can create a new issue for the project' do - expect { click_button 'Submit issue' }.to change { project.issues.reload.size }.by(1) + expect { click_button 'Create issue' }.to change { project.issues.reload.size }.by(1) end it 'resolves the discussion in the merge request' do - click_button 'Submit issue' + click_button 'Create issue' discussion.first_note.reload @@ -26,7 +26,7 @@ RSpec.shared_examples 'creating an issue for a thread' do end it 'shows a flash messaage after resolving a discussion' do - click_button 'Submit issue' + click_button 'Create issue' page.within '.flash-notice' do # Only check for the word 'Resolved' since the spec might have resolved diff --git a/spec/support/shared_examples/features/search_settings_shared_examples.rb b/spec/support/shared_examples/features/search_settings_shared_examples.rb index 6a507c4be56..dda780690b2 100644 --- a/spec/support/shared_examples/features/search_settings_shared_examples.rb +++ b/spec/support/shared_examples/features/search_settings_shared_examples.rb @@ -7,9 +7,7 @@ RSpec.shared_examples 'cannot search settings' do end RSpec.shared_examples 'can search settings' do |search_term, non_match_section| - it 'has search settings field' do - expect(page).to have_field(placeholder: SearchHelpers::INPUT_PLACEHOLDER) - end + it_behaves_like 'can highlight results', search_term it 'hides unmatching sections on search' do expect(page).to have_content(non_match_section) @@ -21,22 +19,19 @@ RSpec.shared_examples 'can search settings' do |search_term, non_match_section| end end -RSpec.shared_examples 'can search settings with feature flag check' do |search_term, non_match_section| - let(:flag) { true } - - before do - stub_feature_flags(search_settings_in_page: flag) - - visit(visit_path) +RSpec.shared_examples 'can highlight results' do |search_term| + it 'has search settings field' do + expect(page).to have_field(placeholder: SearchHelpers::INPUT_PLACEHOLDER) end - context 'with feature flag on' do - it_behaves_like 'can search settings', search_term, non_match_section - end + it 'highlights the search terms' do + selector = '.gl-bg-orange-100' + fill_in SearchHelpers::INPUT_PLACEHOLDER, with: search_term - context 'with feature flag off' do - let(:flag) { false } + expect(page).to have_css(selector) - it_behaves_like 'cannot search settings' + page.find_all(selector) do |element| + expect(element).to have_content(search_term) + end end end diff --git a/spec/support/shared_examples/features/sidebar_shared_examples.rb b/spec/support/shared_examples/features/sidebar_shared_examples.rb new file mode 100644 index 00000000000..429efbe6ba0 --- /dev/null +++ b/spec/support/shared_examples/features/sidebar_shared_examples.rb @@ -0,0 +1,165 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'issue boards sidebar' do + include MobileHelpers + + before do + first_card.click + end + + it 'shows sidebar when clicking issue' do + expect(page).to have_selector('[data-testid="issue-boards-sidebar"]') + end + + it 'closes sidebar when clicking issue' do + expect(page).to have_selector('[data-testid="issue-boards-sidebar"]') + + first_card.click + + expect(page).not_to have_selector('[data-testid="issue-boards-sidebar"]') + end + + it 'shows issue details when sidebar is open', :aggregate_failures do + page.within('[data-testid="issue-boards-sidebar"]') do + expect(page).to have_content(issue.title) + expect(page).to have_content(issue.to_reference) + end + end + + context 'when clicking close button' do + before do + find('[data-testid="issue-boards-sidebar"] .gl-drawer-close-button').click + end + + it 'unhighlights the active issue card' do + expect(first_card[:class]).not_to include('is-active') + expect(first_card[:class]).not_to include('multi-select') + end + + it 'closes sidebar when clicking close button' do + expect(page).not_to have_selector('[data-testid="issue-boards-sidebar"]') + end + end + + context 'in notifications subscription' do + it 'displays notifications toggle', :aggregate_failures do + page.within('[data-testid="sidebar-notifications"]') do + expect(page).to have_selector('[data-testid="notification-subscribe-toggle"]') + expect(page).to have_content('Notifications') + expect(page).not_to have_content('Notifications have been disabled by the project or group owner') + end + end + + it 'shows toggle as on then as off as user toggles to subscribe and unsubscribe', :aggregate_failures do + toggle = find('[data-testid="notification-subscribe-toggle"]') + + toggle.click + + expect(toggle).to have_css("button.is-checked") + + toggle.click + + expect(toggle).not_to have_css("button.is-checked") + end + + context 'when notifications have been disabled' do + before do + project.update_attribute(:emails_disabled, true) + + refresh_and_click_first_card + end + + it 'displays a message that notifications have been disabled' do + page.within('[data-testid="sidebar-notifications"]') do + expect(page).not_to have_selector('[data-testid="notification-subscribe-toggle"]') + expect(page).to have_content('Notifications have been disabled by the project or group owner') + end + end + end + end + + context 'in time tracking' do + it 'displays time tracking feature with default message' do + page.within('[data-testid="time-tracker"]') do + expect(page).to have_content('Time tracking') + expect(page).to have_content('No estimate or time spent') + end + end + + context 'when only spent time is recorded' do + before do + issue.timelogs.create!(time_spent: 3600, user: user) + + refresh_and_click_first_card + end + + it 'shows the total time spent only' do + page.within('[data-testid="time-tracker"]') do + expect(page).to have_content('Spent: 1h') + expect(page).not_to have_content('Estimated') + end + end + end + + context 'when only estimated time is recorded' do + before do + issue.update!(time_estimate: 3600) + + refresh_and_click_first_card + end + + it 'shows the estimated time only', :aggregate_failures do + page.within('[data-testid="time-tracker"]') do + expect(page).to have_content('Estimated: 1h') + expect(page).not_to have_content('Spent') + end + end + end + + context 'when estimated and spent times are available' do + before do + issue.timelogs.create!(time_spent: 1800, user: user) + issue.update!(time_estimate: 3600) + + refresh_and_click_first_card + end + + it 'shows time tracking progress bar' do + page.within('[data-testid="time-tracker"]') do + expect(page).to have_selector('[data-testid="timeTrackingComparisonPane"]') + end + end + + it 'shows both estimated and spent time text', :aggregate_failures do + page.within('[data-testid="time-tracker"]') do + expect(page).to have_content('Spent 30m') + expect(page).to have_content('Est 1h') + end + end + end + + context 'when limitedToHours instance option is turned on' do + before do + # 3600+3600*24 = 1d 1h or 25h + issue.timelogs.create!(time_spent: 3600 + 3600 * 24, user: user) + stub_application_setting(time_tracking_limit_to_hours: true) + + refresh_and_click_first_card + end + + it 'shows the total time spent only' do + page.within('[data-testid="time-tracker"]') do + expect(page).to have_content('Spent: 25h') + end + end + end + end + + def refresh_and_click_first_card + page.refresh + + wait_for_requests + + first_card.click + end +end diff --git a/spec/support/shared_examples/features/wiki/user_creates_wiki_page_shared_examples.rb b/spec/support/shared_examples/features/wiki/user_creates_wiki_page_shared_examples.rb index 2f8ebd0d264..8a6d5d88ca6 100644 --- a/spec/support/shared_examples/features/wiki/user_creates_wiki_page_shared_examples.rb +++ b/spec/support/shared_examples/features/wiki/user_creates_wiki_page_shared_examples.rb @@ -240,7 +240,7 @@ RSpec.shared_examples 'User creates wiki page' do end end - it "shows the emoji autocompletion dropdown" do + it "shows the emoji autocompletion dropdown", :js do click_link("New page") page.within(".wiki-form") do diff --git a/spec/support/shared_examples/features/wiki/user_git_access_wiki_page_shared_examples.rb b/spec/support/shared_examples/features/wiki/user_git_access_wiki_page_shared_examples.rb index d3d2a36147d..4fea450bd64 100644 --- a/spec/support/shared_examples/features/wiki/user_git_access_wiki_page_shared_examples.rb +++ b/spec/support/shared_examples/features/wiki/user_git_access_wiki_page_shared_examples.rb @@ -13,7 +13,7 @@ RSpec.shared_examples 'User views Git access wiki page' do expect(page).to have_text("Clone repository #{wiki.full_path}") - within('.git-clone-holder') do + within('.js-git-clone-holder') do expect(page).to have_css('#clone-dropdown', text: 'HTTP') expect(page).to have_field('clone_url', with: wiki.http_url_to_repo) diff --git a/spec/support/shared_examples/features/wiki/user_previews_wiki_changes_shared_examples.rb b/spec/support/shared_examples/features/wiki/user_previews_wiki_changes_shared_examples.rb index a22d98f20c4..1a981f42086 100644 --- a/spec/support/shared_examples/features/wiki/user_previews_wiki_changes_shared_examples.rb +++ b/spec/support/shared_examples/features/wiki/user_previews_wiki_changes_shared_examples.rb @@ -38,19 +38,19 @@ RSpec.shared_examples 'User previews wiki changes' do end end - context "when there are no spaces or hyphens in the page name" do + context "when there are no spaces or hyphens in the page name", :js do let(:wiki_page) { build(:wiki_page, wiki: wiki, title: 'a/b/c/d', content: page_content) } it_behaves_like 'rewrites relative links' end - context "when there are spaces in the page name" do + context "when there are spaces in the page name", :js do let(:wiki_page) { build(:wiki_page, wiki: wiki, title: 'a page/b page/c page/d page', content: page_content) } it_behaves_like 'rewrites relative links' end - context "when there are hyphens in the page name" do + context "when there are hyphens in the page name", :js do let(:wiki_page) { build(:wiki_page, wiki: wiki, title: 'a-page/b-page/c-page/d-page', content: page_content) } it_behaves_like 'rewrites relative links' diff --git a/spec/support/shared_examples/features/wiki/user_updates_wiki_page_shared_examples.rb b/spec/support/shared_examples/features/wiki/user_updates_wiki_page_shared_examples.rb index 1e325535e81..d185e9dd81c 100644 --- a/spec/support/shared_examples/features/wiki/user_updates_wiki_page_shared_examples.rb +++ b/spec/support/shared_examples/features/wiki/user_updates_wiki_page_shared_examples.rb @@ -11,7 +11,7 @@ RSpec.shared_examples 'User updates wiki page' do sign_in(user) end - context 'when wiki is empty' do + context 'when wiki is empty', :js do before do |example| visit(wiki_path(wiki)) @@ -57,7 +57,7 @@ RSpec.shared_examples 'User updates wiki page' do it_behaves_like 'wiki file attachments' end - context 'when wiki is not empty' do + context 'when wiki is not empty', :js do let!(:wiki_page) { create(:wiki_page, wiki: wiki, title: 'home', content: 'Home page') } before do @@ -147,7 +147,7 @@ RSpec.shared_examples 'User updates wiki page' do it_behaves_like 'wiki file attachments' end - context 'when the page is in a subdir' do + context 'when the page is in a subdir', :js do let(:page_name) { 'page_name' } let(:page_dir) { "foo/bar/#{page_name}" } let!(:wiki_page) { create(:wiki_page, wiki: wiki, title: page_dir, content: 'Home page') } diff --git a/spec/support/shared_examples/features/wiki/user_views_wiki_empty_shared_examples.rb b/spec/support/shared_examples/features/wiki/user_views_wiki_empty_shared_examples.rb index 14180d503df..3514ce286d6 100644 --- a/spec/support/shared_examples/features/wiki/user_views_wiki_empty_shared_examples.rb +++ b/spec/support/shared_examples/features/wiki/user_views_wiki_empty_shared_examples.rb @@ -20,11 +20,11 @@ RSpec.shared_examples 'User views empty wiki' do end end - shared_examples 'empty wiki message' do |writable: false, issuable: false, confluence: false| + shared_examples 'empty wiki message' do |writable: false, issuable: false, confluence: false, expect_button: true| # This mirrors the logic in: # - app/views/shared/empty_states/_wikis.html.haml # - WikiHelper#wiki_empty_state_messages - it 'shows the empty state message with the expected elements' do + it 'shows the empty state message with the expected elements', :js do visit wiki_path(wiki) if writable @@ -37,7 +37,7 @@ RSpec.shared_examples 'User views empty wiki' do if issuable && !writable expect(element).to have_content("improve the wiki for this #{container_name}") expect(element).to have_link("issue tracker", href: project_issues_path(project)) - expect(element).to have_link("Suggest wiki improvement", href: new_project_issue_path(project)) + expect(element.has_link?("Suggest wiki improvement", href: new_project_issue_path(project))).to be(expect_button) else expect(element).not_to have_content("improve the wiki for this #{container_name}") expect(element).not_to have_link("issue tracker") diff --git a/spec/support/shared_examples/graphql/mutations/can_mutate_spammable_examples.rb b/spec/support/shared_examples/graphql/mutations/can_mutate_spammable_examples.rb index bb4270d7db6..fc795012ce7 100644 --- a/spec/support/shared_examples/graphql/mutations/can_mutate_spammable_examples.rb +++ b/spec/support/shared_examples/graphql/mutations/can_mutate_spammable_examples.rb @@ -21,13 +21,13 @@ RSpec.shared_examples 'a mutation which can mutate a spammable' do end end - describe "#with_spam_action_response_fields" do + describe "#spam_action_response_fields" do it 'resolves with spam action fields' do subject # NOTE: We do not need to assert on the specific values of spam action fields here, we only need - # to verify that #with_spam_action_response_fields was invoked and that the fields are present in the - # response. The specific behavior of #with_spam_action_response_fields is covered in the + # to verify that #spam_action_response_fields was invoked and that the fields are present in the + # response. The specific behavior of #spam_action_response_fields is covered in the # HasSpamActionResponseFields unit tests. expect(mutation_response.keys) .to include('spam', 'spamLogId', 'needsCaptchaResponse', 'captchaSiteKey') diff --git a/spec/support/shared_examples/graphql/mutations/set_assignees_shared_examples.rb b/spec/support/shared_examples/graphql/mutations/set_assignees_shared_examples.rb index cfa12171b7e..022e2308517 100644 --- a/spec/support/shared_examples/graphql/mutations/set_assignees_shared_examples.rb +++ b/spec/support/shared_examples/graphql/mutations/set_assignees_shared_examples.rb @@ -10,22 +10,40 @@ RSpec.shared_examples 'an assignable resource' do describe '#resolve' do let_it_be(:assignee) { create(:user) } let_it_be(:assignee2) { create(:user) } + let(:assignee_usernames) { [assignee.username] } let(:mutated_resource) { subject[resource.class.name.underscore.to_sym] } + let(:mode) { described_class.arguments['operationMode'].default_value } - subject { mutation.resolve(project_path: resource.project.full_path, iid: resource.iid, assignee_usernames: assignee_usernames) } - - before do - resource.project.add_developer(assignee) - resource.project.add_developer(assignee2) + subject do + mutation.resolve(project_path: resource.project.full_path, + iid: resource.iid, + operation_mode: mode, + assignee_usernames: assignee_usernames) end it 'raises an error if the resource is not accessible to the user' do expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) end + it 'does not change assignees if the resource is not accessible to the assignees' do + resource.project.add_developer(user) + + expect { subject }.not_to change { resource.reload.assignee_ids } + end + + it 'returns an operational error if the resource is not accessible to the assignees' do + resource.project.add_developer(user) + + result = subject + + expect(result[:errors]).to include a_string_matching(/Cannot assign/) + end + context 'when the user can update the resource' do before do + resource.project.add_developer(assignee) + resource.project.add_developer(assignee2) resource.project.add_developer(user) end diff --git a/spec/support/shared_examples/graphql/sorted_paginated_query_shared_examples.rb b/spec/support/shared_examples/graphql/sorted_paginated_query_shared_examples.rb index f78ea364147..eaeb5faee3b 100644 --- a/spec/support/shared_examples/graphql/sorted_paginated_query_shared_examples.rb +++ b/spec/support/shared_examples/graphql/sorted_paginated_query_shared_examples.rb @@ -44,7 +44,7 @@ # end # end # -RSpec.shared_examples 'sorted paginated query' do +RSpec.shared_examples 'sorted paginated query' do |conditions = {}| # Provided as a convenience when constructing queries using string concatenation let(:page_info) { 'pageInfo { startCursor endCursor }' } # Convenience for using default implementation of pagination_results_data @@ -123,6 +123,16 @@ RSpec.shared_examples 'sorted paginated query' do expect(results).to eq first_page end end + + context 'when last and sort params are present', if: conditions[:is_reversible] do + let(:params) { sort_argument.merge(last: 1) } + + it 'fetches last elements without error' do + post_graphql(pagination_query(params), current_user: current_user) + + expect(results.first).to eq(expected_results.last) + end + end end end end diff --git a/spec/support/shared_examples/graphql/spam_protection_shared_examples.rb b/spec/support/shared_examples/graphql/spam_protection_shared_examples.rb new file mode 100644 index 00000000000..8fb89a4f80e --- /dev/null +++ b/spec/support/shared_examples/graphql/spam_protection_shared_examples.rb @@ -0,0 +1,85 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.shared_examples 'has spam protection' do + include AfterNextHelpers + + describe '#check_spam_action_response!' do + let(:variables) { nil } + let(:headers) { {} } + let(:spam_log_id) { 123 } + let(:captcha_site_key) { 'abc123' } + + def send_request + post_graphql_mutation(mutation, current_user: current_user) + end + + before do + allow_next(mutation_class).to receive(:spam_action_response_fields).and_return( + spam: spam, + needs_captcha_response: render_captcha, + spam_log_id: spam_log_id, + captcha_site_key: captcha_site_key + ) + end + + context 'when the object is spam (DISALLOW)' do + shared_examples 'disallow response' do + it 'informs the client that the request was denied as spam' do + send_request + + expect(graphql_errors) + .to contain_exactly a_hash_including('message' => ::Mutations::SpamProtection::SPAM_DISALLOWED_MESSAGE) + expect(graphql_errors) + .to contain_exactly a_hash_including('extensions' => { "spam" => true }) + end + end + + let(:spam) { true } + + context 'and no CAPTCHA is available' do + let(:render_captcha) { false } + + it_behaves_like 'disallow response' + end + + context 'and a CAPTCHA is required' do + let(:render_captcha) { true } + + it_behaves_like 'disallow response' + end + end + + context 'when the object is not spam (CONDITIONAL ALLOW)' do + let(:spam) { false } + + context 'and no CAPTCHA is required' do + let(:render_captcha) { false } + + it 'does not return a to-level error' do + send_request + + expect(graphql_errors).to be_blank + end + end + + context 'and a CAPTCHA is required' do + let(:render_captcha) { true } + + it 'informs the client that the request may be retried after solving the CAPTCHA' do + send_request + + expect(graphql_errors) + .to contain_exactly a_hash_including('message' => ::Mutations::SpamProtection::NEEDS_CAPTCHA_RESPONSE_MESSAGE) + expect(graphql_errors) + .to contain_exactly a_hash_including('extensions' => { + "captcha_site_key" => captcha_site_key, + "needs_captcha_response" => true, + "spam_log_id" => spam_log_id + }) + end + end + end + end +end diff --git a/spec/support/shared_examples/graphql/types/gitlab_style_deprecations_shared_examples.rb b/spec/support/shared_examples/graphql/types/gitlab_style_deprecations_shared_examples.rb index bc091a678e2..efb2c466f70 100644 --- a/spec/support/shared_examples/graphql/types/gitlab_style_deprecations_shared_examples.rb +++ b/spec/support/shared_examples/graphql/types/gitlab_style_deprecations_shared_examples.rb @@ -13,18 +13,18 @@ RSpec.shared_examples 'Gitlab-style deprecations' do it 'raises an error if a required property is missing', :aggregate_failures do expect { subject(deprecated: { milestone: '1.10' }) }.to raise_error( ArgumentError, - 'Please provide a `reason` within `deprecated`' + include("Reason can't be blank") ) expect { subject(deprecated: { reason: 'Deprecation reason' }) }.to raise_error( ArgumentError, - 'Please provide a `milestone` within `deprecated`' + include("Milestone can't be blank") ) end it 'raises an error if milestone is not a String', :aggregate_failures do expect { subject(deprecated: { milestone: 1.10, reason: 'Deprecation reason' }) }.to raise_error( ArgumentError, - '`milestone` must be a `String`' + include("Milestone must be a string") ) end end @@ -49,4 +49,22 @@ RSpec.shared_examples 'Gitlab-style deprecations' do expect(deprecable.description).to be_nil end + + it 'adds information about the replacement if provided' do + deprecable = subject(deprecated: { milestone: '1.10', reason: :renamed, replacement: 'Foo.bar' }) + + expect(deprecable.deprecation_reason).to include 'Please use `Foo.bar`' + end + + it 'supports named reasons: renamed' do + deprecable = subject(deprecated: { milestone: '1.10', reason: :renamed }) + + expect(deprecable.deprecation_reason).to include 'This was renamed.' + end + + it 'supports named reasons: discouraged' do + deprecable = subject(deprecated: { milestone: '1.10', reason: :discouraged }) + + expect(deprecable.deprecation_reason).to include 'Use of this is not recommended.' + end end diff --git a/spec/support/shared_examples/helpers/groups_shared_examples.rb b/spec/support/shared_examples/helpers/groups_shared_examples.rb new file mode 100644 index 00000000000..9c74d25b31f --- /dev/null +++ b/spec/support/shared_examples/helpers/groups_shared_examples.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +# This shared_example requires the following variables: +# - current_user +# - group +# - type, the issuable type (ie :issues, :merge_requests) +# - count_service, the Service used by the specified issuable type + +RSpec.shared_examples 'cached issuables count' do + subject { helper.cached_issuables_count(group, type: type) } + + before do + allow(helper).to receive(:current_user) { current_user } + allow(count_service).to receive(:new).and_call_original + end + + it 'calls the correct service class' do + subject + expect(count_service).to have_received(:new).with(group, current_user) + end + + it 'returns all digits for count value under 1000' do + allow_next_instance_of(count_service) do |service| + allow(service).to receive(:count).and_return(999) + end + + expect(subject).to eq('999') + end + + it 'returns truncated digits for count value over 1000' do + allow_next_instance_of(count_service) do |service| + allow(service).to receive(:count).and_return(2300) + end + + expect(subject).to eq('2.3k') + end + + it 'returns truncated digits for count value over 10000' do + allow_next_instance_of(count_service) do |service| + allow(service).to receive(:count).and_return(12560) + end + + expect(subject).to eq('12.6k') + end + + it 'returns truncated digits for count value over 100000' do + allow_next_instance_of(count_service) do |service| + allow(service).to receive(:count).and_return(112560) + end + + expect(subject).to eq('112.6k') + end +end diff --git a/spec/support/shared_examples/lib/api/ci/runner_shared_examples.rb b/spec/support/shared_examples/lib/api/ci/runner_shared_examples.rb index bdb0316bf5a..d5ebda28f0a 100644 --- a/spec/support/shared_examples/lib/api/ci/runner_shared_examples.rb +++ b/spec/support/shared_examples/lib/api/ci/runner_shared_examples.rb @@ -8,7 +8,7 @@ RSpec.shared_examples 'API::CI::Runner application context metadata' do |api_rou send_request - Labkit::Context.with_context do |context| + Gitlab::ApplicationContext.with_raw_context do |context| expected_context = { 'meta.caller_id' => api_route, 'meta.user' => job.user.username, diff --git a/spec/support/shared_examples/lib/api/internal_base_shared_examples.rb b/spec/support/shared_examples/lib/api/internal_base_shared_examples.rb index dfa1388e0bb..ef08537dfe9 100644 --- a/spec/support/shared_examples/lib/api/internal_base_shared_examples.rb +++ b/spec/support/shared_examples/lib/api/internal_base_shared_examples.rb @@ -1,17 +1,6 @@ # frozen_string_literal: true RSpec.shared_examples 'actor key validations' do - context 'key id is not provided' do - let(:key_id) { nil } - - it 'returns an error message' do - subject - - expect(json_response['success']).to be_falsey - expect(json_response['message']).to eq('Could not find a user without a key') - end - end - context 'key does not exist' do let(:key_id) { non_existing_record_id } diff --git a/spec/support/shared_examples/lib/gitlab/ci/ci_trace_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/ci/ci_trace_shared_examples.rb index 0df1af3b10a..9c95d1ff9d9 100644 --- a/spec/support/shared_examples/lib/gitlab/ci/ci_trace_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/ci/ci_trace_shared_examples.rb @@ -843,6 +843,17 @@ RSpec.shared_examples 'trace with enabled live trace feature' do expect { subject }.to raise_error(Gitlab::Ci::Trace::AlreadyArchivedError) expect(build.job_artifacts_trace.file.exists?).to be_truthy end + + context 'when live trace chunks still exist' do + before do + create(:ci_build_trace_chunk, build: build) + end + + it 'removes the traces' do + expect { subject }.to raise_error(Gitlab::Ci::Trace::AlreadyArchivedError) + expect(build.trace_chunks).to be_empty + end + end end context 'when job is not finished yet' do diff --git a/spec/support/shared_examples/lib/gitlab/database/cte_materialized_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/database/cte_materialized_shared_examples.rb new file mode 100644 index 00000000000..88e6ffd15a8 --- /dev/null +++ b/spec/support/shared_examples/lib/gitlab/database/cte_materialized_shared_examples.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'CTE with MATERIALIZED keyword examples' do + describe 'adding MATERIALIZE to the CTE' do + let(:options) { {} } + + before do + # Clear the cached value before the test + Gitlab::Database::AsWithMaterialized.clear_memoization(:materialized_supported) + end + + context 'when PG version is <12' do + it 'does not add MATERIALIZE keyword' do + allow(Gitlab::Database).to receive(:version).and_return('11.1') + + expect(query).to include(expected_query_block_without_materialized) + end + end + + context 'when PG version is >=12' do + it 'adds MATERIALIZE keyword' do + allow(Gitlab::Database).to receive(:version).and_return('12.1') + + expect(query).to include(expected_query_block_with_materialized) + end + + context 'when version is higher than 12' do + it 'adds MATERIALIZE keyword' do + allow(Gitlab::Database).to receive(:version).and_return('15.1') + + expect(query).to include(expected_query_block_with_materialized) + end + end + + context 'when materialized is disabled' do + let(:options) { { materialized: false } } + + it 'does not add MATERIALIZE keyword' do + expect(query).to include(expected_query_block_without_materialized) + end + end + end + end +end diff --git a/spec/support/shared_examples/lib/gitlab/sidekiq_middleware/metrics_middleware_with_worker_attribution_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/sidekiq_middleware/metrics_middleware_with_worker_attribution_shared_examples.rb new file mode 100644 index 00000000000..48dc47e8e9b --- /dev/null +++ b/spec/support/shared_examples/lib/gitlab/sidekiq_middleware/metrics_middleware_with_worker_attribution_shared_examples.rb @@ -0,0 +1,132 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'metrics middleware with worker attribution' do + subject { described_class.new } + + let(:queue) { :test } + let(:worker_class) { worker.class } + let(:job) { {} } + let(:default_labels) do + { queue: queue.to_s, + worker: worker_class.to_s, + boundary: "", + external_dependencies: "no", + feature_category: "", + urgency: "low" } + end + + context "when workers are not attributed" do + before do + stub_const('TestNonAttributedWorker', Class.new) + TestNonAttributedWorker.class_eval do + include Sidekiq::Worker + end + end + + it_behaves_like "a metrics middleware" do + let(:worker) { TestNonAttributedWorker.new } + let(:labels) { default_labels.merge(urgency: "") } + end + end + + context "when a worker is wrapped into ActiveJob" do + before do + stub_const('TestWrappedWorker', Class.new) + TestWrappedWorker.class_eval do + include Sidekiq::Worker + end + end + + it_behaves_like "a metrics middleware" do + let(:job) do + { + "class" => ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper, + "wrapped" => TestWrappedWorker + } + end + + let(:worker) { TestWrappedWorker.new } + let(:labels) { default_labels.merge(urgency: "") } + end + end + + context "when workers are attributed" do + def create_attributed_worker_class(urgency, external_dependencies, resource_boundary, category) + klass = Class.new do + include Sidekiq::Worker + include WorkerAttributes + + urgency urgency if urgency + worker_has_external_dependencies! if external_dependencies + worker_resource_boundary resource_boundary unless resource_boundary == :unknown + feature_category category unless category.nil? + end + stub_const("TestAttributedWorker", klass) + end + + let(:urgency) { nil } + let(:external_dependencies) { false } + let(:resource_boundary) { :unknown } + let(:feature_category) { nil } + let(:worker_class) { create_attributed_worker_class(urgency, external_dependencies, resource_boundary, feature_category) } + let(:worker) { worker_class.new } + + context "high urgency" do + it_behaves_like "a metrics middleware" do + let(:urgency) { :high } + let(:labels) { default_labels.merge(urgency: "high") } + end + end + + context "no urgency" do + it_behaves_like "a metrics middleware" do + let(:urgency) { :throttled } + let(:labels) { default_labels.merge(urgency: "throttled") } + end + end + + context "external dependencies" do + it_behaves_like "a metrics middleware" do + let(:external_dependencies) { true } + let(:labels) { default_labels.merge(external_dependencies: "yes") } + end + end + + context "cpu boundary" do + it_behaves_like "a metrics middleware" do + let(:resource_boundary) { :cpu } + let(:labels) { default_labels.merge(boundary: "cpu") } + end + end + + context "memory boundary" do + it_behaves_like "a metrics middleware" do + let(:resource_boundary) { :memory } + let(:labels) { default_labels.merge(boundary: "memory") } + end + end + + context "feature category" do + it_behaves_like "a metrics middleware" do + let(:feature_category) { :authentication } + let(:labels) { default_labels.merge(feature_category: "authentication") } + end + end + + context "combined" do + it_behaves_like "a metrics middleware" do + let(:urgency) { :high } + let(:external_dependencies) { true } + let(:resource_boundary) { :cpu } + let(:feature_category) { :authentication } + let(:labels) do + default_labels.merge( + urgency: "high", + external_dependencies: "yes", + boundary: "cpu", + feature_category: "authentication") + end + end + end + end +end diff --git a/spec/support/shared_examples/lib/gitlab/sql/set_operator_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/sql/set_operator_shared_examples.rb index 73beef06855..aa6a51c3646 100644 --- a/spec/support/shared_examples/lib/gitlab/sql/set_operator_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/sql/set_operator_shared_examples.rb @@ -43,4 +43,33 @@ RSpec.shared_examples 'SQL set operator' do |operator_keyword| expect(set_operator.to_sql).to eq('NULL') end end + + describe 'remove_order parameter' do + let(:scopes) do + [ + User.where(id: 1).order(id: :desc).limit(1), + User.where(id: 2).order(id: :asc).limit(1) + ] + end + + subject(:union_query) { described_class.new(scopes, remove_order: remove_order).to_sql } + + context 'when remove_order: true' do + let(:remove_order) { true } + + it 'removes the ORDER BY from the query' do + expect(union_query).not_to include('ORDER BY "users"."id" DESC') + expect(union_query).not_to include('ORDER BY "users"."id" ASC') + end + end + + context 'when remove_order: false' do + let(:remove_order) { false } + + it 'does not remove the ORDER BY from the query' do + expect(union_query).to include('ORDER BY "users"."id" DESC') + expect(union_query).to include('ORDER BY "users"."id" ASC') + end + end + end end diff --git a/spec/support/shared_examples/lib/gitlab/usage_data_counters/issuable_activity_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/usage_data_counters/issuable_activity_shared_examples.rb index aa6e64a3820..4b956c2b566 100644 --- a/spec/support/shared_examples/lib/gitlab/usage_data_counters/issuable_activity_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/usage_data_counters/issuable_activity_shared_examples.rb @@ -14,10 +14,6 @@ RSpec.shared_examples 'a daily tracked issuable event' do expect(track_action(author: user1)).to be_truthy expect(track_action(author: user1)).to be_truthy expect(track_action(author: user2)).to be_truthy - expect(track_action(author: user3, time: time - 3.days)).to be_truthy - - expect(count_unique(date_from: time, date_to: time)).to eq(2) - expect(count_unique(date_from: time - 5.days, date_to: 1.day.since(time))).to eq(3) end end diff --git a/spec/support/shared_examples/mailers/notify_shared_examples.rb b/spec/support/shared_examples/mailers/notify_shared_examples.rb index 0143bf693c7..b10ebb4d2a3 100644 --- a/spec/support/shared_examples/mailers/notify_shared_examples.rb +++ b/spec/support/shared_examples/mailers/notify_shared_examples.rb @@ -225,7 +225,7 @@ RSpec.shared_examples 'a note email' do sender = subject.header[:from].addrs[0] aggregate_failures do - expect(sender.display_name).to eq(note_author.name) + expect(sender.display_name).to eq("#{note_author.name} (@#{note_author.username})") expect(sender.address).to eq(gitlab_sender) expect(subject).to deliver_to(recipient.notification_email) end diff --git a/spec/support/shared_examples/metrics/active_record_subscriber_shared_examples.rb b/spec/support/shared_examples/metrics/active_record_subscriber_shared_examples.rb index 7bf2456c548..1b110ab02b5 100644 --- a/spec/support/shared_examples/metrics/active_record_subscriber_shared_examples.rb +++ b/spec/support/shared_examples/metrics/active_record_subscriber_shared_examples.rb @@ -16,7 +16,9 @@ RSpec.shared_examples 'store ActiveRecord info in RequestStore' do |db_role| db_primary_duration_s: record_query ? 0.002 : 0, db_replica_cached_count: 0, db_replica_count: 0, - db_replica_duration_s: 0.0 + db_replica_duration_s: 0.0, + db_primary_wal_count: record_wal_query ? 1 : 0, + db_replica_wal_count: 0 ) elsif db_role == :replica expect(described_class.db_counter_payload).to eq( @@ -28,7 +30,9 @@ RSpec.shared_examples 'store ActiveRecord info in RequestStore' do |db_role| db_primary_duration_s: 0.0, db_replica_cached_count: record_cached_query ? 1 : 0, db_replica_count: record_query ? 1 : 0, - db_replica_duration_s: record_query ? 0.002 : 0 + db_replica_duration_s: record_query ? 0.002 : 0, + db_replica_wal_count: record_wal_query ? 1 : 0, + db_primary_wal_count: 0 ) else expect(described_class.db_counter_payload).to eq( @@ -66,6 +70,12 @@ RSpec.shared_examples 'record ActiveRecord metrics in a metrics transaction' do expect(transaction).not_to receive(:increment).with("gitlab_transaction_db_#{db_role}_cached_count_total".to_sym, 1) if db_role end + if record_wal_query + expect(transaction).to receive(:increment).with("gitlab_transaction_db_#{db_role}_wal_count_total".to_sym, 1) if db_role + else + expect(transaction).not_to receive(:increment).with("gitlab_transaction_db_#{db_role}_wal_count_total".to_sym, 1) if db_role + end + subscriber.sql(event) end diff --git a/spec/support/shared_examples/models/boards/listable_shared_examples.rb b/spec/support/shared_examples/models/boards/listable_shared_examples.rb index e733a5488fb..250a4c1b1bd 100644 --- a/spec/support/shared_examples/models/boards/listable_shared_examples.rb +++ b/spec/support/shared_examples/models/boards/listable_shared_examples.rb @@ -16,18 +16,23 @@ RSpec.shared_examples 'boards listable model' do |list_factory| end describe 'scopes' do + let_it_be(:list1) { create(list_factory, list_type: :backlog) } + let_it_be(:list2) { create(list_factory, list_type: :closed) } + let_it_be(:list3) { create(list_factory, position: 1) } + let_it_be(:list4) { create(list_factory, position: 2) } + describe '.ordered' do it 'returns lists ordered by type and position' do - # rubocop:disable Rails/SaveBang - lists = [ - create(list_factory, list_type: :backlog), - create(list_factory, list_type: :closed), - create(list_factory, position: 1), - create(list_factory, position: 2) - ] - # rubocop:enable Rails/SaveBang - - expect(described_class.where(id: lists).ordered).to eq([lists[0], lists[2], lists[3], lists[1]]) + expect(described_class.where(id: [list1, list2, list3, list4]).ordered) + .to eq([list1, list3, list4, list2]) + end + end + + describe '.without_types' do + it 'excludes lists of given types' do + lists = described_class.without_types([:label, :closed]) + + expect(lists).to match_array([list1]) end end end diff --git a/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb b/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb index 7603787a54e..d3f3e15d299 100644 --- a/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb +++ b/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb @@ -138,7 +138,7 @@ RSpec.shared_examples 'cluster application status specs' do |application_name| it 'is installed' do subject.make_externally_installed - expect(subject).to be_installed + expect(subject).to be_externally_installed end context 'helm record does not exist' do @@ -170,7 +170,7 @@ RSpec.shared_examples 'cluster application status specs' do |application_name| it 'is installed' do subject.make_externally_installed - expect(subject).to be_installed + expect(subject).to be_externally_installed end end @@ -180,7 +180,7 @@ RSpec.shared_examples 'cluster application status specs' do |application_name| it 'is installed' do subject.make_externally_installed - expect(subject).to be_installed + expect(subject).to be_externally_installed end it 'clears #status_reason' do @@ -317,6 +317,7 @@ RSpec.shared_examples 'cluster application status specs' do |application_name| :uninstall_errored | false :uninstalled | false :timed_out | false + :externally_installed | true end with_them do diff --git a/spec/support/shared_examples/models/cluster_application_version_shared_examples.rb b/spec/support/shared_examples/models/cluster_application_version_shared_examples.rb index ed2e4fee2de..3acc43eb0da 100644 --- a/spec/support/shared_examples/models/cluster_application_version_shared_examples.rb +++ b/spec/support/shared_examples/models/cluster_application_version_shared_examples.rb @@ -47,4 +47,15 @@ RSpec.shared_examples 'cluster application version specs' do |application_name| end end end + + describe '#make_externally_installed' do + subject { build(application_name) } + + it 'sets to a special version' do + subject.make_externally_installed! + + expect(subject).to be_persisted + expect(subject.version).to eq('EXTERNALLY_INSTALLED') + end + end end diff --git a/spec/support/shared_examples/models/clusters/prometheus_client_shared.rb b/spec/support/shared_examples/models/clusters/prometheus_client_shared.rb new file mode 100644 index 00000000000..8d6dcfef925 --- /dev/null +++ b/spec/support/shared_examples/models/clusters/prometheus_client_shared.rb @@ -0,0 +1,86 @@ +# frozen_string_literal: true + +# Input +# - factory: [:clusters_applications_prometheus, :clusters_integrations_prometheus] +RSpec.shared_examples '#prometheus_client shared' do + shared_examples 'exception caught for prometheus client' do + before do + allow(kube_client).to receive(:proxy_url).and_raise(exception) + end + + it 'returns nil' do + expect(subject.prometheus_client).to be_nil + end + end + + context 'cluster is nil' do + it 'returns nil' do + expect(subject.cluster).to be_nil + expect(subject.prometheus_client).to be_nil + end + end + + context "cluster doesn't have kubeclient" do + let(:cluster) { create(:cluster) } + + subject { create(factory, cluster: cluster) } + + it 'returns nil' do + expect(subject.prometheus_client).to be_nil + end + end + + context 'cluster has kubeclient' do + let(:cluster) { create(:cluster, :project, :provided_by_gcp) } + let(:kubernetes_url) { subject.cluster.platform_kubernetes.api_url } + let(:kube_client) { subject.cluster.kubeclient.core_client } + + subject { create(factory, cluster: cluster) } + + before do + subject.cluster.platform_kubernetes.namespace = 'a-namespace' + stub_kubeclient_discover(cluster.platform_kubernetes.api_url) + + create(:cluster_kubernetes_namespace, + cluster: cluster, + cluster_project: cluster.cluster_project, + project: cluster.cluster_project.project) + end + + it 'creates proxy prometheus_client' do + expect(subject.prometheus_client).to be_instance_of(Gitlab::PrometheusClient) + end + + it 'merges proxy_url, options and headers from kube client with prometheus_client options' do + expect(Gitlab::PrometheusClient) + .to(receive(:new)) + .with(a_valid_url, kube_client.rest_client.options.merge({ + headers: kube_client.headers, + timeout: PrometheusAdapter::DEFAULT_PROMETHEUS_REQUEST_TIMEOUT_SEC + })) + subject.prometheus_client + end + + context 'when cluster is not reachable' do + it_behaves_like 'exception caught for prometheus client' do + let(:exception) { Kubeclient::HttpError.new(401, 'Unauthorized', nil) } + end + end + + context 'when there is a socket error while contacting cluster' do + it_behaves_like 'exception caught for prometheus client' do + let(:exception) { Errno::ECONNREFUSED } + end + + it_behaves_like 'exception caught for prometheus client' do + let(:exception) { Errno::ECONNRESET } + end + end + + context 'when the network is unreachable' do + it_behaves_like 'exception caught for prometheus client' do + let(:exception) { Errno::ENETUNREACH } + end + end + end +end diff --git a/spec/support/shared_examples/models/packages/debian/architecture_shared_examples.rb b/spec/support/shared_examples/models/packages/debian/architecture_shared_examples.rb index b73ff516670..fbb94b4f5c1 100644 --- a/spec/support/shared_examples/models/packages/debian/architecture_shared_examples.rb +++ b/spec/support/shared_examples/models/packages/debian/architecture_shared_examples.rb @@ -34,7 +34,7 @@ RSpec.shared_examples 'Debian Distribution Architecture' do |factory, container, subject { described_class.with_distribution(architecture.distribution) } it 'does not return other distributions' do - expect(subject.to_a).to eq([architecture, architecture_same_distribution]) + expect(subject.to_a).to match_array([architecture, architecture_same_distribution]) end end @@ -42,7 +42,7 @@ RSpec.shared_examples 'Debian Distribution Architecture' do |factory, container, subject { described_class.with_name(architecture.name) } it 'does not return other distributions' do - expect(subject.to_a).to eq([architecture, architecture_same_name]) + expect(subject.to_a).to match_array([architecture, architecture_same_name]) end end end diff --git a/spec/support/shared_examples/models/packages/debian/component_shared_examples.rb b/spec/support/shared_examples/models/packages/debian/component_shared_examples.rb index bf6fc23116c..23e76d32fb0 100644 --- a/spec/support/shared_examples/models/packages/debian/component_shared_examples.rb +++ b/spec/support/shared_examples/models/packages/debian/component_shared_examples.rb @@ -36,7 +36,7 @@ RSpec.shared_examples 'Debian Distribution Component' do |factory, container, ca subject { described_class.with_distribution(component.distribution) } it 'does not return other distributions' do - expect(subject.to_a).to eq([component, component_same_distribution]) + expect(subject.to_a).to match_array([component, component_same_distribution]) end end @@ -44,7 +44,7 @@ RSpec.shared_examples 'Debian Distribution Component' do |factory, container, ca subject { described_class.with_name(component.name) } it 'does not return other distributions' do - expect(subject.to_a).to eq([component, component_same_name]) + expect(subject.to_a).to match_array([component, component_same_name]) end end end diff --git a/spec/support/shared_examples/models/packages/debian/distribution_shared_examples.rb b/spec/support/shared_examples/models/packages/debian/distribution_shared_examples.rb index b4ec146df14..9eacacf725f 100644 --- a/spec/support/shared_examples/models/packages/debian/distribution_shared_examples.rb +++ b/spec/support/shared_examples/models/packages/debian/distribution_shared_examples.rb @@ -179,7 +179,7 @@ RSpec.shared_examples 'Debian Distribution' do |factory, container, can_freeze| subject { described_class.with_codename_or_suite(distribution_with_suite.codename) } it 'does not return other distributions' do - expect(subject.to_a).to eq([distribution_with_suite, distribution_with_same_codename, distribution_with_codename_and_suite_flipped]) + expect(subject.to_a).to contain_exactly(distribution_with_suite, distribution_with_same_codename, distribution_with_codename_and_suite_flipped) end end @@ -187,7 +187,7 @@ RSpec.shared_examples 'Debian Distribution' do |factory, container, can_freeze| subject { described_class.with_codename_or_suite(distribution_with_suite.suite) } it 'does not return other distributions' do - expect(subject.to_a).to eq([distribution_with_suite, distribution_with_same_suite, distribution_with_codename_and_suite_flipped]) + expect(subject.to_a).to contain_exactly(distribution_with_suite, distribution_with_same_suite, distribution_with_codename_and_suite_flipped) end end end diff --git a/spec/support/shared_examples/models/wiki_shared_examples.rb b/spec/support/shared_examples/models/wiki_shared_examples.rb index abc6e3ecce8..6b243aef3e6 100644 --- a/spec/support/shared_examples/models/wiki_shared_examples.rb +++ b/spec/support/shared_examples/models/wiki_shared_examples.rb @@ -354,33 +354,29 @@ RSpec.shared_examples 'wiki model' do subject.repository.create_file(user, 'image.png', image, branch_name: subject.default_branch, message: 'add image') end - shared_examples 'find_file results' do - it 'returns the latest version of the file if it exists' do - file = subject.find_file('image.png') + it 'returns the latest version of the file if it exists' do + file = subject.find_file('image.png') - expect(file.mime_type).to eq('image/png') - end + expect(file.mime_type).to eq('image/png') + end - it 'returns nil if the page does not exist' do - expect(subject.find_file('non-existent')).to eq(nil) - end + it 'returns nil if the page does not exist' do + expect(subject.find_file('non-existent')).to eq(nil) + end - it 'returns a Gitlab::Git::WikiFile instance' do - file = subject.find_file('image.png') + it 'returns a Gitlab::Git::WikiFile instance' do + file = subject.find_file('image.png') - expect(file).to be_a Gitlab::Git::WikiFile - end + expect(file).to be_a Gitlab::Git::WikiFile + end - it 'returns the whole file' do - file = subject.find_file('image.png') - image.rewind + it 'returns the whole file' do + file = subject.find_file('image.png') + image.rewind - expect(file.raw_data.b).to eq(image.read.b) - end + expect(file.raw_data.b).to eq(image.read.b) end - it_behaves_like 'find_file results' - context 'when load_content is disabled' do it 'includes the file data in the Gitlab::Git::WikiFile' do file = subject.find_file('image.png', load_content: false) @@ -388,14 +384,6 @@ RSpec.shared_examples 'wiki model' do expect(file.raw_data).to be_empty end end - - context 'when feature flag :gitaly_find_file is disabled' do - before do - stub_feature_flags(gitaly_find_file: false) - end - - it_behaves_like 'find_file results' - end end describe '#create_page' do @@ -481,28 +469,53 @@ RSpec.shared_examples 'wiki model' do end describe '#delete_page' do - let(:page) { create(:wiki_page, wiki: wiki) } + shared_examples 'delete_page operations' do + let(:page) { create(:wiki_page, wiki: wiki) } - it 'deletes the page' do - subject.delete_page(page) + it 'deletes the page' do + subject.delete_page(page) - expect(subject.list_pages.count).to eq(0) - end + expect(subject.list_pages.count).to eq(0) + end - it 'sets the correct commit email' do - subject.delete_page(page) + it 'sets the correct commit email' do + subject.delete_page(page) - expect(user.commit_email).not_to eq(user.email) - expect(commit.author_email).to eq(user.commit_email) - expect(commit.committer_email).to eq(user.commit_email) + expect(user.commit_email).not_to eq(user.email) + expect(commit.author_email).to eq(user.commit_email) + expect(commit.committer_email).to eq(user.commit_email) + end + + it 'runs after_wiki_activity callbacks' do + page + + expect(subject).to receive(:after_wiki_activity) + + subject.delete_page(page) + end end - it 'runs after_wiki_activity callbacks' do - page + it_behaves_like 'delete_page operations' - expect(subject).to receive(:after_wiki_activity) + context 'when an error is raised' do + it 'logs the error and returns false' do + page = build(:wiki_page, wiki: wiki) + exception = Gitlab::Git::Index::IndexError.new('foo') + + allow(subject.repository).to receive(:delete_file).and_raise(exception) + + expect(Gitlab::ErrorTracking).to receive(:log_exception).with(exception, action: :deleted, wiki_id: wiki.id) + + expect(subject.delete_page(page)).to be_falsey + end + end + + context 'when feature flag :gitaly_replace_wiki_delete_page is disabled' do + before do + stub_feature_flags(gitaly_replace_wiki_delete_page: false) + end - subject.delete_page(page) + it_behaves_like 'delete_page operations' end end diff --git a/spec/support/shared_examples/namespaces/namespace_traversal_examples.rb b/spec/support/shared_examples/namespaces/namespace_traversal_examples.rb new file mode 100644 index 00000000000..36e5808fa28 --- /dev/null +++ b/spec/support/shared_examples/namespaces/namespace_traversal_examples.rb @@ -0,0 +1,124 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'namespace traversal' do + shared_examples 'recursive version' do |method| + let(:recursive_method) { "recursive_#{method}" } + + it "is equivalent to ##{method}" do + groups.each do |group| + expect(group.public_send(method)).to match_array group.public_send(recursive_method) + end + end + + it "makes a recursive query" do + groups.each do |group| + expect { group.public_send(recursive_method).load }.to make_queries_matching(/WITH RECURSIVE/) + end + end + end + + describe '#self_and_hierarchy' do + let!(:group) { create(:group, path: 'git_lab') } + let!(:nested_group) { create(:group, parent: group) } + let!(:deep_nested_group) { create(:group, parent: nested_group) } + let!(:very_deep_nested_group) { create(:group, parent: deep_nested_group) } + let!(:another_group) { create(:group, path: 'gitllab') } + let!(:another_group_nested) { create(:group, path: 'foo', parent: another_group) } + + it 'returns the correct tree' do + expect(group.self_and_hierarchy).to contain_exactly(group, nested_group, deep_nested_group, very_deep_nested_group) + expect(nested_group.self_and_hierarchy).to contain_exactly(group, nested_group, deep_nested_group, very_deep_nested_group) + expect(very_deep_nested_group.self_and_hierarchy).to contain_exactly(group, nested_group, deep_nested_group, very_deep_nested_group) + end + + describe '#recursive_self_and_hierarchy' do + let(:groups) { [group, nested_group, very_deep_nested_group] } + + it_behaves_like 'recursive version', :self_and_hierarchy + end + end + + describe '#ancestors' do + let(:group) { create(:group) } + let(:nested_group) { create(:group, parent: group) } + let(:deep_nested_group) { create(:group, parent: nested_group) } + let(:very_deep_nested_group) { create(:group, parent: deep_nested_group) } + + it 'returns the correct ancestors' do + expect(very_deep_nested_group.ancestors).to include(group, nested_group, deep_nested_group) + expect(deep_nested_group.ancestors).to include(group, nested_group) + expect(nested_group.ancestors).to include(group) + expect(group.ancestors).to eq([]) + end + + describe '#recursive_ancestors' do + let(:groups) { [nested_group, deep_nested_group, very_deep_nested_group] } + + it_behaves_like 'recursive version', :ancestors + end + end + + describe '#self_and_ancestors' do + let(:group) { create(:group) } + let(:nested_group) { create(:group, parent: group) } + let(:deep_nested_group) { create(:group, parent: nested_group) } + let(:very_deep_nested_group) { create(:group, parent: deep_nested_group) } + + it 'returns the correct ancestors' do + expect(very_deep_nested_group.self_and_ancestors).to contain_exactly(group, nested_group, deep_nested_group, very_deep_nested_group) + expect(deep_nested_group.self_and_ancestors).to contain_exactly(group, nested_group, deep_nested_group) + expect(nested_group.self_and_ancestors).to contain_exactly(group, nested_group) + expect(group.self_and_ancestors).to contain_exactly(group) + end + + describe '#recursive_self_and_ancestors' do + let(:groups) { [nested_group, deep_nested_group, very_deep_nested_group] } + + it_behaves_like 'recursive version', :self_and_ancestors + end + end + + describe '#descendants' do + let!(:group) { create(:group, path: 'git_lab') } + let!(:nested_group) { create(:group, parent: group) } + let!(:deep_nested_group) { create(:group, parent: nested_group) } + let!(:very_deep_nested_group) { create(:group, parent: deep_nested_group) } + let!(:another_group) { create(:group, path: 'gitllab') } + let!(:another_group_nested) { create(:group, path: 'foo', parent: another_group) } + + it 'returns the correct descendants' do + expect(very_deep_nested_group.descendants.to_a).to eq([]) + expect(deep_nested_group.descendants.to_a).to include(very_deep_nested_group) + expect(nested_group.descendants.to_a).to include(deep_nested_group, very_deep_nested_group) + expect(group.descendants.to_a).to include(nested_group, deep_nested_group, very_deep_nested_group) + end + + describe '#recursive_descendants' do + let(:groups) { [group, nested_group, deep_nested_group, very_deep_nested_group] } + + it_behaves_like 'recursive version', :descendants + end + end + + describe '#self_and_descendants' do + let!(:group) { create(:group, path: 'git_lab') } + let!(:nested_group) { create(:group, parent: group) } + let!(:deep_nested_group) { create(:group, parent: nested_group) } + let!(:very_deep_nested_group) { create(:group, parent: deep_nested_group) } + let!(:another_group) { create(:group, path: 'gitllab') } + let!(:another_group_nested) { create(:group, path: 'foo', parent: another_group) } + + it 'returns the correct descendants' do + expect(very_deep_nested_group.self_and_descendants).to contain_exactly(very_deep_nested_group) + expect(deep_nested_group.self_and_descendants).to contain_exactly(deep_nested_group, very_deep_nested_group) + expect(nested_group.self_and_descendants).to contain_exactly(nested_group, deep_nested_group, very_deep_nested_group) + expect(group.self_and_descendants).to contain_exactly(group, nested_group, deep_nested_group, very_deep_nested_group) + end + + describe '#recursive_self_and_descendants' do + let(:groups) { [group, nested_group, deep_nested_group, very_deep_nested_group] } + + it_behaves_like 'recursive version', :self_and_descendants + end + end +end diff --git a/spec/support/shared_examples/namespaces/recursive_traversal_examples.rb b/spec/support/shared_examples/namespaces/recursive_traversal_examples.rb deleted file mode 100644 index 2c94be61bc1..00000000000 --- a/spec/support/shared_examples/namespaces/recursive_traversal_examples.rb +++ /dev/null @@ -1,78 +0,0 @@ -# frozen_string_literal: true - -RSpec.shared_examples 'recursive namespace traversal' do - describe '#self_and_hierarchy' do - let!(:group) { create(:group, path: 'git_lab') } - let!(:nested_group) { create(:group, parent: group) } - let!(:deep_nested_group) { create(:group, parent: nested_group) } - let!(:very_deep_nested_group) { create(:group, parent: deep_nested_group) } - let!(:another_group) { create(:group, path: 'gitllab') } - let!(:another_group_nested) { create(:group, path: 'foo', parent: another_group) } - - it 'returns the correct tree' do - expect(group.self_and_hierarchy).to contain_exactly(group, nested_group, deep_nested_group, very_deep_nested_group) - expect(nested_group.self_and_hierarchy).to contain_exactly(group, nested_group, deep_nested_group, very_deep_nested_group) - expect(very_deep_nested_group.self_and_hierarchy).to contain_exactly(group, nested_group, deep_nested_group, very_deep_nested_group) - end - end - - describe '#ancestors' do - let(:group) { create(:group) } - let(:nested_group) { create(:group, parent: group) } - let(:deep_nested_group) { create(:group, parent: nested_group) } - let(:very_deep_nested_group) { create(:group, parent: deep_nested_group) } - - it 'returns the correct ancestors' do - expect(very_deep_nested_group.ancestors).to include(group, nested_group, deep_nested_group) - expect(deep_nested_group.ancestors).to include(group, nested_group) - expect(nested_group.ancestors).to include(group) - expect(group.ancestors).to eq([]) - end - end - - describe '#self_and_ancestors' do - let(:group) { create(:group) } - let(:nested_group) { create(:group, parent: group) } - let(:deep_nested_group) { create(:group, parent: nested_group) } - let(:very_deep_nested_group) { create(:group, parent: deep_nested_group) } - - it 'returns the correct ancestors' do - expect(very_deep_nested_group.self_and_ancestors).to contain_exactly(group, nested_group, deep_nested_group, very_deep_nested_group) - expect(deep_nested_group.self_and_ancestors).to contain_exactly(group, nested_group, deep_nested_group) - expect(nested_group.self_and_ancestors).to contain_exactly(group, nested_group) - expect(group.self_and_ancestors).to contain_exactly(group) - end - end - - describe '#descendants' do - let!(:group) { create(:group, path: 'git_lab') } - let!(:nested_group) { create(:group, parent: group) } - let!(:deep_nested_group) { create(:group, parent: nested_group) } - let!(:very_deep_nested_group) { create(:group, parent: deep_nested_group) } - let!(:another_group) { create(:group, path: 'gitllab') } - let!(:another_group_nested) { create(:group, path: 'foo', parent: another_group) } - - it 'returns the correct descendants' do - expect(very_deep_nested_group.descendants.to_a).to eq([]) - expect(deep_nested_group.descendants.to_a).to include(very_deep_nested_group) - expect(nested_group.descendants.to_a).to include(deep_nested_group, very_deep_nested_group) - expect(group.descendants.to_a).to include(nested_group, deep_nested_group, very_deep_nested_group) - end - end - - describe '#self_and_descendants' do - let!(:group) { create(:group, path: 'git_lab') } - let!(:nested_group) { create(:group, parent: group) } - let!(:deep_nested_group) { create(:group, parent: nested_group) } - let!(:very_deep_nested_group) { create(:group, parent: deep_nested_group) } - let!(:another_group) { create(:group, path: 'gitllab') } - let!(:another_group_nested) { create(:group, path: 'foo', parent: another_group) } - - it 'returns the correct descendants' do - expect(very_deep_nested_group.self_and_descendants).to contain_exactly(very_deep_nested_group) - expect(deep_nested_group.self_and_descendants).to contain_exactly(deep_nested_group, very_deep_nested_group) - expect(nested_group.self_and_descendants).to contain_exactly(nested_group, deep_nested_group, very_deep_nested_group) - expect(group.self_and_descendants).to contain_exactly(group, nested_group, deep_nested_group, very_deep_nested_group) - end - end -end diff --git a/spec/support/shared_examples/nav_sidebar_shared_examples.rb b/spec/support/shared_examples/nav_sidebar_shared_examples.rb index e084a957785..3e500683712 100644 --- a/spec/support/shared_examples/nav_sidebar_shared_examples.rb +++ b/spec/support/shared_examples/nav_sidebar_shared_examples.rb @@ -24,3 +24,13 @@ RSpec.shared_examples 'page has active sub tab' do |title| .to have_content(title) end end + +RSpec.shared_examples 'sidebar includes snowplow attributes' do |track_action, track_label, track_property| + specify do + allow(view).to receive(:tracking_enabled?).and_return(true) + + render + + expect(rendered).to have_css(".nav-sidebar[data-track-action=\"#{track_action}\"][data-track-label=\"#{track_label}\"][data-track-property=\"#{track_property}\"]") + end +end diff --git a/spec/support/shared_examples/policies/resource_access_token_shared_examples.rb b/spec/support/shared_examples/policies/resource_access_token_shared_examples.rb index 7710e756e5b..337ad024fc0 100644 --- a/spec/support/shared_examples/policies/resource_access_token_shared_examples.rb +++ b/spec/support/shared_examples/policies/resource_access_token_shared_examples.rb @@ -5,16 +5,70 @@ RSpec.shared_examples 'Self-managed Core resource access tokens' do allow(::Gitlab).to receive(:com?).and_return(false) end - context 'with owner' do + context 'with owner access' do let(:current_user) { owner } - it { is_expected.to be_allowed(:admin_resource_access_tokens) } + context 'create resource access tokens' do + it { is_expected.to be_allowed(:create_resource_access_tokens) } + + context 'when resource access token creation is not allowed' do + let(:group) { create(:group) } + let(:project) { create(:project, group: group) } + + before do + group.namespace_settings.update_column(:resource_access_token_creation_allowed, false) + end + + it { is_expected.not_to be_allowed(:create_resource_access_tokens) } + end + + context 'when parent group has project access token creation disabled' do + let(:parent) { create(:group) } + let(:group) { create(:group, parent: parent) } + let(:project) { create(:project, group: group) } + + before do + parent.namespace_settings.update_column(:resource_access_token_creation_allowed, false) + end + + it { is_expected.not_to be_allowed(:create_resource_access_tokens) } + end + + context 'with a personal namespace project' do + let(:namespace) { create(:namespace) } + let(:project) { create(:project, namespace: namespace) } + + before do + project.add_maintainer(current_user) + end + + it { is_expected.to be_allowed(:create_resource_access_tokens) } + end + end + + context 'read resource access tokens' do + it { is_expected.to be_allowed(:read_resource_access_tokens) } + end + + context 'destroy resource access tokens' do + it { is_expected.to be_allowed(:destroy_resource_access_tokens) } + end end - context 'with developer' do + context 'with developer access' do let(:current_user) { developer } - it { is_expected.not_to be_allowed(:admin_resource_access_tokens) } + context 'create resource access tokens' do + it { is_expected.not_to be_allowed(:create_resource_access_tokens) } + end + + context 'read resource access tokens' do + it { is_expected.not_to be_allowed(:read_resource_access_tokens) } + end + + context 'destroy resource access tokens' do + it { is_expected.not_to be_allowed(:destroy_resource_access_tokens) } + end end end @@ -24,9 +78,19 @@ RSpec.shared_examples 'GitLab.com Core resource access tokens' do stub_ee_application_setting(should_check_namespace_plan: true) end - context 'with owner' do + context 'with owner access' do let(:current_user) { owner } - it { is_expected.not_to be_allowed(:admin_resource_access_tokens) } + context 'create resource access tokens' do + it { is_expected.not_to be_allowed(:create_resource_access_tokens) } + end + + context 'read resource access tokens' do + it { is_expected.not_to be_allowed(:read_resource_access_tokens) } + end + + context 'destroy resource access tokens' do + it { is_expected.not_to be_allowed(:destroy_resource_access_tokens) } + end end end diff --git a/spec/support/shared_examples/querying_shared_examples.rb b/spec/support/shared_examples/querying_shared_examples.rb new file mode 100644 index 00000000000..1f554ddb441 --- /dev/null +++ b/spec/support/shared_examples/querying_shared_examples.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +def update_column_regex(column) + /UPDATE.+SET.+#{column}[^=*]=.+FROM.*/m +end + +RSpec.shared_examples 'update on column' do |column| + it "#{column} column updated" do + qr = ActiveRecord::QueryRecorder.new do + subject + end + expect(qr.log).to include a_string_matching update_column_regex(column) + end +end + +RSpec.shared_examples 'no update on column' do |column| + it "#{column} column is not updated" do + qr = ActiveRecord::QueryRecorder.new do + subject + end + expect(qr.log).not_to include a_string_matching update_column_regex(column) + end +end diff --git a/spec/support/shared_examples/quick_actions/issuable/close_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issuable/close_quick_action_shared_examples.rb index 4fde68efd60..ca6536444fd 100644 --- a/spec/support/shared_examples/quick_actions/issuable/close_quick_action_shared_examples.rb +++ b/spec/support/shared_examples/quick_actions/issuable/close_quick_action_shared_examples.rb @@ -23,7 +23,7 @@ RSpec.shared_examples 'close quick action' do |issuable_type| it "creates the #{issuable_type} and interprets close quick action accordingly" do fill_in "#{issuable_type}_title", with: 'bug 345' fill_in "#{issuable_type}_description", with: "bug description\n/close" - click_button "Submit #{issuable_type}".humanize + click_button "Create #{issuable_type}".humanize issuable = project.public_send(issuable_type.to_s.pluralize).first diff --git a/spec/support/shared_examples/requests/api/conan_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/conan_packages_shared_examples.rb index 54ea876bed2..87aaac673c1 100644 --- a/spec/support/shared_examples/requests/api/conan_packages_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/conan_packages_shared_examples.rb @@ -205,6 +205,14 @@ RSpec.shared_examples 'empty recipe for not found package' do 'aa/bb/%{project}/ccc' % { project: ::Packages::Conan::Metadatum.package_username_from(full_path: project.full_path) } end + let(:presenter) { double('::Packages::Conan::PackagePresenter') } + + before do + allow(::Packages::Conan::PackagePresenter).to receive(:new) + .with(package, user, package.project, any_args) + .and_return(presenter) + end + it 'returns not found' do allow(::Packages::Conan::PackagePresenter).to receive(:new) .with( @@ -248,8 +256,6 @@ RSpec.shared_examples 'recipe download_urls' do 'conanmanifest.txt' => "#{url_prefix}/packages/conan/v1/files/#{package.conan_recipe_path}/0/export/conanmanifest.txt" } - allow(presenter).to receive(:recipe_urls) { expected_response } - subject expect(json_response).to eq(expected_response) @@ -268,8 +274,6 @@ RSpec.shared_examples 'package download_urls' do 'conan_package.tgz' => "#{url_prefix}/packages/conan/v1/files/#{package.conan_recipe_path}/0/package/123456789/0/conan_package.tgz" } - allow(presenter).to receive(:package_urls) { expected_response } - subject expect(json_response).to eq(expected_response) @@ -309,13 +313,14 @@ RSpec.shared_examples 'recipe snapshot endpoint' do context 'with existing package' do it 'returns a hash of files with their md5 hashes' do + conan_file_file = package.package_files.find_by(file_name: 'conanfile.py') + conan_manifest_file = package.package_files.find_by(file_name: 'conanmanifest.txt') + expected_response = { - 'conanfile.py' => 'md5hash1', - 'conanmanifest.txt' => 'md5hash2' + 'conanfile.py' => conan_file_file.file_md5, + 'conanmanifest.txt' => conan_manifest_file.file_md5 } - allow(presenter).to receive(:recipe_snapshot) { expected_response } - subject expect(json_response).to eq(expected_response) @@ -333,13 +338,11 @@ RSpec.shared_examples 'package snapshot endpoint' do context 'with existing package' do it 'returns a hash of md5 values for the files' do expected_response = { - 'conaninfo.txt' => "md5hash1", - 'conanmanifest.txt' => "md5hash2", - 'conan_package.tgz' => "md5hash3" + 'conaninfo.txt' => "12345abcde", + 'conanmanifest.txt' => "12345abcde", + 'conan_package.tgz' => "12345abcde" } - allow(presenter).to receive(:package_snapshot) { expected_response } - subject expect(json_response).to eq(expected_response) diff --git a/spec/support/shared_examples/requests/api/graphql/projects/alert_management/integrations_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/projects/alert_management/integrations_shared_examples.rb new file mode 100644 index 00000000000..c134f7d1839 --- /dev/null +++ b/spec/support/shared_examples/requests/api/graphql/projects/alert_management/integrations_shared_examples.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'GraphQL query with several integrations requested' do |graphql_query_name:| + context 'when several HTTP integrations requested' do + let(:params_ai) { { id: global_id_of(active_http_integration) } } + let(:params_ii) { { id: global_id_of(inactive_http_integration) } } + let(:fields) { "nodes { id name }" } + + let(:single_selection_query) do + graphql_query_for( + 'project', + { 'fullPath' => project.full_path }, + <<~QUERY + ai: #{query_graphql_field(graphql_query_name, params_ai, fields)} + QUERY + ) + end + + let(:multi_selection_query) do + graphql_query_for( + 'project', + { 'fullPath' => project.full_path }, + <<~QUERY + ai: #{query_graphql_field(graphql_query_name, params_ai, fields)} + ii: #{query_graphql_field(graphql_query_name, params_ii, fields)} + QUERY + ) + end + + it 'returns the correct properties of the integrations', :aggregate_failures do + post_graphql(multi_selection_query, current_user: current_user) + + expect(graphql_data.dig('project', 'ai', 'nodes')).to include( + 'id' => global_id_of(active_http_integration), + 'name' => active_http_integration.name + ) + + expect(graphql_data.dig('project', 'ii', 'nodes')).to include( + 'id' => global_id_of(inactive_http_integration), + 'name' => inactive_http_integration.name + ) + end + + it 'batches queries' do + expect { post_graphql(multi_selection_query, current_user: current_user) } + .to issue_same_number_of_queries_as { post_graphql(single_selection_query, current_user: current_user) }.ignoring_cached_queries + end + end +end diff --git a/spec/support/shared_examples/requests/api/logging_application_context_shared_examples.rb b/spec/support/shared_examples/requests/api/logging_application_context_shared_examples.rb index 4a71b696d57..cb06c9fa596 100644 --- a/spec/support/shared_examples/requests/api/logging_application_context_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/logging_application_context_shared_examples.rb @@ -1,21 +1,13 @@ # frozen_string_literal: true RSpec.shared_examples 'storing arguments in the application context' do - around do |example| - Labkit::Context.with_context { example.run } - end - it 'places the expected params in the application context' do # Stub the clearing of the context so we can validate it later - # The `around` block above makes sure we do clean it up later allow(Labkit::Context).to receive(:pop) subject - Labkit::Context.with_context do |context| - expect(context.to_h) - .to include(log_hash(expected_params)) - end + expect(Gitlab::ApplicationContext.current).to include(log_hash(expected_params)) end def log_hash(hash) diff --git a/spec/support/shared_examples/requests/api/nuget_endpoints_shared_examples.rb b/spec/support/shared_examples/requests/api/nuget_endpoints_shared_examples.rb index 7b7d2a33e8c..db70bc75c63 100644 --- a/spec/support/shared_examples/requests/api/nuget_endpoints_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/nuget_endpoints_shared_examples.rb @@ -156,7 +156,7 @@ RSpec.shared_examples 'handling nuget metadata requests with package name and pa include_context 'with expected presenters dependency groups' let_it_be(:package_name) { 'Dummy.Package' } - let_it_be(:package) { create(:nuget_package, :with_metadatum, name: 'Dummy.Package', project: project) } + let_it_be(:package) { create(:nuget_package, :with_metadatum, name: package_name, project: project) } let_it_be(:tag) { create(:packages_tag, package: package, name: 'test') } subject { get api(url) } @@ -225,7 +225,7 @@ RSpec.shared_examples 'handling nuget search requests' do |anonymous_requests_ex let(:take) { 26 } let(:skip) { 0 } let(:include_prereleases) { true } - let(:query_parameters) { { q: search_term, take: take, skip: skip, prerelease: include_prereleases } } + let(:query_parameters) { { q: search_term, take: take, skip: skip, prerelease: include_prereleases }.compact } subject { get api(url) } diff --git a/spec/support/shared_examples/requests/api/packages_shared_examples.rb b/spec/support/shared_examples/requests/api/packages_shared_examples.rb index 15976eed021..eb86b7c37d5 100644 --- a/spec/support/shared_examples/requests/api/packages_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/packages_shared_examples.rb @@ -100,7 +100,7 @@ RSpec.shared_examples 'job token for package GET requests' do end end -RSpec.shared_examples 'job token for package uploads' do +RSpec.shared_examples 'job token for package uploads' do |authorize_endpoint: false| context 'with job token headers' do let(:headers) { basic_auth_header(::Gitlab::Auth::CI_JOB_USER, job.token).merge(workhorse_headers) } @@ -111,6 +111,17 @@ RSpec.shared_examples 'job token for package uploads' do context 'valid token' do it_behaves_like 'returning response status', :success + + unless authorize_endpoint + it 'creates a package with build info' do + expect { subject }.to change { Packages::Package.count }.by(1) + + pkg = ::Packages::Package.order_created + .last + + expect(pkg.build_infos).to be + end + end end context 'invalid token' do diff --git a/spec/support/shared_examples/requests/api/rubygems_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/rubygems_packages_shared_examples.rb index 15fb6611b90..abdb468353a 100644 --- a/spec/support/shared_examples/requests/api/rubygems_packages_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/rubygems_packages_shared_examples.rb @@ -43,6 +43,8 @@ end RSpec.shared_examples 'process rubygems upload' do |user_type, status, add_member = true| RSpec.shared_examples 'creates rubygems package files' do it 'creates package files', :aggregate_failures do + expect(::Packages::Rubygems::ExtractionWorker).to receive(:perform_async).once + expect { subject } .to change { project.packages.count }.by(1) .and change { Packages::PackageFile.count }.by(1) @@ -51,6 +53,17 @@ RSpec.shared_examples 'process rubygems upload' do |user_type, status, add_membe package_file = project.packages.last.package_files.reload.last expect(package_file.file_name).to eq('package.gem') end + + it 'returns bad request if package creation fails' do + file_service = double('file_service', execute: nil) + + expect(::Packages::CreatePackageFileService).to receive(:new).and_return(file_service) + expect(::Packages::Rubygems::ExtractionWorker).not_to receive(:perform_async) + + subject + + expect(response).to have_gitlab_http_status(:bad_request) + end end context "for user type #{user_type}" do diff --git a/spec/support/shared_examples/requests/clusters/integrations_controller_shared_examples.rb b/spec/support/shared_examples/requests/clusters/integrations_controller_shared_examples.rb new file mode 100644 index 00000000000..490c7d12115 --- /dev/null +++ b/spec/support/shared_examples/requests/clusters/integrations_controller_shared_examples.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +RSpec.shared_examples '#create_or_update action' do + let(:params) do + { integration: { application_type: Clusters::Applications::Prometheus.application_name, enabled: true } } + end + + let(:path) { raise NotImplementedError } + let(:redirect_path) { raise NotImplementedError } + + describe 'authorization' do + subject do + post path, params: params + end + + it_behaves_like 'a secure endpoint' + end + + describe 'functionality' do + before do + sign_in(user) + end + + it 'redirects on success' do + post path, params: params + + expect(response).to have_gitlab_http_status(:redirect) + expect(response).to redirect_to(redirect_path) + expect(flash[:notice]).to be_present + end + + it 'redirects on error' do + error = ServiceResponse.error(message: 'failed') + + expect_next_instance_of(Clusters::Integrations::CreateService) do |service| + expect(service).to receive(:execute).and_return(error) + end + + post path, params: params + + expect(response).to have_gitlab_http_status(:redirect) + expect(response).to redirect_to(redirect_path) + expect(flash[:alert]).to eq(error.message) + end + end +end diff --git a/spec/support/shared_examples/serializers/environment_serializer_shared_examples.rb b/spec/support/shared_examples/serializers/environment_serializer_shared_examples.rb new file mode 100644 index 00000000000..00146335ef7 --- /dev/null +++ b/spec/support/shared_examples/serializers/environment_serializer_shared_examples.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true +RSpec.shared_examples 'avoid N+1 on environments serialization' do + it 'avoids N+1 database queries with grouping', :request_store do + create_environment_with_associations(project) + + control = ActiveRecord::QueryRecorder.new { serialize(grouping: true) } + + create_environment_with_associations(project) + + expect { serialize(grouping: true) }.not_to exceed_query_limit(control.count) + end + + it 'avoids N+1 database queries without grouping', :request_store do + create_environment_with_associations(project) + + control = ActiveRecord::QueryRecorder.new { serialize(grouping: false) } + + create_environment_with_associations(project) + + expect { serialize(grouping: false) }.not_to exceed_query_limit(control.count) + end + + def serialize(grouping:) + EnvironmentSerializer.new(current_user: user, project: project).yield_self do |serializer| + serializer.within_folders if grouping + serializer.represent(Environment.where(project: project)) + end + end +end diff --git a/spec/support/shared_examples/services/boards/lists_destroy_service_shared_examples.rb b/spec/support/shared_examples/services/boards/lists_destroy_service_shared_examples.rb index 6a4f284ec54..94da405e491 100644 --- a/spec/support/shared_examples/services/boards/lists_destroy_service_shared_examples.rb +++ b/spec/support/shared_examples/services/boards/lists_destroy_service_shared_examples.rb @@ -13,7 +13,7 @@ RSpec.shared_examples 'lists destroy service' do development = create(:list, board: board, position: 0) review = create(:list, board: board, position: 1) staging = create(:list, board: board, position: 2) - closed = board.closed_list + closed = board.lists.closed.first described_class.new(parent, user).execute(development) @@ -24,7 +24,7 @@ RSpec.shared_examples 'lists destroy service' do end it 'does not remove list from board when list type is closed' do - list = board.closed_list + list = board.lists.closed.first service = described_class.new(parent, user) expect { service.execute(list) }.not_to change(board.lists, :count) diff --git a/spec/support/shared_examples/services/boards/lists_list_service_shared_examples.rb b/spec/support/shared_examples/services/boards/lists_list_service_shared_examples.rb index 41fd286682e..e1143562661 100644 --- a/spec/support/shared_examples/services/boards/lists_list_service_shared_examples.rb +++ b/spec/support/shared_examples/services/boards/lists_list_service_shared_examples.rb @@ -2,14 +2,34 @@ RSpec.shared_examples 'lists list service' do context 'when the board has a backlog list' do - let!(:backlog_list) { create(:backlog_list, board: board) } + let!(:backlog_list) { create_backlog_list(board) } it 'does not create a backlog list' do expect { service.execute(board) }.not_to change(board.lists, :count) end it "returns board's lists" do - expect(service.execute(board)).to eq [backlog_list, list, board.closed_list] + expect(service.execute(board)).to eq [backlog_list, list, board.lists.closed.first] + end + + context 'when hide_backlog_list is true' do + before do + board.update_column(:hide_backlog_list, true) + end + + it 'hides backlog list' do + expect(service.execute(board)).to match_array([board.lists.closed.first, list]) + end + end + + context 'when hide_closed_list is true' do + before do + board.update_column(:hide_closed_list, true) + end + + it 'hides closed list' do + expect(service.execute(board)).to match_array([backlog_list, list]) + end end end @@ -23,25 +43,21 @@ RSpec.shared_examples 'lists list service' do end it "returns board's lists" do - expect(service.execute(board)).to eq [board.backlog_list, list, board.closed_list] + expect(service.execute(board)).to eq [board.lists.backlog.first, list, board.lists.closed.first] end end context 'when wanting a specific list' do - let!(:list1) { create(:list, board: board) } - it 'returns list specified by id' do - service = described_class.new(parent, user, list_id: list1.id) + service = described_class.new(parent, user, list_id: list.id) - expect(service.execute(board, create_default_lists: false)).to eq [list1] + expect(service.execute(board, create_default_lists: false)).to eq [list] end it 'returns empty result when list is not found' do - external_board = create(:board, resource_parent: create(:project)) - external_list = create(:list, board: external_board) - service = described_class.new(parent, user, list_id: external_list.id) + service = described_class.new(parent, user, list_id: unrelated_list.id) - expect(service.execute(board, create_default_lists: false)).to eq(List.none) + expect(service.execute(board, create_default_lists: false)).to be_empty end end end diff --git a/spec/support/shared_examples/services/clusters/parse_cluster_applications_artifact_shared_examples.rb b/spec/support/shared_examples/services/clusters/parse_cluster_applications_artifact_shared_examples.rb index cbe20928f98..466300017d9 100644 --- a/spec/support/shared_examples/services/clusters/parse_cluster_applications_artifact_shared_examples.rb +++ b/spec/support/shared_examples/services/clusters/parse_cluster_applications_artifact_shared_examples.rb @@ -41,7 +41,7 @@ RSpec.shared_examples 'parse cluster applications artifact' do |release_name| end.to change(application_class, :count) expect(cluster_application).to be_persisted - expect(cluster_application).to be_installed + expect(cluster_application).to be_externally_installed end end @@ -53,7 +53,7 @@ RSpec.shared_examples 'parse cluster applications artifact' do |release_name| it 'marks the application as installed' do described_class.new(job, user).execute(artifact) - expect(cluster_application).to be_installed + expect(cluster_application).to be_externally_installed end end end diff --git a/spec/support/shared_examples/services/groups_count_service_shared_examples.rb b/spec/support/shared_examples/services/groups_count_service_shared_examples.rb new file mode 100644 index 00000000000..84937c3d4d7 --- /dev/null +++ b/spec/support/shared_examples/services/groups_count_service_shared_examples.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +# The calling spec should use `:use_clean_rails_memory_store_caching` +# when including this shared example. E.g.: +# +# describe MyCountService, :use_clean_rails_memory_store_caching do +# it_behaves_like 'a counter caching service with threshold' +# end +RSpec.shared_examples 'a counter caching service with threshold' do + let(:cache_key) { subject.cache_key } + let(:under_threshold) { described_class::CACHED_COUNT_THRESHOLD - 1 } + let(:over_threshold) { described_class::CACHED_COUNT_THRESHOLD + 1 } + + context 'when cache is empty' do + before do + Rails.cache.delete(cache_key) + end + + it 'refreshes cache if value over threshold' do + allow(subject).to receive(:uncached_count).and_return(over_threshold) + + expect(subject.count).to eq(over_threshold) + expect(Rails.cache.read(cache_key)).to eq(over_threshold) + end + + it 'does not refresh cache if value under threshold' do + allow(subject).to receive(:uncached_count).and_return(under_threshold) + + expect(subject.count).to eq(under_threshold) + expect(Rails.cache.read(cache_key)).to be_nil + end + end + + context 'when cached count is under the threshold value' do + before do + Rails.cache.write(cache_key, under_threshold) + end + + it 'does not refresh cache' do + expect(Rails.cache).not_to receive(:write) + expect(subject.count).to eq(under_threshold) + end + end + + context 'when cached count is over the threshold value' do + before do + Rails.cache.write(cache_key, over_threshold) + end + + it 'does not refresh cache' do + expect(Rails.cache).not_to receive(:write) + expect(subject.count).to eq(over_threshold) + end + end +end diff --git a/spec/support/shared_examples/services/issuable/destroy_service_shared_examples.rb b/spec/support/shared_examples/services/issuable/destroy_service_shared_examples.rb new file mode 100644 index 00000000000..ccc287c10de --- /dev/null +++ b/spec/support/shared_examples/services/issuable/destroy_service_shared_examples.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +shared_examples_for 'service deleting todos' do + before do + stub_feature_flags(destroy_issuable_todos_async: group) + end + + it 'destroys associated todos asynchronously' do + expect(TodosDestroyer::DestroyedIssuableWorker) + .to receive(:perform_async) + .with(issuable.id, issuable.class.name) + + subject.execute(issuable) + end + + context 'when destroy_issuable_todos_async feature is disabled for group' do + before do + stub_feature_flags(destroy_issuable_todos_async: false) + end + + it 'destroy associated todos synchronously' do + expect_next_instance_of(TodosDestroyer::DestroyedIssuableWorker) do |worker| + expect(worker) + .to receive(:perform) + .with(issuable.id, issuable.class.name) + end + + subject.execute(issuable) + end + end +end diff --git a/spec/support/shared_examples/services/merge_request_shared_examples.rb b/spec/support/shared_examples/services/merge_request_shared_examples.rb index 56179b6cd00..178b6bc47e1 100644 --- a/spec/support/shared_examples/services/merge_request_shared_examples.rb +++ b/spec/support/shared_examples/services/merge_request_shared_examples.rb @@ -73,3 +73,93 @@ RSpec.shared_examples 'merge request reviewers cache counters invalidator' do described_class.new(project, user, {}).execute(merge_request) end end + +RSpec.shared_examples_for 'a service that can create a merge request' do + subject(:last_mr) { MergeRequest.last } + + it 'creates a merge request with the correct target branch' do + branch = push_options[:target] || project.default_branch + + expect { service.execute }.to change { MergeRequest.count }.by(1) + expect(last_mr.target_branch).to eq(branch) + end + + context 'when project has been forked', :sidekiq_might_not_need_inline do + let(:forked_project) { fork_project(project, user1, repository: true) } + let(:service) { described_class.new(forked_project, user1, changes, push_options) } + + before do + allow(forked_project).to receive(:empty_repo?).and_return(false) + end + + it 'sets the correct source and target project' do + service.execute + + expect(last_mr.source_project).to eq(forked_project) + expect(last_mr.target_project).to eq(project) + end + end +end + +RSpec.shared_examples_for 'a service that does not create a merge request' do + it do + expect { service.execute }.not_to change { MergeRequest.count } + end +end + +# In the non-foss version of GitLab, there can be many assignees, so +# there 'count' can be something other than 0 or 1. In the foss +# version of GitLab, there can be only one assignee though, so 'count' +# can only be 0 or 1. +RSpec.shared_examples_for 'a service that can change assignees of a merge request' do |count| + subject(:last_mr) { MergeRequest.last } + + it 'changes assignee count' do + service.execute + + expect(last_mr.assignees.count).to eq(count) + end +end + +RSpec.shared_examples 'with an existing branch that has a merge request open' do |count| + let(:changes) { existing_branch_changes } + let!(:merge_request) { create(:merge_request, source_project: project, source_branch: source_branch)} + + it_behaves_like 'a service that does not create a merge request' + it_behaves_like 'a service that can change assignees of a merge request', count +end + +RSpec.shared_examples 'when coupled with the `create` push option' do |count| + let(:push_options) { { create: true, assign: assigned, unassign: unassigned } } + + it_behaves_like 'a service that can create a merge request' + it_behaves_like 'a service that can change assignees of a merge request', count +end + +RSpec.shared_examples 'with a new branch' do |count| + let(:changes) { new_branch_changes } + + it_behaves_like 'a service that does not create a merge request' + + it 'adds an error to the service' do + service.execute + + expect(service.errors).to include(error_mr_required) + end + + it_behaves_like 'when coupled with the `create` push option', count +end + +RSpec.shared_examples 'with an existing branch but no open MR' do |count| + let(:changes) { existing_branch_changes } + + it_behaves_like 'a service that does not create a merge request' + + it 'adds an error to the service' do + service.execute + + expect(service.errors).to include(error_mr_required) + end + + it_behaves_like 'when coupled with the `create` push option', count +end diff --git a/spec/support/shared_examples/services/notification_service_shared_examples.rb b/spec/support/shared_examples/services/notification_service_shared_examples.rb index 43fe6789145..cfd674e3c43 100644 --- a/spec/support/shared_examples/services/notification_service_shared_examples.rb +++ b/spec/support/shared_examples/services/notification_service_shared_examples.rb @@ -45,7 +45,7 @@ RSpec.shared_examples 'group emails are disabled' do before do reset_delivered_emails! - target_group.clear_memoization(:emails_disabled) + target_group.clear_memoization(:emails_disabled_memoized) end it 'sends no emails with group emails disabled' do diff --git a/spec/support/shared_examples/services/snippets_shared_examples.rb b/spec/support/shared_examples/services/snippets_shared_examples.rb index 10add3a7299..0c4db7ded69 100644 --- a/spec/support/shared_examples/services/snippets_shared_examples.rb +++ b/spec/support/shared_examples/services/snippets_shared_examples.rb @@ -1,7 +1,8 @@ # frozen_string_literal: true RSpec.shared_examples 'checking spam' do - let(:request) { double(:request) } + let(:request) { double(:request, headers: headers) } + let(:headers) { nil } let(:api) { true } let(:captcha_response) { 'abc123' } let(:spam_log_id) { 1 } @@ -44,6 +45,44 @@ RSpec.shared_examples 'checking spam' do subject end + context 'when CAPTCHA arguments are passed in the headers' do + let(:headers) do + { + 'X-GitLab-Spam-Log-Id' => spam_log_id, + 'X-GitLab-Captcha-Response' => captcha_response + } + end + + let(:extra_opts) do + { + request: request, + api: api, + disable_spam_action_service: disable_spam_action_service + } + end + + it 'executes the SpamActionService correctly' do + spam_params = Spam::SpamParams.new( + api: api, + captcha_response: captcha_response, + spam_log_id: spam_log_id + ) + expect_next_instance_of( + Spam::SpamActionService, + { + spammable: kind_of(Snippet), + request: request, + user: an_instance_of(User), + action: action + } + ) do |instance| + expect(instance).to receive(:execute).with(spam_params: spam_params) + end + + subject + end + end + context 'when spam action service is disabled' do let(:disable_spam_action_service) { true } diff --git a/spec/support/shared_examples/workers/in_product_marketing_email_shared_example.rb b/spec/support/shared_examples/workers/in_product_marketing_email_shared_example.rb new file mode 100644 index 00000000000..c4391f61369 --- /dev/null +++ b/spec/support/shared_examples/workers/in_product_marketing_email_shared_example.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'in-product marketing email' do + before do + stub_application_setting(in_product_marketing_emails_enabled: in_product_marketing_emails_enabled) + stub_experiment(in_product_marketing_emails: experiment_active) + allow(::Gitlab).to receive(:com?).and_return(is_gitlab_com) + end + + it 'executes the email service service' do + expect(Namespaces::InProductMarketingEmailsService).to receive(:send_for_all_tracks_and_intervals).exactly(executes_service).times + + subject.perform + end +end diff --git a/spec/support/shared_examples/workers/worker_with_data_consistency_shared_example.rb b/spec/support/shared_examples/workers/worker_with_data_consistency_shared_example.rb new file mode 100644 index 00000000000..ec09c0380f9 --- /dev/null +++ b/spec/support/shared_examples/workers/worker_with_data_consistency_shared_example.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'worker with data consistency' do |worker_class, data_consistency: :always, feature_flag: nil| + describe '.get_data_consistency_feature_flag_enabled?' do + it 'returns true' do + expect(worker_class.get_data_consistency_feature_flag_enabled?).to be(true) + end + + if feature_flag + context "when feature flag :#{feature_flag} is disabled" do + before do + stub_feature_flags(feature_flag => false) + end + + it 'returns false' do + expect(worker_class.get_data_consistency_feature_flag_enabled?).to be(false) + end + end + end + end + + describe '.get_data_consistency' do + it 'returns correct data consistency' do + expect(worker_class.get_data_consistency).to eq(data_consistency) + end + end +end -- cgit v1.2.3