diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-01-20 12:16:11 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-01-20 12:16:11 +0300 |
commit | edaa33dee2ff2f7ea3fac488d41558eb5f86d68c (patch) | |
tree | 11f143effbfeba52329fb7afbd05e6e2a3790241 /spec/requests/api | |
parent | d8a5691316400a0f7ec4f83832698f1988eb27c1 (diff) |
Add latest changes from gitlab-org/gitlab@14-7-stable-eev14.7.0-rc42
Diffstat (limited to 'spec/requests/api')
35 files changed, 1579 insertions, 597 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 |