summaryrefslogtreecommitdiff
path: root/spec/requests/api
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-05-19 15:44:42 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2021-05-19 15:44:42 +0000
commit4555e1b21c365ed8303ffb7a3325d773c9b8bf31 (patch)
tree5423a1c7516cffe36384133ade12572cf709398d /spec/requests/api
parente570267f2f6b326480d284e0164a6464ba4081bc (diff)
downloadgitlab-ce-4555e1b21c365ed8303ffb7a3325d773c9b8bf31.tar.gz
Add latest changes from gitlab-org/gitlab@13-12-stable-eev13.12.0-rc42
Diffstat (limited to 'spec/requests/api')
-rw-r--r--spec/requests/api/api_guard/admin_mode_middleware_spec.rb2
-rw-r--r--spec/requests/api/api_spec.rb14
-rw-r--r--spec/requests/api/branches_spec.rb6
-rw-r--r--spec/requests/api/ci/pipelines_spec.rb19
-rw-r--r--spec/requests/api/ci/runner/jobs_request_post_spec.rb6
-rw-r--r--spec/requests/api/ci/runner/runners_post_spec.rb30
-rw-r--r--spec/requests/api/ci/runners_spec.rb13
-rw-r--r--spec/requests/api/debian_group_packages_spec.rb32
-rw-r--r--spec/requests/api/debian_project_packages_spec.rb30
-rw-r--r--spec/requests/api/deploy_tokens_spec.rb57
-rw-r--r--spec/requests/api/deployments_spec.rb25
-rw-r--r--spec/requests/api/environments_spec.rb109
-rw-r--r--spec/requests/api/graphql/ci/job_spec.rb13
-rw-r--r--spec/requests/api/graphql/ci/pipelines_spec.rb124
-rw-r--r--spec/requests/api/graphql/ci/runner_spec.rb144
-rw-r--r--spec/requests/api/graphql/ci/runners_spec.rb114
-rw-r--r--spec/requests/api/graphql/ci/template_spec.rb48
-rw-r--r--spec/requests/api/graphql/group/milestones_spec.rb12
-rw-r--r--spec/requests/api/graphql/group/packages_spec.rb48
-rw-r--r--spec/requests/api/graphql/issue/issue_spec.rb6
-rw-r--r--spec/requests/api/graphql/merge_request/merge_request_spec.rb111
-rw-r--r--spec/requests/api/graphql/metadata_query_spec.rb46
-rw-r--r--spec/requests/api/graphql/mutations/boards/destroy_spec.rb3
-rw-r--r--spec/requests/api/graphql/mutations/boards/lists/destroy_spec.rb73
-rw-r--r--spec/requests/api/graphql/mutations/boards/lists/update_spec.rb41
-rw-r--r--spec/requests/api/graphql/mutations/ci/job_play_spec.rb46
-rw-r--r--spec/requests/api/graphql/mutations/ci/job_retry_spec.rb46
-rw-r--r--spec/requests/api/graphql/mutations/issues/create_spec.rb5
-rw-r--r--spec/requests/api/graphql/mutations/issues/set_due_date_spec.rb31
-rw-r--r--spec/requests/api/graphql/mutations/issues/update_spec.rb3
-rw-r--r--spec/requests/api/graphql/mutations/labels/create_spec.rb3
-rw-r--r--spec/requests/api/graphql/mutations/namespace/package_settings/update_spec.rb12
-rw-r--r--spec/requests/api/graphql/mutations/security/ci_configuration/configure_secret_detection_spec.rb26
-rw-r--r--spec/requests/api/graphql/packages/composer_spec.rb64
-rw-r--r--spec/requests/api/graphql/packages/conan_spec.rb90
-rw-r--r--spec/requests/api/graphql/packages/maven_spec.rb94
-rw-r--r--spec/requests/api/graphql/packages/nuget_spec.rb74
-rw-r--r--spec/requests/api/graphql/packages/package_spec.rb78
-rw-r--r--spec/requests/api/graphql/project/issue/design_collection/versions_spec.rb17
-rw-r--r--spec/requests/api/graphql/project/merge_request_spec.rb46
-rw-r--r--spec/requests/api/graphql/project/packages_spec.rb33
-rw-r--r--spec/requests/api/graphql/project/project_members_spec.rb16
-rw-r--r--spec/requests/api/graphql/project/release_spec.rb17
-rw-r--r--spec/requests/api/graphql/project/releases_spec.rb17
-rw-r--r--spec/requests/api/graphql/project/repository_spec.rb24
-rw-r--r--spec/requests/api/graphql/project_query_spec.rb16
-rw-r--r--spec/requests/api/graphql_spec.rb57
-rw-r--r--spec/requests/api/group_export_spec.rb70
-rw-r--r--spec/requests/api/group_labels_spec.rb4
-rw-r--r--spec/requests/api/helpers_spec.rb2
-rw-r--r--spec/requests/api/internal/kubernetes_spec.rb12
-rw-r--r--spec/requests/api/issues/get_project_issues_spec.rb5
-rw-r--r--spec/requests/api/issues/issues_spec.rb29
-rw-r--r--spec/requests/api/issues/put_projects_issues_spec.rb11
-rw-r--r--spec/requests/api/labels_spec.rb14
-rw-r--r--spec/requests/api/maven_packages_spec.rb101
-rw-r--r--spec/requests/api/merge_requests_spec.rb78
-rw-r--r--spec/requests/api/package_files_spec.rb81
-rw-r--r--spec/requests/api/project_attributes.yml2
-rw-r--r--spec/requests/api/project_container_repositories_spec.rb505
-rw-r--r--spec/requests/api/project_import_spec.rb76
-rw-r--r--spec/requests/api/project_packages_spec.rb10
-rw-r--r--spec/requests/api/project_templates_spec.rb17
-rw-r--r--spec/requests/api/projects_spec.rb71
-rw-r--r--spec/requests/api/releases_spec.rb92
-rw-r--r--spec/requests/api/services_spec.rb52
-rw-r--r--spec/requests/api/settings_spec.rb56
-rw-r--r--spec/requests/api/terraform/modules/v1/packages_spec.rb360
-rw-r--r--spec/requests/api/users_spec.rb42
69 files changed, 2840 insertions, 791 deletions
diff --git a/spec/requests/api/api_guard/admin_mode_middleware_spec.rb b/spec/requests/api/api_guard/admin_mode_middleware_spec.rb
index 63bcec4b52a..ba7a01a2cd9 100644
--- a/spec/requests/api/api_guard/admin_mode_middleware_spec.rb
+++ b/spec/requests/api/api_guard/admin_mode_middleware_spec.rb
@@ -13,7 +13,7 @@ RSpec.describe API::APIGuard::AdminModeMiddleware, :request_store do
let(:app) do
Class.new(API::API) do
get 'willfail' do
- raise StandardError.new('oh noes!')
+ raise StandardError, 'oh noes!'
end
end
end
diff --git a/spec/requests/api/api_spec.rb b/spec/requests/api/api_spec.rb
index b3e425630e5..46430e55ff2 100644
--- a/spec/requests/api/api_spec.rb
+++ b/spec/requests/api/api_spec.rb
@@ -170,20 +170,6 @@ RSpec.describe API::API do
expect(response.media_type).to eq('application/json')
expect(response.body).to include('{"id":')
end
-
- context 'when api_always_use_application_json is disabled' do
- before do
- stub_feature_flags(api_always_use_application_json: false)
- end
-
- it 'returns text/plain' do
- subject
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response.media_type).to eq('text/plain')
- expect(response.body).to include('#<API::Entities::User:')
- end
- end
end
end
end
diff --git a/spec/requests/api/branches_spec.rb b/spec/requests/api/branches_spec.rb
index 5298f93886d..a38ba782c44 100644
--- a/spec/requests/api/branches_spec.rb
+++ b/spec/requests/api/branches_spec.rb
@@ -20,7 +20,7 @@ RSpec.describe API::Branches do
stub_feature_flags(branch_list_keyset_pagination: false)
end
- describe "GET /projects/:id/repository/branches" do
+ describe "GET /projects/:id/repository/branches", :use_clean_rails_redis_caching do
let(:route) { "/projects/#{project_id}/repository/branches" }
shared_examples_for 'repository branches' do
@@ -53,7 +53,7 @@ RSpec.describe API::Branches do
end
it 'determines only a limited number of merged branch names' do
- expect(API::Entities::Branch).to receive(:represent).with(anything, has_up_to_merged_branch_names_count(2)).and_call_original
+ expect(API::Entities::Branch).to receive(:represent).with(anything, has_up_to_merged_branch_names_count(2)).at_least(:once).and_call_original
get api(route, current_user), params: base_params.merge(per_page: 2)
@@ -111,7 +111,7 @@ RSpec.describe API::Branches do
end
it 'determines only a limited number of merged branch names' do
- expect(API::Entities::Branch).to receive(:represent).with(anything, has_up_to_merged_branch_names_count(2)).and_call_original
+ expect(API::Entities::Branch).to receive(:represent).with(anything, has_up_to_merged_branch_names_count(2)).at_least(:once).and_call_original
get api(route, current_user), params: base_params.merge(per_page: 2)
diff --git a/spec/requests/api/ci/pipelines_spec.rb b/spec/requests/api/ci/pipelines_spec.rb
index d0c2b383013..092cd00630e 100644
--- a/spec/requests/api/ci/pipelines_spec.rb
+++ b/spec/requests/api/ci/pipelines_spec.rb
@@ -362,6 +362,25 @@ RSpec.describe API::Ci::Pipelines do
it do
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to be_an Array
+
+ expect(json_response).to all match a_hash_including(
+ 'duration' => be_nil,
+ 'queued_duration' => (be >= 0.0)
+ )
+ end
+ end
+
+ context 'when filtering to only running jobs' do
+ let(:query) { { 'scope' => 'running' } }
+
+ it do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to be_an Array
+
+ expect(json_response).to all match a_hash_including(
+ 'duration' => (be >= 0.0),
+ 'queued_duration' => (be >= 0.0)
+ )
end
end
diff --git a/spec/requests/api/ci/runner/jobs_request_post_spec.rb b/spec/requests/api/ci/runner/jobs_request_post_spec.rb
index cf0d8a632f1..63da3340a45 100644
--- a/spec/requests/api/ci/runner/jobs_request_post_spec.rb
+++ b/spec/requests/api/ci/runner/jobs_request_post_spec.rb
@@ -378,7 +378,7 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
{
"name" => "release",
"script" =>
- ["release-cli create --name \"Release $CI_COMMIT_SHA\" --description \"Created using the release-cli $EXTRA_DESCRIPTION\" --tag-name \"release-$CI_COMMIT_SHA\" --ref \"$CI_COMMIT_SHA\""],
+ ["release-cli create --name \"Release $CI_COMMIT_SHA\" --description \"Created using the release-cli $EXTRA_DESCRIPTION\" --tag-name \"release-$CI_COMMIT_SHA\" --ref \"$CI_COMMIT_SHA\" --assets-link \"{\\\"url\\\":\\\"https://example.com/assets/1\\\",\\\"name\\\":\\\"asset1\\\"}\""],
"timeout" => 3600,
"when" => "on_success",
"allow_failure" => false
@@ -502,8 +502,8 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
expect { request_job }.to exceed_all_query_limit(1).for_model(::Ci::JobArtifact)
end
- it 'queries the ci_builds table more than five times' do
- expect { request_job }.to exceed_all_query_limit(5).for_model(::Ci::Build)
+ it 'queries the ci_builds table more than three times' do
+ expect { request_job }.to exceed_all_query_limit(3).for_model(::Ci::Build)
end
end
diff --git a/spec/requests/api/ci/runner/runners_post_spec.rb b/spec/requests/api/ci/runner/runners_post_spec.rb
index 7984b1d4ca8..b38630183f4 100644
--- a/spec/requests/api/ci/runner/runners_post_spec.rb
+++ b/spec/requests/api/ci/runner/runners_post_spec.rb
@@ -91,6 +91,21 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
it_behaves_like 'not executing any extra queries for the application context' do
let(:subject_proc) { proc { request } }
end
+
+ context 'when it exceeds the application limits' do
+ before do
+ create(:ci_runner, runner_type: :project_type, projects: [project])
+ create(:plan_limits, :default_plan, ci_registered_project_runners: 1)
+ end
+
+ it 'does not create runner' do
+ request
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['message']).to include('runner_projects.base' => ['Maximum number of ci registered project runners (1) exceeded'])
+ expect(project.runners.reload.size).to eq(1)
+ end
+ end
end
context 'when group token is used' do
@@ -117,6 +132,21 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
it_behaves_like 'not executing any extra queries for the application context' do
let(:subject_proc) { proc { request } }
end
+
+ context 'when it exceeds the application limits' do
+ before do
+ create(:ci_runner, runner_type: :group_type, groups: [group])
+ create(:plan_limits, :default_plan, ci_registered_group_runners: 1)
+ end
+
+ it 'does not create runner' do
+ request
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['message']).to include('runner_namespaces.base' => ['Maximum number of ci registered group runners (1) exceeded'])
+ expect(group.runners.reload.size).to eq(1)
+ end
+ end
end
end
diff --git a/spec/requests/api/ci/runners_spec.rb b/spec/requests/api/ci/runners_spec.rb
index 670456e5dba..1727bc830fc 100644
--- a/spec/requests/api/ci/runners_spec.rb
+++ b/spec/requests/api/ci/runners_spec.rb
@@ -999,6 +999,19 @@ RSpec.describe API::Ci::Runners do
end.to change { project.runners.count }.by(+1)
expect(response).to have_gitlab_http_status(:created)
end
+
+ context 'when it exceeds the application limits' do
+ before do
+ create(:plan_limits, :default_plan, ci_registered_project_runners: 1)
+ end
+
+ it 'does not enable specific runner' do
+ expect do
+ post api("/projects/#{project.id}/runners", admin), params: { runner_id: new_project_runner.id }
+ end.not_to change { project.runners.count }
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+ end
end
it 'enables a instance type runner' do
diff --git a/spec/requests/api/debian_group_packages_spec.rb b/spec/requests/api/debian_group_packages_spec.rb
index 9d63d675a02..42c6c987872 100644
--- a/spec/requests/api/debian_group_packages_spec.rb
+++ b/spec/requests/api/debian_group_packages_spec.rb
@@ -5,35 +5,35 @@ RSpec.describe API::DebianGroupPackages do
include HttpBasicAuthHelpers
include WorkhorseHelpers
- include_context 'Debian repository shared context', :group do
- describe 'GET groups/:id/packages/debian/dists/*distribution/Release.gpg' do
- let(:url) { "/groups/#{group.id}/packages/debian/dists/#{distribution}/Release.gpg" }
+ include_context 'Debian repository shared context', :group, false do
+ describe 'GET groups/:id/-/packages/debian/dists/*distribution/Release.gpg' do
+ let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution}/Release.gpg" }
- it_behaves_like 'Debian group repository GET endpoint', :not_found, nil
+ it_behaves_like 'Debian repository read endpoint', 'GET request', :not_found
end
- describe 'GET groups/:id/packages/debian/dists/*distribution/Release' do
- let(:url) { "/groups/#{group.id}/packages/debian/dists/#{distribution}/Release" }
+ describe 'GET groups/:id/-/packages/debian/dists/*distribution/Release' do
+ let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution}/Release" }
- it_behaves_like 'Debian group repository GET endpoint', :success, 'TODO Release'
+ it_behaves_like 'Debian repository read endpoint', 'GET request', :success, 'TODO Release'
end
- describe 'GET groups/:id/packages/debian/dists/*distribution/InRelease' do
- let(:url) { "/groups/#{group.id}/packages/debian/dists/#{distribution}/InRelease" }
+ describe 'GET groups/:id/-/packages/debian/dists/*distribution/InRelease' do
+ let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution}/InRelease" }
- it_behaves_like 'Debian group repository GET endpoint', :not_found, nil
+ it_behaves_like 'Debian repository read endpoint', 'GET request', :not_found
end
- describe 'GET groups/:id/packages/debian/dists/*distribution/:component/binary-:architecture/Packages' do
- let(:url) { "/groups/#{group.id}/packages/debian/dists/#{distribution}/#{component}/binary-#{architecture}/Packages" }
+ describe 'GET groups/:id/-/packages/debian/dists/*distribution/:component/binary-:architecture/Packages' do
+ let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution}/#{component}/binary-#{architecture}/Packages" }
- it_behaves_like 'Debian group repository GET endpoint', :success, 'TODO Packages'
+ it_behaves_like 'Debian repository read endpoint', 'GET request', :success, 'TODO Packages'
end
- describe 'GET groups/:id/packages/debian/pool/:component/:letter/:source_package/:file_name' do
- let(:url) { "/groups/#{group.id}/packages/debian/pool/#{component}/#{letter}/#{source_package}/#{package_name}_#{package_version}_#{architecture}.deb" }
+ describe 'GET groups/:id/-/packages/debian/pool/:component/:letter/:source_package/:file_name' do
+ let(:url) { "/groups/#{container.id}/-/packages/debian/pool/#{component}/#{letter}/#{source_package}/#{package_name}_#{package_version}_#{architecture}.deb" }
- it_behaves_like 'Debian group repository GET endpoint', :success, 'TODO File'
+ it_behaves_like 'Debian repository read endpoint', 'GET request', :success, 'TODO File'
end
end
end
diff --git a/spec/requests/api/debian_project_packages_spec.rb b/spec/requests/api/debian_project_packages_spec.rb
index 4941f2a77f4..f400b6e928c 100644
--- a/spec/requests/api/debian_project_packages_spec.rb
+++ b/spec/requests/api/debian_project_packages_spec.rb
@@ -5,49 +5,49 @@ RSpec.describe API::DebianProjectPackages do
include HttpBasicAuthHelpers
include WorkhorseHelpers
- include_context 'Debian repository shared context', :project do
+ include_context 'Debian repository shared context', :project, true do
describe 'GET projects/:id/packages/debian/dists/*distribution/Release.gpg' do
- let(:url) { "/projects/#{project.id}/packages/debian/dists/#{distribution}/Release.gpg" }
+ let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution}/Release.gpg" }
- it_behaves_like 'Debian project repository GET endpoint', :not_found, nil
+ it_behaves_like 'Debian repository read endpoint', 'GET request', :not_found
end
describe 'GET projects/:id/packages/debian/dists/*distribution/Release' do
- let(:url) { "/projects/#{project.id}/packages/debian/dists/#{distribution}/Release" }
+ let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution}/Release" }
- it_behaves_like 'Debian project repository GET endpoint', :success, 'TODO Release'
+ it_behaves_like 'Debian repository read endpoint', 'GET request', :success, 'TODO Release'
end
describe 'GET projects/:id/packages/debian/dists/*distribution/InRelease' do
- let(:url) { "/projects/#{project.id}/packages/debian/dists/#{distribution}/InRelease" }
+ let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution}/InRelease" }
- it_behaves_like 'Debian project repository GET endpoint', :not_found, nil
+ it_behaves_like 'Debian repository read endpoint', 'GET request', :not_found
end
describe 'GET projects/:id/packages/debian/dists/*distribution/:component/binary-:architecture/Packages' do
- let(:url) { "/projects/#{project.id}/packages/debian/dists/#{distribution}/#{component}/binary-#{architecture}/Packages" }
+ let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution}/#{component}/binary-#{architecture}/Packages" }
- it_behaves_like 'Debian project repository GET endpoint', :success, 'TODO Packages'
+ it_behaves_like 'Debian repository read endpoint', 'GET request', :success, 'TODO Packages'
end
describe 'GET projects/:id/packages/debian/pool/:component/:letter/:source_package/:file_name' do
- let(:url) { "/projects/#{project.id}/packages/debian/pool/#{component}/#{letter}/#{source_package}/#{package_name}_#{package_version}_#{architecture}.deb" }
+ let(:url) { "/projects/#{container.id}/packages/debian/pool/#{component}/#{letter}/#{source_package}/#{package_name}_#{package_version}_#{architecture}.deb" }
- it_behaves_like 'Debian project repository GET endpoint', :success, 'TODO File'
+ it_behaves_like 'Debian repository read endpoint', 'GET request', :success, 'TODO File'
end
describe 'PUT projects/:id/packages/debian/:file_name' do
let(:method) { :put }
- let(:url) { "/projects/#{project.id}/packages/debian/#{file_name}" }
+ let(:url) { "/projects/#{container.id}/packages/debian/#{file_name}" }
- it_behaves_like 'Debian project repository PUT endpoint', :created, nil
+ it_behaves_like 'Debian repository write endpoint', 'upload request', :created
end
describe 'PUT projects/:id/packages/debian/:file_name/authorize' do
let(:method) { :put }
- let(:url) { "/projects/#{project.id}/packages/debian/#{file_name}/authorize" }
+ let(:url) { "/projects/#{container.id}/packages/debian/#{file_name}/authorize" }
- it_behaves_like 'Debian project repository PUT endpoint', :created, nil, is_authorize: true
+ it_behaves_like 'Debian repository write endpoint', 'upload authorize request', :created
end
end
end
diff --git a/spec/requests/api/deploy_tokens_spec.rb b/spec/requests/api/deploy_tokens_spec.rb
index 7a31ff725c8..e8426270622 100644
--- a/spec/requests/api/deploy_tokens_spec.rb
+++ b/spec/requests/api/deploy_tokens_spec.rb
@@ -8,7 +8,11 @@ RSpec.describe API::DeployTokens do
let_it_be(:project) { create(:project, creator_id: creator.id) }
let_it_be(:group) { create(:group) }
let!(:deploy_token) { create(:deploy_token, projects: [project]) }
+ let!(:revoked_deploy_token) { create(:deploy_token, projects: [project], revoked: true) }
+ let!(:expired_deploy_token) { create(:deploy_token, projects: [project], expires_at: '1988-01-11T04:33:04-0600') }
let!(:group_deploy_token) { create(:deploy_token, :group, groups: [group]) }
+ let!(:revoked_group_deploy_token) { create(:deploy_token, :group, groups: [group], revoked: true) }
+ let!(:expired_group_deploy_token) { create(:deploy_token, :group, groups: [group], expires_at: '1988-01-11T04:33:04-0600') }
describe 'GET /deploy_tokens' do
subject do
@@ -36,8 +40,31 @@ RSpec.describe API::DeployTokens do
it 'returns all deploy tokens' do
subject
+ token_ids = json_response.map { |token| token['id'] }
expect(response).to include_pagination_headers
expect(response).to match_response_schema('public_api/v4/deploy_tokens')
+ expect(token_ids).to match_array([
+ deploy_token.id,
+ revoked_deploy_token.id,
+ expired_deploy_token.id,
+ group_deploy_token.id,
+ revoked_group_deploy_token.id,
+ expired_group_deploy_token.id
+ ])
+ end
+
+ context 'and active=true' do
+ it 'only returns active deploy tokens' do
+ get api('/deploy_tokens?active=true', user)
+
+ token_ids = json_response.map { |token| token['id'] }
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to include_pagination_headers
+ expect(token_ids).to match_array([
+ deploy_token.id,
+ group_deploy_token.id
+ ])
+ end
end
end
end
@@ -82,7 +109,22 @@ RSpec.describe API::DeployTokens do
subject
token_ids = json_response.map { |token| token['id'] }
- expect(token_ids).not_to include(other_deploy_token.id)
+ expect(token_ids).to match_array([
+ deploy_token.id,
+ expired_deploy_token.id,
+ revoked_deploy_token.id
+ ])
+ end
+
+ context 'and active=true' do
+ it 'only returns active deploy tokens for the project' do
+ get api("/projects/#{project.id}/deploy_tokens?active=true", user)
+
+ token_ids = json_response.map { |token| token['id'] }
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to include_pagination_headers
+ expect(token_ids).to match_array([deploy_token.id])
+ end
end
end
end
@@ -119,8 +161,10 @@ RSpec.describe API::DeployTokens do
it 'returns all deploy tokens for the group' do
subject
+ token_ids = json_response.map { |token| token['id'] }
expect(response).to include_pagination_headers
expect(response).to match_response_schema('public_api/v4/deploy_tokens')
+ expect(token_ids.length).to be(3)
end
it 'does not return deploy tokens for other groups' do
@@ -129,6 +173,17 @@ RSpec.describe API::DeployTokens do
token_ids = json_response.map { |token| token['id'] }
expect(token_ids).not_to include(other_deploy_token.id)
end
+
+ context 'and active=true' do
+ it 'only returns active deploy tokens for the group' do
+ get api("/groups/#{group.id}/deploy_tokens?active=true", user)
+
+ token_ids = json_response.map { |token| token['id'] }
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to include_pagination_headers
+ expect(token_ids).to eql([group_deploy_token.id])
+ end
+ end
end
end
diff --git a/spec/requests/api/deployments_spec.rb b/spec/requests/api/deployments_spec.rb
index c89c59a2151..bbfe37cb70b 100644
--- a/spec/requests/api/deployments_spec.rb
+++ b/spec/requests/api/deployments_spec.rb
@@ -12,9 +12,11 @@ RSpec.describe API::Deployments do
describe 'GET /projects/:id/deployments' do
let_it_be(:project) { create(:project, :repository) }
- let_it_be(:deployment_1) { create(:deployment, :success, project: project, iid: 11, ref: 'master', created_at: Time.now, updated_at: Time.now) }
- let_it_be(:deployment_2) { create(:deployment, :success, project: project, iid: 12, ref: 'master', created_at: 1.day.ago, updated_at: 2.hours.ago) }
- let_it_be(:deployment_3) { create(:deployment, :success, project: project, iid: 8, ref: 'master', created_at: 2.days.ago, updated_at: 1.hour.ago) }
+ let_it_be(:production) { create(:environment, :production, project: project) }
+ let_it_be(:staging) { create(:environment, :staging, project: project) }
+ let_it_be(:deployment_1) { create(:deployment, :success, project: project, environment: production, ref: 'master', created_at: Time.now, updated_at: Time.now) }
+ let_it_be(:deployment_2) { create(:deployment, :success, project: project, environment: staging, ref: 'master', created_at: 1.day.ago, updated_at: 2.hours.ago) }
+ let_it_be(:deployment_3) { create(:deployment, :success, project: project, environment: staging, ref: 'master', created_at: 2.days.ago, updated_at: 1.hour.ago) }
def perform_request(params = {})
get api("/projects/#{project.id}/deployments", user), params: params
@@ -36,17 +38,26 @@ RSpec.describe API::Deployments do
context 'with updated_at filters specified' do
it 'returns projects deployments with last update in specified datetime range' do
- perform_request({ updated_before: 30.minutes.ago, updated_after: 90.minutes.ago })
+ perform_request({ updated_before: 30.minutes.ago, updated_after: 90.minutes.ago, order_by: :updated_at })
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response.first['id']).to eq(deployment_3.id)
end
+
+ context 'when forbidden order_by is specified' do
+ it 'returns projects deployments with last update in specified datetime range' do
+ perform_request({ updated_before: 30.minutes.ago, updated_after: 90.minutes.ago, order_by: :id })
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['message']).to include('`updated_at` filter and `updated_at` sorting must be paired')
+ end
+ end
end
context 'with the environment filter specifed' do
it 'returns deployments for the environment' do
- perform_request({ environment: deployment_1.environment.name })
+ perform_request({ environment: production.name })
expect(json_response.size).to eq(1)
expect(json_response.first['iid']).to eq(deployment_1.iid)
@@ -68,7 +79,7 @@ RSpec.describe API::Deployments do
end
it 'returns ordered deployments' do
- expect(json_response.map { |i| i['id'] }).to eq([deployment_2.id, deployment_1.id, deployment_3.id])
+ expect(json_response.map { |i| i['id'] }).to eq([deployment_3.id, deployment_2.id, deployment_1.id])
end
context 'with invalid order_by' do
@@ -475,7 +486,7 @@ RSpec.describe API::Deployments do
let(:project) { create(:project, :repository) }
let!(:deployment) { create(:deployment, :success, project: project) }
- subject { get api("/projects/#{project.id}/deployments?order_by=updated_at&sort=asc", user) }
+ subject { get api("/projects/#{project.id}/deployments?order_by=id&sort=asc", user) }
it 'succeeds', :aggregate_failures do
subject
diff --git a/spec/requests/api/environments_spec.rb b/spec/requests/api/environments_spec.rb
index aa1a4643593..5d40e8c529a 100644
--- a/spec/requests/api/environments_spec.rb
+++ b/spec/requests/api/environments_spec.rb
@@ -3,28 +3,18 @@
require 'spec_helper'
RSpec.describe API::Environments do
- let(:user) { create(:user) }
- let(:non_member) { create(:user) }
- let(:project) { create(:project, :private, :repository, namespace: user.namespace) }
- let!(:environment) { create(:environment, project: project) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:non_member) { create(:user) }
+ let_it_be(:project) { create(:project, :private, :repository, namespace: user.namespace) }
+ let_it_be_with_reload(:environment) { create(:environment, project: project) }
before do
project.add_maintainer(user)
end
- describe 'GET /projects/:id/environments' do
+ describe 'GET /projects/:id/environments', :aggregate_failures do
context 'as member of the project' do
it 'returns project environments' do
- project_data_keys = %w(
- id description default_branch tag_list
- ssh_url_to_repo http_url_to_repo web_url readme_url
- name name_with_namespace
- path path_with_namespace
- star_count forks_count
- created_at last_activity_at
- avatar_url namespace
- )
-
get api("/projects/#{project.id}/environments", user)
expect(response).to have_gitlab_http_status(:ok)
@@ -33,12 +23,95 @@ RSpec.describe API::Environments do
expect(json_response.size).to eq(1)
expect(json_response.first['name']).to eq(environment.name)
expect(json_response.first['external_url']).to eq(environment.external_url)
- expect(json_response.first['project'].keys).to contain_exactly(*project_data_keys)
- expect(json_response.first).not_to have_key("last_deployment")
+ expect(json_response.first['project']).to match_schema('public_api/v4/project')
+ expect(json_response.first['enable_advanced_logs_querying']).to eq(false)
+ expect(json_response.first).not_to have_key('last_deployment')
+ expect(json_response.first).not_to have_key('gitlab_managed_apps_logs_path')
+ end
+
+ context 'when the user can read pod logs' do
+ context 'with successful deployment on cluster' do
+ let_it_be(:deployment) { create(:deployment, :on_cluster, :success, environment: environment, project: project) }
+
+ it 'returns environment with enable_advanced_logs_querying and logs_api_path' do
+ get api("/projects/#{project.id}/environments", user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.size).to eq(1)
+ expect(json_response.first['gitlab_managed_apps_logs_path']).to eq(
+ "/#{project.full_path}/-/logs/k8s.json?cluster_id=#{deployment.cluster_id}"
+ )
+ end
+ end
+
+ context 'when elastic stack is available' do
+ before do
+ allow_next_found_instance_of(Environment) do |env|
+ allow(env).to receive(:elastic_stack_available?).and_return(true)
+ end
+ end
+
+ it 'returns environment with enable_advanced_logs_querying and logs_api_path' do
+ get api("/projects/#{project.id}/environments", user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.size).to eq(1)
+ expect(json_response.first['enable_advanced_logs_querying']).to eq(true)
+ expect(json_response.first['logs_api_path']).to eq(
+ "/#{project.full_path}/-/logs/elasticsearch.json?environment_name=#{environment.name}"
+ )
+ end
+ end
+
+ context 'when elastic stack is not available' do
+ before do
+ allow_next_found_instance_of(Environment) do |env|
+ allow(env).to receive(:elastic_stack_available?).and_return(false)
+ end
+ end
+
+ it 'returns environment with enable_advanced_logs_querying logs_api_path' do
+ get api("/projects/#{project.id}/environments", user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.size).to eq(1)
+ expect(json_response.first['enable_advanced_logs_querying']).to eq(false)
+ expect(json_response.first['logs_api_path']).to eq(
+ "/#{project.full_path}/-/logs/k8s.json?environment_name=#{environment.name}"
+ )
+ end
+ end
+ end
+
+ context 'when the user cannot read pod logs' do
+ before do
+ allow_next_found_instance_of(User) do |user|
+ allow(user).to receive(:can?).and_call_original
+ allow(user).to receive(:can?).with(:read_pod_logs, project).and_return(false)
+ end
+ end
+
+ it 'does not contain enable_advanced_logs_querying' do
+ get api("/projects/#{project.id}/environments", user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.size).to eq(1)
+ expect(json_response.first).not_to have_key('enable_advanced_logs_querying')
+ expect(json_response.first).not_to have_key('logs_api_path')
+ expect(json_response.first).not_to have_key('gitlab_managed_apps_logs_path')
+ end
end
context 'when filtering' do
- let!(:environment2) { create(:environment, project: project) }
+ let_it_be(:environment2) { create(:environment, project: project) }
it 'returns environment by name' do
get api("/projects/#{project.id}/environments?name=#{environment.name}", user)
diff --git a/spec/requests/api/graphql/ci/job_spec.rb b/spec/requests/api/graphql/ci/job_spec.rb
index 78f7d3e149b..b0514a0a963 100644
--- a/spec/requests/api/graphql/ci/job_spec.rb
+++ b/spec/requests/api/graphql/ci/job_spec.rb
@@ -5,6 +5,10 @@ require 'spec_helper'
RSpec.describe 'Query.project(fullPath).pipelines.job(id)' do
include GraphqlHelpers
+ around do |example|
+ travel_to(Time.current) { example.run }
+ end
+
let_it_be(:user) { create_default(:user) }
let_it_be(:project) { create(:project, :repository, :public) }
let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
@@ -35,13 +39,20 @@ RSpec.describe 'Query.project(fullPath).pipelines.job(id)' do
let(:terminal_type) { 'CiJob' }
it 'retrieves scalar fields' do
+ job_2.update!(
+ created_at: 40.seconds.ago,
+ queued_at: 32.seconds.ago,
+ started_at: 30.seconds.ago,
+ finished_at: 5.seconds.ago
+ )
post_graphql(query, current_user: user)
expect(graphql_data_at(*path)).to match a_hash_including(
'id' => global_id_of(job_2),
'name' => job_2.name,
'allowFailure' => job_2.allow_failure,
- 'duration' => job_2.duration,
+ 'duration' => 25,
+ 'queuedDuration' => 2.0,
'status' => job_2.status.upcase
)
end
diff --git a/spec/requests/api/graphql/ci/pipelines_spec.rb b/spec/requests/api/graphql/ci/pipelines_spec.rb
index 7933251b8e9..f207636283f 100644
--- a/spec/requests/api/graphql/ci/pipelines_spec.rb
+++ b/spec/requests/api/graphql/ci/pipelines_spec.rb
@@ -8,6 +8,130 @@ RSpec.describe 'Query.project(fullPath).pipelines' do
let_it_be(:project) { create(:project, :repository, :public) }
let_it_be(:user) { create(:user) }
+ around do |example|
+ travel_to(Time.current) { example.run }
+ end
+
+ describe 'duration fields' do
+ let_it_be(:pipeline) do
+ create(:ci_pipeline, project: project)
+ end
+
+ let(:query_path) do
+ [
+ [:project, { full_path: project.full_path }],
+ [:pipelines],
+ [:nodes]
+ ]
+ end
+
+ let(:query) do
+ wrap_fields(query_graphql_path(query_path, 'queuedDuration duration'))
+ end
+
+ before do
+ pipeline.update!(
+ created_at: 1.minute.ago,
+ started_at: 55.seconds.ago
+ )
+ create(:ci_build, :success,
+ pipeline: pipeline,
+ started_at: 55.seconds.ago,
+ finished_at: 10.seconds.ago)
+ pipeline.update_duration
+ pipeline.save!
+
+ post_graphql(query, current_user: user)
+ end
+
+ it 'includes the duration fields' do
+ path = query_path.map(&:first)
+ expect(graphql_data_at(*path, :queued_duration)).to eq [5.0]
+ expect(graphql_data_at(*path, :duration)).to eq [45]
+ end
+ end
+
+ describe '.stages' do
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:pipeline) { create(:ci_empty_pipeline, project: project) }
+ let_it_be(:stage) { create(:ci_stage_entity, pipeline: pipeline, project: project) }
+ let_it_be(:other_stage) { create(:ci_stage_entity, pipeline: pipeline, project: project, name: 'other') }
+
+ let(:first_n) { var('Int') }
+ let(:query_path) do
+ [
+ [:project, { full_path: project.full_path }],
+ [:pipelines],
+ [:nodes],
+ [:stages, { first: first_n }],
+ [:nodes]
+ ]
+ end
+
+ let(:query) do
+ with_signature([first_n], wrap_fields(query_graphql_path(query_path, :name)))
+ end
+
+ before_all do
+ # see app/services/ci/ensure_stage_service.rb to explain why we use stage_id
+ create(:ci_build, pipeline: pipeline, stage_id: stage.id, name: 'linux: [foo]')
+ create(:ci_build, pipeline: pipeline, stage_id: stage.id, name: 'linux: [bar]')
+ create(:ci_build, pipeline: pipeline, stage_id: other_stage.id, name: 'linux: [baz]')
+ end
+
+ it 'is null if the user is a guest' do
+ project.add_guest(user)
+
+ post_graphql(query, current_user: user, variables: first_n.with(1))
+
+ expect(graphql_data_at(:project, :pipelines, :nodes)).to contain_exactly a_hash_including('stages' => be_nil)
+ end
+
+ it 'is present if the user has reporter access' do
+ project.add_reporter(user)
+
+ post_graphql(query, current_user: user)
+
+ expect(graphql_data_at(:project, :pipelines, :nodes, :stages, :nodes, :name))
+ .to contain_exactly(eq(stage.name), eq(other_stage.name))
+ end
+
+ describe '.groups' do
+ let(:query_path) do
+ [
+ [:project, { full_path: project.full_path }],
+ [:pipelines],
+ [:nodes],
+ [:stages],
+ [:nodes],
+ [:groups],
+ [:nodes]
+ ]
+ end
+
+ let(:query) do
+ wrap_fields(query_graphql_path(query_path, :name))
+ end
+
+ it 'is empty if the user is a guest' do
+ project.add_guest(user)
+
+ post_graphql(query, current_user: user)
+
+ expect(graphql_data_at(:project, :pipelines, :nodes, :stages, :nodes, :groups)).to be_empty
+ end
+
+ it 'is present if the user has reporter access' do
+ project.add_reporter(user)
+
+ post_graphql(query, current_user: user)
+
+ expect(graphql_data_at(:project, :pipelines, :nodes, :stages, :nodes, :groups, :nodes, :name))
+ .to contain_exactly('linux', 'linux')
+ end
+ end
+ end
+
describe '.jobs' do
let(:first_n) { var('Int') }
let(:query_path) do
diff --git a/spec/requests/api/graphql/ci/runner_spec.rb b/spec/requests/api/graphql/ci/runner_spec.rb
new file mode 100644
index 00000000000..e1f84d23209
--- /dev/null
+++ b/spec/requests/api/graphql/ci/runner_spec.rb
@@ -0,0 +1,144 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Query.runner(id)' do
+ include GraphqlHelpers
+
+ let_it_be(:user) { create_default(:user, :admin) }
+
+ let_it_be(:active_runner) do
+ create(:ci_runner, :instance, description: 'Runner 1', contacted_at: 2.hours.ago,
+ active: true, version: 'adfe156', revision: 'a', locked: true, ip_address: '127.0.0.1', maximum_timeout: 600,
+ access_level: 0, tag_list: %w[tag1 tag2], run_untagged: true)
+ end
+
+ let_it_be(:inactive_runner) do
+ create(:ci_runner, :instance, description: 'Runner 2', contacted_at: 1.day.ago, active: false,
+ version: 'adfe157', revision: 'b', ip_address: '10.10.10.10', access_level: 1, run_untagged: true)
+ end
+
+ def get_runner(id)
+ case id
+ when :active_runner
+ active_runner
+ when :inactive_runner
+ inactive_runner
+ end
+ end
+
+ shared_examples 'runner details fetch' do |runner_id|
+ let(:query) do
+ wrap_fields(query_graphql_path(query_path, all_graphql_fields_for('CiRunner')))
+ end
+
+ let(:query_path) do
+ [
+ [:runner, { id: get_runner(runner_id).to_global_id.to_s }]
+ ]
+ end
+
+ it 'retrieves expected fields' do
+ post_graphql(query, current_user: user)
+
+ runner_data = graphql_data_at(:runner)
+ expect(runner_data).not_to be_nil
+
+ runner = get_runner(runner_id)
+ expect(runner_data).to match a_hash_including(
+ 'id' => "gid://gitlab/Ci::Runner/#{runner.id}",
+ 'description' => runner.description,
+ 'contactedAt' => runner.contacted_at&.iso8601,
+ 'version' => runner.version,
+ 'shortSha' => runner.short_sha,
+ 'revision' => runner.revision,
+ 'locked' => runner.locked,
+ 'active' => runner.active,
+ 'status' => runner.status.to_s.upcase,
+ 'maximumTimeout' => runner.maximum_timeout,
+ 'accessLevel' => runner.access_level.to_s.upcase,
+ 'runUntagged' => runner.run_untagged,
+ 'ipAddress' => runner.ip_address,
+ 'runnerType' => 'INSTANCE_TYPE'
+ )
+ expect(runner_data['tagList']).to match_array runner.tag_list
+ end
+ end
+
+ shared_examples 'retrieval by unauthorized user' do |runner_id|
+ let(:query) do
+ wrap_fields(query_graphql_path(query_path, all_graphql_fields_for('CiRunner')))
+ end
+
+ let(:query_path) do
+ [
+ [:runner, { id: get_runner(runner_id).to_global_id.to_s }]
+ ]
+ end
+
+ it 'returns null runner' do
+ post_graphql(query, current_user: user)
+
+ expect(graphql_data_at(:runner)).to be_nil
+ end
+ end
+
+ describe 'for active runner' do
+ it_behaves_like 'runner details fetch', :active_runner
+ end
+
+ describe 'for inactive runner' do
+ it_behaves_like 'runner details fetch', :inactive_runner
+ end
+
+ describe 'by regular user' do
+ let(:user) { create_default(:user) }
+
+ it_behaves_like 'retrieval by unauthorized user', :active_runner
+ end
+
+ describe 'by unauthenticated user' do
+ let(:user) { nil }
+
+ it_behaves_like 'retrieval by unauthorized user', :active_runner
+ end
+
+ describe 'Query limits' do
+ def runner_query(runner)
+ <<~SINGLE
+ runner(id: "#{runner.to_global_id}") {
+ #{all_graphql_fields_for('CiRunner')}
+ }
+ SINGLE
+ end
+
+ let(:single_query) do
+ <<~QUERY
+ {
+ active: #{runner_query(active_runner)}
+ }
+ QUERY
+ end
+
+ let(:double_query) do
+ <<~QUERY
+ {
+ active: #{runner_query(active_runner)}
+ inactive: #{runner_query(inactive_runner)}
+ }
+ QUERY
+ end
+
+ it 'does not execute more queries per runner', :aggregate_failures do
+ # warm-up license cache and so on:
+ post_graphql(single_query, current_user: user)
+
+ control = ActiveRecord::QueryRecorder.new { post_graphql(single_query, current_user: user) }
+
+ expect { post_graphql(double_query, current_user: user) }
+ .not_to exceed_query_limit(control)
+ expect(graphql_data_at(:active)).not_to be_nil
+ expect(graphql_data_at(:inactive)).not_to be_nil
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/ci/runners_spec.rb b/spec/requests/api/graphql/ci/runners_spec.rb
new file mode 100644
index 00000000000..778fe5b129e
--- /dev/null
+++ b/spec/requests/api/graphql/ci/runners_spec.rb
@@ -0,0 +1,114 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe 'Query.runners' do
+ include GraphqlHelpers
+
+ let_it_be(:current_user) { create_default(:user, :admin) }
+
+ describe 'Query.runners' do
+ let_it_be(:project) { create(:project, :repository, :public) }
+ let_it_be(:instance_runner) { create(:ci_runner, :instance, version: 'abc', revision: '123', description: 'Instance runner', ip_address: '127.0.0.1') }
+ let_it_be(:project_runner) { create(:ci_runner, :project, active: false, version: 'def', revision: '456', description: 'Project runner', projects: [project], ip_address: '127.0.0.1') }
+
+ let(:runners_graphql_data) { graphql_data['runners'] }
+
+ let(:params) { {} }
+
+ let(:fields) do
+ <<~QUERY
+ nodes {
+ #{all_graphql_fields_for('CiRunner')}
+ }
+ QUERY
+ end
+
+ let(:query) do
+ %(
+ query {
+ runners(type:#{runner_type},status:#{status}) {
+ #{fields}
+ }
+ }
+ )
+ end
+
+ before do
+ post_graphql(query, current_user: current_user)
+ end
+
+ shared_examples 'a working graphql query returning expected runner' do
+ it_behaves_like 'a working graphql query'
+
+ it 'returns expected runner' do
+ expect(runners_graphql_data['nodes'].map { |n| n['id'] }).to contain_exactly(expected_runner.to_global_id.to_s)
+ end
+ end
+
+ context 'runner_type is INSTANCE_TYPE and status is ACTIVE' do
+ let(:runner_type) { 'INSTANCE_TYPE' }
+ let(:status) { 'ACTIVE' }
+
+ let!(:expected_runner) { instance_runner }
+
+ it_behaves_like 'a working graphql query returning expected runner'
+ end
+
+ context 'runner_type is PROJECT_TYPE and status is NOT_CONNECTED' do
+ let(:runner_type) { 'PROJECT_TYPE' }
+ let(:status) { 'NOT_CONNECTED' }
+
+ let!(:expected_runner) { project_runner }
+
+ it_behaves_like 'a working graphql query returning expected runner'
+ end
+ end
+
+ describe 'pagination' do
+ let(:data_path) { [:runners] }
+
+ def pagination_query(params)
+ graphql_query_for(:runners, params, "#{page_info} nodes { id }")
+ end
+
+ def pagination_results_data(runners)
+ runners.map { |runner| GitlabSchema.parse_gid(runner['id'], expected_type: ::Ci::Runner).model_id.to_i }
+ end
+
+ let_it_be(:runners) do
+ common_args = {
+ version: 'abc',
+ revision: '123',
+ ip_address: '127.0.0.1'
+ }
+
+ [
+ create(:ci_runner, :instance, created_at: 4.days.ago, contacted_at: 3.days.ago, **common_args),
+ create(:ci_runner, :instance, created_at: 30.hours.ago, contacted_at: 1.day.ago, **common_args),
+ create(:ci_runner, :instance, created_at: 1.day.ago, contacted_at: 1.hour.ago, **common_args),
+ create(:ci_runner, :instance, created_at: 2.days.ago, contacted_at: 2.days.ago, **common_args),
+ create(:ci_runner, :instance, created_at: 3.days.ago, contacted_at: 1.second.ago, **common_args)
+ ]
+ end
+
+ context 'when sorted by contacted_at ascending' do
+ let(:ordered_runners) { runners.sort_by(&:contacted_at) }
+
+ it_behaves_like 'sorted paginated query' do
+ let(:sort_param) { :CONTACTED_ASC }
+ let(:first_param) { 2 }
+ let(:expected_results) { ordered_runners.map(&:id) }
+ end
+ end
+
+ context 'when sorted by created_at' do
+ let(:ordered_runners) { runners.sort_by(&:created_at).reverse }
+
+ it_behaves_like 'sorted paginated query' do
+ let(:sort_param) { :CREATED_DESC }
+ let(:first_param) { 2 }
+ let(:expected_results) { ordered_runners.map(&:id) }
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/ci/template_spec.rb b/spec/requests/api/graphql/ci/template_spec.rb
new file mode 100644
index 00000000000..1bbef7d7f30
--- /dev/null
+++ b/spec/requests/api/graphql/ci/template_spec.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe 'Querying CI template' do
+ include GraphqlHelpers
+
+ let_it_be(:project) { create(:project, :public) }
+ let_it_be(:user) { create(:user) }
+
+ let(:query) do
+ <<~QUERY
+ {
+ project(fullPath: "#{project.full_path}") {
+ name
+ ciTemplate(name: "#{template_name}") {
+ name
+ content
+ }
+ }
+ }
+ QUERY
+ end
+
+ before do
+ post_graphql(query, current_user: user)
+ end
+
+ context 'when the template exists' do
+ let(:template_name) { 'Android' }
+
+ it_behaves_like 'a working graphql query'
+
+ it 'returns correct data' do
+ expect(graphql_data.dig('project', 'ciTemplate', 'name')).to eq(template_name)
+ expect(graphql_data.dig('project', 'ciTemplate', 'content')).not_to be_blank
+ end
+ end
+
+ context 'when the template does not exist' do
+ let(:template_name) { 'doesnotexist' }
+
+ it_behaves_like 'a working graphql query'
+
+ it 'returns correct data' do
+ expect(graphql_data.dig('project', 'ciTemplate')).to eq(nil)
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/group/milestones_spec.rb b/spec/requests/api/graphql/group/milestones_spec.rb
index a5b489d72fd..601cab6aade 100644
--- a/spec/requests/api/graphql/group/milestones_spec.rb
+++ b/spec/requests/api/graphql/group/milestones_spec.rb
@@ -197,18 +197,6 @@ RSpec.describe 'Milestones through GroupQuery' do
}
})
end
-
- context 'when the graphql_milestone_stats feature flag is disabled' do
- before do
- stub_feature_flags(graphql_milestone_stats: false)
- end
-
- it 'returns nil for the stats field' do
- expect(post_query).to eq({
- 'stats' => nil
- })
- end
- end
end
end
end
diff --git a/spec/requests/api/graphql/group/packages_spec.rb b/spec/requests/api/graphql/group/packages_spec.rb
index 85775598b2e..adee556db3a 100644
--- a/spec/requests/api/graphql/group/packages_spec.rb
+++ b/spec/requests/api/graphql/group/packages_spec.rb
@@ -7,46 +7,19 @@ RSpec.describe 'getting a package list for a group' do
let_it_be(:resource) { create(:group, :private) }
let_it_be(:group_two) { create(:group, :private) }
- let_it_be(:project) { create(:project, :repository, group: resource) }
- let_it_be(:another_project) { create(:project, :repository, group: resource) }
- let_it_be(:group_two_project) { create(:project, :repository, group: group_two) }
+ let_it_be(:project1) { create(:project, :repository, group: resource) }
+ let_it_be(:project2) { create(:project, :repository, group: resource) }
let_it_be(:current_user) { create(:user) }
- let_it_be(:package) { create(:package, project: project) }
- let_it_be(:npm_package) { create(:npm_package, project: group_two_project) }
- let_it_be(:maven_package) { create(:maven_package, project: project) }
- let_it_be(:debian_package) { create(:debian_package, project: another_project) }
- let_it_be(:composer_package) { create(:composer_package, project: another_project) }
- let_it_be(:composer_metadatum) do
- create(:composer_metadatum, package: composer_package,
- target_sha: 'afdeh',
- composer_json: { name: 'x', type: 'y', license: 'z', version: 1 })
- end
-
- let(:package_names) { graphql_data_at(:group, :packages, :nodes, :name) }
- let(:target_shas) { graphql_data_at(:group, :packages, :nodes, :metadata, :target_sha) }
- let(:packages) { graphql_data_at(:group, :packages, :nodes) }
-
- let(:fields) do
- <<~QUERY
- nodes {
- #{all_graphql_fields_for('packages'.classify, excluded: ['project'])}
- metadata { #{query_graphql_fragment('ComposerMetadata')} }
- }
- QUERY
- end
-
- let(:query) do
- graphql_query_for(
- 'group',
- { 'fullPath' => resource.full_path },
- query_graphql_field('packages', {}, fields)
- )
- end
+ let(:resource_type) { :group }
it_behaves_like 'group and project packages query'
context 'with a batched query' do
+ let_it_be(:group_two_project) { create(:project, :repository, group: group_two) }
+ let_it_be(:group_one_package) { create(:npm_package, project: project1) }
+ let_it_be(:group_two_package) { create(:npm_package, project: group_two_project) }
+
let(:batch_query) do
<<~QUERY
{
@@ -65,12 +38,7 @@ RSpec.describe 'getting a package list for a group' do
end
it 'returns an error for the second group and data for the first' do
- expect(a_packages_names).to contain_exactly(
- package.name,
- maven_package.name,
- debian_package.name,
- composer_package.name
- )
+ expect(a_packages_names).to contain_exactly(group_one_package.name)
expect_graphql_errors_to_include [/Packages can be requested only for one group at a time/]
expect(graphql_data_at(:b, :packages)).to be(nil)
end
diff --git a/spec/requests/api/graphql/issue/issue_spec.rb b/spec/requests/api/graphql/issue/issue_spec.rb
index e8b8caf6c2d..42ca3348384 100644
--- a/spec/requests/api/graphql/issue/issue_spec.rb
+++ b/spec/requests/api/graphql/issue/issue_spec.rb
@@ -76,7 +76,7 @@ RSpec.describe 'Query.issue(id)' do
post_graphql(query, current_user: current_user)
end
- it "returns the Issue and field #{params['field']}" do
+ it "returns the issue and field #{params['field']}" do
expect(issue_data.keys).to eq([field])
end
end
@@ -86,7 +86,7 @@ RSpec.describe 'Query.issue(id)' do
context 'when selecting multiple fields' do
let(:issue_fields) { ['title', 'description', 'updatedBy { username }'] }
- it 'returns the Issue with the specified fields' do
+ it 'returns the issue with the specified fields' do
post_graphql(query, current_user: current_user)
expect(issue_data.keys).to eq %w[title description updatedBy]
@@ -115,7 +115,7 @@ RSpec.describe 'Query.issue(id)' do
end
end
- context 'when passed a non-Issue gid' do
+ context 'when passed a non-issue gid' do
let(:mr) { create(:merge_request) }
it 'returns an error' do
diff --git a/spec/requests/api/graphql/merge_request/merge_request_spec.rb b/spec/requests/api/graphql/merge_request/merge_request_spec.rb
new file mode 100644
index 00000000000..75dd01a0763
--- /dev/null
+++ b/spec/requests/api/graphql/merge_request/merge_request_spec.rb
@@ -0,0 +1,111 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Query.merge_request(id)' do
+ include GraphqlHelpers
+
+ let_it_be(:project) { create(:project, :empty_repo) }
+ let_it_be(:merge_request) { create(:merge_request, source_project: project) }
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:merge_request_params) { { 'id' => merge_request.to_global_id.to_s } }
+
+ let(:merge_request_data) { graphql_data['mergeRequest'] }
+ let(:merge_request_fields) { all_graphql_fields_for('MergeRequest'.classify) }
+
+ let(:query) do
+ graphql_query_for('mergeRequest', merge_request_params, merge_request_fields)
+ end
+
+ it_behaves_like 'a working graphql query' do
+ before do
+ post_graphql(query, current_user: current_user)
+ end
+ end
+
+ it_behaves_like 'a noteable graphql type we can query' do
+ let(:noteable) { merge_request }
+ let(:project) { merge_request.project }
+ let(:path_to_noteable) { [:merge_request] }
+
+ before do
+ project.add_reporter(current_user)
+ end
+
+ def query(fields)
+ graphql_query_for('mergeRequest', merge_request_params, fields)
+ end
+ end
+
+ context 'when the user does not have access to the merge request' do
+ it 'returns nil' do
+ post_graphql(query)
+
+ expect(merge_request_data).to be nil
+ end
+ end
+
+ context 'when the user does have access' do
+ before do
+ project.add_reporter(current_user)
+ end
+
+ it 'returns the merge request' do
+ post_graphql(query, current_user: current_user)
+
+ expect(merge_request_data).to include(
+ 'title' => merge_request.title,
+ 'description' => merge_request.description
+ )
+ end
+
+ context 'when selecting any single field' do
+ where(:field) do
+ scalar_fields_of('MergeRequest').map { |name| [name] }
+ end
+
+ with_them do
+ it_behaves_like 'a working graphql query' do
+ let(:merge_request_fields) do
+ field
+ end
+
+ before do
+ post_graphql(query, current_user: current_user)
+ end
+
+ it "returns the merge request and field #{params['field']}" do
+ expect(merge_request_data.keys).to eq([field])
+ end
+ end
+ end
+ end
+
+ context 'when selecting multiple fields' do
+ let(:merge_request_fields) { ['title', 'description', 'author { username }'] }
+
+ it 'returns the merge request with the specified fields' do
+ post_graphql(query, current_user: current_user)
+
+ expect(merge_request_data.keys).to eq %w[title description author]
+ expect(merge_request_data['title']).to eq(merge_request.title)
+ expect(merge_request_data['description']).to eq(merge_request.description)
+ expect(merge_request_data['author']['username']).to eq(merge_request.author.username)
+ end
+ end
+
+ context 'when passed a non-merge request gid' do
+ let(:issue) { create(:issue) }
+
+ it 'returns an error' do
+ gid = issue.to_global_id.to_s
+ merge_request_params['id'] = gid
+
+ post_graphql(query, current_user: current_user)
+
+ expect(graphql_errors).not_to be nil
+ expect(graphql_errors.first['message']).to eq("\"#{gid}\" does not represent an instance of MergeRequest")
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/metadata_query_spec.rb b/spec/requests/api/graphql/metadata_query_spec.rb
index 6344ec371c8..840bd7c018c 100644
--- a/spec/requests/api/graphql/metadata_query_spec.rb
+++ b/spec/requests/api/graphql/metadata_query_spec.rb
@@ -8,16 +8,48 @@ RSpec.describe 'getting project information' do
let(:query) { graphql_query_for('metadata', {}, all_graphql_fields_for('Metadata')) }
context 'logged in' do
- it 'returns version and revision' do
- post_graphql(query, current_user: create(:user))
-
- expect(graphql_errors).to be_nil
- expect(graphql_data).to eq(
+ let(:expected_data) do
+ {
'metadata' => {
'version' => Gitlab::VERSION,
- 'revision' => Gitlab.revision
+ 'revision' => Gitlab.revision,
+ 'kas' => {
+ 'enabled' => Gitlab::Kas.enabled?,
+ 'version' => expected_kas_version,
+ 'externalUrl' => expected_kas_external_url
+ }
}
- )
+ }
+ end
+
+ context 'kas is enabled' do
+ let(:expected_kas_version) { Gitlab::Kas.version }
+ let(:expected_kas_external_url) { Gitlab::Kas.external_url }
+
+ before do
+ allow(Gitlab::Kas).to receive(:enabled?).and_return(true)
+ post_graphql(query, current_user: create(:user))
+ end
+
+ it 'returns version, revision, kas_enabled, kas_version, kas_external_url' do
+ expect(graphql_errors).to be_nil
+ expect(graphql_data).to eq(expected_data)
+ end
+ end
+
+ context 'kas is disabled' do
+ let(:expected_kas_version) { nil }
+ let(:expected_kas_external_url) { nil }
+
+ before do
+ allow(Gitlab::Kas).to receive(:enabled?).and_return(false)
+ post_graphql(query, current_user: create(:user))
+ end
+
+ it 'returns version and revision' do
+ expect(graphql_errors).to be_nil
+ expect(graphql_data).to eq(expected_data)
+ end
end
end
diff --git a/spec/requests/api/graphql/mutations/boards/destroy_spec.rb b/spec/requests/api/graphql/mutations/boards/destroy_spec.rb
index a6d894e698d..23e099e94b6 100644
--- a/spec/requests/api/graphql/mutations/boards/destroy_spec.rb
+++ b/spec/requests/api/graphql/mutations/boards/destroy_spec.rb
@@ -8,7 +8,8 @@ RSpec.describe Mutations::Boards::Destroy do
let_it_be(:current_user, reload: true) { create(:user) }
let_it_be(:project, reload: true) { create(:project) }
let_it_be(:board) { create(:board, project: project) }
- let_it_be(:other_board) { create(:board, project: project) }
+ let_it_be(:other_board, refind: true) { create(:board, project: project) }
+
let(:mutation) do
variables = {
id: GitlabSchema.id_from_object(board).to_s
diff --git a/spec/requests/api/graphql/mutations/boards/lists/destroy_spec.rb b/spec/requests/api/graphql/mutations/boards/lists/destroy_spec.rb
index 42f690f53ed..83309ead352 100644
--- a/spec/requests/api/graphql/mutations/boards/lists/destroy_spec.rb
+++ b/spec/requests/api/graphql/mutations/boards/lists/destroy_spec.rb
@@ -6,72 +6,23 @@ RSpec.describe Mutations::Boards::Lists::Destroy do
include GraphqlHelpers
let_it_be(:current_user, reload: true) { create(:user) }
- let_it_be(:project, reload: true) { create(:project) }
- let_it_be(:board) { create(:board, project: project) }
- let_it_be(:list) { create(:list, board: board) }
- let(:mutation) do
- variables = {
- list_id: GitlabSchema.id_from_object(list).to_s
- }
- graphql_mutation(:destroy_board_list, variables)
- end
-
- subject { post_graphql_mutation(mutation, current_user: current_user) }
-
- def mutation_response
- graphql_mutation_response(:destroy_board_list)
- end
-
- context 'when the user does not have permission' do
- it_behaves_like 'a mutation that returns a top-level access error'
-
- it 'does not destroy the list' do
- expect { subject }.not_to change { List.count }
- end
- end
-
- context 'when the user has permission' do
- before do
- project.add_maintainer(current_user)
- end
-
- context 'when given id is not for a list' do
- let_it_be(:list) { build_stubbed(:issue, project: project) }
-
- it 'returns an error' do
- subject
+ it_behaves_like 'board lists destroy request' do
+ let_it_be(:group, reload: true) { create(:group) }
+ let_it_be(:board) { create(:board, group: group) }
+ let_it_be(:list, refind: true) { create(:list, board: board) }
- expect(graphql_errors.first['message']).to include('does not represent an instance of List')
- end
+ let(:variables) do
+ {
+ list_id: GitlabSchema.id_from_object(list).to_s
+ }
end
- context 'when everything is ok' do
- it 'destroys the list' do
- expect { subject }.to change { List.count }.from(2).to(1)
- end
-
- it 'returns an empty list' do
- post_graphql_mutation(mutation, current_user: current_user)
-
- expect(mutation_response).to have_key('list')
- expect(mutation_response['list']).to be_nil
- end
+ let(:mutation) do
+ graphql_mutation(:destroy_board_list, variables)
end
- context 'when the list is not destroyable' do
- let_it_be(:list) { create(:list, board: board, list_type: :backlog) }
-
- it 'does not destroy the list' do
- expect { subject }.not_to change { List.count }.from(3)
- end
-
- it 'returns an error and not nil list' do
- subject
-
- expect(mutation_response['errors']).not_to be_empty
- expect(mutation_response['list']).not_to be_nil
- end
- end
+ let(:mutation_response) { graphql_mutation_response(:destroy_board_list) }
+ let(:klass) { List }
end
end
diff --git a/spec/requests/api/graphql/mutations/boards/lists/update_spec.rb b/spec/requests/api/graphql/mutations/boards/lists/update_spec.rb
index 8e24e053211..c7885879a9d 100644
--- a/spec/requests/api/graphql/mutations/boards/lists/update_spec.rb
+++ b/spec/requests/api/graphql/mutations/boards/lists/update_spec.rb
@@ -11,46 +11,9 @@ RSpec.describe 'Update of an existing board list' do
let_it_be(:list) { create(:list, board: board, position: 0) }
let_it_be(:list2) { create(:list, board: board) }
let_it_be(:input) { { list_id: list.to_global_id.to_s, position: 1, collapsed: true } }
+
let(:mutation) { graphql_mutation(:update_board_list, input) }
let(:mutation_response) { graphql_mutation_response(:update_board_list) }
- context 'the user is not allowed to read board lists' do
- it_behaves_like 'a mutation that returns a top-level access error'
- end
-
- before do
- list.update_preferences_for(current_user, collapsed: false)
- end
-
- context 'when user has permissions to admin board lists' do
- before do
- group.add_reporter(current_user)
- end
-
- it 'updates the list position and collapsed state' do
- post_graphql_mutation(mutation, current_user: current_user)
-
- expect(response).to have_gitlab_http_status(:success)
- expect(mutation_response['list']).to include(
- 'position' => 1,
- 'collapsed' => true
- )
- end
- end
-
- context 'when user has permissions to read board lists' do
- before do
- group.add_guest(current_user)
- end
-
- it 'updates the list collapsed state but not the list position' do
- post_graphql_mutation(mutation, current_user: current_user)
-
- expect(response).to have_gitlab_http_status(:success)
- expect(mutation_response['list']).to include(
- 'position' => 0,
- 'collapsed' => true
- )
- end
- end
+ it_behaves_like 'a GraphQL request to update board list'
end
diff --git a/spec/requests/api/graphql/mutations/ci/job_play_spec.rb b/spec/requests/api/graphql/mutations/ci/job_play_spec.rb
new file mode 100644
index 00000000000..0874e225259
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/ci/job_play_spec.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'JobPlay' do
+ include GraphqlHelpers
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project) }
+ let_it_be(:pipeline) { create(:ci_pipeline, project: project, user: user) }
+ let_it_be(:job) { create(:ci_build, pipeline: pipeline, name: 'build') }
+
+ let(:mutation) do
+ variables = {
+ id: job.to_global_id.to_s
+ }
+ graphql_mutation(:job_play, variables,
+ <<-QL
+ errors
+ job {
+ id
+ }
+ QL
+ )
+ end
+
+ let(:mutation_response) { graphql_mutation_response(:job_play) }
+
+ before_all do
+ project.add_maintainer(user)
+ end
+
+ it 'returns an error if the user is not allowed to play the job' do
+ post_graphql_mutation(mutation, current_user: create(:user))
+
+ expect(graphql_errors).not_to be_empty
+ end
+
+ it 'plays a job' do
+ job_id = ::Gitlab::GlobalId.build(job, id: job.id).to_s
+ post_graphql_mutation(mutation, current_user: user)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_response['job']['id']).to eq(job_id)
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/ci/job_retry_spec.rb b/spec/requests/api/graphql/mutations/ci/job_retry_spec.rb
new file mode 100644
index 00000000000..a14935379dc
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/ci/job_retry_spec.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'JobRetry' do
+ include GraphqlHelpers
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project) }
+ let_it_be(:pipeline) { create(:ci_pipeline, project: project, user: user) }
+ let_it_be(:job) { create(:ci_build, :success, pipeline: pipeline, name: 'build') }
+
+ let(:mutation) do
+ variables = {
+ id: job.to_global_id.to_s
+ }
+ graphql_mutation(:job_retry, variables,
+ <<-QL
+ errors
+ job {
+ id
+ }
+ QL
+ )
+ end
+
+ let(:mutation_response) { graphql_mutation_response(:job_retry) }
+
+ before_all do
+ project.add_maintainer(user)
+ end
+
+ it 'returns an error if the user is not allowed to retry the job' do
+ post_graphql_mutation(mutation, current_user: create(:user))
+
+ expect(graphql_errors).not_to be_empty
+ end
+
+ it 'retries a job' do
+ job_id = ::Gitlab::GlobalId.build(job, id: job.id).to_s
+ post_graphql_mutation(mutation, current_user: user)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_response['job']['id']).to eq(job_id)
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/issues/create_spec.rb b/spec/requests/api/graphql/mutations/issues/create_spec.rb
index 39b408faa90..66450f8c604 100644
--- a/spec/requests/api/graphql/mutations/issues/create_spec.rb
+++ b/spec/requests/api/graphql/mutations/issues/create_spec.rb
@@ -20,7 +20,8 @@ RSpec.describe 'Create an issue' do
'title' => 'new title',
'description' => 'new description',
'confidential' => true,
- 'dueDate' => Date.tomorrow.strftime('%Y-%m-%d')
+ 'dueDate' => Date.tomorrow.strftime('%Y-%m-%d'),
+ 'type' => 'ISSUE'
}
end
@@ -37,7 +38,7 @@ RSpec.describe 'Create an issue' do
project.add_developer(current_user)
end
- it 'updates the issue' do
+ it 'creates the issue' do
post_graphql_mutation(mutation, current_user: current_user)
expect(response).to have_gitlab_http_status(:success)
diff --git a/spec/requests/api/graphql/mutations/issues/set_due_date_spec.rb b/spec/requests/api/graphql/mutations/issues/set_due_date_spec.rb
index b3c9b9d4995..ea5be9f9852 100644
--- a/spec/requests/api/graphql/mutations/issues/set_due_date_spec.rb
+++ b/spec/requests/api/graphql/mutations/issues/set_due_date_spec.rb
@@ -42,11 +42,34 @@ RSpec.describe 'Setting Due Date of an issue' do
expect(graphql_errors).to include(a_hash_including('message' => error))
end
- it 'updates the issue due date' do
- post_graphql_mutation(mutation, current_user: current_user)
+ context 'when due date value is a valid date' do
+ it 'updates the issue due date' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_response['issue']['dueDate']).to eq(2.days.since.to_date.to_s)
+ end
+ end
+
+ context 'when due date value is null' do
+ let(:input) { { due_date: nil } }
+
+ it 'updates the issue to remove the due date' do
+ post_graphql_mutation(mutation, current_user: current_user)
- expect(response).to have_gitlab_http_status(:success)
- expect(mutation_response['issue']['dueDate']).to eq(2.days.since.to_date.to_s)
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_response['issue']['dueDate']).to be nil
+ end
+ end
+
+ context 'when due date argument is not given' do
+ let(:input) { {} }
+
+ it 'returns an error' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(graphql_errors).to include(a_hash_including('message' => /Argument dueDate must be provided/))
+ end
end
context 'when the due date value is not a valid time' do
diff --git a/spec/requests/api/graphql/mutations/issues/update_spec.rb b/spec/requests/api/graphql/mutations/issues/update_spec.rb
index 71f25dbbe49..adfa2a2bc08 100644
--- a/spec/requests/api/graphql/mutations/issues/update_spec.rb
+++ b/spec/requests/api/graphql/mutations/issues/update_spec.rb
@@ -14,7 +14,8 @@ RSpec.describe 'Update of an existing issue' do
'title' => 'new title',
'description' => 'new description',
'confidential' => true,
- 'dueDate' => Date.tomorrow.strftime('%Y-%m-%d')
+ 'dueDate' => Date.tomorrow.strftime('%Y-%m-%d'),
+ 'type' => 'ISSUE'
}
end
diff --git a/spec/requests/api/graphql/mutations/labels/create_spec.rb b/spec/requests/api/graphql/mutations/labels/create_spec.rb
index 28284408306..ca3ccc8e06c 100644
--- a/spec/requests/api/graphql/mutations/labels/create_spec.rb
+++ b/spec/requests/api/graphql/mutations/labels/create_spec.rb
@@ -11,7 +11,8 @@ RSpec.describe Mutations::Labels::Create do
{
'title' => 'foo',
'description' => 'some description',
- 'color' => '#FF0000'
+ 'color' => '#FF0000',
+ 'removeOnClose' => true
}
end
diff --git a/spec/requests/api/graphql/mutations/namespace/package_settings/update_spec.rb b/spec/requests/api/graphql/mutations/namespace/package_settings/update_spec.rb
index 749373e7b8d..202e7e7c333 100644
--- a/spec/requests/api/graphql/mutations/namespace/package_settings/update_spec.rb
+++ b/spec/requests/api/graphql/mutations/namespace/package_settings/update_spec.rb
@@ -12,7 +12,9 @@ RSpec.describe 'Updating the package settings' do
{
namespace_path: namespace.full_path,
maven_duplicates_allowed: false,
- maven_duplicate_exception_regex: 'foo-.*'
+ maven_duplicate_exception_regex: 'foo-.*',
+ generic_duplicates_allowed: false,
+ generic_duplicate_exception_regex: 'bar-.*'
}
end
@@ -22,6 +24,8 @@ RSpec.describe 'Updating the package settings' do
packageSettings {
mavenDuplicatesAllowed
mavenDuplicateExceptionRegex
+ genericDuplicatesAllowed
+ genericDuplicateExceptionRegex
}
errors
QL
@@ -40,6 +44,8 @@ RSpec.describe 'Updating the package settings' do
expect(mutation_response['errors']).to be_empty
expect(package_settings_response['mavenDuplicatesAllowed']).to eq(params[:maven_duplicates_allowed])
expect(package_settings_response['mavenDuplicateExceptionRegex']).to eq(params[:maven_duplicate_exception_regex])
+ expect(package_settings_response['genericDuplicatesAllowed']).to eq(params[:generic_duplicates_allowed])
+ expect(package_settings_response['genericDuplicateExceptionRegex']).to eq(params[:generic_duplicate_exception_regex])
end
end
@@ -69,8 +75,8 @@ RSpec.describe 'Updating the package settings' do
RSpec.shared_examples 'accepting the mutation request updating the package settings' do
it_behaves_like 'updating the namespace package setting attributes',
- from: { maven_duplicates_allowed: true, maven_duplicate_exception_regex: 'SNAPSHOT' },
- to: { maven_duplicates_allowed: false, maven_duplicate_exception_regex: 'foo-.*' }
+ from: { maven_duplicates_allowed: true, maven_duplicate_exception_regex: 'SNAPSHOT', generic_duplicates_allowed: true, generic_duplicate_exception_regex: 'foo' },
+ to: { maven_duplicates_allowed: false, maven_duplicate_exception_regex: 'foo-.*', generic_duplicates_allowed: false, generic_duplicate_exception_regex: 'bar-.*' }
it_behaves_like 'returning a success'
it_behaves_like 'rejecting invalid regex'
diff --git a/spec/requests/api/graphql/mutations/security/ci_configuration/configure_secret_detection_spec.rb b/spec/requests/api/graphql/mutations/security/ci_configuration/configure_secret_detection_spec.rb
new file mode 100644
index 00000000000..23a154b71a0
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/security/ci_configuration/configure_secret_detection_spec.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'ConfigureSecretDetection' do
+ include GraphqlHelpers
+
+ let_it_be(:project) { create(:project, :test_repo) }
+
+ let(:variables) { { project_path: project.full_path } }
+ let(:mutation) { graphql_mutation(:configure_secret_detection, variables) }
+ let(:mutation_response) { graphql_mutation_response(:configureSecretDetection) }
+
+ context 'when authorized' do
+ let_it_be(:user) { project.owner }
+
+ it 'creates a branch with secret detection configured' do
+ post_graphql_mutation(mutation, current_user: user)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_response['errors']).to be_empty
+ expect(mutation_response['branch']).not_to be_empty
+ expect(mutation_response['successPath']).not_to be_empty
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/packages/composer_spec.rb b/spec/requests/api/graphql/packages/composer_spec.rb
new file mode 100644
index 00000000000..34137a07c34
--- /dev/null
+++ b/spec/requests/api/graphql/packages/composer_spec.rb
@@ -0,0 +1,64 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe 'package details' do
+ include GraphqlHelpers
+
+ let_it_be(:project) { create(:project) }
+ let_it_be(:composer_package) { create(:composer_package, project: project) }
+ let_it_be(:composer_json) { { name: 'name', type: 'type', license: 'license', version: 1 } }
+ let_it_be(:composer_metadatum) do
+ # we are forced to manually create the metadatum, without using the factory to force the sha to be a string
+ # and avoid an error where gitaly can't find the repository
+ create(:composer_metadatum, package: composer_package, target_sha: 'foo_sha', composer_json: composer_json)
+ end
+
+ let(:depth) { 3 }
+ let(:excluded) { %w[metadata apiFuzzingCiConfiguration pipeline packageFiles] }
+ let(:metadata) { query_graphql_fragment('ComposerMetadata') }
+ let(:package_files) { all_graphql_fields_for('PackageFile') }
+ let(:user) { project.owner }
+ let(:package_global_id) { global_id_of(composer_package) }
+ let(:package_details) { graphql_data_at(:package) }
+ let(:metadata_response) { graphql_data_at(:package, :metadata) }
+ let(:package_files_response) { graphql_data_at(:package, :package_files, :nodes) }
+
+ let(:query) do
+ graphql_query_for(:package, { id: package_global_id }, <<~FIELDS)
+ #{all_graphql_fields_for('PackageDetailsType', max_depth: depth, excluded: excluded)}
+ metadata {
+ #{metadata}
+ }
+ packageFiles {
+ nodes {
+ #{package_files}
+ }
+ }
+ FIELDS
+ end
+
+ subject { post_graphql(query, current_user: user) }
+
+ before do
+ subject
+ end
+
+ it_behaves_like 'a working graphql query' do
+ it 'matches the JSON schema' do
+ expect(package_details).to match_schema('graphql/packages/package_details')
+ end
+ end
+
+ describe 'Composer' do
+ it 'has the correct metadata' do
+ expect(metadata_response).to include(
+ 'targetSha' => 'foo_sha',
+ 'composerJson' => composer_json.transform_keys(&:to_s).transform_values(&:to_s)
+ )
+ end
+
+ it 'does not have files' do
+ expect(package_files_response).to be_empty
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/packages/conan_spec.rb b/spec/requests/api/graphql/packages/conan_spec.rb
new file mode 100644
index 00000000000..dc64c5057d5
--- /dev/null
+++ b/spec/requests/api/graphql/packages/conan_spec.rb
@@ -0,0 +1,90 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe 'conan package details' do
+ include GraphqlHelpers
+
+ let_it_be(:project) { create(:project) }
+ let_it_be(:conan_package) { create(:conan_package, project: project) }
+
+ let(:package_global_id) { global_id_of(conan_package) }
+ let(:metadata) { query_graphql_fragment('ConanMetadata') }
+ let(:first_file) { conan_package.package_files.find { |f| global_id_of(f) == first_file_response['id'] } }
+
+ let(:depth) { 3 }
+ let(:excluded) { %w[metadata apiFuzzingCiConfiguration pipeline packageFiles] }
+ let(:package_files) { all_graphql_fields_for('PackageFile') }
+ let(:package_files_metadata) {query_graphql_fragment('ConanFileMetadata')}
+
+ let(:user) { project.owner }
+ let(:package_details) { graphql_data_at(:package) }
+ let(:metadata_response) { graphql_data_at(:package, :metadata) }
+ let(:package_files_response) { graphql_data_at(:package, :package_files, :nodes) }
+ let(:first_file_response) { graphql_data_at(:package, :package_files, :nodes, 0)}
+ let(:first_file_response_metadata) { graphql_data_at(:package, :package_files, :nodes, 0, :file_metadata)}
+
+ let(:query) do
+ graphql_query_for(:package, { id: package_global_id }, <<~FIELDS)
+ #{all_graphql_fields_for('PackageDetailsType', max_depth: depth, excluded: excluded)}
+ metadata {
+ #{metadata}
+ }
+ packageFiles {
+ nodes {
+ #{package_files}
+ fileMetadata {
+ #{package_files_metadata}
+ }
+ }
+ }
+ FIELDS
+ end
+
+ subject { post_graphql(query, current_user: user) }
+
+ before do
+ subject
+ end
+
+ it_behaves_like 'a working graphql query' do
+ it 'matches the JSON schema' do
+ expect(package_details).to match_schema('graphql/packages/package_details')
+ end
+ end
+
+ it 'has the correct metadata' do
+ expect(metadata_response).to include(
+ 'id' => global_id_of(conan_package.conan_metadatum),
+ 'recipe' => conan_package.conan_metadatum.recipe,
+ 'packageChannel' => conan_package.conan_metadatum.package_channel,
+ 'packageUsername' => conan_package.conan_metadatum.package_username,
+ 'recipePath' => conan_package.conan_metadatum.recipe_path
+ )
+ end
+
+ it 'has the right amount of files' do
+ expect(package_files_response.length).to be(conan_package.package_files.length)
+ end
+
+ it 'has the basic package files data' do
+ expect(first_file_response).to include(
+ 'id' => global_id_of(first_file),
+ 'fileName' => first_file.file_name,
+ 'size' => first_file.size.to_s,
+ 'downloadPath' => first_file.download_path,
+ 'fileSha1' => first_file.file_sha1,
+ 'fileMd5' => first_file.file_md5,
+ 'fileSha256' => first_file.file_sha256
+ )
+ end
+
+ it 'has the correct file metadata' do
+ expect(first_file_response_metadata).to include(
+ 'id' => global_id_of(first_file.conan_file_metadatum),
+ 'packageRevision' => first_file.conan_file_metadatum.package_revision,
+ 'conanPackageReference' => first_file.conan_file_metadatum.conan_package_reference,
+ 'recipeRevision' => first_file.conan_file_metadatum.recipe_revision,
+ 'conanFileType' => first_file.conan_file_metadatum.conan_file_type.upcase
+ )
+ end
+end
diff --git a/spec/requests/api/graphql/packages/maven_spec.rb b/spec/requests/api/graphql/packages/maven_spec.rb
new file mode 100644
index 00000000000..8b6b5ea0986
--- /dev/null
+++ b/spec/requests/api/graphql/packages/maven_spec.rb
@@ -0,0 +1,94 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe 'maven package details' do
+ include GraphqlHelpers
+
+ let_it_be(:project) { create(:project) }
+ let_it_be(:maven_package) { create(:maven_package, project: project) }
+
+ let(:package_global_id) { global_id_of(maven_package) }
+ let(:metadata) { query_graphql_fragment('MavenMetadata') }
+ let(:first_file) { maven_package.package_files.find { |f| global_id_of(f) == first_file_response['id'] } }
+
+ let(:depth) { 3 }
+ let(:excluded) { %w[metadata apiFuzzingCiConfiguration pipeline packageFiles] }
+ let(:package_files) { all_graphql_fields_for('PackageFile') }
+
+ let(:user) { project.owner }
+ let(:package_details) { graphql_data_at(:package) }
+ let(:metadata_response) { graphql_data_at(:package, :metadata) }
+ let(:package_files_response) { graphql_data_at(:package, :package_files, :nodes) }
+ let(:first_file_response) { graphql_data_at(:package, :package_files, :nodes, 0)}
+
+ let(:query) do
+ graphql_query_for(:package, { id: package_global_id }, <<~FIELDS)
+ #{all_graphql_fields_for('PackageDetailsType', max_depth: depth, excluded: excluded)}
+ metadata {
+ #{metadata}
+ }
+ packageFiles {
+ nodes {
+ #{package_files}
+ }
+ }
+ FIELDS
+ end
+
+ subject { post_graphql(query, current_user: user) }
+
+ shared_examples 'a working maven package' do
+ before do
+ subject
+ end
+
+ it_behaves_like 'a working graphql query' do
+ it 'matches the JSON schema' do
+ expect(package_details).to match_schema('graphql/packages/package_details')
+ end
+ end
+
+ it 'has the correct metadata' do
+ expect(metadata_response).to include(
+ 'id' => global_id_of(maven_package.maven_metadatum),
+ 'path' => maven_package.maven_metadatum.path,
+ 'appGroup' => maven_package.maven_metadatum.app_group,
+ 'appVersion' => maven_package.maven_metadatum.app_version,
+ 'appName' => maven_package.maven_metadatum.app_name
+ )
+ end
+
+ it 'has the right amount of files' do
+ expect(package_files_response.length).to be(maven_package.package_files.length)
+ end
+
+ it 'has the basic package files data' do
+ expect(first_file_response).to include(
+ 'id' => global_id_of(first_file),
+ 'fileName' => first_file.file_name,
+ 'size' => first_file.size.to_s,
+ 'downloadPath' => first_file.download_path,
+ 'fileSha1' => first_file.file_sha1,
+ 'fileMd5' => first_file.file_md5,
+ 'fileSha256' => first_file.file_sha256
+ )
+ end
+ end
+
+ context 'a maven package with version' do
+ it_behaves_like "a working maven package"
+ end
+
+ context 'a versionless maven package' do
+ let_it_be(:maven_metadatum) { create(:maven_metadatum, app_version: nil) }
+ let_it_be(:maven_package) { create(:maven_package, project: project, version: nil, maven_metadatum: maven_metadatum) }
+
+ it_behaves_like "a working maven package"
+
+ it "has an empty version" do
+ subject
+
+ expect(metadata_response['appVersion']).to eq(nil)
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/packages/nuget_spec.rb b/spec/requests/api/graphql/packages/nuget_spec.rb
new file mode 100644
index 00000000000..fa9d8a0e37e
--- /dev/null
+++ b/spec/requests/api/graphql/packages/nuget_spec.rb
@@ -0,0 +1,74 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe 'nuget package details' do
+ include GraphqlHelpers
+
+ let_it_be(:project) { create(:project) }
+ let_it_be(:nuget_package) { create(:nuget_package, :with_metadatum, project: project) }
+
+ let(:package_global_id) { global_id_of(nuget_package) }
+ let(:metadata) { query_graphql_fragment('NugetMetadata') }
+ let(:first_file) { nuget_package.package_files.find { |f| global_id_of(f) == first_file_response['id'] } }
+
+ let(:depth) { 3 }
+ let(:excluded) { %w[metadata apiFuzzingCiConfiguration pipeline packageFiles] }
+ let(:package_files) { all_graphql_fields_for('PackageFile') }
+
+ let(:user) { project.owner }
+ let(:package_details) { graphql_data_at(:package) }
+ let(:metadata_response) { graphql_data_at(:package, :metadata) }
+ let(:package_files_response) { graphql_data_at(:package, :package_files, :nodes) }
+ let(:first_file_response) { graphql_data_at(:package, :package_files, :nodes, 0)}
+
+ let(:query) do
+ graphql_query_for(:package, { id: package_global_id }, <<~FIELDS)
+ #{all_graphql_fields_for('PackageDetailsType', max_depth: depth, excluded: excluded)}
+ metadata {
+ #{metadata}
+ }
+ packageFiles {
+ nodes {
+ #{package_files}
+ }
+ }
+ FIELDS
+ end
+
+ subject { post_graphql(query, current_user: user) }
+
+ before do
+ subject
+ end
+
+ it_behaves_like 'a working graphql query' do
+ it 'matches the JSON schema' do
+ expect(package_details).to match_schema('graphql/packages/package_details')
+ end
+ end
+
+ it 'has the correct metadata' do
+ expect(metadata_response).to include(
+ 'id' => global_id_of(nuget_package.nuget_metadatum),
+ 'licenseUrl' => nuget_package.nuget_metadatum.license_url,
+ 'projectUrl' => nuget_package.nuget_metadatum.project_url,
+ 'iconUrl' => nuget_package.nuget_metadatum.icon_url
+ )
+ end
+
+ it 'has the right amount of files' do
+ expect(package_files_response.length).to be(nuget_package.package_files.length)
+ end
+
+ it 'has the basic package files data' do
+ expect(first_file_response).to include(
+ 'id' => global_id_of(first_file),
+ 'fileName' => first_file.file_name,
+ 'size' => first_file.size.to_s,
+ 'downloadPath' => first_file.download_path,
+ 'fileSha1' => first_file.file_sha1,
+ 'fileMd5' => first_file.file_md5,
+ 'fileSha256' => first_file.file_sha256
+ )
+ end
+end
diff --git a/spec/requests/api/graphql/packages/package_spec.rb b/spec/requests/api/graphql/packages/package_spec.rb
index a0131c7733e..83ea9ff4dc8 100644
--- a/spec/requests/api/graphql/packages/package_spec.rb
+++ b/spec/requests/api/graphql/packages/package_spec.rb
@@ -17,7 +17,9 @@ RSpec.describe 'package details' do
let(:excluded) { %w[metadata apiFuzzingCiConfiguration pipeline packageFiles] }
let(:metadata) { query_graphql_fragment('ComposerMetadata') }
let(:package_files) {all_graphql_fields_for('PackageFile')}
- let(:package_files_metadata) {query_graphql_fragment('ConanFileMetadata')}
+ let(:user) { project.owner }
+ let(:package_global_id) { global_id_of(composer_package) }
+ let(:package_details) { graphql_data_at(:package) }
let(:query) do
graphql_query_for(:package, { id: package_global_id }, <<~FIELDS)
@@ -28,22 +30,11 @@ RSpec.describe 'package details' do
packageFiles {
nodes {
#{package_files}
- fileMetadata {
- #{package_files_metadata}
- }
}
}
FIELDS
end
- let(:user) { project.owner }
- let(:package_global_id) { global_id_of(composer_package) }
- let(:package_details) { graphql_data_at(:package) }
- let(:metadata_response) { graphql_data_at(:package, :metadata) }
- let(:package_files_response) { graphql_data_at(:package, :package_files, :nodes) }
- let(:first_file_response) { graphql_data_at(:package, :package_files, :nodes, 0)}
- let(:first_file_response_metadata) { graphql_data_at(:package, :package_files, :nodes, 0, :file_metadata)}
-
subject { post_graphql(query, current_user: user) }
it_behaves_like 'a working graphql query' do
@@ -56,69 +47,6 @@ RSpec.describe 'package details' do
end
end
- describe 'Packages Metadata' do
- before do
- subject
- end
-
- describe 'Composer' do
- it 'has the correct metadata' do
- expect(metadata_response).to include(
- 'targetSha' => 'foo_sha',
- 'composerJson' => composer_json.transform_keys(&:to_s).transform_values(&:to_s)
- )
- end
-
- it 'does not have files' do
- expect(package_files_response).to be_empty
- end
- end
-
- describe 'Conan' do
- let_it_be(:conan_package) { create(:conan_package, project: project) }
-
- let(:package_global_id) { global_id_of(conan_package) }
- let(:metadata) { query_graphql_fragment('ConanMetadata') }
- let(:first_file) { conan_package.package_files.find { |f| global_id_of(f) == first_file_response['id'] } }
-
- it 'has the correct metadata' do
- expect(metadata_response).to include(
- 'id' => global_id_of(conan_package.conan_metadatum),
- 'recipe' => conan_package.conan_metadatum.recipe,
- 'packageChannel' => conan_package.conan_metadatum.package_channel,
- 'packageUsername' => conan_package.conan_metadatum.package_username,
- 'recipePath' => conan_package.conan_metadatum.recipe_path
- )
- end
-
- it 'has the right amount of files' do
- expect(package_files_response.length).to be(conan_package.package_files.length)
- end
-
- it 'has the basic package files data' do
- expect(first_file_response).to include(
- 'id' => global_id_of(first_file),
- 'fileName' => first_file.file_name,
- 'size' => first_file.size.to_s,
- 'downloadPath' => first_file.download_path,
- 'fileSha1' => first_file.file_sha1,
- 'fileMd5' => first_file.file_md5,
- 'fileSha256' => first_file.file_sha256
- )
- end
-
- it 'has the correct file metadata' do
- expect(first_file_response_metadata).to include(
- 'id' => global_id_of(first_file.conan_file_metadatum),
- 'packageRevision' => first_file.conan_file_metadatum.package_revision,
- 'conanPackageReference' => first_file.conan_file_metadatum.conan_package_reference,
- 'recipeRevision' => first_file.conan_file_metadatum.recipe_revision,
- 'conanFileType' => first_file.conan_file_metadatum.conan_file_type.upcase
- )
- end
- end
- end
-
context 'there are other versions of this package' do
let(:depth) { 3 }
let(:excluded) { %w[metadata project tags pipelines] } # to limit the query complexity
diff --git a/spec/requests/api/graphql/project/issue/design_collection/versions_spec.rb b/spec/requests/api/graphql/project/issue/design_collection/versions_spec.rb
index ee0085718b3..9d98498ca8a 100644
--- a/spec/requests/api/graphql/project/issue/design_collection/versions_spec.rb
+++ b/spec/requests/api/graphql/project/issue/design_collection/versions_spec.rb
@@ -33,6 +33,7 @@ RSpec.describe 'Getting versions related to an issue' do
let(:version_params) { nil }
let(:version_query_fields) { ['edges { node { sha } }'] }
+ let(:edges_path) { %w[project issue designCollection versions edges] }
let(:project) { issue.project }
let(:current_user) { owner }
@@ -50,8 +51,7 @@ RSpec.describe 'Getting versions related to an issue' do
end
def response_values(data = graphql_data, key = 'sha')
- path = %w[project issue designCollection versions edges]
- data.dig(*path).map { |e| e.dig('node', key) }
+ data.dig(*edges_path).map { |e| e.dig('node', key) }
end
before do
@@ -64,6 +64,19 @@ RSpec.describe 'Getting versions related to an issue' do
expect(response_values).to match_array([version_a, version_b, version_c, version_d].map(&:sha))
end
+ context 'with all fields requested' do
+ let(:version_query_fields) do
+ ['edges { node { id sha createdAt author { id } } }']
+ end
+
+ it 'returns correct data' do
+ post_graphql(query, current_user: current_user)
+
+ keys = graphql_data.dig(*edges_path).first['node'].keys
+ expect(keys).to match_array(%w(id sha createdAt author))
+ end
+ end
+
describe 'filter by sha' do
let(:sha) { version_b.sha }
diff --git a/spec/requests/api/graphql/project/merge_request_spec.rb b/spec/requests/api/graphql/project/merge_request_spec.rb
index 15551005502..438ea9bb4c1 100644
--- a/spec/requests/api/graphql/project/merge_request_spec.rb
+++ b/spec/requests/api/graphql/project/merge_request_spec.rb
@@ -311,23 +311,23 @@ RSpec.describe 'getting merge request information nested in a project' do
end
end
- context 'when requesting information about MR interactions' do
+ shared_examples 'when requesting information about MR interactions' do
let_it_be(:user) { create(:user) }
let(:selected_fields) { all_graphql_fields_for('UserMergeRequestInteraction') }
let(:mr_fields) do
query_nodes(
- :reviewers,
+ field,
query_graphql_field(:merge_request_interaction, nil, selected_fields)
)
end
def interaction_data
- graphql_data_at(:project, :merge_request, :reviewers, :nodes, :merge_request_interaction)
+ graphql_data_at(:project, :merge_request, field, :nodes, :merge_request_interaction)
end
- context 'when the user does not have interactions' do
+ context 'when the user is not assigned' do
it 'returns null data' do
post_graphql(query)
@@ -338,7 +338,7 @@ RSpec.describe 'getting merge request information nested in a project' do
context 'when the user is a reviewer, but has not reviewed' do
before do
project.add_guest(user)
- merge_request.merge_request_reviewers.create!(reviewer: user)
+ assign_user(user)
end
it 'returns falsey values' do
@@ -346,8 +346,8 @@ RSpec.describe 'getting merge request information nested in a project' do
expect(interaction_data).to contain_exactly a_hash_including(
'canMerge' => false,
- 'canUpdate' => false,
- 'reviewState' => 'UNREVIEWED',
+ 'canUpdate' => can_update,
+ 'reviewState' => unreviewed,
'reviewed' => false,
'approved' => false
)
@@ -357,7 +357,9 @@ RSpec.describe 'getting merge request information nested in a project' do
context 'when the user has interacted' do
before do
project.add_maintainer(user)
- merge_request.merge_request_reviewers.create!(reviewer: user, state: 'reviewed')
+ assign_user(user)
+ r = merge_request.merge_request_reviewers.find_or_create_by!(reviewer: user)
+ r.update!(state: 'reviewed')
merge_request.approved_by_users << user
end
@@ -392,7 +394,10 @@ RSpec.describe 'getting merge request information nested in a project' do
end
it 'does not suffer from N+1' do
- merge_request.merge_request_reviewers.create!(reviewer: user, state: 'reviewed')
+ assign_user(user)
+ merge_request.merge_request_reviewers
+ .find_or_create_by!(reviewer: user)
+ .update!(state: 'reviewed')
baseline = ActiveRecord::QueryRecorder.new do
post_graphql(query)
@@ -401,7 +406,8 @@ RSpec.describe 'getting merge request information nested in a project' do
expect(interaction_data).to contain_exactly(include(reviewed))
other_users.each do |user|
- merge_request.merge_request_reviewers.create!(reviewer: user)
+ assign_user(user)
+ merge_request.merge_request_reviewers.find_or_create_by!(reviewer: user)
end
expect { post_graphql(query) }.not_to exceed_query_limit(baseline)
@@ -435,4 +441,24 @@ RSpec.describe 'getting merge request information nested in a project' do
end
end
end
+
+ it_behaves_like 'when requesting information about MR interactions' do
+ let(:field) { :reviewers }
+ let(:unreviewed) { 'UNREVIEWED' }
+ let(:can_update) { false }
+
+ def assign_user(user)
+ merge_request.merge_request_reviewers.create!(reviewer: user)
+ end
+ end
+
+ it_behaves_like 'when requesting information about MR interactions' do
+ let(:field) { :assignees }
+ let(:unreviewed) { nil }
+ let(:can_update) { true } # assignees can update MRs
+
+ def assign_user(user)
+ merge_request.assignees << user
+ end
+ end
end
diff --git a/spec/requests/api/graphql/project/packages_spec.rb b/spec/requests/api/graphql/project/packages_spec.rb
index 3c04e0caf61..d9ee997eb02 100644
--- a/spec/requests/api/graphql/project/packages_spec.rb
+++ b/spec/requests/api/graphql/project/packages_spec.rb
@@ -7,37 +7,10 @@ RSpec.describe 'getting a package list for a project' do
let_it_be(:resource) { create(:project, :repository) }
let_it_be(:current_user) { create(:user) }
+ let_it_be(:project1) { resource }
+ let_it_be(:project2) { resource }
- let_it_be(:package) { create(:package, project: resource) }
- let_it_be(:maven_package) { create(:maven_package, project: resource) }
- let_it_be(:debian_package) { create(:debian_package, project: resource) }
- let_it_be(:composer_package) { create(:composer_package, project: resource) }
- let_it_be(:composer_metadatum) do
- create(:composer_metadatum, package: composer_package,
- target_sha: 'afdeh',
- composer_json: { name: 'x', type: 'y', license: 'z', version: 1 })
- end
-
- let(:package_names) { graphql_data_at(:project, :packages, :nodes, :name) }
- let(:target_shas) { graphql_data_at(:project, :packages, :nodes, :metadata, :target_sha) }
- let(:packages) { graphql_data_at(:project, :packages, :nodes) }
-
- let(:fields) do
- <<~QUERY
- nodes {
- #{all_graphql_fields_for('packages'.classify, excluded: ['project'])}
- metadata { #{query_graphql_fragment('ComposerMetadata')} }
- }
- QUERY
- end
-
- let(:query) do
- graphql_query_for(
- 'project',
- { 'fullPath' => resource.full_path },
- query_graphql_field('packages', {}, fields)
- )
- end
+ let(:resource_type) { :project }
it_behaves_like 'group and project packages query'
end
diff --git a/spec/requests/api/graphql/project/project_members_spec.rb b/spec/requests/api/graphql/project/project_members_spec.rb
index 984a0adb8c6..c08bb8dc0a0 100644
--- a/spec/requests/api/graphql/project/project_members_spec.rb
+++ b/spec/requests/api/graphql/project/project_members_spec.rb
@@ -78,6 +78,22 @@ RSpec.describe 'getting project members information' do
.to include('path' => %w[query project projectMembers relations],
'message' => a_string_including('invalid value ([OBLIQUE])'))
end
+
+ context 'when project is owned by a member' do
+ let_it_be(:project) { create(:project, namespace: user.namespace) }
+
+ before_all do
+ project.add_guest(child_user)
+ project.add_guest(invited_user)
+ end
+
+ it 'returns the owner in the response' do
+ fetch_members(project: project)
+
+ expect(graphql_errors).to be_nil
+ expect_array_response(user, child_user, invited_user)
+ end
+ end
end
context 'when unauthenticated' do
diff --git a/spec/requests/api/graphql/project/release_spec.rb b/spec/requests/api/graphql/project/release_spec.rb
index 72197f00df4..7f24d051457 100644
--- a/spec/requests/api/graphql/project/release_spec.rb
+++ b/spec/requests/api/graphql/project/release_spec.rb
@@ -370,23 +370,6 @@ RSpec.describe 'Query.project(fullPath).release(tagName)' do
end
end
- describe 'ensures that the release data can be contolled by a feature flag' do
- context 'when the graphql_release_data feature flag is disabled' do
- let_it_be(:project) { create(:project, :repository, :public) }
- let_it_be(:release) { create(:release, project: project) }
-
- let(:current_user) { developer }
-
- before do
- stub_feature_flags(graphql_release_data: false)
-
- project.add_developer(developer)
- end
-
- it_behaves_like 'no access to the release field'
- end
- end
-
describe 'upcoming release' do
let(:path) { path_prefix }
let(:project) { create(:project, :repository, :private) }
diff --git a/spec/requests/api/graphql/project/releases_spec.rb b/spec/requests/api/graphql/project/releases_spec.rb
index 6e364c7d7b5..43732c2ed18 100644
--- a/spec/requests/api/graphql/project/releases_spec.rb
+++ b/spec/requests/api/graphql/project/releases_spec.rb
@@ -295,23 +295,6 @@ RSpec.describe 'Query.project(fullPath).releases()' do
end
end
- describe 'ensures that the release data can be contolled by a feature flag' do
- context 'when the graphql_release_data feature flag is disabled' do
- let_it_be(:project) { create(:project, :repository, :public) }
- let_it_be(:release) { create(:release, project: project) }
-
- let(:current_user) { developer }
-
- before do
- stub_feature_flags(graphql_release_data: false)
-
- project.add_developer(developer)
- end
-
- it_behaves_like 'no access to any release data'
- end
- end
-
describe 'sorting behavior' do
let_it_be(:today) { Time.now }
let_it_be(:yesterday) { today - 1.day }
diff --git a/spec/requests/api/graphql/project/repository_spec.rb b/spec/requests/api/graphql/project/repository_spec.rb
index a4984688557..bddd300e27f 100644
--- a/spec/requests/api/graphql/project/repository_spec.rb
+++ b/spec/requests/api/graphql/project/repository_spec.rb
@@ -36,6 +36,30 @@ RSpec.describe 'getting a repository in a project' do
end
end
+ context 'as a non-admin' do
+ let(:current_user) { create(:user) }
+
+ before do
+ project.add_role(current_user, :developer)
+ end
+
+ it 'does not return diskPath' do
+ post_graphql(query, current_user: current_user)
+
+ expect(graphql_data['project']['repository']).not_to be_nil
+ expect(graphql_data['project']['repository']['diskPath']).to be_nil
+ end
+ end
+
+ context 'as an admin' do
+ it 'returns diskPath' do
+ post_graphql(query, current_user: create(:admin))
+
+ expect(graphql_data['project']['repository']).not_to be_nil
+ expect(graphql_data['project']['repository']['diskPath']).to eq project.disk_path
+ end
+ end
+
context 'when the repository is only accessible to members' do
let(:project) do
create(:project, :public, :repository, repository_access_level: ProjectFeature::PRIVATE)
diff --git a/spec/requests/api/graphql/project_query_spec.rb b/spec/requests/api/graphql/project_query_spec.rb
index 2cdd7273b18..b367bbaaf43 100644
--- a/spec/requests/api/graphql/project_query_spec.rb
+++ b/spec/requests/api/graphql/project_query_spec.rb
@@ -57,6 +57,22 @@ RSpec.describe 'getting project information' do
end
end
+ context 'topics' do
+ it 'includes empty topics array if no topics set' do
+ post_graphql(query, current_user: current_user)
+
+ expect(graphql_data_at(:project, :topics)).to match([])
+ end
+
+ it 'includes topics array' do
+ project.update!(tag_list: 'topic1, topic2, topic3')
+
+ post_graphql(query, current_user: current_user)
+
+ expect(graphql_data_at(:project, :topics)).to match(%w[topic1 topic2 topic3])
+ end
+ end
+
it 'includes inherited members in project_members' do
group_member = create(:group_member, group: group)
project_member = create(:project_member, project: project)
diff --git a/spec/requests/api/graphql_spec.rb b/spec/requests/api/graphql_spec.rb
index 3a1bcfc69b8..a336d74b135 100644
--- a/spec/requests/api/graphql_spec.rb
+++ b/spec/requests/api/graphql_spec.rb
@@ -101,7 +101,7 @@ RSpec.describe 'GraphQL' do
login_as(user)
get('/')
- post '/api/graphql', params: { query: query }, headers: { 'X-CSRF-Token' => response.session['_csrf_token'] }
+ post '/api/graphql', params: { query: query }, headers: { 'X-CSRF-Token' => session['_csrf_token'] }
expect(graphql_data['echo']).to eq("\"#{user.username}\" says: Hello world")
end
@@ -283,25 +283,50 @@ RSpec.describe 'GraphQL' do
)
end
- it 'paginates datetimes correctly when they have millisecond data' do
- # let's make sure we're actually querying a timestamp, just in case
- expect(Gitlab::Graphql::Pagination::Keyset::QueryBuilder)
- .to receive(:new).with(anything, anything, hash_including('created_at'), anything).and_call_original
+ context 'when new_graphql_keyset_pagination feature flag is off' do
+ before do
+ stub_feature_flags(new_graphql_keyset_pagination: false)
+ end
+
+ it 'paginates datetimes correctly when they have millisecond data' do
+ # let's make sure we're actually querying a timestamp, just in case
+ expect(Gitlab::Graphql::Pagination::Keyset::QueryBuilder)
+ .to receive(:new).with(anything, anything, hash_including('created_at'), anything).and_call_original
+
+ execute_query
+ first_page = graphql_data
+ edges = first_page.dig(*issues_edges)
+ cursor = first_page.dig(*end_cursor)
+
+ expect(edges.count).to eq(6)
+ expect(edges.last['node']['iid']).to eq(issues[4].iid.to_s)
- execute_query
- first_page = graphql_data
- edges = first_page.dig(*issues_edges)
- cursor = first_page.dig(*end_cursor)
+ execute_query(after: cursor)
+ second_page = graphql_data
+ edges = second_page.dig(*issues_edges)
- expect(edges.count).to eq(6)
- expect(edges.last['node']['iid']).to eq(issues[4].iid.to_s)
+ expect(edges.count).to eq(4)
+ expect(edges.last['node']['iid']).to eq(issues[0].iid.to_s)
+ end
+ end
+
+ context 'when new_graphql_keyset_pagination feature flag is on' do
+ it 'paginates datetimes correctly when they have millisecond data' do
+ execute_query
+ first_page = graphql_data
+ edges = first_page.dig(*issues_edges)
+ cursor = first_page.dig(*end_cursor)
- execute_query(after: cursor)
- second_page = graphql_data
- edges = second_page.dig(*issues_edges)
+ expect(edges.count).to eq(6)
+ expect(edges.last['node']['iid']).to eq(issues[4].iid.to_s)
- expect(edges.count).to eq(4)
- expect(edges.last['node']['iid']).to eq(issues[0].iid.to_s)
+ execute_query(after: cursor)
+ second_page = graphql_data
+ edges = second_page.dig(*issues_edges)
+
+ expect(edges.count).to eq(4)
+ expect(edges.last['node']['iid']).to eq(issues[0].iid.to_s)
+ end
end
end
end
diff --git a/spec/requests/api/group_export_spec.rb b/spec/requests/api/group_export_spec.rb
index 50a1e9d0c3d..8309e2ba7c1 100644
--- a/spec/requests/api/group_export_spec.rb
+++ b/spec/requests/api/group_export_spec.rb
@@ -178,4 +178,74 @@ RSpec.describe API::GroupExport do
end
end
end
+
+ describe 'relations export' do
+ let(:path) { "/groups/#{group.id}/export_relations" }
+ let(:download_path) { "/groups/#{group.id}/export_relations/download?relation=labels" }
+ let(:status_path) { "/groups/#{group.id}/export_relations/status" }
+
+ before do
+ stub_feature_flags(group_import_export: true)
+ group.add_owner(user)
+ end
+
+ describe 'POST /groups/:id/export_relations' do
+ it 'accepts the request' do
+ post api(path, user)
+
+ expect(response).to have_gitlab_http_status(:accepted)
+ end
+
+ context 'when response is not success' do
+ it 'returns api error' do
+ allow_next_instance_of(BulkImports::ExportService) do |service|
+ allow(service).to receive(:execute).and_return(ServiceResponse.error(message: 'error', http_status: :error))
+ end
+
+ post api(path, user)
+
+ expect(response).to have_gitlab_http_status(:error)
+ end
+ end
+ end
+
+ describe 'GET /groups/:id/export_relations/download' do
+ let(:export) { create(:bulk_import_export, group: group, relation: 'labels') }
+ let(:upload) { create(:bulk_import_export_upload, export: export) }
+
+ context 'when export file exists' do
+ it 'downloads exported group archive' do
+ upload.update!(export_file: fixture_file_upload('spec/fixtures/bulk_imports/labels.ndjson.gz'))
+
+ get api(download_path, user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+
+ context 'when export_file.file does not exist' do
+ it 'returns 404' do
+ allow(upload).to receive(:export_file).and_return(nil)
+
+ get api(download_path, user)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+
+ describe 'GET /groups/:id/export_relations/status' do
+ it 'returns a list of relation export statuses' do
+ create(:bulk_import_export, :started, group: group, relation: 'labels')
+ create(:bulk_import_export, :finished, group: group, relation: 'milestones')
+ create(:bulk_import_export, :failed, group: group, relation: 'badges')
+
+ get api(status_path, user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response.pluck('relation')).to contain_exactly('labels', 'milestones', 'badges')
+ expect(json_response.pluck('status')).to contain_exactly(-1, 0, 1)
+ end
+ end
+ end
end
diff --git a/spec/requests/api/group_labels_spec.rb b/spec/requests/api/group_labels_spec.rb
index c677e68b285..900ffe6dfc7 100644
--- a/spec/requests/api/group_labels_spec.rb
+++ b/spec/requests/api/group_labels_spec.rb
@@ -290,7 +290,7 @@ RSpec.describe API::GroupLabels do
put api("/groups/#{group.id}/labels", user), params: { name: group_label1.name }
expect(response).to have_gitlab_http_status(:bad_request)
- expect(json_response['error']).to eq('new_name, color, description are missing, '\
+ expect(json_response['error']).to eq('new_name, color, description, remove_on_close are missing, '\
'at least one parameter must be provided')
end
end
@@ -337,7 +337,7 @@ RSpec.describe API::GroupLabels do
put api("/groups/#{group.id}/labels/#{valid_group_label_title_1_esc}", user)
expect(response).to have_gitlab_http_status(:bad_request)
- expect(json_response['error']).to eq('new_name, color, description are missing, '\
+ expect(json_response['error']).to eq('new_name, color, description, remove_on_close are missing, '\
'at least one parameter must be provided')
end
end
diff --git a/spec/requests/api/helpers_spec.rb b/spec/requests/api/helpers_spec.rb
index 8160a94aef2..ce0018d6d0d 100644
--- a/spec/requests/api/helpers_spec.rb
+++ b/spec/requests/api/helpers_spec.rb
@@ -39,7 +39,7 @@ RSpec.describe API::Helpers do
end
def error!(message, status, header)
- raise StandardError.new("#{status} - #{message}")
+ raise StandardError, "#{status} - #{message}"
end
def set_param(key, value)
diff --git a/spec/requests/api/internal/kubernetes_spec.rb b/spec/requests/api/internal/kubernetes_spec.rb
index 47d0c872eb6..7a2cec974b9 100644
--- a/spec/requests/api/internal/kubernetes_spec.rb
+++ b/spec/requests/api/internal/kubernetes_spec.rb
@@ -67,26 +67,26 @@ RSpec.describe API::Internal::Kubernetes do
context 'is authenticated for an agent' do
let!(:agent_token) { create(:cluster_agent_token) }
- it 'returns no_content for valid gitops_sync_count' do
- send_request(params: { gitops_sync_count: 10 })
+ it 'returns no_content for valid events' do
+ send_request(params: { gitops_sync_count: 10, k8s_api_proxy_request_count: 5 })
expect(response).to have_gitlab_http_status(:no_content)
end
- it 'returns no_content 0 gitops_sync_count' do
- send_request(params: { gitops_sync_count: 0 })
+ it 'returns no_content for counts of zero' do
+ send_request(params: { gitops_sync_count: 0, k8s_api_proxy_request_count: 0 })
expect(response).to have_gitlab_http_status(:no_content)
end
it 'returns 400 for non number' do
- send_request(params: { gitops_sync_count: 'string' })
+ send_request(params: { gitops_sync_count: 'string', k8s_api_proxy_request_count: 1 })
expect(response).to have_gitlab_http_status(:bad_request)
end
it 'returns 400 for negative number' do
- send_request(params: { gitops_sync_count: '-1' })
+ send_request(params: { gitops_sync_count: -1, k8s_api_proxy_request_count: 1 })
expect(response).to have_gitlab_http_status(:bad_request)
end
diff --git a/spec/requests/api/issues/get_project_issues_spec.rb b/spec/requests/api/issues/get_project_issues_spec.rb
index da0bae8d5e7..07fa1d40f7b 100644
--- a/spec/requests/api/issues/get_project_issues_spec.rb
+++ b/spec/requests/api/issues/get_project_issues_spec.rb
@@ -186,7 +186,7 @@ RSpec.describe API::Issues do
it 'avoids N+1 queries' do
get api("/projects/#{project.id}/issues", user)
- create_list(:issue, 3, project: project, closed_by: user)
+ issues = create_list(:issue, 3, project: project, closed_by: user)
control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do
get api("/projects/#{project.id}/issues", user)
@@ -195,6 +195,9 @@ RSpec.describe API::Issues do
milestone = create(:milestone, project: project)
create(:issue, project: project, milestone: milestone, closed_by: create(:user))
+ create(:note_on_issue, project: project, noteable: issues[0])
+ create(:note_on_issue, project: project, noteable: issues[1])
+
expect do
get api("/projects/#{project.id}/issues", user)
end.not_to exceed_all_query_limit(control_count)
diff --git a/spec/requests/api/issues/issues_spec.rb b/spec/requests/api/issues/issues_spec.rb
index 8f10de59526..125db58ed69 100644
--- a/spec/requests/api/issues/issues_spec.rb
+++ b/spec/requests/api/issues/issues_spec.rb
@@ -115,6 +115,7 @@ RSpec.describe API::Issues do
expect(response).to have_gitlab_http_status(:ok)
expect(json_response.dig('author', 'id')).to eq(issue.author.id)
expect(json_response['description']).to eq(issue.description)
+ expect(json_response['issue_type']).to eq('issue')
end
end
@@ -378,6 +379,14 @@ RSpec.describe API::Issues do
expect_paginated_array_response([issue.id, closed_issue.id])
end
+ it 'returns issues with a given issue_type' do
+ issue2 = create(:incident, project: project)
+
+ get api('/issues', user), params: { issue_type: 'incident' }
+
+ expect_paginated_array_response(issue2.id)
+ end
+
it 'returns issues matching given search string for title' do
get api('/issues', user), params: { search: issue.title }
@@ -939,7 +948,17 @@ RSpec.describe API::Issues do
end
end
- describe 'PUT /projects/:id/issues/:issue_id' do
+ describe "POST /projects/:id/issues" do
+ it 'creates a new project issue' do
+ post api("/projects/#{project.id}/issues", user), params: { title: 'new issue' }
+
+ expect(response).to have_gitlab_http_status(:created)
+ expect(json_response['title']).to eq('new issue')
+ expect(json_response['issue_type']).to eq('issue')
+ end
+ end
+
+ describe 'PUT /projects/:id/issues/:issue_iid' do
it_behaves_like 'issuable update endpoint' do
let(:entity) { issue }
end
@@ -971,6 +990,14 @@ RSpec.describe API::Issues do
expect(ResourceLabelEvent.last.created_at).to be_like_time(fixed_time)
end
end
+
+ describe 'issue_type param' do
+ it 'allows issue type to be converted' do
+ put api("/projects/#{project.id}/issues/#{issue.iid}", user), params: { issue_type: 'incident' }
+
+ expect(issue.reload.incident?).to be(true)
+ end
+ end
end
describe 'DELETE /projects/:id/issues/:issue_iid' do
diff --git a/spec/requests/api/issues/put_projects_issues_spec.rb b/spec/requests/api/issues/put_projects_issues_spec.rb
index dac721cbea0..38c080059c4 100644
--- a/spec/requests/api/issues/put_projects_issues_spec.rb
+++ b/spec/requests/api/issues/put_projects_issues_spec.rb
@@ -402,6 +402,17 @@ RSpec.describe API::Issues do
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['state']).to eq 'opened'
end
+
+ it 'removes labels marked to be removed on issue closed' do
+ removable_label = create(:label, project: project, remove_on_close: true)
+ create(:label_link, target: issue, label: removable_label)
+
+ put api_for_user, params: { state_event: 'close' }
+
+ expect(issue.reload.label_ids).not_to include(removable_label.id)
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['state']).to eq 'closed'
+ end
end
describe 'PUT /projects/:id/issues/:issue_iid to update updated_at param' do
diff --git a/spec/requests/api/labels_spec.rb b/spec/requests/api/labels_spec.rb
index 26377c40b73..f2ceedf6dbd 100644
--- a/spec/requests/api/labels_spec.rb
+++ b/spec/requests/api/labels_spec.rb
@@ -57,7 +57,7 @@ RSpec.describe API::Labels do
put_labels_api(route_type, user, spec_params)
expect(response).to have_gitlab_http_status(:bad_request)
- expect(json_response['error']).to eq('new_name, color, description, priority are missing, '\
+ expect(json_response['error']).to eq('new_name, color, description, priority, remove_on_close are missing, '\
'at least one parameter must be provided')
end
@@ -112,6 +112,14 @@ RSpec.describe API::Labels do
expect(json_response['id']).to eq(expected_response_label_id)
expect(json_response['priority']).to eq(10)
end
+
+ it "returns 200 if remove_on_close is changed (#{route_type} route)" do
+ put_labels_api(route_type, user, spec_params, remove_on_close: true)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['id']).to eq(expected_response_label_id)
+ expect(json_response['remove_on_close']).to eq(true)
+ end
end
it 'returns 200 if a priority is removed (deprecated route)' do
@@ -301,7 +309,8 @@ RSpec.describe API::Labels do
name: valid_label_title_2,
color: '#FFAABB',
description: 'test',
- priority: 2
+ priority: 2,
+ remove_on_close: true
}
expect(response).to have_gitlab_http_status(:created)
@@ -309,6 +318,7 @@ RSpec.describe API::Labels do
expect(json_response['color']).to eq('#FFAABB')
expect(json_response['description']).to eq('test')
expect(json_response['priority']).to eq(2)
+ expect(json_response['remove_on_close']).to eq(true)
end
it 'returns created label when only required params' do
diff --git a/spec/requests/api/maven_packages_spec.rb b/spec/requests/api/maven_packages_spec.rb
index 3a015e98fb1..4fc5fcf8282 100644
--- a/spec/requests/api/maven_packages_spec.rb
+++ b/spec/requests/api/maven_packages_spec.rb
@@ -49,7 +49,7 @@ RSpec.describe API::MavenPackages do
shared_examples 'rejecting the request for non existing maven path' do |expected_status: :not_found|
before do
- if Feature.enabled?(:check_maven_path_first)
+ if Feature.enabled?(:check_maven_path_first, default_enabled: :yaml)
expect(::Packages::Maven::PackageFinder).not_to receive(:new)
end
end
@@ -299,22 +299,6 @@ RSpec.describe API::MavenPackages do
end
end
- context 'with maven_packages_group_level_improvements enabled' do
- before do
- stub_feature_flags(maven_packages_group_level_improvements: true)
- end
-
- it_behaves_like 'handling all conditions'
- end
-
- context 'with maven_packages_group_level_improvements disabled' do
- before do
- stub_feature_flags(maven_packages_group_level_improvements: false)
- end
-
- it_behaves_like 'handling all conditions'
- end
-
context 'with check_maven_path_first enabled' do
before do
stub_feature_flags(check_maven_path_first: true)
@@ -346,22 +330,6 @@ RSpec.describe API::MavenPackages do
it_behaves_like 'processing HEAD requests', instance_level: true
- context 'with maven_packages_group_level_improvements enabled' do
- before do
- stub_feature_flags(maven_packages_group_level_improvements: true)
- end
-
- it_behaves_like 'processing HEAD requests', instance_level: true
- end
-
- context 'with maven_packages_group_level_improvements disabled' do
- before do
- stub_feature_flags(maven_packages_group_level_improvements: false)
- end
-
- it_behaves_like 'processing HEAD requests', instance_level: true
- end
-
context 'with check_maven_path_first enabled' do
before do
stub_feature_flags(check_maven_path_first: true)
@@ -468,8 +436,7 @@ RSpec.describe API::MavenPackages do
subject
- status = Feature.enabled?(:maven_packages_group_level_improvements, default_enabled: :yaml) ? :not_found : :forbidden
- expect(response).to have_gitlab_http_status(status)
+ expect(response).to have_gitlab_http_status(:not_found)
end
it 'denies download when no private token' do
@@ -594,22 +561,6 @@ RSpec.describe API::MavenPackages do
end
end
- context 'with maven_packages_group_level_improvements enabled' do
- before do
- stub_feature_flags(maven_packages_group_level_improvements: true)
- end
-
- it_behaves_like 'handling all conditions'
- end
-
- context 'with maven_packages_group_level_improvements disabled' do
- before do
- stub_feature_flags(maven_packages_group_level_improvements: false)
- end
-
- it_behaves_like 'handling all conditions'
- end
-
context 'with check_maven_path_first enabled' do
before do
stub_feature_flags(check_maven_path_first: true)
@@ -639,22 +590,6 @@ RSpec.describe API::MavenPackages do
let(:path) { package.maven_metadatum.path }
let(:url) { "/groups/#{group.id}/-/packages/maven/#{path}/#{package_file.file_name}" }
- context 'with maven_packages_group_level_improvements enabled' do
- before do
- stub_feature_flags(maven_packages_group_level_improvements: true)
- end
-
- it_behaves_like 'processing HEAD requests'
- end
-
- context 'with maven_packages_group_level_improvements disabled' do
- before do
- stub_feature_flags(maven_packages_group_level_improvements: false)
- end
-
- it_behaves_like 'processing HEAD requests'
- end
-
context 'with check_maven_path_first enabled' do
before do
stub_feature_flags(check_maven_path_first: true)
@@ -743,22 +678,6 @@ RSpec.describe API::MavenPackages do
end
end
- context 'with maven_packages_group_level_improvements enabled' do
- before do
- stub_feature_flags(maven_packages_group_level_improvements: true)
- end
-
- it_behaves_like 'handling all conditions'
- end
-
- context 'with maven_packages_group_level_improvements disabled' do
- before do
- stub_feature_flags(maven_packages_group_level_improvements: false)
- end
-
- it_behaves_like 'handling all conditions'
- end
-
context 'with check_maven_path_first enabled' do
before do
stub_feature_flags(check_maven_path_first: true)
@@ -789,22 +708,6 @@ RSpec.describe API::MavenPackages do
let(:path) { package.maven_metadatum.path }
let(:url) { "/projects/#{project.id}/packages/maven/#{path}/#{package_file.file_name}" }
- context 'with maven_packages_group_level_improvements enabled' do
- before do
- stub_feature_flags(maven_packages_group_level_improvements: true)
- end
-
- it_behaves_like 'processing HEAD requests'
- end
-
- context 'with maven_packages_group_level_improvements disabled' do
- before do
- stub_feature_flags(maven_packages_group_level_improvements: false)
- end
-
- it_behaves_like 'processing HEAD requests'
- end
-
context 'with check_maven_path_first enabled' do
before do
stub_feature_flags(check_maven_path_first: true)
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index 37cb8fb7ee5..a13db1bb414 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -52,7 +52,7 @@ RSpec.describe API::MergeRequests do
end
context 'when authenticated' do
- it 'avoids N+1 queries' do
+ it 'avoids N+1 queries', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/330335' do
control = ActiveRecord::QueryRecorder.new do
get api(endpoint_path, user)
end
@@ -142,7 +142,7 @@ RSpec.describe API::MergeRequests do
expect(json_response.last['labels'].first).to match_schema('/public_api/v4/label_basic')
end
- it 'avoids N+1 queries' do
+ it 'avoids N+1 queries', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/330335' do
path = endpoint_path + "?with_labels_details=true"
control = ActiveRecord::QueryRecorder.new do
@@ -973,6 +973,14 @@ RSpec.describe API::MergeRequests do
it_behaves_like 'merge requests list'
+ context 'when :api_caching_merge_requests is disabled' do
+ before do
+ stub_feature_flags(api_caching_merge_requests: false)
+ end
+
+ it_behaves_like 'merge requests list'
+ end
+
it "returns 404 for non public projects" do
project = create(:project, :private)
@@ -1049,7 +1057,7 @@ RSpec.describe API::MergeRequests do
include_context 'with merge requests'
- it 'avoids N+1 queries' do
+ it 'avoids N+1 queries', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/330335' do
control = ActiveRecord::QueryRecorder.new do
get api("/projects/#{project.id}/merge_requests", user)
end.count
@@ -2146,7 +2154,7 @@ RSpec.describe API::MergeRequests do
end
end
- describe 'PUT /projects/:id/merge_reuests/:merge_request_iid' do
+ describe 'PUT /projects/:id/merge_requests/:merge_request_iid' do
it_behaves_like 'issuable update endpoint' do
let(:entity) { merge_request }
end
@@ -2168,6 +2176,68 @@ RSpec.describe API::MergeRequests do
end
end
+ context 'when assignee_id=user2.id' do
+ let(:params) do
+ {
+ assignee_id: user2.id
+ }
+ end
+
+ it 'sets the assignees' do
+ put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user), params: params
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['assignees']).to contain_exactly(
+ a_hash_including('name' => user2.name)
+ )
+ end
+ end
+
+ context 'when only assignee_ids are provided, and the list is empty' do
+ let(:params) do
+ {
+ assignee_ids: []
+ }
+ end
+
+ it 'clears the assignees' do
+ put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user), params: params
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['assignees']).to be_empty
+ end
+ end
+
+ context 'when only assignee_ids are provided, and the list contains the sentinel value' do
+ let(:params) do
+ {
+ assignee_ids: [0]
+ }
+ end
+
+ it 'clears the assignees' do
+ put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user), params: params
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['assignees']).to be_empty
+ end
+ end
+
+ context 'when only assignee_id=0' do
+ let(:params) do
+ {
+ assignee_id: 0
+ }
+ end
+
+ it 'clears the assignees' do
+ put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user), params: params
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['assignees']).to be_empty
+ end
+ end
+
context 'accepts reviewer_ids' do
let(:params) do
{
diff --git a/spec/requests/api/package_files_spec.rb b/spec/requests/api/package_files_spec.rb
index 11170066d6e..137ded050c5 100644
--- a/spec/requests/api/package_files_spec.rb
+++ b/spec/requests/api/package_files_spec.rb
@@ -7,13 +7,13 @@ RSpec.describe API::PackageFiles do
let(:project) { create(:project, :public) }
let(:package) { create(:maven_package, project: project) }
- before do
- project.add_developer(user)
- end
-
describe 'GET /projects/:id/packages/:package_id/package_files' do
let(:url) { "/projects/#{project.id}/packages/#{package.id}/package_files" }
+ before do
+ project.add_developer(user)
+ end
+
context 'without the need for a license' do
context 'project is public' do
it 'returns 200' do
@@ -78,4 +78,77 @@ RSpec.describe API::PackageFiles do
end
end
end
+
+ describe 'DELETE /projects/:id/packages/:package_id/package_files/:package_file_id' do
+ let(:package_file_id) { package.package_files.first.id }
+ let(:url) { "/projects/#{project.id}/packages/#{package.id}/package_files/#{package_file_id}" }
+
+ subject(:api_request) { delete api(url, user) }
+
+ context 'project is public' do
+ context 'without user' do
+ let(:user) { nil }
+
+ it 'returns 403 for non authenticated user', :aggregate_failures do
+ expect { api_request }.not_to change { package.package_files.count }
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+
+ it 'returns 403 for a user without access to the project', :aggregate_failures do
+ expect { api_request }.not_to change { package.package_files.count }
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+
+ context 'project is private' do
+ let_it_be_with_refind(:project) { create(:project, :private) }
+
+ it 'returns 404 for a user without access to the project', :aggregate_failures do
+ expect { api_request }.not_to change { package.package_files.count }
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+
+ it 'returns 403 for a user without enough permissions', :aggregate_failures do
+ project.add_developer(user)
+
+ expect { api_request }.not_to change { package.package_files.count }
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+
+ it 'returns 204', :aggregate_failures do
+ project.add_maintainer(user)
+
+ expect { api_request }.to change { package.package_files.count }.by(-1)
+
+ expect(response).to have_gitlab_http_status(:no_content)
+ end
+
+ context 'without user' do
+ let(:user) { nil }
+
+ it 'returns 404 for non authenticated user', :aggregate_failures do
+ expect { api_request }.not_to change { package.package_files.count }
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'invalid file' do
+ let(:url) { "/projects/#{project.id}/packages/#{package.id}/package_files/999999" }
+
+ it 'returns 404 when the package file does not exist', :aggregate_failures do
+ project.add_maintainer(user)
+
+ expect { api_request }.not_to change { package.package_files.count }
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+ end
end
diff --git a/spec/requests/api/project_attributes.yml b/spec/requests/api/project_attributes.yml
index f9eb9de94db..d28442bd692 100644
--- a/spec/requests/api/project_attributes.yml
+++ b/spec/requests/api/project_attributes.yml
@@ -41,6 +41,7 @@ itself: # project
- reset_approvals_on_push
- runners_token_encrypted
- storage_version
+ - topic_list
- updated_at
remapped_attributes:
avatar: avatar_url
@@ -67,6 +68,7 @@ itself: # project
- readme_url
- shared_with_groups
- ssh_url_to_repo
+ - tag_list
- web_url
build_auto_devops: # auto_devops
diff --git a/spec/requests/api/project_container_repositories_spec.rb b/spec/requests/api/project_container_repositories_spec.rb
index 15871426ec5..f3da99573fe 100644
--- a/spec/requests/api/project_container_repositories_spec.rb
+++ b/spec/requests/api/project_container_repositories_spec.rb
@@ -6,12 +6,14 @@ RSpec.describe API::ProjectContainerRepositories do
include ExclusiveLeaseHelpers
let_it_be(:project) { create(:project, :private) }
+ let_it_be(:project2) { create(:project, :public) }
let_it_be(:maintainer) { create(:user) }
let_it_be(:developer) { create(:user) }
let_it_be(:reporter) { create(:user) }
let_it_be(:guest) { create(:user) }
let(:root_repository) { create(:container_repository, :root, project: project) }
let(:test_repository) { create(:container_repository, project: project) }
+ let(:root_repository2) { create(:container_repository, :root, project: project2) }
let(:users) do
{
@@ -24,315 +26,408 @@ RSpec.describe API::ProjectContainerRepositories do
end
let(:api_user) { maintainer }
+ let(:job) { create(:ci_build, :running, user: api_user, project: project) }
+ let(:job2) { create(:ci_build, :running, user: api_user, project: project2) }
- before do
+ let(:method) { :get }
+ let(:params) { {} }
+
+ before_all do
project.add_maintainer(maintainer)
project.add_developer(developer)
project.add_reporter(reporter)
project.add_guest(guest)
- stub_container_registry_config(enabled: true)
+ project2.add_maintainer(maintainer)
+ project2.add_developer(developer)
+ project2.add_reporter(reporter)
+ project2.add_guest(guest)
+ end
+ before do
root_repository
test_repository
- end
- describe 'GET /projects/:id/registry/repositories' do
- let(:url) { "/projects/#{project.id}/registry/repositories" }
-
- subject { get api(url, api_user) }
+ stub_container_registry_config(enabled: true)
+ end
- it_behaves_like 'rejected container repository access', :guest, :forbidden
- it_behaves_like 'rejected container repository access', :anonymous, :not_found
- it_behaves_like 'a package tracking event', described_class.name, 'list_repositories'
+ shared_context 'using API user' do
+ subject { public_send(method, api(url, api_user), params: params) }
+ end
- it_behaves_like 'returns repositories for allowed users', :reporter, 'project' do
- let(:object) { project }
+ shared_context 'using job token' do
+ before do
+ stub_exclusive_lease
+ stub_feature_flags(ci_job_token_scope: true)
end
+
+ subject { public_send(method, api(url), params: params.merge({ job_token: job.token })) }
end
- describe 'DELETE /projects/:id/registry/repositories/:repository_id' do
- subject { delete api("/projects/#{project.id}/registry/repositories/#{root_repository.id}", api_user) }
+ shared_context 'using job token from another project' do
+ before do
+ stub_exclusive_lease
+ stub_feature_flags(ci_job_token_scope: true)
+ end
- it_behaves_like 'rejected container repository access', :developer, :forbidden
- it_behaves_like 'rejected container repository access', :anonymous, :not_found
- it_behaves_like 'a package tracking event', described_class.name, 'delete_repository'
+ subject { public_send(method, api(url), params: { job_token: job2.token }) }
+ end
- context 'for maintainer' do
- let(:api_user) { maintainer }
+ shared_context 'using job token while ci_job_token_scope feature flag is disabled' do
+ before do
+ stub_exclusive_lease
+ stub_feature_flags(ci_job_token_scope: false)
+ end
- it 'schedules removal of repository' do
- expect(DeleteContainerRepositoryWorker).to receive(:perform_async)
- .with(maintainer.id, root_repository.id)
+ subject { public_send(method, api(url), params: params.merge({ job_token: job.token })) }
+ end
- subject
+ shared_examples 'rejected job token scopes' do
+ include_context 'using job token from another project' do
+ it_behaves_like 'rejected container repository access', :maintainer, :forbidden
+ end
- expect(response).to have_gitlab_http_status(:accepted)
- end
+ include_context 'using job token while ci_job_token_scope feature flag is disabled' do
+ it_behaves_like 'rejected container repository access', :maintainer, :forbidden
end
end
- describe 'GET /projects/:id/registry/repositories/:repository_id/tags' do
- subject { get api("/projects/#{project.id}/registry/repositories/#{root_repository.id}/tags", api_user) }
-
- it_behaves_like 'rejected container repository access', :guest, :forbidden
- it_behaves_like 'rejected container repository access', :anonymous, :not_found
-
- context 'for reporter' do
- let(:api_user) { reporter }
-
- before do
- stub_container_registry_tags(repository: root_repository.path, tags: %w(rootA latest))
- end
-
- it_behaves_like 'a package tracking event', described_class.name, 'list_tags'
-
- it 'returns a list of tags' do
- subject
+ describe 'GET /projects/:id/registry/repositories' do
+ let(:url) { "/projects/#{project.id}/registry/repositories" }
- expect(json_response.length).to eq(2)
- expect(json_response.map { |repository| repository['name'] }).to eq %w(latest rootA)
- end
+ ['using API user', 'using job token'].each do |context|
+ context context do
+ include_context context
- it 'returns a matching schema' do
- subject
+ it_behaves_like 'rejected container repository access', :guest, :forbidden unless context == 'using job token'
+ it_behaves_like 'rejected container repository access', :anonymous, :not_found
+ it_behaves_like 'a package tracking event', described_class.name, 'list_repositories'
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to match_response_schema('registry/tags')
+ it_behaves_like 'returns repositories for allowed users', :reporter, 'project' do
+ let(:object) { project }
+ end
end
end
+
+ include_examples 'rejected job token scopes'
end
- describe 'DELETE /projects/:id/registry/repositories/:repository_id/tags' do
- subject { delete api("/projects/#{project.id}/registry/repositories/#{root_repository.id}/tags", api_user), params: params }
+ describe 'DELETE /projects/:id/registry/repositories/:repository_id' do
+ let(:method) { :delete }
+ let(:url) { "/projects/#{project.id}/registry/repositories/#{root_repository.id}" }
- context 'disallowed' do
- let(:params) do
- { name_regex_delete: 'v10.*' }
- end
+ ['using API user', 'using job token'].each do |context|
+ context context do
+ include_context context
- it_behaves_like 'rejected container repository access', :developer, :forbidden
- it_behaves_like 'rejected container repository access', :anonymous, :not_found
- it_behaves_like 'a package tracking event', described_class.name, 'delete_tag_bulk'
- end
+ it_behaves_like 'rejected container repository access', :developer, :forbidden
+ it_behaves_like 'rejected container repository access', :anonymous, :not_found
+ it_behaves_like 'a package tracking event', described_class.name, 'delete_repository'
- context 'for maintainer' do
- let(:api_user) { maintainer }
+ context 'for maintainer' do
+ let(:api_user) { maintainer }
- context 'without required parameters' do
- let(:params) { }
+ it 'schedules removal of repository' do
+ expect(DeleteContainerRepositoryWorker).to receive(:perform_async)
+ .with(maintainer.id, root_repository.id)
- it 'returns bad request' do
- subject
+ subject
- expect(response).to have_gitlab_http_status(:bad_request)
+ expect(response).to have_gitlab_http_status(:accepted)
+ end
end
end
+ end
- context 'without name_regex' do
- let(:params) do
- { keep_n: 100,
- older_than: '1 day',
- other: 'some value' }
- end
-
- it 'returns bad request' do
- subject
-
- expect(response).to have_gitlab_http_status(:bad_request)
- end
- end
+ include_examples 'rejected job token scopes'
+ end
- context 'passes all declared parameters' do
- let(:params) do
- { name_regex_delete: 'v10.*',
- name_regex_keep: 'v10.1.*',
- keep_n: 100,
- older_than: '1 day',
- other: 'some value' }
- end
+ describe 'GET /projects/:id/registry/repositories/:repository_id/tags' do
+ let(:url) { "/projects/#{project.id}/registry/repositories/#{root_repository.id}/tags" }
- let(:worker_params) do
- { name_regex: nil,
- name_regex_delete: 'v10.*',
- name_regex_keep: 'v10.1.*',
- keep_n: 100,
- older_than: '1 day',
- container_expiration_policy: false }
- end
+ ['using API user', 'using job token'].each do |context|
+ context context do
+ include_context context
- let(:lease_key) { "container_repository:cleanup_tags:#{root_repository.id}" }
+ it_behaves_like 'rejected container repository access', :guest, :forbidden unless context == 'using job token'
+ it_behaves_like 'rejected container repository access', :anonymous, :not_found
- it 'schedules cleanup of tags repository' do
- stub_last_activity_update
- stub_exclusive_lease(lease_key, timeout: 1.hour)
- expect(CleanupContainerRepositoryWorker).to receive(:perform_async)
- .with(maintainer.id, root_repository.id, worker_params)
+ context 'for reporter' do
+ let(:api_user) { reporter }
- subject
+ before do
+ stub_container_registry_tags(repository: root_repository.path, tags: %w(rootA latest))
+ end
- expect(response).to have_gitlab_http_status(:accepted)
- end
+ it_behaves_like 'a package tracking event', described_class.name, 'list_tags'
- context 'called multiple times in one hour', :clean_gitlab_redis_shared_state do
- it 'returns 400 with an error message' do
- stub_exclusive_lease_taken(lease_key, timeout: 1.hour)
+ it 'returns a list of tags' do
subject
- expect(response).to have_gitlab_http_status(:bad_request)
- expect(response.body).to include('This request has already been made.')
+ expect(json_response.length).to eq(2)
+ expect(json_response.map { |repository| repository['name'] }).to eq %w(latest rootA)
end
- it 'executes service only for the first time' do
- expect(CleanupContainerRepositoryWorker).to receive(:perform_async).once
+ it 'returns a matching schema' do
+ subject
- 2.times { subject }
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to match_response_schema('registry/tags')
end
end
end
+ end
- context 'with deprecated name_regex param' do
- let(:params) do
- { name_regex: 'v10.*',
- name_regex_keep: 'v10.1.*',
- keep_n: 100,
- older_than: '1 day',
- other: 'some value' }
- end
-
- let(:worker_params) do
- { name_regex: 'v10.*',
- name_regex_delete: nil,
- name_regex_keep: 'v10.1.*',
- keep_n: 100,
- older_than: '1 day',
- container_expiration_policy: false }
- end
+ include_examples 'rejected job token scopes'
+ end
- let(:lease_key) { "container_repository:cleanup_tags:#{root_repository.id}" }
+ describe 'DELETE /projects/:id/registry/repositories/:repository_id/tags' do
+ let(:method) { :delete }
+ let(:url) { "/projects/#{project.id}/registry/repositories/#{root_repository.id}/tags" }
- it 'schedules cleanup of tags repository' do
- stub_last_activity_update
- stub_exclusive_lease(lease_key, timeout: 1.hour)
- expect(CleanupContainerRepositoryWorker).to receive(:perform_async)
- .with(maintainer.id, root_repository.id, worker_params)
+ ['using API user', 'using job token'].each do |context|
+ context context do
+ include_context context
- subject
+ context 'disallowed' do
+ let(:params) do
+ { name_regex_delete: 'v10.*' }
+ end
- expect(response).to have_gitlab_http_status(:accepted)
+ it_behaves_like 'rejected container repository access', :developer, :forbidden
+ it_behaves_like 'rejected container repository access', :anonymous, :not_found
+ it_behaves_like 'a package tracking event', described_class.name, 'delete_tag_bulk'
end
- end
- context 'with invalid regex' do
- let(:invalid_regex) { '*v10.' }
- let(:lease_key) { "container_repository:cleanup_tags:#{root_repository.id}" }
+ context 'for maintainer' do
+ let(:api_user) { maintainer }
- RSpec.shared_examples 'rejecting the invalid regex' do |param_name|
- it 'does not enqueue a job' do
- expect(CleanupContainerRepositoryWorker).not_to receive(:perform_async)
+ context 'without required parameters' do
+ it 'returns bad request' do
+ subject
- subject
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
end
- it_behaves_like 'returning response status', :bad_request
+ context 'without name_regex' do
+ let(:params) do
+ { keep_n: 100,
+ older_than: '1 day',
+ other: 'some value' }
+ end
- it 'returns an error message' do
- subject
+ it 'returns bad request' do
+ subject
- expect(json_response['error']).to include("#{param_name} is an invalid regexp")
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
end
- end
- before do
- stub_last_activity_update
- stub_exclusive_lease(lease_key, timeout: 1.hour)
- end
+ context 'passes all declared parameters' do
+ let(:params) do
+ { name_regex_delete: 'v10.*',
+ name_regex_keep: 'v10.1.*',
+ keep_n: 100,
+ older_than: '1 day',
+ other: 'some value' }
+ end
+
+ let(:worker_params) do
+ { name_regex: nil,
+ name_regex_delete: 'v10.*',
+ name_regex_keep: 'v10.1.*',
+ keep_n: 100,
+ older_than: '1 day',
+ container_expiration_policy: false }
+ end
+
+ let(:lease_key) { "container_repository:cleanup_tags:#{root_repository.id}" }
+
+ it 'schedules cleanup of tags repository' do
+ stub_last_activity_update
+ expect(CleanupContainerRepositoryWorker).to receive(:perform_async)
+ .with(maintainer.id, root_repository.id, worker_params)
+
+ subject
+
+ expect(response).to have_gitlab_http_status(:accepted)
+ end
+
+ context 'called multiple times in one hour', :clean_gitlab_redis_shared_state do
+ it 'returns 400 with an error message' do
+ stub_exclusive_lease_taken(lease_key, timeout: 1.hour)
+ subject
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(response.body).to include('This request has already been made.')
+ end
+
+ it 'executes service only for the first time' do
+ expect(CleanupContainerRepositoryWorker).to receive(:perform_async).once
+
+ 2.times { subject }
+ end
+ end
+ end
+
+ context 'with deprecated name_regex param' do
+ let(:params) do
+ { name_regex: 'v10.*',
+ name_regex_keep: 'v10.1.*',
+ keep_n: 100,
+ older_than: '1 day',
+ other: 'some value' }
+ end
+
+ let(:worker_params) do
+ { name_regex: 'v10.*',
+ name_regex_delete: nil,
+ name_regex_keep: 'v10.1.*',
+ keep_n: 100,
+ older_than: '1 day',
+ container_expiration_policy: false }
+ end
+
+ it 'schedules cleanup of tags repository' do
+ stub_last_activity_update
+ expect(CleanupContainerRepositoryWorker).to receive(:perform_async)
+ .with(maintainer.id, root_repository.id, worker_params)
+
+ subject
+
+ expect(response).to have_gitlab_http_status(:accepted)
+ end
+ end
+
+ context 'with invalid regex' do
+ let(:invalid_regex) { '*v10.' }
+
+ RSpec.shared_examples 'rejecting the invalid regex' do |param_name|
+ it 'does not enqueue a job' do
+ expect(CleanupContainerRepositoryWorker).not_to receive(:perform_async)
+
+ subject
+ end
- %i[name_regex_delete name_regex name_regex_keep].each do |param_name|
- context "for #{param_name}" do
- let(:params) { { param_name => invalid_regex } }
+ it_behaves_like 'returning response status', :bad_request
- it_behaves_like 'rejecting the invalid regex', param_name
+ it 'returns an error message' do
+ subject
+
+ expect(json_response['error']).to include("#{param_name} is an invalid regexp")
+ end
+ end
+
+ before do
+ stub_last_activity_update
+ end
+
+ %i[name_regex_delete name_regex name_regex_keep].each do |param_name|
+ context "for #{param_name}" do
+ let(:params) { { param_name => invalid_regex } }
+
+ it_behaves_like 'rejecting the invalid regex', param_name
+ end
+ end
end
end
end
end
+
+ include_examples 'rejected job token scopes'
end
describe 'GET /projects/:id/registry/repositories/:repository_id/tags/:tag_name' do
- subject { get api("/projects/#{project.id}/registry/repositories/#{root_repository.id}/tags/rootA", api_user) }
+ let(:url) { "/projects/#{project.id}/registry/repositories/#{root_repository.id}/tags/rootA" }
- it_behaves_like 'rejected container repository access', :guest, :forbidden
- it_behaves_like 'rejected container repository access', :anonymous, :not_found
+ ['using API user', 'using job token'].each do |context|
+ context context do
+ include_context context
- context 'for reporter' do
- let(:api_user) { reporter }
+ it_behaves_like 'rejected container repository access', :guest, :forbidden unless context == 'using job token'
+ it_behaves_like 'rejected container repository access', :anonymous, :not_found
- before do
- stub_container_registry_tags(repository: root_repository.path, tags: %w(rootA), with_manifest: true)
- end
+ context 'for reporter' do
+ let(:api_user) { reporter }
- it 'returns a details of tag' do
- subject
+ before do
+ stub_container_registry_tags(repository: root_repository.path, tags: %w(rootA), with_manifest: true)
+ end
- expect(json_response).to include(
- 'name' => 'rootA',
- 'digest' => 'sha256:4c8e63ca4cb663ce6c688cb06f1c372b088dac5b6d7ad7d49cd620d85cf72a15',
- 'revision' => 'd7a513a663c1a6dcdba9ed832ca53c02ac2af0c333322cd6ca92936d1d9917ac',
- 'total_size' => 2319870)
- end
+ it 'returns a details of tag' do
+ subject
+
+ expect(json_response).to include(
+ 'name' => 'rootA',
+ 'digest' => 'sha256:4c8e63ca4cb663ce6c688cb06f1c372b088dac5b6d7ad7d49cd620d85cf72a15',
+ 'revision' => 'd7a513a663c1a6dcdba9ed832ca53c02ac2af0c333322cd6ca92936d1d9917ac',
+ 'total_size' => 2319870)
+ end
- it 'returns a matching schema' do
- subject
+ it 'returns a matching schema' do
+ subject
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to match_response_schema('registry/tag')
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to match_response_schema('registry/tag')
+ end
+ end
end
end
+
+ include_examples 'rejected job token scopes'
end
describe 'DELETE /projects/:id/registry/repositories/:repository_id/tags/:tag_name' do
+ let(:method) { :delete }
+ let(:url) { "/projects/#{project.id}/registry/repositories/#{root_repository.id}/tags/rootA" }
let(:service) { double('service') }
- subject { delete api("/projects/#{project.id}/registry/repositories/#{root_repository.id}/tags/rootA", api_user) }
+ ['using API user', 'using job token'].each do |context|
+ context context do
+ include_context context
- it_behaves_like 'rejected container repository access', :reporter, :forbidden
- it_behaves_like 'rejected container repository access', :anonymous, :not_found
+ it_behaves_like 'rejected container repository access', :reporter, :forbidden
+ it_behaves_like 'rejected container repository access', :anonymous, :not_found
- context 'for developer', :snowplow do
- let(:api_user) { developer }
+ context 'for developer', :snowplow do
+ let(:api_user) { developer }
- context 'when there are multiple tags' do
- before do
- stub_container_registry_tags(repository: root_repository.path, tags: %w(rootA rootB), with_manifest: true)
- end
+ context 'when there are multiple tags' do
+ before do
+ stub_container_registry_tags(repository: root_repository.path, tags: %w(rootA rootB), with_manifest: true)
+ end
- it 'properly removes tag' do
- expect(service).to receive(:execute).with(root_repository) { { status: :success } }
- expect(Projects::ContainerRepository::DeleteTagsService).to receive(:new).with(root_repository.project, api_user, tags: %w[rootA]) { service }
+ it 'properly removes tag' do
+ expect(service).to receive(:execute).with(root_repository) { { status: :success } }
+ expect(Projects::ContainerRepository::DeleteTagsService).to receive(:new).with(root_repository.project, api_user, tags: %w[rootA]) { service }
- subject
+ subject
- expect(response).to have_gitlab_http_status(:ok)
- expect_snowplow_event(category: described_class.name, action: 'delete_tag')
- end
- end
+ expect(response).to have_gitlab_http_status(:ok)
+ expect_snowplow_event(category: described_class.name, action: 'delete_tag')
+ end
+ end
- context 'when there\'s only one tag' do
- before do
- stub_container_registry_tags(repository: root_repository.path, tags: %w(rootA), with_manifest: true)
- end
+ context 'when there\'s only one tag' do
+ before do
+ stub_container_registry_tags(repository: root_repository.path, tags: %w(rootA), with_manifest: true)
+ end
- it 'properly removes tag' do
- expect(service).to receive(:execute).with(root_repository) { { status: :success } }
- expect(Projects::ContainerRepository::DeleteTagsService).to receive(:new).with(root_repository.project, api_user, tags: %w[rootA]) { service }
+ it 'properly removes tag' do
+ expect(service).to receive(:execute).with(root_repository) { { status: :success } }
+ expect(Projects::ContainerRepository::DeleteTagsService).to receive(:new).with(root_repository.project, api_user, tags: %w[rootA]) { service }
- subject
+ subject
- expect(response).to have_gitlab_http_status(:ok)
- expect_snowplow_event(category: described_class.name, action: 'delete_tag')
+ expect(response).to have_gitlab_http_status(:ok)
+ expect_snowplow_event(category: described_class.name, action: 'delete_tag')
+ end
+ end
end
end
end
+
+ include_examples 'rejected job token scopes'
end
end
diff --git a/spec/requests/api/project_import_spec.rb b/spec/requests/api/project_import_spec.rb
index f6cdf370e5c..d3b24eb3832 100644
--- a/spec/requests/api/project_import_spec.rb
+++ b/spec/requests/api/project_import_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
RSpec.describe API::ProjectImport do
include WorkhorseHelpers
+ include AfterNextHelpers
include_context 'workhorse headers'
@@ -31,6 +32,12 @@ RSpec.describe API::ProjectImport do
allow(ImportExportUploader).to receive(:workhorse_upload_path).and_return('/')
end
+ it 'executes a limited number of queries' do
+ control_count = ActiveRecord::QueryRecorder.new { subject }.count
+
+ expect(control_count).to be <= 100
+ end
+
it 'schedules an import using a namespace' do
stub_import(namespace)
params[:namespace] = namespace.id
@@ -273,6 +280,75 @@ RSpec.describe API::ProjectImport do
end
end
+ describe 'POST /projects/remote-import' do
+ let(:params) do
+ {
+ path: 'test-import',
+ url: 'http://some.s3.url/file'
+ }
+ end
+
+ it 'returns NOT FOUND when the feature is disabled' do
+ stub_feature_flags(import_project_from_remote_file: false)
+
+ post api('/projects/remote-import', user), params: params
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+
+ context 'when the feature flag is enabled' do
+ before do
+ stub_feature_flags(import_project_from_remote_file: true)
+ end
+
+ context 'when the response is successful' do
+ it 'schedules the import successfully' do
+ project = create(
+ :project,
+ namespace: user.namespace,
+ name: 'test-import',
+ path: 'test-import'
+ )
+
+ service_response = ServiceResponse.success(payload: project)
+ expect_next(::Import::GitlabProjects::CreateProjectFromRemoteFileService)
+ .to receive(:execute)
+ .and_return(service_response)
+
+ post api('/projects/remote-import', user), params: params
+
+ expect(response).to have_gitlab_http_status(:created)
+ expect(json_response).to include({
+ 'id' => project.id,
+ 'name' => 'test-import',
+ 'name_with_namespace' => "#{user.namespace.name} / test-import",
+ 'path' => 'test-import',
+ 'path_with_namespace' => "#{user.namespace.path}/test-import"
+ })
+ end
+ end
+
+ context 'when the service returns an error' do
+ it 'fails to schedule the import' do
+ service_response = ServiceResponse.error(
+ message: 'Failed to import',
+ http_status: :bad_request
+ )
+ expect_next(::Import::GitlabProjects::CreateProjectFromRemoteFileService)
+ .to receive(:execute)
+ .and_return(service_response)
+
+ post api('/projects/remote-import', user), params: params
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response).to eq({
+ 'message' => 'Failed to import'
+ })
+ end
+ end
+ end
+ end
+
describe 'GET /projects/:id/import' do
it 'returns the import status' do
project = create(:project, :import_started)
diff --git a/spec/requests/api/project_packages_spec.rb b/spec/requests/api/project_packages_spec.rb
index 97414b3b18a..fb1aa65c08d 100644
--- a/spec/requests/api/project_packages_spec.rb
+++ b/spec/requests/api/project_packages_spec.rb
@@ -37,6 +37,16 @@ RSpec.describe API::ProjectPackages do
end
end
+ context 'with terraform module package' do
+ let_it_be(:terraform_module_package) { create(:terraform_module_package, project: project) }
+
+ it 'filters out terraform module packages when no package_type filter is set' do
+ subject
+
+ expect(json_response).not_to include(a_hash_including('package_type' => 'terraform_module'))
+ end
+ end
+
context 'project is private' do
let(:project) { create(:project, :private) }
diff --git a/spec/requests/api/project_templates_spec.rb b/spec/requests/api/project_templates_spec.rb
index a424bc62014..070fd6db3dc 100644
--- a/spec/requests/api/project_templates_spec.rb
+++ b/spec/requests/api/project_templates_spec.rb
@@ -53,15 +53,6 @@ RSpec.describe API::ProjectTemplates do
expect(json_response).to satisfy_one { |template| template['key'] == 'Android' }
end
- it 'returns gitlab_ci_syntax_ymls' do
- get api("/projects/#{public_project.id}/templates/gitlab_ci_syntax_ymls")
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to include_pagination_headers
- expect(response).to match_response_schema('public_api/v4/template_list')
- expect(json_response).to satisfy_one { |template| template['key'] == 'Artifacts example' }
- end
-
it 'returns licenses' do
get api("/projects/#{public_project.id}/templates/licenses")
@@ -172,14 +163,6 @@ RSpec.describe API::ProjectTemplates do
expect(json_response['name']).to eq('Android')
end
- it 'returns a specific gitlab_ci_syntax_yml' do
- get api("/projects/#{public_project.id}/templates/gitlab_ci_syntax_ymls/Artifacts%20example")
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to match_response_schema('public_api/v4/template')
- expect(json_response['name']).to eq('Artifacts example')
- end
-
it 'returns a specific metrics_dashboard_yml' do
get api("/projects/#{public_project.id}/templates/metrics_dashboard_ymls/Default")
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index b0ecb711283..7f804186bc7 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -223,6 +223,52 @@ RSpec.describe API::Projects do
expect(json_response.find { |hash| hash['id'] == project.id }.keys).not_to include('open_issues_count')
end
+ context 'filter by topic (column tag_list)' do
+ before do
+ project.update!(tag_list: %w(ruby javascript))
+ end
+
+ it 'returns no projects' do
+ get api('/projects', user), params: { topic: 'foo' }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_empty
+ end
+
+ it 'returns matching project for a single topic' do
+ get api('/projects', user), params: { topic: 'ruby' }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to include_pagination_headers
+ expect(json_response).to contain_exactly a_hash_including('id' => project.id)
+ end
+
+ it 'returns matching project for multiple topics' do
+ get api('/projects', user), params: { topic: 'ruby, javascript' }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to include_pagination_headers
+ expect(json_response).to contain_exactly a_hash_including('id' => project.id)
+ end
+
+ it 'returns no projects if project match only some topic' do
+ get api('/projects', user), params: { topic: 'ruby, foo' }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_empty
+ end
+
+ it 'ignores topic if it is empty' do
+ get api('/projects', user), params: { topic: '' }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_present
+ end
+ end
+
context 'and with_issues_enabled=true' do
it 'only returns projects with issues enabled' do
project.project_feature.update_attribute(:issues_access_level, ProjectFeature::DISABLED)
@@ -302,22 +348,11 @@ RSpec.describe API::Projects do
context 'and with simple=true' do
it 'returns a simplified version of all the projects' do
- expected_keys = %w(
- id description default_branch tag_list
- ssh_url_to_repo http_url_to_repo web_url readme_url
- name name_with_namespace
- path path_with_namespace
- star_count forks_count
- created_at last_activity_at
- avatar_url namespace
- )
-
get api('/projects?simple=true', user)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- expect(json_response.first.keys).to match_array expected_keys
+ expect(response).to match_response_schema('public_api/v4/projects')
end
end
@@ -1300,6 +1335,7 @@ RSpec.describe API::Projects do
describe 'GET /users/:user_id/starred_projects/' do
before do
user3.update!(starred_projects: [project, project2, project3])
+ user3.reload
end
it 'returns error when user not found' do
@@ -1588,7 +1624,6 @@ RSpec.describe API::Projects do
end
it "does not leave the temporary file in place after uploading, even when the tempfile reaper does not run" do
- stub_env('GITLAB_TEMPFILE_IMMEDIATE_UNLINK', '1')
tempfile = Tempfile.new('foo')
path = tempfile.path
@@ -1648,7 +1683,7 @@ RSpec.describe API::Projects do
let_it_be(:root_group) { create(:group, :public, name: 'root group') }
let_it_be(:project_group) { create(:group, :public, parent: root_group, name: 'project group') }
let_it_be(:shared_group_with_dev_access) { create(:group, :private, parent: root_group, name: 'shared group') }
- let_it_be(:shared_group_with_reporter_access) { create(:group, :private) }
+ let_it_be(:shared_group_with_reporter_access) { create(:group, :public) }
let_it_be(:private_project) { create(:project, :private, group: project_group) }
let_it_be(:public_project) { create(:project, :public, group: project_group) }
@@ -1730,6 +1765,14 @@ RSpec.describe API::Projects do
end
end
+ context 'when shared_visible_only is on' do
+ let(:params) { super().merge(shared_visible_only: true) }
+
+ it_behaves_like 'successful groups response' do
+ let(:expected_groups) { [root_group, project_group, shared_group_with_reporter_access] }
+ end
+ end
+
context 'when search by shared group name' do
let(:params) { super().merge(search: 'shared') }
diff --git a/spec/requests/api/releases_spec.rb b/spec/requests/api/releases_spec.rb
index 70de2e5330b..81ddcd7cf84 100644
--- a/spec/requests/api/releases_spec.rb
+++ b/spec/requests/api/releases_spec.rb
@@ -18,7 +18,7 @@ RSpec.describe API::Releases do
project.add_developer(developer)
end
- describe 'GET /projects/:id/releases' do
+ describe 'GET /projects/:id/releases', :use_clean_rails_redis_caching do
context 'when there are two releases' do
let!(:release_1) do
create(:release,
@@ -129,19 +129,60 @@ RSpec.describe API::Releases do
expect(json_response.first['upcoming_release']).to eq(false)
end
- it 'avoids N+1 queries' do
+ it 'avoids N+1 queries', :use_sql_query_cache do
create(:release, :with_evidence, project: project, tag: 'v0.1', author: maintainer)
+ create(:release_link, release: project.releases.first)
- control_count = ActiveRecord::QueryRecorder.new do
+ control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do
get api("/projects/#{project.id}/releases", maintainer)
end.count
- create(:release, :with_evidence, project: project, tag: 'v0.1', author: maintainer)
- create(:release, :with_evidence, project: project, tag: 'v0.1', author: maintainer)
+ create_list(:release, 2, :with_evidence, project: project, tag: 'v0.1', author: maintainer)
+ create_list(:release, 2, project: project)
+ create_list(:release_link, 2, release: project.releases.first)
+ create_list(:release_link, 2, release: project.releases.last)
expect do
get api("/projects/#{project.id}/releases", maintainer)
- end.not_to exceed_query_limit(control_count)
+ end.not_to exceed_all_query_limit(control_count)
+ end
+
+ it 'serializes releases for the first time and read cached data from the second time' do
+ create_list(:release, 2, project: project)
+
+ expect(API::Entities::Release)
+ .to receive(:represent).with(instance_of(Release), any_args)
+ .twice
+
+ 5.times { get api("/projects/#{project.id}/releases", maintainer) }
+ end
+
+ it 'increments the cache key when link is updated' do
+ releases = create_list(:release, 2, project: project)
+
+ expect(API::Entities::Release)
+ .to receive(:represent).with(instance_of(Release), any_args)
+ .exactly(4).times
+
+ 2.times { get api("/projects/#{project.id}/releases", maintainer) }
+
+ releases.each { |release| create(:release_link, release: release) }
+
+ 3.times { get api("/projects/#{project.id}/releases", maintainer) }
+ end
+
+ it 'increments the cache key when evidence is updated' do
+ releases = create_list(:release, 2, project: project)
+
+ expect(API::Entities::Release)
+ .to receive(:represent).with(instance_of(Release), any_args)
+ .exactly(4).times
+
+ 2.times { get api("/projects/#{project.id}/releases", maintainer) }
+
+ releases.each { |release| create(:evidence, release: release) }
+
+ 3.times { get api("/projects/#{project.id}/releases", maintainer) }
end
context 'when tag does not exist in git repository' do
@@ -227,6 +268,20 @@ RSpec.describe API::Releases do
end
end
end
+
+ context 'when releases are public and request user is absent' do
+ let(:project) { create(:project, :repository, :public) }
+
+ it 'returns the releases' do
+ create(:release, project: project, tag: 'v0.1')
+
+ get api("/projects/#{project.id}/releases")
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response.count).to eq(1)
+ expect(json_response.first['tag_name']).to eq('v0.1')
+ end
+ end
end
describe 'GET /projects/:id/releases/:tag_name' do
@@ -1133,8 +1188,33 @@ RSpec.describe API::Releases do
end
end
+ describe 'Track API events', :snowplow do
+ context 'when tracking event with labels from User-Agent' do
+ it 'adds the tracked User-Agent to the label of the tracked event' do
+ get api("/projects/#{project.id}/releases", maintainer), headers: { 'User-Agent' => described_class::RELEASE_CLI_USER_AGENT }
+
+ assert_snowplow_event('get_releases', true)
+ end
+
+ it 'skips label when User-Agent is invalid' do
+ get api("/projects/#{project.id}/releases", maintainer), headers: { 'User-Agent' => 'invalid_user_agent' }
+ assert_snowplow_event('get_releases', false)
+ end
+ end
+ end
+
def initialize_tags
project.repository.add_tag(maintainer, 'v0.1', commit.id)
project.repository.add_tag(maintainer, 'v0.2', commit.id)
end
+
+ def assert_snowplow_event(action, release_cli, user = maintainer)
+ expect_snowplow_event(
+ category: described_class.name,
+ action: action,
+ project: project,
+ user: user,
+ release_cli: release_cli
+ )
+ end
end
diff --git a/spec/requests/api/services_spec.rb b/spec/requests/api/services_spec.rb
index 2157e69e7bf..1f859622760 100644
--- a/spec/requests/api/services_spec.rb
+++ b/spec/requests/api/services_spec.rb
@@ -42,7 +42,7 @@ RSpec.describe API::Services do
end
end
- Service.available_services_names.each do |service|
+ Integration.available_services_names.each do |service|
describe "PUT /projects/:id/services/#{service.dasherize}" do
include_context service
@@ -51,7 +51,7 @@ RSpec.describe API::Services do
expect(response).to have_gitlab_http_status(:ok)
- current_service = project.services.first
+ current_service = project.integrations.first
events = current_service.event_names.empty? ? ["foo"].freeze : current_service.event_names
query_strings = []
events.each do |event|
@@ -66,7 +66,7 @@ RSpec.describe API::Services do
events.each do |event|
next if event == "foo"
- expect(project.services.first[event]).not_to eq(current_service[event]),
+ expect(project.integrations.first[event]).not_to eq(current_service[event]),
"expected #{!current_service[event]} for event #{event} for service #{current_service.title}, got #{current_service[event]}"
end
end
@@ -114,21 +114,61 @@ RSpec.describe API::Services do
describe "GET /projects/:id/services/#{service.dasherize}" do
include_context service
- # inject some properties into the service
- let!(:initialized_service) { initialize_service(service) }
+ let!(:initialized_service) { initialize_service(service, active: true) }
+
+ let_it_be(:project2) do
+ create(:project, creator_id: user.id, namespace: user.namespace)
+ end
+
+ def deactive_service!
+ return initialized_service.update!(active: false) unless initialized_service.is_a?(PrometheusService)
+
+ # PrometheusService sets `#active` itself within a `before_save`:
+ initialized_service.manual_configuration = false
+ initialized_service.save!
+ end
it 'returns authentication error when unauthenticated' do
get api("/projects/#{project.id}/services/#{dashed_service}")
expect(response).to have_gitlab_http_status(:unauthorized)
end
- it "returns all properties of service #{service}" do
+ it "returns all properties of active service #{service}" do
get api("/projects/#{project.id}/services/#{dashed_service}", user)
+ expect(initialized_service).to be_active
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['properties'].keys).to match_array(service_instance.api_field_names)
end
+ it "returns all properties of inactive service #{service}" do
+ deactive_service!
+
+ get api("/projects/#{project.id}/services/#{dashed_service}", user)
+
+ expect(initialized_service).not_to be_active
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['properties'].keys).to match_array(service_instance.api_field_names)
+ end
+
+ it "returns not found if service does not exist" do
+ get api("/projects/#{project2.id}/services/#{dashed_service}", user)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ expect(json_response['message']).to eq('404 Service Not Found')
+ end
+
+ it "returns not found if service exists but is in `Project#disabled_services`" do
+ expect_next_found_instance_of(Project) do |project|
+ expect(project).to receive(:disabled_services).at_least(:once).and_return([service])
+ end
+
+ get api("/projects/#{project.id}/services/#{dashed_service}", user)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ expect(json_response['message']).to eq('404 Service Not Found')
+ end
+
it "returns error when authenticated but not a project owner" do
project.add_developer(user2)
get api("/projects/#{project.id}/services/#{dashed_service}", user2)
diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb
index 48f5bd114a1..66c0dcaa36c 100644
--- a/spec/requests/api/settings_spec.rb
+++ b/spec/requests/api/settings_spec.rb
@@ -41,10 +41,12 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting do
expect(json_response['snippet_size_limit']).to eq(50.megabytes)
expect(json_response['spam_check_endpoint_enabled']).to be_falsey
expect(json_response['spam_check_endpoint_url']).to be_nil
+ expect(json_response['spam_check_api_key']).to be_nil
expect(json_response['wiki_page_max_content_bytes']).to be_a(Integer)
expect(json_response['require_admin_approval_after_user_signup']).to eq(true)
expect(json_response['personal_access_token_prefix']).to be_nil
expect(json_response['admin_mode']).to be(false)
+ expect(json_response['whats_new_variant']).to eq('all_tiers')
end
end
@@ -121,7 +123,8 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting do
issues_create_limit: 300,
raw_blob_request_limit: 300,
spam_check_endpoint_enabled: true,
- spam_check_endpoint_url: 'https://example.com/spam_check',
+ spam_check_endpoint_url: 'grpc://example.com/spam_check',
+ spam_check_api_key: 'SPAM_CHECK_API_KEY',
disabled_oauth_sign_in_sources: 'unknown',
import_sources: 'github,bitbucket',
wiki_page_max_content_bytes: 12345,
@@ -166,7 +169,8 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting do
expect(json_response['issues_create_limit']).to eq(300)
expect(json_response['raw_blob_request_limit']).to eq(300)
expect(json_response['spam_check_endpoint_enabled']).to be_truthy
- expect(json_response['spam_check_endpoint_url']).to eq('https://example.com/spam_check')
+ expect(json_response['spam_check_endpoint_url']).to eq('grpc://example.com/spam_check')
+ expect(json_response['spam_check_api_key']).to eq('SPAM_CHECK_API_KEY')
expect(json_response['disabled_oauth_sign_in_sources']).to eq([])
expect(json_response['import_sources']).to match_array(%w(github bitbucket))
expect(json_response['wiki_page_max_content_bytes']).to eq(12345)
@@ -459,13 +463,32 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting do
context "missing spam_check_endpoint_url value when spam_check_endpoint_enabled is true" do
it "returns a blank parameter error message" do
- put api("/application/settings", admin), params: { spam_check_endpoint_enabled: true }
+ put api("/application/settings", admin), params: { spam_check_endpoint_enabled: true, spam_check_api_key: "SPAM_CHECK_API_KEY" }
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['error']).to eq('spam_check_endpoint_url is missing')
end
end
+ context "missing spam_check_api_key value when spam_check_endpoint_enabled is true" do
+ it "returns a blank parameter error message" do
+ put api("/application/settings", admin), params: { spam_check_endpoint_enabled: true, spam_check_endpoint_url: "https://example.com/spam_check" }
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['error']).to eq('spam_check_api_key is missing')
+ end
+ end
+
+ context "overly long spam_check_api_key" do
+ it "fails to update the settings with too long spam_check_api_key" do
+ put api("/application/settings", admin), params: { spam_check_api_key: "0123456789" * 500 }
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ message = json_response["message"]
+ expect(message["spam_check_api_key"]).to include(a_string_matching("is too long"))
+ end
+ end
+
context "personal access token prefix settings" do
context "handles validation errors" do
it "fails to update the settings with too long prefix" do
@@ -485,5 +508,32 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting do
end
end
end
+
+ context 'whats_new_variant setting' do
+ before do
+ Gitlab::CurrentSettings.current_application_settings.whats_new_variant_disabled!
+ end
+
+ it 'updates setting' do
+ new_value = 'all_tiers'
+ put api("/application/settings", admin),
+ params: {
+ whats_new_variant: new_value
+ }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['whats_new_variant']).to eq(new_value)
+ end
+
+ it 'fails to update setting with invalid value' do
+ put api("/application/settings", admin),
+ params: {
+ whats_new_variant: 'invalid_value'
+ }
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['error']).to eq('whats_new_variant does not have a valid value')
+ end
+ end
end
end
diff --git a/spec/requests/api/terraform/modules/v1/packages_spec.rb b/spec/requests/api/terraform/modules/v1/packages_spec.rb
new file mode 100644
index 00000000000..d318b22cf27
--- /dev/null
+++ b/spec/requests/api/terraform/modules/v1/packages_spec.rb
@@ -0,0 +1,360 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe API::Terraform::Modules::V1::Packages do
+ include PackagesManagerApiSpecHelpers
+ include WorkhorseHelpers
+ using RSpec::Parameterized::TableSyntax
+
+ let_it_be_with_reload(:group) { create(:group) }
+ let_it_be_with_reload(:project) { create(:project, namespace: group) }
+ let_it_be(:package) { create(:terraform_module_package, project: project) }
+ let_it_be(:personal_access_token) { create(:personal_access_token) }
+ let_it_be(:user) { personal_access_token.user }
+ let_it_be(:job) { create(:ci_build, :running, user: user) }
+ let_it_be(:deploy_token) { create(:deploy_token, read_package_registry: true, write_package_registry: true) }
+ let_it_be(:project_deploy_token) { create(:project_deploy_token, deploy_token: deploy_token, project: project) }
+
+ let(:headers) { {} }
+
+ let(:tokens) do
+ {
+ personal_access_token: personal_access_token.token,
+ deploy_token: deploy_token.token,
+ job_token: job.token
+ }
+ end
+
+ describe 'GET /api/v4/packages/terraform/modules/v1/:module_namespace/:module_name/:module_system/versions' do
+ let(:url) { api("/packages/terraform/modules/v1/#{group.path}/#{package.name}/versions") }
+ let(:headers) { {} }
+
+ subject { get(url, headers: headers) }
+
+ context 'with valid namespace' do
+ where(:visibility, :user_role, :member, :token_type, :valid_token, :shared_examples_name, :expected_status) do
+ :public | :developer | true | :personal_access_token | true | 'returns terraform module packages' | :success
+ :public | :guest | true | :personal_access_token | true | 'returns terraform module packages' | :success
+ :public | :developer | true | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
+ :public | :guest | true | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
+ :public | :developer | false | :personal_access_token | true | 'returns no terraform module packages' | :success
+ :public | :guest | false | :personal_access_token | true | 'returns no terraform module packages' | :success
+ :public | :developer | false | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
+ :public | :guest | false | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
+ :public | :anonymous | false | :personal_access_token | true | 'returns no terraform module packages' | :success
+ :private | :developer | true | :personal_access_token | true | 'returns terraform module packages' | :success
+ :private | :guest | true | :personal_access_token | true | 'rejects terraform module packages access' | :forbidden
+ :private | :developer | true | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
+ :private | :guest | true | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
+ :private | :developer | false | :personal_access_token | true | 'rejects terraform module packages access' | :forbidden
+ :private | :guest | false | :personal_access_token | true | 'rejects terraform module packages access' | :forbidden
+ :private | :developer | false | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
+ :private | :guest | false | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
+ :private | :anonymous | false | :personal_access_token | true | 'rejects terraform module packages access' | :unauthorized
+ :public | :developer | true | :job_token | true | 'returns terraform module packages' | :success
+ :public | :guest | true | :job_token | true | 'returns no terraform module packages' | :success
+ :public | :guest | true | :job_token | false | 'rejects terraform module packages access' | :unauthorized
+ :public | :developer | false | :job_token | true | 'returns no terraform module packages' | :success
+ :public | :guest | false | :job_token | true | 'returns no terraform module packages' | :success
+ :public | :developer | false | :job_token | false | 'rejects terraform module packages access' | :unauthorized
+ :public | :guest | false | :job_token | false | 'rejects terraform module packages access' | :unauthorized
+ :private | :developer | true | :job_token | true | 'returns terraform module packages' | :success
+ :private | :guest | true | :job_token | true | 'rejects terraform module packages access' | :forbidden
+ :private | :developer | true | :job_token | false | 'rejects terraform module packages access' | :unauthorized
+ :private | :guest | true | :job_token | false | 'rejects terraform module packages access' | :unauthorized
+ :private | :developer | false | :job_token | true | 'rejects terraform module packages access' | :forbidden
+ :private | :guest | false | :job_token | true | 'rejects terraform module packages access' | :forbidden
+ :private | :developer | false | :job_token | false | 'rejects terraform module packages access' | :unauthorized
+ :private | :guest | false | :job_token | false | 'rejects terraform module packages access' | :unauthorized
+ end
+
+ with_them do
+ let(:token) { valid_token ? tokens[token_type] : 'invalid-token123' }
+ let(:headers) { user_role == :anonymous ? {} : { 'Authorization' => "Bearer #{token}" } }
+
+ before do
+ group.update!(visibility: visibility.to_s)
+ end
+
+ it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member]
+ end
+ end
+ end
+
+ describe 'GET /api/v4/packages/terraform/modules/v1/:module_namespace/:module_name/:module_system/:module_version/download' do
+ let(:url) { api("/packages/terraform/modules/v1/#{group.path}/#{package.name}/#{package.version}/download") }
+ let(:headers) { {} }
+
+ subject { get(url, headers: headers) }
+
+ context 'with valid namespace' do
+ where(:visibility, :user_role, :member, :token_type, :valid_token, :shared_examples_name, :expected_status) do
+ :public | :developer | true | :personal_access_token | true | 'grants terraform module download' | :success
+ :public | :guest | true | :personal_access_token | true | 'rejects terraform module packages access' | :not_found
+ :public | :developer | true | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
+ :public | :guest | true | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
+ :public | :developer | false | :personal_access_token | true | 'rejects terraform module packages access' | :not_found
+ :public | :guest | false | :personal_access_token | true | 'rejects terraform module packages access' | :not_found
+ :public | :developer | false | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
+ :public | :guest | false | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
+ :public | :anonymous | false | :personal_access_token | true | 'rejects terraform module packages access' | :not_found
+ :private | :developer | true | :personal_access_token | true | 'grants terraform module download' | :success
+ :private | :guest | true | :personal_access_token | true | 'rejects terraform module packages access' | :forbidden
+ :private | :developer | true | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
+ :private | :guest | true | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
+ :private | :developer | false | :personal_access_token | true | 'rejects terraform module packages access' | :forbidden
+ :private | :guest | false | :personal_access_token | true | 'rejects terraform module packages access' | :forbidden
+ :private | :developer | false | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
+ :private | :guest | false | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
+ :private | :anonymous | false | :personal_access_token | true | 'rejects terraform module packages access' | :unauthorized
+ :public | :developer | true | :job_token | true | 'grants terraform module download' | :success
+ :public | :guest | true | :job_token | true | 'rejects terraform module packages access' | :not_found
+ :public | :guest | true | :job_token | false | 'rejects terraform module packages access' | :unauthorized
+ :public | :developer | false | :job_token | true | 'rejects terraform module packages access' | :not_found
+ :public | :guest | false | :job_token | true | 'rejects terraform module packages access' | :not_found
+ :public | :developer | false | :job_token | false | 'rejects terraform module packages access' | :unauthorized
+ :public | :guest | false | :job_token | false | 'rejects terraform module packages access' | :unauthorized
+ :private | :developer | true | :job_token | true | 'grants terraform module download' | :success
+ :private | :guest | true | :job_token | true | 'rejects terraform module packages access' | :forbidden
+ :private | :developer | true | :job_token | false | 'rejects terraform module packages access' | :unauthorized
+ :private | :guest | true | :job_token | false | 'rejects terraform module packages access' | :unauthorized
+ :private | :developer | false | :job_token | true | 'rejects terraform module packages access' | :forbidden
+ :private | :guest | false | :job_token | true | 'rejects terraform module packages access' | :forbidden
+ :private | :developer | false | :job_token | false | 'rejects terraform module packages access' | :unauthorized
+ :private | :guest | false | :job_token | false | 'rejects terraform module packages access' | :unauthorized
+ end
+
+ with_them do
+ let(:token) { valid_token ? tokens[token_type] : 'invalid-token123' }
+ let(:headers) { user_role == :anonymous ? {} : { 'Authorization' => "Bearer #{token}" } }
+
+ before do
+ group.update!(visibility: visibility.to_s)
+ end
+
+ it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member]
+ end
+ end
+ end
+
+ describe 'GET /api/v4/packages/terraform/modules/v1/:module_namespace/:module_name/:module_system/:module_version/file' do
+ let(:tokens) do
+ {
+ personal_access_token: ::Gitlab::JWTToken.new.tap { |jwt| jwt['token'] = personal_access_token.id }.encoded,
+ job_token: ::Gitlab::JWTToken.new.tap { |jwt| jwt['token'] = job.token }.encoded
+ }
+ end
+
+ subject { get(url, headers: headers) }
+
+ context 'with valid namespace' do
+ where(:visibility, :user_role, :member, :token_type, :valid_token, :shared_examples_name, :expected_status) do
+ :public | :developer | true | :personal_access_token | true | 'grants terraform module package file access' | :success
+ :public | :guest | true | :personal_access_token | true | 'rejects terraform module packages access' | :not_found
+ :public | :developer | true | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
+ :public | :guest | true | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
+ :public | :developer | false | :personal_access_token | true | 'rejects terraform module packages access' | :not_found
+ :public | :guest | false | :personal_access_token | true | 'rejects terraform module packages access' | :not_found
+ :public | :developer | false | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
+ :public | :guest | false | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
+ :public | :anonymous | false | :personal_access_token | true | 'rejects terraform module packages access' | :not_found
+ :private | :developer | true | :personal_access_token | true | 'grants terraform module package file access' | :success
+ :private | :guest | true | :personal_access_token | true | 'rejects terraform module packages access' | :forbidden
+ :private | :developer | true | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
+ :private | :guest | true | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
+ :private | :developer | false | :personal_access_token | true | 'rejects terraform module packages access' | :forbidden
+ :private | :guest | false | :personal_access_token | true | 'rejects terraform module packages access' | :forbidden
+ :private | :developer | false | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
+ :private | :guest | false | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
+ :private | :anonymous | false | :personal_access_token | true | 'rejects terraform module packages access' | :forbidden
+ :public | :developer | true | :job_token | true | 'grants terraform module package file access' | :success
+ :public | :guest | true | :job_token | true | 'rejects terraform module packages access' | :not_found
+ :public | :guest | true | :job_token | false | 'rejects terraform module packages access' | :unauthorized
+ :public | :developer | false | :job_token | true | 'rejects terraform module packages access' | :not_found
+ :public | :guest | false | :job_token | true | 'rejects terraform module packages access' | :not_found
+ :public | :developer | false | :job_token | false | 'rejects terraform module packages access' | :unauthorized
+ :public | :guest | false | :job_token | false | 'rejects terraform module packages access' | :unauthorized
+ :private | :developer | true | :job_token | true | 'grants terraform module package file access' | :success
+ :private | :guest | true | :job_token | true | 'rejects terraform module packages access' | :forbidden
+ :private | :developer | true | :job_token | false | 'rejects terraform module packages access' | :unauthorized
+ :private | :guest | true | :job_token | false | 'rejects terraform module packages access' | :unauthorized
+ :private | :developer | false | :job_token | true | 'rejects terraform module packages access' | :forbidden
+ :private | :guest | false | :job_token | true | 'rejects terraform module packages access' | :forbidden
+ :private | :developer | false | :job_token | false | 'rejects terraform module packages access' | :unauthorized
+ :private | :guest | false | :job_token | false | 'rejects terraform module packages access' | :unauthorized
+ end
+
+ with_them do
+ let(:token) { valid_token ? tokens[token_type] : 'invalid-token123' }
+ let(:url) { api("/packages/terraform/modules/v1/#{group.path}/#{package.name}/#{package.version}/file?token=#{token}") }
+
+ before do
+ group.update!(visibility: visibility.to_s)
+ end
+
+ it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member]
+ end
+ end
+ end
+
+ describe 'PUT /api/v4/projects/:project_id/packages/terraform/modules/:module_name/:module_system/:module_version/file/authorize' do
+ include_context 'workhorse headers'
+
+ let(:url) { api("/projects/#{project.id}/packages/terraform/modules/mymodule/mysystem/1.0.0/file/authorize") }
+ let(:headers) { {} }
+
+ subject { put(url, headers: headers) }
+
+ context 'with valid project' do
+ where(:visibility, :user_role, :member, :token_header, :token_type, :valid_token, :shared_examples_name, :expected_status) do
+ :public | :developer | true | 'PRIVATE-TOKEN' | :personal_access_token | true | 'process terraform module workhorse authorization' | :success
+ :public | :guest | true | 'PRIVATE-TOKEN' | :personal_access_token | true | 'rejects terraform module packages access' | :forbidden
+ :public | :developer | true | 'PRIVATE-TOKEN' | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
+ :public | :guest | true | 'PRIVATE-TOKEN' | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
+ :public | :developer | false | 'PRIVATE-TOKEN' | :personal_access_token | true | 'rejects terraform module packages access' | :forbidden
+ :public | :guest | false | 'PRIVATE-TOKEN' | :personal_access_token | true | 'rejects terraform module packages access' | :forbidden
+ :public | :developer | false | 'PRIVATE-TOKEN' | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
+ :public | :guest | false | 'PRIVATE-TOKEN' | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
+ :public | :anonymous | false | 'PRIVATE-TOKEN' | :personal_access_token | true | 'rejects terraform module packages access' | :unauthorized
+ :private | :developer | true | 'PRIVATE-TOKEN' | :personal_access_token | true | 'process terraform module workhorse authorization' | :success
+ :private | :guest | true | 'PRIVATE-TOKEN' | :personal_access_token | true | 'rejects terraform module packages access' | :forbidden
+ :private | :developer | true | 'PRIVATE-TOKEN' | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
+ :private | :guest | true | 'PRIVATE-TOKEN' | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
+ :private | :developer | false | 'PRIVATE-TOKEN' | :personal_access_token | true | 'rejects terraform module packages access' | :not_found
+ :private | :guest | false | 'PRIVATE-TOKEN' | :personal_access_token | true | 'rejects terraform module packages access' | :not_found
+ :private | :developer | false | 'PRIVATE-TOKEN' | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
+ :private | :guest | false | 'PRIVATE-TOKEN' | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
+ :private | :anonymous | false | 'PRIVATE-TOKEN' | :personal_access_token | true | 'rejects terraform module packages access' | :unauthorized
+ :public | :developer | true | 'JOB-TOKEN' | :job_token | true | 'process terraform module workhorse authorization' | :success
+ :public | :guest | true | 'JOB-TOKEN' | :job_token | true | 'rejects terraform module packages access' | :forbidden
+ :public | :developer | true | 'JOB-TOKEN' | :job_token | false | 'rejects terraform module packages access' | :unauthorized
+ :public | :guest | true | 'JOB-TOKEN' | :job_token | false | 'rejects terraform module packages access' | :unauthorized
+ :public | :developer | false | 'JOB-TOKEN' | :job_token | true | 'rejects terraform module packages access' | :forbidden
+ :public | :guest | false | 'JOB-TOKEN' | :job_token | true | 'rejects terraform module packages access' | :forbidden
+ :public | :developer | false | 'JOB-TOKEN' | :job_token | false | 'rejects terraform module packages access' | :unauthorized
+ :public | :guest | false | 'JOB-TOKEN' | :job_token | false | 'rejects terraform module packages access' | :unauthorized
+ :private | :developer | true | 'JOB-TOKEN' | :job_token | true | 'process terraform module workhorse authorization' | :success
+ :private | :guest | true | 'JOB-TOKEN' | :job_token | true | 'rejects terraform module packages access' | :forbidden
+ :private | :developer | true | 'JOB-TOKEN' | :job_token | false | 'rejects terraform module packages access' | :unauthorized
+ :private | :guest | true | 'JOB-TOKEN' | :job_token | false | 'rejects terraform module packages access' | :unauthorized
+ :private | :developer | false | 'JOB-TOKEN' | :job_token | true | 'rejects terraform module packages access' | :not_found
+ :private | :guest | false | 'JOB-TOKEN' | :job_token | true | 'rejects terraform module packages access' | :not_found
+ :private | :developer | false | 'JOB-TOKEN' | :job_token | false | 'rejects terraform module packages access' | :unauthorized
+ :private | :guest | false | 'JOB-TOKEN' | :job_token | false | 'rejects terraform module packages access' | :unauthorized
+ :public | :developer | true | 'DEPLOY-TOKEN' | :deploy_token | true | 'process terraform module workhorse authorization' | :success
+ :public | :developer | true | 'DEPLOY-TOKEN' | :deploy_token | false | 'rejects terraform module packages access' | :unauthorized
+ :private | :developer | true | 'DEPLOY-TOKEN' | :deploy_token | true | 'process terraform module workhorse authorization' | :success
+ :private | :developer | true | 'DEPLOY-TOKEN' | :deploy_token | false | 'rejects terraform module packages access' | :unauthorized
+ end
+
+ with_them do
+ let(:token) { valid_token ? tokens[token_type] : 'invalid-token123' }
+ let(:headers) { user_headers.merge(workhorse_headers) }
+ let(:user_headers) { user_role == :anonymous ? {} : { token_header => token } }
+
+ before do
+ project.update!(visibility: visibility.to_s)
+ end
+
+ it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member]
+ end
+ end
+ end
+
+ describe 'PUT /api/v4/projects/:project_id/packages/terraform/modules/:module_name/:module_system/:module_version/file' do
+ include_context 'workhorse headers'
+
+ let_it_be(:file_name) { 'module-system-v1.0.0.tgz' }
+
+ let(:url) { "/projects/#{project.id}/packages/terraform/modules/mymodule/mysystem/1.0.0/file" }
+ let(:headers) { {} }
+ let(:params) { { file: temp_file(file_name) } }
+ let(:file_key) { :file }
+ let(:send_rewritten_field) { true }
+
+ subject do
+ workhorse_finalize(
+ api(url),
+ method: :put,
+ file_key: file_key,
+ params: params,
+ headers: headers,
+ send_rewritten_field: send_rewritten_field
+ )
+ end
+
+ context 'with valid project' do
+ where(:visibility, :user_role, :member, :token_header, :token_type, :valid_token, :shared_examples_name, :expected_status) do
+ :public | :developer | true | 'PRIVATE-TOKEN' | :personal_access_token | true | 'process terraform module upload' | :created
+ :public | :guest | true | 'PRIVATE-TOKEN' | :personal_access_token | true | 'rejects terraform module packages access' | :forbidden
+ :public | :developer | true | 'PRIVATE-TOKEN' | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
+ :public | :guest | true | 'PRIVATE-TOKEN' | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
+ :public | :developer | false | 'PRIVATE-TOKEN' | :personal_access_token | true | 'rejects terraform module packages access' | :forbidden
+ :public | :guest | false | 'PRIVATE-TOKEN' | :personal_access_token | true | 'rejects terraform module packages access' | :forbidden
+ :public | :developer | false | 'PRIVATE-TOKEN' | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
+ :public | :guest | false | 'PRIVATE-TOKEN' | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
+ :public | :anonymous | false | 'PRIVATE-TOKEN' | :personal_access_token | true | 'rejects terraform module packages access' | :unauthorized
+ :private | :developer | true | 'PRIVATE-TOKEN' | :personal_access_token | true | 'process terraform module upload' | :created
+ :private | :guest | true | 'PRIVATE-TOKEN' | :personal_access_token | true | 'rejects terraform module packages access' | :forbidden
+ :private | :developer | true | 'PRIVATE-TOKEN' | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
+ :private | :guest | true | 'PRIVATE-TOKEN' | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
+ :private | :developer | false | 'PRIVATE-TOKEN' | :personal_access_token | true | 'rejects terraform module packages access' | :not_found
+ :private | :guest | false | 'PRIVATE-TOKEN' | :personal_access_token | true | 'rejects terraform module packages access' | :not_found
+ :private | :developer | false | 'PRIVATE-TOKEN' | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
+ :private | :guest | false | 'PRIVATE-TOKEN' | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
+ :private | :anonymous | false | 'PRIVATE-TOKEN' | :personal_access_token | true | 'rejects terraform module packages access' | :unauthorized
+ :public | :developer | true | 'JOB-TOKEN' | :job_token | true | 'process terraform module upload' | :created
+ :public | :guest | true | 'JOB-TOKEN' | :job_token | true | 'rejects terraform module packages access' | :forbidden
+ :public | :developer | true | 'JOB-TOKEN' | :job_token | false | 'rejects terraform module packages access' | :unauthorized
+ :public | :guest | true | 'JOB-TOKEN' | :job_token | false | 'rejects terraform module packages access' | :unauthorized
+ :public | :developer | false | 'JOB-TOKEN' | :job_token | true | 'rejects terraform module packages access' | :forbidden
+ :public | :guest | false | 'JOB-TOKEN' | :job_token | true | 'rejects terraform module packages access' | :forbidden
+ :public | :developer | false | 'JOB-TOKEN' | :job_token | false | 'rejects terraform module packages access' | :unauthorized
+ :public | :guest | false | 'JOB-TOKEN' | :job_token | false | 'rejects terraform module packages access' | :unauthorized
+ :private | :developer | true | 'JOB-TOKEN' | :job_token | true | 'process terraform module upload' | :created
+ :private | :guest | true | 'JOB-TOKEN' | :job_token | true | 'rejects terraform module packages access' | :forbidden
+ :private | :developer | true | 'JOB-TOKEN' | :job_token | false | 'rejects terraform module packages access' | :unauthorized
+ :private | :guest | true | 'JOB-TOKEN' | :job_token | false | 'rejects terraform module packages access' | :unauthorized
+ :private | :developer | false | 'JOB-TOKEN' | :job_token | true | 'rejects terraform module packages access' | :not_found
+ :private | :guest | false | 'JOB-TOKEN' | :job_token | true | 'rejects terraform module packages access' | :not_found
+ :private | :developer | false | 'JOB-TOKEN' | :job_token | false | 'rejects terraform module packages access' | :unauthorized
+ :private | :guest | false | 'JOB-TOKEN' | :job_token | false | 'rejects terraform module packages access' | :unauthorized
+ :public | :developer | true | 'DEPLOY-TOKEN' | :deploy_token | true | 'process terraform module upload' | :created
+ :public | :developer | true | 'DEPLOY-TOKEN' | :deploy_token | false | 'rejects terraform module packages access' | :unauthorized
+ :private | :developer | true | 'DEPLOY-TOKEN' | :deploy_token | true | 'process terraform module upload' | :created
+ :private | :developer | true | 'DEPLOY-TOKEN' | :deploy_token | false | 'rejects terraform module packages access' | :unauthorized
+ end
+
+ with_them do
+ let(:token) { valid_token ? tokens[token_type] : 'invalid-token123' }
+ let(:user_headers) { user_role == :anonymous ? {} : { token_header => token } }
+ let(:headers) { user_headers.merge(workhorse_headers) }
+
+ before do
+ project.update!(visibility: visibility.to_s)
+ end
+
+ it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member]
+ end
+
+ context 'failed package file save' do
+ let(:user_headers) { { 'PRIVATE-TOKEN' => personal_access_token.token } }
+ let(:headers) { user_headers.merge(workhorse_headers) }
+
+ before do
+ project.add_developer(user)
+ end
+
+ it 'does not create package record', :aggregate_failures do
+ allow(Packages::CreatePackageFileService).to receive(:new).and_raise(StandardError)
+
+ expect { subject }
+ .to change { project.packages.count }.by(0)
+ .and change { Packages::PackageFile.count }.by(0)
+ expect(response).to have_gitlab_http_status(:error)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb
index 01a24be9f20..71fdd986f20 100644
--- a/spec/requests/api/users_spec.rb
+++ b/spec/requests/api/users_spec.rb
@@ -1449,6 +1449,48 @@ RSpec.describe API::Users do
end
end
+ describe "PUT /user/:id/credit_card_validation" do
+ let(:credit_card_validated_time) { Time.utc(2020, 1, 1) }
+
+ context 'when unauthenticated' do
+ it 'returns authentication error' do
+ put api("/user/#{user.id}/credit_card_validation"), params: { credit_card_validated_at: credit_card_validated_time }
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+ end
+
+ context 'when authenticated as non-admin' do
+ it "does not allow updating user's credit card validation", :aggregate_failures do
+ put api("/user/#{user.id}/credit_card_validation", user), params: { credit_card_validated_at: credit_card_validated_time }
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+
+ context 'when authenticated as admin' do
+ it "updates user's credit card validation", :aggregate_failures do
+ put api("/user/#{user.id}/credit_card_validation", admin), params: { credit_card_validated_at: credit_card_validated_time }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(user.reload.credit_card_validated_at).to eq(credit_card_validated_time)
+ end
+
+ it "returns 400 error if credit_card_validated_at is missing" do
+ put api("/user/#{user.id}/credit_card_validation", admin), params: {}
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+
+ it 'returns 404 error if user not found' do
+ put api("/user/#{non_existing_record_id}/credit_card_validation", admin), params: { credit_card_validated_at: credit_card_validated_time }
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ expect(json_response['message']).to eq('404 User Not Found')
+ end
+ end
+ end
+
describe "DELETE /users/:id/identities/:provider" do
let(:test_user) { create(:omniauth_user, provider: 'ldapmain') }