summaryrefslogtreecommitdiff
path: root/spec/requests/api
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-11-19 08:27:35 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2020-11-19 08:27:35 +0000
commit7e9c479f7de77702622631cff2628a9c8dcbc627 (patch)
treec8f718a08e110ad7e1894510980d2155a6549197 /spec/requests/api
parente852b0ae16db4052c1c567d9efa4facc81146e88 (diff)
downloadgitlab-ce-7e9c479f7de77702622631cff2628a9c8dcbc627.tar.gz
Add latest changes from gitlab-org/gitlab@13-6-stable-eev13.6.0-rc42
Diffstat (limited to 'spec/requests/api')
-rw-r--r--spec/requests/api/admin/instance_clusters_spec.rb1
-rw-r--r--spec/requests/api/api_spec.rb32
-rw-r--r--spec/requests/api/boards_spec.rb43
-rw-r--r--spec/requests/api/ci/pipelines_spec.rb615
-rw-r--r--spec/requests/api/commits_spec.rb11
-rw-r--r--spec/requests/api/container_repositories_spec.rb88
-rw-r--r--spec/requests/api/dependency_proxy_spec.rb72
-rw-r--r--spec/requests/api/feature_flags_user_lists_spec.rb33
-rw-r--r--spec/requests/api/files_spec.rb70
-rw-r--r--spec/requests/api/generic_packages_spec.rb4
-rw-r--r--spec/requests/api/graphql/ci/jobs_spec.rb28
-rw-r--r--spec/requests/api/graphql/ci/pipelines_spec.rb221
-rw-r--r--spec/requests/api/graphql/container_repository/container_repository_details_spec.rb108
-rw-r--r--spec/requests/api/graphql/custom_emoji_query_spec.rb37
-rw-r--r--spec/requests/api/graphql/group/container_repositories_spec.rb146
-rw-r--r--spec/requests/api/graphql/instance_statistics_measurements_spec.rb13
-rw-r--r--spec/requests/api/graphql/issue/issue_spec.rb24
-rw-r--r--spec/requests/api/graphql/metrics/dashboard/annotations_spec.rb1
-rw-r--r--spec/requests/api/graphql/mutations/alert_management/http_integration/create_spec.rb67
-rw-r--r--spec/requests/api/graphql/mutations/alert_management/http_integration/destroy_spec.rb55
-rw-r--r--spec/requests/api/graphql/mutations/alert_management/http_integration/reset_token_spec.rb45
-rw-r--r--spec/requests/api/graphql/mutations/alert_management/http_integration/update_spec.rb49
-rw-r--r--spec/requests/api/graphql/mutations/alert_management/prometheus_integration/create_spec.rb67
-rw-r--r--spec/requests/api/graphql/mutations/alert_management/prometheus_integration/reset_token_spec.rb59
-rw-r--r--spec/requests/api/graphql/mutations/alert_management/prometheus_integration/update_spec.rb47
-rw-r--r--spec/requests/api/graphql/mutations/commits/create_spec.rb38
-rw-r--r--spec/requests/api/graphql/mutations/container_expiration_policy/update_spec.rb25
-rw-r--r--spec/requests/api/graphql/mutations/container_repository/destroy_spec.rb91
-rw-r--r--spec/requests/api/graphql/mutations/custom_emoji/create_spec.rb43
-rw-r--r--spec/requests/api/graphql/mutations/labels/create_spec.rb86
-rw-r--r--spec/requests/api/graphql/mutations/metrics/dashboard/annotations/create_spec.rb49
-rw-r--r--spec/requests/api/graphql/mutations/metrics/dashboard/annotations/delete_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/notes/reposition_image_diff_note_spec.rb81
-rw-r--r--spec/requests/api/graphql/mutations/notes/update/image_diff_note_spec.rb8
-rw-r--r--spec/requests/api/graphql/mutations/releases/create_spec.rb375
-rw-r--r--spec/requests/api/graphql/mutations/snippets/destroy_spec.rb5
-rw-r--r--spec/requests/api/graphql/mutations/todos/create_spec.rb38
-rw-r--r--spec/requests/api/graphql/mutations/todos/restore_many_spec.rb70
-rw-r--r--spec/requests/api/graphql/namespace/root_storage_statistics_spec.rb8
-rw-r--r--spec/requests/api/graphql/project/alert_management/integrations_spec.rb83
-rw-r--r--spec/requests/api/graphql/project/container_repositories_spec.rb145
-rw-r--r--spec/requests/api/graphql/project/error_tracking/sentry_errors_request_spec.rb12
-rw-r--r--spec/requests/api/graphql/project/grafana_integration_spec.rb1
-rw-r--r--spec/requests/api/graphql/project/issue/design_collection/version_spec.rb1
-rw-r--r--spec/requests/api/graphql/project/issue/design_collection/versions_spec.rb3
-rw-r--r--spec/requests/api/graphql/project/issue/designs/notes_spec.rb4
-rw-r--r--spec/requests/api/graphql/project/issues_spec.rb45
-rw-r--r--spec/requests/api/graphql/project/jira_import_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/merge_requests_spec.rb11
-rw-r--r--spec/requests/api/graphql/project/project_statistics_spec.rb8
-rw-r--r--spec/requests/api/graphql/project/release_spec.rb22
-rw-r--r--spec/requests/api/graphql/project/releases_spec.rb92
-rw-r--r--spec/requests/api/graphql/project/terraform/states_spec.rb81
-rw-r--r--spec/requests/api/graphql/read_only_spec.rb50
-rw-r--r--spec/requests/api/graphql/terraform/state/delete_spec.rb23
-rw-r--r--spec/requests/api/graphql/terraform/state/lock_spec.rb25
-rw-r--r--spec/requests/api/graphql/terraform/state/unlock_spec.rb24
-rw-r--r--spec/requests/api/graphql/user/group_member_query_spec.rb1
-rw-r--r--spec/requests/api/graphql/user/project_member_query_spec.rb1
-rw-r--r--spec/requests/api/graphql/user_query_spec.rb14
-rw-r--r--spec/requests/api/graphql_spec.rb2
-rw-r--r--spec/requests/api/group_labels_spec.rb129
-rw-r--r--spec/requests/api/import_github_spec.rb16
-rw-r--r--spec/requests/api/internal/base_spec.rb28
-rw-r--r--spec/requests/api/internal/pages_spec.rb50
-rw-r--r--spec/requests/api/invitations_spec.rb301
-rw-r--r--spec/requests/api/issues/get_project_issues_spec.rb2
-rw-r--r--spec/requests/api/issues/issues_spec.rb2
-rw-r--r--spec/requests/api/issues/post_projects_issues_spec.rb2
-rw-r--r--spec/requests/api/labels_spec.rb60
-rw-r--r--spec/requests/api/lint_spec.rb210
-rw-r--r--spec/requests/api/maven_packages_spec.rb42
-rw-r--r--spec/requests/api/members_spec.rb2
-rw-r--r--spec/requests/api/merge_requests_spec.rb88
-rw-r--r--spec/requests/api/npm_instance_packages_spec.rb31
-rw-r--r--spec/requests/api/npm_packages_spec.rb556
-rw-r--r--spec/requests/api/npm_project_packages_spec.rb281
-rw-r--r--spec/requests/api/personal_access_tokens_spec.rb112
-rw-r--r--spec/requests/api/project_container_repositories_spec.rb6
-rw-r--r--spec/requests/api/project_export_spec.rb2
-rw-r--r--spec/requests/api/project_hooks_spec.rb6
-rw-r--r--spec/requests/api/projects_spec.rb5
-rw-r--r--spec/requests/api/release/links_spec.rb2
-rw-r--r--spec/requests/api/releases_spec.rb27
-rw-r--r--spec/requests/api/search_spec.rb63
-rw-r--r--spec/requests/api/settings_spec.rb45
-rw-r--r--spec/requests/api/terraform/state_spec.rb14
-rw-r--r--spec/requests/api/users_spec.rb222
88 files changed, 4444 insertions, 1365 deletions
diff --git a/spec/requests/api/admin/instance_clusters_spec.rb b/spec/requests/api/admin/instance_clusters_spec.rb
index 9d0661089a9..1052080aad4 100644
--- a/spec/requests/api/admin/instance_clusters_spec.rb
+++ b/spec/requests/api/admin/instance_clusters_spec.rb
@@ -13,6 +13,7 @@ RSpec.describe ::API::Admin::InstanceClusters do
user: admin_user,
projects: [project])
end
+
let(:project_cluster_id) { project_cluster.id }
describe "GET /admin/clusters" do
diff --git a/spec/requests/api/api_spec.rb b/spec/requests/api/api_spec.rb
index 7d637757f38..3cc8764de4a 100644
--- a/spec/requests/api/api_spec.rb
+++ b/spec/requests/api/api_spec.rb
@@ -92,4 +92,36 @@ RSpec.describe API::API do
end
end
end
+
+ context 'application context' do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:user) { project.owner }
+
+ it 'logs all application context fields' do
+ allow_any_instance_of(Gitlab::GrapeLogging::Loggers::ContextLogger).to receive(:parameters) do
+ Labkit::Context.current.to_h.tap do |log_context|
+ expect(log_context).to match('correlation_id' => an_instance_of(String),
+ 'meta.caller_id' => '/api/:version/projects/:id/issues',
+ 'meta.project' => project.full_path,
+ 'meta.root_namespace' => project.namespace.full_path,
+ 'meta.user' => user.username,
+ 'meta.feature_category' => 'issue_tracking')
+ end
+ end
+
+ get(api("/projects/#{project.id}/issues", user))
+ end
+
+ it 'skips fields that do not apply' do
+ allow_any_instance_of(Gitlab::GrapeLogging::Loggers::ContextLogger).to receive(:parameters) do
+ Labkit::Context.current.to_h.tap do |log_context|
+ expect(log_context).to match('correlation_id' => an_instance_of(String),
+ 'meta.caller_id' => '/api/:version/users',
+ 'meta.feature_category' => 'users')
+ end
+ end
+
+ get(api('/users'))
+ end
+ end
end
diff --git a/spec/requests/api/boards_spec.rb b/spec/requests/api/boards_spec.rb
index a63198c5407..36fc6101b84 100644
--- a/spec/requests/api/boards_spec.rb
+++ b/spec/requests/api/boards_spec.rb
@@ -35,7 +35,46 @@ RSpec.describe API::Boards do
it_behaves_like 'group and project boards', "/projects/:id/boards"
- describe "POST /projects/:id/boards/lists" do
+ describe "POST /projects/:id/boards" do
+ let(:url) { "/projects/#{board_parent.id}/boards" }
+
+ it 'creates a new issue board' do
+ post api(url, user), params: { name: 'foo' }
+
+ expect(response).to have_gitlab_http_status(:created)
+ expect(json_response['name']).to eq('foo')
+ end
+
+ it 'fails to create a new board' do
+ post api(url, user), params: { some_name: 'foo' }
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['error']).to eq('name is missing')
+ end
+ end
+
+ describe "PUT /projects/:id/boards/:board_id" do
+ let(:url) { "/projects/#{board_parent.id}/boards/#{board.id}" }
+
+ it 'updates the issue board' do
+ put api(url, user), params: { name: 'changed board name' }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['name']).to eq('changed board name')
+ end
+ end
+
+ describe "DELETE /projects/:id/boards/:board_id" do
+ let(:url) { "/projects/#{board_parent.id}/boards/#{board.id}" }
+
+ it 'delete the issue board' do
+ delete api(url, user)
+
+ expect(response).to have_gitlab_http_status(:no_content)
+ end
+ end
+
+ describe "POST /projects/:id/boards/:board_id/lists" do
let(:url) { "/projects/#{board_parent.id}/boards/#{board.id}/lists" }
it 'creates a new issue board list for group labels' do
@@ -65,7 +104,7 @@ RSpec.describe API::Boards do
end
end
- describe "POST /groups/:id/boards/lists" do
+ describe "POST /groups/:id/boards/:board_id/lists" do
let_it_be(:group) { create(:group) }
let_it_be(:board_parent) { create(:group, parent: group ) }
let(:url) { "/groups/#{board_parent.id}/boards/#{board.id}/lists" }
diff --git a/spec/requests/api/ci/pipelines_spec.rb b/spec/requests/api/ci/pipelines_spec.rb
index 577b43e6e42..767b5704851 100644
--- a/spec/requests/api/ci/pipelines_spec.rb
+++ b/spec/requests/api/ci/pipelines_spec.rb
@@ -325,236 +325,113 @@ RSpec.describe API::Ci::Pipelines do
end
end
- context 'with ci_jobs_finder_refactor ff enabled' do
- before do
- stub_feature_flags(ci_jobs_finder_refactor: true)
+ context 'authorized user' do
+ it 'returns pipeline jobs' do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
end
- context 'authorized user' do
- it 'returns pipeline jobs' do
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- end
-
- it 'returns correct values' do
- expect(json_response).not_to be_empty
- expect(json_response.first['commit']['id']).to eq project.commit.id
- expect(Time.parse(json_response.first['artifacts_expire_at'])).to be_like_time(job.artifacts_expire_at)
- expect(json_response.first['artifacts_file']).to be_nil
- expect(json_response.first['artifacts']).to be_an Array
- expect(json_response.first['artifacts']).to be_empty
- end
-
- it_behaves_like 'a job with artifacts and trace' do
- let(:api_endpoint) { "/projects/#{project.id}/pipelines/#{pipeline.id}/jobs" }
- end
-
- it 'returns pipeline data' do
- json_job = json_response.first
-
- expect(json_job['pipeline']).not_to be_empty
- expect(json_job['pipeline']['id']).to eq job.pipeline.id
- expect(json_job['pipeline']['ref']).to eq job.pipeline.ref
- expect(json_job['pipeline']['sha']).to eq job.pipeline.sha
- expect(json_job['pipeline']['status']).to eq job.pipeline.status
- end
-
- context 'filter jobs with one scope element' do
- let(:query) { { 'scope' => 'pending' } }
-
- it do
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response).to be_an Array
- end
- end
-
- context 'filter jobs with hash' do
- let(:query) { { scope: { hello: 'pending', world: 'running' } } }
-
- it { expect(response).to have_gitlab_http_status(:bad_request) }
- end
-
- context 'filter jobs with array of scope elements' do
- let(:query) { { scope: %w(pending running) } }
-
- it do
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response).to be_an Array
- end
- end
-
- context 'respond 400 when scope contains invalid state' do
- let(:query) { { scope: %w(unknown running) } }
-
- it { expect(response).to have_gitlab_http_status(:bad_request) }
- end
-
- context 'jobs in different pipelines' do
- let!(:pipeline2) { create(:ci_empty_pipeline, project: project) }
- let!(:job2) { create(:ci_build, pipeline: pipeline2) }
-
- it 'excludes jobs from other pipelines' do
- json_response.each { |job| expect(job['pipeline']['id']).to eq(pipeline.id) }
- end
- end
-
- it 'avoids N+1 queries' do
- control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do
- get api("/projects/#{project.id}/pipelines/#{pipeline.id}/jobs", api_user), params: query
- end.count
-
- create_list(:ci_build, 3, :trace_artifact, :artifacts, :test_reports, pipeline: pipeline)
-
- expect do
- get api("/projects/#{project.id}/pipelines/#{pipeline.id}/jobs", api_user), params: query
- end.not_to exceed_all_query_limit(control_count)
- end
+ it 'returns correct values' do
+ expect(json_response).not_to be_empty
+ expect(json_response.first['commit']['id']).to eq project.commit.id
+ expect(Time.parse(json_response.first['artifacts_expire_at'])).to be_like_time(job.artifacts_expire_at)
+ expect(json_response.first['artifacts_file']).to be_nil
+ expect(json_response.first['artifacts']).to be_an Array
+ expect(json_response.first['artifacts']).to be_empty
end
- context 'no pipeline is found' do
- it 'does not return jobs' do
- get api("/projects/#{project2.id}/pipelines/#{pipeline.id}/jobs", user)
-
- expect(json_response['message']).to eq '404 Project Not Found'
- expect(response).to have_gitlab_http_status(:not_found)
- end
+ it_behaves_like 'a job with artifacts and trace' do
+ let(:api_endpoint) { "/projects/#{project.id}/pipelines/#{pipeline.id}/jobs" }
end
- context 'unauthorized user' do
- context 'when user is not logged in' do
- let(:api_user) { nil }
+ it 'returns pipeline data' do
+ json_job = json_response.first
- it 'does not return jobs' do
- expect(json_response['message']).to eq '404 Project Not Found'
- expect(response).to have_gitlab_http_status(:not_found)
- end
- end
-
- context 'when user is guest' do
- let(:guest) { create(:project_member, :guest, project: project).user }
- let(:api_user) { guest }
-
- it 'does not return jobs' do
- expect(response).to have_gitlab_http_status(:forbidden)
- end
- end
+ expect(json_job['pipeline']).not_to be_empty
+ expect(json_job['pipeline']['id']).to eq job.pipeline.id
+ expect(json_job['pipeline']['ref']).to eq job.pipeline.ref
+ expect(json_job['pipeline']['sha']).to eq job.pipeline.sha
+ expect(json_job['pipeline']['status']).to eq job.pipeline.status
end
- end
- context 'with ci_jobs_finder ff disabled' do
- before do
- stub_feature_flags(ci_jobs_finder_refactor: false)
- end
+ context 'filter jobs with one scope element' do
+ let(:query) { { 'scope' => 'pending' } }
- context 'authorized user' do
- it 'returns pipeline jobs' do
+ it do
expect(response).to have_gitlab_http_status(:ok)
- expect(response).to include_pagination_headers
expect(json_response).to be_an Array
end
+ end
- it 'returns correct values' do
- expect(json_response).not_to be_empty
- expect(json_response.first['commit']['id']).to eq project.commit.id
- expect(Time.parse(json_response.first['artifacts_expire_at'])).to be_like_time(job.artifacts_expire_at)
- expect(json_response.first['artifacts_file']).to be_nil
- expect(json_response.first['artifacts']).to be_an Array
- expect(json_response.first['artifacts']).to be_empty
- end
-
- it_behaves_like 'a job with artifacts and trace' do
- let(:api_endpoint) { "/projects/#{project.id}/pipelines/#{pipeline.id}/jobs" }
- end
-
- it 'returns pipeline data' do
- json_job = json_response.first
+ context 'filter jobs with hash' do
+ let(:query) { { scope: { hello: 'pending', world: 'running' } } }
- expect(json_job['pipeline']).not_to be_empty
- expect(json_job['pipeline']['id']).to eq job.pipeline.id
- expect(json_job['pipeline']['ref']).to eq job.pipeline.ref
- expect(json_job['pipeline']['sha']).to eq job.pipeline.sha
- expect(json_job['pipeline']['status']).to eq job.pipeline.status
- end
+ it { expect(response).to have_gitlab_http_status(:bad_request) }
+ end
- context 'filter jobs with one scope element' do
- let(:query) { { 'scope' => 'pending' } }
+ context 'filter jobs with array of scope elements' do
+ let(:query) { { scope: %w(pending running) } }
- it do
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response).to be_an Array
- end
+ it do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to be_an Array
end
+ end
- context 'filter jobs with hash' do
- let(:query) { { scope: { hello: 'pending', world: 'running' } } }
+ context 'respond 400 when scope contains invalid state' do
+ let(:query) { { scope: %w(unknown running) } }
- it { expect(response).to have_gitlab_http_status(:bad_request) }
- end
+ it { expect(response).to have_gitlab_http_status(:bad_request) }
+ end
- context 'filter jobs with array of scope elements' do
- let(:query) { { scope: %w(pending running) } }
+ context 'jobs in different pipelines' do
+ let!(:pipeline2) { create(:ci_empty_pipeline, project: project) }
+ let!(:job2) { create(:ci_build, pipeline: pipeline2) }
- it do
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response).to be_an Array
- end
+ it 'excludes jobs from other pipelines' do
+ json_response.each { |job| expect(job['pipeline']['id']).to eq(pipeline.id) }
end
+ end
- context 'respond 400 when scope contains invalid state' do
- let(:query) { { scope: %w(unknown running) } }
+ it 'avoids N+1 queries' do
+ control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do
+ get api("/projects/#{project.id}/pipelines/#{pipeline.id}/jobs", api_user), params: query
+ end.count
- it { expect(response).to have_gitlab_http_status(:bad_request) }
- end
+ create_list(:ci_build, 3, :trace_artifact, :artifacts, :test_reports, pipeline: pipeline)
- context 'jobs in different pipelines' do
- let!(:pipeline2) { create(:ci_empty_pipeline, project: project) }
- let!(:job2) { create(:ci_build, pipeline: pipeline2) }
-
- it 'excludes jobs from other pipelines' do
- json_response.each { |job| expect(job['pipeline']['id']).to eq(pipeline.id) }
- end
- end
-
- it 'avoids N+1 queries' do
- control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do
- get api("/projects/#{project.id}/pipelines/#{pipeline.id}/jobs", api_user), params: query
- end.count
+ expect do
+ get api("/projects/#{project.id}/pipelines/#{pipeline.id}/jobs", api_user), params: query
+ end.not_to exceed_all_query_limit(control_count)
+ end
+ end
- create_list(:ci_build, 3, :trace_artifact, :artifacts, :test_reports, pipeline: pipeline)
+ context 'no pipeline is found' do
+ it 'does not return jobs' do
+ get api("/projects/#{project2.id}/pipelines/#{pipeline.id}/jobs", user)
- expect do
- get api("/projects/#{project.id}/pipelines/#{pipeline.id}/jobs", api_user), params: query
- end.not_to exceed_all_query_limit(control_count)
- end
+ expect(json_response['message']).to eq '404 Project Not Found'
+ expect(response).to have_gitlab_http_status(:not_found)
end
+ end
- context 'no pipeline is found' do
- it 'does not return jobs' do
- get api("/projects/#{project2.id}/pipelines/#{pipeline.id}/jobs", user)
+ context 'unauthorized user' do
+ context 'when user is not logged in' do
+ let(:api_user) { nil }
+ it 'does not return jobs' do
expect(json_response['message']).to eq '404 Project Not Found'
expect(response).to have_gitlab_http_status(:not_found)
end
end
- context 'unauthorized user' do
- context 'when user is not logged in' do
- let(:api_user) { nil }
-
- it 'does not return jobs' do
- expect(json_response['message']).to eq '404 Project Not Found'
- expect(response).to have_gitlab_http_status(:not_found)
- end
- end
+ context 'when user is guest' do
+ let(:guest) { create(:project_member, :guest, project: project).user }
+ let(:api_user) { guest }
- context 'when user is guest' do
- let(:guest) { create(:project_member, :guest, project: project).user }
- let(:api_user) { guest }
-
- it 'does not return jobs' do
- expect(response).to have_gitlab_http_status(:forbidden)
- end
+ it 'does not return jobs' do
+ expect(response).to have_gitlab_http_status(:forbidden)
end
end
end
@@ -583,314 +460,152 @@ RSpec.describe API::Ci::Pipelines do
end
end
- context 'with ci_jobs_finder_refactor ff enabled' do
- before do
- stub_feature_flags(ci_jobs_finder_refactor: true)
+ context 'authorized user' do
+ it 'returns pipeline bridges' do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
end
- context 'authorized user' do
- it 'returns pipeline bridges' do
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- end
+ it 'returns correct values' do
+ expect(json_response).not_to be_empty
+ expect(json_response.first['commit']['id']).to eq project.commit.id
+ expect(json_response.first['id']).to eq bridge.id
+ expect(json_response.first['name']).to eq bridge.name
+ expect(json_response.first['stage']).to eq bridge.stage
+ end
- it 'returns correct values' do
- expect(json_response).not_to be_empty
- expect(json_response.first['commit']['id']).to eq project.commit.id
- expect(json_response.first['id']).to eq bridge.id
- expect(json_response.first['name']).to eq bridge.name
- expect(json_response.first['stage']).to eq bridge.stage
- end
+ it 'returns pipeline data' do
+ json_bridge = json_response.first
- it 'returns pipeline data' do
- json_bridge = json_response.first
+ expect(json_bridge['pipeline']).not_to be_empty
+ expect(json_bridge['pipeline']['id']).to eq bridge.pipeline.id
+ expect(json_bridge['pipeline']['ref']).to eq bridge.pipeline.ref
+ expect(json_bridge['pipeline']['sha']).to eq bridge.pipeline.sha
+ expect(json_bridge['pipeline']['status']).to eq bridge.pipeline.status
+ end
- expect(json_bridge['pipeline']).not_to be_empty
- expect(json_bridge['pipeline']['id']).to eq bridge.pipeline.id
- expect(json_bridge['pipeline']['ref']).to eq bridge.pipeline.ref
- expect(json_bridge['pipeline']['sha']).to eq bridge.pipeline.sha
- expect(json_bridge['pipeline']['status']).to eq bridge.pipeline.status
- end
+ it 'returns downstream pipeline data' do
+ json_bridge = json_response.first
- it 'returns downstream pipeline data' do
- json_bridge = json_response.first
+ expect(json_bridge['downstream_pipeline']).not_to be_empty
+ expect(json_bridge['downstream_pipeline']['id']).to eq downstream_pipeline.id
+ expect(json_bridge['downstream_pipeline']['ref']).to eq downstream_pipeline.ref
+ expect(json_bridge['downstream_pipeline']['sha']).to eq downstream_pipeline.sha
+ expect(json_bridge['downstream_pipeline']['status']).to eq downstream_pipeline.status
+ end
- expect(json_bridge['downstream_pipeline']).not_to be_empty
- expect(json_bridge['downstream_pipeline']['id']).to eq downstream_pipeline.id
- expect(json_bridge['downstream_pipeline']['ref']).to eq downstream_pipeline.ref
- expect(json_bridge['downstream_pipeline']['sha']).to eq downstream_pipeline.sha
- expect(json_bridge['downstream_pipeline']['status']).to eq downstream_pipeline.status
+ context 'filter bridges' do
+ before_all do
+ create_bridge(pipeline, :pending)
+ create_bridge(pipeline, :running)
end
- context 'filter bridges' do
- before_all do
- create_bridge(pipeline, :pending)
- create_bridge(pipeline, :running)
- end
-
- context 'with one scope element' do
- let(:query) { { 'scope' => 'pending' } }
-
- it :skip_before_request do
- get api("/projects/#{project.id}/pipelines/#{pipeline.id}/bridges", api_user), params: query
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response).to be_an Array
- expect(json_response.count).to eq 1
- expect(json_response.first["status"]).to eq "pending"
- end
- end
-
- context 'with array of scope elements' do
- let(:query) { { scope: %w(pending running) } }
+ context 'with one scope element' do
+ let(:query) { { 'scope' => 'pending' } }
- it :skip_before_request do
- get api("/projects/#{project.id}/pipelines/#{pipeline.id}/bridges", api_user), params: query
+ it :skip_before_request do
+ get api("/projects/#{project.id}/pipelines/#{pipeline.id}/bridges", api_user), params: query
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response).to be_an Array
- expect(json_response.count).to eq 2
- json_response.each { |r| expect(%w(pending running).include?(r['status'])).to be true }
- end
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to be_an Array
+ expect(json_response.count).to eq 1
+ expect(json_response.first["status"]).to eq "pending"
end
end
- context 'respond 400 when scope contains invalid state' do
- context 'in an array' do
- let(:query) { { scope: %w(unknown running) } }
-
- it { expect(response).to have_gitlab_http_status(:bad_request) }
- end
-
- context 'in a hash' do
- let(:query) { { scope: { unknown: true } } }
-
- it { expect(response).to have_gitlab_http_status(:bad_request) }
- end
+ context 'with array of scope elements' do
+ let(:query) { { scope: %w(pending running) } }
- context 'in a string' do
- let(:query) { { scope: "unknown" } }
+ it :skip_before_request do
+ get api("/projects/#{project.id}/pipelines/#{pipeline.id}/bridges", api_user), params: query
- it { expect(response).to have_gitlab_http_status(:bad_request) }
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to be_an Array
+ expect(json_response.count).to eq 2
+ json_response.each { |r| expect(%w(pending running).include?(r['status'])).to be true }
end
end
+ end
- context 'bridges in different pipelines' do
- let!(:pipeline2) { create(:ci_empty_pipeline, project: project) }
- let!(:bridge2) { create(:ci_bridge, pipeline: pipeline2) }
+ context 'respond 400 when scope contains invalid state' do
+ context 'in an array' do
+ let(:query) { { scope: %w(unknown running) } }
- it 'excludes bridges from other pipelines' do
- json_response.each { |bridge| expect(bridge['pipeline']['id']).to eq(pipeline.id) }
- end
+ it { expect(response).to have_gitlab_http_status(:bad_request) }
end
- it 'avoids N+1 queries' do
- control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do
- get api("/projects/#{project.id}/pipelines/#{pipeline.id}/bridges", api_user), params: query
- end.count
-
- 3.times { create_bridge(pipeline) }
+ context 'in a hash' do
+ let(:query) { { scope: { unknown: true } } }
- expect do
- get api("/projects/#{project.id}/pipelines/#{pipeline.id}/bridges", api_user), params: query
- end.not_to exceed_all_query_limit(control_count)
+ it { expect(response).to have_gitlab_http_status(:bad_request) }
end
- end
- context 'no pipeline is found' do
- it 'does not return bridges' do
- get api("/projects/#{project2.id}/pipelines/#{pipeline.id}/bridges", user)
+ context 'in a string' do
+ let(:query) { { scope: "unknown" } }
- expect(json_response['message']).to eq '404 Project Not Found'
- expect(response).to have_gitlab_http_status(:not_found)
+ it { expect(response).to have_gitlab_http_status(:bad_request) }
end
end
- context 'unauthorized user' do
- context 'when user is not logged in' do
- let(:api_user) { nil }
+ context 'bridges in different pipelines' do
+ let!(:pipeline2) { create(:ci_empty_pipeline, project: project) }
+ let!(:bridge2) { create(:ci_bridge, pipeline: pipeline2) }
- it 'does not return bridges' do
- expect(json_response['message']).to eq '404 Project Not Found'
- expect(response).to have_gitlab_http_status(:not_found)
- end
+ it 'excludes bridges from other pipelines' do
+ json_response.each { |bridge| expect(bridge['pipeline']['id']).to eq(pipeline.id) }
end
+ end
- context 'when user is guest' do
- let(:api_user) { guest }
- let(:guest) { create(:project_member, :guest, project: project).user }
+ it 'avoids N+1 queries' do
+ control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do
+ get api("/projects/#{project.id}/pipelines/#{pipeline.id}/bridges", api_user), params: query
+ end.count
- it 'does not return bridges' do
- expect(response).to have_gitlab_http_status(:forbidden)
- end
- end
+ 3.times { create_bridge(pipeline) }
- context 'when user has no read_build access for project' do
- before do
- project.add_guest(api_user)
- end
-
- it 'does not return bridges' do
- get api("/projects/#{project.id}/pipelines/#{pipeline.id}/bridges", api_user)
- expect(response).to have_gitlab_http_status(:forbidden)
- end
- end
+ expect do
+ get api("/projects/#{project.id}/pipelines/#{pipeline.id}/bridges", api_user), params: query
+ end.not_to exceed_all_query_limit(control_count)
end
end
- context 'with ci_jobs_finder_refactor ff disabled' do
- before do
- stub_feature_flags(ci_jobs_finder_refactor: false)
- end
-
- context 'authorized user' do
- it 'returns pipeline bridges' do
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- end
-
- it 'returns correct values' do
- expect(json_response).not_to be_empty
- expect(json_response.first['commit']['id']).to eq project.commit.id
- expect(json_response.first['id']).to eq bridge.id
- expect(json_response.first['name']).to eq bridge.name
- expect(json_response.first['stage']).to eq bridge.stage
- end
-
- it 'returns pipeline data' do
- json_bridge = json_response.first
+ context 'no pipeline is found' do
+ it 'does not return bridges' do
+ get api("/projects/#{project2.id}/pipelines/#{pipeline.id}/bridges", user)
- expect(json_bridge['pipeline']).not_to be_empty
- expect(json_bridge['pipeline']['id']).to eq bridge.pipeline.id
- expect(json_bridge['pipeline']['ref']).to eq bridge.pipeline.ref
- expect(json_bridge['pipeline']['sha']).to eq bridge.pipeline.sha
- expect(json_bridge['pipeline']['status']).to eq bridge.pipeline.status
- end
-
- it 'returns downstream pipeline data' do
- json_bridge = json_response.first
-
- expect(json_bridge['downstream_pipeline']).not_to be_empty
- expect(json_bridge['downstream_pipeline']['id']).to eq downstream_pipeline.id
- expect(json_bridge['downstream_pipeline']['ref']).to eq downstream_pipeline.ref
- expect(json_bridge['downstream_pipeline']['sha']).to eq downstream_pipeline.sha
- expect(json_bridge['downstream_pipeline']['status']).to eq downstream_pipeline.status
- end
-
- context 'filter bridges' do
- before_all do
- create_bridge(pipeline, :pending)
- create_bridge(pipeline, :running)
- end
-
- context 'with one scope element' do
- let(:query) { { 'scope' => 'pending' } }
-
- it :skip_before_request do
- get api("/projects/#{project.id}/pipelines/#{pipeline.id}/bridges", api_user), params: query
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response).to be_an Array
- expect(json_response.count).to eq 1
- expect(json_response.first["status"]).to eq "pending"
- end
- end
-
- context 'with array of scope elements' do
- let(:query) { { scope: %w(pending running) } }
-
- it :skip_before_request do
- get api("/projects/#{project.id}/pipelines/#{pipeline.id}/bridges", api_user), params: query
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response).to be_an Array
- expect(json_response.count).to eq 2
- json_response.each { |r| expect(%w(pending running).include?(r['status'])).to be true }
- end
- end
- end
-
- context 'respond 400 when scope contains invalid state' do
- context 'in an array' do
- let(:query) { { scope: %w(unknown running) } }
-
- it { expect(response).to have_gitlab_http_status(:bad_request) }
- end
-
- context 'in a hash' do
- let(:query) { { scope: { unknown: true } } }
-
- it { expect(response).to have_gitlab_http_status(:bad_request) }
- end
-
- context 'in a string' do
- let(:query) { { scope: "unknown" } }
-
- it { expect(response).to have_gitlab_http_status(:bad_request) }
- end
- end
-
- context 'bridges in different pipelines' do
- let!(:pipeline2) { create(:ci_empty_pipeline, project: project) }
- let!(:bridge2) { create(:ci_bridge, pipeline: pipeline2) }
-
- it 'excludes bridges from other pipelines' do
- json_response.each { |bridge| expect(bridge['pipeline']['id']).to eq(pipeline.id) }
- end
- end
-
- it 'avoids N+1 queries' do
- control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do
- get api("/projects/#{project.id}/pipelines/#{pipeline.id}/bridges", api_user), params: query
- end.count
-
- 3.times { create_bridge(pipeline) }
-
- expect do
- get api("/projects/#{project.id}/pipelines/#{pipeline.id}/bridges", api_user), params: query
- end.not_to exceed_all_query_limit(control_count)
- end
+ expect(json_response['message']).to eq '404 Project Not Found'
+ expect(response).to have_gitlab_http_status(:not_found)
end
+ end
- context 'no pipeline is found' do
- it 'does not return bridges' do
- get api("/projects/#{project2.id}/pipelines/#{pipeline.id}/bridges", user)
+ context 'unauthorized user' do
+ context 'when user is not logged in' do
+ let(:api_user) { nil }
+ it 'does not return bridges' do
expect(json_response['message']).to eq '404 Project Not Found'
expect(response).to have_gitlab_http_status(:not_found)
end
end
- context 'unauthorized user' do
- context 'when user is not logged in' do
- let(:api_user) { nil }
+ context 'when user is guest' do
+ let(:api_user) { guest }
+ let(:guest) { create(:project_member, :guest, project: project).user }
- it 'does not return bridges' do
- expect(json_response['message']).to eq '404 Project Not Found'
- expect(response).to have_gitlab_http_status(:not_found)
- end
+ it 'does not return bridges' do
+ expect(response).to have_gitlab_http_status(:forbidden)
end
+ end
- context 'when user is guest' do
- let(:api_user) { guest }
- let(:guest) { create(:project_member, :guest, project: project).user }
-
- it 'does not return bridges' do
- expect(response).to have_gitlab_http_status(:forbidden)
- end
+ context 'when user has no read_build access for project' do
+ before do
+ project.add_guest(api_user)
end
- context 'when user has no read_build access for project' do
- before do
- project.add_guest(api_user)
- end
-
- it 'does not return bridges' do
- get api("/projects/#{project.id}/pipelines/#{pipeline.id}/bridges", api_user)
- expect(response).to have_gitlab_http_status(:forbidden)
- end
+ it 'does not return bridges' do
+ get api("/projects/#{project.id}/pipelines/#{pipeline.id}/bridges", api_user)
+ expect(response).to have_gitlab_http_status(:forbidden)
end
end
end
diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb
index d455ed9c194..de2cfb8fea0 100644
--- a/spec/requests/api/commits_spec.rb
+++ b/spec/requests/api/commits_spec.rb
@@ -1991,6 +1991,17 @@ RSpec.describe API::Commits do
expect(json_response['x509_certificate']['x509_issuer']['subject']).to eq(commit.signature.x509_certificate.x509_issuer.subject)
expect(json_response['x509_certificate']['x509_issuer']['subject_key_identifier']).to eq(commit.signature.x509_certificate.x509_issuer.subject_key_identifier)
expect(json_response['x509_certificate']['x509_issuer']['crl_url']).to eq(commit.signature.x509_certificate.x509_issuer.crl_url)
+ expect(json_response['commit_source']).to eq('gitaly')
+ end
+
+ context 'with Rugged enabled', :enable_rugged do
+ it 'returns correct JSON' do
+ get api(route, current_user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['signature_type']).to eq('PGP')
+ expect(json_response['commit_source']).to eq('rugged')
+ end
end
end
end
diff --git a/spec/requests/api/container_repositories_spec.rb b/spec/requests/api/container_repositories_spec.rb
new file mode 100644
index 00000000000..8d7494ffce1
--- /dev/null
+++ b/spec/requests/api/container_repositories_spec.rb
@@ -0,0 +1,88 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe API::ContainerRepositories do
+ let_it_be(:project) { create(:project, :private) }
+ let_it_be(:reporter) { create(:user) }
+ let_it_be(:guest) { create(:user) }
+ let_it_be(:repository) { create(:container_repository, project: project) }
+
+ let(:users) do
+ {
+ anonymous: nil,
+ guest: guest,
+ reporter: reporter
+ }
+ end
+
+ let(:api_user) { reporter }
+
+ before do
+ project.add_reporter(reporter)
+ project.add_guest(guest)
+
+ stub_container_registry_config(enabled: true)
+ end
+
+ describe 'GET /registry/repositories/:id' do
+ let(:url) { "/registry/repositories/#{repository.id}" }
+
+ subject { get api(url, api_user) }
+
+ it_behaves_like 'rejected container repository access', :guest, :forbidden
+ it_behaves_like 'rejected container repository access', :anonymous, :unauthorized
+
+ context 'for allowed user' do
+ it 'returns a repository' do
+ subject
+
+ expect(json_response['id']).to eq(repository.id)
+ expect(response.body).not_to include('tags')
+ end
+
+ it 'returns a matching schema' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to match_response_schema('registry/repository')
+ end
+
+ context 'with tags param' do
+ let(:url) { "/registry/repositories/#{repository.id}?tags=true" }
+
+ before do
+ stub_container_registry_tags(repository: repository.path, tags: %w(rootA latest), with_manifest: true)
+ end
+
+ it 'returns a repository and its tags' do
+ subject
+
+ expect(json_response['id']).to eq(repository.id)
+ expect(response.body).to include('tags')
+ end
+ end
+
+ context 'with tags_count param' do
+ let(:url) { "/registry/repositories/#{repository.id}?tags_count=true" }
+
+ before do
+ stub_container_registry_tags(repository: repository.path, tags: %w(rootA latest), with_manifest: true)
+ end
+
+ it 'returns a repository and its tags_count' do
+ subject
+
+ expect(response.body).to include('tags_count')
+ expect(json_response['tags_count']).to eq(2)
+ end
+ end
+ end
+
+ context 'with invalid repository id' do
+ let(:url) { "/registry/repositories/#{non_existing_record_id}" }
+
+ it_behaves_like 'returning response status', :not_found
+ end
+ end
+end
diff --git a/spec/requests/api/dependency_proxy_spec.rb b/spec/requests/api/dependency_proxy_spec.rb
new file mode 100644
index 00000000000..d59f2bf06e3
--- /dev/null
+++ b/spec/requests/api/dependency_proxy_spec.rb
@@ -0,0 +1,72 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe API::DependencyProxy, api: true do
+ include ExclusiveLeaseHelpers
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:blob) { create(:dependency_proxy_blob )}
+ let_it_be(:group, reload: true) { blob.group }
+
+ before do
+ group.add_owner(user)
+ stub_config(dependency_proxy: { enabled: true })
+ stub_last_activity_update
+ group.create_dependency_proxy_setting!(enabled: true)
+ end
+
+ describe 'DELETE /groups/:id/dependency_proxy/cache' do
+ subject { delete api("/groups/#{group.id}/dependency_proxy/cache", user) }
+
+ context 'with feature available and enabled' do
+ let_it_be(:lease_key) { "dependency_proxy:delete_group_blobs:#{group.id}" }
+
+ context 'an admin user' do
+ it 'deletes the blobs and returns no content' do
+ stub_exclusive_lease(lease_key, timeout: 1.hour)
+ expect(PurgeDependencyProxyCacheWorker).to receive(:perform_async)
+
+ subject
+
+ expect(response).to have_gitlab_http_status(:no_content)
+ end
+
+ context 'called multiple times in one hour', :clean_gitlab_redis_shared_state do
+ it 'returns 409 with an error message' do
+ stub_exclusive_lease_taken(lease_key, timeout: 1.hour)
+
+ subject
+
+ expect(response).to have_gitlab_http_status(:conflict)
+ expect(response.body).to include('This request has already been made.')
+ end
+
+ it 'executes service only for the first time' do
+ expect(PurgeDependencyProxyCacheWorker).to receive(:perform_async).once
+
+ 2.times { subject }
+ end
+ end
+ end
+
+ context 'a non-admin' do
+ let(:user) { create(:user) }
+
+ before do
+ group.add_maintainer(user)
+ end
+
+ it_behaves_like 'returning response status', :forbidden
+ end
+ end
+
+ context 'depencency proxy is not enabled' do
+ before do
+ stub_config(dependency_proxy: { enabled: false })
+ end
+
+ it_behaves_like 'returning response status', :not_found
+ end
+ end
+end
diff --git a/spec/requests/api/feature_flags_user_lists_spec.rb b/spec/requests/api/feature_flags_user_lists_spec.rb
index 469210040dd..e2a3f92df10 100644
--- a/spec/requests/api/feature_flags_user_lists_spec.rb
+++ b/spec/requests/api/feature_flags_user_lists_spec.rb
@@ -95,6 +95,39 @@ RSpec.describe API::FeatureFlagsUserLists do
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to eq([])
end
+
+ context 'when filtering' do
+ it 'returns lists matching the search term' do
+ create_list(name: 'test_list', user_xids: 'user1')
+ create_list(name: 'list_b', user_xids: 'user1,user2,user3')
+
+ get api("/projects/#{project.id}/feature_flags_user_lists?search=test", developer)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response.map { |list| list['name'] }).to eq(['test_list'])
+ end
+
+ it 'returns lists matching multiple search terms' do
+ create_list(name: 'test_list', user_xids: 'user1')
+ create_list(name: 'list_b', user_xids: 'user1,user2,user3')
+ create_list(name: 'test_again', user_xids: 'user1,user2,user3')
+
+ get api("/projects/#{project.id}/feature_flags_user_lists?search=test list", developer)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response.map { |list| list['name'] }).to eq(['test_list'])
+ end
+
+ it 'returns all lists with no query' do
+ create_list(name: 'list_a', user_xids: 'user1')
+ create_list(name: 'list_b', user_xids: 'user1,user2,user3')
+
+ get api("/projects/#{project.id}/feature_flags_user_lists?search=", developer)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response.map { |list| list['name'] }.sort).to eq(%w[list_a list_b])
+ end
+ end
end
describe 'GET /projects/:id/feature_flags_user_lists/:iid' do
diff --git a/spec/requests/api/files_spec.rb b/spec/requests/api/files_spec.rb
index f77f127ddc8..8cd2f00a718 100644
--- a/spec/requests/api/files_spec.rb
+++ b/spec/requests/api/files_spec.rb
@@ -73,18 +73,20 @@ RSpec.describe API::Files do
describe "HEAD /projects/:id/repository/files/:file_path" do
shared_examples_for 'repository files' do
+ let(:options) { {} }
+
it 'returns 400 when file path is invalid' do
- head api(route(rouge_file_path), current_user), params: params
+ head api(route(rouge_file_path), current_user, **options), params: params
expect(response).to have_gitlab_http_status(:bad_request)
end
it_behaves_like 'when path is absolute' do
- subject { head api(route(absolute_path), current_user), params: params }
+ subject { head api(route(absolute_path), current_user, **options), params: params }
end
it 'returns file attributes in headers' do
- head api(route(file_path), current_user), params: params
+ head api(route(file_path), current_user, **options), params: params
expect(response).to have_gitlab_http_status(:ok)
expect(response.headers['X-Gitlab-File-Path']).to eq(CGI.unescape(file_path))
@@ -98,7 +100,7 @@ RSpec.describe API::Files do
file_path = "files%2Fjs%2Fcommit%2Ejs%2Ecoffee"
params[:ref] = "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9"
- head api(route(file_path), current_user), params: params
+ head api(route(file_path), current_user, **options), params: params
expect(response).to have_gitlab_http_status(:ok)
expect(response.headers['X-Gitlab-File-Name']).to eq('commit.js.coffee')
@@ -107,7 +109,7 @@ RSpec.describe API::Files do
context 'when mandatory params are not given' do
it "responds with a 400 status" do
- head api(route("any%2Ffile"), current_user)
+ head api(route("any%2Ffile"), current_user, **options)
expect(response).to have_gitlab_http_status(:bad_request)
end
@@ -117,7 +119,7 @@ RSpec.describe API::Files do
it "responds with a 404 status" do
params[:ref] = 'master'
- head api(route('app%2Fmodels%2Fapplication%2Erb'), current_user), params: params
+ head api(route('app%2Fmodels%2Fapplication%2Erb'), current_user, **options), params: params
expect(response).to have_gitlab_http_status(:not_found)
end
@@ -127,7 +129,7 @@ RSpec.describe API::Files do
include_context 'disabled repository'
it "responds with a 403 status" do
- head api(route(file_path), current_user), params: params
+ head api(route(file_path), current_user, **options), params: params
expect(response).to have_gitlab_http_status(:forbidden)
end
@@ -154,8 +156,8 @@ RSpec.describe API::Files do
context 'when PATs are used' do
it_behaves_like 'repository files' do
let(:token) { create(:personal_access_token, scopes: ['read_repository'], user: user) }
- let(:current_user) { user }
- let(:api_user) { { personal_access_token: token } }
+ let(:current_user) { nil }
+ let(:options) { { personal_access_token: token } }
end
end
@@ -174,21 +176,21 @@ RSpec.describe API::Files do
describe "GET /projects/:id/repository/files/:file_path" do
shared_examples_for 'repository files' do
- let(:api_user) { current_user }
+ let(:options) { {} }
it 'returns 400 for invalid file path' do
- get api(route(rouge_file_path), api_user), params: params
+ get api(route(rouge_file_path), api_user, **options), params: params
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['error']).to eq(invalid_file_message)
end
it_behaves_like 'when path is absolute' do
- subject { get api(route(absolute_path), api_user), params: params }
+ subject { get api(route(absolute_path), api_user, **options), params: params }
end
it 'returns file attributes as json' do
- get api(route(file_path), api_user), params: params
+ get api(route(file_path), api_user, **options), params: params
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['file_path']).to eq(CGI.unescape(file_path))
@@ -201,10 +203,10 @@ RSpec.describe API::Files do
it 'returns json when file has txt extension' do
file_path = "bar%2Fbranch-test.txt"
- get api(route(file_path), api_user), params: params
+ get api(route(file_path), api_user, **options), params: params
expect(response).to have_gitlab_http_status(:ok)
- expect(response.content_type).to eq('application/json')
+ expect(response.media_type).to eq('application/json')
end
context 'with filename with pathspec characters' do
@@ -218,7 +220,7 @@ RSpec.describe API::Files do
it 'returns JSON wth commit SHA' do
params[:ref] = 'master'
- get api(route(file_path), api_user), params: params
+ get api(route(file_path), api_user, **options), params: params
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['file_path']).to eq(file_path)
@@ -232,7 +234,7 @@ RSpec.describe API::Files do
file_path = "files%2Fjs%2Fcommit%2Ejs%2Ecoffee"
params[:ref] = "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9"
- get api(route(file_path), api_user), params: params
+ get api(route(file_path), api_user, **options), params: params
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['file_name']).to eq('commit.js.coffee')
@@ -244,7 +246,7 @@ RSpec.describe API::Files do
url = route(file_path) + "/raw"
expect(Gitlab::Workhorse).to receive(:send_git_blob)
- get api(url, api_user), params: params
+ get api(url, api_user, **options), params: params
expect(response).to have_gitlab_http_status(:ok)
expect(headers[Gitlab::Workhorse::DETECT_HEADER]).to eq "true"
@@ -253,7 +255,7 @@ RSpec.describe API::Files do
it 'returns blame file info' do
url = route(file_path) + '/blame'
- get api(url, api_user), params: params
+ get api(url, api_user, **options), params: params
expect(response).to have_gitlab_http_status(:ok)
end
@@ -261,14 +263,14 @@ RSpec.describe API::Files do
it 'sets inline content disposition by default' do
url = route(file_path) + "/raw"
- get api(url, api_user), params: params
+ get api(url, api_user, **options), params: params
expect(headers['Content-Disposition']).to eq(%q(inline; filename="popen.rb"; filename*=UTF-8''popen.rb))
end
context 'when mandatory params are not given' do
it_behaves_like '400 response' do
- let(:request) { get api(route("any%2Ffile"), current_user) }
+ let(:request) { get api(route("any%2Ffile"), current_user, **options) }
end
end
@@ -276,7 +278,7 @@ RSpec.describe API::Files do
let(:params) { { ref: 'master' } }
it_behaves_like '404 response' do
- let(:request) { get api(route('app%2Fmodels%2Fapplication%2Erb'), api_user), params: params }
+ let(:request) { get api(route('app%2Fmodels%2Fapplication%2Erb'), api_user, **options), params: params }
let(:message) { '404 File Not Found' }
end
end
@@ -285,7 +287,7 @@ RSpec.describe API::Files do
include_context 'disabled repository'
it_behaves_like '403 response' do
- let(:request) { get api(route(file_path), api_user), params: params }
+ let(:request) { get api(route(file_path), api_user, **options), params: params }
end
end
end
@@ -294,6 +296,7 @@ RSpec.describe API::Files do
it_behaves_like 'repository files' do
let(:project) { create(:project, :public, :repository) }
let(:current_user) { nil }
+ let(:api_user) { nil }
end
end
@@ -301,7 +304,8 @@ RSpec.describe API::Files do
it_behaves_like 'repository files' do
let(:token) { create(:personal_access_token, scopes: ['read_repository'], user: user) }
let(:current_user) { user }
- let(:api_user) { { personal_access_token: token } }
+ let(:api_user) { nil }
+ let(:options) { { personal_access_token: token } }
end
end
@@ -315,6 +319,7 @@ RSpec.describe API::Files do
context 'when authenticated', 'as a developer' do
it_behaves_like 'repository files' do
let(:current_user) { user }
+ let(:api_user) { user }
end
end
@@ -532,13 +537,16 @@ RSpec.describe API::Files do
expect(response).to have_gitlab_http_status(:ok)
end
- it_behaves_like 'uncached response' do
- before do
- url = route('.gitignore') + "/raw"
- expect(Gitlab::Workhorse).to receive(:send_git_blob)
+ it 'sets no-cache headers' do
+ url = route('.gitignore') + "/raw"
+ expect(Gitlab::Workhorse).to receive(:send_git_blob)
- get api(url, current_user), params: params
- end
+ get api(url, current_user), params: params
+
+ expect(response.headers["Cache-Control"]).to include("no-store")
+ expect(response.headers["Cache-Control"]).to include("no-cache")
+ expect(response.headers["Pragma"]).to eq("no-cache")
+ expect(response.headers["Expires"]).to eq("Fri, 01 Jan 1990 00:00:00 GMT")
end
context 'when mandatory params are not given' do
@@ -687,7 +695,7 @@ RSpec.describe API::Files do
post api(route("new_file_with_author%2Etxt"), user), params: params
expect(response).to have_gitlab_http_status(:created)
- expect(response.content_type).to eq('application/json')
+ expect(response.media_type).to eq('application/json')
last_commit = project.repository.commit.raw
expect(last_commit.author_email).to eq(author_email)
expect(last_commit.author_name).to eq(author_name)
diff --git a/spec/requests/api/generic_packages_spec.rb b/spec/requests/api/generic_packages_spec.rb
index 2cb686167f1..b8e79853486 100644
--- a/spec/requests/api/generic_packages_spec.rb
+++ b/spec/requests/api/generic_packages_spec.rb
@@ -195,7 +195,7 @@ RSpec.describe API::GenericPackages do
package = project.packages.generic.last
expect(package.name).to eq('mypackage')
expect(package.version).to eq('0.0.1')
- expect(package.build_info).to be_nil
+ expect(package.original_build_info).to be_nil
package_file = package.package_files.last
expect(package_file.file_name).to eq('myfile.tar.gz')
@@ -215,7 +215,7 @@ RSpec.describe API::GenericPackages do
package = project.packages.generic.last
expect(package.name).to eq('mypackage')
expect(package.version).to eq('0.0.1')
- expect(package.build_info.pipeline).to eq(ci_build.pipeline)
+ expect(package.original_build_info.pipeline).to eq(ci_build.pipeline)
package_file = package.package_files.last
expect(package_file.file_name).to eq('myfile.tar.gz')
diff --git a/spec/requests/api/graphql/ci/jobs_spec.rb b/spec/requests/api/graphql/ci/jobs_spec.rb
index 7d416f4720b..618705e5f94 100644
--- a/spec/requests/api/graphql/ci/jobs_spec.rb
+++ b/spec/requests/api/graphql/ci/jobs_spec.rb
@@ -34,6 +34,9 @@ RSpec.describe 'Query.project.pipeline.stages.groups.jobs' do
jobs {
nodes {
name
+ pipeline {
+ id
+ }
}
}
}
@@ -53,7 +56,7 @@ RSpec.describe 'Query.project.pipeline.stages.groups.jobs' do
end
context 'when fetching jobs from the pipeline' do
- it 'avoids N+1 queries' do
+ it 'avoids N+1 queries', :aggregate_failures do
control_count = ActiveRecord::QueryRecorder.new do
post_graphql(query, current_user: user)
end
@@ -86,8 +89,27 @@ RSpec.describe 'Query.project.pipeline.stages.groups.jobs' do
docker_jobs = docker_group.dig('jobs', 'nodes')
rspec_jobs = rspec_group.dig('jobs', 'nodes')
- expect(docker_jobs).to eq([{ 'name' => 'docker 1 2' }, { 'name' => 'docker 2 2' }])
- expect(rspec_jobs).to eq([{ 'name' => 'rspec 1 2' }, { 'name' => 'rspec 2 2' }])
+ expect(docker_jobs).to eq([
+ {
+ 'name' => 'docker 1 2',
+ 'pipeline' => { 'id' => pipeline.to_global_id.to_s }
+ },
+ {
+ 'name' => 'docker 2 2',
+ 'pipeline' => { 'id' => pipeline.to_global_id.to_s }
+ }
+ ])
+
+ expect(rspec_jobs).to eq([
+ {
+ 'name' => 'rspec 1 2',
+ 'pipeline' => { 'id' => pipeline.to_global_id.to_s }
+ },
+ {
+ 'name' => 'rspec 2 2',
+ 'pipeline' => { 'id' => pipeline.to_global_id.to_s }
+ }
+ ])
end
end
end
diff --git a/spec/requests/api/graphql/ci/pipelines_spec.rb b/spec/requests/api/graphql/ci/pipelines_spec.rb
new file mode 100644
index 00000000000..414ddabbac9
--- /dev/null
+++ b/spec/requests/api/graphql/ci/pipelines_spec.rb
@@ -0,0 +1,221 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Query.project(fullPath).pipelines' do
+ include GraphqlHelpers
+
+ let_it_be(:project) { create(:project, :repository, :public) }
+ let_it_be(:first_user) { create(:user) }
+ let_it_be(:second_user) { create(:user) }
+
+ describe '.jobs' do
+ let_it_be(:query) do
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ pipelines {
+ nodes {
+ jobs {
+ nodes {
+ name
+ }
+ }
+ }
+ }
+ }
+ }
+ )
+ end
+
+ it 'fetches the jobs without an N+1' do
+ pipeline = create(:ci_pipeline, project: project)
+ create(:ci_build, pipeline: pipeline, name: 'Job 1')
+
+ control_count = ActiveRecord::QueryRecorder.new do
+ post_graphql(query, current_user: first_user)
+ end
+
+ pipeline = create(:ci_pipeline, project: project)
+ create(:ci_build, pipeline: pipeline, name: 'Job 2')
+
+ expect do
+ post_graphql(query, current_user: second_user)
+ end.not_to exceed_query_limit(control_count)
+
+ expect(response).to have_gitlab_http_status(:ok)
+
+ pipelines_data = graphql_data.dig('project', 'pipelines', 'nodes')
+
+ job_names = pipelines_data.map do |pipeline_data|
+ jobs_data = pipeline_data.dig('jobs', 'nodes')
+ jobs_data.map { |job_data| job_data['name'] }
+ end.flatten
+
+ expect(job_names).to contain_exactly('Job 1', 'Job 2')
+ end
+ end
+
+ describe '.jobs(securityReportTypes)' do
+ let_it_be(:query) do
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ pipelines {
+ nodes {
+ jobs(securityReportTypes: [SAST]) {
+ nodes {
+ name
+ }
+ }
+ }
+ }
+ }
+ }
+ )
+ end
+
+ it 'fetches the jobs matching the report type filter' do
+ pipeline = create(:ci_pipeline, project: project)
+ create(:ci_build, :dast, name: 'DAST Job 1', pipeline: pipeline)
+ create(:ci_build, :sast, name: 'SAST Job 1', pipeline: pipeline)
+
+ post_graphql(query, current_user: first_user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+
+ pipelines_data = graphql_data.dig('project', 'pipelines', 'nodes')
+
+ job_names = pipelines_data.map do |pipeline_data|
+ jobs_data = pipeline_data.dig('jobs', 'nodes')
+ jobs_data.map { |job_data| job_data['name'] }
+ end.flatten
+
+ expect(job_names).to contain_exactly('SAST Job 1')
+ end
+ end
+
+ describe 'upstream' do
+ let_it_be(:pipeline) { create(:ci_pipeline, project: project, user: first_user) }
+ let_it_be(:upstream_project) { create(:project, :repository, :public) }
+ let_it_be(:upstream_pipeline) { create(:ci_pipeline, project: upstream_project, user: first_user) }
+ let(:upstream_pipelines_graphql_data) { graphql_data.dig(*%w[project pipelines nodes]).first['upstream'] }
+
+ let(:query) do
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ pipelines {
+ nodes {
+ upstream {
+ iid
+ }
+ }
+ }
+ }
+ }
+ )
+ end
+
+ before do
+ create(:ci_sources_pipeline, source_pipeline: upstream_pipeline, pipeline: pipeline )
+
+ post_graphql(query, current_user: first_user)
+ end
+
+ it_behaves_like 'a working graphql query'
+
+ it 'returns the upstream pipeline of a pipeline' do
+ expect(upstream_pipelines_graphql_data['iid'].to_i).to eq(upstream_pipeline.iid)
+ end
+
+ context 'when fetching the upstream pipeline from the pipeline' do
+ it 'avoids N+1 queries' do
+ control_count = ActiveRecord::QueryRecorder.new do
+ post_graphql(query, current_user: first_user)
+ end
+
+ pipeline_2 = create(:ci_pipeline, project: project, user: first_user)
+ upstream_pipeline_2 = create(:ci_pipeline, project: upstream_project, user: first_user)
+ create(:ci_sources_pipeline, source_pipeline: upstream_pipeline_2, pipeline: pipeline_2 )
+ pipeline_3 = create(:ci_pipeline, project: project, user: first_user)
+ upstream_pipeline_3 = create(:ci_pipeline, project: upstream_project, user: first_user)
+ create(:ci_sources_pipeline, source_pipeline: upstream_pipeline_3, pipeline: pipeline_3 )
+
+ expect do
+ post_graphql(query, current_user: second_user)
+ end.not_to exceed_query_limit(control_count)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+ end
+
+ describe 'downstream' do
+ let_it_be(:pipeline) { create(:ci_pipeline, project: project, user: first_user) }
+ let(:pipeline_2) { create(:ci_pipeline, project: project, user: first_user) }
+
+ let_it_be(:downstream_project) { create(:project, :repository, :public) }
+ let_it_be(:downstream_pipeline_a) { create(:ci_pipeline, project: downstream_project, user: first_user) }
+ let_it_be(:downstream_pipeline_b) { create(:ci_pipeline, project: downstream_project, user: first_user) }
+
+ let(:pipelines_graphql_data) { graphql_data.dig(*%w[project pipelines nodes]) }
+
+ let(:query) do
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ pipelines {
+ nodes {
+ downstream {
+ nodes {
+ iid
+ }
+ }
+ }
+ }
+ }
+ }
+ )
+ end
+
+ before do
+ create(:ci_sources_pipeline, source_pipeline: pipeline, pipeline: downstream_pipeline_a)
+ create(:ci_sources_pipeline, source_pipeline: pipeline_2, pipeline: downstream_pipeline_b)
+
+ post_graphql(query, current_user: first_user)
+ end
+
+ it_behaves_like 'a working graphql query'
+
+ it 'returns the downstream pipelines of a pipeline' do
+ downstream_pipelines_graphql_data = pipelines_graphql_data.map { |pip| pip['downstream']['nodes'] }.flatten
+
+ expect(
+ downstream_pipelines_graphql_data.map { |pip| pip['iid'].to_i }
+ ).to contain_exactly(downstream_pipeline_a.iid, downstream_pipeline_b.iid)
+ end
+
+ context 'when fetching the downstream pipelines from the pipeline' do
+ it 'avoids N+1 queries' do
+ control_count = ActiveRecord::QueryRecorder.new do
+ post_graphql(query, current_user: first_user)
+ end
+
+ downstream_pipeline_2a = create(:ci_pipeline, project: downstream_project, user: first_user)
+ create(:ci_sources_pipeline, source_pipeline: pipeline, pipeline: downstream_pipeline_2a)
+ downsteam_pipeline_3a = create(:ci_pipeline, project: downstream_project, user: first_user)
+ create(:ci_sources_pipeline, source_pipeline: pipeline, pipeline: downsteam_pipeline_3a)
+
+ downstream_pipeline_2b = create(:ci_pipeline, project: downstream_project, user: first_user)
+ create(:ci_sources_pipeline, source_pipeline: pipeline_2, pipeline: downstream_pipeline_2b)
+ downsteam_pipeline_3b = create(:ci_pipeline, project: downstream_project, user: first_user)
+ create(:ci_sources_pipeline, source_pipeline: pipeline_2, pipeline: downsteam_pipeline_3b)
+
+ expect do
+ post_graphql(query, current_user: second_user)
+ end.not_to exceed_query_limit(control_count)
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/container_repository/container_repository_details_spec.rb b/spec/requests/api/graphql/container_repository/container_repository_details_spec.rb
new file mode 100644
index 00000000000..3c1c63c1670
--- /dev/null
+++ b/spec/requests/api/graphql/container_repository/container_repository_details_spec.rb
@@ -0,0 +1,108 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe 'container repository details' do
+ using RSpec::Parameterized::TableSyntax
+ include GraphqlHelpers
+
+ let_it_be_with_reload(:project) { create(:project) }
+ let_it_be(:container_repository) { create(:container_repository, project: project) }
+
+ let(:query) do
+ graphql_query_for(
+ 'containerRepository',
+ { id: container_repository_global_id },
+ all_graphql_fields_for('ContainerRepositoryDetails')
+ )
+ end
+
+ let(:user) { project.owner }
+ let(:variables) { {} }
+ let(:tags) { %w(latest tag1 tag2 tag3 tag4 tag5) }
+ let(:container_repository_global_id) { container_repository.to_global_id.to_s }
+ let(:container_repository_details_response) { graphql_data.dig('containerRepository') }
+
+ before do
+ stub_container_registry_config(enabled: true)
+ stub_container_registry_tags(repository: container_repository.path, tags: tags, with_manifest: true)
+ end
+
+ subject { post_graphql(query, current_user: user, variables: variables) }
+
+ it_behaves_like 'a working graphql query' do
+ before do
+ subject
+ end
+
+ it 'matches the JSON schema' do
+ expect(container_repository_details_response).to match_schema('graphql/container_repository_details')
+ end
+ end
+
+ context 'with different permissions' do
+ let_it_be(:user) { create(:user) }
+
+ let(:tags_response) { container_repository_details_response.dig('tags', 'nodes') }
+
+ where(:project_visibility, :role, :access_granted, :can_delete) do
+ :private | :maintainer | true | true
+ :private | :developer | true | true
+ :private | :reporter | true | false
+ :private | :guest | false | false
+ :private | :anonymous | false | false
+ :public | :maintainer | true | true
+ :public | :developer | true | true
+ :public | :reporter | true | false
+ :public | :guest | true | false
+ :public | :anonymous | true | false
+ end
+
+ with_them do
+ before do
+ project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility.to_s.upcase, false))
+ project.add_user(user, role) unless role == :anonymous
+ end
+
+ it 'return the proper response' do
+ subject
+
+ if access_granted
+ expect(tags_response.size).to eq(tags.size)
+ expect(container_repository_details_response.dig('canDelete')).to eq(can_delete)
+ else
+ expect(container_repository_details_response).to eq(nil)
+ end
+ end
+ end
+ end
+
+ context 'limiting the number of tags' do
+ let(:limit) { 2 }
+ let(:tags_response) { container_repository_details_response.dig('tags', 'edges') }
+ let(:variables) do
+ { id: container_repository_global_id, n: limit }
+ end
+
+ let(:query) do
+ <<~GQL
+ query($id: ID!, $n: Int) {
+ containerRepository(id: $id) {
+ tags(first: $n) {
+ edges {
+ node {
+ #{all_graphql_fields_for('ContainerRepositoryTag')}
+ }
+ }
+ }
+ }
+ }
+ GQL
+ end
+
+ it 'only returns n tags' do
+ subject
+
+ expect(tags_response.size).to eq(limit)
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/custom_emoji_query_spec.rb b/spec/requests/api/graphql/custom_emoji_query_spec.rb
new file mode 100644
index 00000000000..d5a423d0eba
--- /dev/null
+++ b/spec/requests/api/graphql/custom_emoji_query_spec.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'getting custom emoji within namespace' do
+ include GraphqlHelpers
+
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:group) { create(:group, :private) }
+ let_it_be(:custom_emoji) { create(:custom_emoji, group: group) }
+
+ before do
+ stub_feature_flags(custom_emoji: true)
+ group.add_developer(current_user)
+ end
+
+ describe "Query CustomEmoji on Group" do
+ def custom_emoji_query(group)
+ graphql_query_for('group', 'fullPath' => group.full_path)
+ end
+
+ it 'returns emojis when authorised' do
+ post_graphql(custom_emoji_query(group), current_user: current_user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(graphql_data['group']['customEmoji']['nodes'].count). to eq(1)
+ expect(graphql_data['group']['customEmoji']['nodes'].first['name']). to eq(custom_emoji.name)
+ end
+
+ it 'returns nil when unauthorised' do
+ user = create(:user)
+ post_graphql(custom_emoji_query(group), current_user: user)
+
+ expect(graphql_data['group']).to be_nil
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/group/container_repositories_spec.rb b/spec/requests/api/graphql/group/container_repositories_spec.rb
new file mode 100644
index 00000000000..bcf689a5e8f
--- /dev/null
+++ b/spec/requests/api/graphql/group/container_repositories_spec.rb
@@ -0,0 +1,146 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe 'getting container repositories in a group' do
+ using RSpec::Parameterized::TableSyntax
+ include GraphqlHelpers
+
+ let_it_be(:owner) { create(:user) }
+ let_it_be_with_reload(:group) { create(:group) }
+ let_it_be_with_reload(:project) { create(:project, group: group) }
+ let_it_be(:container_repository) { create(:container_repository, project: project) }
+ let_it_be(:container_repositories_delete_scheduled) { create_list(:container_repository, 2, :status_delete_scheduled, project: project) }
+ let_it_be(:container_repositories_delete_failed) { create_list(:container_repository, 2, :status_delete_failed, project: project) }
+ let_it_be(:container_repositories) { [container_repository, container_repositories_delete_scheduled, container_repositories_delete_failed].flatten }
+ let_it_be(:container_expiration_policy) { project.container_expiration_policy }
+
+ let(:fields) do
+ <<~GQL
+ edges {
+ node {
+ #{all_graphql_fields_for('container_repositories'.classify)}
+ }
+ }
+ GQL
+ end
+
+ let(:query) do
+ graphql_query_for(
+ 'group',
+ { 'fullPath' => group.full_path },
+ query_graphql_field('container_repositories', {}, fields)
+ )
+ end
+
+ let(:user) { owner }
+ let(:variables) { {} }
+ let(:container_repositories_response) { graphql_data.dig('group', 'containerRepositories', 'edges') }
+
+ before do
+ group.add_owner(owner)
+ stub_container_registry_config(enabled: true)
+ container_repositories.each do |repository|
+ stub_container_registry_tags(repository: repository.path, tags: %w(tag1 tag2 tag3), with_manifest: false)
+ end
+ end
+
+ subject { post_graphql(query, current_user: user, variables: variables) }
+
+ it_behaves_like 'a working graphql query' do
+ before do
+ subject
+ end
+ end
+
+ context 'with different permissions' do
+ let_it_be(:user) { create(:user) }
+
+ where(:group_visibility, :role, :access_granted, :can_delete) do
+ :private | :maintainer | true | true
+ :private | :developer | true | true
+ :private | :reporter | true | false
+ :private | :guest | false | false
+ :private | :anonymous | false | false
+ :public | :maintainer | true | true
+ :public | :developer | true | true
+ :public | :reporter | true | false
+ :public | :guest | false | false
+ :public | :anonymous | false | false
+ end
+
+ with_them do
+ before do
+ group.update!(visibility_level: Gitlab::VisibilityLevel.const_get(group_visibility.to_s.upcase, false))
+ project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(group_visibility.to_s.upcase, false))
+
+ group.add_user(user, role) unless role == :anonymous
+ end
+
+ it 'return the proper response' do
+ subject
+
+ if access_granted
+ expect(container_repositories_response.size).to eq(container_repositories.size)
+ container_repositories_response.each do |repository_response|
+ expect(repository_response.dig('node', 'canDelete')).to eq(can_delete)
+ end
+ else
+ expect(container_repositories_response).to eq(nil)
+ end
+ end
+ end
+ end
+
+ context 'limiting the number of repositories' do
+ let(:limit) { 1 }
+ let(:variables) do
+ { path: group.full_path, n: limit }
+ end
+
+ let(:query) do
+ <<~GQL
+ query($path: ID!, $n: Int) {
+ group(fullPath: $path) {
+ containerRepositories(first: $n) { #{fields} }
+ }
+ }
+ GQL
+ end
+
+ it 'only returns N repositories' do
+ subject
+
+ expect(container_repositories_response.size).to eq(limit)
+ end
+ end
+
+ context 'filter by name' do
+ let_it_be(:container_repository) { create(:container_repository, name: 'fooBar', project: project) }
+
+ let(:name) { 'ooba' }
+ let(:query) do
+ <<~GQL
+ query($path: ID!, $name: String) {
+ group(fullPath: $path) {
+ containerRepositories(name: $name) { #{fields} }
+ }
+ }
+ GQL
+ end
+
+ let(:variables) do
+ { path: group.full_path, name: name }
+ end
+
+ before do
+ stub_container_registry_tags(repository: container_repository.path, tags: %w(tag4 tag5 tag6), with_manifest: false)
+ end
+
+ it 'returns the searched container repository' do
+ subject
+
+ expect(container_repositories_response.size).to eq(1)
+ expect(container_repositories_response.first.dig('node', 'id')).to eq(container_repository.to_global_id.to_s)
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/instance_statistics_measurements_spec.rb b/spec/requests/api/graphql/instance_statistics_measurements_spec.rb
index 5d7dbcf2e3c..eb73dc59253 100644
--- a/spec/requests/api/graphql/instance_statistics_measurements_spec.rb
+++ b/spec/requests/api/graphql/instance_statistics_measurements_spec.rb
@@ -9,7 +9,8 @@ RSpec.describe 'InstanceStatisticsMeasurements' do
let!(:instance_statistics_measurement_1) { create(:instance_statistics_measurement, :project_count, recorded_at: 20.days.ago, count: 5) }
let!(:instance_statistics_measurement_2) { create(:instance_statistics_measurement, :project_count, recorded_at: 10.days.ago, count: 10) }
- let(:query) { graphql_query_for(:instanceStatisticsMeasurements, 'identifier: PROJECTS', 'nodes { count identifier }') }
+ let(:arguments) { 'identifier: PROJECTS' }
+ let(:query) { graphql_query_for(:instanceStatisticsMeasurements, arguments, 'nodes { count identifier }') }
before do
post_graphql(query, current_user: current_user)
@@ -21,4 +22,14 @@ RSpec.describe 'InstanceStatisticsMeasurements' do
{ "count" => 5, 'identifier' => 'PROJECTS' }
])
end
+
+ context 'with recorded_at filters' do
+ let(:arguments) { %(identifier: PROJECTS, recordedAfter: "#{15.days.ago.to_date}", recordedBefore: "#{5.days.ago.to_date}") }
+
+ it 'returns filtered measurement objects' do
+ expect(graphql_data.dig('instanceStatisticsMeasurements', 'nodes')).to eq([
+ { "count" => 10, 'identifier' => 'PROJECTS' }
+ ])
+ end
+ end
end
diff --git a/spec/requests/api/graphql/issue/issue_spec.rb b/spec/requests/api/graphql/issue/issue_spec.rb
index 1c9d6b25856..d7fa680d29b 100644
--- a/spec/requests/api/graphql/issue/issue_spec.rb
+++ b/spec/requests/api/graphql/issue/issue_spec.rb
@@ -71,14 +71,34 @@ RSpec.describe 'Query.issue(id)' do
end
context 'selecting multiple fields' do
- let(:issue_fields) { %w(title description) }
+ let(:issue_fields) { ['title', 'description', 'updatedBy { username }'] }
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) )
+ expect(issue_data.keys).to eq( %w(title description updatedBy) )
expect(issue_data['title']).to eq(issue.title)
expect(issue_data['description']).to eq(issue.description)
+ expect(issue_data['updatedBy']['username']).to eq(issue.author.username)
+ end
+ end
+
+ context 'when issue got moved' do
+ let_it_be(:issue_fields) { ['moved', 'movedTo { title }'] }
+ let_it_be(:new_issue) { create(:issue) }
+ let_it_be(:issue) { create(:issue, project: project, moved_to: new_issue) }
+ let_it_be(:issue_params) { { 'id' => issue.to_global_id.to_s } }
+
+ before_all do
+ new_issue.project.add_developer(current_user)
+ end
+
+ it 'returns correct attributes' do
+ post_graphql(query, current_user: current_user)
+
+ expect(issue_data.keys).to eq( %w(moved movedTo) )
+ expect(issue_data['moved']).to eq(true)
+ expect(issue_data['movedTo']['title']).to eq(new_issue.title)
end
end
diff --git a/spec/requests/api/graphql/metrics/dashboard/annotations_spec.rb b/spec/requests/api/graphql/metrics/dashboard/annotations_spec.rb
index ca5a9165760..72ec2b8e070 100644
--- a/spec/requests/api/graphql/metrics/dashboard/annotations_spec.rb
+++ b/spec/requests/api/graphql/metrics/dashboard/annotations_spec.rb
@@ -17,6 +17,7 @@ RSpec.describe 'Getting Metrics Dashboard Annotations' do
let_it_be(:to_old_annotation) do
create(:metrics_dashboard_annotation, environment: environment, starting_at: Time.parse(from).advance(minutes: -5), dashboard_path: path)
end
+
let_it_be(:to_new_annotation) do
create(:metrics_dashboard_annotation, environment: environment, starting_at: to.advance(minutes: 5), dashboard_path: path)
end
diff --git a/spec/requests/api/graphql/mutations/alert_management/http_integration/create_spec.rb b/spec/requests/api/graphql/mutations/alert_management/http_integration/create_spec.rb
new file mode 100644
index 00000000000..a285cebc805
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/alert_management/http_integration/create_spec.rb
@@ -0,0 +1,67 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Creating a new HTTP Integration' do
+ include GraphqlHelpers
+
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:project) { create(:project) }
+ let(:variables) do
+ {
+ project_path: project.full_path,
+ active: false,
+ name: 'New HTTP Integration'
+ }
+ end
+
+ let(:mutation) do
+ graphql_mutation(:http_integration_create, variables) do
+ <<~QL
+ clientMutationId
+ errors
+ integration {
+ id
+ type
+ name
+ active
+ token
+ url
+ apiUrl
+ }
+ QL
+ end
+ end
+
+ let(:mutation_response) { graphql_mutation_response(:http_integration_create) }
+
+ before do
+ project.add_maintainer(current_user)
+ end
+
+ it 'creates a new integration' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ new_integration = ::AlertManagement::HttpIntegration.last!
+ integration_response = mutation_response['integration']
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(integration_response['id']).to eq(GitlabSchema.id_from_object(new_integration).to_s)
+ expect(integration_response['type']).to eq('HTTP')
+ expect(integration_response['name']).to eq(new_integration.name)
+ expect(integration_response['active']).to eq(new_integration.active)
+ expect(integration_response['token']).to eq(new_integration.token)
+ expect(integration_response['url']).to eq(new_integration.url)
+ expect(integration_response['apiUrl']).to eq(nil)
+ end
+
+ [:project_path, :active, :name].each do |argument|
+ context "without required argument #{argument}" do
+ before do
+ variables.delete(argument)
+ end
+
+ it_behaves_like 'an invalid argument to the mutation', argument_name: argument
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/alert_management/http_integration/destroy_spec.rb b/spec/requests/api/graphql/mutations/alert_management/http_integration/destroy_spec.rb
new file mode 100644
index 00000000000..1ecb5c76b57
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/alert_management/http_integration/destroy_spec.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Removing an HTTP Integration' do
+ include GraphqlHelpers
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project) }
+ let_it_be(:integration) { create(:alert_management_http_integration, project: project) }
+
+ let(:mutation) do
+ variables = {
+ id: GitlabSchema.id_from_object(integration).to_s
+ }
+ graphql_mutation(:http_integration_destroy, variables) do
+ <<~QL
+ clientMutationId
+ errors
+ integration {
+ id
+ type
+ name
+ active
+ token
+ url
+ apiUrl
+ }
+ QL
+ end
+ end
+
+ let(:mutation_response) { graphql_mutation_response(:http_integration_destroy) }
+
+ before do
+ project.add_maintainer(user)
+ end
+
+ it 'removes the integration' do
+ post_graphql_mutation(mutation, current_user: user)
+
+ integration_response = mutation_response['integration']
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(integration_response['id']).to eq(GitlabSchema.id_from_object(integration).to_s)
+ expect(integration_response['type']).to eq('HTTP')
+ expect(integration_response['name']).to eq(integration.name)
+ expect(integration_response['active']).to eq(integration.active)
+ expect(integration_response['token']).to eq(integration.token)
+ expect(integration_response['url']).to eq(integration.url)
+ expect(integration_response['apiUrl']).to eq(nil)
+
+ expect { integration.reload }.to raise_error ActiveRecord::RecordNotFound
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/alert_management/http_integration/reset_token_spec.rb b/spec/requests/api/graphql/mutations/alert_management/http_integration/reset_token_spec.rb
new file mode 100644
index 00000000000..badd9412589
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/alert_management/http_integration/reset_token_spec.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Resetting a token on an existing HTTP Integration' do
+ include GraphqlHelpers
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project) }
+ let_it_be(:integration) { create(:alert_management_http_integration, project: project) }
+
+ let(:mutation) do
+ variables = {
+ id: GitlabSchema.id_from_object(integration).to_s
+ }
+ graphql_mutation(:http_integration_reset_token, variables) do
+ <<~QL
+ clientMutationId
+ errors
+ integration {
+ id
+ token
+ }
+ QL
+ end
+ end
+
+ let(:mutation_response) { graphql_mutation_response(:http_integration_reset_token) }
+
+ before do
+ project.add_maintainer(user)
+ end
+
+ it 'updates the integration' do
+ previous_token = integration.token
+
+ post_graphql_mutation(mutation, current_user: user)
+ integration_response = mutation_response['integration']
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(integration_response['id']).to eq(GitlabSchema.id_from_object(integration).to_s)
+ expect(integration_response['token']).not_to eq(previous_token)
+ expect(integration_response['token']).to eq(integration.reload.token)
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/alert_management/http_integration/update_spec.rb b/spec/requests/api/graphql/mutations/alert_management/http_integration/update_spec.rb
new file mode 100644
index 00000000000..bf7eb3d980c
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/alert_management/http_integration/update_spec.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Updating an existing HTTP Integration' do
+ include GraphqlHelpers
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project) }
+ let_it_be(:integration) { create(:alert_management_http_integration, project: project) }
+
+ let(:mutation) do
+ variables = {
+ id: GitlabSchema.id_from_object(integration).to_s,
+ name: 'Modified Name',
+ active: false
+ }
+ graphql_mutation(:http_integration_update, variables) do
+ <<~QL
+ clientMutationId
+ errors
+ integration {
+ id
+ name
+ active
+ url
+ }
+ QL
+ end
+ end
+
+ let(:mutation_response) { graphql_mutation_response(:http_integration_update) }
+
+ before do
+ project.add_maintainer(user)
+ end
+
+ it 'updates the integration' do
+ post_graphql_mutation(mutation, current_user: user)
+
+ integration_response = mutation_response['integration']
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(integration_response['id']).to eq(GitlabSchema.id_from_object(integration).to_s)
+ expect(integration_response['name']).to eq('Modified Name')
+ expect(integration_response['active']).to be_falsey
+ expect(integration_response['url']).to include('modified-name')
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/alert_management/prometheus_integration/create_spec.rb b/spec/requests/api/graphql/mutations/alert_management/prometheus_integration/create_spec.rb
new file mode 100644
index 00000000000..0ef61ae0d5b
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/alert_management/prometheus_integration/create_spec.rb
@@ -0,0 +1,67 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Creating a new Prometheus Integration' do
+ include GraphqlHelpers
+
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:project) { create(:project) }
+ let(:variables) do
+ {
+ project_path: project.full_path,
+ active: false,
+ api_url: 'https://prometheus-url.com'
+ }
+ end
+
+ let(:mutation) do
+ graphql_mutation(:prometheus_integration_create, variables) do
+ <<~QL
+ clientMutationId
+ errors
+ integration {
+ id
+ type
+ name
+ active
+ token
+ url
+ apiUrl
+ }
+ QL
+ end
+ end
+
+ let(:mutation_response) { graphql_mutation_response(:prometheus_integration_create) }
+
+ before do
+ project.add_maintainer(current_user)
+ end
+
+ it 'creates a new integration' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ new_integration = ::PrometheusService.last!
+ integration_response = mutation_response['integration']
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(integration_response['id']).to eq(GitlabSchema.id_from_object(new_integration).to_s)
+ expect(integration_response['type']).to eq('PROMETHEUS')
+ expect(integration_response['name']).to eq(new_integration.title)
+ expect(integration_response['active']).to eq(new_integration.manual_configuration?)
+ expect(integration_response['token']).to eq(new_integration.project.alerting_setting.token)
+ expect(integration_response['url']).to eq("http://localhost/#{project.full_path}/prometheus/alerts/notify.json")
+ expect(integration_response['apiUrl']).to eq(new_integration.api_url)
+ end
+
+ [:project_path, :active, :api_url].each do |argument|
+ context "without required argument #{argument}" do
+ before do
+ variables.delete(argument)
+ end
+
+ it_behaves_like 'an invalid argument to the mutation', argument_name: argument
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/alert_management/prometheus_integration/reset_token_spec.rb b/spec/requests/api/graphql/mutations/alert_management/prometheus_integration/reset_token_spec.rb
new file mode 100644
index 00000000000..d8d0ace5981
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/alert_management/prometheus_integration/reset_token_spec.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Resetting a token on an existing Prometheus Integration' do
+ include GraphqlHelpers
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project) }
+ let_it_be(:integration) { create(:prometheus_service, project: project) }
+
+ let(:mutation) do
+ variables = {
+ id: GitlabSchema.id_from_object(integration).to_s
+ }
+ graphql_mutation(:prometheus_integration_reset_token, variables) do
+ <<~QL
+ clientMutationId
+ errors
+ integration {
+ id
+ token
+ }
+ QL
+ end
+ end
+
+ let(:mutation_response) { graphql_mutation_response(:prometheus_integration_reset_token) }
+
+ before do
+ project.add_maintainer(user)
+ end
+
+ it 'creates a token' do
+ post_graphql_mutation(mutation, current_user: user)
+ integration_response = mutation_response['integration']
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(integration_response['id']).to eq(GitlabSchema.id_from_object(integration).to_s)
+ expect(integration_response['token']).not_to be_nil
+ expect(integration_response['token']).to eq(project.alerting_setting.token)
+ end
+
+ context 'with an existing alerting setting' do
+ let_it_be(:alerting_setting) { create(:project_alerting_setting, project: project) }
+
+ it 'updates the token' do
+ previous_token = alerting_setting.token
+
+ post_graphql_mutation(mutation, current_user: user)
+ integration_response = mutation_response['integration']
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(integration_response['id']).to eq(GitlabSchema.id_from_object(integration).to_s)
+ expect(integration_response['token']).not_to eq(previous_token)
+ expect(integration_response['token']).to eq(alerting_setting.reload.token)
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/alert_management/prometheus_integration/update_spec.rb b/spec/requests/api/graphql/mutations/alert_management/prometheus_integration/update_spec.rb
new file mode 100644
index 00000000000..6c4a647a353
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/alert_management/prometheus_integration/update_spec.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Updating an existing Prometheus Integration' do
+ include GraphqlHelpers
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project) }
+ let_it_be(:integration) { create(:prometheus_service, project: project) }
+
+ let(:mutation) do
+ variables = {
+ id: GitlabSchema.id_from_object(integration).to_s,
+ api_url: 'http://modified-url.com',
+ active: true
+ }
+ graphql_mutation(:prometheus_integration_update, variables) do
+ <<~QL
+ clientMutationId
+ errors
+ integration {
+ id
+ active
+ apiUrl
+ }
+ QL
+ end
+ end
+
+ let(:mutation_response) { graphql_mutation_response(:prometheus_integration_update) }
+
+ before do
+ project.add_maintainer(user)
+ end
+
+ it 'updates the integration' do
+ post_graphql_mutation(mutation, current_user: user)
+
+ integration_response = mutation_response['integration']
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(integration_response['id']).to eq(GitlabSchema.id_from_object(integration).to_s)
+ expect(integration_response['apiUrl']).to eq('http://modified-url.com')
+ expect(integration_response['active']).to be_truthy
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/commits/create_spec.rb b/spec/requests/api/graphql/mutations/commits/create_spec.rb
index ac4fa7cfe83..375d4f10b40 100644
--- a/spec/requests/api/graphql/mutations/commits/create_spec.rb
+++ b/spec/requests/api/graphql/mutations/commits/create_spec.rb
@@ -23,6 +23,18 @@ RSpec.describe 'Creation of a new commit' do
let(:mutation) { graphql_mutation(:commit_create, input) }
let(:mutation_response) { graphql_mutation_response(:commit_create) }
+ shared_examples 'a commit is successful' do
+ it 'creates a new commit' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(response).to have_gitlab_http_status(:success)
+
+ expect(mutation_response['commit']).to include(
+ 'title' => message
+ )
+ end
+ end
+
context 'the user is not allowed to create a commit' do
it_behaves_like 'a mutation that returns a top-level access error'
end
@@ -32,14 +44,7 @@ RSpec.describe 'Creation of a new commit' do
project.add_developer(current_user)
end
- it 'creates a new commit' do
- post_graphql_mutation(mutation, current_user: current_user)
-
- expect(response).to have_gitlab_http_status(:success)
- expect(mutation_response['commit']).to include(
- 'title' => message
- )
- end
+ it_behaves_like 'a commit is successful'
context 'when branch is not correct' do
let(:branch) { 'unknown' }
@@ -47,5 +52,22 @@ RSpec.describe 'Creation of a new commit' do
it_behaves_like 'a mutation that returns errors in the response',
errors: ['You can only create or edit files when you are on a branch']
end
+
+ context 'when branch is new, and a start_branch is defined' do
+ let(:input) { { project_path: project.full_path, branch: branch, start_branch: start_branch, message: message, actions: actions } }
+ let(:branch) { 'new-branch' }
+ let(:start_branch) { 'master' }
+ let(:actions) do
+ [
+ {
+ action: 'CREATE',
+ filePath: 'ANOTHER_FILE.md',
+ content: 'Bye'
+ }
+ ]
+ end
+
+ it_behaves_like 'a commit is successful'
+ end
end
end
diff --git a/spec/requests/api/graphql/mutations/container_expiration_policy/update_spec.rb b/spec/requests/api/graphql/mutations/container_expiration_policy/update_spec.rb
index 7bef812bfec..23e8e366483 100644
--- a/spec/requests/api/graphql/mutations/container_expiration_policy/update_spec.rb
+++ b/spec/requests/api/graphql/mutations/container_expiration_policy/update_spec.rb
@@ -73,6 +73,29 @@ RSpec.describe 'Updating the container expiration policy' do
end
end
+ RSpec.shared_examples 'rejecting blank name_regex when enabled' do
+ context "for blank name_regex" do
+ let(:params) do
+ {
+ project_path: project.full_path,
+ name_regex: '',
+ enabled: true
+ }
+ end
+
+ it_behaves_like 'returning response status', :success
+
+ it_behaves_like 'not creating the container expiration policy'
+
+ it 'returns an error' do
+ subject
+
+ expect(graphql_data['updateContainerExpirationPolicy']['errors'].size).to eq(1)
+ expect(graphql_data['updateContainerExpirationPolicy']['errors']).to include("Name regex can't be blank")
+ end
+ end
+ end
+
RSpec.shared_examples 'accepting the mutation request updating the container expiration policy' do
it_behaves_like 'updating the container expiration policy attributes', mode: :update, from: { cadence: '1d', keep_n: 10, older_than: '90d' }, to: { cadence: '3month', keep_n: 100, older_than: '14d' }
@@ -80,6 +103,7 @@ RSpec.describe 'Updating the container expiration policy' do
it_behaves_like 'rejecting invalid regex for', :name_regex
it_behaves_like 'rejecting invalid regex for', :name_regex_keep
+ it_behaves_like 'rejecting blank name_regex when enabled'
end
RSpec.shared_examples 'accepting the mutation request creating the container expiration policy' do
@@ -89,6 +113,7 @@ RSpec.describe 'Updating the container expiration policy' do
it_behaves_like 'rejecting invalid regex for', :name_regex
it_behaves_like 'rejecting invalid regex for', :name_regex_keep
+ it_behaves_like 'rejecting blank name_regex when enabled'
end
RSpec.shared_examples 'denying the mutation request' do
diff --git a/spec/requests/api/graphql/mutations/container_repository/destroy_spec.rb b/spec/requests/api/graphql/mutations/container_repository/destroy_spec.rb
new file mode 100644
index 00000000000..645edfc2e43
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/container_repository/destroy_spec.rb
@@ -0,0 +1,91 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Destroying a container repository' do
+ using RSpec::Parameterized::TableSyntax
+
+ include GraphqlHelpers
+
+ let_it_be_with_reload(:container_repository) { create(:container_repository) }
+ let_it_be(:user) { create(:user) }
+
+ let(:project) { container_repository.project }
+ let(:id) { container_repository.to_global_id.to_s }
+
+ let(:query) do
+ <<~GQL
+ containerRepository {
+ #{all_graphql_fields_for('ContainerRepository')}
+ }
+ errors
+ GQL
+ end
+
+ let(:params) { { id: container_repository.to_global_id.to_s } }
+ let(:mutation) { graphql_mutation(:destroy_container_repository, params, query) }
+ let(:mutation_response) { graphql_mutation_response(:destroyContainerRepository) }
+ let(:container_repository_mutation_response) { mutation_response['containerRepository'] }
+
+ before do
+ stub_container_registry_config(enabled: true)
+ stub_container_registry_tags(tags: %w[a b c])
+ end
+
+ shared_examples 'destroying the container repository' do
+ it 'destroy the container repository' do
+ expect(::Packages::CreateEventService)
+ .to receive(:new).with(nil, user, event_name: :delete_repository, scope: :container).and_call_original
+ expect(DeleteContainerRepositoryWorker)
+ .to receive(:perform_async).with(user.id, container_repository.id)
+
+ expect { subject }.to change { ::Packages::Event.count }.by(1)
+
+ expect(container_repository_mutation_response).to match_schema('graphql/container_repository')
+ expect(container_repository_mutation_response['status']).to eq('DELETE_SCHEDULED')
+ end
+
+ it_behaves_like 'returning response status', :success
+ end
+
+ shared_examples 'denying the mutation request' do
+ it 'does not destroy the container repository' do
+ expect(DeleteContainerRepositoryWorker)
+ .not_to receive(:perform_async).with(user.id, container_repository.id)
+
+ expect { subject }.not_to change { ::Packages::Event.count }
+
+ expect(mutation_response).to be_nil
+ end
+
+ it_behaves_like 'returning response status', :success
+ end
+
+ describe 'post graphql mutation' do
+ subject { post_graphql_mutation(mutation, current_user: user) }
+
+ context 'with valid id' do
+ where(:user_role, :shared_examples_name) do
+ :maintainer | 'destroying the container repository'
+ :developer | 'destroying the container repository'
+ :reporter | 'denying the mutation request'
+ :guest | 'denying the mutation request'
+ :anonymous | 'denying the mutation request'
+ end
+
+ with_them do
+ before do
+ project.send("add_#{user_role}", user) unless user_role == :anonymous
+ end
+
+ it_behaves_like params[:shared_examples_name]
+ end
+ end
+
+ context 'with invalid id' do
+ let(:params) { { id: 'gid://gitlab/ContainerRepository/5555' } }
+
+ it_behaves_like 'denying the mutation request'
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/custom_emoji/create_spec.rb b/spec/requests/api/graphql/mutations/custom_emoji/create_spec.rb
new file mode 100644
index 00000000000..c91437fa355
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/custom_emoji/create_spec.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Creation of a new Custom Emoji' do
+ include GraphqlHelpers
+
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:group) { create(:group) }
+
+ let(:attributes) do
+ {
+ name: 'my_new_emoji',
+ url: 'https://example.com/image.png',
+ group_path: group.full_path
+ }
+ end
+
+ let(:mutation) do
+ graphql_mutation(:create_custom_emoji, attributes)
+ end
+
+ context 'when the user has no permission' do
+ it 'does not create custom emoji' do
+ expect { post_graphql_mutation(mutation, current_user: current_user) }.not_to change(CustomEmoji, :count)
+ end
+ end
+
+ context 'when user has permission' do
+ before do
+ group.add_developer(current_user)
+ end
+
+ it 'creates custom emoji' do
+ expect { post_graphql_mutation(mutation, current_user: current_user) }.to change(CustomEmoji, :count).by(1)
+
+ gql_response = graphql_mutation_response(:create_custom_emoji)
+ expect(gql_response['errors']).to eq([])
+ expect(gql_response['customEmoji']['name']).to eq(attributes[:name])
+ expect(gql_response['customEmoji']['url']).to eq(attributes[:url])
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/labels/create_spec.rb b/spec/requests/api/graphql/mutations/labels/create_spec.rb
new file mode 100644
index 00000000000..28284408306
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/labels/create_spec.rb
@@ -0,0 +1,86 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Mutations::Labels::Create do
+ include GraphqlHelpers
+
+ let_it_be(:current_user) { create(:user) }
+
+ let(:params) do
+ {
+ 'title' => 'foo',
+ 'description' => 'some description',
+ 'color' => '#FF0000'
+ }
+ end
+
+ let(:mutation) { graphql_mutation(:label_create, params.merge(extra_params)) }
+
+ subject { post_graphql_mutation(mutation, current_user: current_user) }
+
+ def mutation_response
+ graphql_mutation_response(:label_create)
+ end
+
+ shared_examples_for 'labels create mutation' do
+ context 'when the user does not have permission to create a label' do
+ it_behaves_like 'a mutation that returns a top-level access error'
+
+ it 'does not create the label' do
+ expect { subject }.not_to change { Label.count }
+ end
+ end
+
+ context 'when the user has permission to create a label' do
+ before do
+ parent.add_developer(current_user)
+ end
+
+ context 'when the parent (project_path or group_path) param is given' do
+ it 'creates the label' do
+ expect { subject }.to change { Label.count }.to(1)
+
+ expect(mutation_response).to include(
+ 'label' => a_hash_including(params))
+ end
+
+ it 'does not create a label when there are errors' do
+ label_factory = parent.is_a?(Group) ? :group_label : :label
+ create(label_factory, title: 'foo', parent.class.name.underscore.to_sym => parent)
+
+ expect { subject }.not_to change { Label.count }
+
+ expect(mutation_response).to have_key('label')
+ expect(mutation_response['label']).to be_nil
+ expect(mutation_response['errors'].first).to eq('Title has already been taken')
+ end
+ end
+ end
+ end
+
+ context 'when creating a project label' do
+ let_it_be(:parent) { create(:project) }
+ let(:extra_params) { { project_path: parent.full_path } }
+
+ it_behaves_like 'labels create mutation'
+ end
+
+ context 'when creating a group label' do
+ let_it_be(:parent) { create(:group) }
+ let(:extra_params) { { group_path: parent.full_path } }
+
+ it_behaves_like 'labels create mutation'
+ end
+
+ context 'when neither project_path nor group_path param is given' do
+ let(:mutation) { graphql_mutation(:label_create, params) }
+
+ it_behaves_like 'a mutation that returns top-level errors',
+ errors: ['Exactly one of group_path or project_path arguments is required']
+
+ it 'does not create the label' do
+ expect { subject }.not_to change { Label.count }
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/metrics/dashboard/annotations/create_spec.rb b/spec/requests/api/graphql/mutations/metrics/dashboard/annotations/create_spec.rb
index 81d13b29dde..2a39757e103 100644
--- a/spec/requests/api/graphql/mutations/metrics/dashboard/annotations/create_spec.rb
+++ b/spec/requests/api/graphql/mutations/metrics/dashboard/annotations/create_spec.rb
@@ -101,9 +101,7 @@ RSpec.describe Mutations::Metrics::Dashboard::Annotations::Create do
graphql_mutation(:create_annotation, variables)
end
- it_behaves_like 'a mutation that returns top-level errors' do
- let(:match_errors) { include(/is not a valid Global ID/) }
- end
+ it_behaves_like 'an invalid argument to the mutation', argument_name: :environment_id
end
end
end
@@ -190,9 +188,7 @@ RSpec.describe Mutations::Metrics::Dashboard::Annotations::Create do
graphql_mutation(:create_annotation, variables)
end
- it_behaves_like 'a mutation that returns top-level errors' do
- let(:match_errors) { include(/is not a valid Global ID/) }
- end
+ it_behaves_like 'an invalid argument to the mutation', argument_name: :cluster_id
end
end
@@ -213,35 +209,26 @@ RSpec.describe Mutations::Metrics::Dashboard::Annotations::Create do
it_behaves_like 'a mutation that returns top-level errors', errors: [described_class::ANNOTATION_SOURCE_ARGUMENT_ERROR]
end
- context 'when a non-cluster or environment id is provided' do
- let(:gid) { { environment_id: project.to_global_id.to_s } }
- let(:mutation) do
- variables = {
- starting_at: starting_at,
- ending_at: ending_at,
- dashboard_path: dashboard_path,
- description: description
- }.merge!(gid)
-
- graphql_mutation(:create_annotation, variables)
- end
-
- before do
- project.add_developer(current_user)
- end
+ [:environment_id, :cluster_id].each do |arg_name|
+ context "when #{arg_name} is given an ID of the wrong type" do
+ let(:gid) { global_id_of(project) }
+ let(:mutation) do
+ variables = {
+ starting_at: starting_at,
+ ending_at: ending_at,
+ dashboard_path: dashboard_path,
+ description: description,
+ arg_name => gid
+ }
- describe 'non-environment id' do
- it_behaves_like 'a mutation that returns top-level errors' do
- let(:match_errors) { include(/does not represent an instance of Environment/) }
+ graphql_mutation(:create_annotation, variables)
end
- end
-
- describe 'non-cluster id' do
- let(:gid) { { cluster_id: project.to_global_id.to_s } }
- it_behaves_like 'a mutation that returns top-level errors' do
- let(:match_errors) { include(/does not represent an instance of Clusters::Cluster/) }
+ before do
+ project.add_developer(current_user)
end
+
+ it_behaves_like 'an invalid argument to the mutation', argument_name: arg_name
end
end
end
diff --git a/spec/requests/api/graphql/mutations/metrics/dashboard/annotations/delete_spec.rb b/spec/requests/api/graphql/mutations/metrics/dashboard/annotations/delete_spec.rb
index 9a612c841a2..b956734068c 100644
--- a/spec/requests/api/graphql/mutations/metrics/dashboard/annotations/delete_spec.rb
+++ b/spec/requests/api/graphql/mutations/metrics/dashboard/annotations/delete_spec.rb
@@ -36,7 +36,7 @@ RSpec.describe Mutations::Metrics::Dashboard::Annotations::Delete do
let(:variables) { { id: GitlabSchema.id_from_object(project).to_s } }
it_behaves_like 'a mutation that returns top-level errors' do
- let(:match_errors) { eq(["#{variables[:id]} is not a valid ID for #{annotation.class}."]) }
+ let(:match_errors) { contain_exactly(include('invalid value for id')) }
end
end
diff --git a/spec/requests/api/graphql/mutations/notes/reposition_image_diff_note_spec.rb b/spec/requests/api/graphql/mutations/notes/reposition_image_diff_note_spec.rb
new file mode 100644
index 00000000000..4efa7f9d509
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/notes/reposition_image_diff_note_spec.rb
@@ -0,0 +1,81 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Repositioning an ImageDiffNote' do
+ include GraphqlHelpers
+
+ let_it_be(:noteable) { create(:merge_request) }
+ let_it_be(:project) { noteable.project }
+ let(:note) { create(:image_diff_note_on_merge_request, noteable: noteable, project: project) }
+ let(:new_position) { { x: 10 } }
+ let(:current_user) { project.creator }
+
+ let(:mutation_variables) do
+ {
+ id: global_id_of(note),
+ position: new_position
+ }
+ end
+
+ let(:mutation) do
+ graphql_mutation(:reposition_image_diff_note, mutation_variables) do
+ <<~QL
+ note {
+ id
+ }
+ errors
+ QL
+ end
+ end
+
+ def mutation_response
+ graphql_mutation_response(:reposition_image_diff_note)
+ end
+
+ it 'updates the note', :aggregate_failures do
+ expect do
+ post_graphql_mutation(mutation, current_user: current_user)
+ end.to change { note.reset.position.x }.to(10)
+
+ expect(mutation_response['note']).to eq('id' => global_id_of(note))
+ expect(mutation_response['errors']).to be_empty
+ end
+
+ context 'when the note is not a DiffNote' do
+ let(:note) { project }
+
+ it_behaves_like 'a mutation that returns top-level errors' do
+ let(:match_errors) { include(/does not represent an instance of DiffNote/) }
+ end
+ end
+
+ context 'when a position arg is nil' do
+ let(:new_position) { { x: nil, y: 10 } }
+
+ it 'does not set the property to nil', :aggregate_failures do
+ expect do
+ post_graphql_mutation(mutation, current_user: current_user)
+ end.not_to change { note.reset.position.x }
+
+ expect(mutation_response['note']).to eq('id' => global_id_of(note))
+ expect(mutation_response['errors']).to be_empty
+ end
+ end
+
+ context 'when all position args are nil' do
+ let(:new_position) { { x: nil } }
+
+ it_behaves_like 'a mutation that returns top-level errors' do
+ let(:match_errors) { include(/RepositionImageDiffNoteInput! was provided invalid value/) }
+ end
+
+ it 'contains an explanation for the error' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ explanation = graphql_errors.first['extensions']['problems'].first['explanation']
+
+ expect(explanation).to eq('At least one property of `UpdateDiffImagePositionInput` must be set')
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/notes/update/image_diff_note_spec.rb b/spec/requests/api/graphql/mutations/notes/update/image_diff_note_spec.rb
index efa2ceb65c2..713b26a6a9b 100644
--- a/spec/requests/api/graphql/mutations/notes/update/image_diff_note_spec.rb
+++ b/spec/requests/api/graphql/mutations/notes/update/image_diff_note_spec.rb
@@ -20,6 +20,7 @@ RSpec.describe 'Updating an image DiffNote' do
position_type: 'image'
)
end
+
let_it_be(:updated_body) { 'Updated body' }
let_it_be(:updated_width) { 50 }
let_it_be(:updated_height) { 100 }
@@ -31,7 +32,7 @@ RSpec.describe 'Updating an image DiffNote' do
height: updated_height,
x: updated_x,
y: updated_y
- }
+ }.compact.presence
end
let!(:diff_note) do
@@ -45,10 +46,11 @@ RSpec.describe 'Updating an image DiffNote' do
let(:mutation) do
variables = {
id: GitlabSchema.id_from_object(diff_note).to_s,
- body: updated_body,
- position: updated_position
+ body: updated_body
}
+ variables[:position] = updated_position if updated_position
+
graphql_mutation(:update_image_diff_note, variables)
end
diff --git a/spec/requests/api/graphql/mutations/releases/create_spec.rb b/spec/requests/api/graphql/mutations/releases/create_spec.rb
new file mode 100644
index 00000000000..d745eb3083d
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/releases/create_spec.rb
@@ -0,0 +1,375 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Creation of a new release' do
+ include GraphqlHelpers
+ include Presentable
+
+ let_it_be(:project) { create(:project, :public, :repository) }
+ let_it_be(:milestone_12_3) { create(:milestone, project: project, title: '12.3') }
+ let_it_be(:milestone_12_4) { create(:milestone, project: project, title: '12.4') }
+ let_it_be(:public_user) { create(:user) }
+ let_it_be(:guest) { create(:user) }
+ let_it_be(:reporter) { create(:user) }
+ let_it_be(:developer) { create(:user) }
+
+ let(:mutation_name) { :release_create }
+
+ let(:tag_name) { 'v7.12.5'}
+ let(:ref) { 'master'}
+ let(:name) { 'Version 7.12.5'}
+ let(:description) { 'Release 7.12.5 :rocket:' }
+ let(:released_at) { '2018-12-10' }
+ let(:milestones) { [milestone_12_3.title, milestone_12_4.title] }
+ let(:asset_link) { { name: 'An asset link', url: 'https://gitlab.example.com/link', directAssetPath: '/permanent/link', linkType: 'OTHER' } }
+ let(:assets) { { links: [asset_link] } }
+
+ let(:mutation_arguments) do
+ {
+ projectPath: project.full_path,
+ tagName: tag_name,
+ ref: ref,
+ name: name,
+ description: description,
+ releasedAt: released_at,
+ milestones: milestones,
+ assets: assets
+ }
+ end
+
+ let(:mutation) do
+ graphql_mutation(mutation_name, mutation_arguments, <<~FIELDS)
+ release {
+ tagName
+ name
+ description
+ releasedAt
+ createdAt
+ milestones {
+ nodes {
+ title
+ }
+ }
+ assets {
+ links {
+ nodes {
+ name
+ url
+ linkType
+ external
+ directAssetUrl
+ }
+ }
+ }
+ }
+ errors
+ FIELDS
+ end
+
+ let(:create_release) { post_graphql_mutation(mutation, current_user: current_user) }
+ let(:mutation_response) { graphql_mutation_response(mutation_name)&.with_indifferent_access }
+
+ around do |example|
+ freeze_time { example.run }
+ end
+
+ before do
+ project.add_guest(guest)
+ project.add_reporter(reporter)
+ project.add_developer(developer)
+
+ stub_default_url_options(host: 'www.example.com')
+ end
+
+ shared_examples 'no errors' do
+ it 'returns no errors' do
+ create_release
+
+ expect(graphql_errors).not_to be_present
+ end
+ end
+
+ shared_examples 'top-level error with message' do |error_message|
+ it 'returns a top-level error with message' do
+ create_release
+
+ expect(mutation_response).to be_nil
+ expect(graphql_errors.count).to eq(1)
+ expect(graphql_errors.first['message']).to eq(error_message)
+ end
+ end
+
+ shared_examples 'errors-as-data with message' do |error_message|
+ it 'returns an error-as-data with message' do
+ create_release
+
+ expect(mutation_response[:release]).to be_nil
+ expect(mutation_response[:errors].count).to eq(1)
+ expect(mutation_response[:errors].first).to match(error_message)
+ end
+ end
+
+ context 'when the current user has access to create releases' do
+ let(:current_user) { developer }
+
+ context 'when all available mutation arguments are provided' do
+ it_behaves_like 'no errors'
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ it 'returns the new release data' do
+ create_release
+
+ release = mutation_response[:release]
+ expected_direct_asset_url = Gitlab::Routing.url_helpers.project_release_url(project, Release.find_by(tag: tag_name)) << "/downloads#{asset_link[:directAssetPath]}"
+
+ expected_attributes = {
+ tagName: tag_name,
+ name: name,
+ description: description,
+ releasedAt: Time.parse(released_at).utc.iso8601,
+ createdAt: Time.current.utc.iso8601,
+ assets: {
+ links: {
+ nodes: [{
+ name: asset_link[:name],
+ url: asset_link[:url],
+ linkType: asset_link[:linkType],
+ external: true,
+ directAssetUrl: expected_direct_asset_url
+ }]
+ }
+ }
+ }
+
+ expect(release).to include(expected_attributes)
+
+ # Right now the milestones are returned in a non-deterministic order.
+ # This `milestones` test should be moved up into the expect(release)
+ # above (and `.to include` updated to `.to eq`) once
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/259012 is addressed.
+ expect(release['milestones']['nodes']).to match_array([
+ { 'title' => '12.4' },
+ { 'title' => '12.3' }
+ ])
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+ end
+
+ context 'when only the required mutation arguments are provided' do
+ let(:mutation_arguments) { super().slice(:projectPath, :tagName, :ref) }
+
+ it_behaves_like 'no errors'
+
+ it 'returns the new release data' do
+ create_release
+
+ expected_response = {
+ tagName: tag_name,
+ name: tag_name,
+ description: nil,
+ releasedAt: Time.current.utc.iso8601,
+ createdAt: Time.current.utc.iso8601,
+ milestones: {
+ nodes: []
+ },
+ assets: {
+ links: {
+ nodes: []
+ }
+ }
+ }.with_indifferent_access
+
+ expect(mutation_response[:release]).to eq(expected_response)
+ end
+ end
+
+ context 'when the provided tag already exists' do
+ let(:tag_name) { 'v1.1.0' }
+
+ it_behaves_like 'no errors'
+
+ it 'does not create a new tag' do
+ expect { create_release }.not_to change { Project.find_by_id(project.id).repository.tag_count }
+ end
+ end
+
+ context 'when the provided tag does not already exist' do
+ let(:tag_name) { 'v7.12.5-alpha' }
+
+ it_behaves_like 'no errors'
+
+ it 'creates a new tag' do
+ expect { create_release }.to change { Project.find_by_id(project.id).repository.tag_count }.by(1)
+ end
+ end
+
+ context 'when a local timezone is provided for releasedAt' do
+ let(:released_at) { Time.parse(super()).in_time_zone('Hawaii').iso8601 }
+
+ it_behaves_like 'no errors'
+
+ it 'returns the correct releasedAt date in UTC' do
+ create_release
+
+ expect(mutation_response[:release]).to include({ releasedAt: Time.parse(released_at).utc.iso8601 })
+ end
+ end
+
+ context 'when no releasedAt is provided' do
+ let(:mutation_arguments) { super().except(:releasedAt) }
+
+ it_behaves_like 'no errors'
+
+ it 'sets releasedAt to the current time' do
+ create_release
+
+ expect(mutation_response[:release]).to include({ releasedAt: Time.current.utc.iso8601 })
+ end
+ end
+
+ context "when a release asset doesn't include an explicit linkType" do
+ let(:asset_link) { super().except(:linkType) }
+
+ it_behaves_like 'no errors'
+
+ it 'defaults the linkType to OTHER' do
+ create_release
+
+ returned_asset_link_type = mutation_response.dig(:release, :assets, :links, :nodes, 0, :linkType)
+
+ expect(returned_asset_link_type).to eq('OTHER')
+ end
+ end
+
+ context "when a release asset doesn't include a directAssetPath" do
+ let(:asset_link) { super().except(:directAssetPath) }
+
+ it_behaves_like 'no errors'
+
+ it 'returns the provided url as the directAssetUrl' do
+ create_release
+
+ returned_asset_link_type = mutation_response.dig(:release, :assets, :links, :nodes, 0, :directAssetUrl)
+
+ expect(returned_asset_link_type).to eq(asset_link[:url])
+ end
+ end
+
+ context 'empty milestones' do
+ shared_examples 'no associated milestones' do
+ it_behaves_like 'no errors'
+
+ it 'creates a release with no associated milestones' do
+ create_release
+
+ returned_milestones = mutation_response.dig(:release, :milestones, :nodes)
+
+ expect(returned_milestones.count).to eq(0)
+ end
+ end
+
+ context 'when the milestones parameter is not provided' do
+ let(:mutation_arguments) { super().except(:milestones) }
+
+ it_behaves_like 'no associated milestones'
+ end
+
+ context 'when the milestones parameter is null' do
+ let(:milestones) { nil }
+
+ it_behaves_like 'no associated milestones'
+ end
+
+ context 'when the milestones parameter is an empty array' do
+ let(:milestones) { [] }
+
+ it_behaves_like 'no associated milestones'
+ end
+ end
+
+ context 'validation' do
+ context 'when a release is already associated to the specified tag' do
+ before do
+ create(:release, project: project, tag: tag_name)
+ end
+
+ it_behaves_like 'errors-as-data with message', 'Release already exists'
+ end
+
+ context "when a provided milestone doesn\'t exist" do
+ let(:milestones) { ['a fake milestone'] }
+
+ it_behaves_like 'errors-as-data with message', 'Milestone(s) not found: a fake milestone'
+ end
+
+ context "when a provided milestone belongs to a different project than the release" do
+ let(:milestone_in_different_project) { create(:milestone, title: 'different milestone') }
+ let(:milestones) { [milestone_in_different_project.title] }
+
+ it_behaves_like 'errors-as-data with message', "Milestone(s) not found: different milestone"
+ end
+
+ context 'when two release assets share the same name' do
+ let(:asset_link_1) { { name: 'My link', url: 'https://example.com/1' } }
+ let(:asset_link_2) { { name: 'My link', url: 'https://example.com/2' } }
+ let(:assets) { { links: [asset_link_1, asset_link_2] } }
+
+ # Right now the raw Postgres error message is sent to the user as the validation message.
+ # We should catch this validation error and return a nicer message:
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/277087
+ it_behaves_like 'errors-as-data with message', 'PG::UniqueViolation'
+ end
+
+ context 'when two release assets share the same URL' do
+ let(:asset_link_1) { { name: 'My first link', url: 'https://example.com' } }
+ let(:asset_link_2) { { name: 'My second link', url: 'https://example.com' } }
+ let(:assets) { { links: [asset_link_1, asset_link_2] } }
+
+ # Same note as above about the ugly error message
+ it_behaves_like 'errors-as-data with message', 'PG::UniqueViolation'
+ end
+
+ context 'when the provided tag name is HEAD' do
+ let(:tag_name) { 'HEAD' }
+
+ it_behaves_like 'errors-as-data with message', 'Tag name invalid'
+ end
+
+ context 'when the provided tag name is empty' do
+ let(:tag_name) { '' }
+
+ it_behaves_like 'errors-as-data with message', 'Tag name invalid'
+ end
+
+ context "when the provided tag doesn't already exist, and no ref parameter was provided" do
+ let(:ref) { nil }
+ let(:tag_name) { 'v7.12.5-beta' }
+
+ it_behaves_like 'errors-as-data with message', 'Ref is not specified'
+ end
+ end
+ end
+
+ context "when the current user doesn't have access to create releases" do
+ expected_error_message = "The resource that you are attempting to access does not exist or you don't have permission to perform this action"
+
+ context 'when the current user is a Reporter' do
+ let(:current_user) { reporter }
+
+ it_behaves_like 'top-level error with message', expected_error_message
+ end
+
+ context 'when the current user is a Guest' do
+ let(:current_user) { guest }
+
+ it_behaves_like 'top-level error with message', expected_error_message
+ end
+
+ context 'when the current user is a public user' do
+ let(:current_user) { public_user }
+
+ it_behaves_like 'top-level error with message', expected_error_message
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/snippets/destroy_spec.rb b/spec/requests/api/graphql/mutations/snippets/destroy_spec.rb
index b71f87d2702..1be8ce142ac 100644
--- a/spec/requests/api/graphql/mutations/snippets/destroy_spec.rb
+++ b/spec/requests/api/graphql/mutations/snippets/destroy_spec.rb
@@ -53,10 +53,11 @@ RSpec.describe 'Destroying a Snippet' do
let!(:snippet_gid) { project.to_gid.to_s }
it 'returns an error' do
+ err_message = %Q["#{snippet_gid}" does not represent an instance of Snippet]
+
post_graphql_mutation(mutation, current_user: current_user)
- expect(graphql_errors)
- .to include(a_hash_including('message' => "#{snippet_gid} is not a valid ID for Snippet."))
+ expect(graphql_errors).to include(a_hash_including('message' => a_string_including(err_message)))
end
it 'does not destroy the Snippet' do
diff --git a/spec/requests/api/graphql/mutations/todos/create_spec.rb b/spec/requests/api/graphql/mutations/todos/create_spec.rb
new file mode 100644
index 00000000000..aca00519682
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/todos/create_spec.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Create a todo' do
+ include GraphqlHelpers
+
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:target) { create(:issue) }
+
+ let(:input) do
+ {
+ 'targetId' => target.to_global_id.to_s
+ }
+ end
+
+ let(:mutation) { graphql_mutation(:todoCreate, input) }
+
+ let(:mutation_response) { graphql_mutation_response(:todoCreate) }
+
+ context 'the user is not allowed to create todo' do
+ it_behaves_like 'a mutation that returns a top-level access error'
+ end
+
+ context 'when user has permissions to create todo' do
+ before do
+ target.project.add_guest(current_user)
+ end
+
+ it 'creates todo' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_response['todo']['body']).to eq(target.title)
+ expect(mutation_response['todo']['state']).to eq('pending')
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/todos/restore_many_spec.rb b/spec/requests/api/graphql/mutations/todos/restore_many_spec.rb
new file mode 100644
index 00000000000..3e96d5c5058
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/todos/restore_many_spec.rb
@@ -0,0 +1,70 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Restoring many Todos' do
+ include GraphqlHelpers
+
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:author) { create(:user) }
+ let_it_be(:other_user) { create(:user) }
+
+ let_it_be(:todo1) { create(:todo, user: current_user, author: author, state: :done) }
+ let_it_be(:todo2) { create(:todo, user: current_user, author: author, state: :done) }
+
+ let_it_be(:other_user_todo) { create(:todo, user: other_user, author: author, state: :done) }
+
+ let(:input_ids) { [todo1, todo2].map { |obj| global_id_of(obj) } }
+ let(:input) { { ids: input_ids } }
+
+ let(:mutation) do
+ graphql_mutation(:todo_restore_many, input,
+ <<-QL.strip_heredoc
+ clientMutationId
+ errors
+ updatedIds
+ todos {
+ id
+ state
+ }
+ QL
+ )
+ end
+
+ def mutation_response
+ graphql_mutation_response(:todo_restore_many)
+ end
+
+ it 'restores many todos' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(todo1.reload.state).to eq('pending')
+ expect(todo2.reload.state).to eq('pending')
+ expect(other_user_todo.reload.state).to eq('done')
+
+ expect(mutation_response).to include(
+ 'errors' => be_empty,
+ 'updatedIds' => match_array(input_ids),
+ 'todos' => contain_exactly(
+ { 'id' => global_id_of(todo1), 'state' => 'pending' },
+ { 'id' => global_id_of(todo2), 'state' => 'pending' }
+ )
+ )
+ end
+
+ context 'when using an invalid gid' do
+ let(:input_ids) { [global_id_of(author)] }
+ let(:invalid_gid_error) { /does not represent an instance of #{todo1.class}/ }
+
+ it 'contains the expected error' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ errors = json_response['errors']
+ expect(errors).not_to be_blank
+ expect(errors.first['message']).to match(invalid_gid_error)
+
+ expect(todo1.reload.state).to eq('done')
+ expect(todo2.reload.state).to eq('done')
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/namespace/root_storage_statistics_spec.rb b/spec/requests/api/graphql/namespace/root_storage_statistics_spec.rb
index 44e68c59248..37cc502103d 100644
--- a/spec/requests/api/graphql/namespace/root_storage_statistics_spec.rb
+++ b/spec/requests/api/graphql/namespace/root_storage_statistics_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe 'rendering namespace statistics' do
include GraphqlHelpers
let(:namespace) { user.namespace }
- let!(:statistics) { create(:namespace_root_storage_statistics, namespace: namespace, packages_size: 5.gigabytes) }
+ let!(:statistics) { create(:namespace_root_storage_statistics, namespace: namespace, packages_size: 5.gigabytes, uploads_size: 3.gigabytes) }
let(:user) { create(:user) }
let(:query) do
@@ -28,6 +28,12 @@ RSpec.describe 'rendering namespace statistics' do
expect(graphql_data['namespace']['rootStorageStatistics']).not_to be_blank
expect(graphql_data['namespace']['rootStorageStatistics']['packagesSize']).to eq(5.gigabytes)
end
+
+ it 'includes uploads size if the user can read the statistics' do
+ post_graphql(query, current_user: user)
+
+ expect(graphql_data_at(:namespace, :root_storage_statistics, :uploads_size)).to eq(3.gigabytes)
+ end
end
it_behaves_like 'a working namespace with storage statistics query'
diff --git a/spec/requests/api/graphql/project/alert_management/integrations_spec.rb b/spec/requests/api/graphql/project/alert_management/integrations_spec.rb
new file mode 100644
index 00000000000..b13805a61ce
--- /dev/null
+++ b/spec/requests/api/graphql/project/alert_management/integrations_spec.rb
@@ -0,0 +1,83 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe 'getting Alert Management Integrations' do
+ include ::Gitlab::Routing
+ include GraphqlHelpers
+
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:prometheus_service) { create(:prometheus_service, project: project) }
+ let_it_be(:project_alerting_setting) { create(:project_alerting_setting, project: project) }
+ let_it_be(:active_http_integration) { create(:alert_management_http_integration, project: project) }
+ let_it_be(:inactive_http_integration) { create(:alert_management_http_integration, :inactive, project: project) }
+ let_it_be(:other_project_http_integration) { create(:alert_management_http_integration) }
+
+ let(:fields) do
+ <<~QUERY
+ nodes {
+ #{all_graphql_fields_for('AlertManagementIntegration')}
+ }
+ QUERY
+ end
+
+ let(:query) do
+ graphql_query_for(
+ 'project',
+ { 'fullPath' => project.full_path },
+ query_graphql_field('alertManagementIntegrations', {}, fields)
+ )
+ end
+
+ context 'with integrations' do
+ let(:integrations) { graphql_data.dig('project', 'alertManagementIntegrations', 'nodes') }
+
+ context 'without project permissions' do
+ let(:user) { create(:user) }
+
+ before do
+ post_graphql(query, current_user: current_user)
+ end
+
+ it_behaves_like 'a working graphql query'
+
+ it { expect(integrations).to be_nil }
+ end
+
+ context 'with project permissions' do
+ before do
+ project.add_maintainer(current_user)
+ post_graphql(query, current_user: current_user)
+ end
+
+ let(:http_integration) { integrations.first }
+ let(:prometheus_integration) { integrations.second }
+
+ it_behaves_like 'a working graphql query'
+
+ it { expect(integrations.size).to eq(2) }
+
+ it 'returns the correct properties of the integrations' do
+ expect(http_integration).to include(
+ 'id' => GitlabSchema.id_from_object(active_http_integration).to_s,
+ 'type' => 'HTTP',
+ 'name' => active_http_integration.name,
+ 'active' => active_http_integration.active,
+ 'token' => active_http_integration.token,
+ 'url' => active_http_integration.url,
+ 'apiUrl' => nil
+ )
+
+ expect(prometheus_integration).to include(
+ 'id' => GitlabSchema.id_from_object(prometheus_service).to_s,
+ 'type' => 'PROMETHEUS',
+ 'name' => 'Prometheus',
+ 'active' => prometheus_service.manual_configuration?,
+ 'token' => project_alerting_setting.token,
+ 'url' => "http://localhost/#{project.full_path}/prometheus/alerts/notify.json",
+ 'apiUrl' => prometheus_service.api_url
+ )
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/project/container_repositories_spec.rb b/spec/requests/api/graphql/project/container_repositories_spec.rb
new file mode 100644
index 00000000000..7e32f54bf1d
--- /dev/null
+++ b/spec/requests/api/graphql/project/container_repositories_spec.rb
@@ -0,0 +1,145 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe 'getting container repositories in a project' do
+ using RSpec::Parameterized::TableSyntax
+ include GraphqlHelpers
+
+ let_it_be_with_reload(:project) { create(:project, :private) }
+ let_it_be(:container_repository) { create(:container_repository, project: project) }
+ let_it_be(:container_repositories_delete_scheduled) { create_list(:container_repository, 2, :status_delete_scheduled, project: project) }
+ let_it_be(:container_repositories_delete_failed) { create_list(:container_repository, 2, :status_delete_failed, project: project) }
+ let_it_be(:container_repositories) { [container_repository, container_repositories_delete_scheduled, container_repositories_delete_failed].flatten }
+ let_it_be(:container_expiration_policy) { project.container_expiration_policy }
+
+ let(:fields) do
+ <<~GQL
+ edges {
+ node {
+ #{all_graphql_fields_for('container_repositories'.classify)}
+ }
+ }
+ GQL
+ end
+
+ let(:query) do
+ graphql_query_for(
+ 'project',
+ { 'fullPath' => project.full_path },
+ query_graphql_field('container_repositories', {}, fields)
+ )
+ end
+
+ let(:user) { project.owner }
+ let(:variables) { {} }
+ let(:container_repositories_response) { graphql_data.dig('project', 'containerRepositories', 'edges') }
+
+ before do
+ stub_container_registry_config(enabled: true)
+ container_repositories.each do |repository|
+ stub_container_registry_tags(repository: repository.path, tags: %w(tag1 tag2 tag3), with_manifest: false)
+ end
+ end
+
+ subject { post_graphql(query, current_user: user, variables: variables) }
+
+ it_behaves_like 'a working graphql query' do
+ before do
+ subject
+ end
+
+ it 'matches the JSON schema' do
+ expect(container_repositories_response).to match_schema('graphql/container_repositories')
+ end
+ end
+
+ context 'with different permissions' do
+ let_it_be(:user) { create(:user) }
+
+ where(:project_visibility, :role, :access_granted, :can_delete) do
+ :private | :maintainer | true | true
+ :private | :developer | true | true
+ :private | :reporter | true | false
+ :private | :guest | false | false
+ :private | :anonymous | false | false
+ :public | :maintainer | true | true
+ :public | :developer | true | true
+ :public | :reporter | true | false
+ :public | :guest | true | false
+ :public | :anonymous | true | false
+ end
+
+ with_them do
+ before do
+ project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility.to_s.upcase, false))
+ project.add_user(user, role) unless role == :anonymous
+ end
+
+ it 'return the proper response' do
+ subject
+
+ if access_granted
+ expect(container_repositories_response.size).to eq(container_repositories.size)
+ container_repositories_response.each do |repository_response|
+ expect(repository_response.dig('node', 'canDelete')).to eq(can_delete)
+ end
+ else
+ expect(container_repositories_response).to eq(nil)
+ end
+ end
+ end
+ end
+
+ context 'limiting the number of repositories' do
+ let(:limit) { 1 }
+ let(:variables) do
+ { path: project.full_path, n: limit }
+ end
+
+ let(:query) do
+ <<~GQL
+ query($path: ID!, $n: Int) {
+ project(fullPath: $path) {
+ containerRepositories(first: $n) { #{fields} }
+ }
+ }
+ GQL
+ end
+
+ it 'only returns N repositories' do
+ subject
+
+ expect(container_repositories_response.size).to eq(limit)
+ end
+ end
+
+ context 'filter by name' do
+ let_it_be(:container_repository) { create(:container_repository, name: 'fooBar', project: project) }
+
+ let(:name) { 'ooba' }
+ let(:query) do
+ <<~GQL
+ query($path: ID!, $name: String) {
+ project(fullPath: $path) {
+ containerRepositories(name: $name) { #{fields} }
+ }
+ }
+ GQL
+ end
+
+ let(:variables) do
+ { path: project.full_path, name: name }
+ end
+
+ before do
+ stub_container_registry_tags(repository: container_repository.path, tags: %w(tag4 tag5 tag6), with_manifest: false)
+ end
+
+ it 'returns the searched container repository' do
+ subject
+
+ expect(container_repositories_response.size).to eq(1)
+ expect(container_repositories_response.first.dig('node', 'id')).to eq(container_repository.to_global_id.to_s)
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/project/error_tracking/sentry_errors_request_spec.rb b/spec/requests/api/graphql/project/error_tracking/sentry_errors_request_spec.rb
index cd84ce9cb96..c7d327a62af 100644
--- a/spec/requests/api/graphql/project/error_tracking/sentry_errors_request_spec.rb
+++ b/spec/requests/api/graphql/project/error_tracking/sentry_errors_request_spec.rb
@@ -29,10 +29,12 @@ RSpec.describe 'sentry errors requests' do
let(:error_data) { graphql_data.dig('project', 'sentryErrors', 'detailedError') }
- it_behaves_like 'a working graphql query' do
- before do
- post_graphql(query, current_user: current_user)
- end
+ it 'returns a successful response', :aggregate_failures, :quarantine do
+ post_graphql(query, current_user: current_user)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(graphql_errors).to be_nil
+ expect(json_response.keys).to include('data')
end
context 'when data is loading via reactive cache' do
@@ -191,7 +193,7 @@ RSpec.describe 'sentry errors requests' do
describe 'getting a stack trace' do
let_it_be(:sentry_stack_trace) { build(:error_tracking_error_event) }
- let(:sentry_gid) { Gitlab::ErrorTracking::DetailedError.new(id: 1).to_global_id.to_s }
+ let(:sentry_gid) { global_id_of(Gitlab::ErrorTracking::DetailedError.new(id: 1)) }
let(:stack_trace_fields) do
all_graphql_fields_for('SentryErrorStackTrace'.classify)
diff --git a/spec/requests/api/graphql/project/grafana_integration_spec.rb b/spec/requests/api/graphql/project/grafana_integration_spec.rb
index 688959e622d..9b24698f40c 100644
--- a/spec/requests/api/graphql/project/grafana_integration_spec.rb
+++ b/spec/requests/api/graphql/project/grafana_integration_spec.rb
@@ -45,7 +45,6 @@ RSpec.describe 'Getting Grafana Integration' do
it_behaves_like 'a working graphql query'
- specify { expect(integration_data['token']).to eql grafana_integration.masked_token }
specify { expect(integration_data['grafanaUrl']).to eql grafana_integration.grafana_url }
specify do
diff --git a/spec/requests/api/graphql/project/issue/design_collection/version_spec.rb b/spec/requests/api/graphql/project/issue/design_collection/version_spec.rb
index 1b654e660e3..4bce3c7fe0f 100644
--- a/spec/requests/api/graphql/project/issue/design_collection/version_spec.rb
+++ b/spec/requests/api/graphql/project/issue/design_collection/version_spec.rb
@@ -14,6 +14,7 @@ RSpec.describe 'Query.project(fullPath).issue(iid).designCollection.version(sha)
create(:design_version, issue: issue,
created_designs: create_list(:design, 3, issue: issue))
end
+
let_it_be(:version) do
create(:design_version, issue: issue,
modified_designs: old_version.designs,
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 640ac95cd86..ee0085718b3 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
@@ -11,12 +11,15 @@ RSpec.describe 'Getting versions related to an issue' do
let_it_be(:version_a) do
create(:design_version, issue: issue)
end
+
let_it_be(:version_b) do
create(:design_version, issue: issue)
end
+
let_it_be(:version_c) do
create(:design_version, issue: issue)
end
+
let_it_be(:version_d) do
create(:design_version, issue: issue)
end
diff --git a/spec/requests/api/graphql/project/issue/designs/notes_spec.rb b/spec/requests/api/graphql/project/issue/designs/notes_spec.rb
index e25453510d5..a671ddc7ab1 100644
--- a/spec/requests/api/graphql/project/issue/designs/notes_spec.rb
+++ b/spec/requests/api/graphql/project/issue/designs/notes_spec.rb
@@ -30,7 +30,7 @@ RSpec.describe 'Getting designs related to an issue' do
post_graphql(query(note_fields), current_user: nil)
- designs_data = graphql_data['project']['issue']['designs']['designs']
+ designs_data = graphql_data['project']['issue']['designCollection']['designs']
design_data = designs_data['nodes'].first
note_data = design_data['notes']['nodes'].first
@@ -56,7 +56,7 @@ RSpec.describe 'Getting designs related to an issue' do
'issue',
{ iid: design.issue.iid.to_s },
query_graphql_field(
- 'designs', {}, design_node
+ 'designCollection', {}, design_node
)
)
)
diff --git a/spec/requests/api/graphql/project/issues_spec.rb b/spec/requests/api/graphql/project/issues_spec.rb
index 40fec6ba068..4f27f08bf98 100644
--- a/spec/requests/api/graphql/project/issues_spec.rb
+++ b/spec/requests/api/graphql/project/issues_spec.rb
@@ -9,10 +9,9 @@ RSpec.describe 'getting an issue list for a project' do
let_it_be(:project) { create(:project, :repository, :public) }
let_it_be(:current_user) { create(:user) }
- let_it_be(:issues, reload: true) do
- [create(:issue, project: project, discussion_locked: true),
- create(:issue, :with_alert, project: project)]
- end
+ let_it_be(:issue_a, reload: true) { create(:issue, project: project, discussion_locked: true) }
+ let_it_be(:issue_b, reload: true) { create(:issue, :with_alert, project: project) }
+ let_it_be(:issues, reload: true) { [issue_a, issue_b] }
let(:fields) do
<<~QUERY
@@ -414,4 +413,42 @@ RSpec.describe 'getting an issue list for a project' do
expect(response_assignee_ids(issues_data)).to match_array(assignees_as_global_ids(new_issues))
end
end
+
+ describe 'N+1 query checks' do
+ let(:extra_iid_for_second_query) { issue_b.iid.to_s }
+ let(:search_params) { { iids: [issue_a.iid.to_s] } }
+
+ def execute_query
+ query = graphql_query_for(
+ :project,
+ { full_path: project.full_path },
+ query_graphql_field(:issues, search_params, [
+ query_graphql_field(:nodes, nil, requested_fields)
+ ])
+ )
+ post_graphql(query, current_user: current_user)
+ end
+
+ context 'when requesting `user_notes_count`' do
+ let(:requested_fields) { [:user_notes_count] }
+
+ before do
+ create_list(:note_on_issue, 2, noteable: issue_a, project: project)
+ create(:note_on_issue, noteable: issue_b, project: project)
+ end
+
+ include_examples 'N+1 query check'
+ end
+
+ context 'when requesting `user_discussions_count`' do
+ let(:requested_fields) { [:user_discussions_count] }
+
+ before do
+ create_list(:note_on_issue, 2, noteable: issue_a, project: project)
+ create(:note_on_issue, noteable: issue_b, project: project)
+ end
+
+ include_examples 'N+1 query check'
+ end
+ end
end
diff --git a/spec/requests/api/graphql/project/jira_import_spec.rb b/spec/requests/api/graphql/project/jira_import_spec.rb
index 1cc30b95162..98a3f08baa6 100644
--- a/spec/requests/api/graphql/project/jira_import_spec.rb
+++ b/spec/requests/api/graphql/project/jira_import_spec.rb
@@ -19,6 +19,7 @@ RSpec.describe 'query Jira import data' do
total_issue_count: 4
)
end
+
let_it_be(:jira_import2) do
create(
:jira_import_state, :finished,
@@ -31,6 +32,7 @@ RSpec.describe 'query Jira import data' do
total_issue_count: 3
)
end
+
let(:query) do
%(
query {
diff --git a/spec/requests/api/graphql/project/merge_requests_spec.rb b/spec/requests/api/graphql/project/merge_requests_spec.rb
index c737e0b8caf..2b8d537f9fc 100644
--- a/spec/requests/api/graphql/project/merge_requests_spec.rb
+++ b/spec/requests/api/graphql/project/merge_requests_spec.rb
@@ -243,6 +243,17 @@ RSpec.describe 'getting merge request listings nested in a project' do
include_examples 'N+1 query check'
end
+
+ context 'when requesting `user_discussions_count`' do
+ let(:requested_fields) { [:user_discussions_count] }
+
+ before do
+ create_list(:note_on_merge_request, 2, noteable: merge_request_a, project: project)
+ create(:note_on_merge_request, noteable: merge_request_c, project: project)
+ end
+
+ include_examples 'N+1 query check'
+ end
end
describe 'sorting and pagination' do
diff --git a/spec/requests/api/graphql/project/project_statistics_spec.rb b/spec/requests/api/graphql/project/project_statistics_spec.rb
index c226b10ab51..b57c594c64f 100644
--- a/spec/requests/api/graphql/project/project_statistics_spec.rb
+++ b/spec/requests/api/graphql/project/project_statistics_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe 'rendering project statistics' do
include GraphqlHelpers
let(:project) { create(:project) }
- let!(:project_statistics) { create(:project_statistics, project: project, packages_size: 5.gigabytes) }
+ let!(:project_statistics) { create(:project_statistics, project: project, packages_size: 5.gigabytes, uploads_size: 3.gigabytes) }
let(:user) { create(:user) }
let(:query) do
@@ -31,6 +31,12 @@ RSpec.describe 'rendering project statistics' do
expect(graphql_data['project']['statistics']['packagesSize']).to eq(5.gigabytes)
end
+ it 'includes uploads size if the user can read the statistics' do
+ post_graphql(query, current_user: user)
+
+ expect(graphql_data_at(:project, :statistics, :uploadsSize)).to eq(3.gigabytes)
+ end
+
context 'when the project is public' do
let(:project) { create(:project, :public) }
diff --git a/spec/requests/api/graphql/project/release_spec.rb b/spec/requests/api/graphql/project/release_spec.rb
index 8fce29d0dc6..57dbe258ce4 100644
--- a/spec/requests/api/graphql/project/release_spec.rb
+++ b/spec/requests/api/graphql/project/release_spec.rb
@@ -13,7 +13,11 @@ RSpec.describe 'Query.project(fullPath).release(tagName)' do
let_it_be(:link_filepath) { '/direct/asset/link/path' }
let_it_be(:released_at) { Time.now - 1.day }
- let(:params_for_issues_and_mrs) { { scope: 'all', state: 'opened', release_tag: release.tag } }
+ let(:base_url_params) { { scope: 'all', release_tag: release.tag } }
+ let(:opened_url_params) { { state: 'opened', **base_url_params } }
+ let(:merged_url_params) { { state: 'merged', **base_url_params } }
+ let(:closed_url_params) { { state: 'closed', **base_url_params } }
+
let(:post_query) { post_graphql(query, current_user: current_user) }
let(:path_prefix) { %w[project release] }
let(:data) { graphql_data.dig(*path) }
@@ -143,7 +147,7 @@ RSpec.describe 'Query.project(fullPath).release(tagName)' do
'name' => link.name,
'url' => link.url,
'external' => link.external?,
- 'directAssetUrl' => link.filepath ? Gitlab::Routing.url_helpers.project_release_url(project, release) << link.filepath : link.url
+ 'directAssetUrl' => link.filepath ? Gitlab::Routing.url_helpers.project_release_url(project, release) << "/downloads#{link.filepath}" : link.url
}
end
@@ -180,8 +184,11 @@ RSpec.describe 'Query.project(fullPath).release(tagName)' do
let(:release_fields) do
query_graphql_field(:links, nil, %{
selfUrl
- mergeRequestsUrl
- issuesUrl
+ openedMergeRequestsUrl
+ mergedMergeRequestsUrl
+ closedMergeRequestsUrl
+ openedIssuesUrl
+ closedIssuesUrl
})
end
@@ -190,8 +197,11 @@ RSpec.describe 'Query.project(fullPath).release(tagName)' do
expect(data).to eq(
'selfUrl' => project_release_url(project, release),
- 'mergeRequestsUrl' => project_merge_requests_url(project, params_for_issues_and_mrs),
- 'issuesUrl' => project_issues_url(project, params_for_issues_and_mrs)
+ 'openedMergeRequestsUrl' => project_merge_requests_url(project, opened_url_params),
+ 'mergedMergeRequestsUrl' => project_merge_requests_url(project, merged_url_params),
+ 'closedMergeRequestsUrl' => project_merge_requests_url(project, closed_url_params),
+ 'openedIssuesUrl' => project_issues_url(project, opened_url_params),
+ 'closedIssuesUrl' => project_issues_url(project, closed_url_params)
)
end
end
diff --git a/spec/requests/api/graphql/project/releases_spec.rb b/spec/requests/api/graphql/project/releases_spec.rb
index 7c57c0e9177..6e364c7d7b5 100644
--- a/spec/requests/api/graphql/project/releases_spec.rb
+++ b/spec/requests/api/graphql/project/releases_spec.rb
@@ -10,6 +10,11 @@ RSpec.describe 'Query.project(fullPath).releases()' do
let_it_be(:reporter) { create(:user) }
let_it_be(:developer) { create(:user) }
+ let(:base_url_params) { { scope: 'all', release_tag: release.tag } }
+ let(:opened_url_params) { { state: 'opened', **base_url_params } }
+ let(:merged_url_params) { { state: 'merged', **base_url_params } }
+ let(:closed_url_params) { { state: 'closed', **base_url_params } }
+
let(:query) do
graphql_query_for(:project, { fullPath: project.full_path },
%{
@@ -37,8 +42,11 @@ RSpec.describe 'Query.project(fullPath).releases()' do
}
links {
selfUrl
- mergeRequestsUrl
- issuesUrl
+ openedMergeRequestsUrl
+ mergedMergeRequestsUrl
+ closedMergeRequestsUrl
+ openedIssuesUrl
+ closedIssuesUrl
}
}
}
@@ -101,8 +109,11 @@ RSpec.describe 'Query.project(fullPath).releases()' do
},
'links' => {
'selfUrl' => project_release_url(project, release),
- 'mergeRequestsUrl' => project_merge_requests_url(project, params_for_issues_and_mrs),
- 'issuesUrl' => project_issues_url(project, params_for_issues_and_mrs)
+ 'openedMergeRequestsUrl' => project_merge_requests_url(project, opened_url_params),
+ 'mergedMergeRequestsUrl' => project_merge_requests_url(project, merged_url_params),
+ 'closedMergeRequestsUrl' => project_merge_requests_url(project, closed_url_params),
+ 'openedIssuesUrl' => project_issues_url(project, opened_url_params),
+ 'closedIssuesUrl' => project_issues_url(project, closed_url_params)
}
)
end
@@ -300,4 +311,77 @@ RSpec.describe 'Query.project(fullPath).releases()' do
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 }
+ let_it_be(:tomorrow) { today + 1.day }
+
+ let_it_be(:project) { create(:project, :repository, :public) }
+
+ let_it_be(:release_v1) { create(:release, project: project, tag: 'v1', released_at: yesterday, created_at: tomorrow) }
+ let_it_be(:release_v2) { create(:release, project: project, tag: 'v2', released_at: today, created_at: yesterday) }
+ let_it_be(:release_v3) { create(:release, project: project, tag: 'v3', released_at: tomorrow, created_at: today) }
+
+ let(:current_user) { developer }
+
+ let(:params) { nil }
+
+ let(:sorted_tags) do
+ graphql_data.dig('project', 'releases', 'nodes').map { |release| release['tagName'] }
+ end
+
+ let(:query) do
+ graphql_query_for(:project, { fullPath: project.full_path },
+ %{
+ releases#{params ? "(#{params})" : ""} {
+ nodes {
+ tagName
+ }
+ }
+ })
+ end
+
+ before do
+ post_query
+ end
+
+ context 'when no sort: parameter is provided' do
+ it 'returns the results with the default sort applied (sort: RELEASED_AT_DESC)' do
+ expect(sorted_tags).to eq(%w(v3 v2 v1))
+ end
+ end
+
+ context 'with sort: RELEASED_AT_DESC' do
+ let(:params) { 'sort: RELEASED_AT_DESC' }
+
+ it 'returns the releases ordered by released_at in descending order' do
+ expect(sorted_tags).to eq(%w(v3 v2 v1))
+ end
+ end
+
+ context 'with sort: RELEASED_AT_ASC' do
+ let(:params) { 'sort: RELEASED_AT_ASC' }
+
+ it 'returns the releases ordered by released_at in ascending order' do
+ expect(sorted_tags).to eq(%w(v1 v2 v3))
+ end
+ end
+
+ context 'with sort: CREATED_DESC' do
+ let(:params) { 'sort: CREATED_DESC' }
+
+ it 'returns the releases ordered by created_at in descending order' do
+ expect(sorted_tags).to eq(%w(v1 v3 v2))
+ end
+ end
+
+ context 'with sort: CREATED_ASC' do
+ let(:params) { 'sort: CREATED_ASC' }
+
+ it 'returns the releases ordered by created_at in ascending order' do
+ expect(sorted_tags).to eq(%w(v2 v3 v1))
+ end
+ end
+ end
end
diff --git a/spec/requests/api/graphql/project/terraform/states_spec.rb b/spec/requests/api/graphql/project/terraform/states_spec.rb
new file mode 100644
index 00000000000..8b67b549efa
--- /dev/null
+++ b/spec/requests/api/graphql/project/terraform/states_spec.rb
@@ -0,0 +1,81 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'query terraform states' do
+ include GraphqlHelpers
+
+ let_it_be(:project) { create(:project) }
+ let_it_be(:terraform_state) { create(:terraform_state, :with_version, :locked, project: project) }
+ let_it_be(:latest_version) { terraform_state.latest_version }
+
+ let(:query) do
+ graphql_query_for(:project, { fullPath: project.full_path },
+ %{
+ terraformStates {
+ count
+ nodes {
+ id
+ name
+ lockedAt
+ createdAt
+ updatedAt
+
+ latestVersion {
+ id
+ createdAt
+ updatedAt
+
+ createdByUser {
+ id
+ }
+
+ job {
+ name
+ }
+ }
+
+ lockedByUser {
+ id
+ }
+ }
+ }
+ })
+ end
+
+ let(:current_user) { project.creator }
+ let(:data) { graphql_data.dig('project', 'terraformStates') }
+
+ before do
+ post_graphql(query, current_user: current_user)
+ end
+
+ it 'returns terraform state data', :aggregate_failures do
+ state = data.dig('nodes', 0)
+ version = state['latestVersion']
+
+ expect(state['id']).to eq(terraform_state.to_global_id.to_s)
+ expect(state['name']).to eq(terraform_state.name)
+ expect(state['lockedAt']).to eq(terraform_state.locked_at.iso8601)
+ expect(state['createdAt']).to eq(terraform_state.created_at.iso8601)
+ expect(state['updatedAt']).to eq(terraform_state.updated_at.iso8601)
+ expect(state.dig('lockedByUser', 'id')).to eq(terraform_state.locked_by_user.to_global_id.to_s)
+
+ expect(version['id']).to eq(latest_version.to_global_id.to_s)
+ expect(version['createdAt']).to eq(latest_version.created_at.iso8601)
+ expect(version['updatedAt']).to eq(latest_version.updated_at.iso8601)
+ expect(version.dig('createdByUser', 'id')).to eq(latest_version.created_by_user.to_global_id.to_s)
+ expect(version.dig('job', 'name')).to eq(latest_version.build.name)
+ end
+
+ it 'returns count of terraform states' do
+ count = data.dig('count')
+ expect(count).to be(project.terraform_states.size)
+ end
+
+ context 'unauthorized users' do
+ let(:current_user) { nil }
+
+ it { expect(data).to be_nil }
+ end
+end
diff --git a/spec/requests/api/graphql/read_only_spec.rb b/spec/requests/api/graphql/read_only_spec.rb
index ce8a3f6ef5c..d2a45603886 100644
--- a/spec/requests/api/graphql/read_only_spec.rb
+++ b/spec/requests/api/graphql/read_only_spec.rb
@@ -3,55 +3,11 @@
require 'spec_helper'
RSpec.describe 'Requests on a read-only node' do
- include GraphqlHelpers
-
- before do
- allow(Gitlab::Database).to receive(:read_only?) { true }
- end
-
- context 'mutations' do
- let(:current_user) { note.author }
- let!(:note) { create(:note) }
-
- let(:mutation) do
- variables = {
- id: GitlabSchema.id_from_object(note).to_s
- }
-
- graphql_mutation(:destroy_note, variables)
- end
-
- def mutation_response
- graphql_mutation_response(:destroy_note)
- end
-
- it 'disallows the query' do
- post_graphql_mutation(mutation, current_user: current_user)
-
- expect(json_response['errors'].first['message']).to eq(Mutations::BaseMutation::ERROR_MESSAGE)
- end
-
- it 'does not destroy the Note' do
- expect do
- post_graphql_mutation(mutation, current_user: current_user)
- end.not_to change { Note.count }
- end
- end
-
- context 'read-only queries' do
- let(:current_user) { create(:user) }
- let(:project) { create(:project, :repository) }
-
+ context 'when db is read-only' do
before do
- project.add_developer(current_user)
+ allow(Gitlab::Database).to receive(:read_only?) { true }
end
- it 'allows the query' do
- query = graphql_query_for('project', 'fullPath' => project.full_path)
-
- post_graphql(query, current_user: current_user)
-
- expect(graphql_data['project']).not_to be_nil
- end
+ it_behaves_like 'graphql on a read-only GitLab instance'
end
end
diff --git a/spec/requests/api/graphql/terraform/state/delete_spec.rb b/spec/requests/api/graphql/terraform/state/delete_spec.rb
new file mode 100644
index 00000000000..35927d03b49
--- /dev/null
+++ b/spec/requests/api/graphql/terraform/state/delete_spec.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'delete a terraform state' do
+ include GraphqlHelpers
+
+ let_it_be(:project) { create(:project) }
+ let_it_be(:user) { create(:user, maintainer_projects: [project]) }
+
+ let(:state) { create(:terraform_state, project: project) }
+ let(:mutation) { graphql_mutation(:terraform_state_delete, id: state.to_global_id.to_s) }
+
+ before do
+ post_graphql_mutation(mutation, current_user: user)
+ end
+
+ include_examples 'a working graphql query'
+
+ it 'deletes the state' do
+ expect { state.reload }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+end
diff --git a/spec/requests/api/graphql/terraform/state/lock_spec.rb b/spec/requests/api/graphql/terraform/state/lock_spec.rb
new file mode 100644
index 00000000000..e4d3b6336ab
--- /dev/null
+++ b/spec/requests/api/graphql/terraform/state/lock_spec.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'lock a terraform state' do
+ include GraphqlHelpers
+
+ let_it_be(:project) { create(:project) }
+ let_it_be(:user) { create(:user, maintainer_projects: [project]) }
+
+ let(:state) { create(:terraform_state, project: project) }
+ let(:mutation) { graphql_mutation(:terraform_state_lock, id: state.to_global_id.to_s) }
+
+ before do
+ expect(state).not_to be_locked
+ post_graphql_mutation(mutation, current_user: user)
+ end
+
+ include_examples 'a working graphql query'
+
+ it 'locks the state' do
+ expect(state.reload).to be_locked
+ expect(state.locked_by_user).to eq(user)
+ end
+end
diff --git a/spec/requests/api/graphql/terraform/state/unlock_spec.rb b/spec/requests/api/graphql/terraform/state/unlock_spec.rb
new file mode 100644
index 00000000000..e90730f2d8f
--- /dev/null
+++ b/spec/requests/api/graphql/terraform/state/unlock_spec.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'unlock a terraform state' do
+ include GraphqlHelpers
+
+ let_it_be(:project) { create(:project) }
+ let_it_be(:user) { create(:user, maintainer_projects: [project]) }
+
+ let(:state) { create(:terraform_state, :locked, project: project) }
+ let(:mutation) { graphql_mutation(:terraform_state_unlock, id: state.to_global_id.to_s) }
+
+ before do
+ expect(state).to be_locked
+ post_graphql_mutation(mutation, current_user: user)
+ end
+
+ include_examples 'a working graphql query'
+
+ it 'unlocks the state' do
+ expect(state.reload).not_to be_locked
+ end
+end
diff --git a/spec/requests/api/graphql/user/group_member_query_spec.rb b/spec/requests/api/graphql/user/group_member_query_spec.rb
index 3a16d962214..e47cef8cc37 100644
--- a/spec/requests/api/graphql/user/group_member_query_spec.rb
+++ b/spec/requests/api/graphql/user/group_member_query_spec.rb
@@ -19,6 +19,7 @@ RSpec.describe 'GroupMember' do
}
HEREDOC
end
+
let_it_be(:query) do
graphql_query_for('user', { id: member.user.to_global_id.to_s }, query_graphql_field("groupMemberships", {}, fields))
end
diff --git a/spec/requests/api/graphql/user/project_member_query_spec.rb b/spec/requests/api/graphql/user/project_member_query_spec.rb
index 0790e148caf..01827e94d5d 100644
--- a/spec/requests/api/graphql/user/project_member_query_spec.rb
+++ b/spec/requests/api/graphql/user/project_member_query_spec.rb
@@ -19,6 +19,7 @@ RSpec.describe 'ProjectMember' do
}
HEREDOC
end
+
let_it_be(:query) do
graphql_query_for('user', { id: member.user.to_global_id.to_s }, query_graphql_field("projectMemberships", {}, fields))
end
diff --git a/spec/requests/api/graphql/user_query_spec.rb b/spec/requests/api/graphql/user_query_spec.rb
index 79debd0b7ef..738e120549e 100644
--- a/spec/requests/api/graphql/user_query_spec.rb
+++ b/spec/requests/api/graphql/user_query_spec.rb
@@ -32,22 +32,27 @@ RSpec.describe 'getting user information' do
create(:merge_request, :unique_branches, :unique_author,
source_project: project_a, assignees: [user])
end
+
let_it_be(:assigned_mr_b) do
create(:merge_request, :unique_branches, :unique_author,
source_project: project_b, assignees: [user])
end
+
let_it_be(:assigned_mr_c) do
create(:merge_request, :unique_branches, :unique_author,
source_project: project_b, assignees: [user])
end
+
let_it_be(:authored_mr) do
create(:merge_request, :unique_branches,
source_project: project_a, author: user)
end
+
let_it_be(:authored_mr_b) do
create(:merge_request, :unique_branches,
source_project: project_b, author: user)
end
+
let_it_be(:authored_mr_c) do
create(:merge_request, :unique_branches,
source_project: project_b, author: user)
@@ -59,6 +64,7 @@ RSpec.describe 'getting user information' do
let(:user_params) { { username: user.username } }
before do
+ create(:user_status, user: user)
post_graphql(query, current_user: current_user)
end
@@ -76,9 +82,15 @@ RSpec.describe 'getting user information' do
'username' => presenter.username,
'webUrl' => presenter.web_url,
'avatarUrl' => presenter.avatar_url,
- 'status' => presenter.status,
'email' => presenter.email
))
+
+ expect(graphql_data['user']['status']).to match(
+ a_hash_including(
+ 'emoji' => presenter.status.emoji,
+ 'message' => presenter.status.message,
+ 'availability' => presenter.status.availability.upcase
+ ))
end
describe 'assignedMergeRequests' do
diff --git a/spec/requests/api/graphql_spec.rb b/spec/requests/api/graphql_spec.rb
index 94a66f54e4d..5dc8edb87e9 100644
--- a/spec/requests/api/graphql_spec.rb
+++ b/spec/requests/api/graphql_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe 'GraphQL' do
include GraphqlHelpers
- let(:query) { graphql_query_for('echo', 'text' => 'Hello world' ) }
+ let(:query) { graphql_query_for('echo', text: 'Hello world' ) }
context 'logging' do
shared_examples 'logging a graphql query' do
diff --git a/spec/requests/api/group_labels_spec.rb b/spec/requests/api/group_labels_spec.rb
index f965a845bbe..72621e2ce5e 100644
--- a/spec/requests/api/group_labels_spec.rb
+++ b/spec/requests/api/group_labels_spec.rb
@@ -7,60 +7,97 @@ RSpec.describe API::GroupLabels do
let(:group) { create(:group) }
let(:subgroup) { create(:group, parent: group) }
let!(:group_member) { create(:group_member, group: group, user: user) }
- let!(:group_label1) { create(:group_label, title: 'feature', group: group) }
+ let!(:group_label1) { create(:group_label, title: 'feature-label', group: group) }
let!(:group_label2) { create(:group_label, title: 'bug', group: group) }
- let!(:subgroup_label) { create(:group_label, title: 'support', group: subgroup) }
+ let!(:subgroup_label) { create(:group_label, title: 'support-label', group: subgroup) }
describe 'GET :id/labels' do
- it 'returns all available labels for the group' do
- get api("/groups/#{group.id}/labels", user)
+ context 'get current group labels' do
+ let(:request) { get api("/groups/#{group.id}/labels", user) }
+ let(:expected_labels) { [group_label1.name, group_label2.name] }
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- expect(json_response).to all(match_schema('public_api/v4/labels/label'))
- expect(json_response.size).to eq(2)
- expect(json_response.map {|r| r['name'] }).to contain_exactly('feature', 'bug')
- end
-
- context 'when the with_counts parameter is set' do
- it 'includes counts in the response' do
- get api("/groups/#{group.id}/labels", user), params: { with_counts: true }
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- expect(json_response).to all(match_schema('public_api/v4/labels/label_with_counts'))
- expect(json_response.size).to eq(2)
- expect(json_response.map { |r| r['open_issues_count'] }).to contain_exactly(0, 0)
+ it_behaves_like 'fetches labels'
+
+ context 'when search param is provided' do
+ let(:request) { get api("/groups/#{group.id}/labels?search=lab", user) }
+ let(:expected_labels) { [group_label1.name] }
+
+ it_behaves_like 'fetches labels'
end
- end
- end
- describe 'GET :subgroup_id/labels' do
- context 'when the include_ancestor_groups parameter is not set' do
- it 'returns all available labels for the group and ancestor groups' do
- get api("/groups/#{subgroup.id}/labels", 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).to all(match_schema('public_api/v4/labels/label'))
- expect(json_response.size).to eq(3)
- expect(json_response.map {|r| r['name'] }).to contain_exactly('feature', 'bug', 'support')
+ context 'when the with_counts parameter is set' do
+ it 'includes counts in the response' do
+ get api("/groups/#{group.id}/labels", user), params: { with_counts: true }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response).to all(match_schema('public_api/v4/labels/label_with_counts'))
+ expect(json_response.size).to eq(2)
+ expect(json_response.map { |r| r['open_issues_count'] }).to contain_exactly(0, 0)
+ end
+ end
+
+ context 'when include_descendant_groups param is provided' do
+ let!(:project) { create(:project, group: group) }
+ let!(:project_label1) { create(:label, title: 'project-label1', project: project, priority: 3) }
+ let!(:project_label2) { create(:label, title: 'project-bug', project: project) }
+
+ let(:request) { get api("/groups/#{group.id}/labels", user), params: { include_descendant_groups: true } }
+ let(:expected_labels) { [group_label1.name, group_label2.name, subgroup_label.name] }
+
+ it_behaves_like 'fetches labels'
+
+ context 'when search param is provided' do
+ let(:request) { get api("/groups/#{group.id}/labels", user), params: { search: 'lab', include_descendant_groups: true } }
+ let(:expected_labels) { [group_label1.name, subgroup_label.name] }
+
+ it_behaves_like 'fetches labels'
+ end
+
+ context 'when only_group_labels param is false' do
+ let(:request) { get api("/groups/#{group.id}/labels", user), params: { include_descendant_groups: true, only_group_labels: false } }
+ let(:expected_labels) { [group_label1.name, group_label2.name, subgroup_label.name, project_label1.name, project_label2.name] }
+
+ it_behaves_like 'fetches labels'
+
+ context 'when search param is provided' do
+ let(:request) { get api("/groups/#{group.id}/labels", user), params: { search: 'lab', include_descendant_groups: true, only_group_labels: false } }
+ let(:expected_labels) { [group_label1.name, subgroup_label.name, project_label1.name] }
+
+ it_behaves_like 'fetches labels'
+ end
+ end
end
end
- context 'when the include_ancestor_groups parameter is set to false' do
- it 'returns all available labels for the group but not for ancestor groups' do
- get api("/groups/#{subgroup.id}/labels", user), params: { include_ancestor_groups: false }
+ describe 'with subgroup labels' do
+ context 'when the include_ancestor_groups parameter is not set' do
+ let(:request) { get api("/groups/#{subgroup.id}/labels", user) }
+ let(:expected_labels) { [group_label1.name, group_label2.name, subgroup_label.name] }
+
+ it_behaves_like 'fetches labels'
+
+ context 'when search param is provided' do
+ let(:request) { get api("/groups/#{subgroup.id}/labels?search=lab", user) }
+ let(:expected_labels) { [group_label1.name, subgroup_label.name] }
+
+ it_behaves_like 'fetches labels'
+ end
+ end
+
+ context 'when the include_ancestor_groups parameter is set to false' do
+ let(:request) { get api("/groups/#{subgroup.id}/labels", user), params: { include_ancestor_groups: false } }
+ let(:expected_labels) { [subgroup_label.name] }
+
+ it_behaves_like 'fetches labels'
+
+ context 'when search param is provided' do
+ let(:request) { get api("/groups/#{subgroup.id}/labels?search=lab", user), params: { include_ancestor_groups: false } }
+ let(:expected_labels) { [subgroup_label.name] }
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- expect(json_response).to all(match_schema('public_api/v4/labels/label'))
- expect(json_response.size).to eq(1)
- expect(json_response.map {|r| r['name'] }).to contain_exactly('support')
+ it_behaves_like 'fetches labels'
+ end
end
end
end
@@ -223,7 +260,7 @@ RSpec.describe API::GroupLabels do
expect(response).to have_gitlab_http_status(:ok)
expect(subgroup.labels[0].name).to eq('New Label')
- expect(group_label1.name).to eq('feature')
+ expect(group_label1.name).to eq(group_label1.title)
end
it 'returns 404 if label does not exist' do
@@ -278,7 +315,7 @@ RSpec.describe API::GroupLabels do
expect(response).to have_gitlab_http_status(:ok)
expect(subgroup.labels[0].name).to eq('New Label')
- expect(group_label1.name).to eq('feature')
+ expect(group_label1.name).to eq(group_label1.title)
end
it 'returns 404 if label does not exist' do
diff --git a/spec/requests/api/import_github_spec.rb b/spec/requests/api/import_github_spec.rb
index bbfb17fe753..5bbcb0c1950 100644
--- a/spec/requests/api/import_github_spec.rb
+++ b/spec/requests/api/import_github_spec.rb
@@ -57,6 +57,22 @@ RSpec.describe API::ImportGithub do
expect(json_response['name']).to eq(project.name)
end
+ it 'returns 201 response when the project is imported successfully from GHE' do
+ allow(Gitlab::LegacyGithubImport::ProjectCreator)
+ .to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider)
+ .and_return(double(execute: project))
+
+ post api("/import/github", user), params: {
+ target_namespace: user.namespace_path,
+ personal_access_token: token,
+ repo_id: non_existing_record_id,
+ github_hostname: "https://github.somecompany.com/"
+ }
+ expect(response).to have_gitlab_http_status(:created)
+ expect(json_response).to be_a Hash
+ expect(json_response['name']).to eq(project.name)
+ end
+
it 'returns 422 response when user can not create projects in the chosen namespace' do
other_namespace = create(:group, name: 'other_namespace')
diff --git a/spec/requests/api/internal/base_spec.rb b/spec/requests/api/internal/base_spec.rb
index ab5f09305ce..6fe77727702 100644
--- a/spec/requests/api/internal/base_spec.rb
+++ b/spec/requests/api/internal/base_spec.rb
@@ -253,6 +253,7 @@ RSpec.describe API::Internal::Base do
describe "POST /internal/lfs_authenticate" do
before do
+ stub_lfs_setting(enabled: true)
project.add_developer(user)
end
@@ -293,6 +294,33 @@ RSpec.describe API::Internal::Base do
expect(response).to have_gitlab_http_status(:not_found)
end
+
+ it 'returns a 404 when LFS is disabled on the project' do
+ project.update!(lfs_enabled: false)
+ lfs_auth_user(user.id, project)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+
+ context 'other repository types' do
+ it 'returns the correct information for a project wiki' do
+ wiki = create(:project_wiki, project: project)
+ lfs_auth_user(user.id, wiki)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['username']).to eq(user.username)
+ expect(json_response['repository_http_path']).to eq(wiki.http_url_to_repo)
+ expect(json_response['expires_in']).to eq(Gitlab::LfsToken::DEFAULT_EXPIRE_TIME)
+ expect(Gitlab::LfsToken.new(user).token_valid?(json_response['lfs_token'])).to be_truthy
+ end
+
+ it 'returns a 404 when the container does not support LFS' do
+ snippet = create(:project_snippet)
+ lfs_auth_user(user.id, snippet)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
end
context 'deploy key' do
diff --git a/spec/requests/api/internal/pages_spec.rb b/spec/requests/api/internal/pages_spec.rb
index e58eba02132..9a63e2a8ed5 100644
--- a/spec/requests/api/internal/pages_spec.rb
+++ b/spec/requests/api/internal/pages_spec.rb
@@ -12,6 +12,7 @@ RSpec.describe API::Internal::Pages do
before do
allow(Gitlab::Pages).to receive(:secret).and_return(pages_secret)
+ stub_pages_object_storage(::Pages::DeploymentUploader)
end
describe "GET /internal/pages/status" do
@@ -38,6 +39,12 @@ RSpec.describe API::Internal::Pages do
get api("/internal/pages"), headers: headers, params: { host: host }
end
+ around do |example|
+ freeze_time do
+ example.run
+ end
+ end
+
context 'not authenticated' do
it 'responds with 401 Unauthorized' do
query_host('pages.gitlab.io')
@@ -55,7 +62,9 @@ RSpec.describe API::Internal::Pages do
end
def deploy_pages(project)
+ deployment = create(:pages_deployment, project: project)
project.mark_pages_as_deployed
+ project.update_pages_deployment!(deployment)
end
context 'domain does not exist' do
@@ -182,6 +191,7 @@ RSpec.describe API::Internal::Pages do
expect(json_response['certificate']).to eq(pages_domain.certificate)
expect(json_response['key']).to eq(pages_domain.key)
+ deployment = project.pages_metadatum.pages_deployment
expect(json_response['lookup_paths']).to eq(
[
{
@@ -190,8 +200,12 @@ RSpec.describe API::Internal::Pages do
'https_only' => false,
'prefix' => '/',
'source' => {
- 'type' => 'file',
- 'path' => 'gitlab-org/gitlab-ce/public/'
+ 'type' => 'zip',
+ 'path' => deployment.file.url(expire_at: 1.day.from_now),
+ 'global_id' => "gid://gitlab/PagesDeployment/#{deployment.id}",
+ 'sha256' => deployment.file_sha256,
+ 'file_size' => deployment.size,
+ 'file_count' => deployment.file_count
}
}
]
@@ -218,6 +232,7 @@ RSpec.describe API::Internal::Pages do
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('internal/pages/virtual_domain')
+ deployment = project.pages_metadatum.pages_deployment
expect(json_response['lookup_paths']).to eq(
[
{
@@ -226,8 +241,12 @@ RSpec.describe API::Internal::Pages do
'https_only' => false,
'prefix' => '/myproject/',
'source' => {
- 'type' => 'file',
- 'path' => 'mygroup/myproject/public/'
+ 'type' => 'zip',
+ 'path' => deployment.file.url(expire_at: 1.day.from_now),
+ 'global_id' => "gid://gitlab/PagesDeployment/#{deployment.id}",
+ 'sha256' => deployment.file_sha256,
+ 'file_size' => deployment.size,
+ 'file_count' => deployment.file_count
}
}
]
@@ -235,6 +254,20 @@ RSpec.describe API::Internal::Pages do
end
end
+ it 'avoids N+1 queries' do
+ project = create(:project, group: group)
+ deploy_pages(project)
+
+ control = ActiveRecord::QueryRecorder.new { query_host('mygroup.gitlab-pages.io') }
+
+ 3.times do
+ project = create(:project, group: group)
+ deploy_pages(project)
+ end
+
+ expect { query_host('mygroup.gitlab-pages.io') }.not_to exceed_query_limit(control)
+ end
+
context 'group root project' do
it 'responds with the correct domain configuration' do
project = create(:project, group: group, name: 'mygroup.gitlab-pages.io')
@@ -245,6 +278,7 @@ RSpec.describe API::Internal::Pages do
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('internal/pages/virtual_domain')
+ deployment = project.pages_metadatum.pages_deployment
expect(json_response['lookup_paths']).to eq(
[
{
@@ -253,8 +287,12 @@ RSpec.describe API::Internal::Pages do
'https_only' => false,
'prefix' => '/',
'source' => {
- 'type' => 'file',
- 'path' => 'mygroup/mygroup.gitlab-pages.io/public/'
+ 'type' => 'zip',
+ 'path' => deployment.file.url(expire_at: 1.day.from_now),
+ 'global_id' => "gid://gitlab/PagesDeployment/#{deployment.id}",
+ 'sha256' => deployment.file_sha256,
+ 'file_size' => deployment.size,
+ 'file_count' => deployment.file_count
}
}
]
diff --git a/spec/requests/api/invitations_spec.rb b/spec/requests/api/invitations_spec.rb
new file mode 100644
index 00000000000..75586970abb
--- /dev/null
+++ b/spec/requests/api/invitations_spec.rb
@@ -0,0 +1,301 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe API::Invitations do
+ let(:maintainer) { create(:user, username: 'maintainer_user') }
+ let(:developer) { create(:user) }
+ let(:access_requester) { create(:user) }
+ let(:stranger) { create(:user) }
+ let(:email) { 'email1@example.com' }
+ let(:email2) { 'email2@example.com' }
+
+ let(:project) do
+ create(:project, :public, creator_id: maintainer.id, namespace: maintainer.namespace) do |project|
+ project.add_developer(developer)
+ project.add_maintainer(maintainer)
+ project.request_access(access_requester)
+ end
+ end
+
+ let!(:group) do
+ create(:group, :public) do |group|
+ group.add_developer(developer)
+ group.add_owner(maintainer)
+ group.request_access(access_requester)
+ end
+ end
+
+ def invitations_url(source, user)
+ api("/#{source.model_name.plural}/#{source.id}/invitations", user)
+ end
+
+ shared_examples 'POST /:source_type/:id/invitations' do |source_type|
+ context "with :source_type == #{source_type.pluralize}" do
+ it_behaves_like 'a 404 response when source is private' do
+ let(:route) do
+ post invitations_url(source, stranger),
+ params: { email: email, access_level: Member::MAINTAINER }
+ end
+ end
+
+ context 'when authenticated as a non-member or member with insufficient rights' do
+ %i[access_requester stranger developer].each do |type|
+ context "as a #{type}" do
+ it 'returns 403' do
+ user = public_send(type)
+
+ post invitations_url(source, user), params: { email: email, access_level: Member::MAINTAINER }
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+ end
+ end
+
+ context 'when authenticated as a maintainer/owner' do
+ context 'and new member is already a requester' do
+ it 'does not transform the requester into a proper member' do
+ expect do
+ post api("/#{source_type.pluralize}/#{source.id}/invitations", maintainer),
+ params: { email: email, access_level: Member::MAINTAINER }
+
+ expect(response).to have_gitlab_http_status(:created)
+ end.not_to change { source.members.count }
+ end
+ end
+
+ it 'invites a new member' do
+ expect do
+ post api("/#{source_type.pluralize}/#{source.id}/invitations", maintainer),
+ params: { email: email, access_level: Member::DEVELOPER }
+
+ expect(response).to have_gitlab_http_status(:created)
+ end.to change { source.requesters.count }.by(1)
+ end
+
+ it 'invites a list of new email addresses' do
+ expect do
+ email_list = [email, email2].join(',')
+
+ post api("/#{source_type.pluralize}/#{source.id}/invitations", maintainer),
+ params: { email: email_list, access_level: Member::DEVELOPER }
+
+ expect(response).to have_gitlab_http_status(:created)
+ end.to change { source.requesters.count }.by(2)
+ end
+ end
+
+ context 'access levels' do
+ it 'does not create the member if group level is higher' do
+ parent = create(:group)
+
+ group.update!(parent: parent)
+ project.update!(group: group)
+ parent.add_developer(stranger)
+
+ post api("/#{source_type.pluralize}/#{source.id}/invitations", maintainer),
+ params: { email: stranger.email, access_level: Member::REPORTER }
+
+ expect(response).to have_gitlab_http_status(:created)
+ expect(json_response['message'][stranger.email]).to eq("Access level should be greater than or equal to Developer inherited membership from group #{parent.name}")
+ end
+
+ it 'creates the member if group level is lower' do
+ parent = create(:group)
+
+ group.update!(parent: parent)
+ project.update!(group: group)
+ parent.add_developer(stranger)
+
+ post api("/#{source_type.pluralize}/#{source.id}/invitations", maintainer),
+ params: { email: stranger.email, access_level: Member::MAINTAINER }
+
+ expect(response).to have_gitlab_http_status(:created)
+ end
+ end
+
+ context 'access expiry date' do
+ subject do
+ post api("/#{source_type.pluralize}/#{source.id}/invitations", maintainer),
+ params: { email: email, access_level: Member::DEVELOPER, expires_at: expires_at }
+ end
+
+ context 'when set to a date in the past' do
+ let(:expires_at) { 2.days.ago.to_date }
+
+ it 'does not create a member' do
+ expect do
+ subject
+ end.not_to change { source.members.count }
+
+ expect(response).to have_gitlab_http_status(:created)
+ expect(json_response['message'][email]).to eq('Expires at cannot be a date in the past')
+ end
+ end
+
+ context 'when set to a date in the future' do
+ let(:expires_at) { 2.days.from_now.to_date }
+
+ it 'invites a member' do
+ expect do
+ subject
+ end.to change { source.requesters.count }.by(1)
+
+ expect(response).to have_gitlab_http_status(:created)
+ end
+ end
+ end
+
+ it "returns a message if member already exists" do
+ post api("/#{source_type.pluralize}/#{source.id}/invitations", maintainer),
+ params: { email: maintainer.email, access_level: Member::MAINTAINER }
+
+ expect(response).to have_gitlab_http_status(:created)
+ expect(json_response['message'][maintainer.email]).to eq("Already a member of #{source.name}")
+ end
+
+ it 'returns 404 when the email is not valid' do
+ post api("/#{source_type.pluralize}/#{source.id}/invitations", maintainer),
+ params: { email: '', access_level: Member::MAINTAINER }
+
+ expect(response).to have_gitlab_http_status(:created)
+ expect(json_response['message']).to eq('Email cannot be blank')
+ end
+
+ it 'returns 404 when the email list is not a valid format' do
+ post api("/#{source_type.pluralize}/#{source.id}/invitations", maintainer),
+ params: { email: 'email1@example.com,not-an-email', access_level: Member::MAINTAINER }
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['error']).to eq('email contains an invalid email address')
+ end
+
+ it 'returns 400 when email is not given' do
+ post api("/#{source_type.pluralize}/#{source.id}/invitations", maintainer),
+ params: { access_level: Member::MAINTAINER }
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+
+ it 'returns 400 when access_level is not given' do
+ post api("/#{source_type.pluralize}/#{source.id}/invitations", maintainer),
+ params: { email: email }
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+
+ it 'returns 400 when access_level is not valid' do
+ post invitations_url(source, maintainer),
+ params: { email: email, access_level: non_existing_record_access_level }
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+ end
+ end
+
+ describe 'POST /projects/:id/invitations' do
+ it_behaves_like 'POST /:source_type/:id/invitations', 'project' do
+ let(:source) { project }
+ end
+ end
+
+ describe 'POST /groups/:id/invitations' do
+ it_behaves_like 'POST /:source_type/:id/invitations', 'group' do
+ let(:source) { group }
+ end
+ end
+
+ shared_examples 'GET /:source_type/:id/invitations' do |source_type|
+ context "with :source_type == #{source_type.pluralize}" do
+ it_behaves_like 'a 404 response when source is private' do
+ let(:route) { get invitations_url(source, stranger) }
+ end
+
+ %i[maintainer developer access_requester stranger].each do |type|
+ context "when authenticated as a #{type}" do
+ it 'returns 200' do
+ user = public_send(type)
+
+ get invitations_url(source, 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(0)
+ end
+ end
+ end
+
+ it 'avoids N+1 queries' do
+ # Establish baseline
+ get invitations_url(source, maintainer)
+
+ control = ActiveRecord::QueryRecorder.new do
+ get invitations_url(source, maintainer)
+ end
+
+ invite_member_by_email(source, source_type, email, maintainer)
+
+ expect do
+ get invitations_url(source, maintainer)
+ end.not_to exceed_query_limit(control)
+ end
+
+ it 'does not find confirmed members' do
+ get invitations_url(source, developer)
+
+ 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(0)
+ expect(json_response.map { |u| u['id'] }).not_to match_array [maintainer.id, developer.id]
+ end
+
+ it 'finds all members with no query string specified' do
+ invite_member_by_email(source, source_type, email, developer)
+ invite_member_by_email(source, source_type, email2, developer)
+
+ get invitations_url(source, developer), params: { query: '' }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to include_pagination_headers
+
+ expect(json_response).to be_an Array
+ expect(json_response.count).to eq(2)
+ expect(json_response.map { |u| u['invite_email'] }).to match_array [email, email2]
+ end
+
+ it 'finds the invitation by invite_email with query string' do
+ invite_member_by_email(source, source_type, email, developer)
+ invite_member_by_email(source, source_type, email2, developer)
+
+ get invitations_url(source, developer), params: { query: email }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.count).to eq(1)
+ expect(json_response.first['invite_email']).to eq(email)
+ expect(json_response.first['created_by_name']).to eq(developer.name)
+ expect(json_response.first['user_name']).to eq(nil)
+ end
+
+ def invite_member_by_email(source, source_type, email, created_by)
+ create(:"#{source_type}_member", invite_token: '123', invite_email: email, source: source, user: nil, created_by: created_by)
+ end
+ end
+ end
+
+ describe 'GET /projects/:id/invitations' do
+ it_behaves_like 'GET /:source_type/:id/invitations', 'project' do
+ let(:source) { project }
+ end
+ end
+
+ describe 'GET /groups/:id/invitations' do
+ it_behaves_like 'GET /:source_type/:id/invitations', 'group' do
+ let(:source) { group }
+ end
+ end
+end
diff --git a/spec/requests/api/issues/get_project_issues_spec.rb b/spec/requests/api/issues/get_project_issues_spec.rb
index 4228ca2d5fd..da0bae8d5e7 100644
--- a/spec/requests/api/issues/get_project_issues_spec.rb
+++ b/spec/requests/api/issues/get_project_issues_spec.rb
@@ -54,11 +54,13 @@ RSpec.describe API::Issues do
let_it_be(:label) do
create(:label, title: 'label', color: '#FFAABB', project: project)
end
+
let!(:label_link) { create(:label_link, label: label, target: issue) }
let(:milestone) { create(:milestone, title: '1.0.0', project: project) }
let_it_be(:empty_milestone) do
create(:milestone, title: '2.0.0', project: project)
end
+
let!(:note) { create(:note_on_issue, author: user, project: project, noteable: issue) }
let(:no_milestone_title) { 'None' }
diff --git a/spec/requests/api/issues/issues_spec.rb b/spec/requests/api/issues/issues_spec.rb
index b8cbddd9ed4..0fe68be027c 100644
--- a/spec/requests/api/issues/issues_spec.rb
+++ b/spec/requests/api/issues/issues_spec.rb
@@ -54,11 +54,13 @@ RSpec.describe API::Issues do
let_it_be(:label) do
create(:label, title: 'label', color: '#FFAABB', project: project)
end
+
let!(:label_link) { create(:label_link, label: label, target: issue) }
let(:milestone) { create(:milestone, title: '1.0.0', project: project) }
let_it_be(:empty_milestone) do
create(:milestone, title: '2.0.0', project: project)
end
+
let!(:note) { create(:note_on_issue, author: user, project: project, noteable: issue) }
let(:no_milestone_title) { 'None' }
diff --git a/spec/requests/api/issues/post_projects_issues_spec.rb b/spec/requests/api/issues/post_projects_issues_spec.rb
index a7fe4d4509a..5b3e2363669 100644
--- a/spec/requests/api/issues/post_projects_issues_spec.rb
+++ b/spec/requests/api/issues/post_projects_issues_spec.rb
@@ -53,11 +53,13 @@ RSpec.describe API::Issues do
let_it_be(:label) do
create(:label, title: 'label', color: '#FFAABB', project: project)
end
+
let!(:label_link) { create(:label_link, label: label, target: issue) }
let(:milestone) { create(:milestone, title: '1.0.0', project: project) }
let_it_be(:empty_milestone) do
create(:milestone, title: '2.0.0', project: project)
end
+
let!(:note) { create(:note_on_issue, author: user, project: project, noteable: issue) }
let(:no_milestone_title) { 'None' }
diff --git a/spec/requests/api/labels_spec.rb b/spec/requests/api/labels_spec.rb
index fc674fca9b2..b368f6e329c 100644
--- a/spec/requests/api/labels_spec.rb
+++ b/spec/requests/api/labels_spec.rb
@@ -178,8 +178,8 @@ RSpec.describe API::Labels do
end
describe 'GET /projects/:id/labels' do
- let(:group) { create(:group) }
- let!(:group_label) { create(:group_label, title: 'feature', group: group) }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:group_label) { create(:group_label, title: 'feature label', group: group) }
before do
project.update!(group: group)
@@ -250,49 +250,41 @@ RSpec.describe API::Labels do
end
end
- context 'when the include_ancestor_groups parameter is not set' do
- let(:group) { create(:group) }
- let!(:group_label) { create(:group_label, title: 'feature', group: group) }
- let(:subgroup) { create(:group, parent: group) }
- let!(:subgroup_label) { create(:group_label, title: 'support', group: subgroup) }
+ context 'with subgroups' do
+ let_it_be(:subgroup) { create(:group, parent: group) }
+ let_it_be(:subgroup_label) { create(:group_label, title: 'support label', group: subgroup) }
before do
subgroup.add_owner(user)
project.update!(group: subgroup)
end
- it 'returns all available labels for the project, parent group and ancestor groups' do
- get api("/projects/#{project.id}/labels", user)
+ context 'when the include_ancestor_groups parameter is not set' do
+ let(:request) { get api("/projects/#{project.id}/labels", user) }
+ let(:expected_labels) { [priority_label.name, group_label.name, subgroup_label.name, label1.name] }
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- expect(json_response).to all(match_schema('public_api/v4/labels/label'))
- expect(json_response.size).to eq(4)
- expect(json_response.map {|r| r['name'] }).to contain_exactly(group_label.name, subgroup_label.name, priority_label.name, label1.name)
- end
- end
+ it_behaves_like 'fetches labels'
- context 'when the include_ancestor_groups parameter is set to false' do
- let(:group) { create(:group) }
- let!(:group_label) { create(:group_label, title: 'feature', group: group) }
- let(:subgroup) { create(:group, parent: group) }
- let!(:subgroup_label) { create(:group_label, title: 'support', group: subgroup) }
+ context 'when search param is provided' do
+ let(:request) { get api("/projects/#{project.id}/labels?search=lab", user) }
+ let(:expected_labels) { [group_label.name, subgroup_label.name, label1.name] }
- before do
- subgroup.add_owner(user)
- project.update!(group: subgroup)
+ it_behaves_like 'fetches labels'
+ end
end
- it 'returns all available labels for the project and the parent group only' do
- get api("/projects/#{project.id}/labels", user), params: { include_ancestor_groups: false }
+ context 'when the include_ancestor_groups parameter is set to false' do
+ let(:request) { get api("/projects/#{project.id}/labels", user), params: { include_ancestor_groups: false } }
+ let(:expected_labels) { [subgroup_label.name, priority_label.name, label1.name] }
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- expect(json_response).to all(match_schema('public_api/v4/labels/label'))
- expect(json_response.size).to eq(3)
- expect(json_response.map {|r| r['name'] }).to contain_exactly(subgroup_label.name, priority_label.name, label1.name)
+ it_behaves_like 'fetches labels'
+
+ context 'when search param is provided' do
+ let(:request) { get api("/projects/#{project.id}/labels?search=lab", user), params: { include_ancestor_groups: false } }
+ let(:expected_labels) { [subgroup_label.name, label1.name] }
+
+ it_behaves_like 'fetches labels'
+ end
end
end
end
@@ -513,7 +505,7 @@ RSpec.describe API::Labels do
end
describe 'PUT /projects/:id/labels/promote' do
- let(:group) { create(:group) }
+ let_it_be(:group) { create(:group) }
before do
group.add_owner(user)
diff --git a/spec/requests/api/lint_spec.rb b/spec/requests/api/lint_spec.rb
index 9890cdc20c0..aecbcfb5b5a 100644
--- a/spec/requests/api/lint_spec.rb
+++ b/spec/requests/api/lint_spec.rb
@@ -9,12 +9,13 @@ RSpec.describe API::Lint do
File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml'))
end
- it 'passes validation' do
+ it 'passes validation without warnings or errors' do
post api('/ci/lint'), params: { content: yaml_content }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to be_an Hash
expect(json_response['status']).to eq('valid')
+ expect(json_response['warnings']).to eq([])
expect(json_response['errors']).to eq([])
end
@@ -26,6 +27,20 @@ RSpec.describe API::Lint do
end
end
+ context 'with valid .gitlab-ci.yaml with warnings' do
+ let(:yaml_content) { { job: { script: 'ls', rules: [{ when: 'always' }] } }.to_yaml }
+
+ it 'passes validation but returns warnings' do
+ post api('/ci/lint'), params: { content: yaml_content }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['status']).to eq('valid')
+ expect(json_response['warnings']).not_to be_empty
+ expect(json_response['status']).to eq('valid')
+ expect(json_response['errors']).to eq([])
+ end
+ end
+
context 'with an invalid .gitlab_ci.yml' do
context 'with invalid syntax' do
let(:yaml_content) { 'invalid content' }
@@ -35,6 +50,7 @@ RSpec.describe API::Lint do
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['status']).to eq('invalid')
+ expect(json_response['warnings']).to eq([])
expect(json_response['errors']).to eq(['Invalid configuration format'])
end
@@ -54,6 +70,7 @@ RSpec.describe API::Lint do
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['status']).to eq('invalid')
+ expect(json_response['warnings']).to eq([])
expect(json_response['errors']).to eq(['jobs config should contain at least one visible job'])
end
@@ -82,7 +99,18 @@ RSpec.describe API::Lint do
let(:project) { create(:project, :repository) }
let(:dry_run) { nil }
- RSpec.shared_examples 'valid config' do
+ RSpec.shared_examples 'valid config with warnings' do
+ it 'passes validation with warnings' do
+ ci_lint
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['valid']).to eq(true)
+ expect(json_response['errors']).to eq([])
+ expect(json_response['warnings']).not_to be_empty
+ end
+ end
+
+ RSpec.shared_examples 'valid config without warnings' do
it 'passes validation' do
ci_lint
@@ -94,6 +122,7 @@ RSpec.describe API::Lint do
expect(json_response).to be_an Hash
expect(json_response['merged_yaml']).to eq(expected_yaml)
expect(json_response['valid']).to eq(true)
+ expect(json_response['warnings']).to eq([])
expect(json_response['errors']).to eq([])
end
end
@@ -105,6 +134,7 @@ RSpec.describe API::Lint do
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['merged_yaml']).to eq(yaml_content)
expect(json_response['valid']).to eq(false)
+ expect(json_response['warnings']).to eq([])
expect(json_response['errors']).to eq(['jobs config should contain at least one visible job'])
end
end
@@ -157,6 +187,7 @@ RSpec.describe API::Lint do
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['merged_yaml']).to eq(nil)
expect(json_response['valid']).to eq(false)
+ expect(json_response['warnings']).to eq([])
expect(json_response['errors']).to eq(['Insufficient permissions to create a new pipeline'])
end
end
@@ -186,7 +217,7 @@ RSpec.describe API::Lint do
)
end
- it_behaves_like 'valid config'
+ it_behaves_like 'valid config without warnings'
end
end
end
@@ -242,13 +273,19 @@ RSpec.describe API::Lint do
context 'when running as dry run' do
let(:dry_run) { true }
- it_behaves_like 'valid config'
+ it_behaves_like 'valid config without warnings'
end
context 'when running static validation' do
let(:dry_run) { false }
- it_behaves_like 'valid config'
+ it_behaves_like 'valid config without warnings'
+ end
+
+ context 'With warnings' do
+ let(:yaml_content) { { job: { script: 'ls', rules: [{ when: 'always' }] } }.to_yaml }
+
+ it_behaves_like 'valid config with warnings'
end
end
@@ -275,4 +312,167 @@ RSpec.describe API::Lint do
end
end
end
+
+ describe 'POST /projects/:id/ci/lint' do
+ subject(:ci_lint) { post api("/projects/#{project.id}/ci/lint", api_user), params: { dry_run: dry_run, content: yaml_content } }
+
+ let(:project) { create(:project, :repository) }
+ let(:dry_run) { nil }
+
+ let_it_be(:api_user) { create(:user) }
+
+ let_it_be(:yaml_content) do
+ { include: { local: 'another-gitlab-ci.yml' }, test: { stage: 'test', script: 'echo 1' } }.to_yaml
+ end
+
+ let_it_be(:included_content) do
+ { another_test: { stage: 'test', script: 'echo 1' } }.to_yaml
+ end
+
+ RSpec.shared_examples 'valid project config' do
+ it 'passes validation' do
+ ci_lint
+
+ included_config = YAML.safe_load(included_content, [Symbol])
+ root_config = YAML.safe_load(yaml_content, [Symbol])
+ expected_yaml = included_config.merge(root_config).except(:include).to_yaml
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to be_an Hash
+ expect(json_response['merged_yaml']).to eq(expected_yaml)
+ expect(json_response['valid']).to eq(true)
+ expect(json_response['errors']).to eq([])
+ end
+ end
+
+ RSpec.shared_examples 'invalid project config' do
+ it 'responds with errors about invalid configuration' do
+ ci_lint
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['merged_yaml']).to eq(yaml_content)
+ expect(json_response['valid']).to eq(false)
+ expect(json_response['errors']).to eq(['jobs config should contain at least one visible job'])
+ end
+ end
+
+ context 'when unauthenticated' do
+ let_it_be(:api_user) { nil }
+
+ it 'returns authentication error' do
+ ci_lint
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'when authenticated as non-member' do
+ context 'when project is private' do
+ before do
+ project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
+ end
+
+ it 'returns authentication error' do
+ ci_lint
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'when project is public' do
+ before do
+ project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
+ end
+
+ context 'when running as dry run' do
+ let(:dry_run) { true }
+
+ it 'returns pipeline creation error' do
+ ci_lint
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['merged_yaml']).to eq(nil)
+ expect(json_response['valid']).to eq(false)
+ expect(json_response['errors']).to eq(['Insufficient permissions to create a new pipeline'])
+ end
+ end
+
+ context 'when running static validation' do
+ let(:dry_run) { false }
+
+ before do
+ project.repository.create_file(
+ project.creator,
+ 'another-gitlab-ci.yml',
+ included_content,
+ message: 'Automatically created another-gitlab-ci.yml',
+ branch_name: 'master'
+ )
+ end
+
+ it_behaves_like 'valid project config'
+ end
+ end
+ end
+
+ context 'when authenticated as project guest' do
+ before do
+ project.add_guest(api_user)
+ end
+
+ it 'returns authentication error' do
+ ci_lint
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+
+ context 'when authenticated as project developer' do
+ before do
+ project.add_developer(api_user)
+ end
+
+ context 'with valid .gitlab-ci.yml content' do
+ before do
+ project.repository.create_file(
+ project.creator,
+ 'another-gitlab-ci.yml',
+ included_content,
+ message: 'Automatically created another-gitlab-ci.yml',
+ branch_name: 'master'
+ )
+ end
+
+ context 'when running as dry run' do
+ let(:dry_run) { true }
+
+ it_behaves_like 'valid project config'
+ end
+
+ context 'when running static validation' do
+ let(:dry_run) { false }
+
+ it_behaves_like 'valid project config'
+ end
+ end
+
+ context 'with invalid .gitlab-ci.yml content' do
+ let(:yaml_content) do
+ { image: 'ruby:2.7', services: ['postgres'] }.to_yaml
+ end
+
+ context 'when running as dry run' do
+ let(:dry_run) { true }
+
+ it_behaves_like 'invalid project config'
+ end
+
+ context 'when running static validation' do
+ let(:dry_run) { false }
+
+ it_behaves_like 'invalid project config'
+ end
+ end
+ end
+ end
end
diff --git a/spec/requests/api/maven_packages_spec.rb b/spec/requests/api/maven_packages_spec.rb
index 37748fe5ea7..f9ba819c9aa 100644
--- a/spec/requests/api/maven_packages_spec.rb
+++ b/spec/requests/api/maven_packages_spec.rb
@@ -92,15 +92,30 @@ RSpec.describe API::MavenPackages do
end
shared_examples 'downloads with a deploy token' do
- it 'allows download with deploy token' do
- download_file(
- package_file.file_name,
- {},
- Gitlab::Auth::AuthFinders::DEPLOY_TOKEN_HEADER => deploy_token.token
- )
+ context 'successful download' do
+ subject do
+ download_file(
+ package_file.file_name,
+ {},
+ Gitlab::Auth::AuthFinders::DEPLOY_TOKEN_HEADER => deploy_token.token
+ )
+ end
- expect(response).to have_gitlab_http_status(:ok)
- expect(response.media_type).to eq('application/octet-stream')
+ it 'allows download with deploy token' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.media_type).to eq('application/octet-stream')
+ end
+
+ it 'allows download with deploy token with only write_package_registry scope' do
+ deploy_token.update!(read_package_registry: false)
+
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.media_type).to eq('application/octet-stream')
+ end
end
end
@@ -355,6 +370,15 @@ RSpec.describe API::MavenPackages do
expect(response).to have_gitlab_http_status(:ok)
expect(response.media_type).to eq('application/octet-stream')
end
+
+ it 'returns the file with only write_package_registry scope' do
+ deploy_token_for_group.update!(read_package_registry: false)
+
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.media_type).to eq('application/octet-stream')
+ end
end
end
@@ -601,7 +625,7 @@ RSpec.describe API::MavenPackages do
upload_file(params: params.merge(job_token: job.token))
expect(response).to have_gitlab_http_status(:ok)
- expect(project.reload.packages.last.build_info.pipeline).to eq job.pipeline
+ expect(project.reload.packages.last.original_build_info.pipeline).to eq job.pipeline
end
it 'rejects upload without running job token' do
diff --git a/spec/requests/api/members_spec.rb b/spec/requests/api/members_spec.rb
index 047b9423906..919c8d29406 100644
--- a/spec/requests/api/members_spec.rb
+++ b/spec/requests/api/members_spec.rb
@@ -7,6 +7,7 @@ RSpec.describe API::Members do
let(:developer) { create(:user) }
let(:access_requester) { create(:user) }
let(:stranger) { create(:user) }
+ let(:user_with_minimal_access) { create(:user) }
let(:project) do
create(:project, :public, creator_id: maintainer.id, namespace: maintainer.namespace) do |project|
@@ -20,6 +21,7 @@ RSpec.describe API::Members do
create(:group, :public) do |group|
group.add_developer(developer)
group.add_owner(maintainer)
+ create(:group_member, :minimal_access, source: group, user: user_with_minimal_access)
group.request_access(access_requester)
end
end
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index 506607f4cc2..e7005bd3ec5 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -1312,13 +1312,44 @@ RSpec.describe API::MergeRequests do
end
describe 'GET /projects/:id/merge_requests/:merge_request_iid/changes' do
- let_it_be(:merge_request) { create(:merge_request, :simple, author: user, assignees: [user], source_project: project, target_project: project, source_branch: 'markdown', title: "Test", created_at: base_time) }
+ let_it_be(:merge_request) do
+ create(
+ :merge_request,
+ :simple,
+ author: user,
+ assignees: [user],
+ source_project: project,
+ target_project: project,
+ source_branch: 'markdown',
+ title: "Test",
+ created_at: base_time
+ )
+ end
- it 'returns the change information of the merge_request' do
- get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/changes", user)
+ shared_examples 'find an existing merge request' do
+ it 'returns the change information of the merge_request' do
+ get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/changes", user)
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response['changes'].size).to eq(merge_request.diffs.size)
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['changes'].size).to eq(merge_request.diffs.size)
+ expect(json_response['overflow']).to be_falsy
+ end
+ end
+
+ shared_examples 'accesses diffs via raw_diffs' do
+ let(:params) { {} }
+
+ it 'as expected' do
+ expect_any_instance_of(MergeRequest) do |merge_request|
+ expect(merge_request).to receive(:raw_diffs).and_call_original
+ end
+
+ expect_any_instance_of(MergeRequest) do |merge_request|
+ expect(merge_request).not_to receive(:diffs)
+ end
+
+ get(api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/changes", user), params: params)
+ end
end
it 'returns a 404 when merge_request_iid not found' do
@@ -1331,6 +1362,53 @@ RSpec.describe API::MergeRequests do
expect(response).to have_gitlab_http_status(:not_found)
end
+
+ it_behaves_like 'find an existing merge request'
+ it_behaves_like 'accesses diffs via raw_diffs'
+
+ it 'returns the overflow status as false' do
+ get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/changes", user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['overflow']).to be_falsy
+ end
+
+ context 'when using DB-backed diffs via feature flag' do
+ before do
+ stub_feature_flags(mrc_api_use_raw_diffs_from_gitaly: false)
+ end
+
+ it_behaves_like 'find an existing merge request'
+
+ it 'accesses diffs via DB-backed diffs.diffs' do
+ expect_any_instance_of(MergeRequest) do |merge_request|
+ expect(merge_request).to receive(:diffs).and_call_original
+ end
+
+ get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/changes", user)
+ end
+
+ context 'when the diff_collection has overflowed its size limits' do
+ before do
+ expect_next_instance_of(Gitlab::Git::DiffCollection) do |diff_collection|
+ expect(diff_collection).to receive(:overflow?).and_return(true)
+ end
+ end
+
+ it 'returns the overflow status as true' do
+ get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/changes", user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['overflow']).to be_truthy
+ end
+ end
+
+ context 'when access_raw_diffs is passed as an option' do
+ it_behaves_like 'accesses diffs via raw_diffs' do
+ let(:params) { { access_raw_diffs: true } }
+ end
+ end
+ end
end
describe 'GET /projects/:id/merge_requests/:merge_request_iid/pipelines' do
diff --git a/spec/requests/api/npm_instance_packages_spec.rb b/spec/requests/api/npm_instance_packages_spec.rb
new file mode 100644
index 00000000000..8299717b5c7
--- /dev/null
+++ b/spec/requests/api/npm_instance_packages_spec.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe API::NpmInstancePackages do
+ include_context 'npm api setup'
+
+ describe 'GET /api/v4/packages/npm/*package_name' do
+ it_behaves_like 'handling get metadata requests' do
+ let(:url) { api("/packages/npm/#{package_name}") }
+ end
+ end
+
+ describe 'GET /api/v4/packages/npm/-/package/*package_name/dist-tags' do
+ it_behaves_like 'handling get dist tags requests', scope: :instance do
+ let(:url) { api("/packages/npm/-/package/#{package_name}/dist-tags") }
+ end
+ end
+
+ describe 'PUT /api/v4/packages/npm/-/package/*package_name/dist-tags/:tag' do
+ it_behaves_like 'handling create dist tag requests', scope: :instance do
+ let(:url) { api("/packages/npm/-/package/#{package_name}/dist-tags/#{tag_name}") }
+ end
+ end
+
+ describe 'DELETE /api/v4/packages/npm/-/package/*package_name/dist-tags/:tag' do
+ it_behaves_like 'handling delete dist tag requests', scope: :instance do
+ let(:url) { api("/packages/npm/-/package/#{package_name}/dist-tags/#{tag_name}") }
+ end
+ end
+end
diff --git a/spec/requests/api/npm_packages_spec.rb b/spec/requests/api/npm_packages_spec.rb
deleted file mode 100644
index 8a3ccd7c6e3..00000000000
--- a/spec/requests/api/npm_packages_spec.rb
+++ /dev/null
@@ -1,556 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe API::NpmPackages do
- include PackagesManagerApiSpecHelpers
- include HttpBasicAuthHelpers
-
- let_it_be(:user) { create(:user) }
- let_it_be(:group) { create(:group) }
- let_it_be(:project, reload: true) { create(:project, :public, namespace: group) }
- let_it_be(:package, reload: true) { create(:npm_package, project: project) }
- let_it_be(:token) { create(:oauth_access_token, scopes: 'api', resource_owner: user) }
- let_it_be(:personal_access_token) { create(:personal_access_token, user: user) }
- let_it_be(:job, reload: true) { create(:ci_build, user: user, status: :running) }
- 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) }
-
- before do
- project.add_developer(user)
- end
-
- shared_examples 'a package that requires auth' do
- it 'returns the package info with oauth token' do
- get_package_with_token(package)
-
- expect_a_valid_package_response
- end
-
- it 'returns the package info with running job token' do
- get_package_with_job_token(package)
-
- expect_a_valid_package_response
- end
-
- it 'denies request without running job token' do
- job.update!(status: :success)
- get_package_with_job_token(package)
-
- expect(response).to have_gitlab_http_status(:unauthorized)
- end
-
- it 'denies request without oauth token' do
- get_package(package)
-
- expect(response).to have_gitlab_http_status(:forbidden)
- end
-
- it 'returns the package info with deploy token' do
- get_package_with_deploy_token(package)
-
- expect_a_valid_package_response
- end
- end
-
- describe 'GET /api/v4/packages/npm/*package_name' do
- let_it_be(:package_dependency_link1) { create(:packages_dependency_link, package: package, dependency_type: :dependencies) }
- let_it_be(:package_dependency_link2) { create(:packages_dependency_link, package: package, dependency_type: :devDependencies) }
- let_it_be(:package_dependency_link3) { create(:packages_dependency_link, package: package, dependency_type: :bundleDependencies) }
- let_it_be(:package_dependency_link4) { create(:packages_dependency_link, package: package, dependency_type: :peerDependencies) }
-
- shared_examples 'returning the npm package info' do
- it 'returns the package info' do
- get_package(package)
-
- expect_a_valid_package_response
- end
- end
-
- shared_examples 'returning forbidden for unknown package' do
- context 'with an unknown package' do
- it 'returns forbidden' do
- get api("/packages/npm/unknown")
-
- expect(response).to have_gitlab_http_status(:forbidden)
- end
- end
- end
-
- context 'a public project' do
- it_behaves_like 'returning the npm package info'
-
- context 'with application setting enabled' do
- before do
- stub_application_setting(npm_package_requests_forwarding: true)
- end
-
- it_behaves_like 'returning the npm package info'
-
- context 'with unknown package' do
- subject { get api("/packages/npm/unknown") }
-
- it 'returns a redirect' do
- subject
-
- expect(response).to have_gitlab_http_status(:found)
- expect(response.headers['Location']).to eq('https://registry.npmjs.org/unknown')
- end
-
- it_behaves_like 'a gitlab tracking event', described_class.name, 'npm_request_forward'
- end
- end
-
- context 'with application setting disabled' do
- before do
- stub_application_setting(npm_package_requests_forwarding: false)
- end
-
- it_behaves_like 'returning the npm package info'
-
- it_behaves_like 'returning forbidden for unknown package'
- end
-
- context 'project path with a dot' do
- before do
- project.update!(path: 'foo.bar')
- end
-
- it_behaves_like 'returning the npm package info'
- end
- end
-
- context 'internal project' do
- before do
- project.team.truncate
- project.update!(visibility_level: Gitlab::VisibilityLevel::INTERNAL)
- end
-
- it_behaves_like 'a package that requires auth'
- end
-
- context 'private project' do
- before do
- project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
- end
-
- it_behaves_like 'a package that requires auth'
-
- it 'denies request when not enough permissions' do
- project.add_guest(user)
-
- get_package_with_token(package)
-
- expect(response).to have_gitlab_http_status(:forbidden)
- end
- end
-
- def get_package(package, params = {}, headers = {})
- get api("/packages/npm/#{package.name}"), params: params, headers: headers
- end
-
- def get_package_with_token(package, params = {})
- get_package(package, params.merge(access_token: token.token))
- end
-
- def get_package_with_job_token(package, params = {})
- get_package(package, params.merge(job_token: job.token))
- end
-
- def get_package_with_deploy_token(package, params = {})
- get_package(package, {}, build_token_auth_header(deploy_token.token))
- end
- end
-
- describe 'GET /api/v4/projects/:id/packages/npm/*package_name/-/*file_name' do
- let_it_be(:package_file) { package.package_files.first }
-
- shared_examples 'a package file that requires auth' do
- it 'returns the file with an access token' do
- get_file_with_token(package_file)
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response.media_type).to eq('application/octet-stream')
- end
-
- it 'returns the file with a job token' do
- get_file_with_job_token(package_file)
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response.media_type).to eq('application/octet-stream')
- end
-
- it 'denies download with no token' do
- get_file(package_file)
-
- expect(response).to have_gitlab_http_status(:not_found)
- end
- end
-
- context 'a public project' do
- subject { get_file(package_file) }
-
- it 'returns the file with no token needed' do
- subject
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response.media_type).to eq('application/octet-stream')
- end
-
- it_behaves_like 'a package tracking event', described_class.name, 'pull_package'
- end
-
- context 'private project' do
- before do
- project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
- end
-
- it_behaves_like 'a package file that requires auth'
-
- it 'denies download when not enough permissions' do
- project.add_guest(user)
-
- get_file_with_token(package_file)
-
- expect(response).to have_gitlab_http_status(:forbidden)
- end
- end
-
- context 'internal project' do
- before do
- project.update!(visibility_level: Gitlab::VisibilityLevel::INTERNAL)
- end
-
- it_behaves_like 'a package file that requires auth'
- end
-
- def get_file(package_file, params = {})
- get api("/projects/#{project.id}/packages/npm/" \
- "#{package_file.package.name}/-/#{package_file.file_name}"), params: params
- end
-
- def get_file_with_token(package_file, params = {})
- get_file(package_file, params.merge(access_token: token.token))
- end
-
- def get_file_with_job_token(package_file, params = {})
- get_file(package_file, params.merge(job_token: job.token))
- end
- end
-
- describe 'PUT /api/v4/projects/:id/packages/npm/:package_name' do
- RSpec.shared_examples 'handling invalid record with 400 error' do
- it 'handles an ActiveRecord::RecordInvalid exception with 400 error' do
- expect { upload_package_with_token(package_name, params) }
- .not_to change { project.packages.count }
-
- expect(response).to have_gitlab_http_status(:bad_request)
- end
- end
-
- context 'when params are correct' do
- context 'invalid package record' do
- context 'unscoped package' do
- let(:package_name) { 'my_unscoped_package' }
- let(:params) { upload_params(package_name: package_name) }
-
- it_behaves_like 'handling invalid record with 400 error'
-
- context 'with empty versions' do
- let(:params) { upload_params(package_name: package_name).merge!(versions: {}) }
-
- it 'throws a 400 error' do
- expect { upload_package_with_token(package_name, params) }
- .not_to change { project.packages.count }
-
- expect(response).to have_gitlab_http_status(:bad_request)
- end
- end
- end
-
- context 'invalid package name' do
- let(:package_name) { "@#{group.path}/my_inv@@lid_package_name" }
- let(:params) { upload_params(package_name: package_name) }
-
- it_behaves_like 'handling invalid record with 400 error'
- end
-
- context 'invalid package version' do
- using RSpec::Parameterized::TableSyntax
-
- let(:package_name) { "@#{group.path}/my_package_name" }
-
- where(:version) do
- [
- '1',
- '1.2',
- '1./2.3',
- '../../../../../1.2.3',
- '%2e%2e%2f1.2.3'
- ]
- end
-
- with_them do
- let(:params) { upload_params(package_name: package_name, package_version: version) }
-
- it_behaves_like 'handling invalid record with 400 error'
- end
- end
- end
-
- context 'scoped package' do
- let(:package_name) { "@#{group.path}/my_package_name" }
- let(:params) { upload_params(package_name: package_name) }
-
- context 'with access token' do
- subject { upload_package_with_token(package_name, params) }
-
- it_behaves_like 'a package tracking event', described_class.name, 'push_package'
-
- it 'creates npm package with file' do
- expect { subject }
- .to change { project.packages.count }.by(1)
- .and change { Packages::PackageFile.count }.by(1)
- .and change { Packages::Tag.count }.by(1)
-
- expect(response).to have_gitlab_http_status(:ok)
- end
- end
-
- it 'creates npm package with file with job token' do
- expect { upload_package_with_job_token(package_name, params) }
- .to change { project.packages.count }.by(1)
- .and change { Packages::PackageFile.count }.by(1)
-
- expect(response).to have_gitlab_http_status(:ok)
- end
-
- context 'with an authenticated job token' do
- let!(:job) { create(:ci_build, user: user) }
-
- before do
- Grape::Endpoint.before_each do |endpoint|
- expect(endpoint).to receive(:current_authenticated_job) { job }
- end
- end
-
- after do
- Grape::Endpoint.before_each nil
- end
-
- it 'creates the package metadata' do
- upload_package_with_token(package_name, params)
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(project.reload.packages.find(json_response['id']).build_info.pipeline).to eq job.pipeline
- end
- end
- end
-
- context 'package creation fails' do
- let(:package_name) { "@#{group.path}/my_package_name" }
- let(:params) { upload_params(package_name: package_name) }
-
- it 'returns an error if the package already exists' do
- create(:npm_package, project: project, version: '1.0.1', name: "@#{group.path}/my_package_name")
- expect { upload_package_with_token(package_name, params) }
- .not_to change { project.packages.count }
-
- expect(response).to have_gitlab_http_status(:forbidden)
- end
- end
-
- context 'with dependencies' do
- let(:package_name) { "@#{group.path}/my_package_name" }
- let(:params) { upload_params(package_name: package_name, file: 'npm/payload_with_duplicated_packages.json') }
-
- it 'creates npm package with file and dependencies' do
- expect { upload_package_with_token(package_name, params) }
- .to change { project.packages.count }.by(1)
- .and change { Packages::PackageFile.count }.by(1)
- .and change { Packages::Dependency.count}.by(4)
- .and change { Packages::DependencyLink.count}.by(6)
-
- expect(response).to have_gitlab_http_status(:ok)
- end
-
- context 'with existing dependencies' do
- before do
- name = "@#{group.path}/existing_package"
- upload_package_with_token(name, upload_params(package_name: name, file: 'npm/payload_with_duplicated_packages.json'))
- end
-
- it 'reuses them' do
- expect { upload_package_with_token(package_name, params) }
- .to change { project.packages.count }.by(1)
- .and change { Packages::PackageFile.count }.by(1)
- .and not_change { Packages::Dependency.count}
- .and change { Packages::DependencyLink.count}.by(6)
- end
- end
- end
- end
-
- def upload_package(package_name, params = {})
- put api("/projects/#{project.id}/packages/npm/#{package_name.sub('/', '%2f')}"), params: params
- end
-
- def upload_package_with_token(package_name, params = {})
- upload_package(package_name, params.merge(access_token: token.token))
- end
-
- def upload_package_with_job_token(package_name, params = {})
- upload_package(package_name, params.merge(job_token: job.token))
- end
-
- def upload_params(package_name:, package_version: '1.0.1', file: 'npm/payload.json')
- Gitlab::Json.parse(fixture_file("packages/#{file}")
- .gsub('@root/npm-test', package_name)
- .gsub('1.0.1', package_version))
- end
- end
-
- describe 'GET /api/v4/packages/npm/-/package/*package_name/dist-tags' do
- let_it_be(:package_tag1) { create(:packages_tag, package: package) }
- let_it_be(:package_tag2) { create(:packages_tag, package: package) }
-
- let(:package_name) { package.name }
- let(:url) { "/packages/npm/-/package/#{package_name}/dist-tags" }
-
- subject { get api(url) }
-
- context 'with public project' do
- context 'with authenticated user' do
- subject { get api(url, personal_access_token: personal_access_token) }
-
- it_behaves_like 'returns package tags', :maintainer
- it_behaves_like 'returns package tags', :developer
- it_behaves_like 'returns package tags', :reporter
- it_behaves_like 'returns package tags', :guest
- end
-
- context 'with unauthenticated user' do
- it_behaves_like 'returns package tags', :no_type
- end
- end
-
- context 'with private project' do
- before do
- project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
- end
-
- context 'with authenticated user' do
- subject { get api(url, personal_access_token: personal_access_token) }
-
- it_behaves_like 'returns package tags', :maintainer
- it_behaves_like 'returns package tags', :developer
- it_behaves_like 'returns package tags', :reporter
- it_behaves_like 'rejects package tags access', :guest, :forbidden
- end
-
- context 'with unauthenticated user' do
- it_behaves_like 'rejects package tags access', :no_type, :forbidden
- end
- end
- end
-
- describe 'PUT /api/v4/packages/npm/-/package/*package_name/dist-tags/:tag' do
- let_it_be(:tag_name) { 'test' }
-
- let(:package_name) { package.name }
- let(:version) { package.version }
- let(:url) { "/packages/npm/-/package/#{package_name}/dist-tags/#{tag_name}" }
-
- subject { put api(url), env: { 'api.request.body': version } }
-
- context 'with public project' do
- context 'with authenticated user' do
- subject { put api(url, personal_access_token: personal_access_token), env: { 'api.request.body': version } }
-
- it_behaves_like 'create package tag', :maintainer
- it_behaves_like 'create package tag', :developer
- it_behaves_like 'rejects package tags access', :reporter, :forbidden
- it_behaves_like 'rejects package tags access', :guest, :forbidden
- end
-
- context 'with unauthenticated user' do
- it_behaves_like 'rejects package tags access', :no_type, :unauthorized
- end
- end
-
- context 'with private project' do
- before do
- project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
- end
-
- context 'with authenticated user' do
- subject { put api(url, personal_access_token: personal_access_token), env: { 'api.request.body': version } }
-
- it_behaves_like 'create package tag', :maintainer
- it_behaves_like 'create package tag', :developer
- it_behaves_like 'rejects package tags access', :reporter, :forbidden
- it_behaves_like 'rejects package tags access', :guest, :forbidden
- end
-
- context 'with unauthenticated user' do
- it_behaves_like 'rejects package tags access', :no_type, :unauthorized
- end
- end
- end
-
- describe 'DELETE /api/v4/packages/npm/-/package/*package_name/dist-tags/:tag' do
- let_it_be(:package_tag) { create(:packages_tag, package: package) }
-
- let(:package_name) { package.name }
- let(:tag_name) { package_tag.name }
- let(:url) { "/packages/npm/-/package/#{package_name}/dist-tags/#{tag_name}" }
-
- subject { delete api(url) }
-
- context 'with public project' do
- context 'with authenticated user' do
- subject { delete api(url, personal_access_token: personal_access_token) }
-
- it_behaves_like 'delete package tag', :maintainer
- it_behaves_like 'rejects package tags access', :developer, :forbidden
- it_behaves_like 'rejects package tags access', :reporter, :forbidden
- it_behaves_like 'rejects package tags access', :guest, :forbidden
- end
-
- context 'with unauthenticated user' do
- it_behaves_like 'rejects package tags access', :no_type, :unauthorized
- end
- end
-
- context 'with private project' do
- before do
- project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
- end
-
- context 'with authenticated user' do
- subject { delete api(url, personal_access_token: personal_access_token) }
-
- it_behaves_like 'delete package tag', :maintainer
- it_behaves_like 'rejects package tags access', :developer, :forbidden
- it_behaves_like 'rejects package tags access', :reporter, :forbidden
- it_behaves_like 'rejects package tags access', :guest, :forbidden
- end
-
- context 'with unauthenticated user' do
- it_behaves_like 'rejects package tags access', :no_type, :unauthorized
- end
- end
- end
-
- def expect_a_valid_package_response
- expect(response).to have_gitlab_http_status(:ok)
- expect(response.media_type).to eq('application/json')
- expect(response).to match_response_schema('public_api/v4/packages/npm_package')
- expect(json_response['name']).to eq(package.name)
- expect(json_response['versions'][package.version]).to match_schema('public_api/v4/packages/npm_package_version')
- ::Packages::Npm::PackagePresenter::NPM_VALID_DEPENDENCY_TYPES.each do |dependency_type|
- expect(json_response.dig('versions', package.version, dependency_type.to_s)).to be_any
- end
- expect(json_response['dist-tags']).to match_schema('public_api/v4/packages/npm_package_tags')
- end
-end
diff --git a/spec/requests/api/npm_project_packages_spec.rb b/spec/requests/api/npm_project_packages_spec.rb
new file mode 100644
index 00000000000..1421f20ac28
--- /dev/null
+++ b/spec/requests/api/npm_project_packages_spec.rb
@@ -0,0 +1,281 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe API::NpmProjectPackages do
+ include_context 'npm api setup'
+
+ describe 'GET /api/v4/projects/:id/packages/npm/*package_name' do
+ it_behaves_like 'handling get metadata requests' do
+ let(:url) { api("/projects/#{project.id}/packages/npm/#{package_name}") }
+ end
+ end
+
+ describe 'GET /api/v4/projects/:id/packages/npm/-/package/*package_name/dist-tags' do
+ it_behaves_like 'handling get dist tags requests' do
+ let(:url) { api("/projects/#{project.id}/packages/npm/-/package/#{package_name}/dist-tags") }
+ end
+ end
+
+ describe 'PUT /api/v4/projects/:id/packages/npm/-/package/*package_name/dist-tags/:tag' do
+ it_behaves_like 'handling create dist tag requests' do
+ let(:url) { api("/projects/#{project.id}/packages/npm/-/package/#{package_name}/dist-tags/#{tag_name}") }
+ end
+ end
+
+ describe 'DELETE /api/v4/projects/:id/packages/npm/-/package/*package_name/dist-tags/:tag' do
+ it_behaves_like 'handling delete dist tag requests' do
+ let(:url) { api("/projects/#{project.id}/packages/npm/-/package/#{package_name}/dist-tags/#{tag_name}") }
+ end
+ end
+
+ describe 'GET /api/v4/projects/:id/packages/npm/*package_name/-/*file_name' do
+ let_it_be(:package_file) { package.package_files.first }
+
+ let(:params) { {} }
+ let(:url) { api("/projects/#{project.id}/packages/npm/#{package_file.package.name}/-/#{package_file.file_name}") }
+
+ subject { get(url, params: params) }
+
+ shared_examples 'a package file that requires auth' do
+ it 'denies download with no token' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+
+ context 'with access token' do
+ let(:params) { { access_token: token.token } }
+
+ it 'returns the file' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.media_type).to eq('application/octet-stream')
+ end
+ end
+
+ context 'with job token' do
+ let(:params) { { job_token: job.token } }
+
+ it 'returns the file' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.media_type).to eq('application/octet-stream')
+ end
+ end
+ end
+
+ context 'a public project' do
+ it 'returns the file with no token needed' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.media_type).to eq('application/octet-stream')
+ end
+
+ it_behaves_like 'a package tracking event', 'API::NpmPackages', 'pull_package'
+ end
+
+ context 'private project' do
+ before do
+ project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
+ end
+
+ it_behaves_like 'a package file that requires auth'
+
+ context 'with guest' do
+ let(:params) { { access_token: token.token } }
+
+ it 'denies download when not enough permissions' do
+ project.add_guest(user)
+
+ subject
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+ end
+
+ context 'internal project' do
+ before do
+ project.update!(visibility_level: Gitlab::VisibilityLevel::INTERNAL)
+ end
+
+ it_behaves_like 'a package file that requires auth'
+ end
+ end
+
+ describe 'PUT /api/v4/projects/:id/packages/npm/:package_name' do
+ RSpec.shared_examples 'handling invalid record with 400 error' do
+ it 'handles an ActiveRecord::RecordInvalid exception with 400 error' do
+ expect { upload_package_with_token(package_name, params) }
+ .not_to change { project.packages.count }
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+ end
+
+ context 'when params are correct' do
+ context 'invalid package record' do
+ context 'unscoped package' do
+ let(:package_name) { 'my_unscoped_package' }
+ let(:params) { upload_params(package_name: package_name) }
+
+ it_behaves_like 'handling invalid record with 400 error'
+
+ context 'with empty versions' do
+ let(:params) { upload_params(package_name: package_name).merge!(versions: {}) }
+
+ it 'throws a 400 error' do
+ expect { upload_package_with_token(package_name, params) }
+ .not_to change { project.packages.count }
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+ end
+ end
+
+ context 'invalid package name' do
+ let(:package_name) { "@#{group.path}/my_inv@@lid_package_name" }
+ let(:params) { upload_params(package_name: package_name) }
+
+ it_behaves_like 'handling invalid record with 400 error'
+ end
+
+ context 'invalid package version' do
+ using RSpec::Parameterized::TableSyntax
+
+ let(:package_name) { "@#{group.path}/my_package_name" }
+
+ where(:version) do
+ [
+ '1',
+ '1.2',
+ '1./2.3',
+ '../../../../../1.2.3',
+ '%2e%2e%2f1.2.3'
+ ]
+ end
+
+ with_them do
+ let(:params) { upload_params(package_name: package_name, package_version: version) }
+
+ it_behaves_like 'handling invalid record with 400 error'
+ end
+ end
+ end
+
+ context 'scoped package' do
+ let(:package_name) { "@#{group.path}/my_package_name" }
+ let(:params) { upload_params(package_name: package_name) }
+
+ context 'with access token' do
+ subject { upload_package_with_token(package_name, params) }
+
+ it_behaves_like 'a package tracking event', 'API::NpmPackages', 'push_package'
+
+ it 'creates npm package with file' do
+ expect { subject }
+ .to change { project.packages.count }.by(1)
+ .and change { Packages::PackageFile.count }.by(1)
+ .and change { Packages::Tag.count }.by(1)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+
+ it 'creates npm package with file with job token' do
+ expect { upload_package_with_job_token(package_name, params) }
+ .to change { project.packages.count }.by(1)
+ .and change { Packages::PackageFile.count }.by(1)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+
+ context 'with an authenticated job token' do
+ let!(:job) { create(:ci_build, user: user) }
+
+ before do
+ Grape::Endpoint.before_each do |endpoint|
+ expect(endpoint).to receive(:current_authenticated_job) { job }
+ end
+ end
+
+ after do
+ Grape::Endpoint.before_each nil
+ end
+
+ it 'creates the package metadata' do
+ upload_package_with_token(package_name, params)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(project.reload.packages.find(json_response['id']).original_build_info.pipeline).to eq job.pipeline
+ end
+ end
+ end
+
+ context 'package creation fails' do
+ let(:package_name) { "@#{group.path}/my_package_name" }
+ let(:params) { upload_params(package_name: package_name) }
+
+ it 'returns an error if the package already exists' do
+ create(:npm_package, project: project, version: '1.0.1', name: "@#{group.path}/my_package_name")
+ expect { upload_package_with_token(package_name, params) }
+ .not_to change { project.packages.count }
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+
+ context 'with dependencies' do
+ let(:package_name) { "@#{group.path}/my_package_name" }
+ let(:params) { upload_params(package_name: package_name, file: 'npm/payload_with_duplicated_packages.json') }
+
+ it 'creates npm package with file and dependencies' do
+ expect { upload_package_with_token(package_name, params) }
+ .to change { project.packages.count }.by(1)
+ .and change { Packages::PackageFile.count }.by(1)
+ .and change { Packages::Dependency.count}.by(4)
+ .and change { Packages::DependencyLink.count}.by(6)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+
+ context 'with existing dependencies' do
+ before do
+ name = "@#{group.path}/existing_package"
+ upload_package_with_token(name, upload_params(package_name: name, file: 'npm/payload_with_duplicated_packages.json'))
+ end
+
+ it 'reuses them' do
+ expect { upload_package_with_token(package_name, params) }
+ .to change { project.packages.count }.by(1)
+ .and change { Packages::PackageFile.count }.by(1)
+ .and not_change { Packages::Dependency.count}
+ .and change { Packages::DependencyLink.count}.by(6)
+ end
+ end
+ end
+ end
+
+ def upload_package(package_name, params = {})
+ put api("/projects/#{project.id}/packages/npm/#{package_name.sub('/', '%2f')}"), params: params
+ end
+
+ def upload_package_with_token(package_name, params = {})
+ upload_package(package_name, params.merge(access_token: token.token))
+ end
+
+ def upload_package_with_job_token(package_name, params = {})
+ upload_package(package_name, params.merge(job_token: job.token))
+ end
+
+ def upload_params(package_name:, package_version: '1.0.1', file: 'npm/payload.json')
+ Gitlab::Json.parse(fixture_file("packages/#{file}")
+ .gsub('@root/npm-test', package_name)
+ .gsub('1.0.1', package_version))
+ end
+ end
+end
diff --git a/spec/requests/api/personal_access_tokens_spec.rb b/spec/requests/api/personal_access_tokens_spec.rb
new file mode 100644
index 00000000000..ccc5f322ff9
--- /dev/null
+++ b/spec/requests/api/personal_access_tokens_spec.rb
@@ -0,0 +1,112 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe API::PersonalAccessTokens do
+ let_it_be(:path) { '/personal_access_tokens' }
+ let_it_be(:token1) { create(:personal_access_token) }
+ let_it_be(:token2) { create(:personal_access_token) }
+ let_it_be(:current_user) { create(:user) }
+
+ describe 'GET /personal_access_tokens' do
+ context 'logged in as an Administrator' do
+ let_it_be(:current_user) { create(:admin) }
+
+ it 'returns all PATs by default' do
+ get api(path, current_user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response.count).to eq(PersonalAccessToken.all.count)
+ end
+
+ context 'filtered with user_id parameter' do
+ it 'returns only PATs belonging to that user' do
+ get api(path, current_user), params: { user_id: token1.user.id }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response.count).to eq(1)
+ expect(json_response.first['user_id']).to eq(token1.user.id)
+ end
+ end
+
+ context 'logged in as a non-Administrator' do
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:token) { create(:personal_access_token, user: current_user)}
+ let_it_be(:other_token) { create(:personal_access_token, user: user) }
+
+ it 'returns all PATs belonging to the signed-in user' do
+ get api(path, current_user, personal_access_token: token)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response.count).to eq(1)
+ expect(json_response.map { |r| r['user_id'] }.uniq).to contain_exactly(current_user.id)
+ end
+
+ context 'filtered with user_id parameter' do
+ it 'returns PATs belonging to the specific user' do
+ get api(path, current_user, personal_access_token: token), params: { user_id: current_user.id }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response.count).to eq(1)
+ expect(json_response.map { |r| r['user_id'] }.uniq).to contain_exactly(current_user.id)
+ end
+
+ it 'is unauthorized if filtered by a user other than current_user' do
+ get api(path, current_user, personal_access_token: token), params: { user_id: user.id }
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+ end
+ end
+
+ context 'not authenticated' do
+ it 'is forbidden' do
+ get api(path)
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+ end
+ end
+ end
+
+ describe 'DELETE /personal_access_tokens/:id' do
+ let(:path) { "/personal_access_tokens/#{token1.id}" }
+
+ context 'when current_user is an administrator', :enable_admin_mode do
+ let_it_be(:admin_user) { create(:admin) }
+ let_it_be(:admin_token) { create(:personal_access_token, user: admin_user) }
+ let_it_be(:admin_path) { "/personal_access_tokens/#{admin_token.id}" }
+
+ it 'revokes a different users token' do
+ delete api(path, admin_user)
+
+ expect(response).to have_gitlab_http_status(:no_content)
+ expect(token1.reload.revoked?).to be true
+ end
+
+ it 'revokes their own token' do
+ delete api(admin_path, admin_user)
+
+ expect(response).to have_gitlab_http_status(:no_content)
+ end
+ end
+
+ context 'when current_user is not an administrator' do
+ let_it_be(:user_token) { create(:personal_access_token, user: current_user) }
+ let_it_be(:user_token_path) { "/personal_access_tokens/#{user_token.id}" }
+
+ it 'fails revokes a different users token' do
+ delete api(path, current_user)
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+
+ it 'revokes their own token' do
+ delete api(user_token_path, current_user)
+
+ expect(response).to have_gitlab_http_status(:no_content)
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/project_container_repositories_spec.rb b/spec/requests/api/project_container_repositories_spec.rb
index 34476b10576..15871426ec5 100644
--- a/spec/requests/api/project_container_repositories_spec.rb
+++ b/spec/requests/api/project_container_repositories_spec.rb
@@ -299,7 +299,7 @@ RSpec.describe API::ProjectContainerRepositories do
it_behaves_like 'rejected container repository access', :reporter, :forbidden
it_behaves_like 'rejected container repository access', :anonymous, :not_found
- context 'for developer' do
+ context 'for developer', :snowplow do
let(:api_user) { developer }
context 'when there are multiple tags' do
@@ -310,11 +310,11 @@ RSpec.describe API::ProjectContainerRepositories do
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 }
- expect(Gitlab::Tracking).to receive(:event).with(described_class.name, 'delete_tag', {})
subject
expect(response).to have_gitlab_http_status(:ok)
+ expect_snowplow_event(category: described_class.name, action: 'delete_tag')
end
end
@@ -326,11 +326,11 @@ RSpec.describe API::ProjectContainerRepositories do
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 }
- expect(Gitlab::Tracking).to receive(:event).with(described_class.name, 'delete_tag', {})
subject
expect(response).to have_gitlab_http_status(:ok)
+ expect_snowplow_event(category: described_class.name, action: 'delete_tag')
end
end
end
diff --git a/spec/requests/api/project_export_spec.rb b/spec/requests/api/project_export_spec.rb
index 09d295afbea..ac24aeee52c 100644
--- a/spec/requests/api/project_export_spec.rb
+++ b/spec/requests/api/project_export_spec.rb
@@ -26,7 +26,7 @@ RSpec.describe API::ProjectExport, :clean_gitlab_redis_cache do
let(:export_path) { "#{Dir.tmpdir}/project_export_spec" }
before do
- allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path)
+ allow(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path)
allow_next_instance_of(ProjectExportWorker) do |job|
allow(job).to receive(:jid).and_return(SecureRandom.hex(8))
end
diff --git a/spec/requests/api/project_hooks_spec.rb b/spec/requests/api/project_hooks_spec.rb
index 3b2a7895630..b5aedde2b2e 100644
--- a/spec/requests/api/project_hooks_spec.rb
+++ b/spec/requests/api/project_hooks_spec.rb
@@ -41,6 +41,7 @@ RSpec.describe API::ProjectHooks, 'ProjectHooks' do
expect(json_response.first['pipeline_events']).to eq(true)
expect(json_response.first['wiki_page_events']).to eq(true)
expect(json_response.first['deployment_events']).to eq(true)
+ expect(json_response.first['releases_events']).to eq(true)
expect(json_response.first['enable_ssl_verification']).to eq(true)
expect(json_response.first['push_events_branch_filter']).to eq('master')
end
@@ -72,6 +73,7 @@ RSpec.describe API::ProjectHooks, 'ProjectHooks' do
expect(json_response['job_events']).to eq(hook.job_events)
expect(json_response['pipeline_events']).to eq(hook.pipeline_events)
expect(json_response['wiki_page_events']).to eq(hook.wiki_page_events)
+ expect(json_response['releases_events']).to eq(hook.releases_events)
expect(json_response['deployment_events']).to eq(true)
expect(json_response['enable_ssl_verification']).to eq(hook.enable_ssl_verification)
end
@@ -97,7 +99,7 @@ RSpec.describe API::ProjectHooks, 'ProjectHooks' do
post(api("/projects/#{project.id}/hooks", user),
params: { url: "http://example.com", issues_events: true,
confidential_issues_events: true, wiki_page_events: true,
- job_events: true, deployment_events: true,
+ job_events: true, deployment_events: true, releases_events: true,
push_events_branch_filter: 'some-feature-branch' })
end.to change {project.hooks.count}.by(1)
@@ -114,6 +116,7 @@ RSpec.describe API::ProjectHooks, 'ProjectHooks' do
expect(json_response['pipeline_events']).to eq(false)
expect(json_response['wiki_page_events']).to eq(true)
expect(json_response['deployment_events']).to eq(true)
+ expect(json_response['releases_events']).to eq(true)
expect(json_response['enable_ssl_verification']).to eq(true)
expect(json_response['push_events_branch_filter']).to eq('some-feature-branch')
expect(json_response).not_to include('token')
@@ -169,6 +172,7 @@ RSpec.describe API::ProjectHooks, 'ProjectHooks' do
expect(json_response['job_events']).to eq(hook.job_events)
expect(json_response['pipeline_events']).to eq(hook.pipeline_events)
expect(json_response['wiki_page_events']).to eq(hook.wiki_page_events)
+ expect(json_response['releases_events']).to eq(hook.releases_events)
expect(json_response['enable_ssl_verification']).to eq(hook.enable_ssl_verification)
end
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index 2abcb39a1c8..4a792fc218d 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -890,7 +890,7 @@ RSpec.describe API::Projects do
expect(response).to have_gitlab_http_status(:created)
project.each_pair do |k, v|
- next if %i[has_external_issue_tracker issues_enabled merge_requests_enabled wiki_enabled storage_version].include?(k)
+ next if %i[has_external_issue_tracker has_external_wiki issues_enabled merge_requests_enabled wiki_enabled storage_version].include?(k)
expect(json_response[k.to_s]).to eq(v)
end
@@ -1309,7 +1309,7 @@ RSpec.describe API::Projects do
expect(response).to have_gitlab_http_status(:created)
project.each_pair do |k, v|
- next if %i[has_external_issue_tracker path storage_version].include?(k)
+ next if %i[has_external_issue_tracker has_external_wiki path storage_version].include?(k)
expect(json_response[k.to_s]).to eq(v)
end
@@ -2659,6 +2659,7 @@ RSpec.describe API::Projects do
project_param = {
container_expiration_policy_attributes: {
cadence: '1month',
+ enabled: true,
keep_n: 1,
name_regex_keep: '['
}
diff --git a/spec/requests/api/release/links_spec.rb b/spec/requests/api/release/links_spec.rb
index 82d0d64eba4..c03dd0331cf 100644
--- a/spec/requests/api/release/links_spec.rb
+++ b/spec/requests/api/release/links_spec.rb
@@ -146,7 +146,7 @@ RSpec.describe API::Release::Links do
specify do
get api("/projects/#{project.id}/releases/v0.1/assets/links/#{link.id}", maintainer)
- expect(json_response['direct_asset_url']).to eq("http://localhost/#{project.namespace.path}/#{project.name}/-/releases/#{release.tag}/bin/bigfile.exe")
+ expect(json_response['direct_asset_url']).to eq("http://localhost/#{project.namespace.path}/#{project.name}/-/releases/#{release.tag}/downloads/bin/bigfile.exe")
end
end
diff --git a/spec/requests/api/releases_spec.rb b/spec/requests/api/releases_spec.rb
index e78d05835f2..58b321a255e 100644
--- a/spec/requests/api/releases_spec.rb
+++ b/spec/requests/api/releases_spec.rb
@@ -110,22 +110,6 @@ RSpec.describe API::Releases do
expect(json_response.second['commit_path']).to eq("/#{release_1.project.full_path}/-/commit/#{release_1.commit.id}")
expect(json_response.second['tag_path']).to eq("/#{release_1.project.full_path}/-/tags/#{release_1.tag}")
end
-
- it 'returns the merge requests and issues links, with correct query' do
- get api("/projects/#{project.id}/releases", maintainer)
-
- links = json_response.first['_links']
- release = json_response.first['tag_name']
- expected_query = "release_tag=#{release}&scope=all&state=opened"
- path_base = "/#{project.namespace.path}/#{project.path}"
- mr_uri = URI.parse(links['merge_requests_url'])
- issue_uri = URI.parse(links['issues_url'])
-
- expect(mr_uri.path).to eq("#{path_base}/-/merge_requests")
- expect(issue_uri.path).to eq("#{path_base}/-/issues")
- expect(mr_uri.query).to eq(expected_query)
- expect(issue_uri.query).to eq(expected_query)
- end
end
it 'returns an upcoming_release status for a future release' do
@@ -1014,6 +998,17 @@ RSpec.describe API::Releases do
end
end
+ context 'without milestones parameter' do
+ let(:params) { { name: 'some new name' } }
+
+ it 'does not change the milestone' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(returned_milestones).to match_array(['v1.0'])
+ end
+ end
+
context 'multiple milestones' do
context 'with one new' do
let!(:milestone2) { create(:milestone, project: project, title: 'milestone2') }
diff --git a/spec/requests/api/search_spec.rb b/spec/requests/api/search_spec.rb
index 05cfad9cc62..8012892a571 100644
--- a/spec/requests/api/search_spec.rb
+++ b/spec/requests/api/search_spec.rb
@@ -23,6 +23,48 @@ RSpec.describe API::Search do
end
end
+ shared_examples 'orderable by created_at' do |scope:|
+ it 'allows ordering results by created_at asc' do
+ get api(endpoint, user), params: { scope: scope, search: 'sortable', order_by: 'created_at', sort: 'asc' }
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(json_response.count).to be > 1
+
+ created_ats = json_response.map { |r| Time.parse(r['created_at']) }
+ expect(created_ats.uniq.count).to be > 1
+
+ expect(created_ats).to eq(created_ats.sort)
+ end
+
+ it 'allows ordering results by created_at desc' do
+ get api(endpoint, user), params: { scope: scope, search: 'sortable', order_by: 'created_at', sort: 'desc' }
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(json_response.count).to be > 1
+
+ created_ats = json_response.map { |r| Time.parse(r['created_at']) }
+ expect(created_ats.uniq.count).to be > 1
+
+ expect(created_ats).to eq(created_ats.sort.reverse)
+ end
+ end
+
+ shared_examples 'issues orderable by created_at' do
+ before do
+ create_list(:issue, 3, title: 'sortable item', project: project)
+ end
+
+ it_behaves_like 'orderable by created_at', scope: :issues
+ end
+
+ shared_examples 'merge_requests orderable by created_at' do
+ before do
+ create_list(:merge_request, 3, :unique_branches, title: 'sortable item', target_project: repo_project, source_project: repo_project)
+ end
+
+ it_behaves_like 'orderable by created_at', scope: :merge_requests
+ end
+
shared_examples 'pagination' do |scope:, search: ''|
it 'returns a different result for each page' do
get api(endpoint, user), params: { scope: scope, search: search, page: 1, per_page: 1 }
@@ -121,6 +163,8 @@ RSpec.describe API::Search do
it_behaves_like 'ping counters', scope: :issues
+ it_behaves_like 'issues orderable by created_at'
+
describe 'pagination' do
before do
create(:issue, project: project, title: 'another issue')
@@ -151,7 +195,6 @@ RSpec.describe API::Search do
context 'filter by confidentiality' do
before do
- stub_feature_flags(search_filter_by_confidential: true)
create(:issue, project: project, author: user, title: 'awesome non-confidential issue')
create(:issue, :confidential, project: project, author: user, title: 'awesome confidential issue')
end
@@ -182,6 +225,8 @@ RSpec.describe API::Search do
it_behaves_like 'ping counters', scope: :merge_requests
+ it_behaves_like 'merge_requests orderable by created_at'
+
describe 'pagination' do
before do
create(:merge_request, source_project: repo_project, title: 'another mr', target_branch: 'another_branch')
@@ -355,6 +400,8 @@ RSpec.describe API::Search do
it_behaves_like 'ping counters', scope: :issues
+ it_behaves_like 'issues orderable by created_at'
+
describe 'pagination' do
before do
create(:issue, project: project, title: 'another issue')
@@ -375,6 +422,8 @@ RSpec.describe API::Search do
it_behaves_like 'ping counters', scope: :merge_requests
+ it_behaves_like 'merge_requests orderable by created_at'
+
describe 'pagination' do
before do
create(:merge_request, source_project: repo_project, title: 'another mr', target_branch: 'another_branch')
@@ -507,6 +556,8 @@ RSpec.describe API::Search do
it_behaves_like 'ping counters', scope: :issues
+ it_behaves_like 'issues orderable by created_at'
+
describe 'pagination' do
before do
create(:issue, project: project, title: 'another issue')
@@ -516,6 +567,14 @@ RSpec.describe API::Search do
end
end
+ context 'when requesting basic search' do
+ it 'passes the parameter to search service' do
+ expect(SearchService).to receive(:new).with(user, hash_including(basic_search: 'true'))
+
+ get api(endpoint, user), params: { scope: 'issues', search: 'awesome', basic_search: 'true' }
+ end
+ end
+
context 'for merge_requests scope' do
let(:endpoint) { "/projects/#{repo_project.id}/search" }
@@ -529,6 +588,8 @@ RSpec.describe API::Search do
it_behaves_like 'ping counters', scope: :merge_requests
+ it_behaves_like 'merge_requests orderable by created_at'
+
describe 'pagination' do
before do
create(:merge_request, source_project: repo_project, title: 'another mr', target_branch: 'another_branch')
diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb
index 8b5f74df8f8..03320549e44 100644
--- a/spec/requests/api/settings_spec.rb
+++ b/spec/requests/api/settings_spec.rb
@@ -22,6 +22,8 @@ RSpec.describe API::Settings, 'Settings' do
expect(json_response['default_ci_config_path']).to be_nil
expect(json_response['sourcegraph_enabled']).to be_falsey
expect(json_response['sourcegraph_url']).to be_nil
+ expect(json_response['secret_detection_token_revocation_url']).to be_nil
+ expect(json_response['secret_detection_revocation_token_types_url']).to be_nil
expect(json_response['sourcegraph_public_only']).to be_truthy
expect(json_response['default_project_visibility']).to be_a String
expect(json_response['default_snippet_visibility']).to be_a String
@@ -40,6 +42,7 @@ RSpec.describe API::Settings, 'Settings' do
expect(json_response['spam_check_endpoint_enabled']).to be_falsey
expect(json_response['spam_check_endpoint_url']).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)
end
end
@@ -105,7 +108,7 @@ RSpec.describe API::Settings, 'Settings' do
enforce_terms: true,
terms: 'Hello world!',
performance_bar_allowed_group_path: group.full_path,
- diff_max_patch_bytes: 150_000,
+ diff_max_patch_bytes: 300_000,
default_branch_protection: ::Gitlab::Access::PROTECTION_DEV_CAN_MERGE,
local_markdown_version: 3,
allow_local_requests_from_web_hooks_and_services: true,
@@ -148,7 +151,7 @@ RSpec.describe API::Settings, 'Settings' do
expect(json_response['enforce_terms']).to be(true)
expect(json_response['terms']).to eq('Hello world!')
expect(json_response['performance_bar_allowed_group_id']).to eq(group.id)
- expect(json_response['diff_max_patch_bytes']).to eq(150_000)
+ expect(json_response['diff_max_patch_bytes']).to eq(300_000)
expect(json_response['default_branch_protection']).to eq(Gitlab::Access::PROTECTION_DEV_CAN_MERGE)
expect(json_response['local_markdown_version']).to eq(3)
expect(json_response['allow_local_requests_from_web_hooks_and_services']).to eq(true)
@@ -377,41 +380,41 @@ RSpec.describe API::Settings, 'Settings' do
end
end
- context 'domain_blacklist settings' do
- it 'rejects domain_blacklist_enabled when domain_blacklist is empty' do
+ context 'domain_denylist settings' do
+ it 'rejects domain_denylist_enabled when domain_denylist is empty' do
put api('/application/settings', admin),
params: {
- domain_blacklist_enabled: true,
- domain_blacklist: []
+ domain_denylist_enabled: true,
+ domain_denylist: []
}
expect(response).to have_gitlab_http_status(:bad_request)
message = json_response["message"]
- expect(message["domain_blacklist"]).to eq(["Domain blacklist cannot be empty if Blacklist is enabled."])
+ expect(message["domain_denylist"]).to eq(["Domain denylist cannot be empty if denylist is enabled."])
end
- it 'allows array for domain_blacklist' do
+ it 'allows array for domain_denylist' do
put api('/application/settings', admin),
params: {
- domain_blacklist_enabled: true,
- domain_blacklist: ['domain1.com', 'domain2.com']
+ domain_denylist_enabled: true,
+ domain_denylist: ['domain1.com', 'domain2.com']
}
expect(response).to have_gitlab_http_status(:ok)
- expect(json_response['domain_blacklist_enabled']).to be(true)
- expect(json_response['domain_blacklist']).to eq(['domain1.com', 'domain2.com'])
+ expect(json_response['domain_denylist_enabled']).to be(true)
+ expect(json_response['domain_denylist']).to eq(['domain1.com', 'domain2.com'])
end
- it 'allows a string for domain_blacklist' do
+ it 'allows a string for domain_denylist' do
put api('/application/settings', admin),
params: {
- domain_blacklist_enabled: true,
- domain_blacklist: 'domain3.com, *.domain4.com'
+ domain_denylist_enabled: true,
+ domain_denylist: 'domain3.com, *.domain4.com'
}
expect(response).to have_gitlab_http_status(:ok)
- expect(json_response['domain_blacklist_enabled']).to be(true)
- expect(json_response['domain_blacklist']).to eq(['domain3.com', '*.domain4.com'])
+ expect(json_response['domain_denylist_enabled']).to be(true)
+ expect(json_response['domain_denylist']).to eq(['domain3.com', '*.domain4.com'])
end
end
@@ -423,6 +426,14 @@ RSpec.describe API::Settings, 'Settings' do
expect(json_response['abuse_notification_email']).to eq('test@example.com')
end
+ it 'supports setting require_admin_approval_after_user_signup' do
+ put api('/application/settings', admin),
+ params: { require_admin_approval_after_user_signup: true }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['require_admin_approval_after_user_signup']).to eq(true)
+ end
+
context "missing sourcegraph_url value when sourcegraph_enabled is true" do
it "returns a blank parameter error message" do
put api("/application/settings", admin), params: { sourcegraph_enabled: true }
diff --git a/spec/requests/api/terraform/state_spec.rb b/spec/requests/api/terraform/state_spec.rb
index b91f6e1aa88..0fa088a641e 100644
--- a/spec/requests/api/terraform/state_spec.rb
+++ b/spec/requests/api/terraform/state_spec.rb
@@ -113,7 +113,7 @@ RSpec.describe API::Terraform::State do
end
describe 'POST /projects/:id/terraform/state/:name' do
- let(:params) { { 'instance': 'example-instance', 'serial': '1' } }
+ let(:params) { { 'instance': 'example-instance', 'serial': state.latest_version.version + 1 } }
subject(:request) { post api(state_path), headers: auth_header, as: :json, params: params }
@@ -202,6 +202,18 @@ RSpec.describe API::Terraform::State do
end
end
end
+
+ context 'when using job token authentication' do
+ let(:job) { create(:ci_build, status: :running, project: project, user: maintainer) }
+ let(:auth_header) { job_basic_auth_header(job) }
+
+ it 'associates the job with the newly created state version' do
+ expect { request }.to change { state.versions.count }.by(1)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(state.reload_latest_version.build).to eq(job)
+ end
+ end
end
describe 'DELETE /projects/:id/terraform/state/:name' do
diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb
index 7330c89fe77..98840d6238a 100644
--- a/spec/requests/api/users_spec.rb
+++ b/spec/requests/api/users_spec.rb
@@ -161,7 +161,7 @@ RSpec.describe API::Users, :do_not_mock_admin_mode do
end
context 'accesses the profile of another admin' do
- let(:admin_2) {create(:admin, note: '2010-10-10 | 2FA added | admin requested | www.gitlab.com')}
+ let(:admin_2) { create(:admin, note: '2010-10-10 | 2FA added | admin requested | www.gitlab.com') }
it 'contains the note of the user' do
get api("/user?private_token=#{admin_personal_access_token}&sudo=#{admin_2.id}")
@@ -772,11 +772,11 @@ RSpec.describe API::Users, :do_not_mock_admin_mode do
it "does not create user with invalid email" do
post api('/users', admin),
- params: {
- email: 'invalid email',
- password: 'password',
- name: 'test'
- }
+ params: {
+ email: 'invalid email',
+ password: 'password',
+ name: 'test'
+ }
expect(response).to have_gitlab_http_status(:bad_request)
end
@@ -811,14 +811,14 @@ RSpec.describe API::Users, :do_not_mock_admin_mode do
it 'returns 400 error if user does not validate' do
post api('/users', admin),
- params: {
- password: 'pass',
- email: 'test@example.com',
- username: 'test!',
- name: 'test',
- bio: 'g' * 256,
- projects_limit: -1
- }
+ params: {
+ password: 'pass',
+ email: 'test@example.com',
+ username: 'test!',
+ name: 'test',
+ bio: 'g' * 256,
+ projects_limit: -1
+ }
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['message']['password'])
.to eq(['is too short (minimum is 8 characters)'])
@@ -838,23 +838,23 @@ RSpec.describe API::Users, :do_not_mock_admin_mode do
context 'with existing user' do
before do
post api('/users', admin),
- params: {
- email: 'test@example.com',
- password: 'password',
- username: 'test',
- name: 'foo'
- }
+ params: {
+ email: 'test@example.com',
+ password: 'password',
+ username: 'test',
+ name: 'foo'
+ }
end
it 'returns 409 conflict error if user with same email exists' do
expect do
post api('/users', admin),
- params: {
- name: 'foo',
- email: 'test@example.com',
- password: 'password',
- username: 'foo'
- }
+ params: {
+ name: 'foo',
+ email: 'test@example.com',
+ password: 'password',
+ username: 'foo'
+ }
end.to change { User.count }.by(0)
expect(response).to have_gitlab_http_status(:conflict)
expect(json_response['message']).to eq('Email has already been taken')
@@ -863,12 +863,12 @@ RSpec.describe API::Users, :do_not_mock_admin_mode do
it 'returns 409 conflict error if same username exists' do
expect do
post api('/users', admin),
- params: {
- name: 'foo',
- email: 'foo@example.com',
- password: 'password',
- username: 'test'
- }
+ params: {
+ name: 'foo',
+ email: 'foo@example.com',
+ password: 'password',
+ username: 'test'
+ }
end.to change { User.count }.by(0)
expect(response).to have_gitlab_http_status(:conflict)
expect(json_response['message']).to eq('Username has already been taken')
@@ -877,12 +877,12 @@ RSpec.describe API::Users, :do_not_mock_admin_mode do
it 'returns 409 conflict error if same username exists (case insensitive)' do
expect do
post api('/users', admin),
- params: {
- name: 'foo',
- email: 'foo@example.com',
- password: 'password',
- username: 'TEST'
- }
+ params: {
+ name: 'foo',
+ email: 'foo@example.com',
+ password: 'password',
+ username: 'TEST'
+ }
end.to change { User.count }.by(0)
expect(response).to have_gitlab_http_status(:conflict)
expect(json_response['message']).to eq('Username has already been taken')
@@ -1185,14 +1185,14 @@ RSpec.describe API::Users, :do_not_mock_admin_mode do
it 'returns 400 error if user does not validate' do
put api("/users/#{user.id}", admin),
- params: {
- password: 'pass',
- email: 'test@example.com',
- username: 'test!',
- name: 'test',
- bio: 'g' * 256,
- projects_limit: -1
- }
+ params: {
+ password: 'pass',
+ email: 'test@example.com',
+ username: 'test!',
+ name: 'test',
+ bio: 'g' * 256,
+ projects_limit: -1
+ }
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['message']['password'])
.to eq(['is too short (minimum is 8 characters)'])
@@ -1714,14 +1714,14 @@ RSpec.describe API::Users, :do_not_mock_admin_mode do
context "hard delete disabled" do
it "does not delete user" do
- perform_enqueued_jobs { delete api("/users/#{user.id}", admin)}
+ perform_enqueued_jobs { delete api("/users/#{user.id}", admin) }
expect(response).to have_gitlab_http_status(:conflict)
end
end
context "hard delete enabled" do
it "delete user and group", :sidekiq_might_not_need_inline do
- perform_enqueued_jobs { delete api("/users/#{user.id}?hard_delete=true", admin)}
+ perform_enqueued_jobs { delete api("/users/#{user.id}?hard_delete=true", admin) }
expect(response).to have_gitlab_http_status(:no_content)
expect(Group.exists?(group.id)).to be_falsy
end
@@ -1993,7 +1993,7 @@ RSpec.describe API::Users, :do_not_mock_admin_mode do
delete api("/user/keys/#{key.id}", user)
expect(response).to have_gitlab_http_status(:no_content)
- end.to change { user.keys.count}.by(-1)
+ end.to change { user.keys.count }.by(-1)
end
it_behaves_like '412 response' do
@@ -2124,7 +2124,7 @@ RSpec.describe API::Users, :do_not_mock_admin_mode do
post api("/user/gpg_keys/#{gpg_key.id}/revoke", user)
expect(response).to have_gitlab_http_status(:accepted)
- end.to change { user.gpg_keys.count}.by(-1)
+ end.to change { user.gpg_keys.count }.by(-1)
end
it 'returns 404 if key ID not found' do
@@ -2157,7 +2157,7 @@ RSpec.describe API::Users, :do_not_mock_admin_mode do
delete api("/user/gpg_keys/#{gpg_key.id}", user)
expect(response).to have_gitlab_http_status(:no_content)
- end.to change { user.gpg_keys.count}.by(-1)
+ end.to change { user.gpg_keys.count }.by(-1)
end
it 'returns 404 if key ID not found' do
@@ -2279,7 +2279,7 @@ RSpec.describe API::Users, :do_not_mock_admin_mode do
delete api("/user/emails/#{email.id}", user)
expect(response).to have_gitlab_http_status(:no_content)
- end.to change { user.emails.count}.by(-1)
+ end.to change { user.emails.count }.by(-1)
end
it_behaves_like '412 response' do
@@ -2756,6 +2756,124 @@ RSpec.describe API::Users, :do_not_mock_admin_mode do
end
end
+ describe 'POST /users/:user_id/personal_access_tokens' do
+ let(:name) { 'new pat' }
+ let(:expires_at) { 3.days.from_now.to_date.to_s }
+ let(:scopes) { %w(api read_user) }
+
+ context 'when feature flag is enabled' do
+ before do
+ stub_feature_flags(pat_creation_api_for_admin: true)
+ end
+
+ it 'returns error if required attributes are missing' do
+ post api("/users/#{user.id}/personal_access_tokens", admin)
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['error']).to eq('name is missing, scopes is missing, scopes does not have a valid value')
+ end
+
+ it 'returns a 404 error if user not found' do
+ post api("/users/#{non_existing_record_id}/personal_access_tokens", admin),
+ params: {
+ name: name,
+ scopes: scopes,
+ expires_at: expires_at
+ }
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ expect(json_response['message']).to eq('404 User Not Found')
+ end
+
+ it 'returns a 401 error when not authenticated' do
+ post api("/users/#{user.id}/personal_access_tokens"),
+ params: {
+ name: name,
+ scopes: scopes,
+ expires_at: expires_at
+ }
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ expect(json_response['message']).to eq('401 Unauthorized')
+ end
+
+ it 'returns a 403 error when authenticated as normal user' do
+ post api("/users/#{user.id}/personal_access_tokens", user),
+ params: {
+ name: name,
+ scopes: scopes,
+ expires_at: expires_at
+ }
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ expect(json_response['message']).to eq('403 Forbidden')
+ end
+
+ it 'creates a personal access token when authenticated as admin' do
+ post api("/users/#{user.id}/personal_access_tokens", admin),
+ params: {
+ name: name,
+ expires_at: expires_at,
+ scopes: scopes
+ }
+
+ expect(response).to have_gitlab_http_status(:created)
+ expect(json_response['name']).to eq(name)
+ expect(json_response['scopes']).to eq(scopes)
+ expect(json_response['expires_at']).to eq(expires_at)
+ expect(json_response['id']).to be_present
+ expect(json_response['created_at']).to be_present
+ expect(json_response['active']).to be_truthy
+ expect(json_response['revoked']).to be_falsey
+ expect(json_response['token']).to be_present
+ end
+
+ context 'when an error is thrown by the model' do
+ let!(:admin_personal_access_token) { create(:personal_access_token, user: admin) }
+ let(:error_message) { 'error message' }
+
+ before do
+ allow_next_instance_of(PersonalAccessToken) do |personal_access_token|
+ allow(personal_access_token).to receive_message_chain(:errors, :full_messages)
+ .and_return([error_message])
+
+ allow(personal_access_token).to receive(:save).and_return(false)
+ end
+ end
+
+ it 'returns the error' do
+ post api("/users/#{user.id}/personal_access_tokens", personal_access_token: admin_personal_access_token),
+ params: {
+ name: name,
+ expires_at: expires_at,
+ scopes: scopes
+ }
+
+ expect(response).to have_gitlab_http_status(:unprocessable_entity)
+ expect(json_response['message']).to eq(error_message)
+ end
+ end
+ end
+
+ context 'when feature flag is disabled' do
+ before do
+ stub_feature_flags(pat_creation_api_for_admin: false)
+ end
+
+ it 'returns a 404' do
+ post api("/users/#{user.id}/personal_access_tokens", admin),
+ params: {
+ name: name,
+ expires_at: expires_at,
+ scopes: scopes
+ }
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ expect(json_response['message']).to eq('404 Not Found')
+ end
+ end
+ end
+
describe 'GET /users/:user_id/impersonation_tokens' do
let_it_be(:active_personal_access_token) { create(:personal_access_token, user: user) }
let_it_be(:revoked_personal_access_token) { create(:personal_access_token, :revoked, user: user) }