diff options
Diffstat (limited to 'spec/support/shared_examples')
54 files changed, 1837 insertions, 599 deletions
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 84910d0dfe4..38a5ed244c4 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 @@ -48,7 +48,7 @@ RSpec.shared_examples 'multiple issue boards' do expect(page).to have_button('This is a new board') end - it 'deletes board' do + it 'deletes board', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/280554' do in_boards_switcher_dropdown do click_button 'Delete board' end diff --git a/spec/support/shared_examples/cached_response_shared_examples.rb b/spec/support/shared_examples/cached_response_shared_examples.rb deleted file mode 100644 index 34e5f741b4e..00000000000 --- a/spec/support/shared_examples/cached_response_shared_examples.rb +++ /dev/null @@ -1,12 +0,0 @@ -# frozen_string_literal: true -# -# Negates lib/gitlab/no_cache_headers.rb -# - -RSpec.shared_examples 'cached response' do - it 'defines a cached header response' do - expect(response.headers["Cache-Control"]).not_to include("no-store", "no-cache") - expect(response.headers["Pragma"]).not_to eq("no-cache") - expect(response.headers["Expires"]).not_to eq("Fri, 01 Jan 1990 00:00:00 GMT") - end -end diff --git a/spec/support/shared_examples/controllers/access_tokens_controller_shared_examples.rb b/spec/support/shared_examples/controllers/access_tokens_controller_shared_examples.rb index 54d41f9a68c..dd71107455f 100644 --- a/spec/support/shared_examples/controllers/access_tokens_controller_shared_examples.rb +++ b/spec/support/shared_examples/controllers/access_tokens_controller_shared_examples.rb @@ -73,7 +73,23 @@ RSpec.shared_examples 'project access tokens available #create' do end end - it { expect(subject).to render_template(:index) } + it 'does not create the token' do + expect { subject }.not_to change { PersonalAccessToken.count } + end + + it 'does not add the project bot as a member' do + expect { subject }.not_to change { Member.count } + end + + it 'does not create the project bot user' do + expect { subject }.not_to change { User.count } + end + + it 'shows a failure alert' do + subject + + expect(response.flash[:alert]).to match("Failed to create new project access token: Failed!") + end end end diff --git a/spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb b/spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb index 2fcc88ef36a..5a4322f73b6 100644 --- a/spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb +++ b/spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb @@ -145,6 +145,8 @@ RSpec.shared_examples 'a GitHub-ish import controller: GET status' do group.add_owner(user) client = stub_client(repos: repos, orgs: [org], org_repos: [org_repo]) allow(client).to receive(:each_page).and_return([OpenStruct.new(objects: repos)].to_enum) + # GitHub controller has filtering done using GitHub Search API + stub_feature_flags(remove_legacy_github_client: false) end it 'filters list of repositories by name' do @@ -157,6 +159,16 @@ RSpec.shared_examples 'a GitHub-ish import controller: GET status' do expect(json_response.dig("namespaces", 0, "id")).to eq(group.id) end + it 'filters the list, ignoring the case of the name' do + get :status, params: { filter: 'EMACS' }, format: :json + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response.dig("imported_projects").count).to eq(0) + expect(json_response.dig("provider_repos").count).to eq(1) + expect(json_response.dig("provider_repos", 0, "id")).to eq(repo_2.id) + expect(json_response.dig("namespaces", 0, "id")).to eq(group.id) + end + context 'when user input contains html' do let(:expected_filter) { 'test' } let(:filter) { "<html>#{expected_filter}</html>" } @@ -167,6 +179,23 @@ RSpec.shared_examples 'a GitHub-ish import controller: GET status' do expect(assigns(:filter)).to eq(expected_filter) end end + + context 'when the client returns a non-string name' do + before do + repos = [build(:project, name: 2, path: 'test')] + + client = stub_client(repos: repos) + allow(client).to receive(:each_page).and_return([OpenStruct.new(objects: repos)].to_enum) + end + + it 'does not raise an error' do + get :status, params: { filter: '2' }, format: :json + + expect(response).to have_gitlab_http_status :ok + + expect(json_response.dig("provider_repos").count).to eq(1) + end + end end end diff --git a/spec/support/shared_examples/controllers/trackable_shared_examples.rb b/spec/support/shared_examples/controllers/trackable_shared_examples.rb index e82c27c43f5..dac7d8c94ff 100644 --- a/spec/support/shared_examples/controllers/trackable_shared_examples.rb +++ b/spec/support/shared_examples/controllers/trackable_shared_examples.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true RSpec.shared_examples 'a Trackable Controller' do - describe '#track_event' do + describe '#track_event', :snowplow do before do sign_in user end @@ -14,9 +14,10 @@ RSpec.shared_examples 'a Trackable Controller' do end end - it 'tracks the action name' do - expect(Gitlab::Tracking).to receive(:event).with('AnonymousController', 'index', {}) + it 'tracks the action name', :snowplow do get :index + + expect_snowplow_event(category: 'AnonymousController', action: 'index') end end @@ -29,8 +30,9 @@ RSpec.shared_examples 'a Trackable Controller' do end it 'tracks with the specified param' do - expect(Gitlab::Tracking).to receive(:event).with('SomeCategory', 'some_event', label: 'errorlabel') get :index + + expect_snowplow_event(category: 'SomeCategory', action: 'some_event', label: 'errorlabel') 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 9fc5d8933e5..560cfbfb117 100644 --- a/spec/support/shared_examples/features/discussion_comments_shared_example.rb +++ b/spec/support/shared_examples/features/discussion_comments_shared_example.rb @@ -56,12 +56,12 @@ RSpec.shared_examples 'thread comments' do |resource_name| expect(items.first).to have_content 'Comment' expect(items.first).to have_content "Add a general comment to this #{resource_name}." - expect(items.first).to have_selector '.fa-check' + 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).to have_content "Discuss a specific suggestion or question#{' that needs to be resolved' if resource_name == 'merge request'}." - expect(items.last).not_to have_selector '.fa-check' + expect(items.last).not_to have_selector '[data-testid="check-icon"]' expect(items.last['class']).not_to match 'droplab-item-selected' end @@ -228,11 +228,11 @@ RSpec.shared_examples 'thread comments' do |resource_name| items = all("#{menu_selector} li") expect(items.first).to have_content 'Comment' - expect(items.first).not_to have_selector '.fa-check' + 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 '.fa-check' + expect(items.last).to have_selector '[data-testid="check-icon"]' expect(items.last['class']).to match 'droplab-item-selected' end @@ -274,11 +274,11 @@ RSpec.shared_examples 'thread comments' do |resource_name| aggregate_failures do expect(items.first).to have_content 'Comment' - expect(items.first).to have_selector '.fa-check' + 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 '.fa-check' + expect(items.last).not_to have_selector '[data-testid="check-icon"]' expect(items.last['class']).not_to match 'droplab-item-selected' end end diff --git a/spec/support/shared_examples/features/master_manages_access_requests_shared_example.rb b/spec/support/shared_examples/features/master_manages_access_requests_shared_example.rb index ffe4fb83283..724d6db2705 100644 --- a/spec/support/shared_examples/features/master_manages_access_requests_shared_example.rb +++ b/spec/support/shared_examples/features/master_manages_access_requests_shared_example.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true RSpec.shared_examples 'Maintainer manages access requests' do + include Spec::Support::Helpers::Features::MembersHelpers + let(:user) { create(:user) } let(:maintainer) { create(:user) } @@ -26,7 +28,7 @@ RSpec.shared_examples 'Maintainer manages access requests' do expect_no_visible_access_request(entity, user) - page.within('[data-qa-selector="members_list"]') do + page.within(members_table) do expect(page).to have_content user.name end end @@ -35,7 +37,7 @@ RSpec.shared_examples 'Maintainer manages access requests' do expect_visible_access_request(entity, user) # Open modal - click_on 'Deny access request' + click_on 'Deny access' expect(page).not_to have_field "Also unassign this user from related issues and merge requests" 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 218ef070221..e0d169c6868 100644 --- a/spec/support/shared_examples/features/variable_list_shared_examples.rb +++ b/spec/support/shared_examples/features/variable_list_shared_examples.rb @@ -1,387 +1,295 @@ # frozen_string_literal: true RSpec.shared_examples 'variable list' do - it 'shows list of variables' do - page.within('.js-ci-variable-list-section') do - expect(first('.js-ci-variable-input-key').value).to eq(variable.key) + it 'shows a list of variables' do + page.within('.ci-variable-table') do + expect(find('.js-ci-variable-row:nth-child(1) td[data-label="Key"]').text).to eq(variable.key) end end - it 'adds new CI variable' do - page.within('.js-ci-variable-list-section .js-row:last-child') do - find('.js-ci-variable-input-key').set('key') - find('.js-ci-variable-input-value').set('key_value') + it 'adds a new CI variable' do + click_button('Add Variable') + + fill_variable('key', 'key_value') do + click_button('Add variable') end - click_button('Save variables') wait_for_requests - visit page_path - - # We check the first row because it re-sorts to alphabetical order on refresh - page.within('.js-ci-variable-list-section .js-row:nth-child(2)') do - expect(find('.js-ci-variable-input-key').value).to eq('key') - expect(find('.js-ci-variable-input-value', visible: false).value).to eq('key_value') + page.within('.ci-variable-table') do + expect(find('.js-ci-variable-row:nth-child(1) td[data-label="Key"]').text).to eq('key') end end it 'adds a new protected variable' do - page.within('.js-ci-variable-list-section .js-row:last-child') do - find('.js-ci-variable-input-key').set('key') - find('.js-ci-variable-input-value').set('key_value') + click_button('Add Variable') - expect(find('.js-ci-variable-input-protected', visible: false).value).to eq('true') + fill_variable('key', 'key_value') do + click_button('Add variable') end - click_button('Save variables') wait_for_requests - visit page_path - - # We check the first row because it re-sorts to alphabetical order on refresh - page.within('.js-ci-variable-list-section .js-row:nth-child(2)') do - expect(find('.js-ci-variable-input-key').value).to eq('key') - expect(find('.js-ci-variable-input-value', visible: false).value).to eq('key_value') - expect(find('.js-ci-variable-input-protected', visible: false).value).to eq('true') + page.within('.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 end it 'defaults to unmasked' do - page.within('.js-ci-variable-list-section .js-row:last-child') do - find('.js-ci-variable-input-key').set('key') - find('.js-ci-variable-input-value').set('key_value') + click_button('Add Variable') - expect(find('.js-ci-variable-input-masked', visible: false).value).to eq('false') + fill_variable('key', 'key_value') do + click_button('Add variable') end - click_button('Save variables') wait_for_requests - visit page_path - - # We check the first row because it re-sorts to alphabetical order on refresh - page.within('.js-ci-variable-list-section .js-row:nth-child(2)') do - expect(find('.js-ci-variable-input-key').value).to eq('key') - expect(find('.js-ci-variable-input-value', visible: false).value).to eq('key_value') - expect(find('.js-ci-variable-input-masked', visible: false).value).to eq('false') - end - end - - context 'defaults to the application setting' do - context 'application setting is true' do - before do - stub_application_setting(protected_ci_variables: true) - - visit page_path - end - - it 'defaults to protected' do - page.within('.js-ci-variable-list-section .js-row:last-child') do - find('.js-ci-variable-input-key').set('key') - end - - values = all('.js-ci-variable-input-protected', visible: false).map(&:value) - - expect(values).to eq %w(false true true) - end - - it 'shows a message regarding the changed default' do - expect(page).to have_content 'Environment variables are configured by your administrator to be protected by default' - end - end - - context 'application setting is false' do - before do - stub_application_setting(protected_ci_variables: false) - - visit page_path - end - - it 'defaults to unprotected' do - page.within('.js-ci-variable-list-section .js-row:last-child') do - find('.js-ci-variable-input-key').set('key') - end - - values = all('.js-ci-variable-input-protected', visible: false).map(&:value) - - expect(values).to eq %w(false false false) - end - - it 'does not show a message regarding the default' do - expect(page).not_to have_content 'Environment variables are configured by your administrator to be protected by default' - end + page.within('.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('.js-ci-variable-list-section') do - expect(first('.js-ci-variable-input-key').value).to eq(variable.key) - expect(first('.js-ci-variable-input-value', visible: false).value).to eq(variable.value) + page.within('.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) click_button('Reveal value') - expect(first('.js-ci-variable-input-key').value).to eq(variable.key) - expect(first('.js-ci-variable-input-value').value).to eq(variable.value) + expect(first('.js-ci-variable-row td[data-label="Key"]').text).to eq(variable.key) + expect(first('.js-ci-variable-row td[data-label="Value"]').text).to eq(variable.value) expect(page).not_to have_content('*' * 17) click_button('Hide value') - expect(first('.js-ci-variable-input-key').value).to eq(variable.key) - expect(first('.js-ci-variable-input-value', visible: false).value).to eq(variable.value) + expect(first('.js-ci-variable-row td[data-label="Key"]').text).to eq(variable.key) expect(page).to have_content('*' * 17) end end - it 'deletes variable' do - page.within('.js-ci-variable-list-section') do - expect(page).to have_selector('.js-row', count: 2) + it 'deletes a variable' do + expect(page).to have_selector('.js-ci-variable-row', count: 1) - first('.js-row-remove-button').click - - click_button('Save variables') - wait_for_requests - - expect(page).to have_selector('.js-row', count: 1) + page.within('.ci-variable-table') do + click_button('Edit') end - end - it 'edits variable' do - page.within('.js-ci-variable-list-section') do - click_button('Reveal value') - - page.within('.js-row:nth-child(2)') do - find('.js-ci-variable-input-key').set('new_key') - find('.js-ci-variable-input-value').set('new_value') - end + page.within('#add-ci-variable') do + click_button('Delete variable') + end - click_button('Save variables') - wait_for_requests + wait_for_requests - visit page_path + expect(first('.js-ci-variable-row').text).to eq('There are no variables yet.') + end - page.within('.js-row:nth-child(2)') do - expect(find('.js-ci-variable-input-key').value).to eq('new_key') - expect(find('.js-ci-variable-input-value', visible: false).value).to eq('new_value') - end + it 'edits a variable' do + page.within('.ci-variable-table') do + click_button('Edit') end - end - it 'edits variable to be protected' do - # Create the unprotected variable - page.within('.js-ci-variable-list-section .js-row:last-child') do - find('.js-ci-variable-input-key').set('unprotected_key') - find('.js-ci-variable-input-value').set('unprotected_value') - find('.ci-variable-protected-item .js-project-feature-toggle').click + page.within('#add-ci-variable') do + find('[data-qa-selector="ci_variable_key_field"] input').set('new_key') - expect(find('.js-ci-variable-input-protected', visible: false).value).to eq('false') + click_button('Update variable') end - click_button('Save variables') wait_for_requests - visit page_path + expect(first('.js-ci-variable-row td[data-label="Key"]').text).to eq('new_key') + end + + it 'edits a variable to be unmasked' do + page.within('.ci-variable-table') do + click_button('Edit') + end - # We check the first row because it re-sorts to alphabetical order on refresh - page.within('.js-ci-variable-list-section .js-row:nth-child(3)') do - find('.ci-variable-protected-item .js-project-feature-toggle').click + page.within('#add-ci-variable') do + find('[data-testid="ci-variable-protected-checkbox"]').click + find('[data-testid="ci-variable-masked-checkbox"]').click - expect(find('.js-ci-variable-input-protected', visible: false).value).to eq('true') + click_button('Update variable') end - click_button('Save variables') wait_for_requests - visit page_path - - # We check the first row because it re-sorts to alphabetical order on refresh - page.within('.js-ci-variable-list-section .js-row:nth-child(3)') do - expect(find('.js-ci-variable-input-key').value).to eq('unprotected_key') - expect(find('.js-ci-variable-input-value', visible: false).value).to eq('unprotected_value') - expect(find('.js-ci-variable-input-protected', visible: false).value).to eq('true') + page.within('.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 variable to be unprotected' do - # Create the protected variable - page.within('.js-ci-variable-list-section .js-row:last-child') do - find('.js-ci-variable-input-key').set('protected_key') - find('.js-ci-variable-input-value').set('protected_value') + it 'edits a variable to be masked' do + page.within('.ci-variable-table') do + click_button('Edit') + end + + page.within('#add-ci-variable') do + find('[data-testid="ci-variable-masked-checkbox"]').click - expect(find('.js-ci-variable-input-protected', visible: false).value).to eq('true') + click_button('Update variable') end - click_button('Save variables') wait_for_requests - visit page_path + page.within('.ci-variable-table') do + click_button('Edit') + end - page.within('.js-ci-variable-list-section .js-row:nth-child(2)') do - find('.ci-variable-protected-item .js-project-feature-toggle').click + page.within('#add-ci-variable') do + find('[data-testid="ci-variable-masked-checkbox"]').click - expect(find('.js-ci-variable-input-protected', visible: false).value).to eq('false') + click_button('Update variable') end - click_button('Save variables') - wait_for_requests + page.within('.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 - visit page_path + it 'shows a validation error box about duplicate keys' do + click_button('Add Variable') - page.within('.js-ci-variable-list-section .js-row:nth-child(2)') do - expect(find('.js-ci-variable-input-key').value).to eq('protected_key') - expect(find('.js-ci-variable-input-value', visible: false).value).to eq('protected_value') - expect(find('.js-ci-variable-input-protected', visible: false).value).to eq('false') + fill_variable('key', 'key_value') do + click_button('Add variable') end - end - it 'edits variable to be unmasked' do - page.within('.js-ci-variable-list-section .js-row:last-child') do - find('.js-ci-variable-input-key').set('unmasked_key') - find('.js-ci-variable-input-value').set('unmasked_value') - expect(find('.js-ci-variable-input-masked', visible: false).value).to eq('false') + wait_for_requests - find('.ci-variable-masked-item .js-project-feature-toggle').click + click_button('Add Variable') - expect(find('.js-ci-variable-input-masked', visible: false).value).to eq('true') + fill_variable('key', 'key_value') do + click_button('Add variable') end - click_button('Save variables') wait_for_requests - visit page_path + expect(find('.flash-container')).to be_present + expect(find('.flash-text').text).to have_content('Variables key (key) has already been taken') + end - page.within('.js-ci-variable-list-section .js-row:nth-child(2)') do - expect(find('.js-ci-variable-input-masked', visible: false).value).to eq('true') + it 'prevents a variable to be added if no values are provided when a variable is set to masked' do + click_button('Add Variable') - find('.ci-variable-masked-item .js-project-feature-toggle').click + page.within('#add-ci-variable') do + find('[data-qa-selector="ci_variable_key_field"] input').set('empty_mask_key') + find('[data-testid="ci-variable-protected-checkbox"]').click + find('[data-testid="ci-variable-masked-checkbox"]').click - expect(find('.js-ci-variable-input-masked', visible: false).value).to eq('false') + expect(find_button('Add variable', disabled: true)).to be_present end + end - click_button('Save variables') - wait_for_requests - - visit page_path + it 'shows validation error box about unmaskable values' do + click_button('Add Variable') - page.within('.js-ci-variable-list-section .js-row:nth-child(2)') do - expect(find('.js-ci-variable-input-masked', visible: false).value).to eq('false') + fill_variable('empty_mask_key', '???', protected: true, masked: true) do + expect(page).to have_content('This variable can not be masked') + expect(find_button('Add variable', disabled: true)).to be_present end end - it 'edits variable to be masked' do - page.within('.js-ci-variable-list-section .js-row:last-child') do - find('.js-ci-variable-input-key').set('masked_key') - find('.js-ci-variable-input-value').set('masked_value') - expect(find('.js-ci-variable-input-masked', visible: false).value).to eq('false') + it 'handles multiple edits and a deletion' do + # Create two variables + click_button('Add Variable') - find('.ci-variable-masked-item .js-project-feature-toggle').click - - expect(find('.js-ci-variable-input-masked', visible: false).value).to eq('true') + fill_variable('akey', 'akeyvalue') do + click_button('Add variable') end - click_button('Save variables') wait_for_requests - visit page_path + click_button('Add Variable') - page.within('.js-ci-variable-list-section .js-row:nth-child(2)') do - expect(find('.js-ci-variable-input-masked', visible: false).value).to eq('true') + fill_variable('zkey', 'zkeyvalue') do + click_button('Add variable') end - end - - it 'handles multiple edits and deletion in the middle' do - page.within('.js-ci-variable-list-section') do - # Create 2 variables - page.within('.js-row:last-child') do - find('.js-ci-variable-input-key').set('akey') - find('.js-ci-variable-input-value').set('akeyvalue') - end - page.within('.js-row:last-child') do - find('.js-ci-variable-input-key').set('zkey') - find('.js-ci-variable-input-value').set('zkeyvalue') - end - click_button('Save variables') - wait_for_requests + wait_for_requests - expect(page).to have_selector('.js-row', count: 4) + expect(page).to have_selector('.js-ci-variable-row', count: 3) - # Remove the `akey` variable - page.within('.js-row:nth-child(3)') do - first('.js-row-remove-button').click + # Remove the `akey` variable + page.within('.ci-variable-table') do + page.within('.js-ci-variable-row:first-child') do + click_button('Edit') end + end - # Add another variable - page.within('.js-row:last-child') do - find('.js-ci-variable-input-key').set('ckey') - find('.js-ci-variable-input-value').set('ckeyvalue') - end + page.within('#add-ci-variable') do + click_button('Delete variable') + end - click_button('Save variables') - wait_for_requests + wait_for_requests - visit page_path + # Add another variable + click_button('Add Variable') - # Expect to find 3 variables(4 rows) in alphbetical order - expect(page).to have_selector('.js-row', count: 4) - row_keys = all('.js-ci-variable-input-key') - expect(row_keys[0].value).to eq('ckey') - expect(row_keys[1].value).to eq('test_key') - expect(row_keys[2].value).to eq('zkey') - expect(row_keys[3].value).to eq('') + fill_variable('ckey', 'ckeyvalue') do + click_button('Add variable') end + + wait_for_requests + + # expect to find 3 rows of variables in alphabetical order + expect(page).to have_selector('.js-ci-variable-row', count: 3) + rows = all('.js-ci-variable-row') + expect(rows[0].find('td[data-label="Key"]').text).to eq('ckey') + expect(rows[1].find('td[data-label="Key"]').text).to eq('test_key') + expect(rows[2].find('td[data-label="Key"]').text).to eq('zkey') end - it 'shows validation error box about duplicate keys' do - page.within('.js-ci-variable-list-section .js-row:last-child') do - find('.js-ci-variable-input-key').set('samekey') - find('.js-ci-variable-input-value').set('value123') - end - page.within('.js-ci-variable-list-section .js-row:last-child') do - find('.js-ci-variable-input-key').set('samekey') - find('.js-ci-variable-input-value').set('value456') - end + context 'defaults to the application setting' do + context 'application setting is true' do + before do + stub_application_setting(protected_ci_variables: true) - click_button('Save variables') - wait_for_requests + visit page_path + end - expect(all('.js-ci-variable-list-section .js-ci-variable-error-box ul li').count).to eq(1) + it 'defaults to protected' do + click_button('Add Variable') - # We check the first row because it re-sorts to alphabetical order on refresh - page.within('.js-ci-variable-list-section') do - expect(find('.js-ci-variable-error-box')).to have_content(/Validation failed Variables have duplicate values \(.+\)/) - end - end + page.within('#add-ci-variable') do + expect(find('[data-testid="ci-variable-protected-checkbox"]')).to be_checked + end + end - it 'shows validation error box about masking empty values' do - page.within('.js-ci-variable-list-section .js-row:last-child') do - find('.js-ci-variable-input-key').set('empty_value') - find('.js-ci-variable-input-value').set('') - find('.ci-variable-masked-item .js-project-feature-toggle').click + it 'shows a message regarding the changed default' do + expect(page).to have_content 'Environment variables are configured by your administrator to be protected by default' + end end - click_button('Save variables') - wait_for_requests + context 'application setting is false' do + before do + stub_application_setting(protected_ci_variables: false) - page.within('.js-ci-variable-list-section') do - expect(all('.js-ci-variable-error-box ul li').count).to eq(1) - expect(find('.js-ci-variable-error-box')).to have_content(/Validation failed Variables value is invalid/) - end - end + visit page_path + end - it 'shows validation error box about unmaskable values' do - page.within('.js-ci-variable-list-section .js-row:last-child') do - find('.js-ci-variable-input-key').set('unmaskable_value') - find('.js-ci-variable-input-value').set('???') - find('.ci-variable-masked-item .js-project-feature-toggle').click + it 'defaults to unprotected' do + click_button('Add Variable') + + page.within('#add-ci-variable') do + expect(find('[data-testid="ci-variable-protected-checkbox"]')).not_to be_checked + end + end + + it 'does not show a message regarding the default' do + expect(page).not_to have_content 'Environment variables are configured by your administrator to be protected by default' + end end + end - click_button('Save variables') - wait_for_requests + def fill_variable(key, value, protected: false, masked: false) + page.within('#add-ci-variable') do + find('[data-qa-selector="ci_variable_key_field"] input').set(key) + find('[data-qa-selector="ci_variable_value_field"]').set(value) if value.present? + find('[data-testid="ci-variable-protected-checkbox"]').click if protected + find('[data-testid="ci-variable-masked-checkbox"]').click if masked - page.within('.js-ci-variable-list-section') do - expect(all('.js-ci-variable-error-box ul li').count).to eq(1) - expect(find('.js-ci-variable-error-box')).to have_content(/Validation failed Variables value is invalid/) + yield end end end diff --git a/spec/support/shared_examples/features/wiki/user_deletes_wiki_page_shared_examples.rb b/spec/support/shared_examples/features/wiki/user_deletes_wiki_page_shared_examples.rb index e1fd9c8dbec..ee0261771f9 100644 --- a/spec/support/shared_examples/features/wiki/user_deletes_wiki_page_shared_examples.rb +++ b/spec/support/shared_examples/features/wiki/user_deletes_wiki_page_shared_examples.rb @@ -17,8 +17,8 @@ RSpec.shared_examples 'User deletes wiki page' do it 'deletes a page', :js do click_on('Edit') click_on('Delete') - find('.modal-footer .btn-danger').click + find('[data-testid="confirm_deletion_button"]').click - expect(page).to have_content('Page was successfully deleted') + expect(page).to have_content('Wiki page was successfully deleted.') end end diff --git a/spec/support/shared_examples/features/wiki/user_updates_wiki_page_shared_examples.rb b/spec/support/shared_examples/features/wiki/user_updates_wiki_page_shared_examples.rb index 1a5f8d7d8df..3350e54a8a7 100644 --- a/spec/support/shared_examples/features/wiki/user_updates_wiki_page_shared_examples.rb +++ b/spec/support/shared_examples/features/wiki/user_updates_wiki_page_shared_examples.rb @@ -213,11 +213,11 @@ RSpec.shared_examples 'User updates wiki page' do visit wiki_page_path(wiki_page.wiki, wiki_page, action: :edit) end - it 'allows changing the title if the content does not change' do + it 'allows changing the title if the content does not change', :js do fill_in 'Title', with: 'new title' click_on 'Save changes' - expect(page).to have_content('Wiki was successfully updated.') + expect(page).to have_content('Wiki page was successfully updated.') end it 'shows a validation error when trying to change the content' do diff --git a/spec/support/shared_examples/features/wiki/user_views_wiki_page_shared_examples.rb b/spec/support/shared_examples/features/wiki/user_views_wiki_page_shared_examples.rb index 85eedbf4cc5..af769be6d4b 100644 --- a/spec/support/shared_examples/features/wiki/user_views_wiki_page_shared_examples.rb +++ b/spec/support/shared_examples/features/wiki/user_views_wiki_page_shared_examples.rb @@ -33,7 +33,7 @@ RSpec.shared_examples 'User views a wiki page' do click_on('Create page') end - expect(page).to have_content('Wiki was successfully updated.') + expect(page).to have_content('Wiki page was successfully created.') end it 'shows the history of a page that has a path' do @@ -49,7 +49,7 @@ RSpec.shared_examples 'User views a wiki page' do end end - it 'shows an old version of a page' do + it 'shows an old version of a page', :js do expect(current_path).to include('one/two/three-test') expect(find('.wiki-pages')).to have_content('three') @@ -65,7 +65,7 @@ RSpec.shared_examples 'User views a wiki page' do fill_in('Content', with: 'Updated Wiki Content') click_on('Save changes') - expect(page).to have_content('Wiki was successfully updated.') + expect(page).to have_content('Wiki page was successfully updated.') click_on('Page history') diff --git a/spec/support/shared_examples/finders/security/jobs_finder_shared_examples.rb b/spec/support/shared_examples/finders/security/jobs_finder_shared_examples.rb new file mode 100644 index 00000000000..a332b213866 --- /dev/null +++ b/spec/support/shared_examples/finders/security/jobs_finder_shared_examples.rb @@ -0,0 +1,87 @@ +# frozen_string_literal: true + +RSpec.shared_examples ::Security::JobsFinder do |default_job_types| + let(:pipeline) { create(:ci_pipeline) } + + describe '#new' do + it "does not get initialized for unsupported job types" do + expect { described_class.new(pipeline: pipeline, job_types: [:abcd]) }.to raise_error( + ArgumentError, + "job_types must be from the following: #{default_job_types}" + ) + end + end + + describe '#execute' do + let(:finder) { described_class.new(pipeline: pipeline) } + + subject { finder.execute } + + shared_examples 'JobsFinder core functionality' do + context 'when the pipeline has no jobs' do + it { is_expected.to be_empty } + end + + context 'when the pipeline has no Secure jobs' do + before do + create(:ci_build, pipeline: pipeline) + end + + it { is_expected.to be_empty } + end + + context 'when the pipeline only has jobs without report artifacts' do + before do + create(:ci_build, pipeline: pipeline, options: { artifacts: { file: 'test.file' } }) + end + + it { is_expected.to be_empty } + end + + context 'when the pipeline only has jobs with reports unrelated to Secure products' do + before do + create(:ci_build, pipeline: pipeline, options: { artifacts: { reports: { file: 'test.file' } } }) + end + + it { is_expected.to be_empty } + end + + context 'when the pipeline only has jobs with reports with paths similar but not identical to Secure reports' do + before do + create(:ci_build, pipeline: pipeline, options: { artifacts: { reports: { file: 'report:sast:result.file' } } }) + end + + it { is_expected.to be_empty } + end + + context 'when there is more than one pipeline' do + let(:job_type) { default_job_types.first } + let!(:build) { create(:ci_build, job_type, pipeline: pipeline) } + + before do + create(:ci_build, job_type, pipeline: create(:ci_pipeline)) + end + + it 'returns jobs associated with provided pipeline' do + is_expected.to eq([build]) + end + end + end + + context 'when using legacy CI build metadata config storage' do + before do + stub_feature_flags(ci_build_metadata_config: false) + end + + it_behaves_like 'JobsFinder core functionality' + end + + context 'when using the new CI build metadata config storage' do + before do + stub_feature_flags(ci_build_metadata_config: true) + end + + it_behaves_like 'JobsFinder core functionality' + end + end +end diff --git a/spec/support/shared_examples/graphql/label_fields.rb b/spec/support/shared_examples/graphql/label_fields.rb index b1bfb395bc6..caf5dae409a 100644 --- a/spec/support/shared_examples/graphql/label_fields.rb +++ b/spec/support/shared_examples/graphql/label_fields.rb @@ -106,13 +106,11 @@ RSpec.shared_examples 'querying a GraphQL type with labels' do end it 'batches queries for labels by title' do - pending('See: https://gitlab.com/gitlab-org/gitlab/-/issues/217767') - multi_selection = query_for(label_b, label_c) single_selection = query_for(label_d) expect { run_query(multi_selection) } - .to issue_same_number_of_queries_as { run_query(single_selection) } + .to issue_same_number_of_queries_as { run_query(single_selection) }.ignoring_cached_queries 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 ec64519cd9c..9c0b398a5c1 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 @@ -65,7 +65,7 @@ RSpec.shared_examples 'boards create mutation' do let(:params) { { name: name } } it_behaves_like 'a mutation that returns top-level errors', - errors: ['group_path or project_path arguments are required'] + errors: ['Exactly one of group_path or project_path arguments is required'] it 'does not create the board' do expect { subject }.not_to change { Board.count } diff --git a/spec/support/shared_examples/graphql/mutations/create_todo_shared_examples.rb b/spec/support/shared_examples/graphql/mutations/create_todo_shared_examples.rb new file mode 100644 index 00000000000..fbef8be9e88 --- /dev/null +++ b/spec/support/shared_examples/graphql/mutations/create_todo_shared_examples.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'create todo mutation' do + let_it_be(:current_user) { create(:user) } + + let(:mutation) { described_class.new(object: nil, context: { current_user: current_user }, field: nil) } + + context 'when user does not have permission to create todo' do + it 'raises error' do + expect { mutation.resolve(target_id: global_id_of(target)) } + .to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) + end + end + + context 'when user has permission to create todo' do + it 'creates a todo' do + target.resource_parent.add_reporter(current_user) + + result = mutation.resolve(target_id: global_id_of(target)) + + expect(result[:todo]).to be_valid + expect(result[:todo].target).to eq(target) + expect(result[:todo].state).to eq('pending') + end + end +end diff --git a/spec/support/shared_examples/graphql/mutations/issues/permission_check_shared_examples.rb b/spec/support/shared_examples/graphql/mutations/issues/permission_check_shared_examples.rb new file mode 100644 index 00000000000..34c58f524cd --- /dev/null +++ b/spec/support/shared_examples/graphql/mutations/issues/permission_check_shared_examples.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'permission level for issue mutation is correctly verified' do |raises_for_all_errors = false| + before do + issue.assignees = [] + issue.author = user + end + + shared_examples_for 'when the user does not have access to the resource' do |raise_for_assigned| + it 'raises an error' do + expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) + end + + context 'even if assigned to the issue' do + before do + issue.assignees.push(user) + end + + it 'does not modify issue' do + if raises_for_all_errors || raise_for_assigned + expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) + else + expect(subject[:issue]).to eq issue + end + end + end + + context 'even if author of the issue' do + before do + issue.author = user + end + + it 'raises an error' do + expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) + end + end + end + + context 'when the user is not a project member' do + it_behaves_like 'when the user does not have access to the resource', true + end + + context 'when the user is a project member' do + context 'with guest role' do + before do + issue.project.add_guest(user) + end + + it_behaves_like 'when the user does not have access to the resource', false + end + end +end diff --git a/spec/support/shared_examples/graphql/mutations/merge_requests/permission_check_shared_examples.rb b/spec/support/shared_examples/graphql/mutations/merge_requests/permission_check_shared_examples.rb new file mode 100644 index 00000000000..1ddbad1cea7 --- /dev/null +++ b/spec/support/shared_examples/graphql/mutations/merge_requests/permission_check_shared_examples.rb @@ -0,0 +1,73 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'permission level for merge request mutation is correctly verified' do + before do + merge_request.assignees = [] + merge_request.reviewers = [] + merge_request.author = nil + end + + shared_examples_for 'when the user does not have access to the resource' do |raise_for_assigned| + it 'raises an error' do + expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) + end + + context 'even if assigned to the merge request' do + before do + merge_request.assignees.push(user) + end + + it 'does not modify merge request' do + if raise_for_assigned + expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) + else + # In some cases we simply do nothing instead of raising + # https://gitlab.com/gitlab-org/gitlab/-/issues/196241 + expect(subject[:merge_request]).to eq merge_request + end + end + end + + context 'even if reviewer of the merge request' do + before do + merge_request.reviewers.push(user) + end + + it 'raises an error' do + expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) + end + end + + context 'even if author of the merge request' do + before do + merge_request.author = user + end + + it 'raises an error' do + expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) + end + end + end + + context 'when the user is not a project member' do + it_behaves_like 'when the user does not have access to the resource', true + end + + context 'when the user is a project member' do + context 'with guest role' do + before do + merge_request.project.add_guest(user) + end + + it_behaves_like 'when the user does not have access to the resource', true + end + + context 'with reporter role' do + before do + merge_request.project.add_reporter(user) + end + + it_behaves_like 'when the user does not have access to the resource', false + end + end +end diff --git a/spec/support/shared_examples/helm_commands_shared_examples.rb b/spec/support/shared_examples/helm_commands_shared_examples.rb index 0a94c6648cc..64f176c5ae9 100644 --- a/spec/support/shared_examples/helm_commands_shared_examples.rb +++ b/spec/support/shared_examples/helm_commands_shared_examples.rb @@ -15,6 +15,18 @@ RSpec.shared_examples 'helm command generator' do end RSpec.shared_examples 'helm command' do + describe 'HELM_VERSION' do + subject { command.class::HELM_VERSION } + + it { is_expected.to match(/\d+\.\d+\.\d+/) } + end + + describe '#env' do + subject { command.env } + + it { is_expected.to be_a Hash } + end + describe '#rbac?' do subject { command.rbac? } 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 d0e41605e00..145a7290ac8 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 @@ -1,6 +1,6 @@ # frozen_string_literal: true -RSpec.shared_examples_for 'cycle analytics event' do +RSpec.shared_examples_for 'value stream analytics event' do let(:params) { {} } let(:instance) { described_class.new(params) } diff --git a/spec/support/shared_examples/lib/gitlab/database/background_migration_job_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/database/background_migration_job_shared_examples.rb new file mode 100644 index 00000000000..20f3270526e --- /dev/null +++ b/spec/support/shared_examples/lib/gitlab/database/background_migration_job_shared_examples.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'marks background migration job records' do + it 'marks each job record as succeeded after processing' do + create(:background_migration_job, class_name: "::#{described_class.name}", + arguments: arguments) + + expect(::Gitlab::Database::BackgroundMigrationJob).to receive(:mark_all_as_succeeded).and_call_original + + expect do + subject.perform(*arguments) + end.to change { ::Gitlab::Database::BackgroundMigrationJob.succeeded.count }.from(0).to(1) + end + + it 'returns the number of job records marked as succeeded' do + create(:background_migration_job, class_name: "::#{described_class.name}", + arguments: arguments) + + jobs_updated = subject.perform(*arguments) + + expect(jobs_updated).to eq(1) + end +end diff --git a/spec/support/shared_examples/lib/gitlab/database/postgres_model_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/database/postgres_model_shared_examples.rb new file mode 100644 index 00000000000..ffebbabca58 --- /dev/null +++ b/spec/support/shared_examples/lib/gitlab/database/postgres_model_shared_examples.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'a postgres model' do + describe '.by_identifier' do + it "finds the #{described_class}" do + expect(find(identifier)).to be_a(described_class) + end + + it 'raises an error if not found' do + expect { find('public.idontexist') }.to raise_error(ActiveRecord::RecordNotFound) + end + + it 'raises ArgumentError if given a non-fully qualified identifier' do + expect { find('foo') }.to raise_error(ArgumentError, /not fully qualified/) + end + end + + describe '#to_s' do + it 'returns the name' do + expect(find(identifier).to_s).to eq(name) + end + end + + describe '#schema' do + it 'returns the schema' do + expect(find(identifier).schema).to eq(schema) + end + end + + describe '#name' do + it 'returns the name' do + expect(find(identifier).name).to eq(name) + end + end +end diff --git a/spec/support/shared_examples/lib/gitlab/middleware/read_only_gitlab_instance_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/middleware/read_only_gitlab_instance_shared_examples.rb new file mode 100644 index 00000000000..e07d3e2dec9 --- /dev/null +++ b/spec/support/shared_examples/lib/gitlab/middleware/read_only_gitlab_instance_shared_examples.rb @@ -0,0 +1,189 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'write access for a read-only GitLab instance' do + include Rack::Test::Methods + using RSpec::Parameterized::TableSyntax + + include_context 'with a mocked GitLab instance' + + context 'normal requests to a read-only GitLab instance' do + let(:fake_app) { lambda { |env| [200, { 'Content-Type' => 'text/plain' }, ['OK']] } } + + it 'expects PATCH requests to be disallowed' do + response = request.patch('/test_request') + + expect(response).to be_redirect + expect(subject).to disallow_request + end + + it 'expects PUT requests to be disallowed' do + response = request.put('/test_request') + + expect(response).to be_redirect + expect(subject).to disallow_request + end + + it 'expects POST requests to be disallowed' do + response = request.post('/test_request') + + expect(response).to be_redirect + expect(subject).to disallow_request + end + + it 'expects a internal POST request to be allowed after a disallowed request' do + response = request.post('/test_request') + + expect(response).to be_redirect + + response = request.post("/api/#{API::API.version}/internal") + + expect(response).not_to be_redirect + end + + it 'expects DELETE requests to be disallowed' do + response = request.delete('/test_request') + + expect(response).to be_redirect + expect(subject).to disallow_request + end + + it 'expects POST of new file that looks like an LFS batch url to be disallowed' do + expect(Rails.application.routes).to receive(:recognize_path).and_call_original + response = request.post('/root/gitlab-ce/new/master/app/info/lfs/objects/batch') + + expect(response).to be_redirect + expect(subject).to disallow_request + end + + it 'returns last_vistited_url for disallowed request' do + response = request.post('/test_request') + + expect(response.location).to eq 'http://localhost/' + end + + context 'allowlisted requests' do + it 'expects a POST internal request to be allowed' do + expect(Rails.application.routes).not_to receive(:recognize_path) + response = request.post("/api/#{API::API.version}/internal") + + expect(response).not_to be_redirect + expect(subject).not_to disallow_request + end + + it 'expects a graphql request to be allowed' do + response = request.post("/api/graphql") + + expect(response).not_to be_redirect + expect(subject).not_to disallow_request + end + + context 'relative URL is configured' do + before do + stub_config_setting(relative_url_root: '/gitlab') + end + + it 'expects a graphql request to be allowed' do + response = request.post("/gitlab/api/graphql") + + expect(response).not_to be_redirect + expect(subject).not_to disallow_request + end + end + + context 'sidekiq admin requests' do + where(:mounted_at) do + [ + '', + '/', + '/gitlab', + '/gitlab/', + '/gitlab/gitlab', + '/gitlab/gitlab/' + ] + end + + with_them do + before do + stub_config_setting(relative_url_root: mounted_at) + end + + it 'allows requests' do + path = File.join(mounted_at, 'admin/sidekiq') + response = request.post(path) + + expect(response).not_to be_redirect + expect(subject).not_to disallow_request + + response = request.get(path) + + expect(response).not_to be_redirect + expect(subject).not_to disallow_request + end + end + end + + where(:description, :path) do + 'LFS request to batch' | '/root/rouge.git/info/lfs/objects/batch' + 'request to git-upload-pack' | '/root/rouge.git/git-upload-pack' + end + + with_them do + it "expects a POST #{description} URL to be allowed" do + expect(Rails.application.routes).to receive(:recognize_path).and_call_original + response = request.post(path) + + expect(response).not_to be_redirect + expect(subject).not_to disallow_request + end + end + + where(:description, :path) do + 'LFS request to locks verify' | '/root/rouge.git/info/lfs/locks/verify' + 'LFS request to locks create' | '/root/rouge.git/info/lfs/locks' + 'LFS request to locks unlock' | '/root/rouge.git/info/lfs/locks/1/unlock' + end + + with_them do + it "expects a POST #{description} URL not to be allowed" do + response = request.post(path) + + expect(response).to be_redirect + expect(subject).to disallow_request + end + end + end + end + + context 'json requests to a read-only GitLab instance' do + let(:fake_app) { lambda { |env| [200, { 'Content-Type' => 'application/json' }, ['OK']] } } + let(:content_json) { { 'CONTENT_TYPE' => 'application/json' } } + + before do + allow(Gitlab::Database).to receive(:read_only?) { true } + end + + it 'expects PATCH requests to be disallowed' do + response = request.patch('/test_request', content_json) + + expect(response).to disallow_request_in_json + end + + it 'expects PUT requests to be disallowed' do + response = request.put('/test_request', content_json) + + expect(response).to disallow_request_in_json + end + + it 'expects POST requests to be disallowed' do + response = request.post('/test_request', content_json) + + expect(response).to disallow_request_in_json + end + + it 'expects DELETE requests to be disallowed' do + response = request.delete('/test_request', content_json) + + expect(response).to disallow_request_in_json + end + end +end diff --git a/spec/support/shared_examples/lib/gitlab/repository_size_checker_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/repository_size_checker_shared_examples.rb index bb909ffe82a..30413f206f8 100644 --- a/spec/support/shared_examples/lib/gitlab/repository_size_checker_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/repository_size_checker_shared_examples.rb @@ -17,35 +17,58 @@ RSpec.shared_examples 'checker size not over limit' do end RSpec.shared_examples 'checker size exceeded' do - context 'when current size is below or equal to the limit' do - let(:current_size) { 50 } + context 'when no change size provided' do + context 'when current size is below the limit' do + let(:current_size) { limit - 1 } - it 'returns zero' do - expect(subject.exceeded_size).to eq(0) + it 'returns zero' do + expect(subject.exceeded_size).to eq(0) + end end - end - context 'when current size is over the limit' do - let(:current_size) { 51 } + context 'when current size is equal to the limit' do + let(:current_size) { limit } - it 'returns zero' do - expect(subject.exceeded_size).to eq(1.megabytes) + it 'returns zero' do + expect(subject.exceeded_size).to eq(0) + end end - end - context 'when change size will be over the limit' do - let(:current_size) { 50 } + context 'when current size is over the limit' do + let(:current_size) { limit + 1 } + let(:total_repository_size_excess) { 1 } - it 'returns zero' do - expect(subject.exceeded_size(1.megabytes)).to eq(1.megabytes) + it 'returns a positive number' do + expect(subject.exceeded_size).to eq(1.megabyte) + end end end - context 'when change size will not be over the limit' do - let(:current_size) { 49 } + context 'when a change size is provided' do + let(:change_size) { 1.megabyte } + + context 'when change size will be over the limit' do + let(:current_size) { limit } + + it 'returns a positive number' do + expect(subject.exceeded_size(change_size)).to eq(1.megabyte) + end + end + + context 'when change size will be at the limit' do + let(:current_size) { limit - 1 } + + it 'returns zero' do + expect(subject.exceeded_size(change_size)).to eq(0) + end + end + + context 'when change size will be under the limit' do + let(:current_size) { limit - 2 } - it 'returns zero' do - expect(subject.exceeded_size(1.megabytes)).to eq(0) + it 'returns zero' do + expect(subject.exceeded_size(change_size)).to eq(0) + end end end end diff --git a/spec/support/shared_examples/lib/gitlab/search_confidential_filter_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/search_confidential_filter_shared_examples.rb index d0bef2ad730..e70dfec80b1 100644 --- a/spec/support/shared_examples/lib/gitlab/search_confidential_filter_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/search_confidential_filter_shared_examples.rb @@ -4,66 +4,27 @@ RSpec.shared_examples 'search results filtered by confidential' do context 'filter not provided (all behavior)' do let(:filters) { {} } - context 'when Feature search_filter_by_confidential enabled' do - it 'returns confidential and not confidential results', :aggregate_failures do - expect(results.objects('issues')).to include confidential_result - expect(results.objects('issues')).to include opened_result - end - end - - context 'when Feature search_filter_by_confidential not enabled' do - before do - stub_feature_flags(search_filter_by_confidential: false) - end - - it 'returns confidential and not confidential results', :aggregate_failures do - expect(results.objects('issues')).to include confidential_result - expect(results.objects('issues')).to include opened_result - end + it 'returns confidential and not confidential results', :aggregate_failures do + expect(results.objects('issues')).to include confidential_result + expect(results.objects('issues')).to include opened_result end end context 'confidential filter' do let(:filters) { { confidential: true } } - context 'when Feature search_filter_by_confidential enabled' do - it 'returns only confidential results', :aggregate_failures do - expect(results.objects('issues')).to include confidential_result - expect(results.objects('issues')).not_to include opened_result - end - end - - context 'when Feature search_filter_by_confidential not enabled' do - before do - stub_feature_flags(search_filter_by_confidential: false) - end - - it 'returns confidential and not confidential results', :aggregate_failures do - expect(results.objects('issues')).to include confidential_result - expect(results.objects('issues')).to include opened_result - end + it 'returns only confidential results', :aggregate_failures do + expect(results.objects('issues')).to include confidential_result + expect(results.objects('issues')).not_to include opened_result end end context 'not confidential filter' do let(:filters) { { confidential: false } } - context 'when Feature search_filter_by_confidential enabled' do - it 'returns not confidential results', :aggregate_failures do - expect(results.objects('issues')).not_to include confidential_result - expect(results.objects('issues')).to include opened_result - end - end - - context 'when Feature search_filter_by_confidential not enabled' do - before do - stub_feature_flags(search_filter_by_confidential: false) - end - - it 'returns confidential and not confidential results', :aggregate_failures do - expect(results.objects('issues')).to include confidential_result - expect(results.objects('issues')).to include opened_result - end + it 'returns not confidential results', :aggregate_failures do + expect(results.objects('issues')).not_to include confidential_result + expect(results.objects('issues')).to include opened_result end end end diff --git a/spec/support/shared_examples/lib/gitlab/search_results_sorted_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/search_results_sorted_shared_examples.rb index 765279a78fe..07d01d5c50e 100644 --- a/spec/support/shared_examples/lib/gitlab/search_results_sorted_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/search_results_sorted_shared_examples.rb @@ -2,7 +2,7 @@ RSpec.shared_examples 'search results sorted' do context 'sort: newest' do - let(:sort) { 'newest' } + let(:sort) { 'created_desc' } it 'sorts results by created_at' do expect(results.objects(scope).map(&:id)).to eq([new_result.id, old_result.id, very_old_result.id]) @@ -10,7 +10,7 @@ RSpec.shared_examples 'search results sorted' do end context 'sort: oldest' do - let(:sort) { 'oldest' } + let(:sort) { 'created_asc' } it 'sorts results by created_at' do expect(results.objects(scope).map(&:id)).to eq([very_old_result.id, old_result.id, new_result.id]) diff --git a/spec/support/shared_examples/lib/gitlab/sidekiq_middleware/strategy_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/sidekiq_middleware/strategy_shared_examples.rb new file mode 100644 index 00000000000..2936bb354cf --- /dev/null +++ b/spec/support/shared_examples/lib/gitlab/sidekiq_middleware/strategy_shared_examples.rb @@ -0,0 +1,139 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'deduplicating jobs when scheduling' do |strategy_name| + let(:fake_duplicate_job) do + instance_double(Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob) + end + + let(:expected_message) { "dropped #{strategy_name.to_s.humanize.downcase}" } + + subject(:strategy) { Gitlab::SidekiqMiddleware::DuplicateJobs::Strategies.for(strategy_name).new(fake_duplicate_job) } + + describe '#schedule' do + before do + allow(Gitlab::SidekiqLogging::DeduplicationLogger.instance).to receive(:log) + end + + it 'checks for duplicates before yielding' do + expect(fake_duplicate_job).to receive(:scheduled?).twice.ordered.and_return(false) + expect(fake_duplicate_job).to( + receive(:check!) + .with(Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob::DUPLICATE_KEY_TTL) + .ordered + .and_return('a jid')) + expect(fake_duplicate_job).to receive(:duplicate?).ordered.and_return(false) + + expect { |b| strategy.schedule({}, &b) }.to yield_control + end + + it 'checks worker options for scheduled jobs' do + expect(fake_duplicate_job).to receive(:scheduled?).ordered.and_return(true) + expect(fake_duplicate_job).to receive(:options).ordered.and_return({}) + expect(fake_duplicate_job).not_to receive(:check!) + + expect { |b| strategy.schedule({}, &b) }.to yield_control + end + + context 'job marking' do + it 'adds the jid of the existing job to the job hash' do + allow(fake_duplicate_job).to receive(:scheduled?).and_return(false) + allow(fake_duplicate_job).to receive(:check!).and_return('the jid') + allow(fake_duplicate_job).to receive(:droppable?).and_return(true) + allow(fake_duplicate_job).to receive(:options).and_return({}) + job_hash = {} + + expect(fake_duplicate_job).to receive(:duplicate?).and_return(true) + expect(fake_duplicate_job).to receive(:existing_jid).and_return('the jid') + + strategy.schedule(job_hash) {} + + expect(job_hash).to include('duplicate-of' => 'the jid') + end + + context 'scheduled jobs' do + let(:time_diff) { 1.minute } + + context 'scheduled in the past' do + it 'adds the jid of the existing job to the job hash' do + allow(fake_duplicate_job).to receive(:scheduled?).twice.and_return(true) + allow(fake_duplicate_job).to receive(:scheduled_at).and_return(Time.now - time_diff) + allow(fake_duplicate_job).to receive(:options).and_return({ including_scheduled: true }) + allow(fake_duplicate_job).to( + receive(:check!) + .with(Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob::DUPLICATE_KEY_TTL) + .and_return('the jid')) + allow(fake_duplicate_job).to receive(:droppable?).and_return(true) + job_hash = {} + + expect(fake_duplicate_job).to receive(:duplicate?).and_return(true) + expect(fake_duplicate_job).to receive(:existing_jid).and_return('the jid') + + strategy.schedule(job_hash) {} + + expect(job_hash).to include('duplicate-of' => 'the jid') + end + end + + context 'scheduled in the future' do + it 'adds the jid of the existing job to the job hash' do + freeze_time do + allow(fake_duplicate_job).to receive(:scheduled?).twice.and_return(true) + allow(fake_duplicate_job).to receive(:scheduled_at).and_return(Time.now + time_diff) + allow(fake_duplicate_job).to receive(:options).and_return({ including_scheduled: true }) + allow(fake_duplicate_job).to( + receive(:check!).with(time_diff.to_i).and_return('the jid')) + allow(fake_duplicate_job).to receive(:droppable?).and_return(true) + job_hash = {} + + expect(fake_duplicate_job).to receive(:duplicate?).and_return(true) + expect(fake_duplicate_job).to receive(:existing_jid).and_return('the jid') + + strategy.schedule(job_hash) {} + + expect(job_hash).to include('duplicate-of' => 'the jid') + end + end + end + end + end + + context "when the job is droppable" do + before do + allow(fake_duplicate_job).to receive(:scheduled?).and_return(false) + allow(fake_duplicate_job).to receive(:check!).and_return('the jid') + allow(fake_duplicate_job).to receive(:duplicate?).and_return(true) + allow(fake_duplicate_job).to receive(:options).and_return({}) + allow(fake_duplicate_job).to receive(:existing_jid).and_return('the jid') + allow(fake_duplicate_job).to receive(:droppable?).and_return(true) + end + + it 'drops the job' do + schedule_result = nil + + expect(fake_duplicate_job).to receive(:droppable?).and_return(true) + + expect { |b| schedule_result = strategy.schedule({}, &b) }.not_to yield_control + expect(schedule_result).to be(false) + end + + it 'logs that the job was dropped' do + fake_logger = instance_double(Gitlab::SidekiqLogging::DeduplicationLogger) + + expect(Gitlab::SidekiqLogging::DeduplicationLogger).to receive(:instance).and_return(fake_logger) + expect(fake_logger).to receive(:log).with(a_hash_including({ 'jid' => 'new jid' }), expected_message, {}) + + strategy.schedule({ 'jid' => 'new jid' }) {} + end + + it 'logs the deduplication options of the worker' do + fake_logger = instance_double(Gitlab::SidekiqLogging::DeduplicationLogger) + + expect(Gitlab::SidekiqLogging::DeduplicationLogger).to receive(:instance).and_return(fake_logger) + allow(fake_duplicate_job).to receive(:options).and_return({ foo: :bar }) + expect(fake_logger).to receive(:log).with(a_hash_including({ 'jid' => 'new jid' }), expected_message, { foo: :bar }) + + strategy.schedule({ 'jid' => 'new jid' }) {} + end + end + end +end 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/issue_activity_shared_examples.rb new file mode 100644 index 00000000000..286305f2506 --- /dev/null +++ b/spec/support/shared_examples/lib/gitlab/usage_data_counters/issue_activity_shared_examples.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'a tracked issue edit event' do |event| + before do + stub_application_setting(usage_ping_enabled: true) + end + + def count_unique(date_from:, date_to:) + Gitlab::UsageDataCounters::HLLRedisCounter.unique_events(event_names: action, start_date: date_from, end_date: date_to) + end + + specify do + aggregate_failures do + expect(track_action(author: user1)).to be_truthy + expect(track_action(author: user1)).to be_truthy + expect(track_action(author: user2)).to be_truthy + expect(track_action(author: user3, time: time - 3.days)).to be_truthy + + expect(count_unique(date_from: time, date_to: time)).to eq(2) + expect(count_unique(date_from: time - 5.days, date_to: 1.day.since(time))).to eq(3) + end + end + + it 'does not track edit actions if author is not present' do + expect(track_action(author: nil)).to be_nil + end + + context 'when feature flag track_issue_activity_actions is disabled' do + it 'does not track edit actions' do + stub_feature_flags(track_issue_activity_actions: false) + + expect(track_action(author: user1)).to be_nil + end + end +end diff --git a/spec/support/shared_examples/mailers/notify_shared_examples.rb b/spec/support/shared_examples/mailers/notify_shared_examples.rb index 7ce7b2161f6..0143bf693c7 100644 --- a/spec/support/shared_examples/mailers/notify_shared_examples.rb +++ b/spec/support/shared_examples/mailers/notify_shared_examples.rb @@ -273,3 +273,12 @@ RSpec.shared_examples 'no email is sent' do expect(subject.message).to be_a_kind_of(ActionMailer::Base::NullMail) end end + +RSpec.shared_examples 'does not render a manage notifications link' do + it do + aggregate_failures do + expect(subject).not_to have_body_text("Manage all notifications") + expect(subject).not_to have_body_text(profile_notifications_url) + end + 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 01513161d24..92fd4363134 100644 --- a/spec/support/shared_examples/models/application_setting_shared_examples.rb +++ b/spec/support/shared_examples/models/application_setting_shared_examples.rb @@ -1,84 +1,84 @@ # frozen_string_literal: true -RSpec.shared_examples 'string of domains' do |attribute| +RSpec.shared_examples 'string of domains' do |mapped_name, attribute| it 'sets single domain' do - setting.method("#{attribute}_raw=").call('example.com') + setting.method("#{mapped_name}_raw=").call('example.com') expect(setting.method(attribute).call).to eq(['example.com']) end it 'sets multiple domains with spaces' do - setting.method("#{attribute}_raw=").call('example.com *.example.com') + setting.method("#{mapped_name}_raw=").call('example.com *.example.com') expect(setting.method(attribute).call).to eq(['example.com', '*.example.com']) end it 'sets multiple domains with newlines and a space' do - setting.method("#{attribute}_raw=").call("example.com\n *.example.com") + setting.method("#{mapped_name}_raw=").call("example.com\n *.example.com") expect(setting.method(attribute).call).to eq(['example.com', '*.example.com']) end it 'sets multiple domains with commas' do - setting.method("#{attribute}_raw=").call("example.com, *.example.com") + setting.method("#{mapped_name}_raw=").call("example.com, *.example.com") expect(setting.method(attribute).call).to eq(['example.com', '*.example.com']) end it 'sets multiple domains with semicolon' do - setting.method("#{attribute}_raw=").call("example.com; *.example.com") + setting.method("#{mapped_name}_raw=").call("example.com; *.example.com") expect(setting.method(attribute).call).to contain_exactly('example.com', '*.example.com') end it 'sets multiple domains with mixture of everything' do - setting.method("#{attribute}_raw=").call("example.com; *.example.com\n test.com\sblock.com yes.com") + setting.method("#{mapped_name}_raw=").call("example.com; *.example.com\n test.com\sblock.com yes.com") expect(setting.method(attribute).call).to contain_exactly('example.com', '*.example.com', 'test.com', 'block.com', 'yes.com') end it 'removes duplicates' do - setting.method("#{attribute}_raw=").call("example.com; example.com; 127.0.0.1; 127.0.0.1") + setting.method("#{mapped_name}_raw=").call("example.com; example.com; 127.0.0.1; 127.0.0.1") expect(setting.method(attribute).call).to contain_exactly('example.com', '127.0.0.1') end it 'does not fail with garbage values' do - setting.method("#{attribute}_raw=").call("example;34543:garbage:fdh5654;") + setting.method("#{mapped_name}_raw=").call("example;34543:garbage:fdh5654;") expect(setting.method(attribute).call).to contain_exactly('example', '34543:garbage:fdh5654') end it 'does not raise error with nil' do - setting.method("#{attribute}_raw=").call(nil) + setting.method("#{mapped_name}_raw=").call(nil) expect(setting.method(attribute).call).to eq([]) end end RSpec.shared_examples 'application settings examples' do context 'restricted signup domains' do - it_behaves_like 'string of domains', :domain_whitelist + it_behaves_like 'string of domains', :domain_allowlist, :domain_allowlist end - context 'blacklisted signup domains' do - it_behaves_like 'string of domains', :domain_blacklist + context 'denied signup domains' do + it_behaves_like 'string of domains', :domain_denylist, :domain_denylist it 'sets multiple domain with file' do - setting.domain_blacklist_file = File.open(Rails.root.join('spec/fixtures/', 'domain_blacklist.txt')) - expect(setting.domain_blacklist).to contain_exactly('example.com', 'test.com', 'foo.bar') + setting.domain_denylist_file = File.open(Rails.root.join('spec/fixtures/', 'domain_denylist.txt')) + expect(setting.domain_denylist).to contain_exactly('example.com', 'test.com', 'foo.bar') end end context 'outbound_local_requests_whitelist' do - it_behaves_like 'string of domains', :outbound_local_requests_whitelist + it_behaves_like 'string of domains', :outbound_local_requests_allowlist, :outbound_local_requests_whitelist - it 'clears outbound_local_requests_whitelist_arrays memoization' do - setting.outbound_local_requests_whitelist_raw = 'example.com' + it 'clears outbound_local_requests_allowlist_arrays memoization' do + setting.outbound_local_requests_allowlist_raw = 'example.com' - expect(setting.outbound_local_requests_whitelist_arrays).to contain_exactly( + expect(setting.outbound_local_requests_allowlist_arrays).to contain_exactly( [], [an_object_having_attributes(domain: 'example.com')] ) - setting.outbound_local_requests_whitelist_raw = 'gitlab.com' - expect(setting.outbound_local_requests_whitelist_arrays).to contain_exactly( + setting.outbound_local_requests_allowlist_raw = 'gitlab.com' + expect(setting.outbound_local_requests_allowlist_arrays).to contain_exactly( [], [an_object_having_attributes(domain: 'gitlab.com')] ) end end - context 'outbound_local_requests_whitelist_arrays' do + context 'outbound_local_requests_allowlist_arrays' do it 'separates the IPs and domains' do setting.outbound_local_requests_whitelist = [ '192.168.1.1', @@ -118,7 +118,7 @@ RSpec.shared_examples 'application settings examples' do an_object_having_attributes(domain: 'example.com', port: 8080) ] - expect(setting.outbound_local_requests_whitelist_arrays).to contain_exactly( + expect(setting.outbound_local_requests_allowlist_arrays).to contain_exactly( ip_whitelist, domain_whitelist ) end @@ -139,10 +139,10 @@ RSpec.shared_examples 'application settings examples' do ) end - it 'clears outbound_local_requests_whitelist_arrays memoization' do + it 'clears outbound_local_requests_allowlist_arrays memoization' do setting.outbound_local_requests_whitelist = ['example.com'] - expect(setting.outbound_local_requests_whitelist_arrays).to contain_exactly( + expect(setting.outbound_local_requests_allowlist_arrays).to contain_exactly( [], [an_object_having_attributes(domain: 'example.com')] ) @@ -151,7 +151,7 @@ RSpec.shared_examples 'application settings examples' do ['example.com', 'gitlab.com'] ) - expect(setting.outbound_local_requests_whitelist_arrays).to contain_exactly( + expect(setting.outbound_local_requests_allowlist_arrays).to contain_exactly( [], [an_object_having_attributes(domain: 'example.com'), an_object_having_attributes(domain: 'gitlab.com')] ) @@ -163,7 +163,7 @@ RSpec.shared_examples 'application settings examples' do setting.add_to_outbound_local_requests_whitelist(['gitlab.com']) expect(setting.outbound_local_requests_whitelist).to contain_exactly('gitlab.com') - expect(setting.outbound_local_requests_whitelist_arrays).to contain_exactly( + expect(setting.outbound_local_requests_allowlist_arrays).to contain_exactly( [], [an_object_having_attributes(domain: 'gitlab.com')] ) end @@ -171,7 +171,7 @@ RSpec.shared_examples 'application settings examples' do it 'does not raise error with nil' do setting.outbound_local_requests_whitelist = nil - expect(setting.outbound_local_requests_whitelist_arrays).to contain_exactly([], []) + expect(setting.outbound_local_requests_allowlist_arrays).to contain_exactly([], []) end end diff --git a/spec/support/shared_examples/models/atomic_internal_id_shared_examples.rb b/spec/support/shared_examples/models/atomic_internal_id_shared_examples.rb index 62d56f2e86e..fe99b1cacd9 100644 --- a/spec/support/shared_examples/models/atomic_internal_id_shared_examples.rb +++ b/spec/support/shared_examples/models/atomic_internal_id_shared_examples.rb @@ -76,6 +76,26 @@ RSpec.shared_examples 'AtomicInternalId' do |validate_presence: true| end end + describe 'supply of internal ids' do + let(:scope_value) { scope_attrs.each_value.first } + let(:method_name) { :"with_#{scope}_#{internal_id_attribute}_supply" } + + it 'provides a persistent supply of IID values, sensitive to the current state' do + iid = rand(1..1000) + write_internal_id(iid) + instance.public_send(:"track_#{scope}_#{internal_id_attribute}!") + + # Allocate 3 IID values + described_class.public_send(method_name, scope_value) do |supply| + 3.times { supply.next_value } + end + + current_value = described_class.public_send(method_name, scope_value, &:current_value) + + expect(current_value).to eq(iid + 3) + end + end + describe "#reset_scope_internal_id_attribute" do it 'rewinds the allocated IID' do expect { ensure_scope_attribute! }.not_to raise_error diff --git a/spec/support/shared_examples/models/cluster_application_core_shared_examples.rb b/spec/support/shared_examples/models/cluster_application_core_shared_examples.rb index 85a7c90ee42..51071ae47c3 100644 --- a/spec/support/shared_examples/models/cluster_application_core_shared_examples.rb +++ b/spec/support/shared_examples/models/cluster_application_core_shared_examples.rb @@ -25,4 +25,21 @@ RSpec.shared_examples 'cluster application core specs' do |application_name| describe '.association_name' do it { expect(described_class.association_name).to eq(:"application_#{subject.name}") } end + + describe '#helm_command_module' do + using RSpec::Parameterized::TableSyntax + + where(:helm_major_version, :expected_helm_command_module) do + 2 | Gitlab::Kubernetes::Helm::V2 + 3 | Gitlab::Kubernetes::Helm::V3 + end + + with_them do + subject { described_class.new(cluster: cluster).helm_command_module } + + let(:cluster) { build(:cluster, helm_major_version: helm_major_version)} + + it { is_expected.to eq(expected_helm_command_module) } + end + end end diff --git a/spec/support/shared_examples/models/cluster_application_helm_cert_shared_examples.rb b/spec/support/shared_examples/models/cluster_application_helm_cert_shared_examples.rb index ac8022a4726..187a44ec3cd 100644 --- a/spec/support/shared_examples/models/cluster_application_helm_cert_shared_examples.rb +++ b/spec/support/shared_examples/models/cluster_application_helm_cert_shared_examples.rb @@ -6,7 +6,7 @@ RSpec.shared_examples 'cluster application helm specs' do |application_name| describe '#uninstall_command' do subject { application.uninstall_command } - it { is_expected.to be_an_instance_of(Gitlab::Kubernetes::Helm::DeleteCommand) } + it { is_expected.to be_an_instance_of(Gitlab::Kubernetes::Helm::V3::DeleteCommand) } it 'has files' do expect(subject.files).to eq(application.files) diff --git a/spec/support/shared_examples/models/cycle_analytics_stage_shared_examples.rb b/spec/support/shared_examples/models/cycle_analytics_stage_shared_examples.rb index 8092f87383d..17948d648cb 100644 --- a/spec/support/shared_examples/models/cycle_analytics_stage_shared_examples.rb +++ b/spec/support/shared_examples/models/cycle_analytics_stage_shared_examples.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -RSpec.shared_examples 'cycle analytics stage' do +RSpec.shared_examples 'value stream analytics stage' do let(:valid_params) do { name: 'My Stage', @@ -111,7 +111,7 @@ RSpec.shared_examples 'cycle analytics stage' do end end -RSpec.shared_examples 'cycle analytics label based stage' do +RSpec.shared_examples 'value stream analytics label based stage' do context 'when creating label based event' do context 'when the label id is not passed' do it 'returns validation error when `start_event_label_id` is missing' do 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 37ee2548dfe..17fd2b836d3 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 @@ -13,7 +13,7 @@ RSpec.shared_examples 'issuable time tracker' do |issuable_type| end it 'renders the sidebar component empty state' do - page.within '.time-tracking-no-tracking-pane' do + page.within '[data-testid="noTrackingPane"]' do expect(page).to have_content 'No estimate or time spent' end end @@ -22,7 +22,7 @@ RSpec.shared_examples 'issuable time tracker' do |issuable_type| submit_time('/estimate 3w 1d 1h') wait_for_requests - page.within '.time-tracking-estimate-only-pane' do + page.within '[data-testid="estimateOnlyPane"]' do expect(page).to have_content '3w 1d 1h' end end @@ -31,7 +31,7 @@ RSpec.shared_examples 'issuable time tracker' do |issuable_type| submit_time('/spend 3w 1d 1h') wait_for_requests - page.within '.time-tracking-spend-only-pane' do + page.within '[data-testid="spentOnlyPane"]' do expect(page).to have_content '3w 1d 1h' end end @@ -41,7 +41,7 @@ RSpec.shared_examples 'issuable time tracker' do |issuable_type| submit_time('/spend 3w 1d 1h') wait_for_requests - page.within '.time-tracking-comparison-pane' do + page.within '[data-testid="timeTrackingComparisonPane"]' do expect(page).to have_content '3w 1d 1h' end end diff --git a/spec/support/shared_examples/read_only_message_shared_examples.rb b/spec/support/shared_examples/read_only_message_shared_examples.rb new file mode 100644 index 00000000000..4ae97ea7748 --- /dev/null +++ b/spec/support/shared_examples/read_only_message_shared_examples.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'Read-only instance' do |message| + it 'shows read-only banner' do + visit root_dashboard_path + + expect(page).to have_content(message) + end +end + +RSpec.shared_examples 'Read-write instance' do |message| + it 'does not show read-only banner' do + visit root_dashboard_path + + expect(page).not_to have_content(message) + end +end diff --git a/spec/support/shared_examples/requests/api/debian_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/debian_packages_shared_examples.rb index ec32cb4b2ff..f55043fe64f 100644 --- a/spec/support/shared_examples/requests/api/debian_packages_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/debian_packages_shared_examples.rb @@ -20,7 +20,7 @@ RSpec.shared_context 'Debian repository shared context' do |object_type| let(:source_package) { 'sample' } let(:letter) { source_package[0..2] == 'lib' ? source_package[0..3] : source_package[0] } let(:package_name) { 'libsample0' } - let(:package_version) { '1.2.3~alpha2-1' } + let(:package_version) { '1.2.3~alpha2' } let(:file_name) { "#{package_name}_#{package_version}_#{architecture}.deb" } let(:method) { :get } diff --git a/spec/support/shared_examples/requests/api/discussions_shared_examples.rb b/spec/support/shared_examples/requests/api/discussions_shared_examples.rb index 6315c10b0c4..a12cb24a513 100644 --- a/spec/support/shared_examples/requests/api/discussions_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/discussions_shared_examples.rb @@ -117,15 +117,10 @@ RSpec.shared_examples 'discussions API' do |parent_type, noteable_type, id_name, expect(response).to have_gitlab_http_status(:unauthorized) end - it 'tracks a Notes::CreateService event' do - expect(Gitlab::Tracking).to receive(:event) do |category, action, data| - expect(category).to eq('Notes::CreateService') - expect(action).to eq('execute') - expect(data[:label]).to eq('note') - expect(data[:value]).to be_an(Integer) - end - + it 'tracks a Notes::CreateService event', :snowplow do post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/discussions", user), params: { body: 'hi!' } + + expect_snowplow_event(category: 'Notes::CreateService', action: 'execute', label: 'note', value: anything) end context 'with notes_create_service_tracking feature flag disabled' do @@ -133,10 +128,10 @@ RSpec.shared_examples 'discussions API' do |parent_type, noteable_type, id_name, stub_feature_flags(notes_create_service_tracking: false) end - it 'does not track any events' do - expect(Gitlab::Tracking).not_to receive(:event) - + it 'does not track any events', :snowplow do post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/discussions"), params: { body: 'hi!' } + + expect_no_snowplow_event end end diff --git a/spec/support/shared_examples/requests/api/graphql/read_only_instance_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/read_only_instance_shared_examples.rb new file mode 100644 index 00000000000..be163d6aa0e --- /dev/null +++ b/spec/support/shared_examples/requests/api/graphql/read_only_instance_shared_examples.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'graphql on a read-only GitLab instance' do + include GraphqlHelpers + + context 'mutations' do + let(:current_user) { note.author } + let!(:note) { create(:note) } + + let(:mutation) do + variables = { + id: GitlabSchema.id_from_object(note).to_s + } + + graphql_mutation(:destroy_note, variables) + end + + it 'disallows the query' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(json_response['errors'].first['message']).to eq(Mutations::BaseMutation::ERROR_MESSAGE) + end + + it 'does not destroy the Note' do + expect do + post_graphql_mutation(mutation, current_user: current_user) + end.not_to change { Note.count } + end + end + + context 'read-only queries' do + let(:current_user) { create(:user) } + let(:project) { create(:project, :repository) } + + before do + project.add_developer(current_user) + end + + it 'allows the query' do + query = graphql_query_for('project', 'fullPath' => project.full_path) + + post_graphql(query, current_user: current_user) + + expect(graphql_data['project']).not_to be_nil + end + end +end diff --git a/spec/support/shared_examples/requests/api/labels_api_shared_examples.rb b/spec/support/shared_examples/requests/api/labels_api_shared_examples.rb new file mode 100644 index 00000000000..02e50b789cc --- /dev/null +++ b/spec/support/shared_examples/requests/api/labels_api_shared_examples.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'fetches labels' do + it 'returns correct labels' do + request + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response).to all(match_schema('public_api/v4/labels/label')) + expect(json_response.size).to eq(expected_labels.size) + expect(json_response.map {|r| r['name'] }).to match_array(expected_labels) + end +end diff --git a/spec/support/shared_examples/requests/api/multiple_and_scoped_issue_boards_shared_examples.rb b/spec/support/shared_examples/requests/api/multiple_and_scoped_issue_boards_shared_examples.rb new file mode 100644 index 00000000000..54aa9d47dd8 --- /dev/null +++ b/spec/support/shared_examples/requests/api/multiple_and_scoped_issue_boards_shared_examples.rb @@ -0,0 +1,94 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'multiple and scoped issue boards' do |route_definition| + let(:root_url) { route_definition.gsub(":id", board_parent.id.to_s) } + + context 'multiple issue boards' do + before do + board_parent.add_reporter(user) + stub_licensed_features(multiple_group_issue_boards: true) + end + + describe "POST #{route_definition}" do + it 'creates a board' do + post api(root_url, user), params: { name: "new board" } + + expect(response).to have_gitlab_http_status(:created) + + expect(response).to match_response_schema('public_api/v4/board', dir: "ee") + end + end + + describe "PUT #{route_definition}/:board_id" do + let(:url) { "#{root_url}/#{board.id}" } + + it 'updates a board' do + put api(url, user), params: { name: 'new name', weight: 4, labels: 'foo, bar' } + + expect(response).to have_gitlab_http_status(:ok) + + expect(response).to match_response_schema('public_api/v4/board', dir: "ee") + + board.reload + + expect(board.name).to eq('new name') + expect(board.weight).to eq(4) + expect(board.labels.map(&:title)).to contain_exactly('foo', 'bar') + end + + it 'does not remove missing attributes from the board' do + expect { put api(url, user), params: { name: 'new name' } } + .to not_change { board.reload.assignee } + .and not_change { board.reload.milestone } + .and not_change { board.reload.weight } + .and not_change { board.reload.labels.map(&:title).sort } + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema('public_api/v4/board', dir: "ee") + end + + it 'allows removing optional attributes' do + put api(url, user), params: { name: 'new name', assignee_id: nil, milestone_id: nil, weight: nil, labels: nil } + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema('public_api/v4/board', dir: "ee") + + board.reload + + expect(board.name).to eq('new name') + expect(board.assignee).to be_nil + expect(board.milestone).to be_nil + expect(board.weight).to be_nil + expect(board.labels).to be_empty + end + end + + describe "DELETE #{route_definition}/:board_id" do + let(:url) { "#{root_url}/#{board.id}" } + + it 'deletes a board' do + delete api(url, user) + + expect(response).to have_gitlab_http_status(:no_content) + end + end + end + + context 'with the scoped_issue_board-feature available' do + it 'returns the milestone when the `scoped_issue_board` feature is enabled' do + stub_licensed_features(scoped_issue_board: true) + + get api(root_url, user) + + expect(json_response.first["milestone"]).not_to be_nil + end + + it 'hides the milestone when the `scoped_issue_board` feature is disabled' do + stub_licensed_features(scoped_issue_board: false) + + get api(root_url, user) + + expect(json_response.first["milestone"]).to be_nil + end + 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 new file mode 100644 index 00000000000..d3ad7aa0595 --- /dev/null +++ b/spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb @@ -0,0 +1,270 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'handling get metadata requests' do + let_it_be(:package_dependency_link1) { create(:packages_dependency_link, package: package, dependency_type: :dependencies) } + let_it_be(:package_dependency_link2) { create(:packages_dependency_link, package: package, dependency_type: :devDependencies) } + let_it_be(:package_dependency_link3) { create(:packages_dependency_link, package: package, dependency_type: :bundleDependencies) } + let_it_be(:package_dependency_link4) { create(:packages_dependency_link, package: package, dependency_type: :peerDependencies) } + + let(:params) { {} } + let(:headers) { {} } + + subject { get(url, params: params, headers: headers) } + + shared_examples 'returning the npm package info' do + it 'returns the package info' do + subject + + expect_a_valid_package_response + end + end + + shared_examples 'a package that requires auth' do + it 'denies request without oauth token' do + subject + + expect(response).to have_gitlab_http_status(:not_found) + end + + context 'with oauth token' do + let(:params) { { access_token: token.token } } + + it 'returns the package info with oauth token' do + subject + + expect_a_valid_package_response + end + end + + context 'with job token' do + let(:params) { { job_token: job.token } } + + it 'returns the package info with running job token' do + subject + + expect_a_valid_package_response + end + + it 'denies request without running job token' do + job.update!(status: :success) + + subject + + expect(response).to have_gitlab_http_status(:unauthorized) + end + end + + context 'with deploy token' do + let(:headers) { build_token_auth_header(deploy_token.token) } + + it 'returns the package info with deploy token' do + subject + + expect_a_valid_package_response + end + end + end + + context 'a public project' do + it_behaves_like 'returning the npm package info' + + context 'project path with a dot' do + before do + project.update!(path: 'foo.bar') + end + + it_behaves_like 'returning the npm package info' + end + + context 'with request forward disabled' do + before do + stub_application_setting(npm_package_requests_forwarding: false) + end + + it_behaves_like 'returning the npm package info' + + context 'with unknown package' do + let(:package_name) { 'unknown' } + + it 'returns the proper response' do + subject + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end + + context 'with request forward enabled' do + before do + stub_application_setting(npm_package_requests_forwarding: true) + end + + it_behaves_like 'returning the npm package info' + + context 'with unknown package' do + let(:package_name) { 'unknown' } + + it 'returns a redirect' do + subject + + expect(response).to have_gitlab_http_status(:found) + expect(response.headers['Location']).to eq('https://registry.npmjs.org/unknown') + end + + it_behaves_like 'a gitlab tracking event', described_class.name, 'npm_request_forward' + end + end + end + + context 'internal project' do + before do + project.update!(visibility_level: Gitlab::VisibilityLevel::INTERNAL) + end + + it_behaves_like 'a package that requires auth' + end + + context 'private project' do + before do + project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE) + end + + it_behaves_like 'a package that requires auth' + + context 'with guest' do + let(:params) { { access_token: token.token } } + + it 'denies request when not enough permissions' do + project.add_guest(user) + + subject + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + end + + def expect_a_valid_package_response + expect(response).to have_gitlab_http_status(:ok) + expect(response.media_type).to eq('application/json') + expect(response).to match_response_schema('public_api/v4/packages/npm_package') + expect(json_response['name']).to eq(package.name) + expect(json_response['versions'][package.version]).to match_schema('public_api/v4/packages/npm_package_version') + ::Packages::Npm::PackagePresenter::NPM_VALID_DEPENDENCY_TYPES.each do |dependency_type| + expect(json_response.dig('versions', package.version, dependency_type.to_s)).to be_any + end + expect(json_response['dist-tags']).to match_schema('public_api/v4/packages/npm_package_tags') + end +end + +RSpec.shared_examples 'handling get dist tags requests' do + let_it_be(:package_tag1) { create(:packages_tag, package: package) } + let_it_be(:package_tag2) { create(:packages_tag, package: package) } + + let(:params) { {} } + + subject { get(url, params: params) } + + context 'with public project' do + context 'with authenticated user' do + let(:params) { { private_token: personal_access_token.token } } + + it_behaves_like 'returns package tags', :maintainer + it_behaves_like 'returns package tags', :developer + it_behaves_like 'returns package tags', :reporter + it_behaves_like 'returns package tags', :guest + end + + context 'with unauthenticated user' do + it_behaves_like 'returns package tags', :no_type + end + end + + context 'with private project' do + before do + project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE) + end + + context 'with authenticated user' do + let(:params) { { private_token: personal_access_token.token } } + + it_behaves_like 'returns package tags', :maintainer + it_behaves_like 'returns package tags', :developer + it_behaves_like 'returns package tags', :reporter + it_behaves_like 'rejects package tags access', :guest, :forbidden + end + + context 'with unauthenticated user' do + it_behaves_like 'rejects package tags access', :no_type, :not_found + end + end +end + +RSpec.shared_examples 'handling create dist tag requests' do + let_it_be(:tag_name) { 'test' } + + let(:params) { {} } + let(:env) { {} } + let(:version) { package.version } + + subject { put(url, env: env, params: params) } + + context 'with public project' do + context 'with authenticated user' do + let(:params) { { private_token: personal_access_token.token } } + let(:env) { { 'api.request.body': version } } + + it_behaves_like 'create package tag', :maintainer + it_behaves_like 'create package tag', :developer + it_behaves_like 'rejects package tags access', :reporter, :forbidden + it_behaves_like 'rejects package tags access', :guest, :forbidden + end + + context 'with unauthenticated user' do + it_behaves_like 'rejects package tags access', :no_type, :unauthorized + end + end +end + +RSpec.shared_examples 'handling delete dist tag requests' do + let_it_be(:package_tag) { create(:packages_tag, package: package) } + + let(:params) { {} } + let(:tag_name) { package_tag.name } + + subject { delete(url, params: params) } + + context 'with public project' do + context 'with authenticated user' do + let(:params) { { private_token: personal_access_token.token } } + + it_behaves_like 'delete package tag', :maintainer + it_behaves_like 'rejects package tags access', :developer, :forbidden + it_behaves_like 'rejects package tags access', :reporter, :forbidden + it_behaves_like 'rejects package tags access', :guest, :forbidden + end + + context 'with unauthenticated user' do + it_behaves_like 'rejects package tags access', :no_type, :unauthorized + end + end + + context 'with private project' do + before do + project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE) + end + + context 'with authenticated user' do + let(:params) { { private_token: personal_access_token.token } } + + it_behaves_like 'delete package tag', :maintainer + it_behaves_like 'rejects package tags access', :developer, :forbidden + it_behaves_like 'rejects package tags access', :reporter, :forbidden + it_behaves_like 'rejects package tags access', :guest, :forbidden + end + + context 'with unauthenticated user' do + it_behaves_like 'rejects package tags access', :no_type, :unauthorized + end + end +end diff --git a/spec/support/shared_examples/requests/api/packages_shared_examples.rb b/spec/support/shared_examples/requests/api/packages_shared_examples.rb index d730ed53109..3833604e304 100644 --- a/spec/support/shared_examples/requests/api/packages_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/packages_shared_examples.rb @@ -128,9 +128,13 @@ RSpec.shared_examples 'job token for package uploads' do end RSpec.shared_examples 'a package tracking event' do |category, action| - it "creates a gitlab tracking event #{action}" do - expect(Gitlab::Tracking).to receive(:event).with(category, action, {}) + before do + stub_feature_flags(collect_package_events: true) + end + it "creates a gitlab tracking event #{action}", :snowplow do expect { subject }.to change { Packages::Event.count }.by(1) + + expect_snowplow_event(category: category, action: action) end end diff --git a/spec/support/shared_examples/requests/api/packages_tags_shared_examples.rb b/spec/support/shared_examples/requests/api/packages_tags_shared_examples.rb index a371d380f47..2c203dc096e 100644 --- a/spec/support/shared_examples/requests/api/packages_tags_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/packages_tags_shared_examples.rb @@ -40,7 +40,7 @@ RSpec.shared_examples 'returns package tags' do |user_type| context 'with invalid package name' do where(:package_name, :status) do '%20' | :bad_request - nil | :forbidden + nil | :not_found end with_them do @@ -95,7 +95,7 @@ RSpec.shared_examples 'create package tag' do |user_type| context 'with invalid package name' do where(:package_name, :status) do - 'unknown' | :forbidden + 'unknown' | :not_found '' | :not_found '%20' | :bad_request end @@ -160,7 +160,7 @@ RSpec.shared_examples 'delete package tag' do |user_type| context 'with invalid package name' do where(:package_name, :status) do - 'unknown' | :forbidden + 'unknown' | :not_found '' | :not_found '%20' | :bad_request end diff --git a/spec/support/shared_examples/requests/api/tracking_shared_examples.rb b/spec/support/shared_examples/requests/api/tracking_shared_examples.rb index 2e6feae3f98..826139635ed 100644 --- a/spec/support/shared_examples/requests/api/tracking_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/tracking_shared_examples.rb @@ -1,9 +1,9 @@ # frozen_string_literal: true RSpec.shared_examples 'a gitlab tracking event' do |category, action| - it "creates a gitlab tracking event #{action}" do - expect(Gitlab::Tracking).to receive(:event).with(category, action, {}) - + it "creates a gitlab tracking event #{action}", :snowplow do subject + + expect_snowplow_event(category: category, action: action) end end diff --git a/spec/support/shared_examples/requests/lfs_http_shared_examples.rb b/spec/support/shared_examples/requests/lfs_http_shared_examples.rb index 48c5a5933e6..4ae77179527 100644 --- a/spec/support/shared_examples/requests/lfs_http_shared_examples.rb +++ b/spec/support/shared_examples/requests/lfs_http_shared_examples.rb @@ -2,42 +2,252 @@ RSpec.shared_examples 'LFS http 200 response' do it_behaves_like 'LFS http expected response code and message' do - let(:response_code) { 200 } + let(:response_code) { :ok } + end +end + +RSpec.shared_examples 'LFS http 200 blob response' do + it_behaves_like 'LFS http expected response code and message' do + let(:response_code) { :ok } + let(:content_type) { Repositories::LfsApiController::LFS_TRANSFER_CONTENT_TYPE } + let(:response_headers) { { 'X-Sendfile' => lfs_object.file.path } } + end +end + +RSpec.shared_examples 'LFS http 200 workhorse response' do + it_behaves_like 'LFS http expected response code and message' do + let(:response_code) { :ok } + let(:content_type) { Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE } end end RSpec.shared_examples 'LFS http 401 response' do it_behaves_like 'LFS http expected response code and message' do - let(:response_code) { 401 } + let(:response_code) { :unauthorized } + let(:content_type) { 'text/plain' } end end RSpec.shared_examples 'LFS http 403 response' do it_behaves_like 'LFS http expected response code and message' do - let(:response_code) { 403 } + let(:response_code) { :forbidden } let(:message) { 'Access forbidden. Check your access level.' } end end RSpec.shared_examples 'LFS http 501 response' do it_behaves_like 'LFS http expected response code and message' do - let(:response_code) { 501 } + let(:response_code) { :not_implemented } let(:message) { 'Git LFS is not enabled on this GitLab server, contact your admin.' } end end RSpec.shared_examples 'LFS http 404 response' do it_behaves_like 'LFS http expected response code and message' do - let(:response_code) { 404 } + let(:response_code) { :not_found } end end RSpec.shared_examples 'LFS http expected response code and message' do let(:response_code) { } - let(:message) { } + let(:response_headers) { {} } + let(:content_type) { LfsRequest::CONTENT_TYPE } + let(:message) {} - it 'responds with the expected response code and message' do + specify do expect(response).to have_gitlab_http_status(response_code) + expect(response.headers.to_hash).to include(response_headers) + expect(response.media_type).to match(content_type) expect(json_response['message']).to eq(message) if message end end + +RSpec.shared_examples 'LFS http requests' do + include LfsHttpHelpers + + let(:authorize_guest) {} + let(:authorize_download) {} + let(:authorize_upload) {} + + let(:lfs_object) { create(:lfs_object, :with_file) } + let(:sample_oid) { lfs_object.oid } + + let(:authorization) { authorize_user } + let(:headers) do + { + 'Authorization' => authorization, + 'X-Sendfile-Type' => 'X-Sendfile' + } + end + + let(:request_download) do + get objects_url(container, sample_oid), params: {}, headers: headers + end + + let(:request_upload) do + post_lfs_json batch_url(container), upload_body(multiple_objects), headers + end + + before do + stub_lfs_setting(enabled: true) + end + + context 'when LFS is disabled globally' do + before do + stub_lfs_setting(enabled: false) + end + + describe 'download request' do + before do + request_download + end + + it_behaves_like 'LFS http 501 response' + end + + describe 'upload request' do + before do + request_upload + end + + it_behaves_like 'LFS http 501 response' + end + end + + context 'unauthenticated' do + let(:headers) { {} } + + describe 'download request' do + before do + request_download + end + + it_behaves_like 'LFS http 401 response' + end + + describe 'upload request' do + before do + request_upload + end + + it_behaves_like 'LFS http 401 response' + end + end + + context 'without access' do + describe 'download request' do + before do + request_download + end + + it_behaves_like 'LFS http 404 response' + end + + describe 'upload request' do + before do + request_upload + end + + it_behaves_like 'LFS http 404 response' + end + end + + context 'with guest access' do + before do + authorize_guest + end + + describe 'download request' do + before do + request_download + end + + it_behaves_like 'LFS http 404 response' + end + + describe 'upload request' do + before do + request_upload + end + + it_behaves_like 'LFS http 404 response' + end + end + + context 'with download permission' do + before do + authorize_download + end + + describe 'download request' do + before do + request_download + end + + it_behaves_like 'LFS http 200 blob response' + + context 'when container does not exist' do + def objects_url(*args) + super.sub(container.full_path, 'missing/path') + end + + it_behaves_like 'LFS http 404 response' + end + end + + describe 'upload request' do + before do + request_upload + end + + it_behaves_like 'LFS http 403 response' + end + end + + context 'with upload permission' do + before do + authorize_upload + end + + describe 'upload request' do + before do + request_upload + end + + it_behaves_like 'LFS http 200 response' + end + end + + describe 'deprecated API' do + shared_examples 'deprecated request' do + before do + request + end + + it_behaves_like 'LFS http expected response code and message' do + let(:response_code) { 501 } + let(:message) { 'Server supports batch API only, please update your Git LFS client to version 1.0.1 and up.' } + end + end + + context 'when fetching LFS object using deprecated API' do + subject(:request) do + get deprecated_objects_url(container, sample_oid), params: {}, headers: headers + end + + it_behaves_like 'deprecated request' + end + + context 'when handling LFS request using deprecated API' do + subject(:request) do + post_lfs_json deprecated_objects_url(container), nil, headers + end + + it_behaves_like 'deprecated request' + end + + def deprecated_objects_url(container, oid = nil) + File.join(["#{container.http_url_to_repo}/info/lfs/objects/", oid].compact) + end + end +end diff --git a/spec/support/shared_examples/requests/rack_attack_shared_examples.rb b/spec/support/shared_examples/requests/rack_attack_shared_examples.rb index 730df4dc5ab..d4ee68309ff 100644 --- a/spec/support/shared_examples/requests/rack_attack_shared_examples.rb +++ b/spec/support/shared_examples/requests/rack_attack_shared_examples.rb @@ -81,8 +81,15 @@ RSpec.shared_examples 'rate-limited token-authenticated requests' do end it 'logs RackAttack info into structured logs' do - requests_per_period.times do - make_request(request_args) + control_count = 0 + + requests_per_period.times do |i| + if i == 0 + control_count = ActiveRecord::QueryRecorder.new { make_request(request_args) }.count + else + make_request(request_args) + end + expect(response).not_to have_gitlab_http_status(:too_many_requests) end @@ -93,13 +100,15 @@ RSpec.shared_examples 'rate-limited token-authenticated requests' do request_method: request_method, path: request_args.first, user_id: user.id, - username: user.username, - throttle_type: throttle_types[throttle_setting_prefix] + 'meta.user' => user.username, + matched: throttle_types[throttle_setting_prefix] } expect(Gitlab::AuthLogger).to receive(:error).with(arguments).once - expect_rejection { make_request(request_args) } + expect_rejection do + expect { make_request(request_args) }.not_to exceed_query_limit(control_count) + end end end @@ -210,8 +219,15 @@ RSpec.shared_examples 'rate-limited web authenticated requests' do end it 'logs RackAttack info into structured logs' do - requests_per_period.times do - request_authenticated_web_url + control_count = 0 + + requests_per_period.times do |i| + if i == 0 + control_count = ActiveRecord::QueryRecorder.new { request_authenticated_web_url }.count + else + request_authenticated_web_url + end + expect(response).not_to have_gitlab_http_status(:too_many_requests) end @@ -222,13 +238,12 @@ RSpec.shared_examples 'rate-limited web authenticated requests' do request_method: request_method, path: url_that_requires_authentication, user_id: user.id, - username: user.username, - throttle_type: throttle_types[throttle_setting_prefix] + 'meta.user' => user.username, + matched: throttle_types[throttle_setting_prefix] } expect(Gitlab::AuthLogger).to receive(:error).with(arguments).once - - request_authenticated_web_url + expect { request_authenticated_web_url }.not_to exceed_query_limit(control_count) end end diff --git a/spec/support/shared_examples/serializers/note_entity_shared_examples.rb b/spec/support/shared_examples/serializers/note_entity_shared_examples.rb index a90a2dc3667..9af6ec45e49 100644 --- a/spec/support/shared_examples/serializers/note_entity_shared_examples.rb +++ b/spec/support/shared_examples/serializers/note_entity_shared_examples.rb @@ -5,8 +5,21 @@ RSpec.shared_examples 'note entity' do context 'basic note' do it 'exposes correct elements' do - expect(subject).to include(:type, :author, :note, :note_html, :current_user, :discussion_id, - :emoji_awardable, :award_emoji, :report_abuse_path, :attachment, :noteable_note_url, :resolvable) + expect(subject).to include( + :attachment, + :author, + :award_emoji, + :base_discussion, + :current_user, + :discussion_id, + :emoji_awardable, + :note, + :note_html, + :noteable_note_url, + :report_abuse_path, + :resolvable, + :type + ) end it 'does not expose elements for specific notes cases' do @@ -20,6 +33,39 @@ RSpec.shared_examples 'note entity' do it 'does not expose web_url for author' do expect(subject[:author]).not_to include(:web_url) end + + it 'exposes permission fields on current_user' do + expect(subject[:current_user]).to include(:can_edit, :can_award_emoji, :can_resolve, :can_resolve_discussion) + end + + describe ':can_resolve_discussion' do + context 'discussion is resolvable' do + before do + expect(note.discussion).to receive(:resolvable?).and_return(true) + end + + context 'user can resolve' do + it 'is true' do + expect(note.discussion).to receive(:can_resolve?).with(user).and_return(true) + expect(subject[:current_user][:can_resolve_discussion]).to be_truthy + end + end + + context 'user cannot resolve' do + it 'is false' do + expect(note.discussion).to receive(:can_resolve?).with(user).and_return(false) + expect(subject[:current_user][:can_resolve_discussion]).to be_falsey + end + end + end + + context 'discussion is not resolvable' do + it 'is false' do + expect(note.discussion).to receive(:resolvable?).and_return(false) + expect(subject[:current_user][:can_resolve_discussion]).to be_falsey + end + end + end end context 'when note was edited' do diff --git a/spec/support/shared_examples/services/alert_management_shared_examples.rb b/spec/support/shared_examples/services/alert_management_shared_examples.rb index 1ae74979b7a..003705ca21c 100644 --- a/spec/support/shared_examples/services/alert_management_shared_examples.rb +++ b/spec/support/shared_examples/services/alert_management_shared_examples.rb @@ -8,11 +8,11 @@ RSpec.shared_examples 'creates an alert management alert' do end it 'executes the alert service hooks' do - slack_service = create(:service, type: 'SlackService', project: project, alert_events: true, active: true) + expect_next_instance_of(AlertManagement::Alert) do |alert| + expect(alert).to receive(:execute_services) + end subject - - expect(ProjectServiceWorker).to have_received(:perform_async).with(slack_service.id, an_instance_of(Hash)) end end diff --git a/spec/support/shared_examples/services/common_system_notes_shared_examples.rb b/spec/support/shared_examples/services/common_system_notes_shared_examples.rb index 5b95a5753a1..7b277d4bede 100644 --- a/spec/support/shared_examples/services/common_system_notes_shared_examples.rb +++ b/spec/support/shared_examples/services/common_system_notes_shared_examples.rb @@ -17,13 +17,13 @@ RSpec.shared_examples 'system note creation' do |update_params, note_text| end end -RSpec.shared_examples 'draft notes creation' do |wip_action| +RSpec.shared_examples 'draft notes creation' do |action| subject { described_class.new(project, user).execute(issuable, old_labels: []) } it 'creates Draft toggle and title change notes' do expect { subject }.to change { Note.count }.from(0).to(2) - expect(Note.first.note).to match("#{wip_action} as a **Work In Progress**") + expect(Note.first.note).to match("marked this merge request as **#{action}**") expect(Note.second.note).to match('changed title') end end diff --git a/spec/support/shared_examples/services/jira_import/user_mapper_services_shared_examples.rb b/spec/support/shared_examples/services/jira_import/user_mapper_services_shared_examples.rb index 7fc7ff8a8de..cbe5c7d89db 100644 --- a/spec/support/shared_examples/services/jira_import/user_mapper_services_shared_examples.rb +++ b/spec/support/shared_examples/services/jira_import/user_mapper_services_shared_examples.rb @@ -3,7 +3,6 @@ RSpec.shared_examples 'mapping jira users' do let(:client) { double } - let_it_be(:project) { create(:project) } let_it_be(:jira_service) { create(:jira_service, project: project, active: true) } before do @@ -11,7 +10,7 @@ RSpec.shared_examples 'mapping jira users' do allow(client).to receive(:get).with(url).and_return(jira_users) end - subject { described_class.new(jira_service, start_at) } + subject { described_class.new(current_user, project, start_at) } context 'jira_users is nil' do let(:jira_users) { nil } @@ -22,18 +21,27 @@ RSpec.shared_examples 'mapping jira users' do end context 'when jira_users is present' do - # TODO: now we only create an array in a proper format - # mapping is tracked in https://gitlab.com/gitlab-org/gitlab/-/issues/219023 let(:mapped_users) do [ - { jira_account_id: 'abcd', jira_display_name: 'user1', jira_email: nil, gitlab_id: nil, gitlab_username: nil, gitlab_name: nil }, - { jira_account_id: 'efg', jira_display_name: nil, jira_email: nil, gitlab_id: nil, gitlab_username: nil, gitlab_name: nil }, - { jira_account_id: 'hij', jira_display_name: 'user3', jira_email: 'user3@example.com', gitlab_id: nil, gitlab_username: nil, gitlab_name: nil } + { jira_account_id: 'abcd', jira_display_name: 'User-Name1', jira_email: nil, gitlab_id: user_1.id }, + { jira_account_id: 'efg', jira_display_name: 'username-2', jira_email: nil, gitlab_id: user_2.id }, + { jira_account_id: 'hij', jira_display_name: nil, jira_email: nil, gitlab_id: nil }, + { jira_account_id: '123', jira_display_name: 'user-4', jira_email: 'user-4@example.com', gitlab_id: user_4.id }, + { jira_account_id: '456', jira_display_name: 'username5foo', jira_email: 'user-5@example.com', gitlab_id: nil }, + { jira_account_id: '789', jira_display_name: 'user-6', jira_email: 'user-6@example.com', gitlab_id: nil }, + { jira_account_id: 'xyz', jira_display_name: 'username-7', jira_email: 'user-7@example.com', gitlab_id: nil }, + { jira_account_id: 'vhk', jira_display_name: 'user-8', jira_email: 'user8_email@example.com', gitlab_id: user_8.id }, + { jira_account_id: 'uji', jira_display_name: 'user-9', jira_email: 'uji@example.com', gitlab_id: user_1.id } ] end it 'returns users mapped to Gitlab' do expect(subject.execute).to eq(mapped_users) end + + # 1 query for getting matched users, 3 queries for MembersFinder + it 'runs only 4 queries' do + expect { subject }.not_to exceed_query_limit(4) + end end end diff --git a/spec/support/shared_examples/services/packages_shared_examples.rb b/spec/support/shared_examples/services/packages_shared_examples.rb index 65f4b3b5513..7987f2c296b 100644 --- a/spec/support/shared_examples/services/packages_shared_examples.rb +++ b/spec/support/shared_examples/services/packages_shared_examples.rb @@ -8,8 +8,8 @@ RSpec.shared_examples 'assigns build to package' do it 'assigns the pipeline to the package' do package = subject - expect(package.build_info).to be_present - expect(package.build_info.pipeline).to eq job.pipeline + expect(package.original_build_info).to be_present + expect(package.original_build_info.pipeline).to eq job.pipeline end end end diff --git a/spec/support/shared_examples/services/pages_size_limit_shared_examples.rb b/spec/support/shared_examples/services/pages_size_limit_shared_examples.rb index 15bf0d3698a..d9e906ebb75 100644 --- a/spec/support/shared_examples/services/pages_size_limit_shared_examples.rb +++ b/spec/support/shared_examples/services/pages_size_limit_shared_examples.rb @@ -4,6 +4,7 @@ RSpec.shared_examples 'pages size limit is' do |size_limit| context "when size is below the limit" do before do allow(metadata).to receive(:total_size).and_return(size_limit - 1.megabyte) + allow(metadata).to receive(:entries).and_return([]) end it 'updates pages correctly' do @@ -17,6 +18,7 @@ RSpec.shared_examples 'pages size limit is' do |size_limit| context "when size is above the limit" do before do allow(metadata).to receive(:total_size).and_return(size_limit + 1.megabyte) + allow(metadata).to receive(:entries).and_return([]) end it 'limits the maximum size of gitlab pages' do diff --git a/spec/support/shared_examples/uploaders/workers/object_storage/migrate_uploads_shared_examples.rb b/spec/support/shared_examples/uploaders/workers/object_storage/migrate_uploads_shared_examples.rb deleted file mode 100644 index 5a9a3dfc2d2..00000000000 --- a/spec/support/shared_examples/uploaders/workers/object_storage/migrate_uploads_shared_examples.rb +++ /dev/null @@ -1,120 +0,0 @@ -# frozen_string_literal: true - -# Expects the calling spec to define: -# - model_class -# - mounted_as -# - to_store -RSpec.shared_examples 'uploads migration worker' do - def perform(uploads, store = nil) - described_class.new.perform(uploads.ids, model_class.to_s, mounted_as, store || to_store) - rescue ObjectStorage::MigrateUploadsWorker::Report::MigrationFailures - # swallow - end - - describe '.enqueue!' do - def enqueue! - described_class.enqueue!(uploads, model_class, mounted_as, to_store) - end - - it 'is guarded by .sanity_check!' do - expect(described_class).to receive(:perform_async) - expect(described_class).to receive(:sanity_check!) - - enqueue! - end - - context 'sanity_check! fails' do - include_context 'sanity_check! fails' - - it 'does not enqueue a job' do - expect(described_class).not_to receive(:perform_async) - - expect { enqueue! }.to raise_error(described_class::SanityCheckError) - end - end - end - - describe '.sanity_check!' do - shared_examples 'raises a SanityCheckError' do |expected_message| - let(:mount_point) { nil } - - it do - expect { described_class.sanity_check!(uploads, model_class, mount_point) } - .to raise_error(described_class::SanityCheckError).with_message(expected_message) - end - end - - context 'uploader types mismatch' do - let!(:outlier) { create(:upload, uploader: 'GitlabUploader') } - - include_examples 'raises a SanityCheckError', /Multiple uploaders found/ - end - - context 'mount point not found' do - include_examples 'raises a SanityCheckError', /Mount point [a-z:]+ not found in/ do - let(:mount_point) { :potato } - end - end - end - - describe '#perform' do - shared_examples 'outputs correctly' do |success: 0, failures: 0| - total = success + failures - - if success > 0 - it 'outputs the reports' do - expect(Gitlab::AppLogger).to receive(:info).with(%r{Migrated #{success}/#{total} files}) - - perform(uploads) - end - end - - if failures > 0 - it 'outputs upload failures' do - expect(Gitlab::AppLogger).to receive(:warn).with(/Error .* I am a teapot/) - - perform(uploads) - end - end - end - - it_behaves_like 'outputs correctly', success: 10 - - it 'migrates files to remote storage' do - perform(uploads) - - expect(Upload.where(store: ObjectStorage::Store::LOCAL).count).to eq(0) - end - - context 'reversed' do - let(:to_store) { ObjectStorage::Store::LOCAL } - - before do - perform(uploads, ObjectStorage::Store::REMOTE) - end - - it 'migrates files to local storage' do - expect(Upload.where(store: ObjectStorage::Store::REMOTE).count).to eq(10) - - perform(uploads) - - expect(Upload.where(store: ObjectStorage::Store::LOCAL).count).to eq(10) - end - end - - context 'migration is unsuccessful' do - before do - allow_any_instance_of(ObjectStorage::Concern) - .to receive(:migrate!).and_raise(CarrierWave::UploadError, 'I am a teapot.') - end - - it_behaves_like 'outputs correctly', failures: 10 - end - end -end - -RSpec.shared_context 'sanity_check! fails' do - before do - expect(described_class).to receive(:sanity_check!).and_raise(described_class::SanityCheckError) - end -end diff --git a/spec/support/shared_examples/workers/concerns/reenqueuer_shared_examples.rb b/spec/support/shared_examples/workers/concerns/reenqueuer_shared_examples.rb index 50879969e90..37f44f98cda 100644 --- a/spec/support/shared_examples/workers/concerns/reenqueuer_shared_examples.rb +++ b/spec/support/shared_examples/workers/concerns/reenqueuer_shared_examples.rb @@ -1,40 +1,32 @@ # frozen_string_literal: true -# Expects `worker_class` to be defined +# Expects `subject` to be a job/worker instance RSpec.shared_examples 'reenqueuer' do - subject(:job) { worker_class.new } - before do - allow(job).to receive(:sleep) # faster tests + allow(subject).to receive(:sleep) # faster tests end it 'implements lease_timeout' do - expect(job.lease_timeout).to be_a(ActiveSupport::Duration) + expect(subject.lease_timeout).to be_a(ActiveSupport::Duration) end describe '#perform' do it 'tries to obtain a lease' do - expect_to_obtain_exclusive_lease(job.lease_key) + expect_to_obtain_exclusive_lease(subject.lease_key) - job.perform + subject.perform end end end -# Example usage: -# -# it_behaves_like 'it is rate limited to 1 call per', 5.seconds do -# subject { described_class.new } -# let(:rate_limited_method) { subject.perform } -# end -# -RSpec.shared_examples 'it is rate limited to 1 call per' do |minimum_duration| +# Expects `subject` to be a job/worker instance +RSpec.shared_examples '#perform is rate limited to 1 call per' do |minimum_duration| before do # Allow Timecop freeze and travel without the block form Timecop.safe_mode = false Timecop.freeze - time_travel_during_rate_limited_method(actual_duration) + time_travel_during_perform(actual_duration) end after do @@ -48,7 +40,7 @@ RSpec.shared_examples 'it is rate limited to 1 call per' do |minimum_duration| it 'sleeps exactly the minimum duration' do expect(subject).to receive(:sleep).with(a_value_within(0.01).of(minimum_duration)) - rate_limited_method + subject.perform end end @@ -58,7 +50,7 @@ RSpec.shared_examples 'it is rate limited to 1 call per' do |minimum_duration| it 'sleeps 90% of minimum duration' do expect(subject).to receive(:sleep).with(a_value_within(0.01).of(0.9 * minimum_duration)) - rate_limited_method + subject.perform end end @@ -68,7 +60,7 @@ RSpec.shared_examples 'it is rate limited to 1 call per' do |minimum_duration| it 'sleeps 10% of minimum duration' do expect(subject).to receive(:sleep).with(a_value_within(0.01).of(0.1 * minimum_duration)) - rate_limited_method + subject.perform end end @@ -78,7 +70,7 @@ RSpec.shared_examples 'it is rate limited to 1 call per' do |minimum_duration| it 'does not sleep' do expect(subject).not_to receive(:sleep) - rate_limited_method + subject.perform end end @@ -88,7 +80,7 @@ RSpec.shared_examples 'it is rate limited to 1 call per' do |minimum_duration| it 'does not sleep' do expect(subject).not_to receive(:sleep) - rate_limited_method + subject.perform end end @@ -98,11 +90,11 @@ RSpec.shared_examples 'it is rate limited to 1 call per' do |minimum_duration| it 'does not sleep' do expect(subject).not_to receive(:sleep) - rate_limited_method + subject.perform end end - def time_travel_during_rate_limited_method(actual_duration) + def time_travel_during_perform(actual_duration) # Save the original implementation of ensure_minimum_duration original_ensure_minimum_duration = subject.method(:ensure_minimum_duration) |