Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'spec/support/shared_examples')
-rw-r--r--spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb42
-rw-r--r--spec/support/shared_examples/controllers/repositories/git_http_controller_shared_examples.rb117
-rw-r--r--spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb68
-rw-r--r--spec/support/shared_examples/features/issuable_invite_members_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/features/master_manages_access_requests_shared_example.rb1
-rw-r--r--spec/support/shared_examples/features/reportable_note_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/features/wiki/user_git_access_wiki_page_shared_examples.rb27
-rw-r--r--spec/support/shared_examples/features/wiki/user_uses_wiki_shortcuts_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/graphql/connection_redaction_shared_examples.rb54
-rw-r--r--spec/support/shared_examples/graphql/connection_shared_examples.rb9
-rw-r--r--spec/support/shared_examples/graphql/design_fields_shared_examples.rb30
-rw-r--r--spec/support/shared_examples/graphql/jira_import/jira_import_resolver_shared_examples.rb15
-rw-r--r--spec/support/shared_examples/graphql/members_shared_examples.rb5
-rw-r--r--spec/support/shared_examples/graphql/mutation_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/graphql/mutations/boards_create_shared_examples.rb24
-rw-r--r--spec/support/shared_examples/graphql/mutations/spammable_mutation_fields_examples.rb7
-rw-r--r--spec/support/shared_examples/graphql/projects/services_resolver_shared_examples.rb6
-rw-r--r--spec/support/shared_examples/graphql/sorted_paginated_query_shared_examples.rb93
-rw-r--r--spec/support/shared_examples/graphql/types/gitlab_style_deprecations_shared_examples.rb6
-rw-r--r--spec/support/shared_examples/lib/gitlab/diff_file_collections_shared_examples.rb52
-rw-r--r--spec/support/shared_examples/lib/gitlab/import_export/import_failure_service_shared_examples.rb8
-rw-r--r--spec/support/shared_examples/lib/gitlab/middleware/read_only_gitlab_instance_shared_examples.rb3
-rw-r--r--spec/support/shared_examples/lib/gitlab/sidekiq_middleware/strategy_shared_examples.rb10
-rw-r--r--spec/support/shared_examples/models/concerns/can_move_repository_storage_shared_examples.rb55
-rw-r--r--spec/support/shared_examples/models/concerns/repository_storage_movable_shared_examples.rb115
-rw-r--r--spec/support/shared_examples/models/concerns/shardable_shared_examples.rb6
-rw-r--r--spec/support/shared_examples/models/mentionable_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/models/resource_timebox_event_shared_examples.rb10
-rw-r--r--spec/support/shared_examples/quick_actions/issue/clone_quick_action_shared_examples.rb187
-rw-r--r--spec/support/shared_examples/requests/api/conan_packages_shared_examples.rb16
-rw-r--r--spec/support/shared_examples/requests/api/graphql/group_and_project_boards_query_shared_examples.rb16
-rw-r--r--spec/support/shared_examples/requests/api/nuget_endpoints_shared_examples.rb265
-rw-r--r--spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb10
-rw-r--r--spec/support/shared_examples/requests/graphql_shared_examples.rb5
-rw-r--r--spec/support/shared_examples/requests/lfs_http_shared_examples.rb13
-rw-r--r--spec/support/shared_examples/requests/rack_attack_shared_examples.rb114
-rw-r--r--spec/support/shared_examples/requests/self_monitoring_shared_examples.rb12
-rw-r--r--spec/support/shared_examples/requests/uploads_auhorize_shared_examples.rb61
-rw-r--r--spec/support/shared_examples/routing/git_http_routing_shared_examples.rb43
-rw-r--r--spec/support/shared_examples/services/alert_management_shared_examples.rb32
-rw-r--r--spec/support/shared_examples/services/container_registry_auth_service_shared_examples.rb1006
-rw-r--r--spec/support/shared_examples/services/incident_shared_examples.rb6
-rw-r--r--spec/support/shared_examples/services/metrics/dashboard_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/services/packages_shared_examples.rb18
-rw-r--r--spec/support/shared_examples/workers/concerns/reenqueuer_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/workers/project_export_shared_examples.rb82
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