From 4555e1b21c365ed8303ffb7a3325d773c9b8bf31 Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Wed, 19 May 2021 15:44:42 +0000 Subject: Add latest changes from gitlab-org/gitlab@13-12-stable-ee --- .../api/api_guard/admin_mode_middleware_spec.rb | 2 +- spec/requests/api/api_spec.rb | 14 - spec/requests/api/branches_spec.rb | 6 +- spec/requests/api/ci/pipelines_spec.rb | 19 + .../api/ci/runner/jobs_request_post_spec.rb | 6 +- spec/requests/api/ci/runner/runners_post_spec.rb | 30 ++ spec/requests/api/ci/runners_spec.rb | 13 + spec/requests/api/debian_group_packages_spec.rb | 32 +- spec/requests/api/debian_project_packages_spec.rb | 30 +- spec/requests/api/deploy_tokens_spec.rb | 57 ++- spec/requests/api/deployments_spec.rb | 25 +- spec/requests/api/environments_spec.rb | 109 ++++- spec/requests/api/graphql/ci/job_spec.rb | 13 +- spec/requests/api/graphql/ci/pipelines_spec.rb | 124 +++++ spec/requests/api/graphql/ci/runner_spec.rb | 144 ++++++ spec/requests/api/graphql/ci/runners_spec.rb | 114 +++++ spec/requests/api/graphql/ci/template_spec.rb | 48 ++ spec/requests/api/graphql/group/milestones_spec.rb | 12 - spec/requests/api/graphql/group/packages_spec.rb | 48 +- spec/requests/api/graphql/issue/issue_spec.rb | 6 +- .../graphql/merge_request/merge_request_spec.rb | 111 +++++ spec/requests/api/graphql/metadata_query_spec.rb | 46 +- .../api/graphql/mutations/boards/destroy_spec.rb | 3 +- .../graphql/mutations/boards/lists/destroy_spec.rb | 73 +-- .../graphql/mutations/boards/lists/update_spec.rb | 41 +- .../api/graphql/mutations/ci/job_play_spec.rb | 46 ++ .../api/graphql/mutations/ci/job_retry_spec.rb | 46 ++ .../api/graphql/mutations/issues/create_spec.rb | 5 +- .../graphql/mutations/issues/set_due_date_spec.rb | 31 +- .../api/graphql/mutations/issues/update_spec.rb | 3 +- .../api/graphql/mutations/labels/create_spec.rb | 3 +- .../namespace/package_settings/update_spec.rb | 12 +- .../configure_secret_detection_spec.rb | 26 ++ .../requests/api/graphql/packages/composer_spec.rb | 64 +++ spec/requests/api/graphql/packages/conan_spec.rb | 90 ++++ spec/requests/api/graphql/packages/maven_spec.rb | 94 ++++ spec/requests/api/graphql/packages/nuget_spec.rb | 74 +++ spec/requests/api/graphql/packages/package_spec.rb | 78 +--- .../issue/design_collection/versions_spec.rb | 17 +- .../api/graphql/project/merge_request_spec.rb | 46 +- spec/requests/api/graphql/project/packages_spec.rb | 33 +- .../api/graphql/project/project_members_spec.rb | 16 + spec/requests/api/graphql/project/release_spec.rb | 17 - spec/requests/api/graphql/project/releases_spec.rb | 17 - .../api/graphql/project/repository_spec.rb | 24 + spec/requests/api/graphql/project_query_spec.rb | 16 + spec/requests/api/graphql_spec.rb | 57 ++- spec/requests/api/group_export_spec.rb | 70 +++ spec/requests/api/group_labels_spec.rb | 4 +- spec/requests/api/helpers_spec.rb | 2 +- spec/requests/api/internal/kubernetes_spec.rb | 12 +- .../requests/api/issues/get_project_issues_spec.rb | 5 +- spec/requests/api/issues/issues_spec.rb | 29 +- .../api/issues/put_projects_issues_spec.rb | 11 + spec/requests/api/labels_spec.rb | 14 +- spec/requests/api/maven_packages_spec.rb | 101 +---- spec/requests/api/merge_requests_spec.rb | 78 +++- spec/requests/api/package_files_spec.rb | 81 +++- spec/requests/api/project_attributes.yml | 2 + .../api/project_container_repositories_spec.rb | 505 ++++++++++++--------- spec/requests/api/project_import_spec.rb | 76 ++++ spec/requests/api/project_packages_spec.rb | 10 + spec/requests/api/project_templates_spec.rb | 17 - spec/requests/api/projects_spec.rb | 71 ++- spec/requests/api/releases_spec.rb | 92 +++- spec/requests/api/services_spec.rb | 52 ++- spec/requests/api/settings_spec.rb | 56 ++- .../api/terraform/modules/v1/packages_spec.rb | 360 +++++++++++++++ spec/requests/api/users_spec.rb | 42 ++ 69 files changed, 2840 insertions(+), 791 deletions(-) create mode 100644 spec/requests/api/graphql/ci/runner_spec.rb create mode 100644 spec/requests/api/graphql/ci/runners_spec.rb create mode 100644 spec/requests/api/graphql/ci/template_spec.rb create mode 100644 spec/requests/api/graphql/merge_request/merge_request_spec.rb create mode 100644 spec/requests/api/graphql/mutations/ci/job_play_spec.rb create mode 100644 spec/requests/api/graphql/mutations/ci/job_retry_spec.rb create mode 100644 spec/requests/api/graphql/mutations/security/ci_configuration/configure_secret_detection_spec.rb create mode 100644 spec/requests/api/graphql/packages/composer_spec.rb create mode 100644 spec/requests/api/graphql/packages/conan_spec.rb create mode 100644 spec/requests/api/graphql/packages/maven_spec.rb create mode 100644 spec/requests/api/graphql/packages/nuget_spec.rb create mode 100644 spec/requests/api/terraform/modules/v1/packages_spec.rb (limited to 'spec/requests/api') 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('# 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') } -- cgit v1.2.1