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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-07-20 18:40:28 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2022-07-20 18:40:28 +0300
commitb595cb0c1dec83de5bdee18284abe86614bed33b (patch)
tree8c3d4540f193c5ff98019352f554e921b3a41a72 /spec/requests/api
parent2f9104a328fc8a4bddeaa4627b595166d24671d0 (diff)
Add latest changes from gitlab-org/gitlab@15-2-stable-eev15.2.0-rc42
Diffstat (limited to 'spec/requests/api')
-rw-r--r--spec/requests/api/api_spec.rb4
-rw-r--r--spec/requests/api/award_emoji_spec.rb112
-rw-r--r--spec/requests/api/ci/runner/jobs_request_post_spec.rb73
-rw-r--r--spec/requests/api/commits_spec.rb19
-rw-r--r--spec/requests/api/conan_instance_packages_spec.rb6
-rw-r--r--spec/requests/api/conan_project_packages_spec.rb6
-rw-r--r--spec/requests/api/environments_spec.rb83
-rw-r--r--spec/requests/api/events_spec.rb2
-rw-r--r--spec/requests/api/feature_flags_user_lists_spec.rb7
-rw-r--r--spec/requests/api/geo_spec.rb14
-rw-r--r--spec/requests/api/graphql/boards/board_lists_query_spec.rb3
-rw-r--r--spec/requests/api/graphql/ci/group_variables_spec.rb67
-rw-r--r--spec/requests/api/graphql/ci/instance_variables_spec.rb60
-rw-r--r--spec/requests/api/graphql/ci/job_spec.rb4
-rw-r--r--spec/requests/api/graphql/ci/jobs_spec.rb6
-rw-r--r--spec/requests/api/graphql/ci/manual_variables_spec.rb95
-rw-r--r--spec/requests/api/graphql/ci/pipelines_spec.rb4
-rw-r--r--spec/requests/api/graphql/ci/project_variables_spec.rb67
-rw-r--r--spec/requests/api/graphql/ci/runner_spec.rb2
-rw-r--r--spec/requests/api/graphql/ci/stages_spec.rb2
-rw-r--r--spec/requests/api/graphql/container_repository/container_repository_details_spec.rb2
-rw-r--r--spec/requests/api/graphql/crm/contacts_spec.rb69
-rw-r--r--spec/requests/api/graphql/current_user/groups_query_spec.rb34
-rw-r--r--spec/requests/api/graphql/group/container_repositories_spec.rb2
-rw-r--r--spec/requests/api/graphql/group/dependency_proxy_blobs_spec.rb2
-rw-r--r--spec/requests/api/graphql/group/dependency_proxy_group_setting_spec.rb2
-rw-r--r--spec/requests/api/graphql/group/dependency_proxy_image_ttl_policy_spec.rb2
-rw-r--r--spec/requests/api/graphql/group/dependency_proxy_manifests_spec.rb7
-rw-r--r--spec/requests/api/graphql/group/group_members_spec.rb48
-rw-r--r--spec/requests/api/graphql/mutations/issues/create_spec.rb36
-rw-r--r--spec/requests/api/graphql/mutations/notes/create/diff_note_spec.rb14
-rw-r--r--spec/requests/api/graphql/mutations/snippets/update_spec.rb17
-rw-r--r--spec/requests/api/graphql/mutations/work_items/create_from_task_spec.rb1
-rw-r--r--spec/requests/api/graphql/mutations/work_items/create_spec.rb89
-rw-r--r--spec/requests/api/graphql/mutations/work_items/delete_task_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/work_items/update_spec.rb268
-rw-r--r--spec/requests/api/graphql/mutations/work_items/update_widgets_spec.rb51
-rw-r--r--spec/requests/api/graphql/project/container_repositories_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/issues_spec.rb37
-rw-r--r--spec/requests/api/graphql/project/jobs_spec.rb4
-rw-r--r--spec/requests/api/graphql/project/packages_cleanup_policy_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/pipeline_spec.rb60
-rw-r--r--spec/requests/api/graphql/project/project_members_spec.rb48
-rw-r--r--spec/requests/api/graphql/todo_query_spec.rb50
-rw-r--r--spec/requests/api/graphql/work_item_spec.rb98
-rw-r--r--spec/requests/api/group_export_spec.rb14
-rw-r--r--spec/requests/api/group_variables_spec.rb2
-rw-r--r--spec/requests/api/groups_spec.rb14
-rw-r--r--spec/requests/api/integrations_spec.rb40
-rw-r--r--spec/requests/api/internal/base_spec.rb58
-rw-r--r--spec/requests/api/internal/error_tracking_spec.rb108
-rw-r--r--spec/requests/api/internal/kubernetes_spec.rb4
-rw-r--r--spec/requests/api/invitations_spec.rb40
-rw-r--r--spec/requests/api/issues/issues_spec.rb14
-rw-r--r--spec/requests/api/markdown_snapshot_spec.rb4
-rw-r--r--spec/requests/api/maven_packages_spec.rb40
-rw-r--r--spec/requests/api/metadata_spec.rb94
-rw-r--r--spec/requests/api/npm_project_packages_spec.rb14
-rw-r--r--spec/requests/api/project_attributes.yml6
-rw-r--r--spec/requests/api/project_export_spec.rb14
-rw-r--r--spec/requests/api/project_hooks_spec.rb247
-rw-r--r--spec/requests/api/project_import_spec.rb10
-rw-r--r--spec/requests/api/projects_spec.rb43
-rw-r--r--spec/requests/api/protected_tags_spec.rb7
-rw-r--r--spec/requests/api/pypi_packages_spec.rb15
-rw-r--r--spec/requests/api/repositories_spec.rb74
-rw-r--r--spec/requests/api/settings_spec.rb2
-rw-r--r--spec/requests/api/snippets_spec.rb38
-rw-r--r--spec/requests/api/system_hooks_spec.rb229
-rw-r--r--spec/requests/api/tags_spec.rb7
-rw-r--r--spec/requests/api/terraform/modules/v1/packages_spec.rb210
-rw-r--r--spec/requests/api/unleash_spec.rb42
-rw-r--r--spec/requests/api/users_spec.rb109
73 files changed, 2310 insertions, 772 deletions
diff --git a/spec/requests/api/api_spec.rb b/spec/requests/api/api_spec.rb
index df9be2616c5..b6cb790bb71 100644
--- a/spec/requests/api/api_spec.rb
+++ b/spec/requests/api/api_spec.rb
@@ -133,7 +133,7 @@ RSpec.describe API::API do
'meta.caller_id' => 'GET /api/:version/broadcast_messages',
'meta.remote_ip' => an_instance_of(String),
'meta.client_id' => a_string_matching(%r{\Aip/.+}),
- 'meta.feature_category' => 'navigation',
+ 'meta.feature_category' => 'onboarding',
'route' => '/api/:version/broadcast_messages')
expect(data.stringify_keys).not_to include('meta.project', 'meta.root_namespace', 'meta.user')
@@ -209,7 +209,7 @@ RSpec.describe API::API do
'meta.caller_id' => 'GET /api/:version/broadcast_messages',
'meta.remote_ip' => an_instance_of(String),
'meta.client_id' => a_string_matching(%r{\Aip/.+}),
- 'meta.feature_category' => 'navigation',
+ 'meta.feature_category' => 'onboarding',
'route' => '/api/:version/broadcast_messages')
expect(data.stringify_keys).not_to include('meta.project', 'meta.root_namespace', 'meta.user')
diff --git a/spec/requests/api/award_emoji_spec.rb b/spec/requests/api/award_emoji_spec.rb
index 782e14593f7..67ddaf2fda5 100644
--- a/spec/requests/api/award_emoji_spec.rb
+++ b/spec/requests/api/award_emoji_spec.rb
@@ -3,8 +3,8 @@
require 'spec_helper'
RSpec.describe API::AwardEmoji do
+ let_it_be_with_reload(:project) { create(:project, :private) }
let_it_be(:user) { create(:user) }
- let_it_be(:project) { create(:project) }
let_it_be(:issue) { create(:issue, project: project) }
let_it_be(:award_emoji) { create(:award_emoji, awardable: issue, user: user) }
let_it_be(:note) { create(:note, project: project, noteable: issue) }
@@ -16,10 +16,46 @@ RSpec.describe API::AwardEmoji do
project.add_maintainer(user)
end
+ shared_examples 'request with insufficient permissions' do |request_method|
+ let(:request_params) { {} }
+
+ context 'when user is not signed in' do
+ it 'returns 404' do
+ process request_method, api(request_path), params: request_params
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'when user does not have access' do
+ it 'returns 404' do
+ other_user = create(:user)
+
+ process request_method, api(request_path, other_user), params: request_params
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+
+ shared_examples 'unauthenticated request to public awardable' do
+ before do
+ project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
+ end
+
+ it 'returns the awarded emoji' do
+ get api(request_path)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+
describe "GET /projects/:id/awardable/:awardable_id/award_emoji" do
context 'on an issue' do
+ let(:request_path) { "/projects/#{project.id}/issues/#{issue.iid}/award_emoji" }
+
it "returns an array of award_emoji" do
- get api("/projects/#{project.id}/issues/#{issue.iid}/award_emoji", user)
+ get api(request_path, user)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to be_an Array
@@ -48,6 +84,9 @@ RSpec.describe API::AwardEmoji do
expect(response).to have_gitlab_http_status(:not_found)
end
+
+ it_behaves_like 'unauthenticated request to public awardable'
+ it_behaves_like 'request with insufficient permissions', :get
end
context 'on a merge request' do
@@ -73,34 +112,30 @@ RSpec.describe API::AwardEmoji do
expect(json_response.first['name']).to eq(award.name)
end
end
-
- context 'when the user has no access' do
- it 'returns a status code 404' do
- user1 = create(:user)
-
- get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/award_emoji", user1)
-
- expect(response).to have_gitlab_http_status(:not_found)
- end
- end
end
describe 'GET /projects/:id/awardable/:awardable_id/notes/:note_id/award_emoji' do
- let!(:rocket) { create(:award_emoji, awardable: note, name: 'rocket') }
+ let!(:rocket) { create(:award_emoji, awardable: note, name: 'rocket') }
+ let(:request_path) { "/projects/#{project.id}/issues/#{issue.iid}/notes/#{note.id}/award_emoji" }
it 'returns an array of award emoji' do
- get api("/projects/#{project.id}/issues/#{issue.iid}/notes/#{note.id}/award_emoji", user)
+ get api(request_path, user)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to be_an Array
expect(json_response.first['name']).to eq(rocket.name)
end
+
+ it_behaves_like 'unauthenticated request to public awardable'
+ it_behaves_like 'request with insufficient permissions', :get
end
describe "GET /projects/:id/awardable/:awardable_id/award_emoji/:award_id" do
context 'on an issue' do
+ let(:request_path) { "/projects/#{project.id}/issues/#{issue.iid}/award_emoji/#{award_emoji.id}" }
+
it "returns the award emoji" do
- get api("/projects/#{project.id}/issues/#{issue.iid}/award_emoji/#{award_emoji.id}", user)
+ get api(request_path, user)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['name']).to eq(award_emoji.name)
@@ -113,6 +148,9 @@ RSpec.describe API::AwardEmoji do
expect(response).to have_gitlab_http_status(:not_found)
end
+
+ it_behaves_like 'unauthenticated request to public awardable'
+ it_behaves_like 'request with insufficient permissions', :get
end
context 'on a merge request' do
@@ -139,28 +177,22 @@ RSpec.describe API::AwardEmoji do
expect(json_response['awardable_type']).to eq("Snippet")
end
end
-
- context 'when the user has no access' do
- it 'returns a status code 404' do
- user1 = create(:user)
-
- get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/award_emoji/#{downvote.id}", user1)
-
- expect(response).to have_gitlab_http_status(:not_found)
- end
- end
end
describe 'GET /projects/:id/awardable/:awardable_id/notes/:note_id/award_emoji/:award_id' do
- let!(:rocket) { create(:award_emoji, awardable: note, name: 'rocket') }
+ let!(:rocket) { create(:award_emoji, awardable: note, name: 'rocket') }
+ let(:request_path) { "/projects/#{project.id}/issues/#{issue.iid}/notes/#{note.id}/award_emoji/#{rocket.id}" }
it 'returns an award emoji' do
- get api("/projects/#{project.id}/issues/#{issue.iid}/notes/#{note.id}/award_emoji/#{rocket.id}", user)
+ get api(request_path, user)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).not_to be_an Array
expect(json_response['name']).to eq(rocket.name)
end
+
+ it_behaves_like 'unauthenticated request to public awardable'
+ it_behaves_like 'request with insufficient permissions', :get
end
describe "POST /projects/:id/awardable/:awardable_id/award_emoji" do
@@ -189,12 +221,6 @@ RSpec.describe API::AwardEmoji do
expect(response).to have_gitlab_http_status(:bad_request)
end
- it "returns a 401 unauthorized error if the user is not authenticated" do
- post api("/projects/#{project.id}/issues/#{issue.iid}/award_emoji"), params: { name: 'thumbsup' }
-
- expect(response).to have_gitlab_http_status(:unauthorized)
- end
-
it "normalizes +1 as thumbsup award" do
post api("/projects/#{project.id}/issues/#{issue.iid}/award_emoji", user), params: { name: '+1' }
@@ -223,6 +249,11 @@ RSpec.describe API::AwardEmoji do
expect(json_response['user']['username']).to eq(user.username)
end
end
+
+ it_behaves_like 'request with insufficient permissions', :post do
+ let(:request_path) { "/projects/#{project.id}/issues/#{issue.iid}/award_emoji" }
+ let(:request_params) { { name: 'blowfish' } }
+ end
end
describe "POST /projects/:id/awardable/:awardable_id/notes/:note_id/award_emoji" do
@@ -260,6 +291,11 @@ RSpec.describe API::AwardEmoji do
expect(json_response["message"]).to match("has already been taken")
end
end
+
+ it_behaves_like 'request with insufficient permissions', :post do
+ let(:request_path) { "/projects/#{project.id}/issues/#{issue.iid}/notes/#{note.id}/award_emoji" }
+ let(:request_params) { { name: 'rocket' } }
+ end
end
describe 'DELETE /projects/:id/awardable/:awardable_id/award_emoji/:award_id' do
@@ -319,9 +355,13 @@ RSpec.describe API::AwardEmoji do
let(:request) { api("/projects/#{project.id}/snippets/#{snippet.id}/award_emoji/#{award.id}", user) }
end
end
+
+ it_behaves_like 'request with insufficient permissions', :delete do
+ let(:request_path) { "/projects/#{project.id}/issues/#{issue.iid}/award_emoji/#{award_emoji.id}" }
+ end
end
- describe 'DELETE /projects/:id/awardable/:awardable_id/award_emoji/:award_emoji_id' do
+ describe 'DELETE /projects/:id/awardable/:awardable_id/notes/:note_id/award_emoji/:award_id' do
let!(:rocket) { create(:award_emoji, awardable: note, name: 'rocket', user: user) }
it 'deletes the award' do
@@ -335,5 +375,9 @@ RSpec.describe API::AwardEmoji do
it_behaves_like '412 response' do
let(:request) { api("/projects/#{project.id}/issues/#{issue.iid}/notes/#{note.id}/award_emoji/#{rocket.id}", user) }
end
+
+ it_behaves_like 'request with insufficient permissions', :delete do
+ let(:request_path) { "/projects/#{project.id}/issues/#{issue.iid}/notes/#{note.id}/award_emoji/#{rocket.id}" }
+ end
end
end
diff --git a/spec/requests/api/ci/runner/jobs_request_post_spec.rb b/spec/requests/api/ci/runner/jobs_request_post_spec.rb
index 3c6f9ac2816..746be1ccc44 100644
--- a/spec/requests/api/ci/runner/jobs_request_post_spec.rb
+++ b/spec/requests/api/ci/runner/jobs_request_post_spec.rb
@@ -216,13 +216,17 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
expect(json_response['token']).to eq(job.token)
expect(json_response['job_info']).to eq(expected_job_info)
expect(json_response['git_info']).to eq(expected_git_info)
- expect(json_response['image']).to eq({ 'name' => 'image:1.0', 'entrypoint' => '/bin/sh', 'ports' => [], 'pull_policy' => nil })
- expect(json_response['services']).to eq([{ 'name' => 'postgres', 'entrypoint' => nil,
- 'alias' => nil, 'command' => nil, 'ports' => [], 'variables' => nil },
- { 'name' => 'docker:stable-dind', 'entrypoint' => '/bin/sh',
- 'alias' => 'docker', 'command' => 'sleep 30', 'ports' => [], 'variables' => [] },
- { 'name' => 'mysql:latest', 'entrypoint' => nil,
- 'alias' => nil, 'command' => nil, 'ports' => [], 'variables' => [{ 'key' => 'MYSQL_ROOT_PASSWORD', 'value' => 'root123.' }] }])
+ expect(json_response['image']).to eq(
+ { 'name' => 'image:1.0', 'entrypoint' => '/bin/sh', 'ports' => [], 'pull_policy' => nil }
+ )
+ expect(json_response['services']).to eq([
+ { 'name' => 'postgres', 'entrypoint' => nil, 'alias' => nil, 'command' => nil, 'ports' => [],
+ 'variables' => nil, 'pull_policy' => nil },
+ { 'name' => 'docker:stable-dind', 'entrypoint' => '/bin/sh', 'alias' => 'docker', 'command' => 'sleep 30',
+ 'ports' => [], 'variables' => [], 'pull_policy' => nil },
+ { 'name' => 'mysql:latest', 'entrypoint' => nil, 'alias' => nil, 'command' => nil, 'ports' => [],
+ 'variables' => [{ 'key' => 'MYSQL_ROOT_PASSWORD', 'value' => 'root123.' }], 'pull_policy' => nil }
+ ])
expect(json_response['steps']).to eq(expected_steps)
expect(json_response['artifacts']).to eq(expected_artifacts)
expect(json_response['cache']).to match(expected_cache)
@@ -542,7 +546,7 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
let!(:job) { create(:ci_build, :pending, :queued, :tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) }
let!(:job2) { create(:ci_build, :pending, :queued, :tag, pipeline: pipeline, name: 'rubocop', stage: 'test', stage_idx: 0) }
let!(:test_job) do
- create(:ci_build, :pending, :queued, pipeline: pipeline, token: 'test-job-token', name: 'deploy',
+ create(:ci_build, :pending, :queued, pipeline: pipeline, name: 'deploy',
stage: 'deploy', stage_idx: 1,
options: { script: ['bash'], dependencies: [job2.name] })
end
@@ -566,7 +570,7 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
let!(:job) { create(:ci_build, :pending, :queued, :tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) }
let!(:job2) { create(:ci_build, :pending, :queued, :tag, pipeline: pipeline, name: 'rubocop', stage: 'test', stage_idx: 0) }
let!(:empty_dependencies_job) do
- create(:ci_build, :pending, :queued, pipeline: pipeline, token: 'test-job-token', name: 'empty_dependencies_job',
+ create(:ci_build, :pending, :queued, pipeline: pipeline, name: 'empty_dependencies_job',
stage: 'deploy', stage_idx: 1,
options: { script: ['bash'], dependencies: [] })
end
@@ -671,14 +675,6 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
end
end
- context 'when variables are stored in trigger_request' do
- before do
- trigger_request.update_attribute(:variables, { TRIGGER_KEY_1: 'TRIGGER_VALUE_1' } )
- end
-
- it_behaves_like 'expected variables behavior'
- end
-
context 'when variables are stored in pipeline_variables' do
before do
create(:ci_pipeline_variable, pipeline: pipeline, key: :TRIGGER_KEY_1, value: 'TRIGGER_VALUE_1')
@@ -849,10 +845,51 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
end
end
+ context 'when service has pull_policy' do
+ let(:job) { create(:ci_build, :pending, :queued, pipeline: pipeline, options: options) }
+
+ let(:options) do
+ {
+ services: [{
+ name: 'postgres:11.9',
+ pull_policy: ['if-not-present']
+ }]
+ }
+ end
+
+ it 'returns the service with pull policy' do
+ request_job
+
+ expect(response).to have_gitlab_http_status(:created)
+ expect(json_response).to include(
+ 'id' => job.id,
+ 'services' => [{ 'alias' => nil, 'command' => nil, 'entrypoint' => nil, 'name' => 'postgres:11.9',
+ 'ports' => [], 'pull_policy' => ['if-not-present'], 'variables' => [] }]
+ )
+ end
+
+ context 'when the FF ci_docker_image_pull_policy is disabled' do
+ before do
+ stub_feature_flags(ci_docker_image_pull_policy: false)
+ end
+
+ it 'returns the service without pull policy' do
+ request_job
+
+ expect(response).to have_gitlab_http_status(:created)
+ expect(json_response).to include(
+ 'id' => job.id,
+ 'services' => [{ 'alias' => nil, 'command' => nil, 'entrypoint' => nil, 'name' => 'postgres:11.9',
+ 'ports' => [], 'variables' => [] }]
+ )
+ end
+ end
+ end
+
describe 'a job with excluded artifacts' do
context 'when excluded paths are defined' do
let(:job) do
- create(:ci_build, :pending, :queued, pipeline: pipeline, token: 'test-job-token', name: 'test',
+ create(:ci_build, :pending, :queued, pipeline: pipeline, name: 'test',
stage: 'deploy', stage_idx: 1,
options: { artifacts: { paths: ['abc'], exclude: ['cde'] } })
end
diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb
index 67c2ec91540..9ef845f06bf 100644
--- a/spec/requests/api/commits_spec.rb
+++ b/spec/requests/api/commits_spec.rb
@@ -392,14 +392,25 @@ RSpec.describe API::Commits do
end
end
- context 'when using warden' do
- it 'increments usage counters', :clean_gitlab_redis_sessions do
- stub_session('warden.user.user.key' => [[user.id], user.encrypted_password[0, 29]])
+ context 'when using warden', :snowplow, :clean_gitlab_redis_sessions do
+ before do
+ stub_session('warden.user.user.key' => [[user.id], user.authenticatable_salt])
+ end
+
+ subject { post api(url), params: valid_c_params }
+ it 'increments usage counters' do
expect(::Gitlab::UsageDataCounters::WebIdeCounter).to receive(:increment_commits_count)
expect(::Gitlab::UsageDataCounters::EditorUniqueCounter).to receive(:track_web_ide_edit_action)
- post api(url), params: valid_c_params
+ subject
+ end
+
+ it_behaves_like 'Snowplow event tracking' do
+ let(:namespace) { project.namespace }
+ let(:category) { 'ide_edit' }
+ let(:action) { 'g_edit_by_web_ide' }
+ let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
end
end
diff --git a/spec/requests/api/conan_instance_packages_spec.rb b/spec/requests/api/conan_instance_packages_spec.rb
index ff3b332c620..54cad3093d7 100644
--- a/spec/requests/api/conan_instance_packages_spec.rb
+++ b/spec/requests/api/conan_instance_packages_spec.rb
@@ -17,6 +17,12 @@ RSpec.describe API::ConanInstancePackages do
let_it_be(:url) { '/packages/conan/v1/conans/search' }
it_behaves_like 'conan search endpoint'
+
+ it_behaves_like 'conan FIPS mode' do
+ let(:params) { { q: package.conan_recipe } }
+
+ subject { get api(url), params: params }
+ end
end
describe 'GET /api/v4/packages/conan/v1/users/authenticate' do
diff --git a/spec/requests/api/conan_project_packages_spec.rb b/spec/requests/api/conan_project_packages_spec.rb
index c108f2efaaf..e28105eb8eb 100644
--- a/spec/requests/api/conan_project_packages_spec.rb
+++ b/spec/requests/api/conan_project_packages_spec.rb
@@ -17,6 +17,12 @@ RSpec.describe API::ConanProjectPackages do
let(:url) { "/projects/#{project.id}/packages/conan/v1/conans/search" }
it_behaves_like 'conan search endpoint'
+
+ it_behaves_like 'conan FIPS mode' do
+ let(:params) { { q: package.conan_recipe } }
+
+ subject { get api(url), params: params }
+ end
end
describe 'GET /api/v4/projects/:id/packages/conan/v1/users/authenticate' do
diff --git a/spec/requests/api/environments_spec.rb b/spec/requests/api/environments_spec.rb
index 93f21c880a4..a35c1630caa 100644
--- a/spec/requests/api/environments_spec.rb
+++ b/spec/requests/api/environments_spec.rb
@@ -26,90 +26,7 @@ RSpec.describe API::Environments do
expect(json_response.first['tier']).to eq(environment.tier)
expect(json_response.first['external_url']).to eq(environment.external_url)
expect(json_response.first['project']).to match_schema('public_api/v4/project')
- expect(json_response.first['enable_advanced_logs_querying']).to eq(false)
expect(json_response.first).not_to have_key('last_deployment')
- expect(json_response.first).not_to have_key('gitlab_managed_apps_logs_path')
- end
-
- context 'when the user can read pod logs' do
- context 'with successful deployment on cluster' do
- let_it_be(:deployment) { create(:deployment, :on_cluster, :success, environment: environment, project: project) }
-
- it 'returns environment with enable_advanced_logs_querying and logs_api_path' do
- get api("/projects/#{project.id}/environments", user)
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- expect(json_response.size).to eq(1)
- expect(json_response.first['gitlab_managed_apps_logs_path']).to eq(
- "/#{project.full_path}/-/logs/k8s.json?cluster_id=#{deployment.cluster_id}"
- )
- end
- end
-
- context 'when elastic stack is available' do
- before do
- allow_next_found_instance_of(Environment) do |env|
- allow(env).to receive(:elastic_stack_available?).and_return(true)
- end
- end
-
- it 'returns environment with enable_advanced_logs_querying and logs_api_path' do
- get api("/projects/#{project.id}/environments", user)
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- expect(json_response.size).to eq(1)
- expect(json_response.first['enable_advanced_logs_querying']).to eq(true)
- expect(json_response.first['logs_api_path']).to eq(
- "/#{project.full_path}/-/logs/elasticsearch.json?environment_name=#{environment.name}"
- )
- end
- end
-
- context 'when elastic stack is not available' do
- before do
- allow_next_found_instance_of(Environment) do |env|
- allow(env).to receive(:elastic_stack_available?).and_return(false)
- end
- end
-
- it 'returns environment with enable_advanced_logs_querying logs_api_path' do
- get api("/projects/#{project.id}/environments", user)
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- expect(json_response.size).to eq(1)
- expect(json_response.first['enable_advanced_logs_querying']).to eq(false)
- expect(json_response.first['logs_api_path']).to eq(
- "/#{project.full_path}/-/logs/k8s.json?environment_name=#{environment.name}"
- )
- end
- end
- end
-
- context 'when the user cannot read pod logs' do
- before do
- allow_next_found_instance_of(User) do |user|
- allow(user).to receive(:can?).and_call_original
- allow(user).to receive(:can?).with(:read_pod_logs, project).and_return(false)
- end
- end
-
- it 'does not contain enable_advanced_logs_querying' do
- get api("/projects/#{project.id}/environments", user)
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- expect(json_response.size).to eq(1)
- expect(json_response.first).not_to have_key('enable_advanced_logs_querying')
- expect(json_response.first).not_to have_key('logs_api_path')
- expect(json_response.first).not_to have_key('gitlab_managed_apps_logs_path')
- end
end
context 'when filtering' do
diff --git a/spec/requests/api/events_spec.rb b/spec/requests/api/events_spec.rb
index 110d6e2f99e..d6c3999f22f 100644
--- a/spec/requests/api/events_spec.rb
+++ b/spec/requests/api/events_spec.rb
@@ -173,7 +173,7 @@ RSpec.describe API::Events do
let(:second_note) { create(:note_on_issue, project: create(:project)) }
before do
- second_note.project.add_user(user, :developer)
+ second_note.project.add_member(user, :developer)
[second_note].each do |note|
EventCreateService.new.leave_note(note, user)
diff --git a/spec/requests/api/feature_flags_user_lists_spec.rb b/spec/requests/api/feature_flags_user_lists_spec.rb
index e2a3f92df10..bfc57042ff4 100644
--- a/spec/requests/api/feature_flags_user_lists_spec.rb
+++ b/spec/requests/api/feature_flags_user_lists_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
RSpec.describe API::FeatureFlagsUserLists do
let_it_be(:project, refind: true) { create(:project) }
+ let_it_be(:client, refind: true) { create(:operations_feature_flags_client, project: project) }
let_it_be(:developer) { create(:user) }
let_it_be(:reporter) { create(:user) }
@@ -215,6 +216,7 @@ RSpec.describe API::FeatureFlagsUserLists do
}
expect(response).to have_gitlab_http_status(:forbidden)
+ expect(client.reload.last_feature_flag_updated_at).to be_nil
end
it 'creates the flag' do
@@ -231,6 +233,7 @@ RSpec.describe API::FeatureFlagsUserLists do
})
expect(project.operations_feature_flags_user_lists.count).to eq(1)
expect(project.operations_feature_flags_user_lists.last.name).to eq('mylist')
+ expect(client.reload.last_feature_flag_updated_at).not_to be_nil
end
it 'requires name' do
@@ -298,6 +301,7 @@ RSpec.describe API::FeatureFlagsUserLists do
}
expect(response).to have_gitlab_http_status(:forbidden)
+ expect(client.reload.last_feature_flag_updated_at).to be_nil
end
it 'updates the list' do
@@ -313,6 +317,7 @@ RSpec.describe API::FeatureFlagsUserLists do
'user_xids' => '456,789'
})
expect(list.reload.name).to eq('mylist')
+ expect(client.reload.last_feature_flag_updated_at).not_to be_nil
end
it 'preserves attributes not listed in the request' do
@@ -377,6 +382,7 @@ RSpec.describe API::FeatureFlagsUserLists do
expect(response).to have_gitlab_http_status(:not_found)
expect(json_response).to eq({ 'message' => '404 Not found' })
+ expect(client.reload.last_feature_flag_updated_at).to be_nil
end
it 'deletes the list' do
@@ -387,6 +393,7 @@ RSpec.describe API::FeatureFlagsUserLists do
expect(response).to have_gitlab_http_status(:no_content)
expect(response.body).to be_blank
expect(project.operations_feature_flags_user_lists.count).to eq(0)
+ expect(client.reload.last_feature_flag_updated_at).not_to be_nil
end
it 'does not delete the list if it is associated with a strategy' do
diff --git a/spec/requests/api/geo_spec.rb b/spec/requests/api/geo_spec.rb
index edbca5eb1c6..4e77fa9405c 100644
--- a/spec/requests/api/geo_spec.rb
+++ b/spec/requests/api/geo_spec.rb
@@ -10,12 +10,24 @@ RSpec.describe API::Geo do
include_context 'workhorse headers'
+ let(:non_proxy_response_schema) do
+ {
+ 'type' => 'object',
+ 'additionalProperties' => false,
+ 'required' => %w(geo_enabled),
+ 'properties' => {
+ 'geo_enabled' => { 'type' => 'boolean' }
+ }
+ }
+ end
+
context 'with valid auth' do
it 'returns empty data' do
subject
expect(response).to have_gitlab_http_status(:ok)
- expect(json_response).to be_empty
+ expect(json_response).to match_schema(non_proxy_response_schema)
+ expect(json_response['geo_enabled']).to be_falsey
end
end
diff --git a/spec/requests/api/graphql/boards/board_lists_query_spec.rb b/spec/requests/api/graphql/boards/board_lists_query_spec.rb
index eb206465bce..39ff108a9e1 100644
--- a/spec/requests/api/graphql/boards/board_lists_query_spec.rb
+++ b/spec/requests/api/graphql/boards/board_lists_query_spec.rb
@@ -96,7 +96,8 @@ RSpec.describe 'get board lists' do
context 'when ascending' do
it_behaves_like 'sorted paginated query' do
- let(:sort_param) { }
+ include_context 'no sort argument'
+
let(:first_param) { 2 }
let(:all_records) { lists.map { |list| a_graphql_entity_for(list) } }
end
diff --git a/spec/requests/api/graphql/ci/group_variables_spec.rb b/spec/requests/api/graphql/ci/group_variables_spec.rb
new file mode 100644
index 00000000000..5ea6646ec2c
--- /dev/null
+++ b/spec/requests/api/graphql/ci/group_variables_spec.rb
@@ -0,0 +1,67 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Query.group(fullPath).ciVariables' do
+ include GraphqlHelpers
+
+ let_it_be(:group) { create(:group) }
+ let_it_be(:user) { create(:user) }
+
+ let(:query) do
+ %(
+ query {
+ group(fullPath: "#{group.full_path}") {
+ ciVariables {
+ nodes {
+ id
+ key
+ value
+ variableType
+ protected
+ masked
+ raw
+ environmentScope
+ }
+ }
+ }
+ }
+ )
+ end
+
+ context 'when the user can administer the group' do
+ before do
+ group.add_owner(user)
+ end
+
+ it "returns the group's CI variables" do
+ variable = create(:ci_group_variable, group: group, key: 'TEST_VAR', value: 'test',
+ masked: false, protected: true, raw: true, environment_scope: 'staging')
+
+ post_graphql(query, current_user: user)
+
+ expect(graphql_data.dig('group', 'ciVariables', 'nodes')).to contain_exactly({
+ 'id' => variable.to_global_id.to_s,
+ 'key' => 'TEST_VAR',
+ 'value' => 'test',
+ 'variableType' => 'ENV_VAR',
+ 'masked' => false,
+ 'protected' => true,
+ 'raw' => true,
+ 'environmentScope' => 'staging'
+ })
+ end
+ end
+
+ context 'when the user cannot administer the group' do
+ it 'returns nothing' do
+ create(:ci_group_variable, group: group, value: 'verysecret', masked: true)
+
+ group.add_developer(user)
+
+ post_graphql(query, current_user: user)
+
+ expect(graphql_data.dig('group', 'ciVariables')).to be_nil
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/ci/instance_variables_spec.rb b/spec/requests/api/graphql/ci/instance_variables_spec.rb
new file mode 100644
index 00000000000..7acf73a4e7a
--- /dev/null
+++ b/spec/requests/api/graphql/ci/instance_variables_spec.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Query.ciVariables' do
+ include GraphqlHelpers
+
+ let(:query) do
+ %(
+ query {
+ ciVariables {
+ nodes {
+ id
+ key
+ value
+ variableType
+ protected
+ masked
+ raw
+ environmentScope
+ }
+ }
+ }
+ )
+ end
+
+ context 'when the user is an admin' do
+ let_it_be(:user) { create(:admin) }
+
+ it "returns the instance's CI variables" do
+ variable = create(:ci_instance_variable, key: 'TEST_VAR', value: 'test',
+ masked: false, protected: true, raw: true)
+
+ post_graphql(query, current_user: user)
+
+ expect(graphql_data.dig('ciVariables', 'nodes')).to contain_exactly({
+ 'id' => variable.to_global_id.to_s,
+ 'key' => 'TEST_VAR',
+ 'value' => 'test',
+ 'variableType' => 'ENV_VAR',
+ 'masked' => false,
+ 'protected' => true,
+ 'raw' => true,
+ 'environmentScope' => nil
+ })
+ end
+ end
+
+ context 'when the user is not an admin' do
+ let_it_be(:user) { create(:user) }
+
+ it 'returns nothing' do
+ create(:ci_instance_variable, value: 'verysecret', masked: true)
+
+ post_graphql(query, current_user: user)
+
+ expect(graphql_data.dig('ciVariables')).to be_nil
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/ci/job_spec.rb b/spec/requests/api/graphql/ci/job_spec.rb
index 2fb90dcd92b..3721155c71b 100644
--- a/spec/requests/api/graphql/ci/job_spec.rb
+++ b/spec/requests/api/graphql/ci/job_spec.rb
@@ -13,8 +13,8 @@ RSpec.describe 'Query.project(fullPath).pipelines.job(id)' do
let_it_be(:project) { create(:project, :repository, :public) }
let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
- let_it_be(:prepare_stage) { create(:ci_stage_entity, pipeline: pipeline, project: project, name: 'prepare') }
- let_it_be(:test_stage) { create(:ci_stage_entity, pipeline: pipeline, project: project, name: 'test') }
+ let_it_be(:prepare_stage) { create(:ci_stage, pipeline: pipeline, project: project, name: 'prepare') }
+ let_it_be(:test_stage) { create(:ci_stage, pipeline: pipeline, project: project, name: 'test') }
let_it_be(:job_1) { create(:ci_build, pipeline: pipeline, stage: 'prepare', name: 'Job 1') }
let_it_be(:job_2) { create(:ci_build, pipeline: pipeline, stage: 'test', name: 'Job 2') }
diff --git a/spec/requests/api/graphql/ci/jobs_spec.rb b/spec/requests/api/graphql/ci/jobs_spec.rb
index d1737fc22ae..8c4ab13fc35 100644
--- a/spec/requests/api/graphql/ci/jobs_spec.rb
+++ b/spec/requests/api/graphql/ci/jobs_spec.rb
@@ -14,7 +14,7 @@ RSpec.describe 'Query.project.pipeline' do
describe '.stages.groups.jobs' do
let(:pipeline) do
pipeline = create(:ci_pipeline, project: project, user: user)
- stage = create(:ci_stage_entity, project: project, pipeline: pipeline, name: 'first', position: 1)
+ stage = create(:ci_stage, project: project, pipeline: pipeline, name: 'first', position: 1)
create(:ci_build, stage_id: stage.id, pipeline: pipeline, name: 'my test job', scheduling_type: :stage)
pipeline
@@ -84,8 +84,8 @@ RSpec.describe 'Query.project.pipeline' do
context 'when there is more than one stage and job needs' do
before do
- build_stage = create(:ci_stage_entity, position: 2, name: 'build', project: project, pipeline: pipeline)
- test_stage = create(:ci_stage_entity, position: 3, name: 'test', project: project, pipeline: pipeline)
+ build_stage = create(:ci_stage, position: 2, name: 'build', project: project, pipeline: pipeline)
+ test_stage = create(:ci_stage, position: 3, name: 'test', project: project, pipeline: pipeline)
create(:ci_build, pipeline: pipeline, name: 'docker 1 2', scheduling_type: :stage, stage: build_stage, stage_idx: build_stage.position)
create(:ci_build, pipeline: pipeline, name: 'docker 2 2', stage: build_stage, stage_idx: build_stage.position, scheduling_type: :dag)
diff --git a/spec/requests/api/graphql/ci/manual_variables_spec.rb b/spec/requests/api/graphql/ci/manual_variables_spec.rb
new file mode 100644
index 00000000000..b7aa76511a3
--- /dev/null
+++ b/spec/requests/api/graphql/ci/manual_variables_spec.rb
@@ -0,0 +1,95 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Query.project(fullPath).pipelines.jobs.manualVariables' do
+ include GraphqlHelpers
+
+ let_it_be(:project) { create(:project) }
+ let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
+ let_it_be(:user) { create(:user) }
+
+ let(:query) do
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ pipelines {
+ nodes {
+ jobs {
+ nodes {
+ manualVariables {
+ nodes {
+ key
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ )
+ end
+
+ before do
+ project.add_maintainer(user)
+ end
+
+ it 'returns the manual variables for the jobs' do
+ job = create(:ci_build, :manual, pipeline: pipeline)
+ create(:ci_job_variable, key: 'MANUAL_TEST_VAR', job: job)
+
+ post_graphql(query, current_user: user)
+
+ variables_data = graphql_data.dig('project', 'pipelines', 'nodes').first
+ .dig('jobs', 'nodes').flat_map { |job| job.dig('manualVariables', 'nodes') }
+ expect(variables_data.map { |var| var['key'] }).to match_array(['MANUAL_TEST_VAR'])
+ end
+
+ it 'does not fetch job variables for jobs that are not manual' do
+ job = create(:ci_build, pipeline: pipeline)
+ create(:ci_job_variable, key: 'THIS_VAR_WOULD_SHOULD_NEVER_EXIST', job: job)
+
+ post_graphql(query, current_user: user)
+
+ variables_data = graphql_data.dig('project', 'pipelines', 'nodes').first
+ .dig('jobs', 'nodes').flat_map { |job| job.dig('manualVariables', 'nodes') }
+ expect(variables_data).to be_empty
+ end
+
+ it 'does not fetch job variables for bridges' do
+ create(:ci_bridge, :manual, pipeline: pipeline)
+
+ post_graphql(query, current_user: user)
+
+ variables_data = graphql_data.dig('project', 'pipelines', 'nodes').first
+ .dig('jobs', 'nodes').flat_map { |job| job.dig('manualVariables', 'nodes') }
+ expect(variables_data).to be_empty
+ end
+
+ it 'does not produce N+1 queries', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/367991' do
+ second_user = create(:user)
+ project.add_maintainer(second_user)
+ job = create(:ci_build, :manual, pipeline: pipeline)
+ create(:ci_job_variable, key: 'MANUAL_TEST_VAR_1', job: job)
+
+ control_count = ActiveRecord::QueryRecorder.new do
+ post_graphql(query, current_user: user)
+ end
+
+ variables_data = graphql_data.dig('project', 'pipelines', 'nodes').first
+ .dig('jobs', 'nodes').flat_map { |job| job.dig('manualVariables', 'nodes') }
+ expect(variables_data.map { |var| var['key'] }).to match_array(['MANUAL_TEST_VAR_1'])
+
+ job = create(:ci_build, :manual, pipeline: pipeline)
+ create(:ci_job_variable, key: 'MANUAL_TEST_VAR_2', job: job)
+
+ expect do
+ post_graphql(query, current_user: second_user)
+ end.not_to exceed_query_limit(control_count)
+
+ variables_data = graphql_data.dig('project', 'pipelines', 'nodes').first
+ .dig('jobs', 'nodes').flat_map { |job| job.dig('manualVariables', 'nodes') }
+ expect(variables_data.map { |var| var['key'] }).to match_array(%w(MANUAL_TEST_VAR_1 MANUAL_TEST_VAR_2))
+ end
+end
diff --git a/spec/requests/api/graphql/ci/pipelines_spec.rb b/spec/requests/api/graphql/ci/pipelines_spec.rb
index 741af676b6d..a968e5508cb 100644
--- a/spec/requests/api/graphql/ci/pipelines_spec.rb
+++ b/spec/requests/api/graphql/ci/pipelines_spec.rb
@@ -86,8 +86,8 @@ RSpec.describe 'Query.project(fullPath).pipelines' do
describe '.stages' do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:pipeline) { create(:ci_empty_pipeline, project: project) }
- let_it_be(:stage) { create(:ci_stage_entity, pipeline: pipeline, project: project) }
- let_it_be(:other_stage) { create(:ci_stage_entity, pipeline: pipeline, project: project, name: 'other') }
+ let_it_be(:stage) { create(:ci_stage, pipeline: pipeline, project: project) }
+ let_it_be(:other_stage) { create(:ci_stage, pipeline: pipeline, project: project, name: 'other') }
let(:first_n) { var('Int') }
let(:query_path) do
diff --git a/spec/requests/api/graphql/ci/project_variables_spec.rb b/spec/requests/api/graphql/ci/project_variables_spec.rb
new file mode 100644
index 00000000000..e61f146b24c
--- /dev/null
+++ b/spec/requests/api/graphql/ci/project_variables_spec.rb
@@ -0,0 +1,67 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Query.project(fullPath).ciVariables' do
+ include GraphqlHelpers
+
+ let_it_be(:project) { create(:project) }
+ let_it_be(:user) { create(:user) }
+
+ let(:query) do
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ ciVariables {
+ nodes {
+ id
+ key
+ value
+ variableType
+ protected
+ masked
+ raw
+ environmentScope
+ }
+ }
+ }
+ }
+ )
+ end
+
+ context 'when the user can administer builds' do
+ before do
+ project.add_maintainer(user)
+ end
+
+ it "returns the project's CI variables" do
+ variable = create(:ci_variable, project: project, key: 'TEST_VAR', value: 'test',
+ masked: false, protected: true, raw: true, environment_scope: 'production')
+
+ post_graphql(query, current_user: user)
+
+ expect(graphql_data.dig('project', 'ciVariables', 'nodes')).to contain_exactly({
+ 'id' => variable.to_global_id.to_s,
+ 'key' => 'TEST_VAR',
+ 'value' => 'test',
+ 'variableType' => 'ENV_VAR',
+ 'masked' => false,
+ 'protected' => true,
+ 'raw' => true,
+ 'environmentScope' => 'production'
+ })
+ end
+ end
+
+ context 'when the user cannot administer builds' do
+ it 'returns nothing' do
+ create(:ci_variable, project: project, value: 'verysecret', masked: true)
+
+ project.add_developer(user)
+
+ post_graphql(query, current_user: user)
+
+ expect(graphql_data.dig('project', 'ciVariables')).to be_nil
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/ci/runner_spec.rb b/spec/requests/api/graphql/ci/runner_spec.rb
index 446d1fb1bdb..e17a83d8e47 100644
--- a/spec/requests/api/graphql/ci/runner_spec.rb
+++ b/spec/requests/api/graphql/ci/runner_spec.rb
@@ -424,7 +424,7 @@ RSpec.describe 'Query.runner(id)' do
let(:user) { create(:user) }
before do
- group.add_user(user, Gitlab::Access::OWNER)
+ group.add_member(user, Gitlab::Access::OWNER)
end
it_behaves_like 'retrieval with no admin url' do
diff --git a/spec/requests/api/graphql/ci/stages_spec.rb b/spec/requests/api/graphql/ci/stages_spec.rb
index 50d2cf75097..1edd6e58486 100644
--- a/spec/requests/api/graphql/ci/stages_spec.rb
+++ b/spec/requests/api/graphql/ci/stages_spec.rb
@@ -36,7 +36,7 @@ RSpec.describe 'Query.project.pipeline.stages' do
end
before_all do
- create(:ci_stage_entity, pipeline: pipeline, name: 'deploy')
+ create(:ci_stage, pipeline: pipeline, name: 'deploy')
create_list(:ci_build, 2, pipeline: pipeline, stage: 'deploy')
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
index 847fa72522e..14c55e61a65 100644
--- a/spec/requests/api/graphql/container_repository/container_repository_details_spec.rb
+++ b/spec/requests/api/graphql/container_repository/container_repository_details_spec.rb
@@ -71,7 +71,7 @@ RSpec.describe 'container repository details' do
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
+ project.add_member(user, role) unless role == :anonymous
end
it 'return the proper response' do
diff --git a/spec/requests/api/graphql/crm/contacts_spec.rb b/spec/requests/api/graphql/crm/contacts_spec.rb
new file mode 100644
index 00000000000..7e824140894
--- /dev/null
+++ b/spec/requests/api/graphql/crm/contacts_spec.rb
@@ -0,0 +1,69 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'getting CRM contacts' do
+ include GraphqlHelpers
+
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:group) { create(:group, :crm_enabled) }
+
+ let_it_be(:contact_a) do
+ create(
+ :contact,
+ group: group,
+ first_name: "ABC",
+ last_name: "DEF",
+ email: "ghi@test.com",
+ description: "LMNO",
+ state: "inactive"
+ )
+ end
+
+ let_it_be(:contact_b) do
+ create(
+ :contact,
+ group: group,
+ first_name: "ABC",
+ last_name: "DEF",
+ email: "vwx@test.com",
+ description: "YZ",
+ state: "active"
+ )
+ end
+
+ let_it_be(:contact_c) do
+ create(
+ :contact,
+ group: group,
+ first_name: "PQR",
+ last_name: "STU",
+ email: "aaa@test.com",
+ description: "YZ",
+ state: "active"
+ )
+ end
+
+ before do
+ group.add_reporter(current_user)
+ end
+
+ it_behaves_like 'sorted paginated query' do
+ let(:sort_argument) { {} }
+ let(:first_param) { 2 }
+ let(:all_records) { [contact_a, contact_b, contact_c] }
+ let(:data_path) { [:group, :contacts] }
+
+ def pagination_query(params)
+ graphql_query_for(
+ :group,
+ { full_path: group.full_path },
+ query_graphql_field(:contacts, params, "#{page_info} nodes { id }")
+ )
+ end
+
+ def pagination_results_data(nodes)
+ nodes.map { |item| GlobalID::Locator.locate(item['id']) }
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/current_user/groups_query_spec.rb b/spec/requests/api/graphql/current_user/groups_query_spec.rb
index 39f323b21a3..ef0f32bacf0 100644
--- a/spec/requests/api/graphql/current_user/groups_query_spec.rb
+++ b/spec/requests/api/graphql/current_user/groups_query_spec.rb
@@ -8,8 +8,9 @@ RSpec.describe 'Query current user groups' do
let_it_be(:user) { create(:user) }
let_it_be(:guest_group) { create(:group, name: 'public guest', path: 'public-guest') }
let_it_be(:private_maintainer_group) { create(:group, :private, name: 'b private maintainer', path: 'b-private-maintainer') }
- let_it_be(:public_developer_group) { create(:group, :private, project_creation_level: nil, name: 'c public developer', path: 'c-public-developer') }
- let_it_be(:public_maintainer_group) { create(:group, :private, name: 'a public maintainer', path: 'a-public-maintainer') }
+ let_it_be(:public_developer_group) { create(:group, project_creation_level: nil, name: 'c public developer', path: 'c-public-developer') }
+ let_it_be(:public_maintainer_group) { create(:group, name: 'a public maintainer', path: 'a-public-maintainer') }
+ let_it_be(:public_owner_group) { create(:group, name: 'a public owner', path: 'a-public-owner') }
let(:group_arguments) { {} }
let(:current_user) { user }
@@ -29,6 +30,7 @@ RSpec.describe 'Query current user groups' do
private_maintainer_group.add_maintainer(user)
public_developer_group.add_developer(user)
public_maintainer_group.add_maintainer(user)
+ public_owner_group.add_owner(user)
end
subject { graphql_data.dig('currentUser', 'groups', 'nodes') }
@@ -52,6 +54,7 @@ RSpec.describe 'Query current user groups' do
is_expected.to match(
expected_group_hash(
public_maintainer_group,
+ public_owner_group,
private_maintainer_group,
public_developer_group,
guest_group
@@ -66,6 +69,7 @@ RSpec.describe 'Query current user groups' do
is_expected.to match(
expected_group_hash(
public_maintainer_group,
+ public_owner_group,
private_maintainer_group,
public_developer_group
)
@@ -86,6 +90,32 @@ RSpec.describe 'Query current user groups' do
end
end
+ context 'when permission_scope is TRANSFER_PROJECTS' do
+ let(:group_arguments) { { permission_scope: :TRANSFER_PROJECTS } }
+
+ specify do
+ is_expected.to match(
+ expected_group_hash(
+ public_maintainer_group,
+ public_owner_group,
+ private_maintainer_group
+ )
+ )
+ end
+
+ context 'when search is provided' do
+ let(:group_arguments) { { permission_scope: :TRANSFER_PROJECTS, search: 'owner' } }
+
+ specify do
+ is_expected.to match(
+ expected_group_hash(
+ public_owner_group
+ )
+ )
+ end
+ end
+ end
+
context 'when search is provided' do
let(:group_arguments) { { search: 'maintainer' } }
diff --git a/spec/requests/api/graphql/group/container_repositories_spec.rb b/spec/requests/api/graphql/group/container_repositories_spec.rb
index be0b866af4a..8ec321c8d7c 100644
--- a/spec/requests/api/graphql/group/container_repositories_spec.rb
+++ b/spec/requests/api/graphql/group/container_repositories_spec.rb
@@ -82,7 +82,7 @@ RSpec.describe 'getting container repositories in a group' 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
+ group.add_member(user, role) unless role == :anonymous
end
it 'return the proper response' do
diff --git a/spec/requests/api/graphql/group/dependency_proxy_blobs_spec.rb b/spec/requests/api/graphql/group/dependency_proxy_blobs_spec.rb
index cdb21512894..daa1483e956 100644
--- a/spec/requests/api/graphql/group/dependency_proxy_blobs_spec.rb
+++ b/spec/requests/api/graphql/group/dependency_proxy_blobs_spec.rb
@@ -75,7 +75,7 @@ RSpec.describe 'getting dependency proxy blobs in a group' do
with_them do
before do
group.update_column(:visibility_level, Gitlab::VisibilityLevel.const_get(group_visibility.to_s.upcase, false))
- group.add_user(user, role) unless role == :anonymous
+ group.add_member(user, role) unless role == :anonymous
end
it 'return the proper response' do
diff --git a/spec/requests/api/graphql/group/dependency_proxy_group_setting_spec.rb b/spec/requests/api/graphql/group/dependency_proxy_group_setting_spec.rb
index d21c3046c1a..cc706c3051f 100644
--- a/spec/requests/api/graphql/group/dependency_proxy_group_setting_spec.rb
+++ b/spec/requests/api/graphql/group/dependency_proxy_group_setting_spec.rb
@@ -61,7 +61,7 @@ RSpec.describe 'getting dependency proxy settings for a group' do
with_them do
before do
group.update_column(:visibility_level, Gitlab::VisibilityLevel.const_get(group_visibility.to_s.upcase, false))
- group.add_user(user, role) unless role == :anonymous
+ group.add_member(user, role) unless role == :anonymous
end
it 'return the proper response' do
diff --git a/spec/requests/api/graphql/group/dependency_proxy_image_ttl_policy_spec.rb b/spec/requests/api/graphql/group/dependency_proxy_image_ttl_policy_spec.rb
index 40f4b082072..3b2b04b1322 100644
--- a/spec/requests/api/graphql/group/dependency_proxy_image_ttl_policy_spec.rb
+++ b/spec/requests/api/graphql/group/dependency_proxy_image_ttl_policy_spec.rb
@@ -60,7 +60,7 @@ RSpec.describe 'getting dependency proxy image ttl policy for a group' do
with_them do
before do
group.update_column(:visibility_level, Gitlab::VisibilityLevel.const_get(group_visibility.to_s.upcase, false))
- group.add_user(user, role) unless role == :anonymous
+ group.add_member(user, role) unless role == :anonymous
end
it 'return the proper response' do
diff --git a/spec/requests/api/graphql/group/dependency_proxy_manifests_spec.rb b/spec/requests/api/graphql/group/dependency_proxy_manifests_spec.rb
index c7149c100b2..37ef7089c2f 100644
--- a/spec/requests/api/graphql/group/dependency_proxy_manifests_spec.rb
+++ b/spec/requests/api/graphql/group/dependency_proxy_manifests_spec.rb
@@ -73,7 +73,7 @@ RSpec.describe 'getting dependency proxy manifests in a group' do
with_them do
before do
group.update_column(:visibility_level, Gitlab::VisibilityLevel.const_get(group_visibility.to_s.upcase, false))
- group.add_user(user, role) unless role == :anonymous
+ group.add_member(user, role) unless role == :anonymous
end
it 'return the proper response' do
@@ -125,7 +125,8 @@ RSpec.describe 'getting dependency proxy manifests in a group' do
let_it_be(:descending_manifests) { manifests.reverse.map { |manifest| global_id_of(manifest) } }
it_behaves_like 'sorted paginated query' do
- let(:sort_param) { '' }
+ include_context 'no sort argument'
+
let(:first_param) { 2 }
let(:all_records) { descending_manifests.map(&:to_s) }
end
@@ -134,7 +135,7 @@ RSpec.describe 'getting dependency proxy manifests in a group' do
def pagination_query(params)
# remove sort since the type does not accept sorting, but be future proof
graphql_query_for('group', { 'fullPath' => group.full_path },
- query_nodes(:dependencyProxyManifests, :id, include_pagination_info: true, args: params.merge(sort: nil))
+ query_nodes(:dependencyProxyManifests, :id, include_pagination_info: true, args: params)
)
end
end
diff --git a/spec/requests/api/graphql/group/group_members_spec.rb b/spec/requests/api/graphql/group/group_members_spec.rb
index fec866486ae..1ff5b134e92 100644
--- a/spec/requests/api/graphql/group/group_members_spec.rb
+++ b/spec/requests/api/graphql/group/group_members_spec.rb
@@ -7,8 +7,8 @@ RSpec.describe 'getting group members information' do
let_it_be(:parent_group) { create(:group, :public) }
let_it_be(:user) { create(:user) }
- let_it_be(:user_1) { create(:user, username: 'user') }
- let_it_be(:user_2) { create(:user, username: 'test') }
+ let_it_be(:user_1) { create(:user, username: 'user', name: 'Same Name') }
+ let_it_be(:user_2) { create(:user, username: 'test', name: 'Same Name') }
before_all do
[user_1, user_2].each { |user| parent_group.add_guest(user) }
@@ -45,11 +45,44 @@ RSpec.describe 'getting group members information' do
expect_array_response(user_1, user_2)
end
- it 'returns members that match the search query' do
- fetch_members(args: { search: 'test' })
+ describe 'search argument' do
+ it 'returns members that match the search query' do
+ fetch_members(args: { search: 'test' })
- expect(graphql_errors).to be_nil
- expect_array_response(user_2)
+ expect(graphql_errors).to be_nil
+ expect_array_response(user_2)
+ end
+
+ context 'when paginating' do
+ it 'returns correct results' do
+ fetch_members(args: { search: 'Same Name', first: 1 })
+
+ expect_array_response(user_1)
+
+ next_cursor = graphql_data_at(:group, :groupMembers, :pageInfo, :endCursor)
+ fetch_members(args: { search: 'Same Name', first: 1, after: next_cursor })
+
+ expect_array_response(user_2)
+ end
+
+ context 'when the use_keyset_aware_user_search_query FF is off' do
+ before do
+ stub_feature_flags(use_keyset_aware_user_search_query: false)
+ end
+
+ it 'raises error on the 2nd page due to missing cursor data' do
+ fetch_members(args: { search: 'Same Name', first: 1 })
+
+ # user_2 because the "old" order was undeterministic (insert order), no tie-breaker column
+ expect_array_response(user_2)
+
+ next_cursor = graphql_data_at(:group, :groupMembers, :pageInfo, :endCursor)
+ fetch_members(args: { search: 'Same Name', first: 1, after: next_cursor })
+
+ expect(graphql_errors.first['message']).to include('PG::UndefinedColumn')
+ end
+ end
+ end
end
end
@@ -196,6 +229,9 @@ RSpec.describe 'getting group members information' do
}
}
}
+ pageInfo {
+ endCursor
+ }
NODE
graphql_query_for("group",
diff --git a/spec/requests/api/graphql/mutations/issues/create_spec.rb b/spec/requests/api/graphql/mutations/issues/create_spec.rb
index 3d81b456c9c..9345735afe4 100644
--- a/spec/requests/api/graphql/mutations/issues/create_spec.rb
+++ b/spec/requests/api/graphql/mutations/issues/create_spec.rb
@@ -53,6 +53,42 @@ RSpec.describe 'Create an issue' do
let(:mutation_class) { ::Mutations::Issues::Create }
end
+ context 'when creating an issue of type TASK' do
+ before do
+ input['type'] = 'TASK'
+ end
+
+ context 'when work_items feature flag is disabled' do
+ before do
+ stub_feature_flags(work_items: false)
+ end
+
+ it 'creates an issue with the default ISSUE type' do
+ expect do
+ post_graphql_mutation(mutation, current_user: current_user)
+ end.to change(Issue, :count).by(1)
+
+ created_issue = Issue.last
+
+ expect(created_issue.work_item_type.base_type).to eq('issue')
+ expect(created_issue.issue_type).to eq('issue')
+ end
+ end
+
+ context 'when work_items feature flag is enabled' do
+ it 'creates an issue with TASK type' do
+ expect do
+ post_graphql_mutation(mutation, current_user: current_user)
+ end.to change(Issue, :count).by(1)
+
+ created_issue = Issue.last
+
+ expect(created_issue.work_item_type.base_type).to eq('task')
+ expect(created_issue.issue_type).to eq('task')
+ end
+ end
+ end
+
context 'when position params are provided' do
let(:existing_issue) { create(:issue, project: project, relative_position: 50) }
diff --git a/spec/requests/api/graphql/mutations/notes/create/diff_note_spec.rb b/spec/requests/api/graphql/mutations/notes/create/diff_note_spec.rb
index 8f3ae9f26f6..a432fb17a70 100644
--- a/spec/requests/api/graphql/mutations/notes/create/diff_note_spec.rb
+++ b/spec/requests/api/graphql/mutations/notes/create/diff_note_spec.rb
@@ -10,11 +10,12 @@ RSpec.describe 'Adding a DiffNote' do
let(:noteable) { create(:merge_request, source_project: project, target_project: project) }
let(:project) { create(:project, :repository) }
let(:diff_refs) { noteable.diff_refs }
+ let(:body) { 'Body text' }
let(:base_variables) do
{
noteable_id: GitlabSchema.id_from_object(noteable).to_s,
- body: 'Body text',
+ body: body,
position: {
paths: {
old_path: 'files/ruby/popen.rb',
@@ -65,6 +66,17 @@ RSpec.describe 'Adding a DiffNote' do
it_behaves_like 'a Note mutation when the given resource id is not for a Noteable'
end
+ context 'with /merge quick action' do
+ let(:body) { "Body text \n/merge" }
+
+ it 'merges the merge request', :sidekiq_inline do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(noteable.reload.state).to eq('merged')
+ expect(mutation_response['note']['body']).to eq('Body text')
+ end
+ end
+
it 'returns the note with the correct position' do
post_graphql_mutation(mutation, current_user: current_user)
diff --git a/spec/requests/api/graphql/mutations/snippets/update_spec.rb b/spec/requests/api/graphql/mutations/snippets/update_spec.rb
index eb7e6f840fe..1a5d3620f22 100644
--- a/spec/requests/api/graphql/mutations/snippets/update_spec.rb
+++ b/spec/requests/api/graphql/mutations/snippets/update_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
RSpec.describe 'Updating a Snippet' do
include GraphqlHelpers
+ include SessionHelpers
let_it_be(:original_content) { 'Initial content' }
let_it_be(:original_description) { 'Initial description' }
@@ -162,7 +163,7 @@ RSpec.describe 'Updating a Snippet' do
end
end
- context 'when the author is a member of the project' do
+ context 'when the author is a member of the project', :snowplow do
before do
project.add_developer(current_user)
end
@@ -185,6 +186,20 @@ RSpec.describe 'Updating a Snippet' do
it_behaves_like 'has spam protection' do
let(:mutation_class) { ::Mutations::Snippets::Update }
end
+
+ context 'when not sessionless', :clean_gitlab_redis_sessions do
+ before do
+ stub_session('warden.user.user.key' => [[current_user.id], current_user.authenticatable_salt])
+ end
+
+ it_behaves_like 'Snowplow event tracking' do
+ let(:user) { current_user }
+ let(:namespace) { project.namespace }
+ let(:category) { 'ide_edit' }
+ let(:action) { 'g_edit_by_snippet_ide' }
+ let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
+ end
+ end
end
it_behaves_like 'when the snippet is not found'
diff --git a/spec/requests/api/graphql/mutations/work_items/create_from_task_spec.rb b/spec/requests/api/graphql/mutations/work_items/create_from_task_spec.rb
index 8d33f8e1806..b1356bbe6fd 100644
--- a/spec/requests/api/graphql/mutations/work_items/create_from_task_spec.rb
+++ b/spec/requests/api/graphql/mutations/work_items/create_from_task_spec.rb
@@ -47,6 +47,7 @@ RSpec.describe "Create a work item from a task in a work item's description" do
expect(work_item.description).to eq("- [ ] #{created_work_item.to_reference}+")
expect(created_work_item.issue_type).to eq('task')
expect(created_work_item.work_item_type.base_type).to eq('task')
+ expect(created_work_item.work_item_parent).to eq(work_item)
expect(mutation_response['workItem']).to include('id' => work_item.to_global_id.to_s)
expect(mutation_response['newWorkItem']).to include('id' => created_work_item.to_global_id.to_s)
end
diff --git a/spec/requests/api/graphql/mutations/work_items/create_spec.rb b/spec/requests/api/graphql/mutations/work_items/create_spec.rb
index 6abdaa2c850..911568bc39f 100644
--- a/spec/requests/api/graphql/mutations/work_items/create_spec.rb
+++ b/spec/requests/api/graphql/mutations/work_items/create_spec.rb
@@ -63,6 +63,95 @@ RSpec.describe 'Create a work item' do
let(:mutation_class) { ::Mutations::WorkItems::Create }
end
+ context 'with hierarchy widget input' do
+ let(:widgets_response) { mutation_response['workItem']['widgets'] }
+ let(:fields) do
+ <<~FIELDS
+ workItem {
+ widgets {
+ type
+ ... on WorkItemWidgetHierarchy {
+ parent {
+ id
+ }
+ children {
+ edges {
+ node {
+ id
+ }
+ }
+ }
+ }
+ }
+ }
+ errors
+ FIELDS
+ end
+
+ let(:mutation) { graphql_mutation(:workItemCreate, input.merge('projectPath' => project.full_path), fields) }
+
+ context 'when setting parent' do
+ let_it_be(:parent) { create(:work_item, project: project) }
+
+ let(:input) do
+ {
+ title: 'item1',
+ workItemTypeId: WorkItems::Type.default_by_type(:task).to_global_id.to_s,
+ hierarchyWidget: { 'parentId' => parent.to_global_id.to_s }
+ }
+ end
+
+ it 'updates the work item parent' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(widgets_response).to include(
+ {
+ 'children' => { 'edges' => [] },
+ 'parent' => { 'id' => parent.to_global_id.to_s },
+ 'type' => 'HIERARCHY'
+ }
+ )
+ end
+
+ context 'when parent work item type is invalid' do
+ let_it_be(:parent) { create(:work_item, :task, project: project) }
+
+ it 'returns error' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(mutation_response['errors'])
+ .to contain_exactly(/cannot be added: only Issue and Incident can be parent of Task./)
+ expect(mutation_response['workItem']).to be_nil
+ end
+ end
+
+ context 'when parent work item is not found' do
+ let_it_be(:parent) { build_stubbed(:work_item, id: non_existing_record_id)}
+
+ it 'returns a top level error' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(graphql_errors.first['message']).to include('No object found for `parentId')
+ end
+ end
+ end
+
+ context 'when unsupported widget input is sent' do
+ let(:input) do
+ {
+ 'title' => 'new title',
+ 'description' => 'new description',
+ 'workItemTypeId' => WorkItems::Type.default_by_type(:test_case).to_global_id.to_s,
+ 'hierarchyWidget' => {}
+ }
+ end
+
+ it_behaves_like 'a mutation that returns top-level errors',
+ errors: ['Following widget keys are not supported by Test Case type: [:hierarchy_widget]']
+ end
+ end
+
context 'when the work_items feature flag is disabled' do
before do
stub_feature_flags(work_items: false)
diff --git a/spec/requests/api/graphql/mutations/work_items/delete_task_spec.rb b/spec/requests/api/graphql/mutations/work_items/delete_task_spec.rb
index 05d3587d342..e576d0ee7ef 100644
--- a/spec/requests/api/graphql/mutations/work_items/delete_task_spec.rb
+++ b/spec/requests/api/graphql/mutations/work_items/delete_task_spec.rb
@@ -54,7 +54,7 @@ RSpec.describe "Delete a task in a work item's description" do
end.to change(WorkItem, :count).by(-1).and(
change(IssueLink, :count).by(-1)
).and(
- change(work_item, :description).from("- [ ] #{task.to_reference}+").to('')
+ change(work_item, :description).from("- [ ] #{task.to_reference}+").to("- [ ] #{task.title}")
)
expect(response).to have_gitlab_http_status(:success)
diff --git a/spec/requests/api/graphql/mutations/work_items/update_spec.rb b/spec/requests/api/graphql/mutations/work_items/update_spec.rb
index 71b03103115..77f7b9bacef 100644
--- a/spec/requests/api/graphql/mutations/work_items/update_spec.rb
+++ b/spec/requests/api/graphql/mutations/work_items/update_spec.rb
@@ -11,8 +11,17 @@ RSpec.describe 'Update a work item' do
let(:work_item_event) { 'CLOSE' }
let(:input) { { 'stateEvent' => work_item_event, 'title' => 'updated title' } }
+ let(:fields) do
+ <<~FIELDS
+ workItem {
+ state
+ title
+ }
+ errors
+ FIELDS
+ end
- let(:mutation) { graphql_mutation(:workItemUpdate, input.merge('id' => work_item.to_global_id.to_s)) }
+ let(:mutation) { graphql_mutation(:workItemUpdate, input.merge('id' => work_item.to_global_id.to_s), fields) }
let(:mutation_response) { graphql_mutation_response(:work_item_update) }
@@ -62,6 +71,20 @@ RSpec.describe 'Update a work item' do
end
end
+ context 'when unsupported widget input is sent' do
+ let_it_be(:test_case) { create(:work_item_type, :default, :test_case, name: 'some_test_case_name') }
+ let_it_be(:work_item) { create(:work_item, work_item_type: test_case, project: project) }
+
+ let(:input) do
+ {
+ 'hierarchyWidget' => {}
+ }
+ end
+
+ it_behaves_like 'a mutation that returns top-level errors',
+ errors: ["Following widget keys are not supported by some_test_case_name type: [:hierarchy_widget]"]
+ end
+
it_behaves_like 'has spam protection' do
let(:mutation_class) { ::Mutations::WorkItems::Update }
end
@@ -80,5 +103,248 @@ RSpec.describe 'Update a work item' do
expect(mutation_response['errors']).to contain_exactly('`work_items` feature flag disabled for this project')
end
end
+
+ context 'with description widget input' do
+ let(:fields) do
+ <<~FIELDS
+ workItem {
+ description
+ widgets {
+ type
+ ... on WorkItemWidgetDescription {
+ description
+ }
+ }
+ }
+ errors
+ FIELDS
+ end
+
+ it_behaves_like 'update work item description widget' do
+ let(:new_description) { 'updated description' }
+ let(:input) do
+ { 'descriptionWidget' => { 'description' => new_description } }
+ end
+ end
+ end
+
+ context 'with weight widget input' do
+ let(:fields) do
+ <<~FIELDS
+ workItem {
+ widgets {
+ type
+ ... on WorkItemWidgetWeight {
+ weight
+ }
+ }
+ }
+ errors
+ FIELDS
+ end
+
+ it_behaves_like 'update work item weight widget' do
+ let(:new_weight) { 2 }
+
+ let(:input) do
+ { 'weightWidget' => { 'weight' => new_weight } }
+ end
+ end
+ end
+
+ context 'with hierarchy widget input' do
+ let(:widgets_response) { mutation_response['workItem']['widgets'] }
+ let(:fields) do
+ <<~FIELDS
+ workItem {
+ description
+ widgets {
+ type
+ ... on WorkItemWidgetHierarchy {
+ parent {
+ id
+ }
+ children {
+ edges {
+ node {
+ id
+ }
+ }
+ }
+ }
+ }
+ }
+ errors
+ FIELDS
+ end
+
+ context 'when updating parent' do
+ let_it_be(:work_item) { create(:work_item, :task, project: project) }
+ let_it_be(:valid_parent) { create(:work_item, project: project) }
+ let_it_be(:invalid_parent) { create(:work_item, :task, project: project) }
+
+ context 'when parent work item type is invalid' do
+ let(:error) { "#{work_item.to_reference} cannot be added: only Issue and Incident can be parent of Task." }
+ let(:input) do
+ { 'hierarchyWidget' => { 'parentId' => invalid_parent.to_global_id.to_s }, 'title' => 'new title' }
+ end
+
+ it 'returns response with errors' do
+ expect do
+ post_graphql_mutation(mutation, current_user: current_user)
+ work_item.reload
+ end.to not_change(work_item, :work_item_parent).and(not_change(work_item, :title))
+
+ expect(mutation_response['workItem']).to be_nil
+ expect(mutation_response['errors']).to match_array([error])
+ end
+ end
+
+ context 'when parent work item has a valid type' do
+ let(:input) { { 'hierarchyWidget' => { 'parentId' => valid_parent.to_global_id.to_s } } }
+
+ it 'sets the parent for the work item' do
+ expect do
+ post_graphql_mutation(mutation, current_user: current_user)
+ work_item.reload
+ end.to change(work_item, :work_item_parent).from(nil).to(valid_parent)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(widgets_response).to include(
+ {
+ 'children' => { 'edges' => [] },
+ 'parent' => { 'id' => valid_parent.to_global_id.to_s },
+ 'type' => 'HIERARCHY'
+ }
+ )
+ end
+
+ context 'when a parent is already present' do
+ let_it_be(:existing_parent) { create(:work_item, project: project) }
+
+ before do
+ work_item.update!(work_item_parent: existing_parent)
+ end
+
+ it 'is replaced with new parent' do
+ expect do
+ post_graphql_mutation(mutation, current_user: current_user)
+ work_item.reload
+ end.to change(work_item, :work_item_parent).from(existing_parent).to(valid_parent)
+ end
+ end
+ end
+
+ context 'when parentId is null' do
+ let(:input) { { 'hierarchyWidget' => { 'parentId' => nil } } }
+
+ context 'when parent is present' do
+ before do
+ work_item.update!(work_item_parent: valid_parent)
+ end
+
+ it 'removes parent and returns success message' do
+ expect do
+ post_graphql_mutation(mutation, current_user: current_user)
+ work_item.reload
+ end.to change(work_item, :work_item_parent).from(valid_parent).to(nil)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(widgets_response)
+ .to include(
+ {
+ 'children' => { 'edges' => [] },
+ 'parent' => nil,
+ 'type' => 'HIERARCHY'
+ }
+ )
+ end
+ end
+
+ context 'when parent is not present' do
+ before do
+ work_item.update!(work_item_parent: nil)
+ end
+
+ it 'does not change work item and returns success message' do
+ expect do
+ post_graphql_mutation(mutation, current_user: current_user)
+ work_item.reload
+ end.not_to change(work_item, :work_item_parent)
+
+ expect(response).to have_gitlab_http_status(:success)
+ end
+ end
+ end
+
+ context 'when parent work item is not found' do
+ let(:input) { { 'hierarchyWidget' => { 'parentId' => "gid://gitlab/WorkItem/#{non_existing_record_id}" } } }
+
+ it 'returns a top level error' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(graphql_errors.first['message']).to include('No object found for `parentId')
+ end
+ end
+ end
+
+ context 'when updating children' do
+ let_it_be(:valid_child1) { create(:work_item, :task, project: project) }
+ let_it_be(:valid_child2) { create(:work_item, :task, project: project) }
+ let_it_be(:invalid_child) { create(:work_item, project: project) }
+
+ let(:input) { { 'hierarchyWidget' => { 'childrenIds' => children_ids } } }
+ let(:error) do
+ "#{invalid_child.to_reference} cannot be added: only Task can be assigned as a child in hierarchy."
+ end
+
+ context 'when child work item type is invalid' do
+ let(:children_ids) { [invalid_child.to_global_id.to_s] }
+
+ it 'returns response with errors' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(mutation_response['workItem']).to be_nil
+ expect(mutation_response['errors']).to match_array([error])
+ end
+ end
+
+ context 'when there is a mix of existing and non existing work items' do
+ let(:children_ids) { [valid_child1.to_global_id.to_s, "gid://gitlab/WorkItem/#{non_existing_record_id}"] }
+
+ it 'returns a top level error and does not add valid work item' do
+ expect do
+ post_graphql_mutation(mutation, current_user: current_user)
+ work_item.reload
+ end.not_to change(work_item.work_item_children, :count)
+
+ expect(graphql_errors.first['message']).to include('No object found for `childrenIds')
+ end
+ end
+
+ context 'when child work item type is valid' do
+ let(:children_ids) { [valid_child1.to_global_id.to_s, valid_child2.to_global_id.to_s] }
+
+ it 'updates the work item children' do
+ expect do
+ post_graphql_mutation(mutation, current_user: current_user)
+ work_item.reload
+ end.to change(work_item.work_item_children, :count).by(2)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(widgets_response).to include(
+ {
+ 'children' => { 'edges' => [
+ { 'node' => { 'id' => valid_child2.to_global_id.to_s } },
+ { 'node' => { 'id' => valid_child1.to_global_id.to_s } }
+ ] },
+ 'parent' => nil,
+ 'type' => 'HIERARCHY'
+ }
+ )
+ end
+ end
+ end
+ end
end
end
diff --git a/spec/requests/api/graphql/mutations/work_items/update_widgets_spec.rb b/spec/requests/api/graphql/mutations/work_items/update_widgets_spec.rb
index 595d8fe97ed..2a5cb937a2f 100644
--- a/spec/requests/api/graphql/mutations/work_items/update_widgets_spec.rb
+++ b/spec/requests/api/graphql/mutations/work_items/update_widgets_spec.rb
@@ -9,16 +9,23 @@ RSpec.describe 'Update work item widgets' do
let_it_be(:developer) { create(:user).tap { |user| project.add_developer(user) } }
let_it_be(:work_item, refind: true) { create(:work_item, project: project) }
- let(:input) do
- {
- 'descriptionWidget' => { 'description' => 'updated description' }
+ let(:input) { { 'descriptionWidget' => { 'description' => 'updated description' } } }
+ let(:mutation_response) { graphql_mutation_response(:work_item_update_widgets) }
+ let(:mutation) do
+ graphql_mutation(:workItemUpdateWidgets, input.merge('id' => work_item.to_global_id.to_s), <<~FIELDS)
+ errors
+ workItem {
+ description
+ widgets {
+ type
+ ... on WorkItemWidgetDescription {
+ description
+ }
+ }
}
+ FIELDS
end
- let(:mutation) { graphql_mutation(:workItemUpdateWidgets, input.merge('id' => work_item.to_global_id.to_s)) }
-
- let(:mutation_response) { graphql_mutation_response(:work_item_update_widgets) }
-
context 'the user is not allowed to update a work item' do
let(:current_user) { create(:user) }
@@ -28,32 +35,8 @@ RSpec.describe 'Update work item widgets' do
context 'when user has permissions to update a work item', :aggregate_failures do
let(:current_user) { developer }
- context 'when the updated work item is not valid' do
- it 'returns validation errors without the work item' do
- errors = ActiveModel::Errors.new(work_item).tap { |e| e.add(:description, 'error message') }
-
- allow_next_found_instance_of(::WorkItem) do |instance|
- allow(instance).to receive(:valid?).and_return(false)
- allow(instance).to receive(:errors).and_return(errors)
- end
-
- post_graphql_mutation(mutation, current_user: current_user)
-
- expect(mutation_response['workItem']).to be_nil
- expect(mutation_response['errors']).to match_array(['Description error message'])
- end
- end
-
- it 'updates the work item widgets' do
- expect do
- post_graphql_mutation(mutation, current_user: current_user)
- work_item.reload
- end.to change(work_item, :description).from(nil).to('updated description')
-
- expect(response).to have_gitlab_http_status(:success)
- expect(mutation_response['workItem']).to include(
- 'title' => work_item.title
- )
+ it_behaves_like 'update work item description widget' do
+ let(:new_description) { 'updated description' }
end
it_behaves_like 'has spam protection' do
@@ -69,7 +52,7 @@ RSpec.describe 'Update work item widgets' do
expect do
post_graphql_mutation(mutation, current_user: current_user)
work_item.reload
- end.to not_change(work_item, :title)
+ end.to not_change(work_item, :description)
expect(mutation_response['errors']).to contain_exactly('`work_items` feature flag disabled for this project')
end
diff --git a/spec/requests/api/graphql/project/container_repositories_spec.rb b/spec/requests/api/graphql/project/container_repositories_spec.rb
index bbab6012f3f..01b117a89d8 100644
--- a/spec/requests/api/graphql/project/container_repositories_spec.rb
+++ b/spec/requests/api/graphql/project/container_repositories_spec.rb
@@ -81,7 +81,7 @@ RSpec.describe 'getting container repositories in a project' do
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
+ project.add_member(user, role) unless role == :anonymous
end
it 'return the proper response' do
diff --git a/spec/requests/api/graphql/project/issues_spec.rb b/spec/requests/api/graphql/project/issues_spec.rb
index 69e14eace66..596e023a027 100644
--- a/spec/requests/api/graphql/project/issues_spec.rb
+++ b/spec/requests/api/graphql/project/issues_spec.rb
@@ -223,6 +223,7 @@ RSpec.describe 'getting an issue list for a project' do
end
describe 'sorting and pagination' do
+ let_it_be(:sort_project) { create(:project, :public) }
let_it_be(:data_path) { [:project, :issues] }
def pagination_query(params)
@@ -237,8 +238,38 @@ RSpec.describe 'getting an issue list for a project' do
data.map { |issue| issue['iid'].to_i }
end
+ context 'when sorting by severity' do
+ let_it_be(:severty_issue1) { create(:issue, project: sort_project) }
+ let_it_be(:severty_issue2) { create(:issue, project: sort_project) }
+ let_it_be(:severty_issue3) { create(:issue, project: sort_project) }
+ let_it_be(:severty_issue4) { create(:issue, project: sort_project) }
+ let_it_be(:severty_issue5) { create(:issue, project: sort_project) }
+
+ before(:all) do
+ create(:issuable_severity, issue: severty_issue1, severity: :unknown)
+ create(:issuable_severity, issue: severty_issue2, severity: :low)
+ create(:issuable_severity, issue: severty_issue4, severity: :critical)
+ create(:issuable_severity, issue: severty_issue5, severity: :high)
+ end
+
+ context 'when ascending' do
+ it_behaves_like 'sorted paginated query' do
+ let(:sort_param) { :SEVERITY_ASC }
+ let(:first_param) { 2 }
+ let(:all_records) { [severty_issue3.iid, severty_issue1.iid, severty_issue2.iid, severty_issue5.iid, severty_issue4.iid] }
+ end
+ end
+
+ context 'when descending' do
+ it_behaves_like 'sorted paginated query' do
+ let(:sort_param) { :SEVERITY_DESC }
+ let(:first_param) { 2 }
+ let(:all_records) { [severty_issue4.iid, severty_issue5.iid, severty_issue2.iid, severty_issue1.iid, severty_issue3.iid] }
+ end
+ end
+ end
+
context 'when sorting by due date' do
- let_it_be(:sort_project) { create(:project, :public) }
let_it_be(:due_issue1) { create(:issue, project: sort_project, due_date: 3.days.from_now) }
let_it_be(:due_issue2) { create(:issue, project: sort_project, due_date: nil) }
let_it_be(:due_issue3) { create(:issue, project: sort_project, due_date: 2.days.ago) }
@@ -263,7 +294,6 @@ RSpec.describe 'getting an issue list for a project' do
end
context 'when sorting by relative position' do
- let_it_be(:sort_project) { create(:project, :public) }
let_it_be(:relative_issue1) { create(:issue, project: sort_project, relative_position: 2000) }
let_it_be(:relative_issue2) { create(:issue, project: sort_project, relative_position: nil) }
let_it_be(:relative_issue3) { create(:issue, project: sort_project, relative_position: 1000) }
@@ -285,7 +315,6 @@ RSpec.describe 'getting an issue list for a project' do
end
context 'when sorting by priority' do
- let_it_be(:sort_project) { create(:project, :public) }
let_it_be(:on_project) { { project: sort_project } }
let_it_be(:early_milestone) { create(:milestone, **on_project, due_date: 10.days.from_now) }
let_it_be(:late_milestone) { create(:milestone, **on_project, due_date: 30.days.from_now) }
@@ -321,7 +350,6 @@ RSpec.describe 'getting an issue list for a project' do
end
context 'when sorting by label priority' do
- let_it_be(:sort_project) { create(:project, :public) }
let_it_be(:label1) { create(:label, project: sort_project, priority: 1) }
let_it_be(:label2) { create(:label, project: sort_project, priority: 5) }
let_it_be(:label3) { create(:label, project: sort_project, priority: 10) }
@@ -348,7 +376,6 @@ RSpec.describe 'getting an issue list for a project' do
end
context 'when sorting by milestone due date' do
- let_it_be(:sort_project) { create(:project, :public) }
let_it_be(:early_milestone) { create(:milestone, project: sort_project, due_date: 10.days.from_now) }
let_it_be(:late_milestone) { create(:milestone, project: sort_project, due_date: 30.days.from_now) }
let_it_be(:milestone_issue1) { create(:issue, project: sort_project) }
diff --git a/spec/requests/api/graphql/project/jobs_spec.rb b/spec/requests/api/graphql/project/jobs_spec.rb
index 1a823ede9ac..7d0eb203d60 100644
--- a/spec/requests/api/graphql/project/jobs_spec.rb
+++ b/spec/requests/api/graphql/project/jobs_spec.rb
@@ -31,8 +31,8 @@ RSpec.describe 'Query.project.jobs' do
end
it 'does not generate N+1 queries', :request_store, :use_sql_query_cache do
- build_stage = create(:ci_stage_entity, position: 1, name: 'build', project: project, pipeline: pipeline)
- test_stage = create(:ci_stage_entity, position: 2, name: 'test', project: project, pipeline: pipeline)
+ build_stage = create(:ci_stage, position: 1, name: 'build', project: project, pipeline: pipeline)
+ test_stage = create(:ci_stage, position: 2, name: 'test', project: project, pipeline: pipeline)
create(:ci_build, pipeline: pipeline, stage_idx: build_stage.position, name: 'docker 1 2', stage: build_stage)
create(:ci_build, pipeline: pipeline, stage_idx: build_stage.position, name: 'docker 2 2', stage: build_stage)
create(:ci_build, pipeline: pipeline, stage_idx: test_stage.position, name: 'rspec 1 2', stage: test_stage)
diff --git a/spec/requests/api/graphql/project/packages_cleanup_policy_spec.rb b/spec/requests/api/graphql/project/packages_cleanup_policy_spec.rb
index a025c57d4b8..33e1dbcba27 100644
--- a/spec/requests/api/graphql/project/packages_cleanup_policy_spec.rb
+++ b/spec/requests/api/graphql/project/packages_cleanup_policy_spec.rb
@@ -61,7 +61,7 @@ RSpec.describe 'getting the packages cleanup policy linked to a project' do
with_them do
before do
project.update!(visibility: visibility.to_s)
- project.add_user(current_user, role) unless role == :anonymous
+ project.add_member(current_user, role) unless role == :anonymous
end
it 'return the proper response' do
diff --git a/spec/requests/api/graphql/project/pipeline_spec.rb b/spec/requests/api/graphql/project/pipeline_spec.rb
index ccf97918021..08c6a2d9927 100644
--- a/spec/requests/api/graphql/project/pipeline_spec.rb
+++ b/spec/requests/api/graphql/project/pipeline_spec.rb
@@ -105,6 +105,62 @@ RSpec.describe 'getting pipeline information nested in a project' do
end
end
+ context 'when a job has been retried' do
+ let_it_be(:retried) do
+ create(:ci_build, :retried,
+ name: build_job.name,
+ pipeline: pipeline,
+ stage_idx: 0,
+ stage: build_job.stage)
+ end
+
+ let(:fields) do
+ query_graphql_field(:jobs, { retried: retried_argument },
+ query_graphql_field(:nodes, {}, all_graphql_fields_for('CiJob', max_depth: 3)))
+ end
+
+ context 'when we filter out retried jobs' do
+ let(:retried_argument) { false }
+
+ it 'contains latest jobs' do
+ post_graphql(query, current_user: current_user)
+
+ expect(graphql_data_at(*path, :jobs, :nodes)).to include(
+ a_graphql_entity_for(build_job, :name, :duration, :retried)
+ )
+
+ expect(graphql_data_at(*path, :jobs, :nodes)).not_to include(
+ a_graphql_entity_for(retried)
+ )
+ end
+ end
+
+ context 'when we filter to only retried jobs' do
+ let(:retried_argument) { true }
+
+ it 'contains only retried jobs' do
+ post_graphql(query, current_user: current_user)
+
+ expect(graphql_data_at(*path, :jobs, :nodes)).to contain_exactly(
+ a_graphql_entity_for(retried)
+ )
+ end
+ end
+
+ context 'when we pass null explicitly' do
+ let(:retried_argument) { nil }
+
+ it 'contains all jobs' do
+ post_graphql(query, current_user: current_user)
+
+ expect(graphql_data_at(*path, :jobs, :nodes)).to include(
+ a_graphql_entity_for(build_job),
+ a_graphql_entity_for(retried)
+ )
+ end
+ end
+ end
+
context 'when requesting only builds with certain statuses' do
let(:variables) do
{
@@ -290,8 +346,8 @@ RSpec.describe 'getting pipeline information nested in a project' do
end
it 'does not generate N+1 queries', :request_store, :use_sql_query_cache do
- build_stage = create(:ci_stage_entity, position: 1, name: 'build', project: project, pipeline: pipeline)
- test_stage = create(:ci_stage_entity, position: 2, name: 'test', project: project, pipeline: pipeline)
+ build_stage = create(:ci_stage, position: 1, name: 'build', project: project, pipeline: pipeline)
+ test_stage = create(:ci_stage, position: 2, name: 'test', project: project, pipeline: pipeline)
create(:ci_build, pipeline: pipeline, stage_idx: build_stage.position, name: 'docker 1 2', stage: build_stage)
create(:ci_build, pipeline: pipeline, stage_idx: build_stage.position, name: 'docker 2 2', stage: build_stage)
create(:ci_build, pipeline: pipeline, stage_idx: test_stage.position, name: 'rspec 1 2', stage: test_stage)
diff --git a/spec/requests/api/graphql/project/project_members_spec.rb b/spec/requests/api/graphql/project/project_members_spec.rb
index c3281b44954..4225c3ad3e8 100644
--- a/spec/requests/api/graphql/project/project_members_spec.rb
+++ b/spec/requests/api/graphql/project/project_members_spec.rb
@@ -8,8 +8,8 @@ RSpec.describe 'getting project members information' do
let_it_be(:parent_group) { create(:group, :public) }
let_it_be(:parent_project) { create(:project, :public, group: parent_group) }
let_it_be(:user) { create(:user) }
- let_it_be(:user_1) { create(:user, username: 'user') }
- let_it_be(:user_2) { create(:user, username: 'test') }
+ let_it_be(:user_1) { create(:user, username: 'user', name: 'Same Name') }
+ let_it_be(:user_2) { create(:user, username: 'test', name: 'Same Name') }
before_all do
[user_1, user_2].each { |user| parent_group.add_guest(user) }
@@ -29,11 +29,44 @@ RSpec.describe 'getting project members information' do
expect_array_response(user_1, user_2)
end
- it 'returns members that match the search query' do
- fetch_members(project: parent_project, args: { search: 'test' })
+ describe 'search argument' do
+ it 'returns members that match the search query' do
+ fetch_members(project: parent_project, args: { search: 'test' })
- expect(graphql_errors).to be_nil
- expect_array_response(user_2)
+ expect(graphql_errors).to be_nil
+ expect_array_response(user_2)
+ end
+
+ context 'when paginating' do
+ it 'returns correct results' do
+ fetch_members(project: parent_project, args: { search: 'Same Name', first: 1 })
+
+ expect_array_response(user_1)
+
+ next_cursor = graphql_data_at(:project, :projectMembers, :pageInfo, :endCursor)
+ fetch_members(project: parent_project, args: { search: 'Same Name', first: 1, after: next_cursor })
+
+ expect_array_response(user_2)
+ end
+
+ context 'when the use_keyset_aware_user_search_query FF is off' do
+ before do
+ stub_feature_flags(use_keyset_aware_user_search_query: false)
+ end
+
+ it 'raises error on the 2nd page due to missing cursor data' do
+ fetch_members(project: parent_project, args: { search: 'Same Name', first: 1 })
+
+ # user_2 because the "old" order was undeterministic (insert order), no tie-breaker column
+ expect_array_response(user_2)
+
+ next_cursor = graphql_data_at(:project, :projectMembers, :pageInfo, :endCursor)
+ fetch_members(project: parent_project, args: { search: 'Same Name', first: 1, after: next_cursor })
+
+ expect(graphql_errors.first['message']).to include('PG::UndefinedColumn')
+ end
+ end
+ end
end
end
@@ -231,6 +264,9 @@ RSpec.describe 'getting project members information' do
}
}
}
+ pageInfo {
+ endCursor
+ }
NODE
graphql_query_for('project',
diff --git a/spec/requests/api/graphql/todo_query_spec.rb b/spec/requests/api/graphql/todo_query_spec.rb
new file mode 100644
index 00000000000..3f743f4402a
--- /dev/null
+++ b/spec/requests/api/graphql/todo_query_spec.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Todo Query' do
+ include GraphqlHelpers
+
+ let_it_be(:current_user) { nil }
+ let_it_be(:project) { create(:project) }
+ let_it_be(:issue) { create(:issue, project: project) }
+
+ let_it_be(:todo_owner) { create(:user) }
+
+ let_it_be(:todo) { create(:todo, user: todo_owner, target: project) }
+
+ before do
+ project.add_developer(todo_owner)
+ end
+
+ let(:fields) do
+ <<~GRAPHQL
+ id
+ GRAPHQL
+ end
+
+ let(:query) do
+ graphql_query_for(:todo, { id: todo.to_global_id.to_s }, fields)
+ end
+
+ subject do
+ result = GitlabSchema.execute(query, context: { current_user: current_user }).to_h
+ graphql_dig_at(result, :data, :todo)
+ end
+
+ context 'when requesting user is todo owner' do
+ let(:current_user) { todo_owner }
+
+ it { is_expected.to include('id' => todo.to_global_id.to_s) }
+ end
+
+ context 'when requesting user is not todo owner' do
+ let(:current_user) { create(:user) }
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'when unauthenticated' do
+ it { is_expected.to be_nil }
+ end
+end
diff --git a/spec/requests/api/graphql/work_item_spec.rb b/spec/requests/api/graphql/work_item_spec.rb
index 09bda8ee0d5..f17d2ebbb7e 100644
--- a/spec/requests/api/graphql/work_item_spec.rb
+++ b/spec/requests/api/graphql/work_item_spec.rb
@@ -8,7 +8,7 @@ RSpec.describe 'Query.work_item(id)' do
let_it_be(:developer) { create(:user) }
let_it_be(:guest) { create(:user) }
let_it_be(:project) { create(:project, :private) }
- let_it_be(:work_item) { create(:work_item, project: project, description: '- List item') }
+ let_it_be(:work_item) { create(:work_item, project: project, description: '- List item', weight: 1) }
let_it_be(:child_item1) { create(:work_item, :task, project: project) }
let_it_be(:child_item2) { create(:work_item, :task, confidential: true, project: project) }
let_it_be(:child_link1) { create(:parent_link, work_item_parent: work_item, work_item: child_item1) }
@@ -64,16 +64,13 @@ RSpec.describe 'Query.work_item(id)' do
it 'returns widget information' do
expect(work_item_data).to include(
'id' => work_item.to_gid.to_s,
- 'widgets' => match_array([
+ 'widgets' => include(
hash_including(
'type' => 'DESCRIPTION',
'description' => work_item.description,
'descriptionHtml' => ::MarkupHelper.markdown_field(work_item, :description, {})
- ),
- hash_including(
- 'type' => 'HIERARCHY'
)
- ])
+ )
)
end
end
@@ -101,10 +98,7 @@ RSpec.describe 'Query.work_item(id)' do
it 'returns widget information' do
expect(work_item_data).to include(
'id' => work_item.to_gid.to_s,
- 'widgets' => match_array([
- hash_including(
- 'type' => 'DESCRIPTION'
- ),
+ 'widgets' => include(
hash_including(
'type' => 'HIERARCHY',
'parent' => nil,
@@ -113,7 +107,7 @@ RSpec.describe 'Query.work_item(id)' do
hash_including('id' => child_link2.work_item.to_gid.to_s)
]) }
)
- ])
+ )
)
end
@@ -137,10 +131,7 @@ RSpec.describe 'Query.work_item(id)' do
it 'filters out not accessible children or parent' do
expect(work_item_data).to include(
'id' => work_item.to_gid.to_s,
- 'widgets' => match_array([
- hash_including(
- 'type' => 'DESCRIPTION'
- ),
+ 'widgets' => include(
hash_including(
'type' => 'HIERARCHY',
'parent' => nil,
@@ -148,7 +139,7 @@ RSpec.describe 'Query.work_item(id)' do
hash_including('id' => child_link1.work_item.to_gid.to_s)
]) }
)
- ])
+ )
)
end
end
@@ -160,20 +151,85 @@ RSpec.describe 'Query.work_item(id)' do
it 'returns parent information' do
expect(work_item_data).to include(
'id' => work_item.to_gid.to_s,
- 'widgets' => match_array([
- hash_including(
- 'type' => 'DESCRIPTION'
- ),
+ 'widgets' => include(
hash_including(
'type' => 'HIERARCHY',
'parent' => hash_including('id' => parent_link.work_item_parent.to_gid.to_s),
'children' => { 'nodes' => match_array([]) }
)
- ])
+ )
)
end
end
end
+
+ describe 'weight widget' do
+ let(:work_item_fields) do
+ <<~GRAPHQL
+ id
+ widgets {
+ type
+ ... on WorkItemWidgetWeight {
+ weight
+ }
+ }
+ GRAPHQL
+ end
+
+ it 'returns widget information' do
+ expect(work_item_data).to include(
+ 'id' => work_item.to_gid.to_s,
+ 'widgets' => include(
+ hash_including(
+ 'type' => 'WEIGHT',
+ 'weight' => work_item.weight
+ )
+ )
+ )
+ end
+ end
+
+ describe 'assignees widget' do
+ let(:assignees) { create_list(:user, 2) }
+ let(:work_item) { create(:work_item, project: project, assignees: assignees) }
+
+ let(:work_item_fields) do
+ <<~GRAPHQL
+ id
+ widgets {
+ type
+ ... on WorkItemWidgetAssignees {
+ allowsMultipleAssignees
+ canInviteMembers
+ assignees {
+ nodes {
+ id
+ username
+ }
+ }
+ }
+ }
+ GRAPHQL
+ end
+
+ it 'returns widget information' do
+ expect(work_item_data).to include(
+ 'id' => work_item.to_gid.to_s,
+ 'widgets' => include(
+ hash_including(
+ 'type' => 'ASSIGNEES',
+ 'allowsMultipleAssignees' => boolean,
+ 'canInviteMembers' => boolean,
+ 'assignees' => {
+ 'nodes' => match_array(
+ assignees.map { |a| { 'id' => a.to_gid.to_s, 'username' => a.username } }
+ )
+ }
+ )
+ )
+ )
+ end
+ end
end
context 'when an Issue Global ID is provided' do
diff --git a/spec/requests/api/group_export_spec.rb b/spec/requests/api/group_export_spec.rb
index ffa313d4464..bda46f85140 100644
--- a/spec/requests/api/group_export_spec.rb
+++ b/spec/requests/api/group_export_spec.rb
@@ -32,9 +32,9 @@ RSpec.describe API::GroupExport do
context 'when export file exists' do
before do
- allow(Gitlab::ApplicationRateLimiter)
- .to receive(:increment)
- .and_return(0)
+ allow_next_instance_of(Gitlab::ApplicationRateLimiter::BaseStrategy) do |strategy|
+ allow(strategy).to receive(:increment).and_return(0)
+ end
upload.export_file = fixture_file_upload('spec/fixtures/group_export.tar.gz', "`/tar.gz")
upload.save!
@@ -149,9 +149,11 @@ RSpec.describe API::GroupExport do
before do
group.add_owner(user)
- allow(Gitlab::ApplicationRateLimiter)
- .to receive(:increment)
- .and_return(Gitlab::ApplicationRateLimiter.rate_limits[:group_export][:threshold].call + 1)
+ allow_next_instance_of(Gitlab::ApplicationRateLimiter::BaseStrategy) do |strategy|
+ allow(strategy)
+ .to receive(:increment)
+ .and_return(Gitlab::ApplicationRateLimiter.rate_limits[:group_export][:threshold].call + 1)
+ end
end
it 'throttles the endpoint' do
diff --git a/spec/requests/api/group_variables_spec.rb b/spec/requests/api/group_variables_spec.rb
index 6d5676bbe35..a7b4bea362f 100644
--- a/spec/requests/api/group_variables_spec.rb
+++ b/spec/requests/api/group_variables_spec.rb
@@ -10,7 +10,7 @@ RSpec.describe API::GroupVariables do
let(:access_level) {}
before do
- group.add_user(user, access_level) if access_level
+ group.add_member(user, access_level) if access_level
end
describe 'GET /groups/:id/variables' do
diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb
index 56f08249bdd..3bc3cce5310 100644
--- a/spec/requests/api/groups_spec.rb
+++ b/spec/requests/api/groups_spec.rb
@@ -645,7 +645,7 @@ RSpec.describe API::Groups do
project = create(:project, namespace: group2, path: 'Foo')
create(:project_group_link, project: project, group: group1)
- get api("/groups/#{group1.id}", user1), params: { with_projects: false }
+ get api("/groups/#{group2.id}", user1), params: { with_projects: false }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['projects']).to be_nil
@@ -748,6 +748,18 @@ RSpec.describe API::Groups do
expect(json_response).to include('runners_token')
end
+ it "returns runners_token and no projects when with_projects option is set to false" do
+ project = create(:project, namespace: group2, path: 'Foo')
+ create(:project_group_link, project: project, group: group1)
+
+ get api("/groups/#{group2.id}", admin), params: { with_projects: false }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['projects']).to be_nil
+ expect(json_response['shared_projects']).to be_nil
+ expect(json_response).to include('runners_token')
+ end
+
it "does not return a non existing group" do
get api("/groups/#{non_existing_record_id}", admin)
diff --git a/spec/requests/api/integrations_spec.rb b/spec/requests/api/integrations_spec.rb
index cd9a0746581..b2db7f7caef 100644
--- a/spec/requests/api/integrations_spec.rb
+++ b/spec/requests/api/integrations_spec.rb
@@ -55,25 +55,20 @@ RSpec.describe API::Integrations do
describe "PUT /projects/:id/#{endpoint}/#{integration.dasherize}" do
include_context integration
- # NOTE: Some attributes are not supported for PUT requests, even though in most cases they should be.
- # For some of them the problem is somewhere else, i.e. most chat integrations don't support the `*_channel`
- # fields but they're incorrectly included in `#fields`.
- #
+ # NOTE: Some attributes are not supported for PUT requests, even though they probably should be.
# We can fix these manually, or with a generic approach like https://gitlab.com/gitlab-org/gitlab/-/issues/348208
- let(:missing_channel_attributes) { %i[push_channel issue_channel confidential_issue_channel merge_request_channel note_channel confidential_note_channel tag_push_channel pipeline_channel wiki_page_channel] }
let(:missing_attributes) do
{
datadog: %i[archive_trace_events],
- discord: missing_channel_attributes + %i[branches_to_be_notified notify_only_broken_pipelines],
- hangouts_chat: missing_channel_attributes + %i[notify_only_broken_pipelines],
+ discord: %i[branches_to_be_notified notify_only_broken_pipelines],
+ hangouts_chat: %i[notify_only_broken_pipelines],
jira: %i[issues_enabled project_key vulnerabilities_enabled vulnerabilities_issuetype],
mattermost: %i[deployment_channel labels_to_be_notified],
- microsoft_teams: missing_channel_attributes,
mock_ci: %i[enable_ssl_verification],
prometheus: %i[manual_configuration],
slack: %i[alert_events alert_channel deployment_channel labels_to_be_notified],
- unify_circuit: missing_channel_attributes + %i[branches_to_be_notified notify_only_broken_pipelines],
- webex_teams: missing_channel_attributes + %i[branches_to_be_notified notify_only_broken_pipelines]
+ unify_circuit: %i[branches_to_be_notified notify_only_broken_pipelines],
+ webex_teams: %i[branches_to_be_notified notify_only_broken_pipelines]
}
end
@@ -368,6 +363,31 @@ RSpec.describe API::Integrations do
end
end
+ describe 'Jira integration' do
+ let(:integration_name) { 'jira' }
+ let(:params) do
+ { url: 'https://jira.example.com', username: 'username', password: 'password' }
+ end
+
+ before do
+ project.create_jira_integration(active: true, properties: params)
+ end
+
+ it 'returns the jira_issue_transition_id for get request' do
+ get api("/projects/#{project.id}/#{endpoint}/#{integration_name}", user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['properties']).to include('jira_issue_transition_id' => nil)
+ end
+
+ it 'returns the jira_issue_transition_id for put request' do
+ put api("/projects/#{project.id}/#{endpoint}/#{integration_name}", user), params: params.merge(jira_issue_transition_id: '1')
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['properties']['jira_issue_transition_id']).to eq('1')
+ end
+ end
+
describe 'Pipelines Email Integration' do
let(:integration_name) { 'pipelines-email' }
diff --git a/spec/requests/api/internal/base_spec.rb b/spec/requests/api/internal/base_spec.rb
index 93e4e72f78f..acfe476a864 100644
--- a/spec/requests/api/internal/base_spec.rb
+++ b/spec/requests/api/internal/base_spec.rb
@@ -51,64 +51,6 @@ RSpec.describe API::Internal::Base do
end
end
- describe 'GET /internal/error_tracking_allowed' do
- let_it_be(:project) { create(:project) }
-
- let(:params) { { project_id: project.id, public_key: 'key' } }
-
- context 'when the secret header is missing' do
- it 'responds with unauthorized entity' do
- post api("/internal/error_tracking_allowed"), params: params
-
- expect(response).to have_gitlab_http_status(:unauthorized)
- end
- end
-
- context 'when some params are missing' do
- it 'responds with unprocessable entity' do
- post api("/internal/error_tracking_allowed"), params: params.except(:public_key),
- headers: { API::Helpers::GITLAB_SHARED_SECRET_HEADER => Base64.encode64(secret_token) }
-
- expect(response).to have_gitlab_http_status(:unprocessable_entity)
- end
- end
-
- context 'when the error tracking is disabled' do
- it 'returns enabled: false' do
- create(:error_tracking_client_key, project: project, active: false)
-
- post api("/internal/error_tracking_allowed"), params: params,
- headers: { API::Helpers::GITLAB_SHARED_SECRET_HEADER => Base64.encode64(secret_token) }
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response).to eq({ 'enabled' => false })
- end
-
- context 'when the error tracking record does not exist' do
- it 'returns enabled: false' do
- post api("/internal/error_tracking_allowed"), params: params,
- headers: { API::Helpers::GITLAB_SHARED_SECRET_HEADER => Base64.encode64(secret_token) }
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response).to eq({ 'enabled' => false })
- end
- end
- end
-
- context 'when the error tracking is enabled' do
- it 'returns enabled: true' do
- client_key = create(:error_tracking_client_key, project: project, active: true)
- params[:public_key] = client_key.public_key
-
- post api("/internal/error_tracking_allowed"), params: params,
- headers: { API::Helpers::GITLAB_SHARED_SECRET_HEADER => Base64.encode64(secret_token) }
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response).to eq({ 'enabled' => true })
- end
- end
- end
-
describe 'GET /internal/two_factor_recovery_codes' do
let(:key_id) { key.id }
diff --git a/spec/requests/api/internal/error_tracking_spec.rb b/spec/requests/api/internal/error_tracking_spec.rb
new file mode 100644
index 00000000000..69eb54d5ed2
--- /dev/null
+++ b/spec/requests/api/internal/error_tracking_spec.rb
@@ -0,0 +1,108 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe API::Internal::ErrorTracking do
+ let(:secret_token) { Gitlab::CurrentSettings.error_tracking_access_token }
+ let(:headers) do
+ { ::API::Internal::ErrorTracking::GITLAB_ERROR_TRACKING_TOKEN_HEADER => Base64.encode64(secret_token) }
+ end
+
+ describe 'GET /internal/error_tracking/allowed' do
+ let_it_be(:project) { create(:project) }
+
+ let(:params) { { project_id: project.id, public_key: 'key' } }
+
+ subject(:send_request) do
+ post api('/internal/error_tracking/allowed'), params: params, headers: headers
+ end
+
+ before do
+ # Because the feature flag is disabled in specs we have to enable it explicitly.
+ stub_feature_flags(use_click_house_database_for_error_tracking: true)
+ stub_feature_flags(gitlab_error_tracking: true)
+ end
+
+ context 'when the secret header is missing' do
+ let(:headers) { {} }
+
+ it 'responds with unauthorized entity' do
+ send_request
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+ end
+
+ context 'when some params are missing' do
+ let(:params) { { project_id: project.id } }
+
+ it 'responds with unprocessable entity' do
+ send_request
+
+ expect(response).to have_gitlab_http_status(:unprocessable_entity)
+ end
+ end
+
+ context 'when public_key is unknown' do
+ it 'returns enabled: false' do
+ send_request
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to eq('enabled' => false)
+ end
+ end
+
+ context 'when unknown project_id is unknown' do
+ it 'responds with 404 not found' do
+ params[:project_id] = non_existing_record_id
+
+ send_request
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'when the error tracking is disabled' do
+ it 'returns enabled: false' do
+ create(:error_tracking_client_key, :disabled, project: project)
+
+ send_request
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to eq('enabled' => false)
+ end
+ end
+
+ context 'when the error tracking is enabled' do
+ let_it_be(:client_key) { create(:error_tracking_client_key, project: project) }
+
+ before do
+ params[:public_key] = client_key.public_key
+
+ stub_application_setting(error_tracking_enabled: true)
+ stub_application_setting(error_tracking_api_url: 'https://localhost/error_tracking')
+ end
+
+ it 'returns enabled: true' do
+ send_request
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to eq('enabled' => true)
+ end
+
+ context 'when feature flags use_click_house_database_for_error_tracking or gitlab_error_tracking are disabled' do
+ before do
+ stub_feature_flags(use_click_house_database_for_error_tracking: false)
+ stub_feature_flags(gitlab_error_tracking: false)
+ end
+
+ it 'returns enabled: false' do
+ send_request
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to eq('enabled' => false)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/internal/kubernetes_spec.rb b/spec/requests/api/internal/kubernetes_spec.rb
index 0e566dd8c0e..c0a979995c9 100644
--- a/spec/requests/api/internal/kubernetes_spec.rb
+++ b/spec/requests/api/internal/kubernetes_spec.rb
@@ -169,12 +169,12 @@ RSpec.describe API::Internal::Kubernetes do
'features' => {}
),
'gitaly_repository' => a_hash_including(
- 'default_branch' => project.default_branch_or_main,
'storage_name' => project.repository_storage,
'relative_path' => project.disk_path + '.git',
'gl_repository' => "project-#{project.id}",
'gl_project_path' => project.full_path
- )
+ ),
+ 'default_branch' => project.default_branch_or_main
)
)
end
diff --git a/spec/requests/api/invitations_spec.rb b/spec/requests/api/invitations_spec.rb
index 64ad5733c1b..53154aef21e 100644
--- a/spec/requests/api/invitations_spec.rb
+++ b/spec/requests/api/invitations_spec.rb
@@ -69,6 +69,20 @@ RSpec.describe API::Invitations do
end
end
+ context 'when invitee is already an invited member' do
+ it 'updates the member for that email' do
+ member = source.add_developer(email)
+
+ expect do
+ post invitations_url(source, maintainer),
+ params: { email: email, access_level: Member::MAINTAINER }
+
+ expect(response).to have_gitlab_http_status(:created)
+ end.to change { member.reset.access_level }.from(Member::DEVELOPER).to(Member::MAINTAINER)
+ .and not_change { source.members.invite.count }
+ end
+ end
+
it 'adds a new member by email' do
expect do
post invitations_url(source, maintainer),
@@ -320,7 +334,7 @@ RSpec.describe API::Invitations do
let(:source) { project }
end
- it 'records queries', :request_store, :use_sql_query_cache do
+ it 'does not exceed expected queries count for emails', :request_store, :use_sql_query_cache do
post invitations_url(project, maintainer), params: { email: email, access_level: Member::DEVELOPER }
control = ActiveRecord::QueryRecorder.new(skip_cached: false) do
@@ -336,7 +350,25 @@ RSpec.describe API::Invitations do
end.not_to exceed_all_query_limit(control.count).with_threshold(unresolved_n_plus_ones)
end
- it 'records queries with secondary emails', :request_store, :use_sql_query_cache do
+ it 'does not exceed expected queries count for user_ids', :request_store, :use_sql_query_cache do
+ stranger2 = create(:user)
+
+ post invitations_url(project, maintainer), params: { user_id: stranger.id, access_level: Member::DEVELOPER }
+
+ control = ActiveRecord::QueryRecorder.new(skip_cached: false) do
+ post invitations_url(project, maintainer), params: { user_id: stranger2.id, access_level: Member::DEVELOPER }
+ end
+
+ users = create_list(:user, 5)
+
+ unresolved_n_plus_ones = 136 # 54 for 1 vs 190 for 5 - currently there are 34 queries added per user
+
+ expect do
+ post invitations_url(project, maintainer), params: { user_id: users.map(&:id).join(','), access_level: Member::DEVELOPER }
+ end.not_to exceed_all_query_limit(control.count).with_threshold(unresolved_n_plus_ones)
+ end
+
+ it 'does not exceed expected queries count with secondary emails', :request_store, :use_sql_query_cache do
create(:email, email: email, user: create(:user))
post invitations_url(project, maintainer), params: { email: email, access_level: Member::DEVELOPER }
@@ -365,7 +397,7 @@ RSpec.describe API::Invitations do
let(:source) { group }
end
- it 'records queries', :request_store, :use_sql_query_cache do
+ it 'does not exceed expected queries count for emails', :request_store, :use_sql_query_cache do
post invitations_url(group, maintainer), params: { email: email, access_level: Member::DEVELOPER }
control = ActiveRecord::QueryRecorder.new(skip_cached: false) do
@@ -381,7 +413,7 @@ RSpec.describe API::Invitations do
end.not_to exceed_all_query_limit(control.count).with_threshold(unresolved_n_plus_ones)
end
- it 'records queries with secondary emails', :request_store, :use_sql_query_cache do
+ it 'does not exceed expected queries count for secondary emails', :request_store, :use_sql_query_cache do
create(:email, email: email, user: create(:user))
post invitations_url(group, maintainer), params: { email: email, access_level: Member::DEVELOPER }
diff --git a/spec/requests/api/issues/issues_spec.rb b/spec/requests/api/issues/issues_spec.rb
index 480baff6eed..dd7d32f3565 100644
--- a/spec/requests/api/issues/issues_spec.rb
+++ b/spec/requests/api/issues/issues_spec.rb
@@ -20,6 +20,7 @@ RSpec.describe API::Issues do
let_it_be(:milestone) { create(:milestone, title: '1.0.0', project: project) }
let_it_be(:empty_milestone) { create(:milestone, title: '2.0.0', project: project) }
+ let_it_be(:task) { create(:issue, :task, author: user, project: project) }
let_it_be(:closed_issue) do
create :closed_issue,
@@ -1151,19 +1152,6 @@ RSpec.describe API::Issues do
expected_url = expose_url(api_v4_project_issue_path(id: new_issue.project_id, issue_iid: new_issue.iid))
expect(json_response.dig('_links', 'closed_as_duplicate_of')).to eq(expected_url)
end
-
- context 'feature flag is disabled' do
- before do
- stub_feature_flags(closed_as_duplicate_of_issues_api: false)
- end
-
- it 'does not return the issue as closed_as_duplicate_of' do
- get api("/projects/#{project.id}/issues/#{issue_closed_as_dup.iid}", user)
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response.dig('_links', 'closed_as_duplicate_of')).to eq(nil)
- end
- end
end
end
end
diff --git a/spec/requests/api/markdown_snapshot_spec.rb b/spec/requests/api/markdown_snapshot_spec.rb
index 37607a4e866..1270efdfd6f 100644
--- a/spec/requests/api/markdown_snapshot_spec.rb
+++ b/spec/requests/api/markdown_snapshot_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
# See https://docs.gitlab.com/ee/development/gitlab_flavored_markdown/specification_guide/#markdown-snapshot-testing
# for documentation on this spec.
RSpec.describe API::Markdown, 'Snapshot' do
+ # noinspection RubyMismatchedArgumentType (ignore RBS type warning: __dir__ can be nil, but 2nd argument can't be nil)
glfm_specification_dir = File.expand_path('../../../glfm_specification', __dir__)
- glfm_example_snapshots_dir = File.expand_path('../../fixtures/glfm/example_snapshots', __dir__)
- include_context 'with API::Markdown Snapshot shared context', glfm_specification_dir, glfm_example_snapshots_dir
+ include_context 'with API::Markdown Snapshot shared context', glfm_specification_dir
end
diff --git a/spec/requests/api/maven_packages_spec.rb b/spec/requests/api/maven_packages_spec.rb
index bc325aad823..ba82d2facc6 100644
--- a/spec/requests/api/maven_packages_spec.rb
+++ b/spec/requests/api/maven_packages_spec.rb
@@ -226,14 +226,26 @@ RSpec.describe API::MavenPackages do
end
end
+ shared_examples 'file download in FIPS mode' do
+ context 'in FIPS mode', :fips_mode do
+ it_behaves_like 'successfully returning the file'
+
+ it 'rejects the request for an md5 file' do
+ download_file(file_name: package_file.file_name + '.md5')
+
+ expect(response).to have_gitlab_http_status(:unprocessable_entity)
+ end
+ end
+ end
+
describe 'GET /api/v4/packages/maven/*path/:file_name' do
context 'a public project' do
subject { download_file(file_name: package_file.file_name) }
shared_examples 'getting a file' do
it_behaves_like 'tracking the file download event'
-
it_behaves_like 'successfully returning the file'
+ it_behaves_like 'file download in FIPS mode'
it 'returns sha1 of the file' do
download_file(file_name: package_file.file_name + '.sha1')
@@ -402,8 +414,8 @@ RSpec.describe API::MavenPackages do
shared_examples 'getting a file for a group' do
it_behaves_like 'tracking the file download event'
-
it_behaves_like 'successfully returning the file'
+ it_behaves_like 'file download in FIPS mode'
it 'returns sha1 of the file' do
download_file(file_name: package_file.file_name + '.sha1')
@@ -625,8 +637,8 @@ RSpec.describe API::MavenPackages do
subject { download_file(file_name: package_file.file_name) }
it_behaves_like 'tracking the file download event'
-
it_behaves_like 'successfully returning the file'
+ it_behaves_like 'file download in FIPS mode'
it 'returns sha1 of the file' do
download_file(file_name: package_file.file_name + '.sha1')
@@ -833,6 +845,16 @@ RSpec.describe API::MavenPackages do
subject { upload_file_with_token(params: params) }
+ context 'FIPS mode', :fips_mode do
+ it_behaves_like 'package workhorse uploads'
+
+ it 'rejects the request for md5 file' do
+ upload_file_with_token(params: params, file_extension: 'jar.md5')
+
+ expect(response).to have_gitlab_http_status(:unprocessable_entity)
+ end
+ end
+
context 'file size is too large' do
it 'rejects the request' do
allow_next_instance_of(UploadedFile) do |uploaded_file|
@@ -995,12 +1017,22 @@ RSpec.describe API::MavenPackages do
end
context 'for md5 file' do
+ subject { upload_file_with_token(params: params, file_extension: 'jar.md5') }
+
it 'returns an empty body' do
- upload_file_with_token(params: params, file_extension: 'jar.md5')
+ subject
expect(response.body).to eq('')
expect(response).to have_gitlab_http_status(:ok)
end
+
+ context 'with FIPS mode enabled', :fips_mode do
+ it 'rejects the request' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:unprocessable_entity)
+ end
+ end
end
end
diff --git a/spec/requests/api/metadata_spec.rb b/spec/requests/api/metadata_spec.rb
new file mode 100644
index 00000000000..dbca06b7f3e
--- /dev/null
+++ b/spec/requests/api/metadata_spec.rb
@@ -0,0 +1,94 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe API::Metadata do
+ shared_examples_for 'GET /metadata' do
+ context 'when unauthenticated' do
+ it 'returns authentication error' do
+ get api('/metadata')
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+ end
+
+ context 'when authenticated as user' do
+ let(:user) { create(:user) }
+
+ it 'returns the metadata information' do
+ get api('/metadata', user)
+
+ expect_metadata
+ end
+ end
+
+ context 'when authenticated with token' do
+ let(:personal_access_token) { create(:personal_access_token, scopes: scopes) }
+
+ context 'with api scope' do
+ let(:scopes) { %i(api) }
+
+ it 'returns the metadata information' do
+ get api('/metadata', personal_access_token: personal_access_token)
+
+ expect_metadata
+ end
+
+ it 'returns "200" response on head requests' do
+ head api('/metadata', personal_access_token: personal_access_token)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+
+ context 'with read_user scope' do
+ let(:scopes) { %i(read_user) }
+
+ it 'returns the metadata information' do
+ get api('/metadata', personal_access_token: personal_access_token)
+
+ expect_metadata
+ end
+
+ it 'returns "200" response on head requests' do
+ head api('/metadata', personal_access_token: personal_access_token)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+
+ context 'with neither api nor read_user scope' do
+ let(:scopes) { %i(read_repository) }
+
+ it 'returns authorization error' do
+ get api('/metadata', personal_access_token: personal_access_token)
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+ end
+
+ def expect_metadata
+ aggregate_failures("testing response") do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to match_response_schema('public_api/v4/metadata')
+ end
+ end
+ end
+
+ context 'with graphql enabled' do
+ before do
+ stub_feature_flags(graphql: true)
+ end
+
+ include_examples 'GET /metadata'
+ end
+
+ context 'with graphql disabled' do
+ before do
+ stub_feature_flags(graphql: false)
+ end
+
+ include_examples 'GET /metadata'
+ end
+end
diff --git a/spec/requests/api/npm_project_packages_spec.rb b/spec/requests/api/npm_project_packages_spec.rb
index 7c3f1890095..62809b432af 100644
--- a/spec/requests/api/npm_project_packages_spec.rb
+++ b/spec/requests/api/npm_project_packages_spec.rb
@@ -30,6 +30,7 @@ RSpec.describe API::NpmProjectPackages do
end
describe 'GET /api/v4/projects/:id/packages/npm/*package_name/-/*file_name' do
+ let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace } }
let(:package_file) { package.package_files.first }
let(:headers) { {} }
@@ -61,18 +62,18 @@ RSpec.describe API::NpmProjectPackages do
let(:headers) { build_token_auth_header(token.token) }
it_behaves_like 'successfully downloads the file'
+ it_behaves_like 'a package tracking event', 'API::NpmPackages', 'pull_package'
end
context 'with job token' do
let(:headers) { build_token_auth_header(job.token) }
it_behaves_like 'successfully downloads the file'
+ it_behaves_like 'a package tracking event', 'API::NpmPackages', 'pull_package'
end
end
context 'a public project' do
- let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace } }
-
it_behaves_like 'successfully downloads the file'
it_behaves_like 'a package tracking event', 'API::NpmPackages', 'pull_package'
@@ -112,6 +113,15 @@ RSpec.describe API::NpmProjectPackages do
end
it_behaves_like 'a package file that requires auth'
+
+ context 'with a job token for a different user' do
+ let_it_be(:other_user) { create(:user) }
+ let_it_be_with_reload(:other_job) { create(:ci_build, :running, user: other_user) }
+
+ let(:headers) { build_token_auth_header(other_job.token) }
+
+ it_behaves_like 'successfully downloads the file'
+ end
end
end
diff --git a/spec/requests/api/project_attributes.yml b/spec/requests/api/project_attributes.yml
index 35844631287..8d3622ca17d 100644
--- a/spec/requests/api/project_attributes.yml
+++ b/spec/requests/api/project_attributes.yml
@@ -100,6 +100,7 @@ ci_cd_settings:
forward_deployment_enabled: ci_forward_deployment_enabled
job_token_scope_enabled: ci_job_token_scope_enabled
separated_caches: ci_separated_caches
+ opt_in_jwt: ci_opt_in_jwt
build_import_state: # import_state
unexposed_attributes:
@@ -123,6 +124,11 @@ project_feature:
- created_at
- metrics_dashboard_access_level
- package_registry_access_level
+ - monitor_access_level
+ - infrastructure_access_level
+ - feature_flags_access_level
+ - environments_access_level
+ - releases_access_level
- project_id
- updated_at
computed_attributes:
diff --git a/spec/requests/api/project_export_spec.rb b/spec/requests/api/project_export_spec.rb
index 8a8cd8512f8..d74fd82ca09 100644
--- a/spec/requests/api/project_export_spec.rb
+++ b/spec/requests/api/project_export_spec.rb
@@ -248,9 +248,10 @@ RSpec.describe API::ProjectExport, :clean_gitlab_redis_cache do
let(:request) { get api(download_path, admin) }
before do
- allow(Gitlab::ApplicationRateLimiter)
- .to receive(:increment)
- .and_return(Gitlab::ApplicationRateLimiter.rate_limits[:project_download_export][:threshold].call + 1)
+ allow_next_instance_of(Gitlab::ApplicationRateLimiter::BaseStrategy) do |strategy|
+ threshold = Gitlab::ApplicationRateLimiter.rate_limits[:project_download_export][:threshold].call
+ allow(strategy).to receive(:increment).and_return(threshold + 1)
+ end
end
it 'prevents requesting project export' do
@@ -433,9 +434,10 @@ RSpec.describe API::ProjectExport, :clean_gitlab_redis_cache do
context 'when rate limit is exceeded across projects' do
before do
- allow(Gitlab::ApplicationRateLimiter)
- .to receive(:increment)
- .and_return(Gitlab::ApplicationRateLimiter.rate_limits[:project_export][:threshold].call + 1)
+ allow_next_instance_of(Gitlab::ApplicationRateLimiter::BaseStrategy) do |strategy|
+ threshold = Gitlab::ApplicationRateLimiter.rate_limits[:project_export][:threshold].call
+ allow(strategy).to receive(:increment).and_return(threshold + 1)
+ end
end
it 'prevents requesting project export' do
diff --git a/spec/requests/api/project_hooks_spec.rb b/spec/requests/api/project_hooks_spec.rb
index 26e0adc11b3..2d925620a91 100644
--- a/spec/requests/api/project_hooks_spec.rb
+++ b/spec/requests/api/project_hooks_spec.rb
@@ -3,10 +3,10 @@
require 'spec_helper'
RSpec.describe API::ProjectHooks, 'ProjectHooks' do
- let(:user) { create(:user) }
- let(:user3) { create(:user) }
- let!(:project) { create(:project, creator_id: user.id, namespace: user.namespace) }
- let!(:hook) do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:user3) { create(:user) }
+ let_it_be(:project) { create(:project, creator_id: user.id, namespace: user.namespace) }
+ let_it_be_with_refind(:hook) do
create(:project_hook,
:all_events_enabled,
project: project,
@@ -15,232 +15,55 @@ RSpec.describe API::ProjectHooks, 'ProjectHooks' do
push_events_branch_filter: 'master')
end
- before do
+ before_all do
project.add_maintainer(user)
project.add_developer(user3)
end
- describe "GET /projects/:id/hooks" do
- context "authorized user" do
- it "returns project hooks" do
- get api("/projects/#{project.id}/hooks", user)
+ it_behaves_like 'web-hook API endpoints', '/projects/:id' do
+ let(:unauthorized_user) { user3 }
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response).to be_an Array
- expect(response).to include_pagination_headers
- expect(json_response.count).to eq(1)
- expect(json_response.first['url']).to eq("http://example.com")
- expect(json_response.first['issues_events']).to eq(true)
- expect(json_response.first['confidential_issues_events']).to eq(true)
- expect(json_response.first['push_events']).to eq(true)
- expect(json_response.first['merge_requests_events']).to eq(true)
- expect(json_response.first['tag_push_events']).to eq(true)
- expect(json_response.first['note_events']).to eq(true)
- expect(json_response.first['confidential_note_events']).to eq(true)
- expect(json_response.first['job_events']).to eq(true)
- 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')
- expect(json_response.first['alert_status']).to eq('executable')
- expect(json_response.first['disabled_until']).to be_nil
- end
+ def scope
+ project.hooks
end
- context "unauthorized user" do
- it "does not access project hooks" do
- get api("/projects/#{project.id}/hooks", user3)
-
- expect(response).to have_gitlab_http_status(:forbidden)
- end
- end
- end
-
- describe "GET /projects/:id/hooks/:hook_id" do
- context "authorized user" do
- it "returns a project hook" do
- get api("/projects/#{project.id}/hooks/#{hook.id}", user)
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response['url']).to eq(hook.url)
- expect(json_response['issues_events']).to eq(hook.issues_events)
- expect(json_response['confidential_issues_events']).to eq(hook.confidential_issues_events)
- expect(json_response['push_events']).to eq(hook.push_events)
- expect(json_response['merge_requests_events']).to eq(hook.merge_requests_events)
- expect(json_response['tag_push_events']).to eq(hook.tag_push_events)
- expect(json_response['note_events']).to eq(hook.note_events)
- expect(json_response['confidential_note_events']).to eq(hook.confidential_note_events)
- 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)
- expect(json_response['alert_status']).to eq(hook.alert_status.to_s)
- expect(json_response['disabled_until']).to be_nil
- end
-
- it "returns a 404 error if hook id is not available" do
- get api("/projects/#{project.id}/hooks/#{non_existing_record_id}", user)
-
- expect(response).to have_gitlab_http_status(:not_found)
- end
- end
-
- context "unauthorized user" do
- it "does not access an existing hook" do
- get api("/projects/#{project.id}/hooks/#{hook.id}", user3)
- expect(response).to have_gitlab_http_status(:forbidden)
- end
- end
- end
-
- describe "POST /projects/:id/hooks" do
- it "adds hook to project" do
- expect 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, releases_events: true,
- push_events_branch_filter: 'some-feature-branch' })
- end.to change {project.hooks.count}.by(1)
-
- expect(response).to have_gitlab_http_status(:created)
- expect(json_response['url']).to eq('http://example.com')
- expect(json_response['issues_events']).to eq(true)
- expect(json_response['confidential_issues_events']).to eq(true)
- expect(json_response['push_events']).to eq(true)
- expect(json_response['merge_requests_events']).to eq(false)
- expect(json_response['tag_push_events']).to eq(false)
- expect(json_response['note_events']).to eq(false)
- expect(json_response['confidential_note_events']).to eq(nil)
- expect(json_response['job_events']).to eq(true)
- 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')
+ def collection_uri
+ "/projects/#{project.id}/hooks"
end
- it "adds the token without including it in the response" do
- token = "secret token"
-
- expect do
- post api("/projects/#{project.id}/hooks", user), params: { url: "http://example.com", token: token }
- end.to change {project.hooks.count}.by(1)
-
- expect(response).to have_gitlab_http_status(:created)
- expect(json_response["url"]).to eq("http://example.com")
- expect(json_response).not_to include("token")
-
- hook = project.hooks.find(json_response["id"])
-
- expect(hook.url).to eq("http://example.com")
- expect(hook.token).to eq(token)
+ def match_collection_schema
+ match_response_schema('public_api/v4/project_hooks')
end
- it "returns a 400 error if url not given" do
- post api("/projects/#{project.id}/hooks", user)
- expect(response).to have_gitlab_http_status(:bad_request)
+ def hook_uri(hook_id = hook.id)
+ "/projects/#{project.id}/hooks/#{hook_id}"
end
- it "returns a 422 error if url not valid" do
- post api("/projects/#{project.id}/hooks", user), params: { url: "ftp://example.com" }
- expect(response).to have_gitlab_http_status(:unprocessable_entity)
+ def match_hook_schema
+ match_response_schema('public_api/v4/project_hook')
end
- it "returns a 422 error if branch filter is not valid" do
- post api("/projects/#{project.id}/hooks", user), params: { url: "http://example.com", push_events_branch_filter: '~badbranchname/' }
- expect(response).to have_gitlab_http_status(:unprocessable_entity)
+ def event_names
+ %i[
+ push_events
+ tag_push_events
+ merge_requests_events
+ issues_events
+ confidential_issues_events
+ note_events
+ confidential_note_events
+ pipeline_events
+ wiki_page_events
+ job_events
+ deployment_events
+ releases_events
+ ]
end
- end
-
- describe "PUT /projects/:id/hooks/:hook_id" do
- it "updates an existing project hook" do
- put api("/projects/#{project.id}/hooks/#{hook.id}", user),
- params: { url: 'http://example.org', push_events: false, job_events: true }
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response['url']).to eq('http://example.org')
- expect(json_response['issues_events']).to eq(hook.issues_events)
- expect(json_response['confidential_issues_events']).to eq(hook.confidential_issues_events)
- expect(json_response['push_events']).to eq(false)
- expect(json_response['merge_requests_events']).to eq(hook.merge_requests_events)
- expect(json_response['tag_push_events']).to eq(hook.tag_push_events)
- expect(json_response['note_events']).to eq(hook.note_events)
- expect(json_response['confidential_note_events']).to eq(hook.confidential_note_events)
- 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)
+ let(:default_values) do
+ { push_events: true, confidential_note_events: nil }
end
- it "adds the token without including it in the response" do
- token = "secret token"
-
- put api("/projects/#{project.id}/hooks/#{hook.id}", user), params: { url: "http://example.org", token: token }
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response["url"]).to eq("http://example.org")
- expect(json_response).not_to include("token")
-
- expect(hook.reload.url).to eq("http://example.org")
- expect(hook.reload.token).to eq(token)
- end
-
- it "returns 404 error if hook id not found" do
- put api("/projects/#{project.id}/hooks/#{non_existing_record_id}", user), params: { url: 'http://example.org' }
- expect(response).to have_gitlab_http_status(:not_found)
- end
-
- it "returns 400 error if url is not given" do
- put api("/projects/#{project.id}/hooks/#{hook.id}", user)
- expect(response).to have_gitlab_http_status(:bad_request)
- end
-
- it "returns a 422 error if url is not valid" do
- put api("/projects/#{project.id}/hooks/#{hook.id}", user), params: { url: 'ftp://example.com' }
- expect(response).to have_gitlab_http_status(:unprocessable_entity)
- end
- end
-
- describe "DELETE /projects/:id/hooks/:hook_id" do
- it "deletes hook from project" do
- expect do
- delete api("/projects/#{project.id}/hooks/#{hook.id}", user)
-
- expect(response).to have_gitlab_http_status(:no_content)
- end.to change {project.hooks.count}.by(-1)
- end
-
- it "returns a 404 error when deleting non existent hook" do
- delete api("/projects/#{project.id}/hooks/42", user)
- expect(response).to have_gitlab_http_status(:not_found)
- end
-
- it "returns a 404 error if hook id not given" do
- delete api("/projects/#{project.id}/hooks", user)
-
- expect(response).to have_gitlab_http_status(:not_found)
- end
-
- it "returns a 404 if a user attempts to delete project hooks they do not own" do
- test_user = create(:user)
- other_project = create(:project)
- other_project.add_maintainer(test_user)
-
- delete api("/projects/#{other_project.id}/hooks/#{hook.id}", test_user)
- expect(response).to have_gitlab_http_status(:not_found)
- expect(WebHook.exists?(hook.id)).to be_truthy
- end
-
- it_behaves_like '412 response' do
- let(:request) { api("/projects/#{project.id}/hooks/#{hook.id}", user) }
- end
+ it_behaves_like 'web-hook API endpoints with branch-filter', '/projects/:id'
end
end
diff --git a/spec/requests/api/project_import_spec.rb b/spec/requests/api/project_import_spec.rb
index 7e6d80c047c..8655e5b0238 100644
--- a/spec/requests/api/project_import_spec.rb
+++ b/spec/requests/api/project_import_spec.rb
@@ -462,6 +462,16 @@ RSpec.describe API::ProjectImport, :aggregate_failures do
expect(json_response).to include('import_status' => 'failed',
'import_error' => 'error')
end
+
+ it 'returns the import status if canceled' do
+ project = create(:project, :import_canceled)
+ project.add_maintainer(user)
+
+ get api("/projects/#{project.id}/import", user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to include('import_status' => 'canceled')
+ end
end
describe 'POST /projects/import/authorize' do
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index 431d2e56cb5..ae689d7327b 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -328,6 +328,45 @@ RSpec.describe API::Projects do
end
end
+ context 'filter by topic_id' do
+ let_it_be(:topic1) { create(:topic) }
+ let_it_be(:topic2) { create(:topic) }
+
+ let(:current_user) { user }
+
+ before do
+ project.topics << topic1
+ end
+
+ context 'with id of assigned topic' do
+ it_behaves_like 'projects response' do
+ let(:filter) { { topic_id: topic1.id } }
+ let(:projects) { [project] }
+ end
+ end
+
+ context 'with id of unassigned topic' do
+ it_behaves_like 'projects response' do
+ let(:filter) { { topic_id: topic2.id } }
+ let(:projects) { [] }
+ end
+ end
+
+ context 'with non-existing topic id' do
+ it_behaves_like 'projects response' do
+ let(:filter) { { topic_id: non_existing_record_id } }
+ let(:projects) { [] }
+ end
+ end
+
+ context 'with empty topic id' do
+ it_behaves_like 'projects response' do
+ let(:filter) { { topic_id: '' } }
+ let(:projects) { user_projects }
+ end
+ end
+ end
+
context 'and with_issues_enabled=true' do
it 'only returns projects with issues enabled' do
project.project_feature.update_attribute(:issues_access_level, ProjectFeature::DISABLED)
@@ -2388,6 +2427,7 @@ RSpec.describe API::Projects do
expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to eq(project.only_allow_merge_if_all_discussions_are_resolved)
expect(json_response['ci_default_git_depth']).to eq(project.ci_default_git_depth)
expect(json_response['ci_forward_deployment_enabled']).to eq(project.ci_forward_deployment_enabled)
+ expect(json_response['ci_separated_caches']).to eq(project.ci_separated_caches)
expect(json_response['merge_method']).to eq(project.merge_method.to_s)
expect(json_response['squash_option']).to eq(project.squash_option.to_s)
expect(json_response['readme_url']).to eq(project.readme_url)
@@ -3199,7 +3239,7 @@ RSpec.describe API::Projects do
measure_project.add_developer(create(:user))
measure_project.add_developer(create(:user)) # make this 2nd one to find any n+1
- unresolved_n_plus_ones = 21 # 21 queries added per member
+ unresolved_n_plus_ones = 27 # 27 queries added per member
expect do
post api("/projects/#{project.id}/import_project_members/#{measure_project.id}", user)
@@ -3652,6 +3692,7 @@ RSpec.describe API::Projects do
merge_method: 'ff',
ci_default_git_depth: 20,
ci_forward_deployment_enabled: false,
+ ci_separated_caches: false,
description: 'new description' }
put api("/projects/#{project3.id}", user4), params: project_param
diff --git a/spec/requests/api/protected_tags_spec.rb b/spec/requests/api/protected_tags_spec.rb
index cc7261dafc9..84b7df86f31 100644
--- a/spec/requests/api/protected_tags_spec.rb
+++ b/spec/requests/api/protected_tags_spec.rb
@@ -3,9 +3,10 @@
require 'spec_helper'
RSpec.describe API::ProtectedTags do
- let(:user) { create(:user) }
- let!(:project) { create(:project, :repository) }
- let(:project2) { create(:project, path: 'project2', namespace: user.namespace) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:project2) { create(:project, path: 'project2', namespace: user.namespace) }
+
let(:protected_name) { 'feature' }
let(:tag_name) { protected_name }
let!(:protected_tag) do
diff --git a/spec/requests/api/pypi_packages_spec.rb b/spec/requests/api/pypi_packages_spec.rb
index a24b852cdac..9e0d3780fd8 100644
--- a/spec/requests/api/pypi_packages_spec.rb
+++ b/spec/requests/api/pypi_packages_spec.rb
@@ -197,7 +197,7 @@ RSpec.describe API::PypiPackages do
let(:url) { "/projects/#{project.id}/packages/pypi" }
let(:headers) { {} }
let(:requires_python) { '>=3.7' }
- let(:base_params) { { requires_python: requires_python, version: '1.0.0', name: 'sample-project', sha256_digest: '1' * 64 } }
+ let(:base_params) { { requires_python: requires_python, version: '1.0.0', name: 'sample-project', sha256_digest: '1' * 64, md5_digest: '1' * 32 } }
let(:params) { base_params.merge(content: temp_file(file_name)) }
let(:send_rewritten_field) { true }
let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace, user: user } }
@@ -254,6 +254,19 @@ RSpec.describe API::PypiPackages do
it_behaves_like 'PyPI package creation', :developer, :created, true
end
+
+ context 'without md5_digest' do
+ let(:token) { personal_access_token.token }
+ let(:user_headers) { basic_auth_header(user.username, token) }
+ let(:headers) { user_headers.merge(workhorse_headers) }
+ let(:params) { base_params.merge(content: temp_file(file_name)) }
+
+ before do
+ params.delete(:md5_digest)
+ end
+
+ it_behaves_like 'PyPI package creation', :developer, :created, true, false
+ end
end
context 'with required_python too big' do
diff --git a/spec/requests/api/repositories_spec.rb b/spec/requests/api/repositories_spec.rb
index d6d2bd5baf2..cf0165d123f 100644
--- a/spec/requests/api/repositories_spec.rb
+++ b/spec/requests/api/repositories_spec.rb
@@ -784,6 +784,40 @@ RSpec.describe API::Repositories do
expect(json_response['notes']).to be_present
end
+ it 'supports specified config file path' do
+ spy = instance_spy(Repositories::ChangelogService)
+
+ expect(Repositories::ChangelogService)
+ .to receive(:new)
+ .with(
+ project,
+ user,
+ version: '1.0.0',
+ from: 'foo',
+ to: 'bar',
+ date: DateTime.new(2020, 1, 1),
+ trailer: 'Foo',
+ config_file: 'specified_changelog_config.yml'
+ )
+ .and_return(spy)
+
+ expect(spy).to receive(:execute).with(commit_to_changelog: false)
+
+ get(
+ api("/projects/#{project.id}/repository/changelog", user),
+ params: {
+ version: '1.0.0',
+ from: 'foo',
+ to: 'bar',
+ date: '2020-01-01',
+ trailer: 'Foo',
+ config_file: 'specified_changelog_config.yml'
+ }
+ )
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+
context 'when previous tag version does not exist' do
it_behaves_like '422 response' do
let(:request) { get api("/projects/#{project.id}/repository/changelog", user), params: { version: 'v0.0.0' } }
@@ -905,5 +939,45 @@ RSpec.describe API::Repositories do
expect(response).to have_gitlab_http_status(:unprocessable_entity)
expect(json_response['message']).to eq('Failed to generate the changelog: oops')
end
+
+ it "support specified config file path" do
+ spy = instance_spy(Repositories::ChangelogService)
+
+ expect(Repositories::ChangelogService)
+ .to receive(:new)
+ .with(
+ project,
+ user,
+ version: '1.0.0',
+ from: 'foo',
+ to: 'bar',
+ date: DateTime.new(2020, 1, 1),
+ branch: 'kittens',
+ trailer: 'Foo',
+ config_file: 'specified_changelog_config.yml',
+ file: 'FOO.md',
+ message: 'Commit message'
+ )
+ .and_return(spy)
+
+ allow(spy).to receive(:execute).with(commit_to_changelog: true)
+
+ post(
+ api("/projects/#{project.id}/repository/changelog", user),
+ params: {
+ version: '1.0.0',
+ from: 'foo',
+ to: 'bar',
+ date: '2020-01-01',
+ branch: 'kittens',
+ trailer: 'Foo',
+ config_file: 'specified_changelog_config.yml',
+ file: 'FOO.md',
+ message: 'Commit message'
+ }
+ )
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
end
end
diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb
index cfda06da8f3..d4a8e591622 100644
--- a/spec/requests/api/settings_spec.rb
+++ b/spec/requests/api/settings_spec.rb
@@ -373,7 +373,7 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting do
end
end
- context "snowplow tracking settings" do
+ context "snowplow tracking settings", :do_not_stub_snowplow_by_default do
let(:settings) do
{
snowplow_collector_hostname: "snowplow.example.com",
diff --git a/spec/requests/api/snippets_spec.rb b/spec/requests/api/snippets_spec.rb
index 13160519996..0ba1011684a 100644
--- a/spec/requests/api/snippets_spec.rb
+++ b/spec/requests/api/snippets_spec.rb
@@ -9,9 +9,9 @@ RSpec.describe API::Snippets, factory_default: :keep do
let_it_be(:user) { create(:user) }
let_it_be(:other_user) { create(:user) }
- let_it_be(:public_snippet) { create(:personal_snippet, :repository, :public, author: user) }
- let_it_be(:private_snippet) { create(:personal_snippet, :repository, :private, author: user) }
- let_it_be(:internal_snippet) { create(:personal_snippet, :repository, :internal, author: user) }
+ let_it_be(:public_snippet) { create(:personal_snippet, :repository, :public, author: user) }
+ let_it_be_with_refind(:private_snippet) { create(:personal_snippet, :repository, :private, author: user) }
+ let_it_be(:internal_snippet) { create(:personal_snippet, :repository, :internal, author: user) }
let_it_be(:user_token) { create(:personal_access_token, user: user) }
let_it_be(:other_user_token) { create(:personal_access_token, user: other_user) }
@@ -63,6 +63,23 @@ RSpec.describe API::Snippets, factory_default: :keep do
expect(snippet["id"]).not_to eq(public_snippet.id)
end
end
+
+ context 'filtering snippets by created_after/created_before' do
+ let_it_be(:private_snippet_before_time_range) { create(:personal_snippet, :repository, :private, author: user, created_at: Time.parse("2021-08-20T00:00:00Z")) }
+ let_it_be(:private_snippet_in_time_range1) { create(:personal_snippet, :repository, :private, author: user, created_at: Time.parse("2021-08-22T00:00:00Z")) }
+ let_it_be(:private_snippet_in_time_range2) { create(:personal_snippet, :repository, :private, author: user, created_at: Time.parse("2021-08-24T00:00:00Z")) }
+ let_it_be(:private_snippet_after_time_range) { create(:personal_snippet, :repository, :private, author: user, created_at: Time.parse("2021-08-26T00:00:00Z")) }
+
+ let(:path) { "/snippets?created_after=2021-08-21T00:00:00Z&created_before=2021-08-25T00:00:00Z" }
+
+ it 'returns snippets available for user in given time range' do
+ get api(path, personal_access_token: user_token)
+
+ expect(json_response.map { |snippet| snippet['id']} ).to contain_exactly(
+ private_snippet_in_time_range1.id,
+ private_snippet_in_time_range2.id)
+ end
+ end
end
describe 'GET /snippets/public' do
@@ -98,6 +115,21 @@ RSpec.describe API::Snippets, factory_default: :keep do
expect(response).to have_gitlab_http_status(:unauthorized)
end
+
+ context 'filtering public snippets by created_after/created_before' do
+ let_it_be(:public_snippet_before_time_range) { create(:personal_snippet, :repository, :public, author: other_user, created_at: Time.parse("2021-08-20T00:00:00Z")) }
+ let_it_be(:public_snippet_in_time_range) { create(:personal_snippet, :repository, :public, author: other_user, created_at: Time.parse("2021-08-22T00:00:00Z")) }
+ let_it_be(:public_snippet_after_time_range) { create(:personal_snippet, :repository, :public, author: other_user, created_at: Time.parse("2021-08-24T00:00:00Z")) }
+
+ let(:path) { "/snippets/public?created_after=2021-08-21T00:00:00Z&created_before=2021-08-23T00:00:00Z" }
+
+ it 'returns public snippets available to user in given time range' do
+ get api(path, personal_access_token: user_token)
+
+ expect(json_response.map { |snippet| snippet['id']} ).to contain_exactly(
+ public_snippet_in_time_range.id)
+ end
+ end
end
describe 'GET /snippets/:id/raw' do
diff --git a/spec/requests/api/system_hooks_spec.rb b/spec/requests/api/system_hooks_spec.rb
index 2460a98129f..0f1dbea2e73 100644
--- a/spec/requests/api/system_hooks_spec.rb
+++ b/spec/requests/api/system_hooks_spec.rb
@@ -3,221 +3,58 @@
require 'spec_helper'
RSpec.describe API::SystemHooks do
- include StubRequests
+ let_it_be(:non_admin) { create(:user) }
+ let_it_be(:admin) { create(:admin) }
+ let_it_be_with_refind(:hook) { create(:system_hook, url: "http://example.com") }
- let(:user) { create(:user) }
- let(:admin) { create(:admin) }
- let!(:hook) { create(:system_hook, url: "http://example.com") }
+ it_behaves_like 'web-hook API endpoints', '' do
+ let(:user) { admin }
+ let(:unauthorized_user) { non_admin }
- before do
- stub_full_request(hook.url, method: :post)
- end
-
- describe "GET /hooks" do
- context "when no user" do
- it "returns authentication error" do
- get api("/hooks")
-
- expect(response).to have_gitlab_http_status(:unauthorized)
- end
+ def scope
+ SystemHook
end
- context "when not an admin" do
- it "returns forbidden error" do
- get api("/hooks", user)
-
- expect(response).to have_gitlab_http_status(:forbidden)
- end
+ def collection_uri
+ "/hooks"
end
- context "when authenticated as admin" do
- it "returns an array of hooks" do
- get api("/hooks", admin)
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to include_pagination_headers
- expect(response).to match_response_schema('public_api/v4/system_hooks')
- expect(json_response.first).not_to have_key("token")
- expect(json_response.first['url']).to eq(hook.url)
- expect(json_response.first['push_events']).to be false
- expect(json_response.first['tag_push_events']).to be false
- expect(json_response.first['merge_requests_events']).to be false
- expect(json_response.first['repository_update_events']).to be true
- expect(json_response.first['enable_ssl_verification']).to be true
- expect(json_response.first['disabled_until']).to be nil
- expect(json_response.first['alert_status']).to eq 'executable'
- end
+ def match_collection_schema
+ match_response_schema('public_api/v4/system_hooks')
end
- end
- describe "GET /hooks/:id" do
- context "when no user" do
- it "returns authentication error" do
- get api("/hooks/#{hook.id}")
-
- expect(response).to have_gitlab_http_status(:unauthorized)
- end
+ def hook_uri(hook_id = hook.id)
+ "/hooks/#{hook_id}"
end
- context "when not an admin" do
- it "returns forbidden error" do
- get api("/hooks/#{hook.id}", user)
-
- expect(response).to have_gitlab_http_status(:forbidden)
- end
- end
-
- context "when authenticated as admin" do
- it "gets a hook", :aggregate_failures do
- get api("/hooks/#{hook.id}", admin)
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to match_response_schema('public_api/v4/system_hook')
- expect(json_response).to match(
- 'id' => be(hook.id),
- 'url' => eq(hook.url),
- 'created_at' => eq(hook.created_at.iso8601(3)),
- 'push_events' => be(hook.push_events),
- 'tag_push_events' => be(hook.tag_push_events),
- 'merge_requests_events' => be(hook.merge_requests_events),
- 'repository_update_events' => be(hook.repository_update_events),
- 'enable_ssl_verification' => be(hook.enable_ssl_verification),
- 'alert_status' => eq(hook.alert_status.to_s),
- 'disabled_until' => eq(hook.disabled_until&.iso8601(3))
- )
- end
-
- context 'the hook is disabled' do
- before do
- hook.disable!
- end
-
- it "has the correct alert status", :aggregate_failures do
- get api("/hooks/#{hook.id}", admin)
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to match_response_schema('public_api/v4/system_hook')
- expect(json_response).to include('alert_status' => 'disabled')
- end
- end
-
- context 'the hook is backed-off' do
- before do
- hook.backoff!
- end
-
- it "has the correct alert status", :aggregate_failures do
- get api("/hooks/#{hook.id}", admin)
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to match_response_schema('public_api/v4/system_hook')
- expect(json_response).to include(
- 'alert_status' => 'temporarily_disabled',
- 'disabled_until' => hook.disabled_until.iso8601(3)
- )
- end
- end
-
- it 'returns 404 if the system hook does not exist' do
- get api("/hooks/#{non_existing_record_id}", admin)
-
- expect(response).to have_gitlab_http_status(:not_found)
- end
+ def match_hook_schema
+ match_response_schema('public_api/v4/system_hook')
end
- end
- describe "POST /hooks" do
- it "creates new hook" do
- expect do
- post api("/hooks", admin), params: { url: 'http://example.com' }
- end.to change { SystemHook.count }.by(1)
+ def event_names
+ %i[
+ push_events
+ tag_push_events
+ merge_requests_events
+ repository_update_events
+ ]
end
- it "responds with 400 if url not given" do
- post api("/hooks", admin)
-
- expect(response).to have_gitlab_http_status(:bad_request)
+ def hook_param_overrides
+ {}
end
- it "responds with 400 if url is invalid" do
- post api("/hooks", admin), params: { url: 'hp://mep.mep' }
-
- expect(response).to have_gitlab_http_status(:bad_request)
+ let(:update_params) do
+ {
+ push_events: false,
+ tag_push_events: true
+ }
end
- it "does not create new hook without url" do
- expect do
- post api("/hooks", admin)
- end.not_to change { SystemHook.count }
+ let(:default_values) do
+ { repository_update_events: true }
end
- it 'sets default values for events' do
- stub_full_request('http://mep.mep', method: :post)
-
- post api('/hooks', admin), params: { url: 'http://mep.mep' }
-
- expect(response).to have_gitlab_http_status(:created)
- expect(response).to match_response_schema('public_api/v4/system_hook')
- expect(json_response['enable_ssl_verification']).to be true
- expect(json_response['push_events']).to be false
- expect(json_response['tag_push_events']).to be false
- expect(json_response['merge_requests_events']).to be false
- expect(json_response['repository_update_events']).to be true
- end
-
- it 'sets explicit values for events' do
- stub_full_request('http://mep.mep', method: :post)
-
- post api('/hooks', admin),
- params: {
- url: 'http://mep.mep',
- enable_ssl_verification: false,
- push_events: true,
- tag_push_events: true,
- merge_requests_events: true,
- repository_update_events: false
- }
-
- expect(response).to have_gitlab_http_status(:created)
- expect(response).to match_response_schema('public_api/v4/system_hook')
- expect(json_response['enable_ssl_verification']).to be false
- expect(json_response['push_events']).to be true
- expect(json_response['tag_push_events']).to be true
- expect(json_response['merge_requests_events']).to be true
- expect(json_response['repository_update_events']).to be false
- end
- end
-
- describe 'POST /hooks/:id' do
- it "returns and trigger hook by id" do
- post api("/hooks/#{hook.id}", admin)
- expect(response).to have_gitlab_http_status(:created)
- expect(json_response['event_name']).to eq('project_create')
- end
-
- it "returns 404 on failure" do
- post api("/hooks/404", admin)
- expect(response).to have_gitlab_http_status(:not_found)
- end
- end
-
- describe "DELETE /hooks/:id" do
- it "deletes a hook" do
- expect do
- delete api("/hooks/#{hook.id}", admin)
-
- expect(response).to have_gitlab_http_status(:no_content)
- end.to change { SystemHook.count }.by(-1)
- end
-
- it 'returns 404 if the system hook does not exist' do
- delete api("/hooks/#{non_existing_record_id}", admin)
-
- expect(response).to have_gitlab_http_status(:not_found)
- end
-
- it_behaves_like '412 response' do
- let(:request) { api("/hooks/#{hook.id}", admin) }
- end
+ it_behaves_like 'web-hook API endpoints test hook', ''
end
end
diff --git a/spec/requests/api/tags_spec.rb b/spec/requests/api/tags_spec.rb
index 3558babf2f1..e81e9e0bf2f 100644
--- a/spec/requests/api/tags_spec.rb
+++ b/spec/requests/api/tags_spec.rb
@@ -90,6 +90,13 @@ RSpec.describe API::Tags do
let(:request) { get api(route, current_user) }
end
end
+
+ context 'when repository does not exist' do
+ it_behaves_like '404 response' do
+ let(:project) { create(:project, creator: user) }
+ let(:request) { get api(route, current_user) }
+ end
+ end
end
context 'when unauthenticated', 'and project is public' do
diff --git a/spec/requests/api/terraform/modules/v1/packages_spec.rb b/spec/requests/api/terraform/modules/v1/packages_spec.rb
index 12bce4da011..dff44a45de4 100644
--- a/spec/requests/api/terraform/modules/v1/packages_spec.rb
+++ b/spec/requests/api/terraform/modules/v1/packages_spec.rb
@@ -98,6 +98,216 @@ RSpec.describe API::Terraform::Modules::V1::Packages do
end
end
+ describe 'GET /api/v4/packages/terraform/modules/v1/:module_namespace/:module_name/:module_system/download' do
+ context 'empty registry' do
+ let(:url) { api("/packages/terraform/modules/v1/#{group.path}/module-2/system/download") }
+ let(:headers) { {} }
+
+ subject { get(url, headers: headers) }
+
+ it 'returns not found when there is no module' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'with valid namespace' do
+ let(:url) { api("/packages/terraform/modules/v1/#{group.path}/#{package.name}/download") }
+ let(:headers) { {} }
+
+ subject { get(url, headers: headers) }
+
+ before_all do
+ create(:terraform_module_package, project: project, name: package.name, version: '1.0.1')
+ end
+
+ where(:visibility, :user_role, :member, :token_type, :shared_examples_name, :expected_status) do
+ :public | :developer | true | :personal_access_token | 'redirects to version download' | :found
+ :public | :guest | true | :personal_access_token | 'redirects to version download' | :found
+ :public | :developer | true | :invalid | 'rejects terraform module packages access' | :unauthorized
+ :public | :guest | true | :invalid | 'rejects terraform module packages access' | :unauthorized
+ :public | :developer | false | :personal_access_token | 'redirects to version download' | :found
+ :public | :guest | false | :personal_access_token | 'redirects to version download' | :found
+ :public | :developer | false | :invalid | 'rejects terraform module packages access' | :unauthorized
+ :public | :guest | false | :invalid | 'rejects terraform module packages access' | :unauthorized
+ :public | :anonymous | false | nil | 'redirects to version download' | :found
+ :private | :developer | true | :personal_access_token | 'redirects to version download' | :found
+ :private | :guest | true | :personal_access_token | 'rejects terraform module packages access' | :forbidden
+ :private | :developer | true | :invalid | 'rejects terraform module packages access' | :unauthorized
+ :private | :guest | true | :invalid | 'rejects terraform module packages access' | :unauthorized
+ :private | :developer | false | :personal_access_token | 'rejects terraform module packages access' | :forbidden
+ :private | :guest | false | :personal_access_token | 'rejects terraform module packages access' | :forbidden
+ :private | :developer | false | :invalid | 'rejects terraform module packages access' | :unauthorized
+ :private | :guest | false | :invalid | 'rejects terraform module packages access' | :unauthorized
+ :private | :anonymous | false | nil | 'rejects terraform module packages access' | :unauthorized
+ :public | :developer | true | :job_token | 'redirects to version download' | :found
+ :public | :guest | true | :job_token | 'redirects to version download' | :found
+ :public | :guest | true | :invalid | 'rejects terraform module packages access' | :unauthorized
+ :public | :developer | false | :job_token | 'redirects to version download' | :found
+ :public | :guest | false | :job_token | 'redirects to version download' | :found
+ :public | :developer | false | :invalid | 'rejects terraform module packages access' | :unauthorized
+ :public | :guest | false | :invalid | 'rejects terraform module packages access' | :unauthorized
+ :private | :developer | true | :job_token | 'redirects to version download' | :found
+ :private | :guest | true | :job_token | 'rejects terraform module packages access' | :forbidden
+ :private | :developer | true | :invalid | 'rejects terraform module packages access' | :unauthorized
+ :private | :guest | true | :invalid | 'rejects terraform module packages access' | :unauthorized
+ :private | :developer | false | :job_token | 'rejects terraform module packages access' | :forbidden
+ :private | :guest | false | :job_token | 'rejects terraform module packages access' | :forbidden
+ :private | :developer | false | :invalid | 'rejects terraform module packages access' | :unauthorized
+ :private | :guest | false | :invalid | 'rejects terraform module packages access' | :unauthorized
+ end
+
+ with_them do
+ let(:headers) { user_role == :anonymous ? {} : { 'Authorization' => "Bearer #{token}" } }
+
+ before do
+ group.update!(visibility: visibility.to_s)
+ project.update!(visibility: visibility.to_s)
+ end
+
+ it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member]
+ end
+ end
+ end
+
+ describe 'GET /api/v4/packages/terraform/modules/v1/:module_namespace/:module_name/:module_system' do
+ context 'empty registry' do
+ let(:url) { api("/packages/terraform/modules/v1/#{group.path}/non-existent/system") }
+ let(:headers) { { 'Authorization' => "Bearer #{tokens[:personal_access_token]}" } }
+
+ subject { get(url, headers: headers) }
+
+ it 'returns not found when there is no module' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'with valid namespace' do
+ let(:url) { api("/packages/terraform/modules/v1/#{group.path}/#{package.name}") }
+
+ subject { get(url, headers: headers) }
+
+ where(:visibility, :user_role, :member, :token_type, :shared_examples_name, :expected_status) do
+ :public | :developer | true | :personal_access_token | 'returns terraform module version' | :success
+ :public | :guest | true | :personal_access_token | 'returns terraform module version' | :success
+ :public | :developer | true | :invalid | 'rejects terraform module packages access' | :unauthorized
+ :public | :guest | true | :invalid | 'rejects terraform module packages access' | :unauthorized
+ :public | :developer | false | :personal_access_token | 'returns terraform module version' | :success
+ :public | :guest | false | :personal_access_token | 'returns terraform module version' | :success
+ :public | :developer | false | :invalid | 'rejects terraform module packages access' | :unauthorized
+ :public | :guest | false | :invalid | 'rejects terraform module packages access' | :unauthorized
+ :public | :anonymous | false | nil | 'returns terraform module version' | :success
+ :private | :developer | true | :personal_access_token | 'returns terraform module version' | :success
+ :private | :guest | true | :personal_access_token | 'rejects terraform module packages access' | :forbidden
+ :private | :developer | true | :invalid | 'rejects terraform module packages access' | :unauthorized
+ :private | :guest | true | :invalid | 'rejects terraform module packages access' | :unauthorized
+ :private | :developer | false | :personal_access_token | 'rejects terraform module packages access' | :forbidden
+ :private | :guest | false | :personal_access_token | 'rejects terraform module packages access' | :forbidden
+ :private | :developer | false | :invalid | 'rejects terraform module packages access' | :unauthorized
+ :private | :guest | false | :invalid | 'rejects terraform module packages access' | :unauthorized
+ :private | :anonymous | false | nil | 'rejects terraform module packages access' | :unauthorized
+ :public | :developer | true | :job_token | 'returns terraform module version' | :success
+ :public | :guest | true | :job_token | 'returns terraform module version' | :success
+ :public | :guest | true | :invalid | 'rejects terraform module packages access' | :unauthorized
+ :public | :developer | false | :job_token | 'returns terraform module version' | :success
+ :public | :guest | false | :job_token | 'returns terraform module version' | :success
+ :public | :developer | false | :invalid | 'rejects terraform module packages access' | :unauthorized
+ :public | :guest | false | :invalid | 'rejects terraform module packages access' | :unauthorized
+ :private | :developer | true | :job_token | 'returns terraform module version' | :success
+ :private | :guest | true | :job_token | 'rejects terraform module packages access' | :forbidden
+ :private | :developer | true | :invalid | 'rejects terraform module packages access' | :unauthorized
+ :private | :guest | true | :invalid | 'rejects terraform module packages access' | :unauthorized
+ :private | :developer | false | :job_token | 'rejects terraform module packages access' | :forbidden
+ :private | :guest | false | :job_token | 'rejects terraform module packages access' | :forbidden
+ :private | :developer | false | :invalid | 'rejects terraform module packages access' | :unauthorized
+ :private | :guest | false | :invalid | 'rejects terraform module packages access' | :unauthorized
+ end
+
+ with_them do
+ let(:headers) { user_role == :anonymous ? {} : { 'Authorization' => "Bearer #{token}" } }
+
+ before do
+ group.update!(visibility: visibility.to_s)
+ project.update!(visibility: visibility.to_s)
+ end
+
+ it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member]
+ end
+ end
+ end
+
+ describe 'GET /api/v4/packages/terraform/modules/v1/:module_namespace/:module_name/:module_system/:module_version' do
+ let(:url) { api("/packages/terraform/modules/v1/#{group.path}/#{package.name}/#{package.version}") }
+ let(:headers) { {} }
+
+ subject { get(url, headers: headers) }
+
+ context 'not found' do
+ let(:url) { api("/packages/terraform/modules/v1/#{group.path}/#{package.name}/2.0.0") }
+ let(:headers) { { 'Authorization' => "Bearer #{tokens[:job_token]}" } }
+
+ subject { get(url, headers: headers) }
+
+ it 'returns not found when the specified version is not present in the registry' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'with valid namespace' do
+ where(:visibility, :user_role, :member, :token_type, :shared_examples_name, :expected_status) do
+ :public | :developer | true | :personal_access_token | 'returns terraform module version' | :success
+ :public | :guest | true | :personal_access_token | 'returns terraform module version' | :success
+ :public | :developer | true | :invalid | 'rejects terraform module packages access' | :unauthorized
+ :public | :guest | true | :invalid | 'rejects terraform module packages access' | :unauthorized
+ :public | :developer | false | :personal_access_token | 'returns terraform module version' | :success
+ :public | :guest | false | :personal_access_token | 'returns terraform module version' | :success
+ :public | :developer | false | :invalid | 'rejects terraform module packages access' | :unauthorized
+ :public | :guest | false | :invalid | 'rejects terraform module packages access' | :unauthorized
+ :public | :anonymous | false | nil | 'returns terraform module version' | :success
+ :private | :developer | true | :personal_access_token | 'returns terraform module version' | :success
+ :private | :guest | true | :personal_access_token | 'rejects terraform module packages access' | :forbidden
+ :private | :developer | true | :invalid | 'rejects terraform module packages access' | :unauthorized
+ :private | :guest | true | :invalid | 'rejects terraform module packages access' | :unauthorized
+ :private | :developer | false | :personal_access_token | 'rejects terraform module packages access' | :forbidden
+ :private | :guest | false | :personal_access_token | 'rejects terraform module packages access' | :forbidden
+ :private | :developer | false | :invalid | 'rejects terraform module packages access' | :unauthorized
+ :private | :guest | false | :invalid | 'rejects terraform module packages access' | :unauthorized
+ :private | :anonymous | false | nil | 'rejects terraform module packages access' | :unauthorized
+ :public | :developer | true | :job_token | 'returns terraform module version' | :success
+ :public | :guest | true | :job_token | 'returns terraform module version' | :success
+ :public | :guest | true | :invalid | 'rejects terraform module packages access' | :unauthorized
+ :public | :developer | false | :job_token | 'returns terraform module version' | :success
+ :public | :guest | false | :job_token | 'returns terraform module version' | :success
+ :public | :developer | false | :invalid | 'rejects terraform module packages access' | :unauthorized
+ :public | :guest | false | :invalid | 'rejects terraform module packages access' | :unauthorized
+ :private | :developer | true | :job_token | 'returns terraform module version' | :success
+ :private | :guest | true | :job_token | 'rejects terraform module packages access' | :forbidden
+ :private | :developer | true | :invalid | 'rejects terraform module packages access' | :unauthorized
+ :private | :guest | true | :invalid | 'rejects terraform module packages access' | :unauthorized
+ :private | :developer | false | :job_token | 'rejects terraform module packages access' | :forbidden
+ :private | :guest | false | :job_token | 'rejects terraform module packages access' | :forbidden
+ :private | :developer | false | :invalid | 'rejects terraform module packages access' | :unauthorized
+ :private | :guest | false | :invalid | 'rejects terraform module packages access' | :unauthorized
+ end
+
+ with_them do
+ let(:headers) { user_role == :anonymous ? {} : { 'Authorization' => "Bearer #{token}" } }
+
+ before do
+ group.update!(visibility: visibility.to_s)
+ project.update!(visibility: visibility.to_s)
+ end
+
+ it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member]
+ end
+ end
+ end
+
describe 'GET /api/v4/packages/terraform/modules/v1/:module_namespace/:module_name/:module_system/:module_version/download' do
let(:url) { api("/packages/terraform/modules/v1/#{group.path}/#{package.name}/#{package.version}/download") }
let(:headers) { {} }
diff --git a/spec/requests/api/unleash_spec.rb b/spec/requests/api/unleash_spec.rb
index 6cb801538c6..7bdb89fb286 100644
--- a/spec/requests/api/unleash_spec.rb
+++ b/spec/requests/api/unleash_spec.rb
@@ -168,7 +168,7 @@ RSpec.describe API::Unleash do
end
%w(/feature_flags/unleash/:project_id/features /feature_flags/unleash/:project_id/client/features).each do |features_endpoint|
- describe "GET #{features_endpoint}" do
+ describe "GET #{features_endpoint}", :use_clean_rails_redis_caching do
let(:features_url) { features_endpoint.sub(':project_id', project_id.to_s) }
let(:client) { create(:operations_feature_flags_client, project: project) }
@@ -176,6 +176,46 @@ RSpec.describe API::Unleash do
it_behaves_like 'authenticated request'
+ context 'when a client fetches feature flags several times' do
+ let(:headers) { { 'UNLEASH-INSTANCEID' => client.token, 'UNLEASH-APPNAME' => 'production' } }
+
+ before do
+ create_list(:operations_feature_flag, 3, project: project)
+ end
+
+ it 'serializes feature flags for the first time and read cached data from the second time' do
+ expect(API::Entities::Unleash::ClientFeatureFlags)
+ .to receive(:represent).with(instance_of(Operations::FeatureFlagsClient), any_args)
+ .once
+
+ 5.times { get api(features_url), params: params, headers: headers }
+ end
+
+ it 'increments the cache key when feature flags are modified' do
+ expect(API::Entities::Unleash::ClientFeatureFlags)
+ .to receive(:represent).with(instance_of(Operations::FeatureFlagsClient), any_args)
+ .twice
+
+ 2.times { get api(features_url), params: params, headers: headers }
+
+ ::FeatureFlags::CreateService.new(project, project.owner, name: 'feature_flag').execute
+
+ 3.times { get api(features_url), params: params, headers: headers }
+ end
+
+ context 'when cache_unleash_client_api is disabled' do
+ before do
+ stub_feature_flags(cache_unleash_client_api: false)
+ end
+
+ it 'serializes feature flags every time' do
+ expect(::API::Entities::UnleashFeature).to receive(:represent).exactly(5).times
+
+ 5.times { get api(features_url), params: params, headers: headers }
+ end
+ end
+ end
+
context 'with version 2 feature flags' do
it 'does not return a flag without any strategies' do
create(:operations_feature_flag, project: project,
diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb
index d4dc7375e9e..68d5fad8ff4 100644
--- a/spec/requests/api/users_spec.rb
+++ b/spec/requests/api/users_spec.rb
@@ -17,6 +17,8 @@ RSpec.describe API::Users do
let(:deactivated_user) { create(:user, state: 'deactivated') }
let(:banned_user) { create(:user, :banned) }
let(:internal_user) { create(:user, :bot) }
+ let(:user_with_2fa) { create(:user, :two_factor_via_otp) }
+ let(:admin_with_2fa) { create(:admin, :two_factor_via_otp) }
context 'admin notes' do
let_it_be(:admin) { create(:admin, note: '2019-10-06 | 2FA added | user requested | www.gitlab.com') }
@@ -81,6 +83,79 @@ RSpec.describe API::Users do
end
end
+ describe "PATCH /users/:id/disable_two_factor" do
+ context "when current user is an admin" do
+ it "returns a 204 when 2FA is disabled for the target user" do
+ expect do
+ patch api("/users/#{user_with_2fa.id}/disable_two_factor", admin)
+ end.to change { user_with_2fa.reload.two_factor_enabled? }
+ .from(true)
+ .to(false)
+ expect(response).to have_gitlab_http_status(:no_content)
+ end
+
+ it "uses TwoFactor Destroy Service" do
+ destroy_service = instance_double(TwoFactor::DestroyService, execute: nil)
+ expect(TwoFactor::DestroyService).to receive(:new)
+ .with(admin, user: user_with_2fa)
+ .and_return(destroy_service)
+ expect(destroy_service).to receive(:execute)
+
+ patch api("/users/#{user_with_2fa.id}/disable_two_factor", admin)
+ end
+
+ it "returns a 400 if 2FA is not enabled for the target user" do
+ expect(TwoFactor::DestroyService).to receive(:new).and_call_original
+
+ expect do
+ patch api("/users/#{user.id}/disable_two_factor", admin)
+ end.not_to change { user.reload.two_factor_enabled? }
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['message']).to eq("400 Bad request - Two-factor authentication is not enabled for this user")
+ end
+
+ it "returns a 403 if the target user is an admin" do
+ expect(TwoFactor::DestroyService).to receive(:new).never
+
+ expect do
+ patch api("/users/#{admin_with_2fa.id}/disable_two_factor", admin)
+ end.not_to change { admin_with_2fa.reload.two_factor_enabled? }
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ expect(json_response['message']).to eq("403 Forbidden - Two-factor authentication for admins cannot be disabled via the API. Use the Rails console")
+ end
+
+ it "returns a 404 if the target user cannot be found" do
+ expect(TwoFactor::DestroyService).to receive(:new).never
+
+ patch api("/users/#{non_existing_record_id}/disable_two_factor", admin)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ expect(json_response['message']).to eq("404 User Not Found")
+ end
+ end
+
+ context "when current user is not an admin" do
+ it "returns a 403" do
+ expect do
+ patch api("/users/#{user_with_2fa.id}/disable_two_factor", user)
+ end.not_to change { user_with_2fa.reload.two_factor_enabled? }
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ expect(json_response['message']).to eq("403 Forbidden")
+ end
+ end
+
+ context "when unauthenticated" do
+ it "returns a 401" do
+ patch api("/users/#{user_with_2fa.id}/disable_two_factor")
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+ end
+ end
+
describe 'GET /users/' do
context 'when unauthenticated' do
it "does not contain certain fields" do
@@ -110,6 +185,40 @@ RSpec.describe API::Users do
expect(json_response.first['note']).to eq '2018-11-05 | 2FA removed | user requested | www.gitlab.com'
end
end
+
+ context 'N+1 queries' do
+ before do
+ create_list(:user, 2)
+ end
+
+ it 'avoids N+1 queries when requested by admin' do
+ control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do
+ get api("/users", admin)
+ end.count
+
+ create_list(:user, 3)
+
+ # There is a still a pending N+1 query related to fetching
+ # project count for each user.
+ # Refer issue https://gitlab.com/gitlab-org/gitlab/-/issues/367080
+
+ expect do
+ get api("/users", admin)
+ end.not_to exceed_all_query_limit(control_count + 3)
+ end
+
+ it 'avoids N+1 queries when requested by a regular user' do
+ control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do
+ get api("/users", user)
+ end.count
+
+ create_list(:user, 3)
+
+ expect do
+ get api("/users", user)
+ end.not_to exceed_all_query_limit(control_count)
+ end
+ end
end
end