From 6438df3a1e0fb944485cebf07976160184697d72 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Wed, 20 Jan 2021 13:34:23 -0600 Subject: Add latest changes from gitlab-org/gitlab@13-8-stable-ee --- .../git_http_controller_shared_examples.rb | 24 --- .../sessionless_auth_controller_shared_examples.rb | 4 + .../controllers/unique_hll_events_examples.rb | 2 +- .../features/file_uploads_shared_examples.rb | 24 +-- ...aster_manages_access_requests_shared_example.rb | 10 +- .../wiki/user_creates_wiki_page_shared_examples.rb | 32 ++- .../wiki/user_updates_wiki_page_shared_examples.rb | 35 +++- .../user_uses_wiki_shortcuts_shared_examples.rb | 2 +- .../wiki/user_views_wiki_empty_shared_examples.rb | 2 +- .../wiki/user_views_wiki_page_shared_examples.rb | 4 +- .../user_views_wiki_sidebar_shared_examples.rb | 52 ++++- .../debian/distributions_finder_shared_examples.rb | 79 ++++++++ .../finders/packages_shared_examples.rb | 19 ++ .../mutations/http_integrations_shared_examples.rb | 19 ++ .../merge_request_n_plus_one_query_examples.rb | 6 +- .../filters/sanitization_filter_shared_examples.rb | 4 +- .../cycle_analytics/base_stage_shared_examples.rb | 74 ------- .../default_query_config_shared_examples.rb | 16 -- .../gitlab/middleware/multipart_shared_examples.rb | 24 +-- .../project_search_results_shared_examples.rb | 19 +- ...incident_management_activity_shared_examples.rb | 2 +- .../metrics/sampler_shared_examples.rb | 27 +++ .../models/boards/listable_shared_examples.rb | 97 +++++++++ .../can_housekeep_repository_shared_examples.rb | 45 +++++ .../can_housekeep_repository_shared_examples.rb | 45 +++++ .../repository_storage_movable_shared_examples.rb | 4 +- .../debian/architecture_shared_examples.rb | 48 +++++ .../debian/distribution_shared_examples.rb | 225 +++++++++++++++++++++ .../rebase_quick_action_shared_examples.rb | 92 +++++++++ .../requests/api/boards_shared_examples.rb | 33 ++- .../api/debian_packages_shared_examples.rb | 69 ++++++- .../api/nuget_endpoints_shared_examples.rb | 146 +++++++------ .../requests/api/nuget_packages_shared_examples.rb | 58 +++--- .../repository_storage_moves_shared_examples.rb | 219 ++++++++++++++++++++ .../api/resolvable_discussions_shared_examples.rb | 2 + .../requests/rack_attack_shared_examples.rb | 5 +- .../requests/releases_shared_examples.rb | 61 ++++++ .../sessionless_auth_request_shared_examples.rb | 84 ++++++++ .../legacy_path_redirect_shared_examples.rb | 2 +- .../pipeline_artifacts_shared_example.rb | 21 ++ .../boards/issues_list_service_shared_examples.rb | 80 +------- .../boards/items_list_service_shared_examples.rb | 65 ++++++ .../services/merge_request_shared_examples.rb | 15 ++ .../namespace_package_settings_shared_examples.rb | 32 +++ .../onboarding_progress_shared_examples.rb | 20 ++ .../services/packages_shared_examples.rb | 42 ++++ .../repositories/housekeeping_shared_examples.rb | 118 +++++++++++ ..._bulk_repository_shard_moves_shared_examples.rb | 44 ++++ ..._bulk_repository_shard_moves_shared_examples.rb | 30 +++ .../update_repository_move_shared_examples.rb | 39 ++++ 50 files changed, 1831 insertions(+), 390 deletions(-) create mode 100644 spec/support/shared_examples/finders/packages/debian/distributions_finder_shared_examples.rb create mode 100644 spec/support/shared_examples/finders/packages_shared_examples.rb create mode 100644 spec/support/shared_examples/graphql/mutations/http_integrations_shared_examples.rb delete mode 100644 spec/support/shared_examples/lib/gitlab/cycle_analytics/base_stage_shared_examples.rb delete mode 100644 spec/support/shared_examples/lib/gitlab/cycle_analytics/default_query_config_shared_examples.rb create mode 100644 spec/support/shared_examples/metrics/sampler_shared_examples.rb create mode 100644 spec/support/shared_examples/models/boards/listable_shared_examples.rb create mode 100644 spec/support/shared_examples/models/concerns/can_housekeep_repository_shared_examples.rb create mode 100644 spec/support/shared_examples/models/concerns/repositories/can_housekeep_repository_shared_examples.rb create mode 100644 spec/support/shared_examples/models/packages/debian/architecture_shared_examples.rb create mode 100644 spec/support/shared_examples/models/packages/debian/distribution_shared_examples.rb create mode 100644 spec/support/shared_examples/quick_actions/merge_request/rebase_quick_action_shared_examples.rb create mode 100644 spec/support/shared_examples/requests/api/repository_storage_moves_shared_examples.rb create mode 100644 spec/support/shared_examples/requests/releases_shared_examples.rb create mode 100644 spec/support/shared_examples/requests/sessionless_auth_request_shared_examples.rb create mode 100644 spec/support/shared_examples/serializers/pipeline_artifacts_shared_example.rb create mode 100644 spec/support/shared_examples/services/boards/items_list_service_shared_examples.rb create mode 100644 spec/support/shared_examples/services/namespace_package_settings_shared_examples.rb create mode 100644 spec/support/shared_examples/services/onboarding_progress_shared_examples.rb create mode 100644 spec/support/shared_examples/services/repositories/housekeeping_shared_examples.rb create mode 100644 spec/support/shared_examples/services/schedule_bulk_repository_shard_moves_shared_examples.rb create mode 100644 spec/support/shared_examples/workers/schedule_bulk_repository_shard_moves_shared_examples.rb create mode 100644 spec/support/shared_examples/workers/update_repository_move_shared_examples.rb (limited to 'spec/support/shared_examples') diff --git a/spec/support/shared_examples/controllers/repositories/git_http_controller_shared_examples.rb b/spec/support/shared_examples/controllers/repositories/git_http_controller_shared_examples.rb index 9b738a4b002..00a0fb7e4c5 100644 --- a/spec/support/shared_examples/controllers/repositories/git_http_controller_shared_examples.rb +++ b/spec/support/shared_examples/controllers/repositories/git_http_controller_shared_examples.rb @@ -77,30 +77,6 @@ RSpec.shared_examples Repositories::GitHttpController do end end end - - context 'with exceptions' do - before do - allow(controller).to receive(:authenticate_user).and_return(true) - allow(controller).to receive(:verify_workhorse_api!).and_return(true) - end - - it 'returns 503 with GRPC Unavailable' do - allow(controller).to receive(:access_check).and_raise(GRPC::Unavailable) - - get :info_refs, params: params - - expect(response).to have_gitlab_http_status(:service_unavailable) - end - - it 'returns 503 with timeout error' do - allow(controller).to receive(:access_check).and_raise(Gitlab::GitAccess::TimeoutError) - - get :info_refs, params: params - - expect(response).to have_gitlab_http_status(:service_unavailable) - expect(response.body).to eq 'Gitlab::GitAccess::TimeoutError' - end - end end describe 'POST #git_upload_pack' do diff --git a/spec/support/shared_examples/controllers/sessionless_auth_controller_shared_examples.rb b/spec/support/shared_examples/controllers/sessionless_auth_controller_shared_examples.rb index b67eb0d99fd..041695d8111 100644 --- a/spec/support/shared_examples/controllers/sessionless_auth_controller_shared_examples.rb +++ b/spec/support/shared_examples/controllers/sessionless_auth_controller_shared_examples.rb @@ -1,5 +1,9 @@ # frozen_string_literal: true +# This controller shared examples will be migrated to +# spec/support/shared_examples/requests/sessionless_auth_request_shared_examples.rb +# See also https://gitlab.com/groups/gitlab-org/-/epics/5076 + RSpec.shared_examples 'authenticates sessionless user' do |path, format, params| params ||= {} diff --git a/spec/support/shared_examples/controllers/unique_hll_events_examples.rb b/spec/support/shared_examples/controllers/unique_hll_events_examples.rb index cf7ee17ea13..c5d65743810 100644 --- a/spec/support/shared_examples/controllers/unique_hll_events_examples.rb +++ b/spec/support/shared_examples/controllers/unique_hll_events_examples.rb @@ -7,7 +7,7 @@ RSpec.shared_examples 'tracking unique hll events' do |feature_flag| it 'tracks unique event' do - expect(Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:track_event).with(expected_type, target_id) + expect(Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:track_event).with(target_id, values: expected_type) request end diff --git a/spec/support/shared_examples/features/file_uploads_shared_examples.rb b/spec/support/shared_examples/features/file_uploads_shared_examples.rb index ea8c8d44501..d586bf03b59 100644 --- a/spec/support/shared_examples/features/file_uploads_shared_examples.rb +++ b/spec/support/shared_examples/features/file_uploads_shared_examples.rb @@ -2,28 +2,6 @@ RSpec.shared_examples 'handling file uploads' do |shared_examples_name| context 'with object storage disabled' do - context 'with upload_middleware_jwt_params_handler disabled' do - before do - stub_feature_flags(upload_middleware_jwt_params_handler: false) - - expect_next_instance_of(Gitlab::Middleware::Multipart::Handler) do |handler| - expect(handler).to receive(:with_open_files).and_call_original - end - end - - it_behaves_like shared_examples_name - end - - context 'with upload_middleware_jwt_params_handler enabled' do - before do - stub_feature_flags(upload_middleware_jwt_params_handler: true) - - expect_next_instance_of(Gitlab::Middleware::Multipart::HandlerForJWTParams) do |handler| - expect(handler).to receive(:with_open_files).and_call_original - end - end - - it_behaves_like shared_examples_name - end + it_behaves_like shared_examples_name end end diff --git a/spec/support/shared_examples/features/master_manages_access_requests_shared_example.rb b/spec/support/shared_examples/features/master_manages_access_requests_shared_example.rb index 1dbaace1c89..c2dc87b0fb0 100644 --- a/spec/support/shared_examples/features/master_manages_access_requests_shared_example.rb +++ b/spec/support/shared_examples/features/master_manages_access_requests_shared_example.rb @@ -12,9 +12,7 @@ RSpec.shared_examples 'Maintainer manages access requests' do sign_in(maintainer) visit members_page_path - if has_tabs - click_on 'Access requests' - end + click_on 'Access requests' end it 'maintainer can see access requests', :js do @@ -48,11 +46,7 @@ RSpec.shared_examples 'Maintainer manages access requests' do end def expect_visible_access_request(entity, user) - if has_tabs - expect(page).to have_content "Access requests 1" - else - expect(page).to have_content "Users requesting access to #{entity.name} 1" - end + expect(page).to have_content "Access requests 1" expect(page).to have_content user.name end diff --git a/spec/support/shared_examples/features/wiki/user_creates_wiki_page_shared_examples.rb b/spec/support/shared_examples/features/wiki/user_creates_wiki_page_shared_examples.rb index 44d82d2e753..2f8ebd0d264 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 @@ -20,15 +20,25 @@ RSpec.shared_examples 'User creates wiki page' do click_link "Create your first page" end - it "shows validation error message" do + it "shows validation error message if the form is force submitted", :js do page.within(".wiki-form") do fill_in(:wiki_content, with: "") - click_on("Create page") + page.execute_script("window.onbeforeunload = null") + page.execute_script("document.querySelector('.wiki-form').submit()") end expect(page).to have_content("The form contains the following error:").and have_content("Content can't be blank") + end + + it "disables the submit button", :js do + page.within(".wiki-form") do + fill_in(:wiki_content, with: "") + expect(page).to have_button('Create page', disabled: true) + end + end + it "makes sure links to unknown pages work correctly", :js do page.within(".wiki-form") do fill_in(:wiki_content, with: "[link test](test)") @@ -42,7 +52,7 @@ RSpec.shared_examples 'User creates wiki page' do expect(page).to have_content("Create New Page") end - it "shows non-escaped link in the pages list" do + it "shows non-escaped link in the pages list", :js do fill_in(:wiki_title, with: "one/two/three-test") page.within(".wiki-form") do @@ -61,7 +71,7 @@ RSpec.shared_examples 'User creates wiki page' do expect(page).to have_field("wiki[message]", with: "Create home") end - it "creates a page from the home page" do + it "creates a page from the home page", :js do fill_in(:wiki_content, with: "[test](test)\n[GitLab API doc](api)\n[Rake tasks](raketasks)\n# Wiki header\n") fill_in(:wiki_message, with: "Adding links to wiki") @@ -79,7 +89,7 @@ RSpec.shared_examples 'User creates wiki page' do expect(current_path).to eq(wiki_page_path(wiki, "test")) - page.within(:css, ".nav-text") do + page.within(:css, ".wiki-page-header") do expect(page).to have_content("Create New Page") end @@ -91,7 +101,7 @@ RSpec.shared_examples 'User creates wiki page' do expect(current_path).to eq(wiki_page_path(wiki, "api")) - page.within(:css, ".nav-text") do + page.within(:css, ".wiki-page-header") do expect(page).to have_content("Create") end @@ -103,7 +113,7 @@ RSpec.shared_examples 'User creates wiki page' do expect(current_path).to eq(wiki_page_path(wiki, "raketasks")) - page.within(:css, ".nav-text") do + page.within(:css, ".wiki-page-header") do expect(page).to have_content("Create") end end @@ -142,7 +152,7 @@ RSpec.shared_examples 'User creates wiki page' do end end - it 'creates a wiki page with Org markup', :aggregate_failures do + it 'creates a wiki page with Org markup', :aggregate_failures, :js do org_content = <<~ORG * Heading ** Subheading @@ -170,7 +180,7 @@ RSpec.shared_examples 'User creates wiki page' do visit wiki_path(wiki) end - context "via the `new wiki page` page" do + context "via the `new wiki page` page", :js do it "creates a page with a single word" do click_link("New page") @@ -189,7 +199,7 @@ RSpec.shared_examples 'User creates wiki page' do .and have_content("My awesome wiki!") end - it "creates a page with spaces in the name" do + it "creates a page with spaces in the name", :js do click_link("New page") page.within(".wiki-form") do @@ -207,7 +217,7 @@ RSpec.shared_examples 'User creates wiki page' do .and have_content("My awesome wiki!") end - it "creates a page with hyphens in the name" do + it "creates a page with hyphens in the name", :js do click_link("New page") page.within(".wiki-form") do 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 3350e54a8a7..1e325535e81 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 @@ -90,9 +90,11 @@ RSpec.shared_examples 'User updates wiki page' do expect(page).to have_field('wiki[message]', with: 'Update Wiki title') end - it 'shows a validation error message' do + it 'shows a validation error message if the form is force submitted', :js do fill_in(:wiki_content, with: '') - click_button('Save changes') + + page.execute_script("window.onbeforeunload = null") + page.execute_script("document.querySelector('.wiki-form').submit()") expect(page).to have_selector('.wiki-form') expect(page).to have_content('Edit Page') @@ -101,6 +103,13 @@ RSpec.shared_examples 'User updates wiki page' do expect(find('textarea#wiki_content').value).to eq('') end + it "disables the submit button", :js do + page.within(".wiki-form") do + fill_in(:wiki_content, with: "") + expect(page).to have_button('Save changes', disabled: true) + end + end + it 'shows the emoji autocompletion dropdown', :js do find('#wiki_content').native.send_keys('') fill_in(:wiki_content, with: ':') @@ -108,7 +117,7 @@ RSpec.shared_examples 'User updates wiki page' do expect(page).to have_selector('.atwho-view') end - it 'shows the error message' do + it 'shows the error message', :js do wiki_page.update(content: 'Update') # rubocop:disable Rails/SaveBang click_button('Save changes') @@ -116,13 +125,17 @@ RSpec.shared_examples 'User updates wiki page' do expect(page).to have_content('Someone edited the page the same time you did.') end - it 'updates a page' do + it 'updates a page', :js do fill_in('Content', with: 'Updated Wiki Content') click_on('Save changes') expect(page).to have_content('Updated Wiki Content') end + it 'focuses on the content field', :js do + expect(page).to have_selector '.note-textarea:focus' + end + it 'cancels editing of a page' do page.within(:css, '.wiki-form .form-actions') do click_on('Cancel') @@ -143,7 +156,7 @@ RSpec.shared_examples 'User updates wiki page' do visit wiki_page_path(wiki, wiki_page, action: :edit) end - it 'moves the page to the root folder' do + it 'moves the page to the root folder', :js do fill_in(:wiki_title, with: "/#{page_name}") click_button('Save changes') @@ -151,7 +164,7 @@ RSpec.shared_examples 'User updates wiki page' do expect(current_path).to eq(wiki_page_path(wiki, page_name)) end - it 'moves the page to other dir' do + it 'moves the page to other dir', :js do new_page_dir = "foo1/bar1/#{page_name}" fill_in(:wiki_title, with: new_page_dir) @@ -161,7 +174,7 @@ RSpec.shared_examples 'User updates wiki page' do expect(current_path).to eq(wiki_page_path(wiki, new_page_dir)) end - it 'remains in the same place if title has not changed' do + it 'remains in the same place if title has not changed', :js do original_path = wiki_page_path(wiki, wiki_page) fill_in(:wiki_title, with: page_name) @@ -171,7 +184,7 @@ RSpec.shared_examples 'User updates wiki page' do expect(current_path).to eq(original_path) end - it 'can be moved to a different dir with a different name' do + it 'can be moved to a different dir with a different name', :js do new_page_dir = "foo1/bar1/new_page_name" fill_in(:wiki_title, with: new_page_dir) @@ -181,7 +194,7 @@ RSpec.shared_examples 'User updates wiki page' do expect(current_path).to eq(wiki_page_path(wiki, new_page_dir)) end - it 'can be renamed and moved to the root folder' do + it 'can be renamed and moved to the root folder', :js do new_name = 'new_page_name' fill_in(:wiki_title, with: "/#{new_name}") @@ -191,7 +204,7 @@ RSpec.shared_examples 'User updates wiki page' do expect(current_path).to eq(wiki_page_path(wiki, new_name)) end - it 'squishes the title before creating the page' do + it 'squishes the title before creating the page', :js do new_page_dir = " foo1 / bar1 / #{page_name} " fill_in(:wiki_title, with: new_page_dir) @@ -220,7 +233,7 @@ RSpec.shared_examples 'User updates wiki page' do expect(page).to have_content('Wiki page was successfully updated.') end - it 'shows a validation error when trying to change the content' do + it 'shows a validation error when trying to change the content', :js do fill_in 'Content', with: 'new content' click_on 'Save changes' diff --git a/spec/support/shared_examples/features/wiki/user_uses_wiki_shortcuts_shared_examples.rb b/spec/support/shared_examples/features/wiki/user_uses_wiki_shortcuts_shared_examples.rb index 759cfaf6b1f..857d923785f 100644 --- a/spec/support/shared_examples/features/wiki/user_uses_wiki_shortcuts_shared_examples.rb +++ b/spec/support/shared_examples/features/wiki/user_uses_wiki_shortcuts_shared_examples.rb @@ -15,6 +15,6 @@ RSpec.shared_examples 'User uses wiki shortcuts' do it 'visit edit wiki page using "e" keyboard shortcut', :js do find('body').native.send_key('e') - expect(find('.wiki-page-title')).to have_content('Edit Page') + expect(find('.page-title')).to have_content('Edit Page') end end diff --git a/spec/support/shared_examples/features/wiki/user_views_wiki_empty_shared_examples.rb b/spec/support/shared_examples/features/wiki/user_views_wiki_empty_shared_examples.rb index d7f5b485a82..14180d503df 100644 --- a/spec/support/shared_examples/features/wiki/user_views_wiki_empty_shared_examples.rb +++ b/spec/support/shared_examples/features/wiki/user_views_wiki_empty_shared_examples.rb @@ -53,7 +53,7 @@ RSpec.shared_examples 'User views empty wiki' do if writable element.click_link 'Create your first page' - expect(page).to have_button('Create page') + expect(page).to have_button('Create page', disabled: true) else expect(element).not_to have_link('Create your first page') end diff --git a/spec/support/shared_examples/features/wiki/user_views_wiki_page_shared_examples.rb b/spec/support/shared_examples/features/wiki/user_views_wiki_page_shared_examples.rb index af769be6d4b..61feeff57bb 100644 --- a/spec/support/shared_examples/features/wiki/user_views_wiki_page_shared_examples.rb +++ b/spec/support/shared_examples/features/wiki/user_views_wiki_page_shared_examples.rb @@ -44,7 +44,7 @@ RSpec.shared_examples 'User views a wiki page' do expect(current_path).to include('one/two/three-test') - page.within(:css, '.nav-text') do + page.within(:css, '.wiki-page-header') do expect(page).to have_content('History') end end @@ -69,7 +69,7 @@ RSpec.shared_examples 'User views a wiki page' do click_on('Page history') - within('.nav-text') do + within('.wiki-page-header') do expect(page).to have_content('History') end diff --git a/spec/support/shared_examples/features/wiki/user_views_wiki_sidebar_shared_examples.rb b/spec/support/shared_examples/features/wiki/user_views_wiki_sidebar_shared_examples.rb index a7ba7a8ad07..639eb3f2b99 100644 --- a/spec/support/shared_examples/features/wiki/user_views_wiki_sidebar_shared_examples.rb +++ b/spec/support/shared_examples/features/wiki/user_views_wiki_sidebar_shared_examples.rb @@ -17,23 +17,55 @@ RSpec.shared_examples 'User views wiki sidebar' do create(:wiki_page, wiki: wiki, title: 'another', content: 'another') end - it 'renders a default sidebar when there is no customized sidebar' do - visit wiki_path(wiki) + context 'when there is no custom sidebar' do + before do + visit wiki_path(wiki) + end - expect(page).to have_content('another') - expect(page).not_to have_link('View All Pages') + it 'renders a default sidebar' do + within('.right-sidebar') do + expect(page).to have_content('another') + expect(page).not_to have_link('View All Pages') + end + end + + it 'can create a custom sidebar', :js do + click_on 'Edit sidebar' + fill_in :wiki_content, with: 'My custom sidebar' + click_on 'Create page' + + within('.right-sidebar') do + expect(page).to have_content('My custom sidebar') + expect(page).not_to have_content('another') + end + end end - context 'when there is a customized sidebar' do + context 'when there is a custom sidebar' do before do - create(:wiki_page, wiki: wiki, title: '_sidebar', content: 'My customized sidebar') - end + create(:wiki_page, wiki: wiki, title: '_sidebar', content: 'My custom sidebar') - it 'renders my customized sidebar instead of the default one' do visit wiki_path(wiki) + end + + it 'renders the custom sidebar instead of the default one' do + within('.right-sidebar') do + expect(page).to have_content('My custom sidebar') + expect(page).not_to have_content('another') + end + end + + it 'can edit the custom sidebar', :js do + click_on 'Edit sidebar' + + expect(page).to have_field(:wiki_content, with: 'My custom sidebar') + + fill_in :wiki_content, with: 'My other custom sidebar' + click_on 'Save changes' - expect(page).to have_content('My customized sidebar') - expect(page).not_to have_content('Another') + within('.right-sidebar') do + expect(page).to have_content('My other custom sidebar') + end end end end diff --git a/spec/support/shared_examples/finders/packages/debian/distributions_finder_shared_examples.rb b/spec/support/shared_examples/finders/packages/debian/distributions_finder_shared_examples.rb new file mode 100644 index 00000000000..2700d29bf0e --- /dev/null +++ b/spec/support/shared_examples/finders/packages/debian/distributions_finder_shared_examples.rb @@ -0,0 +1,79 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.shared_examples 'Debian Distributions Finder' do |factory, can_freeze| + let_it_be(:distribution_with_suite, freeze: can_freeze) { create(factory, suite: 'mysuite') } + let_it_be(:container) { distribution_with_suite.container } + let_it_be(:distribution_with_same_container, freeze: can_freeze) { create(factory, container: container ) } + let_it_be(:distribution_with_same_codename, freeze: can_freeze) { create(factory, codename: distribution_with_suite.codename ) } + let_it_be(:distribution_with_same_suite, freeze: can_freeze) { create(factory, suite: distribution_with_suite.suite ) } + let_it_be(:distribution_with_codename_and_suite_flipped, freeze: can_freeze) { create(factory, codename: distribution_with_suite.suite, suite: distribution_with_suite.codename) } + + let(:params) { {} } + let(:service) { described_class.new(container, params) } + + subject { service.execute.to_a } + + context 'by codename' do + context 'with existing codename' do + let(:params) { { codename: distribution_with_suite.codename } } + + it 'finds distributions by codename' do + is_expected.to contain_exactly(distribution_with_suite) + end + end + + context 'with non-existing codename' do + let(:params) { { codename: 'does_not_exists' } } + + it 'finds nothing' do + is_expected.to be_empty + end + end + end + + context 'by suite' do + context 'with existing suite' do + let(:params) { { suite: 'mysuite' } } + + it 'finds distribution by suite' do + is_expected.to contain_exactly(distribution_with_suite) + end + end + + context 'with non-existing suite' do + let(:params) { { suite: 'does_not_exists' } } + + it 'finds nothing' do + is_expected.to be_empty + end + end + end + + context 'by codename_or_suite' do + context 'with existing codename' do + let(:params) { { codename_or_suite: distribution_with_suite.codename } } + + it 'finds distribution by codename' do + is_expected.to contain_exactly(distribution_with_suite) + end + end + + context 'with existing suite' do + let(:params) { { codename_or_suite: 'mysuite' } } + + it 'finds distribution by suite' do + is_expected.to contain_exactly(distribution_with_suite) + end + end + + context 'with non-existing suite' do + let(:params) { { codename_or_suite: 'does_not_exists' } } + + it 'finds nothing' do + is_expected.to be_empty + end + end + end +end diff --git a/spec/support/shared_examples/finders/packages_shared_examples.rb b/spec/support/shared_examples/finders/packages_shared_examples.rb new file mode 100644 index 00000000000..52976565b21 --- /dev/null +++ b/spec/support/shared_examples/finders/packages_shared_examples.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'concerning versionless param' do + let_it_be(:versionless_package) { create(:maven_package, project: project, version: nil) } + + it { is_expected.not_to include(versionless_package) } + + context 'with valid include_versionless param' do + let(:params) { { include_versionless: true } } + + it { is_expected.to include(versionless_package) } + end + + context 'with empty include_versionless param' do + let(:params) { { include_versionless: '' } } + + it { is_expected.not_to include(versionless_package) } + end +end diff --git a/spec/support/shared_examples/graphql/mutations/http_integrations_shared_examples.rb b/spec/support/shared_examples/graphql/mutations/http_integrations_shared_examples.rb new file mode 100644 index 00000000000..0338eb43f8d --- /dev/null +++ b/spec/support/shared_examples/graphql/mutations/http_integrations_shared_examples.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'creating a new HTTP integration' do + it 'creates a new integration' do + post_graphql_mutation(mutation, current_user: current_user) + + new_integration = ::AlertManagement::HttpIntegration.last! + integration_response = mutation_response['integration'] + + expect(response).to have_gitlab_http_status(:success) + expect(integration_response['id']).to eq(GitlabSchema.id_from_object(new_integration).to_s) + expect(integration_response['type']).to eq('HTTP') + expect(integration_response['name']).to eq(new_integration.name) + expect(integration_response['active']).to eq(new_integration.active) + expect(integration_response['token']).to eq(new_integration.token) + expect(integration_response['url']).to eq(new_integration.url) + expect(integration_response['apiUrl']).to eq(nil) + end +end diff --git a/spec/support/shared_examples/graphql/projects/merge_request_n_plus_one_query_examples.rb b/spec/support/shared_examples/graphql/projects/merge_request_n_plus_one_query_examples.rb index 397e22ace28..738edd43c92 100644 --- a/spec/support/shared_examples/graphql/projects/merge_request_n_plus_one_query_examples.rb +++ b/spec/support/shared_examples/graphql/projects/merge_request_n_plus_one_query_examples.rb @@ -2,10 +2,12 @@ shared_examples 'N+1 query check' do it 'prevents N+1 queries' do execute_query # "warm up" to prevent undeterministic counts + expect(graphql_errors).to be_blank # Sanity check - ex falso quodlibet! - control_count = ActiveRecord::QueryRecorder.new { execute_query }.count + control = ActiveRecord::QueryRecorder.new { execute_query } + expect(control.count).to be > 0 search_params[:iids] << extra_iid_for_second_query - expect { execute_query }.not_to exceed_query_limit(control_count) + expect { execute_query }.not_to exceed_query_limit(control) end end diff --git a/spec/support/shared_examples/lib/banzai/filters/sanitization_filter_shared_examples.rb b/spec/support/shared_examples/lib/banzai/filters/sanitization_filter_shared_examples.rb index 134e38833cf..b5c07f45d59 100644 --- a/spec/support/shared_examples/lib/banzai/filters/sanitization_filter_shared_examples.rb +++ b/spec/support/shared_examples/lib/banzai/filters/sanitization_filter_shared_examples.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true -RSpec.shared_examples 'default whitelist' do - it 'sanitizes tags that are not whitelisted' do +RSpec.shared_examples 'default allowlist' do + it 'sanitizes tags that are not allowed' do act = %q{ and no blinks} exp = 'no inputs and no blinks' expect(filter(act).to_html).to eq exp diff --git a/spec/support/shared_examples/lib/gitlab/cycle_analytics/base_stage_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/cycle_analytics/base_stage_shared_examples.rb deleted file mode 100644 index d76089d56dd..00000000000 --- a/spec/support/shared_examples/lib/gitlab/cycle_analytics/base_stage_shared_examples.rb +++ /dev/null @@ -1,74 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -ISSUES_MEDIAN = 30.minutes.to_i - -RSpec.shared_examples 'base stage' do - let(:stage) { described_class.new(options: { project: double }) } - - before do - allow(stage).to receive(:project_median).and_return(1.12) - allow_next_instance_of(Gitlab::CycleAnalytics::BaseEventFetcher) do |instance| - allow(instance).to receive(:event_result).and_return({}) - end - end - - it 'has the median data value' do - expect(stage.as_json[:value]).not_to be_nil - end - - it 'has the median data stage' do - expect(stage.as_json[:title]).not_to be_nil - end - - it 'has the median data description' do - expect(stage.as_json[:description]).not_to be_nil - end - - it 'has the title' do - expect(stage.title).to eq(stage_name.to_s.capitalize) - end - - it 'has the events' do - expect(stage.events).not_to be_nil - end -end - -RSpec.shared_examples 'calculate #median with date range' do - context 'when valid date range is given' do - before do - stage_options[:from] = 5.days.ago - stage_options[:to] = 5.days.from_now - end - - it { expect(stage.project_median).to eq(ISSUES_MEDIAN) } - end - - context 'when records are out of the date range' do - before do - stage_options[:from] = 2.years.ago - stage_options[:to] = 1.year.ago - end - - it { expect(stage.project_median).to eq(nil) } - end -end - -RSpec.shared_examples 'Gitlab::Analytics::CycleAnalytics::DataCollector backend examples' do - let(:stage_params) { Gitlab::Analytics::CycleAnalytics::DefaultStages.send("params_for_#{stage_name}_stage").merge(project: project) } - let(:stage) { Analytics::CycleAnalytics::ProjectStage.new(stage_params) } - let(:data_collector) { Gitlab::Analytics::CycleAnalytics::DataCollector.new(stage: stage, params: { from: stage_options[:from], current_user: project.creator }) } - let(:attribute_to_verify) { :title } - - context 'provides the same results as the old implementation' do - it 'for the median' do - expect(data_collector.median.seconds).to be_within(0.5).of(ISSUES_MEDIAN) - end - - it 'for the list of event records' do - records = data_collector.records_fetcher.serialized_records - expect(records.map { |event| event[attribute_to_verify] }).to eq(expected_ordered_attribute_values) - end - end -end diff --git a/spec/support/shared_examples/lib/gitlab/cycle_analytics/default_query_config_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/cycle_analytics/default_query_config_shared_examples.rb deleted file mode 100644 index 4f648b27ea2..00000000000 --- a/spec/support/shared_examples/lib/gitlab/cycle_analytics/default_query_config_shared_examples.rb +++ /dev/null @@ -1,16 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.shared_examples 'default query config' do - let(:project) { create(:project) } - let(:event) { described_class.new(stage: stage_name, options: { from: 1.day.ago, project: project }) } - - it 'has the stage attribute' do - expect(event.stage).not_to be_nil - end - - it 'has the projection attributes' do - expect(event.projections).not_to be_nil - end -end diff --git a/spec/support/shared_examples/lib/gitlab/middleware/multipart_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/middleware/multipart_shared_examples.rb index 6327367fcc2..40deaa27955 100644 --- a/spec/support/shared_examples/lib/gitlab/middleware/multipart_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/middleware/multipart_shared_examples.rb @@ -5,7 +5,7 @@ RSpec.shared_examples 'handling all upload parameters conditions' do include_context 'with one temporary file for multipart' let(:rewritten_fields) { rewritten_fields_hash('file' => uploaded_filepath) } - let(:params) { upload_parameters_for(filepath: uploaded_filepath, key: 'file', filename: filename, remote_id: remote_id) } + let(:params) { upload_parameters_for(filepath: uploaded_filepath, key: 'file', mode: mode, filename: filename, remote_id: remote_id) } it 'builds an UploadedFile' do expect_uploaded_files(filepath: uploaded_filepath, original_filename: filename, remote_id: remote_id, size: uploaded_file.size, params_path: %w(file)) @@ -19,8 +19,8 @@ RSpec.shared_examples 'handling all upload parameters conditions' do let(:rewritten_fields) { rewritten_fields_hash('file1' => uploaded_filepath, 'file2' => uploaded_filepath2) } let(:params) do - upload_parameters_for(filepath: uploaded_filepath, key: 'file1', filename: filename, remote_id: remote_id).merge( - upload_parameters_for(filepath: uploaded_filepath2, key: 'file2', filename: filename2, remote_id: remote_id2) + upload_parameters_for(filepath: uploaded_filepath, key: 'file1', mode: mode, filename: filename, remote_id: remote_id).merge( + upload_parameters_for(filepath: uploaded_filepath2, key: 'file2', mode: mode, filename: filename2, remote_id: remote_id2) ) end @@ -38,7 +38,7 @@ RSpec.shared_examples 'handling all upload parameters conditions' do include_context 'with one temporary file for multipart' let(:rewritten_fields) { rewritten_fields_hash('user[avatar]' => uploaded_filepath) } - let(:params) { { 'user' => { 'avatar' => upload_parameters_for(filepath: uploaded_filepath, filename: filename, remote_id: remote_id) } } } + let(:params) { { 'user' => { 'avatar' => upload_parameters_for(filepath: uploaded_filepath, mode: mode, filename: filename, remote_id: remote_id) } } } it 'builds an UploadedFile' do expect_uploaded_files(filepath: uploaded_filepath, original_filename: filename, remote_id: remote_id, size: uploaded_file.size, params_path: %w(user avatar)) @@ -54,8 +54,8 @@ RSpec.shared_examples 'handling all upload parameters conditions' do let(:params) do { 'user' => { - 'avatar' => upload_parameters_for(filepath: uploaded_filepath, filename: filename, remote_id: remote_id), - 'screenshot' => upload_parameters_for(filepath: uploaded_filepath2, filename: filename2, remote_id: remote_id2) + 'avatar' => upload_parameters_for(filepath: uploaded_filepath, mode: mode, filename: filename, remote_id: remote_id), + 'screenshot' => upload_parameters_for(filepath: uploaded_filepath2, mode: mode, filename: filename2, remote_id: remote_id2) } } end @@ -74,7 +74,7 @@ RSpec.shared_examples 'handling all upload parameters conditions' do include_context 'with one temporary file for multipart' let(:rewritten_fields) { rewritten_fields_hash('user[avatar][bananas]' => uploaded_filepath) } - let(:params) { { 'user' => { 'avatar' => { 'bananas' => upload_parameters_for(filepath: uploaded_filepath, filename: filename, remote_id: remote_id) } } } } + let(:params) { { 'user' => { 'avatar' => { 'bananas' => upload_parameters_for(filepath: uploaded_filepath, mode: mode, filename: filename, remote_id: remote_id) } } } } it 'builds an UploadedFile' do expect_uploaded_files(filepath: uploaded_file, original_filename: filename, remote_id: remote_id, size: uploaded_file.size, params_path: %w(user avatar bananas)) @@ -91,10 +91,10 @@ RSpec.shared_examples 'handling all upload parameters conditions' do { 'user' => { 'avatar' => { - 'bananas' => upload_parameters_for(filepath: uploaded_filepath, filename: filename, remote_id: remote_id) + 'bananas' => upload_parameters_for(filepath: uploaded_filepath, mode: mode, filename: filename, remote_id: remote_id) }, 'friend' => { - 'ananas' => upload_parameters_for(filepath: uploaded_filepath2, filename: filename2, remote_id: remote_id2) + 'ananas' => upload_parameters_for(filepath: uploaded_filepath2, mode: mode, filename: filename2, remote_id: remote_id2) } } } @@ -122,11 +122,11 @@ RSpec.shared_examples 'handling all upload parameters conditions' do end let(:params) do - upload_parameters_for(filepath: uploaded_filepath, filename: filename, key: 'file', remote_id: remote_id).merge( + upload_parameters_for(filepath: uploaded_filepath, filename: filename, key: 'file', mode: mode, remote_id: remote_id).merge( 'user' => { - 'avatar' => upload_parameters_for(filepath: uploaded_filepath2, filename: filename2, remote_id: remote_id2), + 'avatar' => upload_parameters_for(filepath: uploaded_filepath2, mode: mode, filename: filename2, remote_id: remote_id2), 'friend' => { - 'avatar' => upload_parameters_for(filepath: uploaded_filepath3, filename: filename3, remote_id: remote_id3) + 'avatar' => upload_parameters_for(filepath: uploaded_filepath3, mode: mode, filename: filename3, remote_id: remote_id3) } } ) diff --git a/spec/support/shared_examples/lib/gitlab/project_search_results_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/project_search_results_shared_examples.rb index 94ef41ce5a5..f83fecee4ea 100644 --- a/spec/support/shared_examples/lib/gitlab/project_search_results_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/project_search_results_shared_examples.rb @@ -54,7 +54,7 @@ RSpec.shared_examples 'access restricted confidential issues' do end end - context 'when the user is a developper' do + context 'when the user is a developer' do let(:user) do create(:user) { |user| project.add_developer(user) } end @@ -70,10 +70,19 @@ RSpec.shared_examples 'access restricted confidential issues' do context 'when the user is admin', :request_store do let(:user) { create(:user, admin: true) } - it 'lists all project issues' do - expect(objects).to contain_exactly(issue, - security_issue_1, - security_issue_2) + context 'when admin mode is enabled', :enable_admin_mode do + it 'lists all project issues' do + expect(objects).to contain_exactly(issue, + security_issue_1, + security_issue_2) + end + end + + context 'when admin mode is disabled' do + it 'does not list project confidential issues' do + expect(objects).to contain_exactly(issue) + expect(results.limited_issues_count).to eq 1 + end end end end diff --git a/spec/support/shared_examples/lib/gitlab/usage_data_counters/incident_management_activity_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/usage_data_counters/incident_management_activity_shared_examples.rb index 788c35dd5bf..88bc8e8d0c1 100644 --- a/spec/support/shared_examples/lib/gitlab/usage_data_counters/incident_management_activity_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/usage_data_counters/incident_management_activity_shared_examples.rb @@ -13,7 +13,7 @@ RSpec.shared_examples 'an incident management tracked event' do |event| expect(Gitlab::UsageDataCounters::HLLRedisCounter) .to receive(:track_event) - .with(current_user.id, event.to_s) + .with(event.to_s, values: current_user.id) .and_call_original expect { subject } diff --git a/spec/support/shared_examples/metrics/sampler_shared_examples.rb b/spec/support/shared_examples/metrics/sampler_shared_examples.rb new file mode 100644 index 00000000000..ebf199c3a8d --- /dev/null +++ b/spec/support/shared_examples/metrics/sampler_shared_examples.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'metrics sampler' do |env_prefix| + context 'when sampling interval is passed explicitly' do + subject { described_class.new(42) } + + specify { expect(subject.interval).to eq(42) } + end + + context 'when sampling interval is passed through the environment' do + subject { described_class.new } + + before do + stub_env("#{env_prefix}_INTERVAL_SECONDS", '42') + end + + specify { expect(subject.interval).to eq(42) } + end + + context 'when no sampling interval is passed anywhere' do + subject { described_class.new } + + it 'uses the hardcoded default' do + expect(subject.interval).to eq(described_class::DEFAULT_SAMPLING_INTERVAL_SECONDS) + end + end +end diff --git a/spec/support/shared_examples/models/boards/listable_shared_examples.rb b/spec/support/shared_examples/models/boards/listable_shared_examples.rb new file mode 100644 index 00000000000..e733a5488fb --- /dev/null +++ b/spec/support/shared_examples/models/boards/listable_shared_examples.rb @@ -0,0 +1,97 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'boards listable model' do |list_factory| + subject { build(list_factory) } + + describe 'associations' do + it { is_expected.to validate_presence_of(:position) } + it { is_expected.to validate_numericality_of(:position).only_integer.is_greater_than_or_equal_to(0) } + + context 'when list_type is set to closed' do + subject { build(list_factory, list_type: :closed) } + + it { is_expected.not_to validate_presence_of(:label) } + it { is_expected.not_to validate_presence_of(:position) } + end + end + + describe 'scopes' do + describe '.ordered' do + it 'returns lists ordered by type and position' do + # rubocop:disable Rails/SaveBang + lists = [ + create(list_factory, list_type: :backlog), + create(list_factory, list_type: :closed), + create(list_factory, position: 1), + create(list_factory, position: 2) + ] + # rubocop:enable Rails/SaveBang + + expect(described_class.where(id: lists).ordered).to eq([lists[0], lists[2], lists[3], lists[1]]) + end + end + end + + describe '#destroyable?' do + it 'returns true when list_type is set to label' do + subject.list_type = :label + + expect(subject).to be_destroyable + end + + it 'returns false when list_type is set to closed' do + subject.list_type = :closed + + expect(subject).not_to be_destroyable + end + end + + describe '#movable?' do + it 'returns true when list_type is set to label' do + subject.list_type = :label + + expect(subject).to be_movable + end + + it 'returns false when list_type is set to closed' do + subject.list_type = :closed + + expect(subject).not_to be_movable + end + end + + describe '#title' do + it 'returns label name when list_type is set to label' do + subject.list_type = :label + subject.label = Label.new(name: 'Development') + + expect(subject.title).to eq 'Development' + end + + it 'returns Open when list_type is set to backlog' do + subject.list_type = :backlog + + expect(subject.title).to eq 'Open' + end + + it 'returns Closed when list_type is set to closed' do + subject.list_type = :closed + + expect(subject.title).to eq 'Closed' + end + end + + describe '#destroy' do + it 'can be destroyed when list_type is set to label' do + subject = create(list_factory) # rubocop:disable Rails/SaveBang + + expect(subject.destroy).to be_truthy + end + + it 'can not be destroyed when list_type is set to closed' do + subject = create(list_factory, list_type: :closed) # rubocop:disable Rails/SaveBang + + expect(subject.destroy).to be_falsey + end + end +end diff --git a/spec/support/shared_examples/models/concerns/can_housekeep_repository_shared_examples.rb b/spec/support/shared_examples/models/concerns/can_housekeep_repository_shared_examples.rb new file mode 100644 index 00000000000..2f0b95427d2 --- /dev/null +++ b/spec/support/shared_examples/models/concerns/can_housekeep_repository_shared_examples.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'can housekeep repository' do + context 'with a clean redis state', :clean_gitlab_redis_shared_state do + describe '#pushes_since_gc' do + context 'without any pushes' do + it 'returns 0' do + expect(resource.pushes_since_gc).to eq(0) + end + end + + context 'with a number of pushes' do + it 'returns the number of pushes' do + 3.times { resource.increment_pushes_since_gc } + + expect(resource.pushes_since_gc).to eq(3) + end + end + end + + describe '#increment_pushes_since_gc' do + it 'increments the number of pushes since the last GC' do + 3.times { resource.increment_pushes_since_gc } + + expect(resource.pushes_since_gc).to eq(3) + end + end + + describe '#reset_pushes_since_gc' do + it 'resets the number of pushes since the last GC' do + 3.times { resource.increment_pushes_since_gc } + + resource.reset_pushes_since_gc + + expect(resource.pushes_since_gc).to eq(0) + end + end + + describe '#pushes_since_gc_redis_shared_state_key' do + it 'returns the proper redis key format' do + expect(resource.send(:pushes_since_gc_redis_shared_state_key)).to eq("#{resource_key}/#{resource.id}/pushes_since_gc") + end + end + end +end diff --git a/spec/support/shared_examples/models/concerns/repositories/can_housekeep_repository_shared_examples.rb b/spec/support/shared_examples/models/concerns/repositories/can_housekeep_repository_shared_examples.rb new file mode 100644 index 00000000000..2f0b95427d2 --- /dev/null +++ b/spec/support/shared_examples/models/concerns/repositories/can_housekeep_repository_shared_examples.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'can housekeep repository' do + context 'with a clean redis state', :clean_gitlab_redis_shared_state do + describe '#pushes_since_gc' do + context 'without any pushes' do + it 'returns 0' do + expect(resource.pushes_since_gc).to eq(0) + end + end + + context 'with a number of pushes' do + it 'returns the number of pushes' do + 3.times { resource.increment_pushes_since_gc } + + expect(resource.pushes_since_gc).to eq(3) + end + end + end + + describe '#increment_pushes_since_gc' do + it 'increments the number of pushes since the last GC' do + 3.times { resource.increment_pushes_since_gc } + + expect(resource.pushes_since_gc).to eq(3) + end + end + + describe '#reset_pushes_since_gc' do + it 'resets the number of pushes since the last GC' do + 3.times { resource.increment_pushes_since_gc } + + resource.reset_pushes_since_gc + + expect(resource.pushes_since_gc).to eq(0) + end + end + + describe '#pushes_since_gc_redis_shared_state_key' do + it 'returns the proper redis key format' do + expect(resource.send(:pushes_since_gc_redis_shared_state_key)).to eq("#{resource_key}/#{resource.id}/pushes_since_gc") + end + 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 5a8388d01df..4c617f3ba46 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 @@ -63,7 +63,6 @@ RSpec.shared_examples 'handles repository moves' do context 'and transits to scheduled' do it 'triggers the corresponding repository storage worker' do - skip unless repository_storage_worker # TODO remove after https://gitlab.com/gitlab-org/gitlab/-/issues/218991 is implemented expect(repository_storage_worker).to receive(:perform_async).with(container.id, 'test_second_storage', storage_move.id) storage_move.schedule! @@ -72,8 +71,7 @@ RSpec.shared_examples 'handles repository moves' do end context 'when the transition fails' do - it 'does not trigger ProjectUpdateRepositoryStorageWorker and adds an error' do - skip unless repository_storage_worker # TODO remove after https://gitlab.com/gitlab-org/gitlab/-/issues/218991 is implemented + it 'does not trigger the corresponding repository storage worker and adds an error' do allow(storage_move.container).to receive(:set_repository_read_only!).and_raise(StandardError, 'foobar') expect(repository_storage_worker).not_to receive(:perform_async) 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 new file mode 100644 index 00000000000..38983f752f4 --- /dev/null +++ b/spec/support/shared_examples/models/packages/debian/architecture_shared_examples.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +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(:architecture_same_name, freeze: can_freeze) { create(factory, name: architecture.name) } + + subject { architecture } + + describe 'relationships' do + it { is_expected.to belong_to(:distribution).class_name("Packages::Debian::#{container.capitalize}Distribution").inverse_of(:architectures) } + end + + describe 'validations' do + describe "#distribution" do + it { is_expected.to validate_presence_of(:distribution) } + end + + describe '#name' do + it { is_expected.to validate_presence_of(:name) } + + it { is_expected.to allow_value('amd64').for(:name) } + it { is_expected.to allow_value('kfreebsd-i386').for(:name) } + it { is_expected.not_to allow_value('-a').for(:name) } + it { is_expected.not_to allow_value('AMD64').for(:name) } + end + end + + describe 'scopes' do + describe '.with_distribution' do + subject { described_class.with_distribution(architecture.distribution) } + + it 'does not return other distributions' do + expect(subject.to_a).to eq([architecture, architecture_same_distribution]) + end + end + + describe '.with_name' do + subject { described_class.with_name(architecture.name) } + + it 'does not return other distributions' do + expect(subject.to_a).to eq([architecture, architecture_same_name]) + end + end + end +end diff --git a/spec/support/shared_examples/models/packages/debian/distribution_shared_examples.rb b/spec/support/shared_examples/models/packages/debian/distribution_shared_examples.rb new file mode 100644 index 00000000000..af87d30099f --- /dev/null +++ b/spec/support/shared_examples/models/packages/debian/distribution_shared_examples.rb @@ -0,0 +1,225 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.shared_examples 'Debian Distribution' do |factory, container, can_freeze| + let_it_be(:distribution_with_suite, freeze: can_freeze) { create(factory, suite: 'mysuite') } + let_it_be(:distribution_with_same_container, freeze: can_freeze) { create(factory, container: distribution_with_suite.container ) } + let_it_be(:distribution_with_same_codename, freeze: can_freeze) { create(factory, codename: distribution_with_suite.codename ) } + let_it_be(:distribution_with_same_suite, freeze: can_freeze) { create(factory, suite: distribution_with_suite.suite ) } + let_it_be(:distribution_with_codename_and_suite_flipped, freeze: can_freeze) { create(factory, codename: distribution_with_suite.suite, suite: distribution_with_suite.codename) } + + let_it_be_with_refind(:distribution) { create(factory, container: distribution_with_suite.container ) } + + subject { distribution } + + describe 'relationships' do + it { is_expected.to belong_to(container) } + it { is_expected.to belong_to(:creator).class_name('User') } + + it { is_expected.to have_many(:architectures).class_name("Packages::Debian::#{container.capitalize}Architecture").inverse_of(:distribution) } + end + + describe 'validations' do + describe "##{container}" do + it { is_expected.to validate_presence_of(container) } + end + + describe "#creator" do + it { is_expected.not_to validate_presence_of(:creator) } + end + + describe '#codename' do + it { is_expected.to validate_presence_of(:codename) } + + it { is_expected.to allow_value('buster').for(:codename) } + it { is_expected.to allow_value('buster-updates').for(:codename) } + it { is_expected.to allow_value('Debian10.5').for(:codename) } + it { is_expected.not_to allow_value('jessie/updates').for(:codename) } + it { is_expected.not_to allow_value('hé').for(:codename) } + end + + describe '#suite' do + it { is_expected.to allow_value(nil).for(:suite) } + it { is_expected.to allow_value('testing').for(:suite) } + it { is_expected.not_to allow_value('hé').for(:suite) } + end + + describe '#unique_debian_suite_and_codename' do + using RSpec::Parameterized::TableSyntax + + where(:with_existing_suite, :suite, :codename, :errors) do + false | nil | :keep | nil + false | 'testing' | :keep | nil + false | nil | :codename | ["Codename has already been taken"] + false | :codename | :keep | ["Suite has already been taken as Codename"] + false | :codename | :codename | ["Codename has already been taken", "Suite has already been taken as Codename"] + true | nil | :keep | nil + true | 'testing' | :keep | nil + true | nil | :codename | ["Codename has already been taken"] + true | :codename | :keep | ["Suite has already been taken as Codename"] + true | :codename | :codename | ["Codename has already been taken", "Suite has already been taken as Codename"] + true | nil | :suite | ["Codename has already been taken as Suite"] + true | :suite | :keep | ["Suite has already been taken"] + true | :suite | :suite | ["Suite has already been taken", "Codename has already been taken as Suite"] + end + + with_them do + context factory do + let(:new_distribution) { build(factory, container: distribution.container) } + + before do + distribution.update_column(:suite, 'suite-' + distribution.codename) if with_existing_suite + + if suite.is_a?(Symbol) + new_distribution.suite = distribution.send suite unless suite == :keep + else + new_distribution.suite = suite + end + + if codename.is_a?(Symbol) + new_distribution.codename = distribution.send codename unless codename == :keep + else + new_distribution.codename = codename + end + end + + it do + if errors + expect(new_distribution).not_to be_valid + expect(new_distribution.errors.to_a).to eq(errors) + else + expect(new_distribution).to be_valid + end + end + end + end + end + + describe '#origin' do + it { is_expected.to allow_value(nil).for(:origin) } + it { is_expected.to allow_value('Debian').for(:origin) } + it { is_expected.not_to allow_value('hé').for(:origin) } + end + + describe '#label' do + it { is_expected.to allow_value(nil).for(:label) } + it { is_expected.to allow_value('Debian').for(:label) } + it { is_expected.not_to allow_value('hé').for(:label) } + end + + describe '#version' do + it { is_expected.to allow_value(nil).for(:version) } + it { is_expected.to allow_value('10.6').for(:version) } + it { is_expected.not_to allow_value('hé').for(:version) } + end + + describe '#description' do + it { is_expected.to allow_value(nil).for(:description) } + it { is_expected.to allow_value('Debian 10.6 Released 26 September 2020').for(:description) } + it { is_expected.to allow_value('Hé !').for(:description) } + end + + describe '#valid_time_duration_seconds' do + it { is_expected.to allow_value(nil).for(:valid_time_duration_seconds) } + it { is_expected.to allow_value(24.hours.to_i).for(:valid_time_duration_seconds) } + it { is_expected.not_to allow_value(12.hours.to_i).for(:valid_time_duration_seconds) } + end + + describe '#signing_keys' do + it { is_expected.to validate_absence_of(:signing_keys) } + end + + describe '#file' do + it { is_expected.not_to validate_presence_of(:file) } + end + + describe '#file_store' do + it { is_expected.to validate_presence_of(:file_store) } + end + + describe '#file_signature' do + it { is_expected.to validate_absence_of(:file_signature) } + end + end + + describe 'scopes' do + describe '.with_container' do + subject { described_class.with_container(distribution_with_suite.container) } + + it 'does not return other distributions' do + expect(subject).to match_array([distribution_with_suite, distribution, distribution_with_same_container]) + end + end + + describe '.with_codename' do + subject { described_class.with_codename(distribution_with_suite.codename) } + + it 'does not return other distributions' do + expect(subject).to match_array([distribution_with_suite, distribution_with_same_codename]) + end + end + + describe '.with_suite' do + subject { described_class.with_suite(distribution_with_suite.suite) } + + it 'does not return other distributions' do + expect(subject).to match_array([distribution_with_suite, distribution_with_same_suite]) + end + end + + describe '.with_codename_or_suite' do + describe 'passing codename' do + subject { described_class.with_codename_or_suite(distribution_with_suite.codename) } + + it 'does not return other distributions' do + expect(subject.to_a).to eq([distribution_with_suite, distribution_with_same_codename, distribution_with_codename_and_suite_flipped]) + end + end + + describe 'passing suite' do + subject { described_class.with_codename_or_suite(distribution_with_suite.suite) } + + it 'does not return other distributions' do + expect(subject.to_a).to eq([distribution_with_suite, distribution_with_same_suite, distribution_with_codename_and_suite_flipped]) + end + end + end + end + + describe '#needs_update?' do + subject { distribution.needs_update? } + + context 'with new distribution' do + let(:distribution) { create(factory, container: distribution_with_suite.container) } + + it { is_expected.to be_truthy } + end + + context 'with file' do + context 'without valid_time_duration_seconds' do + let(:distribution) { create(factory, :with_file, container: distribution_with_suite.container) } + + it { is_expected.to be_falsey } + end + + context 'with valid_time_duration_seconds' do + let(:distribution) { create(factory, :with_file, container: distribution_with_suite.container, valid_time_duration_seconds: 2.days.to_i) } + + context 'when not yet expired' do + it { is_expected.to be_falsey } + end + + context 'when expired' do + it do + distribution + + travel_to(4.days.from_now) do + is_expected.to be_truthy + end + end + end + end + end + end +end diff --git a/spec/support/shared_examples/quick_actions/merge_request/rebase_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/merge_request/rebase_quick_action_shared_examples.rb new file mode 100644 index 00000000000..28decb4011d --- /dev/null +++ b/spec/support/shared_examples/quick_actions/merge_request/rebase_quick_action_shared_examples.rb @@ -0,0 +1,92 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'rebase quick action' do + context 'when updating the description' do + before do + sign_in(user) + visit edit_project_merge_request_path(project, merge_request) + end + + it 'rebases the MR', :sidekiq_inline do + fill_in('Description', with: '/rebase') + click_button('Save changes') + + expect(page).not_to have_content('commit behind the target branch') + expect(merge_request.reload).not_to be_merged + end + + it 'ignores /merge if /rebase is specified', :sidekiq_inline do + fill_in('Description', with: "/merge\n/rebase") + click_button('Save changes') + + expect(page).not_to have_content('commit behind the target branch') + expect(merge_request.reload).not_to be_merged + end + end + + context 'when creating a new note' do + context 'when the current user can rebase the MR' do + before do + sign_in(user) + visit project_merge_request_path(project, merge_request) + end + + it 'rebase the MR', :sidekiq_inline do + add_note("/rebase") + + expect(page).to have_content "Scheduled a rebase of branch #{merge_request.source_branch}." + end + + context 'when the merge request is closed' do + before do + merge_request.close! + end + + it 'does not rebase the MR', :sidekiq_inline do + add_note("/rebase") + + expect(page).not_to have_content 'Scheduled a rebase' + end + end + + context 'when a rebase is in progress', :sidekiq_inline, :clean_gitlab_redis_shared_state do + before do + jid = SecureRandom.hex + merge_request.update!(rebase_jid: jid) + Gitlab::SidekiqStatus.set(jid) + end + + it 'tells the user a rebase is in progress' do + add_note('/rebase') + + expect(page).to have_content 'A rebase is already in progress.' + expect(page).not_to have_content 'Scheduled a rebase' + end + end + + context 'when there are conflicts in the merge request' do + let!(:merge_request) { create(:merge_request, source_project: project, target_project: project, source_branch: 'conflict-missing-side', target_branch: 'conflict-start', merge_status: :cannot_be_merged) } + + it 'does not rebase the MR' do + add_note("/rebase") + + expect(page).to have_content 'This merge request cannot be rebased while there are conflicts.' + end + end + end + + context 'when the current user cannot rebase the MR' do + before do + project.add_guest(guest) + sign_in(guest) + visit project_merge_request_path(project, merge_request) + end + + it 'does not rebase the MR' do + add_note("/rebase") + + expect(page).not_to have_content 'Scheduled a rebase' + end + end + end +end diff --git a/spec/support/shared_examples/requests/api/boards_shared_examples.rb b/spec/support/shared_examples/requests/api/boards_shared_examples.rb index 0096aab55e3..8e8edd61ef9 100644 --- a/spec/support/shared_examples/requests/api/boards_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/boards_shared_examples.rb @@ -44,16 +44,35 @@ RSpec.shared_examples 'group and project boards' do |route_definition, ee = fals expect_schema_match_for(response, 'public_api/v4/boards', ee) end + end + end - describe "GET #{route_definition}/:board_id" do - let(:url) { "#{root_url}/#{board.id}" } + describe "GET #{route_definition}/:board_id" do + let(:url) { "#{root_url}/#{board.id}" } - it 'get a single board by id' do - get api(url, user) + it 'get a single board by id' do + get api(url, user) - expect_schema_match_for(response, 'public_api/v4/board', ee) - end - end + expect_schema_match_for(response, 'public_api/v4/board', ee) + end + end + + describe "PUT #{route_definition}/:board_id" do + let(:url) { "#{root_url}/#{board.id}" } + + it 'updates the board name' do + put api(url, user), params: { name: 'changed board name' } + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['name']).to eq('changed board name') + end + + it 'updates the issue board booleans' do + put api(url, user), params: { hide_backlog_list: true, hide_closed_list: true } + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['hide_backlog_list']).to eq(true) + expect(json_response['hide_closed_list']).to eq(true) end end diff --git a/spec/support/shared_examples/requests/api/debian_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/debian_packages_shared_examples.rb index f55043fe64f..83ba72c12aa 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 @@ -37,9 +37,9 @@ RSpec.shared_context 'Debian repository shared context' do |object_type| let(:params) { workhorse_params } let(:auth_headers) { {} } + let(:workhorse_token) { JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256') } let(:workhorse_headers) do if method == :put - workhorse_token = JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256') { 'GitLab-Workhorse' => '1.0', Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER => workhorse_token } else {} @@ -117,12 +117,13 @@ RSpec.shared_examples 'Debian project repository PUT request' do |user_role, add and_body = body.nil? ? '' : ' and expected body' if status == :created - it 'creates package files' do + it 'creates package files', :aggregate_failures do pending "Debian package creation not implemented" expect { subject } .to change { project.packages.debian.count }.by(1) 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) @@ -130,7 +131,59 @@ RSpec.shared_examples 'Debian project repository PUT request' do |user_role, add end it_behaves_like 'a package tracking event', described_class.name, 'push_package' else - it "returns #{status}#{and_body}" do + it "returns #{status}#{and_body}", :aggregate_failures do + subject + + expect(response).to have_gitlab_http_status(status) + + unless body.nil? + expect(response.body).to eq(body) + end + 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' + + 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 + + context 'without a valid token' do + let(:workhorse_token) { 'invalid' } + + it 'rejects request' do + subject + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + + context 'bypassing gitlab-workhorse' do + let(:workhorse_headers) { {} } + + it 'rejects request' do + subject + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + else + it "returns #{status}#{and_body}", :aggregate_failures do subject expect(response).to have_gitlab_http_status(status) @@ -194,7 +247,7 @@ RSpec.shared_examples 'Debian project repository GET endpoint' do |success_statu it_behaves_like 'rejects Debian access with unknown project id' end -RSpec.shared_examples 'Debian project repository PUT endpoint' do |success_status, success_body| +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 @@ -221,7 +274,13 @@ RSpec.shared_examples 'Debian project repository PUT endpoint' do |success_statu 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 PUT request', params[:user_role], params[:member], params[:expected_status], params[:expected_body] + 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 diff --git a/spec/support/shared_examples/requests/api/nuget_endpoints_shared_examples.rb b/spec/support/shared_examples/requests/api/nuget_endpoints_shared_examples.rb index f808d12baf4..7b7d2a33e8c 100644 --- a/spec/support/shared_examples/requests/api/nuget_endpoints_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/nuget_endpoints_shared_examples.rb @@ -1,31 +1,31 @@ # frozen_string_literal: true -RSpec.shared_examples 'handling nuget service requests' do +RSpec.shared_examples 'handling nuget service requests' do |anonymous_requests_example_name: 'process nuget service index request', anonymous_requests_status: :success| subject { get api(url) } - context 'with valid project' do + context 'with valid target' do using RSpec::Parameterized::TableSyntax context 'personal token' do - where(:project_visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do - 'PUBLIC' | :developer | true | true | 'process nuget service index request' | :success - 'PUBLIC' | :guest | true | true | 'process nuget service index request' | :success - 'PUBLIC' | :developer | true | false | 'process nuget service index request' | :success - 'PUBLIC' | :guest | true | false | 'process nuget service index request' | :success - 'PUBLIC' | :developer | false | true | 'process nuget service index request' | :success - 'PUBLIC' | :guest | false | true | 'process nuget service index request' | :success - 'PUBLIC' | :developer | false | false | 'process nuget service index request' | :success - 'PUBLIC' | :guest | false | false | 'process nuget service index request' | :success - 'PUBLIC' | :anonymous | false | true | 'process nuget service index request' | :success - 'PRIVATE' | :developer | true | true | 'process nuget service index request' | :success - 'PRIVATE' | :guest | true | true | 'rejects nuget packages access' | :forbidden - 'PRIVATE' | :developer | true | false | 'rejects nuget packages access' | :unauthorized - 'PRIVATE' | :guest | true | false | 'rejects nuget packages access' | :unauthorized - 'PRIVATE' | :developer | false | true | 'rejects nuget packages access' | :not_found - 'PRIVATE' | :guest | false | true | 'rejects nuget packages access' | :not_found - 'PRIVATE' | :developer | false | false | 'rejects nuget packages access' | :unauthorized - 'PRIVATE' | :guest | false | false | 'rejects nuget packages access' | :unauthorized - 'PRIVATE' | :anonymous | false | true | 'rejects nuget packages access' | :unauthorized + where(:visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do + 'PUBLIC' | :developer | true | true | 'process nuget service index request' | :success + 'PUBLIC' | :guest | true | true | 'process nuget service index request' | :success + 'PUBLIC' | :developer | true | false | 'rejects nuget packages access' | :unauthorized + 'PUBLIC' | :guest | true | false | 'rejects nuget packages access' | :unauthorized + 'PUBLIC' | :developer | false | true | 'process nuget service index request' | :success + 'PUBLIC' | :guest | false | true | 'process nuget service index request' | :success + 'PUBLIC' | :developer | false | false | 'rejects nuget packages access' | :unauthorized + 'PUBLIC' | :guest | false | false | 'rejects nuget packages access' | :unauthorized + 'PUBLIC' | :anonymous | false | true | anonymous_requests_example_name | anonymous_requests_status + 'PRIVATE' | :developer | true | true | 'process nuget service index request' | :success + 'PRIVATE' | :guest | true | true | 'rejects nuget packages access' | :forbidden + 'PRIVATE' | :developer | true | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :guest | true | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :developer | false | true | 'rejects nuget packages access' | :not_found + 'PRIVATE' | :guest | false | true | 'rejects nuget packages access' | :not_found + 'PRIVATE' | :developer | false | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :guest | false | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :anonymous | false | true | 'rejects nuget packages access' | :unauthorized end with_them do @@ -35,7 +35,7 @@ RSpec.shared_examples 'handling nuget service requests' do subject { get api(url), headers: headers } before do - project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility_level, false)) + update_visibility_to(Gitlab::VisibilityLevel.const_get(visibility_level, false)) end it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member] @@ -43,7 +43,7 @@ RSpec.shared_examples 'handling nuget service requests' do end context 'with job token' do - where(:project_visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do + where(:visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do 'PUBLIC' | :developer | true | true | 'process nuget service index request' | :success 'PUBLIC' | :guest | true | true | 'process nuget service index request' | :success 'PUBLIC' | :developer | true | false | 'rejects nuget packages access' | :unauthorized @@ -52,7 +52,7 @@ RSpec.shared_examples 'handling nuget service requests' do 'PUBLIC' | :guest | false | true | 'process nuget service index request' | :success 'PUBLIC' | :developer | false | false | 'rejects nuget packages access' | :unauthorized 'PUBLIC' | :guest | false | false | 'rejects nuget packages access' | :unauthorized - 'PUBLIC' | :anonymous | false | true | 'process nuget service index request' | :success + 'PUBLIC' | :anonymous | false | true | anonymous_requests_example_name | anonymous_requests_status 'PRIVATE' | :developer | true | true | 'process nuget service index request' | :success 'PRIVATE' | :guest | true | true | 'rejects nuget packages access' | :forbidden 'PRIVATE' | :developer | true | false | 'rejects nuget packages access' | :unauthorized @@ -71,7 +71,7 @@ RSpec.shared_examples 'handling nuget service requests' do subject { get api(url), headers: headers } before do - project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility_level, false)) + update_visibility_to(Gitlab::VisibilityLevel.const_get(visibility_level, false)) end it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member] @@ -79,14 +79,18 @@ RSpec.shared_examples 'handling nuget service requests' do end end - it_behaves_like 'deploy token for package GET requests' + it_behaves_like 'deploy token for package GET requests' do + before do + update_visibility_to(Gitlab::VisibilityLevel::PRIVATE) + end + end - it_behaves_like 'rejects nuget access with unknown project id' + it_behaves_like 'rejects nuget access with unknown target id' - it_behaves_like 'rejects nuget access with invalid project id' + it_behaves_like 'rejects nuget access with invalid target id' end -RSpec.shared_examples 'handling nuget metadata requests with package name' do +RSpec.shared_examples 'handling nuget metadata requests with package name' do |anonymous_requests_example_name: 'process nuget metadata request at package name level', anonymous_requests_status: :success| include_context 'with expected presenters dependency groups' let_it_be(:package_name) { 'Dummy.Package' } @@ -99,19 +103,19 @@ RSpec.shared_examples 'handling nuget metadata requests with package name' do packages.each { |pkg| create_dependencies_for(pkg) } end - context 'with valid project' do + context 'with valid target' do using RSpec::Parameterized::TableSyntax - where(:project_visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do + where(:visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do 'PUBLIC' | :developer | true | true | 'process nuget metadata request at package name level' | :success 'PUBLIC' | :guest | true | true | 'process nuget metadata request at package name level' | :success - 'PUBLIC' | :developer | true | false | 'process nuget metadata request at package name level' | :success - 'PUBLIC' | :guest | true | false | 'process nuget metadata request at package name level' | :success + 'PUBLIC' | :developer | true | false | 'rejects nuget packages access' | :unauthorized + 'PUBLIC' | :guest | true | false | 'rejects nuget packages access' | :unauthorized 'PUBLIC' | :developer | false | true | 'process nuget metadata request at package name level' | :success 'PUBLIC' | :guest | false | true | 'process nuget metadata request at package name level' | :success - 'PUBLIC' | :developer | false | false | 'process nuget metadata request at package name level' | :success - 'PUBLIC' | :guest | false | false | 'process nuget metadata request at package name level' | :success - 'PUBLIC' | :anonymous | false | true | 'process nuget metadata request at package name level' | :success + 'PUBLIC' | :developer | false | false | 'rejects nuget packages access' | :unauthorized + 'PUBLIC' | :guest | false | false | 'rejects nuget packages access' | :unauthorized + 'PUBLIC' | :anonymous | false | true | anonymous_requests_example_name | anonymous_requests_status 'PRIVATE' | :developer | true | true | 'process nuget metadata request at package name level' | :success 'PRIVATE' | :guest | true | true | 'rejects nuget packages access' | :forbidden 'PRIVATE' | :developer | true | false | 'rejects nuget packages access' | :unauthorized @@ -130,21 +134,25 @@ RSpec.shared_examples 'handling nuget metadata requests with package name' do subject { get api(url), headers: headers } before do - project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility_level, false)) + update_visibility_to(Gitlab::VisibilityLevel.const_get(visibility_level, false)) end it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member] end - it_behaves_like 'deploy token for package GET requests' + it_behaves_like 'deploy token for package GET requests' do + before do + update_visibility_to(Gitlab::VisibilityLevel::PRIVATE) + end + end - it_behaves_like 'rejects nuget access with unknown project id' + it_behaves_like 'rejects nuget access with unknown target id' - it_behaves_like 'rejects nuget access with invalid project id' + it_behaves_like 'rejects nuget access with invalid target id' end end -RSpec.shared_examples 'handling nuget metadata requests with package name and package version' do +RSpec.shared_examples 'handling nuget metadata requests with package name and package version' do |anonymous_requests_example_name: 'process nuget metadata request at package name and package version level', anonymous_requests_status: :success| include_context 'with expected presenters dependency groups' let_it_be(:package_name) { 'Dummy.Package' } @@ -157,19 +165,19 @@ RSpec.shared_examples 'handling nuget metadata requests with package name and pa create_dependencies_for(package) end - context 'with valid project' do + context 'with valid target' do using RSpec::Parameterized::TableSyntax - where(:project_visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do + where(:visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do 'PUBLIC' | :developer | true | true | 'process nuget metadata request at package name and package version level' | :success 'PUBLIC' | :guest | true | true | 'process nuget metadata request at package name and package version level' | :success - 'PUBLIC' | :developer | true | false | 'process nuget metadata request at package name and package version level' | :success - 'PUBLIC' | :guest | true | false | 'process nuget metadata request at package name and package version level' | :success + 'PUBLIC' | :developer | true | false | 'rejects nuget packages access' | :unauthorized + 'PUBLIC' | :guest | true | false | 'rejects nuget packages access' | :unauthorized 'PUBLIC' | :developer | false | true | 'process nuget metadata request at package name and package version level' | :success 'PUBLIC' | :guest | false | true | 'process nuget metadata request at package name and package version level' | :success - 'PUBLIC' | :developer | false | false | 'process nuget metadata request at package name and package version level' | :success - 'PUBLIC' | :guest | false | false | 'process nuget metadata request at package name and package version level' | :success - 'PUBLIC' | :anonymous | false | true | 'process nuget metadata request at package name and package version level' | :success + 'PUBLIC' | :developer | false | false | 'rejects nuget packages access' | :unauthorized + 'PUBLIC' | :guest | false | false | 'rejects nuget packages access' | :unauthorized + 'PUBLIC' | :anonymous | false | true | anonymous_requests_example_name | anonymous_requests_status 'PRIVATE' | :developer | true | true | 'process nuget metadata request at package name and package version level' | :success 'PRIVATE' | :guest | true | true | 'rejects nuget packages access' | :forbidden 'PRIVATE' | :developer | true | false | 'rejects nuget packages access' | :unauthorized @@ -188,23 +196,25 @@ RSpec.shared_examples 'handling nuget metadata requests with package name and pa subject { get api(url), headers: headers } before do - project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility_level, false)) + update_visibility_to(Gitlab::VisibilityLevel.const_get(visibility_level, false)) end it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member] end end - it_behaves_like 'deploy token for package GET requests' + it_behaves_like 'deploy token for package GET requests' do + before do + update_visibility_to(Gitlab::VisibilityLevel::PRIVATE) + end + end - context 'with invalid package name' do - let_it_be(:package_name) { 'Unkown' } + it_behaves_like 'rejects nuget access with unknown target id' - it_behaves_like 'rejects nuget packages access', :developer, :not_found - end + it_behaves_like 'rejects nuget access with invalid target id' end -RSpec.shared_examples 'handling nuget search requests' do +RSpec.shared_examples 'handling nuget search requests' do |anonymous_requests_example_name: 'process nuget search request', anonymous_requests_status: :success| let_it_be(:package_a) { create(:nuget_package, :with_metadatum, name: 'Dummy.PackageA', project: project) } let_it_be(:tag) { create(:packages_tag, package: package_a, name: 'test') } let_it_be(:packages_b) { create_list(:nuget_package, 5, name: 'Dummy.PackageB', project: project) } @@ -219,19 +229,19 @@ RSpec.shared_examples 'handling nuget search requests' do subject { get api(url) } - context 'with valid project' do + context 'with valid target' do using RSpec::Parameterized::TableSyntax - where(:project_visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do + where(:visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do 'PUBLIC' | :developer | true | true | 'process nuget search request' | :success 'PUBLIC' | :guest | true | true | 'process nuget search request' | :success - 'PUBLIC' | :developer | true | false | 'process nuget search request' | :success - 'PUBLIC' | :guest | true | false | 'process nuget search request' | :success + 'PUBLIC' | :developer | true | false | 'rejects nuget packages access' | :unauthorized + 'PUBLIC' | :guest | true | false | 'rejects nuget packages access' | :unauthorized 'PUBLIC' | :developer | false | true | 'process nuget search request' | :success 'PUBLIC' | :guest | false | true | 'process nuget search request' | :success - 'PUBLIC' | :developer | false | false | 'process nuget search request' | :success - 'PUBLIC' | :guest | false | false | 'process nuget search request' | :success - 'PUBLIC' | :anonymous | false | true | 'process nuget search request' | :success + 'PUBLIC' | :developer | false | false | 'rejects nuget packages access' | :unauthorized + 'PUBLIC' | :guest | false | false | 'rejects nuget packages access' | :unauthorized + 'PUBLIC' | :anonymous | false | true | anonymous_requests_example_name | anonymous_requests_status 'PRIVATE' | :developer | true | true | 'process nuget search request' | :success 'PRIVATE' | :guest | true | true | 'rejects nuget packages access' | :forbidden 'PRIVATE' | :developer | true | false | 'rejects nuget packages access' | :unauthorized @@ -250,16 +260,20 @@ RSpec.shared_examples 'handling nuget search requests' do subject { get api(url), headers: headers } before do - project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility_level, false)) + update_visibility_to(Gitlab::VisibilityLevel.const_get(visibility_level, false)) end it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member] end end - it_behaves_like 'deploy token for package GET requests' + it_behaves_like 'deploy token for package GET requests' do + before do + update_visibility_to(Gitlab::VisibilityLevel::PRIVATE) + end + end - it_behaves_like 'rejects nuget access with unknown project id' + it_behaves_like 'rejects nuget access with unknown target id' - it_behaves_like 'rejects nuget access with invalid project id' + it_behaves_like 'rejects nuget access with invalid target id' end diff --git a/spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb index dc6ac5f0371..8b60857cdaf 100644 --- a/spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb @@ -3,7 +3,7 @@ RSpec.shared_examples 'rejects nuget 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 + target.send("add_#{user_type}", user) if add_member && user_type != :anonymous end it_behaves_like 'returning response status', status @@ -21,7 +21,7 @@ end RSpec.shared_examples 'process nuget service index request' 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 + target.send("add_#{user_type}", user) if add_member && user_type != :anonymous end it_behaves_like 'returning response status', status @@ -37,7 +37,7 @@ RSpec.shared_examples 'process nuget service index request' do |user_type, statu end context 'with invalid format' do - let(:url) { "/projects/#{project.id}/packages/nuget/index.xls" } + let(:url) { "/#{target_type}/#{target.id}/packages/nuget/index.xls" } it_behaves_like 'rejects nuget packages access', :anonymous, :not_found end @@ -57,7 +57,7 @@ end RSpec.shared_examples 'process nuget metadata request at package name level' 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 + target.send("add_#{user_type}", user) if add_member && user_type != :anonymous end it_behaves_like 'returning response status', status @@ -65,7 +65,7 @@ RSpec.shared_examples 'process nuget metadata request at package name level' do it_behaves_like 'returning nuget metadata json response with json schema', 'public_api/v4/packages/nuget/packages_metadata' context 'with invalid format' do - let(:url) { "/projects/#{project.id}/packages/nuget/metadata/#{package_name}/index.xls" } + let(:url) { "/#{target_type}/#{target.id}/packages/nuget/metadata/#{package_name}/index.xls" } it_behaves_like 'rejects nuget packages access', :anonymous, :not_found end @@ -83,7 +83,7 @@ end RSpec.shared_examples 'process nuget metadata request at package name and package version level' 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 + target.send("add_#{user_type}", user) if add_member && user_type != :anonymous end it_behaves_like 'returning response status', status @@ -91,7 +91,7 @@ RSpec.shared_examples 'process nuget metadata request at package name and packag it_behaves_like 'returning nuget metadata json response with json schema', 'public_api/v4/packages/nuget/package_metadata' context 'with invalid format' do - let(:url) { "/projects/#{project.id}/packages/nuget/metadata/#{package_name}/#{package.version}.xls" } + let(:url) { "/#{target_type}/#{target.id}/packages/nuget/metadata/#{package_name}/#{package.version}.xls" } it_behaves_like 'rejects nuget packages access', :anonymous, :not_found end @@ -109,7 +109,7 @@ end RSpec.shared_examples 'process nuget 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 + target.send("add_#{user_type}", user) if add_member && user_type != :anonymous end it_behaves_like 'returning response status', status @@ -128,7 +128,7 @@ RSpec.shared_examples 'process nuget workhorse authorization' do |user_type, sta end before do - project.add_maintainer(user) + target.add_maintainer(user) end it_behaves_like 'returning response status', :forbidden @@ -141,18 +141,18 @@ RSpec.shared_examples 'process nuget upload' do |user_type, status, add_member = it 'creates package files' do expect(::Packages::Nuget::ExtractionWorker).to receive(:perform_async).once expect { subject } - .to change { project.packages.count }.by(1) + .to change { target.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 + package_file = target.packages.last.package_files.reload.last expect(package_file.file_name).to eq('package.nupkg') end end context "for user type #{user_type}" do before do - project.send("add_#{user_type}", user) if add_member && user_type != :anonymous + target.send("add_#{user_type}", user) if add_member && user_type != :anonymous end context 'with object storage disabled' do @@ -206,7 +206,7 @@ RSpec.shared_examples 'process nuget upload' do |user_type, status, add_member = context 'with crafted package.path param' do let(:crafted_file) { Tempfile.new('nuget.crafted.package.path') } - let(:url) { "/projects/#{project.id}/packages/nuget?package.path=#{crafted_file.path}" } + let(:url) { "/#{target_type}/#{target.id}/packages/nuget?package.path=#{crafted_file.path}" } let(:params) { { file: temp_file(file_name) } } let(:file_key) { :file } @@ -255,7 +255,7 @@ RSpec.shared_examples 'process nuget download versions request' do |user_type, s context "for user type #{user_type}" do before do - project.send("add_#{user_type}", user) if add_member && user_type != :anonymous + target.send("add_#{user_type}", user) if add_member && user_type != :anonymous end it_behaves_like 'returning response status', status @@ -263,7 +263,7 @@ RSpec.shared_examples 'process nuget download versions request' do |user_type, s it_behaves_like 'returns a valid nuget download versions json response' context 'with invalid format' do - let(:url) { "/projects/#{project.id}/packages/nuget/download/#{package_name}/index.xls" } + let(:url) { "/#{target_type}/#{target.id}/packages/nuget/download/#{package_name}/index.xls" } it_behaves_like 'rejects nuget packages access', :anonymous, :not_found end @@ -281,7 +281,7 @@ end RSpec.shared_examples 'process nuget download content request' 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 + target.send("add_#{user_type}", user) if add_member && user_type != :anonymous end it_behaves_like 'returning response status', status @@ -295,7 +295,7 @@ RSpec.shared_examples 'process nuget download content request' do |user_type, st end context 'with invalid format' do - let(:url) { "/projects/#{project.id}/packages/nuget/download/#{package.name}/#{package.version}/#{package.name}.#{package.version}.xls" } + let(:url) { "/#{target_type}/#{target.id}/packages/nuget/download/#{package.name}/#{package.version}/#{package.name}.#{package.version}.xls" } it_behaves_like 'rejects nuget packages access', :anonymous, :not_found end @@ -331,7 +331,7 @@ RSpec.shared_examples 'process nuget search request' do |user_type, status, add_ context "for user type #{user_type}" do before do - project.send("add_#{user_type}", user) if add_member && user_type != :anonymous + target.send("add_#{user_type}", user) if add_member && user_type != :anonymous end it_behaves_like 'returns a valid json search response', status, 4, [1, 5, 5, 1] @@ -370,20 +370,20 @@ RSpec.shared_examples 'process nuget search request' do |user_type, status, add_ end end -RSpec.shared_examples 'rejects nuget access with invalid project id' do - context 'with a project id with invalid integers' do +RSpec.shared_examples 'rejects nuget access with invalid target id' do + context 'with a target id with invalid integers' do using RSpec::Parameterized::TableSyntax - let(:project) { OpenStruct.new(id: id) } + let(:target) { OpenStruct.new(id: id) } where(:id, :status) do - '/../' | :unauthorized + '/../' | :bad_request '' | :not_found - '%20' | :unauthorized - '%2e%2e%2f' | :unauthorized - 'NaN' | :unauthorized + '%20' | :bad_request + '%2e%2e%2f' | :bad_request + 'NaN' | :bad_request 00002345 | :unauthorized - 'anything25' | :unauthorized + 'anything25' | :bad_request end with_them do @@ -392,9 +392,9 @@ RSpec.shared_examples 'rejects nuget access with invalid project id' do end end -RSpec.shared_examples 'rejects nuget access with unknown project id' do - context 'with an unknown project' do - let(:project) { OpenStruct.new(id: 1234567890) } +RSpec.shared_examples 'rejects nuget access with unknown target id' do + context 'with an unknown target' do + let(:target) { OpenStruct.new(id: 1234567890) } context 'as anonymous' do it_behaves_like 'rejects nuget packages access', :anonymous, :unauthorized diff --git a/spec/support/shared_examples/requests/api/repository_storage_moves_shared_examples.rb b/spec/support/shared_examples/requests/api/repository_storage_moves_shared_examples.rb new file mode 100644 index 00000000000..b2970fd265d --- /dev/null +++ b/spec/support/shared_examples/requests/api/repository_storage_moves_shared_examples.rb @@ -0,0 +1,219 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'repository_storage_moves API' do |container_type| + include AccessMatchersForRequest + + let_it_be(:user) { create(:admin) } + + shared_examples 'get single container repository storage move' do + let(:repository_storage_move_id) { storage_move.id } + + def get_container_repository_storage_move + get api(url, user) + end + + it 'returns a container repository storage move', :aggregate_failures do + get_container_repository_storage_move + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema("public_api/v4/#{container_type.singularize}_repository_storage_move") + expect(json_response['id']).to eq(storage_move.id) + expect(json_response['state']).to eq(storage_move.human_state_name) + end + + context 'non-existent container repository storage move' do + let(:repository_storage_move_id) { non_existing_record_id } + + it 'returns not found' do + get_container_repository_storage_move + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + describe 'permissions' do + it { expect { get_container_repository_storage_move }.to be_allowed_for(:admin) } + it { expect { get_container_repository_storage_move }.to be_denied_for(:user) } + end + end + + shared_examples 'get container repository storage move list' do + def get_container_repository_storage_moves + get api(url, user) + end + + it 'returns container repository storage moves', :aggregate_failures do + get_container_repository_storage_moves + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to include_pagination_headers + expect(response).to match_response_schema("public_api/v4/#{container_type.singularize}_repository_storage_moves") + expect(json_response.size).to eq(1) + expect(json_response.first['id']).to eq(storage_move.id) + expect(json_response.first['state']).to eq(storage_move.human_state_name) + end + + it 'avoids N+1 queries', :request_store do + # prevent `let` from polluting the control + get_container_repository_storage_moves + + control = ActiveRecord::QueryRecorder.new { get_container_repository_storage_moves } + + create(repository_storage_move_factory, :scheduled, container: container) + + expect { get_container_repository_storage_moves }.not_to exceed_query_limit(control) + end + + it 'returns the most recently created first' do + storage_move_oldest = create(repository_storage_move_factory, :scheduled, container: container, created_at: 2.days.ago) + storage_move_middle = create(repository_storage_move_factory, :scheduled, container: container, created_at: 1.day.ago) + + get_container_repository_storage_moves + + json_ids = json_response.map {|storage_move| storage_move['id'] } + expect(json_ids).to eq([ + storage_move.id, + storage_move_middle.id, + storage_move_oldest.id + ]) + end + + describe 'permissions' do + it { expect { get_container_repository_storage_moves }.to be_allowed_for(:admin) } + it { expect { get_container_repository_storage_moves }.to be_denied_for(:user) } + end + end + + describe "GET /#{container_type}/:id/repository_storage_moves" do + it_behaves_like 'get container repository storage move list' do + let(:url) { "/#{container_type}/#{container.id}/repository_storage_moves" } + end + end + + describe "GET /#{container_type}/:id/repository_storage_moves/:repository_storage_move_id" do + it_behaves_like 'get single container repository storage move' do + let(:url) { "/#{container_type}/#{container.id}/repository_storage_moves/#{repository_storage_move_id}" } + end + end + + describe "GET /#{container_type.singularize}_repository_storage_moves" do + it_behaves_like 'get container repository storage move list' do + let(:url) { "/#{container_type.singularize}_repository_storage_moves" } + end + end + + describe "GET /#{container_type.singularize}_repository_storage_moves/:repository_storage_move_id" do + it_behaves_like 'get single container repository storage move' do + let(:url) { "/#{container_type.singularize}_repository_storage_moves/#{repository_storage_move_id}" } + end + end + + describe "POST /#{container_type}/:id/repository_storage_moves" do + let(:url) { "/#{container_type}/#{container.id}/repository_storage_moves" } + let(:destination_storage_name) { 'test_second_storage' } + + def create_container_repository_storage_move + post api(url, user), params: { destination_storage_name: destination_storage_name } + end + + before do + stub_storage_settings('test_second_storage' => { 'path' => 'tmp/tests/extra_storage' }) + end + + it 'schedules a container repository storage move', :aggregate_failures do + create_container_repository_storage_move + + storage_move = container.repository_storage_moves.last + + expect(response).to have_gitlab_http_status(:created) + expect(response).to match_response_schema("public_api/v4/#{container_type.singularize}_repository_storage_move") + expect(json_response['id']).to eq(storage_move.id) + expect(json_response['state']).to eq('scheduled') + expect(json_response['source_storage_name']).to eq('default') + expect(json_response['destination_storage_name']).to eq(destination_storage_name) + end + + describe 'permissions' do + it { expect { create_container_repository_storage_move }.to be_allowed_for(:admin) } + it { expect { create_container_repository_storage_move }.to be_denied_for(:user) } + end + + context 'destination_storage_name is missing', :aggregate_failures do + let(:destination_storage_name) { nil } + + it 'schedules a container repository storage move' do + create_container_repository_storage_move + + storage_move = container.repository_storage_moves.last + + expect(response).to have_gitlab_http_status(:created) + expect(response).to match_response_schema("public_api/v4/#{container_type.singularize}_repository_storage_move") + expect(json_response['id']).to eq(storage_move.id) + expect(json_response['state']).to eq('scheduled') + expect(json_response['source_storage_name']).to eq('default') + expect(json_response['destination_storage_name']).to be_present + end + end + end + + describe "POST /#{container_type.singularize}_repository_storage_moves" do + let(:url) { "/#{container_type.singularize}_repository_storage_moves" } + let(:source_storage_name) { 'default' } + let(:destination_storage_name) { 'test_second_storage' } + + def create_container_repository_storage_moves + post api(url, user), params: { + source_storage_name: source_storage_name, + destination_storage_name: destination_storage_name + } + end + + before do + stub_storage_settings('test_second_storage' => { 'path' => 'tmp/tests/extra_storage' }) + end + + it 'schedules the worker' do + expect(bulk_worker_klass).to receive(:perform_async).with(source_storage_name, destination_storage_name) + + create_container_repository_storage_moves + + expect(response).to have_gitlab_http_status(:accepted) + end + + context 'source_storage_name is invalid' do + let(:destination_storage_name) { 'not-a-real-storage' } + + it 'gives an error' do + create_container_repository_storage_moves + + expect(response).to have_gitlab_http_status(:bad_request) + end + end + + context 'destination_storage_name is missing' do + let(:destination_storage_name) { nil } + + it 'schedules the worker' do + expect(bulk_worker_klass).to receive(:perform_async).with(source_storage_name, destination_storage_name) + + create_container_repository_storage_moves + + expect(response).to have_gitlab_http_status(:accepted) + end + end + + context 'destination_storage_name is invalid' do + let(:destination_storage_name) { 'not-a-real-storage' } + + it 'gives an error' do + create_container_repository_storage_moves + + expect(response).to have_gitlab_http_status(:bad_request) + end + end + + describe 'normal user' do + it { expect { create_container_repository_storage_moves }.to be_denied_for(:user) } + end + end +end diff --git a/spec/support/shared_examples/requests/api/resolvable_discussions_shared_examples.rb b/spec/support/shared_examples/requests/api/resolvable_discussions_shared_examples.rb index 5748e873fd4..460e8d57a2b 100644 --- a/spec/support/shared_examples/requests/api/resolvable_discussions_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/resolvable_discussions_shared_examples.rb @@ -9,6 +9,7 @@ RSpec.shared_examples 'resolvable discussions API' do |parent_type, noteable_typ expect(response).to have_gitlab_http_status(:ok) expect(json_response['notes'].size).to eq(1) expect(json_response['notes'][0]['resolved']).to eq(true) + expect(Time.parse(json_response['notes'][0]['resolved_at'])).to be_like_time(note.reload.resolved_at) end it "unresolves discussion if resolved is false" do @@ -18,6 +19,7 @@ RSpec.shared_examples 'resolvable discussions API' do |parent_type, noteable_typ expect(response).to have_gitlab_http_status(:ok) expect(json_response['notes'].size).to eq(1) expect(json_response['notes'][0]['resolved']).to eq(false) + expect(json_response['notes'][0]['resolved_at']).to be_nil end it "returns a 400 bad request error if resolved parameter is not passed" do 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 5d300d38e4a..3b039049ca9 100644 --- a/spec/support/shared_examples/requests/rack_attack_shared_examples.rb +++ b/spec/support/shared_examples/requests/rack_attack_shared_examples.rb @@ -154,10 +154,11 @@ RSpec.shared_examples 'rate-limited token-authenticated requests' do end def make_request(args) + path, options = args if request_method == 'POST' - post(*args) + post(path, **options) else - get(*args) + get(path, **options) end end end diff --git a/spec/support/shared_examples/requests/releases_shared_examples.rb b/spec/support/shared_examples/requests/releases_shared_examples.rb new file mode 100644 index 00000000000..b835947e497 --- /dev/null +++ b/spec/support/shared_examples/requests/releases_shared_examples.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'correct release milestone order' do + let_it_be_with_reload(:milestone_1) { create(:milestone, project: project) } + let_it_be_with_reload(:milestone_2) { create(:milestone, project: project) } + + shared_examples 'correct sort order' do + it 'sorts milestonee_1 before milestone_2' do + freeze_time do + expect(actual_milestone_title_order).to eq([milestone_1.title, milestone_2.title]) + end + end + end + + context 'due_date' do + before do + milestone_1.update!(due_date: Time.zone.now, start_date: 1.day.ago, title: 'z') + milestone_2.update!(due_date: 1.day.from_now, start_date: 2.days.ago, title: 'a') + end + + context 'when both milestones have a due_date' do + it_behaves_like 'correct sort order' + end + + context 'when one milestone does not have a due_date' do + before do + milestone_2.update!(due_date: nil) + end + + it_behaves_like 'correct sort order' + end + end + + context 'start_date' do + before do + milestone_1.update!(due_date: 1.day.from_now, start_date: 1.day.ago, title: 'z' ) + milestone_2.update!(due_date: 1.day.from_now, start_date: milestone_2_start_date, title: 'a' ) + end + + context 'when both milestones have a start_date' do + let(:milestone_2_start_date) { Time.zone.now } + + it_behaves_like 'correct sort order' + end + + context 'when one milestone does not have a start_date' do + let(:milestone_2_start_date) { nil } + + it_behaves_like 'correct sort order' + end + end + + context 'title' do + before do + milestone_1.update!(due_date: 1.day.from_now, start_date: Time.zone.now, title: 'a' ) + milestone_2.update!(due_date: 1.day.from_now, start_date: Time.zone.now, title: 'z' ) + end + + it_behaves_like 'correct sort order' + end +end diff --git a/spec/support/shared_examples/requests/sessionless_auth_request_shared_examples.rb b/spec/support/shared_examples/requests/sessionless_auth_request_shared_examples.rb new file mode 100644 index 00000000000..d82da1b01e1 --- /dev/null +++ b/spec/support/shared_examples/requests/sessionless_auth_request_shared_examples.rb @@ -0,0 +1,84 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'authenticates sessionless user for the request spec' do |params| + params ||= {} + + before do + stub_authentication_activity_metrics(debug: false) + end + + let(:user) { create(:user) } + let(:personal_access_token) { create(:personal_access_token, user: user) } + let(:default_params) { params.except(:public) || {} } + + context "when the 'personal_access_token' param is populated with the personal access token" do + it 'logs the user in' do + expect(authentication_metrics) + .to increment(:user_authenticated_counter) + .and increment(:user_session_override_counter) + .and increment(:user_sessionless_authentication_counter) + + get url, params: default_params.merge(private_token: personal_access_token.token) + + expect(response).to have_gitlab_http_status(:ok) + expect(controller.current_user).to eq(user) + end + + it 'does not log the user in if page is public', if: params[:public] do + get url, params: default_params + + expect(response).to have_gitlab_http_status(:ok) + expect(controller.current_user).to be_nil + end + end + + context 'when the personal access token has no api scope', unless: params[:public] do + it 'does not log the user in' do + # Several instances of where these specs are shared route the request + # through ApplicationController#route_not_found which does not involve + # the usual auth code from Devise, so does not increment the + # :user_unauthenticated_counter + # + unless params[:ignore_incrementing] + expect(authentication_metrics) + .to increment(:user_unauthenticated_counter) + end + + personal_access_token.update!(scopes: [:read_user]) + + get url, params: default_params.merge(private_token: personal_access_token.token) + + expect(response).not_to have_gitlab_http_status(:ok) + end + end + + context "when the 'PERSONAL_ACCESS_TOKEN' header is populated with the personal access token" do + it 'logs the user in' do + expect(authentication_metrics) + .to increment(:user_authenticated_counter) + .and increment(:user_session_override_counter) + .and increment(:user_sessionless_authentication_counter) + + headers = { 'PRIVATE-TOKEN': personal_access_token.token } + get url, params: default_params, headers: headers + + expect(response).to have_gitlab_http_status(:ok) + end + end + + it "doesn't log the user in otherwise", unless: params[:public] do + # Several instances of where these specs are shared route the request + # through ApplicationController#route_not_found which does not involve + # the usual auth code from Devise, so does not increment the + # :user_unauthenticated_counter + # + unless params[:ignore_incrementing] + expect(authentication_metrics) + .to increment(:user_unauthenticated_counter) + end + + get url, params: default_params.merge(private_token: 'token') + + expect(response).not_to have_gitlab_http_status(:ok) + end +end diff --git a/spec/support/shared_examples/routing/legacy_path_redirect_shared_examples.rb b/spec/support/shared_examples/routing/legacy_path_redirect_shared_examples.rb index 808336db7b1..ae3f6425b5e 100644 --- a/spec/support/shared_examples/routing/legacy_path_redirect_shared_examples.rb +++ b/spec/support/shared_examples/routing/legacy_path_redirect_shared_examples.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -RSpec.shared_examples 'redirecting a legacy project path' do |source, target| +RSpec.shared_examples 'redirecting a legacy path' do |source, target| include RSpec::Rails::RequestExampleGroup it "redirects #{source} to #{target}" do diff --git a/spec/support/shared_examples/serializers/pipeline_artifacts_shared_example.rb b/spec/support/shared_examples/serializers/pipeline_artifacts_shared_example.rb new file mode 100644 index 00000000000..d5ffd5e7510 --- /dev/null +++ b/spec/support/shared_examples/serializers/pipeline_artifacts_shared_example.rb @@ -0,0 +1,21 @@ +# 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/boards/issues_list_service_shared_examples.rb b/spec/support/shared_examples/services/boards/issues_list_service_shared_examples.rb index 06e2b715e6d..197b0694741 100644 --- a/spec/support/shared_examples/services/boards/issues_list_service_shared_examples.rb +++ b/spec/support/shared_examples/services/boards/issues_list_service_shared_examples.rb @@ -19,78 +19,12 @@ RSpec.shared_examples 'issues list service' do end end - it 'avoids N+1' do - params = { board_id: board.id } - control = ActiveRecord::QueryRecorder.new { described_class.new(parent, user, params).execute } - - create(:list, board: board) - - expect { described_class.new(parent, user, params).execute }.not_to exceed_query_limit(control) - end - - context 'issues are ordered by priority' do - it 'returns opened issues when list_id is missing' do - params = { board_id: board.id } - - issues = described_class.new(parent, user, params).execute - - expect(issues).to eq [opened_issue2, reopened_issue1, opened_issue1] - end - - it 'returns opened issues when listing issues from Backlog' do - params = { board_id: board.id, id: backlog.id } - - issues = described_class.new(parent, user, params).execute - - expect(issues).to eq [opened_issue2, reopened_issue1, opened_issue1] - end - - it 'returns opened issues that have label list applied when listing issues from a label list' do - params = { board_id: board.id, id: list1.id } - - issues = described_class.new(parent, user, params).execute - - expect(issues).to eq [list1_issue3, list1_issue1, list1_issue2] - end - end - - context 'issues are ordered by date of closing' do - it 'returns closed issues when listing issues from Closed' do - params = { board_id: board.id, id: closed.id } - - issues = described_class.new(parent, user, params).execute - - expect(issues).to eq [closed_issue1, closed_issue2, closed_issue3, closed_issue4, closed_issue5] - end - end - - context 'with list that does not belong to the board' do - it 'raises an error' do - list = create(:list) - service = described_class.new(parent, user, board_id: board.id, id: list.id) - - expect { service.execute }.to raise_error(ActiveRecord::RecordNotFound) - end - end - - context 'with invalid list id' do - it 'raises an error' do - service = described_class.new(parent, user, board_id: board.id, id: nil) - - expect { service.execute }.to raise_error(ActiveRecord::RecordNotFound) - end - end - - context 'when :all_lists is used' do - it 'returns issues from all lists' do - params = { board_id: board.id, all_lists: true } - - issues = described_class.new(parent, user, params).execute - - expected = [opened_issue2, reopened_issue1, opened_issue1, list1_issue1, - list1_issue2, list1_issue3, list2_issue1, closed_issue1, - closed_issue2, closed_issue3, closed_issue4, closed_issue5] - expect(issues).to match_array(expected) - end + it_behaves_like 'items list service' do + let(:backlog_items) { [opened_issue2, reopened_issue1, opened_issue1] } + let(:list1_items) { [list1_issue3, list1_issue1, list1_issue2] } + let(:closed_items) { [closed_issue1, closed_issue2, closed_issue3, closed_issue4, closed_issue5] } + let(:all_items) { backlog_items + list1_items + closed_items + [list2_issue1] } + let(:list_factory) { :list } + let(:new_list) { create(:list, board: board) } end end diff --git a/spec/support/shared_examples/services/boards/items_list_service_shared_examples.rb b/spec/support/shared_examples/services/boards/items_list_service_shared_examples.rb new file mode 100644 index 00000000000..9a3a0cc9cc8 --- /dev/null +++ b/spec/support/shared_examples/services/boards/items_list_service_shared_examples.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'items list service' do + it 'avoids N+1' do + params = { board_id: board.id } + control = ActiveRecord::QueryRecorder.new { described_class.new(parent, user, params).execute } + + new_list + + expect { described_class.new(parent, user, params).execute }.not_to exceed_query_limit(control) + end + + it 'returns opened items when list_id is missing' do + params = { board_id: board.id } + + items = described_class.new(parent, user, params).execute + + expect(items).to match_array(backlog_items) + end + + it 'returns opened items when listing items from Backlog' do + params = { board_id: board.id, id: backlog.id } + + items = described_class.new(parent, user, params).execute + + expect(items).to match_array(backlog_items) + end + + it 'returns opened items that have label list applied when listing items from a label list' do + params = { board_id: board.id, id: list1.id } + + items = described_class.new(parent, user, params).execute + + expect(items).to match_array(list1_items) + end + + it 'returns closed items when listing items from Closed sorted by closed_at in descending order' do + params = { board_id: board.id, id: closed.id } + + items = described_class.new(parent, user, params).execute + + expect(items).to eq(closed_items) + end + + it 'raises an error if the list does not belong to the board' do + list = create(list_factory) # rubocop:disable Rails/SaveBang + service = described_class.new(parent, user, board_id: board.id, id: list.id) + + expect { service.execute }.to raise_error(ActiveRecord::RecordNotFound) + end + + it 'raises an error if list id is invalid' do + service = described_class.new(parent, user, board_id: board.id, id: nil) + + expect { service.execute }.to raise_error(ActiveRecord::RecordNotFound) + end + + it 'returns items from all lists if :all_list is used' do + params = { board_id: board.id, all_lists: true } + + items = described_class.new(parent, user, params).execute + + expect(items).to match_array(all_items) + end +end diff --git a/spec/support/shared_examples/services/merge_request_shared_examples.rb b/spec/support/shared_examples/services/merge_request_shared_examples.rb index 2bd06ac3e9c..56179b6cd00 100644 --- a/spec/support/shared_examples/services/merge_request_shared_examples.rb +++ b/spec/support/shared_examples/services/merge_request_shared_examples.rb @@ -58,3 +58,18 @@ RSpec.shared_examples 'reviewer_ids filter' do end end end + +RSpec.shared_examples 'merge request reviewers cache counters invalidator' do + let(:reviewer_1) { create(:user) } + let(:reviewer_2) { create(:user) } + + before do + merge_request.update!(reviewers: [reviewer_1, reviewer_2]) + end + + 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) + 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 new file mode 100644 index 00000000000..8398dd3c453 --- /dev/null +++ b/spec/support/shared_examples/services/namespace_package_settings_shared_examples.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'updating the namespace package setting attributes' do |from: {}, to:| + it_behaves_like 'not creating the namespace package setting' + + it 'updates the namespace package setting' do + 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]) + end +end + +RSpec.shared_examples 'not creating the namespace package setting' do + it "doesn't create the namespace package setting" do + expect { subject }.not_to change { Namespace::PackageSetting.count } + end +end + +RSpec.shared_examples 'creating the namespace package setting' do + it 'creates a new package setting' do + expect { subject }.to change { Namespace::PackageSetting.count }.by(1) + end + + it 'saves the settings', :aggregate_failures do + subject + + 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]) + end + + it_behaves_like 'returning a success' +end diff --git a/spec/support/shared_examples/services/onboarding_progress_shared_examples.rb b/spec/support/shared_examples/services/onboarding_progress_shared_examples.rb new file mode 100644 index 00000000000..8c6c2271af3 --- /dev/null +++ b/spec/support/shared_examples/services/onboarding_progress_shared_examples.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'records an onboarding progress action' do |action| + include AfterNextHelpers + + it do + expect_next(OnboardingProgressService, namespace) + .to receive(:execute).with(action: action).and_call_original + + subject + end +end + +RSpec.shared_examples 'does not record an onboarding progress action' do + it do + expect(OnboardingProgressService).not_to receive(:new) + + subject + end +end diff --git a/spec/support/shared_examples/services/packages_shared_examples.rb b/spec/support/shared_examples/services/packages_shared_examples.rb index 70d29b4bc99..fa307d2a9a6 100644 --- a/spec/support/shared_examples/services/packages_shared_examples.rb +++ b/spec/support/shared_examples/services/packages_shared_examples.rb @@ -220,3 +220,45 @@ RSpec.shared_examples 'package workhorse uploads' do end end end + +RSpec.shared_examples 'with versionless packages' do + context 'with versionless package' do + let!(:versionless_package) { create(:maven_package, project: project, version: nil) } + + shared_examples 'not including the package' do + it 'does not return the package' do + subject + + expect(json_response.map { |package| package['id'] }).not_to include(versionless_package.id) + end + end + + it_behaves_like 'not including the package' + + context 'with include_versionless param' do + context 'with true include_versionless param' do + [true, 'true', 1, '1'].each do |param| + context "for param #{param}" do + let(:params) { super().merge(include_versionless: param) } + + it 'returns the package' do + subject + + expect(json_response.map { |package| package['id'] }).to include(versionless_package.id) + end + end + end + end + + context 'with falsy include_versionless param' do + [false, '', nil, 'false', 0, '0'].each do |param| + context "for param #{param}" do + let(:params) { super().merge(include_versionless: param) } + + it_behaves_like 'not including the package' + end + end + end + end + end +end diff --git a/spec/support/shared_examples/services/repositories/housekeeping_shared_examples.rb b/spec/support/shared_examples/services/repositories/housekeeping_shared_examples.rb new file mode 100644 index 00000000000..a174ae94b75 --- /dev/null +++ b/spec/support/shared_examples/services/repositories/housekeeping_shared_examples.rb @@ -0,0 +1,118 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'housekeeps repository' do + subject { described_class.new(resource) } + + context 'with a clean redis state', :clean_gitlab_redis_shared_state do + describe '#execute' do + it 'enqueues a sidekiq job' do + expect(subject).to receive(:try_obtain_lease).and_return(:the_uuid) + expect(subject).to receive(:lease_key).and_return(:the_lease_key) + expect(subject).to receive(:task).and_return(:incremental_repack) + expect(GitGarbageCollectWorker).to receive(:perform_async).with(resource.id, :incremental_repack, :the_lease_key, :the_uuid).and_call_original + + Sidekiq::Testing.fake! do + expect { subject.execute }.to change(GitGarbageCollectWorker.jobs, :size).by(1) + end + end + + it 'yields the block if given' do + expect do |block| + subject.execute(&block) + end.to yield_with_no_args + end + + it 'resets counter after execution' do + expect(subject).to receive(:try_obtain_lease).and_return(:the_uuid) + allow(subject).to receive(:gc_period).and_return(1) + resource.increment_pushes_since_gc + + perform_enqueued_jobs do + expect { subject.execute }.to change { resource.pushes_since_gc }.to(0) + end + end + + context 'when no lease can be obtained' do + before do + expect(subject).to receive(:try_obtain_lease).and_return(false) + end + + it 'does not enqueue a job' do + expect(GitGarbageCollectWorker).not_to receive(:perform_async) + + expect { subject.execute }.to raise_error(Repositories::HousekeepingService::LeaseTaken) + end + + it 'does not reset pushes_since_gc' do + expect do + expect { subject.execute }.to raise_error(Repositories::HousekeepingService::LeaseTaken) + end.not_to change { resource.pushes_since_gc } + end + + it 'does not yield' do + expect do |block| + expect { subject.execute(&block) } + .to raise_error(Repositories::HousekeepingService::LeaseTaken) + end.not_to yield_with_no_args + end + end + + context 'task type' do + it 'goes through all three housekeeping tasks, executing only the highest task when there is overlap' do + allow(subject).to receive(:try_obtain_lease).and_return(:the_uuid) + allow(subject).to receive(:lease_key).and_return(:the_lease_key) + + # At push 200 + expect(GitGarbageCollectWorker).to receive(:perform_async).with(resource.id, :gc, :the_lease_key, :the_uuid) + .once + # At push 50, 100, 150 + expect(GitGarbageCollectWorker).to receive(:perform_async).with(resource.id, :full_repack, :the_lease_key, :the_uuid) + .exactly(3).times + # At push 10, 20, ... (except those above) + expect(GitGarbageCollectWorker).to receive(:perform_async).with(resource.id, :incremental_repack, :the_lease_key, :the_uuid) + .exactly(16).times + # At push 6, 12, 18, ... (except those above) + expect(GitGarbageCollectWorker).to receive(:perform_async).with(resource.id, :pack_refs, :the_lease_key, :the_uuid) + .exactly(27).times + + 201.times do + subject.increment! + subject.execute if subject.needed? + end + + expect(resource.pushes_since_gc).to eq(1) + end + end + + it 'runs the task specifically requested' do + housekeeping = described_class.new(resource, :gc) + + allow(housekeeping).to receive(:try_obtain_lease).and_return(:gc_uuid) + allow(housekeeping).to receive(:lease_key).and_return(:gc_lease_key) + + expect(GitGarbageCollectWorker).to receive(:perform_async).with(resource.id, :gc, :gc_lease_key, :gc_uuid).twice + + 2.times do + housekeeping.execute + end + end + end + + describe '#needed?' do + it 'when the count is low enough' do + expect(subject.needed?).to eq(false) + end + + it 'when the count is high enough' do + allow(resource).to receive(:pushes_since_gc).and_return(10) + expect(subject.needed?).to eq(true) + end + end + + describe '#increment!' do + it 'increments the pushes_since_gc counter' do + expect { subject.increment! }.to change { resource.pushes_since_gc }.by(1) + end + end + end +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 new file mode 100644 index 00000000000..e67fc4ab04a --- /dev/null +++ b/spec/support/shared_examples/services/schedule_bulk_repository_shard_moves_shared_examples.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'moves repository shard in bulk' do + let(:source_storage_name) { 'default' } + let(:destination_storage_name) { 'test_second_storage' } + + before do + stub_storage_settings(destination_storage_name => { 'path' => 'tmp/tests/extra_storage' }) + end + + describe '#execute' do + it 'schedules container repository storage moves' do + expect { subject.execute(source_storage_name, destination_storage_name) } + .to change(move_service_klass, :count).by(1) + + storage_move = container.repository_storage_moves.last! + + expect(storage_move).to have_attributes( + source_storage_name: source_storage_name, + destination_storage_name: destination_storage_name, + state_name: :scheduled + ) + end + + context 'read-only repository' do + it 'does not get scheduled' 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/) + expect { subject.execute(source_storage_name, destination_storage_name) } + .to change(move_service_klass, :count).by(0) + end + end + end + + describe '.enqueue' do + it 'defers to the worker' do + expect(bulk_worker_klass).to receive(:perform_async).with(source_storage_name, destination_storage_name) + + described_class.enqueue(source_storage_name, destination_storage_name) + end + end +end diff --git a/spec/support/shared_examples/workers/schedule_bulk_repository_shard_moves_shared_examples.rb b/spec/support/shared_examples/workers/schedule_bulk_repository_shard_moves_shared_examples.rb new file mode 100644 index 00000000000..465aca63148 --- /dev/null +++ b/spec/support/shared_examples/workers/schedule_bulk_repository_shard_moves_shared_examples.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'schedules bulk repository shard moves' do + let(:source_storage_name) { 'default' } + let(:destination_storage_name) { 'test_second_storage' } + + describe "#perform" do + before do + stub_storage_settings(destination_storage_name => { 'path' => 'tmp/tests/extra_storage' }) + + allow(worker_klass).to receive(:perform_async) + end + + include_examples 'an idempotent worker' do + let(:job_args) { [source_storage_name, destination_storage_name] } + + it 'schedules container repository storage moves' do + expect { subject }.to change(move_service_klass, :count).by(1) + + storage_move = container.repository_storage_moves.last! + + expect(storage_move).to have_attributes( + source_storage_name: source_storage_name, + destination_storage_name: destination_storage_name, + state_name: :scheduled + ) + end + end + end +end diff --git a/spec/support/shared_examples/workers/update_repository_move_shared_examples.rb b/spec/support/shared_examples/workers/update_repository_move_shared_examples.rb new file mode 100644 index 00000000000..babd7cfbbeb --- /dev/null +++ b/spec/support/shared_examples/workers/update_repository_move_shared_examples.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'an update storage move worker' do + describe '#perform' do + let(:service) { double(:update_repository_storage_service) } + + before do + allow(Gitlab.config.repositories.storages).to receive(:keys).and_return(%w[default test_second_storage]) + end + + context 'without repository storage move' do + it 'calls the update repository storage service' do + expect(service_klass).to receive(:new).and_return(service) + expect(service).to receive(:execute) + + expect do + subject.perform(container.id, 'test_second_storage') + end.to change(repository_storage_move_klass, :count).by(1) + + storage_move = container.repository_storage_moves.last + expect(storage_move).to have_attributes( + source_storage_name: 'default', + destination_storage_name: 'test_second_storage' + ) + end + end + + context 'with repository storage move' do + it 'calls the update repository storage service' do + expect(service_klass).to receive(:new).and_return(service) + expect(service).to receive(:execute) + + expect do + subject.perform(nil, nil, repository_storage_move.id) + end.not_to change(repository_storage_move_klass, :count) + end + end + end +end -- cgit v1.2.3