diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-05-19 15:44:42 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-05-19 15:44:42 +0000 |
commit | 4555e1b21c365ed8303ffb7a3325d773c9b8bf31 (patch) | |
tree | 5423a1c7516cffe36384133ade12572cf709398d /spec/support/shared_examples | |
parent | e570267f2f6b326480d284e0164a6464ba4081bc (diff) | |
download | gitlab-ce-4555e1b21c365ed8303ffb7a3325d773c9b8bf31.tar.gz |
Add latest changes from gitlab-org/gitlab@13-12-stable-eev13.12.0-rc42
Diffstat (limited to 'spec/support/shared_examples')
70 files changed, 2096 insertions, 594 deletions
diff --git a/spec/support/shared_examples/alert_notification_service_shared_examples.rb b/spec/support/shared_examples/alert_notification_service_shared_examples.rb deleted file mode 100644 index fc935effe0e..00000000000 --- a/spec/support/shared_examples/alert_notification_service_shared_examples.rb +++ /dev/null @@ -1,44 +0,0 @@ -# frozen_string_literal: true - -RSpec.shared_examples 'Alert Notification Service sends notification email' do - let(:notification_service) { spy } - - it 'sends a notification' do - expect(NotificationService) - .to receive(:new) - .and_return(notification_service) - - expect(notification_service) - .to receive_message_chain(:async, :prometheus_alerts_fired) - - expect(subject).to be_success - end -end - -RSpec.shared_examples 'Alert Notification Service sends no notifications' do |http_status: nil| - it 'does not notify' do - expect(NotificationService).not_to receive(:new) - - if http_status.present? - expect(subject).to be_error - expect(subject.http_status).to eq(http_status) - else - expect(subject).to be_success - end - end -end - -RSpec.shared_examples 'creates status-change system note for an auto-resolved alert' do - it 'has 2 new system notes' do - expect { subject }.to change(Note, :count).by(2) - expect(Note.last.note).to include('Resolved') - end -end - -# Requires `source` to be defined -RSpec.shared_examples 'creates single system note based on the source of the alert' do - it 'has one new system note' do - expect { subject }.to change(Note, :count).by(1) - expect(Note.last.note).to include(source) - end -end diff --git a/spec/support/shared_examples/boards/lists/update_service_shared_examples.rb b/spec/support/shared_examples/boards/lists/update_service_shared_examples.rb index d8a74f2582d..1fab31cd513 100644 --- a/spec/support/shared_examples/boards/lists/update_service_shared_examples.rb +++ b/spec/support/shared_examples/boards/lists/update_service_shared_examples.rb @@ -2,14 +2,30 @@ RSpec.shared_examples 'moving list' do context 'when user can admin list' do - it 'calls Lists::MoveService to update list position' do + before do board.resource_parent.add_developer(user) + end + + context 'when the new position is valid' do + it 'calls Lists::MoveService to update list position' do + expect_next_instance_of(Boards::Lists::MoveService, board.resource_parent, user, params) do |move_service| + expect(move_service).to receive(:execute).with(list).and_call_original + end - expect_next_instance_of(Boards::Lists::MoveService, board.resource_parent, user, params) do |move_service| - expect(move_service).to receive(:execute).with(list).and_call_original + service.execute(list) end - service.execute(list) + it 'returns a success response' do + expect(service.execute(list)).to be_success + end + end + + context 'when the new position is invalid' do + let(:params) { { position: 10 } } + + it 'returns error response' do + expect(service.execute(list)).to be_error + end end end @@ -19,6 +35,10 @@ RSpec.shared_examples 'moving list' do service.execute(list) end + + it 'returns an error response' do + expect(service.execute(list)).to be_error + end 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 dd71107455f..70a684c12bf 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 @@ -40,7 +40,7 @@ RSpec.shared_examples 'project access tokens available #create' do it 'returns success message' do subject - expect(response.flash[:notice]).to match('Your new project access token has been created.') + expect(controller).to set_flash[:notice].to match('Your new project access token has been created.') end it 'creates project access token' do @@ -88,7 +88,7 @@ RSpec.shared_examples 'project access tokens available #create' do it 'shows a failure alert' do subject - expect(response.flash[:alert]).to match("Failed to create new project access token: Failed!") + expect(controller).to set_flash[:alert].to match("Failed to create new project access token: Failed!") end end end diff --git a/spec/support/shared_examples/controllers/issuable_notes_filter_shared_examples.rb b/spec/support/shared_examples/controllers/issuable_notes_filter_shared_examples.rb index 5ecc5c08bbd..a4eb6a839c0 100644 --- a/spec/support/shared_examples/controllers/issuable_notes_filter_shared_examples.rb +++ b/spec/support/shared_examples/controllers/issuable_notes_filter_shared_examples.rb @@ -35,7 +35,7 @@ RSpec.shared_examples 'issuable notes filter' do get :discussions, params: params.merge(notes_filter: notes_filter) end - it 'does not set notes filter when database is in read only mode' do + it 'does not set notes filter when database is in read-only mode' do allow(Gitlab::Database).to receive(:read_only?).and_return(true) notes_filter = UserPreference::NOTES_FILTERS[:only_comments] diff --git a/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb b/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb index 0a040557ffe..cfee26a0d6a 100644 --- a/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb +++ b/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb @@ -130,8 +130,8 @@ RSpec.shared_examples 'wiki controller actions' do it_behaves_like 'fetching history', :ok do let(:allow_read_wiki) { true } - it 'assigns @page_versions' do - expect(assigns(:page_versions)).to be_present + it 'assigns @commits' do + expect(assigns(:commits)).to be_present end end diff --git a/spec/support/shared_examples/features/board_sidebar_labels_examples.rb b/spec/support/shared_examples/features/board_sidebar_labels_examples.rb new file mode 100644 index 00000000000..520980c2615 --- /dev/null +++ b/spec/support/shared_examples/features/board_sidebar_labels_examples.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +RSpec.shared_context 'labels from nested groups and projects' do + let_it_be(:group) { create(:group) } + let_it_be(:group_label) { create(:group_label, group: group, name: 'Group label') } + let_it_be(:project) { create(:project, group: group) } + let_it_be(:project_label) { create(:label, project: project, name: 'Project label') } + + let_it_be(:subgroup) { create(:group, parent: group) } + let_it_be(:subgroup_label) { create(:group_label, group: subgroup, name: 'Subgroup label') } + let_it_be(:subproject) { create(:project, group: subgroup) } + let_it_be(:subproject_label) { create(:label, project: subproject, name: 'Subproject label') } + + let_it_be(:subgroup2) { create(:group, parent: group) } + let_it_be(:subgroup2_label) { create(:group_label, group: subgroup2, name: 'Subgroup2 label') } + + let_it_be(:maintainer) { create(:user) } + + let(:labels_select) { find("[data-testid='sidebar-labels']") } + let(:labels_dropdown) { labels_select.find('[data-testid="dropdown-content"]')} + + before do + group.add_maintainer(maintainer) + + sign_in(maintainer) + end +end + +RSpec.shared_examples "an issue from a subgroup's project is selected" do + context 'when editing labels' do + before do + click_card_and_edit_label + end + + it 'displays the label from the top-level group' do + expect(labels_dropdown).to have_content(group_label.name) + end + + it 'displays the label from the subgroup' do + expect(labels_dropdown).to have_content(subgroup_label.name) + end + + it 'displays the label from the project' do + expect(labels_dropdown).to have_content(subproject_label.name) + end + + it "does not display labels from the subgroup's siblings (project or group)" do + aggregate_failures do + expect(labels_dropdown).not_to have_content(project_label.name) + expect(labels_dropdown).not_to have_content(subgroup2_label.name) + end + end + end +end + +RSpec.shared_examples 'an issue from a direct descendant project is selected' do + context 'when editing labels' do + before do + click_card_and_edit_label + end + + it 'displays the label from the top-level group' do + expect(labels_dropdown).to have_content(group_label.name) + end + + it 'displays the label from the project' do + expect(labels_dropdown).to have_content(project_label.name) + end + + it "does not display labels from the project's siblings or their descendents" do + aggregate_failures do + expect(labels_dropdown).not_to have_content(subgroup_label.name) + expect(labels_dropdown).not_to have_content(subproject_label.name) + end + end + end +end diff --git a/spec/support/shared_examples/features/issuable_invite_members_shared_examples.rb b/spec/support/shared_examples/features/issuable_invite_members_shared_examples.rb index 49c3674277d..736c353c2aa 100644 --- a/spec/support/shared_examples/features/issuable_invite_members_shared_examples.rb +++ b/spec/support/shared_examples/features/issuable_invite_members_shared_examples.rb @@ -22,32 +22,7 @@ RSpec.shared_examples 'issuable invite members experiments' do end end - context 'when invite_members_version_b experiment is enabled' do - before do - stub_experiment_for_subject(invite_members_version_b: true) - end - - it 'shows a link for inviting members and follows through to modal' do - project.add_developer(user) - visit issuable_path - - find('.block.assignee .edit-link').click - - wait_for_requests - - page.within '.dropdown-menu-user' do - expect(page).to have_link('Invite Members', href: '#') - expect(page).to have_selector('[data-track-event="click_invite_members_version_b"]') - expect(page).to have_selector('[data-track-label="edit_assignee"]') - end - - click_link 'Invite Members' - - expect(page).to have_content("Oops, this feature isn't ready yet") - end - end - - context 'when invite_members_version_b experiment is disabled' do + context 'when user cannot invite members in assignee dropdown' do it 'shows author in assignee dropdown and no invite link' do project.add_developer(user) visit issuable_path diff --git a/spec/support/shared_examples/features/sidebar_shared_examples.rb b/spec/support/shared_examples/features/sidebar_shared_examples.rb index 429efbe6ba0..c9508818f74 100644 --- a/spec/support/shared_examples/features/sidebar_shared_examples.rb +++ b/spec/support/shared_examples/features/sidebar_shared_examples.rb @@ -44,22 +44,24 @@ RSpec.shared_examples 'issue boards sidebar' do context 'in notifications subscription' do it 'displays notifications toggle', :aggregate_failures do page.within('[data-testid="sidebar-notifications"]') do - expect(page).to have_selector('[data-testid="notification-subscribe-toggle"]') + expect(page).to have_selector('[data-testid="subscription-toggle"]') expect(page).to have_content('Notifications') - expect(page).not_to have_content('Notifications have been disabled by the project or group owner') + expect(page).not_to have_content('Disabled by project owner') end end it 'shows toggle as on then as off as user toggles to subscribe and unsubscribe', :aggregate_failures do - toggle = find('[data-testid="notification-subscribe-toggle"]') + wait_for_requests - toggle.click + click_button 'Notifications' - expect(toggle).to have_css("button.is-checked") + expect(page).to have_button('Notifications', class: 'is-checked') - toggle.click + click_button 'Notifications' - expect(toggle).not_to have_css("button.is-checked") + wait_for_requests + + expect(page).not_to have_button('Notifications', class: 'is-checked') end context 'when notifications have been disabled' do @@ -71,9 +73,28 @@ RSpec.shared_examples 'issue boards sidebar' do it 'displays a message that notifications have been disabled' do page.within('[data-testid="sidebar-notifications"]') do - expect(page).not_to have_selector('[data-testid="notification-subscribe-toggle"]') - expect(page).to have_content('Notifications have been disabled by the project or group owner') + expect(page).to have_button('Notifications', class: 'is-disabled') + expect(page).to have_content('Disabled by project owner') + end + end + end + end + + context 'confidentiality' do + it 'make issue confidential' do + page.within('.confidentiality') do + expect(page).to have_content('Not confidential') + + click_button 'Edit' + expect(page).to have_css('.sidebar-item-warning-message') + + within('.sidebar-item-warning-message') do + click_button 'Turn on' end + + wait_for_requests + + expect(page).to have_content('This issue is confidential') end end end diff --git a/spec/support/shared_examples/features/variable_list_shared_examples.rb b/spec/support/shared_examples/features/variable_list_shared_examples.rb index 2fd88b610e9..4b94411f009 100644 --- a/spec/support/shared_examples/features/variable_list_shared_examples.rb +++ b/spec/support/shared_examples/features/variable_list_shared_examples.rb @@ -8,7 +8,7 @@ RSpec.shared_examples 'variable list' do end it 'adds a new CI variable' do - click_button('Add Variable') + click_button('Add variable') fill_variable('key', 'key_value') do click_button('Add variable') @@ -22,7 +22,7 @@ RSpec.shared_examples 'variable list' do end it 'adds a new protected variable' do - click_button('Add Variable') + click_button('Add variable') fill_variable('key', 'key_value') do click_button('Add variable') @@ -37,7 +37,7 @@ RSpec.shared_examples 'variable list' do end it 'defaults to unmasked' do - click_button('Add Variable') + click_button('Add variable') fill_variable('key', 'key_value') do click_button('Add variable') @@ -149,7 +149,7 @@ RSpec.shared_examples 'variable list' do end it 'shows a validation error box about duplicate keys' do - click_button('Add Variable') + click_button('Add variable') fill_variable('key', 'key_value') do click_button('Add variable') @@ -157,7 +157,7 @@ RSpec.shared_examples 'variable list' do wait_for_requests - click_button('Add Variable') + click_button('Add variable') fill_variable('key', 'key_value') do click_button('Add variable') @@ -170,7 +170,7 @@ RSpec.shared_examples 'variable list' do end it 'prevents a variable to be added if no values are provided when a variable is set to masked' do - click_button('Add Variable') + click_button('Add variable') page.within('#add-ci-variable') do find('[data-qa-selector="ci_variable_key_field"] input').set('empty_mask_key') @@ -182,7 +182,7 @@ RSpec.shared_examples 'variable list' do end it 'shows validation error box about unmaskable values' do - click_button('Add Variable') + click_button('Add variable') fill_variable('empty_mask_key', '???', protected: true, masked: true) do expect(page).to have_content('This variable can not be masked') @@ -192,7 +192,7 @@ RSpec.shared_examples 'variable list' do it 'handles multiple edits and a deletion' do # Create two variables - click_button('Add Variable') + click_button('Add variable') fill_variable('akey', 'akeyvalue') do click_button('Add variable') @@ -200,7 +200,7 @@ RSpec.shared_examples 'variable list' do wait_for_requests - click_button('Add Variable') + click_button('Add variable') fill_variable('zkey', 'zkeyvalue') do click_button('Add variable') @@ -224,7 +224,7 @@ RSpec.shared_examples 'variable list' do wait_for_requests # Add another variable - click_button('Add Variable') + click_button('Add variable') fill_variable('ckey', 'ckeyvalue') do click_button('Add variable') @@ -249,7 +249,7 @@ RSpec.shared_examples 'variable list' do end it 'defaults to protected' do - click_button('Add Variable') + click_button('Add variable') page.within('#add-ci-variable') do expect(find('[data-testid="ci-variable-protected-checkbox"]')).to be_checked @@ -269,7 +269,7 @@ RSpec.shared_examples 'variable list' do end it 'defaults to unprotected' do - click_button('Add Variable') + click_button('Add variable') page.within('#add-ci-variable') do expect(find('[data-testid="ci-variable-protected-checkbox"]')).not_to be_checked diff --git a/spec/support/shared_examples/features/wiki/user_creates_wiki_page_shared_examples.rb b/spec/support/shared_examples/features/wiki/user_creates_wiki_page_shared_examples.rb index 8a6d5d88ca6..f2576931642 100644 --- a/spec/support/shared_examples/features/wiki/user_creates_wiki_page_shared_examples.rb +++ b/spec/support/shared_examples/features/wiki/user_creates_wiki_page_shared_examples.rb @@ -24,8 +24,8 @@ RSpec.shared_examples 'User creates wiki page' do page.within(".wiki-form") do fill_in(:wiki_content, with: "") - page.execute_script("window.onbeforeunload = null") page.execute_script("document.querySelector('.wiki-form').submit()") + page.accept_alert # manually force form submit end expect(page).to have_content("The form contains the following error:").and have_content("Content can't be blank") 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 d185e9dd81c..db2a96d9649 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 @@ -93,8 +93,8 @@ RSpec.shared_examples 'User updates wiki page' do it 'shows a validation error message if the form is force submitted', :js do fill_in(:wiki_content, with: '') - page.execute_script("window.onbeforeunload = null") page.execute_script("document.querySelector('.wiki-form').submit()") + page.accept_alert # manually force form submit expect(page).to have_selector('.wiki-form') expect(page).to have_content('Edit Page') @@ -117,14 +117,6 @@ RSpec.shared_examples 'User updates wiki page' do expect(page).to have_selector('.atwho-view') end - it 'shows the error message', :js do - wiki_page.update(content: 'Update') # rubocop:disable Rails/SaveBang - - click_button('Save changes') - - expect(page).to have_content('Someone edited the page the same time you did.') - end - it 'updates a page', :js do fill_in('Content', with: 'Updated Wiki Content') click_on('Save changes') @@ -145,6 +137,18 @@ RSpec.shared_examples 'User updates wiki page' do end it_behaves_like 'wiki file attachments' + + context 'when multiple people edit the page at the same time' do + it 'preserves user changes in the wiki editor', :js do + wiki_page.update(content: 'Some Other Updates') # rubocop:disable Rails/SaveBang + + fill_in('Content', with: 'Updated Wiki Content') + click_on('Save changes') + + expect(page).to have_content('Someone edited the page the same time you did.') + expect(find('textarea#wiki_content').value).to eq('Updated Wiki Content') + end + end end context 'when the page is in a subdir', :js do diff --git a/spec/support/shared_examples/finders/packages_shared_examples.rb b/spec/support/shared_examples/finders/packages_shared_examples.rb index 2d4e8d0df1f..b3ec2336cca 100644 --- a/spec/support/shared_examples/finders/packages_shared_examples.rb +++ b/spec/support/shared_examples/finders/packages_shared_examples.rb @@ -20,9 +20,11 @@ end RSpec.shared_examples 'concerning package statuses' do let_it_be(:hidden_package) { create(:maven_package, :hidden, project: project) } + let_it_be(:error_package) { create(:maven_package, :error, project: project) } - context 'hidden packages' do + context 'displayable packages' do it { is_expected.not_to include(hidden_package) } + it { is_expected.to include(error_package) } end context 'with status param' do diff --git a/spec/support/shared_examples/graphql/mutations/boards/update_list_shared_examples.rb b/spec/support/shared_examples/graphql/mutations/boards/update_list_shared_examples.rb new file mode 100644 index 00000000000..4385cd519be --- /dev/null +++ b/spec/support/shared_examples/graphql/mutations/boards/update_list_shared_examples.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'update board list mutation' do + describe '#resolve' do + let(:mutation) { described_class.new(object: nil, context: { current_user: current_user }, field: nil) } + let(:list_update_params) { { position: 1, collapsed: true } } + + subject { mutation.resolve(list: list, **list_update_params) } + + before_all do + group.add_reporter(reporter) + group.add_guest(guest) + list.update_preferences_for(reporter, collapsed: false) + end + + context 'with permission to admin board lists' do + let(:current_user) { reporter } + + it 'updates the list position and collapsed state as expected' do + subject + + reloaded_list = list.reload + expect(reloaded_list.position).to eq(1) + expect(reloaded_list.collapsed?(current_user)).to eq(true) + end + end + + context 'with permission to read board lists' do + let(:current_user) { guest } + + it 'updates the list collapsed state but not the list position' do + subject + + reloaded_list = list.reload + expect(reloaded_list.position).to eq(0) + expect(reloaded_list.collapsed?(current_user)).to eq(true) + end + end + + context 'without permission to read board lists' do + let(:current_user) { create(:user) } + + it 'raises Resource Not Found error' do + expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) + end + end + end +end diff --git a/spec/support/shared_examples/graphql/mutations/security/ci_configuration_shared_examples.rb b/spec/support/shared_examples/graphql/mutations/security/ci_configuration_shared_examples.rb new file mode 100644 index 00000000000..2bb3d807aa7 --- /dev/null +++ b/spec/support/shared_examples/graphql/mutations/security/ci_configuration_shared_examples.rb @@ -0,0 +1,114 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.shared_examples_for 'graphql mutations security ci configuration' do + let_it_be(:project) { create(:project, :public, :repository) } + let_it_be(:user) { create(:user) } + + let(:branch) do + "set-secret-config" + end + + let(:success_path) do + "http://127.0.0.1:3000/root/demo-historic-secrets/-/merge_requests/new?" + end + + let(:service_response) do + ServiceResponse.success(payload: { branch: branch, success_path: success_path }) + end + + let(:error) { "An error occured!" } + + let(:service_error_response) do + ServiceResponse.error(message: error) + end + + specify { expect(described_class).to require_graphql_authorizations(:push_code) } + + describe '#resolve' do + let(:result) { subject } + + it 'raises an error if the resource is not accessible to the user' do + expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) + end + + context 'when user does not have enough permissions' do + before do + project.add_guest(user) + end + + it 'raises an error' do + expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) + end + end + + context 'when user is a maintainer of a different project' do + before do + create(:project_empty_repo).add_maintainer(user) + end + + it 'raises an error' do + expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) + end + end + + context 'when the user does not have permission to create a new branch' do + let(:error_message) { 'You are not allowed to create protected branches on this project.' } + + before do + project.add_developer(user) + + allow_next_instance_of(::Files::MultiService) do |multi_service| + allow(multi_service).to receive(:execute).and_raise(Gitlab::Git::PreReceiveError.new("GitLab: #{error_message}")) + end + end + + it 'returns an array of errors' do + expect(result).to match( + branch: be_nil, + success_path: be_nil, + errors: match_array([error_message]) + ) + end + end + + context 'when the user can create a merge request' do + before do + project.add_developer(user) + end + + context 'when service successfully generates a path to create a new merge request' do + before do + allow_next_instance_of(service) do |service| + allow(service).to receive(:execute).and_return(service_response) + end + end + + it 'returns a success path' do + expect(result).to match( + branch: branch, + success_path: success_path, + errors: [] + ) + end + end + + context 'when service can not generate any path to create a new merge request' do + before do + allow_next_instance_of(service) do |service| + allow(service).to receive(:execute).and_return(service_error_response) + end + end + + it 'returns an array of errors' do + expect(result).to match( + branch: be_nil, + success_path: be_nil, + errors: match_array([error]) + ) + end + end + end + end +end diff --git a/spec/support/shared_examples/graphql/resolvers/packages_resolvers_shared_examples.rb b/spec/support/shared_examples/graphql/resolvers/packages_resolvers_shared_examples.rb new file mode 100644 index 00000000000..3d6fec85490 --- /dev/null +++ b/spec/support/shared_examples/graphql/resolvers/packages_resolvers_shared_examples.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'group and projects packages resolver' do + context 'without sort' do + let_it_be(:npm_package) { create(:package, project: project) } + + it { is_expected.to contain_exactly(npm_package) } + end + + context 'with sorting and filtering' do + let_it_be(:conan_package) do + create(:conan_package, name: 'bar', project: project, created_at: 1.day.ago, version: "1.0.0", status: 'default') + end + + let_it_be(:maven_package) do + create(:maven_package, name: 'foo', project: project, created_at: 1.hour.ago, version: "2.0.0", status: 'error') + end + + let_it_be(:repository3) do + create(:maven_package, name: 'baz', project: project, created_at: 1.minute.ago, version: nil) + end + + [:created_desc, :name_desc, :version_desc, :type_asc].each do |order| + context "#{order}" do + let(:args) { { sort: order } } + + it { is_expected.to eq([maven_package, conan_package]) } + end + end + + [:created_asc, :name_asc, :version_asc, :type_desc].each do |order| + context "#{order}" do + let(:args) { { sort: order } } + + it { is_expected.to eq([conan_package, maven_package]) } + end + end + + context 'filter by package_name' do + let(:args) { { package_name: 'bar', sort: :created_desc } } + + it { is_expected.to eq([conan_package]) } + end + + context 'filter by package_type' do + let(:args) { { package_type: 'conan', sort: :created_desc } } + + it { is_expected.to eq([conan_package]) } + end + + context 'filter by status' do + let(:args) { { status: 'error', sort: :created_desc } } + + it { is_expected.to eq([maven_package]) } + end + + context 'include_versionless' do + let(:args) { { include_versionless: true, sort: :created_desc } } + + it { is_expected.to include(repository3) } + end + end +end diff --git a/spec/support/shared_examples/lib/gitlab/diff_file_collections_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/diff_file_collections_shared_examples.rb index c9e03ced0dd..1f7325df11a 100644 --- a/spec/support/shared_examples/lib/gitlab/diff_file_collections_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/diff_file_collections_shared_examples.rb @@ -166,16 +166,6 @@ shared_examples_for 'sortable diff files' do it 'returns sorted diff files' do expect(raw_diff_files_paths).to eq(sorted_diff_files_paths) end - - context 'when sort_diffs feature flag is disabled' do - before do - stub_feature_flags(sort_diffs: false) - end - - it 'returns unsorted diff files' do - expect(raw_diff_files_paths).to eq(unsorted_diff_files_paths) - end - end end end end diff --git a/spec/support/shared_examples/lib/gitlab/jwt_token_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/jwt_token_shared_examples.rb new file mode 100644 index 00000000000..5c92bb3b0d4 --- /dev/null +++ b/spec/support/shared_examples/lib/gitlab/jwt_token_shared_examples.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'a gitlab jwt token' do + let_it_be(:base_secret) { SecureRandom.base64(64) } + + let(:jwt_secret) do + OpenSSL::HMAC.hexdigest( + 'SHA256', + base_secret, + described_class::HMAC_KEY + ) + end + + before do + allow(Settings).to receive(:attr_encrypted_db_key_base).and_return(base_secret) + end + + describe '#secret' do + subject { described_class.secret } + + it { is_expected.to eq(jwt_secret) } + end + + describe '#decode' do + let(:encoded_jwt_token) { jwt_token.encoded } + + subject(:decoded_jwt_token) { described_class.decode(encoded_jwt_token) } + + context 'with a custom payload' do + let(:personal_access_token) { create(:personal_access_token) } + let(:jwt_token) { described_class.new.tap { |jwt_token| jwt_token['token'] = personal_access_token.token } } + + it 'returns the correct token' do + expect(decoded_jwt_token['token']).to eq jwt_token['token'] + end + + it 'returns nil and logs the exception after expiration' do + travel_to((described_class::HMAC_EXPIRES_IN + 1.minute).ago) do + encoded_jwt_token + end + + expect(Gitlab::ErrorTracking).to receive(:track_exception) + .with(instance_of(JWT::ExpiredSignature)) + + expect(decoded_jwt_token).to be_nil + end + end + end +end diff --git a/spec/support/shared_examples/lib/gitlab/sql/set_operator_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/sql/set_operator_shared_examples.rb index aa6a51c3646..8d758ed1655 100644 --- a/spec/support/shared_examples/lib/gitlab/sql/set_operator_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/sql/set_operator_shared_examples.rb @@ -21,7 +21,7 @@ RSpec.shared_examples 'SQL set operator' do |operator_keyword| expect(set_operator.to_sql).to eq("(#{to_sql(relation_1)})\n#{operator_keyword}\n(#{to_sql(relation_2)})") end - it 'skips Model.none segements' do + it 'skips Model.none segments' do empty_relation = User.none set_operator = described_class.new([empty_relation, relation_1, relation_2]) diff --git a/spec/support/shared_examples/models/chat_service_shared_examples.rb b/spec/support/shared_examples/models/chat_service_shared_examples.rb index 59e249bb865..4a47aad0957 100644 --- a/spec/support/shared_examples/models/chat_service_shared_examples.rb +++ b/spec/support/shared_examples/models/chat_service_shared_examples.rb @@ -163,7 +163,7 @@ RSpec.shared_examples "chat service" do |service_name| context "with issue events" do let(:opts) { { title: "Awesome issue", description: "please fix" } } let(:sample_data) do - service = Issues::CreateService.new(project, user, opts) + service = Issues::CreateService.new(project: project, current_user: user, params: opts) issue = service.execute service.hook_data(issue, "open") end @@ -182,7 +182,7 @@ RSpec.shared_examples "chat service" do |service_name| end let(:sample_data) do - service = MergeRequests::CreateService.new(project, user, opts) + service = MergeRequests::CreateService.new(project: project, current_user: user, params: opts) merge_request = service.execute service.hook_data(merge_request, "open") end diff --git a/spec/support/shared_examples/models/chat_slash_commands_shared_examples.rb b/spec/support/shared_examples/models/chat_slash_commands_shared_examples.rb index 0ee24dd93d7..49729afce61 100644 --- a/spec/support/shared_examples/models/chat_slash_commands_shared_examples.rb +++ b/spec/support/shared_examples/models/chat_slash_commands_shared_examples.rb @@ -81,7 +81,7 @@ RSpec.shared_examples 'chat slash commands service' do end context 'when the user is authenticated' do - let!(:chat_name) { create(:chat_name, service: subject) } + let!(:chat_name) { create(:chat_name, integration: subject) } let(:params) { { token: 'token', team_id: chat_name.team_id, user_id: chat_name.chat_id } } subject do diff --git a/spec/support/shared_examples/models/clusters/elastic_stack_client_shared.rb b/spec/support/shared_examples/models/clusters/elastic_stack_client_shared.rb new file mode 100644 index 00000000000..d3ce916cd64 --- /dev/null +++ b/spec/support/shared_examples/models/clusters/elastic_stack_client_shared.rb @@ -0,0 +1,82 @@ +# frozen_string_literal: true + +# Input +# - factory: [:clusters_applications_elastic_stack, :clusters_integrations_elastic_stack] +RSpec.shared_examples 'cluster-based #elasticsearch_client' do |factory| + describe '#elasticsearch_client' do + context 'cluster is nil' do + subject { build(factory, cluster: nil) } + + it 'returns nil' do + expect(subject.cluster).to be_nil + expect(subject.elasticsearch_client).to be_nil + end + end + + context "cluster doesn't have kubeclient" do + let(:cluster) { create(:cluster) } + + subject { create(factory, cluster: cluster) } + + it 'returns nil' do + expect(subject.elasticsearch_client).to be_nil + end + end + + context 'cluster has kubeclient' do + let(:cluster) { create(:cluster, :project, :provided_by_gcp) } + let(:kubernetes_url) { subject.cluster.platform_kubernetes.api_url } + let(:kube_client) { subject.cluster.kubeclient.core_client } + + subject { create(factory, cluster: cluster) } + + before do + subject.cluster.platform_kubernetes.namespace = 'a-namespace' + stub_kubeclient_discover(cluster.platform_kubernetes.api_url) + + create(:cluster_kubernetes_namespace, + cluster: cluster, + cluster_project: cluster.cluster_project, + project: cluster.cluster_project.project) + end + + it 'creates proxy elasticsearch_client' do + expect(subject.elasticsearch_client).to be_instance_of(Elasticsearch::Transport::Client) + end + + it 'copies proxy_url, options and headers from kube client to elasticsearch_client' do + expect(Elasticsearch::Client) + .to(receive(:new)) + .with(url: a_valid_url) + .and_call_original + + client = subject.elasticsearch_client + faraday_connection = client.transport.connections.first.connection + + expect(faraday_connection.headers["Authorization"]).to eq(kube_client.headers[:Authorization]) + expect(faraday_connection.ssl.cert_store).to be_instance_of(OpenSSL::X509::Store) + expect(faraday_connection.ssl.verify).to eq(1) + expect(faraday_connection.options.timeout).to be_nil + end + + context 'when cluster is not reachable' do + before do + allow(kube_client).to receive(:proxy_url).and_raise(Kubeclient::HttpError.new(401, 'Unauthorized', nil)) + end + + it 'returns nil' do + expect(subject.elasticsearch_client).to be_nil + end + end + + context 'when timeout is provided' do + it 'sets timeout in elasticsearch_client' do + client = subject.elasticsearch_client(timeout: 123) + faraday_connection = client.transport.connections.first.connection + + expect(faraday_connection.options.timeout).to eq(123) + end + end + end + end +end diff --git a/spec/support/shared_examples/models/concerns/bulk_insert_safe_shared_examples.rb b/spec/support/shared_examples/models/concerns/bulk_insert_safe_shared_examples.rb index 3db5d7a8d7d..ec9756007f1 100644 --- a/spec/support/shared_examples/models/concerns/bulk_insert_safe_shared_examples.rb +++ b/spec/support/shared_examples/models/concerns/bulk_insert_safe_shared_examples.rb @@ -30,10 +30,6 @@ RSpec.shared_examples 'a BulkInsertSafe model' do |klass| expect { target_class.set_callback(name) {} }.not_to raise_error end end - - it 'does not raise an error when the call is triggered by belongs_to' do - expect { target_class.belongs_to(:other_record) }.not_to raise_error - end end describe '.bulk_insert!' do diff --git a/spec/support/shared_examples/models/concerns/cron_schedulable_shared_examples.rb b/spec/support/shared_examples/models/concerns/cron_schedulable_shared_examples.rb new file mode 100644 index 00000000000..47a02a8fa49 --- /dev/null +++ b/spec/support/shared_examples/models/concerns/cron_schedulable_shared_examples.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'handles set_next_run_at' do + context 'when schedule runs every minute' do + it "updates next_run_at to the worker's execution time" do + travel_to(1.day.ago) do + expect(schedule.next_run_at).to eq(cron_worker_next_run_at) + end + end + end + + context 'when there are two different schedules in the same time zones' do + it 'sets the sames next_run_at' do + expect(schedule_1.next_run_at).to eq(schedule_2.next_run_at) + end + end + + context 'when cron is updated for existing schedules' do + it 'updates next_run_at automatically' do + expect { schedule.update!(cron: new_cron) }.to change { schedule.next_run_at } + end + end +end diff --git a/spec/support/shared_examples/models/concerns/repository_storage_movable_shared_examples.rb b/spec/support/shared_examples/models/concerns/repository_storage_movable_shared_examples.rb index 819cf6018fe..3f1588c46b3 100644 --- a/spec/support/shared_examples/models/concerns/repository_storage_movable_shared_examples.rb +++ b/spec/support/shared_examples/models/concerns/repository_storage_movable_shared_examples.rb @@ -36,7 +36,7 @@ RSpec.shared_examples 'handles repository moves' do container.set_repository_read_only! expect(subject).not_to be_valid - expect(subject.errors[error_key].first).to match(/is read only/) + expect(subject.errors[error_key].first).to match(/is read-only/) end end end 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 17948d648cb..d23f95b2e9e 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 @@ -58,6 +58,19 @@ RSpec.shared_examples 'value stream analytics stage' do it { expect(stage).not_to be_valid } end + + # rubocop: disable Rails/SaveBang + describe '.by_value_stream' do + it 'finds stages by value stream' do + stage1 = create(factory) + create(factory) # other stage with different value stream + + result = described_class.by_value_stream(stage1.value_stream) + + expect(result).to eq([stage1]) + end + end + # rubocop: enable Rails/SaveBang end describe '#subject_class' do diff --git a/spec/support/shared_examples/models/packages/debian/architecture_shared_examples.rb b/spec/support/shared_examples/models/packages/debian/architecture_shared_examples.rb index fbb94b4f5c1..33a04059491 100644 --- a/spec/support/shared_examples/models/packages/debian/architecture_shared_examples.rb +++ b/spec/support/shared_examples/models/packages/debian/architecture_shared_examples.rb @@ -3,8 +3,8 @@ require 'spec_helper' RSpec.shared_examples 'Debian Distribution Architecture' do |factory, container, can_freeze| - let_it_be_with_refind(:architecture) { create(factory) } # rubocop:disable Rails/SaveBang - let_it_be(:architecture_same_distribution, freeze: can_freeze) { create(factory, distribution: architecture.distribution) } + let_it_be_with_refind(:architecture) { create(factory, name: 'name1') } + let_it_be(:architecture_same_distribution, freeze: can_freeze) { create(factory, distribution: architecture.distribution, name: 'name2') } let_it_be(:architecture_same_name, freeze: can_freeze) { create(factory, name: architecture.name) } subject { architecture } @@ -30,20 +30,22 @@ RSpec.shared_examples 'Debian Distribution Architecture' do |factory, container, end describe 'scopes' do + describe '.ordered_by_name' do + subject { described_class.with_distribution(architecture.distribution).ordered_by_name } + + it { expect(subject).to match_array([architecture, architecture_same_distribution]) } + end + describe '.with_distribution' do subject { described_class.with_distribution(architecture.distribution) } - it 'does not return other distributions' do - expect(subject.to_a).to match_array([architecture, architecture_same_distribution]) - end + it { expect(subject).to match_array([architecture, architecture_same_distribution]) } end describe '.with_name' do subject { described_class.with_name(architecture.name) } - it 'does not return other distributions' do - expect(subject.to_a).to match_array([architecture, architecture_same_name]) - end + it { expect(subject).to match_array([architecture, architecture_same_name]) } end end end diff --git a/spec/support/shared_examples/models/packages/debian/component_file_shared_example.rb b/spec/support/shared_examples/models/packages/debian/component_file_shared_example.rb index 02ced49ee94..e6b16d5881d 100644 --- a/spec/support/shared_examples/models/packages/debian/component_file_shared_example.rb +++ b/spec/support/shared_examples/models/packages/debian/component_file_shared_example.rb @@ -114,11 +114,7 @@ RSpec.shared_examples 'Debian Component File' do |container_type, can_freeze| subject { described_class.with_container(container2) } it do - queries = ActiveRecord::QueryRecorder.new do - expect(subject.to_a).to contain_exactly(component_file_other_container) - end - - expect(queries.count).to eq(1) + expect(subject.to_a).to contain_exactly(component_file_other_container) end end @@ -126,11 +122,7 @@ RSpec.shared_examples 'Debian Component File' do |container_type, can_freeze| subject { described_class.with_codename_or_suite(distribution2.codename) } it do - queries = ActiveRecord::QueryRecorder.new do - expect(subject.to_a).to contain_exactly(component_file_other_container) - end - - expect(queries.count).to eq(1) + expect(subject.to_a).to contain_exactly(component_file_other_container) end end @@ -138,11 +130,7 @@ RSpec.shared_examples 'Debian Component File' do |container_type, can_freeze| subject { described_class.with_component_name(component1_2.name) } it do - queries = ActiveRecord::QueryRecorder.new do - expect(subject.to_a).to contain_exactly(component_file_other_component) - end - - expect(queries.count).to eq(1) + expect(subject.to_a).to contain_exactly(component_file_other_component) end end @@ -150,14 +138,7 @@ RSpec.shared_examples 'Debian Component File' do |container_type, can_freeze| subject { described_class.with_file_type(:source) } it do - # let_it_be_with_refind triggers a query - component_file_with_file_type_source - - queries = ActiveRecord::QueryRecorder.new do - expect(subject.to_a).to contain_exactly(component_file_with_file_type_source) - end - - expect(queries.count).to eq(1) + expect(subject.to_a).to contain_exactly(component_file_with_file_type_source) end end @@ -165,11 +146,7 @@ RSpec.shared_examples 'Debian Component File' do |container_type, can_freeze| subject { described_class.with_architecture_name(architecture1_2.name) } it do - queries = ActiveRecord::QueryRecorder.new do - expect(subject.to_a).to contain_exactly(component_file_other_architecture) - end - - expect(queries.count).to eq(1) + expect(subject.to_a).to contain_exactly(component_file_other_architecture) end end @@ -177,11 +154,7 @@ RSpec.shared_examples 'Debian Component File' do |container_type, can_freeze| subject { described_class.with_compression_type(:xz) } it do - queries = ActiveRecord::QueryRecorder.new do - expect(subject.to_a).to contain_exactly(component_file_other_compression_type) - end - - expect(queries.count).to eq(1) + expect(subject.to_a).to contain_exactly(component_file_other_compression_type) end end @@ -189,11 +162,19 @@ RSpec.shared_examples 'Debian Component File' do |container_type, can_freeze| subject { described_class.with_file_sha256('other_sha256') } it do - queries = ActiveRecord::QueryRecorder.new do - expect(subject.to_a).to contain_exactly(component_file_other_file_sha256) - end + expect(subject.to_a).to contain_exactly(component_file_other_file_sha256) + end + end + + describe '.created_before' do + let_it_be(:component_file1) { create("debian_#{container_type}_component_file", component: component1_1, architecture: architecture1_1, created_at: 4.hours.ago) } + let_it_be(:component_file2) { create("debian_#{container_type}_component_file", component: component1_1, architecture: architecture1_1, created_at: 3.hours.ago) } + let_it_be(:component_file3) { create("debian_#{container_type}_component_file", component: component1_1, architecture: architecture1_1, created_at: 1.hour.ago) } - expect(queries.count).to eq(1) + subject { described_class.created_before(2.hours.ago) } + + it do + expect(subject.to_a).to contain_exactly(component_file1, component_file2) end end end diff --git a/spec/support/shared_examples/models/packages/debian/component_shared_examples.rb b/spec/support/shared_examples/models/packages/debian/component_shared_examples.rb index 23e76d32fb0..635d45f40e5 100644 --- a/spec/support/shared_examples/models/packages/debian/component_shared_examples.rb +++ b/spec/support/shared_examples/models/packages/debian/component_shared_examples.rb @@ -3,8 +3,8 @@ require 'spec_helper' RSpec.shared_examples 'Debian Distribution Component' do |factory, container, can_freeze| - let_it_be_with_refind(:component) { create(factory) } # rubocop:disable Rails/SaveBang - let_it_be(:component_same_distribution, freeze: can_freeze) { create(factory, distribution: component.distribution) } + let_it_be_with_refind(:component) { create(factory, name: 'name1') } + let_it_be(:component_same_distribution, freeze: can_freeze) { create(factory, distribution: component.distribution, name: 'name2') } let_it_be(:component_same_name, freeze: can_freeze) { create(factory, name: component.name) } subject { component } @@ -32,6 +32,14 @@ RSpec.shared_examples 'Debian Distribution Component' do |factory, container, ca end describe 'scopes' do + describe '.ordered_by_name' do + subject { described_class.with_distribution(component.distribution).ordered_by_name } + + it 'sorts by name' do + expect(subject.to_a).to eq([component, component_same_distribution]) + end + end + describe '.with_distribution' do subject { described_class.with_distribution(component.distribution) } diff --git a/spec/support/shared_examples/models/packages/debian/distribution_shared_examples.rb b/spec/support/shared_examples/models/packages/debian/distribution_shared_examples.rb index 9eacacf725f..8693d6868e9 100644 --- a/spec/support/shared_examples/models/packages/debian/distribution_shared_examples.rb +++ b/spec/support/shared_examples/models/packages/debian/distribution_shared_examples.rb @@ -19,11 +19,6 @@ RSpec.shared_examples 'Debian Distribution' do |factory, container, can_freeze| it { is_expected.to have_many(:components).class_name("Packages::Debian::#{container.capitalize}Component").inverse_of(:distribution) } it { is_expected.to have_many(:architectures).class_name("Packages::Debian::#{container.capitalize}Architecture").inverse_of(:distribution) } - - if container != :group - it { is_expected.to have_many(:publications).class_name('Packages::Debian::Publication').inverse_of(:distribution).with_foreign_key(:distribution_id) } - it { is_expected.to have_many(:packages).class_name('Packages::Package').through(:publications) } - end end describe 'validations' do @@ -228,4 +223,44 @@ RSpec.shared_examples 'Debian Distribution' do |factory, container, can_freeze| end end end + + if container == :project + describe 'project distribution specifics' do + describe 'relationships' do + it { is_expected.to have_many(:publications).class_name('Packages::Debian::Publication').inverse_of(:distribution).with_foreign_key(:distribution_id) } + it { is_expected.to have_many(:packages).class_name('Packages::Package').through(:publications) } + it { is_expected.to have_many(:package_files).class_name('Packages::PackageFile').through(:packages) } + end + end + else + describe 'group distribution specifics' do + let_it_be(:public_project) { create(:project, :public, group: distribution_with_suite.container)} + let_it_be(:public_distribution_with_same_codename) { create(:debian_project_distribution, container: public_project, codename: distribution_with_suite.codename) } + let_it_be(:public_package_with_same_codename) { create(:debian_package, project: public_project, published_in: public_distribution_with_same_codename)} + let_it_be(:public_distribution_with_same_suite) { create(:debian_project_distribution, container: public_project, suite: distribution_with_suite.suite) } + let_it_be(:public_package_with_same_suite) { create(:debian_package, project: public_project, published_in: public_distribution_with_same_suite)} + + let_it_be(:private_project) { create(:project, :private, group: distribution_with_suite.container)} + let_it_be(:private_distribution_with_same_codename) { create(:debian_project_distribution, container: private_project, codename: distribution_with_suite.codename) } + let_it_be(:private_package_with_same_codename) { create(:debian_package, project: private_project, published_in: private_distribution_with_same_codename)} + let_it_be(:private_distribution_with_same_suite) { create(:debian_project_distribution, container: private_project, suite: distribution_with_suite.suite) } + let_it_be(:private_package_with_same_suite) { create(:debian_package, project: private_project, published_in: private_distribution_with_same_codename)} + + describe '#packages' do + subject { distribution_with_suite.packages } + + it 'returns only public packages with same codename' do + expect(subject.to_a).to contain_exactly(public_package_with_same_codename) + end + end + + describe '#package_files' do + subject { distribution_with_suite.package_files } + + it 'returns only files from public packages with same codename' do + expect(subject.to_a).to contain_exactly(*public_package_with_same_codename.package_files) + end + end + end + end end diff --git a/spec/support/shared_examples/models/slack_mattermost_notifications_shared_examples.rb b/spec/support/shared_examples/models/slack_mattermost_notifications_shared_examples.rb index 71a76121d38..09b7d1be704 100644 --- a/spec/support/shared_examples/models/slack_mattermost_notifications_shared_examples.rb +++ b/spec/support/shared_examples/models/slack_mattermost_notifications_shared_examples.rb @@ -201,7 +201,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do |service_name| context 'deployment events' do let_it_be(:deployment) { create(:deployment) } - let(:data) { Gitlab::DataBuilder::Deployment.build(deployment) } + let(:data) { Gitlab::DataBuilder::Deployment.build(deployment, Time.current) } it_behaves_like 'calls the service API with the event message', /Deploy to (.*?) created/ end diff --git a/spec/support/shared_examples/models/wiki_shared_examples.rb b/spec/support/shared_examples/models/wiki_shared_examples.rb index 6b243aef3e6..2498bf35a09 100644 --- a/spec/support/shared_examples/models/wiki_shared_examples.rb +++ b/spec/support/shared_examples/models/wiki_shared_examples.rb @@ -469,34 +469,30 @@ RSpec.shared_examples 'wiki model' do end describe '#delete_page' do - shared_examples 'delete_page operations' do - let(:page) { create(:wiki_page, wiki: wiki) } + let(:page) { create(:wiki_page, wiki: wiki) } - it 'deletes the page' do - subject.delete_page(page) + it 'deletes the page' do + subject.delete_page(page) - expect(subject.list_pages.count).to eq(0) - end + expect(subject.list_pages.count).to eq(0) + end - it 'sets the correct commit email' do - subject.delete_page(page) + it 'sets the correct commit email' do + subject.delete_page(page) - expect(user.commit_email).not_to eq(user.email) - expect(commit.author_email).to eq(user.commit_email) - expect(commit.committer_email).to eq(user.commit_email) - end + expect(user.commit_email).not_to eq(user.email) + expect(commit.author_email).to eq(user.commit_email) + expect(commit.committer_email).to eq(user.commit_email) + end - it 'runs after_wiki_activity callbacks' do - page + it 'runs after_wiki_activity callbacks' do + page - expect(subject).to receive(:after_wiki_activity) + expect(subject).to receive(:after_wiki_activity) - subject.delete_page(page) - end + subject.delete_page(page) end - it_behaves_like 'delete_page operations' - context 'when an error is raised' do it 'logs the error and returns false' do page = build(:wiki_page, wiki: wiki) @@ -509,14 +505,6 @@ RSpec.shared_examples 'wiki model' do expect(subject.delete_page(page)).to be_falsey end end - - context 'when feature flag :gitaly_replace_wiki_delete_page is disabled' do - before do - stub_feature_flags(gitaly_replace_wiki_delete_page: false) - end - - it_behaves_like 'delete_page operations' - end end describe '#ensure_repository' do diff --git a/spec/support/shared_examples/namespaces/namespace_traversal_examples.rb b/spec/support/shared_examples/namespaces/traversal_examples.rb index 36e5808fa28..77a1705627e 100644 --- a/spec/support/shared_examples/namespaces/namespace_traversal_examples.rb +++ b/spec/support/shared_examples/namespaces/traversal_examples.rb @@ -39,16 +39,17 @@ RSpec.shared_examples 'namespace traversal' do end describe '#ancestors' do - let(:group) { create(:group) } - let(:nested_group) { create(:group, parent: group) } - let(:deep_nested_group) { create(:group, parent: nested_group) } - let(:very_deep_nested_group) { create(:group, parent: deep_nested_group) } + let_it_be(:group) { create(:group) } + let_it_be(:nested_group) { create(:group, parent: group) } + let_it_be(:deep_nested_group) { create(:group, parent: nested_group) } + let_it_be(:very_deep_nested_group) { create(:group, parent: deep_nested_group) } it 'returns the correct ancestors' do - expect(very_deep_nested_group.ancestors).to include(group, nested_group, deep_nested_group) - expect(deep_nested_group.ancestors).to include(group, nested_group) - expect(nested_group.ancestors).to include(group) - expect(group.ancestors).to eq([]) + # #reload is called to make sure traversal_ids are reloaded + expect(very_deep_nested_group.reload.ancestors).to contain_exactly(group, nested_group, deep_nested_group) + expect(deep_nested_group.reload.ancestors).to contain_exactly(group, nested_group) + expect(nested_group.reload.ancestors).to contain_exactly(group) + expect(group.reload.ancestors).to eq([]) end describe '#recursive_ancestors' do diff --git a/spec/support/shared_examples/policies/project_policy_shared_examples.rb b/spec/support/shared_examples/policies/project_policy_shared_examples.rb index d05e5eb9120..013c9b61b99 100644 --- a/spec/support/shared_examples/policies/project_policy_shared_examples.rb +++ b/spec/support/shared_examples/policies/project_policy_shared_examples.rb @@ -57,7 +57,7 @@ RSpec.shared_examples 'project policies as anonymous' do context 'when a project has pending invites' do let(:group) { create(:group, :public) } let(:project) { create(:project, :public, namespace: group) } - let(:user_permissions) { [:create_merge_request_in, :create_project, :create_issue, :create_note, :upload_file, :award_emoji] } + let(:user_permissions) { [:create_merge_request_in, :create_project, :create_issue, :create_note, :upload_file, :award_emoji, :create_incident] } let(:anonymous_permissions) { guest_permissions - user_permissions } let(:current_user) { anonymous } diff --git a/spec/support/shared_examples/requests/api/conan_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/conan_packages_shared_examples.rb index 87aaac673c1..c938c6432fe 100644 --- a/spec/support/shared_examples/requests/api/conan_packages_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/conan_packages_shared_examples.rb @@ -106,7 +106,7 @@ RSpec.shared_examples 'conan authenticate endpoint' do expect(payload['user_id']).to eq(personal_access_token.user_id) duration = payload['exp'] - payload['iat'] - expect(duration).to eq(1.hour) + expect(duration).to eq(::Gitlab::ConanToken::CONAN_TOKEN_EXPIRE_TIME) end end end @@ -661,7 +661,7 @@ RSpec.shared_examples 'workhorse package file upload endpoint' do end RSpec.shared_examples 'creates build_info when there is a job' do - context 'with job token' do + context 'with job token', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/294047' do let(:jwt) { build_jwt_from_job(job) } it 'creates a build_info record' do 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 acaa0d8c2bc..dfd19167dcd 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 @@ -1,20 +1,16 @@ # frozen_string_literal: true -RSpec.shared_context 'Debian repository shared context' do |object_type| +RSpec.shared_context 'Debian repository shared context' do |container_type, can_freeze| include_context 'workhorse headers' before do stub_feature_flags(debian_packages: true) end - if object_type == :project - let(:project) { create(:project, :public) } - elsif object_type == :group - let(:group) { create(:group, :public) } - end - - let(:user) { create(:user) } - let(:personal_access_token) { create(:personal_access_token, user: user) } + let_it_be(:private_container, freeze: can_freeze) { create(container_type, :private) } + let_it_be(:public_container, freeze: can_freeze) { create(container_type, :public) } + let_it_be(:user, freeze: true) { create(:user) } + let_it_be(:personal_access_token, freeze: true) { create(:personal_access_token, user: user) } let(:distribution) { 'bullseye' } let(:component) { 'main' } @@ -36,7 +32,7 @@ RSpec.shared_context 'Debian repository shared context' do |object_type| end end - let(:params) { workhorse_params } + let(:api_params) { workhorse_params } let(:auth_headers) { {} } let(:wh_headers) do @@ -57,12 +53,12 @@ RSpec.shared_context 'Debian repository shared context' do |object_type| api(url), method: method, file_key: :file, - params: params, + params: api_params, headers: headers, send_rewritten_field: send_rewritten_field ) else - send method, api(url), headers: headers, params: params + send method, api(url), headers: headers, params: api_params end end end @@ -81,289 +77,190 @@ RSpec.shared_context 'Debian repository auth headers' do |user_role, user_token, end end -RSpec.shared_context 'Debian repository project access' do |project_visibility_level, user_role, user_token, auth_method| +RSpec.shared_context 'Debian repository access' do |visibility_level, user_role, add_member, user_token, auth_method| include_context 'Debian repository auth headers', user_role, user_token, auth_method do + let(:containers) { { private: private_container, public: public_container } } + let(:container) { containers[visibility_level] } + before do - project.update_column(:visibility_level, Gitlab::VisibilityLevel.const_get(project_visibility_level, false)) + container.send("add_#{user_role}", user) if add_member && user_role != :anonymous end end end -RSpec.shared_examples 'Debian project repository GET request' do |user_role, add_member, status, body| - context "for user type #{user_role}" do - before do - project.send("add_#{user_role}", user) if add_member && user_role != :anonymous - end +RSpec.shared_examples 'Debian repository GET request' do |status, body = nil| + and_body = body.nil? ? '' : ' and expected body' - and_body = body.nil? ? '' : ' and expected body' + it "returns #{status}#{and_body}" do + subject - it "returns #{status}#{and_body}" do - subject + expect(response).to have_gitlab_http_status(status) - expect(response).to have_gitlab_http_status(status) - - unless body.nil? - expect(response.body).to eq(body) - end + unless body.nil? + expect(response.body).to eq(body) end end end -RSpec.shared_examples 'Debian project repository PUT request' do |user_role, add_member, status, body| - context "for user type #{user_role}" do - before do - project.send("add_#{user_role}", user) if add_member && user_role != :anonymous - end +RSpec.shared_examples 'Debian repository upload request' do |status, body = nil| + and_body = body.nil? ? '' : ' and expected body' - and_body = body.nil? ? '' : ' and expected body' + if status == :created + it 'creates package files', :aggregate_failures do + pending "Debian package creation not implemented" - if status == :created - it 'creates package files', :aggregate_failures do - pending "Debian package creation not implemented" - expect { subject } - .to change { project.packages.debian.count }.by(1) + expect { subject } + .to change { container.packages.debian.count }.by(1) - expect(response).to have_gitlab_http_status(status) - expect(response.media_type).to eq('text/plain') + expect(response).to have_gitlab_http_status(status) + expect(response.media_type).to eq('text/plain') - unless body.nil? - expect(response.body).to eq(body) - end + unless body.nil? + expect(response.body).to eq(body) end - it_behaves_like 'a package tracking event', described_class.name, 'push_package' - else - it "returns #{status}#{and_body}", :aggregate_failures do - subject + end + it_behaves_like 'a package tracking event', described_class.name, 'push_package' + else + it "returns #{status}#{and_body}", :aggregate_failures do + subject - expect(response).to have_gitlab_http_status(status) + expect(response).to have_gitlab_http_status(status) - unless body.nil? - expect(response.body).to eq(body) - end + unless body.nil? + expect(response.body).to eq(body) end end end end -RSpec.shared_examples 'Debian project repository PUT authorize request' do |user_role, add_member, status, body, is_authorize| - context "for user type #{user_role}" do - before do - project.send("add_#{user_role}", user) if add_member && user_role != :anonymous - end - - and_body = body.nil? ? '' : ' and expected body' +RSpec.shared_examples 'Debian repository upload authorize request' do |status, body = nil| + and_body = body.nil? ? '' : ' and expected body' - if status == :created - it 'authorizes package file upload', :aggregate_failures do - subject + if status == :created + it 'authorizes package file upload', :aggregate_failures do + subject - expect(response).to have_gitlab_http_status(:ok) - expect(response.media_type).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE) - expect(json_response['TempPath']).to eq(Packages::PackageFileUploader.workhorse_local_upload_path) - expect(json_response['RemoteObject']).to be_nil - expect(json_response['MaximumSize']).to be_nil - end + expect(response).to have_gitlab_http_status(:ok) + expect(response.media_type).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE) + expect(json_response['TempPath']).to eq(Packages::PackageFileUploader.workhorse_local_upload_path) + expect(json_response['RemoteObject']).to be_nil + expect(json_response['MaximumSize']).to be_nil + end - context 'without a valid token' do - let(:workhorse_token) { 'invalid' } + context 'without a valid token' do + let(:workhorse_token) { 'invalid' } - it 'rejects request' do - subject + it 'rejects request' do + subject - expect(response).to have_gitlab_http_status(:forbidden) - end + expect(response).to have_gitlab_http_status(:forbidden) end + end - context 'bypassing gitlab-workhorse' do - let(:workhorse_headers) { {} } + context 'bypassing gitlab-workhorse' do + let(:workhorse_headers) { {} } - it 'rejects request' do - subject + it 'rejects request' do + subject - expect(response).to have_gitlab_http_status(:forbidden) - end + expect(response).to have_gitlab_http_status(:forbidden) end - else - it "returns #{status}#{and_body}", :aggregate_failures do - subject + end + else + it "returns #{status}#{and_body}", :aggregate_failures do + subject - expect(response).to have_gitlab_http_status(status) + expect(response).to have_gitlab_http_status(status) - unless body.nil? - expect(response.body).to eq(body) - end + unless body.nil? + expect(response.body).to eq(body) end end end end -RSpec.shared_examples 'rejects Debian access with unknown project id' do - context 'with an unknown project' do - let(:project) { double(id: non_existing_record_id) } +RSpec.shared_examples 'rejects Debian access with unknown container id' do + context 'with an unknown container' do + let(:container) { double(id: non_existing_record_id) } context 'as anonymous' do - it_behaves_like 'Debian project repository GET request', :anonymous, true, :unauthorized, nil + it_behaves_like 'Debian repository GET request', :unauthorized, nil end context 'as authenticated user' do subject { get api(url), headers: basic_auth_header(user.username, personal_access_token.token) } - it_behaves_like 'Debian project repository GET request', :anonymous, true, :not_found, nil + it_behaves_like 'Debian repository GET request', :not_found, nil end end end -RSpec.shared_examples 'Debian project repository GET endpoint' do |success_status, success_body| - context 'with valid project' do +RSpec.shared_examples 'Debian repository read endpoint' do |desired_behavior, success_status, success_body| + context 'with valid container' do using RSpec::Parameterized::TableSyntax - where(:project_visibility_level, :user_role, :member, :user_token, :expected_status, :expected_body) do - 'PUBLIC' | :developer | true | true | success_status | success_body - 'PUBLIC' | :guest | true | true | success_status | success_body - 'PUBLIC' | :developer | true | false | success_status | success_body - 'PUBLIC' | :guest | true | false | success_status | success_body - 'PUBLIC' | :developer | false | true | success_status | success_body - 'PUBLIC' | :guest | false | true | success_status | success_body - 'PUBLIC' | :developer | false | false | success_status | success_body - 'PUBLIC' | :guest | false | false | success_status | success_body - 'PUBLIC' | :anonymous | false | true | success_status | success_body - 'PRIVATE' | :developer | true | true | success_status | success_body - 'PRIVATE' | :guest | true | true | :forbidden | nil - 'PRIVATE' | :developer | true | false | :unauthorized | nil - 'PRIVATE' | :guest | true | false | :unauthorized | nil - 'PRIVATE' | :developer | false | true | :not_found | nil - 'PRIVATE' | :guest | false | true | :not_found | nil - 'PRIVATE' | :developer | false | false | :unauthorized | nil - 'PRIVATE' | :guest | false | false | :unauthorized | nil - 'PRIVATE' | :anonymous | false | true | :unauthorized | nil + where(:visibility_level, :user_role, :member, :user_token, :expected_status, :expected_body) do + :public | :developer | true | true | success_status | success_body + :public | :guest | true | true | success_status | success_body + :public | :developer | true | false | success_status | success_body + :public | :guest | true | false | success_status | success_body + :public | :developer | false | true | success_status | success_body + :public | :guest | false | true | success_status | success_body + :public | :developer | false | false | success_status | success_body + :public | :guest | false | false | success_status | success_body + :public | :anonymous | false | true | success_status | success_body + :private | :developer | true | true | success_status | success_body + :private | :guest | true | true | :forbidden | nil + :private | :developer | true | false | :unauthorized | nil + :private | :guest | true | false | :unauthorized | nil + :private | :developer | false | true | :not_found | nil + :private | :guest | false | true | :not_found | nil + :private | :developer | false | false | :unauthorized | nil + :private | :guest | false | false | :unauthorized | nil + :private | :anonymous | false | true | :unauthorized | nil end with_them do - include_context 'Debian repository project access', params[:project_visibility_level], params[:user_role], params[:user_token], :basic do - it_behaves_like 'Debian project repository GET request', params[:user_role], params[:member], params[:expected_status], params[:expected_body] + include_context 'Debian repository access', params[:visibility_level], params[:user_role], params[:member], params[:user_token], :basic do + it_behaves_like "Debian repository #{desired_behavior}", params[:expected_status], params[:expected_body] end end end - it_behaves_like 'rejects Debian access with unknown project id' -end - -RSpec.shared_examples 'Debian project repository PUT endpoint' do |success_status, success_body, is_authorize = false| - context 'with valid project' do - using RSpec::Parameterized::TableSyntax - - where(:project_visibility_level, :user_role, :member, :user_token, :expected_status, :expected_body) do - 'PUBLIC' | :developer | true | true | success_status | nil - 'PUBLIC' | :guest | true | true | :forbidden | nil - 'PUBLIC' | :developer | true | false | :unauthorized | nil - 'PUBLIC' | :guest | true | false | :unauthorized | nil - 'PUBLIC' | :developer | false | true | :forbidden | nil - 'PUBLIC' | :guest | false | true | :forbidden | nil - 'PUBLIC' | :developer | false | false | :unauthorized | nil - 'PUBLIC' | :guest | false | false | :unauthorized | nil - 'PUBLIC' | :anonymous | false | true | :unauthorized | nil - 'PRIVATE' | :developer | true | true | success_status | nil - 'PRIVATE' | :guest | true | true | :forbidden | nil - 'PRIVATE' | :developer | true | false | :unauthorized | nil - 'PRIVATE' | :guest | true | false | :unauthorized | nil - 'PRIVATE' | :developer | false | true | :not_found | nil - 'PRIVATE' | :guest | false | true | :not_found | nil - 'PRIVATE' | :developer | false | false | :unauthorized | nil - 'PRIVATE' | :guest | false | false | :unauthorized | nil - 'PRIVATE' | :anonymous | false | true | :unauthorized | nil - end - - with_them do - include_context 'Debian repository project access', params[:project_visibility_level], params[:user_role], params[:user_token], :basic do - desired_behavior = if is_authorize - 'Debian project repository PUT authorize request' - else - 'Debian project repository PUT request' - end - - it_behaves_like desired_behavior, params[:user_role], params[:member], params[:expected_status], params[:expected_body] - end - end - end - - it_behaves_like 'rejects Debian access with unknown project id' -end - -RSpec.shared_context 'Debian repository group access' do |group_visibility_level, user_role, user_token, auth_method| - include_context 'Debian repository auth headers', user_role, user_token, auth_method do - before do - group.update_column(:visibility_level, Gitlab::VisibilityLevel.const_get(group_visibility_level, false)) - end - end -end - -RSpec.shared_examples 'Debian group repository GET request' do |user_role, add_member, status, body| - context "for user type #{user_role}" do - before do - group.send("add_#{user_role}", user) if add_member && user_role != :anonymous - end - - and_body = body.nil? ? '' : ' and expected body' - - it "returns #{status}#{and_body}" do - subject - - expect(response).to have_gitlab_http_status(status) - - unless body.nil? - expect(response.body).to eq(body) - end - end - end -end - -RSpec.shared_examples 'rejects Debian access with unknown group id' do - context 'with an unknown group' do - let(:group) { double(id: non_existing_record_id) } - - context 'as anonymous' do - it_behaves_like 'Debian group repository GET request', :anonymous, true, :unauthorized, nil - end - - context 'as authenticated user' do - subject { get api(url), headers: basic_auth_header(user.username, personal_access_token.token) } - - it_behaves_like 'Debian group repository GET request', :anonymous, true, :not_found, nil - end - end + it_behaves_like 'rejects Debian access with unknown container id' end -RSpec.shared_examples 'Debian group repository GET endpoint' do |success_status, success_body| - context 'with valid group' do +RSpec.shared_examples 'Debian repository write endpoint' do |desired_behavior, success_status, success_body| + context 'with valid container' do using RSpec::Parameterized::TableSyntax - where(:group_visibility_level, :user_role, :member, :user_token, :expected_status, :expected_body) do - 'PUBLIC' | :developer | true | true | success_status | success_body - 'PUBLIC' | :guest | true | true | success_status | success_body - 'PUBLIC' | :developer | true | false | success_status | success_body - 'PUBLIC' | :guest | true | false | success_status | success_body - 'PUBLIC' | :developer | false | true | success_status | success_body - 'PUBLIC' | :guest | false | true | success_status | success_body - 'PUBLIC' | :developer | false | false | success_status | success_body - 'PUBLIC' | :guest | false | false | success_status | success_body - 'PUBLIC' | :anonymous | false | true | success_status | success_body - 'PRIVATE' | :developer | true | true | success_status | success_body - 'PRIVATE' | :guest | true | true | :forbidden | nil - 'PRIVATE' | :developer | true | false | :unauthorized | nil - 'PRIVATE' | :guest | true | false | :unauthorized | nil - 'PRIVATE' | :developer | false | true | :not_found | nil - 'PRIVATE' | :guest | false | true | :not_found | nil - 'PRIVATE' | :developer | false | false | :unauthorized | nil - 'PRIVATE' | :guest | false | false | :unauthorized | nil - 'PRIVATE' | :anonymous | false | true | :unauthorized | nil + where(:visibility_level, :user_role, :member, :user_token, :expected_status, :expected_body) do + :public | :developer | true | true | success_status | success_body + :public | :guest | true | true | :forbidden | nil + :public | :developer | true | false | :unauthorized | nil + :public | :guest | true | false | :unauthorized | nil + :public | :developer | false | true | :forbidden | nil + :public | :guest | false | true | :forbidden | nil + :public | :developer | false | false | :unauthorized | nil + :public | :guest | false | false | :unauthorized | nil + :public | :anonymous | false | true | :unauthorized | nil + :private | :developer | true | true | success_status | success_body + :private | :guest | true | true | :forbidden | nil + :private | :developer | true | false | :unauthorized | nil + :private | :guest | true | false | :unauthorized | nil + :private | :developer | false | true | :not_found | nil + :private | :guest | false | true | :not_found | nil + :private | :developer | false | false | :unauthorized | nil + :private | :guest | false | false | :unauthorized | nil + :private | :anonymous | false | true | :unauthorized | nil end with_them do - include_context 'Debian repository group access', params[:group_visibility_level], params[:user_role], params[:user_token], :basic do - it_behaves_like 'Debian group repository GET request', params[:user_role], params[:member], params[:expected_status], params[:expected_body] + include_context 'Debian repository access', params[:visibility_level], params[:user_role], params[:member], params[:user_token], :basic do + it_behaves_like "Debian repository #{desired_behavior}", params[:expected_status], params[:expected_body] end end end - it_behaves_like 'rejects Debian access with unknown group id' + it_behaves_like 'rejects Debian access with unknown container id' end diff --git a/spec/support/shared_examples/requests/api/graphql/mutations/boards/update_list_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/mutations/boards/update_list_shared_examples.rb new file mode 100644 index 00000000000..9b55b0f061f --- /dev/null +++ b/spec/support/shared_examples/requests/api/graphql/mutations/boards/update_list_shared_examples.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'a GraphQL request to update board list' do + context 'the user is not allowed to read board lists' do + it_behaves_like 'a mutation that returns a top-level access error' + end + + before do + list.update_preferences_for(current_user, collapsed: false) + end + + context 'when user has permissions to admin board lists' do + before do + group.add_reporter(current_user) + end + + it 'updates the list position and collapsed state' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(response).to have_gitlab_http_status(:success) + expect(mutation_response['list']).to include( + 'position' => 1, + 'collapsed' => true + ) + end + end + + context 'when user has permissions to read board lists' do + before do + group.add_guest(current_user) + end + + it 'updates the list collapsed state but not the list position' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(response).to have_gitlab_http_status(:success) + expect(mutation_response['list']).to include( + 'position' => 0, + 'collapsed' => true + ) + end + end +end diff --git a/spec/support/shared_examples/requests/api/graphql/mutations/destroy_list_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/mutations/destroy_list_shared_examples.rb new file mode 100644 index 00000000000..0cec67ff541 --- /dev/null +++ b/spec/support/shared_examples/requests/api/graphql/mutations/destroy_list_shared_examples.rb @@ -0,0 +1,87 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.shared_examples 'board lists destroy request' do + include GraphqlHelpers + + subject { post_graphql_mutation(mutation, current_user: current_user) } + + shared_examples 'does not destroy the list and returns an error' do + it 'does not destroy the list' do + expect { subject }.not_to change { klass.count } + end + + it 'returns an error and not nil list' do + subject + + expect(mutation_response['errors']).not_to be_empty + expect(mutation_response['list']).not_to be_nil + end + end + + context 'when the user does not have permission' do + it 'does not destroy the list' do + expect { subject }.not_to change { klass.count } + end + + it 'returns an error' do + subject + + expect(graphql_errors.first['message']).to include("The resource that you are attempting to access does not exist or you don't have permission to perform this action") + end + end + + context 'when the user has permission' do + before do + group.add_maintainer(current_user) + end + + context 'when given id is not for a list' do + # could be any non-list thing + let_it_be(:list) { group } + + it 'returns an error' do + subject + + expect(graphql_errors.first['message']).to include('does not represent an instance of') + end + end + + context 'when list does not exist' do + let(:variables) do + { + list_id: "gid://gitlab/#{klass}/#{non_existing_record_id}" + } + end + + it 'returns a top level error' do + subject + + expect(graphql_errors.first['message']).to include('No object found for') + end + end + + context 'when everything is ok' do + it 'destroys the list' do + expect { subject }.to change { klass.count }.by(-1) + end + + it 'returns an empty list' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(mutation_response).to have_key('list') + expect(mutation_response['list']).to be_nil + expect(mutation_response['errors']).to be_empty + end + end + + context 'when the list is not destroyable' do + before do + list.update!(list_type: :backlog) + end + + it_behaves_like 'does not destroy the list and returns an error' + end + end +end diff --git a/spec/support/shared_examples/requests/api/graphql/packages/group_and_project_packages_list_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/packages/group_and_project_packages_list_shared_examples.rb index 66fbfa798b0..af4c9286e7c 100644 --- a/spec/support/shared_examples/requests/api/graphql/packages/group_and_project_packages_list_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/graphql/packages/group_and_project_packages_list_shared_examples.rb @@ -3,6 +3,38 @@ RSpec.shared_examples 'group and project packages query' do include GraphqlHelpers + let_it_be(:versionaless_package) { create(:maven_package, project: project1, version: nil) } + let_it_be(:maven_package) { create(:maven_package, project: project1, name: 'tab', version: '4.0.0', created_at: 5.days.ago) } + let_it_be(:package) { create(:npm_package, project: project1, name: 'uab', version: '5.0.0', created_at: 4.days.ago) } + let_it_be(:composer_package) { create(:composer_package, project: project2, name: 'vab', version: '6.0.0', created_at: 3.days.ago) } + let_it_be(:debian_package) { create(:debian_package, project: project2, name: 'zab', version: '7.0.0', created_at: 2.days.ago) } + let_it_be(:composer_metadatum) do + create(:composer_metadatum, package: composer_package, + target_sha: 'afdeh', + composer_json: { name: 'x', type: 'y', license: 'z', version: 1 }) + end + + let(:package_names) { graphql_data_at(resource_type, :packages, :nodes, :name) } + let(:target_shas) { graphql_data_at(resource_type, :packages, :nodes, :metadata, :target_sha) } + let(:packages) { graphql_data_at(resource_type, :packages, :nodes) } + + let(:fields) do + <<~QUERY + nodes { + #{all_graphql_fields_for('packages'.classify, excluded: ['project'])} + metadata { #{query_graphql_fragment('ComposerMetadata')} } + } + QUERY + end + + let(:query) do + graphql_query_for( + resource_type, + { 'fullPath' => resource.full_path }, + query_graphql_field('packages', {}, fields) + ) + end + context 'when user has access to the resource' do before do resource.add_reporter(current_user) @@ -48,4 +80,101 @@ RSpec.shared_examples 'group and project packages query' do expect(packages).to be_nil end end + + describe 'sorting and pagination' do + let_it_be(:ascending_packages) { [maven_package, package, composer_package, debian_package].map { |package| global_id_of(package)} } + + let(:data_path) { [resource_type, :packages] } + + before do + resource.add_reporter(current_user) + end + + [:CREATED_ASC, :NAME_ASC, :VERSION_ASC, :TYPE_ASC].each do |order| + context "#{order}" do + it_behaves_like 'sorted paginated query' do + let(:sort_param) { order } + let(:first_param) { 4 } + let(:expected_results) { ascending_packages } + end + end + end + + [:CREATED_DESC, :NAME_DESC, :VERSION_DESC, :TYPE_DESC].each do |order| + context "#{order}" do + it_behaves_like 'sorted paginated query' do + let(:sort_param) { order } + let(:first_param) { 4 } + let(:expected_results) { ascending_packages.reverse } + end + end + end + + context 'with an invalid sort' do + let(:query) do + graphql_query_for( + resource_type, + { 'fullPath' => resource.full_path }, + query_nodes(:packages, :name, args: { sort: :WRONG_ORDER }) + ) + end + + before do + post_graphql(query, current_user: current_user) + end + + it 'throws an error' do + expect_graphql_errors_to_include(/Argument \'sort\' on Field \'packages\' has an invalid value/) + end + end + + def pagination_query(params) + graphql_query_for(resource_type, { 'fullPath' => resource.full_path }, + query_nodes(:packages, :id, include_pagination_info: true, args: params) + ) + end + end + + describe 'filtering' do + subject { packages } + + let(:query) do + graphql_query_for( + resource_type, + { 'fullPath' => resource.full_path }, + query_nodes(:packages, :name, args: params) + ) + end + + before do + resource.add_reporter(current_user) + post_graphql(query, current_user: current_user) + end + + context 'package_name' do + let(:params) { { package_name: maven_package.name } } + + it { is_expected.to contain_exactly({ "name" => maven_package.name }) } + end + + context 'package_type' do + let(:params) { { package_type: :COMPOSER } } + + it { is_expected.to contain_exactly({ "name" => composer_package.name }) } + end + + context 'status' do + let_it_be(:errored_package) { create(:maven_package, project: project1, status: 'error') } + + let(:params) { { status: :ERROR } } + + it { is_expected.to contain_exactly({ "name" => errored_package.name }) } + end + + context 'include_versionless' do + let(:params) { { include_versionless: true } } + + it { is_expected.to include({ "name" => versionaless_package.name }) } + end + end end diff --git a/spec/support/shared_examples/requests/api/issuable_update_shared_examples.rb b/spec/support/shared_examples/requests/api/issuable_update_shared_examples.rb index ded381fd402..a3378d4619b 100644 --- a/spec/support/shared_examples/requests/api/issuable_update_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/issuable_update_shared_examples.rb @@ -3,7 +3,7 @@ RSpec.shared_examples 'issuable update endpoint' do let(:area) { entity.class.name.underscore.pluralize } - describe 'PUT /projects/:id/issues/:issue_id' do + describe 'PUT /projects/:id/issues/:issue_iid' do let(:url) { "/projects/#{project.id}/#{area}/#{entity.iid}" } it 'clears labels when labels param is nil' do 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 index 54aa9d47dd8..fa111ca5811 100644 --- 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 @@ -14,7 +14,6 @@ RSpec.shared_examples 'multiple and scoped issue boards' do |route_definition| 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 diff --git a/spec/support/shared_examples/requests/api/terraform/modules/v1/packages_shared_examples.rb b/spec/support/shared_examples/requests/api/terraform/modules/v1/packages_shared_examples.rb new file mode 100644 index 00000000000..70cc9b1e6b5 --- /dev/null +++ b/spec/support/shared_examples/requests/api/terraform/modules/v1/packages_shared_examples.rb @@ -0,0 +1,251 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'when package feature is disabled' do + before do + stub_config(packages: { enabled: false }) + end + + it_behaves_like 'returning response status', :not_found +end + +RSpec.shared_examples 'without authentication' do + it_behaves_like 'returning response status', :unauthorized +end + +RSpec.shared_examples 'with authentication' do + where(:user_role, :token_header, :token_type, :valid_token, :status) do + :guest | 'PRIVATE-TOKEN' | :personal_access_token | true | :not_found + :guest | 'PRIVATE-TOKEN' | :personal_access_token | false | :unauthorized + :guest | 'DEPLOY-TOKEN' | :deploy_token | true | :not_found + :guest | 'DEPLOY-TOKEN' | :deploy_token | false | :unauthorized + :guest | 'JOB-TOKEN' | :job_token | true | :not_found + :guest | 'JOB-TOKEN' | :job_token | false | :unauthorized + :reporter | 'PRIVATE-TOKEN' | :personal_access_token | true | :not_found + :reporter | 'PRIVATE-TOKEN' | :personal_access_token | false | :unauthorized + :reporter | 'DEPLOY-TOKEN' | :deploy_token | true | :not_found + :reporter | 'DEPLOY-TOKEN' | :deploy_token | false | :unauthorized + :reporter | 'JOB-TOKEN' | :job_token | true | :not_found + :reporter | 'JOB-TOKEN' | :job_token | false | :unauthorized + :developer | 'PRIVATE-TOKEN' | :personal_access_token | true | :not_found + :developer | 'PRIVATE-TOKEN' | :personal_access_token | false | :unauthorized + :developer | 'DEPLOY-TOKEN' | :deploy_token | true | :not_found + :developer | 'DEPLOY-TOKEN' | :deploy_token | false | :unauthorized + :developer | 'JOB-TOKEN' | :job_token | true | :not_found + :developer | 'JOB-TOKEN' | :job_token | false | :unauthorized + end + + with_them do + before do + project.send("add_#{user_role}", user) unless user_role == :anonymous + end + + let(:token) { valid_token ? tokens[token_type] : 'invalid-token123' } + let(:headers) { { token_header => token } } + + it_behaves_like 'returning response status', params[:status] + end +end + +RSpec.shared_examples 'an unimplemented route' do + it_behaves_like 'without authentication' + it_behaves_like 'with authentication' + it_behaves_like 'when package feature is disabled' +end + +RSpec.shared_examples 'grants terraform module download' do |user_type, status, add_member = true| + context "for user type #{user_type}" do + before do + group.send("add_#{user_type}", user) if add_member && user_type != :anonymous + end + + it_behaves_like 'returning response status', status + + it 'returns a valid response' do + subject + + expect(response.headers).to include 'X-Terraform-Get' + end + end +end + +RSpec.shared_examples 'returns terraform module packages' do |user_type, status, add_member = true| + context "for user type #{user_type}" do + before do + group.send("add_#{user_type}", user) if add_member && user_type != :anonymous + end + + it_behaves_like 'returning response status', status + + it 'returning a valid response' do + subject + + expect(json_response).to match_schema('public_api/v4/packages/terraform/modules/v1/versions') + end + end +end + +RSpec.shared_examples 'returns no terraform module packages' do |user_type, status, add_member = true| + context "for user type #{user_type}" do + before do + group.send("add_#{user_type}", user) if add_member && user_type != :anonymous + end + + it_behaves_like 'returning response status', status + + it 'returns a response with no versions' do + subject + + expect(json_response['modules'][0]['versions'].size).to eq(0) + end + end +end + +RSpec.shared_examples 'grants terraform module packages access' do |user_type, status, add_member = true| + context "for user type #{user_type}" do + before do + project.send("add_#{user_type}", user) if add_member && user_type != :anonymous + end + + it_behaves_like 'returning response status', status + end +end + +RSpec.shared_examples 'grants terraform module package file access' do |user_type, status, add_member = true| + context "for user type #{user_type}" do + before do + project.send("add_#{user_type}", user) if add_member && user_type != :anonymous + end + + it_behaves_like 'returning response status', status + it_behaves_like 'a package tracking event', described_class.name, 'pull_package' + end +end + +RSpec.shared_examples 'rejects terraform module packages access' do |user_type, status, add_member = true| + context "for user type #{user_type}" do + before do + project.send("add_#{user_type}", user) if add_member && user_type != :anonymous + end + + it_behaves_like 'returning response status', status + end +end + +RSpec.shared_examples 'process terraform module workhorse authorization' do |user_type, status, add_member = true| + context "for user type #{user_type}" do + before do + project.send("add_#{user_type}", user) if add_member && user_type != :anonymous + end + + it_behaves_like 'returning response status', status + + it 'has the proper content type' do + subject + + expect(response.media_type).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE) + end + + context 'with a request that bypassed gitlab-workhorse' do + let(:headers) do + { 'HTTP_PRIVATE_TOKEN' => personal_access_token.token } + .merge(workhorse_headers) + .tap { |h| h.delete(Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER) } + end + + before do + project.add_maintainer(user) + end + + it_behaves_like 'returning response status', :forbidden + end + end +end + +RSpec.shared_examples 'process terraform module upload' do |user_type, status, add_member = true| + RSpec.shared_examples 'creates terraform module package files' do + it 'creates package files', :aggregate_failures do + expect { subject } + .to change { project.packages.count }.by(1) + .and change { Packages::PackageFile.count }.by(1) + expect(response).to have_gitlab_http_status(status) + + package_file = project.packages.last.package_files.reload.last + expect(package_file.file_name).to eq('mymodule-mysystem-1.0.0.tgz') + end + end + + context "for user type #{user_type}" do + before do + project.send("add_#{user_type}", user) if add_member && user_type != :anonymous + end + + context 'with object storage disabled' do + before do + stub_package_file_object_storage(enabled: false) + end + + context 'without a file from workhorse' do + let(:send_rewritten_field) { false } + + it_behaves_like 'returning response status', :bad_request + end + + context 'with correct params' do + it_behaves_like 'package workhorse uploads' + it_behaves_like 'creates terraform module package files' + it_behaves_like 'a package tracking event', described_class.name, 'push_package' + end + end + + context 'with object storage enabled' do + let(:tmp_object) do + fog_connection.directories.new(key: 'packages').files.create( # rubocop:disable Rails/SaveBang + key: "tmp/uploads/#{file_name}", + body: 'content' + ) + end + + let(:fog_file) { fog_to_uploaded_file(tmp_object) } + let(:params) { { file: fog_file, 'file.remote_id' => file_name } } + + context 'and direct upload enabled' do + let(:fog_connection) do + stub_package_file_object_storage(direct_upload: true) + end + + it_behaves_like 'creates terraform module package files' + + ['123123', '../../123123'].each do |remote_id| + context "with invalid remote_id: #{remote_id}" do + let(:params) do + { + file: fog_file, + 'file.remote_id' => remote_id + } + end + + it_behaves_like 'returning response status', :forbidden + end + end + end + + context 'and direct upload disabled' do + context 'and background upload disabled' do + let(:fog_connection) do + stub_package_file_object_storage(direct_upload: false, background_upload: false) + end + + it_behaves_like 'creates terraform module package files' + end + + context 'and background upload enabled' do + let(:fog_connection) do + stub_package_file_object_storage(direct_upload: false, background_upload: true) + end + + it_behaves_like 'creates terraform module package files' + end + end + end + end +end diff --git a/spec/support/shared_examples/requests/api/time_tracking_shared_examples.rb b/spec/support/shared_examples/requests/api/time_tracking_shared_examples.rb index fb6d6603beb..afc902dd184 100644 --- a/spec/support/shared_examples/requests/api/time_tracking_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/time_tracking_shared_examples.rb @@ -125,6 +125,22 @@ RSpec.shared_examples 'time tracking endpoints' do |issuable_name| expect(json_response['message']['base'].first).to eq(_('Time to subtract exceeds the total time spent')) end end + + if issuable_name == 'merge_request' + it 'calls update service with :use_specialized_service param' do + expect(::MergeRequests::UpdateService).to receive(:new).with(project: project, current_user: user, params: hash_including(use_specialized_service: true)) + + post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/add_spent_time", user), params: { duration: '2h' } + end + end + + if issuable_name == 'issue' + it 'calls update service without :use_specialized_service param' do + expect(::Issues::UpdateService).to receive(:new).with(project: project, current_user: user, params: hash_not_including(use_specialized_service: true)) + + post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/add_spent_time", user), params: { duration: '2h' } + end + end end describe "POST /projects/:id/#{issuable_collection_name}/:#{issuable_name}_id/reset_spent_time" do diff --git a/spec/support/shared_examples/requests/clusters/integrations_controller_shared_examples.rb b/spec/support/shared_examples/requests/clusters/integrations_controller_shared_examples.rb index 490c7d12115..91fdcbd9b1d 100644 --- a/spec/support/shared_examples/requests/clusters/integrations_controller_shared_examples.rb +++ b/spec/support/shared_examples/requests/clusters/integrations_controller_shared_examples.rb @@ -2,7 +2,7 @@ RSpec.shared_examples '#create_or_update action' do let(:params) do - { integration: { application_type: Clusters::Applications::Prometheus.application_name, enabled: true } } + { integration: { application_type: 'prometheus', enabled: true } } end let(:path) { raise NotImplementedError } 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 926da827e75..95817624658 100644 --- a/spec/support/shared_examples/requests/rack_attack_shared_examples.rb +++ b/spec/support/shared_examples/requests/rack_attack_shared_examples.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true # # Requires let variables: -# * throttle_setting_prefix: "throttle_authenticated_api", "throttle_authenticated_web", "throttle_protected_paths" +# * throttle_setting_prefix: "throttle_authenticated_api", "throttle_authenticated_web", "throttle_protected_paths", "throttle_authenticated_packages_api" # * request_method # * request_args # * other_user_request_args @@ -13,7 +13,8 @@ RSpec.shared_examples 'rate-limited token-authenticated requests' do { "throttle_protected_paths" => "throttle_authenticated_protected_paths_api", "throttle_authenticated_api" => "throttle_authenticated_api", - "throttle_authenticated_web" => "throttle_authenticated_web" + "throttle_authenticated_web" => "throttle_authenticated_web", + "throttle_authenticated_packages_api" => "throttle_authenticated_packages_api" } end diff --git a/spec/support/shared_examples/row_lock_shared_examples.rb b/spec/support/shared_examples/row_lock_shared_examples.rb new file mode 100644 index 00000000000..5e003172215 --- /dev/null +++ b/spec/support/shared_examples/row_lock_shared_examples.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +# Ensure that a SQL command to lock this row(s) was requested. +# Ensure a transaction also occurred. +# Be careful! This form of spec is not foolproof, but better than nothing. + +RSpec.shared_examples 'locked row' do + it "has locked row" do + table_name = row.class.table_name + ids_regex = /SELECT.*FROM.*#{table_name}.*"#{table_name}"."id" = #{row.id}.+FOR UPDATE/m + + expect(recorded_queries.log).to include a_string_matching 'SAVEPOINT' + expect(recorded_queries.log).to include a_string_matching ids_regex + end +end + +RSpec.shared_examples 'locked rows' do + it "has locked rows" do + table_name = rows.first.class.table_name + + row_ids = rows.map(&:id).join(', ') + ids_regex = /SELECT.+FROM.+"#{table_name}".+"#{table_name}"."id" IN \(#{row_ids}\).+FOR UPDATE/m + + expect(recorded_queries.log).to include a_string_matching 'SAVEPOINT' + expect(recorded_queries.log).to include a_string_matching ids_regex + end +end diff --git a/spec/support/shared_examples/serializers/environment_serializer_shared_examples.rb b/spec/support/shared_examples/serializers/environment_serializer_shared_examples.rb index 00146335ef7..9d7ae6bcb3d 100644 --- a/spec/support/shared_examples/serializers/environment_serializer_shared_examples.rb +++ b/spec/support/shared_examples/serializers/environment_serializer_shared_examples.rb @@ -20,9 +20,27 @@ RSpec.shared_examples 'avoid N+1 on environments serialization' do expect { serialize(grouping: false) }.not_to exceed_query_limit(control.count) end - def serialize(grouping:) + it 'does not preload for environments that does not exist in the page', :request_store do + create_environment_with_associations(project) + + first_page_query = ActiveRecord::QueryRecorder.new do + serialize(grouping: false, query: { page: 1, per_page: 1 } ) + end + + second_page_query = ActiveRecord::QueryRecorder.new do + serialize(grouping: false, query: { page: 2, per_page: 1 } ) + end + + expect(second_page_query.count).to be < first_page_query.count + end + + def serialize(grouping:, query: nil) + query ||= { page: 1, per_page: 1 } + request = double(url: "#{Gitlab.config.gitlab.url}:8080/api/v4/projects?#{query.to_query}", query_parameters: query) + EnvironmentSerializer.new(current_user: user, project: project).yield_self do |serializer| serializer.within_folders if grouping + serializer.with_pagination(request, spy('response')) serializer.represent(Environment.where(project: project)) end end diff --git a/spec/support/shared_examples/serializers/pipeline_artifacts_shared_example.rb b/spec/support/shared_examples/serializers/pipeline_artifacts_shared_example.rb deleted file mode 100644 index d5ffd5e7510..00000000000 --- a/spec/support/shared_examples/serializers/pipeline_artifacts_shared_example.rb +++ /dev/null @@ -1,21 +0,0 @@ -# frozen_string_literal: true -RSpec.shared_examples 'public artifacts' do - let_it_be(:project) { create(:project, :public) } - let(:pipeline) { create(:ci_empty_pipeline, status: :success, project: project) } - - context 'that has artifacts' do - let!(:build) { create(:ci_build, :success, :artifacts, pipeline: pipeline) } - - it 'contains information about artifacts' do - expect(subject[:details][:artifacts].length).to eq(1) - end - end - - context 'that has non public artifacts' do - let!(:build) { create(:ci_build, :success, :artifacts, :non_public_artifacts, pipeline: pipeline) } - - it 'does not contain information about artifacts' do - expect(subject[:details][:artifacts].length).to eq(0) - end - end -end diff --git a/spec/support/shared_examples/services/alert_management/alert_processing/alert_firing_shared_examples.rb b/spec/support/shared_examples/services/alert_management/alert_processing/alert_firing_shared_examples.rb new file mode 100644 index 00000000000..218a3462c35 --- /dev/null +++ b/spec/support/shared_examples/services/alert_management/alert_processing/alert_firing_shared_examples.rb @@ -0,0 +1,161 @@ +# frozen_string_literal: true + +# This shared_example requires the following variables: +# - `service`, the service which includes AlertManagement::AlertProcessing +RSpec.shared_examples 'creates an alert management alert or errors' do + it { is_expected.to be_success } + + it 'creates AlertManagement::Alert' do + expect(Gitlab::AppLogger).not_to receive(:warn) + + expect { subject }.to change(AlertManagement::Alert, :count).by(1) + end + + it 'executes the alert service hooks' do + expect_next_instance_of(AlertManagement::Alert) do |alert| + expect(alert).to receive(:execute_services) + end + + subject + end + + context 'and fails to save' do + let(:errors) { double(messages: { hosts: ['hosts array is over 255 chars'] })} + + before do + allow(service).to receive(:alert).and_call_original + allow(service).to receive_message_chain(:alert, :save).and_return(false) + allow(service).to receive_message_chain(:alert, :errors).and_return(errors) + end + + it_behaves_like 'alerts service responds with an error', :bad_request + + it 'writes a warning to the log' do + expect(Gitlab::AppLogger).to receive(:warn).with( + message: "Unable to create AlertManagement::Alert from #{source}", + project_id: project.id, + alert_errors: { hosts: ['hosts array is over 255 chars'] } + ) + + subject + end + end +end + +# This shared_example requires the following variables: +# - last_alert_attributes, last created alert +# - project, project that alert created +# - payload_raw, hash representation of payload +# - environment, project's environment +# - fingerprint, fingerprint hash +RSpec.shared_examples 'properly assigns the alert properties' do + specify do + subject + + expect(last_alert_attributes).to match({ + project_id: project.id, + title: payload_raw.fetch(:title), + started_at: Time.zone.parse(payload_raw.fetch(:start_time)), + severity: payload_raw.fetch(:severity, nil), + status: AlertManagement::Alert.status_value(:triggered), + events: 1, + domain: domain, + hosts: payload_raw.fetch(:hosts, nil), + payload: payload_raw.with_indifferent_access, + issue_id: nil, + description: payload_raw.fetch(:description, nil), + monitoring_tool: payload_raw.fetch(:monitoring_tool, nil), + service: payload_raw.fetch(:service, nil), + fingerprint: Digest::SHA1.hexdigest(fingerprint), + environment_id: environment.id, + ended_at: nil, + prometheus_alert_id: nil + }.with_indifferent_access) + end +end + +RSpec.shared_examples 'does not create an alert management alert' do + specify do + expect { subject }.not_to change(AlertManagement::Alert, :count) + end +end + +# This shared_example requires the following variables: +# - `alert`, the alert for which events should be incremented +RSpec.shared_examples 'adds an alert management alert event' do + specify do + expect(alert).not_to receive(:execute_services) + + expect { subject }.to change { alert.reload.events }.by(1) + + expect(subject).to be_success + end + + it_behaves_like 'does not create an alert management alert' +end + +# This shared_example requires the following variables: +# - `alert`, the alert for which events should not be incremented +RSpec.shared_examples 'does not add an alert management alert event' do + specify do + expect { subject }.not_to change { alert.reload.events } + end +end + +RSpec.shared_examples 'processes new firing alert' do + include_examples 'processes never-before-seen alert' + + context 'for an existing alert with the same fingerprint' do + let_it_be(:gitlab_fingerprint) { Digest::SHA1.hexdigest(fingerprint) } + + context 'which is triggered' do + let_it_be(:alert) { create(:alert_management_alert, :triggered, fingerprint: gitlab_fingerprint, project: project) } + + it_behaves_like 'adds an alert management alert event' + it_behaves_like 'sends alert notification emails if enabled' + it_behaves_like 'processes incident issues if enabled', with_issue: true + + it_behaves_like 'does not create an alert management alert' + it_behaves_like 'does not create a system note for alert' + + context 'with an existing resolved alert as well' do + let_it_be(:resolved_alert) { create(:alert_management_alert, :resolved, project: project, fingerprint: gitlab_fingerprint) } + + it_behaves_like 'adds an alert management alert event' + it_behaves_like 'sends alert notification emails if enabled' + it_behaves_like 'processes incident issues if enabled', with_issue: true + + it_behaves_like 'does not create an alert management alert' + it_behaves_like 'does not create a system note for alert' + end + end + + context 'which is acknowledged' do + let_it_be(:alert) { create(:alert_management_alert, :acknowledged, fingerprint: gitlab_fingerprint, project: project) } + + it_behaves_like 'adds an alert management alert event' + it_behaves_like 'processes incident issues if enabled', with_issue: true + + it_behaves_like 'does not create an alert management alert' + it_behaves_like 'does not create a system note for alert' + it_behaves_like 'does not send alert notification emails' + end + + context 'which is ignored' do + let_it_be(:alert) { create(:alert_management_alert, :ignored, fingerprint: gitlab_fingerprint, project: project) } + + it_behaves_like 'adds an alert management alert event' + it_behaves_like 'processes incident issues if enabled', with_issue: true + + it_behaves_like 'does not create an alert management alert' + it_behaves_like 'does not create a system note for alert' + it_behaves_like 'does not send alert notification emails' + end + + context 'which is resolved' do + let_it_be(:alert) { create(:alert_management_alert, :resolved, fingerprint: gitlab_fingerprint, project: project) } + + include_examples 'processes never-before-seen alert' + end + end +end diff --git a/spec/support/shared_examples/services/alert_management/alert_processing/alert_recovery_shared_examples.rb b/spec/support/shared_examples/services/alert_management/alert_processing/alert_recovery_shared_examples.rb new file mode 100644 index 00000000000..86e7da5bcbe --- /dev/null +++ b/spec/support/shared_examples/services/alert_management/alert_processing/alert_recovery_shared_examples.rb @@ -0,0 +1,113 @@ +# frozen_string_literal: true + +# This shared_example requires the following variables: +# - `alert`, the alert to be resolved +RSpec.shared_examples 'resolves an existing alert management alert' do + it 'sets the end time and status' do + expect(Gitlab::AppLogger).not_to receive(:warn) + + expect { subject } + .to change { alert.reload.resolved? }.to(true) + .and change { alert.ended_at.present? }.to(true) + + expect(subject).to be_success + end +end + +# This shared_example requires the following variables: +# - `alert`, the alert not to be updated +RSpec.shared_examples 'does not change the alert end time' do + specify do + expect { subject }.not_to change { alert.reload.ended_at } + end +end + +# This shared_example requires the following variables: +# - `project`, expected project for an incoming alert +# - `service`, a service which includes AlertManagement::AlertProcessing +# - `alert` (optional), the alert which should fail to resolve. If not +# included, the log is expected to correspond to a new alert +RSpec.shared_examples 'writes a warning to the log for a failed alert status update' do + before do + allow(service).to receive(:alert).and_call_original + allow(service).to receive_message_chain(:alert, :resolve).and_return(false) + end + + specify do + expect(Gitlab::AppLogger).to receive(:warn).with( + message: 'Unable to update AlertManagement::Alert status to resolved', + project_id: project.id, + alert_id: alert ? alert.id : (last_alert_id + 1) + ) + + # Failure to resolve a recovery alert is not a critical failure + expect(subject).to be_success + end + + private + + def last_alert_id + AlertManagement::Alert.connection + .select_value("SELECT nextval('#{AlertManagement::Alert.sequence_name}')") + end +end + +RSpec.shared_examples 'processes recovery alert' do + context 'seen for the first time' do + let(:alert) { AlertManagement::Alert.last } + + include_examples 'processes never-before-seen recovery alert' + end + + context 'for an existing alert with the same fingerprint' do + let_it_be(:gitlab_fingerprint) { Digest::SHA1.hexdigest(fingerprint) } + + context 'which is triggered' do + let_it_be(:alert) { create(:alert_management_alert, :triggered, project: project, fingerprint: gitlab_fingerprint, monitoring_tool: source) } + + it_behaves_like 'resolves an existing alert management alert' + it_behaves_like 'creates expected system notes for alert', :recovery_alert, :resolve_alert + it_behaves_like 'sends alert notification emails if enabled' + it_behaves_like 'closes related incident if enabled' + it_behaves_like 'writes a warning to the log for a failed alert status update' + + it_behaves_like 'does not create an alert management alert' + it_behaves_like 'does not process incident issues' + it_behaves_like 'does not add an alert management alert event' + end + + context 'which is ignored' do + let_it_be(:alert) { create(:alert_management_alert, :ignored, project: project, fingerprint: gitlab_fingerprint, monitoring_tool: source) } + + it_behaves_like 'resolves an existing alert management alert' + it_behaves_like 'creates expected system notes for alert', :recovery_alert, :resolve_alert + it_behaves_like 'sends alert notification emails if enabled' + it_behaves_like 'closes related incident if enabled' + it_behaves_like 'writes a warning to the log for a failed alert status update' + + it_behaves_like 'does not create an alert management alert' + it_behaves_like 'does not process incident issues' + it_behaves_like 'does not add an alert management alert event' + end + + context 'which is acknowledged' do + let_it_be(:alert) { create(:alert_management_alert, :acknowledged, project: project, fingerprint: gitlab_fingerprint, monitoring_tool: source) } + + it_behaves_like 'resolves an existing alert management alert' + it_behaves_like 'creates expected system notes for alert', :recovery_alert, :resolve_alert + it_behaves_like 'sends alert notification emails if enabled' + it_behaves_like 'closes related incident if enabled' + it_behaves_like 'writes a warning to the log for a failed alert status update' + + it_behaves_like 'does not create an alert management alert' + it_behaves_like 'does not process incident issues' + it_behaves_like 'does not add an alert management alert event' + end + + context 'which is resolved' do + let_it_be(:alert) { create(:alert_management_alert, :resolved, project: project, fingerprint: gitlab_fingerprint, monitoring_tool: source) } + + include_examples 'processes never-before-seen recovery alert' + end + end +end diff --git a/spec/support/shared_examples/services/alert_management/alert_processing/incident_creation_shared_examples.rb b/spec/support/shared_examples/services/alert_management/alert_processing/incident_creation_shared_examples.rb new file mode 100644 index 00000000000..c6ac07b6dd5 --- /dev/null +++ b/spec/support/shared_examples/services/alert_management/alert_processing/incident_creation_shared_examples.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +# Expects usage of 'incident settings enabled' context. +# +# This shared_example includes the following option: +# - with_issue: includes a test for when the defined `alert` has an associated issue +# +# This shared_example requires the following variables: +# - `alert`, required if :with_issue is true +RSpec.shared_examples 'processes incident issues if enabled' do |with_issue: false| + include_examples 'processes incident issues', with_issue + + context 'with incident setting disabled' do + let(:create_issue) { false } + + it_behaves_like 'does not process incident issues' + end +end + +RSpec.shared_examples 'processes incident issues' do |with_issue: false| + before do + allow_next_instance_of(AlertManagement::Alert) do |alert| + allow(alert).to receive(:execute_services) + end + end + + specify do + expect(IncidentManagement::ProcessAlertWorkerV2) + .to receive(:perform_async) + .with(kind_of(Integer)) + + Sidekiq::Testing.inline! do + expect(subject).to be_success + end + end + + context 'with issue', if: with_issue do + before do + alert.update!(issue: create(:issue, project: project)) + end + + it_behaves_like 'does not process incident issues' + end +end + +RSpec.shared_examples 'does not process incident issues' do + specify do + expect(IncidentManagement::ProcessAlertWorkerV2).not_to receive(:perform_async) + + subject + end +end diff --git a/spec/support/shared_examples/services/alert_management/alert_processing/incident_resolution_shared_examples.rb b/spec/support/shared_examples/services/alert_management/alert_processing/incident_resolution_shared_examples.rb new file mode 100644 index 00000000000..132f1e0422e --- /dev/null +++ b/spec/support/shared_examples/services/alert_management/alert_processing/incident_resolution_shared_examples.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +# Expects usage of 'incident settings enabled' context. +# +# This shared_example requires the following variables: +# - `alert`, alert for which related incidents should be closed +# - `project`, project of the alert +RSpec.shared_examples 'closes related incident if enabled' do + context 'with issue' do + before do + alert.update!(issue: create(:issue, project: project)) + end + + it { expect { subject }.to change { alert.issue.reload.closed? }.from(false).to(true) } + it { expect { subject }.to change(ResourceStateEvent, :count).by(1) } + end + + context 'without issue' do + it { expect { subject }.not_to change { alert.reload.issue } } + it { expect { subject }.not_to change(ResourceStateEvent, :count) } + end + + context 'with incident setting disabled' do + let(:auto_close_incident) { false } + + it_behaves_like 'does not close related incident' + end +end + +RSpec.shared_examples 'does not close related incident' do + context 'with issue' do + before do + alert.update!(issue: create(:issue, project: project)) + end + + it { expect { subject }.not_to change { alert.issue.reload.state } } + it { expect { subject }.not_to change(ResourceStateEvent, :count) } + end + + context 'without issue' do + it { expect { subject }.not_to change { alert.reload.issue } } + it { expect { subject }.not_to change(ResourceStateEvent, :count) } + end +end diff --git a/spec/support/shared_examples/services/alert_management/alert_processing/notifications_shared_examples.rb b/spec/support/shared_examples/services/alert_management/alert_processing/notifications_shared_examples.rb new file mode 100644 index 00000000000..5f30b58176b --- /dev/null +++ b/spec/support/shared_examples/services/alert_management/alert_processing/notifications_shared_examples.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +# Expects usage of 'incident settings enabled' context. +# +# This shared_example includes the following option: +# - count: number of notifications expected to be sent +RSpec.shared_examples 'sends alert notification emails if enabled' do |count: 1| + include_examples 'sends alert notification emails', count + + context 'with email setting disabled' do + let(:send_email) { false } + + it_behaves_like 'does not send alert notification emails' + end +end + +RSpec.shared_examples 'sends alert notification emails' do |count: 1| + let(:notification_async) { double(NotificationService::Async) } + + specify do + allow(NotificationService).to receive_message_chain(:new, :async).and_return(notification_async) + expect(notification_async).to receive(:prometheus_alerts_fired).exactly(count).times + + subject + end +end + +RSpec.shared_examples 'does not send alert notification emails' do + specify do + expect(NotificationService).not_to receive(:new) + + subject + end +end diff --git a/spec/support/shared_examples/services/alert_management/alert_processing/system_notes_shared_examples.rb b/spec/support/shared_examples/services/alert_management/alert_processing/system_notes_shared_examples.rb new file mode 100644 index 00000000000..57d598c0259 --- /dev/null +++ b/spec/support/shared_examples/services/alert_management/alert_processing/system_notes_shared_examples.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +# This shared_example includes the following option: +# - notes: any of [:new_alert, :recovery_alert, :resolve_alert]. +# Represents which notes are expected to be created. +# +# This shared_example requires the following variables: +# - `source` (optional), the monitoring tool or integration name +# expected in the applicable system notes +RSpec.shared_examples 'creates expected system notes for alert' do |*notes| + let(:expected_note_count) { expected_notes.length } + let(:new_notes) { Note.last(expected_note_count).pluck(:note) } + let(:expected_notes) do + { + new_alert: source, + recovery_alert: source, + resolve_alert: 'Resolved' + }.slice(*notes) + end + + it "for #{notes.join(', ')}" do + expect { subject }.to change(Note, :count).by(expected_note_count) + + expected_notes.each_value.with_index do |value, index| + expect(new_notes[index]).to include(value) + end + end +end + +RSpec.shared_examples 'does not create a system note for alert' do + specify do + expect { subject }.not_to change(Note, :count) + end +end 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 d9f28a97a0f..827ae42f970 100644 --- a/spec/support/shared_examples/services/alert_management_shared_examples.rb +++ b/spec/support/shared_examples/services/alert_management_shared_examples.rb @@ -1,111 +1,77 @@ # frozen_string_literal: true -RSpec.shared_examples 'creates an alert management alert' do - it { is_expected.to be_success } +RSpec.shared_examples 'alerts service responds with an error and takes no actions' do |http_status| + include_examples 'alerts service responds with an error', http_status - it 'creates AlertManagement::Alert' do - expect { subject }.to change(AlertManagement::Alert, :count).by(1) - end - - it 'executes the alert service hooks' do - expect_next_instance_of(AlertManagement::Alert) do |alert| - expect(alert).to receive(:execute_services) - end + it_behaves_like 'does not create an alert management alert' + it_behaves_like 'does not create a system note for alert' + it_behaves_like 'does not process incident issues' + it_behaves_like 'does not send alert notification emails' +end - subject +RSpec.shared_examples 'alerts service responds with an error' do |http_status| + specify do + expect(subject).to be_error + expect(subject.http_status).to eq(http_status) end end # This shared_example requires the following variables: -# - last_alert_attributes, last created alert -# - project, project that alert created -# - payload_raw, hash representation of payload -# - environment, project's environment -# - fingerprint, fingerprint hash -RSpec.shared_examples 'assigns the alert properties' do - it 'ensures that created alert has all data properly assigned' do - subject - - expect(last_alert_attributes).to match( - project_id: project.id, - title: payload_raw.fetch(:title), - started_at: Time.zone.parse(payload_raw.fetch(:start_time)), - severity: payload_raw.fetch(:severity), - status: AlertManagement::Alert.status_value(:triggered), - events: 1, - domain: domain, - hosts: payload_raw.fetch(:hosts), - payload: payload_raw.with_indifferent_access, - issue_id: nil, - description: payload_raw.fetch(:description), - monitoring_tool: payload_raw.fetch(:monitoring_tool), - service: payload_raw.fetch(:service), - fingerprint: Digest::SHA1.hexdigest(fingerprint), - environment_id: environment.id, - ended_at: nil, - prometheus_alert_id: nil +# - `service`, a service which includes ::IncidentManagement::Settings +RSpec.shared_context 'incident management settings enabled' do + let(:auto_close_incident) { true } + let(:create_issue) { true } + let(:send_email) { true } + + let(:incident_management_setting) do + double( + auto_close_incident?: auto_close_incident, + create_issue?: create_issue, + send_email?: send_email ) end -end -RSpec.shared_examples 'does not an create alert management alert' do - it 'does not create alert' do - expect { subject }.not_to change(AlertManagement::Alert, :count) + before do + allow(ProjectServiceWorker).to receive(:perform_async) + allow(service) + .to receive(:incident_management_setting) + .and_return(incident_management_setting) end end -RSpec.shared_examples 'adds an alert management alert event' do - it { is_expected.to be_success } - - it 'does not create an alert' do - expect { subject }.not_to change(AlertManagement::Alert, :count) - end - - it 'increases alert events count' do - expect { subject }.to change { alert.reload.events }.by(1) - end - - it 'does not executes the alert service hooks' do - expect(alert).not_to receive(:execute_services) - - subject - end +RSpec.shared_examples 'processes never-before-seen alert' do + it_behaves_like 'creates an alert management alert or errors' + it_behaves_like 'creates expected system notes for alert', :new_alert + it_behaves_like 'processes incident issues if enabled' + it_behaves_like 'sends alert notification emails if enabled' end -RSpec.shared_examples 'processes incident issues' do - let(:create_incident_service) { spy } - - before do - allow_any_instance_of(AlertManagement::Alert).to receive(:execute_services) +RSpec.shared_examples 'processes never-before-seen recovery alert' do + it_behaves_like 'creates an alert management alert or errors' + it_behaves_like 'creates expected system notes for alert', :new_alert, :recovery_alert, :resolve_alert + it_behaves_like 'sends alert notification emails if enabled' + it_behaves_like 'does not process incident issues' + it_behaves_like 'writes a warning to the log for a failed alert status update' do + let(:alert) { nil } # Ensure the next alert id is used end - it 'processes issues' do - expect(IncidentManagement::ProcessAlertWorker) - .to receive(:perform_async) - .with(nil, nil, kind_of(Integer)) - .once + it 'resolves the alert' do + subject - Sidekiq::Testing.inline! do - expect(subject).to be_success - end + expect(AlertManagement::Alert.last.ended_at).to be_present + expect(AlertManagement::Alert.last.resolved?).to be(true) end end -RSpec.shared_examples 'does not process incident issues' do - it 'does not process issues' do - expect(IncidentManagement::ProcessAlertWorker) - .not_to receive(:perform_async) +RSpec.shared_examples 'processes one firing and one resolved prometheus alerts' do + it 'creates AlertManagement::Alert' do + expect(Gitlab::AppLogger).not_to receive(:warn) - expect(subject).to be_success + expect { subject } + .to change(AlertManagement::Alert, :count).by(2) + .and change(Note, :count).by(4) end -end - -RSpec.shared_examples 'does not process incident issues due to error' do |http_status:| - it 'does not process issues' do - expect(IncidentManagement::ProcessAlertWorker) - .not_to receive(:perform_async) - expect(subject).to be_error - expect(subject.http_status).to eq(http_status) - end + it_behaves_like 'processes incident issues' + it_behaves_like 'sends alert notification emails', count: 2 end diff --git a/spec/support/shared_examples/services/boards/boards_recent_visit_shared_examples.rb b/spec/support/shared_examples/services/boards/boards_recent_visit_shared_examples.rb new file mode 100644 index 00000000000..68ea460dabc --- /dev/null +++ b/spec/support/shared_examples/services/boards/boards_recent_visit_shared_examples.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'boards recent visit' do + let_it_be(:user) { create(:user) } + + describe '#visited' do + it 'creates a visit if one does not exists' do + expect { described_class.visited!(user, board) }.to change(described_class, :count).by(1) + end + + shared_examples 'was visited previously' do + let_it_be(:visit) do + create(visit_relation, + board_parent_relation => board_parent, + board_relation => board, + user: user, + updated_at: 7.days.ago + ) + end + + it 'updates the timestamp' do + freeze_time do + described_class.visited!(user, board) + + expect(described_class.count).to eq 1 + expect(described_class.first.updated_at).to be_like_time(Time.zone.now) + end + end + end + + it_behaves_like 'was visited previously' + + context 'when we try to create a visit that is not unique' do + before do + expect(described_class).to receive(:find_or_create_by).and_raise(ActiveRecord::RecordNotUnique, 'record not unique') + expect(described_class).to receive(:find_or_create_by).and_return(visit) + end + + it_behaves_like 'was visited previously' + end + end + + describe '#latest' do + def create_visit(time) + create(visit_relation, board_parent_relation => board_parent, user: user, updated_at: time) + end + + it 'returns the most recent visited' do + create_visit(7.days.ago) + create_visit(5.days.ago) + recent = create_visit(1.day.ago) + + expect(described_class.latest(user, board_parent)).to eq recent + end + + it 'returns last 3 visited boards' do + create_visit(7.days.ago) + visit1 = create_visit(3.days.ago) + visit2 = create_visit(2.days.ago) + visit3 = create_visit(5.days.ago) + + expect(described_class.latest(user, board_parent, count: 3)).to eq([visit2, visit1, visit3]) + end + end +end diff --git a/spec/support/shared_examples/services/boards/create_service_shared_examples.rb b/spec/support/shared_examples/services/boards/create_service_shared_examples.rb new file mode 100644 index 00000000000..63b5e3a5a84 --- /dev/null +++ b/spec/support/shared_examples/services/boards/create_service_shared_examples.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'boards recent visit create service' do + let_it_be(:user) { create(:user) } + + subject(:service) { described_class.new(board.resource_parent, user) } + + it 'returns nil when there is no user' do + service.current_user = nil + + expect(service.execute(board)).to be_nil + end + + it 'returns nil when database is read only' do + allow(Gitlab::Database).to receive(:read_only?) { true } + + expect(service.execute(board)).to be_nil + end + + it 'records the visit' do + expect(model).to receive(:visited!).once + + service.execute(board) + end +end diff --git a/spec/support/shared_examples/services/boards/issues_move_service_shared_examples.rb b/spec/support/shared_examples/services/boards/issues_move_service_shared_examples.rb index 4aa5d7d890b..7d4fbeea0dc 100644 --- a/spec/support/shared_examples/services/boards/issues_move_service_shared_examples.rb +++ b/spec/support/shared_examples/services/boards/issues_move_service_shared_examples.rb @@ -146,7 +146,7 @@ RSpec.shared_examples 'issues move service' do |group| params.merge!(move_after_id: issue1.id, move_before_id: issue2.id) match_params = { move_between_ids: [issue1.id, issue2.id], board_group_id: parent.id } - expect(Issues::UpdateService).to receive(:new).with(issue.project, user, match_params).and_return(double(execute: build(:issue))) + expect(Issues::UpdateService).to receive(:new).with(project: issue.project, current_user: user, params: match_params).and_return(double(execute: build(:issue))) described_class.new(parent, user, params).execute(issue) end diff --git a/spec/support/shared_examples/services/boards/lists_destroy_service_shared_examples.rb b/spec/support/shared_examples/services/boards/lists_destroy_service_shared_examples.rb index 94da405e491..af88644ced7 100644 --- a/spec/support/shared_examples/services/boards/lists_destroy_service_shared_examples.rb +++ b/spec/support/shared_examples/services/boards/lists_destroy_service_shared_examples.rb @@ -3,30 +3,27 @@ RSpec.shared_examples 'lists destroy service' do context 'when list type is label' do it 'removes list from board' do - list = create(:list, board: board) service = described_class.new(parent, user) expect { service.execute(list) }.to change(board.lists, :count).by(-1) end it 'decrements position of higher lists' do - development = create(:list, board: board, position: 0) - review = create(:list, board: board, position: 1) - staging = create(:list, board: board, position: 2) - closed = board.lists.closed.first + development = create(list_type, params.merge(position: 0)) + review = create(list_type, params.merge(position: 1)) + staging = create(list_type, params.merge(position: 2)) described_class.new(parent, user).execute(development) expect(review.reload.position).to eq 0 expect(staging.reload.position).to eq 1 - expect(closed.reload.position).to be_nil + expect(closed_list.reload.position).to be_nil end end it 'does not remove list from board when list type is closed' do - list = board.lists.closed.first service = described_class.new(parent, user) - expect { service.execute(list) }.not_to change(board.lists, :count) + expect { service.execute(closed_list) }.not_to change(board.lists, :count) 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 7b277d4bede..ce412ef55de 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 @@ -1,7 +1,7 @@ # frozen_string_literal: true RSpec.shared_examples 'system note creation' do |update_params, note_text| - subject { described_class.new(project, user).execute(issuable, old_labels: []) } + subject { described_class.new(project: project, current_user: user).execute(issuable, old_labels: []) } before do issuable.assign_attributes(update_params) @@ -18,7 +18,7 @@ RSpec.shared_examples 'system note creation' do |update_params, note_text| end RSpec.shared_examples 'draft notes creation' do |action| - subject { described_class.new(project, user).execute(issuable, old_labels: []) } + subject { described_class.new(project: project, current_user: user).execute(issuable, old_labels: []) } it 'creates Draft toggle and title change notes' do expect { subject }.to change { Note.count }.from(0).to(2) diff --git a/spec/support/shared_examples/services/destroy_label_links_shared_examples.rb b/spec/support/shared_examples/services/destroy_label_links_shared_examples.rb new file mode 100644 index 00000000000..d2b52468c25 --- /dev/null +++ b/spec/support/shared_examples/services/destroy_label_links_shared_examples.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +RSpec.shared_examples_for 'service deleting label links of an issuable' do + let_it_be(:label_link) { create(:label_link, target: target) } + + def execute + described_class.new(target.id, target.class.name).execute + end + + it 'deletes label links for specified target ID and type' do + control_count = ActiveRecord::QueryRecorder.new { execute }.count + + # Create more label links for the target + create(:label_link, target: target) + create(:label_link, target: target) + + expect { execute }.not_to exceed_query_limit(control_count) + expect(target.reload.label_links.count).to eq(0) + end +end diff --git a/spec/support/shared_examples/services/issuable/destroy_service_shared_examples.rb b/spec/support/shared_examples/services/issuable/destroy_service_shared_examples.rb index ccc287c10de..e776c098fa0 100644 --- a/spec/support/shared_examples/services/issuable/destroy_service_shared_examples.rb +++ b/spec/support/shared_examples/services/issuable/destroy_service_shared_examples.rb @@ -1,10 +1,6 @@ # frozen_string_literal: true shared_examples_for 'service deleting todos' do - before do - stub_feature_flags(destroy_issuable_todos_async: group) - end - it 'destroys associated todos asynchronously' do expect(TodosDestroyer::DestroyedIssuableWorker) .to receive(:perform_async) @@ -12,20 +8,14 @@ shared_examples_for 'service deleting todos' do subject.execute(issuable) end +end - context 'when destroy_issuable_todos_async feature is disabled for group' do - before do - stub_feature_flags(destroy_issuable_todos_async: false) - end - - it 'destroy associated todos synchronously' do - expect_next_instance_of(TodosDestroyer::DestroyedIssuableWorker) do |worker| - expect(worker) - .to receive(:perform) - .with(issuable.id, issuable.class.name) - end +shared_examples_for 'service deleting label links' do + it 'destroys associated label links asynchronously' do + expect(Issuable::LabelLinksDestroyWorker) + .to receive(:perform_async) + .with(issuable.id, issuable.class.name) - subject.execute(issuable) - end + subject.execute(issuable) end end diff --git a/spec/support/shared_examples/services/issuable_shared_examples.rb b/spec/support/shared_examples/services/issuable_shared_examples.rb index 5b3e0f9e0b9..a50a386afe1 100644 --- a/spec/support/shared_examples/services/issuable_shared_examples.rb +++ b/spec/support/shared_examples/services/issuable_shared_examples.rb @@ -4,14 +4,14 @@ RSpec.shared_examples 'cache counters invalidator' do it 'invalidates counter cache for assignees' do expect_any_instance_of(User).to receive(:invalidate_merge_request_cache_counts) - described_class.new(project, user, {}).execute(merge_request) + described_class.new(project: project, current_user: user).execute(merge_request) end end RSpec.shared_examples 'updating a single task' do def update_issuable(opts) issuable = try(:issue) || try(:merge_request) - described_class.new(project, user, opts).execute(issuable) + described_class.new(project: project, current_user: user, params: opts).execute(issuable) end before do diff --git a/spec/support/shared_examples/services/merge_request_shared_examples.rb b/spec/support/shared_examples/services/merge_request_shared_examples.rb index 178b6bc47e1..d2595b92cbc 100644 --- a/spec/support/shared_examples/services/merge_request_shared_examples.rb +++ b/spec/support/shared_examples/services/merge_request_shared_examples.rb @@ -70,7 +70,7 @@ RSpec.shared_examples 'merge request reviewers cache counters invalidator' do it 'invalidates counter cache for reviewers' do expect(merge_request.reviewers).to all(receive(:invalidate_merge_request_cache_counts)) - described_class.new(project, user, {}).execute(merge_request) + described_class.new(project: project, current_user: user).execute(merge_request) end end @@ -86,7 +86,7 @@ RSpec.shared_examples_for 'a service that can create a merge request' do context 'when project has been forked', :sidekiq_might_not_need_inline do let(:forked_project) { fork_project(project, user1, repository: true) } - let(:service) { described_class.new(forked_project, user1, changes, push_options) } + let(:service) { described_class.new(project: forked_project, current_user: user1, changes: changes, push_options: push_options) } before do allow(forked_project).to receive(:empty_repo?).and_return(false) diff --git a/spec/support/shared_examples/services/metrics/dashboard_shared_examples.rb b/spec/support/shared_examples/services/metrics/dashboard_shared_examples.rb index b6c33eac7b4..4df12f7849b 100644 --- a/spec/support/shared_examples/services/metrics/dashboard_shared_examples.rb +++ b/spec/support/shared_examples/services/metrics/dashboard_shared_examples.rb @@ -12,13 +12,22 @@ RSpec.shared_examples 'misconfigured dashboard service response' do |status_code end RSpec.shared_examples 'valid dashboard service response for schema' do + file_ref_resolver = proc do |uri| + file = Rails.root.join(uri.path) + raise StandardError, "Ref file #{uri.path} must be json" unless uri.path.ends_with?('.json') + raise StandardError, "File #{file.to_path} doesn't exists" unless file.exist? + + Gitlab::Json.parse(File.read(file)) + end + it 'returns a json representation of the dashboard' do result = service_call expect(result.keys).to contain_exactly(:dashboard, :status) expect(result[:status]).to eq(:success) - expect(JSON::Validator.fully_validate(dashboard_schema, result[:dashboard])).to be_empty + validator = JSONSchemer.schema(dashboard_schema, ref_resolver: file_ref_resolver) + expect(validator.valid?(result[:dashboard].with_indifferent_access)).to be true end end diff --git a/spec/support/shared_examples/services/namespace_package_settings_shared_examples.rb b/spec/support/shared_examples/services/namespace_package_settings_shared_examples.rb index 8398dd3c453..f7a6bd3676a 100644 --- a/spec/support/shared_examples/services/namespace_package_settings_shared_examples.rb +++ b/spec/support/shared_examples/services/namespace_package_settings_shared_examples.rb @@ -7,6 +7,8 @@ RSpec.shared_examples 'updating the namespace package setting attributes' do |fr expect { subject } .to change { namespace.package_settings.reload.maven_duplicates_allowed }.from(from[:maven_duplicates_allowed]).to(to[:maven_duplicates_allowed]) .and change { namespace.package_settings.reload.maven_duplicate_exception_regex }.from(from[:maven_duplicate_exception_regex]).to(to[:maven_duplicate_exception_regex]) + .and change { namespace.package_settings.reload.generic_duplicates_allowed }.from(from[:generic_duplicates_allowed]).to(to[:generic_duplicates_allowed]) + .and change { namespace.package_settings.reload.generic_duplicate_exception_regex }.from(from[:generic_duplicate_exception_regex]).to(to[:generic_duplicate_exception_regex]) end end @@ -26,6 +28,8 @@ RSpec.shared_examples 'creating the namespace package setting' do expect(namespace.package_setting_relation.maven_duplicates_allowed).to eq(package_settings[:maven_duplicates_allowed]) expect(namespace.package_setting_relation.maven_duplicate_exception_regex).to eq(package_settings[:maven_duplicate_exception_regex]) + expect(namespace.package_setting_relation.generic_duplicates_allowed).to eq(package_settings[:generic_duplicates_allowed]) + expect(namespace.package_setting_relation.generic_duplicate_exception_regex).to eq(package_settings[:generic_duplicate_exception_regex]) end it_behaves_like 'returning a success' diff --git a/spec/support/shared_examples/services/packages_shared_examples.rb b/spec/support/shared_examples/services/packages_shared_examples.rb index 4e34c191306..72878e925dc 100644 --- a/spec/support/shared_examples/services/packages_shared_examples.rb +++ b/spec/support/shared_examples/services/packages_shared_examples.rb @@ -203,7 +203,9 @@ RSpec.shared_examples 'filters on each package_type' do |is_project: false| let_it_be(:package7) { create(:generic_package, project: project) } let_it_be(:package8) { create(:golang_package, project: project) } let_it_be(:package9) { create(:debian_package, project: project) } - let_it_be(:package9) { create(:rubygems_package, project: project) } + let_it_be(:package10) { create(:rubygems_package, project: project) } + let_it_be(:package11) { create(:helm_package, project: project) } + let_it_be(:package12) { create(:terraform_module_package, project: project) } Packages::Package.package_types.keys.each do |package_type| context "for package type #{package_type}" do diff --git a/spec/support/shared_examples/services/projects/update_repository_storage_service_shared_examples.rb b/spec/support/shared_examples/services/projects/update_repository_storage_service_shared_examples.rb index 1fb1b9f79b2..275ddebc18c 100644 --- a/spec/support/shared_examples/services/projects/update_repository_storage_service_shared_examples.rb +++ b/spec/support/shared_examples/services/projects/update_repository_storage_service_shared_examples.rb @@ -47,7 +47,7 @@ RSpec.shared_examples 'moves repository to another storage' do |repository_type| expect(original_repository_double).to receive(:remove) end - it "moves the project and its #{repository_type} repository to the new storage and unmarks the repository as read only" do + it "moves the project and its #{repository_type} repository to the new storage and unmarks the repository as read-only" do old_project_repository_path = Gitlab::GitalyClient::StorageSettings.allow_disk_access do project.repository.path_to_repo end diff --git a/spec/support/shared_examples/services/schedule_bulk_repository_shard_moves_shared_examples.rb b/spec/support/shared_examples/services/schedule_bulk_repository_shard_moves_shared_examples.rb index e67fc4ab04a..97304680316 100644 --- a/spec/support/shared_examples/services/schedule_bulk_repository_shard_moves_shared_examples.rb +++ b/spec/support/shared_examples/services/schedule_bulk_repository_shard_moves_shared_examples.rb @@ -27,7 +27,7 @@ RSpec.shared_examples 'moves repository shard in bulk' do container.set_repository_read_only! expect(subject).to receive(:log_info) - .with(/Container #{container.full_path} \(#{container.id}\) was skipped: #{container.class} is read only/) + .with(/Container #{container.full_path} \(#{container.id}\) was skipped: #{container.class} is read-only/) expect { subject.execute(source_storage_name, destination_storage_name) } .to change(move_service_klass, :count).by(0) end diff --git a/spec/support/shared_examples/services/security/ci_configuration/create_service_shared_examples.rb b/spec/support/shared_examples/services/security/ci_configuration/create_service_shared_examples.rb new file mode 100644 index 00000000000..538fd2bb513 --- /dev/null +++ b/spec/support/shared_examples/services/security/ci_configuration/create_service_shared_examples.rb @@ -0,0 +1,91 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.shared_examples_for 'services security ci configuration create service' do |skip_w_params| + let_it_be(:project) { create(:project, :repository) } + let_it_be(:user) { create(:user) } + + describe '#execute' do + let(:params) { {} } + + context 'user does not belong to project' do + it 'returns an error status' do + expect(result.status).to eq(:error) + expect(result.payload[:success_path]).to be_nil + end + + it 'does not track a snowplow event' do + subject + + expect_no_snowplow_event + end + end + + context 'user belongs to project' do + before do + project.add_developer(user) + end + + it 'does track the snowplow event' do + subject + + expect_snowplow_event(**snowplow_event) + end + + it 'raises exception if the user does not have permission to create a new branch' do + allow(project).to receive(:repository).and_raise(Gitlab::Git::PreReceiveError, "You are not allowed to create protected branches on this project.") + + expect { subject }.to raise_error(Gitlab::Git::PreReceiveError) + end + + context 'when exception is raised' do + let_it_be(:project) { create(:project, :repository) } + + before do + allow(project.repository).to receive(:add_branch).and_raise(StandardError, "The unexpected happened!") + end + + context 'when branch was created' do + before do + allow(project.repository).to receive(:branch_exists?).and_return(true) + end + + it 'tries to rm branch' do + expect(project.repository).to receive(:rm_branch).with(user, branch_name) + expect { subject }.to raise_error(StandardError) + end + end + + context 'when branch was not created' do + before do + allow(project.repository).to receive(:branch_exists?).and_return(false) + end + + it 'does not try to rm branch' do + expect(project.repository).not_to receive(:rm_branch) + expect { subject }.to raise_error(StandardError) + end + end + end + + context 'with no parameters' do + it 'returns the path to create a new merge request' do + expect(result.status).to eq(:success) + expect(result.payload[:success_path]).to match(/#{Gitlab::Routing.url_helpers.project_new_merge_request_url(project, {})}(.*)description(.*)source_branch/) + end + end + + unless skip_w_params + context 'with parameters' do + let(:params) { non_empty_params } + + it 'returns the path to create a new merge request' do + expect(result.status).to eq(:success) + expect(result.payload[:success_path]).to match(/#{Gitlab::Routing.url_helpers.project_new_merge_request_url(project, {})}(.*)description(.*)source_branch/) + end + end + end + end + end +end diff --git a/spec/support/shared_examples/services/updating_mentions_shared_examples.rb b/spec/support/shared_examples/services/updating_mentions_shared_examples.rb index 84f6c4d136a..13a2aa9ddac 100644 --- a/spec/support/shared_examples/services/updating_mentions_shared_examples.rb +++ b/spec/support/shared_examples/services/updating_mentions_shared_examples.rb @@ -15,7 +15,7 @@ RSpec.shared_examples 'updating mentions' do |service_class| def update_mentionable(opts) perform_enqueued_jobs do - service_class.new(project, user, opts).execute(mentionable) + service_class.new(project: project, current_user: user, params: opts).execute(mentionable) end mentionable.reload |