diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-03-18 23:02:30 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-03-18 23:02:30 +0300 |
commit | 41fe97390ceddf945f3d967b8fdb3de4c66b7dea (patch) | |
tree | 9c8d89a8624828992f06d892cd2f43818ff5dcc8 /spec/requests | |
parent | 0804d2dc31052fb45a1efecedc8e06ce9bc32862 (diff) |
Add latest changes from gitlab-org/gitlab@14-9-stable-eev14.9.0-rc42
Diffstat (limited to 'spec/requests')
75 files changed, 2411 insertions, 322 deletions
diff --git a/spec/requests/admin/background_migrations_controller_spec.rb b/spec/requests/admin/background_migrations_controller_spec.rb index 67c9c4df827..55971a00e55 100644 --- a/spec/requests/admin/background_migrations_controller_spec.rb +++ b/spec/requests/admin/background_migrations_controller_spec.rb @@ -16,7 +16,13 @@ RSpec.describe Admin::BackgroundMigrationsController, :enable_admin_mode do create(:batched_background_migration_job, :failed, batched_migration: migration, batch_size: 10, min_value: 6, max_value: 15, attempts: 3) allow_next_instance_of(Gitlab::BackgroundMigration::BatchingStrategies::PrimaryKeyBatchingStrategy) do |batch_class| - allow(batch_class).to receive(:next_batch).with(anything, anything, batch_min_value: 6, batch_size: 5).and_return([6, 10]) + allow(batch_class).to receive(:next_batch).with( + anything, + anything, + batch_min_value: 6, + batch_size: 5, + job_arguments: migration.job_arguments + ).and_return([6, 10]) end end diff --git a/spec/requests/api/admin/instance_clusters_spec.rb b/spec/requests/api/admin/instance_clusters_spec.rb index ab3b6b718e1..7b3224f58c5 100644 --- a/spec/requests/api/admin/instance_clusters_spec.rb +++ b/spec/requests/api/admin/instance_clusters_spec.rb @@ -21,6 +21,10 @@ RSpec.describe ::API::Admin::InstanceClusters do create_list(:cluster, 3, :provided_by_gcp, :instance, :production_environment) end + include_examples ':certificate_based_clusters feature flag API responses' do + let(:subject) { get api("/admin/clusters", admin_user) } + end + context "when authenticated as a non-admin user" do it 'returns 403' do get api('/admin/clusters', regular_user) @@ -62,6 +66,10 @@ RSpec.describe ::API::Admin::InstanceClusters do let(:cluster_id) { cluster.id } + include_examples ':certificate_based_clusters feature flag API responses' do + let(:subject) { get api("/admin/clusters/#{cluster_id}", admin_user) } + end + context "when authenticated as admin" do before do get api("/admin/clusters/#{cluster_id}", admin_user) @@ -188,6 +196,10 @@ RSpec.describe ::API::Admin::InstanceClusters do } end + include_examples ':certificate_based_clusters feature flag API responses' do + let(:subject) { post api('/admin/clusters/add', admin_user), params: cluster_params } + end + context 'authorized user' do before do post api('/admin/clusters/add', admin_user), params: cluster_params @@ -317,6 +329,10 @@ RSpec.describe ::API::Admin::InstanceClusters do create(:cluster, :instance, :provided_by_gcp, domain: 'old-domain.com') end + include_examples ':certificate_based_clusters feature flag API responses' do + let(:subject) { put api("/admin/clusters/#{cluster.id}", admin_user), params: update_params } + end + context 'authorized user' do before do put api("/admin/clusters/#{cluster.id}", admin_user), params: update_params @@ -448,6 +464,10 @@ RSpec.describe ::API::Admin::InstanceClusters do create(:cluster, :instance, :provided_by_gcp) end + include_examples ':certificate_based_clusters feature flag API responses' do + let(:subject) { delete api("/admin/clusters/#{cluster.id}", admin_user), params: cluster_params } + end + context 'authorized user' do before do delete api("/admin/clusters/#{cluster.id}", admin_user), params: cluster_params diff --git a/spec/requests/api/broadcast_messages_spec.rb b/spec/requests/api/broadcast_messages_spec.rb index b023ec398a2..76412c80f4c 100644 --- a/spec/requests/api/broadcast_messages_spec.rb +++ b/spec/requests/api/broadcast_messages_spec.rb @@ -17,7 +17,7 @@ RSpec.describe API::BroadcastMessages do expect(response).to include_pagination_headers expect(json_response).to be_kind_of(Array) expect(json_response.first.keys) - .to match_array(%w(id message starts_at ends_at color font active target_path broadcast_type dismissable)) + .to match_array(%w(id message starts_at ends_at color font active target_access_levels target_path broadcast_type dismissable)) end end @@ -28,7 +28,7 @@ RSpec.describe API::BroadcastMessages do expect(response).to have_gitlab_http_status(:ok) expect(json_response['id']).to eq message.id expect(json_response.keys) - .to match_array(%w(id message starts_at ends_at color font active target_path broadcast_type dismissable)) + .to match_array(%w(id message starts_at ends_at color font active target_access_levels target_path broadcast_type dismissable)) end end @@ -77,6 +77,16 @@ RSpec.describe API::BroadcastMessages do expect(json_response['font']).to eq attrs[:font] end + it 'accepts target access levels' do + target_access_levels = [Gitlab::Access::GUEST, Gitlab::Access::DEVELOPER] + attrs = attributes_for(:broadcast_message, target_access_levels: target_access_levels) + + post api('/broadcast_messages', admin), params: attrs + + expect(response).to have_gitlab_http_status(:created) + expect(json_response['target_access_levels']).to eq attrs[:target_access_levels] + end + it 'accepts a target path' do attrs = attributes_for(:broadcast_message, target_path: "*/welcome") @@ -171,6 +181,15 @@ RSpec.describe API::BroadcastMessages do expect { message.reload }.to change { message.message }.to('new message') end + it 'accepts a new target_access_levels' do + attrs = { target_access_levels: [Gitlab::Access::MAINTAINER] } + + put api("/broadcast_messages/#{message.id}", admin), params: attrs + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['target_access_levels']).to eq attrs[:target_access_levels] + end + it 'accepts a new target_path' do attrs = { target_path: '*/welcome' } diff --git a/spec/requests/api/ci/jobs_spec.rb b/spec/requests/api/ci/jobs_spec.rb index 7c85cbc31a5..f6dae7e8e23 100644 --- a/spec/requests/api/ci/jobs_spec.rb +++ b/spec/requests/api/ci/jobs_spec.rb @@ -707,12 +707,14 @@ RSpec.describe API::Ci::Jobs do end describe 'POST /projects/:id/jobs/:job_id/play' do + let(:params) { {} } + before do - post api("/projects/#{project.id}/jobs/#{job.id}/play", api_user) + post api("/projects/#{project.id}/jobs/#{job.id}/play", api_user), params: params end context 'on a playable job' do - let_it_be(:job) { create(:ci_bridge, :playable, pipeline: pipeline, downstream: project) } + let_it_be(:job) { create(:ci_build, :manual, project: project, pipeline: pipeline) } before do project.add_developer(user) @@ -720,6 +722,8 @@ RSpec.describe API::Ci::Jobs do context 'when user is authorized to trigger a manual action' do context 'that is a bridge' do + let_it_be(:job) { create(:ci_bridge, :playable, pipeline: pipeline, downstream: project) } + it 'plays the job' do expect(response).to have_gitlab_http_status(:ok) expect(json_response['user']['id']).to eq(user.id) @@ -729,8 +733,6 @@ RSpec.describe API::Ci::Jobs do end context 'that is a build' do - let_it_be(:job) { create(:ci_build, :manual, project: project, pipeline: pipeline) } - it 'plays the job' do expect(response).to have_gitlab_http_status(:ok) expect(json_response['user']['id']).to eq(user.id) @@ -738,6 +740,47 @@ RSpec.describe API::Ci::Jobs do expect(job.reload).to be_pending end end + + context 'when the user provides valid custom variables' do + let(:params) { { job_variables_attributes: [{ key: 'TEST_VAR', value: 'test' }] } } + + it 'applies the variables to the job' do + expect(response).to have_gitlab_http_status(:ok) + expect(job.reload).to be_pending + expect(job.job_variables.map(&:key)).to contain_exactly('TEST_VAR') + expect(job.job_variables.map(&:value)).to contain_exactly('test') + end + end + + context 'when the user provides a variable without a key' do + let(:params) { { job_variables_attributes: [{ value: 'test' }] } } + + it 'reports that the key is missing' do + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['error']).to eq('job_variables_attributes[0][key] is missing') + expect(job.reload).to be_manual + end + end + + context 'when the user provides a variable without a value' do + let(:params) { { job_variables_attributes: [{ key: 'TEST_VAR' }] } } + + it 'reports that the value is missing' do + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['error']).to eq('job_variables_attributes[0][value] is missing') + expect(job.reload).to be_manual + end + end + + context 'when the user provides both valid and invalid variables' do + let(:params) { { job_variables_attributes: [{ key: 'TEST_VAR', value: 'test' }, { value: 'test2' }] } } + + it 'reports the invalid variables and does not run the job' do + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['error']).to eq('job_variables_attributes[1][key] is missing') + expect(job.reload).to be_manual + end + end end context 'when user is not authorized to trigger a manual action' do diff --git a/spec/requests/api/ci/pipelines_spec.rb b/spec/requests/api/ci/pipelines_spec.rb index 1b87a5e24f5..12faeec94da 100644 --- a/spec/requests/api/ci/pipelines_spec.rb +++ b/spec/requests/api/ci/pipelines_spec.rb @@ -1075,6 +1075,23 @@ RSpec.describe API::Ci::Pipelines do expect(json_response['id']).to be nil end end + + context 'handles errors' do + before do + service_response = ServiceResponse.error(http_status: 403, message: 'hello world') + allow_next_instance_of(::Ci::RetryPipelineService) do |service| + allow(service).to receive(:check_access).and_return(service_response) + end + end + + it 'returns error' do + post api("/projects/#{project.id}/pipelines/#{pipeline.id}/retry", user) + + expect(response).to have_gitlab_http_status(:forbidden) + expect(json_response['message']).to eq 'hello world' + expect(json_response['id']).to be nil + end + end end describe 'POST /projects/:id/pipelines/:pipeline_id/cancel' do diff --git a/spec/requests/api/ci/runner/jobs_request_post_spec.rb b/spec/requests/api/ci/runner/jobs_request_post_spec.rb index 68f7581bf06..d317386dc73 100644 --- a/spec/requests/api/ci/runner/jobs_request_post_spec.rb +++ b/spec/requests/api/ci/runner/jobs_request_post_spec.rb @@ -156,7 +156,7 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do 'sha' => job.sha, 'before_sha' => job.before_sha, 'ref_type' => 'branch', - 'refspecs' => ["+#{pipeline.sha}:refs/pipelines/#{pipeline.id}", + 'refspecs' => ["+refs/pipelines/#{pipeline.id}:refs/pipelines/#{pipeline.id}", "+refs/heads/#{job.ref}:refs/remotes/origin/#{job.ref}"], 'depth' => project.ci_default_git_depth } end @@ -291,7 +291,7 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do expect(response).to have_gitlab_http_status(:created) expect(json_response['git_info']['refspecs']) - .to contain_exactly("+#{pipeline.sha}:refs/pipelines/#{pipeline.id}", + .to contain_exactly("+refs/pipelines/#{pipeline.id}:refs/pipelines/#{pipeline.id}", '+refs/tags/*:refs/tags/*', '+refs/heads/*:refs/remotes/origin/*') end @@ -359,7 +359,7 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do expect(response).to have_gitlab_http_status(:created) expect(json_response['git_info']['refspecs']) - .to contain_exactly("+#{pipeline.sha}:refs/pipelines/#{pipeline.id}", + .to contain_exactly("+refs/pipelines/#{pipeline.id}:refs/pipelines/#{pipeline.id}", '+refs/tags/*:refs/tags/*', '+refs/heads/*:refs/remotes/origin/*') end diff --git a/spec/requests/api/ci/runner/jobs_trace_spec.rb b/spec/requests/api/ci/runner/jobs_trace_spec.rb index 2760e306693..d6928969beb 100644 --- a/spec/requests/api/ci/runner/jobs_trace_spec.rb +++ b/spec/requests/api/ci/runner/jobs_trace_spec.rb @@ -35,7 +35,7 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_trace_chunks do let(:headers) { { API::Ci::Helpers::Runner::JOB_TOKEN_HEADER => job.token, 'Content-Type' => 'text/plain' } } let(:headers_with_range) { headers.merge({ 'Content-Range' => '11-20' }) } - let(:update_interval) { 10.seconds.to_i } + let(:update_interval) { 10.seconds } before do initial_patch_the_trace @@ -81,7 +81,7 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_trace_chunks do end context 'when job was not updated recently' do - let(:update_interval) { 15.minutes.to_i } + let(:update_interval) { 16.minutes } it { expect { patch_the_trace }.to change { job.updated_at } } @@ -293,10 +293,10 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_trace_chunks do end end - Timecop.travel(job.updated_at + update_interval) do + travel_to(job.updated_at + update_interval) do patch api("/jobs/#{job_id}/trace"), params: content, headers: request_headers - job.reload end + job.reload end def initial_patch_the_trace diff --git a/spec/requests/api/ci/runner/runners_post_spec.rb b/spec/requests/api/ci/runner/runners_post_spec.rb index 5eb5d3977a3..1d553751eea 100644 --- a/spec/requests/api/ci/runner/runners_post_spec.rb +++ b/spec/requests/api/ci/runner/runners_post_spec.rb @@ -15,7 +15,7 @@ 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_next_instance_of(::Ci::Runners::RegisterRunnerService) do |service| allow(service).to receive(:execute).and_return(nil) end @@ -43,7 +43,7 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do let_it_be(:new_runner) { create(:ci_runner) } before do - allow_next_instance_of(::Ci::RegisterRunnerService) do |service| + allow_next_instance_of(::Ci::Runners::RegisterRunnerService) do |service| expected_params = { description: 'server.hostname', maintenance_note: 'Some maintainer notes', @@ -108,7 +108,7 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do let(:new_runner) { create(:ci_runner) } it 'converts to maintenance_note param' do - allow_next_instance_of(::Ci::RegisterRunnerService) do |service| + allow_next_instance_of(::Ci::Runners::RegisterRunnerService) do |service| expect(service).to receive(:execute) .once .with('valid token', a_hash_including('maintenance_note' => 'Some maintainer notes') @@ -133,7 +133,7 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do let_it_be(:new_runner) { create(:ci_runner) } it 'uses active value in registration' do - expect_next_instance_of(::Ci::RegisterRunnerService) do |service| + expect_next_instance_of(::Ci::Runners::RegisterRunnerService) do |service| expected_params = { active: false }.stringify_keys expect(service).to receive(:execute) diff --git a/spec/requests/api/ci/runner/runners_reset_spec.rb b/spec/requests/api/ci/runner/runners_reset_spec.rb new file mode 100644 index 00000000000..8a61012ead1 --- /dev/null +++ b/spec/requests/api/ci/runner/runners_reset_spec.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do + include StubGitlabCalls + include RedisHelpers + include WorkhorseHelpers + + before do + stub_feature_flags(ci_enable_live_trace: true) + stub_feature_flags(runner_registration_control: false) + stub_gitlab_calls + stub_application_setting(valid_runner_registrars: ApplicationSetting::VALID_RUNNER_REGISTRAR_TYPES) + end + + let_it_be(:group_settings) { create(:namespace_settings, runner_token_expiration_interval: 5.days.to_i) } + let_it_be(:group) { create(:group, namespace_settings: group_settings) } + let_it_be(:instance_runner, reload: true) { create(:ci_runner, :instance) } + let_it_be(:group_runner) { create(:ci_runner, :group, groups: [group], token_expires_at: 1.day.from_now) } + + describe 'POST /runners/reset_authentication_token', :freeze_time do + context 'current token provided' do + it "resets authentication token when token doesn't have an expiration" do + expect do + post api("/runners/reset_authentication_token"), params: { token: instance_runner.reload.token } + + expect(response).to have_gitlab_http_status(:success) + expect(json_response).to eq({ 'token' => instance_runner.reload.token, 'token_expires_at' => nil }) + expect(instance_runner.reload.token_expires_at).to be_nil + end.to change { instance_runner.reload.token } + end + + it 'resets authentication token when token is not expired' do + expect do + post api("/runners/reset_authentication_token"), params: { token: group_runner.reload.token } + + expect(response).to have_gitlab_http_status(:success) + expect(json_response).to eq({ 'token' => group_runner.reload.token, 'token_expires_at' => group_runner.reload.token_expires_at.iso8601(3) }) + expect(group_runner.reload.token_expires_at).to eq(5.days.from_now) + end.to change { group_runner.reload.token } + end + + it 'does not reset authentication token when token is expired' do + expect do + instance_runner.update!(token_expires_at: 1.day.ago) + post api("/runners/reset_authentication_token"), params: { token: instance_runner.reload.token } + + expect(response).to have_gitlab_http_status(:forbidden) + instance_runner.update!(token_expires_at: nil) + end.not_to change { instance_runner.reload.token } + end + end + + context 'wrong current token provided' do + it 'does not reset authentication token' do + expect do + post api("/runners/reset_authentication_token"), params: { token: 'garbage' } + + expect(response).to have_gitlab_http_status(:forbidden) + end.not_to change { instance_runner.reload.token } + end + end + end +end diff --git a/spec/requests/api/ci/runners_spec.rb b/spec/requests/api/ci/runners_spec.rb index 336ce70d8d2..a1fda68b77b 100644 --- a/spec/requests/api/ci/runners_spec.rb +++ b/spec/requests/api/ci/runners_spec.rb @@ -530,7 +530,7 @@ RSpec.describe API::Ci::Runners do context 'admin user' do context 'when runner is shared' do it 'deletes runner' do - expect_next_instance_of(Ci::UnregisterRunnerService, shared_runner) do |service| + expect_next_instance_of(Ci::Runners::UnregisterRunnerService, shared_runner, admin) do |service| expect(service).to receive(:execute).once.and_call_original end @@ -548,7 +548,7 @@ RSpec.describe API::Ci::Runners do context 'when runner is not shared' do it 'deletes used project runner' do - expect_next_instance_of(Ci::UnregisterRunnerService, project_runner) do |service| + expect_next_instance_of(Ci::Runners::UnregisterRunnerService, project_runner, admin) do |service| expect(service).to receive(:execute).once.and_call_original end @@ -561,7 +561,7 @@ RSpec.describe API::Ci::Runners do end it 'returns 404 if runner does not exist' do - allow_next_instance_of(Ci::UnregisterRunnerService) do |service| + allow_next_instance_of(Ci::Runners::UnregisterRunnerService) do |service| expect(service).not_to receive(:execute) end @@ -646,7 +646,7 @@ RSpec.describe API::Ci::Runners do context 'unauthorized user' do it 'does not delete project runner' do - allow_next_instance_of(Ci::UnregisterRunnerService) do |service| + allow_next_instance_of(Ci::Runners::UnregisterRunnerService) do |service| expect(service).not_to receive(:execute) end diff --git a/spec/requests/api/ci/secure_files_spec.rb b/spec/requests/api/ci/secure_files_spec.rb index 5cf6999f60a..aa479cb8713 100644 --- a/spec/requests/api/ci/secure_files_spec.rb +++ b/spec/requests/api/ci/secure_files_spec.rb @@ -8,49 +8,72 @@ RSpec.describe API::Ci::SecureFiles do stub_feature_flags(ci_secure_files: true) end - let_it_be(:user) { create(:user) } - let_it_be(:user2) { create(:user) } - let_it_be(:project) { create(:project, creator_id: user.id) } - let_it_be(:maintainer) { create(:project_member, :maintainer, user: user, project: project) } - let_it_be(:developer) { create(:project_member, :developer, user: user2, project: project) } + let_it_be(:maintainer) { create(:user) } + let_it_be(:developer) { create(:user) } + let_it_be(:guest) { create(:user) } + let_it_be(:anonymous) { create(:user) } + let_it_be(:project) { create(:project, creator_id: maintainer.id) } let_it_be(:secure_file) { create(:ci_secure_file, project: project) } + before_all do + project.add_maintainer(maintainer) + project.add_developer(developer) + project.add_guest(guest) + end + describe 'GET /projects/:id/secure_files' do context 'feature flag' do it 'returns a 503 when the feature flag is disabled' do stub_feature_flags(ci_secure_files: false) - get api("/projects/#{project.id}/secure_files", user) + get api("/projects/#{project.id}/secure_files", maintainer) expect(response).to have_gitlab_http_status(:service_unavailable) end it 'returns a 200 when the feature flag is enabled' do - get api("/projects/#{project.id}/secure_files", user) + get api("/projects/#{project.id}/secure_files", maintainer) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to be_a(Array) + end + end + + context 'authenticated user with admin permissions' do + it 'returns project secure files' do + get api("/projects/#{project.id}/secure_files", maintainer) expect(response).to have_gitlab_http_status(:ok) expect(json_response).to be_a(Array) end end - context 'authorized user with proper permissions' do + context 'authenticated user with read permissions' do it 'returns project secure files' do - get api("/projects/#{project.id}/secure_files", user) + get api("/projects/#{project.id}/secure_files", developer) expect(response).to have_gitlab_http_status(:ok) expect(json_response).to be_a(Array) end end - context 'authorized user with invalid permissions' do + context 'authenticated user with guest permissions' do it 'does not return project secure files' do - get api("/projects/#{project.id}/secure_files", user2) + get api("/projects/#{project.id}/secure_files", guest) expect(response).to have_gitlab_http_status(:forbidden) end end - context 'unauthorized user' do + context 'authenticated user with no permissions' do + it 'does not return project secure files' do + get api("/projects/#{project.id}/secure_files", anonymous) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'unauthenticated user' do it 'does not return project secure files' do get api("/projects/#{project.id}/secure_files") @@ -60,9 +83,9 @@ RSpec.describe API::Ci::SecureFiles do end describe 'GET /projects/:id/secure_files/:secure_file_id' do - context 'authorized user with proper permissions' do + context 'authenticated user with admin permissions' do it 'returns project secure file details' do - get api("/projects/#{project.id}/secure_files/#{secure_file.id}", user) + get api("/projects/#{project.id}/secure_files/#{secure_file.id}", maintainer) expect(response).to have_gitlab_http_status(:ok) expect(json_response['name']).to eq(secure_file.name) @@ -70,21 +93,31 @@ RSpec.describe API::Ci::SecureFiles do end it 'responds with 404 Not Found if requesting non-existing secure file' do - get api("/projects/#{project.id}/secure_files/99999", user) + get api("/projects/#{project.id}/secure_files/#{non_existing_record_id}", maintainer) expect(response).to have_gitlab_http_status(:not_found) end end - context 'authorized user with invalid permissions' do + context 'authenticated user with read permissions' do + it 'returns project secure file details' do + get api("/projects/#{project.id}/secure_files/#{secure_file.id}", developer) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['name']).to eq(secure_file.name) + expect(json_response['permissions']).to eq(secure_file.permissions) + end + end + + context 'authenticated user with no permissions' do it 'does not return project secure file details' do - get api("/projects/#{project.id}/secure_files/#{secure_file.id}", user2) + get api("/projects/#{project.id}/secure_files/#{secure_file.id}", anonymous) - expect(response).to have_gitlab_http_status(:forbidden) + expect(response).to have_gitlab_http_status(:not_found) end end - context 'unauthorized user' do + context 'unauthenticated user' do it 'does not return project secure file details' do get api("/projects/#{project.id}/secure_files/#{secure_file.id}") @@ -94,34 +127,47 @@ RSpec.describe API::Ci::SecureFiles do end describe 'GET /projects/:id/secure_files/:secure_file_id/download' do - context 'authorized user with proper permissions' do + context 'authenticated user with admin permissions' do it 'returns a secure file' do sample_file = fixture_file('ci_secure_files/upload-keystore.jks') secure_file.file = CarrierWaveStringFile.new(sample_file) secure_file.save! - get api("/projects/#{project.id}/secure_files/#{secure_file.id}/download", user) + get api("/projects/#{project.id}/secure_files/#{secure_file.id}/download", maintainer) expect(response).to have_gitlab_http_status(:ok) expect(Base64.encode64(response.body)).to eq(Base64.encode64(sample_file)) end it 'responds with 404 Not Found if requesting non-existing secure file' do - get api("/projects/#{project.id}/secure_files/99999/download", user) + get api("/projects/#{project.id}/secure_files/#{non_existing_record_id}/download", maintainer) expect(response).to have_gitlab_http_status(:not_found) end end - context 'authorized user with invalid permissions' do + context 'authenticated user with read permissions' do + it 'returns a secure file' do + sample_file = fixture_file('ci_secure_files/upload-keystore.jks') + secure_file.file = CarrierWaveStringFile.new(sample_file) + secure_file.save! + + get api("/projects/#{project.id}/secure_files/#{secure_file.id}/download", developer) + + expect(response).to have_gitlab_http_status(:ok) + expect(Base64.encode64(response.body)).to eq(Base64.encode64(sample_file)) + end + end + + context 'authenticated user with no permissions' do it 'does not return project secure file details' do - get api("/projects/#{project.id}/secure_files/#{secure_file.id}/download", user2) + get api("/projects/#{project.id}/secure_files/#{secure_file.id}/download", anonymous) - expect(response).to have_gitlab_http_status(:forbidden) + expect(response).to have_gitlab_http_status(:not_found) end end - context 'unauthorized user' do + context 'unauthenticated user' do it 'does not return project secure file details' do get api("/projects/#{project.id}/secure_files/#{secure_file.id}/download") @@ -131,7 +177,7 @@ RSpec.describe API::Ci::SecureFiles do end describe 'POST /projects/:id/secure_files' do - context 'authorized user with proper permissions' do + context 'authenticated user with admin permissions' do it 'creates a secure file' do params = { file: fixture_file_upload('spec/fixtures/ci_secure_files/upload-keystore.jks'), @@ -140,7 +186,7 @@ RSpec.describe API::Ci::SecureFiles do } expect do - post api("/projects/#{project.id}/secure_files", user), params: params + post api("/projects/#{project.id}/secure_files", maintainer), params: params end.to change {project.secure_files.count}.by(1) expect(response).to have_gitlab_http_status(:created) @@ -154,6 +200,7 @@ RSpec.describe API::Ci::SecureFiles do Digest::SHA256.hexdigest(fixture_file('ci_secure_files/upload-keystore.jks')) ) expect(json_response['id']).to eq(secure_file.id) + expect(Time.parse(json_response['created_at'])).to be_like_time(secure_file.created_at) end it 'creates a secure file with read_only permissions by default' do @@ -163,7 +210,7 @@ RSpec.describe API::Ci::SecureFiles do } expect do - post api("/projects/#{project.id}/secure_files", user), params: params + post api("/projects/#{project.id}/secure_files", maintainer), params: params end.to change {project.secure_files.count}.by(1) expect(json_response['permissions']).to eq('read_only') @@ -176,11 +223,11 @@ RSpec.describe API::Ci::SecureFiles do permissions: 'read_write' } - post api("/projects/#{project.id}/secure_files", user), params: post_params + post api("/projects/#{project.id}/secure_files", maintainer), params: post_params secure_file_id = json_response['id'] - get api("/projects/#{project.id}/secure_files/#{secure_file_id}/download", user) + get api("/projects/#{project.id}/secure_files/#{secure_file_id}/download", maintainer) expect(Base64.encode64(response.body)).to eq(Base64.encode64(fixture_file_upload('spec/fixtures/ci_secure_files/upload-keystore.jks').read)) end @@ -188,7 +235,9 @@ RSpec.describe API::Ci::SecureFiles do it 'returns an error when the file checksum fails to validate' do secure_file.update!(checksum: 'foo') - get api("/projects/#{project.id}/secure_files/#{secure_file.id}/download", user) + expect do + get api("/projects/#{project.id}/secure_files/#{secure_file.id}/download", maintainer) + end.not_to change { project.secure_files.count } expect(response.code).to eq("500") end @@ -198,7 +247,9 @@ RSpec.describe API::Ci::SecureFiles do name: 'upload-keystore.jks' } - post api("/projects/#{project.id}/secure_files", user), params: post_params + expect do + post api("/projects/#{project.id}/secure_files", maintainer), params: post_params + end.not_to change { project.secure_files.count } expect(response).to have_gitlab_http_status(:bad_request) expect(json_response['error']).to eq('file is missing') @@ -209,7 +260,9 @@ RSpec.describe API::Ci::SecureFiles do file: fixture_file_upload('spec/fixtures/ci_secure_files/upload-keystore.jks') } - post api("/projects/#{project.id}/secure_files", user), params: post_params + expect do + post api("/projects/#{project.id}/secure_files", maintainer), params: post_params + end.not_to change { project.secure_files.count } expect(response).to have_gitlab_http_status(:bad_request) expect(json_response['error']).to eq('name is missing') @@ -222,7 +275,9 @@ RSpec.describe API::Ci::SecureFiles do permissions: 'foo' } - post api("/projects/#{project.id}/secure_files", user), params: post_params + expect do + post api("/projects/#{project.id}/secure_files", maintainer), params: post_params + end.not_to change { project.secure_files.count } expect(response).to have_gitlab_http_status(:bad_request) expect(json_response['error']).to eq('permissions does not have a valid value') @@ -240,7 +295,9 @@ RSpec.describe API::Ci::SecureFiles do name: 'upload-keystore.jks' } - post api("/projects/#{project.id}/secure_files", user), params: post_params + expect do + post api("/projects/#{project.id}/secure_files", maintainer), params: post_params + end.not_to change { project.secure_files.count } expect(response).to have_gitlab_http_status(:bad_request) end @@ -255,23 +312,39 @@ RSpec.describe API::Ci::SecureFiles do name: 'upload-keystore.jks' } - post api("/projects/#{project.id}/secure_files", user), params: post_params + expect do + post api("/projects/#{project.id}/secure_files", maintainer), params: post_params + end.not_to change { project.secure_files.count } expect(response).to have_gitlab_http_status(:payload_too_large) end end - context 'authorized user with invalid permissions' do + context 'authenticated user with read permissions' do it 'does not create a secure file' do - post api("/projects/#{project.id}/secure_files", user2) + expect do + post api("/projects/#{project.id}/secure_files", developer) + end.not_to change { project.secure_files.count } expect(response).to have_gitlab_http_status(:forbidden) end end - context 'unauthorized user' do + context 'authenticated user with no permissions' do it 'does not create a secure file' do - post api("/projects/#{project.id}/secure_files") + expect do + post api("/projects/#{project.id}/secure_files", anonymous) + end.not_to change { project.secure_files.count } + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'unauthenticated user' do + it 'does not create a secure file' do + expect do + post api("/projects/#{project.id}/secure_files") + end.not_to change { project.secure_files.count } expect(response).to have_gitlab_http_status(:unauthorized) end @@ -279,33 +352,49 @@ RSpec.describe API::Ci::SecureFiles do end describe 'DELETE /projects/:id/secure_files/:secure_file_id' do - context 'authorized user with proper permissions' do + context 'authenticated user with admin permissions' do it 'deletes the secure file' do expect do - delete api("/projects/#{project.id}/secure_files/#{secure_file.id}", user) + delete api("/projects/#{project.id}/secure_files/#{secure_file.id}", maintainer) expect(response).to have_gitlab_http_status(:no_content) - end.to change {project.secure_files.count}.by(-1) + end.to change { project.secure_files.count } end it 'responds with 404 Not Found if requesting non-existing secure_file' do - delete api("/projects/#{project.id}/secure_files/99999", user) + expect do + delete api("/projects/#{project.id}/secure_files/#{non_existing_record_id}", maintainer) + end.not_to change { project.secure_files.count } expect(response).to have_gitlab_http_status(:not_found) end end - context 'authorized user with invalid permissions' do + context 'authenticated user with read permissions' do it 'does not delete the secure_file' do - delete api("/projects/#{project.id}/secure_files/#{secure_file.id}", user2) + expect do + delete api("/projects/#{project.id}/secure_files/#{secure_file.id}", developer) + end.not_to change { project.secure_files.count } expect(response).to have_gitlab_http_status(:forbidden) end end - context 'unauthorized user' do + context 'authenticated user with no permissions' do it 'does not delete the secure_file' do - delete api("/projects/#{project.id}/secure_files/#{secure_file.id}") + expect do + delete api("/projects/#{project.id}/secure_files/#{secure_file.id}", anonymous) + end.not_to change { project.secure_files.count } + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'unauthenticated user' do + it 'does not delete the secure_file' do + expect do + delete api("/projects/#{project.id}/secure_files/#{secure_file.id}") + end.not_to change { project.secure_files.count } expect(response).to have_gitlab_http_status(:unauthorized) end diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb index 156a4cf5ff3..67c2ec91540 100644 --- a/spec/requests/api/commits_spec.rb +++ b/spec/requests/api/commits_spec.rb @@ -127,6 +127,15 @@ RSpec.describe API::Commits do it_behaves_like 'project commits' end + context 'when repository does not exist' do + let(:project) { create(:project, creator: user, path: 'my.project') } + + it_behaves_like '404 response' do + let(:request) { get api(route, current_user) } + let(:message) { '404 Repository Not Found' } + end + end + context "path optional parameter" do it "returns project commits matching provided path parameter" do path = 'files/ruby/popen.rb' diff --git a/spec/requests/api/container_repositories_spec.rb b/spec/requests/api/container_repositories_spec.rb index 9809702467d..90f0243dbfc 100644 --- a/spec/requests/api/container_repositories_spec.rb +++ b/spec/requests/api/container_repositories_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' RSpec.describe API::ContainerRepositories do + include_context 'container registry client stubs' + let_it_be(:project) { create(:project, :private) } let_it_be(:reporter) { create(:user) } let_it_be(:guest) { create(:user) } @@ -103,6 +105,68 @@ RSpec.describe API::ContainerRepositories do expect(json_response['tags_count']).to eq(2) end end + + context 'with size param' do + let(:url) { "/registry/repositories/#{repository.id}?size=true" } + let(:on_com) { true } + let(:created_at) { ::ContainerRepository::MIGRATION_PHASE_1_STARTED_AT + 3.months } + + before do + allow(::Gitlab).to receive(:com?).and_return(on_com) + repository.update_column(:created_at, created_at) + end + + it 'returns a repository and its size' do + stub_container_registry_gitlab_api_support(supported: true) do |client| + stub_container_registry_gitlab_api_repository_details(client, path: repository.path, size_bytes: 12345) + end + + subject + + expect(json_response['size']).to eq(12345) + end + + context 'with a network error' do + it 'returns an error message' do + stub_container_registry_gitlab_api_network_error + + subject + + expect(response).to have_gitlab_http_status(:service_unavailable) + expect(json_response['message']).to include('We are having trouble connecting to the Container Registry') + end + end + + context 'with not supporting the gitlab api' do + it 'returns nil' do + stub_container_registry_gitlab_api_support(supported: false) + + subject + + expect(json_response['size']).to eq(nil) + end + end + + context 'not on .com' do + let(:on_com) { false } + + it 'returns nil' do + subject + + expect(json_response['size']).to eq(nil) + end + end + + context 'with an older container repository' do + let(:created_at) { ::ContainerRepository::MIGRATION_PHASE_1_STARTED_AT - 3.months } + + it 'returns nil' do + subject + + expect(json_response['size']).to eq(nil) + end + end + end end context 'with invalid repository id' do diff --git a/spec/requests/api/deploy_tokens_spec.rb b/spec/requests/api/deploy_tokens_spec.rb index 38380fa4460..b5f8da1f327 100644 --- a/spec/requests/api/deploy_tokens_spec.rb +++ b/spec/requests/api/deploy_tokens_spec.rb @@ -130,6 +130,55 @@ RSpec.describe API::DeployTokens do end end + describe 'GET /projects/:id/deploy_tokens/:token_id' do + subject do + get api("/projects/#{project.id}/deploy_tokens/#{deploy_token.id}", user) + response + end + + context 'when unauthenticated' do + let(:user) { nil } + + it { is_expected.to have_gitlab_http_status(:not_found) } + end + + context 'when authenticated as non-admin user' do + before do + project.add_developer(user) + end + + it { is_expected.to have_gitlab_http_status(:forbidden) } + end + + context 'when authenticated as maintainer' do + before do + project.add_maintainer(user) + end + + it { is_expected.to have_gitlab_http_status(:ok) } + + it 'returns specific deploy token for the project' do + subject + + expect(response).to match_response_schema('public_api/v4/deploy_token') + end + + context 'invalid request' do + it 'returns not found with invalid project id' do + get api("/projects/bad_id/deploy_tokens/#{deploy_token.id}", user) + + expect(response).to have_gitlab_http_status(:not_found) + end + + it 'returns not found with invalid token id' do + get api("/projects/#{project.id}/deploy_tokens/#{non_existing_record_id}", user) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end + end + describe 'GET /groups/:id/deploy_tokens' do subject do get api("/groups/#{group.id}/deploy_tokens", user) @@ -188,6 +237,55 @@ RSpec.describe API::DeployTokens do end end + describe 'GET /groups/:id/deploy_tokens/:token_id' do + subject do + get api("/groups/#{group.id}/deploy_tokens/#{group_deploy_token.id}", user) + response + end + + context 'when unauthenticated' do + let(:user) { nil } + + it { is_expected.to have_gitlab_http_status(:forbidden) } + end + + context 'when authenticated as non-admin user' do + before do + group.add_developer(user) + end + + it { is_expected.to have_gitlab_http_status(:forbidden) } + end + + context 'when authenticated as maintainer' do + before do + group.add_maintainer(user) + end + + it { is_expected.to have_gitlab_http_status(:ok) } + + it 'returns specific deploy token for the group' do + subject + + expect(response).to match_response_schema('public_api/v4/deploy_token') + end + + context 'invalid request' do + it 'returns not found with invalid group id' do + get api("/groups/bad_id/deploy_tokens/#{group_deploy_token.id}", user) + + expect(response).to have_gitlab_http_status(:not_found) + end + + it 'returns not found with invalid token id' do + get api("/groups/#{group.id}/deploy_tokens/#{non_existing_record_id}", user) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end + end + describe 'DELETE /projects/:id/deploy_tokens/:token_id' do subject do delete api("/projects/#{project.id}/deploy_tokens/#{deploy_token.id}", user) @@ -232,10 +330,10 @@ RSpec.describe API::DeployTokens do it 'returns bad_request with invalid token id' do expect(::Projects::DeployTokens::DestroyService).to receive(:new) - .with(project, user, token_id: 999) + .with(project, user, token_id: non_existing_record_id) .and_raise(ActiveRecord::RecordNotFound) - delete api("/projects/#{project.id}/deploy_tokens/999", user) + delete api("/projects/#{project.id}/deploy_tokens/#{non_existing_record_id}", user) expect(response).to have_gitlab_http_status(:not_found) end @@ -395,10 +493,10 @@ RSpec.describe API::DeployTokens do it 'returns not found with invalid deploy token id' do expect(::Groups::DeployTokens::DestroyService).to receive(:new) - .with(group, user, token_id: 999) + .with(group, user, token_id: non_existing_record_id) .and_raise(ActiveRecord::RecordNotFound) - delete api("/groups/#{group.id}/deploy_tokens/999", user) + delete api("/groups/#{group.id}/deploy_tokens/#{non_existing_record_id}", user) expect(response).to have_gitlab_http_status(:not_found) end diff --git a/spec/requests/api/error_tracking/collector_spec.rb b/spec/requests/api/error_tracking/collector_spec.rb index 573da862b57..fa0b238dcad 100644 --- a/spec/requests/api/error_tracking/collector_spec.rb +++ b/spec/requests/api/error_tracking/collector_spec.rb @@ -26,7 +26,6 @@ RSpec.describe API::ErrorTracking::Collector do RSpec.shared_examples 'successful request' do it 'writes to the database and returns OK', :aggregate_failures do expect { subject }.to change { ErrorTracking::ErrorEvent.count }.by(1) - expect(response).to have_gitlab_http_status(:ok) end end @@ -42,6 +41,14 @@ RSpec.describe API::ErrorTracking::Collector do it_behaves_like 'successful request' + context 'intergrated error tracking feature flag is disabled' do + before do + stub_feature_flags(integrated_error_tracking: false) + end + + it_behaves_like 'not found' + end + context 'error tracking feature is disabled' do before do setting.update!(enabled: false) @@ -171,6 +178,12 @@ RSpec.describe API::ErrorTracking::Collector do it_behaves_like 'successful request' end + context 'when JSON key transaction is empty string' do + let_it_be(:raw_event) { fixture_file('error_tracking/php_empty_transaction.json') } + + it_behaves_like 'successful request' + end + context 'sentry_key as param and empty headers' do let(:url) { "/error_tracking/collector/api/#{project.id}/store?sentry_key=#{sentry_key}" } let(:headers) { {} } diff --git a/spec/requests/api/error_tracking/project_settings_spec.rb b/spec/requests/api/error_tracking/project_settings_spec.rb index 161e4f01ea5..c0c0680ef31 100644 --- a/spec/requests/api/error_tracking/project_settings_spec.rb +++ b/spec/requests/api/error_tracking/project_settings_spec.rb @@ -23,6 +23,21 @@ RSpec.describe API::ErrorTracking::ProjectSettings do end end + shared_examples 'returns project settings with false for integrated' do + specify do + make_request + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to eq( + 'active' => setting.reload.enabled, + 'project_name' => setting.project_name, + 'sentry_external_url' => setting.sentry_external_url, + 'api_url' => setting.api_url, + 'integrated' => false + ) + end + end + shared_examples 'returns 404' do it 'returns no project settings' do make_request @@ -46,7 +61,17 @@ RSpec.describe API::ErrorTracking::ProjectSettings do end context 'patch settings' do - it_behaves_like 'returns project settings' + context 'integrated_error_tracking feature enabled' do + it_behaves_like 'returns project settings' + end + + context 'integrated_error_tracking feature disabled' do + before do + stub_feature_flags(integrated_error_tracking: false) + end + + it_behaves_like 'returns project settings with false for integrated' + end it 'updates enabled flag' do expect(setting).to be_enabled @@ -84,13 +109,19 @@ RSpec.describe API::ErrorTracking::ProjectSettings do context 'with integrated param' do let(:params) { { active: true, integrated: true } } - it 'updates the integrated flag' do - expect(setting.integrated).to be_falsey + context 'integrated_error_tracking feature enabled' do + before do + stub_feature_flags(integrated_error_tracking: true) + end - make_request + it 'updates the integrated flag' do + expect(setting.integrated).to be_falsey + + make_request - expect(json_response).to include('integrated' => true) - expect(setting.reload.integrated).to be_truthy + expect(json_response).to include('integrated' => true) + expect(setting.reload.integrated).to be_truthy + end end end end @@ -170,7 +201,21 @@ RSpec.describe API::ErrorTracking::ProjectSettings do end context 'get settings' do - it_behaves_like 'returns project settings' + context 'integrated_error_tracking feature enabled' do + before do + stub_feature_flags(integrated_error_tracking: true) + end + + it_behaves_like 'returns project settings' + end + + context 'integrated_error_tracking feature disabled' do + before do + stub_feature_flags(integrated_error_tracking: false) + end + + it_behaves_like 'returns project settings with false for integrated' + end end end diff --git a/spec/requests/api/generic_packages_spec.rb b/spec/requests/api/generic_packages_spec.rb index e1d8a9f0229..3a5c6103781 100644 --- a/spec/requests/api/generic_packages_spec.rb +++ b/spec/requests/api/generic_packages_spec.rb @@ -170,17 +170,6 @@ RSpec.describe API::GenericPackages do end end - context 'generic_packages feature flag is disabled' do - it 'responds with 404 Not Found' do - stub_feature_flags(generic_packages: false) - project.add_developer(user) - - authorize_upload_file(workhorse_headers.merge(personal_access_token_header)) - - expect(response).to have_gitlab_http_status(:not_found) - end - end - def authorize_upload_file(request_headers, package_name: 'mypackage', file_name: 'myfile.tar.gz') url = "/projects/#{project.id}/packages/generic/#{package_name}/0.0.1/#{file_name}/authorize" diff --git a/spec/requests/api/graphql/ci/pipelines_spec.rb b/spec/requests/api/graphql/ci/pipelines_spec.rb index 5ae68be46a2..741af676b6d 100644 --- a/spec/requests/api/graphql/ci/pipelines_spec.rb +++ b/spec/requests/api/graphql/ci/pipelines_spec.rb @@ -528,4 +528,37 @@ RSpec.describe 'Query.project(fullPath).pipelines' do end.not_to exceed_query_limit(control_count) end end + + describe 'filtering' do + let(:query) do + %( + query { + project(fullPath: "#{project.full_path}") { + pipelines(updatedAfter: "#{updated_after_arg}", updatedBefore: "#{updated_before_arg}") { + nodes { + id + }}}} + ) + end + + context 'when filtered by updated_at' do + let_it_be(:oldish_pipeline) { create(:ci_empty_pipeline, project: project, updated_at: 3.days.ago) } + let_it_be(:older_pipeline) { create(:ci_empty_pipeline, project: project, updated_at: 10.days.ago) } + + let(:updated_after_arg) { 5.days.ago } + let(:updated_before_arg) { 1.day.ago } + + before do + post_graphql(query, current_user: user) + end + + it_behaves_like 'a working graphql query' + + it 'accepts filter params' do + pipeline_ids = graphql_data.dig('project', 'pipelines', 'nodes').map { |pipeline| pipeline.fetch('id') } + + expect(pipeline_ids).to match_array(oldish_pipeline.to_global_id.to_s) + end + end + end end diff --git a/spec/requests/api/graphql/ci/runner_spec.rb b/spec/requests/api/graphql/ci/runner_spec.rb index fa16b9e1ddd..b99a3d14fb9 100644 --- a/spec/requests/api/graphql/ci/runner_spec.rb +++ b/spec/requests/api/graphql/ci/runner_spec.rb @@ -196,39 +196,6 @@ RSpec.describe 'Query.runner(id)' do it_behaves_like 'runner details fetch', :inactive_instance_runner end - describe 'for runner inside group request' do - let(:query) do - %( - query { - group(fullPath: "#{group.full_path}") { - runners { - edges { - webUrl - node { - id - } - } - } - } - } - ) - end - - it 'retrieves webUrl field with expected value' do - post_graphql(query, current_user: user) - - runner_data = graphql_data_at(:group, :runners, :edges) - expect(runner_data).to match_array [ - a_hash_including( - 'webUrl' => "http://localhost/groups/#{group.full_path}/-/runners/#{active_group_runner.id}", - 'node' => { - 'id' => active_group_runner.to_global_id.to_s - } - ) - ] - end - end - describe 'for group runner request' do let(:query) do %( diff --git a/spec/requests/api/graphql/ci/runner_web_url_edge_spec.rb b/spec/requests/api/graphql/ci/runner_web_url_edge_spec.rb new file mode 100644 index 00000000000..767e958ea82 --- /dev/null +++ b/spec/requests/api/graphql/ci/runner_web_url_edge_spec.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true +require 'spec_helper' + +RSpec.describe 'RunnerWebUrlEdge' do + include GraphqlHelpers + + describe 'inside a Query.group' do + let_it_be(:group) { create(:group) } + let_it_be(:group_runner) { create(:ci_runner, :group, groups: [group]) } + + let(:edges_graphql_data) { graphql_data.dig('group', 'runners', 'edges') } + + let(:query) do + <<~GQL + query($path: ID!) { + group(fullPath: $path) { + runners { + edges { + editUrl + webUrl + } + } + } + } + GQL + end + + before do + post_graphql(query, current_user: user, variables: { path: group.full_path }) + end + + context 'with an authorized user' do + let(:user) { create_default(:user, :admin) } + + it_behaves_like 'a working graphql query' + + it 'returns correct URLs' do + expect(edges_graphql_data).to match_array [ + { + 'editUrl' => Gitlab::Routing.url_helpers.edit_group_runner_url(group, group_runner), + 'webUrl' => Gitlab::Routing.url_helpers.group_runner_url(group, group_runner) + } + ] + end + end + + context 'with an unauthorized user' do + let(:user) { create(:user) } + + it_behaves_like 'a working graphql query' + + it 'returns no edges' do + expect(edges_graphql_data).to be_empty + end + end + end +end diff --git a/spec/requests/api/graphql/container_repository/container_repository_details_spec.rb b/spec/requests/api/graphql/container_repository/container_repository_details_spec.rb index 35a70a180a2..922a9ab277e 100644 --- a/spec/requests/api/graphql/container_repository/container_repository_details_spec.rb +++ b/spec/requests/api/graphql/container_repository/container_repository_details_spec.rb @@ -3,17 +3,19 @@ require 'spec_helper' RSpec.describe 'container repository details' do include_context 'container registry tags' + include_context 'container registry client stubs' + using RSpec::Parameterized::TableSyntax include GraphqlHelpers let_it_be_with_reload(:project) { create(:project) } - let_it_be(:container_repository) { create(:container_repository, project: project) } + let_it_be_with_reload(:container_repository) { create(:container_repository, project: project) } let(:query) do graphql_query_for( 'containerRepository', { id: container_repository_global_id }, - all_graphql_fields_for('ContainerRepositoryDetails', excluded: ['pipeline']) + all_graphql_fields_for('ContainerRepositoryDetails', excluded: %w[pipeline size]) ) end @@ -220,6 +222,80 @@ RSpec.describe 'container repository details' do end end + context 'size field' do + let(:size_response) { container_repository_details_response.dig('size') } + let(:on_com) { true } + let(:created_at) { ::ContainerRepository::MIGRATION_PHASE_1_STARTED_AT + 3.months } + let(:variables) do + { id: container_repository_global_id } + end + + let(:query) do + <<~GQL + query($id: ID!) { + containerRepository(id: $id) { + size + } + } + GQL + end + + before do + allow(::Gitlab).to receive(:com?).and_return(on_com) + container_repository.update_column(:created_at, created_at) + end + + it 'returns the size' do + stub_container_registry_gitlab_api_support(supported: true) do |client| + stub_container_registry_gitlab_api_repository_details(client, path: container_repository.path, size_bytes: 12345) + end + + subject + + expect(size_response).to eq(12345) + end + + context 'with a network error' do + it 'returns an error' do + stub_container_registry_gitlab_api_network_error + + subject + + expect_graphql_errors_to_include("Can't connect to the Container Registry. If this error persists, please review the troubleshooting documentation.") + end + end + + context 'with not supporting the gitlab api' do + it 'returns nil' do + stub_container_registry_gitlab_api_support(supported: false) + + subject + + expect(size_response).to eq(nil) + end + end + + context 'not on .com' do + let(:on_com) { false } + + it 'returns nil' do + subject + + expect(size_response).to eq(nil) + end + end + + context 'with an older container repository' do + let(:created_at) { ::ContainerRepository::MIGRATION_PHASE_1_STARTED_AT - 3.months } + + it 'returns nil' do + subject + + expect(size_response).to eq(nil) + end + end + end + context 'with tags with a manifest containing nil fields' do let(:tags_response) { container_repository_details_response.dig('tags', 'nodes') } let(:errors) { container_repository_details_response.dig('errors') } diff --git a/spec/requests/api/graphql/group/group_members_spec.rb b/spec/requests/api/graphql/group/group_members_spec.rb index 06afb5b9a49..78852622835 100644 --- a/spec/requests/api/graphql/group/group_members_spec.rb +++ b/spec/requests/api/graphql/group/group_members_spec.rb @@ -53,6 +53,30 @@ RSpec.describe 'getting group members information' do end end + context "when requesting member's notification email" do + context 'when current_user is admin' do + let_it_be(:admin_user) { create(:user, :admin) } + + it 'returns notification email' do + fetch_members_notification_email(current_user: admin_user) + notification_emails = graphql_data_at(:group, :group_members, :edges, :node, :notification_email) + + expect(notification_emails).to all be_present + expect(graphql_errors).to be_nil + end + end + + context 'when current_user is not admin' do + it 'returns an error' do + fetch_members_notification_email + + expect(graphql_errors.first) + .to include('path' => ['group', 'groupMembers', 'edges', 0, 'node', 'notificationEmail'], + 'message' => a_string_including("you don't have permission to perform this action")) + end + end + end + 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) } @@ -117,6 +141,10 @@ RSpec.describe 'getting group members information' do post_graphql(members_query(group.full_path, args), current_user: current_user) end + def fetch_members_notification_email(group: parent_group, current_user: user) + post_graphql(member_notification_email_query(group.full_path), current_user: current_user) + end + def members_query(group_path, args = {}) members_node = <<~NODE edges { @@ -134,6 +162,24 @@ RSpec.describe 'getting group members information' do ) end + def member_notification_email_query(group_path) + members_node = <<~NODE + edges { + node { + user { + id + } + notificationEmail + } + } + NODE + + graphql_query_for("group", + { full_path: group_path }, + [query_graphql_field("groupMembers", {}, members_node)] + ) + end + def expect_array_response(*items) expect(response).to have_gitlab_http_status(:success) member_gids = graphql_data_at(:group, :group_members, :edges, :node, :user, :id) diff --git a/spec/requests/api/graphql/group/issues_spec.rb b/spec/requests/api/graphql/group/issues_spec.rb index 332bf242e9c..26338f46611 100644 --- a/spec/requests/api/graphql/group/issues_spec.rb +++ b/spec/requests/api/graphql/group/issues_spec.rb @@ -44,6 +44,31 @@ RSpec.describe 'getting an issue list for a group' do end end + context 'when there are archived projects' do + let_it_be(:archived_project) { create(:project, :archived, group: group1) } + let_it_be(:archived_issue) { create(:issue, project: archived_project) } + + before_all do + group1.add_developer(current_user) + end + + it 'excludes issues from archived projects by default' do + post_graphql(query, current_user: current_user) + + expect(issues_ids).to contain_exactly(issue1_gid, issue2_gid) + end + + context 'when include_archived is true' do + let(:issue_filter_params) { { include_archived: true } } + + it 'includes issues from archived projects' do + post_graphql(query, current_user: current_user) + + expect(issues_ids).to contain_exactly(issue1_gid, issue2_gid, archived_issue.to_global_id.to_s) + end + end + end + context 'when there is a confidential issue' do let_it_be(:confidential_issue1) { create(:issue, :confidential, project: project1) } let_it_be(:confidential_issue2) { create(:issue, :confidential, project: project2) } diff --git a/spec/requests/api/graphql/group/merge_requests_spec.rb b/spec/requests/api/graphql/group/merge_requests_spec.rb index e9a5e558b1d..c0faff11c8d 100644 --- a/spec/requests/api/graphql/group/merge_requests_spec.rb +++ b/spec/requests/api/graphql/group/merge_requests_spec.rb @@ -16,6 +16,9 @@ RSpec.describe 'Query.group.mergeRequests' do let_it_be(:project_x) { create(:project, :repository) } let_it_be(:user) { create(:user, developer_projects: [project_x]) } + let_it_be(:archived_project) { create(:project, :archived, :repository, group: group) } + let_it_be(:archived_mr) { create(:merge_request, source_project: archived_project) } + let_it_be(:mr_attrs) do { target_branch: 'master' } end @@ -119,4 +122,22 @@ RSpec.describe 'Query.group.mergeRequests' do expect(mrs_data).to match_array(expected_mrs(mrs_a + mrs_b + mrs_c)) end end + + describe 'passing include_archived: true' do + let(:query) do + <<~GQL + query($path: ID!) { + group(fullPath: $path) { + mergeRequests(includeArchived: true) { nodes { id } } + } + } + GQL + end + + it 'can find all merge requests in the group, including from archived projects' do + post_graphql(query, current_user: user, variables: { path: group.full_path }) + + expect(mrs_data).to match_array(expected_mrs(mrs_a + mrs_b + [archived_mr])) + end + end end diff --git a/spec/requests/api/graphql/group/work_item_types_spec.rb b/spec/requests/api/graphql/group/work_item_types_spec.rb index 0667e09d1e9..a33e3ae5427 100644 --- a/spec/requests/api/graphql/group/work_item_types_spec.rb +++ b/spec/requests/api/graphql/group/work_item_types_spec.rb @@ -64,8 +64,8 @@ RSpec.describe 'getting a list of work item types for a group' do 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'")) + it 'returns null' do + expect(graphql_data.dig('group', 'workItemTypes')).to be_nil 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 79d687a2bdb..02b79dac489 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 @@ -9,12 +9,10 @@ RSpec.describe 'Setting issues crm contacts' do 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_it_be(:contacts) { create_list(:contact, 4, group: group) } let(:issue) { create(:issue, project: project) } let(:operation_mode) { Types::MutationOperationModeEnum.default_mode } - let(:contacts) { subgroup_contacts } let(:initial_contacts) { contacts[0..1] } let(:mutation_contacts) { contacts[1..2] } let(:contact_ids) { contact_global_ids(mutation_contacts) } @@ -116,15 +114,7 @@ RSpec.describe 'Setting issues crm contacts' do end end - context 'with issue group contacts' do - let(:contacts) { subgroup_contacts } - - it_behaves_like 'successful mutation' - end - - context 'with issue ancestor group contacts' do - it_behaves_like 'successful mutation' - end + it_behaves_like 'successful mutation' context 'when the contact does not exist' do let(:contact_ids) { ["gid://gitlab/CustomerRelations::Contact/#{non_existing_record_id}"] } diff --git a/spec/requests/api/graphql/mutations/notes/create/note_spec.rb b/spec/requests/api/graphql/mutations/notes/create/note_spec.rb index 87c752393ea..2bc671e4ca5 100644 --- a/spec/requests/api/graphql/mutations/notes/create/note_spec.rb +++ b/spec/requests/api/graphql/mutations/notes/create/note_spec.rb @@ -8,13 +8,16 @@ RSpec.describe 'Adding a Note' do let_it_be(:current_user) { create(:user) } let(:noteable) { create(:merge_request, source_project: project, target_project: project) } - let(:project) { create(:project) } + let(:project) { create(:project, :repository) } let(:discussion) { nil } + let(:head_sha) { nil } + let(:body) { 'Body text' } let(:mutation) do variables = { noteable_id: GitlabSchema.id_from_object(noteable).to_s, discussion_id: (GitlabSchema.id_from_object(discussion).to_s if discussion), - body: 'Body text', + merge_request_diff_head_sha: head_sha.presence, + body: body, confidential: true } @@ -54,7 +57,7 @@ RSpec.describe 'Adding a Note' do let(:discussion) { create(:discussion_note).to_discussion } it_behaves_like 'a mutation that returns top-level errors', - errors: ["The discussion does not exist or you don't have permission to perform this action"] + errors: ["The discussion does not exist or you don't have permission to perform this action"] end context 'when the user has permission to create notes on the discussion' do @@ -75,5 +78,29 @@ RSpec.describe 'Adding a Note' do end end end + + context 'when body only contains quick actions' do + let(:head_sha) { noteable.diff_head_sha } + let(:body) { '/merge' } + + before do + project.add_developer(current_user) + end + + # NOTE: Known issue https://gitlab.com/gitlab-org/gitlab/-/issues/346557 + it 'returns a nil note and info about the command in errors' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(mutation_response).to include( + 'errors' => [/Merged this merge request/], + 'note' => nil + ) + end + + it 'starts the merge process' do + expect { post_graphql_mutation(mutation, current_user: current_user) } + .to change { noteable.reload.merge_jid.present? }.from(false).to(true) + end + end end end diff --git a/spec/requests/api/graphql/mutations/work_items/create_from_task_spec.rb b/spec/requests/api/graphql/mutations/work_items/create_from_task_spec.rb new file mode 100644 index 00000000000..8d33f8e1806 --- /dev/null +++ b/spec/requests/api/graphql/mutations/work_items/create_from_task_spec.rb @@ -0,0 +1,87 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe "Create a work item from a task in a work item's description" do + include GraphqlHelpers + + let_it_be(:project) { create(:project) } + let_it_be(:developer) { create(:user).tap { |user| project.add_developer(user) } } + let_it_be(:work_item, refind: true) { create(:work_item, project: project, description: '- [ ] A task in a list', lock_version: 3) } + + let(:lock_version) { work_item.lock_version } + let(:input) do + { + 'id' => work_item.to_global_id.to_s, + 'workItemData' => { + 'title' => 'A task in a list', + 'workItemTypeId' => WorkItems::Type.default_by_type(:task).to_global_id.to_s, + 'lineNumberStart' => 1, + 'lineNumberEnd' => 1, + 'lockVersion' => lock_version + } + } + end + + let(:mutation) { graphql_mutation(:workItemCreateFromTask, input) } + let(:mutation_response) { graphql_mutation_response(:work_item_create_from_task) } + + context 'the user is not allowed to update 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 + work_item.reload + + expect(response).to have_gitlab_http_status(:success) + expect(work_item.description).to eq("- [ ] #{created_work_item.to_reference}+") + expect(created_work_item.issue_type).to eq('task') + expect(created_work_item.work_item_type.base_type).to eq('task') + expect(mutation_response['workItem']).to include('id' => work_item.to_global_id.to_s) + expect(mutation_response['newWorkItem']).to include('id' => created_work_item.to_global_id.to_s) + end + + context 'when creating a work item fails' do + let(:lock_version) { 2 } + + it 'makes no changes to the DB and returns an error message' do + expect do + post_graphql_mutation(mutation, current_user: current_user) + work_item.reload + end.to not_change(WorkItem, :count).and( + not_change(work_item, :description) + ) + + expect(mutation_response['errors']).to contain_exactly('Stale work item. Check lock version') + end + end + + it_behaves_like 'has spam protection' do + let(:mutation_class) { ::Mutations::WorkItems::CreateFromTask } + end + + context 'when the work_items feature flag is disabled' do + before do + stub_feature_flags(work_items: false) + end + + it 'does nothing and returns and error' do + expect do + post_graphql_mutation(mutation, current_user: current_user) + end.to not_change(WorkItem, :count) + + expect(mutation_response['errors']).to contain_exactly('`work_items` feature flag disabled for this project') + end + end + end +end diff --git a/spec/requests/api/graphql/namespace_query_spec.rb b/spec/requests/api/graphql/namespace_query_spec.rb index f7ee2bcb55d..e17469901c6 100644 --- a/spec/requests/api/graphql/namespace_query_spec.rb +++ b/spec/requests/api/graphql/namespace_query_spec.rb @@ -31,7 +31,8 @@ RSpec.describe 'Query' do it 'fetches the expected data' do expect(query_result).to include( 'fullPath' => target_namespace.full_path, - 'name' => target_namespace.name + 'name' => target_namespace.name, + 'crossProjectPipelineAvailable' => target_namespace.licensed_feature_available?(:cross_project_pipeline) ) end end diff --git a/spec/requests/api/graphql/project/jira_service_spec.rb b/spec/requests/api/graphql/project/jira_service_spec.rb index 64e9e04ae44..d6abe94b873 100644 --- a/spec/requests/api/graphql/project/jira_service_spec.rb +++ b/spec/requests/api/graphql/project/jira_service_spec.rb @@ -16,6 +16,7 @@ RSpec.describe 'query Jira service' do services(active: true, type: JIRA_SERVICE) { nodes { type + serviceType } } } @@ -23,7 +24,7 @@ RSpec.describe 'query Jira service' do ) end - let(:services) { graphql_data.dig('project', 'services', 'nodes')} + let(:services) { graphql_data.dig('project', 'services', 'nodes') } it_behaves_like 'unauthorized users cannot read services' @@ -35,10 +36,8 @@ RSpec.describe 'query Jira service' do it_behaves_like 'a working graphql query' - it 'retuns list of jira imports' do - service = services.first - - expect(service['type']).to eq('JiraService') + it 'returns list of jira integrations' do + expect(services).to contain_exactly({ 'type' => 'JiraService', 'serviceType' => 'JIRA_SERVICE' }) end end end diff --git a/spec/requests/api/graphql/project/merge_request_spec.rb b/spec/requests/api/graphql/project/merge_request_spec.rb index 353bf0356f6..cefe88aafc8 100644 --- a/spec/requests/api/graphql/project/merge_request_spec.rb +++ b/spec/requests/api/graphql/project/merge_request_spec.rb @@ -76,6 +76,24 @@ RSpec.describe 'getting merge request information nested in a project' do end end + context 'when the merge_request has committers' do + let(:mr_fields) do + <<~SELECT + committers { nodes { id username } } + SELECT + end + + it 'includes committers' do + expected = merge_request.committers.map do |r| + a_hash_including('id' => global_id_of(r), 'username' => r.username) + end + + post_graphql(query, current_user: current_user) + + expect(graphql_data_at(:project, :merge_request, :committers, :nodes)).to match_array(expected) + end + end + describe 'diffStats' do let(:mr_fields) do <<~FIELDS diff --git a/spec/requests/api/graphql/project/work_item_types_spec.rb b/spec/requests/api/graphql/project/work_item_types_spec.rb index 2caaedda2a1..157961c3f66 100644 --- a/spec/requests/api/graphql/project/work_item_types_spec.rb +++ b/spec/requests/api/graphql/project/work_item_types_spec.rb @@ -64,8 +64,8 @@ RSpec.describe 'getting a list of work item types for a project' do 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'")) + it 'returns null' do + expect(graphql_data.dig('project', 'workItemTypes')).to be_nil end end end diff --git a/spec/requests/api/graphql/query_spec.rb b/spec/requests/api/graphql/query_spec.rb index ecc7fffaef7..d650acc8354 100644 --- a/spec/requests/api/graphql/query_spec.rb +++ b/spec/requests/api/graphql/query_spec.rb @@ -11,6 +11,30 @@ RSpec.describe 'Query' do let(:current_user) { developer } + describe 'gitpodEnabled field' do + let(:gitpod_enabled) { true } + let(:gitpod_enabled_query) do + <<~GRAPHQL + { gitpodEnabled } + GRAPHQL + end + + before do + allow(Gitlab::CurrentSettings.current_application_settings).to receive(:gitpod_enabled).and_return(gitpod_enabled) + post_graphql(gitpod_enabled_query) + end + + context 'When Gitpod is enabled for the application' do + it { expect(graphql_data).to include('gitpodEnabled' => true) } + end + + context 'When Gitpod is disabled for the application' do + let(:gitpod_enabled) { false } + + it { expect(graphql_data).to include('gitpodEnabled' => false) } + end + end + describe '.designManagement' do include DesignManagementTestHelpers diff --git a/spec/requests/api/graphql/work_item_spec.rb b/spec/requests/api/graphql/work_item_spec.rb new file mode 100644 index 00000000000..bc5a8b3e006 --- /dev/null +++ b/spec/requests/api/graphql/work_item_spec.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Query.work_item(id)' do + include GraphqlHelpers + + let_it_be(:developer) { create(:user) } + let_it_be(:project) { create(:project, :private).tap { |project| project.add_developer(developer) } } + let_it_be(:work_item) { create(:work_item, project: project) } + + let(:current_user) { developer } + let(:work_item_data) { graphql_data['workItem'] } + let(:work_item_fields) { all_graphql_fields_for('WorkItem') } + let(:global_id) { work_item.to_gid.to_s } + + let(:query) do + graphql_query_for('workItem', { 'id' => global_id }, work_item_fields) + end + + context 'when the user can read the work item' do + before do + post_graphql(query, current_user: current_user) + end + + it_behaves_like 'a working graphql query' + + it 'returns all fields' do + expect(work_item_data).to include( + 'description' => work_item.description, + 'id' => work_item.to_gid.to_s, + 'iid' => work_item.iid.to_s, + 'lockVersion' => work_item.lock_version, + 'state' => "OPEN", + 'title' => work_item.title, + 'workItemType' => hash_including('id' => work_item.work_item_type.to_gid.to_s) + ) + end + + context 'when an Issue Global ID is provided' do + let(:global_id) { Issue.find(work_item.id).to_gid.to_s } + + it 'allows an Issue GID as input' do + expect(work_item_data).to include('id' => work_item.to_gid.to_s) + end + end + end + + context 'when the user can not read the work item' do + let(:current_user) { create(:user) } + + before do + post_graphql(query) + end + + it 'returns an access error' do + expect(work_item_data).to be_nil + expect(graphql_errors).to contain_exactly( + hash_including('message' => ::Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR) + ) + end + end + + context 'when the work_items feature flag is disabled' do + before do + stub_feature_flags(work_items: false) + end + + it 'returns nil' do + post_graphql(query) + + expect(work_item_data).to be_nil + end + end +end diff --git a/spec/requests/api/group_clusters_spec.rb b/spec/requests/api/group_clusters_spec.rb index c48b5007f91..8e127bf0710 100644 --- a/spec/requests/api/group_clusters_spec.rb +++ b/spec/requests/api/group_clusters_spec.rb @@ -22,6 +22,10 @@ RSpec.describe API::GroupClusters do groups: [group]) end + include_examples ':certificate_based_clusters feature flag API responses' do + let(:subject) { get api("/groups/#{group.id}/clusters", current_user) } + end + context 'non-authorized user' do it 'responds with 403' do get api("/groups/#{group.id}/clusters", unauthorized_user) @@ -66,6 +70,10 @@ RSpec.describe API::GroupClusters do groups: [group]) end + include_examples ':certificate_based_clusters feature flag API responses' do + let(:subject) { get api("/groups/#{group.id}/clusters/#{cluster_id}", current_user) } + end + context 'non-authorized user' do it 'responds with 403' do get api("/groups/#{group.id}/clusters/#{cluster_id}", unauthorized_user) @@ -181,6 +189,10 @@ RSpec.describe API::GroupClusters do } end + include_examples ':certificate_based_clusters feature flag API responses' do + let(:subject) { post api("/groups/#{group.id}/clusters/user", current_user), params: cluster_params } + end + context 'non-authorized user' do it 'responds with 403' do post api("/groups/#{group.id}/clusters/user", unauthorized_user), params: cluster_params @@ -362,6 +374,10 @@ RSpec.describe API::GroupClusters do groups: [group], domain: 'old-domain.com') end + include_examples ':certificate_based_clusters feature flag API responses' do + let(:subject) { put api("/groups/#{group.id}/clusters/#{cluster.id}", current_user), params: update_params } + end + context 'non-authorized user' do it 'responds with 403' do put api("/groups/#{group.id}/clusters/#{cluster.id}", unauthorized_user), params: update_params @@ -503,6 +519,10 @@ RSpec.describe API::GroupClusters do groups: [group]) end + include_examples ':certificate_based_clusters feature flag API responses' do + let(:subject) { delete api("/groups/#{group.id}/clusters/#{cluster.id}", current_user), params: cluster_params } + end + context 'non-authorized user' do it 'responds with 403' do delete api("/groups/#{group.id}/clusters/#{cluster.id}", unauthorized_user), params: cluster_params diff --git a/spec/requests/api/group_labels_spec.rb b/spec/requests/api/group_labels_spec.rb index 11738e3cba8..34533da53dd 100644 --- a/spec/requests/api/group_labels_spec.rb +++ b/spec/requests/api/group_labels_spec.rb @@ -140,7 +140,7 @@ RSpec.describe API::GroupLabels do expect(response).to have_gitlab_http_status(:ok) expect(json_response['name']).to eq(group_label1.name) - expect(json_response['color']).to eq(group_label1.color) + expect(json_response['color']).to be_color(group_label1.color) expect(json_response['description']).to eq(group_label1.description) end end @@ -156,7 +156,7 @@ RSpec.describe API::GroupLabels do expect(response).to have_gitlab_http_status(:created) expect(json_response['name']).to eq(valid_new_label_title) - expect(json_response['color']).to eq('#FFAABB') + expect(json_response['color']).to be_color('#FFAABB') expect(json_response['description']).to eq('test') end @@ -169,7 +169,7 @@ RSpec.describe API::GroupLabels do expect(response).to have_gitlab_http_status(:created) expect(json_response['name']).to eq(valid_new_label_title) - expect(json_response['color']).to eq('#FFAABB') + expect(json_response['color']).to be_color('#FFAABB') expect(json_response['description']).to be_nil end @@ -276,7 +276,7 @@ RSpec.describe API::GroupLabels do expect(response).to have_gitlab_http_status(:ok) expect(json_response['name']).to eq(valid_new_label_title) - expect(json_response['color']).to eq('#FFFFFF') + expect(json_response['color']).to be_color('#FFFFFF') expect(json_response['description']).to eq('test') end @@ -332,7 +332,7 @@ RSpec.describe API::GroupLabels do expect(response).to have_gitlab_http_status(:ok) expect(json_response['name']).to eq(valid_new_label_title) - expect(json_response['color']).to eq('#FFFFFF') + expect(json_response['color']).to be_color('#FFFFFF') expect(json_response['description']).to eq('test') end diff --git a/spec/requests/api/integrations_spec.rb b/spec/requests/api/integrations_spec.rb index 033c80a5696..220c58afbe9 100644 --- a/spec/requests/api/integrations_spec.rb +++ b/spec/requests/api/integrations_spec.rb @@ -10,6 +10,14 @@ RSpec.describe API::Integrations do create(:project, creator_id: user.id, namespace: user.namespace) end + # The API supports all integrations except the GitLab Slack Application + # integration; this integration must be installed via the UI. + def self.integration_names + names = Integration.available_integration_names + names.delete(Integrations::GitlabSlackApplication.to_param) if Gitlab.ee? + names + end + %w[integrations services].each do |endpoint| describe "GET /projects/:id/#{endpoint}" do it 'returns authentication error when unauthenticated' do @@ -43,7 +51,7 @@ RSpec.describe API::Integrations do end end - Integration.available_integration_names.each do |integration| + integration_names.each do |integration| describe "PUT /projects/:id/#{endpoint}/#{integration.dasherize}" do include_context integration diff --git a/spec/requests/api/internal/kubernetes_spec.rb b/spec/requests/api/internal/kubernetes_spec.rb index 59d185fe6c8..0e566dd8c0e 100644 --- a/spec/requests/api/internal/kubernetes_spec.rb +++ b/spec/requests/api/internal/kubernetes_spec.rb @@ -169,6 +169,7 @@ RSpec.describe API::Internal::Kubernetes do 'features' => {} ), 'gitaly_repository' => a_hash_including( + 'default_branch' => project.default_branch_or_main, 'storage_name' => project.repository_storage, 'relative_path' => project.disk_path + '.git', 'gl_repository' => "project-#{project.id}", diff --git a/spec/requests/api/internal/mail_room_spec.rb b/spec/requests/api/internal/mail_room_spec.rb index f3ca3708c0c..67ea617f90d 100644 --- a/spec/requests/api/internal/mail_room_spec.rb +++ b/spec/requests/api/internal/mail_room_spec.rb @@ -28,7 +28,7 @@ RSpec.describe API::Internal::MailRoom do } end - let(:auth_payload) { { 'iss' => Gitlab::MailRoom::Authenticator::INTERNAL_API_REQUEST_JWT_ISSUER, 'iat' => (Time.now - 10.seconds).to_i } } + let(:auth_payload) { { 'iss' => Gitlab::MailRoom::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' } @@ -51,7 +51,7 @@ RSpec.describe API::Internal::MailRoom 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 } + { Gitlab::MailRoom::INTERNAL_API_REQUEST_HEADER => jwt_token } end it 'schedules a EmailReceiverWorker job with raw email content' do @@ -71,7 +71,7 @@ RSpec.describe API::Internal::MailRoom do 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 } + { Gitlab::MailRoom::INTERNAL_API_REQUEST_HEADER => jwt_token } end it 'schedules a ServiceDeskEmailReceiverWorker job with raw email content' do @@ -91,7 +91,7 @@ RSpec.describe API::Internal::MailRoom do 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 } + { Gitlab::MailRoom::INTERNAL_API_REQUEST_HEADER => jwt_token } end before do @@ -134,7 +134,7 @@ RSpec.describe API::Internal::MailRoom do 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 } + { Gitlab::MailRoom::INTERNAL_API_REQUEST_HEADER => jwt_token } end it 'responds with 401 Unauthorized' do @@ -147,7 +147,7 @@ RSpec.describe API::Internal::MailRoom do 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 } + { Gitlab::MailRoom::INTERNAL_API_REQUEST_HEADER => jwt_token } end it 'responds with 401 Unauthorized' do @@ -160,7 +160,7 @@ RSpec.describe API::Internal::MailRoom do 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 } + { Gitlab::MailRoom::INTERNAL_API_REQUEST_HEADER => jwt_token } end it 'responds with 401 Unauthorized' do @@ -181,7 +181,7 @@ RSpec.describe API::Internal::MailRoom 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 } + { Gitlab::MailRoom::INTERNAL_API_REQUEST_HEADER => jwt_token } end it 'responds with 401 Unauthorized' do diff --git a/spec/requests/api/invitations_spec.rb b/spec/requests/api/invitations_spec.rb index 702e6ef0a2a..741cf793a77 100644 --- a/spec/requests/api/invitations_spec.rb +++ b/spec/requests/api/invitations_spec.rb @@ -10,7 +10,7 @@ RSpec.describe API::Invitations do let(:email) { 'email1@example.com' } let(:email2) { 'email2@example.com' } - let_it_be(:project) do + let_it_be(:project, reload: true) do create(:project, :public, creator_id: maintainer.id, namespace: maintainer.namespace) do |project| project.add_developer(developer) project.add_maintainer(maintainer) @@ -208,6 +208,25 @@ RSpec.describe API::Invitations do end end + context 'when adding project bot' do + let_it_be(:project_bot) { create(:user, :project_bot) } + + before do + unrelated_project = create(:project) + unrelated_project.add_maintainer(project_bot) + end + + it 'returns error' do + expect do + post invitations_url(source, maintainer), + params: { email: project_bot.email, access_level: Member::DEVELOPER } + + expect(json_response['status']).to eq 'error' + expect(json_response['message'][project_bot.email]).to include('User project bots cannot be added to other groups / projects') + end.not_to change { source.members.count } + end + end + it "returns a message if member already exists" do post invitations_url(source, maintainer), params: { email: developer.email, access_level: Member::MAINTAINER } diff --git a/spec/requests/api/issues/post_projects_issues_spec.rb b/spec/requests/api/issues/post_projects_issues_spec.rb index 82692366589..7c8994ad9ba 100644 --- a/spec/requests/api/issues/post_projects_issues_spec.rb +++ b/spec/requests/api/issues/post_projects_issues_spec.rb @@ -447,7 +447,7 @@ RSpec.describe API::Issues do post_issue expect(response).to have_gitlab_http_status(:bad_request) - expect(json_response['message']).to eq({ 'error' => 'Spam detected' }) + expect(json_response['message']['base']).to match_array([/issue has been recognized as spam/]) end it 'creates a new spam log entry' do diff --git a/spec/requests/api/issues/put_projects_issues_spec.rb b/spec/requests/api/issues/put_projects_issues_spec.rb index dac721cbea0..6ea77cc6578 100644 --- a/spec/requests/api/issues/put_projects_issues_spec.rb +++ b/spec/requests/api/issues/put_projects_issues_spec.rb @@ -199,8 +199,8 @@ RSpec.describe API::Issues do expect(spam_service).to receive_messages(check_for_spam?: true) end - expect_next_instance_of(Spam::SpamVerdictService) do |verdict_service| - expect(verdict_service).to receive(:execute).and_return(DISALLOW) + allow_next_instance_of(Spam::AkismetService) do |akismet_service| + allow(akismet_service).to receive(:spam?).and_return(true) end end @@ -217,7 +217,7 @@ RSpec.describe API::Issues do update_issue expect(response).to have_gitlab_http_status(:bad_request) - expect(json_response).to include('message' => { 'error' => 'Spam detected' }) + expect(json_response['message']['base']).to match_array([/issue has been recognized as spam/]) end it 'creates a new spam log entry' do @@ -323,44 +323,44 @@ RSpec.describe API::Issues do end it 'removes all labels and touches the record' do - Timecop.travel(1.minute.from_now) do + travel_to(2.minutes.from_now) do put api_for_user, params: { labels: '' } end expect(response).to have_gitlab_http_status(:ok) expect(json_response['labels']).to eq([]) - expect(json_response['updated_at']).to be > Time.now + expect(json_response['updated_at']).to be > Time.current end it 'removes all labels and touches the record with labels param as array' do - Timecop.travel(1.minute.from_now) do + travel_to(2.minutes.from_now) do put api_for_user, params: { labels: [''] } end expect(response).to have_gitlab_http_status(:ok) expect(json_response['labels']).to eq([]) - expect(json_response['updated_at']).to be > Time.now + expect(json_response['updated_at']).to be > Time.current end it 'updates labels and touches the record' do - Timecop.travel(1.minute.from_now) do + travel_to(2.minutes.from_now) do put api_for_user, params: { labels: 'foo,bar' } end expect(response).to have_gitlab_http_status(:ok) expect(json_response['labels']).to contain_exactly('foo', 'bar') - expect(json_response['updated_at']).to be > Time.now + expect(json_response['updated_at']).to be > Time.current end it 'updates labels and touches the record with labels param as array' do - Timecop.travel(1.minute.from_now) do + travel_to(2.minutes.from_now) do put api_for_user, params: { labels: %w(foo bar) } end expect(response).to have_gitlab_http_status(:ok) expect(json_response['labels']).to include 'foo' expect(json_response['labels']).to include 'bar' - expect(json_response['updated_at']).to be > Time.now + expect(json_response['updated_at']).to be > Time.current end it 'allows special label names' do diff --git a/spec/requests/api/labels_spec.rb b/spec/requests/api/labels_spec.rb index db9d72245b3..48f2c45bd98 100644 --- a/spec/requests/api/labels_spec.rb +++ b/spec/requests/api/labels_spec.rb @@ -34,7 +34,7 @@ RSpec.describe API::Labels do expect(response).to have_gitlab_http_status(:ok) expect(json_response['name']).to eq(valid_label_title_2) - expect(json_response['color']).to eq(label1.color) + expect(json_response['color']).to be_color(label1.color) end it "returns 200 if colors is changed (#{route_type} route)" do @@ -42,7 +42,7 @@ RSpec.describe API::Labels do expect(response).to have_gitlab_http_status(:ok) expect(json_response['name']).to eq(label1.name) - expect(json_response['color']).to eq('#FFFFFF') + expect(json_response['color']).to be_color('#FFFFFF') end it "returns 200 if a priority is added (#{route_type} route)" do @@ -86,7 +86,7 @@ RSpec.describe API::Labels do expect(response).to have_gitlab_http_status(:ok) expect(json_response['name']).to eq(valid_label_title_2) - expect(json_response['color']).to eq('#FFFFFF') + expect(json_response['color']).to be_color('#FFFFFF') expect(json_response['description']).to eq('test') end @@ -266,8 +266,8 @@ RSpec.describe API::Labels do 'open_merge_requests_count' => 0, 'name' => group_label.name, 'description' => nil, - 'color' => a_string_matching(/^#\h{6}$/), - 'text_color' => a_string_matching(/^#\h{6}$/), + 'color' => a_valid_color, + 'text_color' => a_valid_color, 'priority' => nil, 'subscribed' => false, 'is_project_label' => false) @@ -277,8 +277,8 @@ RSpec.describe API::Labels do 'open_merge_requests_count' => 1, 'name' => priority_label.name, 'description' => nil, - 'color' => a_string_matching(/^#\h{6}$/), - 'text_color' => a_string_matching(/^#\h{6}$/), + 'color' => a_valid_color, + 'text_color' => a_valid_color, 'priority' => 3, 'subscribed' => false, 'is_project_label' => true) @@ -336,7 +336,7 @@ RSpec.describe API::Labels do expect(response).to have_gitlab_http_status(:created) expect(json_response['name']).to eq(valid_label_title_2) - expect(json_response['color']).to eq('#FFAABB') + expect(json_response['color']).to be_color('#FFAABB') expect(json_response['description']).to eq('test') expect(json_response['priority']).to eq(2) end @@ -350,7 +350,7 @@ RSpec.describe API::Labels do expect(response).to have_gitlab_http_status(:created) expect(json_response['name']).to eq(valid_label_title_2) - expect(json_response['color']).to eq('#FFAABB') + expect(json_response['color']).to be_color('#FFAABB') expect(json_response['description']).to be_nil expect(json_response['priority']).to be_nil end @@ -365,7 +365,7 @@ RSpec.describe API::Labels do expect(response).to have_gitlab_http_status(:created) expect(json_response['name']).to eq(valid_label_title_2) - expect(json_response['color']).to eq('#FFAABB') + expect(json_response['color']).to be_color('#FFAABB') expect(json_response['description']).to be_nil expect(json_response['priority']).to eq(3) end @@ -552,7 +552,7 @@ RSpec.describe API::Labels do expect(response).to have_gitlab_http_status(:ok) expect(json_response['name']).to eq(label1.name) - expect(json_response['color']).to eq(label1.color) + expect(json_response['color']).to be_color(label1.color.to_s) end context 'if group label already exists' do diff --git a/spec/requests/api/members_spec.rb b/spec/requests/api/members_spec.rb index 6186a43f992..561d81f9860 100644 --- a/spec/requests/api/members_spec.rb +++ b/spec/requests/api/members_spec.rb @@ -675,13 +675,13 @@ RSpec.describe API::Members do end context 'adding owner to project' do - it 'returns 403' do + it 'returns created status' do expect do post api("/projects/#{project.id}/members", maintainer), params: { user_id: stranger.id, access_level: Member::OWNER } - expect(response).to have_gitlab_http_status(:bad_request) - end.not_to change { project.members.count } + expect(response).to have_gitlab_http_status(:created) + end.to change { project.members.count }.by(1) end end diff --git a/spec/requests/api/notes_spec.rb b/spec/requests/api/notes_spec.rb index 3c28aed6cac..455400072bf 100644 --- a/spec/requests/api/notes_spec.rb +++ b/spec/requests/api/notes_spec.rb @@ -228,44 +228,83 @@ RSpec.describe API::Notes do end let(:request_body) { 'Hi!' } + let(:params) { { body: request_body } } let(:request_path) { "/projects/#{project.id}/merge_requests/#{merge_request.iid}/notes" } - subject { post api(request_path, user), params: { body: request_body } } + subject { post api(request_path, user), params: params } context 'a command only note' do - let(:request_body) { "/spend 1h" } + context '/spend' do + let(:request_body) { "/spend 1h" } - before do - project.add_developer(user) - end + before do + project.add_developer(user) + end - it 'returns 202 Accepted status' do - subject + it 'returns 202 Accepted status' do + subject - expect(response).to have_gitlab_http_status(:accepted) - end + expect(response).to have_gitlab_http_status(:accepted) + end - it 'does not actually create a new note' do - expect { subject }.not_to change { Note.where(system: false).count } - end + it 'does not actually create a new note' do + expect { subject }.not_to change { Note.where(system: false).count } + end - it 'does however create a system note about the change', :sidekiq_inline do - expect { subject }.to change { Note.system.count }.by(1) - end + it 'does however create a system note about the change', :sidekiq_inline do + expect { subject }.to change { Note.system.count }.by(1) + end + + it 'applies the commands' do + expect { subject }.to change { merge_request.reset.total_time_spent } + end + + it 'reports the changes' do + subject - it 'applies the commands' do - expect { subject }.to change { merge_request.reset.total_time_spent } + expect(json_response).to include( + 'commands_changes' => include( + 'spend_time' => include('duration' => 3600) + ), + 'summary' => include('Added 1h spent time.') + ) + end end - it 'reports the changes' do - subject + context '/merge' do + let(:request_body) { "/merge" } + let(:project) { create(:project, :public, :repository) } + let(:merge_request) { create(:merge_request_with_multiple_diffs, source_project: project, target_project: project, author: user) } + let(:params) { { body: request_body, merge_request_diff_head_sha: merge_request.diff_head_sha } } + + before do + project.add_developer(user) + end + + it 'returns 202 Accepted status' do + subject + + expect(response).to have_gitlab_http_status(:accepted) + end + + it 'does not actually create a new note' do + expect { subject }.not_to change { Note.where(system: false).count } + end + + it 'applies the commands' do + expect { subject }.to change { merge_request.reload.merge_jid.present? }.from(false).to(true) + end - expect(json_response).to include( - 'commands_changes' => include( - 'spend_time' => include('duration' => 3600) - ), - 'summary' => include('Added 1h spent time.') - ) + it 'reports the changes' do + subject + + expect(json_response).to include( + 'commands_changes' => include( + 'merge' => merge_request.diff_head_sha + ), + 'summary' => ['Merged this merge request.'] + ) + end end end diff --git a/spec/requests/api/project_attributes.yml b/spec/requests/api/project_attributes.yml index 8a6e87944ec..02d377efd95 100644 --- a/spec/requests/api/project_attributes.yml +++ b/spec/requests/api/project_attributes.yml @@ -9,6 +9,7 @@ itself: # project - external_webhook_token - has_external_issue_tracker - has_external_wiki + - hidden - import_source - import_type - import_url @@ -121,7 +122,6 @@ project_feature: - created_at - metrics_dashboard_access_level - project_id - - security_and_compliance_access_level - updated_at computed_attributes: - issues_enabled diff --git a/spec/requests/api/project_clusters_spec.rb b/spec/requests/api/project_clusters_spec.rb index b83b41a881a..4c7da78f0d4 100644 --- a/spec/requests/api/project_clusters_spec.rb +++ b/spec/requests/api/project_clusters_spec.rb @@ -24,6 +24,10 @@ RSpec.describe API::ProjectClusters do projects: [project]) end + include_examples ':certificate_based_clusters feature flag API responses' do + let(:subject) { get api("/projects/#{project.id}/clusters", developer_user) } + end + context 'non-authorized user' do it 'responds with 403' do get api("/projects/#{project.id}/clusters", reporter_user) @@ -67,6 +71,10 @@ RSpec.describe API::ProjectClusters do projects: [project]) end + include_examples ':certificate_based_clusters feature flag API responses' do + let(:subject) { get api("/projects/#{project.id}/clusters/#{cluster_id}", developer_user) } + end + context 'non-authorized user' do it 'responds with 403' do get api("/projects/#{project.id}/clusters/#{cluster_id}", reporter_user) @@ -182,6 +190,10 @@ RSpec.describe API::ProjectClusters do } end + include_examples ':certificate_based_clusters feature flag API responses' do + let(:subject) { post api("/projects/#{project.id}/clusters/user", maintainer_user), params: cluster_params } + end + context 'non-authorized user' do it 'responds with 403' do post api("/projects/#{project.id}/clusters/user", developer_user), params: cluster_params @@ -361,6 +373,10 @@ RSpec.describe API::ProjectClusters do projects: [project]) end + include_examples ':certificate_based_clusters feature flag API responses' do + let(:subject) { put api("/projects/#{project.id}/clusters/#{cluster.id}", maintainer_user), params: update_params } + end + context 'non-authorized user' do it 'responds with 403' do put api("/projects/#{project.id}/clusters/#{cluster.id}", developer_user), params: update_params @@ -493,6 +509,10 @@ RSpec.describe API::ProjectClusters do projects: [project]) end + include_examples ':certificate_based_clusters feature flag API responses' do + let(:subject) { delete api("/projects/#{project.id}/clusters/#{cluster.id}", maintainer_user), params: cluster_params } + end + context 'non-authorized user' do it 'responds with 403' do delete api("/projects/#{project.id}/clusters/#{cluster.id}", developer_user), params: cluster_params diff --git a/spec/requests/api/project_import_spec.rb b/spec/requests/api/project_import_spec.rb index 3ed08afd57d..a0f6d3d0081 100644 --- a/spec/requests/api/project_import_spec.rb +++ b/spec/requests/api/project_import_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe API::ProjectImport do +RSpec.describe API::ProjectImport, :aggregate_failures do include WorkhorseHelpers include AfterNextHelpers @@ -47,7 +47,7 @@ RSpec.describe API::ProjectImport do it 'executes a limited number of queries' do control_count = ActiveRecord::QueryRecorder.new { subject }.count - expect(control_count).to be <= 104 + expect(control_count).to be <= 105 end it 'schedules an import using a namespace' do @@ -329,7 +329,7 @@ RSpec.describe API::ProjectImport do ) service_response = ServiceResponse.success(payload: project) - expect_next(::Import::GitlabProjects::CreateProjectFromRemoteFileService) + expect_next(::Import::GitlabProjects::CreateProjectService) .to receive(:execute) .and_return(service_response) @@ -352,7 +352,86 @@ RSpec.describe API::ProjectImport do message: 'Failed to import', http_status: :bad_request ) - expect_next(::Import::GitlabProjects::CreateProjectFromRemoteFileService) + expect_next(::Import::GitlabProjects::CreateProjectService) + .to receive(:execute) + .and_return(service_response) + + subject + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response).to eq({ + 'message' => 'Failed to import' + }) + end + end + end + end + + describe 'POST /projects/remote-import-s3' do + subject do + post api('/projects/remote-import-s3', user), params: params + end + + let(:params) do + { + path: 'test-import', + region: 'region_name', + bucket_name: 'bucket_name', + file_key: 'file_key', + access_key_id: 'access_key_id', + secret_access_key: 'secret_access_key' + } + end + + it_behaves_like 'requires authentication' + + it 'returns NOT FOUND when the feature is disabled' do + stub_feature_flags(import_project_from_remote_file_s3: false) + + subject + + expect(response).to have_gitlab_http_status(:not_found) + end + + context 'when the feature flag is enabled' do + before do + stub_feature_flags(import_project_from_remote_file_s3: true) + end + + context 'when the response is successful' do + it 'schedules the import successfully' do + project = create( + :project, + namespace: user.namespace, + name: 'test-import', + path: 'test-import' + ) + + service_response = ServiceResponse.success(payload: project) + expect_next(::Import::GitlabProjects::CreateProjectService) + .to receive(:execute) + .and_return(service_response) + + subject + + expect(response).to have_gitlab_http_status(:created) + expect(json_response).to include({ + 'id' => project.id, + 'name' => 'test-import', + 'name_with_namespace' => "#{user.namespace.name} / test-import", + 'path' => 'test-import', + 'path_with_namespace' => "#{user.namespace.path}/test-import" + }) + end + end + + context 'when the service returns an error' do + it 'fails to schedule the import' do + service_response = ServiceResponse.error( + message: 'Failed to import', + http_status: :bad_request + ) + expect_next(::Import::GitlabProjects::CreateProjectService) .to receive(:execute) .and_return(service_response) diff --git a/spec/requests/api/project_snippets_spec.rb b/spec/requests/api/project_snippets_spec.rb index 512cbf7c321..72519ed1683 100644 --- a/spec/requests/api/project_snippets_spec.rb +++ b/spec/requests/api/project_snippets_spec.rb @@ -276,7 +276,7 @@ RSpec.describe API::ProjectSnippets do it 'rejects the snippet' do expect { subject }.not_to change { Snippet.count } expect(response).to have_gitlab_http_status(:bad_request) - expect(json_response['message']).to eq({ "error" => "Spam detected" }) + expect(json_response['message']['error']).to match(/snippet has been recognized as spam/) end it 'creates a spam log' do @@ -344,7 +344,7 @@ RSpec.describe API::ProjectSnippets do .not_to change { snippet.reload.title } expect(response).to have_gitlab_http_status(:bad_request) - expect(json_response['message']).to eq({ "error" => "Spam detected" }) + expect(json_response['message']['error']).to match(/snippet has been recognized as spam/) end it 'creates a spam log' do diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 02df82d14a8..fc1d815a64e 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -1077,6 +1077,7 @@ RSpec.describe API::Projects do attrs[:operations_access_level] = 'disabled' attrs[:analytics_access_level] = 'disabled' attrs[:container_registry_access_level] = 'private' + attrs[:security_and_compliance_access_level] = 'private' end post api('/projects', user), params: project @@ -1100,6 +1101,7 @@ RSpec.describe API::Projects do expect(project.operations_access_level).to eq(ProjectFeature::DISABLED) expect(project.project_feature.analytics_access_level).to eq(ProjectFeature::DISABLED) expect(project.project_feature.container_registry_access_level).to eq(ProjectFeature::PRIVATE) + expect(project.project_feature.security_and_compliance_access_level).to eq(ProjectFeature::PRIVATE) end it 'assigns container_registry_enabled to project', :aggregate_failures do @@ -2227,6 +2229,7 @@ RSpec.describe API::Projects do expect(json_response['restrict_user_defined_variables']).to eq(project.restrict_user_defined_variables?) expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to eq(project.only_allow_merge_if_all_discussions_are_resolved) expect(json_response['operations_access_level']).to be_present + expect(json_response['security_and_compliance_access_level']).to be_present end it 'exposes all necessary attributes' do @@ -2295,6 +2298,7 @@ RSpec.describe API::Projects do expect(json_response['wiki_access_level']).to be_present expect(json_response['builds_access_level']).to be_present expect(json_response['operations_access_level']).to be_present + expect(json_response['security_and_compliance_access_level']).to be_present expect(json_response).to have_key('emails_disabled') expect(json_response['resolve_outdated_diff_discussions']).to eq(project.resolve_outdated_diff_discussions) expect(json_response['remove_source_branch_after_merge']).to be_truthy @@ -2542,9 +2546,11 @@ RSpec.describe API::Projects do get api("/projects", user) expect(response).to have_gitlab_http_status(:ok) - expect(json_response.first['permissions']['project_access']['access_level']) + detail_of_project = json_response.find { |detail| detail['id'] == project.id } + + expect(detail_of_project.dig('permissions', 'project_access', 'access_level')) .to eq(Gitlab::Access::MAINTAINER) - expect(json_response.first['permissions']['group_access']).to be_nil + expect(detail_of_project.dig('permissions', 'group_access')).to be_nil end end @@ -3220,6 +3226,30 @@ RSpec.describe API::Projects do expect(project.reload.container_registry_access_level).to eq(ProjectFeature::ENABLED) end + it 'sets security_and_compliance_access_level', :aggregate_failures do + put api("/projects/#{project.id}", user), params: { security_and_compliance_access_level: 'private' } + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['security_and_compliance_access_level']).to eq('private') + expect(Project.find_by(path: project[:path]).security_and_compliance_access_level).to eq(ProjectFeature::PRIVATE) + end + + it 'sets operations_access_level', :aggregate_failures do + put api("/projects/#{project.id}", user), params: { operations_access_level: 'private' } + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['operations_access_level']).to eq('private') + expect(Project.find_by(path: project[:path]).operations_access_level).to eq(ProjectFeature::PRIVATE) + end + + it 'sets analytics_access_level', :aggregate_failures do + put api("/projects/#{project.id}", user), params: { analytics_access_level: 'private' } + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['analytics_access_level']).to eq('private') + expect(Project.find_by(path: project[:path]).analytics_access_level).to eq(ProjectFeature::PRIVATE) + end + it 'returns 400 when nothing sent' do project_param = {} diff --git a/spec/requests/api/pypi_packages_spec.rb b/spec/requests/api/pypi_packages_spec.rb index fcd2d56e655..078db4f1509 100644 --- a/spec/requests/api/pypi_packages_spec.rb +++ b/spec/requests/api/pypi_packages_spec.rb @@ -185,6 +185,14 @@ RSpec.describe API::PypiPackages do it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member] end + + context 'without requires_python' do + let(:token) { personal_access_token.token } + let(:user_headers) { basic_auth_header(user.username, token) } + let(:headers) { user_headers.merge(workhorse_headers) } + + it_behaves_like 'PyPI package creation', :developer, :created, true + end end context 'with required_python too big' do diff --git a/spec/requests/api/releases_spec.rb b/spec/requests/api/releases_spec.rb index cb9b6a072b1..6038682de1e 100644 --- a/spec/requests/api/releases_spec.rb +++ b/spec/requests/api/releases_spec.rb @@ -160,7 +160,7 @@ RSpec.describe API::Releases do get api("/projects/#{project.id}/releases", maintainer) end.count - create_list(:release, 2, :with_evidence, project: project, tag: 'v0.1', author: maintainer) + create_list(:release, 2, :with_evidence, project: project, author: maintainer) create_list(:release, 2, project: project) create_list(:release_link, 2, release: project.releases.first) create_list(:release_link, 2, release: project.releases.last) @@ -467,10 +467,10 @@ RSpec.describe API::Releases do it "exposes tag and commit" do create(:release, project: project, - tag: 'v0.1', + tag: 'v0.0.1', author: maintainer, created_at: 2.days.ago) - get api("/projects/#{project.id}/releases/v0.1", guest) + get api("/projects/#{project.id}/releases/v0.0.1", guest) expect(response).to match_response_schema('public_api/v4/release') end diff --git a/spec/requests/api/repositories_spec.rb b/spec/requests/api/repositories_spec.rb index f42fc7aabc2..1d199a72d1d 100644 --- a/spec/requests/api/repositories_spec.rb +++ b/spec/requests/api/repositories_spec.rb @@ -783,6 +783,13 @@ RSpec.describe API::Repositories do expect(response).to have_gitlab_http_status(:ok) expect(json_response['notes']).to be_present end + + context 'when previous tag version does not exist' do + it_behaves_like '422 response' do + let(:request) { get api("/projects/#{project.id}/repository/changelog", user), params: { version: 'v0.0.0' } } + let(:message) { 'Failed to generate the changelog: The commit start range is unspecified, and no previous tag could be found to use instead' } + end + end end describe 'POST /projects/:id/repository/changelog' do diff --git a/spec/requests/api/search_spec.rb b/spec/requests/api/search_spec.rb index 24cd95781c3..4d2a69cd85b 100644 --- a/spec/requests/api/search_spec.rb +++ b/spec/requests/api/search_spec.rb @@ -8,6 +8,11 @@ RSpec.describe API::Search do let_it_be(:project, reload: true) { create(:project, :wiki_repo, :public, name: 'awesome project', group: group) } let_it_be(:repo_project) { create(:project, :public, :repository, group: group) } + before do + allow(Gitlab::ApplicationRateLimiter).to receive(:threshold).with(:search_rate_limit).and_return(1000) + allow(Gitlab::ApplicationRateLimiter).to receive(:threshold).with(:search_rate_limit_unauthenticated).and_return(1000) + end + shared_examples 'response is correct' do |schema:, size: 1| it { expect(response).to have_gitlab_http_status(:ok) } it { expect(response).to match_response_schema(schema) } @@ -347,7 +352,7 @@ RSpec.describe API::Search do end end - it_behaves_like 'rate limited endpoint', rate_limit_key: :user_email_lookup do + it_behaves_like 'rate limited endpoint', rate_limit_key: :search_rate_limit do let(:current_user) { user } def request @@ -522,7 +527,7 @@ 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 + it_behaves_like 'rate limited endpoint', rate_limit_key: :search_rate_limit do let(:current_user) { user } def request @@ -803,7 +808,7 @@ RSpec.describe API::Search do end end - it_behaves_like 'rate limited endpoint', rate_limit_key: :user_email_lookup do + it_behaves_like 'rate limited endpoint', rate_limit_key: :search_rate_limit do let(:current_user) { user } def request diff --git a/spec/requests/api/snippets_spec.rb b/spec/requests/api/snippets_spec.rb index dd5e6ac8a5e..13160519996 100644 --- a/spec/requests/api/snippets_spec.rb +++ b/spec/requests/api/snippets_spec.rb @@ -325,7 +325,7 @@ RSpec.describe API::Snippets, factory_default: :keep do expect { subject }.not_to change { Snippet.count } expect(response).to have_gitlab_http_status(:bad_request) - expect(json_response['message']).to eq({ "error" => "Spam detected" }) + expect(json_response['message']['error']).to match(/snippet has been recognized as spam/) end it 'creates a spam log' do @@ -392,7 +392,7 @@ RSpec.describe API::Snippets, factory_default: :keep do .not_to change { snippet.reload.title } expect(response).to have_gitlab_http_status(:bad_request) - expect(json_response['message']).to eq({ "error" => "Spam detected" }) + expect(json_response['message']['error']).to match(/snippet has been recognized as spam/) end it 'creates a spam log' do diff --git a/spec/requests/api/system_hooks_spec.rb b/spec/requests/api/system_hooks_spec.rb index 1511872d183..d94b70ec0f9 100644 --- a/spec/requests/api/system_hooks_spec.rb +++ b/spec/requests/api/system_hooks_spec.rb @@ -36,12 +36,57 @@ RSpec.describe API::SystemHooks do expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers - expect(json_response).to be_an Array + expect(response).to match_response_schema('public_api/v4/system_hooks') + expect(json_response.first).not_to have_key("token") expect(json_response.first['url']).to eq(hook.url) expect(json_response.first['push_events']).to be false expect(json_response.first['tag_push_events']).to be false expect(json_response.first['merge_requests_events']).to be false expect(json_response.first['repository_update_events']).to be true + expect(json_response.first['enable_ssl_verification']).to be true + end + end + end + + describe "GET /hooks/:id" do + context "when no user" do + it "returns authentication error" do + get api("/hooks/#{hook.id}") + + expect(response).to have_gitlab_http_status(:unauthorized) + end + end + + context "when not an admin" do + it "returns forbidden error" do + get api("/hooks/#{hook.id}", user) + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + + context "when authenticated as admin" do + it "gets a hook", :aggregate_failures do + get api("/hooks/#{hook.id}", admin) + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema('public_api/v4/system_hook') + expect(json_response).to match( + 'id' => be(hook.id), + 'url' => eq(hook.url), + 'created_at' => eq(hook.created_at.iso8601(3)), + 'push_events' => be(hook.push_events), + 'tag_push_events' => be(hook.tag_push_events), + 'merge_requests_events' => be(hook.merge_requests_events), + 'repository_update_events' => be(hook.repository_update_events), + 'enable_ssl_verification' => be(hook.enable_ssl_verification) + ) + end + + it 'returns 404 if the system hook does not exist' do + get api("/hooks/#{non_existing_record_id}", admin) + + expect(response).to have_gitlab_http_status(:not_found) end end end @@ -77,6 +122,7 @@ RSpec.describe API::SystemHooks do post api('/hooks', admin), params: { url: 'http://mep.mep' } expect(response).to have_gitlab_http_status(:created) + expect(response).to match_response_schema('public_api/v4/system_hook') expect(json_response['enable_ssl_verification']).to be true expect(json_response['push_events']).to be false expect(json_response['tag_push_events']).to be false @@ -98,6 +144,7 @@ RSpec.describe API::SystemHooks do } expect(response).to have_gitlab_http_status(:created) + expect(response).to match_response_schema('public_api/v4/system_hook') expect(json_response['enable_ssl_verification']).to be false expect(json_response['push_events']).to be true expect(json_response['tag_push_events']).to be true diff --git a/spec/requests/api/terraform/state_spec.rb b/spec/requests/api/terraform/state_spec.rb index 24f38b04348..ae1e461d433 100644 --- a/spec/requests/api/terraform/state_spec.rb +++ b/spec/requests/api/terraform/state_spec.rb @@ -36,8 +36,8 @@ RSpec.describe API::Terraform::State do let(:current_user) { maintainer } it_behaves_like 'tracking unique hll events' do - let(:target_id) { 'p_terraform_state_api_unique_users' } - let(:expected_type) { instance_of(Integer) } + let(:target_event) { 'p_terraform_state_api_unique_users' } + let(:expected_value) { instance_of(Integer) } end end end diff --git a/spec/requests/api/topics_spec.rb b/spec/requests/api/topics_spec.rb index 70eee8a1af9..5c17ca9581e 100644 --- a/spec/requests/api/topics_spec.rb +++ b/spec/requests/api/topics_spec.rb @@ -7,9 +7,9 @@ RSpec.describe API::Topics do let_it_be(:file) { fixture_file_upload('spec/fixtures/dk.png') } - let_it_be(:topic_1) { create(:topic, name: 'Git', total_projects_count: 1, avatar: file) } - let_it_be(:topic_2) { create(:topic, name: 'GitLab', total_projects_count: 2) } - let_it_be(:topic_3) { create(:topic, name: 'other-topic', total_projects_count: 3) } + let_it_be(:topic_1) { create(:topic, name: 'Git', total_projects_count: 1, non_private_projects_count: 1, avatar: file) } + let_it_be(:topic_2) { create(:topic, name: 'GitLab', total_projects_count: 2, non_private_projects_count: 2) } + let_it_be(:topic_3) { create(:topic, name: 'other-topic', total_projects_count: 3, non_private_projects_count: 3) } let_it_be(:admin) { create(:user, :admin) } let_it_be(:user) { create(:user) } @@ -142,6 +142,13 @@ RSpec.describe API::Topics do expect(response).to have_gitlab_http_status(:bad_request) expect(json_response['error']).to eql('name is missing') end + + it 'returns 400 if name is not unique (case insensitive)' do + post api('/topics/', admin), params: { name: topic_1.name.downcase } + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['message']['name']).to eq(['has already been taken']) + end end context 'as normal user' do @@ -248,4 +255,43 @@ RSpec.describe API::Topics do end end end + + describe 'DELETE /topics', :aggregate_failures do + context 'as administrator' do + it 'deletes a topic' do + delete api("/topics/#{topic_3.id}", admin), params: { name: 'my-topic' } + + expect(response).to have_gitlab_http_status(:no_content) + end + + it 'returns 404 for non existing id' do + delete api("/topics/#{non_existing_record_id}", admin), params: { name: 'my-topic' } + + expect(response).to have_gitlab_http_status(:not_found) + end + + it 'returns 400 for invalid `id` parameter' do + delete api('/topics/invalid', admin), params: { name: 'my-topic' } + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['error']).to eql('id is invalid') + end + end + + context 'as normal user' do + it 'returns 403 Forbidden' do + delete api("/topics/#{topic_3.id}", user), params: { name: 'my-topic' } + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + + context 'as anonymous' do + it 'returns 401 Unauthorized' do + delete api("/topics/#{topic_3.id}"), params: { name: 'my-topic' } + + expect(response).to have_gitlab_http_status(:unauthorized) + end + end + end end diff --git a/spec/requests/api/user_counts_spec.rb b/spec/requests/api/user_counts_spec.rb index ab2aa87d1b7..27ebf02dd81 100644 --- a/spec/requests/api/user_counts_spec.rb +++ b/spec/requests/api/user_counts_spec.rb @@ -43,6 +43,21 @@ RSpec.describe API::UserCounts do expect(response).to have_gitlab_http_status(:ok) expect(json_response).to be_a Hash expect(json_response['merge_requests']).to eq(2) + expect(json_response['attention_requests']).to eq(2) + end + + describe 'mr_attention_requests is disabled' do + before do + stub_feature_flags(mr_attention_requests: false) + end + + it 'does not include attention_requests count' do + create(:merge_request, source_project: project, author: user, assignees: [user]) + + get api('/user_counts', user) + + expect(json_response.key?('attention_requests')).to be(false) + end end end diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index 985e07bf174..2d71674273b 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -11,6 +11,7 @@ RSpec.describe API::Users do let(:blocked_user) { create(:user, :blocked) } let(:omniauth_user) { create(:omniauth_user) } + let(:ldap_user) { create(:omniauth_user, provider: 'ldapmain') } let(:ldap_blocked_user) { create(:omniauth_user, provider: 'ldapmain', state: 'ldap_blocked') } let(:private_user) { create(:user, private_profile: true) } let(:deactivated_user) { create(:user, state: 'deactivated') } @@ -649,20 +650,6 @@ RSpec.describe API::Users do 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 @@ -1307,10 +1294,10 @@ RSpec.describe API::Users do end it "updates user's existing identity" do - put api("/users/#{omniauth_user.id}", admin), params: { provider: 'ldapmain', extern_uid: '654321' } + put api("/users/#{ldap_user.id}", admin), params: { provider: 'ldapmain', extern_uid: '654321' } expect(response).to have_gitlab_http_status(:ok) - expect(omniauth_user.reload.identities.first.extern_uid).to eq('654321') + expect(ldap_user.reload.identities.first.extern_uid).to eq('654321') end it 'updates user with new identity' do @@ -1735,6 +1722,33 @@ RSpec.describe API::Users do end end + describe 'GET /user/:id/keys/:key_id' do + it 'gets existing key', :aggregate_failures do + user.keys << key + + get api("/users/#{user.id}/keys/#{key.id}") + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['title']).to eq(key.title) + end + + it 'returns 404 error if user not found', :aggregate_failures do + user.keys << key + + get api("/users/0/keys/#{key.id}") + + expect(response).to have_gitlab_http_status(:not_found) + expect(json_response['message']).to eq('404 User Not Found') + end + + it 'returns 404 error if key not found', :aggregate_failures do + get api("/users/#{user.id}/keys/#{non_existing_record_id}") + + expect(response).to have_gitlab_http_status(:not_found) + expect(json_response['message']).to eq('404 Key Not Found') + end + end + describe 'DELETE /user/:id/keys/:key_id' do context 'when unauthenticated' do it 'returns authentication error' do @@ -3103,6 +3117,18 @@ RSpec.describe API::Users do expect(response.body).to eq('null') end end + + context 'with the API initiating user' do + let(:user_id) { admin.id } + + it 'does not block the API initiating user, returns 403' do + block_user + + expect(response).to have_gitlab_http_status(:forbidden) + expect(json_response['message']).to eq('403 Forbidden - The API initiating user cannot be blocked by the API') + expect(admin.reload.state).to eq('active') + end + end end it 'is not available for non admin users' do diff --git a/spec/requests/api/wikis_spec.rb b/spec/requests/api/wikis_spec.rb index ec34dc7e7a1..06ae61ca5eb 100644 --- a/spec/requests/api/wikis_spec.rb +++ b/spec/requests/api/wikis_spec.rb @@ -31,7 +31,7 @@ RSpec.describe API::Wikis do let(:project_wiki) { create(:project_wiki, project: project, user: user) } let(:payload) { { content: 'content', format: 'rdoc', title: 'title' } } - let(:expected_keys_with_content) { %w(content format slug title) } + let(:expected_keys_with_content) { %w(content format slug title encoding) } let(:expected_keys_without_content) { %w(format slug title) } let(:wiki) { project_wiki } @@ -130,41 +130,42 @@ RSpec.describe API::Wikis do describe 'GET /projects/:id/wikis/:slug' do let(:page) { create(:wiki_page, wiki: project.wiki) } let(:url) { "/projects/#{project.id}/wikis/#{page.slug}" } + let(:params) { {} } + + subject(:request) { get api(url, user), params: params } context 'when wiki is disabled' do let(:project) { project_wiki_disabled } + before do + request + end + context 'when user is guest' do - before do - get api(url) - end + let(:user) { nil } include_examples 'wiki API 404 Project Not Found' end context 'when user is developer' do - before do - get api(url, developer) - end + let(:user) { developer } include_examples 'wiki API 403 Forbidden' end context 'when user is maintainer' do - before do - get api(url, maintainer) - end + let(:user) { maintainer } include_examples 'wiki API 403 Forbidden' end end context 'when wiki is available only for team members' do - let(:project) { create(:project, :wiki_repo, :wiki_private) } + let_it_be_with_reload(:project) { create(:project, :wiki_repo, :wiki_private) } context 'when user is guest' do before do - get api(url) + request end include_examples 'wiki API 404 Project Not Found' @@ -173,7 +174,6 @@ RSpec.describe API::Wikis do context 'when user is developer' do before do project.add_developer(user) - get api(url, user) end include_examples 'wikis API returns wiki page' @@ -181,6 +181,10 @@ RSpec.describe API::Wikis do context 'when page is not existing' do let(:url) { "/projects/#{project.id}/wikis/unknown" } + before do + request + end + include_examples 'wiki API 404 Wiki Page Not Found' end end @@ -188,8 +192,6 @@ RSpec.describe API::Wikis do context 'when user is maintainer' do before do project.add_maintainer(user) - - get api(url, user) end include_examples 'wikis API returns wiki page' @@ -197,17 +199,23 @@ RSpec.describe API::Wikis do context 'when page is not existing' do let(:url) { "/projects/#{project.id}/wikis/unknown" } + before do + request + end + include_examples 'wiki API 404 Wiki Page Not Found' end end end context 'when wiki is available for everyone with access' do - let(:project) { create(:project, :wiki_repo) } + let_it_be_with_reload(:project) { create(:project, :wiki_repo) } context 'when user is guest' do + let(:user) { nil } + before do - get api(url) + request end include_examples 'wiki API 404 Project Not Found' @@ -216,8 +224,6 @@ RSpec.describe API::Wikis do context 'when user is developer' do before do project.add_developer(user) - - get api(url, user) end include_examples 'wikis API returns wiki page' @@ -225,6 +231,10 @@ RSpec.describe API::Wikis do context 'when page is not existing' do let(:url) { "/projects/#{project.id}/wikis/unknown" } + before do + request + end + include_examples 'wiki API 404 Wiki Page Not Found' end end @@ -232,8 +242,6 @@ RSpec.describe API::Wikis do context 'when user is maintainer' do before do project.add_maintainer(user) - - get api(url, user) end include_examples 'wikis API returns wiki page' @@ -241,6 +249,10 @@ RSpec.describe API::Wikis do context 'when page is not existing' do let(:url) { "/projects/#{project.id}/wikis/unknown" } + before do + request + end + include_examples 'wiki API 404 Wiki Page Not Found' end end diff --git a/spec/requests/content_security_policy_spec.rb b/spec/requests/content_security_policy_spec.rb new file mode 100644 index 00000000000..06fc5b0e190 --- /dev/null +++ b/spec/requests/content_security_policy_spec.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +require 'spec_helper' + +# The AnonymousController doesn't support setting the CSP +# This is why an arbitrary test request was chosen instead +# of testing in application_controller_spec. +RSpec.describe 'Content Security Policy' do + let(:snowplow_host) { 'snowplow.example.com' } + + shared_examples 'snowplow is not in the CSP' do + it 'does not add the snowplow collector hostname to the CSP' do + get explore_root_url + + expect(response).to have_gitlab_http_status(:ok) + expect(response.headers['Content-Security-Policy']).not_to include(snowplow_host) + end + end + + describe 'GET #explore' do + context 'snowplow is enabled' do + before do + stub_application_setting(snowplow_enabled: true, snowplow_collector_hostname: snowplow_host) + end + + it 'adds the snowplow collector hostname to the CSP' do + get explore_root_url + + expect(response).to have_gitlab_http_status(:ok) + expect(response.headers['Content-Security-Policy']).to include(snowplow_host) + end + end + + context 'snowplow is enabled but host is not configured' do + before do + stub_application_setting(snowplow_enabled: true) + end + + it_behaves_like 'snowplow is not in the CSP' + end + + context 'snowplow is disabled' do + before do + stub_application_setting(snowplow_enabled: false, snowplow_collector_hostname: snowplow_host) + end + + it_behaves_like 'snowplow is not in the CSP' + end + end +end diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb index 340ed7bde53..9f9e1cfd90e 100644 --- a/spec/requests/git_http_spec.rb +++ b/spec/requests/git_http_spec.rb @@ -1019,7 +1019,11 @@ RSpec.describe 'Git HTTP requests' do let(:path) { "#{project.full_path}.git" } context "when the project is public" do - let(:project) { create(:project, :repository, :public, path: 'foo.') } + let(:project) do + project = create(:project, :repository, :public) + project.update_attribute(:path, 'foo.') + project + end it_behaves_like 'pushes require Basic HTTP Authentication' @@ -1158,7 +1162,11 @@ RSpec.describe 'Git HTTP requests' do end context "when the project is private" do - let(:project) { create(:project, :repository, :private, path: 'foo.') } + let(:project) do + project = create(:project, :repository, :private) + project.update_attribute(:path, 'foo.') + project + end it_behaves_like 'pulls require Basic HTTP Authentication' it_behaves_like 'pushes require Basic HTTP Authentication' @@ -1586,11 +1594,19 @@ RSpec.describe 'Git HTTP requests' do end it_behaves_like 'project path without .git suffix' do - let(:repository_path) { create(:project, :repository, :public, path: 'project.').full_path } + let(:repository_path) do + project = create(:project, :repository, :public) + project.update_attribute(:path, 'project.') + project.full_path + end end context "retrieving an info/refs file" do - let(:project) { create(:project, :repository, :public, path: 'project.') } + let(:project) do + project = create(:project, :repository, :public) + project.update_attribute(:path, 'project.') + project + end context "when the file exists" do before do @@ -1625,7 +1641,11 @@ RSpec.describe 'Git HTTP requests' do let(:path) { "/#{wiki.repository.full_path}.git" } context "when the project is public" do - let(:project) { create(:project, :wiki_repo, :public, :wiki_enabled, path: 'foo.') } + let(:project) do + project = create(:project, :wiki_repo, :public, :wiki_enabled) + project.update_attribute(:path, 'foo.') + project + end it_behaves_like 'pushes require Basic HTTP Authentication' @@ -1652,7 +1672,11 @@ RSpec.describe 'Git HTTP requests' do end context 'but the repo is disabled' do - let(:project) { create(:project, :wiki_repo, :public, :repository_disabled, :wiki_enabled, path: 'foo.') } + let(:project) do + project = create(:project, :wiki_repo, :public, :repository_disabled, :wiki_enabled) + project.update_attribute(:path, 'foo.') + project + end it_behaves_like 'pulls are allowed' it_behaves_like 'pushes are allowed' @@ -1673,7 +1697,11 @@ RSpec.describe 'Git HTTP requests' do end context "when the project is private" do - let(:project) { create(:project, :wiki_repo, :private, :wiki_enabled, path: 'foo.') } + let(:project) do + project = create(:project, :wiki_repo, :private, :wiki_enabled) + project.update_attribute(:path, 'foo.') + project + end it_behaves_like 'pulls require Basic HTTP Authentication' it_behaves_like 'pushes require Basic HTTP Authentication' @@ -1700,7 +1728,11 @@ RSpec.describe 'Git HTTP requests' do end context 'but the repo is disabled' do - let(:project) { create(:project, :wiki_repo, :private, :repository_disabled, :wiki_enabled, path: 'foo.') } + let(:project) do + project = create(:project, :wiki_repo, :private, :repository_disabled, :wiki_enabled) + project.update_attribute(:path, 'foo.') + project + end it 'allows clones' do download(path, user: user.username, password: user.password) do |response| diff --git a/spec/requests/groups/crm/contacts_controller_spec.rb b/spec/requests/groups/crm/contacts_controller_spec.rb index 5d126c6ead5..4d8ca0fcd60 100644 --- a/spec/requests/groups/crm/contacts_controller_spec.rb +++ b/spec/requests/groups/crm/contacts_controller_spec.rb @@ -49,6 +49,12 @@ RSpec.describe Groups::Crm::ContactsController do it_behaves_like 'response with 404 status' end + + context 'when subgroup' do + let(:group) { create(:group, :private, :crm_enabled, parent: create(:group)) } + + it_behaves_like 'response with 404 status' + end end context 'with unauthorized user' do diff --git a/spec/requests/groups/crm/organizations_controller_spec.rb b/spec/requests/groups/crm/organizations_controller_spec.rb index f38300c3c5b..37ffac71772 100644 --- a/spec/requests/groups/crm/organizations_controller_spec.rb +++ b/spec/requests/groups/crm/organizations_controller_spec.rb @@ -49,6 +49,12 @@ RSpec.describe Groups::Crm::OrganizationsController do it_behaves_like 'response with 404 status' end + + context 'when subgroup' do + let(:group) { create(:group, :private, :crm_enabled, parent: create(:group)) } + + it_behaves_like 'response with 404 status' + end end context 'with unauthorized user' do diff --git a/spec/requests/groups/deploy_tokens_controller_spec.rb b/spec/requests/groups/deploy_tokens_controller_spec.rb new file mode 100644 index 00000000000..b3dce9b9cf1 --- /dev/null +++ b/spec/requests/groups/deploy_tokens_controller_spec.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Groups::DeployTokensController do + let_it_be(:group) { create(:group) } + let_it_be(:user) { create(:user) } + let_it_be(:deploy_token) { create(:deploy_token, :group, groups: [group]) } + let_it_be(:params) do + { id: deploy_token.id, group_id: group } + end + + before do + group.add_owner(user) + + sign_in(user) + end + + describe 'PUT /groups/:group_path_with_namespace/-/deploy_tokens/:id/revoke' do + subject(:put_revoke) do + put "/groups/#{group.full_path}/-/deploy_tokens/#{deploy_token.id}/revoke", params: params + end + + it 'invokes the Groups::DeployTokens::RevokeService' do + expect(deploy_token.revoked).to eq(false) + expect(Groups::DeployTokens::RevokeService).to receive(:new).and_call_original + + put_revoke + + expect(deploy_token.reload.revoked).to eq(true) + end + + it 'redirects to group repository settings with correct anchor' do + put_revoke + + expect(response).to have_gitlab_http_status(:redirect) + expect(response).to redirect_to(group_settings_repository_path(group, anchor: 'js-deploy-tokens')) + end + end +end diff --git a/spec/requests/groups/harbor/repositories_controller_spec.rb b/spec/requests/groups/harbor/repositories_controller_spec.rb new file mode 100644 index 00000000000..3e475dc410e --- /dev/null +++ b/spec/requests/groups/harbor/repositories_controller_spec.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Groups::Harbor::RepositoriesController do + let_it_be(:group, reload: true) { create(:group) } + let_it_be(:user) { create(:user) } + + shared_examples 'responds with 404 status' do + it 'returns 404' do + subject + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + shared_examples 'responds with 200 status' do + it 'renders the index template' do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to render_template(:index) + end + end + + before do + stub_feature_flags(harbor_registry_integration: true) + group.add_reporter(user) + login_as(user) + end + + describe 'GET #index' do + subject do + get group_harbor_registries_path(group) + response + end + + context 'with harbor registry feature flag enabled' do + it_behaves_like 'responds with 200 status' + end + + context 'with harbor registry feature flag disabled' do + before do + stub_feature_flags(harbor_registry_integration: false) + end + + it_behaves_like 'responds with 404 status' + end + end + + describe 'GET #show' do + subject do + get group_harbor_registry_path(group, 1) + response + end + + context 'with harbor registry feature flag enabled' do + it_behaves_like 'responds with 200 status' + end + + context 'with harbor registry feature flag disabled' do + before do + stub_feature_flags(harbor_registry_integration: false) + end + + it_behaves_like 'responds with 404 status' + end + end +end diff --git a/spec/requests/jira_connect/oauth_callbacks_controller_spec.rb b/spec/requests/jira_connect/oauth_callbacks_controller_spec.rb new file mode 100644 index 00000000000..1e4628e5d59 --- /dev/null +++ b/spec/requests/jira_connect/oauth_callbacks_controller_spec.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe JiraConnect::OauthCallbacksController do + describe 'GET /-/jira_connect/oauth_callbacks' do + context 'when logged in' do + let_it_be(:user) { create(:user) } + + before do + sign_in(user) + end + + it 'renders a page prompting the user to close the window' do + get '/-/jira_connect/oauth_callbacks' + + expect(response).to have_gitlab_http_status(:ok) + expect(response.body).to include('You can close this window.') + end + end + end +end diff --git a/spec/requests/projects/google_cloud/deployments_controller_spec.rb b/spec/requests/projects/google_cloud/deployments_controller_spec.rb index fd356bc61c7..7bd9609a7dc 100644 --- a/spec/requests/projects/google_cloud/deployments_controller_spec.rb +++ b/spec/requests/projects/google_cloud/deployments_controller_spec.rb @@ -22,13 +22,21 @@ RSpec.describe Projects::GoogleCloud::DeploymentsController do project.add_maintainer(user_maintainer) end - describe "Routes must be restricted behind Google OAuth2" do + describe "Routes must be restricted behind Google OAuth2", :snowplow do context 'when a public request is made' do it 'returns not found on GET request' do urls_list.each do |url| get url expect(response).to have_gitlab_http_status(:not_found) + expect_snowplow_event( + category: 'Projects::GoogleCloud', + action: 'admin_project_google_cloud!', + label: 'access_denied', + property: 'invalid_user', + project: project, + user: nil + ) end end end @@ -40,6 +48,14 @@ RSpec.describe Projects::GoogleCloud::DeploymentsController do get url expect(response).to have_gitlab_http_status(:not_found) + expect_snowplow_event( + category: 'Projects::GoogleCloud', + action: 'admin_project_google_cloud!', + label: 'access_denied', + property: 'invalid_user', + project: project, + user: nil + ) end end end @@ -60,7 +76,7 @@ RSpec.describe Projects::GoogleCloud::DeploymentsController do end end - describe 'Authorized GET project/-/google_cloud/deployments/cloud_run' do + describe 'Authorized GET project/-/google_cloud/deployments/cloud_run', :snowplow do let_it_be(:url) { "#{project_google_cloud_deployments_cloud_run_path(project)}" } before do @@ -72,25 +88,39 @@ RSpec.describe Projects::GoogleCloud::DeploymentsController do end it 'redirects to google_cloud home on enable service error' do - # since GPC_PROJECT_ID is not set, enable cloud run service should return an error - get url expect(response).to redirect_to(project_google_cloud_index_path(project)) + # since GPC_PROJECT_ID is not set, enable cloud run service should return an error + expect_snowplow_event( + category: 'Projects::GoogleCloud', + action: 'deployments#cloud_run', + label: 'enable_cloud_run_error', + extra: { message: 'No GCP projects found. Configure a service account or GCP_PROJECT_ID ci variable.', + status: :error }, + project: project, + user: user_maintainer + ) end - it 'tracks error and redirects to gcp_error' do - mock_google_error = Google::Apis::ClientError.new('some_error') + it 'redirects to gcp_error' do + mock_gcp_error = Google::Apis::ClientError.new('some_error') allow_next_instance_of(GoogleCloud::EnableCloudRunService) do |service| - allow(service).to receive(:execute).and_raise(mock_google_error) + allow(service).to receive(:execute).and_raise(mock_gcp_error) end - expect(Gitlab::ErrorTracking).to receive(:track_exception).with(mock_google_error, { project_id: project.id }) - get url expect(response).to render_template(:gcp_error) + expect_snowplow_event( + category: 'Projects::GoogleCloud', + action: 'deployments#cloud_run', + label: 'gcp_error', + extra: mock_gcp_error, + project: project, + user: user_maintainer + ) end context 'GCP_PROJECT_IDs are defined' do @@ -106,6 +136,14 @@ RSpec.describe Projects::GoogleCloud::DeploymentsController do get url expect(response).to redirect_to(project_google_cloud_index_path(project)) + expect_snowplow_event( + category: 'Projects::GoogleCloud', + action: 'deployments#cloud_run', + label: 'generate_pipeline_error', + extra: { status: :error }, + project: project, + user: user_maintainer + ) end it 'redirects to create merge request form' do @@ -121,11 +159,24 @@ RSpec.describe Projects::GoogleCloud::DeploymentsController do expect(response).to have_gitlab_http_status(:found) expect(response.location).to include(project_new_merge_request_path(project)) + expect_snowplow_event( + category: 'Projects::GoogleCloud', + action: 'deployments#cloud_run', + label: 'cloud_run_success', + extra: { "title": "Enable deployments to Cloud Run", + "description": "This merge request includes a Cloud Run deployment job in the pipeline definition (.gitlab-ci.yml).\n\nThe `deploy-to-cloud-run` job:\n* Requires the following environment variables\n * `GCP_PROJECT_ID`\n * `GCP_SERVICE_ACCOUNT_KEY`\n* Job definition can be found at: https://gitlab.com/gitlab-org/incubation-engineering/five-minute-production/library\n\nThis pipeline definition has been committed to the branch ``.\nYou may modify the pipeline definition further or accept the changes as-is if suitable.\n", + "source_project_id": project.id, + "target_project_id": project.id, + "source_branch": nil, + "target_branch": project.default_branch }, + project: project, + user: user_maintainer + ) end end end - describe 'Authorized GET project/-/google_cloud/deployments/cloud_storage' do + describe 'Authorized GET project/-/google_cloud/deployments/cloud_storage', :snowplow do let_it_be(:url) { "#{project_google_cloud_deployments_cloud_storage_path(project)}" } before do diff --git a/spec/requests/projects/google_cloud/gcp_regions_controller_spec.rb b/spec/requests/projects/google_cloud/gcp_regions_controller_spec.rb new file mode 100644 index 00000000000..56474b6520d --- /dev/null +++ b/spec/requests/projects/google_cloud/gcp_regions_controller_spec.rb @@ -0,0 +1,152 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Projects::GoogleCloud::GcpRegionsController do + let_it_be(:project) { create(:project, :public, :repository) } + let_it_be(:repository) { project.repository } + + let(:user_guest) { create(:user) } + let(:user_maintainer) { create(:user) } + + RSpec.shared_examples "should track not_found event" do + it "tracks event" do + is_expected.to be(404) + expect_snowplow_event( + category: 'Projects::GoogleCloud', + action: 'admin_project_google_cloud!', + label: 'access_denied', + property: 'invalid_user', + project: project, + user: nil + ) + end + end + + RSpec.shared_examples "should track access_denied event" do + it "tracks event" do + is_expected.to be(404) + expect_snowplow_event( + category: 'Projects::GoogleCloud', + action: 'admin_project_google_cloud!', + label: 'access_denied', + property: 'invalid_user', + project: project, + user: nil + ) + end + end + + RSpec.shared_examples "should track feature_flag_disabled event" do |user| + it "tracks event" do + is_expected.to be(404) + expect_snowplow_event( + category: 'Projects::GoogleCloud', + action: 'feature_flag_enabled!', + label: 'access_denied', + property: 'feature_flag_not_enabled', + project: project, + user: user_maintainer + ) + end + end + + RSpec.shared_examples "should track gcp_error event" do |config| + it "tracks event" do + is_expected.to be(403) + expect_snowplow_event( + category: 'Projects::GoogleCloud', + action: 'google_oauth2_enabled!', + label: 'access_denied', + extra: { reason: 'google_oauth2_not_configured', config: config }, + project: project, + user: user_maintainer + ) + end + end + + RSpec.shared_examples "should be not found" do + it 'returns not found' do + is_expected.to be(404) + end + end + + RSpec.shared_examples "should be forbidden" do + it 'returns forbidden' do + is_expected.to be(403) + end + end + + RSpec.shared_examples "public request should 404" do + it_behaves_like "should be not found" + it_behaves_like "should track not_found event" + end + + RSpec.shared_examples "unauthorized access should 404" do + before do + project.add_guest(user_guest) + end + + it_behaves_like "should be not found" + it_behaves_like "should track access_denied event" + end + + describe 'GET #index', :snowplow do + subject { get project_google_cloud_gcp_regions_path(project) } + + it_behaves_like "public request should 404" + it_behaves_like "unauthorized access should 404" + + context 'when authorized members make requests' do + before do + project.add_maintainer(user_maintainer) + sign_in(user_maintainer) + end + + it 'renders gcp_regions' do + is_expected.to render_template('projects/google_cloud/gcp_regions/index') + end + + context 'but gitlab instance is not configured for google oauth2' do + unconfigured_google_oauth2 = Struct.new(:app_id, :app_secret) + .new('', '') + + before do + allow(Gitlab::Auth::OAuth::Provider).to receive(:config_for) + .with('google_oauth2') + .and_return(unconfigured_google_oauth2) + end + + it_behaves_like "should be forbidden" + it_behaves_like "should track gcp_error event", unconfigured_google_oauth2 + end + + context 'but feature flag is disabled' do + before do + stub_feature_flags(incubation_5mp_google_cloud: false) + end + + it_behaves_like "should be not found" + it_behaves_like "should track feature_flag_disabled event" + end + end + end + + describe 'POST #index', :snowplow do + subject { post project_google_cloud_gcp_regions_path(project), params: { gcp_region: 'region1', environment: 'env1' } } + + it_behaves_like "public request should 404" + it_behaves_like "unauthorized access should 404" + + context 'when authorized members make requests' do + before do + project.add_maintainer(user_maintainer) + sign_in(user_maintainer) + end + + it 'redirects to google cloud index' do + is_expected.to redirect_to(project_google_cloud_index_path(project)) + end + end + end +end diff --git a/spec/requests/projects/google_cloud/revoke_oauth_controller_spec.rb b/spec/requests/projects/google_cloud/revoke_oauth_controller_spec.rb new file mode 100644 index 00000000000..07590d3710e --- /dev/null +++ b/spec/requests/projects/google_cloud/revoke_oauth_controller_spec.rb @@ -0,0 +1,86 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Projects::GoogleCloud::RevokeOauthController do + include SessionHelpers + + describe 'POST #create', :snowplow, :clean_gitlab_redis_sessions, :aggregate_failures do + let_it_be(:project) { create(:project, :public) } + let_it_be(:url) { project_google_cloud_revoke_oauth_index_path(project).to_s } + + let(:user) { project.creator } + + before do + sign_in(user) + + stub_session(GoogleApi::CloudPlatform::Client.session_key_for_token => 'token') + + allow_next_instance_of(GoogleApi::CloudPlatform::Client) do |client| + allow(client).to receive(:validate_token).and_return(true) + end + end + + context 'when GCP token is invalid' do + before do + allow_next_instance_of(GoogleApi::CloudPlatform::Client) do |client| + allow(client).to receive(:validate_token).and_return(false) + end + end + + it 'redirects to Google OAuth2 authorize URL' do + sign_in(user) + + post url + + expect(response).to redirect_to(assigns(:authorize_url)) + end + end + + context 'when revocation is successful' do + before do + stub_request(:post, "https://oauth2.googleapis.com/revoke") + .to_return(status: 200, body: "", headers: {}) + end + + it 'calls revoke endpoint and redirects' do + post url + + expect(request.session[GoogleApi::CloudPlatform::Client.session_key_for_token]).to be_nil + expect(response).to redirect_to(project_google_cloud_index_path(project)) + expect(flash[:notice]).to eq('Google OAuth2 token revocation requested') + expect_snowplow_event( + category: 'Projects::GoogleCloud', + action: 'revoke_oauth#create', + label: 'create', + property: 'success', + project: project, + user: user + ) + end + end + + context 'when revocation fails' do + before do + stub_request(:post, "https://oauth2.googleapis.com/revoke") + .to_return(status: 400, body: "", headers: {}) + end + + it 'calls revoke endpoint and redirects' do + post url + + expect(request.session[GoogleApi::CloudPlatform::Client.session_key_for_token]).to be_nil + expect(response).to redirect_to(project_google_cloud_index_path(project)) + expect(flash[:alert]).to eq('Google OAuth2 token revocation request failed') + expect_snowplow_event( + category: 'Projects::GoogleCloud', + action: 'revoke_oauth#create', + label: 'create', + property: 'failed', + project: project, + user: user + ) + end + end + end +end diff --git a/spec/requests/projects/google_cloud/service_accounts_controller_spec.rb b/spec/requests/projects/google_cloud/service_accounts_controller_spec.rb index 0f243a6a7a9..4b32965e2b0 100644 --- a/spec/requests/projects/google_cloud/service_accounts_controller_spec.rb +++ b/spec/requests/projects/google_cloud/service_accounts_controller_spec.rb @@ -5,7 +5,7 @@ require 'spec_helper' RSpec.describe Projects::GoogleCloud::ServiceAccountsController do let_it_be(:project) { create(:project, :public) } - describe 'GET index' do + describe 'GET index', :snowplow do let_it_be(:url) { "#{project_google_cloud_service_accounts_path(project)}" } let(:user_guest) { create(:user) } @@ -27,6 +27,14 @@ RSpec.describe Projects::GoogleCloud::ServiceAccountsController do get url expect(response).to have_gitlab_http_status(:not_found) + expect_snowplow_event( + category: 'Projects::GoogleCloud', + action: 'admin_project_google_cloud!', + label: 'access_denied', + property: 'invalid_user', + project: project, + user: nil + ) end it 'returns not found on POST request' do @@ -42,6 +50,14 @@ RSpec.describe Projects::GoogleCloud::ServiceAccountsController do sign_in(unauthorized_member) get url + expect_snowplow_event( + category: 'Projects::GoogleCloud', + action: 'admin_project_google_cloud!', + label: 'access_denied', + property: 'invalid_user', + project: project, + user: unauthorized_member + ) expect(response).to have_gitlab_http_status(:not_found) end @@ -52,6 +68,14 @@ RSpec.describe Projects::GoogleCloud::ServiceAccountsController do sign_in(unauthorized_member) post url + expect_snowplow_event( + category: 'Projects::GoogleCloud', + action: 'admin_project_google_cloud!', + label: 'access_denied', + property: 'invalid_user', + project: project, + user: unauthorized_member + ) expect(response).to have_gitlab_http_status(:not_found) end @@ -80,34 +104,75 @@ RSpec.describe Projects::GoogleCloud::ServiceAccountsController do end context 'and user has successfully completed the google oauth2 flow' do - before do - allow_next_instance_of(GoogleApi::CloudPlatform::Client) do |client| - mock_service_account = Struct.new(:project_id, :unique_id, :email).new(123, 456, 'em@ai.l') - allow(client).to receive(:validate_token).and_return(true) - allow(client).to receive(:list_projects).and_return([{}, {}, {}]) - allow(client).to receive(:create_service_account).and_return(mock_service_account) - allow(client).to receive(:create_service_account_key).and_return({}) - allow(client).to receive(:grant_service_account_roles) + context 'but the user does not have gcp projects' do + before do + allow_next_instance_of(GoogleApi::CloudPlatform::Client) do |client| + mock_service_account = Struct.new(:project_id, :unique_id, :email).new(123, 456, 'em@ai.l') + allow(client).to receive(:list_projects).and_return([]) + allow(client).to receive(:validate_token).and_return(true) + allow(client).to receive(:create_service_account).and_return(mock_service_account) + allow(client).to receive(:create_service_account_key).and_return({}) + allow(client).to receive(:grant_service_account_roles) + end end - end - it 'returns success on GET' do - authorized_members.each do |authorized_member| - sign_in(authorized_member) + it 'renders no_gcp_projects' do + authorized_members.each do |authorized_member| + allow_next_instance_of(BranchesFinder) do |branches_finder| + allow(branches_finder).to receive(:execute).and_return([]) + end - get url + allow_next_instance_of(TagsFinder) do |branches_finder| + allow(branches_finder).to receive(:execute).and_return([]) + end + + sign_in(authorized_member) - expect(response).to have_gitlab_http_status(:ok) + get url + + expect(response).to render_template('projects/google_cloud/errors/no_gcp_projects') + end end end - it 'returns success on POST' do - authorized_members.each do |authorized_member| - sign_in(authorized_member) + context 'user has three gcp_projects' do + before do + allow_next_instance_of(GoogleApi::CloudPlatform::Client) do |client| + mock_service_account = Struct.new(:project_id, :unique_id, :email).new(123, 456, 'em@ai.l') + allow(client).to receive(:list_projects).and_return([{}, {}, {}]) + allow(client).to receive(:validate_token).and_return(true) + allow(client).to receive(:create_service_account).and_return(mock_service_account) + allow(client).to receive(:create_service_account_key).and_return({}) + allow(client).to receive(:grant_service_account_roles) + end + end - post url, params: { gcp_project: 'prj1', environment: 'env1' } + it 'returns success on GET' do + authorized_members.each do |authorized_member| + allow_next_instance_of(BranchesFinder) do |branches_finder| + allow(branches_finder).to receive(:execute).and_return([]) + end + + allow_next_instance_of(TagsFinder) do |branches_finder| + allow(branches_finder).to receive(:execute).and_return([]) + end + + sign_in(authorized_member) + + get url + + expect(response).to have_gitlab_http_status(:ok) + end + end + + it 'returns success on POST' do + authorized_members.each do |authorized_member| + sign_in(authorized_member) + + post url, params: { gcp_project: 'prj1', ref: 'env1' } - expect(response).to redirect_to(project_google_cloud_index_path(project)) + expect(response).to redirect_to(project_google_cloud_index_path(project)) + end end end end diff --git a/spec/requests/projects/google_cloud_controller_spec.rb b/spec/requests/projects/google_cloud_controller_spec.rb index 37682152994..d0814990989 100644 --- a/spec/requests/projects/google_cloud_controller_spec.rb +++ b/spec/requests/projects/google_cloud_controller_spec.rb @@ -8,7 +8,7 @@ MockGoogleOAuth2Credentials = Struct.new(:app_id, :app_secret) RSpec.describe Projects::GoogleCloudController do let_it_be(:project) { create(:project, :public) } - describe 'GET index' do + describe 'GET index', :snowplow do let_it_be(:url) { "#{project_google_cloud_index_path(project)}" } context 'when a public request is made' do @@ -16,6 +16,13 @@ RSpec.describe Projects::GoogleCloudController do get url expect(response).to have_gitlab_http_status(:not_found) + expect_snowplow_event( + category: 'Projects::GoogleCloud', + action: 'admin_project_google_cloud!', + label: 'access_denied', + property: 'invalid_user', + project: project, + user: nil) end end @@ -29,6 +36,14 @@ RSpec.describe Projects::GoogleCloudController do get url expect(response).to have_gitlab_http_status(:not_found) + expect_snowplow_event( + category: 'Projects::GoogleCloud', + action: 'admin_project_google_cloud!', + label: 'access_denied', + property: 'invalid_user', + project: project, + user: user + ) end end @@ -42,6 +57,14 @@ RSpec.describe Projects::GoogleCloudController do get url expect(response).to have_gitlab_http_status(:not_found) + expect_snowplow_event( + category: 'Projects::GoogleCloud', + action: 'admin_project_google_cloud!', + label: 'access_denied', + property: 'invalid_user', + project: project, + user: user + ) end end @@ -74,19 +97,26 @@ RSpec.describe Projects::GoogleCloudController do let(:user) { project.creator } context 'but gitlab instance is not configured for google oauth2' do - before do + it 'returns forbidden' do unconfigured_google_oauth2 = MockGoogleOAuth2Credentials.new('', '') allow(Gitlab::Auth::OAuth::Provider).to receive(:config_for) .with('google_oauth2') .and_return(unconfigured_google_oauth2) - end - it 'returns forbidden' do sign_in(user) get url expect(response).to have_gitlab_http_status(:forbidden) + expect_snowplow_event( + category: 'Projects::GoogleCloud', + action: 'google_oauth2_enabled!', + label: 'access_denied', + extra: { reason: 'google_oauth2_not_configured', + config: unconfigured_google_oauth2 }, + project: project, + user: user + ) end end @@ -101,6 +131,46 @@ RSpec.describe Projects::GoogleCloudController do get url expect(response).to have_gitlab_http_status(:not_found) + expect_snowplow_event( + category: 'Projects::GoogleCloud', + action: 'feature_flag_enabled!', + label: 'access_denied', + property: 'feature_flag_not_enabled', + project: project, + user: user + ) + end + end + + context 'but google oauth2 token is not valid' do + it 'does not return revoke oauth url' do + allow_next_instance_of(GoogleApi::CloudPlatform::Client) do |client| + allow(client).to receive(:validate_token).and_return(false) + end + + sign_in(user) + + get url + + expect(response).to be_successful + expect_snowplow_event( + category: 'Projects::GoogleCloud', + action: 'google_cloud#index', + label: 'index', + extra: { + screen: 'home', + serviceAccounts: [], + createServiceAccountUrl: project_google_cloud_service_accounts_path(project), + enableCloudRunUrl: project_google_cloud_deployments_cloud_run_path(project), + enableCloudStorageUrl: project_google_cloud_deployments_cloud_storage_path(project), + emptyIllustrationUrl: ActionController::Base.helpers.image_path('illustrations/pipelines_empty.svg'), + configureGcpRegionsUrl: project_google_cloud_gcp_regions_path(project), + gcpRegions: [], + revokeOauthUrl: nil + }, + project: project, + user: user + ) end end end diff --git a/spec/requests/projects/harbor/repositories_controller_spec.rb b/spec/requests/projects/harbor/repositories_controller_spec.rb new file mode 100644 index 00000000000..cdb5a696d7e --- /dev/null +++ b/spec/requests/projects/harbor/repositories_controller_spec.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Projects::Harbor::RepositoriesController do + let_it_be(:project, reload: true) { create(:project) } + let_it_be(:user) { create(:user) } + + shared_examples 'responds with 404 status' do + it 'returns 404' do + subject + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + shared_examples 'responds with 200 status' do + it 'renders the index template' do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to render_template(:index) + end + end + + before do + stub_feature_flags(harbor_registry_integration: true) + project.add_developer(user) + sign_in(user) + end + + describe 'GET #index' do + subject do + get project_harbor_registry_index_path(project) + response + end + + context 'with harbor registry feature flag enabled' do + it_behaves_like 'responds with 200 status' + end + + context 'with harbor registry feature flag disabled' do + before do + stub_feature_flags(harbor_registry_integration: false) + end + + it_behaves_like 'responds with 404 status' + end + end + + describe 'GET #show' do + subject do + get project_harbor_registry_path(project, 1) + response + end + + context 'with harbor registry feature flag enabled' do + it_behaves_like 'responds with 200 status' + end + + context 'with harbor registry feature flag disabled' do + before do + stub_feature_flags(harbor_registry_integration: false) + end + + it_behaves_like 'responds with 404 status' + end + end +end diff --git a/spec/requests/projects/redirect_controller_spec.rb b/spec/requests/projects/redirect_controller_spec.rb new file mode 100644 index 00000000000..3bbca3ca32b --- /dev/null +++ b/spec/requests/projects/redirect_controller_spec.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe "Projects::RedirectController requests" do + using RSpec::Parameterized::TableSyntax + + let_it_be(:private_project) { create(:project, :private) } + let_it_be(:public_project) { create(:project, :public) } + let_it_be(:user) { create(:user) } + + before_all do + private_project.add_developer(user) + end + + describe 'GET redirect_from_id' do + where(:authenticated, :project, :is_found) do + true | ref(:private_project) | true + false | ref(:private_project) | false + true | ref(:public_project) | true + false | ref(:public_project) | true + true | build(:project, id: 0) | false + end + + with_them do + before do + sign_in(user) if authenticated + + get "/projects/#{project.id}" + end + + if params[:is_found] + it 'redirects to the project page' do + expect(response).to have_gitlab_http_status(:found) + expect(response).to redirect_to(project_path(project)) + end + else + it 'gives 404' do + expect(response).to have_gitlab_http_status(:not_found) + end + end + end + end + + # This is a regression test for https://gitlab.com/gitlab-org/gitlab/-/issues/351058 + context 'with sourcegraph enabled' do + let_it_be(:sourcegraph_url) { 'https://sourcegraph.test' } + + before do + allow(Gitlab::CurrentSettings).to receive(:sourcegraph_url).and_return(sourcegraph_url) + allow(Gitlab::CurrentSettings).to receive(:sourcegraph_enabled).and_return(true) + + sign_in(user) + end + + context 'with projects/:id route' do + subject { get "/projects/#{public_project.id}" } + + it 'redirects successfully' do + subject + + expect(response).to redirect_to(project_path(public_project)) + end + end + end +end |