diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-10-20 12:40:42 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-10-20 12:40:42 +0300 |
commit | ee664acb356f8123f4f6b00b73c1e1cf0866c7fb (patch) | |
tree | f8479f94a28f66654c6a4f6fb99bad6b4e86a40e /spec/support/shared_examples | |
parent | 62f7d5c5b69180e82ae8196b7b429eeffc8e7b4f (diff) |
Add latest changes from gitlab-org/gitlab@15-5-stable-eev15.5.0-rc42
Diffstat (limited to 'spec/support/shared_examples')
46 files changed, 1246 insertions, 512 deletions
diff --git a/spec/support/shared_examples/bulk_imports/common/pipelines/wiki_pipeline_examples.rb b/spec/support/shared_examples/bulk_imports/common/pipelines/wiki_pipeline_examples.rb index 7e7460cd602..cd4432af4ed 100644 --- a/spec/support/shared_examples/bulk_imports/common/pipelines/wiki_pipeline_examples.rb +++ b/spec/support/shared_examples/bulk_imports/common/pipelines/wiki_pipeline_examples.rb @@ -57,5 +57,53 @@ RSpec.shared_examples 'wiki pipeline imports a wiki for an entity' do expect(tracker.entity.failures.first.exception_message).to eq('Only allowed schemes are http, https') end end + + context 'when wiki is disabled' do + before do + allow_next_instance_of(BulkImports::Clients::HTTP) do |client| + allow(client) + .to receive(:get) + .and_raise( + BulkImports::NetworkError.new( + 'Unsuccessful response 403 from ...', + response: response_double + ) + ) + end + end + + describe 'unsuccessful response' do + shared_examples 'does not raise an error' do + it 'does not raise an error' do + expect(parent.wiki).not_to receive(:ensure_repository) + expect(parent.wiki.repository).not_to receive(:ensure_repository) + + expect { subject.run }.not_to raise_error + end + end + + context 'when response is forbidden' do + let(:response_double) { instance_double(HTTParty::Response, forbidden?: true, code: 403) } + + include_examples 'does not raise an error' + end + + context 'when response is not found' do + let(:response_double) { instance_double(HTTParty::Response, forbidden?: false, not_found?: true) } + + include_examples 'does not raise an error' + end + + context 'when response is not 403' do + let(:response_double) { instance_double(HTTParty::Response, forbidden?: false, not_found?: false, code: 301) } + + it 'marks tracker as failed' do + subject.run + + expect(tracker.failed?).to eq(true) + end + end + end + end end end diff --git a/spec/support/shared_examples/controllers/snowplow_event_tracking_examples.rb b/spec/support/shared_examples/controllers/snowplow_event_tracking_examples.rb index 4af3c0cc6cc..6749ebd471f 100644 --- a/spec/support/shared_examples/controllers/snowplow_event_tracking_examples.rb +++ b/spec/support/shared_examples/controllers/snowplow_event_tracking_examples.rb @@ -32,7 +32,8 @@ RSpec.shared_examples 'Snowplow event tracking' do |overrides: {}| user: try(:user), project: try(:project), label: try(:label), - property: try(:property) + property: try(:property), + context: try(:context) }.merge(overrides).compact.merge(extra) subject @@ -40,3 +41,12 @@ RSpec.shared_examples 'Snowplow event tracking' do |overrides: {}| expect_snowplow_event(**params) end end + +RSpec.shared_examples 'Snowplow event tracking with RedisHLL context' do |overrides: {}| + it_behaves_like 'Snowplow event tracking', overrides: overrides do + let(:context) do + event = try(:property) || action + [Gitlab::Tracking::ServicePingContext.new(data_source: :redis_hll, event: event).to_context.to_json] + end + end +end diff --git a/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb b/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb index 885c0229038..5d77ed5fdfc 100644 --- a/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb +++ b/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb @@ -48,7 +48,7 @@ RSpec.shared_examples 'wiki controller actions' do context 'when the wiki repository cannot be created' do before do expect(Wiki).to receive(:for_container).and_return(wiki) - expect(wiki).to receive(:wiki) { raise Wiki::CouldNotCreateWikiError } + expect(wiki).to receive(:create_wiki_repository) { raise Wiki::CouldNotCreateWikiError } end it 'redirects to the wiki container and displays an error message' do @@ -200,7 +200,7 @@ RSpec.shared_examples 'wiki controller actions' do context 'the sidebar fails to load' do before do allow(Wiki).to receive(:for_container).and_return(wiki) - wiki.wiki + wiki.create_wiki_repository expect(wiki).to receive(:find_sidebar) do raise ::Gitlab::Git::CommandTimedOut, 'Deadline Exceeded' end @@ -288,7 +288,7 @@ RSpec.shared_examples 'wiki controller actions' do context 'when page is a file' do include WikiHelpers - where(:file_name) { ['dk.png', 'unsanitized.svg', 'git-cheat-sheet.pdf'] } + where(:file_name) { ['dk.png', 'unsanitized.svg', 'sample.pdf'] } with_them do let(:id) { upload_file_to_wiki(wiki, user, file_name) } @@ -300,7 +300,7 @@ RSpec.shared_examples 'wiki controller actions' do expect(response.headers['Content-Disposition']).to match(/^inline/) expect(response.headers[Gitlab::Workhorse::DETECT_HEADER]).to eq('true') expect(response.cache_control[:public]).to be(false) - expect(response.headers['Cache-Control']).to eq('max-age=60, private') + expect(response.headers['Cache-Control']).to eq('max-age=60, private, must-revalidate, stale-while-revalidate=60, stale-if-error=300, s-maxage=60') end end end diff --git a/spec/support/shared_examples/features/access_tokens_shared_examples.rb b/spec/support/shared_examples/features/access_tokens_shared_examples.rb index 0fc45b154d8..cd255abd7a8 100644 --- a/spec/support/shared_examples/features/access_tokens_shared_examples.rb +++ b/spec/support/shared_examples/features/access_tokens_shared_examples.rb @@ -10,11 +10,11 @@ end RSpec.shared_examples 'resource access tokens creation' do |resource_type| def active_resource_access_tokens - find('.table.active-tokens') + find("[data-testid='active-tokens']") end def created_resource_access_token - find('#created-personal-access-token').value + find_field('new-access-token').value end it 'allows creation of an access token', :aggregate_failures do @@ -106,7 +106,7 @@ end RSpec.shared_examples 'active resource access tokens' do def active_resource_access_tokens - find('.table.active-tokens') + find("[data-testid='active-tokens']") end it 'shows active access tokens' do @@ -129,24 +129,22 @@ RSpec.shared_examples 'active resource access tokens' do end RSpec.shared_examples 'inactive resource access tokens' do |no_active_tokens_text| - def no_resource_access_tokens_message - find('.settings-message') + def active_resource_access_tokens + find("[data-testid='active-tokens']") end it 'allows revocation of an active token' do visit resource_settings_access_tokens_path accept_gl_confirm(button_text: 'Revoke') { click_on 'Revoke' } - expect(page).to have_selector('.settings-message') - expect(no_resource_access_tokens_message).to have_text(no_active_tokens_text) + expect(active_resource_access_tokens).to have_text(no_active_tokens_text) end it 'removes expired tokens from active section' do resource_access_token.update!(expires_at: 5.days.ago) visit resource_settings_access_tokens_path - expect(page).to have_selector('.settings-message') - expect(no_resource_access_tokens_message).to have_text(no_active_tokens_text) + expect(active_resource_access_tokens).to have_text(no_active_tokens_text) end context 'when resource access token creation is not allowed' do @@ -158,8 +156,39 @@ RSpec.shared_examples 'inactive resource access tokens' do |no_active_tokens_tex visit resource_settings_access_tokens_path accept_gl_confirm(button_text: 'Revoke') { click_on 'Revoke' } - expect(page).to have_selector('.settings-message') - expect(no_resource_access_tokens_message).to have_text(no_active_tokens_text) + expect(active_resource_access_tokens).to have_text(no_active_tokens_text) + end + end +end + +RSpec.shared_examples '#create access token' do + let(:url) { {} } + let_it_be(:admin) { create(:admin) } + let_it_be(:token_attributes) { attributes_for(:personal_access_token) } + + before do + sign_in(admin) + end + + context "when POST is successful" do + it "renders JSON with a new token" do + post url, params: { personal_access_token: token_attributes } + + parsed_body = Gitlab::Json.parse(response.body) + expect(parsed_body['new_token']).not_to be_blank + expect(parsed_body['errors']).to be_blank + expect(response).to have_gitlab_http_status(:success) + end + end + + context "when POST is unsuccessful" do + it "renders JSON with an error" do + post url, params: { personal_access_token: token_attributes.merge(scopes: []) } + + parsed_body = Gitlab::Json.parse(response.body) + expect(parsed_body['new_token']).to be_blank + expect(parsed_body['errors']).not_to be_blank + expect(response).to have_gitlab_http_status(:unprocessable_entity) end end end diff --git a/spec/support/shared_examples/features/comments_on_merge_request_files_shared_examples.rb b/spec/support/shared_examples/features/comments_on_merge_request_files_shared_examples.rb index f7cdc4c61ec..8a07e52019c 100644 --- a/spec/support/shared_examples/features/comments_on_merge_request_files_shared_examples.rb +++ b/spec/support/shared_examples/features/comments_on_merge_request_files_shared_examples.rb @@ -1,10 +1,6 @@ # frozen_string_literal: true RSpec.shared_examples 'comment on merge request file' do - before do - stub_feature_flags(remove_user_attributes_projects: false) - end - it 'adds a comment' do click_diff_line(find_by_scrolling("[id='#{sample_commit.line_code}']")) diff --git a/spec/support/shared_examples/features/content_editor_shared_examples.rb b/spec/support/shared_examples/features/content_editor_shared_examples.rb index 21f264a8b6a..7863548e7f3 100644 --- a/spec/support/shared_examples/features/content_editor_shared_examples.rb +++ b/spec/support/shared_examples/features/content_editor_shared_examples.rb @@ -35,6 +35,34 @@ RSpec.shared_examples 'edits content using the content editor' do attach_file('content_editor_image', Rails.root.join('spec', 'fixtures', fixture_name), make_visible: true) end + def wait_until_hidden_field_is_updated(value) + expect(page).to have_field('wiki[content]', with: value, type: 'hidden') + end + + it 'saves page content in local storage if the user navigates away' do + switch_to_content_editor + + expect(page).to have_css(content_editor_testid) + + type_in_content_editor ' Typing text in the content editor' + + wait_until_hidden_field_is_updated /Typing text in the content editor/ + + refresh + + expect(page).to have_text('Typing text in the content editor') + + refresh # also retained after second refresh + + expect(page).to have_text('Typing text in the content editor') + + click_link 'Cancel' # draft is deleted on cancel + + page.go_back + + expect(page).not_to have_text('Typing text in the content editor') + end + describe 'formatting bubble menu' do it 'shows a formatting bubble menu for a regular paragraph and headings' do switch_to_content_editor @@ -189,4 +217,101 @@ RSpec.shared_examples 'edits content using the content editor' do end end end + + describe 'autocomplete suggestions' do + let(:suggestions_dropdown) { '[data-testid="content-editor-suggestions-dropdown"]' } + + before do + if defined?(project) + create(:issue, project: project, title: 'My Cool Linked Issue') + create(:merge_request, source_project: project, title: 'My Cool Merge Request') + create(:label, project: project, title: 'My Cool Label') + create(:milestone, project: project, title: 'My Cool Milestone') + + project.add_maintainer(create(:user, name: 'abc123', username: 'abc123')) + else # group wikis + project = create(:project, group: group) + + create(:issue, project: project, title: 'My Cool Linked Issue') + create(:merge_request, source_project: project, title: 'My Cool Merge Request') + create(:group_label, group: group, title: 'My Cool Label') + create(:milestone, group: group, title: 'My Cool Milestone') + + project.add_maintainer(create(:user, name: 'abc123', username: 'abc123')) + end + + switch_to_content_editor + + type_in_content_editor :enter + end + + it 'shows suggestions for members with descriptions' do + type_in_content_editor '@a' + + expect(find(suggestions_dropdown)).to have_text('abc123') + expect(find(suggestions_dropdown)).to have_text('all') + expect(find(suggestions_dropdown)).to have_text('Group Members (2)') + + send_keys [:arrow_down, :enter] + + expect(page).not_to have_css(suggestions_dropdown) + expect(page).to have_text('@abc123') + end + + it 'shows suggestions for merge requests' do + type_in_content_editor '!' + + expect(find(suggestions_dropdown)).to have_text('My Cool Merge Request') + + send_keys :enter + + expect(page).not_to have_css(suggestions_dropdown) + expect(page).to have_text('!1') + end + + it 'shows suggestions for issues' do + type_in_content_editor '#' + + expect(find(suggestions_dropdown)).to have_text('My Cool Linked Issue') + + send_keys :enter + + expect(page).not_to have_css(suggestions_dropdown) + expect(page).to have_text('#1') + end + + it 'shows suggestions for milestones' do + type_in_content_editor '%' + + expect(find(suggestions_dropdown)).to have_text('My Cool Milestone') + + send_keys :enter + + expect(page).not_to have_css(suggestions_dropdown) + expect(page).to have_text('%My Cool Milestone') + end + + it 'shows suggestions for emojis' do + type_in_content_editor ':smile' + + expect(find(suggestions_dropdown)).to have_text('🙂 slight_smile') + expect(find(suggestions_dropdown)).to have_text('😸 smile_cat') + + send_keys :enter + + expect(page).not_to have_css(suggestions_dropdown) + + expect(page).to have_text('🙂') + end + + it 'doesn\'t show suggestions dropdown if there are no suggestions to show' do + type_in_content_editor '%' + + expect(find(suggestions_dropdown)).to have_text('My Cool Milestone') + + type_in_content_editor 'x' + + expect(page).not_to have_css(suggestions_dropdown) + end + end end diff --git a/spec/support/shared_examples/features/deploy_token_shared_examples.rb b/spec/support/shared_examples/features/deploy_token_shared_examples.rb index 79ad5bd6c7f..9fe08e5c996 100644 --- a/spec/support/shared_examples/features/deploy_token_shared_examples.rb +++ b/spec/support/shared_examples/features/deploy_token_shared_examples.rb @@ -14,32 +14,32 @@ RSpec.shared_examples 'a deploy token in settings' do end end - it 'add a new deploy token' do + it 'add a new deploy token', :js do visit page_path - fill_in 'deploy_token_name', with: 'new_deploy_key' - fill_in 'deploy_token_expires_at', with: (Date.today + 1.month).to_s - fill_in 'deploy_token_username', with: 'deployer' - check 'deploy_token_read_repository' - check 'deploy_token_read_registry' + fill_in _('Name'), with: 'new_deploy_key' + fill_in _('Expiration date (optional)'), with: (Date.today + 1.month).to_s + fill_in _('Username (optional)'), with: 'deployer' + check 'read_repository' + check 'read_registry' click_button 'Create deploy token' expect(page).to have_content("Your new #{entity_type} deploy token has been created") within('.created-deploy-token-container') do - expect(page).to have_selector("input[name='deploy-token-user'][value='deployer']") - expect(page).to have_selector("input[name='deploy-token'][readonly='readonly']") + expect(find("input[name='deploy-token-user']").value).to eq("deployer") + expect(find("input[name='deploy-token'][readonly='readonly']")).to be_visible end - expect(find("input#deploy_token_name").value).to eq nil + expect(find("input#deploy_token_name").value).to be_empty expect(find("input#deploy_token_read_repository").checked?).to eq false end - context "with form errors" do + context "with form errors", :js do before do visit page_path - fill_in "deploy_token_name", with: "new_deploy_key" - fill_in "deploy_token_username", with: "deployer" + fill_in _('Name'), with: "new_deploy_key" + fill_in _('Username (optional)'), with: "deployer" click_button "Create deploy token" end 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 f209070d82a..68c0d06e7d0 100644 --- a/spec/support/shared_examples/features/discussion_comments_shared_example.rb +++ b/spec/support/shared_examples/features/discussion_comments_shared_example.rb @@ -209,7 +209,7 @@ RSpec.shared_examples 'thread comments for issue, epic and merge request' do |re wait_for_all_requests expect(page).to have_content(comment) - expect(page).to have_content "@#{user.username} closed" + expect(page).to have_content "#{user.name} closed" new_comment = all(comments_selector).last @@ -334,7 +334,7 @@ RSpec.shared_examples 'thread comments for issue, epic and merge request' do |re click_button 'Start thread & close issue' expect(page).to have_content(comment) - expect(page).to have_content "@#{user.username} closed" + expect(page).to have_content "#{user.name} closed" new_discussion = all(comments_selector)[-2] 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 0a5ad5a59c0..7737f8a73c5 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 @@ -77,9 +77,9 @@ RSpec.shared_examples 'it uploads and commits a new pdf file' do |drop: false| end if drop - find(".upload-dropzone-card").drop(File.join(Rails.root, 'spec', 'fixtures', 'git-cheat-sheet.pdf')) + find(".upload-dropzone-card").drop(File.join(Rails.root, 'spec', 'fixtures', 'sample.pdf')) else - attach_file('upload_file', File.join(Rails.root, 'spec', 'fixtures', 'git-cheat-sheet.pdf'), make_visible: true) + attach_file('upload_file', File.join(Rails.root, 'spec', 'fixtures', 'sample.pdf'), make_visible: true) end page.within('#modal-upload-blob') do @@ -90,7 +90,7 @@ RSpec.shared_examples 'it uploads and commits a new pdf file' do |drop: false| wait_for_all_requests - visit(project_blob_path(project, 'upload_image/git-cheat-sheet.pdf')) + visit(project_blob_path(project, 'upload_image/sample.pdf')) expect(page).to have_css('.js-pdf-viewer') end diff --git a/spec/support/shared_examples/features/runners_shared_examples.rb b/spec/support/shared_examples/features/runners_shared_examples.rb index 31ee08ea9db..1d4af944187 100644 --- a/spec/support/shared_examples/features/runners_shared_examples.rb +++ b/spec/support/shared_examples/features/runners_shared_examples.rb @@ -146,6 +146,18 @@ RSpec.shared_examples 'pauses, resumes and deletes a runner' do end end +RSpec.shared_examples 'filters by tag' do + it 'shows correct runner when tag matches' do + expect(page).to have_content found_runner + expect(page).to have_content missing_runner + + input_filtered_search_filter_is_only('Tags', tag) + + expect(page).to have_content found_runner + expect(page).not_to have_content missing_runner + end +end + RSpec.shared_examples 'submits edit runner form' do it 'breadcrumb contains runner id and token' do page.within '[data-testid="breadcrumb-links"]' do diff --git a/spec/support/shared_examples/features/search/search_timeouts_shared_examples.rb b/spec/support/shared_examples/features/search/search_timeouts_shared_examples.rb index 095c48cade8..84dc2b20ddc 100644 --- a/spec/support/shared_examples/features/search/search_timeouts_shared_examples.rb +++ b/spec/support/shared_examples/features/search/search_timeouts_shared_examples.rb @@ -3,6 +3,7 @@ RSpec.shared_examples 'search timeouts' do |scope| context 'when search times out' do before do + stub_feature_flags(search_page_vertical_nav: false) allow_next_instance_of(SearchService) do |service| allow(service).to receive(:search_objects).and_raise(ActiveRecord::QueryCanceled) end diff --git a/spec/support/shared_examples/features/sidebar/sidebar_due_date_shared_examples.rb b/spec/support/shared_examples/features/sidebar/sidebar_due_date_shared_examples.rb index 95c0a76d726..206116d66c8 100644 --- a/spec/support/shared_examples/features/sidebar/sidebar_due_date_shared_examples.rb +++ b/spec/support/shared_examples/features/sidebar/sidebar_due_date_shared_examples.rb @@ -20,6 +20,8 @@ RSpec.shared_examples 'date sidebar widget' do scroll_to(button) button.click + execute_script('document.querySelector(".issuable-sidebar")?.scrollBy(0, 50)') + click_button today.to_s wait_for_requests 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 8081c51577a..ed885d7a226 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 @@ -147,6 +147,18 @@ RSpec.shared_examples 'User creates wiki page' do end end + it 'saves page content in local storage if the user navigates away', :js do + fill_in(:wiki_title, with: "Test title") + fill_in(:wiki_content, with: "This is a test") + fill_in(:wiki_message, with: "Test commit message") + + refresh + + expect(page).to have_field(:wiki_title, with: "Test title") + expect(page).to have_field(:wiki_content, with: "This is a test") + expect(page).to have_field(:wiki_message, with: "Test commit message") + end + it 'creates a wiki page with Org markup', :aggregate_failures, :js do org_content = <<~ORG * Heading 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 5c63d6a973d..0334187e4b1 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 @@ -78,6 +78,18 @@ RSpec.shared_examples 'User updates wiki page' do expect(page).to have_content('My awesome wiki!') end + it 'saves page content in local storage if the user navigates away', :js do + fill_in(:wiki_title, with: "Test title") + fill_in(:wiki_content, with: "This is a test") + fill_in(:wiki_message, with: "Test commit message") + + refresh + + expect(page).to have_field(:wiki_title, with: "Test title") + expect(page).to have_field(:wiki_content, with: "This is a test") + expect(page).to have_field(:wiki_message, with: "Test commit message") + end + it 'updates the commit message as the title is changed', :js do fill_in(:wiki_title, with: '& < > \ \ { } &') diff --git a/spec/support/shared_examples/features/wiki/user_views_wiki_pages_shared_examples.rb b/spec/support/shared_examples/features/wiki/user_views_wiki_pages_shared_examples.rb index 32cb2b1d187..9b5326026b1 100644 --- a/spec/support/shared_examples/features/wiki/user_views_wiki_pages_shared_examples.rb +++ b/spec/support/shared_examples/features/wiki/user_views_wiki_pages_shared_examples.rb @@ -53,37 +53,4 @@ RSpec.shared_examples 'User views wiki pages' do end end end - - context 'ordered by created_at' do - let(:pages_ordered_by_created_at) { [wiki_page1, wiki_page2, wiki_page3] } - - before do - page.within('.wiki-sort-dropdown') do - click_button('Title') - click_button('Created date') - end - end - - context 'asc' do - it 'pages are displayed in direct order' do - pages.each.with_index do |page_title, index| - expect(page_title.text).to eq(pages_ordered_by_created_at[index].title) - end - end - end - - context 'desc' do - before do - page.within('.wiki-sort-dropdown') do - page.find('.rspec-reverse-sort').click - end - end - - it 'pages are displayed in reversed order' do - pages.reverse_each.with_index do |page_title, index| - expect(page_title.text).to eq(pages_ordered_by_created_at[index].title) - end - end - end - end end diff --git a/spec/support/shared_examples/graphql/n_plus_one_query_examples.rb b/spec/support/shared_examples/graphql/n_plus_one_query_examples.rb index faf1bb204c9..b4afde311ba 100644 --- a/spec/support/shared_examples/graphql/n_plus_one_query_examples.rb +++ b/spec/support/shared_examples/graphql/n_plus_one_query_examples.rb @@ -1,13 +1,23 @@ # frozen_string_literal: true -RSpec.shared_examples 'N+1 query check' do + +RSpec.shared_examples 'N+1 query check' do |threshold: 0, skip_cached: true| it 'prevents N+1 queries' do execute_query # "warm up" to prevent undeterministic counts expect(graphql_errors).to be_blank # Sanity check - ex falso quodlibet! - control = ActiveRecord::QueryRecorder.new { execute_query } + control = ActiveRecord::QueryRecorder.new(skip_cached: skip_cached) { execute_query } expect(control.count).to be > 0 search_params[:iids] << extra_iid_for_second_query - expect { execute_query }.not_to exceed_query_limit(control) + + expect { execute_query }.not_to exceed_query_count_limit(control, skip_cached: skip_cached, threshold: threshold) + end + + def exceed_query_count_limit(control, skip_cached: true, threshold: 0) + if skip_cached + exceed_query_limit(control).with_threshold(threshold) + else + exceed_all_query_limit(control).with_threshold(threshold) + end end end diff --git a/spec/support/shared_examples/lib/cache_helpers_shared_examples.rb b/spec/support/shared_examples/lib/cache_helpers_shared_examples.rb index 82a9e8130f7..2e00abe2f8e 100644 --- a/spec/support/shared_examples/lib/cache_helpers_shared_examples.rb +++ b/spec/support/shared_examples/lib/cache_helpers_shared_examples.rb @@ -54,31 +54,6 @@ RSpec.shared_examples_for 'object cache helper' do allow(Gitlab::ApplicationContext).to receive(:current_context_attribute).with(:caller_id).and_return(caller_id) end - context 'when feature flag is off' do - before do - stub_feature_flags(add_timing_to_certain_cache_actions: false) - end - - it 'does not call increment' do - expect(transaction).not_to receive(:increment).with(:cached_object_operations_total, any_args) - - subject - end - - it 'does not call histogram' do - expect(Gitlab::Metrics).not_to receive(:histogram) - - subject - end - - it "is valid JSON" do - parsed = Gitlab::Json.parse(subject.to_s) - - expect(parsed).to be_a(Hash) - expect(parsed["id"]).to eq(presentable.id) - end - end - it 'increments the counter' do expect(transaction) .to receive(:increment) @@ -157,34 +132,6 @@ RSpec.shared_examples_for 'collection cache helper' do allow(Gitlab::ApplicationContext).to receive(:current_context_attribute).with(:caller_id).and_return(caller_id) end - context 'when feature flag is off' do - before do - stub_feature_flags(add_timing_to_certain_cache_actions: false) - end - - it 'does not call increment' do - expect(transaction).not_to receive(:increment).with(:cached_object_operations_total, any_args) - - subject - end - - it 'does not call histogram' do - expect(Gitlab::Metrics).not_to receive(:histogram) - - subject - end - - it "is valid JSON" do - parsed = Gitlab::Json.parse(subject.to_s) - - expect(parsed).to be_an(Array) - - presentable.each_with_index do |item, i| - expect(parsed[i]["id"]).to eq(item.id) - end - end - end - context 'when presentable has a group by clause' do let(:presentable) { MergeRequest.group(:id) } diff --git a/spec/support/shared_examples/lib/gitlab/memory/watchdog/monitor_result_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/memory/watchdog/monitor_result_shared_examples.rb new file mode 100644 index 00000000000..98c0e7d506b --- /dev/null +++ b/spec/support/shared_examples/lib/gitlab/memory/watchdog/monitor_result_shared_examples.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'returns Watchdog Monitor result' do |threshold_violated:| + it 'returns if threshold is violated and payload' do + result = monitor.call + + expect(result[:threshold_violated]).to eq(threshold_violated) + expect(result[:payload]).to eq(payload) + end +end diff --git a/spec/support/shared_examples/lib/gitlab/regex_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/regex_shared_examples.rb new file mode 100644 index 00000000000..150741c6344 --- /dev/null +++ b/spec/support/shared_examples/lib/gitlab/regex_shared_examples.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'regex rejecting path traversal' do + it { is_expected.not_to match('a../b') } + it { is_expected.not_to match('a..%2fb') } + it { is_expected.not_to match('a%2e%2e%2fb') } + it { is_expected.not_to match('a%2e%2e/b') } +end diff --git a/spec/support/shared_examples/lib/gitlab/template/template_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/template/template_shared_examples.rb index 6b6e25ca1dd..4b4a7f4ce9d 100644 --- a/spec/support/shared_examples/lib/gitlab/template/template_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/template/template_shared_examples.rb @@ -47,3 +47,47 @@ RSpec.shared_examples 'file template shared examples' do |filename, file_extensi end end end + +RSpec.shared_examples 'acts as branch pipeline' do |jobs| + context 'when branch pipeline' do + let(:pipeline_branch) { default_branch } + let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_branch) } + let(:pipeline) { service.execute!(:push).payload } + + it 'includes a job' do + expect(pipeline.builds.pluck(:name)).to match_array(jobs) + end + end +end + +RSpec.shared_examples 'acts as MR pipeline' do |jobs, files| + context 'when MR pipeline' do + let(:pipeline_branch) { 'patch-1' } + let(:service) { MergeRequests::CreatePipelineService.new(project: project, current_user: user) } + let(:pipeline) { service.execute(merge_request).payload } + + let(:merge_request) do + create(:merge_request, + source_project: project, + source_branch: pipeline_branch, + target_project: project, + target_branch: default_branch) + end + + before do + files.each do |filename, contents| + project.repository.create_file( + project.creator, + filename, + contents, + message: "Add #{filename}", + branch_name: pipeline_branch) + end + end + + it 'includes a job' do + expect(pipeline).to be_merge_request_event + expect(pipeline.builds.pluck(:name)).to match_array(jobs) + end + end +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 250a4c1b1bd..ac8655a907f 100644 --- a/spec/support/shared_examples/models/boards/listable_shared_examples.rb +++ b/spec/support/shared_examples/models/boards/listable_shared_examples.rb @@ -27,14 +27,6 @@ RSpec.shared_examples 'boards listable model' do |list_factory| .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 describe '#destroyable?' do diff --git a/spec/support/shared_examples/models/chat_integration_shared_examples.rb b/spec/support/shared_examples/models/chat_integration_shared_examples.rb index 6cfeeabc952..6d0462a9ee8 100644 --- a/spec/support/shared_examples/models/chat_integration_shared_examples.rb +++ b/spec/support/shared_examples/models/chat_integration_shared_examples.rb @@ -166,10 +166,14 @@ RSpec.shared_examples "chat integration" do |integration_name| let(:opts) { { title: "Awesome issue", description: "please fix" } } let(:sample_data) do service = Issues::CreateService.new(project: project, current_user: user, params: opts, spam_params: nil) - issue = service.execute + issue = service.execute[:issue] service.hook_data(issue, "open") end + before do + project.add_developer(user) + end + it_behaves_like "triggered #{integration_name} integration" end diff --git a/spec/support/shared_examples/models/concerns/cascading_namespace_setting_shared_examples.rb b/spec/support/shared_examples/models/concerns/cascading_namespace_setting_shared_examples.rb new file mode 100644 index 00000000000..a4db4e25db3 --- /dev/null +++ b/spec/support/shared_examples/models/concerns/cascading_namespace_setting_shared_examples.rb @@ -0,0 +1,355 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.shared_examples 'a cascading namespace setting boolean attribute' do + |settings_attribute_name:, settings_association: :namespace_settings| + let_it_be_with_reload(:group) { create(:group) } + let_it_be_with_reload(:subgroup) { create(:group, parent: group) } + let(:group_settings) { group.send(settings_association) } + let(:subgroup_settings) { subgroup.send(settings_association) } + + describe "##{settings_attribute_name}" do + subject(:cascading_attribute) { subgroup_settings.send(settings_attribute_name) } + + before do + stub_application_setting(settings_attribute_name => false) + end + + context 'when there is no parent' do + context 'and the value is not nil' do + before do + group_settings.update!(settings_attribute_name => true) + end + + it 'returns the local value' do + expect(group_settings.send(settings_attribute_name)).to eq(true) + end + end + + context 'and the value is nil' do + before do + group_settings.update!(settings_attribute_name => nil) + end + + it 'returns the application settings value' do + expect(group_settings.send(settings_attribute_name)).to eq(false) + end + end + end + + context 'when parent does not lock the attribute' do + context 'and value is not nil' do + before do + group_settings.update!(settings_attribute_name => false) + end + + it 'returns local setting when present' do + subgroup_settings.update!(settings_attribute_name => true) + + expect(cascading_attribute).to eq(true) + end + + it 'returns the parent value when local value is nil' do + subgroup_settings.update!(settings_attribute_name => nil) + + expect(cascading_attribute).to eq(false) + end + + it 'returns the correct dirty value' do + subgroup_settings.send("#{settings_attribute_name}=", true) + + expect(cascading_attribute).to eq(true) + end + + it 'does not return the application setting value when parent value is false' do + stub_application_setting(settings_attribute_name => true) + + expect(cascading_attribute).to eq(false) + end + end + + context 'and the value is nil' do + before do + group_settings.update!(settings_attribute_name => nil, "lock_#{settings_attribute_name}".to_sym => false) + subgroup_settings.update!(settings_attribute_name => nil) + + subgroup_settings.clear_memoization(settings_attribute_name) + end + + it 'cascades to the application settings value' do + expect(cascading_attribute).to eq(false) + end + end + + context 'when multiple ancestors set a value' do + let(:third_level_subgroup) { create(:group, parent: subgroup) } + + before do + group_settings.update!(settings_attribute_name => true) + subgroup_settings.update!(settings_attribute_name => false) + end + + it 'returns the closest ancestor value' do + expect(third_level_subgroup.send(settings_association).send(settings_attribute_name)).to eq(false) + end + end + end + + context 'when parent locks the attribute' do + before do + subgroup_settings.update!(settings_attribute_name => true) + group_settings.update!("lock_#{settings_attribute_name}" => true, settings_attribute_name => false) + + subgroup_settings.clear_memoization(settings_attribute_name) + subgroup_settings.clear_memoization("#{settings_attribute_name}_locked_ancestor") + end + + it 'returns the parent value' do + expect(cascading_attribute).to eq(false) + end + + it 'does not allow the local value to be saved' do + subgroup_settings.send("#{settings_attribute_name}=", nil) + + expect { subgroup_settings.save! } + .to raise_error(ActiveRecord::RecordInvalid, + /cannot be changed because it is locked by an ancestor/) + end + end + + context 'when the application settings locks the attribute' do + before do + subgroup_settings.update!(settings_attribute_name => true) + stub_application_setting("lock_#{settings_attribute_name}" => true, settings_attribute_name => true) + end + + it 'returns the application setting value' do + expect(cascading_attribute).to eq(true) + end + + it 'does not allow the local value to be saved' do + subgroup_settings.send("#{settings_attribute_name}=", false) + + expect { subgroup_settings.save! } + .to raise_error( + ActiveRecord::RecordInvalid, + /cannot be changed because it is locked by an ancestor/ + ) + end + end + + context 'when parent locked the attribute then the application settings locks it' do + before do + subgroup_settings.update!(settings_attribute_name => true) + group_settings.update!("lock_#{settings_attribute_name}" => true, settings_attribute_name => false) + stub_application_setting("lock_#{settings_attribute_name}" => true, settings_attribute_name => true) + + subgroup_settings.clear_memoization(settings_attribute_name) + subgroup_settings.clear_memoization("#{settings_attribute_name}_locked_ancestor") + end + + it 'returns the application setting value' do + expect(cascading_attribute).to eq(true) + end + end + end + + describe "##{settings_attribute_name}?" do + before do + subgroup_settings.update!(settings_attribute_name => true) + group_settings.update!("lock_#{settings_attribute_name}" => true, settings_attribute_name => false) + + subgroup_settings.clear_memoization(settings_attribute_name) + subgroup_settings.clear_memoization("#{settings_attribute_name}_locked_ancestor") + end + + it 'aliases the method when the attribute is a boolean' do + expect(subgroup_settings.send("#{settings_attribute_name}?")) + .to eq(subgroup_settings.send(settings_attribute_name)) + end + end + + describe "##{settings_attribute_name}=" do + before do + subgroup_settings.update!(settings_attribute_name => nil) + group_settings.update!(settings_attribute_name => true) + end + + it 'does not save the value locally when it matches the cascaded value' do + subgroup_settings.update!(settings_attribute_name => true) + + expect(subgroup_settings.read_attribute(settings_attribute_name)).to eq(nil) + end + end + + describe "##{settings_attribute_name}_locked?" do + shared_examples 'not locked' do + it 'is not locked by an ancestor' do + expect(subgroup_settings.send("#{settings_attribute_name}_locked_by_ancestor?")).to eq(false) + end + + it 'is not locked by application setting' do + expect(subgroup_settings.send("#{settings_attribute_name}_locked_by_application_setting?")).to eq(false) + end + + it 'does not return a locked namespace' do + expect(subgroup_settings.send("#{settings_attribute_name}_locked_ancestor")).to be_nil + end + end + + context 'when attribute is locked by self' do + before do + subgroup_settings.update!("lock_#{settings_attribute_name}" => true) + end + + it 'is not locked by default' do + expect(subgroup_settings.send("#{settings_attribute_name}_locked?")).to eq(false) + end + + it 'is locked when including self' do + expect(subgroup_settings.send("#{settings_attribute_name}_locked?", include_self: true)).to eq(true) + end + end + + context 'when parent does not lock the attribute' do + it_behaves_like 'not locked' + end + + context 'when parent locks the attribute' do + before do + group_settings.update!("lock_#{settings_attribute_name}".to_sym => true, settings_attribute_name => false) + + subgroup_settings.clear_memoization(settings_attribute_name) + subgroup_settings.clear_memoization("#{settings_attribute_name}_locked_ancestor") + end + + it 'is locked by an ancestor' do + expect(subgroup_settings.send("#{settings_attribute_name}_locked_by_ancestor?")).to eq(true) + end + + it 'is not locked by application setting' do + expect(subgroup_settings.send("#{settings_attribute_name}_locked_by_application_setting?")).to eq(false) + end + + it 'returns a locked namespace settings object' do + expect(subgroup_settings.send("#{settings_attribute_name}_locked_ancestor").namespace_id) + .to eq(group_settings.namespace_id) + end + end + + context 'when not locked by application settings' do + before do + stub_application_setting("lock_#{settings_attribute_name}" => false) + end + + it_behaves_like 'not locked' + end + + context 'when locked by application settings' do + before do + stub_application_setting("lock_#{settings_attribute_name}" => true) + end + + it 'is not locked by an ancestor' do + expect(subgroup_settings.send("#{settings_attribute_name}_locked_by_ancestor?")).to eq(false) + end + + it 'is locked by application setting' do + expect(subgroup_settings.send("#{settings_attribute_name}_locked_by_application_setting?")).to eq(true) + end + + it 'does not return a locked namespace' do + expect(subgroup_settings.send("#{settings_attribute_name}_locked_ancestor")).to be_nil + end + end + end + + describe "#lock_#{settings_attribute_name}=" do + context 'when parent locks the attribute' do + before do + group_settings.update!("lock_#{settings_attribute_name}".to_sym => true, settings_attribute_name => false) + + subgroup_settings.clear_memoization(settings_attribute_name) + subgroup_settings.clear_memoization("#{settings_attribute_name}_locked_ancestor") + end + + it 'does not allow the attribute to be saved' do + subgroup_settings.send("lock_#{settings_attribute_name}=", true) + + expect { subgroup_settings.save! } + .to raise_error(ActiveRecord::RecordInvalid, + /cannot be changed because it is locked by an ancestor/) + end + end + + context 'when parent does not lock the attribute' do + before do + group_settings.update!("lock_#{settings_attribute_name}" => false, settings_attribute_name => false) + + subgroup_settings.send("lock_#{settings_attribute_name}=", true) + end + + it 'allows the lock to be set when the attribute is not nil' do + subgroup_settings.send("#{settings_attribute_name}=", true) + + expect(subgroup_settings.save).to eq(true) + end + + it 'does not allow the lock to be saved when the attribute is nil' do + subgroup_settings.send("#{settings_attribute_name}=", nil) + + expect { subgroup_settings.save! } + .to raise_error(ActiveRecord::RecordInvalid, + /cannot be nil when locking the attribute/) + end + + it 'copies the cascaded value when locking the attribute if the local value is nil', :aggregate_failures do + subgroup_settings.send("#{settings_attribute_name}=", nil) + subgroup_settings.send("lock_#{settings_attribute_name}=", true) + + expect(subgroup_settings.read_attribute(settings_attribute_name)).to eq(false) + end + end + + context 'when application settings locks the attribute' do + before do + stub_application_setting("lock_#{settings_attribute_name}".to_sym => true) + end + + it 'does not allow the attribute to be saved' do + subgroup_settings.send("lock_#{settings_attribute_name}=", true) + + expect { subgroup_settings.save! } + .to raise_error(ActiveRecord::RecordInvalid, + /cannot be changed because it is locked by an ancestor/) + end + end + + context 'when application_settings does not lock the attribute' do + before do + stub_application_setting("lock_#{settings_attribute_name}".to_sym => false) + end + + it 'allows the attribute to be saved' do + subgroup_settings.send("#{settings_attribute_name}=", true) + subgroup_settings.send("lock_#{settings_attribute_name}=", true) + + expect(subgroup_settings.save).to eq(true) + end + end + end + + describe 'after update callback' do + before do + group_settings.update!("lock_#{settings_attribute_name}" => false, settings_attribute_name => false) + subgroup_settings.update!("lock_#{settings_attribute_name}" => true, settings_attribute_name => false) + end + + it 'clears descendant locks' do + group_settings.update!("lock_#{settings_attribute_name}" => true, settings_attribute_name => true) + + expect(subgroup_settings.reload.send("lock_#{settings_attribute_name}")).to eq(false) + end + end +end diff --git a/spec/support/shared_examples/models/concerns/counter_attribute_shared_examples.rb b/spec/support/shared_examples/models/concerns/counter_attribute_shared_examples.rb index f3a12578912..a658d02f09a 100644 --- a/spec/support/shared_examples/models/concerns/counter_attribute_shared_examples.rb +++ b/spec/support/shared_examples/models/concerns/counter_attribute_shared_examples.rb @@ -92,8 +92,8 @@ RSpec.shared_examples_for CounterAttribute do |counter_attributes| it 'obtains an exclusive lease during processing' do expect(model) .to receive(:in_lock) - .with(model.counter_lock_key(incremented_attribute), ttl: described_class::WORKER_LOCK_TTL) - .and_call_original + .with(model.counter_lock_key(incremented_attribute), ttl: described_class::WORKER_LOCK_TTL) + .and_call_original subject end @@ -104,7 +104,14 @@ RSpec.shared_examples_for CounterAttribute do |counter_attributes| model.delayed_increment_counter(incremented_attribute, -3) end - it 'updates the record and logs it' do + it 'updates the record and logs it', :aggregate_failures do + expect(Gitlab::AppLogger).to receive(:info).with( + hash_including( + message: 'Acquiring lease for project statistics update', + attributes: [incremented_attribute] + ) + ) + expect(Gitlab::AppLogger).to receive(:info).with( hash_including( message: 'Flush counter attribute to database', @@ -124,14 +131,14 @@ RSpec.shared_examples_for CounterAttribute do |counter_attributes| it 'removes the increment entry from Redis' do Gitlab::Redis::SharedState.with do |redis| - key_exists = redis.exists(model.counter_key(incremented_attribute)) + key_exists = redis.exists?(model.counter_key(incremented_attribute)) expect(key_exists).to be_truthy end subject Gitlab::Redis::SharedState.with do |redis| - key_exists = redis.exists(model.counter_key(incremented_attribute)) + key_exists = redis.exists?(model.counter_key(incremented_attribute)) expect(key_exists).to be_falsey end end @@ -162,7 +169,7 @@ RSpec.shared_examples_for CounterAttribute do |counter_attributes| subject Gitlab::Redis::SharedState.with do |redis| - key_exists = redis.exists(model.counter_flushed_key(incremented_attribute)) + key_exists = redis.exists?(model.counter_flushed_key(incremented_attribute)) expect(key_exists).to be_falsey end end @@ -186,31 +193,88 @@ RSpec.shared_examples_for CounterAttribute do |counter_attributes| end end - describe '#clear_counter!' do + describe '#reset_counter!' do let(:attribute) { counter_attributes.first } before do + model.update!(attribute => 123) model.increment_counter(attribute, 10) end - it 'deletes the counter key for the given attribute and logs it' do - expect(Gitlab::AppLogger).to receive(:info).with( - hash_including( - message: 'Clear counter attribute', - attribute: attribute, - project_id: model.project_id, - 'correlation_id' => an_instance_of(String), - 'meta.feature_category' => 'test', - 'meta.caller_id' => 'caller' - ) - ) + subject { model.reset_counter!(attribute) } - model.clear_counter!(attribute) + it 'resets the attribute value to 0 and clears existing counter', :aggregate_failures do + expect { subject }.to change { model.reload.send(attribute) }.from(123).to(0) Gitlab::Redis::SharedState.with do |redis| - key_exists = redis.exists(model.counter_key(attribute)) + key_exists = redis.exists?(model.counter_key(attribute)) expect(key_exists).to be_falsey end end + + it_behaves_like 'obtaining lease to update database' do + context 'when the execution raises error' do + before do + allow(model).to receive(:update!).and_raise(StandardError, 'Something went wrong') + end + + it 'reraises error' do + expect { subject }.to raise_error(StandardError, 'Something went wrong') + end + end + end + end + + describe '#update_counters_with_lease' do + let(:increments) { { build_artifacts_size: 1, packages_size: 2 } } + + subject { model.update_counters_with_lease(increments) } + + it 'updates counters of the record' do + expect { subject } + .to change { model.reload.build_artifacts_size }.by(1) + .and change { model.reload.packages_size }.by(2) + end + + it_behaves_like 'obtaining lease to update database' do + context 'when the execution raises error' do + before do + allow(model.class).to receive(:update_counters).and_raise(StandardError, 'Something went wrong') + end + + it 'reraises error' do + expect { subject }.to raise_error(StandardError, 'Something went wrong') + end + end + end + end +end + +RSpec.shared_examples 'obtaining lease to update database' do + context 'when it is unable to obtain lock' do + before do + allow(model).to receive(:in_lock).and_raise(Gitlab::ExclusiveLeaseHelpers::FailedToObtainLockError) + end + + it 'logs a warning' do + allow(model).to receive(:in_lock).and_raise(Gitlab::ExclusiveLeaseHelpers::FailedToObtainLockError) + + expect(Gitlab::AppLogger).to receive(:warn).once + + expect { subject }.not_to raise_error + end + end + + context 'when feature flag counter_attribute_db_lease_for_update is disabled' do + before do + stub_feature_flags(counter_attribute_db_lease_for_update: false) + allow(model).to receive(:in_lock).and_call_original + end + + it 'does not attempt to get a lock' do + expect(model).not_to receive(:in_lock) + + subject + end end end diff --git a/spec/support/shared_examples/models/concerns/has_wiki_shared_examples.rb b/spec/support/shared_examples/models/concerns/has_wiki_shared_examples.rb index 0357b7462fb..65bc6c10490 100644 --- a/spec/support/shared_examples/models/concerns/has_wiki_shared_examples.rb +++ b/spec/support/shared_examples/models/concerns/has_wiki_shared_examples.rb @@ -15,7 +15,7 @@ RSpec.shared_examples 'model with wiki' do context 'when the repository cannot be created' do before do - expect(container.wiki).to receive(:wiki) { raise Wiki::CouldNotCreateWikiError } + expect(container.wiki).to receive(:create_wiki_repository) { raise Wiki::CouldNotCreateWikiError } end it 'returns false and adds a validation error' do diff --git a/spec/support/shared_examples/models/concerns/participable_shared_examples.rb b/spec/support/shared_examples/models/concerns/participable_shared_examples.rb new file mode 100644 index 00000000000..ec7a9105bb2 --- /dev/null +++ b/spec/support/shared_examples/models/concerns/participable_shared_examples.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'visible participants for issuable with read ability' do |model_class| + let_it_be(:user1) { create(:user) } + + let(:model) { model_class.to_s.classify.constantize } + + before do + allow(Ability).to receive(:allowed?).with(anything, :"read_#{model_class}", anything).and_return(true) + allow(model).to receive(:participant_attrs).and_return([:bar]) + end + + shared_examples 'check for participables read ability' do |ability_name| + it 'receives expected ability' do + instance = model.new + + allow(instance).to receive(:bar).and_return(participable_source) + + expect(Ability).to receive(:allowed?).with(anything, ability_name, instance) + + expect(instance.visible_participants(user1)).to be_empty + end + end + + context 'when source is an award emoji' do + let(:participable_source) { build(:award_emoji, :upvote) } + + it_behaves_like 'check for participables read ability', :read_issuable_participables + end + + context 'when source is a note' do + let(:participable_source) { build(:note) } + + it_behaves_like 'check for participables read ability', :read_note + end + + context 'when source is an internal note' do + let(:participable_source) { build(:note, :confidential) } + + it_behaves_like 'check for participables read ability', :read_internal_note + end +end diff --git a/spec/support/shared_examples/models/concerns/timebox_shared_examples.rb b/spec/support/shared_examples/models/concerns/timebox_shared_examples.rb index d06e8391a9a..e4958779957 100644 --- a/spec/support/shared_examples/models/concerns/timebox_shared_examples.rb +++ b/spec/support/shared_examples/models/concerns/timebox_shared_examples.rb @@ -1,10 +1,7 @@ # frozen_string_literal: true RSpec.shared_examples 'a timebox' do |timebox_type| - let(:project) { create(:project, :public) } - let(:group) { create(:group) } let(:timebox_args) { [] } - let(:timebox) { create(timebox_type, *timebox_args, project: project) } let(:issue) { create(:issue, project: project) } let(:user) { create(:user) } let(:timebox_table_name) { timebox_type.to_s.pluralize.to_sym } @@ -14,28 +11,6 @@ RSpec.shared_examples 'a timebox' do |timebox_type| let(:open_on_left) { nil } let(:open_on_right) { nil } - describe 'modules' do - context 'with a project' do - it_behaves_like 'AtomicInternalId' do - let(:internal_id_attribute) { :iid } - let(:instance) { build(timebox_type, *timebox_args, project: create(:project), group: nil) } - let(:scope) { :project } - let(:scope_attrs) { { project: instance.project } } - let(:usage) { timebox_table_name } - end - end - - context 'with a group' do - it_behaves_like 'AtomicInternalId' do - let(:internal_id_attribute) { :iid } - let(:instance) { build(timebox_type, *timebox_args, project: nil, group: create(:group)) } - let(:scope) { :group } - let(:scope_attrs) { { namespace: instance.group } } - let(:usage) { timebox_table_name } - end - end - end - describe "Validation" do before do allow(subject).to receive(:set_iid).and_return(false) @@ -65,21 +40,9 @@ RSpec.shared_examples 'a timebox' do |timebox_type| expect(timebox.errors[:due_date]).to include("date must not be after 9999-12-31") end end - - describe '#timebox_type_check' do - it 'is invalid if it has both project_id and group_id' do - timebox = build(timebox_type, *timebox_args, group: group) - timebox.project = project - - expect(timebox).not_to be_valid - expect(timebox.errors[:project_id]).to include("#{timebox_type} should belong either to a project or a group.") - end - end end describe "Associations" do - it { is_expected.to belong_to(:project) } - it { is_expected.to belong_to(:group) } it { is_expected.to have_many(:issues) } it { is_expected.to have_many(:merge_requests) } it { is_expected.to have_many(:labels).through(:issues) } @@ -91,38 +54,6 @@ RSpec.shared_examples 'a timebox' do |timebox_type| end end - describe '#project_timebox?' do - context 'when project_id is present' do - it 'returns true' do - expect(timebox.project_timebox?).to be_truthy - end - end - - context 'when project_id is not present' do - let(:timebox) { build(timebox_type, *timebox_args, group: group) } - - it 'returns false' do - expect(timebox.project_timebox?).to be_falsey - end - end - end - - describe '#group_timebox?' do - context 'when group_id is present' do - let(:timebox) { build(timebox_type, *timebox_args, group: group) } - - it 'returns true' do - expect(timebox.group_timebox?).to be_truthy - end - end - - context 'when group_id is not present' do - it 'returns false' do - expect(timebox.group_timebox?).to be_falsey - end - end - end - describe '#safe_title' do let(:timebox) { create(timebox_type, *timebox_args, title: "<b>foo & bar -> 2.2</b>") } @@ -131,22 +62,6 @@ RSpec.shared_examples 'a timebox' do |timebox_type| end end - describe '#resource_parent' do - context 'when group is present' do - let(:timebox) { build(timebox_type, *timebox_args, group: group) } - - it 'returns the group' do - expect(timebox.resource_parent).to eq(group) - end - end - - context 'when project is present' do - it 'returns the project' do - expect(timebox.resource_parent).to eq(project) - end - end - end - describe "#title" do let(:timebox) { create(timebox_type, *timebox_args, title: "<b>foo & bar -> 2.2</b>") } @@ -155,39 +70,6 @@ RSpec.shared_examples 'a timebox' do |timebox_type| end end - describe '#merge_requests_enabled?' do - context "per project" do - it "is true for projects with MRs enabled" do - project = create(:project, :merge_requests_enabled) - timebox = create(timebox_type, *timebox_args, project: project) - - expect(timebox.merge_requests_enabled?).to be_truthy - end - - it "is false for projects with MRs disabled" do - project = create(:project, :repository_enabled, :merge_requests_disabled) - timebox = create(timebox_type, *timebox_args, project: project) - - expect(timebox.merge_requests_enabled?).to be_falsey - end - - it "is false for projects with repository disabled" do - project = create(:project, :repository_disabled) - timebox = create(timebox_type, *timebox_args, project: project) - - expect(timebox.merge_requests_enabled?).to be_falsey - end - end - - context "per group" do - let(:timebox) { create(timebox_type, *timebox_args, group: group) } - - it "is always true for groups, for performance reasons" do - expect(timebox.merge_requests_enabled?).to be_truthy - end - end - end - describe '#to_ability_name' do it 'returns timebox' do timebox = build(timebox_type, *timebox_args) diff --git a/spec/support/shared_examples/models/integrations/has_web_hook_shared_examples.rb b/spec/support/shared_examples/models/integrations/has_web_hook_shared_examples.rb index e309aa50c6e..31ec25249d7 100644 --- a/spec/support/shared_examples/models/integrations/has_web_hook_shared_examples.rb +++ b/spec/support/shared_examples/models/integrations/has_web_hook_shared_examples.rb @@ -4,7 +4,7 @@ RSpec.shared_examples Integrations::HasWebHook do include AfterNextHelpers describe 'associations' do - it { is_expected.to have_one(:service_hook).inverse_of(:integration).with_foreign_key(:service_id) } + it { is_expected.to have_one(:service_hook).inverse_of(:integration).with_foreign_key(:integration_id) } end describe 'callbacks' do diff --git a/spec/support/shared_examples/models/project_ci_cd_settings_shared_examples.rb b/spec/support/shared_examples/models/project_ci_cd_settings_shared_examples.rb index c92e819db19..3caf58da4d2 100644 --- a/spec/support/shared_examples/models/project_ci_cd_settings_shared_examples.rb +++ b/spec/support/shared_examples/models/project_ci_cd_settings_shared_examples.rb @@ -5,12 +5,14 @@ RSpec.shared_examples 'ci_cd_settings delegation' do context 'when ci_cd_settings is destroyed but project is not' do it 'allows methods delegated to ci_cd_settings to be nil', :aggregate_failures do - project = create(:project) attributes = project.ci_cd_settings.attributes.keys - %w(id project_id) - exclude_attributes + + expect(attributes).to match_array(attributes_with_prefix.keys) + project.ci_cd_settings.destroy! project.reload - attributes.each do |attr| - method = project.respond_to?("ci_#{attr}") ? "ci_#{attr}" : attr + attributes_with_prefix.each do |attr, prefix| + method = "#{prefix}#{attr}" expect(project.send(method)).to be_nil, "#{attr} was not nil" end end @@ -20,8 +22,6 @@ end RSpec.shared_examples 'a ci_cd_settings predicate method' do |prefix: ''| using RSpec::Parameterized::TableSyntax - let_it_be(:project) { create(:project) } - context 'when ci_cd_settings is nil' do before do allow(project).to receive(:ci_cd_settings).and_return(nil) diff --git a/spec/support/shared_examples/models/wiki_shared_examples.rb b/spec/support/shared_examples/models/wiki_shared_examples.rb index 5f6a10bd754..b1aa90449e1 100644 --- a/spec/support/shared_examples/models/wiki_shared_examples.rb +++ b/spec/support/shared_examples/models/wiki_shared_examples.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true RSpec.shared_examples 'wiki model' do + using RSpec::Parameterized::TableSyntax + let_it_be(:user) { create(:user, :commit_email) } let(:wiki_container) { raise NotImplementedError } @@ -124,36 +126,6 @@ RSpec.shared_examples 'wiki model' do end end - describe '#wiki' do - it 'contains a Gitlab::Git::Wiki instance' do - expect(subject.wiki).to be_a Gitlab::Git::Wiki - end - - it 'creates a new wiki repo if one does not yet exist' do - expect(subject.create_page('index', 'test content')).to be_truthy - end - - it 'creates a new wiki repo with a default commit message' do - expect(subject.create_page('index', 'test content', :markdown, '')).to be_truthy - - page = subject.find_page('index') - - expect(page.last_version.message).to eq("#{user.username} created page: index") - end - - context 'when the repository cannot be created' do - let(:wiki_container) { wiki_container_without_repo } - - before do - expect(subject.repository).to receive(:create_if_not_exists) { false } - end - - it 'raises CouldNotCreateWikiError' do - expect { subject.wiki }.to raise_exception(Wiki::CouldNotCreateWikiError) - end - end - end - describe '#empty?' do context 'when the wiki repository is empty' do it 'returns true' do @@ -180,70 +152,71 @@ RSpec.shared_examples 'wiki model' do it 'returns false' do expect(subject.empty?).to be(false) end - - it 'only instantiates a Wiki page once' do - expect(WikiPage).to receive(:new).once.and_call_original - - subject.empty? - end end end end describe '#list_pages' do - let(:wiki_pages) { subject.list_pages } + shared_examples 'wiki model #list_pages' do + let(:wiki_pages) { subject.list_pages } - before do - subject.create_page('index', 'This is an index') - subject.create_page('index2', 'This is an index2') - subject.create_page('an index3', 'This is an index3') - end + before do + subject.create_page('index', 'This is an index') + subject.create_page('index2', 'This is an index2') + subject.create_page('an index3', 'This is an index3') + end - it 'returns an array of WikiPage instances' do - expect(wiki_pages).to be_present - expect(wiki_pages).to all(be_a(WikiPage)) - end + it 'returns an array of WikiPage instances' do + expect(wiki_pages).to be_present + expect(wiki_pages).to all(be_a(WikiPage)) + end - it 'does not load WikiPage content by default' do - wiki_pages.each do |page| - expect(page.content).to be_empty + it 'does not load WikiPage content by default' do + wiki_pages.each do |page| + expect(page.content).to be_empty + end end - end - it 'returns all pages by default' do - expect(wiki_pages.count).to eq(3) - end + it 'returns all pages by default' do + expect(wiki_pages.count).to eq(3) + end - context 'with limit option' do - it 'returns limited set of pages' do - expect(subject.list_pages(limit: 1).count).to eq(1) + context 'with limit option' do + it 'returns limited set of pages' do + expect(subject.list_pages(limit: 1).count).to eq(1) + end end - end - context 'with sorting options' do - it 'returns pages sorted by title by default' do - pages = ['an index3', 'index', 'index2'] + context 'with sorting options' do + it 'returns pages sorted by title by default' do + pages = ['an index3', 'index', 'index2'] - expect(subject.list_pages.map(&:title)).to eq(pages) - expect(subject.list_pages(direction: 'desc').map(&:title)).to eq(pages.reverse) + expect(subject.list_pages.map(&:title)).to eq(pages) + expect(subject.list_pages(direction: 'desc').map(&:title)).to eq(pages.reverse) + end end - it 'returns pages sorted by created_at' do - pages = ['index', 'index2', 'an index3'] + context 'with load_content option' do + let(:pages) { subject.list_pages(load_content: true) } - expect(subject.list_pages(sort: 'created_at').map(&:title)).to eq(pages) - expect(subject.list_pages(sort: 'created_at', direction: 'desc').map(&:title)).to eq(pages.reverse) + it 'loads WikiPage content' do + expect(pages.first.content).to eq('This is an index3') + expect(pages.second.content).to eq('This is an index') + expect(pages.third.content).to eq('This is an index2') + end end end - context 'with load_content option' do - let(:pages) { subject.list_pages(load_content: true) } - - it 'loads WikiPage content' do - expect(pages.first.content).to eq('This is an index3') - expect(pages.second.content).to eq('This is an index') - expect(pages.third.content).to eq('This is an index2') + context 'list pages with legacy wiki rpcs' do + before do + stub_feature_flags(wiki_list_page_with_normal_repository_rpcs: false) end + + it_behaves_like 'wiki model #list_pages' + end + + context 'list pages with normal repository rpcs' do + it_behaves_like 'wiki model #list_pages' end end @@ -338,6 +311,74 @@ RSpec.shared_examples 'wiki model' do end end + context "wiki repository's default branch is updated" do + before do + old_default_branch = wiki.default_branch + subject.create_page('page in updated default branch', 'content') + subject.repository.add_branch(user, 'another_branch', old_default_branch) + subject.repository.rm_branch(user, old_default_branch) + subject.repository.expire_status_cache + end + + it 'returns the page in the updated default branch' do + wiki = described_class.new(wiki_container, user) + page = wiki.find_page('page in updated default branch') + + expect(wiki.default_branch).to eql('another_branch') + expect(page.title).to eq('page in updated default branch') + end + end + + context "wiki repository's HEAD is updated" do + before do + subject.create_page('page in updated HEAD', 'content') + subject.repository.add_branch(user, 'another_branch', subject.default_branch) + subject.repository.change_head('another_branch') + subject.repository.expire_status_cache + end + + it 'returns the page in the new HEAD' do + wiki = described_class.new(wiki_container, user) + page = subject.find_page('page in updated HEAD') + + expect(wiki.default_branch).to eql('another_branch') + expect(page.title).to eq('page in updated HEAD') + end + end + + context 'pages with relative paths' do + where(:path, :title) do + [ + ['~hello.md', '~Hello'], + ['hello~world.md', 'Hello~World'], + ['~~~hello.md', '~~~Hello'], + ['~/hello.md', '~/Hello'], + ['hello.md', '/Hello'], + ['hello.md', '../Hello'], + ['hello.md', './Hello'], + ['dir/hello.md', '/dir/Hello'] + ] + end + + with_them do + before do + wiki.repository.create_file( + user, path, "content of wiki file", + branch_name: wiki.default_branch, + message: "created page #{path}", + author_email: user.email, + author_name: user.name + ) + end + + it "can find page with `#{params[:title]}` title" do + page = subject.find_page(title) + + expect(page.content).to eq("content of wiki file") + end + end + end + context 'pages with different file extensions' do where(:extension, :path, :title) do [ @@ -378,14 +419,6 @@ RSpec.shared_examples 'wiki model' do end end - context 'find page with legacy wiki service' do - before do - stub_feature_flags(wiki_find_page_with_normal_repository_rpcs: false) - end - - it_behaves_like 'wiki model #find_page' - end - context 'find page with normal repository RPCs' do it_behaves_like 'wiki model #find_page' end @@ -404,14 +437,6 @@ RSpec.shared_examples 'wiki model' do end end - context 'find sidebar with legacy wiki service' do - before do - stub_feature_flags(wiki_find_page_with_normal_repository_rpcs: false) - end - - it_behaves_like 'wiki model #find_sidebar' - end - context 'find sidebar with normal repository RPCs' do it_behaves_like 'wiki model #find_sidebar' end @@ -421,7 +446,7 @@ RSpec.shared_examples 'wiki model' do let(:image) { File.open(Rails.root.join('spec', 'fixtures', 'big-image.png')) } before do - subject.wiki # Make sure the wiki repo exists + subject.create_wiki_repository # Make sure the wiki repo exists subject.repository.create_file(user, 'image.png', image, branch_name: subject.default_branch, message: 'add image') end @@ -456,6 +481,22 @@ RSpec.shared_examples 'wiki model' do expect(file.raw_data).to be_empty end end + + context "wiki repository's default branch is updated" do + before do + old_default_branch = wiki.default_branch + subject.repository.add_branch(user, 'another_branch', old_default_branch) + subject.repository.rm_branch(user, old_default_branch) + subject.repository.expire_status_cache + end + + it 'returns the page in the updated default branch' do + wiki = described_class.new(wiki_container, user) + file = wiki.find_file('image.png') + + expect(file.mime_type).to eq('image/png') + end + end end describe '#create_page' do @@ -480,7 +521,7 @@ RSpec.shared_examples 'wiki model' do it 'sets the correct commit message' do subject.create_page('test page', 'some content', :markdown, 'commit message') - expect(subject.list_pages.first.page.version.message).to eq('commit message') + expect(subject.list_pages.first.version.message).to eq('commit message') end it 'sets the correct commit email' do @@ -577,6 +618,8 @@ RSpec.shared_examples 'wiki model' do 'foo' | :org | ['foo.md'] | false 'foo' | :markdown | ['dir/foo.md'] | true '/foo' | :markdown | ['foo.md'] | false + '~foo' | :markdown | [] | true + '~~~foo' | :markdown | [] | true './foo' | :markdown | ['foo.md'] | false '../foo' | :markdown | ['foo.md'] | false '../../foo' | :markdown | ['foo.md'] | false @@ -607,14 +650,6 @@ RSpec.shared_examples 'wiki model' do end it_behaves_like 'create_page tests' - - context 'create page with legacy find_page wiki service' do - it_behaves_like 'create_page tests' do - before do - stub_feature_flags(wiki_find_page_with_normal_repository_rpcs: false) - end - end - end end describe '#update_page' do @@ -687,6 +722,8 @@ RSpec.shared_examples 'wiki model' do using RSpec::Parameterized::TableSyntax where(:original_title, :original_format, :updated_title, :updated_format, :expected_title, :expected_path) do + 'test page' | :markdown | '~new test page' | :asciidoc | '~new test page' | '~new-test-page.asciidoc' + 'test page' | :markdown | '~~~new test page' | :asciidoc | '~~~new test page' | '~~~new-test-page.asciidoc' 'test page' | :markdown | 'new test page' | :asciidoc | 'new test page' | 'new-test-page.asciidoc' 'test page' | :markdown | 'new dir/new test page' | :asciidoc | 'new dir/new test page' | 'new-dir/new-test-page.asciidoc' 'test dir/test page' | :markdown | 'new dir/new test page' | :asciidoc | 'new dir/new test page' | 'new-dir/new-test-page.asciidoc' @@ -696,13 +733,13 @@ RSpec.shared_examples 'wiki model' do 'test dir/test page' | :markdown | nil | :markdown | 'test dir/test page' | 'test-dir/test-page.md' 'test page' | :markdown | '' | :markdown | 'test page' | 'test-page.md' 'test.page' | :markdown | '' | :markdown | 'test.page' | 'test.page.md' - 'testpage' | :markdown | '../testpage' | :markdown | 'testpage' | 'testpage.md' - 'dir/testpage' | :markdown | 'dir/../testpage' | :markdown | 'testpage' | 'testpage.md' - 'dir/testpage' | :markdown | './dir/testpage' | :markdown | 'dir/testpage' | 'dir/testpage.md' - 'dir/testpage' | :markdown | '../dir/testpage' | :markdown | 'dir/testpage' | 'dir/testpage.md' - 'dir/testpage' | :markdown | '../dir/../testpage' | :markdown | 'testpage' | 'testpage.md' - 'dir/testpage' | :markdown | '../dir/../dir/testpage' | :markdown | 'dir/testpage' | 'dir/testpage.md' - 'dir/testpage' | :markdown | '../dir/../another/testpage' | :markdown | 'another/testpage' | 'another/testpage.md' + 'testpage' | :markdown | '../testpage' | :markdown | 'testpage' | 'testpage.md' + 'dir/testpage' | :markdown | 'dir/../testpage' | :markdown | 'testpage' | 'testpage.md' + 'dir/testpage' | :markdown | './dir/testpage' | :markdown | 'dir/testpage' | 'dir/testpage.md' + 'dir/testpage' | :markdown | '../dir/testpage' | :markdown | 'dir/testpage' | 'dir/testpage.md' + 'dir/testpage' | :markdown | '../dir/../testpage' | :markdown | 'testpage' | 'testpage.md' + 'dir/testpage' | :markdown | '../dir/../dir/testpage' | :markdown | 'dir/testpage' | 'dir/testpage.md' + 'dir/testpage' | :markdown | '../dir/../another/testpage' | :markdown | 'another/testpage' | 'another/testpage.md' end end @@ -711,17 +748,6 @@ RSpec.shared_examples 'wiki model' do include_context 'extended examples' end - context 'update page with legacy find_page wiki service' do - it_behaves_like 'update_page tests' do - before do - stub_feature_flags(wiki_find_page_with_normal_repository_rpcs: false) - end - - include_context 'common examples' - include_context 'extended examples' - end - end - context 'when format is invalid' do let!(:page) { create(:wiki_page, wiki: subject, title: 'test page') } @@ -862,7 +888,7 @@ RSpec.shared_examples 'wiki model' do end describe '#create_wiki_repository' do - let(:head_path) { Rails.root.join(TestEnv.repos_path, "#{wiki.disk_path}.git", 'HEAD') } + let(:head_path) { Gitlab::GitalyClient::StorageSettings.allow_disk_access { Rails.root.join(TestEnv.repos_path, "#{wiki.disk_path}.git", 'HEAD') } } let(:default_branch) { 'foo' } before do @@ -895,4 +921,40 @@ RSpec.shared_examples 'wiki model' do end end end + + describe '#preview_slug' do + where(:title, :file_extension, :format, :expected_slug) do + 'The Best Thing' | :md | :markdown | 'The-Best-Thing' + 'The Best Thing' | :txt | :plaintext | 'The-Best-Thing' + 'A Subject/Title Here' | :txt | :plaintext | 'A-Subject/Title-Here' + 'A subject' | :txt | :plaintext | 'A-subject' + 'A 1/B 2/C 3' | :txt | :plaintext | 'A-1/B-2/C-3' + 'subject/title' | :txt | :plaintext | 'subject/title' + 'subject/title.md' | :txt | :plaintext | 'subject/title.md' + 'foo%2Fbar' | :txt | :plaintext | 'foo%2Fbar' + '' | :md | :markdown | '.md' + '' | :txt | :plaintext | '.txt' + end + + with_them do + before do + subject.repository.create_file( + user, "#{title}.#{file_extension}", 'content', + branch_name: subject.default_branch, + message: "Add #{title}" + ) + end + + it do + expect(described_class.preview_slug(title, file_extension)).to eq(expected_slug) + end + + it 'matches the slug generated by gitaly' do + skip('Gitaly cannot generate a slug for an empty title') unless title.present? + + gitaly_slug = subject.list_pages.first.slug + expect(described_class.preview_slug(title, file_extension)).to eq(gitaly_slug) + end + end + end end diff --git a/spec/support/shared_examples/policies/wiki_policies_shared_examples.rb b/spec/support/shared_examples/policies/wiki_policies_shared_examples.rb index 991d6289373..b9d4709efd5 100644 --- a/spec/support/shared_examples/policies/wiki_policies_shared_examples.rb +++ b/spec/support/shared_examples/policies/wiki_policies_shared_examples.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true RSpec.shared_examples 'model with wiki policies' do - include ProjectHelpers + include UserHelpers include AdminModeHelper let(:container) { raise NotImplementedError } diff --git a/spec/support/shared_examples/projects/container_repository/cleanup_tags_service_shared_examples.rb b/spec/support/shared_examples/projects/container_repository/cleanup_tags_service_shared_examples.rb index 9c2d30a9c8c..f7731af8dc6 100644 --- a/spec/support/shared_examples/projects/container_repository/cleanup_tags_service_shared_examples.rb +++ b/spec/support/shared_examples/projects/container_repository/cleanup_tags_service_shared_examples.rb @@ -1,53 +1,5 @@ # frozen_string_literal: true -RSpec.shared_examples 'handling invalid params' do |service_response_extra: {}, supports_caching: false| - context 'when no params are specified' do - let(:params) { {} } - - it_behaves_like 'not removing anything', - service_response_extra: service_response_extra, - supports_caching: supports_caching - end - - context 'with invalid regular expressions' do - shared_examples 'handling an invalid regex' do - it 'keeps all tags' do - expect(Projects::ContainerRepository::DeleteTagsService) - .not_to receive(:new) - expect_no_caching unless supports_caching - - subject - end - - it { is_expected.to eq(status: :error, message: 'invalid regex') } - - it 'calls error tracking service' do - expect(Gitlab::ErrorTracking).to receive(:log_exception).and_call_original - - subject - end - end - - context 'when name_regex_delete is invalid' do - let(:params) { { 'name_regex_delete' => '*test*' } } - - it_behaves_like 'handling an invalid regex' - end - - context 'when name_regex is invalid' do - let(:params) { { 'name_regex' => '*test*' } } - - it_behaves_like 'handling an invalid regex' - end - - context 'when name_regex_keep is invalid' do - let(:params) { { 'name_regex_keep' => '*test*' } } - - it_behaves_like 'handling an invalid regex' - end - end -end - RSpec.shared_examples 'when regex matching everything is specified' do |service_response_extra: {}, supports_caching: false, delete_expectations:| let(:params) do @@ -227,20 +179,6 @@ RSpec.shared_examples 'when running a container_expiration_policy' do is_expected.to eq(expected_service_response(deleted: delete_expectations.flatten).merge(service_response_extra)) end end - - context 'without container_expiration_policy param' do - let(:params) do - { - 'name_regex_delete' => '.*', - 'keep_n' => 1, - 'older_than' => '1 day' - } - end - - it 'fails' do - is_expected.to eq(status: :error, message: 'access denied') - end - end end RSpec.shared_examples 'not removing anything' do |service_response_extra: {}, supports_caching: false| diff --git a/spec/support/shared_examples/quick_actions/issuable/max_issuable_examples.rb b/spec/support/shared_examples/quick_actions/issuable/max_issuable_examples.rb new file mode 100644 index 00000000000..e725de8ad31 --- /dev/null +++ b/spec/support/shared_examples/quick_actions/issuable/max_issuable_examples.rb @@ -0,0 +1,85 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'does not exceed the issuable size limit' do + let(:user1) { create(:user) } + let(:user2) { create(:user) } + let(:user3) { create(:user) } + + before do + project.add_maintainer(user) + project.add_maintainer(user1) + project.add_maintainer(user2) + project.add_maintainer(user3) + end + + context 'when feature flag is turned on' do + context "when the number of users of issuable does exceed the limit" do + before do + stub_const("Issuable::MAX_NUMBER_OF_ASSIGNEES_OR_REVIEWERS", 2) + end + + it 'will not add more than the allowed number of users' do + allow_next_instance_of(update_service) do |service| + expect(service).not_to receive(:execute) + end + + note = described_class.new(project, user, opts.merge( + note: note_text, + noteable_type: noteable_type, + noteable_id: issuable.id, + confidential: false + )).execute + + expect(note.errors[:validation]).to match_array([validation_message]) + end + end + + context "when the number of users does not exceed the limit" do + before do + stub_const("Issuable::MAX_NUMBER_OF_ASSIGNEES_OR_REVIEWERS", 6) + end + + it 'calls execute and does not return an error' do + allow_next_instance_of(update_service) do |service| + expect(service).to receive(:execute).and_call_original + end + + note = described_class.new(project, user, opts.merge( + note: note_text, + noteable_type: noteable_type, + noteable_id: issuable.id, + confidential: false + )).execute + + expect(note.errors[:validation]).to be_empty + end + end + end + + context 'when feature flag is off' do + before do + stub_feature_flags(feature_flag_hash) + end + + context "when the number of users of issuable does exceed the limit" do + before do + stub_const("Issuable::MAX_NUMBER_OF_ASSIGNEES_OR_REVIEWERS", 2) + end + + it 'will not add more than the allowed number of users' do + allow_next_instance_of(MergeRequests::UpdateService) do |service| + expect(service).to receive(:execute).and_call_original + end + + note = described_class.new(project, user, opts.merge( + note: note_text, + noteable_type: 'MergeRequest', + noteable_id: issuable.id, + confidential: false + )).execute + + expect(note.errors[:validation]).to be_empty + end + end + end +end diff --git a/spec/support/shared_examples/requests/access_tokens_controller_shared_examples.rb b/spec/support/shared_examples/requests/access_tokens_controller_shared_examples.rb index 017e6274cb0..59e641e2af6 100644 --- a/spec/support/shared_examples/requests/access_tokens_controller_shared_examples.rb +++ b/spec/support/shared_examples/requests/access_tokens_controller_shared_examples.rb @@ -2,18 +2,13 @@ RSpec.shared_examples 'GET resource access tokens available' do let_it_be(:active_resource_access_token) { create(:personal_access_token, user: bot_user) } - let_it_be(:inactive_resource_access_token) { create(:personal_access_token, :revoked, user: bot_user) } it 'retrieves active resource access tokens' do subject - expect(assigns(:active_resource_access_tokens)).to contain_exactly(active_resource_access_token) - end - - it 'retrieves inactive resource access tokens' do - subject - - expect(assigns(:inactive_resource_access_tokens)).to contain_exactly(inactive_resource_access_token) + token_entities = assigns(:active_resource_access_tokens) + expect(token_entities.length).to eq(1) + expect(token_entities[0][:name]).to eq(active_resource_access_token.name) end it 'lists all available scopes' do @@ -21,15 +16,6 @@ RSpec.shared_examples 'GET resource access tokens available' do expect(assigns(:scopes)).to eq(Gitlab::Auth.resource_bot_scopes) end - - it 'retrieves newly created personal access token value' do - token_value = 'random-value' - allow(PersonalAccessToken).to receive(:redis_getdel).with("#{user.id}:#{resource.id}").and_return(token_value) - - subject - - expect(assigns(:new_resource_access_token)).to eq(token_value) - end end RSpec.shared_examples 'POST resource access tokens available' do @@ -37,10 +23,13 @@ RSpec.shared_examples 'POST resource access tokens available' do PersonalAccessToken.order(:created_at).last end - it 'returns success message' do + it 'renders JSON with a token' do subject - expect(flash[:notice]).to match('Your new access token has been created.') + parsed_body = Gitlab::Json.parse(response.body) + expect(parsed_body['new_token']).not_to be_blank + expect(parsed_body['errors']).to be_blank + expect(response).to have_gitlab_http_status(:success) end it 'creates resource access token' do @@ -59,12 +48,6 @@ RSpec.shared_examples 'POST resource access tokens available' do expect(created_token.user).to be_project_bot end - it 'stores newly created token redis store' do - expect(PersonalAccessToken).to receive(:redis_store!) - - subject - end - it { expect { subject }.to change { User.count }.by(1) } it { expect { subject }.to change { PersonalAccessToken.count }.by(1) } @@ -87,10 +70,13 @@ RSpec.shared_examples 'POST resource access tokens available' do expect { subject }.not_to change { User.count } end - it 'shows a failure alert' do + it 'renders JSON with an error' do subject - expect(flash[:alert]).to match("Failed to create new access token: Failed!") + parsed_body = Gitlab::Json.parse(response.body) + expect(parsed_body['new_token']).to be_blank + expect(parsed_body['errors']).to contain_exactly('Failed!') + expect(response).to have_gitlab_http_status(:unprocessable_entity) end end end diff --git a/spec/support/shared_examples/requests/api/debian_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/debian_packages_shared_examples.rb index de7032450a5..14a83d2889b 100644 --- a/spec/support/shared_examples/requests/api/debian_packages_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/debian_packages_shared_examples.rb @@ -24,7 +24,7 @@ RSpec.shared_examples 'Debian packages upload request' do |status, body = nil| if status == :created it 'creates package files', :aggregate_failures do expect(::Packages::Debian::FindOrCreateIncomingService).to receive(:new).with(container, user).and_call_original - expect(::Packages::Debian::CreatePackageFileService).to receive(:new).with(be_a(Packages::Package), be_an(Hash)).and_call_original + expect(::Packages::Debian::CreatePackageFileService).to receive(:new).with(package: be_a(Packages::Package), current_user: be_an(User), params: be_an(Hash)).and_call_original if file_name.end_with? '.changes' expect(::Packages::Debian::ProcessChangesWorker).to receive(:perform_async) diff --git a/spec/support/shared_examples/requests/api/helm_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/helm_packages_shared_examples.rb index 06ed0448b50..8bf6b162508 100644 --- a/spec/support/shared_examples/requests/api/helm_packages_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/helm_packages_shared_examples.rb @@ -247,6 +247,15 @@ RSpec.shared_examples 'handling helm chart index requests' do end end + context 'with access to package registry for everyone' do + before do + project.update!(visibility: Gitlab::VisibilityLevel::PRIVATE) + project.project_feature.update!(package_registry_access_level: ProjectFeature::PUBLIC) + end + + it_behaves_like 'process helm service index request', :anonymous, :success + end + context 'when an invalid token is passed' do let(:headers) { basic_auth_header(user.username, 'wrong') } diff --git a/spec/support/shared_examples/requests/api/hooks_shared_examples.rb b/spec/support/shared_examples/requests/api/hooks_shared_examples.rb index 013945bd578..d666a754d9f 100644 --- a/spec/support/shared_examples/requests/api/hooks_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/hooks_shared_examples.rb @@ -134,6 +134,7 @@ RSpec.shared_examples 'web-hook API endpoints' do |prefix| context 'the hook is backed-off' do before do + WebHook::FAILURE_THRESHOLD.times { hook.backoff! } hook.backoff! end diff --git a/spec/support/shared_examples/requests/api/issuable_update_shared_examples.rb b/spec/support/shared_examples/requests/api/issuable_update_shared_examples.rb index a3378d4619b..1045a92f332 100644 --- a/spec/support/shared_examples/requests/api/issuable_update_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/issuable_update_shared_examples.rb @@ -20,7 +20,7 @@ RSpec.shared_examples 'issuable update endpoint' do end it 'updates the issuable with labels param as array' do - stub_const("Gitlab::QueryLimiting::Transaction::THRESHOLD", 110) + allow(Gitlab::QueryLimiting::Transaction).to receive(:threshold).and_return(110) params = { labels: ['label1', 'label2', 'foo, bar', '&,?'] } diff --git a/spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb index b651ffc8996..85ac2b5e1ea 100644 --- a/spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb @@ -260,7 +260,11 @@ RSpec.shared_examples 'handling get metadata requests' do |scope: :project| project.send("add_#{user_role}", user) if user_role project.update!(visibility: visibility.to_s) package.update!(name: package_name) unless package_name == 'non-existing-package' - allow_fetch_application_setting(attribute: "npm_package_requests_forwarding", return_value: request_forward) + if scope == :instance + allow_fetch_application_setting(attribute: "npm_package_requests_forwarding", return_value: request_forward) + else + allow_fetch_cascade_application_setting(attribute: "npm_package_requests_forwarding", return_value: request_forward) + end end example_name = "#{params[:expected_result]} metadata request" diff --git a/spec/support/shared_examples/requests/api/npm_packages_tags_shared_examples.rb b/spec/support/shared_examples/requests/api/npm_packages_tags_shared_examples.rb index 86b6975bf9f..1d79a61fbb0 100644 --- a/spec/support/shared_examples/requests/api/npm_packages_tags_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/npm_packages_tags_shared_examples.rb @@ -14,6 +14,7 @@ RSpec.shared_examples 'accept package tags request' do |status:| before do allow_fetch_application_setting(attribute: "npm_package_requests_forwarding", return_value: false) + allow_fetch_cascade_application_setting(attribute: "npm_package_requests_forwarding", return_value: false) end context 'with valid package name' do diff --git a/spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb index f411b5699a9..11e19d8d067 100644 --- a/spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb @@ -291,7 +291,7 @@ RSpec.shared_examples 'pypi simple API endpoint' do end before do - allow_fetch_application_setting(attribute: "pypi_package_requests_forwarding", return_value: forward) + allow_fetch_cascade_application_setting(attribute: "pypi_package_requests_forwarding", return_value: forward) end it_behaves_like params[:shared_examples_name], :reporter, params[:expected_status] diff --git a/spec/support/shared_examples/requests/projects/google_cloud/google_oauth2_token_examples.rb b/spec/support/shared_examples/requests/projects/google_cloud/google_oauth2_token_examples.rb index 379327be0db..e2e2658d803 100644 --- a/spec/support/shared_examples/requests/projects/google_cloud/google_oauth2_token_examples.rb +++ b/spec/support/shared_examples/requests/projects/google_cloud/google_oauth2_token_examples.rb @@ -18,6 +18,7 @@ RSpec.shared_examples 'requires valid Google Oauth2 token' do allow_next_instance_of(GoogleApi::CloudPlatform::Client) do |client| allow(client).to receive(:validate_token).and_return(true) allow(client).to receive(:list_projects).and_return(mock_gcp_projects) if mock_gcp_projects + allow(client).to receive(:create_cloudsql_instance) end allow_next_instance_of(BranchesFinder) do |finder| diff --git a/spec/support/shared_examples/serializers/issuable_current_user_properties_shared_examples.rb b/spec/support/shared_examples/serializers/issuable_current_user_properties_shared_examples.rb new file mode 100644 index 00000000000..6c285bfba91 --- /dev/null +++ b/spec/support/shared_examples/serializers/issuable_current_user_properties_shared_examples.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'issuable entity current_user properties' do + describe 'can_create_confidential_note' do + subject do + described_class.new(resource, request: request) + .as_json[:current_user][:can_create_confidential_note] + end + + context 'when user can create confidential notes' do + before do + resource.resource_parent.add_reporter(user) + end + + it { is_expected.to be(true) } + end + + context 'when user cannot create confidential notes' do + it { is_expected.to eq(false) } + end + end +end diff --git a/spec/support/shared_examples/services/base_rpm_service_shared_examples.rb b/spec/support/shared_examples/services/base_rpm_service_shared_examples.rb new file mode 100644 index 00000000000..c9520852a5b --- /dev/null +++ b/spec/support/shared_examples/services/base_rpm_service_shared_examples.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'handling rpm xml file' do + include_context 'with rpm package data' + + let(:xml) { nil } + let(:data) { {} } + + context 'when generate empty xml' do + it 'generate expected xml' do + expect(subject).to eq(empty_xml) + end + end + + context 'when updating existing xml' do + let(:xml) { empty_xml } + let(:data) { xml_update_params } + + shared_examples 'changing root tag attribute' do + it "increment previous 'packages' value by 1" do + previous_value = Nokogiri::XML(xml).at(described_class::ROOT_TAG).attributes["packages"].value.to_i + new_value = Nokogiri::XML(subject).at(described_class::ROOT_TAG).attributes["packages"].value.to_i + + expect(previous_value + 1).to eq(new_value) + end + end + + it 'generate valid xml add expected xml node to existing xml' do + # Have one root attribute + result = Nokogiri::XML::Document.parse(subject).remove_namespaces! + expect(result.children.count).to eq(1) + + # Root node has 1 child with generated node + expect(result.xpath("//#{described_class::ROOT_TAG}/package").count).to eq(1) + end + + context 'when empty xml' do + it_behaves_like 'changing root tag attribute' + end + + context 'when xml has children' do + let(:xml) { described_class.new(xml: empty_xml, data: data).execute } + + it 'has children nodes' do + result = Nokogiri::XML::Document.parse(xml).remove_namespaces! + expect(result.children.count).to be > 0 + end + + it_behaves_like 'changing root tag attribute' + 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 b3ba0a1be93..cfd75d3cfcd 100644 --- a/spec/support/shared_examples/services/merge_request_shared_examples.rb +++ b/spec/support/shared_examples/services/merge_request_shared_examples.rb @@ -19,29 +19,13 @@ RSpec.shared_examples 'reviewer_ids filter' do let(:reviewer2) { create(:user) } context 'when the current user can admin the merge_request' do - context 'when merge_request_reviewer feature is enabled' do + context 'with a reviewer who can read the merge_request' do before do - stub_feature_flags(merge_request_reviewer: true) + project.add_developer(reviewer1) end - context 'with a reviewer who can read the merge_request' do - before do - project.add_developer(reviewer1) - end - - it 'contains reviewers who can read the merge_request' do - expect(execute.reviewers).to contain_exactly(reviewer1) - end - end - end - - context 'when merge_request_reviewer feature is disabled' do - before do - stub_feature_flags(merge_request_reviewer: false) - end - - it 'contains no reviewers' do - expect(execute.reviewers).to eq [] + it 'contains reviewers who can read the merge_request' do + expect(execute.reviewers).to contain_exactly(reviewer1) end end end diff --git a/spec/support/shared_examples/services/reviewers_change_trigger_shared_examples.rb b/spec/support/shared_examples/services/reviewers_change_trigger_shared_examples.rb new file mode 100644 index 00000000000..cc37ea0c6f0 --- /dev/null +++ b/spec/support/shared_examples/services/reviewers_change_trigger_shared_examples.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'triggers GraphQL subscription mergeRequestReviewersUpdated' do + specify do + expect(GraphqlTriggers).to receive(:merge_request_reviewers_updated).with(merge_request) + + action + end +end + +RSpec.shared_examples 'does not trigger GraphQL subscription mergeRequestReviewersUpdated' do + specify do + expect(GraphqlTriggers).not_to receive(:merge_request_reviewers_updated) + + action + end +end |