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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'spec/requests')
-rw-r--r--spec/requests/api/ci/job_artifacts_spec.rb65
-rw-r--r--spec/requests/api/ci/runner/runners_post_spec.rb423
-rw-r--r--spec/requests/api/ci/runners_spec.rb12
-rw-r--r--spec/requests/api/ci/triggers_spec.rb2
-rw-r--r--spec/requests/api/commits_spec.rb8
-rw-r--r--spec/requests/api/generic_packages_spec.rb21
-rw-r--r--spec/requests/api/graphql/ci/config_spec.rb18
-rw-r--r--spec/requests/api/graphql/ci/jobs_spec.rb8
-rw-r--r--spec/requests/api/graphql/ci/pipelines_spec.rb108
-rw-r--r--spec/requests/api/graphql/ci/runner_spec.rb6
-rw-r--r--spec/requests/api/graphql/group/group_members_spec.rb11
-rw-r--r--spec/requests/api/graphql/group/work_item_types_spec.rb71
-rw-r--r--spec/requests/api/graphql/mutations/issues/set_crm_contacts_spec.rb103
-rw-r--r--spec/requests/api/graphql/mutations/issues/set_escalation_status_spec.rb82
-rw-r--r--spec/requests/api/graphql/mutations/work_items/create_spec.rb63
-rw-r--r--spec/requests/api/graphql/packages/package_spec.rb238
-rw-r--r--spec/requests/api/graphql/project/issues_spec.rb37
-rw-r--r--spec/requests/api/graphql/project/work_item_types_spec.rb71
-rw-r--r--spec/requests/api/groups_spec.rb48
-rw-r--r--spec/requests/api/integrations_spec.rb6
-rw-r--r--spec/requests/api/internal/base_spec.rb75
-rw-r--r--spec/requests/api/internal/kubernetes_spec.rb6
-rw-r--r--spec/requests/api/internal/mail_room_spec.rb194
-rw-r--r--spec/requests/api/lint_spec.rb19
-rw-r--r--spec/requests/api/maven_packages_spec.rb2
-rw-r--r--spec/requests/api/merge_requests_spec.rb35
-rw-r--r--spec/requests/api/package_files_spec.rb50
-rw-r--r--spec/requests/api/projects_spec.rb49
-rw-r--r--spec/requests/api/resource_access_tokens_spec.rb187
-rw-r--r--spec/requests/api/rubygem_packages_spec.rb28
-rw-r--r--spec/requests/api/search_spec.rb24
-rw-r--r--spec/requests/api/terraform/modules/v1/packages_spec.rb37
-rw-r--r--spec/requests/api/usage_data_non_sql_metrics_spec.rb1
-rw-r--r--spec/requests/api/usage_data_queries_spec.rb1
-rw-r--r--spec/requests/api/users_spec.rb67
-rw-r--r--spec/requests/git_http_spec.rb4
-rw-r--r--spec/requests/groups/crm/contacts_controller_spec.rb16
-rw-r--r--spec/requests/groups/crm/organizations_controller_spec.rb16
-rw-r--r--spec/requests/groups/settings/access_tokens_controller_spec.rb90
-rw-r--r--spec/requests/projects/google_cloud/deployments_controller_spec.rb103
-rw-r--r--spec/requests/projects/merge_requests/context_commit_diffs_spec.rb1
-rw-r--r--spec/requests/projects/merge_requests/diffs_spec.rb16
-rw-r--r--spec/requests/projects/merge_requests_discussions_spec.rb2
-rw-r--r--spec/requests/projects/settings/access_tokens_controller_spec.rb91
-rw-r--r--spec/requests/rack_attack_global_spec.rb14
-rw-r--r--spec/requests/recursive_webhook_detection_spec.rb182
-rw-r--r--spec/requests/sandbox_controller_spec.rb14
-rw-r--r--spec/requests/users_controller_spec.rb13
48 files changed, 2100 insertions, 638 deletions
diff --git a/spec/requests/api/ci/job_artifacts_spec.rb b/spec/requests/api/ci/job_artifacts_spec.rb
index 585fab33708..0db6acbc7b8 100644
--- a/spec/requests/api/ci/job_artifacts_spec.rb
+++ b/spec/requests/api/ci/job_artifacts_spec.rb
@@ -81,6 +81,71 @@ RSpec.describe API::Ci::JobArtifacts do
end
end
+ describe 'DELETE /projects/:id/artifacts' do
+ context 'when feature flag is disabled' do
+ before do
+ stub_feature_flags(bulk_expire_project_artifacts: false)
+ end
+
+ it 'returns 404' do
+ delete api("/projects/#{project.id}/artifacts", api_user)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'when user is anonymous' do
+ let(:api_user) { nil }
+
+ it 'does not execute Ci::JobArtifacts::DeleteProjectArtifactsService' do
+ expect(Ci::JobArtifacts::DeleteProjectArtifactsService)
+ .not_to receive(:new)
+
+ delete api("/projects/#{project.id}/artifacts", api_user)
+ end
+
+ it 'returns status 401 (unauthorized)' do
+ delete api("/projects/#{project.id}/artifacts", api_user)
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+ end
+
+ context 'with developer' do
+ it 'does not execute Ci::JobArtifacts::DeleteProjectArtifactsService' do
+ expect(Ci::JobArtifacts::DeleteProjectArtifactsService)
+ .not_to receive(:new)
+
+ delete api("/projects/#{project.id}/artifacts", api_user)
+ end
+
+ it 'returns status 403 (forbidden)' do
+ delete api("/projects/#{project.id}/artifacts", api_user)
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+
+ context 'with authorized user' do
+ let(:maintainer) { create(:project_member, :maintainer, project: project).user }
+ let!(:api_user) { maintainer }
+
+ it 'executes Ci::JobArtifacts::DeleteProjectArtifactsService' do
+ expect_next_instance_of(Ci::JobArtifacts::DeleteProjectArtifactsService, project: project) do |service|
+ expect(service).to receive(:execute).and_call_original
+ end
+
+ delete api("/projects/#{project.id}/artifacts", api_user)
+ end
+
+ it 'returns status 202 (accepted)' do
+ delete api("/projects/#{project.id}/artifacts", api_user)
+
+ expect(response).to have_gitlab_http_status(:accepted)
+ end
+ end
+ end
+
describe 'GET /projects/:id/jobs/:job_id/artifacts/:artifact_path' do
context 'when job has artifacts' do
let(:job) { create(:ci_build, :artifacts, pipeline: pipeline) }
diff --git a/spec/requests/api/ci/runner/runners_post_spec.rb b/spec/requests/api/ci/runner/runners_post_spec.rb
index a51d8b458f8..530b601add9 100644
--- a/spec/requests/api/ci/runner/runners_post_spec.rb
+++ b/spec/requests/api/ci/runner/runners_post_spec.rb
@@ -3,21 +3,6 @@
require 'spec_helper'
RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
- include StubGitlabCalls
- include RedisHelpers
- include WorkhorseHelpers
-
- let(:registration_token) { 'abcdefg123456' }
-
- before do
- stub_feature_flags(ci_enable_live_trace: true)
- stub_feature_flags(runner_registration_control: false)
- stub_gitlab_calls
- stub_application_setting(runners_registration_token: registration_token)
- stub_application_setting(valid_runner_registrars: ApplicationSetting::VALID_RUNNER_REGISTRAR_TYPES)
- allow_any_instance_of(::Ci::Runner).to receive(:cache_attributes)
- end
-
describe '/api/v4/runners' do
describe 'POST /api/v4/runners' do
context 'when no token is provided' do
@@ -30,380 +15,108 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
context 'when invalid token is provided' do
it 'returns 403 error' do
+ allow_next_instance_of(::Ci::RegisterRunnerService) do |service|
+ allow(service).to receive(:execute).and_return(nil)
+ end
+
post api('/runners'), params: { token: 'invalid' }
expect(response).to have_gitlab_http_status(:forbidden)
end
end
- context 'when valid token is provided' do
+ context 'when valid parameters are provided' do
def request
- post api('/runners'), params: { token: token }
- end
-
- context 'with a registration token' do
- let(:token) { registration_token }
-
- it 'creates runner with default values' do
- request
-
- runner = ::Ci::Runner.first
-
- expect(response).to have_gitlab_http_status(:created)
- expect(json_response['id']).to eq(runner.id)
- expect(json_response['token']).to eq(runner.token)
- expect(runner.run_untagged).to be true
- expect(runner.active).to be true
- expect(runner.token).not_to eq(registration_token)
- expect(runner).to be_instance_type
- end
-
- it_behaves_like 'storing arguments in the application context for the API' do
- subject { request }
-
- let(:expected_params) { { client_id: "runner/#{::Ci::Runner.first.id}" } }
- end
-
- it_behaves_like 'not executing any extra queries for the application context' do
- let(:subject_proc) { proc { request } }
- end
- end
-
- context 'when project token is used' do
- let(:project) { create(:project) }
- let(:token) { project.runners_token }
-
- it 'creates project runner' do
- request
-
- expect(response).to have_gitlab_http_status(:created)
- expect(project.runners.size).to eq(1)
- runner = ::Ci::Runner.first
- expect(runner.token).not_to eq(registration_token)
- expect(runner.token).not_to eq(project.runners_token)
- expect(runner).to be_project_type
- end
-
- it_behaves_like 'storing arguments in the application context for the API' do
- subject { request }
-
- let(:expected_params) { { project: project.full_path, client_id: "runner/#{::Ci::Runner.first.id}" } }
- end
-
- it_behaves_like 'not executing any extra queries for the application context' do
- let(:subject_proc) { proc { request } }
- end
-
- context 'when it exceeds the application limits' do
- before do
- create(:ci_runner, runner_type: :project_type, projects: [project], contacted_at: 1.second.ago)
- create(:plan_limits, :default_plan, ci_registered_project_runners: 1)
- end
-
- it 'does not create runner' do
- request
-
- expect(response).to have_gitlab_http_status(:bad_request)
- expect(json_response['message']).to include('runner_projects.base' => ['Maximum number of ci registered project runners (1) exceeded'])
- expect(project.runners.reload.size).to eq(1)
- end
- end
-
- context 'when abandoned runners cause application limits to not be exceeded' do
- before do
- create(:ci_runner, runner_type: :project_type, projects: [project], created_at: 14.months.ago, contacted_at: 13.months.ago)
- create(:plan_limits, :default_plan, ci_registered_project_runners: 1)
- end
-
- it 'creates runner' do
- request
-
- expect(response).to have_gitlab_http_status(:created)
- expect(json_response['message']).to be_nil
- expect(project.runners.reload.size).to eq(2)
- expect(project.runners.recent.size).to eq(1)
- end
- end
-
- context 'when valid runner registrars do not include project' do
- before do
- stub_application_setting(valid_runner_registrars: ['group'])
- end
-
- context 'when feature flag is enabled' do
- before do
- stub_feature_flags(runner_registration_control: true)
- end
-
- it 'returns 403 error' do
- request
-
- expect(response).to have_gitlab_http_status(:forbidden)
- end
- end
-
- context 'when feature flag is disabled' do
- it 'registers the runner' do
- request
-
- expect(response).to have_gitlab_http_status(:created)
- expect(::Ci::Runner.first.active).to be true
- end
- end
- end
- end
-
- context 'when group token is used' do
- let(:group) { create(:group) }
- let(:token) { group.runners_token }
-
- it 'creates a group runner' do
- request
-
- expect(response).to have_gitlab_http_status(:created)
- expect(group.runners.reload.size).to eq(1)
- runner = ::Ci::Runner.first
- expect(runner.token).not_to eq(registration_token)
- expect(runner.token).not_to eq(group.runners_token)
- expect(runner).to be_group_type
- end
-
- it_behaves_like 'storing arguments in the application context for the API' do
- subject { request }
-
- let(:expected_params) { { root_namespace: group.full_path_components.first, client_id: "runner/#{::Ci::Runner.first.id}" } }
- end
-
- it_behaves_like 'not executing any extra queries for the application context' do
- let(:subject_proc) { proc { request } }
- end
-
- context 'when it exceeds the application limits' do
- before do
- create(:ci_runner, runner_type: :group_type, groups: [group], contacted_at: nil, created_at: 1.month.ago)
- create(:plan_limits, :default_plan, ci_registered_group_runners: 1)
- end
-
- it 'does not create runner' do
- request
-
- expect(response).to have_gitlab_http_status(:bad_request)
- expect(json_response['message']).to include('runner_namespaces.base' => ['Maximum number of ci registered group runners (1) exceeded'])
- expect(group.runners.reload.size).to eq(1)
- end
- end
-
- context 'when abandoned runners cause application limits to not be exceeded' do
- before do
- create(:ci_runner, runner_type: :group_type, groups: [group], created_at: 4.months.ago, contacted_at: 3.months.ago)
- create(:ci_runner, runner_type: :group_type, groups: [group], contacted_at: nil, created_at: 4.months.ago)
- create(:plan_limits, :default_plan, ci_registered_group_runners: 1)
- end
-
- it 'creates runner' do
- request
-
- expect(response).to have_gitlab_http_status(:created)
- expect(json_response['message']).to be_nil
- expect(group.runners.reload.size).to eq(3)
- expect(group.runners.recent.size).to eq(1)
- end
- end
-
- context 'when valid runner registrars do not include group' do
- before do
- stub_application_setting(valid_runner_registrars: ['project'])
- end
-
- context 'when feature flag is enabled' do
- before do
- stub_feature_flags(runner_registration_control: true)
- end
-
- it 'returns 403 error' do
- request
-
- expect(response).to have_gitlab_http_status(:forbidden)
- end
- end
-
- context 'when feature flag is disabled' do
- it 'registers the runner' do
- request
-
- expect(response).to have_gitlab_http_status(:created)
- expect(::Ci::Runner.first.active).to be true
- end
- end
- end
- end
- end
-
- context 'when runner description is provided' do
- it 'creates runner' do
- post api('/runners'), params: {
- token: registration_token,
- description: 'server.hostname'
- }
-
- expect(response).to have_gitlab_http_status(:created)
- expect(::Ci::Runner.first.description).to eq('server.hostname')
- end
- end
-
- context 'when runner tags are provided' do
- it 'creates runner' do
post api('/runners'), params: {
- token: registration_token,
- tag_list: 'tag1, tag2'
- }
-
- expect(response).to have_gitlab_http_status(:created)
- expect(::Ci::Runner.first.tag_list.sort).to eq(%w(tag1 tag2))
- end
- end
-
- context 'when option for running untagged jobs is provided' do
- context 'when tags are provided' do
- it 'creates runner' do
- post api('/runners'), params: {
- token: registration_token,
- run_untagged: false,
- tag_list: ['tag']
- }
-
- expect(response).to have_gitlab_http_status(:created)
- expect(::Ci::Runner.first.run_untagged).to be false
- expect(::Ci::Runner.first.tag_list.sort).to eq(['tag'])
- end
- end
-
- context 'when tags are not provided' do
- it 'returns 400 error' do
- post api('/runners'), params: {
- token: registration_token,
- run_untagged: false
- }
-
- expect(response).to have_gitlab_http_status(:bad_request)
- expect(json_response['message']).to include(
- 'tags_list' => ['can not be empty when runner is not allowed to pick untagged jobs'])
+ token: 'valid token',
+ description: 'server.hostname',
+ maintainer_note: 'Some maintainer notes',
+ run_untagged: false,
+ tag_list: 'tag1, tag2',
+ locked: true,
+ active: true,
+ access_level: 'ref_protected',
+ maximum_timeout: 9000
+ }
+ end
+
+ let_it_be(:new_runner) { create(:ci_runner) }
+
+ before do
+ allow_next_instance_of(::Ci::RegisterRunnerService) do |service|
+ expected_params = {
+ description: 'server.hostname',
+ maintainer_note: 'Some maintainer notes',
+ run_untagged: false,
+ tag_list: %w(tag1 tag2),
+ locked: true,
+ active: true,
+ access_level: 'ref_protected',
+ maximum_timeout: 9000
+ }.stringify_keys
+
+ allow(service).to receive(:execute)
+ .once
+ .with('valid token', a_hash_including(expected_params))
+ .and_return(new_runner)
end
end
- end
- context 'when option for locking Runner is provided' do
it 'creates runner' do
- post api('/runners'), params: {
- token: registration_token,
- locked: true
- }
+ request
expect(response).to have_gitlab_http_status(:created)
- expect(::Ci::Runner.first.locked).to be true
+ expect(json_response['id']).to eq(new_runner.id)
+ expect(json_response['token']).to eq(new_runner.token)
end
- end
- context 'when option for activating a Runner is provided' do
- context 'when active is set to true' do
- it 'creates runner' do
- post api('/runners'), params: {
- token: registration_token,
- active: true
- }
+ it_behaves_like 'storing arguments in the application context for the API' do
+ subject { request }
- expect(response).to have_gitlab_http_status(:created)
- expect(::Ci::Runner.first.active).to be true
- end
+ let(:expected_params) { { client_id: "runner/#{new_runner.id}" } }
end
- context 'when active is set to false' do
- it 'creates runner' do
- post api('/runners'), params: {
- token: registration_token,
- active: false
- }
-
- expect(response).to have_gitlab_http_status(:created)
- expect(::Ci::Runner.first.active).to be false
- end
+ it_behaves_like 'not executing any extra queries for the application context' do
+ let(:subject_proc) { proc { request } }
end
end
- context 'when access_level is provided for Runner' do
- context 'when access_level is set to ref_protected' do
- it 'creates runner' do
- post api('/runners'), params: {
- token: registration_token,
- access_level: 'ref_protected'
- }
-
- expect(response).to have_gitlab_http_status(:created)
- expect(::Ci::Runner.first.ref_protected?).to be true
- end
- end
+ context 'calling actual register service' do
+ include StubGitlabCalls
- context 'when access_level is set to not_protected' do
- it 'creates runner' do
- post api('/runners'), params: {
- token: registration_token,
- access_level: 'not_protected'
- }
+ let(:registration_token) { 'abcdefg123456' }
- expect(response).to have_gitlab_http_status(:created)
- expect(::Ci::Runner.first.ref_protected?).to be false
- end
+ before do
+ stub_gitlab_calls
+ stub_application_setting(runners_registration_token: registration_token)
+ allow_any_instance_of(::Ci::Runner).to receive(:cache_attributes)
end
- end
-
- context 'when maximum job timeout is specified' do
- it 'creates runner' do
- post api('/runners'), params: {
- token: registration_token,
- maximum_timeout: 9000
- }
- expect(response).to have_gitlab_http_status(:created)
- expect(::Ci::Runner.first.maximum_timeout).to eq(9000)
- end
+ %w(name version revision platform architecture).each do |param|
+ context "when info parameter '#{param}' info is present" do
+ let(:value) { "#{param}_value" }
- context 'when maximum job timeout is empty' do
- it 'creates runner' do
- post api('/runners'), params: {
- token: registration_token,
- maximum_timeout: ''
- }
+ it "updates provided Runner's parameter" do
+ post api('/runners'), params: {
+ token: registration_token,
+ info: { param => value }
+ }
- expect(response).to have_gitlab_http_status(:created)
- expect(::Ci::Runner.first.maximum_timeout).to be_nil
+ expect(response).to have_gitlab_http_status(:created)
+ expect(::Ci::Runner.last.read_attribute(param.to_sym)).to eq(value)
+ end
end
end
- end
- %w(name version revision platform architecture).each do |param|
- context "when info parameter '#{param}' info is present" do
- let(:value) { "#{param}_value" }
+ it "sets the runner's ip_address" do
+ post api('/runners'),
+ params: { token: registration_token },
+ headers: { 'X-Forwarded-For' => '123.111.123.111' }
- it "updates provided Runner's parameter" do
- post api('/runners'), params: {
- token: registration_token,
- info: { param => value }
- }
-
- expect(response).to have_gitlab_http_status(:created)
- expect(::Ci::Runner.first.read_attribute(param.to_sym)).to eq(value)
- end
+ expect(response).to have_gitlab_http_status(:created)
+ expect(::Ci::Runner.last.ip_address).to eq('123.111.123.111')
end
end
-
- it "sets the runner's ip_address" do
- post api('/runners'),
- params: { token: registration_token },
- headers: { 'X-Forwarded-For' => '123.111.123.111' }
-
- expect(response).to have_gitlab_http_status(:created)
- expect(::Ci::Runner.first.ip_address).to eq('123.111.123.111')
- end
end
end
end
diff --git a/spec/requests/api/ci/runners_spec.rb b/spec/requests/api/ci/runners_spec.rb
index 6ca380a3cb9..305c0bd9df0 100644
--- a/spec/requests/api/ci/runners_spec.rb
+++ b/spec/requests/api/ci/runners_spec.rb
@@ -980,7 +980,7 @@ RSpec.describe API::Ci::Runners do
end
end
- describe 'GET /groups/:id/runners' do
+ shared_context 'GET /groups/:id/runners' do
context 'authorized user with maintainer privileges' do
it 'returns all runners' do
get api("/groups/#{group.id}/runners", user)
@@ -1048,6 +1048,16 @@ RSpec.describe API::Ci::Runners do
end
end
+ it_behaves_like 'GET /groups/:id/runners'
+
+ context 'when the FF ci_find_runners_by_ci_mirrors is disabled' do
+ before do
+ stub_feature_flags(ci_find_runners_by_ci_mirrors: false)
+ end
+
+ it_behaves_like 'GET /groups/:id/runners'
+ end
+
describe 'POST /projects/:id/runners' do
context 'authorized user' do
let_it_be(:project_runner2) { create(:ci_runner, :project, projects: [project2]) }
diff --git a/spec/requests/api/ci/triggers_spec.rb b/spec/requests/api/ci/triggers_spec.rb
index d270a16d28d..a036a55f5f3 100644
--- a/spec/requests/api/ci/triggers_spec.rb
+++ b/spec/requests/api/ci/triggers_spec.rb
@@ -162,7 +162,7 @@ RSpec.describe API::Ci::Triggers do
expect do
post api("/projects/#{project.id}/ref/master/trigger/pipeline?token=#{trigger_token}"),
params: { ref: 'refs/heads/other-branch' },
- headers: { WebHookService::GITLAB_EVENT_HEADER => 'Pipeline Hook' }
+ headers: { ::Gitlab::WebHooks::GITLAB_EVENT_HEADER => 'Pipeline Hook' }
end.not_to change(Ci::Pipeline, :count)
expect(response).to have_gitlab_http_status(:forbidden)
diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb
index 1e587480fd9..2bc642f8b14 100644
--- a/spec/requests/api/commits_spec.rb
+++ b/spec/requests/api/commits_spec.rb
@@ -1056,9 +1056,7 @@ RSpec.describe API::Commits do
shared_examples_for 'ref with pipeline' do
let!(:pipeline) do
- project
- .ci_pipelines
- .create!(source: :push, ref: 'master', sha: commit.sha, protected: false)
+ create(:ci_empty_pipeline, project: project, status: :created, source: :push, ref: 'master', sha: commit.sha, protected: false)
end
it 'includes status as "created" and a last_pipeline object' do
@@ -1090,9 +1088,7 @@ RSpec.describe API::Commits do
shared_examples_for 'ref with unaccessible pipeline' do
let!(:pipeline) do
- project
- .ci_pipelines
- .create!(source: :push, ref: 'master', sha: commit.sha, protected: false)
+ create(:ci_empty_pipeline, project: project, status: :created, source: :push, ref: 'master', sha: commit.sha, protected: false)
end
it 'does not include last_pipeline' do
diff --git a/spec/requests/api/generic_packages_spec.rb b/spec/requests/api/generic_packages_spec.rb
index 2d85d7b9583..1836233594d 100644
--- a/spec/requests/api/generic_packages_spec.rb
+++ b/spec/requests/api/generic_packages_spec.rb
@@ -574,6 +574,27 @@ RSpec.describe API::GenericPackages do
end
end
+ context 'with package status' do
+ where(:package_status, :expected_status) do
+ :default | :success
+ :hidden | :success
+ :error | :not_found
+ end
+
+ with_them do
+ before do
+ project.add_developer(user)
+ package.update!(status: package_status)
+ end
+
+ it "responds with #{params[:expected_status]}" do
+ download_file(personal_access_token_header)
+
+ expect(response).to have_gitlab_http_status(expected_status)
+ end
+ end
+ end
+
context 'event tracking' do
let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace, user: user } }
diff --git a/spec/requests/api/graphql/ci/config_spec.rb b/spec/requests/api/graphql/ci/config_spec.rb
index 8ede6e1538c..755585f8e0e 100644
--- a/spec/requests/api/graphql/ci/config_spec.rb
+++ b/spec/requests/api/graphql/ci/config_spec.rb
@@ -20,6 +20,7 @@ RSpec.describe 'Query.ciConfig' do
ciConfig(projectPath: "#{project.full_path}", content: "#{content}", dryRun: false) {
status
errors
+ warnings
stages {
nodes {
name
@@ -73,6 +74,7 @@ RSpec.describe 'Query.ciConfig' do
expect(graphql_data['ciConfig']).to eq(
"status" => "VALID",
"errors" => [],
+ "warnings" => [],
"stages" =>
{
"nodes" =>
@@ -220,6 +222,21 @@ RSpec.describe 'Query.ciConfig' do
)
end
+ context 'when using deprecated keywords' do
+ let_it_be(:content) do
+ YAML.dump(
+ rspec: { script: 'ls' },
+ types: ['test']
+ )
+ end
+
+ it 'returns a warning' do
+ post_graphql_query
+
+ expect(graphql_data['ciConfig']['warnings']).to include('root `types` is deprecated in 9.0 and will be removed in 15.0.')
+ end
+ end
+
context 'when the config file includes other files' do
let_it_be(:content) do
YAML.dump(
@@ -250,6 +267,7 @@ RSpec.describe 'Query.ciConfig' do
expect(graphql_data['ciConfig']).to eq(
"status" => "VALID",
"errors" => [],
+ "warnings" => [],
"stages" =>
{
"nodes" =>
diff --git a/spec/requests/api/graphql/ci/jobs_spec.rb b/spec/requests/api/graphql/ci/jobs_spec.rb
index 3a1df3525ef..b191b585d06 100644
--- a/spec/requests/api/graphql/ci/jobs_spec.rb
+++ b/spec/requests/api/graphql/ci/jobs_spec.rb
@@ -44,6 +44,10 @@ RSpec.describe 'Query.project.pipeline' do
name
jobs {
nodes {
+ downstreamPipeline {
+ id
+ path
+ }
name
needs {
nodes { #{all_graphql_fields_for('CiBuildNeed')} }
@@ -131,6 +135,8 @@ RSpec.describe 'Query.project.pipeline' do
end
it 'does not generate N+1 queries', :request_store, :use_sql_query_cache do
+ create(:ci_bridge, name: 'bridge-1', pipeline: pipeline, downstream_pipeline: create(:ci_pipeline))
+
post_graphql(query, current_user: user)
control = ActiveRecord::QueryRecorder.new(skip_cached: false) do
@@ -139,6 +145,8 @@ RSpec.describe 'Query.project.pipeline' do
create(:ci_build, name: 'test-a', pipeline: pipeline)
create(:ci_build, name: 'test-b', pipeline: pipeline)
+ create(:ci_bridge, name: 'bridge-2', pipeline: pipeline, downstream_pipeline: create(:ci_pipeline))
+ create(:ci_bridge, name: 'bridge-3', pipeline: pipeline, downstream_pipeline: create(:ci_pipeline))
expect do
post_graphql(query, current_user: user)
diff --git a/spec/requests/api/graphql/ci/pipelines_spec.rb b/spec/requests/api/graphql/ci/pipelines_spec.rb
index 95ddd0250e7..5ae68be46a2 100644
--- a/spec/requests/api/graphql/ci/pipelines_spec.rb
+++ b/spec/requests/api/graphql/ci/pipelines_spec.rb
@@ -12,6 +12,38 @@ RSpec.describe 'Query.project(fullPath).pipelines' do
travel_to(Time.current) { example.run }
end
+ describe 'sha' do
+ let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
+
+ let(:pipelines_graphql_data) { graphql_data.dig(*%w[project pipelines nodes]).first }
+
+ let(:query) do
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ pipelines {
+ nodes {
+ fullSha: sha
+ shortSha: sha(format: SHORT)
+ alsoFull: sha(format: LONG)
+ }
+ }
+ }
+ }
+ )
+ end
+
+ it 'returns all formats of the SHA' do
+ post_graphql(query, current_user: user)
+
+ expect(pipelines_graphql_data).to include(
+ 'fullSha' => eq(pipeline.sha),
+ 'alsoFull' => eq(pipeline.sha),
+ 'shortSha' => eq(pipeline.short_sha)
+ )
+ end
+ end
+
describe 'duration fields' do
let_it_be(:pipeline) do
create(:ci_pipeline, project: project)
@@ -251,6 +283,50 @@ RSpec.describe 'Query.project(fullPath).pipelines' do
end
end
+ describe 'warningMessages' do
+ let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
+ let_it_be(:warning_message) { create(:ci_pipeline_message, pipeline: pipeline, content: 'warning') }
+
+ let(:pipelines_graphql_data) { graphql_data.dig(*%w[project pipelines nodes]).first }
+
+ let(:query) do
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ pipelines {
+ nodes {
+ warningMessages {
+ content
+ }
+ }
+ }
+ }
+ }
+ )
+ end
+
+ it 'returns pipeline warnings' do
+ post_graphql(query, current_user: user)
+
+ expect(pipelines_graphql_data['warningMessages']).to contain_exactly(
+ a_hash_including('content' => 'warning')
+ )
+ end
+
+ it 'avoids N+1 queries' do
+ control_count = ActiveRecord::QueryRecorder.new do
+ post_graphql(query, current_user: user)
+ end
+
+ pipeline_2 = create(:ci_pipeline, project: project)
+ create(:ci_pipeline_message, pipeline: pipeline_2, content: 'warning')
+
+ expect do
+ post_graphql(query, current_user: user)
+ end.not_to exceed_query_limit(control_count)
+ end
+ end
+
describe '.jobs(securityReportTypes)' do
let_it_be(:query) do
%(
@@ -420,4 +496,36 @@ RSpec.describe 'Query.project(fullPath).pipelines' do
end
end
end
+
+ describe 'ref_path' do
+ let_it_be(:merge_request) { create(:merge_request, source_project: project) }
+ let_it_be(:pipeline_1) { create(:ci_pipeline, project: project, user: user, merge_request: merge_request) }
+ let_it_be(:pipeline_2) { create(:ci_pipeline, project: project, user: user, merge_request: merge_request) }
+
+ let(:query) do
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ pipelines {
+ nodes {
+ refPath
+ }
+ }
+ }
+ }
+ )
+ end
+
+ it 'avoids N+1 queries' do
+ control_count = ActiveRecord::QueryRecorder.new do
+ post_graphql(query, current_user: user)
+ end
+
+ create(:ci_pipeline, project: project, user: user, merge_request: merge_request)
+
+ expect do
+ post_graphql(query, current_user: user)
+ end.not_to exceed_query_limit(control_count)
+ end
+ end
end
diff --git a/spec/requests/api/graphql/ci/runner_spec.rb b/spec/requests/api/graphql/ci/runner_spec.rb
index 98d3a3b1c51..8c919b48849 100644
--- a/spec/requests/api/graphql/ci/runner_spec.rb
+++ b/spec/requests/api/graphql/ci/runner_spec.rb
@@ -11,7 +11,7 @@ RSpec.describe 'Query.runner(id)' do
let_it_be(:active_instance_runner) do
create(:ci_runner, :instance, description: 'Runner 1', contacted_at: 2.hours.ago,
active: true, version: 'adfe156', revision: 'a', locked: true, ip_address: '127.0.0.1', maximum_timeout: 600,
- access_level: 0, tag_list: %w[tag1 tag2], run_untagged: true)
+ access_level: 0, tag_list: %w[tag1 tag2], run_untagged: true, executor_type: :custom)
end
let_it_be(:inactive_instance_runner) do
@@ -22,7 +22,7 @@ RSpec.describe 'Query.runner(id)' do
let_it_be(:active_group_runner) do
create(:ci_runner, :group, groups: [group], description: 'Group runner 1', contacted_at: 2.hours.ago,
active: true, version: 'adfe156', revision: 'a', locked: true, ip_address: '127.0.0.1', maximum_timeout: 600,
- access_level: 0, tag_list: %w[tag1 tag2], run_untagged: true)
+ access_level: 0, tag_list: %w[tag1 tag2], run_untagged: true, executor_type: :shell)
end
def get_runner(id)
@@ -57,6 +57,7 @@ RSpec.describe 'Query.runner(id)' do
expect(runner_data).to match a_hash_including(
'id' => "gid://gitlab/Ci::Runner/#{runner.id}",
'description' => runner.description,
+ 'createdAt' => runner.created_at&.iso8601,
'contactedAt' => runner.contacted_at&.iso8601,
'version' => runner.version,
'shortSha' => runner.short_sha,
@@ -69,6 +70,7 @@ RSpec.describe 'Query.runner(id)' do
'runUntagged' => runner.run_untagged,
'ipAddress' => runner.ip_address,
'runnerType' => runner.instance_type? ? 'INSTANCE_TYPE' : 'PROJECT_TYPE',
+ 'executorName' => runner.executor_type&.dasherize,
'jobCount' => 0,
'projectCount' => nil,
'adminUrl' => "http://localhost/admin/runners/#{runner.id}",
diff --git a/spec/requests/api/graphql/group/group_members_spec.rb b/spec/requests/api/graphql/group/group_members_spec.rb
index 31cb0393d7f..06afb5b9a49 100644
--- a/spec/requests/api/graphql/group/group_members_spec.rb
+++ b/spec/requests/api/graphql/group/group_members_spec.rb
@@ -56,12 +56,16 @@ RSpec.describe 'getting group members information' do
context 'member relations' do
let_it_be(:child_group) { create(:group, :public, parent: parent_group) }
let_it_be(:grandchild_group) { create(:group, :public, parent: child_group) }
+ let_it_be(:invited_group) { create(:group, :public) }
let_it_be(:child_user) { create(:user) }
let_it_be(:grandchild_user) { create(:user) }
+ let_it_be(:invited_user) { create(:user) }
+ let_it_be(:group_link) { create(:group_group_link, shared_group: child_group, shared_with_group: invited_group) }
before_all do
child_group.add_guest(child_user)
grandchild_group.add_guest(grandchild_user)
+ invited_group.add_guest(invited_user)
end
it 'returns direct members' do
@@ -71,6 +75,13 @@ RSpec.describe 'getting group members information' do
expect_array_response(child_user)
end
+ it 'returns invited members plus inherited members' do
+ fetch_members(group: child_group, args: { relations: [:DIRECT, :INHERITED, :SHARED_FROM_GROUPS] })
+
+ expect(graphql_errors).to be_nil
+ expect_array_response(invited_user, user_1, user_2, child_user)
+ end
+
it 'returns direct and inherited members' do
fetch_members(group: child_group, args: { relations: [:DIRECT, :INHERITED] })
diff --git a/spec/requests/api/graphql/group/work_item_types_spec.rb b/spec/requests/api/graphql/group/work_item_types_spec.rb
new file mode 100644
index 00000000000..0667e09d1e9
--- /dev/null
+++ b/spec/requests/api/graphql/group/work_item_types_spec.rb
@@ -0,0 +1,71 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'getting a list of work item types for a group' do
+ include GraphqlHelpers
+
+ let_it_be(:developer) { create(:user) }
+ let_it_be(:group) { create(:group, :private) }
+
+ before_all do
+ group.add_developer(developer)
+ end
+
+ let(:current_user) { developer }
+
+ let(:fields) do
+ <<~GRAPHQL
+ workItemTypes{
+ nodes { id name iconName }
+ }
+ GRAPHQL
+ end
+
+ let(:query) do
+ graphql_query_for(
+ 'group',
+ { 'fullPath' => group.full_path },
+ fields
+ )
+ end
+
+ context 'when user has access to the group' do
+ before do
+ post_graphql(query, current_user: current_user)
+ end
+
+ it_behaves_like 'a working graphql query'
+
+ it 'returns all default work item types' do
+ expect(graphql_data.dig('group', 'workItemTypes', 'nodes')).to match_array(
+ WorkItems::Type.default.map do |type|
+ hash_including('id' => type.to_global_id.to_s, 'name' => type.name, 'iconName' => type.icon_name)
+ end
+ )
+ end
+ end
+
+ context "when user doesn't have acces to the group" do
+ let(:current_user) { create(:user) }
+
+ before do
+ post_graphql(query, current_user: current_user)
+ end
+
+ it 'does not return the group' do
+ expect(graphql_data).to eq('group' => nil)
+ end
+ end
+
+ context 'when the work_items feature flag is disabled' do
+ before do
+ stub_feature_flags(work_items: false)
+ post_graphql(query, current_user: current_user)
+ end
+
+ it 'makes the workItemTypes field unavailable' do
+ expect(graphql_errors).to contain_exactly(hash_including("message" => "Field 'workItemTypes' doesn't exist on type 'Group'"))
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/issues/set_crm_contacts_spec.rb b/spec/requests/api/graphql/mutations/issues/set_crm_contacts_spec.rb
index 2da69509ad6..79d687a2bdb 100644
--- a/spec/requests/api/graphql/mutations/issues/set_crm_contacts_spec.rb
+++ b/spec/requests/api/graphql/mutations/issues/set_crm_contacts_spec.rb
@@ -6,13 +6,18 @@ RSpec.describe 'Setting issues crm contacts' do
include GraphqlHelpers
let_it_be(:user) { create(:user) }
- let_it_be(:group) { create(:group) }
- let_it_be(:project) { create(:project, group: group) }
- let_it_be(:contacts) { create_list(:contact, 4, group: group) }
+ let_it_be(:group) { create(:group, :crm_enabled) }
+ let_it_be(:subgroup) { create(:group, :crm_enabled, parent: group) }
+ let_it_be(:project) { create(:project, group: subgroup) }
+ let_it_be(:group_contacts) { create_list(:contact, 4, group: group) }
+ let_it_be(:subgroup_contacts) { create_list(:contact, 4, group: subgroup) }
let(:issue) { create(:issue, project: project) }
let(:operation_mode) { Types::MutationOperationModeEnum.default_mode }
- let(:contact_ids) { [global_id_of(contacts[1]), global_id_of(contacts[2])] }
+ let(:contacts) { subgroup_contacts }
+ let(:initial_contacts) { contacts[0..1] }
+ let(:mutation_contacts) { contacts[1..2] }
+ let(:contact_ids) { contact_global_ids(mutation_contacts) }
let(:does_not_exist_or_no_permission) { "The resource that you are attempting to access does not exist or you don't have permission to perform this action" }
let(:mutation) do
@@ -42,9 +47,47 @@ RSpec.describe 'Setting issues crm contacts' do
graphql_mutation_response(:issue_set_crm_contacts)
end
+ def contact_global_ids(contacts)
+ contacts.map { |contact| global_id_of(contact) }
+ end
+
before do
- create(:issue_customer_relations_contact, issue: issue, contact: contacts[0])
- create(:issue_customer_relations_contact, issue: issue, contact: contacts[1])
+ initial_contacts.each { |contact| create(:issue_customer_relations_contact, issue: issue, contact: contact) }
+ end
+
+ shared_examples 'successful mutation' do
+ context 'replace' do
+ it 'updates the issue with correct contacts' do
+ post_graphql_mutation(mutation, current_user: user)
+
+ expect(graphql_data_at(:issue_set_crm_contacts, :issue, :customer_relations_contacts, :nodes, :id))
+ .to match_array(contact_global_ids(mutation_contacts))
+ end
+ end
+
+ context 'append' do
+ let(:mutation_contacts) { [contacts[3]] }
+ let(:operation_mode) { Types::MutationOperationModeEnum.enum[:append] }
+
+ it 'updates the issue with correct contacts' do
+ post_graphql_mutation(mutation, current_user: user)
+
+ expect(graphql_data_at(:issue_set_crm_contacts, :issue, :customer_relations_contacts, :nodes, :id))
+ .to match_array(contact_global_ids(initial_contacts + mutation_contacts))
+ end
+ end
+
+ context 'remove' do
+ let(:mutation_contacts) { [contacts[0]] }
+ let(:operation_mode) { Types::MutationOperationModeEnum.enum[:remove] }
+
+ it 'updates the issue with correct contacts' do
+ post_graphql_mutation(mutation, current_user: user)
+
+ expect(graphql_data_at(:issue_set_crm_contacts, :issue, :customer_relations_contacts, :nodes, :id))
+ .to match_array(contact_global_ids(initial_contacts - mutation_contacts))
+ end
+ end
end
context 'when the user has no permission' do
@@ -73,37 +116,14 @@ RSpec.describe 'Setting issues crm contacts' do
end
end
- context 'replace' do
- it 'updates the issue with correct contacts' do
- post_graphql_mutation(mutation, current_user: user)
-
- expect(graphql_data_at(:issue_set_crm_contacts, :issue, :customer_relations_contacts, :nodes, :id))
- .to match_array([global_id_of(contacts[1]), global_id_of(contacts[2])])
- end
- end
+ context 'with issue group contacts' do
+ let(:contacts) { subgroup_contacts }
- context 'append' do
- let(:contact_ids) { [global_id_of(contacts[3])] }
- let(:operation_mode) { Types::MutationOperationModeEnum.enum[:append] }
-
- it 'updates the issue with correct contacts' do
- post_graphql_mutation(mutation, current_user: user)
-
- expect(graphql_data_at(:issue_set_crm_contacts, :issue, :customer_relations_contacts, :nodes, :id))
- .to match_array([global_id_of(contacts[0]), global_id_of(contacts[1]), global_id_of(contacts[3])])
- end
+ it_behaves_like 'successful mutation'
end
- context 'remove' do
- let(:contact_ids) { [global_id_of(contacts[0])] }
- let(:operation_mode) { Types::MutationOperationModeEnum.enum[:remove] }
-
- it 'updates the issue with correct contacts' do
- post_graphql_mutation(mutation, current_user: user)
-
- expect(graphql_data_at(:issue_set_crm_contacts, :issue, :customer_relations_contacts, :nodes, :id))
- .to match_array([global_id_of(contacts[1])])
- end
+ context 'with issue ancestor group contacts' do
+ it_behaves_like 'successful mutation'
end
context 'when the contact does not exist' do
@@ -118,7 +138,7 @@ RSpec.describe 'Setting issues crm contacts' do
end
context 'when the contact belongs to a different group' do
- let(:group2) { create(:group) }
+ let(:group2) { create(:group, :crm_enabled) }
let(:contact) { create(:contact, group: group2) }
let(:contact_ids) { [global_id_of(contact)] }
@@ -158,4 +178,17 @@ RSpec.describe 'Setting issues crm contacts' do
end
end
end
+
+ context 'when crm_enabled is false' do
+ let(:issue) { create(:issue) }
+ let(:initial_contacts) { [] }
+
+ it 'raises expected error' do
+ issue.project.add_reporter(user)
+
+ post_graphql_mutation(mutation, current_user: user)
+
+ expect(graphql_errors).to include(a_hash_including('message' => 'Feature disabled'))
+ end
+ end
end
diff --git a/spec/requests/api/graphql/mutations/issues/set_escalation_status_spec.rb b/spec/requests/api/graphql/mutations/issues/set_escalation_status_spec.rb
new file mode 100644
index 00000000000..0166871502b
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/issues/set_escalation_status_spec.rb
@@ -0,0 +1,82 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Setting the escalation status of an incident' do
+ include GraphqlHelpers
+
+ let_it_be(:project) { create(:project) }
+ let_it_be(:issue) { create(:incident, project: project) }
+ let_it_be(:escalation_status) { create(:incident_management_issuable_escalation_status, issue: issue) }
+ let_it_be(:user) { create(:user) }
+
+ let(:status) { 'ACKNOWLEDGED' }
+ let(:input) { { project_path: project.full_path, iid: issue.iid.to_s, status: status } }
+
+ let(:current_user) { user }
+ let(:mutation) do
+ graphql_mutation(:issue_set_escalation_status, input) do
+ <<~QL
+ clientMutationId
+ errors
+ issue {
+ iid
+ escalationStatus
+ }
+ QL
+ end
+ end
+
+ let(:mutation_response) { graphql_mutation_response(:issue_set_escalation_status) }
+
+ before_all do
+ project.add_developer(user)
+ end
+
+ context 'when user does not have permission to edit the escalation status' do
+ let(:current_user) { create(:user) }
+
+ before_all do
+ project.add_reporter(user)
+ end
+
+ it_behaves_like 'a mutation that returns a top-level access error'
+ end
+
+ context 'with non-incident issue is provided' do
+ let_it_be(:issue) { create(:issue, project: project) }
+
+ it_behaves_like 'a mutation that returns top-level errors', errors: ['Feature unavailable for provided issue']
+ end
+
+ context 'with feature disabled' do
+ before do
+ stub_feature_flags(incident_escalations: false)
+ end
+
+ it_behaves_like 'a mutation that returns top-level errors', errors: ['Feature unavailable for provided issue']
+ end
+
+ it 'sets given escalation_policy to the escalation status for the issue' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_response['errors']).to be_empty
+ expect(mutation_response['issue']['escalationStatus']).to eq(status)
+ expect(escalation_status.reload.status_name).to eq(:acknowledged)
+ end
+
+ context 'when status argument is not given' do
+ let(:input) { {} }
+
+ it_behaves_like 'a mutation that returns top-level errors' do
+ let(:match_errors) { contain_exactly(include('status (Expected value to not be null)')) }
+ end
+ end
+
+ context 'when status argument is invalid' do
+ let(:status) { 'INVALID' }
+
+ it_behaves_like 'an invalid argument to the mutation', argument_name: :status
+ end
+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
new file mode 100644
index 00000000000..e7a0c7753fb
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/work_items/create_spec.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Create a work item' do
+ include GraphqlHelpers
+
+ let_it_be(:project) { create(:project) }
+ let_it_be(:developer) { create(:user).tap { |user| project.add_developer(user) } }
+
+ let(:input) do
+ {
+ 'title' => 'new title',
+ 'description' => 'new description',
+ 'workItemTypeId' => WorkItems::Type.default_by_type(:task).to_global_id.to_s
+ }
+ end
+
+ let(:mutation) { graphql_mutation(:workItemCreate, input.merge('projectPath' => project.full_path)) }
+
+ let(:mutation_response) { graphql_mutation_response(:work_item_create) }
+
+ context 'the user is not allowed to create a work item' do
+ let(:current_user) { create(:user) }
+
+ it_behaves_like 'a mutation that returns a top-level access error'
+ end
+
+ context 'when user has permissions to create a work item' do
+ let(:current_user) { developer }
+
+ it 'creates the work item' do
+ expect do
+ post_graphql_mutation(mutation, current_user: current_user)
+ end.to change(WorkItem, :count).by(1)
+
+ created_work_item = WorkItem.last
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(created_work_item.issue_type).to eq('task')
+ expect(created_work_item.work_item_type.base_type).to eq('task')
+ expect(mutation_response['workItem']).to include(
+ input.except('workItemTypeId').merge(
+ 'id' => created_work_item.to_global_id.to_s,
+ 'workItemType' => hash_including('name' => 'Task')
+ )
+ )
+ end
+
+ it_behaves_like 'has spam protection' do
+ let(:mutation_class) { ::Mutations::WorkItems::Create }
+ end
+
+ context 'when the work_items feature flag is disabled' do
+ before do
+ stub_feature_flags(work_items: false)
+ end
+
+ it_behaves_like 'a mutation that returns top-level errors',
+ errors: ["Field 'workItemCreate' doesn't exist on type 'Mutation'", "Variable $workItemCreateInput is declared by anonymous mutation but not used"]
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/packages/package_spec.rb b/spec/requests/api/graphql/packages/package_spec.rb
index a9019a7611a..2ff3bc7cc47 100644
--- a/spec/requests/api/graphql/packages/package_spec.rb
+++ b/spec/requests/api/graphql/packages/package_spec.rb
@@ -4,7 +4,9 @@ require 'spec_helper'
RSpec.describe 'package details' do
include GraphqlHelpers
- let_it_be_with_reload(:project) { create(:project) }
+ let_it_be_with_reload(:group) { create(:group) }
+ let_it_be_with_reload(:project) { create(:project, group: group) }
+ let_it_be(:user) { create(:user) }
let_it_be(:composer_package) { create(:composer_package, project: project) }
let_it_be(:composer_json) { { name: 'name', type: 'type', license: 'license', version: 1 } }
let_it_be(:composer_metadatum) do
@@ -17,7 +19,6 @@ RSpec.describe 'package details' do
let(:excluded) { %w[metadata apiFuzzingCiConfiguration pipeline packageFiles] }
let(:metadata) { query_graphql_fragment('ComposerMetadata') }
let(:package_files) {all_graphql_fields_for('PackageFile')}
- let(:user) { project.owner }
let(:package_global_id) { global_id_of(composer_package) }
let(:package_details) { graphql_data_at(:package) }
@@ -37,145 +38,198 @@ RSpec.describe 'package details' do
subject { post_graphql(query, current_user: user) }
- it_behaves_like 'a working graphql query' do
+ context 'with unauthorized user' do
before do
- subject
+ project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
end
- it 'matches the JSON schema' do
- expect(package_details).to match_schema('graphql/packages/package_details')
+ it 'returns no packages' do
+ subject
+
+ expect(graphql_data_at(:package)).to be_nil
end
end
- context 'there are other versions of this package' do
- let(:depth) { 3 }
- let(:excluded) { %w[metadata project tags pipelines] } # to limit the query complexity
-
- let_it_be(:siblings) { create_list(:composer_package, 2, project: project, name: composer_package.name) }
+ context 'with authorized user' do
+ before do
+ project.add_developer(user)
+ end
- it 'includes the sibling versions' do
- subject
+ it_behaves_like 'a working graphql query' do
+ before do
+ subject
+ end
- expect(graphql_data_at(:package, :versions, :nodes)).to match_array(
- siblings.map { |p| a_hash_including('id' => global_id_of(p)) }
- )
+ it 'matches the JSON schema' do
+ expect(package_details).to match_schema('graphql/packages/package_details')
+ end
end
- context 'going deeper' do
- let(:depth) { 6 }
+ context 'there are other versions of this package' do
+ let(:depth) { 3 }
+ let(:excluded) { %w[metadata project tags pipelines] } # to limit the query complexity
- it 'does not create a cycle of versions' do
+ let_it_be(:siblings) { create_list(:composer_package, 2, project: project, name: composer_package.name) }
+
+ it 'includes the sibling versions' do
subject
- expect(graphql_data_at(:package, :versions, :nodes, :version)).to be_present
- expect(graphql_data_at(:package, :versions, :nodes, :versions, :nodes)).to eq [nil, nil]
+ expect(graphql_data_at(:package, :versions, :nodes)).to match_array(
+ siblings.map { |p| a_hash_including('id' => global_id_of(p)) }
+ )
end
- end
- end
- context 'with a batched query' do
- let_it_be(:conan_package) { create(:conan_package, project: project) }
+ context 'going deeper' do
+ let(:depth) { 6 }
- let(:batch_query) do
- <<~QUERY
- {
- a: package(id: "#{global_id_of(composer_package)}") { name }
- b: package(id: "#{global_id_of(conan_package)}") { name }
- }
- QUERY
+ it 'does not create a cycle of versions' do
+ subject
+
+ expect(graphql_data_at(:package, :versions, :nodes, :version)).to be_present
+ expect(graphql_data_at(:package, :versions, :nodes, :versions, :nodes)).to match_array [nil, nil]
+ end
+ end
end
- let(:a_packages_names) { graphql_data_at(:a, :packages, :nodes, :name) }
+ context 'with package files pending destruction' do
+ let_it_be(:package_file) { create(:package_file, package: composer_package) }
+ let_it_be(:package_file_pending_destruction) { create(:package_file, :pending_destruction, package: composer_package) }
- it 'returns an error for the second package and data for the first' do
- post_graphql(batch_query, current_user: user)
+ let(:package_file_ids) { graphql_data_at(:package, :package_files, :nodes).map { |node| node["id"] } }
- expect(graphql_data_at(:a, :name)).to eq(composer_package.name)
+ it 'does not return them' do
+ subject
- expect_graphql_errors_to_include [/Package details can be requested only for one package at a time/]
- expect(graphql_data_at(:b)).to be(nil)
- end
- end
+ expect(package_file_ids).to contain_exactly(package_file.to_global_id.to_s)
+ end
- context 'with unauthorized user' do
- let_it_be(:user) { create(:user) }
+ context 'with packages_installable_package_files disabled' do
+ before do
+ stub_feature_flags(packages_installable_package_files: false)
+ end
- before do
- project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
+ it 'returns them' do
+ subject
+
+ expect(package_file_ids).to contain_exactly(package_file_pending_destruction.to_global_id.to_s, package_file.to_global_id.to_s)
+ end
+ end
end
- it 'returns no packages' do
- subject
+ context 'with a batched query' do
+ let_it_be(:conan_package) { create(:conan_package, project: project) }
- expect(graphql_data_at(:package)).to be_nil
- end
- end
+ let(:batch_query) do
+ <<~QUERY
+ {
+ a: package(id: "#{global_id_of(composer_package)}") { name }
+ b: package(id: "#{global_id_of(conan_package)}") { name }
+ }
+ QUERY
+ end
- context 'pipelines field', :aggregate_failures do
- let(:pipelines) { create_list(:ci_pipeline, 6, project: project) }
- let(:pipeline_gids) { pipelines.sort_by(&:id).map(&:to_gid).map(&:to_s).reverse }
+ let(:a_packages_names) { graphql_data_at(:a, :packages, :nodes, :name) }
- before do
- composer_package.pipelines = pipelines
- composer_package.save!
- end
+ it 'returns an error for the second package and data for the first' do
+ post_graphql(batch_query, current_user: user)
- def run_query(args)
- pipelines_nodes = <<~QUERY
- nodes {
- id
- }
- pageInfo {
- startCursor
- endCursor
- }
- QUERY
+ expect(graphql_data_at(:a, :name)).to eq(composer_package.name)
- query = graphql_query_for(:package, { id: package_global_id }, query_graphql_field("pipelines", args, pipelines_nodes))
- post_graphql(query, current_user: user)
+ expect_graphql_errors_to_include [/Package details can be requested only for one package at a time/]
+ expect(graphql_data_at(:b)).to be(nil)
+ end
end
- it 'loads the second page with pagination first correctly' do
- run_query(first: 2)
- pipeline_ids = graphql_data.dig('package', 'pipelines', 'nodes').pluck('id')
+ context 'pipelines field', :aggregate_failures do
+ let(:pipelines) { create_list(:ci_pipeline, 6, project: project) }
+ let(:pipeline_gids) { pipelines.sort_by(&:id).map(&:to_gid).map(&:to_s).reverse }
- expect(pipeline_ids).to eq(pipeline_gids[0..1])
+ before do
+ composer_package.pipelines = pipelines
+ composer_package.save!
+ end
- cursor = graphql_data.dig('package', 'pipelines', 'pageInfo', 'endCursor')
+ def run_query(args)
+ pipelines_nodes = <<~QUERY
+ nodes {
+ id
+ }
+ pageInfo {
+ startCursor
+ endCursor
+ }
+ QUERY
+
+ query = graphql_query_for(:package, { id: package_global_id }, query_graphql_field("pipelines", args, pipelines_nodes))
+ post_graphql(query, current_user: user)
+ end
- run_query(first: 2, after: cursor)
+ it 'loads the second page with pagination first correctly' do
+ run_query(first: 2)
+ pipeline_ids = graphql_data.dig('package', 'pipelines', 'nodes').pluck('id')
- pipeline_ids = graphql_data.dig('package', 'pipelines', 'nodes').pluck('id')
+ expect(pipeline_ids).to eq(pipeline_gids[0..1])
- expect(pipeline_ids).to eq(pipeline_gids[2..3])
- end
+ cursor = graphql_data.dig('package', 'pipelines', 'pageInfo', 'endCursor')
- it 'loads the second page with pagination last correctly' do
- run_query(last: 2)
- pipeline_ids = graphql_data.dig('package', 'pipelines', 'nodes').pluck('id')
+ run_query(first: 2, after: cursor)
- expect(pipeline_ids).to eq(pipeline_gids[4..5])
+ pipeline_ids = graphql_data.dig('package', 'pipelines', 'nodes').pluck('id')
- cursor = graphql_data.dig('package', 'pipelines', 'pageInfo', 'startCursor')
+ expect(pipeline_ids).to eq(pipeline_gids[2..3])
+ end
- run_query(last: 2, before: cursor)
+ it 'loads the second page with pagination last correctly' do
+ run_query(last: 2)
+ pipeline_ids = graphql_data.dig('package', 'pipelines', 'nodes').pluck('id')
- pipeline_ids = graphql_data.dig('package', 'pipelines', 'nodes').pluck('id')
+ expect(pipeline_ids).to eq(pipeline_gids[4..5])
- expect(pipeline_ids).to eq(pipeline_gids[2..3])
- end
+ cursor = graphql_data.dig('package', 'pipelines', 'pageInfo', 'startCursor')
+
+ run_query(last: 2, before: cursor)
+
+ pipeline_ids = graphql_data.dig('package', 'pipelines', 'nodes').pluck('id')
- context 'with unauthorized user' do
- let_it_be(:user) { create(:user) }
+ expect(pipeline_ids).to eq(pipeline_gids[2..3])
+ end
+ end
+ context 'package managers paths' do
before do
- project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
+ subject
end
- it 'returns no packages' do
- run_query(first: 2)
+ it 'returns npm_url correctly' do
+ expect(graphql_data_at(:package, :npm_url)).to eq("http://localhost/api/v4/projects/#{project.id}/packages/npm")
+ end
+
+ it 'returns maven_url correctly' do
+ expect(graphql_data_at(:package, :maven_url)).to eq("http://localhost/api/v4/projects/#{project.id}/packages/maven")
+ end
+
+ it 'returns conan_url correctly' do
+ expect(graphql_data_at(:package, :conan_url)).to eq("http://localhost/api/v4/projects/#{project.id}/packages/conan")
+ end
+
+ it 'returns nuget_url correctly' do
+ expect(graphql_data_at(:package, :nuget_url)).to eq("http://localhost/api/v4/projects/#{project.id}/packages/nuget/index.json")
+ end
+
+ it 'returns pypi_url correctly' do
+ expect(graphql_data_at(:package, :pypi_url)).to eq("http://__token__:<your_personal_token>@localhost/api/v4/projects/#{project.id}/packages/pypi/simple")
+ end
+
+ it 'returns pypi_setup_url correctly' do
+ expect(graphql_data_at(:package, :pypi_setup_url)).to eq("http://localhost/api/v4/projects/#{project.id}/packages/pypi")
+ end
+
+ it 'returns composer_url correctly' do
+ expect(graphql_data_at(:package, :composer_url)).to eq("http://localhost/api/v4/group/#{group.id}/-/packages/composer/packages.json")
+ end
- expect(graphql_data_at(:package)).to be_nil
+ it 'returns composer_config_repository_url correctly' do
+ expect(graphql_data_at(:package, :composer_config_repository_url)).to eq("localhost/#{group.id}")
end
end
end
diff --git a/spec/requests/api/graphql/project/issues_spec.rb b/spec/requests/api/graphql/project/issues_spec.rb
index b3e91afb5b3..f358ec3e53f 100644
--- a/spec/requests/api/graphql/project/issues_spec.rb
+++ b/spec/requests/api/graphql/project/issues_spec.rb
@@ -539,6 +539,43 @@ RSpec.describe 'getting an issue list for a project' do
end
end
+ context 'when fetching escalation status' do
+ let_it_be(:escalation_status) { create(:incident_management_issuable_escalation_status, issue: issue_a) }
+
+ let(:statuses) { issue_data.to_h { |issue| [issue['iid'], issue['escalationStatus']] } }
+ let(:fields) do
+ <<~QUERY
+ edges {
+ node {
+ id
+ escalationStatus
+ }
+ }
+ QUERY
+ end
+
+ before do
+ issue_a.update!(issue_type: Issue.issue_types[:incident])
+ end
+
+ it 'returns the escalation status values' do
+ post_graphql(query, current_user: current_user)
+
+ statuses = issues_data.map { |issue| issue.dig('node', 'escalationStatus') }
+
+ expect(statuses).to contain_exactly(escalation_status.status_name.upcase.to_s, nil)
+ end
+
+ it 'avoids N+1 queries', :aggregate_failures do
+ base_count = ActiveRecord::QueryRecorder.new { run_with_clean_state(query, context: { current_user: current_user }) }
+
+ new_incident = create(:incident, project: project)
+ create(:incident_management_issuable_escalation_status, issue: new_incident)
+
+ expect { run_with_clean_state(query, context: { current_user: current_user }) }.not_to exceed_query_limit(base_count)
+ end
+ end
+
describe 'N+1 query checks' do
let(:extra_iid_for_second_query) { issue_b.iid.to_s }
let(:search_params) { { iids: [issue_a.iid.to_s] } }
diff --git a/spec/requests/api/graphql/project/work_item_types_spec.rb b/spec/requests/api/graphql/project/work_item_types_spec.rb
new file mode 100644
index 00000000000..2caaedda2a1
--- /dev/null
+++ b/spec/requests/api/graphql/project/work_item_types_spec.rb
@@ -0,0 +1,71 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'getting a list of work item types for a project' do
+ include GraphqlHelpers
+
+ let_it_be(:developer) { create(:user) }
+ let_it_be(:project) { create(:project) }
+
+ before_all do
+ project.add_developer(developer)
+ end
+
+ let(:current_user) { developer }
+
+ let(:fields) do
+ <<~GRAPHQL
+ workItemTypes{
+ nodes { id name iconName }
+ }
+ GRAPHQL
+ end
+
+ let(:query) do
+ graphql_query_for(
+ 'project',
+ { 'fullPath' => project.full_path },
+ fields
+ )
+ end
+
+ context 'when user has access to the project' do
+ before do
+ post_graphql(query, current_user: current_user)
+ end
+
+ it_behaves_like 'a working graphql query'
+
+ it 'returns all default work item types' do
+ expect(graphql_data.dig('project', 'workItemTypes', 'nodes')).to match_array(
+ WorkItems::Type.default.map do |type|
+ hash_including('id' => type.to_global_id.to_s, 'name' => type.name, 'iconName' => type.icon_name)
+ end
+ )
+ end
+ end
+
+ context "when user doesn't have access to the project" do
+ let(:current_user) { create(:user) }
+
+ before do
+ post_graphql(query, current_user: current_user)
+ end
+
+ it 'does not return the project' do
+ expect(graphql_data).to eq('project' => nil)
+ end
+ end
+
+ context 'when the work_items feature flag is disabled' do
+ before do
+ stub_feature_flags(work_items: false)
+ post_graphql(query, current_user: current_user)
+ end
+
+ it 'makes the workItemTypes field unavailable' do
+ expect(graphql_errors).to contain_exactly(hash_including("message" => "Field 'workItemTypes' doesn't exist on type 'Project'"))
+ end
+ end
+end
diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb
index d226bb07c73..88c004345fc 100644
--- a/spec/requests/api/groups_spec.rb
+++ b/spec/requests/api/groups_spec.rb
@@ -801,6 +801,54 @@ RSpec.describe API::Groups do
expect(json_response['shared_projects'].count).to eq(limit)
end
end
+
+ context 'when a group is shared', :aggregate_failures do
+ let_it_be(:shared_group) { create(:group) }
+ let_it_be(:group2_sub) { create(:group, :private, parent: group2) }
+ let_it_be(:group_link_1) { create(:group_group_link, shared_group: shared_group, shared_with_group: group1) }
+ let_it_be(:group_link_2) { create(:group_group_link, shared_group: shared_group, shared_with_group: group2_sub) }
+
+ subject(:shared_with_groups) { json_response['shared_with_groups'].map { _1['group_id']} }
+
+ context 'when authenticated as admin' do
+ it 'returns all groups that share the group' do
+ get api("/groups/#{shared_group.id}", admin)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(shared_with_groups).to contain_exactly(group_link_1.shared_with_group_id, group_link_2.shared_with_group_id)
+ end
+ end
+
+ context 'when unauthenticated' do
+ it 'returns only public groups that share the group' do
+ get api("/groups/#{shared_group.id}")
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(shared_with_groups).to contain_exactly(group_link_1.shared_with_group_id)
+ end
+ end
+
+ context 'when authenticated as a member of a parent group that has shared the group' do
+ it 'returns private group if direct member' do
+ group2_sub.add_guest(user3)
+
+ get api("/groups/#{shared_group.id}", user3)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(shared_with_groups).to contain_exactly(group_link_1.shared_with_group_id, group_link_2.shared_with_group_id)
+ end
+
+ it 'returns private group if inherited member' do
+ inherited_guest_member = create(:user)
+ group2.add_guest(inherited_guest_member)
+
+ get api("/groups/#{shared_group.id}", inherited_guest_member)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(shared_with_groups).to contain_exactly(group_link_1.shared_with_group_id, group_link_2.shared_with_group_id)
+ end
+ end
+ end
end
describe 'PUT /groups/:id' do
diff --git a/spec/requests/api/integrations_spec.rb b/spec/requests/api/integrations_spec.rb
index 649647804c0..033c80a5696 100644
--- a/spec/requests/api/integrations_spec.rb
+++ b/spec/requests/api/integrations_spec.rb
@@ -55,8 +55,10 @@ RSpec.describe API::Integrations do
current_integration = project.integrations.first
events = current_integration.event_names.empty? ? ["foo"].freeze : current_integration.event_names
query_strings = []
- events.each do |event|
- query_strings << "#{event}=#{!current_integration[event]}"
+ events.map(&:to_sym).each do |event|
+ event_value = !current_integration[event]
+ query_strings << "#{event}=#{event_value}"
+ integration_attrs[event] = event_value if integration_attrs[event].present?
end
query_strings = query_strings.join('&')
diff --git a/spec/requests/api/internal/base_spec.rb b/spec/requests/api/internal/base_spec.rb
index 0a71eb43f81..9aa8aaafc68 100644
--- a/spec/requests/api/internal/base_spec.rb
+++ b/spec/requests/api/internal/base_spec.rb
@@ -372,7 +372,38 @@ RSpec.describe API::Internal::Base do
end
end
- describe "POST /internal/allowed", :clean_gitlab_redis_shared_state do
+ describe "POST /internal/allowed", :clean_gitlab_redis_shared_state, :clean_gitlab_redis_rate_limiting do
+ shared_examples 'rate limited request' do
+ let(:action) { 'git-upload-pack' }
+ let(:actor) { key }
+
+ it 'is throttled by rate limiter' do
+ allow(::Gitlab::ApplicationRateLimiter).to receive(:threshold).and_return(1)
+ expect(::Gitlab::ApplicationRateLimiter).to receive(:throttled?).with(:gitlab_shell_operation, scope: [action, project.full_path, actor]).twice.and_call_original
+
+ request
+
+ expect(response).to have_gitlab_http_status(:ok)
+
+ request
+
+ expect(response).to have_gitlab_http_status(:too_many_requests)
+ expect(json_response['message']['error']).to eq('This endpoint has been requested too many times. Try again later.')
+ end
+
+ context 'when rate_limit_gitlab_shell feature flag is disabled' do
+ before do
+ stub_feature_flags(rate_limit_gitlab_shell: false)
+ end
+
+ it 'is not throttled by rate limiter' do
+ expect(::Gitlab::ApplicationRateLimiter).not_to receive(:throttled?)
+
+ subject
+ end
+ end
+ end
+
context "access granted" do
let(:env) { {} }
@@ -530,6 +561,32 @@ RSpec.describe API::Internal::Base do
expect(json_response["gitaly"]["features"]).to eq('gitaly-feature-mep-mep' => 'true')
expect(user.reload.last_activity_on).to eql(Date.today)
end
+
+ it_behaves_like 'rate limited request' do
+ def request
+ pull(key, project)
+ end
+ end
+
+ context 'when user_id is passed' do
+ it_behaves_like 'rate limited request' do
+ let(:actor) { user }
+
+ def request
+ post(
+ api("/internal/allowed"),
+ params: {
+ user_id: user.id,
+ project: full_path_for(project),
+ gl_repository: gl_repository_for(project),
+ action: 'git-upload-pack',
+ secret_token: secret_token,
+ protocol: 'ssh'
+ }
+ )
+ end
+ end
+ end
end
context "with a feature flag enabled for a project" do
@@ -576,6 +633,14 @@ RSpec.describe API::Internal::Base do
expect(json_response["gitaly"]["token"]).to eq(Gitlab::GitalyClient.token(project.repository_storage))
expect(user.reload.last_activity_on).to be_nil
end
+
+ it_behaves_like 'rate limited request' do
+ let(:action) { 'git-receive-pack' }
+
+ def request
+ push(key, project)
+ end
+ end
end
context 'when receive_max_input_size has been updated' do
@@ -838,6 +903,14 @@ RSpec.describe API::Internal::Base do
expect(json_response["gitaly"]["address"]).to eq(Gitlab::GitalyClient.address(project.repository_storage))
expect(json_response["gitaly"]["token"]).to eq(Gitlab::GitalyClient.token(project.repository_storage))
end
+
+ it_behaves_like 'rate limited request' do
+ let(:action) { 'git-upload-archive' }
+
+ def request
+ archive(key, project)
+ end
+ end
end
context "not added to project" do
diff --git a/spec/requests/api/internal/kubernetes_spec.rb b/spec/requests/api/internal/kubernetes_spec.rb
index 245e4e6ba15..59d185fe6c8 100644
--- a/spec/requests/api/internal/kubernetes_spec.rb
+++ b/spec/requests/api/internal/kubernetes_spec.rb
@@ -53,7 +53,9 @@ RSpec.describe API::Internal::Kubernetes do
shared_examples 'agent token tracking' do
it 'tracks token usage' do
- expect { response }.to change { agent_token.reload.read_attribute(:last_used_at) }
+ expect do
+ send_request(headers: { 'Authorization' => "Bearer #{agent_token.token}" })
+ end.to change { agent_token.reload.read_attribute(:last_used_at) }
end
end
@@ -149,7 +151,7 @@ RSpec.describe API::Internal::Kubernetes do
let(:agent) { agent_token.agent }
let(:project) { agent.project }
- shared_examples 'agent token tracking'
+ include_examples 'agent token tracking'
it 'returns expected data', :aggregate_failures do
send_request(headers: { 'Authorization' => "Bearer #{agent_token.token}" })
diff --git a/spec/requests/api/internal/mail_room_spec.rb b/spec/requests/api/internal/mail_room_spec.rb
new file mode 100644
index 00000000000..f3ca3708c0c
--- /dev/null
+++ b/spec/requests/api/internal/mail_room_spec.rb
@@ -0,0 +1,194 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe API::Internal::MailRoom do
+ let(:base_configs) do
+ {
+ enabled: true,
+ address: 'address@example.com',
+ port: 143,
+ ssl: false,
+ start_tls: false,
+ mailbox: 'inbox',
+ idle_timeout: 60,
+ log_path: Rails.root.join('log', 'mail_room_json.log').to_s,
+ expunge_deleted: false
+ }
+ end
+
+ let(:enabled_configs) do
+ {
+ incoming_email: base_configs.merge(
+ secure_file: Rails.root.join('tmp', 'tests', '.incoming_email_secret').to_s
+ ),
+ service_desk_email: base_configs.merge(
+ secure_file: Rails.root.join('tmp', 'tests', '.service_desk_email').to_s
+ )
+ }
+ end
+
+ let(:auth_payload) { { 'iss' => Gitlab::MailRoom::Authenticator::INTERNAL_API_REQUEST_JWT_ISSUER, 'iat' => (Time.now - 10.seconds).to_i } }
+
+ let(:incoming_email_secret) { 'incoming_email_secret' }
+ let(:service_desk_email_secret) { 'service_desk_email_secret' }
+
+ let(:email_content) { fixture_file("emails/commands_in_reply.eml") }
+
+ before do
+ allow(Gitlab::MailRoom::Authenticator).to receive(:secret).with(:incoming_email).and_return(incoming_email_secret)
+ allow(Gitlab::MailRoom::Authenticator).to receive(:secret).with(:service_desk_email).and_return(service_desk_email_secret)
+ allow(Gitlab::MailRoom).to receive(:enabled_configs).and_return(enabled_configs)
+ end
+
+ around do |example|
+ freeze_time do
+ example.run
+ end
+ end
+
+ describe "POST /internal/mail_room/*mailbox_type" do
+ context 'handle incoming_email successfully' do
+ let(:auth_headers) do
+ jwt_token = JWT.encode(auth_payload, incoming_email_secret, 'HS256')
+ { Gitlab::MailRoom::Authenticator::INTERNAL_API_REQUEST_HEADER => jwt_token }
+ end
+
+ it 'schedules a EmailReceiverWorker job with raw email content' do
+ Sidekiq::Testing.fake! do
+ expect do
+ post api("/internal/mail_room/incoming_email"), headers: auth_headers, params: email_content
+ end.to change { EmailReceiverWorker.jobs.size }.by(1)
+ end
+
+ expect(response).to have_gitlab_http_status(:ok)
+
+ job = EmailReceiverWorker.jobs.last
+ expect(job).to match a_hash_including('args' => [email_content])
+ end
+ end
+
+ context 'handle service_desk_email successfully' do
+ let(:auth_headers) do
+ jwt_token = JWT.encode(auth_payload, service_desk_email_secret, 'HS256')
+ { Gitlab::MailRoom::Authenticator::INTERNAL_API_REQUEST_HEADER => jwt_token }
+ end
+
+ it 'schedules a ServiceDeskEmailReceiverWorker job with raw email content' do
+ Sidekiq::Testing.fake! do
+ expect do
+ post api("/internal/mail_room/service_desk_email"), headers: auth_headers, params: email_content
+ end.to change { ServiceDeskEmailReceiverWorker.jobs.size }.by(1)
+ end
+
+ expect(response).to have_gitlab_http_status(:ok)
+
+ job = ServiceDeskEmailReceiverWorker.jobs.last
+ expect(job).to match a_hash_including('args' => [email_content])
+ end
+ end
+
+ context 'email content exceeds limit' do
+ let(:auth_headers) do
+ jwt_token = JWT.encode(auth_payload, incoming_email_secret, 'HS256')
+ { Gitlab::MailRoom::Authenticator::INTERNAL_API_REQUEST_HEADER => jwt_token }
+ end
+
+ before do
+ allow(EmailReceiverWorker).to receive(:perform_async).and_raise(
+ Gitlab::SidekiqMiddleware::SizeLimiter::ExceedLimitError.new(EmailReceiverWorker, email_content.bytesize, email_content.bytesize - 1)
+ )
+ end
+
+ it 'responds with 400 bad request and replies with a failure message' do
+ perform_enqueued_jobs do
+ Sidekiq::Testing.fake! do
+ expect do
+ post api("/internal/mail_room/incoming_email"), headers: auth_headers, params: email_content
+ end.not_to change { EmailReceiverWorker.jobs.size }
+ end
+ end
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(Gitlab::Json.parse(response.body)).to match a_hash_including(
+ "success" => false,
+ "message" => "We couldn't process your email because it is too large. Please create your issue or comment through the web interface."
+ )
+
+ email = ActionMailer::Base.deliveries.last
+ expect(email).not_to be_nil
+ expect(email.to).to match_array(["jake@adventuretime.ooo"])
+ expect(email.subject).to include("Rejected")
+ expect(email.body.parts.last.to_s).to include("We couldn't process your email")
+ end
+ end
+
+ context 'not authenticated' do
+ it 'responds with 401 Unauthorized' do
+ post api("/internal/mail_room/incoming_email")
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+ end
+
+ context 'wrong token authentication' do
+ let(:auth_headers) do
+ jwt_token = JWT.encode(auth_payload, 'wrongsecret', 'HS256')
+ { Gitlab::MailRoom::Authenticator::INTERNAL_API_REQUEST_HEADER => jwt_token }
+ end
+
+ it 'responds with 401 Unauthorized' do
+ post api("/internal/mail_room/incoming_email"), headers: auth_headers
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+ end
+
+ context 'wrong mailbox type authentication' do
+ let(:auth_headers) do
+ jwt_token = JWT.encode(auth_payload, service_desk_email_secret, 'HS256')
+ { Gitlab::MailRoom::Authenticator::INTERNAL_API_REQUEST_HEADER => jwt_token }
+ end
+
+ it 'responds with 401 Unauthorized' do
+ post api("/internal/mail_room/incoming_email"), headers: auth_headers
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+ end
+
+ context 'not supported mailbox type' do
+ let(:auth_headers) do
+ jwt_token = JWT.encode(auth_payload, incoming_email_secret, 'HS256')
+ { Gitlab::MailRoom::Authenticator::INTERNAL_API_REQUEST_HEADER => jwt_token }
+ end
+
+ it 'responds with 401 Unauthorized' do
+ post api("/internal/mail_room/invalid_mailbox_type"), headers: auth_headers
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+ end
+
+ context 'not enabled mailbox type' do
+ let(:enabled_configs) do
+ {
+ incoming_email: base_configs.merge(
+ secure_file: Rails.root.join('tmp', 'tests', '.incoming_email_secret').to_s
+ )
+ }
+ end
+
+ let(:auth_headers) do
+ jwt_token = JWT.encode(auth_payload, service_desk_email_secret, 'HS256')
+ { Gitlab::MailRoom::Authenticator::INTERNAL_API_REQUEST_HEADER => jwt_token }
+ end
+
+ it 'responds with 401 Unauthorized' do
+ post api("/internal/mail_room/service_desk_email"), headers: auth_headers
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/lint_spec.rb b/spec/requests/api/lint_spec.rb
index 0e83b964121..7c1e731a99a 100644
--- a/spec/requests/api/lint_spec.rb
+++ b/spec/requests/api/lint_spec.rb
@@ -121,8 +121,8 @@ RSpec.describe API::Lint do
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to be_an Hash
expect(json_response['status']).to eq('valid')
- expect(json_response['warnings']).to eq([])
- expect(json_response['errors']).to eq([])
+ expect(json_response['warnings']).to match_array([])
+ expect(json_response['errors']).to match_array([])
end
it 'outputs expanded yaml content' do
@@ -149,7 +149,20 @@ RSpec.describe API::Lint do
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['status']).to eq('valid')
expect(json_response['warnings']).not_to be_empty
- expect(json_response['errors']).to eq([])
+ expect(json_response['errors']).to match_array([])
+ end
+ end
+
+ context 'with valid .gitlab-ci.yaml using deprecated keywords' do
+ let(:yaml_content) { { job: { script: 'ls' }, types: ['test'] }.to_yaml }
+
+ it 'passes validation but returns warnings' do
+ post api('/ci/lint', api_user), params: { content: yaml_content }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['status']).to eq('valid')
+ expect(json_response['warnings']).not_to be_empty
+ expect(json_response['errors']).to match_array([])
end
end
diff --git a/spec/requests/api/maven_packages_spec.rb b/spec/requests/api/maven_packages_spec.rb
index 5a682ee8532..bc325aad823 100644
--- a/spec/requests/api/maven_packages_spec.rb
+++ b/spec/requests/api/maven_packages_spec.rb
@@ -425,7 +425,7 @@ RSpec.describe API::MavenPackages do
context 'internal project' do
before do
- group.group_member(user).destroy!
+ group.member(user).destroy!
project.update!(visibility_level: Gitlab::VisibilityLevel::INTERNAL)
end
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index 7c147419354..a751f785913 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -1269,6 +1269,7 @@ RSpec.describe API::MergeRequests do
get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user)
expect(json_response).to include('merged_by',
+ 'merge_user',
'merged_at',
'closed_by',
'closed_at',
@@ -1279,9 +1280,10 @@ RSpec.describe API::MergeRequests do
end
it 'returns correct values' do
- get api("/projects/#{project.id}/merge_requests/#{merge_request.reload.iid}", user)
+ get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user)
expect(json_response['merged_by']['id']).to eq(merge_request.metrics.merged_by_id)
+ expect(json_response['merge_user']['id']).to eq(merge_request.metrics.merged_by_id)
expect(Time.parse(json_response['merged_at'])).to be_like_time(merge_request.metrics.merged_at)
expect(json_response['closed_by']['id']).to eq(merge_request.metrics.latest_closed_by_id)
expect(Time.parse(json_response['closed_at'])).to be_like_time(merge_request.metrics.latest_closed_at)
@@ -1292,6 +1294,32 @@ RSpec.describe API::MergeRequests do
end
end
+ context 'merge_user' do
+ context 'when MR is set to MWPS' do
+ let(:merge_request) { create(:merge_request, :merge_when_pipeline_succeeds, source_project: project, target_project: project) }
+
+ it 'returns user who set MWPS' do
+ get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['merge_user']['id']).to eq(user.id)
+ end
+
+ context 'when MR is already merged' do
+ before do
+ merge_request.metrics.update!(merged_by: user2)
+ end
+
+ it 'returns user who actually merged' do
+ get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['merge_user']['id']).to eq(user2.id)
+ end
+ end
+ end
+ end
+
context 'head_pipeline' do
let(:project) { create(:project, :repository) }
let(:merge_request) { create(:merge_request, :simple, author: user, source_project: project, source_branch: 'markdown', title: "Test") }
@@ -3278,9 +3306,10 @@ RSpec.describe API::MergeRequests do
context 'when skip_ci parameter is set' do
it 'enqueues a rebase of the merge request with skip_ci flag set' do
- allow(RebaseWorker).to receive(:with_status).and_return(RebaseWorker)
+ with_status = RebaseWorker.with_status
- expect(RebaseWorker).to receive(:perform_async).with(merge_request.id, user.id, true).and_call_original
+ expect(RebaseWorker).to receive(:with_status).and_return(with_status)
+ expect(with_status).to receive(:perform_async).with(merge_request.id, user.id, true).and_call_original
Sidekiq::Testing.fake! do
expect do
diff --git a/spec/requests/api/package_files_spec.rb b/spec/requests/api/package_files_spec.rb
index eb1f04d193e..7a6b1599154 100644
--- a/spec/requests/api/package_files_spec.rb
+++ b/spec/requests/api/package_files_spec.rb
@@ -76,6 +76,30 @@ RSpec.describe API::PackageFiles do
end
end
end
+
+ context 'with package files pending destruction' do
+ let!(:package_file_pending_destruction) { create(:package_file, :pending_destruction, package: package) }
+
+ let(:package_file_ids) { json_response.map { |e| e['id'] } }
+
+ it 'does not return them' do
+ get api(url, user)
+
+ expect(package_file_ids).not_to include(package_file_pending_destruction.id)
+ end
+
+ context 'with packages_installable_package_files disabled' do
+ before do
+ stub_feature_flags(packages_installable_package_files: false)
+ end
+
+ it 'returns them' do
+ get api(url, user)
+
+ expect(package_file_ids).to include(package_file_pending_destruction.id)
+ end
+ end
+ end
end
end
@@ -149,6 +173,32 @@ RSpec.describe API::PackageFiles do
expect(response).to have_gitlab_http_status(:not_found)
end
end
+
+ context 'with package file pending destruction' do
+ let!(:package_file_id) { create(:package_file, :pending_destruction, package: package).id }
+
+ before do
+ project.add_maintainer(user)
+ end
+
+ it 'can not be accessed', :aggregate_failures do
+ expect { api_request }.not_to change { package.package_files.count }
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+
+ context 'with packages_installable_package_files disabled' do
+ before do
+ stub_feature_flags(packages_installable_package_files: false)
+ end
+
+ it 'can be accessed', :aggregate_failures do
+ expect { api_request }.to change { package.package_files.count }.by(-1)
+
+ expect(response).to have_gitlab_http_status(:no_content)
+ end
+ end
+ end
end
end
end
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index 8406ded85d8..bf41a808219 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -3704,6 +3704,46 @@ RSpec.describe API::Projects do
expect { subject }.to change { project.reload.keep_latest_artifact }.to(true)
end
end
+
+ context 'attribute mr_default_target_self' do
+ let_it_be(:source_project) { create(:project, :public) }
+
+ let(:forked_project) { fork_project(source_project, user) }
+
+ it 'is by default set to false' do
+ expect(source_project.mr_default_target_self).to be false
+ expect(forked_project.mr_default_target_self).to be false
+ end
+
+ describe 'for a non-forked project' do
+ before_all do
+ source_project.add_maintainer(user)
+ end
+
+ it 'is not exposed' do
+ get api("/projects/#{source_project.id}", user)
+
+ expect(json_response).not_to include('mr_default_target_self')
+ end
+
+ it 'is not possible to update' do
+ put api("/projects/#{source_project.id}", user), params: { mr_default_target_self: true }
+
+ source_project.reload
+ expect(source_project.mr_default_target_self).to be false
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+ end
+
+ describe 'for a forked project' do
+ it 'updates to true' do
+ put api("/projects/#{forked_project.id}", user), params: { mr_default_target_self: true }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['mr_default_target_self']).to eq(true)
+ end
+ end
+ end
end
describe 'POST /projects/:id/archive' do
@@ -4213,7 +4253,13 @@ RSpec.describe API::Projects do
end
it 'accepts custom parameters for the target project' do
- post api("/projects/#{project.id}/fork", user2), params: { name: 'My Random Project', description: 'A description', visibility: 'private' }
+ post api("/projects/#{project.id}/fork", user2),
+ params: {
+ name: 'My Random Project',
+ description: 'A description',
+ visibility: 'private',
+ mr_default_target_self: true
+ }
expect(response).to have_gitlab_http_status(:created)
expect(json_response['name']).to eq('My Random Project')
@@ -4224,6 +4270,7 @@ RSpec.describe API::Projects do
expect(json_response['description']).to eq('A description')
expect(json_response['visibility']).to eq('private')
expect(json_response['import_status']).to eq('scheduled')
+ expect(json_response['mr_default_target_self']).to eq(true)
expect(json_response).to include("import_error")
end
diff --git a/spec/requests/api/resource_access_tokens_spec.rb b/spec/requests/api/resource_access_tokens_spec.rb
index 23061ab4bf0..7e3e682767f 100644
--- a/spec/requests/api/resource_access_tokens_spec.rb
+++ b/spec/requests/api/resource_access_tokens_spec.rb
@@ -3,25 +3,27 @@
require "spec_helper"
RSpec.describe API::ResourceAccessTokens do
- context "when the resource is a project" do
- let_it_be(:project) { create(:project) }
- let_it_be(:other_project) { create(:project) }
- let_it_be(:user) { create(:user) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:user_non_priviledged) { create(:user) }
- describe "GET projects/:id/access_tokens" do
- subject(:get_tokens) { get api("/projects/#{project_id}/access_tokens", user) }
+ shared_examples 'resource access token API' do |source_type|
+ context "GET #{source_type}s/:id/access_tokens" do
+ subject(:get_tokens) { get api("/#{source_type}s/#{resource_id}/access_tokens", user) }
- context "when the user has maintainer permissions" do
+ context "when the user has valid permissions" do
let_it_be(:project_bot) { create(:user, :project_bot) }
let_it_be(:access_tokens) { create_list(:personal_access_token, 3, user: project_bot) }
- let_it_be(:project_id) { project.id }
+ let_it_be(:resource_id) { resource.id }
before do
- project.add_maintainer(user)
- project.add_maintainer(project_bot)
+ if source_type == 'project'
+ resource.add_maintainer(project_bot)
+ else
+ resource.add_owner(project_bot)
+ end
end
- it "gets a list of access tokens for the specified project" do
+ it "gets a list of access tokens for the specified #{source_type}" do
get_tokens
token_ids = json_response.map { |token| token['id'] }
@@ -38,16 +40,22 @@ RSpec.describe API::ResourceAccessTokens do
expect(api_get_token["name"]).to eq(token.name)
expect(api_get_token["scopes"]).to eq(token.scopes)
- expect(api_get_token["access_level"]).to eq(project.team.max_member_access(token.user.id))
+
+ if source_type == 'project'
+ expect(api_get_token["access_level"]).to eq(resource.team.max_member_access(token.user.id))
+ else
+ expect(api_get_token["access_level"]).to eq(resource.max_member_access_for_user(token.user))
+ end
+
expect(api_get_token["expires_at"]).to eq(token.expires_at.to_date.iso8601)
expect(api_get_token).not_to have_key('token')
end
- context "when using a project access token to GET other project access tokens" do
+ context "when using a #{source_type} access token to GET other #{source_type} access tokens" do
let_it_be(:token) { access_tokens.first }
- it "gets a list of access tokens for the specified project" do
- get api("/projects/#{project_id}/access_tokens", personal_access_token: token)
+ it "gets a list of access tokens for the specified #{source_type}" do
+ get api("/#{source_type}s/#{resource_id}/access_tokens", personal_access_token: token)
token_ids = json_response.map { |token| token['id'] }
@@ -56,16 +64,15 @@ RSpec.describe API::ResourceAccessTokens do
end
end
- context "when tokens belong to a different project" do
+ context "when tokens belong to a different #{source_type}" do
let_it_be(:bot) { create(:user, :project_bot) }
let_it_be(:token) { create(:personal_access_token, user: bot) }
before do
- other_project.add_maintainer(bot)
- other_project.add_maintainer(user)
+ other_resource.add_maintainer(bot)
end
- it "does not return tokens from a different project" do
+ it "does not return tokens from a different #{source_type}" do
get_tokens
token_ids = json_response.map { |token| token['id'] }
@@ -74,12 +81,8 @@ RSpec.describe API::ResourceAccessTokens do
end
end
- context "when the project has no access tokens" do
- let(:project_id) { other_project.id }
-
- before do
- other_project.add_maintainer(user)
- end
+ context "when the #{source_type} has no access tokens" do
+ let(:resource_id) { other_resource.id }
it 'returns an empty array' do
get_tokens
@@ -89,8 +92,8 @@ RSpec.describe API::ResourceAccessTokens do
end
end
- context "when trying to get the tokens of a different project" do
- let_it_be(:project_id) { other_project.id }
+ context "when trying to get the tokens of a different #{source_type}" do
+ let_it_be(:resource_id) { unknown_resource.id }
it "returns 404" do
get_tokens
@@ -99,8 +102,8 @@ RSpec.describe API::ResourceAccessTokens do
end
end
- context "when the project does not exist" do
- let(:project_id) { non_existing_record_id }
+ context "when the #{source_type} does not exist" do
+ let(:resource_id) { non_existing_record_id }
it "returns 404" do
get_tokens
@@ -111,13 +114,13 @@ RSpec.describe API::ResourceAccessTokens do
end
context "when the user does not have valid permissions" do
+ let_it_be(:user) { user_non_priviledged }
let_it_be(:project_bot) { create(:user, :project_bot) }
let_it_be(:access_tokens) { create_list(:personal_access_token, 3, user: project_bot) }
- let_it_be(:project_id) { project.id }
+ let_it_be(:resource_id) { resource.id }
before do
- project.add_developer(user)
- project.add_maintainer(project_bot)
+ resource.add_maintainer(project_bot)
end
it "returns 401" do
@@ -128,40 +131,36 @@ RSpec.describe API::ResourceAccessTokens do
end
end
- describe "DELETE projects/:id/access_tokens/:token_id", :sidekiq_inline do
- subject(:delete_token) { delete api("/projects/#{project_id}/access_tokens/#{token_id}", user) }
+ context "DELETE #{source_type}s/:id/access_tokens/:token_id", :sidekiq_inline do
+ subject(:delete_token) { delete api("/#{source_type}s/#{resource_id}/access_tokens/#{token_id}", user) }
let_it_be(:project_bot) { create(:user, :project_bot) }
let_it_be(:token) { create(:personal_access_token, user: project_bot) }
- let_it_be(:project_id) { project.id }
+ let_it_be(:resource_id) { resource.id }
let_it_be(:token_id) { token.id }
before do
- project.add_maintainer(project_bot)
+ resource.add_maintainer(project_bot)
end
- context "when the user has maintainer permissions" do
- before do
- project.add_maintainer(user)
- end
-
- it "deletes the project access token from the project" do
+ context "when the user has valid permissions" do
+ it "deletes the #{source_type} access token from the #{source_type}" do
delete_token
expect(response).to have_gitlab_http_status(:no_content)
expect(User.exists?(project_bot.id)).to be_falsy
end
- context "when using project access token to DELETE other project access token" do
+ context "when using #{source_type} access token to DELETE other #{source_type} access token" do
let_it_be(:other_project_bot) { create(:user, :project_bot) }
let_it_be(:other_token) { create(:personal_access_token, user: other_project_bot) }
let_it_be(:token_id) { other_token.id }
before do
- project.add_maintainer(other_project_bot)
+ resource.add_maintainer(other_project_bot)
end
- it "deletes the project access token from the project" do
+ it "deletes the #{source_type} access token from the #{source_type}" do
delete_token
expect(response).to have_gitlab_http_status(:no_content)
@@ -169,37 +168,31 @@ RSpec.describe API::ResourceAccessTokens do
end
end
- context "when attempting to delete a non-existent project access token" do
+ context "when attempting to delete a non-existent #{source_type} access token" do
let_it_be(:token_id) { non_existing_record_id }
it "does not delete the token, and returns 404" do
delete_token
expect(response).to have_gitlab_http_status(:not_found)
- expect(response.body).to include("Could not find project access token with token_id: #{token_id}")
+ expect(response.body).to include("Could not find #{source_type} access token with token_id: #{token_id}")
end
end
- context "when attempting to delete a token that does not belong to the specified project" do
- let_it_be(:project_id) { other_project.id }
-
- before do
- other_project.add_maintainer(user)
- end
+ context "when attempting to delete a token that does not belong to the specified #{source_type}" do
+ let_it_be(:resource_id) { other_resource.id }
it "does not delete the token, and returns 404" do
delete_token
expect(response).to have_gitlab_http_status(:not_found)
- expect(response.body).to include("Could not find project access token with token_id: #{token_id}")
+ expect(response.body).to include("Could not find #{source_type} access token with token_id: #{token_id}")
end
end
end
context "when the user does not have valid permissions" do
- before do
- project.add_developer(user)
- end
+ let_it_be(:user) { user_non_priviledged }
it "does not delete the token, and returns 400", :aggregate_failures do
delete_token
@@ -211,23 +204,19 @@ RSpec.describe API::ResourceAccessTokens do
end
end
- describe "POST projects/:id/access_tokens" do
+ context "POST #{source_type}s/:id/access_tokens" do
let(:params) { { name: "test", scopes: ["api"], expires_at: expires_at, access_level: access_level } }
let(:expires_at) { 1.month.from_now }
let(:access_level) { 20 }
- subject(:create_token) { post api("/projects/#{project_id}/access_tokens", user), params: params }
+ subject(:create_token) { post api("/#{source_type}s/#{resource_id}/access_tokens", user), params: params }
- context "when the user has maintainer permissions" do
- let_it_be(:project_id) { project.id }
-
- before do
- project.add_maintainer(user)
- end
+ context "when the user has valid permissions" do
+ let_it_be(:resource_id) { resource.id }
context "with valid params" do
context "with full params" do
- it "creates a project access token with the params", :aggregate_failures do
+ it "creates a #{source_type} access token with the params", :aggregate_failures do
create_token
expect(response).to have_gitlab_http_status(:created)
@@ -242,7 +231,7 @@ RSpec.describe API::ResourceAccessTokens do
context "when 'expires_at' is not set" do
let(:expires_at) { nil }
- it "creates a project access token with the params", :aggregate_failures do
+ it "creates a #{source_type} access token with the params", :aggregate_failures do
create_token
expect(response).to have_gitlab_http_status(:created)
@@ -255,7 +244,7 @@ RSpec.describe API::ResourceAccessTokens do
context "when 'access_level' is not set" do
let(:access_level) { nil }
- it 'creates a project access token with the default access level', :aggregate_failures do
+ it "creates a #{source_type} access token with the default access level", :aggregate_failures do
create_token
expect(response).to have_gitlab_http_status(:created)
@@ -272,7 +261,7 @@ RSpec.describe API::ResourceAccessTokens do
context "when missing the 'name' param" do
let_it_be(:params) { { scopes: ["api"], expires_at: 5.days.from_now } }
- it "does not create a project access token without 'name'" do
+ it "does not create a #{source_type} access token without 'name'" do
create_token
expect(response).to have_gitlab_http_status(:bad_request)
@@ -283,7 +272,7 @@ RSpec.describe API::ResourceAccessTokens do
context "when missing the 'scopes' param" do
let_it_be(:params) { { name: "test", expires_at: 5.days.from_now } }
- it "does not create a project access token without 'scopes'" do
+ it "does not create a #{source_type} access token without 'scopes'" do
create_token
expect(response).to have_gitlab_http_status(:bad_request)
@@ -292,50 +281,80 @@ RSpec.describe API::ResourceAccessTokens do
end
end
- context "when trying to create a token in a different project" do
- let_it_be(:project_id) { other_project.id }
+ context "when trying to create a token in a different #{source_type}" do
+ let_it_be(:resource_id) { unknown_resource.id }
- it "does not create the token, and returns the project not found error" do
+ it "does not create the token, and returns the #{source_type} not found error" do
create_token
expect(response).to have_gitlab_http_status(:not_found)
- expect(response.body).to include("Project Not Found")
+ expect(response.body).to include("#{source_type.capitalize} Not Found")
end
end
end
context "when the user does not have valid permissions" do
- let_it_be(:project_id) { project.id }
+ let_it_be(:resource_id) { resource.id }
- context "when the user is a developer" do
- before do
- project.add_developer(user)
- end
+ context "when the user role is too low" do
+ let_it_be(:user) { user_non_priviledged }
it "does not create the token, and returns the permission error" do
create_token
expect(response).to have_gitlab_http_status(:bad_request)
- expect(response.body).to include("User does not have permission to create project access token")
+ expect(response.body).to include("User does not have permission to create #{source_type} access token")
end
end
- context "when a project access token tries to create another project access token" do
+ context "when a #{source_type} access token tries to create another #{source_type} access token" do
let_it_be(:project_bot) { create(:user, :project_bot) }
let_it_be(:user) { project_bot }
before do
- project.add_maintainer(user)
+ if source_type == 'project'
+ resource.add_maintainer(project_bot)
+ else
+ resource.add_owner(project_bot)
+ end
end
- it "does not allow a project access token to create another project access token" do
+ it "does not allow a #{source_type} access token to create another #{source_type} access token" do
create_token
expect(response).to have_gitlab_http_status(:bad_request)
- expect(response.body).to include("User does not have permission to create project access token")
+ expect(response.body).to include("User does not have permission to create #{source_type} access token")
end
end
end
end
end
+
+ context 'when the resource is a project' do
+ let_it_be(:resource) { create(:project) }
+ let_it_be(:other_resource) { create(:project) }
+ let_it_be(:unknown_resource) { create(:project) }
+
+ before_all do
+ resource.add_maintainer(user)
+ other_resource.add_maintainer(user)
+ resource.add_developer(user_non_priviledged)
+ end
+
+ it_behaves_like 'resource access token API', 'project'
+ end
+
+ context 'when the resource is a group' do
+ let_it_be(:resource) { create(:group) }
+ let_it_be(:other_resource) { create(:group) }
+ let_it_be(:unknown_resource) { create(:project) }
+
+ before_all do
+ resource.add_owner(user)
+ other_resource.add_owner(user)
+ resource.add_maintainer(user_non_priviledged)
+ end
+
+ it_behaves_like 'resource access token API', 'group'
+ end
end
diff --git a/spec/requests/api/rubygem_packages_spec.rb b/spec/requests/api/rubygem_packages_spec.rb
index 9b104520b52..0e63a7269e7 100644
--- a/spec/requests/api/rubygem_packages_spec.rb
+++ b/spec/requests/api/rubygem_packages_spec.rb
@@ -173,6 +173,34 @@ RSpec.describe API::RubygemPackages do
it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member]
end
end
+
+ context 'with package files pending destruction' do
+ let_it_be(:package_file_pending_destruction) { create(:package_file, :pending_destruction, :xml, package: package, file_name: file_name) }
+
+ before do
+ project.update_column(:visibility_level, Gitlab::VisibilityLevel::PUBLIC)
+ end
+
+ it 'does not return them' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.body).not_to eq(package_file_pending_destruction.file.file.read)
+ end
+
+ context 'with packages_installable_package_files disabled' do
+ before do
+ stub_feature_flags(packages_installable_package_files: false)
+ end
+
+ it 'returns them' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.body).to eq(package_file_pending_destruction.file.file.read)
+ end
+ end
+ end
end
describe 'POST /api/v4/projects/:project_id/packages/rubygems/api/v1/gems/authorize' do
diff --git a/spec/requests/api/search_spec.rb b/spec/requests/api/search_spec.rb
index b75fe11b06d..24cd95781c3 100644
--- a/spec/requests/api/search_spec.rb
+++ b/spec/requests/api/search_spec.rb
@@ -346,6 +346,14 @@ RSpec.describe API::Search do
end
end
end
+
+ it_behaves_like 'rate limited endpoint', rate_limit_key: :user_email_lookup do
+ let(:current_user) { user }
+
+ def request
+ get api(endpoint, current_user), params: { scope: 'users', search: 'foo@bar.com' }
+ end
+ end
end
describe "GET /groups/:id/search" do
@@ -513,6 +521,14 @@ RSpec.describe API::Search do
it_behaves_like 'response is correct', schema: 'public_api/v4/user/basics'
end
+
+ it_behaves_like 'rate limited endpoint', rate_limit_key: :user_email_lookup do
+ let(:current_user) { user }
+
+ def request
+ get api(endpoint, current_user), params: { scope: 'users', search: 'foo@bar.com' }
+ end
+ end
end
end
@@ -786,6 +802,14 @@ RSpec.describe API::Search do
end
end
end
+
+ it_behaves_like 'rate limited endpoint', rate_limit_key: :user_email_lookup do
+ let(:current_user) { user }
+
+ def request
+ get api(endpoint, current_user), params: { scope: 'users', search: 'foo@bar.com' }
+ end
+ end
end
end
end
diff --git a/spec/requests/api/terraform/modules/v1/packages_spec.rb b/spec/requests/api/terraform/modules/v1/packages_spec.rb
index b17bc11a451..c0f04ba09be 100644
--- a/spec/requests/api/terraform/modules/v1/packages_spec.rb
+++ b/spec/requests/api/terraform/modules/v1/packages_spec.rb
@@ -154,6 +154,7 @@ RSpec.describe API::Terraform::Modules::V1::Packages do
end
describe 'GET /api/v4/packages/terraform/modules/v1/:module_namespace/:module_name/:module_system/:module_version/file' do
+ let(:url) { api("/packages/terraform/modules/v1/#{group.path}/#{package.name}/#{package.version}/file?token=#{token}") }
let(:tokens) do
{
personal_access_token: ::Gitlab::JWTToken.new.tap { |jwt| jwt['token'] = personal_access_token.id }.encoded,
@@ -202,7 +203,6 @@ RSpec.describe API::Terraform::Modules::V1::Packages do
with_them do
let(:token) { valid_token ? tokens[token_type] : 'invalid-token123' }
- let(:url) { api("/packages/terraform/modules/v1/#{group.path}/#{package.name}/#{package.version}/file?token=#{token}") }
let(:snowplow_gitlab_standard_context) { { project: project, user: user, namespace: project.namespace } }
before do
@@ -212,6 +212,41 @@ RSpec.describe API::Terraform::Modules::V1::Packages do
it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member]
end
end
+
+ context 'with package file pending destruction' do
+ let_it_be(:package) { create(:package, package_type: :terraform_module, project: project, name: "module-555/pending-destruction", version: '1.0.0') }
+ let_it_be(:package_file_pending_destruction) { create(:package_file, :pending_destruction, :xml, package: package) }
+ let_it_be(:package_file) { create(:package_file, :terraform_module, package: package) }
+
+ let(:token) { tokens[:personal_access_token] }
+ let(:headers) { { 'Authorization' => "Bearer #{token}" } }
+
+ before do
+ project.add_maintainer(user)
+ end
+
+ it 'does not return them' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.body).not_to eq(package_file_pending_destruction.file.file.read)
+ expect(response.body).to eq(package_file.file.file.read)
+ end
+
+ context 'with packages_installable_package_files disabled' do
+ before do
+ stub_feature_flags(packages_installable_package_files: false)
+ end
+
+ it 'returns them' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.body).to eq(package_file_pending_destruction.file.file.read)
+ expect(response.body).not_to eq(package_file.file.file.read)
+ end
+ end
+ end
end
describe 'PUT /api/v4/projects/:project_id/packages/terraform/modules/:module_name/:module_system/:module_version/file/authorize' do
diff --git a/spec/requests/api/usage_data_non_sql_metrics_spec.rb b/spec/requests/api/usage_data_non_sql_metrics_spec.rb
index 225af57a267..0b73d0f96a4 100644
--- a/spec/requests/api/usage_data_non_sql_metrics_spec.rb
+++ b/spec/requests/api/usage_data_non_sql_metrics_spec.rb
@@ -18,6 +18,7 @@ RSpec.describe API::UsageDataNonSqlMetrics do
context 'with authentication' do
before do
stub_feature_flags(usage_data_non_sql_metrics: true)
+ stub_database_flavor_check
end
it 'returns non sql metrics if user is admin' do
diff --git a/spec/requests/api/usage_data_queries_spec.rb b/spec/requests/api/usage_data_queries_spec.rb
index 0ba4a37bc9b..69a8d865a59 100644
--- a/spec/requests/api/usage_data_queries_spec.rb
+++ b/spec/requests/api/usage_data_queries_spec.rb
@@ -10,6 +10,7 @@ RSpec.describe API::UsageDataQueries do
before do
stub_usage_data_connections
+ stub_database_flavor_check
end
describe 'GET /usage_data/usage_data_queries' do
diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb
index b93df2f3bae..98875d7e8d2 100644
--- a/spec/requests/api/users_spec.rb
+++ b/spec/requests/api/users_spec.rb
@@ -498,6 +498,10 @@ RSpec.describe API::Users do
describe "GET /users/:id" do
let_it_be(:user2, reload: true) { create(:user, username: 'another_user') }
+ before do
+ allow(Gitlab::ApplicationRateLimiter).to receive(:throttled?).with(:users_get_by_id, scope: user).and_return(false)
+ end
+
it "returns a user by id" do
get api("/users/#{user.id}", user)
@@ -593,6 +597,55 @@ RSpec.describe API::Users do
expect(json_response).not_to have_key('sign_in_count')
end
+ context 'when the rate limit is not exceeded' do
+ it 'returns a success status' do
+ expect(Gitlab::ApplicationRateLimiter)
+ .to receive(:throttled?).with(:users_get_by_id, scope: user)
+ .and_return(false)
+
+ get api("/users/#{user.id}", user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+
+ context 'when the rate limit is exceeded' do
+ context 'when feature flag is enabled' do
+ it 'returns "too many requests" status' do
+ expect(Gitlab::ApplicationRateLimiter)
+ .to receive(:throttled?).with(:users_get_by_id, scope: user)
+ .and_return(true)
+
+ get api("/users/#{user.id}", user)
+
+ expect(response).to have_gitlab_http_status(:too_many_requests)
+ end
+
+ it 'still allows admin users' do
+ expect(Gitlab::ApplicationRateLimiter)
+ .not_to receive(:throttled?)
+
+ get api("/users/#{user.id}", admin)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+
+ context 'when feature flag is disabled' do
+ before do
+ stub_feature_flags(rate_limit_user_by_id_endpoint: false)
+ end
+
+ it 'does not throttle the request' do
+ expect(Gitlab::ApplicationRateLimiter).not_to receive(:throttled?)
+
+ get api("/users/#{user.id}", user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+ end
+
context 'when job title is present' do
let(:job_title) { 'Fullstack Engineer' }
@@ -974,7 +1027,7 @@ RSpec.describe API::Users do
post api('/users', admin),
params: {
email: 'invalid email',
- password: 'password',
+ password: Gitlab::Password.test_default,
name: 'test'
}
expect(response).to have_gitlab_http_status(:bad_request)
@@ -1040,7 +1093,7 @@ RSpec.describe API::Users do
post api('/users', admin),
params: {
email: 'test@example.com',
- password: 'password',
+ password: Gitlab::Password.test_default,
username: 'test',
name: 'foo'
}
@@ -1052,7 +1105,7 @@ RSpec.describe API::Users do
params: {
name: 'foo',
email: 'test@example.com',
- password: 'password',
+ password: Gitlab::Password.test_default,
username: 'foo'
}
end.to change { User.count }.by(0)
@@ -1066,7 +1119,7 @@ RSpec.describe API::Users do
params: {
name: 'foo',
email: 'foo@example.com',
- password: 'password',
+ password: Gitlab::Password.test_default,
username: 'test'
}
end.to change { User.count }.by(0)
@@ -1080,7 +1133,7 @@ RSpec.describe API::Users do
params: {
name: 'foo',
email: 'foo@example.com',
- password: 'password',
+ password: Gitlab::Password.test_default,
username: 'TEST'
}
end.to change { User.count }.by(0)
@@ -1425,8 +1478,8 @@ RSpec.describe API::Users do
context "with existing user" do
before do
- post api("/users", admin), params: { email: 'test@example.com', password: 'password', username: 'test', name: 'test' }
- post api("/users", admin), params: { email: 'foo@bar.com', password: 'password', username: 'john', name: 'john' }
+ post api("/users", admin), params: { email: 'test@example.com', password: Gitlab::Password.test_default, username: 'test', name: 'test' }
+ post api("/users", admin), params: { email: 'foo@bar.com', password: Gitlab::Password.test_default, username: 'john', name: 'john' }
@user = User.all.last
end
diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb
index d2528600477..623cf24b9cb 100644
--- a/spec/requests/git_http_spec.rb
+++ b/spec/requests/git_http_spec.rb
@@ -319,7 +319,7 @@ RSpec.describe 'Git HTTP requests' do
context 'when user is using credentials with special characters' do
context 'with password with special characters' do
before do
- user.update!(password: 'RKszEwéC5kFnû∆f243fycGu§Gh9ftDj!U')
+ user.update!(password: Gitlab::Password.test_default)
end
it 'allows clones' do
@@ -1670,7 +1670,7 @@ RSpec.describe 'Git HTTP requests' do
context 'when user is using credentials with special characters' do
context 'with password with special characters' do
before do
- user.update!(password: 'RKszEwéC5kFnû∆f243fycGu§Gh9ftDj!U')
+ user.update!(password: Gitlab::Password.test_default)
end
it 'allows clones' do
diff --git a/spec/requests/groups/crm/contacts_controller_spec.rb b/spec/requests/groups/crm/contacts_controller_spec.rb
index a4b2a28e77a..5d126c6ead5 100644
--- a/spec/requests/groups/crm/contacts_controller_spec.rb
+++ b/spec/requests/groups/crm/contacts_controller_spec.rb
@@ -24,7 +24,7 @@ RSpec.describe Groups::Crm::ContactsController do
shared_examples 'ok response with index template if authorized' do
context 'private group' do
- let(:group) { create(:group, :private) }
+ let(:group) { create(:group, :private, :crm_enabled) }
context 'with authorized user' do
before do
@@ -32,11 +32,17 @@ RSpec.describe Groups::Crm::ContactsController do
sign_in(user)
end
- context 'when feature flag is enabled' do
+ context 'when crm_enabled is true' do
it_behaves_like 'ok response with index template'
end
- context 'when feature flag is not enabled' do
+ context 'when crm_enabled is false' do
+ let(:group) { create(:group, :private) }
+
+ it_behaves_like 'response with 404 status'
+ end
+
+ context 'when feature flag is disabled' do
before do
stub_feature_flags(customer_relations: false)
end
@@ -64,10 +70,10 @@ RSpec.describe Groups::Crm::ContactsController do
end
context 'public group' do
- let(:group) { create(:group, :public) }
+ let(:group) { create(:group, :public, :crm_enabled) }
context 'with anonymous user' do
- it_behaves_like 'ok response with index template'
+ it_behaves_like 'response with 404 status'
end
end
end
diff --git a/spec/requests/groups/crm/organizations_controller_spec.rb b/spec/requests/groups/crm/organizations_controller_spec.rb
index 7595950350d..f38300c3c5b 100644
--- a/spec/requests/groups/crm/organizations_controller_spec.rb
+++ b/spec/requests/groups/crm/organizations_controller_spec.rb
@@ -24,7 +24,7 @@ RSpec.describe Groups::Crm::OrganizationsController do
shared_examples 'ok response with index template if authorized' do
context 'private group' do
- let(:group) { create(:group, :private) }
+ let(:group) { create(:group, :private, :crm_enabled) }
context 'with authorized user' do
before do
@@ -32,11 +32,17 @@ RSpec.describe Groups::Crm::OrganizationsController do
sign_in(user)
end
- context 'when feature flag is enabled' do
+ context 'when crm_enabled is true' do
it_behaves_like 'ok response with index template'
end
- context 'when feature flag is not enabled' do
+ context 'when crm_enabled is false' do
+ let(:group) { create(:group, :private) }
+
+ it_behaves_like 'response with 404 status'
+ end
+
+ context 'when feature flag is disabled' do
before do
stub_feature_flags(customer_relations: false)
end
@@ -64,10 +70,10 @@ RSpec.describe Groups::Crm::OrganizationsController do
end
context 'public group' do
- let(:group) { create(:group, :public) }
+ let(:group) { create(:group, :public, :crm_enabled) }
context 'with anonymous user' do
- it_behaves_like 'ok response with index template'
+ it_behaves_like 'response with 404 status'
end
end
end
diff --git a/spec/requests/groups/settings/access_tokens_controller_spec.rb b/spec/requests/groups/settings/access_tokens_controller_spec.rb
new file mode 100644
index 00000000000..eabdef3c41e
--- /dev/null
+++ b/spec/requests/groups/settings/access_tokens_controller_spec.rb
@@ -0,0 +1,90 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Groups::Settings::AccessTokensController do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:resource) { create(:group) }
+ let_it_be(:bot_user) { create(:user, :project_bot) }
+
+ before_all do
+ resource.add_owner(user)
+ resource.add_maintainer(bot_user)
+ end
+
+ before do
+ sign_in(user)
+ end
+
+ shared_examples 'feature unavailable' do
+ context 'user is not a owner' do
+ before do
+ resource.add_maintainer(user)
+ end
+
+ it { expect(subject).to have_gitlab_http_status(:not_found) }
+ end
+ end
+
+ describe 'GET /:namespace/-/settings/access_tokens' do
+ subject do
+ get group_settings_access_tokens_path(resource)
+ response
+ end
+
+ it_behaves_like 'feature unavailable'
+ it_behaves_like 'GET resource access tokens available'
+ end
+
+ describe 'POST /:namespace/-/settings/access_tokens' do
+ let(:access_token_params) { { name: 'Nerd bot', scopes: ["api"], expires_at: Date.today + 1.month } }
+
+ subject do
+ post group_settings_access_tokens_path(resource), params: { resource_access_token: access_token_params }
+ response
+ end
+
+ it_behaves_like 'feature unavailable'
+ it_behaves_like 'POST resource access tokens available'
+
+ context 'when group access token creation is disabled' do
+ before do
+ resource.namespace_settings.update_column(:resource_access_token_creation_allowed, false)
+ end
+
+ it { expect(subject).to have_gitlab_http_status(:not_found) }
+
+ it 'does not create the token' do
+ expect { subject }.not_to change { PersonalAccessToken.count }
+ end
+
+ it 'does not add the project bot as a member' do
+ expect { subject }.not_to change { Member.count }
+ end
+
+ it 'does not create the project bot user' do
+ expect { subject }.not_to change { User.count }
+ end
+ end
+
+ context 'with custom access level' do
+ let(:access_token_params) { { name: 'Nerd bot', scopes: ["api"], expires_at: Date.today + 1.month, access_level: 20 } }
+
+ subject { post group_settings_access_tokens_path(resource), params: { resource_access_token: access_token_params } }
+
+ it_behaves_like 'POST resource access tokens available'
+ end
+ end
+
+ describe 'PUT /:namespace/-/settings/access_tokens/:id', :sidekiq_inline do
+ let(:resource_access_token) { create(:personal_access_token, user: bot_user) }
+
+ subject do
+ put revoke_group_settings_access_token_path(resource, resource_access_token)
+ response
+ end
+
+ it_behaves_like 'feature unavailable'
+ it_behaves_like 'PUT resource access tokens available'
+ end
+end
diff --git a/spec/requests/projects/google_cloud/deployments_controller_spec.rb b/spec/requests/projects/google_cloud/deployments_controller_spec.rb
new file mode 100644
index 00000000000..a5eccc43147
--- /dev/null
+++ b/spec/requests/projects/google_cloud/deployments_controller_spec.rb
@@ -0,0 +1,103 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Projects::GoogleCloud::DeploymentsController do
+ let_it_be(:project) { create(:project, :public) }
+
+ let_it_be(:user_guest) { create(:user) }
+ let_it_be(:user_developer) { create(:user) }
+ let_it_be(:user_maintainer) { create(:user) }
+ let_it_be(:user_creator) { project.creator }
+
+ let_it_be(:unauthorized_members) { [user_guest, user_developer] }
+ let_it_be(:authorized_members) { [user_maintainer, user_creator] }
+
+ let_it_be(:urls_list) { %W[#{project_google_cloud_deployments_cloud_run_path(project)} #{project_google_cloud_deployments_cloud_storage_path(project)}] }
+
+ before do
+ project.add_guest(user_guest)
+ project.add_developer(user_developer)
+ project.add_maintainer(user_maintainer)
+ end
+
+ describe "Routes must be restricted behind Google OAuth2" do
+ context 'when a public request is made' do
+ it 'returns not found on GET request' do
+ urls_list.each do |url|
+ get url
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+
+ context 'when unauthorized members make requests' do
+ it 'returns not found on GET request' do
+ urls_list.each do |url|
+ unauthorized_members.each do |unauthorized_member|
+ sign_in(unauthorized_member)
+
+ get url
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+ end
+
+ context 'when authorized members make requests' do
+ it 'redirects on GET request' do
+ urls_list.each do |url|
+ authorized_members.each do |authorized_member|
+ sign_in(authorized_member)
+
+ get url
+
+ expect(response).to redirect_to(assigns(:authorize_url))
+ end
+ end
+ end
+ end
+ end
+
+ describe 'Authorized GET project/-/google_cloud/deployments/cloud_run' do
+ let_it_be(:url) { "#{project_google_cloud_deployments_cloud_run_path(project)}" }
+
+ before do
+ allow_next_instance_of(GoogleApi::CloudPlatform::Client) do |client|
+ allow(client).to receive(:validate_token).and_return(true)
+ end
+ end
+
+ it 'renders placeholder' do
+ authorized_members.each do |authorized_member|
+ sign_in(authorized_member)
+
+ get url
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+ end
+
+ describe 'Authorized GET project/-/google_cloud/deployments/cloud_storage' do
+ let_it_be(:url) { "#{project_google_cloud_deployments_cloud_storage_path(project)}" }
+
+ before do
+ allow_next_instance_of(GoogleApi::CloudPlatform::Client) do |client|
+ allow(client).to receive(:validate_token).and_return(true)
+ end
+ end
+
+ it 'renders placeholder' do
+ authorized_members.each do |authorized_member|
+ sign_in(authorized_member)
+
+ get url
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+ end
+end
diff --git a/spec/requests/projects/merge_requests/context_commit_diffs_spec.rb b/spec/requests/projects/merge_requests/context_commit_diffs_spec.rb
index 434e6f19ff5..7be863aae75 100644
--- a/spec/requests/projects/merge_requests/context_commit_diffs_spec.rb
+++ b/spec/requests/projects/merge_requests/context_commit_diffs_spec.rb
@@ -31,7 +31,6 @@ RSpec.describe 'Merge Requests Context Commit Diffs' do
def collection_arguments(pagination_data = {})
{
- environment: nil,
merge_request: merge_request,
commit: nil,
diff_view: :inline,
diff --git a/spec/requests/projects/merge_requests/diffs_spec.rb b/spec/requests/projects/merge_requests/diffs_spec.rb
index ad50c39c65d..e17be1ff984 100644
--- a/spec/requests/projects/merge_requests/diffs_spec.rb
+++ b/spec/requests/projects/merge_requests/diffs_spec.rb
@@ -29,7 +29,6 @@ RSpec.describe 'Merge Requests Diffs' do
def collection_arguments(pagination_data = {})
{
- environment: nil,
merge_request: merge_request,
commit: nil,
diff_view: :inline,
@@ -110,21 +109,6 @@ RSpec.describe 'Merge Requests Diffs' do
end
end
- context 'with a new environment' do
- let(:environment) do
- create(:environment, :available, project: project)
- end
-
- let!(:deployment) do
- create(:deployment, :success, environment: environment, ref: merge_request.source_branch)
- end
-
- it_behaves_like 'serializes diffs with expected arguments' do
- let(:collection) { Gitlab::Diff::FileCollection::MergeRequestDiffBatch }
- let(:expected_options) { collection_arguments(total_pages: 20).merge(environment: environment) }
- end
- end
-
context 'with disabled display_merge_conflicts_in_diff feature' do
before do
stub_feature_flags(display_merge_conflicts_in_diff: false)
diff --git a/spec/requests/projects/merge_requests_discussions_spec.rb b/spec/requests/projects/merge_requests_discussions_spec.rb
index 4921a43ab8b..6cf7bfb1795 100644
--- a/spec/requests/projects/merge_requests_discussions_spec.rb
+++ b/spec/requests/projects/merge_requests_discussions_spec.rb
@@ -244,7 +244,7 @@ RSpec.describe 'merge requests discussions' do
context 'when current_user role changes' do
before do
- Members::UpdateService.new(owner, access_level: Gitlab::Access::GUEST).execute(project.project_member(user))
+ Members::UpdateService.new(owner, access_level: Gitlab::Access::GUEST).execute(project.member(user))
end
it_behaves_like 'cache miss' do
diff --git a/spec/requests/projects/settings/access_tokens_controller_spec.rb b/spec/requests/projects/settings/access_tokens_controller_spec.rb
new file mode 100644
index 00000000000..780d1b8caef
--- /dev/null
+++ b/spec/requests/projects/settings/access_tokens_controller_spec.rb
@@ -0,0 +1,91 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Projects::Settings::AccessTokensController do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:resource) { create(:project, group: group) }
+ let_it_be(:bot_user) { create(:user, :project_bot) }
+
+ before_all do
+ resource.add_maintainer(user)
+ resource.add_maintainer(bot_user)
+ end
+
+ before do
+ sign_in(user)
+ end
+
+ shared_examples 'feature unavailable' do
+ context 'user is not a maintainer' do
+ before do
+ resource.add_developer(user)
+ end
+
+ it { expect(subject).to have_gitlab_http_status(:not_found) }
+ end
+ end
+
+ describe 'GET /:namespace/:project/-/settings/access_tokens' do
+ subject do
+ get project_settings_access_tokens_path(resource)
+ response
+ end
+
+ it_behaves_like 'feature unavailable'
+ it_behaves_like 'GET resource access tokens available'
+ end
+
+ describe 'POST /:namespace/:project/-/settings/access_tokens' do
+ let(:access_token_params) { { name: 'Nerd bot', scopes: ["api"], expires_at: Date.today + 1.month } }
+
+ subject do
+ post project_settings_access_tokens_path(resource), params: { resource_access_token: access_token_params }
+ response
+ end
+
+ it_behaves_like 'feature unavailable'
+ it_behaves_like 'POST resource access tokens available'
+
+ context 'when project access token creation is disabled' do
+ before do
+ group.namespace_settings.update_column(:resource_access_token_creation_allowed, false)
+ end
+
+ it { expect(subject).to have_gitlab_http_status(:not_found) }
+
+ it 'does not create the token' do
+ expect { subject }.not_to change { PersonalAccessToken.count }
+ end
+
+ it 'does not add the project bot as a member' do
+ expect { subject }.not_to change { Member.count }
+ end
+
+ it 'does not create the project bot user' do
+ expect { subject }.not_to change { User.count }
+ end
+ end
+
+ context 'with custom access level' do
+ let(:access_token_params) { { name: 'Nerd bot', scopes: ["api"], expires_at: Date.today + 1.month, access_level: 20 } }
+
+ subject { post project_settings_access_tokens_path(resource), params: { resource_access_token: access_token_params } }
+
+ it_behaves_like 'POST resource access tokens available'
+ end
+ end
+
+ describe 'PUT /:namespace/:project/-/settings/access_tokens/:id', :sidekiq_inline do
+ let(:resource_access_token) { create(:personal_access_token, user: bot_user) }
+
+ subject do
+ put revoke_project_settings_access_token_path(resource, resource_access_token)
+ response
+ end
+
+ it_behaves_like 'feature unavailable'
+ it_behaves_like 'PUT resource access tokens available'
+ end
+end
diff --git a/spec/requests/rack_attack_global_spec.rb b/spec/requests/rack_attack_global_spec.rb
index 244ec111a0c..793438808a5 100644
--- a/spec/requests/rack_attack_global_spec.rb
+++ b/spec/requests/rack_attack_global_spec.rb
@@ -499,9 +499,7 @@ RSpec.describe 'Rack Attack global throttles', :use_clean_rails_memory_store_cac
before do
group.add_owner(user)
- group.create_dependency_proxy_setting!(enabled: true)
other_group.add_owner(other_user)
- other_group.create_dependency_proxy_setting!(enabled: true)
allow(Gitlab.config.dependency_proxy)
.to receive(:enabled).and_return(true)
@@ -533,16 +531,10 @@ RSpec.describe 'Rack Attack global throttles', :use_clean_rails_memory_store_cac
context 'getting a blob' do
let_it_be(:blob) { create(:dependency_proxy_blob) }
+ let_it_be(:other_blob) { create(:dependency_proxy_blob) }
- let(:path) { "/v2/#{group.path}/dependency_proxy/containers/alpine/blobs/sha256:a0d0a0d46f8b52473982a3c466318f479767577551a53ffc9074c9fa7035982e" }
- let(:other_path) { "/v2/#{other_group.path}/dependency_proxy/containers/alpine/blobs/sha256:a0d0a0d46f8b52473982a3c466318f479767577551a53ffc9074c9fa7035982e" }
- let(:blob_response) { { status: :success, blob: blob, from_cache: false } }
-
- before do
- allow_next_instance_of(DependencyProxy::FindOrCreateBlobService) do |instance|
- allow(instance).to receive(:execute).and_return(blob_response)
- end
- end
+ let(:path) { "/v2/#{blob.group.path}/dependency_proxy/containers/alpine/blobs/sha256:a0d0a0d46f8b52473982a3c466318f479767577551a53ffc9074c9fa7035982e" }
+ let(:other_path) { "/v2/#{other_blob.group.path}/dependency_proxy/containers/alpine/blobs/sha256:a0d0a0d46f8b52473982a3c466318f479767577551a53ffc9074c9fa7035982e" }
it_behaves_like 'rate-limited token-authenticated requests'
end
diff --git a/spec/requests/recursive_webhook_detection_spec.rb b/spec/requests/recursive_webhook_detection_spec.rb
new file mode 100644
index 00000000000..a3014bf1d73
--- /dev/null
+++ b/spec/requests/recursive_webhook_detection_spec.rb
@@ -0,0 +1,182 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Recursive webhook detection', :sidekiq_inline, :clean_gitlab_redis_shared_state, :request_store do
+ include StubRequests
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project, :repository, namespace: user.namespace, creator: user) }
+ let_it_be(:merge_request) { create(:merge_request, source_project: project) }
+ let_it_be(:project_hook) { create(:project_hook, project: project, merge_requests_events: true) }
+ let_it_be(:system_hook) { create(:system_hook, merge_requests_events: true) }
+
+ # Trigger a change to the merge request to fire the webhooks.
+ def trigger_web_hooks
+ params = { merge_request: { description: FFaker::Lorem.sentence } }
+ put project_merge_request_path(project, merge_request), params: params, headers: headers
+ end
+
+ def stub_requests
+ stub_full_request(project_hook.url, method: :post, ip_address: '8.8.8.8')
+ stub_full_request(system_hook.url, method: :post, ip_address: '8.8.8.9')
+ end
+
+ before do
+ login_as(user)
+ end
+
+ context 'when the request headers include the recursive webhook detection header' do
+ let(:uuid) { SecureRandom.uuid }
+ let(:headers) { { Gitlab::WebHooks::RecursionDetection::UUID::HEADER => uuid } }
+
+ it 'executes all webhooks, logs no errors, and the webhook requests contain the same UUID header', :aggregate_failures do
+ stub_requests
+
+ expect(Gitlab::AuthLogger).not_to receive(:error)
+
+ trigger_web_hooks
+
+ expect(WebMock).to have_requested(:post, stubbed_hostname(project_hook.url))
+ .with { |req| req.headers['X-Gitlab-Event-Uuid'] == uuid }
+ .once
+ expect(WebMock).to have_requested(:post, stubbed_hostname(system_hook.url))
+ .with { |req| req.headers['X-Gitlab-Event-Uuid'] == uuid }
+ .once
+ end
+
+ context 'when one of the webhooks is recursive' do
+ before do
+ # Recreate the necessary state for the previous request to be
+ # considered made from the webhook.
+ Gitlab::WebHooks::RecursionDetection.set_request_uuid(uuid)
+ Gitlab::WebHooks::RecursionDetection.register!(project_hook)
+ Gitlab::WebHooks::RecursionDetection.set_request_uuid(nil)
+ end
+
+ it 'executes all webhooks and logs an error for the recursive hook', :aggregate_failures do
+ stub_requests
+
+ expect(Gitlab::AuthLogger).to receive(:error).with(
+ include(
+ message: 'Webhook recursion detected and will be blocked in future',
+ hook_id: project_hook.id,
+ recursion_detection: {
+ uuid: uuid,
+ ids: [project_hook.id]
+ }
+ )
+ ).twice # Twice: once in `#async_execute`, and again in `#execute`.
+
+ trigger_web_hooks
+
+ expect(WebMock).to have_requested(:post, stubbed_hostname(project_hook.url)).once
+ expect(WebMock).to have_requested(:post, stubbed_hostname(system_hook.url)).once
+ end
+ end
+
+ context 'when the count limit has been reached' do
+ let_it_be(:previous_hooks) { create_list(:project_hook, 3) }
+
+ before do
+ stub_const('Gitlab::WebHooks::RecursionDetection::COUNT_LIMIT', 2)
+ # Recreate the necessary state for a number of previous webhooks to
+ # have been triggered previously.
+ Gitlab::WebHooks::RecursionDetection.set_request_uuid(uuid)
+ previous_hooks.each { Gitlab::WebHooks::RecursionDetection.register!(_1) }
+ Gitlab::WebHooks::RecursionDetection.set_request_uuid(nil)
+ end
+
+ it 'executes and logs errors for all hooks', :aggregate_failures do
+ stub_requests
+ previous_hook_ids = previous_hooks.map(&:id)
+
+ expect(Gitlab::AuthLogger).to receive(:error).with(
+ include(
+ message: 'Webhook recursion detected and will be blocked in future',
+ hook_id: project_hook.id,
+ recursion_detection: {
+ uuid: uuid,
+ ids: include(*previous_hook_ids)
+ }
+ )
+ ).twice
+ expect(Gitlab::AuthLogger).to receive(:error).with(
+ include(
+ message: 'Webhook recursion detected and will be blocked in future',
+ hook_id: system_hook.id,
+ recursion_detection: {
+ uuid: uuid,
+ ids: include(*previous_hook_ids)
+ }
+ )
+ ).twice
+
+ trigger_web_hooks
+
+ expect(WebMock).to have_requested(:post, stubbed_hostname(project_hook.url)).once
+ expect(WebMock).to have_requested(:post, stubbed_hostname(system_hook.url)).once
+ end
+ end
+ end
+
+ context 'when the recursive webhook detection header is absent' do
+ let(:headers) { {} }
+
+ let(:uuid_header_spy) do
+ Class.new do
+ attr_reader :values
+
+ def initialize
+ @values = []
+ end
+
+ def to_proc
+ proc do |method, *args|
+ method.call(*args).tap do |headers|
+ @values << headers[Gitlab::WebHooks::RecursionDetection::UUID::HEADER]
+ end
+ end
+ end
+ end.new
+ end
+
+ before do
+ allow(Gitlab::WebHooks::RecursionDetection).to receive(:header).at_least(:once).and_wrap_original(&uuid_header_spy)
+ end
+
+ it 'executes all webhooks, logs no errors, and the webhook requests contain different UUID headers', :aggregate_failures do
+ stub_requests
+
+ expect(Gitlab::AuthLogger).not_to receive(:error)
+
+ trigger_web_hooks
+
+ uuid_headers = uuid_header_spy.values
+
+ expect(uuid_headers).to all(be_present)
+ expect(uuid_headers.uniq.length).to eq(2)
+ expect(WebMock).to have_requested(:post, stubbed_hostname(project_hook.url))
+ .with { |req| uuid_headers.include?(req.headers['X-Gitlab-Event-Uuid']) }
+ .once
+ expect(WebMock).to have_requested(:post, stubbed_hostname(system_hook.url))
+ .with { |req| uuid_headers.include?(req.headers['X-Gitlab-Event-Uuid']) }
+ .once
+ end
+
+ it 'uses new UUID values between requests' do
+ stub_requests
+
+ trigger_web_hooks
+ trigger_web_hooks
+
+ uuid_headers = uuid_header_spy.values
+
+ expect(uuid_headers).to all(be_present)
+ expect(uuid_headers.length).to eq(4)
+ expect(uuid_headers.uniq.length).to eq(4)
+ expect(WebMock).to have_requested(:post, stubbed_hostname(project_hook.url)).twice
+ expect(WebMock).to have_requested(:post, stubbed_hostname(system_hook.url)).twice
+ end
+ end
+end
diff --git a/spec/requests/sandbox_controller_spec.rb b/spec/requests/sandbox_controller_spec.rb
new file mode 100644
index 00000000000..4fc26580123
--- /dev/null
+++ b/spec/requests/sandbox_controller_spec.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe SandboxController do
+ describe 'GET #mermaid' do
+ it 'renders page without template' do
+ get sandbox_mermaid_path
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to render_template(layout: nil)
+ end
+ end
+end
diff --git a/spec/requests/users_controller_spec.rb b/spec/requests/users_controller_spec.rb
index eefc24f7824..dacc11eece7 100644
--- a/spec/requests/users_controller_spec.rb
+++ b/spec/requests/users_controller_spec.rb
@@ -636,6 +636,8 @@ RSpec.describe UsersController do
describe 'GET #exists' do
before do
sign_in(user)
+
+ allow(::Gitlab::ApplicationRateLimiter).to receive(:throttled?).and_return(false)
end
context 'when user exists' do
@@ -677,6 +679,17 @@ RSpec.describe UsersController do
end
end
end
+
+ context 'when the rate limit has been reached' do
+ it 'returns status 429 Too Many Requests', :aggregate_failures do
+ ip = '1.2.3.4'
+ expect(::Gitlab::ApplicationRateLimiter).to receive(:throttled?).with(:username_exists, scope: ip).and_return(true)
+
+ get user_exists_url(user.username), env: { 'REMOTE_ADDR': ip }
+
+ expect(response).to have_gitlab_http_status(:too_many_requests)
+ end
+ end
end
describe '#ensure_canonical_path' do