diff options
Diffstat (limited to 'spec/support/shared_examples')
37 files changed, 1614 insertions, 523 deletions
diff --git a/spec/support/shared_examples/alert_notification_service_shared_examples.rb b/spec/support/shared_examples/alert_notification_service_shared_examples.rb index 7bd6df8c608..fc935effe0e 100644 --- a/spec/support/shared_examples/alert_notification_service_shared_examples.rb +++ b/spec/support/shared_examples/alert_notification_service_shared_examples.rb @@ -27,3 +27,18 @@ RSpec.shared_examples 'Alert Notification Service sends no notifications' do |ht end end end + +RSpec.shared_examples 'creates status-change system note for an auto-resolved alert' do + it 'has 2 new system notes' do + expect { subject }.to change(Note, :count).by(2) + expect(Note.last.note).to include('Resolved') + end +end + +# Requires `source` to be defined +RSpec.shared_examples 'creates single system note based on the source of the alert' do + it 'has one new system note' do + expect { subject }.to change(Note, :count).by(1) + expect(Note.last.note).to include(source) + end +end diff --git a/spec/support/shared_examples/banzai/filters/emoji_shared_examples.rb b/spec/support/shared_examples/banzai/filters/emoji_shared_examples.rb new file mode 100644 index 00000000000..da305f5ccaa --- /dev/null +++ b/spec/support/shared_examples/banzai/filters/emoji_shared_examples.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'emoji filter' do + it 'keeps whitespace intact' do + doc = filter("This deserves a #{emoji_name}, big time.") + + expect(doc.to_html).to match(/^This deserves a <gl-emoji.+>, big time\.\z/) + end + + it 'does not match emoji in a string' do + doc = filter("'2a00:a4c0#{emoji_name}:1'") + + expect(doc.css('gl-emoji')).to be_empty + end + + it 'ignores non existent/unsupported emoji' do + exp = '<p>:foo:</p>' + doc = filter(exp) + + expect(doc.to_html).to eq(exp) + end + + it 'matches with adjacent text' do + doc = filter("#{emoji_name.delete(':')} (#{emoji_name})") + + expect(doc.css('gl-emoji').size).to eq 1 + end + + it 'does not match emoji in a pre tag' do + doc = filter("<p><pre>#{emoji_name}</pre></p>") + + expect(doc.css('img')).to be_empty + end + + it 'does not match emoji in code tag' do + doc = filter("<p><code>#{emoji_name} wow</code></p>") + + expect(doc.css('img')).to be_empty + end + + it 'does not match emoji in tt tag' do + doc = filter("<p><tt>#{emoji_name} yes!</tt></p>") + + expect(doc.css('img')).to be_empty + end +end diff --git a/spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb b/spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb index 7f49d20c83e..9c8006ce4f1 100644 --- a/spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb +++ b/spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb @@ -9,6 +9,8 @@ RSpec.shared_examples 'multiple issue boards' do login_as(user) + stub_feature_flags(board_new_list: false) + visit boards_path wait_for_requests end diff --git a/spec/support/shared_examples/controllers/create_notes_rate_limit_shared_examples.rb b/spec/support/shared_examples/controllers/create_notes_rate_limit_shared_examples.rb new file mode 100644 index 00000000000..74a98c20383 --- /dev/null +++ b/spec/support/shared_examples/controllers/create_notes_rate_limit_shared_examples.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true +# +# Requires a context containing: +# - user +# - params +# - request_full_path + +RSpec.shared_examples 'request exceeding rate limit' do + before do + stub_application_setting(notes_create_limit: 2) + 2.times { post :create, params: params } + end + + it 'prevents from creating more notes', :request_store do + expect { post :create, params: params } + .to change { Note.count }.by(0) + + expect(response).to have_gitlab_http_status(:too_many_requests) + expect(response.body).to eq(_('This endpoint has been requested too many times. Try again later.')) + end + + it 'logs the event in auth.log' do + attributes = { + message: 'Application_Rate_Limiter_Request', + env: :notes_create_request_limit, + remote_ip: '0.0.0.0', + request_method: 'POST', + path: request_full_path, + user_id: user.id, + username: user.username + } + + expect(Gitlab::AuthLogger).to receive(:error).with(attributes).once + post :create, params: params + end + + it 'allows user in allow-list to create notes, even if the case is different' do + user.update_attribute(:username, user.username.titleize) + stub_application_setting(notes_create_limit_allowlist: ["#{user.username.downcase}"]) + + post :create, params: params + expect(response).to have_gitlab_http_status(:found) + end +end diff --git a/spec/support/shared_examples/controllers/snippet_blob_shared_examples.rb b/spec/support/shared_examples/controllers/snippet_blob_shared_examples.rb index c3e8f807afb..62aaec85162 100644 --- a/spec/support/shared_examples/controllers/snippet_blob_shared_examples.rb +++ b/spec/support/shared_examples/controllers/snippet_blob_shared_examples.rb @@ -17,6 +17,38 @@ RSpec.shared_examples 'raw snippet blob' do end end + context 'Content Disposition' do + context 'when the disposition is inline' do + let(:inline) { true } + + it 'returns inline in the content disposition header' do + subject + + expect(response.header['Content-Disposition']).to eq('inline') + end + end + + context 'when the disposition is attachment' do + let(:inline) { false } + + it 'returns attachment plus the filename in the content disposition header' do + subject + + expect(response.header['Content-Disposition']).to match "attachment; filename=\"#{filepath}\"" + end + + context 'when the feature flag attachment_with_filename is disabled' do + it 'returns just attachment in the disposition header' do + stub_feature_flags(attachment_with_filename: false) + + subject + + expect(response.header['Content-Disposition']).to eq 'attachment' + end + end + end + end + context 'with invalid file path' do let(:filepath) { 'doesnotexist' } diff --git a/spec/support/shared_examples/features/comment_and_close_button_shared_examples.rb b/spec/support/shared_examples/features/comment_and_close_button_shared_examples.rb deleted file mode 100644 index 4ee2840ed9f..00000000000 --- a/spec/support/shared_examples/features/comment_and_close_button_shared_examples.rb +++ /dev/null @@ -1,29 +0,0 @@ -# frozen_string_literal: true - -RSpec.shared_examples 'page with comment and close button' do |button_text| - context 'when remove_comment_close_reopen feature flag is enabled' do - before do - stub_feature_flags(remove_comment_close_reopen: true) - setup - end - - it "does not show #{button_text} button" do - within '.note-form-actions' do - expect(page).not_to have_button(button_text) - end - end - end - - context 'when remove_comment_close_reopen feature flag is disabled' do - before do - stub_feature_flags(remove_comment_close_reopen: false) - setup - end - - it "shows #{button_text} button" do - within '.note-form-actions' do - expect(page).to have_button(button_text) - end - end - end -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 6bebd59ed70..86ba2821c78 100644 --- a/spec/support/shared_examples/features/discussion_comments_shared_example.rb +++ b/spec/support/shared_examples/features/discussion_comments_shared_example.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -RSpec.shared_examples 'thread comments' do |resource_name| +RSpec.shared_examples 'thread comments for commit and snippet' do |resource_name| let(:form_selector) { '.js-main-target-form' } let(:dropdown_selector) { "#{form_selector} .comment-type-dropdown" } let(:toggle_selector) { "#{dropdown_selector} .dropdown-toggle" } @@ -24,23 +24,6 @@ RSpec.shared_examples 'thread comments' do |resource_name| expect(new_comment).not_to have_selector '.discussion' end - if resource_name == 'issue' - it "clicking 'Comment & close #{resource_name}' will post a comment and close the #{resource_name}" do - find("#{form_selector} .note-textarea").send_keys(comment) - - click_button 'Comment & close issue' - - wait_for_all_requests - - expect(page).to have_content(comment) - expect(page).to have_content "@#{user.username} closed" - - new_comment = all(comments_selector).last - - expect(new_comment).not_to have_selector '.discussion' - end - end - describe 'when the toggle is clicked' do before do find("#{form_selector} .note-textarea").send_keys(comment) @@ -110,33 +93,172 @@ RSpec.shared_examples 'thread comments' do |resource_name| end it 'updates the submit button text and closes the dropdown' do - button = find(submit_selector) + expect(find(submit_selector).value).to eq 'Start thread' - # on issues page, the submit input is a <button>, on other pages it is <input> - if button.tag_name == 'button' - expect(find(submit_selector)).to have_content 'Start thread' - else - expect(find(submit_selector).value).to eq 'Start thread' + expect(page).not_to have_selector menu_selector + end + + describe 'creating a thread' do + before do + find(submit_selector).click + wait_for_requests + + find(comments_selector, match: :first) end - expect(page).not_to have_selector menu_selector + def submit_reply(text) + find("#{comments_selector} .js-vue-discussion-reply").click + find("#{comments_selector} .note-textarea").send_keys(text) + + find("#{comments_selector} .js-comment-button").click + wait_for_requests + end + + it 'clicking "Start thread" will post a thread' do + expect(page).to have_content(comment) + + new_comment = all(comments_selector).last + + expect(new_comment).to have_selector('.discussion') + end end - if resource_name =~ /(issue|merge request)/ - it 'updates the close button text' do - expect(find(close_selector)).to have_content "Start thread & close #{resource_name}" + describe 'when opening the menu' do + before do + find(toggle_selector).click + end + + it 'has "Start thread" selected' do + find("#{menu_selector} li", match: :first) + items = all("#{menu_selector} li") + + expect(items.first).to have_content 'Comment' + expect(items.first).not_to have_selector '[data-testid="check-icon"]' + expect(items.first['class']).not_to match 'droplab-item-selected' + + expect(items.last).to have_content 'Start thread' + expect(items.last).to have_selector '[data-testid="check-icon"]' + expect(items.last['class']).to match 'droplab-item-selected' end - it 'typing does not change the close button text' do - find("#{form_selector} .note-textarea").send_keys('b') + describe 'when selecting "Comment"' do + before do + find("#{menu_selector} li", match: :first).click + end + + it 'updates the submit button text and closes the dropdown' do + button = find(submit_selector) + + expect(button.value).to eq 'Comment' + + expect(page).not_to have_selector menu_selector + end - expect(find(close_selector)).to have_content "Start thread & close #{resource_name}" + it 'has "Comment" selected when opening the menu', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/196825' do + find(toggle_selector).click + + find("#{menu_selector} li", match: :first) + items = all("#{menu_selector} li") + + aggregate_failures do + expect(items.first).to have_content 'Comment' + expect(items.first).to have_selector '[data-testid="check-icon"]' + expect(items.first['class']).to match 'droplab-item-selected' + + expect(items.last).to have_content 'Start thread' + expect(items.last).not_to have_selector '[data-testid="check-icon"]' + expect(items.last['class']).not_to match 'droplab-item-selected' + end + end end end + end + end +end + +RSpec.shared_examples 'thread comments for issue, epic and merge request' do |resource_name| + let(:form_selector) { '.js-main-target-form' } + let(:dropdown_selector) { "#{form_selector} [data-testid='comment-button']" } + let(:submit_button_selector) { "#{dropdown_selector} .split-content-button" } + let(:toggle_selector) { "#{dropdown_selector} .dropdown-toggle-split" } + let(:menu_selector) { "#{dropdown_selector} .dropdown-menu" } + let(:close_selector) { "#{form_selector} .btn-comment-and-close" } + let(:comments_selector) { '.timeline > .note.timeline-entry' } + let(:comment) { 'My comment' } + + it 'clicking "Comment" will post a comment' do + expect(page).to have_selector toggle_selector + + find("#{form_selector} .note-textarea").send_keys(comment) + + find(submit_button_selector).click + + expect(page).to have_content(comment) + + new_comment = all(comments_selector).last + + expect(new_comment).not_to have_selector '.discussion' + end + + if resource_name == 'issue' + it "clicking 'Comment & close #{resource_name}' will post a comment and close the #{resource_name}" do + find("#{form_selector} .note-textarea").send_keys(comment) + + click_button 'Comment & close issue' + + wait_for_all_requests + + expect(page).to have_content(comment) + expect(page).to have_content "@#{user.username} closed" + + new_comment = all(comments_selector).last + + expect(new_comment).not_to have_selector '.discussion' + end + end + + describe 'when the toggle is clicked' do + before do + find("#{form_selector} .note-textarea").send_keys(comment) + + find(toggle_selector).click + end + + it 'has a "Comment" item (selected by default) and "Start thread" item' do + expect(page).to have_selector menu_selector + + find("#{menu_selector} li", match: :first) + items = all("#{menu_selector} li") + + expect(page).to have_selector("#{dropdown_selector}[data-track-label='comment_button']") + + expect(items.first).to have_content 'Comment' + expect(items.first).to have_content "Add a general comment to this #{resource_name}." + + expect(items.last).to have_content 'Start thread' + expect(items.last).to have_content "Discuss a specific suggestion or question#{' that needs to be resolved' if resource_name == 'merge request'}." + end + + it 'closes the menu when clicking the toggle or body' do + find(toggle_selector).click + + expect(page).not_to have_selector menu_selector + + find(toggle_selector).click + find("#{form_selector} .note-textarea").click + + expect(page).not_to have_selector menu_selector + end + + describe 'when selecting "Start thread"' do + before do + find("#{menu_selector} li", match: :first) + all("#{menu_selector} li").last.click + end describe 'creating a thread' do before do - find(submit_selector).click + find(submit_button_selector).click wait_for_requests find(comments_selector, match: :first) @@ -146,6 +268,7 @@ RSpec.shared_examples 'thread comments' do |resource_name| find("#{comments_selector} .js-vue-discussion-reply").click find("#{comments_selector} .note-textarea").send_keys(text) + # .js-comment-button here refers to the reply button in note_form.vue find("#{comments_selector} .js-comment-button").click wait_for_requests end @@ -228,13 +351,11 @@ RSpec.shared_examples 'thread comments' do |resource_name| find("#{menu_selector} li", match: :first) items = all("#{menu_selector} li") + expect(page).to have_selector("#{dropdown_selector}[data-track-label='start_thread_button']") + expect(items.first).to have_content 'Comment' - expect(items.first).not_to have_selector '[data-testid="check-icon"]' - expect(items.first['class']).not_to match 'droplab-item-selected' expect(items.last).to have_content 'Start thread' - expect(items.last).to have_selector '[data-testid="check-icon"]' - expect(items.last['class']).to match 'droplab-item-selected' end describe 'when selecting "Comment"' do @@ -243,14 +364,9 @@ RSpec.shared_examples 'thread comments' do |resource_name| end it 'updates the submit button text and closes the dropdown' do - button = find(submit_selector) + button = find(submit_button_selector) - # on issues page, the submit input is a <button>, on other pages it is <input> - if button.tag_name == 'button' - expect(button).to have_content 'Comment' - else - expect(button.value).to eq 'Comment' - end + expect(button).to have_content 'Comment' expect(page).not_to have_selector menu_selector end @@ -267,21 +383,17 @@ RSpec.shared_examples 'thread comments' do |resource_name| end end - it 'has "Comment" selected when opening the menu', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/196825' do + it 'has "Comment" selected when opening the menu' do find(toggle_selector).click find("#{menu_selector} li", match: :first) items = all("#{menu_selector} li") - aggregate_failures do - expect(items.first).to have_content 'Comment' - expect(items.first).to have_selector '[data-testid="check-icon"]' - expect(items.first['class']).to match 'droplab-item-selected' + expect(page).to have_selector("#{dropdown_selector}[data-track-label='comment_button']") - expect(items.last).to have_content 'Start thread' - expect(items.last).not_to have_selector '[data-testid="check-icon"]' - expect(items.last['class']).not_to match 'droplab-item-selected' - end + expect(items.first).to have_content 'Comment' + + expect(items.last).to have_content 'Start thread' end end end diff --git a/spec/support/shared_examples/features/issuable_invite_members_shared_examples.rb b/spec/support/shared_examples/features/issuable_invite_members_shared_examples.rb index 3fec1a56c0c..7a32f61d4fa 100644 --- a/spec/support/shared_examples/features/issuable_invite_members_shared_examples.rb +++ b/spec/support/shared_examples/features/issuable_invite_members_shared_examples.rb @@ -1,11 +1,7 @@ # frozen_string_literal: true RSpec.shared_examples 'issuable invite members experiments' do - context 'when invite_members_version_a experiment is enabled' do - before do - stub_experiment_for_subject(invite_members_version_a: true) - end - + context 'when a privileged user can invite' do it 'shows a link for inviting members and follows through to the members page' do project.add_maintainer(user) visit issuable_path @@ -51,9 +47,9 @@ RSpec.shared_examples 'issuable invite members experiments' do end end - context 'when no invite members experiments are enabled' do + context 'when invite_members_version_b experiment is disabled' do it 'shows author in assignee dropdown and no invite link' do - project.add_maintainer(user) + project.add_developer(user) visit issuable_path find('.block.assignee .edit-link').click 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 25203fa3182..00d3bd08218 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 @@ -3,7 +3,13 @@ RSpec.shared_examples 'it uploads and commit a new text file' do it 'uploads and commit a new text file', :js do find('.add-to-tree').click - click_link('Upload file') + + page.within('.dropdown-menu') do + click_link('Upload file') + + wait_for_requests + end + drop_in_dropzone(File.join(Rails.root, 'spec', 'fixtures', 'doc_sample.txt')) page.within('#modal-upload-blob') do @@ -29,7 +35,13 @@ end RSpec.shared_examples 'it uploads and commit a new image file' do it 'uploads and commit a new image file', :js do find('.add-to-tree').click - click_link('Upload file') + + page.within('.dropdown-menu') do + click_link('Upload file') + + wait_for_requests + end + drop_in_dropzone(File.join(Rails.root, 'spec', 'fixtures', 'logo_sample.svg')) page.within('#modal-upload-blob') do @@ -82,3 +94,21 @@ RSpec.shared_examples 'it uploads and commit a new file to a forked project' do expect(page).to have_content('Sed ut perspiciatis unde omnis') end end + +RSpec.shared_examples 'uploads and commits a new text file via "upload file" button' do + it 'uploads and commits a new text file via "upload file" button', :js do + find('[data-testid="upload-file-button"]').click + + attach_file('upload_file', File.join(Rails.root, 'spec', 'fixtures', 'doc_sample.txt'), make_visible: true) + + page.within('#details-modal-upload-blob') do + fill_in(:commit_message, with: 'New commit message') + end + + click_button('Upload file') + + expect(page).to have_content('New commit message') + expect(page).to have_content('Lorem ipsum dolor sit amet') + expect(page).to have_content('Sed ut perspiciatis unde omnis') + end +end diff --git a/spec/support/shared_examples/features/variable_list_shared_examples.rb b/spec/support/shared_examples/features/variable_list_shared_examples.rb index e0d169c6868..2fd88b610e9 100644 --- a/spec/support/shared_examples/features/variable_list_shared_examples.rb +++ b/spec/support/shared_examples/features/variable_list_shared_examples.rb @@ -2,7 +2,7 @@ RSpec.shared_examples 'variable list' do it 'shows a list of variables' do - page.within('.ci-variable-table') do + page.within('[data-testid="ci-variable-table"]') do expect(find('.js-ci-variable-row:nth-child(1) td[data-label="Key"]').text).to eq(variable.key) end end @@ -16,7 +16,7 @@ RSpec.shared_examples 'variable list' do wait_for_requests - page.within('.ci-variable-table') do + page.within('[data-testid="ci-variable-table"]') do expect(find('.js-ci-variable-row:nth-child(1) td[data-label="Key"]').text).to eq('key') end end @@ -30,7 +30,7 @@ RSpec.shared_examples 'variable list' do wait_for_requests - page.within('.ci-variable-table') do + page.within('[data-testid="ci-variable-table"]') do expect(find('.js-ci-variable-row:nth-child(1) td[data-label="Key"]').text).to eq('key') expect(find('.js-ci-variable-row:nth-child(1) td[data-label="Protected"] svg[data-testid="mobile-issue-close-icon"]')).to be_present end @@ -45,14 +45,14 @@ RSpec.shared_examples 'variable list' do wait_for_requests - page.within('.ci-variable-table') do + page.within('[data-testid="ci-variable-table"]') do expect(find('.js-ci-variable-row:nth-child(1) td[data-label="Key"]').text).to eq('key') expect(find('.js-ci-variable-row:nth-child(1) td[data-label="Masked"] svg[data-testid="close-icon"]')).to be_present end end it 'reveals and hides variables' do - page.within('.ci-variable-table') do + page.within('[data-testid="ci-variable-table"]') do expect(first('.js-ci-variable-row td[data-label="Key"]').text).to eq(variable.key) expect(page).to have_content('*' * 17) @@ -72,7 +72,7 @@ RSpec.shared_examples 'variable list' do it 'deletes a variable' do expect(page).to have_selector('.js-ci-variable-row', count: 1) - page.within('.ci-variable-table') do + page.within('[data-testid="ci-variable-table"]') do click_button('Edit') end @@ -86,7 +86,7 @@ RSpec.shared_examples 'variable list' do end it 'edits a variable' do - page.within('.ci-variable-table') do + page.within('[data-testid="ci-variable-table"]') do click_button('Edit') end @@ -102,7 +102,7 @@ RSpec.shared_examples 'variable list' do end it 'edits a variable to be unmasked' do - page.within('.ci-variable-table') do + page.within('[data-testid="ci-variable-table"]') do click_button('Edit') end @@ -115,13 +115,13 @@ RSpec.shared_examples 'variable list' do wait_for_requests - page.within('.ci-variable-table') do + page.within('[data-testid="ci-variable-table"]') do expect(find('.js-ci-variable-row:nth-child(1) td[data-label="Masked"] svg[data-testid="close-icon"]')).to be_present end end it 'edits a variable to be masked' do - page.within('.ci-variable-table') do + page.within('[data-testid="ci-variable-table"]') do click_button('Edit') end @@ -133,7 +133,7 @@ RSpec.shared_examples 'variable list' do wait_for_requests - page.within('.ci-variable-table') do + page.within('[data-testid="ci-variable-table"]') do click_button('Edit') end @@ -143,7 +143,7 @@ RSpec.shared_examples 'variable list' do click_button('Update variable') end - page.within('.ci-variable-table') do + page.within('[data-testid="ci-variable-table"]') do expect(find('.js-ci-variable-row:nth-child(1) td[data-label="Masked"] svg[data-testid="mobile-issue-close-icon"]')).to be_present end end @@ -211,7 +211,7 @@ RSpec.shared_examples 'variable list' do expect(page).to have_selector('.js-ci-variable-row', count: 3) # Remove the `akey` variable - page.within('.ci-variable-table') do + page.within('[data-testid="ci-variable-table"]') do page.within('.js-ci-variable-row:first-child') do click_button('Edit') end diff --git a/spec/support/shared_examples/graphql/mutation_shared_examples.rb b/spec/support/shared_examples/graphql/mutation_shared_examples.rb index 84ebd4852b9..51d52cbb901 100644 --- a/spec/support/shared_examples/graphql/mutation_shared_examples.rb +++ b/spec/support/shared_examples/graphql/mutation_shared_examples.rb @@ -48,6 +48,6 @@ RSpec.shared_examples 'a mutation that returns errors in the response' do |error it do post_graphql_mutation(mutation, current_user: current_user) - expect(mutation_response['errors']).to eq(errors) + expect(mutation_response['errors']).to match_array(errors) end end diff --git a/spec/support/shared_examples/graphql/mutations/boards_create_shared_examples.rb b/spec/support/shared_examples/graphql/mutations/boards_create_shared_examples.rb index 2b93d174653..2e3a3ce6b41 100644 --- a/spec/support/shared_examples/graphql/mutations/boards_create_shared_examples.rb +++ b/spec/support/shared_examples/graphql/mutations/boards_create_shared_examples.rb @@ -66,9 +66,7 @@ RSpec.shared_examples 'boards create mutation' do context 'when the Boards::CreateService returns an error response' do before do - allow_next_instance_of(Boards::CreateService) do |service| - allow(service).to receive(:execute).and_return(ServiceResponse.error(message: 'There was an error.')) - end + params[:name] = '' end it 'does not create a board' do @@ -80,7 +78,7 @@ RSpec.shared_examples 'boards create mutation' do expect(mutation_response).to have_key('board') expect(mutation_response['board']).to be_nil - expect(mutation_response['errors'].first).to eq('There was an error.') + expect(mutation_response['errors'].first).to eq('There was an error when creating a board.') end end end diff --git a/spec/support/shared_examples/graphql/mutations/can_mutate_spammable_examples.rb b/spec/support/shared_examples/graphql/mutations/can_mutate_spammable_examples.rb index d294f034d2e..bb4270d7db6 100644 --- a/spec/support/shared_examples/graphql/mutations/can_mutate_spammable_examples.rb +++ b/spec/support/shared_examples/graphql/mutations/can_mutate_spammable_examples.rb @@ -21,14 +21,14 @@ RSpec.shared_examples 'a mutation which can mutate a spammable' do end end - describe "#with_spam_action_fields" do + describe "#with_spam_action_response_fields" do it 'resolves with spam action fields' do subject # NOTE: We do not need to assert on the specific values of spam action fields here, we only need - # to verify that #with_spam_action_fields was invoked and that the fields are present in the - # response. The specific behavior of #with_spam_action_fields is covered in the - # CanMutateSpammable unit tests. + # to verify that #with_spam_action_response_fields was invoked and that the fields are present in the + # response. The specific behavior of #with_spam_action_response_fields is covered in the + # HasSpamActionResponseFields unit tests. expect(mutation_response.keys) .to include('spam', 'spamLogId', 'needsCaptchaResponse', 'captchaSiteKey') end diff --git a/spec/support/shared_examples/graphql/notes_on_noteables_shared_examples.rb b/spec/support/shared_examples/graphql/notes_on_noteables_shared_examples.rb index 41b7da07d2d..0d2e9f6ec8c 100644 --- a/spec/support/shared_examples/graphql/notes_on_noteables_shared_examples.rb +++ b/spec/support/shared_examples/graphql/notes_on_noteables_shared_examples.rb @@ -17,7 +17,7 @@ RSpec.shared_context 'exposing regular notes on a noteable in GraphQL' do notes { edges { node { - #{all_graphql_fields_for('Note')} + #{all_graphql_fields_for('Note', max_depth: 1)} } } } diff --git a/spec/support/shared_examples/graphql/types/gitlab_style_deprecations_shared_examples.rb b/spec/support/shared_examples/graphql/types/gitlab_style_deprecations_shared_examples.rb index 269e9170906..bc091a678e2 100644 --- a/spec/support/shared_examples/graphql/types/gitlab_style_deprecations_shared_examples.rb +++ b/spec/support/shared_examples/graphql/types/gitlab_style_deprecations_shared_examples.rb @@ -6,7 +6,7 @@ RSpec.shared_examples 'Gitlab-style deprecations' do expect { subject(deprecation_reason: 'foo') }.to raise_error( ArgumentError, 'Use `deprecated` property instead of `deprecation_reason`. ' \ - 'See https://docs.gitlab.com/ee/development/api_graphql_styleguide.html#deprecating-fields-and-enum-values' + 'See https://docs.gitlab.com/ee/development/api_graphql_styleguide.html#deprecating-fields-arguments-and-enum-values' ) end diff --git a/spec/support/shared_examples/helpers/issuable_description_templates_shared_examples.rb b/spec/support/shared_examples/helpers/issuable_description_templates_shared_examples.rb index 9e8c96d576a..47e34b21036 100644 --- a/spec/support/shared_examples/helpers/issuable_description_templates_shared_examples.rb +++ b/spec/support/shared_examples/helpers/issuable_description_templates_shared_examples.rb @@ -23,11 +23,11 @@ RSpec.shared_examples 'project issuable templates' do end it 'returns only md files as issue templates' do - expect(helper.issuable_templates(project, 'issue')).to eq(templates('issue', project)) + expect(helper.issuable_templates(project, 'issue')).to eq(expected_templates('issue')) end it 'returns only md files as merge_request templates' do - expect(helper.issuable_templates(project, 'merge_request')).to eq(templates('merge_request', project)) + expect(helper.issuable_templates(project, 'merge_request')).to eq(expected_templates('merge_request')) end end diff --git a/spec/support/shared_examples/lib/gitlab/cycle_analytics/event_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/cycle_analytics/event_shared_examples.rb index 145a7290ac8..7d341d79bae 100644 --- a/spec/support/shared_examples/lib/gitlab/cycle_analytics/event_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/cycle_analytics/event_shared_examples.rb @@ -8,6 +8,7 @@ RSpec.shared_examples_for 'value stream analytics event' do it { expect(described_class.identifier).to be_a_kind_of(Symbol) } it { expect(instance.object_type.ancestors).to include(ApplicationRecord) } it { expect(instance).to respond_to(:timestamp_projection) } + it { expect(instance).to respond_to(:markdown_description) } it { expect(instance.column_list).to be_a_kind_of(Array) } describe '#apply_query_customization' do diff --git a/spec/support/shared_examples/lib/gitlab/usage_data_counters/issue_activity_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/usage_data_counters/issuable_activity_shared_examples.rb index edd9b6cdf37..aa6e64a3820 100644 --- a/spec/support/shared_examples/lib/gitlab/usage_data_counters/issue_activity_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/usage_data_counters/issuable_activity_shared_examples.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -RSpec.shared_examples 'a tracked issue edit event' do |event| +RSpec.shared_examples 'a daily tracked issuable event' do before do stub_application_setting(usage_ping_enabled: true) end @@ -25,3 +25,13 @@ RSpec.shared_examples 'a tracked issue edit event' do |event| expect(track_action(author: nil)).to be_nil end end + +RSpec.shared_examples 'does not track when feature flag is disabled' do |feature_flag| + context "when feature flag #{feature_flag} is disabled" do + it 'does not track action' do + stub_feature_flags(feature_flag => false) + + expect(track_action(author: user1)).to be_nil + end + end +end diff --git a/spec/support/shared_examples/lib/sentry/client_shared_examples.rb b/spec/support/shared_examples/lib/sentry/client_shared_examples.rb index 4221708b55c..d73c7b6848d 100644 --- a/spec/support/shared_examples/lib/sentry/client_shared_examples.rb +++ b/spec/support/shared_examples/lib/sentry/client_shared_examples.rb @@ -26,7 +26,7 @@ RSpec.shared_examples 'no Sentry redirects' do |http_method| end it 'does not follow redirects' do - expect { subject }.to raise_exception(Sentry::Client::Error, 'Sentry response status code: 302') + expect { subject }.to raise_exception(ErrorTracking::SentryClient::Error, 'Sentry response status code: 302') expect(redirect_req_stub).to have_been_requested expect(redirected_req_stub).not_to have_been_requested end @@ -53,7 +53,7 @@ RSpec.shared_examples 'maps Sentry exceptions' do |http_method| it do expect { subject } - .to raise_exception(Sentry::Client::Error, message) + .to raise_exception(ErrorTracking::SentryClient::Error, message) end end end diff --git a/spec/support/shared_examples/metrics/active_record_subscriber_shared_examples.rb b/spec/support/shared_examples/metrics/active_record_subscriber_shared_examples.rb new file mode 100644 index 00000000000..7bf2456c548 --- /dev/null +++ b/spec/support/shared_examples/metrics/active_record_subscriber_shared_examples.rb @@ -0,0 +1,137 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'store ActiveRecord info in RequestStore' do |db_role| + it 'prevents db counters from leaking to the next transaction' do + 2.times do + Gitlab::WithRequestStore.with_request_store do + subscriber.sql(event) + + if db_role == :primary + expect(described_class.db_counter_payload).to eq( + db_count: record_query ? 1 : 0, + db_write_count: record_write_query ? 1 : 0, + db_cached_count: record_cached_query ? 1 : 0, + db_primary_cached_count: record_cached_query ? 1 : 0, + db_primary_count: record_query ? 1 : 0, + db_primary_duration_s: record_query ? 0.002 : 0, + db_replica_cached_count: 0, + db_replica_count: 0, + db_replica_duration_s: 0.0 + ) + elsif db_role == :replica + expect(described_class.db_counter_payload).to eq( + db_count: record_query ? 1 : 0, + db_write_count: record_write_query ? 1 : 0, + db_cached_count: record_cached_query ? 1 : 0, + db_primary_cached_count: 0, + db_primary_count: 0, + db_primary_duration_s: 0.0, + db_replica_cached_count: record_cached_query ? 1 : 0, + db_replica_count: record_query ? 1 : 0, + db_replica_duration_s: record_query ? 0.002 : 0 + ) + else + expect(described_class.db_counter_payload).to eq( + db_count: record_query ? 1 : 0, + db_write_count: record_write_query ? 1 : 0, + db_cached_count: record_cached_query ? 1 : 0 + ) + end + end + end + end +end + +RSpec.shared_examples 'record ActiveRecord metrics in a metrics transaction' do |db_role| + it 'increments only db counters' do + if record_query + expect(transaction).to receive(:increment).with(:gitlab_transaction_db_count_total, 1) + expect(transaction).to receive(:increment).with("gitlab_transaction_db_#{db_role}_count_total".to_sym, 1) if db_role + else + expect(transaction).not_to receive(:increment).with(:gitlab_transaction_db_count_total, 1) + expect(transaction).not_to receive(:increment).with("gitlab_transaction_db_#{db_role}_count_total".to_sym, 1) if db_role + end + + if record_write_query + expect(transaction).to receive(:increment).with(:gitlab_transaction_db_write_count_total, 1) + else + expect(transaction).not_to receive(:increment).with(:gitlab_transaction_db_write_count_total, 1) + end + + if record_cached_query + expect(transaction).to receive(:increment).with(:gitlab_transaction_db_cached_count_total, 1) + expect(transaction).to receive(:increment).with("gitlab_transaction_db_#{db_role}_cached_count_total".to_sym, 1) if db_role + else + expect(transaction).not_to receive(:increment).with(:gitlab_transaction_db_cached_count_total, 1) + expect(transaction).not_to receive(:increment).with("gitlab_transaction_db_#{db_role}_cached_count_total".to_sym, 1) if db_role + end + + subscriber.sql(event) + end + + it 'observes sql_duration metric' do + if record_query + expect(transaction).to receive(:observe).with(:gitlab_sql_duration_seconds, 0.002) + expect(transaction).to receive(:observe).with("gitlab_sql_#{db_role}_duration_seconds".to_sym, 0.002) if db_role + else + expect(transaction).not_to receive(:observe) + end + + subscriber.sql(event) + end +end + +RSpec.shared_examples 'record ActiveRecord metrics' do |db_role| + context 'when both web and background transaction are available' do + let(:transaction) { double('Gitlab::Metrics::WebTransaction') } + let(:background_transaction) { double('Gitlab::Metrics::WebTransaction') } + + before do + allow(::Gitlab::Metrics::WebTransaction).to receive(:current) + .and_return(transaction) + allow(::Gitlab::Metrics::BackgroundTransaction).to receive(:current) + .and_return(background_transaction) + allow(transaction).to receive(:increment) + allow(transaction).to receive(:observe) + end + + it_behaves_like 'record ActiveRecord metrics in a metrics transaction', db_role + + it 'captures the metrics for web only' do + expect(background_transaction).not_to receive(:observe) + expect(background_transaction).not_to receive(:increment) + + subscriber.sql(event) + end + end + + context 'when web transaction is available' do + let(:transaction) { double('Gitlab::Metrics::WebTransaction') } + + before do + allow(::Gitlab::Metrics::WebTransaction).to receive(:current) + .and_return(transaction) + allow(::Gitlab::Metrics::BackgroundTransaction).to receive(:current) + .and_return(nil) + allow(transaction).to receive(:increment) + allow(transaction).to receive(:observe) + end + + it_behaves_like 'record ActiveRecord metrics in a metrics transaction', db_role + end + + context 'when background transaction is available' do + let(:transaction) { double('Gitlab::Metrics::BackgroundTransaction') } + + before do + allow(::Gitlab::Metrics::WebTransaction).to receive(:current) + .and_return(nil) + allow(::Gitlab::Metrics::BackgroundTransaction).to receive(:current) + .and_return(transaction) + allow(transaction).to receive(:increment) + allow(transaction).to receive(:observe) + end + + it_behaves_like 'record ActiveRecord metrics in a metrics transaction', db_role + end +end diff --git a/spec/support/shared_examples/models/application_setting_shared_examples.rb b/spec/support/shared_examples/models/application_setting_shared_examples.rb index 92fd4363134..60a02d85a1e 100644 --- a/spec/support/shared_examples/models/application_setting_shared_examples.rb +++ b/spec/support/shared_examples/models/application_setting_shared_examples.rb @@ -289,6 +289,7 @@ RSpec.shared_examples 'application settings examples' do describe '#pick_repository_storage' do before do + allow(Gitlab.config.repositories.storages).to receive(:keys).and_return(%w(default backup)) allow(setting).to receive(:repository_storages_weighted).and_return({ 'default' => 20, 'backup' => 80 }) end @@ -304,15 +305,19 @@ RSpec.shared_examples 'application settings examples' do describe '#normalized_repository_storage_weights' do using RSpec::Parameterized::TableSyntax - where(:storages, :normalized) do - { 'default' => 0, 'backup' => 100 } | { 'default' => 0.0, 'backup' => 1.0 } - { 'default' => 100, 'backup' => 100 } | { 'default' => 0.5, 'backup' => 0.5 } - { 'default' => 20, 'backup' => 80 } | { 'default' => 0.2, 'backup' => 0.8 } - { 'default' => 0, 'backup' => 0 } | { 'default' => 0.0, 'backup' => 0.0 } + where(:config_storages, :storages, :normalized) do + %w(default backup) | { 'default' => 0, 'backup' => 100 } | { 'default' => 0.0, 'backup' => 1.0 } + %w(default backup) | { 'default' => 100, 'backup' => 100 } | { 'default' => 0.5, 'backup' => 0.5 } + %w(default backup) | { 'default' => 20, 'backup' => 80 } | { 'default' => 0.2, 'backup' => 0.8 } + %w(default backup) | { 'default' => 0, 'backup' => 0 } | { 'default' => 0.0, 'backup' => 0.0 } + %w(default) | { 'default' => 0, 'backup' => 100 } | { 'default' => 0.0 } + %w(default) | { 'default' => 100, 'backup' => 100 } | { 'default' => 1.0 } + %w(default) | { 'default' => 20, 'backup' => 80 } | { 'default' => 1.0 } end with_them do before do + allow(Gitlab.config.repositories.storages).to receive(:keys).and_return(config_storages) allow(setting).to receive(:repository_storages_weighted).and_return(storages) end diff --git a/spec/support/shared_examples/models/boards/user_preferences_shared_examples.rb b/spec/support/shared_examples/models/boards/user_preferences_shared_examples.rb new file mode 100644 index 00000000000..766aeac9476 --- /dev/null +++ b/spec/support/shared_examples/models/boards/user_preferences_shared_examples.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'list_preferences_for user' do |list_factory, list_id_attribute| + subject { create(list_factory) } # rubocop:disable Rails/SaveBang + + let_it_be(:user) { create(:user) } + + describe '#preferences_for' do + context 'when user is nil' do + it 'returns not persisted preferences' do + preferences = subject.preferences_for(nil) + + expect(preferences).not_to be_persisted + expect(preferences[list_id_attribute]).to eq(subject.id) + expect(preferences.user_id).to be_nil + end + end + + context 'when a user preference already exists' do + before do + subject.update_preferences_for(user, collapsed: true) + end + + it 'loads preference for user' do + preferences = subject.preferences_for(user) + + expect(preferences).to be_persisted + expect(preferences.collapsed).to eq(true) + end + end + + context 'when preferences for user does not exist' do + it 'returns not persisted preferences' do + preferences = subject.preferences_for(user) + + expect(preferences).not_to be_persisted + expect(preferences.user_id).to eq(user.id) + expect(preferences.public_send(list_id_attribute)).to eq(subject.id) + end + end + end + + describe '#update_preferences_for' do + context 'when user is present' do + context 'when there are no preferences for user' do + it 'creates new user preferences' do + expect { subject.update_preferences_for(user, collapsed: true) }.to change { subject.preferences.count }.by(1) + expect(subject.preferences_for(user).collapsed).to eq(true) + end + end + + context 'when there are preferences for user' do + it 'updates user preferences' do + subject.update_preferences_for(user, collapsed: false) + + expect { subject.update_preferences_for(user, collapsed: true) }.not_to change { subject.preferences.count } + expect(subject.preferences_for(user).collapsed).to eq(true) + end + end + + context 'when user is nil' do + it 'does not create user preferences' do + expect { subject.update_preferences_for(nil, collapsed: true) }.not_to change { subject.preferences.count } + end + end + end + end +end diff --git a/spec/support/shared_examples/models/chat_service_shared_examples.rb b/spec/support/shared_examples/models/chat_service_shared_examples.rb index ad237ad9f49..59e249bb865 100644 --- a/spec/support/shared_examples/models/chat_service_shared_examples.rb +++ b/spec/support/shared_examples/models/chat_service_shared_examples.rb @@ -53,9 +53,13 @@ RSpec.shared_examples "chat service" do |service_name| end it "calls #{service_name} API" do - subject.execute(sample_data) + result = subject.execute(sample_data) - expect(WebMock).to have_requested(:post, webhook_url).with { |req| req.body =~ /\A{"#{content_key}":.+}\Z/ }.once + expect(result).to be(true) + expect(WebMock).to have_requested(:post, webhook_url).once.with { |req| + json_body = Gitlab::Json.parse(req.body).with_indifferent_access + expect(json_body).to include(payload) + } end end @@ -67,7 +71,8 @@ RSpec.shared_examples "chat service" do |service_name| it "does not call #{service_name} API" do result = subject.execute(sample_data) - expect(result).to be_falsy + expect(result).to be(false) + expect(WebMock).not_to have_requested(:post, webhook_url) 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 f91e4bd8cf7..68142e667a4 100644 --- a/spec/support/shared_examples/models/concerns/timebox_shared_examples.rb +++ b/spec/support/shared_examples/models/concerns/timebox_shared_examples.rb @@ -18,7 +18,7 @@ RSpec.shared_examples 'a timebox' do |timebox_type| context 'with a project' do it_behaves_like 'AtomicInternalId' do let(:internal_id_attribute) { :iid } - let(:instance) { build(timebox_type, *timebox_args, project: build(:project), group: nil) } + 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 } @@ -28,7 +28,7 @@ RSpec.shared_examples 'a timebox' do |timebox_type| 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: build(:group)) } + 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 } diff --git a/spec/support/shared_examples/models/email_format_shared_examples.rb b/spec/support/shared_examples/models/email_format_shared_examples.rb index a8115e440a4..77ded168637 100644 --- a/spec/support/shared_examples/models/email_format_shared_examples.rb +++ b/spec/support/shared_examples/models/email_format_shared_examples.rb @@ -6,7 +6,7 @@ # Note: You have access to `email_value` which is the email address value # being currently tested). -RSpec.shared_examples 'an object with email-formated attributes' do |*attributes| +RSpec.shared_examples 'an object with email-formatted attributes' do |*attributes| attributes.each do |attribute| describe "specifically its :#{attribute} attribute" do %w[ @@ -45,7 +45,7 @@ RSpec.shared_examples 'an object with email-formated attributes' do |*attributes end end -RSpec.shared_examples 'an object with RFC3696 compliant email-formated attributes' do |*attributes| +RSpec.shared_examples 'an object with RFC3696 compliant email-formatted attributes' do |*attributes| attributes.each do |attribute| describe "specifically its :#{attribute} attribute" do %w[ diff --git a/spec/support/shared_examples/models/slack_mattermost_notifications_shared_examples.rb b/spec/support/shared_examples/models/slack_mattermost_notifications_shared_examples.rb index a1867e1ce39..71a76121d38 100644 --- a/spec/support/shared_examples/models/slack_mattermost_notifications_shared_examples.rb +++ b/spec/support/shared_examples/models/slack_mattermost_notifications_shared_examples.rb @@ -7,7 +7,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do |service_name| let(:webhook_url) { 'https://example.gitlab.com' } def execute_with_options(options) - receive(:new).with(webhook_url, options.merge(http_client: SlackService::Notifier::HTTPClient)) + receive(:new).with(webhook_url, options.merge(http_client: SlackMattermost::Notifier::HTTPClient)) .and_return(double(:slack_service).as_null_object) end @@ -66,193 +66,180 @@ RSpec.shared_examples 'slack or mattermost notifications' do |service_name| end describe "#execute" do - let(:user) { create(:user) } - let(:project) { create(:project, :repository, :wiki_repo) } - let(:username) { 'slack_username' } - let(:channel) { 'slack_channel' } - let(:issue_service_options) { { title: 'Awesome issue', description: 'please fix' } } + let_it_be(:project) { create(:project, :repository, :wiki_repo) } + let_it_be(:user) { create(:user) } - let(:data) do - Gitlab::DataBuilder::Push.build_sample(project, user) - end + let(:chat_service) { described_class.new( { project: project, webhook: webhook_url, branches_to_be_notified: 'all' }.merge(chat_service_params)) } + let(:chat_service_params) { {} } + let(:data) { Gitlab::DataBuilder::Push.build_sample(project, user) } let!(:stubbed_resolved_hostname) do stub_full_request(webhook_url, method: :post).request_pattern.uri_pattern.to_s end - before do - allow(chat_service).to receive_messages( - project: project, - project_id: project.id, - service_hook: true, - webhook: webhook_url - ) + subject(:execute_service) { chat_service.execute(data) } - issue_service = Issues::CreateService.new(project, user, issue_service_options) - @issue = issue_service.execute - @issues_sample_data = issue_service.hook_data(@issue, 'open') - - project.add_developer(user) - opts = { - title: 'Awesome merge_request', - description: 'please fix', - source_branch: 'feature', - target_branch: 'master' - } - merge_service = MergeRequests::CreateService.new(project, - user, opts) - @merge_request = merge_service.execute - @merge_sample_data = merge_service.hook_data(@merge_request, - 'open') - - opts = { - title: "Awesome wiki_page", - content: "Some text describing some thing or another", - format: "md", - message: "user created page: Awesome wiki_page" - } - - @wiki_page = create(:wiki_page, wiki: project.wiki, **opts) - @wiki_page_sample_data = Gitlab::DataBuilder::WikiPage.build(@wiki_page, user, 'create') - end - - it "calls #{service_name} API for push events" do - chat_service.execute(data) - - expect(WebMock).to have_requested(:post, stubbed_resolved_hostname).once - end + shared_examples 'calls the service API with the event message' do |event_message| + specify do + expect_next_instance_of(Slack::Messenger) do |messenger| + expect(messenger).to receive(:ping).with(event_message, anything).and_call_original + end - it "calls #{service_name} API for issue events" do - chat_service.execute(@issues_sample_data) + execute_service - expect(WebMock).to have_requested(:post, stubbed_resolved_hostname).once + expect(WebMock).to have_requested(:post, stubbed_resolved_hostname).once + end end - it "calls #{service_name} API for merge requests events" do - chat_service.execute(@merge_sample_data) + context 'with username for slack configured' do + let(:chat_service_params) { { username: 'slack_username' } } + + it 'uses the username as an option' do + expect(Slack::Messenger).to execute_with_options(username: 'slack_username') - expect(WebMock).to have_requested(:post, stubbed_resolved_hostname).once + execute_service + end end - it "calls #{service_name} API for wiki page events" do - chat_service.execute(@wiki_page_sample_data) + context 'push events' do + let(:data) { Gitlab::DataBuilder::Push.build_sample(project, user) } - expect(WebMock).to have_requested(:post, stubbed_resolved_hostname).once - end + it_behaves_like 'calls the service API with the event message', /pushed to branch/ - it "calls #{service_name} API for deployment events" do - deployment_event_data = { object_kind: 'deployment' } + context 'with event channel' do + let(:chat_service_params) { { push_channel: 'random' } } - chat_service.execute(deployment_event_data) + it 'uses the right channel for push event' do + expect(Slack::Messenger).to execute_with_options(channel: ['random']) - expect(WebMock).to have_requested(:post, stubbed_resolved_hostname).once + execute_service + end + end end - it 'uses the username as an option for slack when configured' do - allow(chat_service).to receive(:username).and_return(username) - - expect(Slack::Messenger).to execute_with_options(username: username) + context 'tag_push events' do + let(:oldrev) { Gitlab::Git::BLANK_SHA } + let(:newrev) { '8a2a6eb295bb170b34c24c76c49ed0e9b2eaf34b' } # gitlab-test: git rev-parse refs/tags/v1.1.0 + let(:ref) { 'refs/tags/v1.1.0' } + let(:data) { Git::TagHooksService.new(project, user, change: { oldrev: oldrev, newrev: newrev, ref: ref }).send(:push_data) } - chat_service.execute(data) + it_behaves_like 'calls the service API with the event message', /pushed new tag/ end - it 'uses the channel as an option when it is configured' do - allow(chat_service).to receive(:channel).and_return(channel) - expect(Slack::Messenger).to execute_with_options(channel: [channel]) - chat_service.execute(data) - end + context 'issue events' do + let_it_be(:issue) { create(:issue) } + let(:data) { issue.to_hook_data(user) } - context "event channels" do - it "uses the right channel for push event" do - chat_service.update!(push_channel: "random") + it_behaves_like 'calls the service API with the event message', /Issue (.*?) opened by/ - expect(Slack::Messenger).to execute_with_options(channel: ['random']) + context 'whith event channel' do + let(:chat_service_params) { { issue_channel: 'random' } } - chat_service.execute(data) - end + it 'uses the right channel for issue event' do + expect(Slack::Messenger).to execute_with_options(channel: ['random']) - it "uses the right channel for merge request event" do - chat_service.update!(merge_request_channel: "random") + execute_service + end - expect(Slack::Messenger).to execute_with_options(channel: ['random']) + context 'for confidential issues' do + before_all do + issue.update!(confidential: true) + end - chat_service.execute(@merge_sample_data) - end + it 'falls back to issue channel' do + expect(Slack::Messenger).to execute_with_options(channel: ['random']) + + execute_service + end - it "uses the right channel for issue event" do - chat_service.update!(issue_channel: "random") + context 'and confidential_issue_channel is defined' do + let(:chat_service_params) { { issue_channel: 'random', confidential_issue_channel: 'confidential' } } - expect(Slack::Messenger).to execute_with_options(channel: ['random']) + it 'uses the confidential issue channel when it is defined' do + expect(Slack::Messenger).to execute_with_options(channel: ['confidential']) - chat_service.execute(@issues_sample_data) + execute_service + end + end + end end + end + + context 'merge request events' do + let_it_be(:merge_request) { create(:merge_request) } + let(:data) { merge_request.to_hook_data(user) } - context 'for confidential issues' do - let(:issue_service_options) { { title: 'Secret', confidential: true } } + it_behaves_like 'calls the service API with the event message', /opened merge request/ - it "uses confidential issue channel" do - chat_service.update!(confidential_issue_channel: 'confidential') + context 'with event channel' do + let(:chat_service_params) { { merge_request_channel: 'random' } } - expect(Slack::Messenger).to execute_with_options(channel: ['confidential']) + it 'uses the right channel for merge request event' do + expect(Slack::Messenger).to execute_with_options(channel: ['random']) - chat_service.execute(@issues_sample_data) + execute_service end + end + end + + context 'wiki page events' do + let_it_be(:wiki_page) { create(:wiki_page, wiki: project.wiki, message: 'user created page: Awesome wiki_page') } + let(:data) { Gitlab::DataBuilder::WikiPage.build(wiki_page, user, 'create') } - it 'falls back to issue channel' do - chat_service.update!(issue_channel: 'fallback_channel') + it_behaves_like 'calls the service API with the event message', / created (.*?)wikis\/(.*?)|wiki page> in/ - expect(Slack::Messenger).to execute_with_options(channel: ['fallback_channel']) + context 'with event channel' do + let(:chat_service_params) { { wiki_page_channel: 'random' } } - chat_service.execute(@issues_sample_data) + it 'uses the right channel for wiki event' do + expect(Slack::Messenger).to execute_with_options(channel: ['random']) + + execute_service end end + end - it "uses the right channel for wiki event" do - chat_service.update!(wiki_page_channel: "random") - - expect(Slack::Messenger).to execute_with_options(channel: ['random']) + context 'deployment events' do + let_it_be(:deployment) { create(:deployment) } + let(:data) { Gitlab::DataBuilder::Deployment.build(deployment) } - chat_service.execute(@wiki_page_sample_data) - end + it_behaves_like 'calls the service API with the event message', /Deploy to (.*?) created/ + end - context "note event" do - let(:issue_note) do - create(:note_on_issue, project: project, note: "issue note") - end + context 'note event' do + let_it_be(:issue_note) { create(:note_on_issue, project: project, note: "issue note") } + let(:data) { Gitlab::DataBuilder::Note.build(issue_note, user) } - it "uses the right channel" do - chat_service.update!(note_channel: "random") + it_behaves_like 'calls the service API with the event message', /commented on issue/ - note_data = Gitlab::DataBuilder::Note.build(issue_note, user) + context 'with event channel' do + let(:chat_service_params) { { note_channel: 'random' } } + it 'uses the right channel' do expect(Slack::Messenger).to execute_with_options(channel: ['random']) - chat_service.execute(note_data) + execute_service end context 'for confidential notes' do - before do - issue_note.noteable.update!(confidential: true) + before_all do + issue_note.update!(confidential: true) end - it "uses confidential channel" do - chat_service.update!(confidential_note_channel: "confidential") - - note_data = Gitlab::DataBuilder::Note.build(issue_note, user) - - expect(Slack::Messenger).to execute_with_options(channel: ['confidential']) + it 'falls back to note channel' do + expect(Slack::Messenger).to execute_with_options(channel: ['random']) - chat_service.execute(note_data) + execute_service end - it 'falls back to note channel' do - chat_service.update!(note_channel: "fallback_channel") - - note_data = Gitlab::DataBuilder::Note.build(issue_note, user) + context 'and confidential_note_channel is defined' do + let(:chat_service_params) { { note_channel: 'random', confidential_note_channel: 'confidential' } } - expect(Slack::Messenger).to execute_with_options(channel: ['fallback_channel']) + it 'uses confidential channel' do + expect(Slack::Messenger).to execute_with_options(channel: ['confidential']) - chat_service.execute(note_data) + execute_service + end end end end diff --git a/spec/support/shared_examples/models/wiki_shared_examples.rb b/spec/support/shared_examples/models/wiki_shared_examples.rb index 89d30688b5c..abc6e3ecce8 100644 --- a/spec/support/shared_examples/models/wiki_shared_examples.rb +++ b/spec/support/shared_examples/models/wiki_shared_examples.rb @@ -354,27 +354,47 @@ RSpec.shared_examples 'wiki model' do subject.repository.create_file(user, 'image.png', image, branch_name: subject.default_branch, message: 'add image') end - it 'returns the latest version of the file if it exists' do - file = subject.find_file('image.png') + shared_examples 'find_file results' do + it 'returns the latest version of the file if it exists' do + file = subject.find_file('image.png') - expect(file.mime_type).to eq('image/png') - end + expect(file.mime_type).to eq('image/png') + end + + it 'returns nil if the page does not exist' do + expect(subject.find_file('non-existent')).to eq(nil) + end + + it 'returns a Gitlab::Git::WikiFile instance' do + file = subject.find_file('image.png') + + expect(file).to be_a Gitlab::Git::WikiFile + end - it 'returns nil if the page does not exist' do - expect(subject.find_file('non-existent')).to eq(nil) + it 'returns the whole file' do + file = subject.find_file('image.png') + image.rewind + + expect(file.raw_data.b).to eq(image.read.b) + end end - it 'returns a Gitlab::Git::WikiFile instance' do - file = subject.find_file('image.png') + it_behaves_like 'find_file results' + + context 'when load_content is disabled' do + it 'includes the file data in the Gitlab::Git::WikiFile' do + file = subject.find_file('image.png', load_content: false) - expect(file).to be_a Gitlab::Git::WikiFile + expect(file.raw_data).to be_empty + end end - it 'returns the whole file' do - file = subject.find_file('image.png') - image.rewind + context 'when feature flag :gitaly_find_file is disabled' do + before do + stub_feature_flags(gitaly_find_file: false) + end - expect(file.raw_data.b).to eq(image.read.b) + it_behaves_like 'find_file results' end end diff --git a/spec/support/shared_examples/quick_actions/issuable/time_tracking_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issuable/time_tracking_quick_action_shared_examples.rb index 17fd2b836d3..92849ddf1cb 100644 --- a/spec/support/shared_examples/quick_actions/issuable/time_tracking_quick_action_shared_examples.rb +++ b/spec/support/shared_examples/quick_actions/issuable/time_tracking_quick_action_shared_examples.rb @@ -93,6 +93,6 @@ end def submit_time(quick_action) fill_in 'note[note]', with: quick_action - find('.js-comment-submit-button').click + find('[data-testid="comment-button"]').click wait_for_requests end diff --git a/spec/support/shared_examples/requests/api/conan_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/conan_packages_shared_examples.rb index 49b6fc13900..54ea876bed2 100644 --- a/spec/support/shared_examples/requests/api/conan_packages_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/conan_packages_shared_examples.rb @@ -1,63 +1,13 @@ # frozen_string_literal: true RSpec.shared_examples 'conan ping endpoint' do - it 'responds with 401 Unauthorized when no token provided' do + it 'responds with 200 OK when no token provided' do get api(url) - expect(response).to have_gitlab_http_status(:unauthorized) - end - - it 'responds with 200 OK when valid token is provided' do - jwt = build_jwt(personal_access_token) - get api(url), headers: build_token_auth_header(jwt.encoded) - - expect(response).to have_gitlab_http_status(:ok) - expect(response.headers['X-Conan-Server-Capabilities']).to eq("") - end - - it 'responds with 200 OK when valid job token is provided' do - jwt = build_jwt_from_job(job) - get api(url), headers: build_token_auth_header(jwt.encoded) - expect(response).to have_gitlab_http_status(:ok) expect(response.headers['X-Conan-Server-Capabilities']).to eq("") end - it 'responds with 200 OK when valid deploy token is provided' do - jwt = build_jwt_from_deploy_token(deploy_token) - get api(url), headers: build_token_auth_header(jwt.encoded) - - expect(response).to have_gitlab_http_status(:ok) - expect(response.headers['X-Conan-Server-Capabilities']).to eq("") - end - - it 'responds with 401 Unauthorized when invalid access token ID is provided' do - jwt = build_jwt(double(id: 12345), user_id: personal_access_token.user_id) - get api(url), headers: build_token_auth_header(jwt.encoded) - - expect(response).to have_gitlab_http_status(:unauthorized) - end - - it 'responds with 401 Unauthorized when invalid user is provided' do - jwt = build_jwt(personal_access_token, user_id: 12345) - get api(url), headers: build_token_auth_header(jwt.encoded) - - expect(response).to have_gitlab_http_status(:unauthorized) - end - - it 'responds with 401 Unauthorized when the provided JWT is signed with different secret' do - jwt = build_jwt(personal_access_token, secret: SecureRandom.base64(32)) - get api(url), headers: build_token_auth_header(jwt.encoded) - - expect(response).to have_gitlab_http_status(:unauthorized) - end - - it 'responds with 401 Unauthorized when invalid JWT is provided' do - get api(url), headers: build_token_auth_header('invalid-jwt') - - expect(response).to have_gitlab_http_status(:unauthorized) - end - context 'packages feature disabled' do it 'responds with 404 Not Found' do stub_packages_setting(enabled: false) @@ -72,7 +22,10 @@ RSpec.shared_examples 'conan search endpoint' do before do project.update_column(:visibility_level, Gitlab::VisibilityLevel::PUBLIC) - get api(url), headers: headers, params: params + # Do not pass the HTTP_AUTHORIZATION header, + # in order to test that this public project's packages + # are visible to anonymous search. + get api(url), params: params end subject { json_response['results'] } @@ -109,6 +62,33 @@ RSpec.shared_examples 'conan authenticate endpoint' do end end + it 'responds with 401 Unauthorized when an invalid access token ID is provided' do + jwt = build_jwt(double(id: 12345), user_id: personal_access_token.user_id) + get api(url), headers: build_token_auth_header(jwt.encoded) + + expect(response).to have_gitlab_http_status(:unauthorized) + end + + it 'responds with 401 Unauthorized when invalid user is provided' do + jwt = build_jwt(personal_access_token, user_id: 12345) + get api(url), headers: build_token_auth_header(jwt.encoded) + + expect(response).to have_gitlab_http_status(:unauthorized) + end + + it 'responds with 401 Unauthorized when the provided JWT is signed with different secret' do + jwt = build_jwt(personal_access_token, secret: SecureRandom.base64(32)) + get api(url), headers: build_token_auth_header(jwt.encoded) + + expect(response).to have_gitlab_http_status(:unauthorized) + end + + it 'responds with 401 UnauthorizedOK when invalid JWT is provided' do + get api(url), headers: build_token_auth_header('invalid-jwt') + + expect(response).to have_gitlab_http_status(:unauthorized) + end + context 'when valid JWT access token is provided' do it 'responds with 200' do subject @@ -507,19 +487,37 @@ RSpec.shared_examples 'delete package endpoint' do end end +RSpec.shared_examples 'allows download with no token' do + context 'with no private token' do + let(:headers) { {} } + + it 'returns 200' do + subject + + expect(response).to have_gitlab_http_status(:ok) + end + end +end + RSpec.shared_examples 'denies download with no token' do context 'with no private token' do let(:headers) { {} } - it 'returns 400' do + it 'returns 404' do subject - expect(response).to have_gitlab_http_status(:unauthorized) + expect(response).to have_gitlab_http_status(:not_found) end end end RSpec.shared_examples 'a public project with packages' do + before do + project.update_column(:visibility_level, Gitlab::VisibilityLevel::PUBLIC) + end + + it_behaves_like 'allows download with no token' + it 'returns the file' do subject diff --git a/spec/support/shared_examples/requests/api/graphql/group_and_project_boards_query_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/group_and_project_boards_query_shared_examples.rb index 54f4ba7ff73..274516cd87b 100644 --- a/spec/support/shared_examples/requests/api/graphql/group_and_project_boards_query_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/graphql/group_and_project_boards_query_shared_examples.rb @@ -25,7 +25,7 @@ RSpec.shared_examples 'group and project boards query' do board = create(:board, resource_parent: board_parent, name: 'A') allow(Ability).to receive(:allowed?).and_call_original - allow(Ability).to receive(:allowed?).with(user, :read_board, board).and_return(false) + allow(Ability).to receive(:allowed?).with(user, :read_issue_board, board).and_return(false) post_graphql(query, current_user: current_user) diff --git a/spec/support/shared_examples/requests/api/graphql/packages/group_and_project_packages_list_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/packages/group_and_project_packages_list_shared_examples.rb new file mode 100644 index 00000000000..66fbfa798b0 --- /dev/null +++ b/spec/support/shared_examples/requests/api/graphql/packages/group_and_project_packages_list_shared_examples.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'group and project packages query' do + include GraphqlHelpers + + context 'when user has access to the resource' do + before do + resource.add_reporter(current_user) + post_graphql(query, current_user: current_user) + end + + it_behaves_like 'a working graphql query' + + it 'returns packages successfully' do + expect(package_names).to contain_exactly( + package.name, + maven_package.name, + debian_package.name, + composer_package.name + ) + end + + it 'deals with metadata' do + expect(target_shas).to contain_exactly(composer_metadatum.target_sha) + end + end + + context 'when the user does not have access to the resource' do + before do + post_graphql(query, current_user: current_user) + end + + it_behaves_like 'a working graphql query' + + it 'returns nil' do + expect(packages).to be_nil + end + end + + context 'when the user is not authenticated' do + before do + post_graphql(query) + end + + it_behaves_like 'a working graphql query' + + it 'returns nil' do + expect(packages).to be_nil + end + end +end diff --git a/spec/support/shared_examples/requests/api/logging_application_context_shared_examples.rb b/spec/support/shared_examples/requests/api/logging_application_context_shared_examples.rb index 038ede884c8..4a71b696d57 100644 --- a/spec/support/shared_examples/requests/api/logging_application_context_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/logging_application_context_shared_examples.rb @@ -22,3 +22,19 @@ RSpec.shared_examples 'storing arguments in the application context' do hash.transform_keys! { |key| "meta.#{key}" } end end + +RSpec.shared_examples 'not executing any extra queries for the application context' do |expected_extra_queries = 0| + it 'does not execute more queries than without adding anything to the application context' do + # Call the subject once to memoize all factories being used for the spec, so they won't + # add any queries to the expectation. + subject_proc.call + + expect do + allow(Gitlab::ApplicationContext).to receive(:push).and_call_original + subject_proc.call + end.to issue_same_number_of_queries_as { + allow(Gitlab::ApplicationContext).to receive(:push) + subject_proc.call + }.with_threshold(expected_extra_queries).ignoring_cached_queries + end +end 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 be051dcbb7b..c15c59e1a1d 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 @@ -45,136 +45,234 @@ RSpec.shared_examples 'handling get metadata requests' do |scope: :project| end end - where(:auth, :package_name_type, :request_forward, :visibility, :user_role, :expected_result, :expected_status) do - nil | :scoped_naming_convention | true | 'PUBLIC' | nil | :accept | :ok - nil | :scoped_naming_convention | false | 'PUBLIC' | nil | :accept | :ok - nil | :non_existing | true | 'PUBLIC' | nil | :redirect | :redirected - nil | :non_existing | false | 'PUBLIC' | nil | :reject | :not_found - nil | :scoped_naming_convention | true | 'PRIVATE' | nil | :reject | :not_found - nil | :scoped_naming_convention | false | 'PRIVATE' | nil | :reject | :not_found - nil | :non_existing | true | 'PRIVATE' | nil | :redirect | :redirected - nil | :non_existing | false | 'PRIVATE' | nil | :reject | :not_found - nil | :scoped_naming_convention | true | 'INTERNAL' | nil | :reject | :not_found - nil | :scoped_naming_convention | false | 'INTERNAL' | nil | :reject | :not_found - nil | :non_existing | true | 'INTERNAL' | nil | :redirect | :redirected - nil | :non_existing | false | 'INTERNAL' | nil | :reject | :not_found - - :oauth | :scoped_naming_convention | true | 'PUBLIC' | :guest | :accept | :ok - :oauth | :scoped_naming_convention | true | 'PUBLIC' | :reporter | :accept | :ok - :oauth | :scoped_naming_convention | false | 'PUBLIC' | :guest | :accept | :ok - :oauth | :scoped_naming_convention | false | 'PUBLIC' | :reporter | :accept | :ok - :oauth | :non_existing | true | 'PUBLIC' | :guest | :redirect | :redirected - :oauth | :non_existing | true | 'PUBLIC' | :reporter | :redirect | :redirected - :oauth | :non_existing | false | 'PUBLIC' | :guest | :reject | :not_found - :oauth | :non_existing | false | 'PUBLIC' | :reporter | :reject | :not_found - :oauth | :scoped_naming_convention | true | 'PRIVATE' | :guest | :reject | :forbidden - :oauth | :scoped_naming_convention | true | 'PRIVATE' | :reporter | :accept | :ok - :oauth | :scoped_naming_convention | false | 'PRIVATE' | :guest | :reject | :forbidden - :oauth | :scoped_naming_convention | false | 'PRIVATE' | :reporter | :accept | :ok - :oauth | :non_existing | true | 'PRIVATE' | :guest | :redirect | :redirected - :oauth | :non_existing | true | 'PRIVATE' | :reporter | :redirect | :redirected - :oauth | :non_existing | false | 'PRIVATE' | :guest | :reject | :forbidden - :oauth | :non_existing | false | 'PRIVATE' | :reporter | :reject | :not_found - :oauth | :scoped_naming_convention | true | 'INTERNAL' | :guest | :accept | :ok - :oauth | :scoped_naming_convention | true | 'INTERNAL' | :reporter | :accept | :ok - :oauth | :scoped_naming_convention | false | 'INTERNAL' | :guest | :accept | :ok - :oauth | :scoped_naming_convention | false | 'INTERNAL' | :reporter | :accept | :ok - :oauth | :non_existing | true | 'INTERNAL' | :guest | :redirect | :redirected - :oauth | :non_existing | true | 'INTERNAL' | :reporter | :redirect | :redirected - :oauth | :non_existing | false | 'INTERNAL' | :guest | :reject | :not_found - :oauth | :non_existing | false | 'INTERNAL' | :reporter | :reject | :not_found - - :personal_access_token | :scoped_naming_convention | true | 'PUBLIC' | :guest | :accept | :ok - :personal_access_token | :scoped_naming_convention | true | 'PUBLIC' | :reporter | :accept | :ok - :personal_access_token | :scoped_naming_convention | false | 'PUBLIC' | :guest | :accept | :ok - :personal_access_token | :scoped_naming_convention | false | 'PUBLIC' | :reporter | :accept | :ok - :personal_access_token | :non_existing | true | 'PUBLIC' | :guest | :redirect | :redirected - :personal_access_token | :non_existing | true | 'PUBLIC' | :reporter | :redirect | :redirected - :personal_access_token | :non_existing | false | 'PUBLIC' | :guest | :reject | :not_found - :personal_access_token | :non_existing | false | 'PUBLIC' | :reporter | :reject | :not_found - :personal_access_token | :scoped_naming_convention | true | 'PRIVATE' | :guest | :reject | :forbidden - :personal_access_token | :scoped_naming_convention | true | 'PRIVATE' | :reporter | :accept | :ok - :personal_access_token | :scoped_naming_convention | false | 'PRIVATE' | :guest | :reject | :forbidden - :personal_access_token | :scoped_naming_convention | false | 'PRIVATE' | :reporter | :accept | :ok - :personal_access_token | :non_existing | true | 'PRIVATE' | :guest | :redirect | :redirected - :personal_access_token | :non_existing | true | 'PRIVATE' | :reporter | :redirect | :redirected - :personal_access_token | :non_existing | false | 'PRIVATE' | :guest | :reject | :forbidden - :personal_access_token | :non_existing | false | 'PRIVATE' | :reporter | :reject | :not_found - :personal_access_token | :scoped_naming_convention | true | 'INTERNAL' | :guest | :accept | :ok - :personal_access_token | :scoped_naming_convention | true | 'INTERNAL' | :reporter | :accept | :ok - :personal_access_token | :scoped_naming_convention | false | 'INTERNAL' | :guest | :accept | :ok - :personal_access_token | :scoped_naming_convention | false | 'INTERNAL' | :reporter | :accept | :ok - :personal_access_token | :non_existing | true | 'INTERNAL' | :guest | :redirect | :redirected - :personal_access_token | :non_existing | true | 'INTERNAL' | :reporter | :redirect | :redirected - :personal_access_token | :non_existing | false | 'INTERNAL' | :guest | :reject | :not_found - :personal_access_token | :non_existing | false | 'INTERNAL' | :reporter | :reject | :not_found - - :job_token | :scoped_naming_convention | true | 'PUBLIC' | :developer | :accept | :ok - :job_token | :scoped_naming_convention | false | 'PUBLIC' | :developer | :accept | :ok - :job_token | :non_existing | true | 'PUBLIC' | :developer | :redirect | :redirected - :job_token | :non_existing | false | 'PUBLIC' | :developer | :reject | :not_found - :job_token | :scoped_naming_convention | true | 'PRIVATE' | :developer | :accept | :ok - :job_token | :scoped_naming_convention | false | 'PRIVATE' | :developer | :accept | :ok - :job_token | :non_existing | true | 'PRIVATE' | :developer | :redirect | :redirected - :job_token | :non_existing | false | 'PRIVATE' | :developer | :reject | :not_found - :job_token | :scoped_naming_convention | true | 'INTERNAL' | :developer | :accept | :ok - :job_token | :scoped_naming_convention | false | 'INTERNAL' | :developer | :accept | :ok - :job_token | :non_existing | true | 'INTERNAL' | :developer | :redirect | :redirected - :job_token | :non_existing | false | 'INTERNAL' | :developer | :reject | :not_found - - :deploy_token | :scoped_naming_convention | true | 'PUBLIC' | nil | :accept | :ok - :deploy_token | :scoped_naming_convention | false | 'PUBLIC' | nil | :accept | :ok - :deploy_token | :non_existing | true | 'PUBLIC' | nil | :redirect | :redirected - :deploy_token | :non_existing | false | 'PUBLIC' | nil | :reject | :not_found - :deploy_token | :scoped_naming_convention | true | 'PRIVATE' | nil | :accept | :ok - :deploy_token | :scoped_naming_convention | false | 'PRIVATE' | nil | :accept | :ok - :deploy_token | :non_existing | true | 'PRIVATE' | nil | :redirect | :redirected - :deploy_token | :non_existing | false | 'PRIVATE' | nil | :reject | :not_found - :deploy_token | :scoped_naming_convention | true | 'INTERNAL' | nil | :accept | :ok - :deploy_token | :scoped_naming_convention | false | 'INTERNAL' | nil | :accept | :ok - :deploy_token | :non_existing | true | 'INTERNAL' | nil | :redirect | :redirected - :deploy_token | :non_existing | false | 'INTERNAL' | nil | :reject | :not_found - end + shared_examples 'handling all conditions' do + where(:auth, :package_name_type, :request_forward, :visibility, :user_role, :expected_result, :expected_status) do + nil | :scoped_naming_convention | true | :public | nil | :accept | :ok + nil | :scoped_naming_convention | false | :public | nil | :accept | :ok + nil | :scoped_no_naming_convention | true | :public | nil | :accept | :ok + nil | :scoped_no_naming_convention | false | :public | nil | :accept | :ok + nil | :unscoped | true | :public | nil | :accept | :ok + nil | :unscoped | false | :public | nil | :accept | :ok + nil | :non_existing | true | :public | nil | :redirect | :redirected + nil | :non_existing | false | :public | nil | :reject | :not_found + nil | :scoped_naming_convention | true | :private | nil | :reject | :not_found + nil | :scoped_naming_convention | false | :private | nil | :reject | :not_found + nil | :scoped_no_naming_convention | true | :private | nil | :reject | :not_found + nil | :scoped_no_naming_convention | false | :private | nil | :reject | :not_found + nil | :unscoped | true | :private | nil | :reject | :not_found + nil | :unscoped | false | :private | nil | :reject | :not_found + nil | :non_existing | true | :private | nil | :redirect | :redirected + nil | :non_existing | false | :private | nil | :reject | :not_found + nil | :scoped_naming_convention | true | :internal | nil | :reject | :not_found + nil | :scoped_naming_convention | false | :internal | nil | :reject | :not_found + nil | :scoped_no_naming_convention | true | :internal | nil | :reject | :not_found + nil | :scoped_no_naming_convention | false | :internal | nil | :reject | :not_found + nil | :unscoped | true | :internal | nil | :reject | :not_found + nil | :unscoped | false | :internal | nil | :reject | :not_found + nil | :non_existing | true | :internal | nil | :redirect | :redirected + nil | :non_existing | false | :internal | nil | :reject | :not_found + + :oauth | :scoped_naming_convention | true | :public | :guest | :accept | :ok + :oauth | :scoped_naming_convention | true | :public | :reporter | :accept | :ok + :oauth | :scoped_naming_convention | false | :public | :guest | :accept | :ok + :oauth | :scoped_naming_convention | false | :public | :reporter | :accept | :ok + :oauth | :scoped_no_naming_convention | true | :public | :guest | :accept | :ok + :oauth | :scoped_no_naming_convention | true | :public | :reporter | :accept | :ok + :oauth | :scoped_no_naming_convention | false | :public | :guest | :accept | :ok + :oauth | :scoped_no_naming_convention | false | :public | :reporter | :accept | :ok + :oauth | :unscoped | true | :public | :guest | :accept | :ok + :oauth | :unscoped | true | :public | :reporter | :accept | :ok + :oauth | :unscoped | false | :public | :guest | :accept | :ok + :oauth | :unscoped | false | :public | :reporter | :accept | :ok + :oauth | :non_existing | true | :public | :guest | :redirect | :redirected + :oauth | :non_existing | true | :public | :reporter | :redirect | :redirected + :oauth | :non_existing | false | :public | :guest | :reject | :not_found + :oauth | :non_existing | false | :public | :reporter | :reject | :not_found + :oauth | :scoped_naming_convention | true | :private | :guest | :reject | :forbidden + :oauth | :scoped_naming_convention | true | :private | :reporter | :accept | :ok + :oauth | :scoped_naming_convention | false | :private | :guest | :reject | :forbidden + :oauth | :scoped_naming_convention | false | :private | :reporter | :accept | :ok + :oauth | :scoped_no_naming_convention | true | :private | :guest | :reject | :forbidden + :oauth | :scoped_no_naming_convention | true | :private | :reporter | :accept | :ok + :oauth | :scoped_no_naming_convention | false | :private | :guest | :reject | :forbidden + :oauth | :scoped_no_naming_convention | false | :private | :reporter | :accept | :ok + :oauth | :unscoped | true | :private | :guest | :reject | :forbidden + :oauth | :unscoped | true | :private | :reporter | :accept | :ok + :oauth | :unscoped | false | :private | :guest | :reject | :forbidden + :oauth | :unscoped | false | :private | :reporter | :accept | :ok + :oauth | :non_existing | true | :private | :guest | :redirect | :redirected + :oauth | :non_existing | true | :private | :reporter | :redirect | :redirected + :oauth | :non_existing | false | :private | :guest | :reject | :forbidden + :oauth | :non_existing | false | :private | :reporter | :reject | :not_found + :oauth | :scoped_naming_convention | true | :internal | :guest | :accept | :ok + :oauth | :scoped_naming_convention | true | :internal | :reporter | :accept | :ok + :oauth | :scoped_naming_convention | false | :internal | :guest | :accept | :ok + :oauth | :scoped_naming_convention | false | :internal | :reporter | :accept | :ok + :oauth | :scoped_no_naming_convention | true | :internal | :guest | :accept | :ok + :oauth | :scoped_no_naming_convention | true | :internal | :reporter | :accept | :ok + :oauth | :scoped_no_naming_convention | false | :internal | :guest | :accept | :ok + :oauth | :scoped_no_naming_convention | false | :internal | :reporter | :accept | :ok + :oauth | :unscoped | true | :internal | :guest | :accept | :ok + :oauth | :unscoped | true | :internal | :reporter | :accept | :ok + :oauth | :unscoped | false | :internal | :guest | :accept | :ok + :oauth | :unscoped | false | :internal | :reporter | :accept | :ok + :oauth | :non_existing | true | :internal | :guest | :redirect | :redirected + :oauth | :non_existing | true | :internal | :reporter | :redirect | :redirected + :oauth | :non_existing | false | :internal | :guest | :reject | :not_found + :oauth | :non_existing | false | :internal | :reporter | :reject | :not_found + + :personal_access_token | :scoped_naming_convention | true | :public | :guest | :accept | :ok + :personal_access_token | :scoped_naming_convention | true | :public | :reporter | :accept | :ok + :personal_access_token | :scoped_naming_convention | false | :public | :guest | :accept | :ok + :personal_access_token | :scoped_naming_convention | false | :public | :reporter | :accept | :ok + :personal_access_token | :scoped_no_naming_convention | true | :public | :guest | :accept | :ok + :personal_access_token | :scoped_no_naming_convention | true | :public | :reporter | :accept | :ok + :personal_access_token | :scoped_no_naming_convention | false | :public | :guest | :accept | :ok + :personal_access_token | :scoped_no_naming_convention | false | :public | :reporter | :accept | :ok + :personal_access_token | :unscoped | true | :public | :guest | :accept | :ok + :personal_access_token | :unscoped | true | :public | :reporter | :accept | :ok + :personal_access_token | :unscoped | false | :public | :guest | :accept | :ok + :personal_access_token | :unscoped | false | :public | :reporter | :accept | :ok + :personal_access_token | :non_existing | true | :public | :guest | :redirect | :redirected + :personal_access_token | :non_existing | true | :public | :reporter | :redirect | :redirected + :personal_access_token | :non_existing | false | :public | :guest | :reject | :not_found + :personal_access_token | :non_existing | false | :public | :reporter | :reject | :not_found + :personal_access_token | :scoped_naming_convention | true | :private | :guest | :reject | :forbidden + :personal_access_token | :scoped_naming_convention | true | :private | :reporter | :accept | :ok + :personal_access_token | :scoped_naming_convention | false | :private | :guest | :reject | :forbidden + :personal_access_token | :scoped_naming_convention | false | :private | :reporter | :accept | :ok + :personal_access_token | :scoped_no_naming_convention | true | :private | :guest | :reject | :forbidden + :personal_access_token | :scoped_no_naming_convention | true | :private | :reporter | :accept | :ok + :personal_access_token | :scoped_no_naming_convention | false | :private | :guest | :reject | :forbidden + :personal_access_token | :scoped_no_naming_convention | false | :private | :reporter | :accept | :ok + :personal_access_token | :unscoped | true | :private | :guest | :reject | :forbidden + :personal_access_token | :unscoped | true | :private | :reporter | :accept | :ok + :personal_access_token | :unscoped | false | :private | :guest | :reject | :forbidden + :personal_access_token | :unscoped | false | :private | :reporter | :accept | :ok + :personal_access_token | :non_existing | true | :private | :guest | :redirect | :redirected + :personal_access_token | :non_existing | true | :private | :reporter | :redirect | :redirected + :personal_access_token | :non_existing | false | :private | :guest | :reject | :forbidden + :personal_access_token | :non_existing | false | :private | :reporter | :reject | :not_found + :personal_access_token | :scoped_naming_convention | true | :internal | :guest | :accept | :ok + :personal_access_token | :scoped_naming_convention | true | :internal | :reporter | :accept | :ok + :personal_access_token | :scoped_naming_convention | false | :internal | :guest | :accept | :ok + :personal_access_token | :scoped_naming_convention | false | :internal | :reporter | :accept | :ok + :personal_access_token | :scoped_no_naming_convention | true | :internal | :guest | :accept | :ok + :personal_access_token | :scoped_no_naming_convention | true | :internal | :reporter | :accept | :ok + :personal_access_token | :scoped_no_naming_convention | false | :internal | :guest | :accept | :ok + :personal_access_token | :scoped_no_naming_convention | false | :internal | :reporter | :accept | :ok + :personal_access_token | :unscoped | true | :internal | :guest | :accept | :ok + :personal_access_token | :unscoped | true | :internal | :reporter | :accept | :ok + :personal_access_token | :unscoped | false | :internal | :guest | :accept | :ok + :personal_access_token | :unscoped | false | :internal | :reporter | :accept | :ok + :personal_access_token | :non_existing | true | :internal | :guest | :redirect | :redirected + :personal_access_token | :non_existing | true | :internal | :reporter | :redirect | :redirected + :personal_access_token | :non_existing | false | :internal | :guest | :reject | :not_found + :personal_access_token | :non_existing | false | :internal | :reporter | :reject | :not_found + + :job_token | :scoped_naming_convention | true | :public | :developer | :accept | :ok + :job_token | :scoped_naming_convention | false | :public | :developer | :accept | :ok + :job_token | :scoped_no_naming_convention | true | :public | :developer | :accept | :ok + :job_token | :scoped_no_naming_convention | false | :public | :developer | :accept | :ok + :job_token | :unscoped | true | :public | :developer | :accept | :ok + :job_token | :unscoped | false | :public | :developer | :accept | :ok + :job_token | :non_existing | true | :public | :developer | :redirect | :redirected + :job_token | :non_existing | false | :public | :developer | :reject | :not_found + :job_token | :scoped_naming_convention | true | :private | :developer | :accept | :ok + :job_token | :scoped_naming_convention | false | :private | :developer | :accept | :ok + :job_token | :scoped_no_naming_convention | true | :private | :developer | :accept | :ok + :job_token | :scoped_no_naming_convention | false | :private | :developer | :accept | :ok + :job_token | :unscoped | true | :private | :developer | :accept | :ok + :job_token | :unscoped | false | :private | :developer | :accept | :ok + :job_token | :non_existing | true | :private | :developer | :redirect | :redirected + :job_token | :non_existing | false | :private | :developer | :reject | :not_found + :job_token | :scoped_naming_convention | true | :internal | :developer | :accept | :ok + :job_token | :scoped_naming_convention | false | :internal | :developer | :accept | :ok + :job_token | :scoped_no_naming_convention | true | :internal | :developer | :accept | :ok + :job_token | :scoped_no_naming_convention | false | :internal | :developer | :accept | :ok + :job_token | :unscoped | true | :internal | :developer | :accept | :ok + :job_token | :unscoped | false | :internal | :developer | :accept | :ok + :job_token | :non_existing | true | :internal | :developer | :redirect | :redirected + :job_token | :non_existing | false | :internal | :developer | :reject | :not_found + + :deploy_token | :scoped_naming_convention | true | :public | nil | :accept | :ok + :deploy_token | :scoped_naming_convention | false | :public | nil | :accept | :ok + :deploy_token | :scoped_no_naming_convention | true | :public | nil | :accept | :ok + :deploy_token | :scoped_no_naming_convention | false | :public | nil | :accept | :ok + :deploy_token | :unscoped | true | :public | nil | :accept | :ok + :deploy_token | :unscoped | false | :public | nil | :accept | :ok + :deploy_token | :non_existing | true | :public | nil | :redirect | :redirected + :deploy_token | :non_existing | false | :public | nil | :reject | :not_found + :deploy_token | :scoped_naming_convention | true | :private | nil | :accept | :ok + :deploy_token | :scoped_naming_convention | false | :private | nil | :accept | :ok + :deploy_token | :scoped_no_naming_convention | true | :private | nil | :accept | :ok + :deploy_token | :scoped_no_naming_convention | false | :private | nil | :accept | :ok + :deploy_token | :unscoped | true | :private | nil | :accept | :ok + :deploy_token | :unscoped | false | :private | nil | :accept | :ok + :deploy_token | :non_existing | true | :private | nil | :redirect | :redirected + :deploy_token | :non_existing | false | :private | nil | :reject | :not_found + :deploy_token | :scoped_naming_convention | true | :internal | nil | :accept | :ok + :deploy_token | :scoped_naming_convention | false | :internal | nil | :accept | :ok + :deploy_token | :scoped_no_naming_convention | true | :internal | nil | :accept | :ok + :deploy_token | :scoped_no_naming_convention | false | :internal | nil | :accept | :ok + :deploy_token | :unscoped | true | :internal | nil | :accept | :ok + :deploy_token | :unscoped | false | :internal | nil | :accept | :ok + :deploy_token | :non_existing | true | :internal | nil | :redirect | :redirected + :deploy_token | :non_existing | false | :internal | nil | :reject | :not_found + end - with_them do - include_context 'set package name from package name type' - - let(:headers) do - case auth - when :oauth - build_token_auth_header(token.token) - when :personal_access_token - build_token_auth_header(personal_access_token.token) - when :job_token - build_token_auth_header(job.token) - when :deploy_token - build_token_auth_header(deploy_token.token) - else - {} + with_them do + include_context 'set package name from package name type' + + let(:headers) do + case auth + when :oauth + build_token_auth_header(token.token) + when :personal_access_token + build_token_auth_header(personal_access_token.token) + when :job_token + build_token_auth_header(job.token) + when :deploy_token + build_token_auth_header(deploy_token.token) + else + {} + end end - end - before do - project.send("add_#{user_role}", user) if user_role - project.update!(visibility: Gitlab::VisibilityLevel.const_get(visibility, false)) - package.update!(name: package_name) unless package_name == 'non-existing-package' - stub_application_setting(npm_package_requests_forwarding: request_forward) - end + before do + 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' + stub_application_setting(npm_package_requests_forwarding: request_forward) + end - example_name = "#{params[:expected_result]} metadata request" - status = params[:expected_status] + example_name = "#{params[:expected_result]} metadata request" + status = params[:expected_status] - if scope == :instance && params[:package_name_type] != :scoped_naming_convention - if params[:request_forward] - example_name = 'redirect metadata request' - status = :redirected - else - example_name = 'reject metadata request' - status = :not_found + if scope == :instance && params[:package_name_type] != :scoped_naming_convention + if params[:request_forward] + example_name = 'redirect metadata request' + status = :redirected + else + example_name = 'reject metadata request' + status = :not_found + end end + + it_behaves_like example_name, status: status end + end - it_behaves_like example_name, status: status + context 'with a group namespace' do + it_behaves_like 'handling all conditions' + end + + if scope != :project + context 'with a user namespace' do + let_it_be(:namespace) { user.namespace } + + it_behaves_like 'handling all conditions' + end end context 'with a developer' do @@ -225,26 +323,44 @@ RSpec.shared_examples 'handling get dist tags requests' do |scope: :project| shared_examples 'handling different package names, visibilities and user roles' do where(:package_name_type, :visibility, :user_role, :expected_result, :expected_status) do - :scoped_naming_convention | 'PUBLIC' | :anonymous | :accept | :ok - :scoped_naming_convention | 'PUBLIC' | :guest | :accept | :ok - :scoped_naming_convention | 'PUBLIC' | :reporter | :accept | :ok - :non_existing | 'PUBLIC' | :anonymous | :reject | :not_found - :non_existing | 'PUBLIC' | :guest | :reject | :not_found - :non_existing | 'PUBLIC' | :reporter | :reject | :not_found - - :scoped_naming_convention | 'PRIVATE' | :anonymous | :reject | :not_found - :scoped_naming_convention | 'PRIVATE' | :guest | :reject | :forbidden - :scoped_naming_convention | 'PRIVATE' | :reporter | :accept | :ok - :non_existing | 'PRIVATE' | :anonymous | :reject | :not_found - :non_existing | 'PRIVATE' | :guest | :reject | :forbidden - :non_existing | 'PRIVATE' | :reporter | :reject | :not_found - - :scoped_naming_convention | 'INTERNAL' | :anonymous | :reject | :not_found - :scoped_naming_convention | 'INTERNAL' | :guest | :accept | :ok - :scoped_naming_convention | 'INTERNAL' | :reporter | :accept | :ok - :non_existing | 'INTERNAL' | :anonymous | :reject | :not_found - :non_existing | 'INTERNAL' | :guest | :reject | :not_found - :non_existing | 'INTERNAL' | :reporter | :reject | :not_found + :scoped_naming_convention | :public | :anonymous | :accept | :ok + :scoped_naming_convention | :public | :guest | :accept | :ok + :scoped_naming_convention | :public | :reporter | :accept | :ok + :scoped_no_naming_convention | :public | :anonymous | :accept | :ok + :scoped_no_naming_convention | :public | :guest | :accept | :ok + :scoped_no_naming_convention | :public | :reporter | :accept | :ok + :unscoped | :public | :anonymous | :accept | :ok + :unscoped | :public | :guest | :accept | :ok + :unscoped | :public | :reporter | :accept | :ok + :non_existing | :public | :anonymous | :reject | :not_found + :non_existing | :public | :guest | :reject | :not_found + :non_existing | :public | :reporter | :reject | :not_found + + :scoped_naming_convention | :private | :anonymous | :reject | :not_found + :scoped_naming_convention | :private | :guest | :reject | :forbidden + :scoped_naming_convention | :private | :reporter | :accept | :ok + :scoped_no_naming_convention | :private | :anonymous | :reject | :not_found + :scoped_no_naming_convention | :private | :guest | :reject | :forbidden + :scoped_no_naming_convention | :private | :reporter | :accept | :ok + :unscoped | :private | :anonymous | :reject | :not_found + :unscoped | :private | :guest | :reject | :forbidden + :unscoped | :private | :reporter | :accept | :ok + :non_existing | :private | :anonymous | :reject | :not_found + :non_existing | :private | :guest | :reject | :forbidden + :non_existing | :private | :reporter | :reject | :not_found + + :scoped_naming_convention | :internal | :anonymous | :reject | :not_found + :scoped_naming_convention | :internal | :guest | :accept | :ok + :scoped_naming_convention | :internal | :reporter | :accept | :ok + :scoped_no_naming_convention | :internal | :anonymous | :reject | :not_found + :scoped_no_naming_convention | :internal | :guest | :accept | :ok + :scoped_no_naming_convention | :internal | :reporter | :accept | :ok + :unscoped | :internal | :anonymous | :reject | :not_found + :unscoped | :internal | :guest | :accept | :ok + :unscoped | :internal | :reporter | :accept | :ok + :non_existing | :internal | :anonymous | :reject | :not_found + :non_existing | :internal | :guest | :reject | :not_found + :non_existing | :internal | :reporter | :reject | :not_found end with_them do @@ -254,7 +370,7 @@ RSpec.shared_examples 'handling get dist tags requests' do |scope: :project| before do project.send("add_#{user_role}", user) unless anonymous - project.update!(visibility: Gitlab::VisibilityLevel.const_get(visibility, false)) + project.update!(visibility: visibility.to_s) end example_name = "#{params[:expected_result]} package tags request" @@ -269,16 +385,30 @@ RSpec.shared_examples 'handling get dist tags requests' do |scope: :project| end end - context 'with oauth token' do - let(:headers) { build_token_auth_header(token.token) } + shared_examples 'handling all conditions' do + context 'with oauth token' do + let(:headers) { build_token_auth_header(token.token) } + + it_behaves_like 'handling different package names, visibilities and user roles' + end + + context 'with personal access token' do + let(:headers) { build_token_auth_header(personal_access_token.token) } - it_behaves_like 'handling different package names, visibilities and user roles' + it_behaves_like 'handling different package names, visibilities and user roles' + end end - context 'with personal access token' do - let(:headers) { build_token_auth_header(personal_access_token.token) } + context 'with a group namespace' do + it_behaves_like 'handling all conditions' + end - it_behaves_like 'handling different package names, visibilities and user roles' + if scope != :project + context 'with a user namespace' do + let_it_be(:namespace) { user.namespace } + + it_behaves_like 'handling all conditions' + end end end @@ -303,26 +433,44 @@ RSpec.shared_examples 'handling create dist tag requests' do |scope: :project| shared_examples 'handling different package names, visibilities and user roles' do where(:package_name_type, :visibility, :user_role, :expected_result, :expected_status) do - :scoped_naming_convention | 'PUBLIC' | :anonymous | :reject | :forbidden - :scoped_naming_convention | 'PUBLIC' | :guest | :reject | :forbidden - :scoped_naming_convention | 'PUBLIC' | :developer | :accept | :ok - :non_existing | 'PUBLIC' | :anonymous | :reject | :forbidden - :non_existing | 'PUBLIC' | :guest | :reject | :forbidden - :non_existing | 'PUBLIC' | :developer | :reject | :not_found - - :scoped_naming_convention | 'PRIVATE' | :anonymous | :reject | :not_found - :scoped_naming_convention | 'PRIVATE' | :guest | :reject | :forbidden - :scoped_naming_convention | 'PRIVATE' | :developer | :accept | :ok - :non_existing | 'PRIVATE' | :anonymous | :reject | :not_found - :non_existing | 'PRIVATE' | :guest | :reject | :forbidden - :non_existing | 'PRIVATE' | :developer | :reject | :not_found - - :scoped_naming_convention | 'INTERNAL' | :anonymous | :reject | :forbidden - :scoped_naming_convention | 'INTERNAL' | :guest | :reject | :forbidden - :scoped_naming_convention | 'INTERNAL' | :developer | :accept | :ok - :non_existing | 'INTERNAL' | :anonymous | :reject | :forbidden - :non_existing | 'INTERNAL' | :guest | :reject | :forbidden - :non_existing | 'INTERNAL' | :developer | :reject | :not_found + :scoped_naming_convention | :public | :anonymous | :reject | :forbidden + :scoped_naming_convention | :public | :guest | :reject | :forbidden + :scoped_naming_convention | :public | :developer | :accept | :ok + :scoped_no_naming_convention | :public | :anonymous | :reject | :forbidden + :scoped_no_naming_convention | :public | :guest | :reject | :forbidden + :scoped_no_naming_convention | :public | :developer | :accept | :ok + :unscoped | :public | :anonymous | :reject | :forbidden + :unscoped | :public | :guest | :reject | :forbidden + :unscoped | :public | :developer | :accept | :ok + :non_existing | :public | :anonymous | :reject | :forbidden + :non_existing | :public | :guest | :reject | :forbidden + :non_existing | :public | :developer | :reject | :not_found + + :scoped_naming_convention | :private | :anonymous | :reject | :not_found + :scoped_naming_convention | :private | :guest | :reject | :forbidden + :scoped_naming_convention | :private | :developer | :accept | :ok + :scoped_no_naming_convention | :private | :anonymous | :reject | :not_found + :scoped_no_naming_convention | :private | :guest | :reject | :forbidden + :scoped_no_naming_convention | :private | :developer | :accept | :ok + :unscoped | :private | :anonymous | :reject | :not_found + :unscoped | :private | :guest | :reject | :forbidden + :unscoped | :private | :developer | :accept | :ok + :non_existing | :private | :anonymous | :reject | :not_found + :non_existing | :private | :guest | :reject | :forbidden + :non_existing | :private | :developer | :reject | :not_found + + :scoped_naming_convention | :internal | :anonymous | :reject | :forbidden + :scoped_naming_convention | :internal | :guest | :reject | :forbidden + :scoped_naming_convention | :internal | :developer | :accept | :ok + :scoped_no_naming_convention | :internal | :anonymous | :reject | :forbidden + :scoped_no_naming_convention | :internal | :guest | :reject | :forbidden + :scoped_no_naming_convention | :internal | :developer | :accept | :ok + :unscoped | :internal | :anonymous | :reject | :forbidden + :unscoped | :internal | :guest | :reject | :forbidden + :unscoped | :internal | :developer | :accept | :ok + :non_existing | :internal | :anonymous | :reject | :forbidden + :non_existing | :internal | :guest | :reject | :forbidden + :non_existing | :internal | :developer | :reject | :not_found end with_them do @@ -332,7 +480,7 @@ RSpec.shared_examples 'handling create dist tag requests' do |scope: :project| before do project.send("add_#{user_role}", user) unless anonymous - project.update!(visibility: Gitlab::VisibilityLevel.const_get(visibility, false)) + project.update!(visibility: visibility.to_s) end example_name = "#{params[:expected_result]} create package tag request" @@ -347,16 +495,30 @@ RSpec.shared_examples 'handling create dist tag requests' do |scope: :project| end end - context 'with oauth token' do - let(:headers) { build_token_auth_header(token.token) } + shared_examples 'handling all conditions' do + context 'with oauth token' do + let(:headers) { build_token_auth_header(token.token) } - it_behaves_like 'handling different package names, visibilities and user roles' + it_behaves_like 'handling different package names, visibilities and user roles' + end + + context 'with personal access token' do + let(:headers) { build_token_auth_header(personal_access_token.token) } + + it_behaves_like 'handling different package names, visibilities and user roles' + end end - context 'with personal access token' do - let(:headers) { build_token_auth_header(personal_access_token.token) } + context 'with a group namespace' do + it_behaves_like 'handling all conditions' + end - it_behaves_like 'handling different package names, visibilities and user roles' + if scope != :project + context 'with a user namespace' do + let_it_be(:namespace) { user.namespace } + + it_behaves_like 'handling all conditions' + end end end @@ -379,19 +541,44 @@ RSpec.shared_examples 'handling delete dist tag requests' do |scope: :project| shared_examples 'handling different package names, visibilities and user roles' do where(:package_name_type, :visibility, :user_role, :expected_result, :expected_status) do - :scoped_naming_convention | 'PUBLIC' | :anonymous | :reject | :forbidden - :scoped_naming_convention | 'PUBLIC' | :guest | :reject | :forbidden - :scoped_naming_convention | 'PUBLIC' | :maintainer | :accept | :ok - :non_existing | 'PUBLIC' | :anonymous | :reject | :forbidden - :non_existing | 'PUBLIC' | :guest | :reject | :forbidden - :non_existing | 'PUBLIC' | :maintainer | :reject | :not_found - - :scoped_naming_convention | 'PRIVATE' | :anonymous | :reject | :not_found - :scoped_naming_convention | 'PRIVATE' | :guest | :reject | :forbidden - :scoped_naming_convention | 'PRIVATE' | :maintainer | :accept | :ok - :non_existing | 'INTERNAL' | :anonymous | :reject | :forbidden - :non_existing | 'INTERNAL' | :guest | :reject | :forbidden - :non_existing | 'INTERNAL' | :maintainer | :reject | :not_found + :scoped_naming_convention | :public | :anonymous | :reject | :forbidden + :scoped_naming_convention | :public | :guest | :reject | :forbidden + :scoped_naming_convention | :public | :maintainer | :accept | :ok + :scoped_no_naming_convention | :public | :anonymous | :reject | :forbidden + :scoped_no_naming_convention | :public | :guest | :reject | :forbidden + :scoped_no_naming_convention | :public | :maintainer | :accept | :ok + :unscoped | :public | :anonymous | :reject | :forbidden + :unscoped | :public | :guest | :reject | :forbidden + :unscoped | :public | :maintainer | :accept | :ok + :non_existing | :public | :anonymous | :reject | :forbidden + :non_existing | :public | :guest | :reject | :forbidden + :non_existing | :public | :maintainer | :reject | :not_found + + :scoped_naming_convention | :private | :anonymous | :reject | :not_found + :scoped_naming_convention | :private | :guest | :reject | :forbidden + :scoped_naming_convention | :private | :maintainer | :accept | :ok + :scoped_no_naming_convention | :private | :anonymous | :reject | :not_found + :scoped_no_naming_convention | :private | :guest | :reject | :forbidden + :scoped_no_naming_convention | :private | :maintainer | :accept | :ok + :unscoped | :private | :anonymous | :reject | :not_found + :unscoped | :private | :guest | :reject | :forbidden + :unscoped | :private | :maintainer | :accept | :ok + :non_existing | :private | :anonymous | :reject | :not_found + :non_existing | :private | :guest | :reject | :forbidden + :non_existing | :private | :maintainer | :reject | :not_found + + :scoped_naming_convention | :internal | :anonymous | :reject | :forbidden + :scoped_naming_convention | :internal | :guest | :reject | :forbidden + :scoped_naming_convention | :internal | :maintainer | :accept | :ok + :scoped_no_naming_convention | :internal | :anonymous | :reject | :forbidden + :scoped_no_naming_convention | :internal | :guest | :reject | :forbidden + :scoped_no_naming_convention | :internal | :maintainer | :accept | :ok + :unscoped | :internal | :anonymous | :reject | :forbidden + :unscoped | :internal | :guest | :reject | :forbidden + :unscoped | :internal | :maintainer | :accept | :ok + :non_existing | :internal | :anonymous | :reject | :forbidden + :non_existing | :internal | :guest | :reject | :forbidden + :non_existing | :internal | :maintainer | :reject | :not_found end with_them do @@ -401,7 +588,7 @@ RSpec.shared_examples 'handling delete dist tag requests' do |scope: :project| before do project.send("add_#{user_role}", user) unless anonymous - project.update!(visibility: Gitlab::VisibilityLevel.const_get(visibility, false)) + project.update!(visibility: visibility.to_s) end example_name = "#{params[:expected_result]} delete package tag request" @@ -416,15 +603,29 @@ RSpec.shared_examples 'handling delete dist tag requests' do |scope: :project| end end - context 'with oauth token' do - let(:headers) { build_token_auth_header(token.token) } + shared_examples 'handling all conditions' do + context 'with oauth token' do + let(:headers) { build_token_auth_header(token.token) } + + it_behaves_like 'handling different package names, visibilities and user roles' + end + + context 'with personal access token' do + let(:headers) { build_token_auth_header(personal_access_token.token) } - it_behaves_like 'handling different package names, visibilities and user roles' + it_behaves_like 'handling different package names, visibilities and user roles' + end end - context 'with personal access token' do - let(:headers) { build_token_auth_header(personal_access_token.token) } + context 'with a group namespace' do + it_behaves_like 'handling all conditions' + end - it_behaves_like 'handling different package names, visibilities and user roles' + if scope != :project + context 'with a user namespace' do + let_it_be(:namespace) { user.namespace } + + it_behaves_like 'handling all conditions' + end end end diff --git a/spec/support/shared_examples/requests/api/rubygems_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/rubygems_packages_shared_examples.rb new file mode 100644 index 00000000000..15fb6611b90 --- /dev/null +++ b/spec/support/shared_examples/requests/api/rubygems_packages_shared_examples.rb @@ -0,0 +1,194 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'rejects rubygems packages access' do |user_type, status, add_member = true| + context "for user type #{user_type}" do + before do + project.send("add_#{user_type}", user) if add_member && user_type != :anonymous + end + + it_behaves_like 'returning response status', status + end +end + +RSpec.shared_examples 'process rubygems workhorse authorization' do |user_type, status, add_member = true| + context "for user type #{user_type}" do + before do + project.send("add_#{user_type}", user) if add_member && user_type != :anonymous + end + + it_behaves_like 'returning response status', status + + it 'has the proper content type' do + subject + + expect(response.media_type).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE) + end + + context 'with a request that bypassed gitlab-workhorse' do + let(:headers) do + { 'HTTP_AUTHORIZATION' => personal_access_token.token } + .merge(workhorse_headers) + .tap { |h| h.delete(Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER) } + end + + before do + project.add_maintainer(user) + end + + it_behaves_like 'returning response status', :forbidden + end + end +end + +RSpec.shared_examples 'process rubygems upload' do |user_type, status, add_member = true| + RSpec.shared_examples 'creates rubygems package files' do + it 'creates package files', :aggregate_failures do + expect { subject } + .to change { project.packages.count }.by(1) + .and change { Packages::PackageFile.count }.by(1) + expect(response).to have_gitlab_http_status(status) + + package_file = project.packages.last.package_files.reload.last + expect(package_file.file_name).to eq('package.gem') + end + end + + context "for user type #{user_type}" do + before do + project.send("add_#{user_type}", user) if add_member && user_type != :anonymous + end + + context 'with object storage disabled' do + before do + stub_package_file_object_storage(enabled: false) + end + + context 'without a file from workhorse' do + let(:send_rewritten_field) { false } + + it_behaves_like 'returning response status', :bad_request + end + + context 'with correct params' do + it_behaves_like 'package workhorse uploads' + it_behaves_like 'creates rubygems package files' + it_behaves_like 'a package tracking event', 'API::RubygemPackages', 'push_package' + end + end + + context 'with object storage enabled' do + let(:tmp_object) do + fog_connection.directories.new(key: 'packages').files.create( # rubocop:disable Rails/SaveBang + key: "tmp/uploads/#{file_name}", + body: 'content' + ) + end + + let(:fog_file) { fog_to_uploaded_file(tmp_object) } + let(:params) { { file: fog_file, 'file.remote_id' => file_name } } + + context 'and direct upload enabled' do + let(:fog_connection) do + stub_package_file_object_storage(direct_upload: true) + end + + it_behaves_like 'creates rubygems package files' + + ['123123', '../../123123'].each do |remote_id| + context "with invalid remote_id: #{remote_id}" do + let(:params) do + { + file: fog_file, + 'file.remote_id' => remote_id + } + end + + it_behaves_like 'returning response status', :forbidden + end + end + end + + context 'and direct upload disabled' do + context 'and background upload disabled' do + let(:fog_connection) do + stub_package_file_object_storage(direct_upload: false, background_upload: false) + end + + it_behaves_like 'creates rubygems package files' + end + + context 'and background upload enabled' do + let(:fog_connection) do + stub_package_file_object_storage(direct_upload: false, background_upload: true) + end + + it_behaves_like 'creates rubygems package files' + end + end + end + end +end + +RSpec.shared_examples 'dependency endpoint success' do |user_type, status, add_member = true| + context "for user type #{user_type}" do + before do + project.send("add_#{user_type}", user) if add_member && user_type != :anonymous + end + + raise 'Status is not :success' if status != :success + + context 'with no params', :aggregate_failures do + it 'returns empty' do + subject + + expect(response.body).to eq('200') + expect(response).to have_gitlab_http_status(status) + end + end + + context 'with gems params' do + let(:params) { { gems: 'foo,bar' } } + let(:expected_response) { Marshal.dump(%w(result result)) } + + it 'returns successfully', :aggregate_failures do + service_result = double('DependencyResolverService', execute: ServiceResponse.success(payload: 'result')) + + expect(Packages::Rubygems::DependencyResolverService).to receive(:new).with(project, anything, gem_name: 'foo').and_return(service_result) + expect(Packages::Rubygems::DependencyResolverService).to receive(:new).with(project, anything, gem_name: 'bar').and_return(service_result) + + subject + + expect(response.body).to eq(expected_response) # rubocop:disable Security/MarshalLoad + expect(response).to have_gitlab_http_status(status) + end + + it 'rejects if the service fails', :aggregate_failures do + service_result = double('DependencyResolverService', execute: ServiceResponse.error(message: 'rejected', http_status: :bad_request)) + + expect(Packages::Rubygems::DependencyResolverService).to receive(:new).with(project, anything, gem_name: 'foo').and_return(service_result) + + subject + + expect(response.body).to match(/rejected/) + expect(response).to have_gitlab_http_status(:bad_request) + end + end + end +end + +RSpec.shared_examples 'Rubygems gem download' do |user_type, status, add_member = true| + context "for user type #{user_type}" do + before do + project.send("add_#{user_type}", user) if add_member && user_type != :anonymous + end + + it 'returns the gem', :aggregate_failures do + subject + + expect(response.media_type).to eq('application/octet-stream') + expect(response).to have_gitlab_http_status(status) + end + + it_behaves_like 'a package tracking event', described_class.name, 'pull_package' + end +end diff --git a/spec/support/shared_examples/service_desk_issue_templates_examples.rb b/spec/support/shared_examples/service_desk_issue_templates_examples.rb new file mode 100644 index 00000000000..fd9645df7a3 --- /dev/null +++ b/spec/support/shared_examples/service_desk_issue_templates_examples.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'issue description templates from current project only' do + it 'loads issue description templates from the project only' do + within('#service-desk-template-select') do + expect(page).to have_content('project-issue-bar') + expect(page).to have_content('project-issue-foo') + expect(page).not_to have_content('group-issue-bar') + expect(page).not_to have_content('group-issue-foo') + end + end +end diff --git a/spec/support/shared_examples/services/boards/update_boards_shared_examples.rb b/spec/support/shared_examples/services/boards/update_boards_shared_examples.rb new file mode 100644 index 00000000000..cd773a2a04a --- /dev/null +++ b/spec/support/shared_examples/services/boards/update_boards_shared_examples.rb @@ -0,0 +1,83 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'board update service' do + subject(:service) { described_class.new(board.resource_parent, user, all_params) } + + it 'updates the board with valid params' do + result = described_class.new(group, user, name: 'Engineering').execute(board) + + expect(result).to eq(true) + expect(board.reload.name).to eq('Engineering') + end + + it 'does not update the board with invalid params' do + orig_name = board.name + + result = described_class.new(group, user, name: nil).execute(board) + + expect(result).to eq(false) + expect(board.reload.name).to eq(orig_name) + end + + context 'with scoped_issue_board available' do + before do + stub_licensed_features(scoped_issue_board: true) + end + + context 'user is member of the board parent' do + before do + board.resource_parent.add_reporter(user) + end + + it 'updates the configuration params when scoped issue board is enabled' do + service.execute(board) + + labels = updated_scoped_params.delete(:labels) + expect(board.reload).to have_attributes(updated_scoped_params) + expect(board.labels).to match_array(labels) + end + end + + context 'when labels param is used' do + let(:params) { { labels: [label.name, parent_label.name, 'new label'].join(',') } } + + subject(:service) { described_class.new(board.resource_parent, user, params) } + + context 'when user can create new labels' do + before do + board.resource_parent.add_reporter(user) + end + + it 'adds labels to the board' do + service.execute(board) + + expect(board.reload.labels.map(&:name)).to match_array([label.name, parent_label.name, 'new label']) + end + end + + context 'when user can not create new labels' do + before do + board.resource_parent.add_guest(user) + end + + it 'adds only existing labels to the board' do + service.execute(board) + + expect(board.reload.labels.map(&:name)).to match_array([label.name, parent_label.name]) + end + end + end + end + + context 'without scoped_issue_board available' do + before do + stub_licensed_features(scoped_issue_board: false) + end + + it 'filters unpermitted params when scoped issue board is not enabled' do + service.execute(board) + + expect(board.reload).to have_attributes(updated_without_scoped_params) + end + end +end diff --git a/spec/support/shared_examples/services/packages/maven/metadata_shared_examples.rb b/spec/support/shared_examples/services/packages/maven/metadata_shared_examples.rb new file mode 100644 index 00000000000..4de672bb732 --- /dev/null +++ b/spec/support/shared_examples/services/packages/maven/metadata_shared_examples.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'handling metadata content pointing to a file for the create xml service' do + context 'with metadata content pointing to a file' do + let(:service) { described_class.new(metadata_content: file, package: package) } + let(:file) do + Tempfile.new('metadata').tap do |file| + if file_contents + file.write(file_contents) + file.flush + file.rewind + end + end + end + + after do + file.close + file.unlink + end + + context 'with valid content' do + let(:file_contents) { metadata_xml } + + it 'returns no changes' do + expect(subject).to be_success + expect(subject.payload).to eq(changes_exist: false, empty_versions: false) + end + end + + context 'with invalid content' do + let(:file_contents) { '<meta></metadata>' } + + it_behaves_like 'returning an error service response', message: 'metadata_content is invalid' + end + + context 'with no content' do + let(:file_contents) { nil } + + it_behaves_like 'returning an error service response', message: 'metadata_content is invalid' + end + end +end + +RSpec.shared_examples 'handling invalid parameters for create xml service' do + context 'with no package' do + let(:metadata_xml) { '' } + let(:package) { nil } + + it_behaves_like 'returning an error service response', message: 'package not set' + end + + context 'with no metadata content' do + let(:metadata_xml) { nil } + + it_behaves_like 'returning an error service response', message: 'metadata_content not set' + end +end |