diff options
Diffstat (limited to 'spec/support/shared_examples')
47 files changed, 2541 insertions, 132 deletions
diff --git a/spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb b/spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb index 38a5ed244c4..f89d52f81ad 100644 --- a/spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb +++ b/spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb @@ -85,7 +85,7 @@ RSpec.shared_examples 'multiple issue boards' do wait_for_requests - expect(page).to have_selector('.board', count: 5) + expect(page).to have_selector('.board', count: 3) in_boards_switcher_dropdown do click_link board.name @@ -93,7 +93,7 @@ RSpec.shared_examples 'multiple issue boards' do wait_for_requests - expect(page).to have_selector('.board', count: 4) + expect(page).to have_selector('.board', count: 2) end it 'maintains sidebar state over board switch' do diff --git a/spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb b/spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb index 5a4322f73b6..422282da4d8 100644 --- a/spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb +++ b/spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb @@ -219,7 +219,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do it 'returns 200 response when the project is imported successfully' do allow(Gitlab::LegacyGithubImport::ProjectCreator) - .to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider) + .to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, type: provider, **access_params) .and_return(double(execute: project)) post :create, format: :json @@ -233,7 +233,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do project.errors.add(:path, 'is old') allow(Gitlab::LegacyGithubImport::ProjectCreator) - .to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider) + .to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, type: provider, **access_params) .and_return(double(execute: project)) post :create, format: :json @@ -244,7 +244,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do it "touches the etag cache store" do allow(Gitlab::LegacyGithubImport::ProjectCreator) - .to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider) + .to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, type: provider, **access_params) .and_return(double(execute: project)) expect_next_instance_of(Gitlab::EtagCaching::Store) do |store| expect(store).to receive(:touch) { "realtime_changes_import_#{provider}_path" } @@ -257,7 +257,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do context "when the provider user and GitLab user's usernames match" do it "takes the current user's namespace" do expect(Gitlab::LegacyGithubImport::ProjectCreator) - .to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider) + .to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, type: provider, **access_params) .and_return(double(execute: project)) post :create, format: :json @@ -269,7 +269,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do it "takes the current user's namespace" do expect(Gitlab::LegacyGithubImport::ProjectCreator) - .to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider) + .to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, type: provider, **access_params) .and_return(double(execute: project)) post :create, format: :json @@ -296,7 +296,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do it "takes the existing namespace" do expect(Gitlab::LegacyGithubImport::ProjectCreator) - .to receive(:new).with(provider_repo, provider_repo.name, existing_namespace, user, access_params, type: provider) + .to receive(:new).with(provider_repo, provider_repo.name, existing_namespace, user, type: provider, **access_params) .and_return(double(execute: project)) post :create, format: :json @@ -308,7 +308,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do create(:user, username: other_username) expect(Gitlab::LegacyGithubImport::ProjectCreator) - .to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider) + .to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, type: provider, **access_params) .and_return(double(execute: project)) post :create, format: :json @@ -327,7 +327,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do it "takes the new namespace" do expect(Gitlab::LegacyGithubImport::ProjectCreator) - .to receive(:new).with(provider_repo, provider_repo.name, an_instance_of(Group), user, access_params, type: provider) + .to receive(:new).with(provider_repo, provider_repo.name, an_instance_of(Group), user, type: provider, **access_params) .and_return(double(execute: project)) post :create, params: { target_namespace: provider_repo.name }, format: :json @@ -348,7 +348,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do it "takes the current user's namespace" do expect(Gitlab::LegacyGithubImport::ProjectCreator) - .to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider) + .to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, type: provider, **access_params) .and_return(double(execute: project)) post :create, format: :json @@ -366,7 +366,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do it 'takes the selected namespace and name' do expect(Gitlab::LegacyGithubImport::ProjectCreator) - .to receive(:new).with(provider_repo, test_name, test_namespace, user, access_params, type: provider) + .to receive(:new).with(provider_repo, test_name, test_namespace, user, type: provider, **access_params) .and_return(double(execute: project)) post :create, params: { target_namespace: test_namespace.name, new_name: test_name }, format: :json @@ -374,7 +374,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do it 'takes the selected name and default namespace' do expect(Gitlab::LegacyGithubImport::ProjectCreator) - .to receive(:new).with(provider_repo, test_name, user.namespace, user, access_params, type: provider) + .to receive(:new).with(provider_repo, test_name, user.namespace, user, type: provider, **access_params) .and_return(double(execute: project)) post :create, params: { new_name: test_name }, format: :json @@ -393,7 +393,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do it 'takes the selected namespace and name' do expect(Gitlab::LegacyGithubImport::ProjectCreator) - .to receive(:new).with(provider_repo, test_name, nested_namespace, user, access_params, type: provider) + .to receive(:new).with(provider_repo, test_name, nested_namespace, user, type: provider, **access_params) .and_return(double(execute: project)) post :create, params: { target_namespace: nested_namespace.full_path, new_name: test_name }, format: :json @@ -405,7 +405,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do it 'takes the selected namespace and name' do expect(Gitlab::LegacyGithubImport::ProjectCreator) - .to receive(:new).with(provider_repo, test_name, kind_of(Namespace), user, access_params, type: provider) + .to receive(:new).with(provider_repo, test_name, kind_of(Namespace), user, type: provider, **access_params) .and_return(double(execute: project)) post :create, params: { target_namespace: 'foo/bar', new_name: test_name }, format: :json @@ -413,7 +413,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do it 'creates the namespaces' do allow(Gitlab::LegacyGithubImport::ProjectCreator) - .to receive(:new).with(provider_repo, test_name, kind_of(Namespace), user, access_params, type: provider) + .to receive(:new).with(provider_repo, test_name, kind_of(Namespace), user, type: provider, **access_params) .and_return(double(execute: project)) expect { post :create, params: { target_namespace: 'foo/bar', new_name: test_name }, format: :json } @@ -422,7 +422,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do it 'new namespace has the right parent' do allow(Gitlab::LegacyGithubImport::ProjectCreator) - .to receive(:new).with(provider_repo, test_name, kind_of(Namespace), user, access_params, type: provider) + .to receive(:new).with(provider_repo, test_name, kind_of(Namespace), user, type: provider, **access_params) .and_return(double(execute: project)) post :create, params: { target_namespace: 'foo/bar', new_name: test_name }, format: :json @@ -441,7 +441,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do it 'takes the selected namespace and name' do expect(Gitlab::LegacyGithubImport::ProjectCreator) - .to receive(:new).with(provider_repo, test_name, kind_of(Namespace), user, access_params, type: provider) + .to receive(:new).with(provider_repo, test_name, kind_of(Namespace), user, type: provider, **access_params) .and_return(double(execute: project)) post :create, params: { target_namespace: 'foo/foobar/bar', new_name: test_name }, format: :json @@ -449,7 +449,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do it 'creates the namespaces' do allow(Gitlab::LegacyGithubImport::ProjectCreator) - .to receive(:new).with(provider_repo, test_name, kind_of(Namespace), user, access_params, type: provider) + .to receive(:new).with(provider_repo, test_name, kind_of(Namespace), user, type: provider, **access_params) .and_return(double(execute: project)) expect { post :create, params: { target_namespace: 'foo/foobar/bar', new_name: test_name }, format: :json } @@ -458,7 +458,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do it 'does not create a new namespace under the user namespace' do expect(Gitlab::LegacyGithubImport::ProjectCreator) - .to receive(:new).with(provider_repo, test_name, user.namespace, user, access_params, type: provider) + .to receive(:new).with(provider_repo, test_name, user.namespace, user, type: provider, **access_params) .and_return(double(execute: project)) expect { post :create, params: { target_namespace: "#{user.namespace_path}/test_group", new_name: test_name }, format: :js } @@ -472,7 +472,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do it 'does not take the selected namespace and name' do expect(Gitlab::LegacyGithubImport::ProjectCreator) - .to receive(:new).with(provider_repo, test_name, user.namespace, user, access_params, type: provider) + .to receive(:new).with(provider_repo, test_name, user.namespace, user, type: provider, **access_params) .and_return(double(execute: project)) post :create, params: { target_namespace: 'foo/foobar/bar', new_name: test_name }, format: :js @@ -480,7 +480,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do it 'does not create the namespaces' do allow(Gitlab::LegacyGithubImport::ProjectCreator) - .to receive(:new).with(provider_repo, test_name, kind_of(Namespace), user, access_params, type: provider) + .to receive(:new).with(provider_repo, test_name, kind_of(Namespace), user, type: provider, **access_params) .and_return(double(execute: project)) expect { post :create, params: { target_namespace: 'foo/foobar/bar', new_name: test_name }, format: :js } @@ -497,7 +497,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do user.update!(can_create_group: false) expect(Gitlab::LegacyGithubImport::ProjectCreator) - .to receive(:new).with(provider_repo, test_name, group, user, access_params, type: provider) + .to receive(:new).with(provider_repo, test_name, group, user, type: provider, **access_params) .and_return(double(execute: project)) post :create, params: { target_namespace: 'foo', new_name: test_name }, format: :js 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 new file mode 100644 index 00000000000..9b738a4b002 --- /dev/null +++ b/spec/support/shared_examples/controllers/repositories/git_http_controller_shared_examples.rb @@ -0,0 +1,117 @@ +# frozen_string_literal: true + +RSpec.shared_examples Repositories::GitHttpController do + include GitHttpHelpers + + let(:repository_path) { "#{container.full_path}.git" } + let(:params) { { repository_path: repository_path } } + + describe 'HEAD #info_refs' do + it 'returns 403' do + head :info_refs, params: params + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + + describe 'GET #info_refs' do + let(:params) { super().merge(service: 'git-upload-pack') } + + it 'returns 401 for unauthenticated requests to public repositories when http protocol is disabled' do + stub_application_setting(enabled_git_access_protocol: 'ssh') + allow(controller).to receive(:basic_auth_provided?).and_call_original + + expect(controller).to receive(:http_download_allowed?).and_call_original + + get :info_refs, params: params + + expect(response).to have_gitlab_http_status(:unauthorized) + end + + it 'calls the right access checker class with the right object' do + allow(controller).to receive(:verify_workhorse_api!).and_return(true) + + access_double = double + options = { + authentication_abilities: [:download_code], + repository_path: repository_path, + redirected_path: nil, + auth_result_type: :none + } + + expect(access_checker_class).to receive(:new) + .with(nil, container, 'http', hash_including(options)) + .and_return(access_double) + + allow(access_double).to receive(:check).and_return(false) + + get :info_refs, params: params + end + + context 'with authorized user' do + before do + request.headers.merge! auth_env(user.username, user.password, nil) + end + + it 'returns 200' do + get :info_refs, params: params + + expect(response).to have_gitlab_http_status(:ok) + end + + it 'updates the user activity' do + expect_next_instance_of(Users::ActivityService) do |activity_service| + expect(activity_service).to receive(:execute) + end + + get :info_refs, params: params + end + + include_context 'parsed logs' do + it 'adds user info to the logs' do + get :info_refs, params: params + + expect(log_data).to include('username' => user.username, + 'user_id' => user.id, + 'meta.user' => user.username) + 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 + before do + allow(controller).to receive(:verify_workhorse_api!).and_return(true) + end + + it 'returns 200' do + post :git_upload_pack, params: params + + expect(response).to have_gitlab_http_status(:ok) + end + end +end diff --git a/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb b/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb index a6ad8fc594c..dcbf494186a 100644 --- a/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb +++ b/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb @@ -14,6 +14,22 @@ RSpec.shared_examples 'wiki controller actions' do sign_in(user) end + shared_examples 'recovers from git timeout' do + let(:method_name) { :page } + + context 'when we encounter git command errors' do + it 'renders the appropriate template', :aggregate_failures do + expect(controller).to receive(method_name) do + raise ::Gitlab::Git::CommandTimedOut, 'Deadline Exceeded' + end + + request + + expect(response).to render_template('shared/wikis/git_error') + end + end + end + describe 'GET #new' do subject(:request) { get :new, params: routing_params } @@ -48,6 +64,12 @@ RSpec.shared_examples 'wiki controller actions' do get :pages, params: routing_params.merge(id: wiki_title) end + it_behaves_like 'recovers from git timeout' do + subject(:request) { get :pages, params: routing_params.merge(id: wiki_title) } + + let(:method_name) { :wiki_pages } + end + it 'assigns the page collections' do expect(assigns(:wiki_pages)).to contain_exactly(an_instance_of(WikiPage)) expect(assigns(:wiki_entries)).to contain_exactly(an_instance_of(WikiPage)) @@ -99,6 +121,12 @@ RSpec.shared_examples 'wiki controller actions' do end end + it_behaves_like 'recovers from git timeout' do + subject(:request) { get :history, params: routing_params.merge(id: wiki_title) } + + let(:allow_read_wiki) { true } + end + it_behaves_like 'fetching history', :ok do let(:allow_read_wiki) { true } @@ -139,6 +167,10 @@ RSpec.shared_examples 'wiki controller actions' do expect(response).to have_gitlab_http_status(:not_found) end end + + it_behaves_like 'recovers from git timeout' do + subject(:request) { get :diff, params: routing_params.merge(id: wiki_title, version_id: wiki.repository.commit.id) } + end end describe 'GET #show' do @@ -151,6 +183,8 @@ RSpec.shared_examples 'wiki controller actions' do context 'when page exists' do let(:id) { wiki_title } + it_behaves_like 'recovers from git timeout' + it 'renders the page' do request @@ -161,6 +195,28 @@ RSpec.shared_examples 'wiki controller actions' do expect(assigns(:sidebar_limited)).to be(false) end + context 'the sidebar fails to load' do + before do + allow(Wiki).to receive(:for_container).and_return(wiki) + wiki.wiki + expect(wiki).to receive(:find_sidebar) do + raise ::Gitlab::Git::CommandTimedOut, 'Deadline Exceeded' + end + end + + it 'renders the page, and marks the sidebar as failed' do + request + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to render_template('shared/wikis/_sidebar') + expect(assigns(:page).title).to eq(wiki_title) + expect(assigns(:sidebar_page)).to be_nil + expect(assigns(:sidebar_wiki_entries)).to be_nil + expect(assigns(:sidebar_limited)).to be_nil + expect(assigns(:sidebar_error)).to be_a_kind_of(::Gitlab::Git::CommandError) + end + end + context 'page view tracking' do it_behaves_like 'tracking unique hll events', :track_unique_wiki_page_views do let(:target_id) { 'wiki_action' } @@ -308,6 +364,7 @@ RSpec.shared_examples 'wiki controller actions' do subject(:request) { get(:edit, params: routing_params.merge(id: id_param)) } it_behaves_like 'edit action' + it_behaves_like 'recovers from git timeout' context 'when page content encoding is valid' do render_views @@ -447,6 +504,17 @@ RSpec.shared_examples 'wiki controller actions' do end end + describe '#git_access' do + render_views + + it 'renders the git access page' do + get :git_access, params: routing_params + + expect(response).to render_template('shared/wikis/git_access') + expect(response.body).to include(wiki.http_url_to_repo) + end + end + def redirect_to_wiki(wiki, page) redirect_to(controller.wiki_page_path(wiki, page)) end diff --git a/spec/support/shared_examples/features/issuable_invite_members_shared_examples.rb b/spec/support/shared_examples/features/issuable_invite_members_shared_examples.rb index ac1cc2da7e3..3fec1a56c0c 100644 --- a/spec/support/shared_examples/features/issuable_invite_members_shared_examples.rb +++ b/spec/support/shared_examples/features/issuable_invite_members_shared_examples.rb @@ -3,7 +3,7 @@ RSpec.shared_examples 'issuable invite members experiments' do context 'when invite_members_version_a experiment is enabled' do before do - stub_experiment_for_user(invite_members_version_a: true) + stub_experiment_for_subject(invite_members_version_a: true) end it 'shows a link for inviting members and follows through to the members page' do @@ -28,7 +28,7 @@ RSpec.shared_examples 'issuable invite members experiments' do context 'when invite_members_version_b experiment is enabled' do before do - stub_experiment_for_user(invite_members_version_b: true) + stub_experiment_for_subject(invite_members_version_b: true) end it 'shows a link for inviting members and follows through to modal' do 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 724d6db2705..1dbaace1c89 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 @@ -50,7 +50,6 @@ RSpec.shared_examples 'Maintainer manages access requests' do def expect_visible_access_request(entity, user) if has_tabs expect(page).to have_content "Access requests 1" - expect(page).to have_content "Users requesting access to #{entity.name}" else expect(page).to have_content "Users requesting access to #{entity.name} 1" end diff --git a/spec/support/shared_examples/features/reportable_note_shared_examples.rb b/spec/support/shared_examples/features/reportable_note_shared_examples.rb index bdaa375721f..288e1df9b2a 100644 --- a/spec/support/shared_examples/features/reportable_note_shared_examples.rb +++ b/spec/support/shared_examples/features/reportable_note_shared_examples.rb @@ -29,7 +29,7 @@ RSpec.shared_examples 'reportable note' do |type| end end - it 'Report button links to a report page' do + it 'report button links to a report page' do dropdown = comment.find(more_actions_selector) open_dropdown(dropdown) diff --git a/spec/support/shared_examples/features/wiki/user_git_access_wiki_page_shared_examples.rb b/spec/support/shared_examples/features/wiki/user_git_access_wiki_page_shared_examples.rb new file mode 100644 index 00000000000..d3d2a36147d --- /dev/null +++ b/spec/support/shared_examples/features/wiki/user_git_access_wiki_page_shared_examples.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'User views Git access wiki page' do + let(:wiki_page) { create(:wiki_page, wiki: wiki) } + + before do + sign_in(user) + end + + it 'shows the correct clone URLs', :js do + visit wiki_page_path(wiki, wiki_page) + click_link 'Clone repository' + + expect(page).to have_text("Clone repository #{wiki.full_path}") + + within('.git-clone-holder') do + expect(page).to have_css('#clone-dropdown', text: 'HTTP') + expect(page).to have_field('clone_url', with: wiki.http_url_to_repo) + + click_link 'HTTP' # open the dropdown + click_link 'SSH' # select the dropdown item + + expect(page).to have_css('#clone-dropdown', text: 'SSH') + expect(page).to have_field('clone_url', with: wiki.ssh_url_to_repo) + end + end +end 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 0330b345a18..759cfaf6b1f 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 @@ -12,7 +12,7 @@ RSpec.shared_examples 'User uses wiki shortcuts' do visit wiki_page_path(wiki, wiki_page) end - it 'Visit edit wiki page using "e" keyboard shortcut', :js 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') diff --git a/spec/support/shared_examples/graphql/connection_redaction_shared_examples.rb b/spec/support/shared_examples/graphql/connection_redaction_shared_examples.rb new file mode 100644 index 00000000000..12a7b3fe414 --- /dev/null +++ b/spec/support/shared_examples/graphql/connection_redaction_shared_examples.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +# requires: +# - `connection` (no-empty, containing `unwanted` and at least one more item) +# - `unwanted` (single item in collection) +RSpec.shared_examples 'a redactable connection' do + context 'no redactor set' do + it 'contains the unwanted item' do + expect(connection.nodes).to include(unwanted) + end + + it 'does not redact more than once' do + connection.nodes + r_state = connection.send(:redaction_state) + + expect(r_state.redacted { raise 'Should not be called!' }).to be_present + end + end + + let_it_be(:constant_redactor) do + Class.new do + def initialize(remove) + @remove = remove + end + + def redact(items) + items - @remove + end + end + end + + context 'redactor is set' do + let(:redactor) do + constant_redactor.new([unwanted]) + end + + before do + connection.redactor = redactor + end + + it 'does not contain the unwanted item' do + expect(connection.nodes).not_to include(unwanted) + expect(connection.nodes).not_to be_empty + end + + it 'does not redact more than once' do + expect(redactor).to receive(:redact).once.and_call_original + + connection.nodes + connection.nodes + connection.nodes + end + end +end diff --git a/spec/support/shared_examples/graphql/connection_shared_examples.rb b/spec/support/shared_examples/graphql/connection_shared_examples.rb new file mode 100644 index 00000000000..4cba5b5a69d --- /dev/null +++ b/spec/support/shared_examples/graphql/connection_shared_examples.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'a connection with collection methods' do + %i[to_a size include? empty?].each do |method_name| + it "responds to #{method_name}" do + expect(connection).to respond_to(method_name) + end + end +end diff --git a/spec/support/shared_examples/graphql/design_fields_shared_examples.rb b/spec/support/shared_examples/graphql/design_fields_shared_examples.rb index ef7086234c4..9c2eb3e5a5c 100644 --- a/spec/support/shared_examples/graphql/design_fields_shared_examples.rb +++ b/spec/support/shared_examples/graphql/design_fields_shared_examples.rb @@ -26,27 +26,35 @@ RSpec.shared_examples 'a GraphQL type with design fields' do end describe '#image' do + let_it_be(:current_user) { create(:user) } let(:schema) { GitlabSchema } let(:query) { GraphQL::Query.new(schema) } - let(:context) { double('Context', schema: schema, query: query, parent: nil) } + let(:context) { query.context } let(:field) { described_class.fields['image'] } let(:args) { GraphQL::Query::Arguments::NO_ARGS } - let(:instance) do + let(:instance) { instantiate(object_id) } + let(:instance_b) { instantiate(object_id_b) } + + def instantiate(object_id) object = GitlabSchema.sync_lazy(GitlabSchema.object_from_id(object_id)) object_type.authorized_new(object, query.context) end - let(:instance_b) do - object_b = GitlabSchema.sync_lazy(GitlabSchema.object_from_id(object_id_b)) - object_type.authorized_new(object_b, query.context) + def resolve_image(instance) + field.resolve_field(instance, args, context) + end + + before do + context[:current_user] = current_user + allow(Ability).to receive(:allowed?).with(current_user, :read_design, anything).and_return(true) + allow(context).to receive(:parent).and_return(nil) end it 'resolves to the design image URL' do - image = field.resolve_field(instance, args, context) sha = design.versions.first.sha url = ::Gitlab::Routing.url_helpers.project_design_management_designs_raw_image_url(design.project, design, sha) - expect(image).to eq(url) + expect(resolve_image(instance)).to eq(url) end it 'has better than O(N) peformance', :request_store do @@ -68,10 +76,10 @@ RSpec.shared_examples 'a GraphQL type with design fields' do # = 10 expect(instance).not_to eq(instance_b) # preload designs themselves. expect do - image_a = field.resolve_field(instance, args, context) - image_b = field.resolve_field(instance, args, context) - image_c = field.resolve_field(instance_b, args, context) - image_d = field.resolve_field(instance_b, args, context) + image_a = resolve_image(instance) + image_b = resolve_image(instance) + image_c = resolve_image(instance_b) + image_d = resolve_image(instance_b) expect(image_a).to eq(image_b) expect(image_c).not_to eq(image_b) expect(image_c).to eq(image_d) diff --git a/spec/support/shared_examples/graphql/jira_import/jira_import_resolver_shared_examples.rb b/spec/support/shared_examples/graphql/jira_import/jira_import_resolver_shared_examples.rb deleted file mode 100644 index b2047f1d32c..00000000000 --- a/spec/support/shared_examples/graphql/jira_import/jira_import_resolver_shared_examples.rb +++ /dev/null @@ -1,15 +0,0 @@ -# frozen_string_literal: true - -RSpec.shared_examples 'no Jira import data present' do - it 'returns none' do - expect(resolve_imports).to eq JiraImportState.none - end -end - -RSpec.shared_examples 'no Jira import access' do - it 'raises error' do - expect do - resolve_imports - end.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) - end -end diff --git a/spec/support/shared_examples/graphql/members_shared_examples.rb b/spec/support/shared_examples/graphql/members_shared_examples.rb index 3a046c3feec..b0bdd27a95f 100644 --- a/spec/support/shared_examples/graphql/members_shared_examples.rb +++ b/spec/support/shared_examples/graphql/members_shared_examples.rb @@ -36,9 +36,10 @@ RSpec.shared_examples 'querying members with a group' do let_it_be(:group_2_member) { create(:group_member, user: user_3, group: group_2) } let(:args) { {} } + let(:base_args) { { relations: described_class.arguments['relations'].default_value } } subject do - resolve(described_class, obj: resource, args: args, ctx: { current_user: user_4 }) + resolve(described_class, obj: resource, args: base_args.merge(args), ctx: { current_user: user_4 }) end describe '#resolve' do @@ -72,7 +73,7 @@ RSpec.shared_examples 'querying members with a group' do let_it_be(:other_user) { create(:user) } subject do - resolve(described_class, obj: resource, args: args, ctx: { current_user: other_user }) + resolve(described_class, obj: resource, args: base_args.merge(args), ctx: { current_user: other_user }) end it 'raises an error' do diff --git a/spec/support/shared_examples/graphql/mutation_shared_examples.rb b/spec/support/shared_examples/graphql/mutation_shared_examples.rb index b67cac94547..84ebd4852b9 100644 --- a/spec/support/shared_examples/graphql/mutation_shared_examples.rb +++ b/spec/support/shared_examples/graphql/mutation_shared_examples.rb @@ -13,6 +13,8 @@ RSpec.shared_examples 'a mutation that returns top-level errors' do |errors: []| it do post_graphql_mutation(mutation, current_user: current_user) + expect(graphql_errors).to be_present + error_messages = graphql_errors.map { |e| e['message'] } expect(error_messages).to match_errors diff --git a/spec/support/shared_examples/graphql/mutations/boards_create_shared_examples.rb b/spec/support/shared_examples/graphql/mutations/boards_create_shared_examples.rb index 9c0b398a5c1..2b93d174653 100644 --- a/spec/support/shared_examples/graphql/mutations/boards_create_shared_examples.rb +++ b/spec/support/shared_examples/graphql/mutations/boards_create_shared_examples.rb @@ -40,6 +40,30 @@ RSpec.shared_examples 'boards create mutation' do end end + context 'when hide_backlog_list parameter is true' do + before do + params[:hide_backlog_list] = true + end + + it 'returns the board with correct hide_backlog_list field' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(mutation_response['board']['hideBacklogList']).to eq(true) + end + end + + context 'when hide_closed_list parameter is true' do + before do + params[:hide_closed_list] = true + end + + it 'returns the board with correct hide_closed_list field' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(mutation_response['board']['hideClosedList']).to eq(true) + end + end + context 'when the Boards::CreateService returns an error response' do before do allow_next_instance_of(Boards::CreateService) do |service| diff --git a/spec/support/shared_examples/graphql/mutations/spammable_mutation_fields_examples.rb b/spec/support/shared_examples/graphql/mutations/spammable_mutation_fields_examples.rb index 54b3f84a6e6..8678b23ad31 100644 --- a/spec/support/shared_examples/graphql/mutations/spammable_mutation_fields_examples.rb +++ b/spec/support/shared_examples/graphql/mutations/spammable_mutation_fields_examples.rb @@ -13,7 +13,8 @@ end RSpec.shared_examples 'can raise spam flag' do it 'spam parameters are passed to the service' do - expect(service).to receive(:new).with(anything, anything, hash_including(api: true, request: instance_of(ActionDispatch::Request))) + args = [anything, anything, hash_including(api: true, request: instance_of(ActionDispatch::Request))] + expect(service).to receive(:new).with(*args).and_call_original subject end @@ -39,7 +40,9 @@ RSpec.shared_examples 'can raise spam flag' do end it 'request parameter is not passed to the service' do - expect(service).to receive(:new).with(anything, anything, hash_not_including(request: instance_of(ActionDispatch::Request))) + expect(service).to receive(:new) + .with(anything, anything, hash_not_including(request: instance_of(ActionDispatch::Request))) + .and_call_original subject end diff --git a/spec/support/shared_examples/graphql/projects/services_resolver_shared_examples.rb b/spec/support/shared_examples/graphql/projects/services_resolver_shared_examples.rb index 94b7ed1618d..16c2ab07f3a 100644 --- a/spec/support/shared_examples/graphql/projects/services_resolver_shared_examples.rb +++ b/spec/support/shared_examples/graphql/projects/services_resolver_shared_examples.rb @@ -2,14 +2,12 @@ RSpec.shared_examples 'no project services' do it 'returns empty collection' do - expect(resolve_services).to eq [] + expect(resolve_services).to be_empty end end RSpec.shared_examples 'cannot access project services' do it 'raises error' do - expect do - resolve_services - end.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) + expect(resolve_services).to be_nil end end diff --git a/spec/support/shared_examples/graphql/sorted_paginated_query_shared_examples.rb b/spec/support/shared_examples/graphql/sorted_paginated_query_shared_examples.rb index 7627a7b4d59..f78ea364147 100644 --- a/spec/support/shared_examples/graphql/sorted_paginated_query_shared_examples.rb +++ b/spec/support/shared_examples/graphql/sorted_paginated_query_shared_examples.rb @@ -16,80 +16,111 @@ # # Example: # describe 'sorting and pagination' do -# let(:sort_project) { create(:project, :public) } +# let_it_be(:sort_project) { create(:project, :public) } # let(:data_path) { [:project, :issues] } # -# def pagination_query(params, page_info) -# graphql_query_for( -# 'project', -# { 'fullPath' => sort_project.full_path }, -# query_graphql_field('issues', params, "#{page_info} edges { node { id } }") +# def pagination_query(arguments) +# graphql_query_for(:project, { full_path: sort_project.full_path }, +# query_nodes(:issues, :iid, include_pagination_info: true, args: arguments) # ) # end # -# def pagination_results_data(data) -# data.map { |issue| issue.dig('node', 'iid').to_i } +# # A method transforming nodes to data to match against +# # default: the identity function +# def pagination_results_data(issues) +# issues.map { |issue| issue['iid].to_i } # end # # context 'when sorting by weight' do -# ... +# let_it_be(:issues) { make_some_issues_with_weights } +# # context 'when ascending' do +# let(:ordered_issues) { issues.sort_by(&:weight) } +# # it_behaves_like 'sorted paginated query' do -# let(:sort_param) { 'WEIGHT_ASC' } +# let(:sort_param) { :WEIGHT_ASC } # let(:first_param) { 2 } -# let(:expected_results) { [weight_issue3.iid, weight_issue5.iid, weight_issue1.iid, weight_issue4.iid, weight_issue2.iid] } +# let(:expected_results) { ordered_issues.map(&:iid) } # end # end # RSpec.shared_examples 'sorted paginated query' do + # Provided as a convenience when constructing queries using string concatenation + let(:page_info) { 'pageInfo { startCursor endCursor }' } + # Convenience for using default implementation of pagination_results_data + let(:node_path) { ['id'] } + it_behaves_like 'requires variables' do let(:required_variables) { [:sort_param, :first_param, :expected_results, :data_path, :current_user] } end describe do - let(:sort_argument) { "sort: #{sort_param}" if sort_param.present? } - let(:first_argument) { "first: #{first_param}" if first_param.present? } + let(:sort_argument) { graphql_args(sort: sort_param) } let(:params) { sort_argument } - let(:start_cursor) { graphql_data_at(*data_path, :pageInfo, :startCursor) } - let(:end_cursor) { graphql_data_at(*data_path, :pageInfo, :endCursor) } - let(:sorted_edges) { graphql_data_at(*data_path, :edges) } - let(:page_info) { "pageInfo { startCursor endCursor }" } - def pagination_query(params, page_info) - raise('pagination_query(params, page_info) must be defined in the test, see example in comment') unless defined?(super) + # Convenience helper for the large number of queries defined as a projection + # from some root value indexed by full_path to a collection of objects with IID + def nested_internal_id_query(root_field, parent, field, args, selection: :iid) + graphql_query_for(root_field, { full_path: parent.full_path }, + query_nodes(field, selection, args: args, include_pagination_info: true) + ) + end + + def pagination_query(params) + raise('pagination_query(params) must be defined in the test, see example in comment') unless defined?(super) super end - def pagination_results_data(data) - raise('pagination_results_data(data) must be defined in the test, see example in comment') unless defined?(super) + def pagination_results_data(nodes) + if defined?(super) + super(nodes) + else + nodes.map { |n| n.dig(*node_path) } + end + end + + def results + nodes = graphql_dig_at(graphql_data(fresh_response_data), *data_path, :nodes) + pagination_results_data(nodes) + end + + def end_cursor + graphql_dig_at(graphql_data(fresh_response_data), *data_path, :page_info, :end_cursor) + end - super(data) + def start_cursor + graphql_dig_at(graphql_data(fresh_response_data), *data_path, :page_info, :start_cursor) end + let(:query) { pagination_query(params) } + before do - post_graphql(pagination_query(params, page_info), current_user: current_user) + post_graphql(query, current_user: current_user) end context 'when sorting' do it 'sorts correctly' do - expect(pagination_results_data(sorted_edges)).to eq expected_results + expect(results).to eq expected_results end context 'when paginating' do - let(:params) { [sort_argument, first_argument].compact.join(',') } + let(:params) { sort_argument.merge(first: first_param) } + let(:first_page) { expected_results.first(first_param) } + let(:rest) { expected_results.drop(first_param) } it 'paginates correctly' do - expect(pagination_results_data(sorted_edges)).to eq expected_results.first(first_param) + expect(results).to eq first_page - cursored_query = pagination_query([sort_argument, "after: \"#{end_cursor}\""].compact.join(','), page_info) - post_graphql(cursored_query, current_user: current_user) + fwds = pagination_query(sort_argument.merge(after: end_cursor)) + post_graphql(fwds, current_user: current_user) - expect(response).to have_gitlab_http_status(:ok) + expect(results).to eq rest - response_data = graphql_dig_at(Gitlab::Json.parse(response.body), :data, *data_path, :edges) + bwds = pagination_query(sort_argument.merge(before: start_cursor)) + post_graphql(bwds, current_user: current_user) - expect(pagination_results_data(response_data)).to eq expected_results.drop(first_param) + expect(results).to eq first_page end end end diff --git a/spec/support/shared_examples/graphql/types/gitlab_style_deprecations_shared_examples.rb b/spec/support/shared_examples/graphql/types/gitlab_style_deprecations_shared_examples.rb index ed139e638bf..269e9170906 100644 --- a/spec/support/shared_examples/graphql/types/gitlab_style_deprecations_shared_examples.rb +++ b/spec/support/shared_examples/graphql/types/gitlab_style_deprecations_shared_examples.rb @@ -32,16 +32,16 @@ RSpec.shared_examples 'Gitlab-style deprecations' do it 'adds a formatted `deprecated_reason` to the subject' do deprecable = subject(deprecated: { milestone: '1.10', reason: 'Deprecation reason' }) - expect(deprecable.deprecation_reason).to eq('Deprecation reason. Deprecated in 1.10') + expect(deprecable.deprecation_reason).to eq('Deprecation reason. Deprecated in 1.10.') end it 'appends to the description if given' do deprecable = subject( deprecated: { milestone: '1.10', reason: 'Deprecation reason' }, - description: 'Deprecable description' + description: 'Deprecable description.' ) - expect(deprecable.description).to eq('Deprecable description. Deprecated in 1.10: Deprecation reason') + expect(deprecable.description).to eq('Deprecable description. Deprecated in 1.10: Deprecation reason.') end it 'does not append to the description if it is absent' do diff --git a/spec/support/shared_examples/lib/gitlab/diff_file_collections_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/diff_file_collections_shared_examples.rb index 469c0c287b1..c9e03ced0dd 100644 --- a/spec/support/shared_examples/lib/gitlab/diff_file_collections_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/diff_file_collections_shared_examples.rb @@ -143,3 +143,55 @@ RSpec.shared_examples 'cacheable diff collection' do end end end + +shared_examples_for 'sortable diff files' do + subject { described_class.new(diffable, **collection_default_args) } + + describe '#raw_diff_files' do + let(:raw_diff_files_paths) do + subject.raw_diff_files(sorted: sorted).map { |file| file.new_path.presence || file.old_path } + end + + context 'when sorted is false (default)' do + let(:sorted) { false } + + it 'returns unsorted diff files' do + expect(raw_diff_files_paths).to eq(unsorted_diff_files_paths) + end + end + + context 'when sorted is true' do + let(:sorted) { true } + + it 'returns sorted diff files' do + expect(raw_diff_files_paths).to eq(sorted_diff_files_paths) + end + + context 'when sort_diffs feature flag is disabled' do + before do + stub_feature_flags(sort_diffs: false) + end + + it 'returns unsorted diff files' do + expect(raw_diff_files_paths).to eq(unsorted_diff_files_paths) + end + end + end + end +end + +shared_examples_for 'unsortable diff files' do + subject { described_class.new(diffable, **collection_default_args) } + + describe '#raw_diff_files' do + it 'does not call Gitlab::Diff::FileCollectionSorter even when sorted is true' do + # Ensure that diffable is created before expectation to ensure that we are + # not calling it from `FileCollectionSorter` from `#raw_diff_files`. + diffable + + expect(Gitlab::Diff::FileCollectionSorter).not_to receive(:new) + + subject.raw_diff_files(sorted: true) + end + end +end diff --git a/spec/support/shared_examples/lib/gitlab/import_export/import_failure_service_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/import_export/import_failure_service_shared_examples.rb index 801be5ae946..67afd2035c4 100644 --- a/spec/support/shared_examples/lib/gitlab/import_export/import_failure_service_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/import_export/import_failure_service_shared_examples.rb @@ -3,10 +3,10 @@ RSpec.shared_examples 'log import failure' do |importable_column| it 'tracks error' do extra = { - source: action, - relation_key: relation_key, - relation_index: relation_index, - retry_count: retry_count + source: action, + relation_name: relation_key, + relation_index: relation_index, + retry_count: retry_count } extra[importable_column] = importable.id diff --git a/spec/support/shared_examples/lib/gitlab/middleware/read_only_gitlab_instance_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/middleware/read_only_gitlab_instance_shared_examples.rb index e07d3e2dec9..5b3d30df739 100644 --- a/spec/support/shared_examples/lib/gitlab/middleware/read_only_gitlab_instance_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/middleware/read_only_gitlab_instance_shared_examples.rb @@ -125,6 +125,9 @@ RSpec.shared_examples 'write access for a read-only GitLab instance' do where(:description, :path) do 'LFS request to batch' | '/root/rouge.git/info/lfs/objects/batch' 'request to git-upload-pack' | '/root/rouge.git/git-upload-pack' + 'user sign out' | '/users/sign_out' + 'admin session' | '/admin/session' + 'admin session destroy' | '/admin/session/destroy' end with_them do diff --git a/spec/support/shared_examples/lib/gitlab/sidekiq_middleware/strategy_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/sidekiq_middleware/strategy_shared_examples.rb index 2936bb354cf..89b793d5e16 100644 --- a/spec/support/shared_examples/lib/gitlab/sidekiq_middleware/strategy_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/sidekiq_middleware/strategy_shared_examples.rb @@ -38,7 +38,7 @@ RSpec.shared_examples 'deduplicating jobs when scheduling' do |strategy_name| it 'adds the jid of the existing job to the job hash' do allow(fake_duplicate_job).to receive(:scheduled?).and_return(false) allow(fake_duplicate_job).to receive(:check!).and_return('the jid') - allow(fake_duplicate_job).to receive(:droppable?).and_return(true) + allow(fake_duplicate_job).to receive(:idempotent?).and_return(true) allow(fake_duplicate_job).to receive(:options).and_return({}) job_hash = {} @@ -62,7 +62,7 @@ RSpec.shared_examples 'deduplicating jobs when scheduling' do |strategy_name| receive(:check!) .with(Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob::DUPLICATE_KEY_TTL) .and_return('the jid')) - allow(fake_duplicate_job).to receive(:droppable?).and_return(true) + allow(fake_duplicate_job).to receive(:idempotent?).and_return(true) job_hash = {} expect(fake_duplicate_job).to receive(:duplicate?).and_return(true) @@ -82,7 +82,7 @@ RSpec.shared_examples 'deduplicating jobs when scheduling' do |strategy_name| allow(fake_duplicate_job).to receive(:options).and_return({ including_scheduled: true }) allow(fake_duplicate_job).to( receive(:check!).with(time_diff.to_i).and_return('the jid')) - allow(fake_duplicate_job).to receive(:droppable?).and_return(true) + allow(fake_duplicate_job).to receive(:idempotent?).and_return(true) job_hash = {} expect(fake_duplicate_job).to receive(:duplicate?).and_return(true) @@ -104,13 +104,13 @@ RSpec.shared_examples 'deduplicating jobs when scheduling' do |strategy_name| allow(fake_duplicate_job).to receive(:duplicate?).and_return(true) allow(fake_duplicate_job).to receive(:options).and_return({}) allow(fake_duplicate_job).to receive(:existing_jid).and_return('the jid') - allow(fake_duplicate_job).to receive(:droppable?).and_return(true) + allow(fake_duplicate_job).to receive(:idempotent?).and_return(true) end it 'drops the job' do schedule_result = nil - expect(fake_duplicate_job).to receive(:droppable?).and_return(true) + expect(fake_duplicate_job).to receive(:idempotent?).and_return(true) expect { |b| schedule_result = strategy.schedule({}, &b) }.not_to yield_control expect(schedule_result).to be(false) diff --git a/spec/support/shared_examples/models/concerns/can_move_repository_storage_shared_examples.rb b/spec/support/shared_examples/models/concerns/can_move_repository_storage_shared_examples.rb new file mode 100644 index 00000000000..85a2c6f1449 --- /dev/null +++ b/spec/support/shared_examples/models/concerns/can_move_repository_storage_shared_examples.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'can move repository storage' do + let(:container) { raise NotImplementedError } + + describe '#set_repository_read_only!' do + it 'makes the repository read-only' do + expect { container.set_repository_read_only! } + .to change(container, :repository_read_only?) + .from(false) + .to(true) + end + + it 'raises an error if the project is already read-only' do + container.set_repository_read_only! + + expect { container.set_repository_read_only! }.to raise_error(described_class::RepositoryReadOnlyError, /already read-only/) + end + + it 'raises an error when there is an existing git transfer in progress' do + allow(container).to receive(:git_transfer_in_progress?) { true } + + expect { container.set_repository_read_only! }.to raise_error(described_class::RepositoryReadOnlyError, /in progress/) + end + + context 'skip_git_transfer_check is true' do + it 'makes the project read-only when git transfers are in progress' do + allow(container).to receive(:git_transfer_in_progress?) { true } + + expect { container.set_repository_read_only!(skip_git_transfer_check: true) } + .to change(container, :repository_read_only?) + .from(false) + .to(true) + end + end + end + + describe '#set_repository_writable!' do + it 'sets repository_read_only to false' do + expect { container.set_repository_writable! } + .to change(container, :repository_read_only) + .from(true).to(false) + end + end + + describe '#reference_counter' do + it 'returns a Gitlab::ReferenceCounter object' do + expect(Gitlab::ReferenceCounter).to receive(:new).with(container.repository.gl_repository).and_call_original + + result = container.reference_counter(type: container.repository.repo_type) + + expect(result).to be_a Gitlab::ReferenceCounter + 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 new file mode 100644 index 00000000000..5a8388d01df --- /dev/null +++ b/spec/support/shared_examples/models/concerns/repository_storage_movable_shared_examples.rb @@ -0,0 +1,115 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'handles repository moves' do + describe 'associations' do + it { is_expected.to belong_to(:container) } + end + + describe 'validations' do + it { is_expected.to validate_presence_of(:container) } + it { is_expected.to validate_presence_of(:state) } + it { is_expected.to validate_presence_of(:source_storage_name) } + it { is_expected.to validate_presence_of(:destination_storage_name) } + + context 'source_storage_name inclusion' do + subject { build(repository_storage_factory_key, source_storage_name: 'missing') } + + it "does not allow repository storages that don't match a label in the configuration" do + expect(subject).not_to be_valid + expect(subject.errors[:source_storage_name].first).to match(/is not included in the list/) + end + end + + context 'destination_storage_name inclusion' do + subject { build(repository_storage_factory_key, destination_storage_name: 'missing') } + + it "does not allow repository storages that don't match a label in the configuration" do + expect(subject).not_to be_valid + expect(subject.errors[:destination_storage_name].first).to match(/is not included in the list/) + end + end + + context 'container repository read-only' do + subject { build(repository_storage_factory_key, container: container) } + + it "does not allow the container to be read-only on create" do + container.update!(repository_read_only: true) + + expect(subject).not_to be_valid + expect(subject.errors[error_key].first).to match(/is read only/) + end + end + end + + describe 'defaults' do + context 'destination_storage_name' do + subject { build(repository_storage_factory_key) } + + it 'picks storage from ApplicationSetting' do + expect(Gitlab::CurrentSettings).to receive(:pick_repository_storage).and_return('picked').at_least(:once) + + expect(subject.destination_storage_name).to eq('picked') + end + end + end + + describe 'state transitions' do + before do + stub_storage_settings('test_second_storage' => { 'path' => 'tmp/tests/extra_storage' }) + end + + context 'when in the default state' do + subject(:storage_move) { create(repository_storage_factory_key, container: container, destination_storage_name: 'test_second_storage') } + + 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! + + expect(container).to be_repository_read_only + 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 + allow(storage_move.container).to receive(:set_repository_read_only!).and_raise(StandardError, 'foobar') + expect(repository_storage_worker).not_to receive(:perform_async) + + storage_move.schedule! + + expect(storage_move.errors[error_key]).to include('foobar') + end + end + end + + context 'and transits to started' do + it 'does not allow the transition' do + expect { storage_move.start! } + .to raise_error(StateMachines::InvalidTransition) + end + end + end + + context 'when started' do + subject(:storage_move) { create(repository_storage_factory_key, :started, container: container, destination_storage_name: 'test_second_storage') } + + context 'and transits to replicated' do + it 'marks the container as writable' do + storage_move.finish_replication! + + expect(container).not_to be_repository_read_only + end + end + + context 'and transits to failed' do + it 'marks the container as writable' do + storage_move.do_fail! + + expect(container).not_to be_repository_read_only + end + end + end + end +end diff --git a/spec/support/shared_examples/models/concerns/shardable_shared_examples.rb b/spec/support/shared_examples/models/concerns/shardable_shared_examples.rb index fa929d5b791..fd0639b628e 100644 --- a/spec/support/shared_examples/models/concerns/shardable_shared_examples.rb +++ b/spec/support/shared_examples/models/concerns/shardable_shared_examples.rb @@ -18,4 +18,10 @@ RSpec.shared_examples 'shardable scopes' do expect(described_class.excluding_repository_storage('default')).to eq([record_2]) end end + + describe '.for_shard' do + it 'returns the objects for a given shard' do + expect(described_class.for_shard(record_1.shard)).to eq([record_1]) + end + end end diff --git a/spec/support/shared_examples/models/mentionable_shared_examples.rb b/spec/support/shared_examples/models/mentionable_shared_examples.rb index 0ee0b7e6d88..2392658e584 100644 --- a/spec/support/shared_examples/models/mentionable_shared_examples.rb +++ b/spec/support/shared_examples/models/mentionable_shared_examples.rb @@ -92,7 +92,7 @@ RSpec.shared_examples 'a mentionable' do end end - expect(subject).to receive(:cached_markdown_fields).at_least(:once).and_call_original + expect(subject).to receive(:cached_markdown_fields).at_least(1).and_call_original subject.all_references(author) end @@ -151,7 +151,7 @@ RSpec.shared_examples 'an editable mentionable' do end it 'persists the refreshed cache so that it does not have to be refreshed every time' do - expect(subject).to receive(:refresh_markdown_cache).once.and_call_original + expect(subject).to receive(:refresh_markdown_cache).at_least(1).and_call_original subject.all_references(author) diff --git a/spec/support/shared_examples/models/resource_timebox_event_shared_examples.rb b/spec/support/shared_examples/models/resource_timebox_event_shared_examples.rb index 5198508d48b..f56e8d4e085 100644 --- a/spec/support/shared_examples/models/resource_timebox_event_shared_examples.rb +++ b/spec/support/shared_examples/models/resource_timebox_event_shared_examples.rb @@ -75,11 +75,17 @@ RSpec.shared_examples 'timebox resource event actions' do end RSpec.shared_examples 'timebox resource tracks issue metrics' do |type| - describe '#usage_metrics' do - it 'tracks usage' do + describe '#issue_usage_metrics' do + it 'tracks usage for issues' do expect(Gitlab::UsageDataCounters::IssueActivityUniqueCounter).to receive(:"track_issue_#{type}_changed_action") create(described_class.name.underscore.to_sym, issue: create(:issue)) end + + it 'does not track usage for merge requests' do + expect(Gitlab::UsageDataCounters::IssueActivityUniqueCounter).not_to receive(:"track_issue_#{type}_changed_action") + + create(described_class.name.underscore.to_sym, merge_request: create(:merge_request)) + end end end diff --git a/spec/support/shared_examples/quick_actions/issue/clone_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issue/clone_quick_action_shared_examples.rb new file mode 100644 index 00000000000..a99304f7214 --- /dev/null +++ b/spec/support/shared_examples/quick_actions/issue/clone_quick_action_shared_examples.rb @@ -0,0 +1,187 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'clone quick action' do + context 'clone the issue to another project' do + let(:target_project) { create(:project, :public) } + + context 'when no target is given' do + it 'clones the issue in the current project' do + add_note("/clone") + + expect(page).to have_content "Cloned this issue to #{project.full_path}." + expect(issue.reload).to be_open + + visit project_issue_path(project, issue) + + expect(page).to have_content 'Issues 2' + end + end + + context 'when the project is valid' do + before do + target_project.add_maintainer(user) + end + + it 'clones the issue' do + add_note("/clone #{target_project.full_path}") + + expect(page).to have_content "Cloned this issue to #{target_project.full_path}." + expect(issue.reload).to be_open + + visit project_issue_path(target_project, issue) + + expect(page).to have_content 'Issues 1' + end + + context 'when cloning with notes', :aggregate_failures do + it 'clones the issue with all notes' do + add_note("Some random note") + add_note("Another note") + + add_note("/clone --with_notes #{target_project.full_path}") + + expect(page).to have_content "Cloned this issue to #{target_project.full_path}." + expect(issue.reload).to be_open + + visit project_issue_path(target_project, issue) + + expect(page).to have_content 'Issues 1' + expect(page).to have_content 'Some random note' + expect(page).to have_content 'Another note' + end + + it 'returns an error if the params are malformed' do + # Note that this is missing one `-` + add_note("/clone -with_notes #{target_project.full_path}") + + wait_for_requests + + expect(page).to have_content 'Failed to clone this issue: wrong parameters.' + expect(issue.reload).to be_open + end + end + end + + context 'when the project is valid but the user not authorized' do + let(:project_unauthorized) { create(:project, :public) } + + it 'does not clone the issue' do + add_note("/clone #{project_unauthorized.full_path}") + + wait_for_requests + + expect(page).to have_content "Cloned this issue to #{project_unauthorized.full_path}." + expect(issue.reload).to be_open + + visit project_issue_path(target_project, issue) + + expect(page).not_to have_content 'Issues 1' + end + end + + context 'when the project is invalid' do + it 'does not clone the issue' do + add_note("/clone not/valid") + + wait_for_requests + + expect(page).to have_content "Failed to clone this issue because target project doesn't exist." + expect(issue.reload).to be_open + end + end + + context 'when the user issues multiple commands' do + let(:milestone) { create(:milestone, title: '1.0', project: project) } + let(:bug) { create(:label, project: project, title: 'bug') } + let(:wontfix) { create(:label, project: project, title: 'wontfix') } + + let!(:target_milestone) { create(:milestone, title: '1.0', project: target_project) } + + before do + target_project.add_maintainer(user) + end + + shared_examples 'applies the commands to issues in both projects, target and source' do + it "applies quick actions" do + expect(page).to have_content "Cloned this issue to #{target_project.full_path}." + expect(issue.reload).to be_open + + visit project_issue_path(target_project, issue) + + expect(page).to have_content 'bug' + expect(page).to have_content 'wontfix' + expect(page).to have_content '1.0' + + visit project_issue_path(project, issue) + expect(page).to have_content 'bug' + expect(page).to have_content 'wontfix' + expect(page).to have_content '1.0' + end + end + + context 'applies multiple commands with clone command in the end' do + before do + add_note("/label ~#{bug.title} ~#{wontfix.title}\n\n/milestone %\"#{milestone.title}\"\n\n/clone #{target_project.full_path}") + end + + it_behaves_like 'applies the commands to issues in both projects, target and source' + end + + context 'applies multiple commands with clone command in the begining' do + before do + add_note("/clone #{target_project.full_path}\n\n/label ~#{bug.title} ~#{wontfix.title}\n\n/milestone %\"#{milestone.title}\"") + end + + it_behaves_like 'applies the commands to issues in both projects, target and source' + end + end + + context 'when editing comments' do + let(:target_project) { create(:project, :public) } + + before do + target_project.add_maintainer(user) + + sign_in(user) + visit project_issue_path(project, issue) + wait_for_all_requests + end + + it 'clones the issue after quickcommand note was updated' do + # misspelled quick action + add_note("test note.\n/cloe #{target_project.full_path}") + + expect(issue.reload).not_to be_closed + + edit_note("/cloe #{target_project.full_path}", "test note.\n/clone #{target_project.full_path}") + wait_for_all_requests + + expect(page).to have_content 'test note.' + expect(issue.reload).to be_open + + visit project_issue_path(target_project, issue) + wait_for_all_requests + + expect(page).to have_content 'Issues 1' + end + + it 'deletes the note if it was updated to just contain a command' do + # missspelled quick action + add_note("test note.\n/cloe #{target_project.full_path}") + + expect(page).not_to have_content 'Commands applied' + + edit_note("/cloe #{target_project.full_path}", "/clone #{target_project.full_path}") + wait_for_all_requests + + expect(page).not_to have_content "/clone #{target_project.full_path}" + expect(issue.reload).to be_open + + visit project_issue_path(target_project, issue) + wait_for_all_requests + + expect(page).to have_content 'Issues 1' + end + end + end +end diff --git a/spec/support/shared_examples/requests/api/conan_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/conan_packages_shared_examples.rb index c56290a0aa9..49b6fc13900 100644 --- a/spec/support/shared_examples/requests/api/conan_packages_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/conan_packages_shared_examples.rb @@ -629,6 +629,7 @@ RSpec.shared_examples 'workhorse recipe file upload endpoint' do it_behaves_like 'rejects invalid recipe' it_behaves_like 'rejects invalid file_name', 'conanfile.py.git%2fgit-upload-pack' it_behaves_like 'uploads a package file' + it_behaves_like 'creates build_info when there is a job' end RSpec.shared_examples 'workhorse package file upload endpoint' do @@ -649,6 +650,7 @@ RSpec.shared_examples 'workhorse package file upload endpoint' do it_behaves_like 'rejects invalid recipe' it_behaves_like 'rejects invalid file_name', 'conaninfo.txttest' it_behaves_like 'uploads a package file' + it_behaves_like 'creates build_info when there is a job' context 'tracking the conan_package.tgz upload' do let(:file_name) { ::Packages::Conan::FileMetadatum::PACKAGE_BINARY } @@ -657,6 +659,20 @@ RSpec.shared_examples 'workhorse package file upload endpoint' do end end +RSpec.shared_examples 'creates build_info when there is a job' do + context 'with job token' do + let(:jwt) { build_jwt_from_job(job) } + + it 'creates a build_info record' do + expect { subject }.to change { Packages::BuildInfo.count }.by(1) + end + + it 'creates a package_file_build_info record' do + expect { subject }.to change { Packages::PackageFileBuildInfo.count }.by(1) + end + end +end + RSpec.shared_examples 'uploads a package file' do context 'file size above maximum limit' do before do diff --git a/spec/support/shared_examples/requests/api/graphql/group_and_project_boards_query_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/group_and_project_boards_query_shared_examples.rb index 5145880ef9a..54f4ba7ff73 100644 --- a/spec/support/shared_examples/requests/api/graphql/group_and_project_boards_query_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/graphql/group_and_project_boards_query_shared_examples.rb @@ -47,18 +47,12 @@ RSpec.shared_examples 'group and project boards query' do describe 'sorting and pagination' do let(:data_path) { [board_parent_type, :boards] } - def pagination_query(params, page_info) - graphql_query_for( - board_parent_type, - { 'fullPath' => board_parent.full_path }, - query_graphql_field('boards', params, "#{page_info} edges { node { id } }") + def pagination_query(params) + graphql_query_for(board_parent_type, { full_path: board_parent.full_path }, + query_nodes(:boards, :id, include_pagination_info: true, args: params) ) end - def pagination_results_data(data) - data.map { |board| board.dig('node', 'id') } - end - context 'when using default sorting' do let!(:board_B) { create(:board, resource_parent: board_parent, name: 'B') } let!(:board_C) { create(:board, resource_parent: board_parent, name: 'C') } @@ -72,9 +66,9 @@ RSpec.shared_examples 'group and project boards query' do let(:first_param) { 2 } let(:expected_results) do if board_parent.multiple_issue_boards_available? - boards.map { |board| board.to_global_id.to_s } + boards.map { |board| global_id_of(board) } else - [boards.first.to_global_id.to_s] + [global_id_of(boards.first)] 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 new file mode 100644 index 00000000000..f808d12baf4 --- /dev/null +++ b/spec/support/shared_examples/requests/api/nuget_endpoints_shared_examples.rb @@ -0,0 +1,265 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'handling nuget service requests' do + subject { get api(url) } + + context 'with valid project' 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 + end + + with_them do + let(:token) { user_token ? personal_access_token.token : 'wrong' } + let(:headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) } + + subject { get api(url), headers: headers } + + before do + project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility_level, false)) + end + + it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member] + end + end + + context 'with job 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 | '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 | '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 + end + + with_them do + let(:job) { user_token ? create(:ci_build, project: project, user: user, status: :running) : double(token: 'wrong') } + let(:headers) { user_role == :anonymous ? {} : job_basic_auth_header(job) } + + subject { get api(url), headers: headers } + + before do + project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility_level, false)) + end + + it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member] + end + end + end + + it_behaves_like 'deploy token for package GET requests' + + it_behaves_like 'rejects nuget access with unknown project id' + + it_behaves_like 'rejects nuget access with invalid project id' +end + +RSpec.shared_examples 'handling nuget metadata requests with package name' do + include_context 'with expected presenters dependency groups' + + let_it_be(:package_name) { 'Dummy.Package' } + let_it_be(:packages) { create_list(:nuget_package, 5, :with_metadatum, name: package_name, project: project) } + let_it_be(:tags) { packages.each { |pkg| create(:packages_tag, package: pkg, name: 'test') } } + + subject { get api(url) } + + before do + packages.each { |pkg| create_dependencies_for(pkg) } + end + + context 'with valid project' do + using RSpec::Parameterized::TableSyntax + + where(:project_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 | 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 + '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 + '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 + let(:token) { user_token ? personal_access_token.token : 'wrong' } + let(:headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) } + + subject { get api(url), headers: headers } + + before do + project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_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 'rejects nuget access with unknown project id' + + it_behaves_like 'rejects nuget access with invalid project id' + end +end + +RSpec.shared_examples 'handling nuget metadata requests with package name and package version' do + include_context 'with expected presenters dependency groups' + + let_it_be(:package_name) { 'Dummy.Package' } + let_it_be(:package) { create(:nuget_package, :with_metadatum, name: 'Dummy.Package', project: project) } + let_it_be(:tag) { create(:packages_tag, package: package, name: 'test') } + + subject { get api(url) } + + before do + create_dependencies_for(package) + end + + context 'with valid project' do + using RSpec::Parameterized::TableSyntax + + where(:project_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 | 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 + '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 + '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 + let(:token) { user_token ? personal_access_token.token : 'wrong' } + let(:headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) } + + subject { get api(url), headers: headers } + + before do + project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_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' + + context 'with invalid package name' do + let_it_be(:package_name) { 'Unkown' } + + it_behaves_like 'rejects nuget packages access', :developer, :not_found + end +end + +RSpec.shared_examples 'handling nuget search requests' do + 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) } + let_it_be(:packages_c) { create_list(:nuget_package, 5, name: 'Dummy.PackageC', project: project) } + let_it_be(:package_d) { create(:nuget_package, name: 'Dummy.PackageD', version: '5.0.5-alpha', project: project) } + let_it_be(:package_e) { create(:nuget_package, name: 'Foo.BarE', project: project) } + let(:search_term) { 'uMmy' } + let(:take) { 26 } + let(:skip) { 0 } + let(:include_prereleases) { true } + let(:query_parameters) { { q: search_term, take: take, skip: skip, prerelease: include_prereleases } } + + subject { get api(url) } + + context 'with valid project' do + using RSpec::Parameterized::TableSyntax + + where(:project_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 | 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 + '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 + '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 + let(:token) { user_token ? personal_access_token.token : 'wrong' } + let(:headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) } + + subject { get api(url), headers: headers } + + before do + project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_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 'rejects nuget access with unknown project id' + + it_behaves_like 'rejects nuget access with invalid project 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 58e99776fd9..dc6ac5f0371 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 @@ -12,7 +12,7 @@ RSpec.shared_examples 'rejects nuget packages access' do |user_type, status, add it 'has the correct response header' do subject - expect(response.headers['Www-Authenticate: Basic realm']).to eq 'GitLab Packages Registry' + expect(response.headers['WWW-Authenticate']).to eq 'Basic realm="GitLab Packages Registry"' end end end @@ -26,7 +26,7 @@ RSpec.shared_examples 'process nuget service index request' do |user_type, statu it_behaves_like 'returning response status', status - it_behaves_like 'a package tracking event', described_class.name, 'cli_metadata' + it_behaves_like 'a package tracking event', 'API::NugetPackages', 'cli_metadata' it 'returns a valid json response' do subject @@ -169,7 +169,7 @@ RSpec.shared_examples 'process nuget upload' do |user_type, status, add_member = context 'with correct params' do it_behaves_like 'package workhorse uploads' it_behaves_like 'creates nuget package files' - it_behaves_like 'a package tracking event', described_class.name, 'push_package' + it_behaves_like 'a package tracking event', 'API::NugetPackages', 'push_package' end end @@ -286,7 +286,7 @@ RSpec.shared_examples 'process nuget download content request' do |user_type, st it_behaves_like 'returning response status', status - it_behaves_like 'a package tracking event', described_class.name, 'pull_package' + it_behaves_like 'a package tracking event', 'API::NugetPackages', 'pull_package' it 'returns a valid package archive' do subject @@ -336,7 +336,7 @@ RSpec.shared_examples 'process nuget search request' do |user_type, status, add_ it_behaves_like 'returns a valid json search response', status, 4, [1, 5, 5, 1] - it_behaves_like 'a package tracking event', described_class.name, 'search_package' + it_behaves_like 'a package tracking event', 'API::NugetPackages', 'search_package' context 'with skip set to 2' do let(:skip) { 2 } diff --git a/spec/support/shared_examples/requests/graphql_shared_examples.rb b/spec/support/shared_examples/requests/graphql_shared_examples.rb index 0045fe14501..a66bc7112fe 100644 --- a/spec/support/shared_examples/requests/graphql_shared_examples.rb +++ b/spec/support/shared_examples/requests/graphql_shared_examples.rb @@ -9,3 +9,8 @@ RSpec.shared_examples 'a working graphql query' do expect(json_response.keys).to include('data') end end + +RSpec.shared_examples 'a mutation on an unauthorized resource' do + it_behaves_like 'a mutation that returns top-level errors', + errors: [::Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR] +end diff --git a/spec/support/shared_examples/requests/lfs_http_shared_examples.rb b/spec/support/shared_examples/requests/lfs_http_shared_examples.rb index 4ae77179527..294ceffd77b 100644 --- a/spec/support/shared_examples/requests/lfs_http_shared_examples.rb +++ b/spec/support/shared_examples/requests/lfs_http_shared_examples.rb @@ -65,12 +65,19 @@ end RSpec.shared_examples 'LFS http requests' do include LfsHttpHelpers + let(:lfs_enabled) { true } let(:authorize_guest) {} let(:authorize_download) {} let(:authorize_upload) {} let(:lfs_object) { create(:lfs_object, :with_file) } let(:sample_oid) { lfs_object.oid } + let(:sample_size) { lfs_object.size } + let(:sample_object) { { 'oid' => sample_oid, 'size' => sample_size } } + let(:non_existing_object_oid) { '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897' } + let(:non_existing_object_size) { 1575078 } + let(:non_existing_object) { { 'oid' => non_existing_object_oid, 'size' => non_existing_object_size } } + let(:multiple_objects) { [sample_object, non_existing_object] } let(:authorization) { authorize_user } let(:headers) do @@ -89,13 +96,11 @@ RSpec.shared_examples 'LFS http requests' do end before do - stub_lfs_setting(enabled: true) + stub_lfs_setting(enabled: lfs_enabled) end context 'when LFS is disabled globally' do - before do - stub_lfs_setting(enabled: false) - end + let(:lfs_enabled) { false } describe 'download request' do before 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 d4ee68309ff..5d300d38e4a 100644 --- a/spec/support/shared_examples/requests/rack_attack_shared_examples.rb +++ b/spec/support/shared_examples/requests/rack_attack_shared_examples.rb @@ -23,6 +23,11 @@ RSpec.shared_examples 'rate-limited token-authenticated requests' do settings_to_set[:"#{throttle_setting_prefix}_period_in_seconds"] = period_in_seconds end + after do + stub_env('GITLAB_THROTTLE_USER_ALLOWLIST', nil) + Gitlab::RackAttack.configure_user_allowlist + end + context 'when the throttle is enabled' do before do settings_to_set[:"#{throttle_setting_prefix}_enabled"] = true @@ -30,6 +35,8 @@ RSpec.shared_examples 'rate-limited token-authenticated requests' do end it 'rejects requests over the rate limit' do + expect(Gitlab::Instrumentation::Throttle).not_to receive(:safelist=) + # At first, allow requests under the rate limit. requests_per_period.times do make_request(request_args) @@ -40,6 +47,18 @@ RSpec.shared_examples 'rate-limited token-authenticated requests' do expect_rejection { make_request(request_args) } end + it 'does not reject requests if the user is in the allowlist' do + stub_env('GITLAB_THROTTLE_USER_ALLOWLIST', user.id.to_s) + Gitlab::RackAttack.configure_user_allowlist + + expect(Gitlab::Instrumentation::Throttle).to receive(:safelist=).with('throttle_user_allowlist').at_least(:once) + + (requests_per_period + 1).times do + make_request(request_args) + expect(response).not_to have_gitlab_http_status(:too_many_requests) + end + end + it 'allows requests after throttling and then waiting for the next period' do requests_per_period.times do make_request(request_args) @@ -110,6 +129,14 @@ RSpec.shared_examples 'rate-limited token-authenticated requests' do expect { make_request(request_args) }.not_to exceed_query_limit(control_count) end end + + it_behaves_like 'tracking when dry-run mode is set' do + let(:throttle_name) { throttle_types[throttle_setting_prefix] } + + def do_request + make_request(request_args) + end + end end context 'when the throttle is disabled' do @@ -159,6 +186,11 @@ RSpec.shared_examples 'rate-limited web authenticated requests' do settings_to_set[:"#{throttle_setting_prefix}_period_in_seconds"] = period_in_seconds end + after do + stub_env('GITLAB_THROTTLE_USER_ALLOWLIST', nil) + Gitlab::RackAttack.configure_user_allowlist + end + context 'when the throttle is enabled' do before do settings_to_set[:"#{throttle_setting_prefix}_enabled"] = true @@ -166,6 +198,8 @@ RSpec.shared_examples 'rate-limited web authenticated requests' do end it 'rejects requests over the rate limit' do + expect(Gitlab::Instrumentation::Throttle).not_to receive(:safelist=) + # At first, allow requests under the rate limit. requests_per_period.times do request_authenticated_web_url @@ -176,6 +210,18 @@ RSpec.shared_examples 'rate-limited web authenticated requests' do expect_rejection { request_authenticated_web_url } end + it 'does not reject requests if the user is in the allowlist' do + stub_env('GITLAB_THROTTLE_USER_ALLOWLIST', user.id.to_s) + Gitlab::RackAttack.configure_user_allowlist + + expect(Gitlab::Instrumentation::Throttle).to receive(:safelist=).with('throttle_user_allowlist').at_least(:once) + + (requests_per_period + 1).times do + request_authenticated_web_url + expect(response).not_to have_gitlab_http_status(:too_many_requests) + end + end + it 'allows requests after throttling and then waiting for the next period' do requests_per_period.times do request_authenticated_web_url @@ -245,6 +291,14 @@ RSpec.shared_examples 'rate-limited web authenticated requests' do expect(Gitlab::AuthLogger).to receive(:error).with(arguments).once expect { request_authenticated_web_url }.not_to exceed_query_limit(control_count) end + + it_behaves_like 'tracking when dry-run mode is set' do + let(:throttle_name) { throttle_types[throttle_setting_prefix] } + + def do_request + request_authenticated_web_url + end + end end context 'when the throttle is disabled' do @@ -269,3 +323,63 @@ RSpec.shared_examples 'rate-limited web authenticated requests' do end end end + +# Requires: +# - #do_request - This needs to be a method so the result isn't memoized +# - throttle_name +RSpec.shared_examples 'tracking when dry-run mode is set' do + let(:dry_run_config) { '*' } + + # we can't use `around` here, because stub_env isn't supported outside of the + # example itself + before do + stub_env('GITLAB_THROTTLE_DRY_RUN', dry_run_config) + reset_rack_attack + end + + after do + stub_env('GITLAB_THROTTLE_DRY_RUN', '') + reset_rack_attack + end + + def reset_rack_attack + Rack::Attack.reset! + Rack::Attack.clear_configuration + Gitlab::RackAttack.configure(Rack::Attack) + end + + it 'does not throttle the requests when `*` is configured' do + (1 + requests_per_period).times do + do_request + expect(response).not_to have_gitlab_http_status(:too_many_requests) + end + end + + it 'logs RackAttack info into structured logs' do + arguments = a_hash_including({ + message: 'Rack_Attack', + env: :track, + remote_ip: '127.0.0.1', + matched: throttle_name + }) + + expect(Gitlab::AuthLogger).to receive(:error).with(arguments) + + (1 + requests_per_period).times do + do_request + end + end + + context 'when configured with the the throttled name in a list' do + let(:dry_run_config) do + "throttle_list, #{throttle_name}, other_throttle" + end + + it 'does not throttle' do + (1 + requests_per_period).times do + do_request + expect(response).not_to have_gitlab_http_status(:too_many_requests) + end + end + end +end diff --git a/spec/support/shared_examples/requests/self_monitoring_shared_examples.rb b/spec/support/shared_examples/requests/self_monitoring_shared_examples.rb index db11b1fe07d..ff87fc5d8df 100644 --- a/spec/support/shared_examples/requests/self_monitoring_shared_examples.rb +++ b/spec/support/shared_examples/requests/self_monitoring_shared_examples.rb @@ -20,6 +20,18 @@ RSpec.shared_examples 'not accessible to non-admin users' do expect(response).to have_gitlab_http_status(:not_found) end end + + context 'with authenticated admin user without admin mode' do + before do + login_as(create(:admin)) + end + + it 'redirects to enable admin mode' do + subject + + expect(response).to redirect_to(new_admin_session_path) + end + end end # Requires subject and worker_class and status_api to be defined diff --git a/spec/support/shared_examples/requests/uploads_auhorize_shared_examples.rb b/spec/support/shared_examples/requests/uploads_auhorize_shared_examples.rb new file mode 100644 index 00000000000..9cef5cfc25e --- /dev/null +++ b/spec/support/shared_examples/requests/uploads_auhorize_shared_examples.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'handle uploads authorize request' do + before do + login_as(user) + end + + describe 'POST authorize' do + it 'authorizes workhorse header' do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(response.media_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE) + expect(json_response['TempPath']).to eq(uploader_class.workhorse_local_upload_path) + end + + it 'rejects requests that bypassed gitlab-workhorse' do + workhorse_headers.delete(Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER) + + expect { subject }.to raise_error(JWT::DecodeError) + end + + context 'when using remote storage' do + context 'when direct upload is enabled' do + before do + stub_uploads_object_storage(uploader_class, enabled: true, direct_upload: true) + end + + it 'responds with status 200, location of file remote store and object details' do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(response.media_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE) + expect(json_response).not_to have_key('TempPath') + expect(json_response['RemoteObject']).to have_key('ID') + expect(json_response['RemoteObject']).to have_key('GetURL') + expect(json_response['RemoteObject']).to have_key('StoreURL') + expect(json_response['RemoteObject']).to have_key('DeleteURL') + expect(json_response['RemoteObject']).to have_key('MultipartUpload') + expect(json_response['MaximumSize']).to eq(maximum_size) + end + end + + context 'when direct upload is disabled' do + before do + stub_uploads_object_storage(uploader_class, enabled: true, direct_upload: false) + end + + it 'handles as a local file' do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(response.media_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE) + expect(json_response['TempPath']).to eq(uploader_class.workhorse_local_upload_path) + expect(json_response['RemoteObject']).to be_nil + expect(json_response['MaximumSize']).to eq(maximum_size) + end + end + end + end +end diff --git a/spec/support/shared_examples/routing/git_http_routing_shared_examples.rb b/spec/support/shared_examples/routing/git_http_routing_shared_examples.rb new file mode 100644 index 00000000000..b0e1e942d81 --- /dev/null +++ b/spec/support/shared_examples/routing/git_http_routing_shared_examples.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'git repository routes' do + let(:params) { { repository_path: path.delete_prefix('/') } } + let(:container_path) { path.delete_suffix('.git') } + + it 'routes Git endpoints' do + expect(get("#{path}/info/refs")).to route_to('repositories/git_http#info_refs', **params) + expect(post("#{path}/git-upload-pack")).to route_to('repositories/git_http#git_upload_pack', **params) + expect(post("#{path}/git-receive-pack")).to route_to('repositories/git_http#git_receive_pack', **params) + end + + context 'requests without .git format' do + it 'redirects requests to /info/refs', type: :request do + expect(get("#{container_path}/info/refs")).to redirect_to("#{container_path}.git/info/refs") + expect(get("#{container_path}/info/refs?service=git-upload-pack")).to redirect_to("#{container_path}.git/info/refs?service=git-upload-pack") + expect(get("#{container_path}/info/refs?service=git-receive-pack")).to redirect_to("#{container_path}.git/info/refs?service=git-receive-pack") + end + + it 'does not redirect other requests' do + expect(post("#{container_path}/git-upload-pack")).not_to be_routable + end + end + + it 'routes LFS endpoints' do + oid = generate(:oid) + + expect(post("#{path}/info/lfs/objects/batch")).to route_to('repositories/lfs_api#batch', **params) + expect(post("#{path}/info/lfs/objects")).to route_to('repositories/lfs_api#deprecated', **params) + expect(get("#{path}/info/lfs/objects/#{oid}")).to route_to('repositories/lfs_api#deprecated', oid: oid, **params) + + expect(post("#{path}/info/lfs/locks/123/unlock")).to route_to('repositories/lfs_locks_api#unlock', id: '123', **params) + expect(post("#{path}/info/lfs/locks/verify")).to route_to('repositories/lfs_locks_api#verify', **params) + + expect(get("#{path}/gitlab-lfs/objects/#{oid}")).to route_to('repositories/lfs_storage#download', oid: oid, **params) + expect(put("#{path}/gitlab-lfs/objects/#{oid}/456/authorize")).to route_to('repositories/lfs_storage#upload_authorize', oid: oid, size: '456', **params) + expect(put("#{path}/gitlab-lfs/objects/#{oid}/456")).to route_to('repositories/lfs_storage#upload_finalize', oid: oid, size: '456', **params) + + expect(put("#{path}/gitlab-lfs/objects/foo")).not_to be_routable + expect(put("#{path}/gitlab-lfs/objects/#{oid}/foo")).not_to be_routable + expect(put("#{path}/gitlab-lfs/objects/#{oid}/foo/authorize")).not_to be_routable + end +end diff --git a/spec/support/shared_examples/services/alert_management_shared_examples.rb b/spec/support/shared_examples/services/alert_management_shared_examples.rb index 003705ca21c..d9f28a97a0f 100644 --- a/spec/support/shared_examples/services/alert_management_shared_examples.rb +++ b/spec/support/shared_examples/services/alert_management_shared_examples.rb @@ -16,6 +16,38 @@ RSpec.shared_examples 'creates an alert management alert' do end end +# This shared_example requires the following variables: +# - last_alert_attributes, last created alert +# - project, project that alert created +# - payload_raw, hash representation of payload +# - environment, project's environment +# - fingerprint, fingerprint hash +RSpec.shared_examples 'assigns the alert properties' do + it 'ensures that created alert has all data properly assigned' do + subject + + expect(last_alert_attributes).to match( + project_id: project.id, + title: payload_raw.fetch(:title), + started_at: Time.zone.parse(payload_raw.fetch(:start_time)), + severity: payload_raw.fetch(:severity), + status: AlertManagement::Alert.status_value(:triggered), + events: 1, + domain: domain, + hosts: payload_raw.fetch(:hosts), + payload: payload_raw.with_indifferent_access, + issue_id: nil, + description: payload_raw.fetch(:description), + monitoring_tool: payload_raw.fetch(:monitoring_tool), + service: payload_raw.fetch(:service), + fingerprint: Digest::SHA1.hexdigest(fingerprint), + environment_id: environment.id, + ended_at: nil, + prometheus_alert_id: nil + ) + end +end + RSpec.shared_examples 'does not an create alert management alert' do it 'does not create alert' do expect { subject }.not_to change(AlertManagement::Alert, :count) diff --git a/spec/support/shared_examples/services/container_registry_auth_service_shared_examples.rb b/spec/support/shared_examples/services/container_registry_auth_service_shared_examples.rb new file mode 100644 index 00000000000..ba176b616c3 --- /dev/null +++ b/spec/support/shared_examples/services/container_registry_auth_service_shared_examples.rb @@ -0,0 +1,1006 @@ +# frozen_string_literal: true + +RSpec.shared_context 'container registry auth service context' do + let(:current_project) { nil } + let(:current_user) { nil } + let(:current_params) { {} } + let(:rsa_key) { OpenSSL::PKey::RSA.generate(512) } + let(:payload) { JWT.decode(subject[:token], rsa_key, true, { algorithm: 'RS256' }).first } + + let(:authentication_abilities) do + [:read_container_image, :create_container_image, :admin_container_image] + end + + let(:log_data) { { message: 'Denied container registry permissions' } } + + subject do + described_class.new(current_project, current_user, current_params) + .execute(authentication_abilities: authentication_abilities) + end + + before do + allow(Gitlab.config.registry).to receive_messages(enabled: true, issuer: 'rspec', key: nil) + allow_next_instance_of(JSONWebToken::RSAToken) do |instance| + allow(instance).to receive(:key).and_return(rsa_key) + end + end +end + +RSpec.shared_examples 'an authenticated' do + it { is_expected.to include(:token) } + it { expect(payload).to include('access') } +end + +RSpec.shared_examples 'a valid token' do + it { is_expected.to include(:token) } + it { expect(payload).to include('access') } + + context 'a expirable' do + let(:expires_at) { Time.zone.at(payload['exp']) } + let(:expire_delay) { 10 } + + context 'for default configuration' do + it { expect(expires_at).not_to be_within(2.seconds).of(Time.current + expire_delay.minutes) } + end + + context 'for changed configuration' do + before do + stub_application_setting(container_registry_token_expire_delay: expire_delay) + end + + it { expect(expires_at).to be_within(2.seconds).of(Time.current + expire_delay.minutes) } + end + end +end + +RSpec.shared_examples 'a browsable' do + let(:access) do + [{ 'type' => 'registry', + 'name' => 'catalog', + 'actions' => ['*'] }] + end + + it_behaves_like 'a valid token' + it_behaves_like 'not a container repository factory' + + it 'has the correct scope' do + expect(payload).to include('access' => access) + end +end + +RSpec.shared_examples 'an accessible' do + let(:access) do + [{ 'type' => 'repository', + 'name' => project.full_path, + 'actions' => actions }] + end + + it_behaves_like 'a valid token' + + it 'has the correct scope' do + expect(payload).to include('access' => access) + end +end + +RSpec.shared_examples 'an inaccessible' do + it_behaves_like 'a valid token' + it { expect(payload).to include('access' => []) } +end + +RSpec.shared_examples 'a deletable' do + it_behaves_like 'an accessible' do + let(:actions) { ['*'] } + end +end + +RSpec.shared_examples 'a deletable since registry 2.7' do + it_behaves_like 'an accessible' do + let(:actions) { ['delete'] } + end +end + +RSpec.shared_examples 'a pullable' do + it_behaves_like 'an accessible' do + let(:actions) { ['pull'] } + end +end + +RSpec.shared_examples 'a pushable' do + it_behaves_like 'an accessible' do + let(:actions) { ['push'] } + end +end + +RSpec.shared_examples 'a pullable and pushable' do + it_behaves_like 'an accessible' do + let(:actions) { %w(pull push) } + end +end + +RSpec.shared_examples 'a forbidden' do + it { is_expected.to include(http_status: 403) } + it { is_expected.not_to include(:token) } +end + +RSpec.shared_examples 'container repository factory' do + it 'creates a new container repository resource' do + expect { subject } + .to change { project.container_repositories.count }.by(1) + end +end + +RSpec.shared_examples 'not a container repository factory' do + it 'does not create a new container repository resource' do + expect { subject }.not_to change { ContainerRepository.count } + end +end + +RSpec.shared_examples 'logs an auth warning' do |requested_actions| + let(:expected) do + { + scope_type: 'repository', + requested_project_path: project.full_path, + requested_actions: requested_actions, + authorized_actions: [], + user_id: current_user.id, + username: current_user.username + } + end + + it do + expect(Gitlab::AuthLogger).to receive(:warn).with(expected.merge!(log_data)) + + subject + end +end + +RSpec.shared_examples 'a container registry auth service' do + include_context 'container registry auth service context' + + describe '#full_access_token' do + let_it_be(:project) { create(:project) } + let(:token) { described_class.full_access_token(project.full_path) } + + subject { { token: token } } + + it_behaves_like 'an accessible' do + let(:actions) { ['*'] } + end + + it_behaves_like 'not a container repository factory' + end + + describe '#pull_access_token' do + let_it_be(:project) { create(:project) } + let(:token) { described_class.pull_access_token(project.full_path) } + + subject { { token: token } } + + it_behaves_like 'an accessible' do + let(:actions) { ['pull'] } + end + + it_behaves_like 'not a container repository factory' + end + + context 'user authorization' do + let_it_be(:current_user) { create(:user) } + + context 'for registry catalog' do + let(:current_params) do + { scopes: ["registry:catalog:*"] } + end + + context 'disallow browsing for users without GitLab admin rights' do + it_behaves_like 'an inaccessible' + it_behaves_like 'not a container repository factory' + end + end + + context 'for private project' do + let_it_be(:project) { create(:project) } + + context 'allow to use scope-less authentication' do + it_behaves_like 'a valid token' + end + + context 'allow developer to push images' do + before_all do + project.add_developer(current_user) + end + + let(:current_params) do + { scopes: ["repository:#{project.full_path}:push"] } + end + + it_behaves_like 'a pushable' + it_behaves_like 'container repository factory' + end + + context 'disallow developer to delete images' do + before_all do + project.add_developer(current_user) + end + + let(:current_params) do + { scopes: ["repository:#{project.full_path}:*"] } + end + + it_behaves_like 'an inaccessible' + it_behaves_like 'not a container repository factory' + + it_behaves_like 'logs an auth warning', ['*'] + end + + context 'disallow developer to delete images since registry 2.7' do + before_all do + project.add_developer(current_user) + end + + let(:current_params) do + { scopes: ["repository:#{project.full_path}:delete"] } + end + + it_behaves_like 'an inaccessible' + it_behaves_like 'not a container repository factory' + end + + context 'allow reporter to pull images' do + before_all do + project.add_reporter(current_user) + end + + context 'when pulling from root level repository' do + let(:current_params) do + { scopes: ["repository:#{project.full_path}:pull"] } + end + + it_behaves_like 'a pullable' + it_behaves_like 'not a container repository factory' + end + end + + context 'disallow reporter to delete images' do + before_all do + project.add_reporter(current_user) + end + + let(:current_params) do + { scopes: ["repository:#{project.full_path}:*"] } + end + + it_behaves_like 'an inaccessible' + it_behaves_like 'not a container repository factory' + end + + context 'disallow reporter to delete images since registry 2.7' do + before_all do + project.add_reporter(current_user) + end + + let(:current_params) do + { scopes: ["repository:#{project.full_path}:delete"] } + end + + it_behaves_like 'an inaccessible' + it_behaves_like 'not a container repository factory' + end + + context 'return a least of privileges' do + before_all do + project.add_reporter(current_user) + end + + let(:current_params) do + { scopes: ["repository:#{project.full_path}:push,pull"] } + end + + it_behaves_like 'a pullable' + it_behaves_like 'not a container repository factory' + end + + context 'disallow guest to pull or push images' do + before_all do + project.add_guest(current_user) + end + + let(:current_params) do + { scopes: ["repository:#{project.full_path}:pull,push"] } + end + + it_behaves_like 'an inaccessible' + it_behaves_like 'not a container repository factory' + end + + context 'disallow guest to delete images' do + before_all do + project.add_guest(current_user) + end + + let(:current_params) do + { scopes: ["repository:#{project.full_path}:*"] } + end + + it_behaves_like 'an inaccessible' + it_behaves_like 'not a container repository factory' + end + + context 'disallow guest to delete images since registry 2.7' do + before_all do + project.add_guest(current_user) + end + + let(:current_params) do + { scopes: ["repository:#{project.full_path}:delete"] } + end + + it_behaves_like 'an inaccessible' + it_behaves_like 'not a container repository factory' + end + end + + context 'for public project' do + let_it_be(:project) { create(:project, :public) } + + context 'allow anyone to pull images' do + let(:current_params) do + { scopes: ["repository:#{project.full_path}:pull"] } + end + + it_behaves_like 'a pullable' + it_behaves_like 'not a container repository factory' + end + + context 'disallow anyone to push images' do + let(:current_params) do + { scopes: ["repository:#{project.full_path}:push"] } + end + + it_behaves_like 'an inaccessible' + it_behaves_like 'not a container repository factory' + end + + context 'disallow anyone to delete images' do + let(:current_params) do + { scopes: ["repository:#{project.full_path}:*"] } + end + + it_behaves_like 'an inaccessible' + it_behaves_like 'not a container repository factory' + end + + context 'disallow anyone to delete images since registry 2.7' do + let(:current_params) do + { scopes: ["repository:#{project.full_path}:delete"] } + end + + it_behaves_like 'an inaccessible' + it_behaves_like 'not a container repository factory' + end + + context 'when repository name is invalid' do + let(:current_params) do + { scopes: ['repository:invalid:push'] } + end + + it_behaves_like 'an inaccessible' + it_behaves_like 'not a container repository factory' + end + end + + context 'for internal project' do + let_it_be(:project) { create(:project, :internal) } + + context 'for internal user' do + context 'allow anyone to pull images' do + let(:current_params) do + { scopes: ["repository:#{project.full_path}:pull"] } + end + + it_behaves_like 'a pullable' + it_behaves_like 'not a container repository factory' + end + + context 'disallow anyone to push images' do + let(:current_params) do + { scopes: ["repository:#{project.full_path}:push"] } + end + + it_behaves_like 'an inaccessible' + it_behaves_like 'not a container repository factory' + end + + context 'disallow anyone to delete images' do + let(:current_params) do + { scopes: ["repository:#{project.full_path}:*"] } + end + + it_behaves_like 'an inaccessible' + it_behaves_like 'not a container repository factory' + end + + context 'disallow anyone to delete images since registry 2.7' do + let(:current_params) do + { scopes: ["repository:#{project.full_path}:delete"] } + end + + it_behaves_like 'an inaccessible' + it_behaves_like 'not a container repository factory' + end + end + + context 'for external user' do + context 'disallow anyone to pull or push images' do + let_it_be(:current_user) { create(:user, external: true) } + let(:current_params) do + { scopes: ["repository:#{project.full_path}:pull,push"] } + end + + it_behaves_like 'an inaccessible' + it_behaves_like 'not a container repository factory' + end + + context 'disallow anyone to delete images' do + let_it_be(:current_user) { create(:user, external: true) } + let(:current_params) do + { scopes: ["repository:#{project.full_path}:*"] } + end + + it_behaves_like 'an inaccessible' + it_behaves_like 'not a container repository factory' + end + + context 'disallow anyone to delete images since registry 2.7' do + let_it_be(:current_user) { create(:user, external: true) } + let(:current_params) do + { scopes: ["repository:#{project.full_path}:delete"] } + end + + it_behaves_like 'an inaccessible' + it_behaves_like 'not a container repository factory' + end + end + end + end + + context 'delete authorized as maintainer' do + let_it_be(:current_project) { create(:project) } + let_it_be(:current_user) { create(:user) } + + let(:authentication_abilities) do + [:admin_container_image] + end + + before_all do + current_project.add_maintainer(current_user) + end + + it_behaves_like 'a valid token' + + context 'allow to delete images' do + let(:current_params) do + { scopes: ["repository:#{current_project.full_path}:*"] } + end + + it_behaves_like 'a deletable' do + let(:project) { current_project } + end + end + + context 'allow to delete images since registry 2.7' do + let(:current_params) do + { scopes: ["repository:#{current_project.full_path}:delete"] } + end + + it_behaves_like 'a deletable since registry 2.7' do + let(:project) { current_project } + end + end + end + + context 'build authorized as user' do + let_it_be(:current_project) { create(:project) } + let_it_be(:current_user) { create(:user) } + + let(:authentication_abilities) do + [:build_read_container_image, :build_create_container_image, :build_destroy_container_image] + end + + before_all do + current_project.add_developer(current_user) + end + + context 'allow to use offline_token' do + let(:current_params) do + { offline_token: true } + end + + it_behaves_like 'an authenticated' + end + + it_behaves_like 'a valid token' + + context 'allow to pull and push images' do + let(:current_params) do + { scopes: ["repository:#{current_project.full_path}:pull,push"] } + end + + it_behaves_like 'a pullable and pushable' do + let(:project) { current_project } + end + + it_behaves_like 'container repository factory' do + let(:project) { current_project } + end + end + + context 'allow to delete images since registry 2.7' do + let(:current_params) do + { scopes: ["repository:#{current_project.full_path}:delete"] } + end + + it_behaves_like 'a deletable since registry 2.7' do + let(:project) { current_project } + end + end + + context 'disallow to delete images' do + let(:current_params) do + { scopes: ["repository:#{current_project.full_path}:*"] } + end + + it_behaves_like 'an inaccessible' do + let(:project) { current_project } + end + end + + context 'for other projects' do + context 'when pulling' do + let(:current_params) do + { scopes: ["repository:#{project.full_path}:pull"] } + end + + context 'allow for public' do + let_it_be(:project) { create(:project, :public) } + + it_behaves_like 'a pullable' + it_behaves_like 'not a container repository factory' + end + + shared_examples 'pullable for being team member' do + context 'when you are not member' do + it_behaves_like 'an inaccessible' + it_behaves_like 'not a container repository factory' + end + + context 'when you are member' do + before_all do + project.add_developer(current_user) + end + + it_behaves_like 'a pullable' + it_behaves_like 'not a container repository factory' + end + + context 'when you are owner' do + let_it_be(:project) { create(:project, namespace: current_user.namespace) } + + it_behaves_like 'a pullable' + it_behaves_like 'not a container repository factory' + end + end + + context 'for private' do + let_it_be(:project) { create(:project, :private) } + + it_behaves_like 'pullable for being team member' + + context 'when you are admin' do + let_it_be(:current_user) { create(:admin) } + + context 'when you are not member' do + it_behaves_like 'an inaccessible' + it_behaves_like 'not a container repository factory' + end + + context 'when you are member' do + before_all do + project.add_developer(current_user) + end + + it_behaves_like 'a pullable' + it_behaves_like 'not a container repository factory' + end + + context 'when you are owner' do + let_it_be(:project) { create(:project, namespace: current_user.namespace) } + + it_behaves_like 'a pullable' + it_behaves_like 'not a container repository factory' + end + end + end + end + + context 'when pushing' do + let(:current_params) do + { scopes: ["repository:#{project.full_path}:push"] } + end + + context 'disallow for all' do + context 'when you are member' do + let_it_be(:project) { create(:project, :public) } + + before_all do + project.add_developer(current_user) + end + + it_behaves_like 'an inaccessible' + it_behaves_like 'not a container repository factory' + end + + context 'when you are owner' do + let_it_be(:project) { create(:project, :public, namespace: current_user.namespace) } + + it_behaves_like 'an inaccessible' + it_behaves_like 'not a container repository factory' + end + end + end + end + + context 'for project without container registry' do + let_it_be(:project) { create(:project, :public, container_registry_enabled: false) } + + before do + project.update!(container_registry_enabled: false) + end + + context 'disallow when pulling' do + let(:current_params) do + { scopes: ["repository:#{project.full_path}:pull"] } + end + + it_behaves_like 'an inaccessible' + it_behaves_like 'not a container repository factory' + end + end + + context 'for project that disables repository' do + let_it_be(:project) { create(:project, :public, :repository_disabled) } + + context 'disallow when pulling' do + let(:current_params) do + { scopes: ["repository:#{project.full_path}:pull"] } + end + + it_behaves_like 'an inaccessible' + it_behaves_like 'not a container repository factory' + end + end + end + + context 'registry catalog browsing authorized as admin' do + let_it_be(:current_user) { create(:user, :admin) } + let_it_be(:project) { create(:project, :public) } + + let(:current_params) do + { scopes: ["registry:catalog:*"] } + end + + it_behaves_like 'a browsable' + end + + context 'support for multiple scopes' do + let_it_be(:internal_project) { create(:project, :internal) } + let_it_be(:private_project) { create(:project, :private) } + + let(:current_params) do + { + scopes: [ + "repository:#{internal_project.full_path}:pull", + "repository:#{private_project.full_path}:pull" + ] + } + end + + context 'user has access to all projects' do + let_it_be(:current_user) { create(:user, :admin) } + + before do + enable_admin_mode!(current_user) + end + + it_behaves_like 'a browsable' do + let(:access) do + [ + { 'type' => 'repository', + 'name' => internal_project.full_path, + 'actions' => ['pull'] }, + { 'type' => 'repository', + 'name' => private_project.full_path, + 'actions' => ['pull'] } + ] + end + end + end + + context 'user only has access to internal project' do + let_it_be(:current_user) { create(:user) } + + it_behaves_like 'a browsable' do + let(:access) do + [ + { 'type' => 'repository', + 'name' => internal_project.full_path, + 'actions' => ['pull'] } + ] + end + end + end + + context 'anonymous access is rejected' do + let(:current_user) { nil } + + it_behaves_like 'a forbidden' + end + end + + context 'unauthorized' do + context 'disallow to use scope-less authentication' do + it_behaves_like 'a forbidden' + it_behaves_like 'not a container repository factory' + end + + context 'for invalid scope' do + let(:current_params) do + { scopes: ['invalid:aa:bb'] } + end + + it_behaves_like 'a forbidden' + it_behaves_like 'not a container repository factory' + end + + context 'for private project' do + let_it_be(:project) { create(:project, :private) } + + let(:current_params) do + { scopes: ["repository:#{project.full_path}:pull"] } + end + + it_behaves_like 'a forbidden' + end + + context 'for public project' do + let_it_be(:project) { create(:project, :public) } + + context 'when pulling and pushing' do + let(:current_params) do + { scopes: ["repository:#{project.full_path}:pull,push"] } + end + + it_behaves_like 'a pullable' + it_behaves_like 'not a container repository factory' + end + + context 'when pushing' do + let(:current_params) do + { scopes: ["repository:#{project.full_path}:push"] } + end + + it_behaves_like 'a forbidden' + it_behaves_like 'not a container repository factory' + end + end + + context 'for registry catalog' do + let(:current_params) do + { scopes: ["registry:catalog:*"] } + end + + it_behaves_like 'a forbidden' + it_behaves_like 'not a container repository factory' + end + end + + context 'for deploy tokens' do + let(:current_params) do + { scopes: ["repository:#{project.full_path}:pull"] } + end + + context 'when deploy token has read and write registry as scopes' do + let(:current_user) { create(:deploy_token, write_registry: true, projects: [project]) } + + shared_examples 'able to login' do + context 'registry provides read_container_image authentication_abilities' do + let(:current_params) { {} } + let(:authentication_abilities) { [:read_container_image] } + + it_behaves_like 'an authenticated' + end + end + + context 'for public project' do + let_it_be(:project) { create(:project, :public) } + + context 'when pulling' do + it_behaves_like 'a pullable' + end + + context 'when pushing' do + let(:current_params) do + { scopes: ["repository:#{project.full_path}:push"] } + end + + it_behaves_like 'a pushable' + end + + it_behaves_like 'able to login' + end + + context 'for internal project' do + let_it_be(:project) { create(:project, :internal) } + + context 'when pulling' do + it_behaves_like 'a pullable' + end + + context 'when pushing' do + let(:current_params) do + { scopes: ["repository:#{project.full_path}:push"] } + end + + it_behaves_like 'a pushable' + end + + it_behaves_like 'able to login' + end + + context 'for private project' do + let_it_be(:project) { create(:project, :private) } + + context 'when pulling' do + it_behaves_like 'a pullable' + end + + context 'when pushing' do + let(:current_params) do + { scopes: ["repository:#{project.full_path}:push"] } + end + + it_behaves_like 'a pushable' + end + + it_behaves_like 'able to login' + end + end + + context 'when deploy token does not have read_registry scope' do + let(:current_user) { create(:deploy_token, projects: [project], read_registry: false) } + + shared_examples 'unable to login' do + context 'registry provides no container authentication_abilities' do + let(:current_params) { {} } + let(:authentication_abilities) { [] } + + it_behaves_like 'a forbidden' + end + + context 'registry provides inapplicable container authentication_abilities' do + let(:current_params) { {} } + let(:authentication_abilities) { [:download_code] } + + it_behaves_like 'a forbidden' + end + end + + context 'for public project' do + let_it_be(:project) { create(:project, :public) } + + context 'when pulling' do + it_behaves_like 'a pullable' + end + + it_behaves_like 'unable to login' + end + + context 'for internal project' do + let_it_be(:project) { create(:project, :internal) } + + context 'when pulling' do + it_behaves_like 'an inaccessible' + end + + it_behaves_like 'unable to login' + end + + context 'for private project' do + let_it_be(:project) { create(:project, :internal) } + + context 'when pulling' do + it_behaves_like 'an inaccessible' + end + + context 'when logging in' do + let(:current_params) { {} } + let(:authentication_abilities) { [] } + + it_behaves_like 'a forbidden' + end + + it_behaves_like 'unable to login' + end + end + + context 'when deploy token is not related to the project' do + let_it_be(:current_user) { create(:deploy_token, read_registry: false) } + + context 'for public project' do + let_it_be(:project) { create(:project, :public) } + + context 'when pulling' do + it_behaves_like 'a pullable' + end + end + + context 'for internal project' do + let_it_be(:project) { create(:project, :internal) } + + context 'when pulling' do + it_behaves_like 'an inaccessible' + end + end + + context 'for private project' do + let_it_be(:project) { create(:project, :internal) } + + context 'when pulling' do + it_behaves_like 'an inaccessible' + end + end + end + + context 'when deploy token has been revoked' do + let(:current_user) { create(:deploy_token, :revoked, projects: [project]) } + + context 'for public project' do + let_it_be(:project) { create(:project, :public) } + + it_behaves_like 'a pullable' + end + + context 'for internal project' do + let_it_be(:project) { create(:project, :internal) } + + it_behaves_like 'an inaccessible' + end + + context 'for private project' do + let_it_be(:project) { create(:project, :internal) } + + it_behaves_like 'an inaccessible' + end + end + end + + context 'user authorization' do + let_it_be(:current_user) { create(:user) } + + context 'with multiple scopes' do + let_it_be(:project) { create(:project) } + + context 'allow developer to push images' do + before_all do + project.add_developer(current_user) + end + + let(:current_params) do + { scopes: ["repository:#{project.full_path}:push"] } + end + + it_behaves_like 'a pushable' + it_behaves_like 'container repository factory' + end + end + end +end diff --git a/spec/support/shared_examples/services/incident_shared_examples.rb b/spec/support/shared_examples/services/incident_shared_examples.rb index 39c22ac8aa3..9fced12b543 100644 --- a/spec/support/shared_examples/services/incident_shared_examples.rb +++ b/spec/support/shared_examples/services/incident_shared_examples.rb @@ -11,11 +11,13 @@ # # include_examples 'incident issue' RSpec.shared_examples 'incident issue' do - let(:label_properties) { attributes_for(:label, :incident) } - it 'has incident as issue type' do expect(issue.issue_type).to eq('incident') end +end + +RSpec.shared_examples 'has incident label' do + let(:label_properties) { attributes_for(:label, :incident) } it 'has exactly one incident label' do expect(issue.labels).to be_one do |label| diff --git a/spec/support/shared_examples/services/metrics/dashboard_shared_examples.rb b/spec/support/shared_examples/services/metrics/dashboard_shared_examples.rb index 1501a2a0f52..b6c33eac7b4 100644 --- a/spec/support/shared_examples/services/metrics/dashboard_shared_examples.rb +++ b/spec/support/shared_examples/services/metrics/dashboard_shared_examples.rb @@ -46,7 +46,7 @@ RSpec.shared_examples 'refreshes cache when dashboard_version is changed' do allow(service).to receive(:dashboard_version).and_return('1', '2') end - expect(File).to receive(:read).twice.and_call_original + expect_file_read(Rails.root.join(described_class::DASHBOARD_PATH)).twice.and_call_original service = described_class.new(*service_params) diff --git a/spec/support/shared_examples/services/packages_shared_examples.rb b/spec/support/shared_examples/services/packages_shared_examples.rb index 7987f2c296b..70d29b4bc99 100644 --- a/spec/support/shared_examples/services/packages_shared_examples.rb +++ b/spec/support/shared_examples/services/packages_shared_examples.rb @@ -14,6 +14,24 @@ RSpec.shared_examples 'assigns build to package' do end end +RSpec.shared_examples 'assigns build to package file' do + context 'with build info' do + let(:job) { create(:ci_build, user: user) } + let(:params) { super().merge(build: job) } + + it 'assigns the pipeline to the package' do + package_file = subject + + expect(package_file.package_file_build_infos).to be_present + expect(package_file.pipelines.first).to eq job.pipeline + end + + it 'creates a new PackageFileBuildInfo record' do + expect { subject }.to change { Packages::PackageFileBuildInfo.count }.by(1) + end + end +end + RSpec.shared_examples 'assigns the package creator' do it 'assigns the package creator' do subject diff --git a/spec/support/shared_examples/workers/concerns/reenqueuer_shared_examples.rb b/spec/support/shared_examples/workers/concerns/reenqueuer_shared_examples.rb index 37f44f98cda..1b09b5fe613 100644 --- a/spec/support/shared_examples/workers/concerns/reenqueuer_shared_examples.rb +++ b/spec/support/shared_examples/workers/concerns/reenqueuer_shared_examples.rb @@ -10,6 +10,10 @@ RSpec.shared_examples 'reenqueuer' do expect(subject.lease_timeout).to be_a(ActiveSupport::Duration) end + it 'uses the :none deduplication strategy' do + expect(subject.class.get_deduplicate_strategy).to eq(:none) + end + describe '#perform' do it 'tries to obtain a lease' do expect_to_obtain_exclusive_lease(subject.lease_key) diff --git a/spec/support/shared_examples/workers/project_export_shared_examples.rb b/spec/support/shared_examples/workers/project_export_shared_examples.rb new file mode 100644 index 00000000000..a9bcc3f4f7c --- /dev/null +++ b/spec/support/shared_examples/workers/project_export_shared_examples.rb @@ -0,0 +1,82 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'export worker' do + describe '#perform' do + let!(:user) { create(:user) } + let!(:project) { create(:project) } + + before do + allow_next_instance_of(described_class) do |job| + allow(job).to receive(:jid).and_return(SecureRandom.hex(8)) + end + end + + context 'when it succeeds' do + it 'calls the ExportService' do + expect_next_instance_of(::Projects::ImportExport::ExportService) do |service| + expect(service).to receive(:execute) + end + + subject.perform(user.id, project.id, { 'klass' => 'Gitlab::ImportExport::AfterExportStrategies::DownloadNotificationStrategy' }) + end + + context 'export job' do + before do + allow_next_instance_of(::Projects::ImportExport::ExportService) do |service| + allow(service).to receive(:execute) + end + end + + it 'creates an export job record for the project' do + expect { subject.perform(user.id, project.id, {}) }.to change { project.export_jobs.count }.from(0).to(1) + end + + it 'sets the export job status to started' do + expect_next_instance_of(ProjectExportJob) do |job| + expect(job).to receive(:start) + end + + subject.perform(user.id, project.id, {}) + end + + it 'sets the export job status to finished' do + expect_next_instance_of(ProjectExportJob) do |job| + expect(job).to receive(:finish) + end + + subject.perform(user.id, project.id, {}) + end + end + end + + context 'when it fails' do + it 'does not raise an exception when strategy is invalid' do + expect(::Projects::ImportExport::ExportService).not_to receive(:new) + + expect { subject.perform(user.id, project.id, { 'klass' => 'Whatever' }) }.not_to raise_error + end + + it 'does not raise error when project cannot be found' do + expect { subject.perform(user.id, non_existing_record_id, {}) }.not_to raise_error + end + + it 'does not raise error when user cannot be found' do + expect { subject.perform(non_existing_record_id, project.id, {}) }.not_to raise_error + end + end + end + + describe 'sidekiq options' do + it 'disables retry' do + expect(described_class.sidekiq_options['retry']).to eq(false) + end + + it 'disables dead' do + expect(described_class.sidekiq_options['dead']).to eq(false) + end + + it 'sets default status expiration' do + expect(described_class.sidekiq_options['status_expiration']).to eq(StuckExportJobsWorker::EXPORT_JOBS_EXPIRATION) + end + end +end |