diff options
Diffstat (limited to 'spec/requests/api')
59 files changed, 2022 insertions, 170 deletions
diff --git a/spec/requests/api/api_spec.rb b/spec/requests/api/api_spec.rb index 95eb503c6bc..6a02f81fcae 100644 --- a/spec/requests/api/api_spec.rb +++ b/spec/requests/api/api_spec.rb @@ -116,7 +116,7 @@ RSpec.describe API::API do 'meta.root_namespace' => project.namespace.full_path, 'meta.user' => user.username, 'meta.client_id' => a_string_matching(%r{\Auser/.+}), - 'meta.feature_category' => 'issue_tracking', + 'meta.feature_category' => 'team_planning', 'route' => '/api/:version/projects/:id/issues') end @@ -200,6 +200,28 @@ RSpec.describe API::API do expect(response).to have_gitlab_http_status(:not_found) end end + + context 'when there is an unhandled exception for an anonymous request' do + it 'logs all application context fields and the route' do + expect(described_class::LOG_FORMATTER).to receive(:call) do |_severity, _datetime, _, data| + expect(data.stringify_keys) + .to include('correlation_id' => an_instance_of(String), + 'meta.caller_id' => 'GET /api/:version/broadcast_messages', + 'meta.remote_ip' => an_instance_of(String), + 'meta.client_id' => a_string_matching(%r{\Aip/.+}), + 'meta.feature_category' => 'navigation', + 'route' => '/api/:version/broadcast_messages') + + expect(data.stringify_keys).not_to include('meta.project', 'meta.root_namespace', 'meta.user') + end + + expect(BroadcastMessage).to receive(:all).and_raise('An error!') + + get(api('/broadcast_messages')) + + expect(response).to have_gitlab_http_status(:internal_server_error) + end + end end describe 'Marginalia comments' do diff --git a/spec/requests/api/ci/jobs_spec.rb b/spec/requests/api/ci/jobs_spec.rb index b6ab9310471..410020b68cd 100644 --- a/spec/requests/api/ci/jobs_spec.rb +++ b/spec/requests/api/ci/jobs_spec.rb @@ -176,6 +176,111 @@ RSpec.describe API::Ci::Jobs do end end + describe 'GET /job/allowed_agents' do + let_it_be(:group) { create(:group) } + let_it_be(:group_agent) { create(:cluster_agent, project: create(:project, group: group)) } + let_it_be(:group_authorization) { create(:agent_group_authorization, agent: group_agent, group: group) } + let_it_be(:project_agent) { create(:cluster_agent, project: project) } + + before(:all) do + project.update!(group: group_authorization.group) + end + + let(:implicit_authorization) { Clusters::Agents::ImplicitAuthorization.new(agent: project_agent) } + + let(:headers) { { API::Ci::Helpers::Runner::JOB_TOKEN_HEADER => job.token } } + let(:job) { create(:ci_build, :artifacts, pipeline: pipeline, user: api_user, status: job_status) } + let(:job_status) { 'running' } + let(:params) { {} } + + subject do + get api('/job/allowed_agents'), headers: headers, params: params + end + + before do + subject + end + + context 'when token is valid and user is authorized' do + shared_examples_for 'valid allowed_agents request' do + it 'returns agent info', :aggregate_failures do + expect(response).to have_gitlab_http_status(:ok) + + expect(json_response.dig('job', 'id')).to eq(job.id) + expect(json_response.dig('pipeline', 'id')).to eq(job.pipeline_id) + expect(json_response.dig('project', 'id')).to eq(job.project_id) + expect(json_response.dig('project', 'groups')).to match_array([{ 'id' => group_authorization.group.id }]) + expect(json_response.dig('user', 'id')).to eq(api_user.id) + expect(json_response.dig('user', 'username')).to eq(api_user.username) + expect(json_response.dig('user', 'roles_in_project')).to match_array %w(guest reporter developer) + expect(json_response).not_to include('environment') + expect(json_response['allowed_agents']).to match_array([ + { + 'id' => implicit_authorization.agent_id, + 'config_project' => hash_including('id' => implicit_authorization.agent.project_id), + 'configuration' => implicit_authorization.config + }, + { + 'id' => group_authorization.agent_id, + 'config_project' => hash_including('id' => group_authorization.agent.project_id), + 'configuration' => group_authorization.config + } + ]) + end + end + + it_behaves_like 'valid allowed_agents request' + + context 'when deployment' do + let(:job) { create(:ci_build, :artifacts, :with_deployment, environment: 'production', pipeline: pipeline, user: api_user, status: job_status) } + + it 'includes environment slug' do + expect(json_response.dig('environment', 'slug')).to eq('production') + end + end + + context 'when passing the token as params' do + let(:headers) { {} } + let(:params) { { job_token: job.token } } + + it_behaves_like 'valid allowed_agents request' + end + end + + context 'when user is anonymous' do + let(:api_user) { nil } + + it 'returns unauthorized' do + expect(response).to have_gitlab_http_status(:unauthorized) + end + end + + context 'when token is invalid because job has finished' do + let(:job_status) { 'success' } + + it 'returns unauthorized' do + expect(response).to have_gitlab_http_status(:unauthorized) + end + end + + context 'when token is invalid' do + let(:headers) { { API::Ci::Helpers::Runner::JOB_TOKEN_HEADER => 'bad_token' } } + + it 'returns unauthorized' do + expect(response).to have_gitlab_http_status(:unauthorized) + end + end + + context 'when token is valid but not CI_JOB_TOKEN' do + let(:token) { create(:personal_access_token, user: user) } + let(:headers) { { 'Private-Token' => token.token } } + + it 'returns not found' do + expect(response).to have_gitlab_http_status(:not_found) + end + end + end + describe 'GET /projects/:id/jobs' do let(:query) { {} } @@ -203,6 +308,7 @@ RSpec.describe API::Ci::Jobs do it 'returns no artifacts nor trace data' do json_job = json_response.first + expect(response).to have_gitlab_http_status(:ok) expect(json_job['artifacts_file']).to be_nil expect(json_job['artifacts']).to be_an Array expect(json_job['artifacts']).to be_empty @@ -321,6 +427,22 @@ RSpec.describe API::Ci::Jobs do expect(response).to have_gitlab_http_status(:unauthorized) end end + + context 'when trace artifact record exists with no stored file', :skip_before_request do + before do + create(:ci_job_artifact, :unarchived_trace_artifact, job: job, project: job.project) + end + + it 'returns no artifacts nor trace data' do + get api("/projects/#{project.id}/jobs/#{job.id}", api_user) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['artifacts']).to be_an Array + expect(json_response['artifacts'].size).to eq(1) + expect(json_response['artifacts'][0]['file_type']).to eq('trace') + expect(json_response['artifacts'][0]['filename']).to eq('job.log') + end + end end describe 'DELETE /projects/:id/jobs/:job_id/artifacts' do @@ -456,6 +578,7 @@ RSpec.describe API::Ci::Jobs do expect(response.headers.to_h) .to include('Content-Type' => 'application/json', 'Gitlab-Workhorse-Send-Data' => /artifacts-entry/) + expect(response.parsed_body).to be_empty end context 'when artifacts are locked' do @@ -826,6 +949,7 @@ RSpec.describe API::Ci::Jobs do expect(response.headers.to_h) .to include('Content-Type' => 'application/json', 'Gitlab-Workhorse-Send-Data' => /artifacts-entry/) + expect(response.parsed_body).to be_empty end end @@ -919,7 +1043,16 @@ RSpec.describe API::Ci::Jobs do end end - context 'when trace is file' do + context 'when live trace and uploadless trace artifact' do + let(:job) { create(:ci_build, :trace_live, :unarchived_trace_artifact, pipeline: pipeline) } + + it 'returns specific job trace' do + expect(response).to have_gitlab_http_status(:ok) + expect(response.body).to eq(job.trace.raw) + end + end + + context 'when trace is live' do let(:job) { create(:ci_build, :trace_live, pipeline: pipeline) } it 'returns specific job trace' do @@ -927,6 +1060,28 @@ RSpec.describe API::Ci::Jobs do expect(response.body).to eq(job.trace.raw) end end + + context 'when no trace' do + let(:job) { create(:ci_build, pipeline: pipeline) } + + it 'returns empty trace' do + expect(response).to have_gitlab_http_status(:ok) + expect(response.body).to be_empty + end + end + + context 'when trace artifact record exists with no stored file' do + let(:job) { create(:ci_build, pipeline: pipeline) } + + before do + create(:ci_job_artifact, :unarchived_trace_artifact, job: job, project: job.project) + end + + it 'returns empty trace' do + expect(response).to have_gitlab_http_status(:ok) + expect(response.body).to be_empty + end + end end context 'unauthorized user' do @@ -1038,9 +1193,7 @@ RSpec.describe API::Ci::Jobs do post api("/projects/#{project.id}/jobs/#{job.id}/erase", user) end - context 'job is erasable' do - let(:job) { create(:ci_build, :trace_artifact, :artifacts, :test_reports, :success, project: project, pipeline: pipeline) } - + shared_examples_for 'erases job' do it 'erases job content' do expect(response).to have_gitlab_http_status(:created) expect(job.job_artifacts.count).to eq(0) @@ -1049,6 +1202,12 @@ RSpec.describe API::Ci::Jobs do expect(job.artifacts_metadata.present?).to be_falsy expect(job.has_job_artifacts?).to be_falsy end + end + + context 'job is erasable' do + let(:job) { create(:ci_build, :trace_artifact, :artifacts, :test_reports, :success, project: project, pipeline: pipeline) } + + it_behaves_like 'erases job' it 'updates job' do job.reload @@ -1058,6 +1217,12 @@ RSpec.describe API::Ci::Jobs do end end + context 'when job has an unarchived trace artifact' do + let(:job) { create(:ci_build, :success, :trace_live, :unarchived_trace_artifact, project: project, pipeline: pipeline) } + + it_behaves_like 'erases job' + end + context 'job is not erasable' do let(:job) { create(:ci_build, :trace_live, project: project, pipeline: pipeline) } 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 c3fbef9be48..fdf1a278d4c 100644 --- a/spec/requests/api/ci/runner/jobs_request_post_spec.rb +++ b/spec/requests/api/ci/runner/jobs_request_post_spec.rb @@ -218,9 +218,11 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do expect(json_response['git_info']).to eq(expected_git_info) expect(json_response['image']).to eq({ 'name' => 'ruby:2.7', 'entrypoint' => '/bin/sh', 'ports' => [] }) expect(json_response['services']).to eq([{ 'name' => 'postgres', 'entrypoint' => nil, - 'alias' => nil, 'command' => nil, 'ports' => [] }, + 'alias' => nil, 'command' => nil, 'ports' => [], 'variables' => nil }, { 'name' => 'docker:stable-dind', 'entrypoint' => '/bin/sh', - 'alias' => 'docker', 'command' => 'sleep 30', 'ports' => [] }]) + 'alias' => 'docker', 'command' => 'sleep 30', 'ports' => [], 'variables' => [] }, + { 'name' => 'mysql:latest', 'entrypoint' => nil, + 'alias' => nil, 'command' => nil, 'ports' => [], 'variables' => [{ 'key' => 'MYSQL_ROOT_PASSWORD', 'value' => 'root123.' }] }]) expect(json_response['steps']).to eq(expected_steps) expect(json_response['artifacts']).to eq(expected_artifacts) expect(json_response['cache']).to eq(expected_cache) diff --git a/spec/requests/api/debian_group_packages_spec.rb b/spec/requests/api/debian_group_packages_spec.rb index 3e11b480860..d881d4350fb 100644 --- a/spec/requests/api/debian_group_packages_spec.rb +++ b/spec/requests/api/debian_group_packages_spec.rb @@ -9,35 +9,36 @@ RSpec.describe API::DebianGroupPackages do context 'with invalid parameter' do let(:url) { "/groups/1/-/packages/debian/dists/with+space/InRelease" } - it_behaves_like 'Debian repository GET request', :bad_request, /^distribution is invalid$/ + it_behaves_like 'Debian packages GET request', :bad_request, /^distribution is invalid$/ end describe 'GET groups/:id/-/packages/debian/dists/*distribution/Release.gpg' do let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution.codename}/Release.gpg" } - it_behaves_like 'Debian repository read endpoint', 'GET request', :success, /^-----BEGIN PGP SIGNATURE-----/ + it_behaves_like 'Debian packages read endpoint', 'GET', :success, /^-----BEGIN PGP SIGNATURE-----/ end describe 'GET groups/:id/-/packages/debian/dists/*distribution/Release' do let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution.codename}/Release" } - it_behaves_like 'Debian repository read endpoint', 'GET request', :success, /^Codename: fixture-distribution\n$/ + it_behaves_like 'Debian packages read endpoint', 'GET', :success, /^Codename: fixture-distribution\n$/ end describe 'GET groups/:id/-/packages/debian/dists/*distribution/InRelease' do let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution.codename}/InRelease" } - it_behaves_like 'Debian repository read endpoint', 'GET request', :success, /^-----BEGIN PGP SIGNED MESSAGE-----/ + it_behaves_like 'Debian packages read endpoint', 'GET', :success, /^-----BEGIN PGP SIGNED MESSAGE-----/ end describe 'GET groups/:id/-/packages/debian/dists/*distribution/:component/binary-:architecture/Packages' do let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution.codename}/#{component.name}/binary-#{architecture.name}/Packages" } - it_behaves_like 'Debian repository read endpoint', 'GET request', :success, /Description: This is an incomplete Packages file/ + it_behaves_like 'Debian packages read endpoint', 'GET', :success, /Description: This is an incomplete Packages file/ end describe 'GET groups/:id/-/packages/debian/pool/:codename/:project_id/:letter/:package_name/:package_version/:file_name' do let(:url) { "/groups/#{container.id}/-/packages/debian/pool/#{package.debian_distribution.codename}/#{project.id}/#{letter}/#{package.name}/#{package.version}/#{file_name}" } + let(:file_name) { params[:file_name] } using RSpec::Parameterized::TableSyntax @@ -51,9 +52,7 @@ RSpec.describe API::DebianGroupPackages do end with_them do - include_context 'with file_name', params[:file_name] - - it_behaves_like 'Debian repository read endpoint', 'GET request', :success, params[:success_body] + it_behaves_like 'Debian packages read endpoint', 'GET', :success, params[:success_body] end end end diff --git a/spec/requests/api/debian_project_packages_spec.rb b/spec/requests/api/debian_project_packages_spec.rb index d0b0debaf13..bd68bf912e1 100644 --- a/spec/requests/api/debian_project_packages_spec.rb +++ b/spec/requests/api/debian_project_packages_spec.rb @@ -9,35 +9,36 @@ RSpec.describe API::DebianProjectPackages do context 'with invalid parameter' do let(:url) { "/projects/1/packages/debian/dists/with+space/InRelease" } - it_behaves_like 'Debian repository GET request', :bad_request, /^distribution is invalid$/ + it_behaves_like 'Debian packages GET request', :bad_request, /^distribution is invalid$/ end describe 'GET projects/:id/packages/debian/dists/*distribution/Release.gpg' do let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/Release.gpg" } - it_behaves_like 'Debian repository read endpoint', 'GET request', :success, /^-----BEGIN PGP SIGNATURE-----/ + it_behaves_like 'Debian packages read endpoint', 'GET', :success, /^-----BEGIN PGP SIGNATURE-----/ end describe 'GET projects/:id/packages/debian/dists/*distribution/Release' do let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/Release" } - it_behaves_like 'Debian repository read endpoint', 'GET request', :success, /^Codename: fixture-distribution\n$/ + it_behaves_like 'Debian packages read endpoint', 'GET', :success, /^Codename: fixture-distribution\n$/ end describe 'GET projects/:id/packages/debian/dists/*distribution/InRelease' do let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/InRelease" } - it_behaves_like 'Debian repository read endpoint', 'GET request', :success, /^-----BEGIN PGP SIGNED MESSAGE-----/ + it_behaves_like 'Debian packages read endpoint', 'GET', :success, /^-----BEGIN PGP SIGNED MESSAGE-----/ end describe 'GET projects/:id/packages/debian/dists/*distribution/:component/binary-:architecture/Packages' do let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/#{component.name}/binary-#{architecture.name}/Packages" } - it_behaves_like 'Debian repository read endpoint', 'GET request', :success, /Description: This is an incomplete Packages file/ + it_behaves_like 'Debian packages read endpoint', 'GET', :success, /Description: This is an incomplete Packages file/ end describe 'GET projects/:id/packages/debian/pool/:codename/:letter/:package_name/:package_version/:file_name' do let(:url) { "/projects/#{container.id}/packages/debian/pool/#{package.debian_distribution.codename}/#{letter}/#{package.name}/#{package.version}/#{file_name}" } + let(:file_name) { params[:file_name] } using RSpec::Parameterized::TableSyntax @@ -51,9 +52,7 @@ RSpec.describe API::DebianProjectPackages do end with_them do - include_context 'with file_name', params[:file_name] - - it_behaves_like 'Debian repository read endpoint', 'GET request', :success, params[:success_body] + it_behaves_like 'Debian packages read endpoint', 'GET', :success, params[:success_body] end end @@ -65,13 +64,13 @@ RSpec.describe API::DebianProjectPackages do context 'with a deb' do let(:file_name) { 'libsample0_1.2.3~alpha2_amd64.deb' } - it_behaves_like 'Debian repository write endpoint', 'upload request', :created + it_behaves_like 'Debian packages write endpoint', 'upload', :created, nil end context 'with a changes file' do let(:file_name) { 'sample_1.2.3~alpha2_amd64.changes' } - it_behaves_like 'Debian repository write endpoint', 'upload request', :created + it_behaves_like 'Debian packages write endpoint', 'upload', :created, nil end end @@ -80,7 +79,7 @@ RSpec.describe API::DebianProjectPackages do let(:method) { :put } let(:url) { "/projects/#{container.id}/packages/debian/#{file_name}/authorize" } - it_behaves_like 'Debian repository write endpoint', 'upload authorize request', :created + it_behaves_like 'Debian packages write endpoint', 'upload authorize', :created, nil end end end diff --git a/spec/requests/api/deploy_keys_spec.rb b/spec/requests/api/deploy_keys_spec.rb index a01c66a311c..1daa7c38e04 100644 --- a/spec/requests/api/deploy_keys_spec.rb +++ b/spec/requests/api/deploy_keys_spec.rb @@ -8,8 +8,9 @@ RSpec.describe API::DeployKeys do let_it_be(:admin) { create(:admin) } let_it_be(:project) { create(:project, creator_id: user.id) } let_it_be(:project2) { create(:project, creator_id: user.id) } - - let(:deploy_key) { create(:deploy_key, public: true) } + let_it_be(:project3) { create(:project, creator_id: user.id) } + let_it_be(:deploy_key) { create(:deploy_key, public: true) } + let_it_be(:deploy_key_private) { create(:deploy_key, public: false) } let!(:deploy_keys_project) do create(:deploy_keys_project, project: project, deploy_key: deploy_key) @@ -33,13 +34,56 @@ RSpec.describe API::DeployKeys do end context 'when authenticated as admin' do + let_it_be(:pat) { create(:personal_access_token, user: admin) } + + def make_api_request(params = {}) + get api('/deploy_keys', personal_access_token: pat), params: params + end + it 'returns all deploy keys' do - get api('/deploy_keys', admin) + make_api_request expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers + expect(response).to match_response_schema('public_api/v4/deploy_keys') expect(json_response).to be_an Array - expect(json_response.first['id']).to eq(deploy_keys_project.deploy_key.id) + + expect(json_response[0]['id']).to eq(deploy_key.id) + expect(json_response[1]['id']).to eq(deploy_key_private.id) + end + + it 'avoids N+1 database queries', :use_sql_query_cache, :request_store do + create(:deploy_keys_project, :write_access, project: project2, deploy_key: deploy_key) + + control = ActiveRecord::QueryRecorder.new(skip_cached: false) { make_api_request } + + deploy_key2 = create(:deploy_key, public: true) + create(:deploy_keys_project, :write_access, project: project3, deploy_key: deploy_key2) + + expect { make_api_request }.not_to exceed_all_query_limit(control) + end + + context 'when `public` parameter is `true`' do + it 'only returns public deploy keys' do + make_api_request({ public: true }) + + expect(json_response.length).to eq(1) + expect(json_response.first['id']).to eq(deploy_key.id) + end + end + + context 'projects_with_write_access' do + let!(:deploy_keys_project2) { create(:deploy_keys_project, :write_access, project: project2, deploy_key: deploy_key) } + let!(:deploy_keys_project3) { create(:deploy_keys_project, :write_access, project: project3, deploy_key: deploy_key) } + + it 'returns projects with write access' do + make_api_request + + response_projects_with_write_access = json_response.first['projects_with_write_access'] + + expect(response_projects_with_write_access[0]['id']).to eq(project2.id) + expect(response_projects_with_write_access[1]['id']).to eq(project3.id) + end end end end @@ -58,6 +102,7 @@ RSpec.describe API::DeployKeys do expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.first['title']).to eq(deploy_key.title) + expect(json_response.first).not_to have_key(:projects_with_write_access) end it 'returns multiple deploy keys without N + 1' do @@ -77,6 +122,7 @@ RSpec.describe API::DeployKeys do expect(response).to have_gitlab_http_status(:ok) expect(json_response['title']).to eq(deploy_key.title) + expect(json_response).not_to have_key(:projects_with_write_access) end it 'returns 404 Not Found with invalid ID' do diff --git a/spec/requests/api/error_tracking/collector_spec.rb b/spec/requests/api/error_tracking/collector_spec.rb index 7acadeb1287..21e2849fef0 100644 --- a/spec/requests/api/error_tracking/collector_spec.rb +++ b/spec/requests/api/error_tracking/collector_spec.rb @@ -24,10 +24,10 @@ RSpec.describe API::ErrorTracking::Collector do end RSpec.shared_examples 'successful request' do - it 'writes to the database and returns no content' do + it 'writes to the database and returns OK' do expect { subject }.to change { ErrorTracking::ErrorEvent.count }.by(1) - expect(response).to have_gitlab_http_status(:no_content) + expect(response).to have_gitlab_http_status(:ok) end end @@ -89,13 +89,27 @@ RSpec.describe API::ErrorTracking::Collector do context 'transaction request type' do let(:params) { fixture_file('error_tracking/transaction.txt') } - it 'does nothing and returns no content' do + it 'does nothing and returns ok' do expect { subject }.not_to change { ErrorTracking::ErrorEvent.count } - expect(response).to have_gitlab_http_status(:no_content) + expect(response).to have_gitlab_http_status(:ok) end end + context 'gzip body' do + let(:headers) do + { + 'X-Sentry-Auth' => "Sentry sentry_key=#{client_key.public_key}", + 'HTTP_CONTENT_ENCODING' => 'gzip', + 'CONTENT_TYPE' => 'application/x-sentry-envelope' + } + end + + let(:params) { ActiveSupport::Gzip.compress(raw_event) } + + it_behaves_like 'successful request' + end + it_behaves_like 'successful request' end @@ -122,6 +136,35 @@ RSpec.describe API::ErrorTracking::Collector do it_behaves_like 'bad request' end + context 'body with string instead of json' do + let(:params) { '"********"' } + + it_behaves_like 'bad request' + end + + context 'collector fails with validation error' do + before do + allow(::ErrorTracking::CollectErrorService) + .to receive(:new).and_raise(ActiveRecord::RecordInvalid) + end + + it_behaves_like 'bad request' + end + + context 'gzip body' do + let(:headers) do + { + 'X-Sentry-Auth' => "Sentry sentry_key=#{client_key.public_key}", + 'HTTP_CONTENT_ENCODING' => 'gzip', + 'CONTENT_TYPE' => 'application/json' + } + end + + let(:params) { ActiveSupport::Gzip.compress(raw_event) } + + 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/features_spec.rb b/spec/requests/api/features_spec.rb index 0e163ec2154..35dba93b766 100644 --- a/spec/requests/api/features_spec.rb +++ b/spec/requests/api/features_spec.rb @@ -256,6 +256,21 @@ RSpec.describe API::Features, stub_feature_flags: false do ) end + it 'creates a feature with the given percentage of time if passed a float' do + post api("/features/#{feature_name}", admin), params: { value: '0.01' } + + expect(response).to have_gitlab_http_status(:created) + expect(json_response).to match( + 'name' => feature_name, + 'state' => 'conditional', + 'gates' => [ + { 'key' => 'boolean', 'value' => false }, + { 'key' => 'percentage_of_time', 'value' => 0.01 } + ], + 'definition' => known_feature_flag_definition_hash + ) + end + it 'creates a feature with the given percentage of actors if passed an integer' do post api("/features/#{feature_name}", admin), params: { value: '50', key: 'percentage_of_actors' } @@ -270,6 +285,21 @@ RSpec.describe API::Features, stub_feature_flags: false do 'definition' => known_feature_flag_definition_hash ) end + + it 'creates a feature with the given percentage of actors if passed a float' do + post api("/features/#{feature_name}", admin), params: { value: '0.01', key: 'percentage_of_actors' } + + expect(response).to have_gitlab_http_status(:created) + expect(json_response).to match( + 'name' => feature_name, + 'state' => 'conditional', + 'gates' => [ + { 'key' => 'boolean', 'value' => false }, + { 'key' => 'percentage_of_actors', 'value' => 0.01 } + ], + 'definition' => known_feature_flag_definition_hash + ) + end end context 'when the feature exists' do diff --git a/spec/requests/api/files_spec.rb b/spec/requests/api/files_spec.rb index 0b898496dd6..6aa12b6ff48 100644 --- a/spec/requests/api/files_spec.rb +++ b/spec/requests/api/files_spec.rb @@ -47,6 +47,15 @@ RSpec.describe API::Files do "/projects/#{project.id}/repository/files/#{file_path}" end + def expect_to_send_git_blob(url, params) + expect(Gitlab::Workhorse).to receive(:send_git_blob) + + get url, params: params + + expect(response).to have_gitlab_http_status(:ok) + expect(response.parsed_body).to be_empty + end + context 'http headers' do it 'converts value into string' do helper.set_http_headers(test: 1) @@ -257,11 +266,7 @@ RSpec.describe API::Files do it 'returns raw file info' do url = route(file_path) + "/raw" - expect(Gitlab::Workhorse).to receive(:send_git_blob) - - get api(url, api_user, **options), params: params - - expect(response).to have_gitlab_http_status(:ok) + expect_to_send_git_blob(api(url, api_user, **options), params) expect(headers[Gitlab::Workhorse::DETECT_HEADER]).to eq "true" end @@ -523,11 +528,8 @@ RSpec.describe API::Files do it 'returns raw file info' do url = route(file_path) + "/raw" - expect(Gitlab::Workhorse).to receive(:send_git_blob) - get api(url, current_user), params: params - - expect(response).to have_gitlab_http_status(:ok) + expect_to_send_git_blob(api(url, current_user), params) end context 'when ref is not provided' do @@ -537,39 +539,29 @@ RSpec.describe API::Files do it 'returns response :ok', :aggregate_failures do url = route(file_path) + "/raw" - expect(Gitlab::Workhorse).to receive(:send_git_blob) - get api(url, current_user), params: {} - - expect(response).to have_gitlab_http_status(:ok) + expect_to_send_git_blob(api(url, current_user), {}) end end it 'returns raw file info for files with dots' do url = route('.gitignore') + "/raw" - expect(Gitlab::Workhorse).to receive(:send_git_blob) - get api(url, current_user), params: params - - expect(response).to have_gitlab_http_status(:ok) + expect_to_send_git_blob(api(url, current_user), params) end it 'returns file by commit sha' do # This file is deleted on HEAD file_path = "files%2Fjs%2Fcommit%2Ejs%2Ecoffee" params[:ref] = "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9" - expect(Gitlab::Workhorse).to receive(:send_git_blob) - get api(route(file_path) + "/raw", current_user), params: params - - expect(response).to have_gitlab_http_status(:ok) + expect_to_send_git_blob(api(route(file_path) + "/raw", current_user), params) end it 'sets no-cache headers' do url = route('.gitignore') + "/raw" - expect(Gitlab::Workhorse).to receive(:send_git_blob) - get api(url, current_user), params: params + expect_to_send_git_blob(api(url, current_user), params) expect(response.headers["Cache-Control"]).to eq("max-age=0, private, must-revalidate, no-store, no-cache") expect(response.headers["Pragma"]).to eq("no-cache") @@ -633,11 +625,9 @@ RSpec.describe API::Files do # This file is deleted on HEAD file_path = "files%2Fjs%2Fcommit%2Ejs%2Ecoffee" params[:ref] = "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9" - expect(Gitlab::Workhorse).to receive(:send_git_blob) + url = api(route(file_path) + "/raw", personal_access_token: token) - get api(route(file_path) + "/raw", personal_access_token: token), params: params - - expect(response).to have_gitlab_http_status(:ok) + expect_to_send_git_blob(url, params) end end end diff --git a/spec/requests/api/generic_packages_spec.rb b/spec/requests/api/generic_packages_spec.rb index 7e439a22e4b..2d85d7b9583 100644 --- a/spec/requests/api/generic_packages_spec.rb +++ b/spec/requests/api/generic_packages_spec.rb @@ -297,6 +297,37 @@ RSpec.describe API::GenericPackages do end end + context 'with select' do + context 'with a valid value' do + context 'package_file' do + let(:params) { super().merge(select: 'package_file') } + + it 'returns a package file' do + headers = workhorse_headers.merge(auth_header) + + upload_file(params, headers) + + aggregate_failures do + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to have_key('id') + end + end + end + end + + context 'with an invalid value' do + let(:params) { super().merge(select: 'invalid_value') } + + it 'returns a package file' do + headers = workhorse_headers.merge(auth_header) + + upload_file(params, headers) + + expect(response).to have_gitlab_http_status(:bad_request) + end + end + end + context 'with a status' do context 'valid status' do let(:params) { super().merge(status: 'hidden') } diff --git a/spec/requests/api/graphql/ci/pipelines_spec.rb b/spec/requests/api/graphql/ci/pipelines_spec.rb index 6587061094d..1f47f678898 100644 --- a/spec/requests/api/graphql/ci/pipelines_spec.rb +++ b/spec/requests/api/graphql/ci/pipelines_spec.rb @@ -186,6 +186,69 @@ RSpec.describe 'Query.project(fullPath).pipelines' do end end + describe '.job_artifacts' do + let_it_be(:pipeline) { create(:ci_pipeline, project: project) } + let_it_be(:pipeline_job_1) { create(:ci_build, pipeline: pipeline, name: 'Job 1') } + let_it_be(:pipeline_job_artifact_1) { create(:ci_job_artifact, job: pipeline_job_1) } + let_it_be(:pipeline_job_2) { create(:ci_build, pipeline: pipeline, name: 'Job 2') } + let_it_be(:pipeline_job_artifact_2) { create(:ci_job_artifact, job: pipeline_job_2) } + + let(:path) { %i[project pipelines nodes jobArtifacts] } + + let(:query) do + %( + query { + project(fullPath: "#{project.full_path}") { + pipelines { + nodes { + jobArtifacts { + name + downloadPath + fileType + } + } + } + } + } + ) + end + + before do + post_graphql(query, current_user: user) + end + + it_behaves_like 'a working graphql query' + + it 'returns the job_artifacts of a pipeline' do + job_artifacts_graphql_data = graphql_data_at(*path).flatten + + expect( + job_artifacts_graphql_data.map { |pip| pip['name'] } + ).to contain_exactly(pipeline_job_artifact_1.filename, pipeline_job_artifact_2.filename) + end + + it 'avoids N+1 queries' do + first_user = create(:user) + second_user = create(:user) + + control_count = ActiveRecord::QueryRecorder.new do + post_graphql(query, current_user: first_user) + end + + pipeline_2 = create(:ci_pipeline, project: project) + pipeline_2_job_1 = create(:ci_build, pipeline: pipeline_2, name: 'Pipeline 2 Job 1') + create(:ci_job_artifact, job: pipeline_2_job_1) + pipeline_2_job_2 = create(:ci_build, pipeline: pipeline_2, name: 'Pipeline 2 Job 2') + create(:ci_job_artifact, job: pipeline_2_job_2) + + expect do + post_graphql(query, current_user: second_user) + end.not_to exceed_query_limit(control_count) + + expect(response).to have_gitlab_http_status(:ok) + end + end + describe '.jobs(securityReportTypes)' do let_it_be(:query) do %( diff --git a/spec/requests/api/graphql/gitlab_schema_spec.rb b/spec/requests/api/graphql/gitlab_schema_spec.rb index b41d851439b..8bbeae97f57 100644 --- a/spec/requests/api/graphql/gitlab_schema_spec.rb +++ b/spec/requests/api/graphql/gitlab_schema_spec.rb @@ -190,19 +190,18 @@ RSpec.describe 'GitlabSchema configurations' do let(:query) { File.read(Rails.root.join('spec/fixtures/api/graphql/introspection.graphql')) } it 'logs the query complexity and depth' do - analyzer_memo = { - query_string: query, - variables: {}.to_s, - complexity: 181, - depth: 13, - duration_s: 7, - operation_name: 'IntrospectionQuery', - used_fields: an_instance_of(Array), - used_deprecated_fields: an_instance_of(Array) - } - expect_any_instance_of(Gitlab::Graphql::QueryAnalyzers::LoggerAnalyzer).to receive(:duration).and_return(7) - expect(Gitlab::GraphqlLogger).to receive(:info).with(analyzer_memo) + + expect(Gitlab::GraphqlLogger).to receive(:info).with( + hash_including( + trace_type: 'execute_query', + "query_analysis.duration_s" => 7, + "query_analysis.complexity" => 181, + "query_analysis.depth" => 13, + "query_analysis.used_deprecated_fields" => an_instance_of(Array), + "query_analysis.used_fields" => an_instance_of(Array) + ) + ) post_graphql(query, current_user: nil) end diff --git a/spec/requests/api/graphql/group/dependency_proxy_manifests_spec.rb b/spec/requests/api/graphql/group/dependency_proxy_manifests_spec.rb index 30e704adb92..3527c8183f6 100644 --- a/spec/requests/api/graphql/group/dependency_proxy_manifests_spec.rb +++ b/spec/requests/api/graphql/group/dependency_proxy_manifests_spec.rb @@ -116,4 +116,26 @@ RSpec.describe 'getting dependency proxy manifests in a group' do expect(dependency_proxy_image_count_response).to eq(manifests.size) end + + describe 'sorting and pagination' do + let(:data_path) { ['group', :dependencyProxyManifests] } + let(:current_user) { owner } + + context 'with default sorting' do + let_it_be(:descending_manifests) { manifests.reverse.map { |manifest| global_id_of(manifest)} } + + it_behaves_like 'sorted paginated query' do + let(:sort_param) { '' } + let(:first_param) { 2 } + let(:all_records) { descending_manifests } + end + end + + def pagination_query(params) + # remove sort since the type does not accept sorting, but be future proof + graphql_query_for('group', { 'fullPath' => group.full_path }, + query_nodes(:dependencyProxyManifests, :id, include_pagination_info: true, args: params.merge(sort: nil)) + ) + end + end end diff --git a/spec/requests/api/graphql/mutations/ci/runners_registration_token/reset_spec.rb b/spec/requests/api/graphql/mutations/ci/runners_registration_token/reset_spec.rb index 0fd8fdc3f59..322706be119 100644 --- a/spec/requests/api/graphql/mutations/ci/runners_registration_token/reset_spec.rb +++ b/spec/requests/api/graphql/mutations/ci/runners_registration_token/reset_spec.rb @@ -15,7 +15,7 @@ RSpec.describe 'RunnersRegistrationTokenReset' do subject expect(graphql_errors).not_to be_empty - expect(graphql_errors).to include(a_hash_including('message' => "The resource that you are attempting to access does not exist or you don't have permission to perform this action")) + expect(graphql_errors).to include(a_hash_including('message' => Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR)) expect(mutation_response).to be_nil end end diff --git a/spec/requests/api/graphql/mutations/design_management/delete_spec.rb b/spec/requests/api/graphql/mutations/design_management/delete_spec.rb index e329416faee..1dffb86b344 100644 --- a/spec/requests/api/graphql/mutations/design_management/delete_spec.rb +++ b/spec/requests/api/graphql/mutations/design_management/delete_spec.rb @@ -53,7 +53,7 @@ RSpec.describe "deleting designs" do context 'the designs list contains filenames we cannot find' do it_behaves_like 'a failed request' do - let(:designs) { %w/foo bar baz/.map { |fn| OpenStruct.new(filename: fn) } } + let(:designs) { %w/foo bar baz/.map { |fn| instance_double('file', filename: fn) } } let(:the_error) { a_string_matching %r/filenames were not found/ } end end diff --git a/spec/requests/api/graphql/mutations/issues/create_spec.rb b/spec/requests/api/graphql/mutations/issues/create_spec.rb index 886f3140086..6baed352b37 100644 --- a/spec/requests/api/graphql/mutations/issues/create_spec.rb +++ b/spec/requests/api/graphql/mutations/issues/create_spec.rb @@ -48,5 +48,9 @@ RSpec.describe 'Create an issue' do expect(mutation_response['issue']).to include('discussionLocked' => true) expect(Issue.last.work_item_type.base_type).to eq('issue') end + + it_behaves_like 'has spam protection' do + let(:mutation_class) { ::Mutations::Issues::Create } + end end end diff --git a/spec/requests/api/graphql/mutations/issues/move_spec.rb b/spec/requests/api/graphql/mutations/issues/move_spec.rb index 5bbaff61edd..20ed16879f6 100644 --- a/spec/requests/api/graphql/mutations/issues/move_spec.rb +++ b/spec/requests/api/graphql/mutations/issues/move_spec.rb @@ -33,7 +33,7 @@ RSpec.describe 'Moving an issue' do context 'when the user is not allowed to read source project' do it 'returns an error' do - error = "The resource that you are attempting to access does not exist or you don't have permission to perform this action" + error = Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR post_graphql_mutation(mutation, current_user: user) expect(response).to have_gitlab_http_status(:success) diff --git a/spec/requests/api/graphql/mutations/issues/set_confidential_spec.rb b/spec/requests/api/graphql/mutations/issues/set_confidential_spec.rb index 3f804a46992..12ab504da14 100644 --- a/spec/requests/api/graphql/mutations/issues/set_confidential_spec.rb +++ b/spec/requests/api/graphql/mutations/issues/set_confidential_spec.rb @@ -36,7 +36,7 @@ RSpec.describe 'Setting an issue as confidential' do end it 'returns an error if the user is not allowed to update the issue' do - error = "The resource that you are attempting to access does not exist or you don't have permission to perform this action" + error = Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR post_graphql_mutation(mutation, current_user: create(:user)) expect(graphql_errors).to include(a_hash_including('message' => error)) 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 new file mode 100644 index 00000000000..3da702c55d7 --- /dev/null +++ b/spec/requests/api/graphql/mutations/issues/set_crm_contacts_spec.rb @@ -0,0 +1,161 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Setting issues crm contacts' do + include GraphqlHelpers + + let_it_be(:user) { create(:user) } + let_it_be(:group) { create(:group) } + let_it_be(:project) { create(:project, group: group) } + let_it_be(:contacts) { create_list(:contact, 4, group: group) } + + let(:issue) { create(:issue, project: project) } + let(:operation_mode) { Types::MutationOperationModeEnum.default_mode } + let(:crm_contact_ids) { [global_id_of(contacts[1]), global_id_of(contacts[2])] } + let(:does_not_exist_or_no_permission) { "The resource that you are attempting to access does not exist or you don't have permission to perform this action" } + + let(:mutation) do + variables = { + project_path: issue.project.full_path, + iid: issue.iid.to_s, + operation_mode: operation_mode, + crm_contact_ids: crm_contact_ids + } + + graphql_mutation(:issue_set_crm_contacts, variables, + <<-QL.strip_heredoc + clientMutationId + errors + issue { + customerRelationsContacts { + nodes { + id + } + } + } + QL + ) + end + + def mutation_response + graphql_mutation_response(:issue_set_crm_contacts) + end + + before do + create(:issue_customer_relations_contact, issue: issue, contact: contacts[0]) + create(:issue_customer_relations_contact, issue: issue, contact: contacts[1]) + end + + context 'when the user has no permission' do + it 'returns expected error' do + error = Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR + post_graphql_mutation(mutation, current_user: user) + + expect(graphql_errors).to include(a_hash_including('message' => error)) + end + end + + context 'when the user has permission' do + before do + group.add_reporter(user) + end + + context 'when the feature is disabled' do + before do + stub_feature_flags(customer_relations: false) + end + + it 'raises expected error' do + post_graphql_mutation(mutation, current_user: user) + + expect(graphql_errors).to include(a_hash_including('message' => 'Feature disabled')) + end + end + + context 'replace' do + it 'updates the issue with correct contacts' do + post_graphql_mutation(mutation, current_user: user) + + expect(graphql_data_at(:issue_set_crm_contacts, :issue, :customer_relations_contacts, :nodes, :id)) + .to match_array([global_id_of(contacts[1]), global_id_of(contacts[2])]) + end + end + + context 'append' do + let(:crm_contact_ids) { [global_id_of(contacts[3])] } + let(:operation_mode) { Types::MutationOperationModeEnum.enum[:append] } + + it 'updates the issue with correct contacts' do + post_graphql_mutation(mutation, current_user: user) + + expect(graphql_data_at(:issue_set_crm_contacts, :issue, :customer_relations_contacts, :nodes, :id)) + .to match_array([global_id_of(contacts[0]), global_id_of(contacts[1]), global_id_of(contacts[3])]) + end + end + + context 'remove' do + let(:crm_contact_ids) { [global_id_of(contacts[0])] } + let(:operation_mode) { Types::MutationOperationModeEnum.enum[:remove] } + + it 'updates the issue with correct contacts' do + post_graphql_mutation(mutation, current_user: user) + + expect(graphql_data_at(:issue_set_crm_contacts, :issue, :customer_relations_contacts, :nodes, :id)) + .to match_array([global_id_of(contacts[1])]) + end + end + + context 'when the contact does not exist' do + let(:crm_contact_ids) { ["gid://gitlab/CustomerRelations::Contact/#{non_existing_record_id}"] } + + it 'returns expected error' do + post_graphql_mutation(mutation, current_user: user) + + expect(graphql_data_at(:issue_set_crm_contacts, :errors)) + .to match_array(["Issue customer relations contacts #{non_existing_record_id}: #{does_not_exist_or_no_permission}"]) + end + end + + context 'when the contact belongs to a different group' do + let(:group2) { create(:group) } + let(:contact) { create(:contact, group: group2) } + let(:crm_contact_ids) { [global_id_of(contact)] } + + before do + group2.add_reporter(user) + end + + it 'returns expected error' do + post_graphql_mutation(mutation, current_user: user) + + expect(graphql_data_at(:issue_set_crm_contacts, :errors)) + .to match_array(["Issue customer relations contacts #{contact.id}: #{does_not_exist_or_no_permission}"]) + end + end + + context 'when attempting to add more than 6' do + let(:operation_mode) { Types::MutationOperationModeEnum.enum[:append] } + let(:gid) { global_id_of(contacts[0]) } + let(:crm_contact_ids) { [gid, gid, gid, gid, gid, gid, gid] } + + it 'returns expected error' do + post_graphql_mutation(mutation, current_user: user) + + expect(graphql_data_at(:issue_set_crm_contacts, :errors)) + .to match_array(["You can only add up to 6 contacts at one time"]) + end + end + + context 'when trying to remove non-existent contact' do + let(:operation_mode) { Types::MutationOperationModeEnum.enum[:remove] } + let(:crm_contact_ids) { ["gid://gitlab/CustomerRelations::Contact/#{non_existing_record_id}"] } + + it 'raises expected error' do + post_graphql_mutation(mutation, current_user: user) + + expect(graphql_data_at(:issue_set_crm_contacts, :errors)).to be_empty + end + end + end +end diff --git a/spec/requests/api/graphql/mutations/issues/set_due_date_spec.rb b/spec/requests/api/graphql/mutations/issues/set_due_date_spec.rb index 72e47a98373..8e223b6fdaf 100644 --- a/spec/requests/api/graphql/mutations/issues/set_due_date_spec.rb +++ b/spec/requests/api/graphql/mutations/issues/set_due_date_spec.rb @@ -36,7 +36,7 @@ RSpec.describe 'Setting Due Date of an issue' do end it 'returns an error if the user is not allowed to update the issue' do - error = "The resource that you are attempting to access does not exist or you don't have permission to perform this action" + error = Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR post_graphql_mutation(mutation, current_user: create(:user)) expect(graphql_errors).to include(a_hash_including('message' => error)) diff --git a/spec/requests/api/graphql/mutations/issues/set_severity_spec.rb b/spec/requests/api/graphql/mutations/issues/set_severity_spec.rb index 41997f151a2..cd9d695bd2c 100644 --- a/spec/requests/api/graphql/mutations/issues/set_severity_spec.rb +++ b/spec/requests/api/graphql/mutations/issues/set_severity_spec.rb @@ -35,7 +35,7 @@ RSpec.describe 'Setting severity level of an incident' do context 'when the user is not allowed to update the incident' do it 'returns an error' do - error = "The resource that you are attempting to access does not exist or you don't have permission to perform this action" + error = Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR post_graphql_mutation(mutation, current_user: user) expect(response).to have_gitlab_http_status(:success) diff --git a/spec/requests/api/graphql/mutations/merge_requests/set_wip_spec.rb b/spec/requests/api/graphql/mutations/merge_requests/set_draft_spec.rb index 2143abd3031..bea2365eaa6 100644 --- a/spec/requests/api/graphql/mutations/merge_requests/set_wip_spec.rb +++ b/spec/requests/api/graphql/mutations/merge_requests/set_draft_spec.rb @@ -8,14 +8,14 @@ RSpec.describe 'Setting Draft status of a merge request' do let(:current_user) { create(:user) } let(:merge_request) { create(:merge_request) } let(:project) { merge_request.project } - let(:input) { { wip: true } } + let(:input) { { draft: true } } let(:mutation) do variables = { project_path: project.full_path, iid: merge_request.iid.to_s } - graphql_mutation(:merge_request_set_wip, variables.merge(input), + graphql_mutation(:merge_request_set_draft, variables.merge(input), <<-QL.strip_heredoc clientMutationId errors @@ -28,7 +28,7 @@ RSpec.describe 'Setting Draft status of a merge request' do end def mutation_response - graphql_mutation_response(:merge_request_set_wip) + graphql_mutation_response(:merge_request_set_draft) end before do @@ -58,7 +58,7 @@ RSpec.describe 'Setting Draft status of a merge request' do end context 'when passing Draft false as input' do - let(:input) { { wip: false } } + let(:input) { { draft: false } } it 'does not do anything if the merge reqeust was not marked draft' do post_graphql_mutation(mutation, current_user: current_user) diff --git a/spec/requests/api/graphql/mutations/merge_requests/update_reviewer_state_spec.rb b/spec/requests/api/graphql/mutations/merge_requests/update_reviewer_state_spec.rb new file mode 100644 index 00000000000..cf497cb2579 --- /dev/null +++ b/spec/requests/api/graphql/mutations/merge_requests/update_reviewer_state_spec.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Toggle attention requested for reviewer' do + include GraphqlHelpers + + let(:current_user) { create(:user) } + let(:merge_request) { create(:merge_request, reviewers: [user]) } + let(:project) { merge_request.project } + let(:user) { create(:user) } + let(:input) { { user_id: global_id_of(user) } } + + let(:mutation) do + variables = { + project_path: project.full_path, + iid: merge_request.iid.to_s + } + graphql_mutation(:merge_request_toggle_attention_requested, variables.merge(input), + <<-QL.strip_heredoc + clientMutationId + errors + QL + ) + end + + def mutation_response + graphql_mutation_response(:merge_request_toggle_attention_requested) + end + + def mutation_errors + mutation_response['errors'] + end + + before do + project.add_developer(current_user) + project.add_developer(user) + end + + it 'returns an error if the user is not allowed to update the merge request' do + post_graphql_mutation(mutation, current_user: create(:user)) + + expect(graphql_errors).not_to be_empty + end + + describe 'reviewer does not exist' do + let(:input) { { user_id: global_id_of(create(:user)) } } + + it 'returns an error' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(response).to have_gitlab_http_status(:success) + expect(mutation_errors).not_to be_empty + end + end + + describe 'reviewer exists' do + it 'does not return an error' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(response).to have_gitlab_http_status(:success) + expect(mutation_errors).to be_empty + end + end +end diff --git a/spec/requests/api/graphql/mutations/releases/create_spec.rb b/spec/requests/api/graphql/mutations/releases/create_spec.rb index a4918cd560c..86995c10f10 100644 --- a/spec/requests/api/graphql/mutations/releases/create_spec.rb +++ b/spec/requests/api/graphql/mutations/releases/create_spec.rb @@ -342,7 +342,7 @@ RSpec.describe 'Creation of a new release' do end context "when the current user doesn't have access to create releases" do - expected_error_message = "The resource that you are attempting to access does not exist or you don't have permission to perform this action" + expected_error_message = Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR context 'when the current user is a Reporter' do let(:current_user) { reporter } diff --git a/spec/requests/api/graphql/mutations/releases/delete_spec.rb b/spec/requests/api/graphql/mutations/releases/delete_spec.rb index 40063156609..eb4f0b594ea 100644 --- a/spec/requests/api/graphql/mutations/releases/delete_spec.rb +++ b/spec/requests/api/graphql/mutations/releases/delete_spec.rb @@ -50,7 +50,7 @@ RSpec.describe 'Deleting a release' do expect(mutation_response).to be_nil expect(graphql_errors.count).to eq(1) - expect(graphql_errors.first['message']).to eq("The resource that you are attempting to access does not exist or you don't have permission to perform this action") + expect(graphql_errors.first['message']).to eq(Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR) end end diff --git a/spec/requests/api/graphql/mutations/releases/update_spec.rb b/spec/requests/api/graphql/mutations/releases/update_spec.rb index c9a6c3abd57..0fa3d7de299 100644 --- a/spec/requests/api/graphql/mutations/releases/update_spec.rb +++ b/spec/requests/api/graphql/mutations/releases/update_spec.rb @@ -218,13 +218,13 @@ RSpec.describe 'Updating an existing release' do context 'when the project does not exist' do let(:mutation_arguments) { super().merge(projectPath: 'not/a/real/path') } - it_behaves_like 'top-level error with message', "The resource that you are attempting to access does not exist or you don't have permission to perform this action" + it_behaves_like 'top-level error with message', Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR end end end context "when the current user doesn't have access to update releases" do - expected_error_message = "The resource that you are attempting to access does not exist or you don't have permission to perform this action" + expected_error_message = Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR context 'when the current user is a Reporter' do let(:current_user) { reporter } diff --git a/spec/requests/api/graphql/mutations/security/ci_configuration/configure_sast_iac_spec.rb b/spec/requests/api/graphql/mutations/security/ci_configuration/configure_sast_iac_spec.rb new file mode 100644 index 00000000000..929609d4160 --- /dev/null +++ b/spec/requests/api/graphql/mutations/security/ci_configuration/configure_sast_iac_spec.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'ConfigureSastIac' do + include GraphqlHelpers + + let_it_be(:project) { create(:project, :test_repo) } + + let(:variables) { { project_path: project.full_path } } + let(:mutation) { graphql_mutation(:configure_sast_iac, variables) } + let(:mutation_response) { graphql_mutation_response(:configureSastIac) } + + context 'when authorized' do + let_it_be(:user) { project.owner } + + it 'creates a branch with sast iac configured' do + post_graphql_mutation(mutation, current_user: user) + + expect(response).to have_gitlab_http_status(:success) + expect(mutation_response['errors']).to be_empty + expect(mutation_response['branch']).not_to be_empty + expect(mutation_response['successPath']).not_to be_empty + end + end +end diff --git a/spec/requests/api/graphql/namespace_query_spec.rb b/spec/requests/api/graphql/namespace_query_spec.rb new file mode 100644 index 00000000000..f7ee2bcb55d --- /dev/null +++ b/spec/requests/api/graphql/namespace_query_spec.rb @@ -0,0 +1,86 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Query' do + include GraphqlHelpers + + let_it_be(:user) { create(:user) } + let_it_be(:other_user) { create(:user) } + + let_it_be(:group_namespace) { create(:group) } + let_it_be(:user_namespace) { create(:user_namespace, owner: user) } + let_it_be(:project_namespace) { create(:project_namespace, parent: group_namespace) } + + describe '.namespace' do + subject { post_graphql(query, current_user: current_user) } + + let(:current_user) { user } + + let(:query) { graphql_query_for(:namespace, { 'fullPath' => target_namespace.full_path }, all_graphql_fields_for('Namespace')) } + let(:query_result) { graphql_data['namespace'] } + + shared_examples 'retrieving a namespace' do + context 'authorised query' do + before do + subject + end + + it_behaves_like 'a working graphql query' + + it 'fetches the expected data' do + expect(query_result).to include( + 'fullPath' => target_namespace.full_path, + 'name' => target_namespace.name + ) + end + end + + context 'unauthorised query' do + before do + subject + end + + context 'anonymous user' do + let(:current_user) { nil } + + it 'does not retrieve the record' do + expect(query_result).to be_nil + end + end + + context 'the current user does not have permission' do + let(:current_user) { other_user } + + it 'does not retrieve the record' do + expect(query_result).to be_nil + end + end + end + end + + it_behaves_like 'retrieving a namespace' do + let(:target_namespace) { group_namespace } + + before do + group_namespace.add_developer(user) + end + end + + it_behaves_like 'retrieving a namespace' do + let(:target_namespace) { user_namespace } + end + + context 'does not retrieve project namespace' do + let(:target_namespace) { project_namespace } + + before do + subject + end + + it 'does not retrieve the record' do + expect(query_result).to be_nil + end + end + end +end diff --git a/spec/requests/api/graphql/packages/helm_spec.rb b/spec/requests/api/graphql/packages/helm_spec.rb new file mode 100644 index 00000000000..397096f70db --- /dev/null +++ b/spec/requests/api/graphql/packages/helm_spec.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true +require 'spec_helper' + +RSpec.describe 'helm package details' do + include GraphqlHelpers + include_context 'package details setup' + + let_it_be(:package) { create(:helm_package, project: project) } + + let(:package_files_metadata) {query_graphql_fragment('HelmFileMetadata')} + + let(:query) do + graphql_query_for(:package, { id: package_global_id }, <<~FIELDS) + #{all_graphql_fields_for('PackageDetailsType', max_depth: depth, excluded: excluded)} + packageFiles { + nodes { + #{package_files} + fileMetadata { + #{package_files_metadata} + } + } + } + FIELDS + end + + subject { post_graphql(query, current_user: user) } + + before do + subject + end + + it_behaves_like 'a package detail' + it_behaves_like 'a package with files' + + it 'has the correct file metadata' do + expect(first_file_response_metadata).to include( + 'channel' => first_file.helm_file_metadatum.channel + ) + expect(first_file_response_metadata['metadata']).to include( + 'name' => first_file.helm_file_metadatum.metadata['name'], + 'home' => first_file.helm_file_metadatum.metadata['home'], + 'sources' => first_file.helm_file_metadatum.metadata['sources'], + 'version' => first_file.helm_file_metadatum.metadata['version'], + 'description' => first_file.helm_file_metadatum.metadata['description'], + 'keywords' => first_file.helm_file_metadatum.metadata['keywords'], + 'maintainers' => first_file.helm_file_metadatum.metadata['maintainers'], + 'icon' => first_file.helm_file_metadatum.metadata['icon'], + 'apiVersion' => first_file.helm_file_metadatum.metadata['apiVersion'], + 'condition' => first_file.helm_file_metadatum.metadata['condition'], + 'tags' => first_file.helm_file_metadatum.metadata['tags'], + 'appVersion' => first_file.helm_file_metadatum.metadata['appVersion'], + 'deprecated' => first_file.helm_file_metadatum.metadata['deprecated'], + 'annotations' => first_file.helm_file_metadatum.metadata['annotations'], + 'kubeVersion' => first_file.helm_file_metadatum.metadata['kubeVersion'], + 'dependencies' => first_file.helm_file_metadatum.metadata['dependencies'], + 'type' => first_file.helm_file_metadatum.metadata['type'] + ) + end +end diff --git a/spec/requests/api/graphql/project/issues_spec.rb b/spec/requests/api/graphql/project/issues_spec.rb index 1c6d6ce4707..b3e91afb5b3 100644 --- a/spec/requests/api/graphql/project/issues_spec.rb +++ b/spec/requests/api/graphql/project/issues_spec.rb @@ -429,11 +429,11 @@ RSpec.describe 'getting an issue list for a project' do end it 'avoids N+1 queries' do - create(:contact, group_id: group.id, issues: [issue_a]) + create(:issue_customer_relations_contact, :for_issue, issue: issue_a) control = ActiveRecord::QueryRecorder.new(skip_cached: false) { clean_state_query } - create(:contact, group_id: group.id, issues: [issue_a]) + create(:issue_customer_relations_contact, :for_issue, issue: issue_a) expect { clean_state_query }.not_to exceed_all_query_limit(control) end diff --git a/spec/requests/api/graphql/project/merge_request_spec.rb b/spec/requests/api/graphql/project/merge_request_spec.rb index 438ea9bb4c1..353bf0356f6 100644 --- a/spec/requests/api/graphql/project/merge_request_spec.rb +++ b/spec/requests/api/graphql/project/merge_request_spec.rb @@ -347,7 +347,7 @@ RSpec.describe 'getting merge request information nested in a project' do expect(interaction_data).to contain_exactly a_hash_including( 'canMerge' => false, 'canUpdate' => can_update, - 'reviewState' => unreviewed, + 'reviewState' => attention_requested, 'reviewed' => false, 'approved' => false ) @@ -380,8 +380,8 @@ RSpec.describe 'getting merge request information nested in a project' do describe 'scalability' do let_it_be(:other_users) { create_list(:user, 3) } - let(:unreviewed) do - { 'reviewState' => 'UNREVIEWED' } + let(:attention_requested) do + { 'reviewState' => 'ATTENTION_REQUESTED' } end let(:reviewed) do @@ -413,9 +413,9 @@ RSpec.describe 'getting merge request information nested in a project' do expect { post_graphql(query) }.not_to exceed_query_limit(baseline) expect(interaction_data).to contain_exactly( - include(unreviewed), - include(unreviewed), - include(unreviewed), + include(attention_requested), + include(attention_requested), + include(attention_requested), include(reviewed) ) end @@ -444,7 +444,7 @@ RSpec.describe 'getting merge request information nested in a project' do it_behaves_like 'when requesting information about MR interactions' do let(:field) { :reviewers } - let(:unreviewed) { 'UNREVIEWED' } + let(:attention_requested) { 'ATTENTION_REQUESTED' } let(:can_update) { false } def assign_user(user) @@ -454,7 +454,7 @@ RSpec.describe 'getting merge request information nested in a project' do it_behaves_like 'when requesting information about MR interactions' do let(:field) { :assignees } - let(:unreviewed) { nil } + let(:attention_requested) { nil } let(:can_update) { true } # assignees can update MRs def assign_user(user) diff --git a/spec/requests/api/graphql/project/release_spec.rb b/spec/requests/api/graphql/project/release_spec.rb index 7f24d051457..77abac4ef04 100644 --- a/spec/requests/api/graphql/project/release_spec.rb +++ b/spec/requests/api/graphql/project/release_spec.rb @@ -228,6 +228,189 @@ RSpec.describe 'Query.project(fullPath).release(tagName)' do end end + shared_examples 'restricted access to release fields' do + describe 'scalar fields' do + let(:path) { path_prefix } + + let(:release_fields) do + %{ + tagName + tagPath + description + descriptionHtml + name + createdAt + releasedAt + upcomingRelease + } + end + + before do + post_query + end + + it 'finds all release data' do + expect(data).to eq({ + 'tagName' => release.tag, + 'tagPath' => nil, + 'description' => release.description, + 'descriptionHtml' => release.description_html, + 'name' => release.name, + 'createdAt' => release.created_at.iso8601, + 'releasedAt' => release.released_at.iso8601, + 'upcomingRelease' => false + }) + end + end + + describe 'milestones' do + let(:path) { path_prefix + %w[milestones nodes] } + + let(:release_fields) do + query_graphql_field(:milestones, nil, 'nodes { id title }') + end + + it 'finds milestones associated to a release' do + post_query + + expected = release.milestones.order_by_dates_and_title.map do |milestone| + { 'id' => global_id_of(milestone), 'title' => milestone.title } + end + + expect(data).to eq(expected) + end + end + + describe 'author' do + let(:path) { path_prefix + %w[author] } + + let(:release_fields) do + query_graphql_field(:author, nil, 'id username') + end + + it 'finds the author of the release' do + post_query + + expect(data).to eq( + 'id' => global_id_of(release.author), + 'username' => release.author.username + ) + end + end + + describe 'commit' do + let(:path) { path_prefix + %w[commit] } + + let(:release_fields) do + query_graphql_field(:commit, nil, 'sha') + end + + it 'restricts commit associated with the release' do + post_query + + expect(data).to eq(nil) + end + end + + describe 'assets' do + describe 'count' do + let(:path) { path_prefix + %w[assets] } + + let(:release_fields) do + query_graphql_field(:assets, nil, 'count') + end + + it 'returns non source release links count' do + post_query + + expect(data).to eq('count' => release.assets_count(except: [:sources])) + end + end + + describe 'links' do + let(:path) { path_prefix + %w[assets links nodes] } + + let(:release_fields) do + query_graphql_field(:assets, nil, + query_graphql_field(:links, nil, 'nodes { id name url external, directAssetUrl }')) + end + + it 'finds all non source external release links' do + post_query + + expected = release.links.map do |link| + { + 'id' => global_id_of(link), + 'name' => link.name, + 'url' => link.url, + 'external' => true, + 'directAssetUrl' => link.filepath ? Gitlab::Routing.url_helpers.project_release_url(project, release) << "/downloads#{link.filepath}" : link.url + } + end + + expect(data).to match_array(expected) + end + end + + describe 'sources' do + let(:path) { path_prefix + %w[assets sources nodes] } + + let(:release_fields) do + query_graphql_field(:assets, nil, + query_graphql_field(:sources, nil, 'nodes { format url }')) + end + + it 'restricts release sources' do + post_query + + expect(data).to match_array([]) + end + end + end + + describe 'links' do + let(:path) { path_prefix + %w[links] } + + let(:release_fields) do + query_graphql_field(:links, nil, %{ + selfUrl + openedMergeRequestsUrl + mergedMergeRequestsUrl + closedMergeRequestsUrl + openedIssuesUrl + closedIssuesUrl + }) + end + + it 'finds only selfUrl' do + post_query + + expect(data).to eq( + 'selfUrl' => project_release_url(project, release), + 'openedMergeRequestsUrl' => nil, + 'mergedMergeRequestsUrl' => nil, + 'closedMergeRequestsUrl' => nil, + 'openedIssuesUrl' => nil, + 'closedIssuesUrl' => nil + ) + end + end + + describe 'evidences' do + let(:path) { path_prefix + %w[evidences] } + + let(:release_fields) do + query_graphql_field(:evidences, nil, 'nodes { id sha filepath collectedAt }') + end + + it 'restricts all evidence fields' do + post_query + + expect(data).to eq('nodes' => []) + end + end + end + shared_examples 'no access to the release field' do describe 'repository-related fields' do let(:path) { path_prefix } @@ -302,7 +485,8 @@ RSpec.describe 'Query.project(fullPath).release(tagName)' do context 'when the user has Guest permissions' do let(:current_user) { guest } - it_behaves_like 'no access to the release field' + it_behaves_like 'restricted access to release fields' + it_behaves_like 'no access to editUrl' end context 'when the user has Reporter permissions' do diff --git a/spec/requests/api/graphql/project/releases_spec.rb b/spec/requests/api/graphql/project/releases_spec.rb index 2816ce90a6b..c28a6fa7666 100644 --- a/spec/requests/api/graphql/project/releases_spec.rb +++ b/spec/requests/api/graphql/project/releases_spec.rb @@ -129,10 +129,12 @@ RSpec.describe 'Query.project(fullPath).releases()' do end it 'does not return data for fields that expose repository information' do + tag_name = release.tag + release_name = release.name expect(data).to eq( - 'tagName' => nil, + 'tagName' => tag_name, 'tagPath' => nil, - 'name' => "Release-#{release.id}", + 'name' => release_name, 'commit' => nil, 'assets' => { 'count' => release.assets_count(except: [:sources]), @@ -143,7 +145,14 @@ RSpec.describe 'Query.project(fullPath).releases()' do 'evidences' => { 'nodes' => [] }, - 'links' => nil + 'links' => { + 'closedIssuesUrl' => nil, + 'closedMergeRequestsUrl' => nil, + 'mergedMergeRequestsUrl' => nil, + 'openedIssuesUrl' => nil, + 'openedMergeRequestsUrl' => nil, + 'selfUrl' => project_release_url(project, release) + } ) end end diff --git a/spec/requests/api/graphql_spec.rb b/spec/requests/api/graphql_spec.rb index 7d182a3414b..b8f7af29a9f 100644 --- a/spec/requests/api/graphql_spec.rb +++ b/spec/requests/api/graphql_spec.rb @@ -12,21 +12,33 @@ RSpec.describe 'GraphQL' do describe 'logging' do shared_examples 'logging a graphql query' do - let(:expected_params) do + let(:expected_execute_query_log) do { - query_string: query, - variables: variables.to_s, - duration_s: anything, + "correlation_id" => kind_of(String), + "meta.caller_id" => "graphql:anonymous", + "meta.client_id" => kind_of(String), + "meta.feature_category" => "not_owned", + "meta.remote_ip" => kind_of(String), + "query_analysis.duration_s" => kind_of(Numeric), + "query_analysis.depth" => 1, + "query_analysis.complexity" => 1, + "query_analysis.used_fields" => ['Query.echo'], + "query_analysis.used_deprecated_fields" => [], + # query_fingerprint starts with operation name + query_fingerprint: %r{^anonymous\/}, + duration_s: kind_of(Numeric), + trace_type: 'execute_query', operation_name: nil, - depth: 1, - complexity: 1, - used_fields: ['Query.echo'], - used_deprecated_fields: [] + # operation_fingerprint starts with operation name + operation_fingerprint: %r{^anonymous\/}, + is_mutation: false, + variables: variables.to_s, + query_string: query } end it 'logs a query with the expected params' do - expect(Gitlab::GraphqlLogger).to receive(:info).with(expected_params).once + expect(Gitlab::GraphqlLogger).to receive(:info).with(expected_execute_query_log).once post_graphql(query, variables: variables) end diff --git a/spec/requests/api/group_debian_distributions_spec.rb b/spec/requests/api/group_debian_distributions_spec.rb index ec1912b72bf..21c5f2f09a0 100644 --- a/spec/requests/api/group_debian_distributions_spec.rb +++ b/spec/requests/api/group_debian_distributions_spec.rb @@ -11,19 +11,25 @@ RSpec.describe API::GroupDebianDistributions do let(:url) { "/groups/#{container.id}/-/debian_distributions" } let(:api_params) { { 'codename': 'my-codename' } } - it_behaves_like 'Debian repository write endpoint', 'POST distribution request', :created, /^{.*"codename":"my-codename",.*"components":\["main"\],.*"architectures":\["all","amd64"\]/, authenticate_non_public: false + it_behaves_like 'Debian distributions write endpoint', 'POST', :created, /^{.*"codename":"my-codename",.*"components":\["main"\],.*"architectures":\["all","amd64"\]/ end describe 'GET groups/:id/-/debian_distributions' do let(:url) { "/groups/#{container.id}/-/debian_distributions" } - it_behaves_like 'Debian repository read endpoint', 'GET request', :success, /^\[{.*"codename":"existing-codename",.*"components":\["existing-component"\],.*"architectures":\["all","existing-arch"\]/, authenticate_non_public: false + it_behaves_like 'Debian distributions read endpoint', 'GET', :success, /^\[{.*"codename":"existing-codename",.*"components":\["existing-component"\],.*"architectures":\["all","existing-arch"\]/ end describe 'GET groups/:id/-/debian_distributions/:codename' do let(:url) { "/groups/#{container.id}/-/debian_distributions/#{distribution.codename}" } - it_behaves_like 'Debian repository read endpoint', 'GET request', :success, /^{.*"codename":"existing-codename",.*"components":\["existing-component"\],.*"architectures":\["all","existing-arch"\]/, authenticate_non_public: false + it_behaves_like 'Debian distributions read endpoint', 'GET', :success, /^{.*"codename":"existing-codename",.*"components":\["existing-component"\],.*"architectures":\["all","existing-arch"\]/ + end + + describe 'GET groups/:id/-/debian_distributions/:codename/key.asc' do + let(:url) { "/groups/#{container.id}/-/debian_distributions/#{distribution.codename}/key.asc" } + + it_behaves_like 'Debian distributions read endpoint', 'GET', :success, /^-----BEGIN PGP PUBLIC KEY BLOCK-----/ end describe 'PUT groups/:id/-/debian_distributions/:codename' do @@ -31,14 +37,14 @@ RSpec.describe API::GroupDebianDistributions do let(:url) { "/groups/#{container.id}/-/debian_distributions/#{distribution.codename}" } let(:api_params) { { suite: 'my-suite' } } - it_behaves_like 'Debian repository write endpoint', 'PUT distribution request', :success, /^{.*"codename":"existing-codename",.*"suite":"my-suite",/, authenticate_non_public: false + it_behaves_like 'Debian distributions write endpoint', 'PUT', :success, /^{.*"codename":"existing-codename",.*"suite":"my-suite",/ end describe 'DELETE groups/:id/-/debian_distributions/:codename' do let(:method) { :delete } let(:url) { "/groups/#{container.id}/-/debian_distributions/#{distribution.codename}" } - it_behaves_like 'Debian repository maintainer write endpoint', 'DELETE distribution request', :success, /^{"message":"202 Accepted"}$/, authenticate_non_public: false + it_behaves_like 'Debian distributions maintainer write endpoint', 'DELETE', :success, /^{"message":"202 Accepted"}$/ end end end diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb index cee727ae6fe..75f5a974d22 100644 --- a/spec/requests/api/groups_spec.rb +++ b/spec/requests/api/groups_spec.rb @@ -319,12 +319,15 @@ RSpec.describe API::Groups do it "includes statistics if requested" do attributes = { - storage_size: 2392, + storage_size: 4093, repository_size: 123, wiki_size: 456, lfs_objects_size: 234, build_artifacts_size: 345, - snippets_size: 1234 + pipeline_artifacts_size: 456, + packages_size: 567, + snippets_size: 1234, + uploads_size: 678 }.stringify_keys exposed_attributes = attributes.dup exposed_attributes['job_artifacts_size'] = exposed_attributes.delete('build_artifacts_size') diff --git a/spec/requests/api/internal/base_spec.rb b/spec/requests/api/internal/base_spec.rb index aeca4e435f4..0a71eb43f81 100644 --- a/spec/requests/api/internal/base_spec.rb +++ b/spec/requests/api/internal/base_spec.rb @@ -948,7 +948,7 @@ RSpec.describe API::Internal::Base do context 'user does not exist' do it do - pull(OpenStruct.new(id: 0), project) + pull(double('key', id: 0), project) expect(response).to have_gitlab_http_status(:not_found) expect(json_response["status"]).to be_falsey diff --git a/spec/requests/api/invitations_spec.rb b/spec/requests/api/invitations_spec.rb index b23ba0021e0..cba4256adc5 100644 --- a/spec/requests/api/invitations_spec.rb +++ b/spec/requests/api/invitations_spec.rb @@ -166,6 +166,38 @@ RSpec.describe API::Invitations do end end + context 'with tasks_to_be_done and tasks_project_id in the params' do + before do + stub_experiments(invite_members_for_task: true) + end + + let(:project_id) { source_type == 'project' ? source.id : create(:project, namespace: source).id } + + context 'when there is 1 invitation' do + it 'creates a member_task with the tasks_to_be_done and the project' do + post invitations_url(source, maintainer), + params: { email: email, access_level: Member::DEVELOPER, tasks_to_be_done: %w(code ci), tasks_project_id: project_id } + + member = source.members.find_by(invite_email: email) + expect(member.tasks_to_be_done).to match_array([:code, :ci]) + expect(member.member_task.project_id).to eq(project_id) + end + end + + context 'when there are multiple invitations' do + it 'creates a member_task with the tasks_to_be_done and the project' do + post invitations_url(source, maintainer), + params: { email: [email, email2].join(','), access_level: Member::DEVELOPER, tasks_to_be_done: %w(code ci), tasks_project_id: project_id } + + members = source.members.where(invite_email: [email, email2]) + members.each do |member| + expect(member.tasks_to_be_done).to match_array([:code, :ci]) + expect(member.member_task.project_id).to eq(project_id) + end + end + end + end + context 'with invite_source considerations', :snowplow do let(:params) { { email: email, access_level: Member::DEVELOPER } } diff --git a/spec/requests/api/lint_spec.rb b/spec/requests/api/lint_spec.rb index d7f22b9d619..ac30da99afe 100644 --- a/spec/requests/api/lint_spec.rb +++ b/spec/requests/api/lint_spec.rb @@ -102,6 +102,13 @@ RSpec.describe API::Lint do expect(response).to have_gitlab_http_status(:ok) expect(json_response).to have_key('merged_yaml') end + + it 'outputs jobs' do + post api('/ci/lint', api_user), params: { content: yaml_content, include_jobs: true } + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to have_key('jobs') + end end context 'with valid .gitlab-ci.yaml with warnings' do @@ -136,6 +143,13 @@ RSpec.describe API::Lint do expect(response).to have_gitlab_http_status(:ok) expect(json_response).to have_key('merged_yaml') end + + it 'outputs jobs' do + post api('/ci/lint', api_user), params: { content: yaml_content, include_jobs: true } + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to have_key('jobs') + end end context 'with invalid configuration' do @@ -156,6 +170,13 @@ RSpec.describe API::Lint do expect(response).to have_gitlab_http_status(:ok) expect(json_response).to have_key('merged_yaml') end + + it 'outputs jobs' do + post api('/ci/lint', api_user), params: { content: yaml_content, include_jobs: true } + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to have_key('jobs') + end end end @@ -171,10 +192,11 @@ RSpec.describe API::Lint do end describe 'GET /projects/:id/ci/lint' do - subject(:ci_lint) { get api("/projects/#{project.id}/ci/lint", api_user), params: { dry_run: dry_run } } + subject(:ci_lint) { get api("/projects/#{project.id}/ci/lint", api_user), params: { dry_run: dry_run, include_jobs: include_jobs } } let(:project) { create(:project, :repository) } let(:dry_run) { nil } + let(:include_jobs) { nil } RSpec.shared_examples 'valid config with warnings' do it 'passes validation with warnings' do @@ -359,6 +381,30 @@ RSpec.describe API::Lint do it_behaves_like 'valid config without warnings' end + context 'when running with include jobs' do + let(:include_jobs) { true } + + it_behaves_like 'valid config without warnings' + + it 'returns jobs key' do + ci_lint + + expect(json_response).to have_key('jobs') + end + end + + context 'when running without include jobs' do + let(:include_jobs) { false } + + it_behaves_like 'valid config without warnings' + + it 'does not return jobs key' do + ci_lint + + expect(json_response).not_to have_key('jobs') + end + end + context 'With warnings' do let(:yaml_content) { { job: { script: 'ls', rules: [{ when: 'always' }] } }.to_yaml } @@ -386,15 +432,40 @@ RSpec.describe API::Lint do it_behaves_like 'invalid config' end + + context 'when running with include jobs' do + let(:include_jobs) { true } + + it_behaves_like 'invalid config' + + it 'returns jobs key' do + ci_lint + + expect(json_response).to have_key('jobs') + end + end + + context 'when running without include jobs' do + let(:include_jobs) { false } + + it_behaves_like 'invalid config' + + it 'does not return jobs key' do + ci_lint + + expect(json_response).not_to have_key('jobs') + end + end end end end describe 'POST /projects/:id/ci/lint' do - subject(:ci_lint) { post api("/projects/#{project.id}/ci/lint", api_user), params: { dry_run: dry_run, content: yaml_content } } + subject(:ci_lint) { post api("/projects/#{project.id}/ci/lint", api_user), params: { dry_run: dry_run, content: yaml_content, include_jobs: include_jobs } } let(:project) { create(:project, :repository) } let(:dry_run) { nil } + let(:include_jobs) { nil } let_it_be(:api_user) { create(:user) } @@ -562,6 +633,30 @@ RSpec.describe API::Lint do it_behaves_like 'valid project config' end + + context 'when running with include jobs param' do + let(:include_jobs) { true } + + it_behaves_like 'valid project config' + + it 'contains jobs key' do + ci_lint + + expect(json_response).to have_key('jobs') + end + end + + context 'when running without include jobs param' do + let(:include_jobs) { false } + + it_behaves_like 'valid project config' + + it 'does not contain jobs key' do + ci_lint + + expect(json_response).not_to have_key('jobs') + end + end end context 'with invalid .gitlab-ci.yml content' do @@ -580,6 +675,30 @@ RSpec.describe API::Lint do it_behaves_like 'invalid project config' end + + context 'when running with include jobs set to false' do + let(:include_jobs) { false } + + it_behaves_like 'invalid project config' + + it 'does not contain jobs key' do + ci_lint + + expect(json_response).not_to have_key('jobs') + end + end + + context 'when running with param include jobs' do + let(:include_jobs) { true } + + it_behaves_like 'invalid project config' + + it 'contains jobs key' do + ci_lint + + expect(json_response).to have_key('jobs') + end + end end end end diff --git a/spec/requests/api/members_spec.rb b/spec/requests/api/members_spec.rb index a1daf86de31..7f4345faabb 100644 --- a/spec/requests/api/members_spec.rb +++ b/spec/requests/api/members_spec.rb @@ -81,14 +81,22 @@ RSpec.describe API::Members do expect(json_response.map { |u| u['id'] }).to match_array [maintainer.id, developer.id] end - it 'finds members with query string' do - get api(members_url, developer), params: { query: maintainer.username } + context 'with cross db check disabled' do + around do |example| + allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/343305') do + example.run + end + end - expect(response).to have_gitlab_http_status(:ok) - expect(response).to include_pagination_headers - expect(json_response).to be_an Array - expect(json_response.count).to eq(1) - expect(json_response.first['username']).to eq(maintainer.username) + it 'finds members with query string' do + get api(members_url, developer), params: { query: maintainer.username } + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.count).to eq(1) + expect(json_response.first['username']).to eq(maintainer.username) + end end it 'finds members with the given user_ids' do @@ -406,6 +414,38 @@ RSpec.describe API::Members do end end + context 'with tasks_to_be_done and tasks_project_id in the params' do + before do + stub_experiments(invite_members_for_task: true) + end + + let(:project_id) { source_type == 'project' ? source.id : create(:project, namespace: source).id } + + context 'when there is 1 user to add' do + it 'creates a member_task with the correct attributes' do + post api("/#{source_type.pluralize}/#{source.id}/members", maintainer), + params: { user_id: stranger.id, access_level: Member::DEVELOPER, tasks_to_be_done: %w(code ci), tasks_project_id: project_id } + + member = source.members.find_by(user_id: stranger.id) + expect(member.tasks_to_be_done).to match_array([:code, :ci]) + expect(member.member_task.project_id).to eq(project_id) + end + end + + context 'when there are multiple users to add' do + it 'creates a member_task with the correct attributes' do + post api("/#{source_type.pluralize}/#{source.id}/members", maintainer), + params: { user_id: [developer.id, stranger.id].join(','), access_level: Member::DEVELOPER, tasks_to_be_done: %w(code ci), tasks_project_id: project_id } + + members = source.members.where(user_id: [developer.id, stranger.id]) + members.each do |member| + expect(member.tasks_to_be_done).to match_array([:code, :ci]) + expect(member.member_task.project_id).to eq(project_id) + end + end + end + end + it "returns 409 if member already exists" do post api("/#{source_type.pluralize}/#{source.id}/members", maintainer), params: { user_id: maintainer.id, access_level: Member::MAINTAINER } diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index bdbc73a59d8..7c147419354 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -3278,6 +3278,8 @@ RSpec.describe API::MergeRequests do context 'when skip_ci parameter is set' do it 'enqueues a rebase of the merge request with skip_ci flag set' do + allow(RebaseWorker).to receive(:with_status).and_return(RebaseWorker) + expect(RebaseWorker).to receive(:perform_async).with(merge_request.id, user.id, true).and_call_original Sidekiq::Testing.fake! do diff --git a/spec/requests/api/namespaces_spec.rb b/spec/requests/api/namespaces_spec.rb index 222d8992d1b..01dbf523071 100644 --- a/spec/requests/api/namespaces_spec.rb +++ b/spec/requests/api/namespaces_spec.rb @@ -3,10 +3,12 @@ require 'spec_helper' RSpec.describe API::Namespaces do - let(:admin) { create(:admin) } - let(:user) { create(:user) } - let!(:group1) { create(:group, name: 'group.one') } - let!(:group2) { create(:group, :nested) } + let_it_be(:admin) { create(:admin) } + let_it_be(:user) { create(:user) } + let_it_be(:group1) { create(:group, name: 'group.one') } + let_it_be(:group2) { create(:group, :nested) } + let_it_be(:project) { create(:project, namespace: group2, name: group2.name, path: group2.path) } + let_it_be(:project_namespace) { project.project_namespace } describe "GET /namespaces" do context "when unauthenticated" do @@ -26,7 +28,7 @@ RSpec.describe API::Namespaces do expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers expect(group_kind_json_response.keys).to include('id', 'kind', 'name', 'path', 'full_path', - 'parent_id', 'members_count_with_descendants') + 'parent_id', 'members_count_with_descendants') expect(user_kind_json_response.keys).to include('id', 'kind', 'name', 'path', 'full_path', 'parent_id') end @@ -37,7 +39,8 @@ RSpec.describe API::Namespaces do expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers expect(json_response).to be_an Array - expect(json_response.length).to eq(Namespace.count) + # project namespace is excluded + expect(json_response.length).to eq(Namespace.count - 1) end it "admin: returns an array of matched namespaces" do @@ -61,7 +64,7 @@ RSpec.describe API::Namespaces do owned_group_response = json_response.find { |resource| resource['id'] == group1.id } expect(owned_group_response.keys).to include('id', 'kind', 'name', 'path', 'full_path', - 'parent_id', 'members_count_with_descendants') + 'parent_id', 'members_count_with_descendants') end it "returns correct attributes when user cannot admin group" do @@ -109,7 +112,8 @@ RSpec.describe API::Namespaces do describe 'GET /namespaces/:id' do let(:owned_group) { group1 } - let(:user2) { create(:user) } + + let_it_be(:user2) { create(:user) } shared_examples 'can access namespace' do it 'returns namespace details' do @@ -144,6 +148,16 @@ RSpec.describe API::Namespaces do it_behaves_like 'can access namespace' end + + context 'when requesting project_namespace' do + let(:namespace_id) { project_namespace.id } + + it 'returns not-found' do + get api("/namespaces/#{namespace_id}", request_actor) + + expect(response).to have_gitlab_http_status(:not_found) + end + end end context 'when requested by path' do @@ -159,6 +173,16 @@ RSpec.describe API::Namespaces do it_behaves_like 'can access namespace' end + + context 'when requesting project_namespace' do + let(:namespace_id) { project_namespace.full_path } + + it 'returns not-found' do + get api("/namespaces/#{namespace_id}", request_actor) + + expect(response).to have_gitlab_http_status(:not_found) + end + end end end @@ -177,6 +201,12 @@ RSpec.describe API::Namespaces do expect(response).to have_gitlab_http_status(:unauthorized) end + + it 'returns authentication error' do + get api("/namespaces/#{project_namespace.id}") + + expect(response).to have_gitlab_http_status(:unauthorized) + end end context 'when authenticated as regular user' do @@ -231,10 +261,10 @@ RSpec.describe API::Namespaces do end describe 'GET /namespaces/:namespace/exists' do - let!(:namespace1) { create(:group, name: 'Namespace 1', path: 'namespace-1') } - let!(:namespace2) { create(:group, name: 'Namespace 2', path: 'namespace-2') } - let!(:namespace1sub) { create(:group, name: 'Sub Namespace 1', path: 'sub-namespace-1', parent: namespace1) } - let!(:namespace2sub) { create(:group, name: 'Sub Namespace 2', path: 'sub-namespace-2', parent: namespace2) } + let_it_be(:namespace1) { create(:group, name: 'Namespace 1', path: 'namespace-1') } + let_it_be(:namespace2) { create(:group, name: 'Namespace 2', path: 'namespace-2') } + let_it_be(:namespace1sub) { create(:group, name: 'Sub Namespace 1', path: 'sub-namespace-1', parent: namespace1) } + let_it_be(:namespace2sub) { create(:group, name: 'Sub Namespace 2', path: 'sub-namespace-2', parent: namespace2) } context 'when unauthenticated' do it 'returns authentication error' do @@ -242,6 +272,16 @@ RSpec.describe API::Namespaces do expect(response).to have_gitlab_http_status(:unauthorized) end + + context 'when requesting project_namespace' do + let(:namespace_id) { project_namespace.id } + + it 'returns authentication error' do + get api("/namespaces/#{project_namespace.path}/exists"), params: { parent_id: group2.id } + + expect(response).to have_gitlab_http_status(:unauthorized) + end + end end context 'when authenticated' do @@ -300,6 +340,18 @@ RSpec.describe API::Namespaces do expect(response).to have_gitlab_http_status(:ok) expect(response.body).to eq(expected_json) end + + context 'when requesting project_namespace' do + let(:namespace_id) { project_namespace.id } + + it 'returns JSON indicating the namespace does not exist without a suggestion' do + get api("/namespaces/#{project_namespace.path}/exists", user), params: { parent_id: group2.id } + + expected_json = { exists: false, suggests: [] }.to_json + expect(response).to have_gitlab_http_status(:ok) + expect(response.body).to eq(expected_json) + end + end end end end diff --git a/spec/requests/api/npm_project_packages_spec.rb b/spec/requests/api/npm_project_packages_spec.rb index 0d04c2cad5b..7c3f1890095 100644 --- a/spec/requests/api/npm_project_packages_spec.rb +++ b/spec/requests/api/npm_project_packages_spec.rb @@ -180,6 +180,7 @@ RSpec.describe API::NpmProjectPackages do .to change { project.packages.count }.by(1) .and change { Packages::PackageFile.count }.by(1) .and change { Packages::Tag.count }.by(1) + .and change { Packages::Npm::Metadatum.count }.by(1) expect(response).to have_gitlab_http_status(:ok) end @@ -317,6 +318,25 @@ RSpec.describe API::NpmProjectPackages do end end end + + context 'with a too large metadata structure' do + let(:package_name) { "@#{group.path}/my_package_name" } + let(:params) do + upload_params(package_name: package_name, package_version: '1.2.3').tap do |h| + h['versions']['1.2.3']['test'] = 'test' * 10000 + end + end + + it_behaves_like 'not a package tracking event' + + it 'returns an error' do + expect { upload_package_with_token } + .not_to change { project.packages.count } + + expect(response).to have_gitlab_http_status(:bad_request) + expect(response.body).to include('Validation failed: Package json structure is too large') + end + end end def upload_package(package_name, params = {}) diff --git a/spec/requests/api/project_attributes.yml b/spec/requests/api/project_attributes.yml index dd00d413664..01d2fb18f00 100644 --- a/spec/requests/api/project_attributes.yml +++ b/spec/requests/api/project_attributes.yml @@ -137,6 +137,7 @@ project_setting: unexposed_attributes: - created_at - has_confluence + - has_shimo - has_vulnerabilities - prevent_merge_without_jira_issue - warn_about_potentially_unwanted_characters diff --git a/spec/requests/api/project_debian_distributions_spec.rb b/spec/requests/api/project_debian_distributions_spec.rb index de7362758f7..2b993f24046 100644 --- a/spec/requests/api/project_debian_distributions_spec.rb +++ b/spec/requests/api/project_debian_distributions_spec.rb @@ -11,25 +11,31 @@ RSpec.describe API::ProjectDebianDistributions do let(:url) { "/projects/#{container.id}/debian_distributions" } let(:api_params) { { 'codename': 'my-codename' } } - it_behaves_like 'Debian repository write endpoint', 'POST distribution request', :created, /^{.*"codename":"my-codename",.*"components":\["main"\],.*"architectures":\["all","amd64"\]/, authenticate_non_public: false + it_behaves_like 'Debian distributions write endpoint', 'POST', :created, /^{.*"codename":"my-codename",.*"components":\["main"\],.*"architectures":\["all","amd64"\]/ context 'with invalid parameters' do let(:api_params) { { codename: distribution.codename } } - it_behaves_like 'Debian repository write endpoint', 'GET request', :bad_request, /^{"message":{"codename":\["has already been taken"\]}}$/, authenticate_non_public: false + it_behaves_like 'Debian distributions write endpoint', 'GET', :bad_request, /^{"message":{"codename":\["has already been taken"\]}}$/ end end describe 'GET projects/:id/debian_distributions' do let(:url) { "/projects/#{container.id}/debian_distributions" } - it_behaves_like 'Debian repository read endpoint', 'GET request', :success, /^\[{.*"codename":"existing-codename\",.*"components":\["existing-component"\],.*"architectures":\["all","existing-arch"\]/, authenticate_non_public: false + it_behaves_like 'Debian distributions read endpoint', 'GET', :success, /^\[{.*"codename":"existing-codename\",.*"components":\["existing-component"\],.*"architectures":\["all","existing-arch"\]/ end describe 'GET projects/:id/debian_distributions/:codename' do let(:url) { "/projects/#{container.id}/debian_distributions/#{distribution.codename}" } - it_behaves_like 'Debian repository read endpoint', 'GET request', :success, /^{.*"codename":"existing-codename\",.*"components":\["existing-component"\],.*"architectures":\["all","existing-arch"\]/, authenticate_non_public: false + it_behaves_like 'Debian distributions read endpoint', 'GET', :success, /^{.*"codename":"existing-codename\",.*"components":\["existing-component"\],.*"architectures":\["all","existing-arch"\]/ + end + + describe 'GET projects/:id/debian_distributions/:codename/key.asc' do + let(:url) { "/projects/#{container.id}/debian_distributions/#{distribution.codename}/key.asc" } + + it_behaves_like 'Debian distributions read endpoint', 'GET', :success, /^-----BEGIN PGP PUBLIC KEY BLOCK-----/ end describe 'PUT projects/:id/debian_distributions/:codename' do @@ -37,12 +43,12 @@ RSpec.describe API::ProjectDebianDistributions do let(:url) { "/projects/#{container.id}/debian_distributions/#{distribution.codename}" } let(:api_params) { { suite: 'my-suite' } } - it_behaves_like 'Debian repository write endpoint', 'PUT distribution request', :success, /^{.*"codename":"existing-codename",.*"suite":"my-suite",/, authenticate_non_public: false + it_behaves_like 'Debian distributions write endpoint', 'PUT', :success, /^{.*"codename":"existing-codename",.*"suite":"my-suite",/ context 'with invalid parameters' do let(:api_params) { { suite: distribution.codename } } - it_behaves_like 'Debian repository write endpoint', 'GET request', :bad_request, /^{"message":{"suite":\["has already been taken as Codename"\]}}$/, authenticate_non_public: false + it_behaves_like 'Debian distributions write endpoint', 'GET', :bad_request, /^{"message":{"suite":\["has already been taken as Codename"\]}}$/ end end @@ -50,7 +56,7 @@ RSpec.describe API::ProjectDebianDistributions do let(:method) { :delete } let(:url) { "/projects/#{container.id}/debian_distributions/#{distribution.codename}" } - it_behaves_like 'Debian repository maintainer write endpoint', 'DELETE distribution request', :success, /^{\"message\":\"202 Accepted\"}$/, authenticate_non_public: false + it_behaves_like 'Debian distributions maintainer write endpoint', 'DELETE', :success, /^{\"message\":\"202 Accepted\"}$/ context 'when destroy fails' do before do @@ -59,7 +65,7 @@ RSpec.describe API::ProjectDebianDistributions do end end - it_behaves_like 'Debian repository maintainer write endpoint', 'GET request', :bad_request, /^{"message":"Failed to delete distribution"}$/, authenticate_non_public: false + it_behaves_like 'Debian distributions maintainer write endpoint', 'GET', :bad_request, /^{"message":"Failed to delete distribution"}$/ end end end diff --git a/spec/requests/api/project_import_spec.rb b/spec/requests/api/project_import_spec.rb index 0c9e125cc90..097d374640c 100644 --- a/spec/requests/api/project_import_spec.rb +++ b/spec/requests/api/project_import_spec.rb @@ -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 <= 100 + expect(control_count).to be <= 101 end it 'schedules an import using a namespace' do diff --git a/spec/requests/api/project_snapshots_spec.rb b/spec/requests/api/project_snapshots_spec.rb index f23e374407b..33c86d56ed4 100644 --- a/spec/requests/api/project_snapshots_spec.rb +++ b/spec/requests/api/project_snapshots_spec.rb @@ -29,6 +29,7 @@ RSpec.describe API::ProjectSnapshots do repository: repository.gitaly_repository ).to_json ) + expect(response.parsed_body).to be_empty end it 'returns authentication error as project owner' do diff --git a/spec/requests/api/project_snippets_spec.rb b/spec/requests/api/project_snippets_spec.rb index 8cd1f15a88d..512cbf7c321 100644 --- a/spec/requests/api/project_snippets_spec.rb +++ b/spec/requests/api/project_snippets_spec.rb @@ -400,6 +400,7 @@ RSpec.describe API::ProjectSnippets do expect(response).to have_gitlab_http_status(:ok) expect(response.media_type).to eq 'text/plain' + expect(response.parsed_body).to be_empty end it 'returns 404 for invalid snippet id' do diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index dd6afa869e0..4f84e6f2562 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -48,6 +48,7 @@ end RSpec.describe API::Projects do include ProjectForksHelper + include StubRequests let_it_be(:user) { create(:user) } let_it_be(:user2) { create(:user) } @@ -358,7 +359,7 @@ RSpec.describe API::Projects do statistics = json_response.find { |p| p['id'] == project.id }['statistics'] expect(statistics).to be_present - expect(statistics).to include('commit_count', 'storage_size', 'repository_size', 'wiki_size', 'lfs_objects_size', 'job_artifacts_size', 'snippets_size', 'packages_size') + expect(statistics).to include('commit_count', 'storage_size', 'repository_size', 'wiki_size', 'lfs_objects_size', 'job_artifacts_size', 'pipeline_artifacts_size', 'snippets_size', 'packages_size', 'uploads_size') end it "does not include license by default" do @@ -1159,6 +1160,34 @@ RSpec.describe API::Projects do expect(response).to have_gitlab_http_status(:forbidden) end + it 'disallows creating a project with an import_url that is not reachable', :aggregate_failures do + url = 'http://example.com' + endpoint_url = "#{url}/info/refs?service=git-upload-pack" + stub_full_request(endpoint_url, method: :get).to_return({ status: 301, body: '', headers: nil }) + project_params = { import_url: url, path: 'path-project-Foo', name: 'Foo Project' } + + expect { post api('/projects', user), params: project_params }.not_to change { Project.count } + + expect(response).to have_gitlab_http_status(:unprocessable_entity) + expect(json_response['message']).to eq("#{url} is not a valid HTTP Git repository") + end + + it 'creates a project with an import_url that is valid', :aggregate_failures do + url = 'http://example.com' + endpoint_url = "#{url}/info/refs?service=git-upload-pack" + git_response = { + status: 200, + body: '001e# service=git-upload-pack', + headers: { 'Content-Type': 'application/x-git-upload-pack-advertisement' } + } + stub_full_request(endpoint_url, method: :get).to_return(git_response) + project_params = { import_url: url, path: 'path-project-Foo', name: 'Foo Project' } + + expect { post api('/projects', user), params: project_params }.to change { Project.count }.by(1) + + expect(response).to have_gitlab_http_status(:created) + end + it 'sets a project as public' do project = attributes_for(:project, visibility: 'public') diff --git a/spec/requests/api/releases_spec.rb b/spec/requests/api/releases_spec.rb index 90b03a480a8..cb9b6a072b1 100644 --- a/spec/requests/api/releases_spec.rb +++ b/spec/requests/api/releases_spec.rb @@ -42,6 +42,14 @@ RSpec.describe API::Releases do expect(response).to have_gitlab_http_status(:ok) end + it 'returns 200 HTTP status when using JOB-TOKEN auth' do + job = create(:ci_build, :running, project: project, user: maintainer) + + get api("/projects/#{project.id}/releases"), params: { job_token: job.token } + + expect(response).to have_gitlab_http_status(:ok) + end + it 'returns releases ordered by released_at' do get api("/projects/#{project.id}/releases", maintainer) @@ -316,6 +324,14 @@ RSpec.describe API::Releases do expect(response).to have_gitlab_http_status(:ok) end + it 'returns 200 HTTP status when using JOB-TOKEN auth' do + job = create(:ci_build, :running, project: project, user: maintainer) + + get api("/projects/#{project.id}/releases/v0.1"), params: { job_token: job.token } + + expect(response).to have_gitlab_http_status(:ok) + end + it 'returns a release entry' do get api("/projects/#{project.id}/releases/v0.1", maintainer) @@ -1008,6 +1024,14 @@ RSpec.describe API::Releases do expect(response).to have_gitlab_http_status(:ok) end + it 'accepts the request when using JOB-TOKEN auth' do + job = create(:ci_build, :running, project: project, user: maintainer) + + put api("/projects/#{project.id}/releases/v0.1"), params: params.merge(job_token: job.token) + + expect(response).to have_gitlab_http_status(:ok) + end + it 'updates the description' do put api("/projects/#{project.id}/releases/v0.1", maintainer), params: params @@ -1220,6 +1244,14 @@ RSpec.describe API::Releases do expect(response).to have_gitlab_http_status(:ok) end + it 'accepts the request when using JOB-TOKEN auth' do + job = create(:ci_build, :running, project: project, user: maintainer) + + delete api("/projects/#{project.id}/releases/v0.1"), params: { job_token: job.token } + + expect(response).to have_gitlab_http_status(:ok) + end + it 'destroys the release' do expect do delete api("/projects/#{project.id}/releases/v0.1", maintainer) diff --git a/spec/requests/api/repositories_spec.rb b/spec/requests/api/repositories_spec.rb index f05f125c974..f3146480be2 100644 --- a/spec/requests/api/repositories_spec.rb +++ b/spec/requests/api/repositories_spec.rb @@ -197,6 +197,7 @@ RSpec.describe API::Repositories do expect(response).to have_gitlab_http_status(:ok) expect(headers[Gitlab::Workhorse::DETECT_HEADER]).to eq "true" + expect(response.parsed_body).to be_empty end it 'sets inline content disposition by default' do @@ -274,6 +275,7 @@ RSpec.describe API::Repositories do expect(type).to eq('git-archive') expect(params['ArchivePath']).to match(/#{project.path}\-[^\.]+\.tar.gz/) + expect(response.parsed_body).to be_empty end it 'returns the repository archive archive.zip' do @@ -495,6 +497,43 @@ RSpec.describe API::Repositories do expect(response).to have_gitlab_http_status(:not_found) end + + it "returns a newly created commit", :use_clean_rails_redis_caching do + # Parse the commits ourselves because json_response is cached + def commit_messages(response) + Gitlab::Json.parse(response.body)["commits"].map do |commit| + commit["message"] + end + end + + # First trigger the rate limit cache + get api(route, current_user), params: { from: 'master', to: 'feature' } + + expect(response).to have_gitlab_http_status(:ok) + expect(commit_messages(response)).not_to include("Cool new commit") + + # Then create a new commit via the API + post api("/projects/#{project.id}/repository/commits", user), params: { + branch: "feature", + commit_message: "Cool new commit", + actions: [ + { + action: "create", + file_path: "foo/bar/baz.txt", + content: "puts 8" + } + ] + } + + expect(response).to have_gitlab_http_status(:created) + + # Now perform the same query as before, but the cache should have expired + # and our new commit should exist + get api(route, current_user), params: { from: 'master', to: 'feature' } + + expect(response).to have_gitlab_http_status(:ok) + expect(commit_messages(response)).to include("Cool new commit") + end end context 'when unauthenticated', 'and project is public' do diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb index 423e19c3971..641c6a2cd91 100644 --- a/spec/requests/api/settings_spec.rb +++ b/spec/requests/api/settings_spec.rb @@ -612,5 +612,46 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting do expect(json_response.slice(*settings.keys)).to eq(settings) end end + + context 'Sentry settings' do + let(:settings) do + { + sentry_enabled: true, + sentry_dsn: 'http://sentry.example.com', + sentry_clientside_dsn: 'http://sentry.example.com', + sentry_environment: 'production' + } + end + + let(:attribute_names) { settings.keys.map(&:to_s) } + + it 'includes the attributes in the API' do + get api('/application/settings', admin) + + expect(response).to have_gitlab_http_status(:ok) + attribute_names.each do |attribute| + expect(json_response.keys).to include(attribute) + end + end + + it 'allows updating the settings' do + put api('/application/settings', admin), params: settings + + expect(response).to have_gitlab_http_status(:ok) + settings.each do |attribute, value| + expect(ApplicationSetting.current.public_send(attribute)).to eq(value) + end + end + + context 'missing sentry_dsn value when sentry_enabled is true' do + it 'returns a blank parameter error message' do + put api('/application/settings', admin), params: { sentry_enabled: true } + + expect(response).to have_gitlab_http_status(:bad_request) + message = json_response['message'] + expect(message["sentry_dsn"]).to include(a_string_matching("can't be blank")) + end + end + end end end diff --git a/spec/requests/api/snippets_spec.rb b/spec/requests/api/snippets_spec.rb index f4d15d0525e..dd5e6ac8a5e 100644 --- a/spec/requests/api/snippets_spec.rb +++ b/spec/requests/api/snippets_spec.rb @@ -113,6 +113,7 @@ RSpec.describe API::Snippets, factory_default: :keep do expect(response).to have_gitlab_http_status(:ok) expect(response.media_type).to eq 'text/plain' expect(headers['Content-Disposition']).to match(/^inline/) + expect(response.parsed_body).to be_empty end it 'returns 404 for invalid snippet id' do diff --git a/spec/requests/api/tags_spec.rb b/spec/requests/api/tags_spec.rb index 1aa1ad87be9..bb56192a2ff 100644 --- a/spec/requests/api/tags_spec.rb +++ b/spec/requests/api/tags_spec.rb @@ -17,6 +17,10 @@ RSpec.describe API::Tags do end describe 'GET /projects/:id/repository/tags' do + before do + stub_feature_flags(tag_list_keyset_pagination: false) + end + shared_examples "get repository tags" do let(:route) { "/projects/#{project_id}/repository/tags" } @@ -143,6 +147,55 @@ RSpec.describe API::Tags do expect(expected_tag['release']['description']).to eq(description) end end + + context 'with keyset pagination on', :aggregate_errors do + before do + stub_feature_flags(tag_list_keyset_pagination: true) + end + + context 'with keyset pagination option' do + let(:base_params) { { pagination: 'keyset' } } + + context 'with gitaly pagination params' do + context 'with high limit' do + let(:params) { base_params.merge(per_page: 100) } + + it 'returns all repository tags' do + get api(route, user), params: params + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema('public_api/v4/tags') + expect(response.headers).not_to include('Link') + tag_names = json_response.map { |x| x['name'] } + expect(tag_names).to match_array(project.repository.tag_names) + end + end + + context 'with low limit' do + let(:params) { base_params.merge(per_page: 2) } + + it 'returns limited repository tags' do + get api(route, user), params: params + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema('public_api/v4/tags') + expect(response.headers).to include('Link') + tag_names = json_response.map { |x| x['name'] } + expect(tag_names).to match_array(%w(v1.1.0 v1.1.1)) + end + end + + context 'with missing page token' do + let(:params) { base_params.merge(page_token: 'unknown') } + + it_behaves_like '422 response' do + let(:request) { get api(route, user), params: params } + let(:message) { 'Invalid page token: refs/tags/unknown' } + end + end + end + end + end end context ":api_caching_tags flag enabled", :use_clean_rails_memory_store_caching do @@ -208,6 +261,20 @@ RSpec.describe API::Tags do it_behaves_like "get repository tags" end + + context 'when gitaly is unavailable' do + let(:route) { "/projects/#{project_id}/repository/tags" } + + before do + expect_next_instance_of(TagsFinder) do |finder| + allow(finder).to receive(:execute).and_raise(Gitlab::Git::CommandError) + end + end + + it_behaves_like '503 response' do + let(:request) { get api(route, user) } + end + end end describe 'GET /projects/:id/repository/tags/:tag_name' do diff --git a/spec/requests/api/terraform/modules/v1/packages_spec.rb b/spec/requests/api/terraform/modules/v1/packages_spec.rb index b04f5ad9a94..b17bc11a451 100644 --- a/spec/requests/api/terraform/modules/v1/packages_spec.rb +++ b/spec/requests/api/terraform/modules/v1/packages_spec.rb @@ -28,10 +28,25 @@ RSpec.describe API::Terraform::Modules::V1::Packages do describe 'GET /api/v4/packages/terraform/modules/v1/:module_namespace/:module_name/:module_system/versions' do let(:url) { api("/packages/terraform/modules/v1/#{group.path}/#{package.name}/versions") } - let(:headers) { {} } + let(:headers) { { 'Authorization' => "Bearer #{tokens[:job_token]}" } } subject { get(url, headers: headers) } + context 'with a conflicting package name' do + let!(:conflicting_package) { create(:terraform_module_package, project: project, name: "conflict-#{package.name}", version: '2.0.0') } + + before do + group.add_developer(user) + end + + it 'returns only one version' do + subject + + expect(json_response['modules'][0]['versions'].size).to eq(1) + expect(json_response['modules'][0]['versions'][0]['version']).to eq('1.0.0') + end + end + context 'with valid namespace' do where(:visibility, :user_role, :member, :token_type, :valid_token, :shared_examples_name, :expected_status) do :public | :developer | true | :personal_access_token | true | 'returns terraform module packages' | :success diff --git a/spec/requests/api/todos_spec.rb b/spec/requests/api/todos_spec.rb index d31f571e636..c9deb84ff98 100644 --- a/spec/requests/api/todos_spec.rb +++ b/spec/requests/api/todos_spec.rb @@ -13,6 +13,8 @@ RSpec.describe API::Todos do let_it_be(:john_doe) { create(:user, username: 'john_doe') } let_it_be(:issue) { create(:issue, project: project_1) } let_it_be(:merge_request) { create(:merge_request, source_project: project_1) } + let_it_be(:alert) { create(:alert_management_alert, project: project_1) } + let_it_be(:alert_todo) { create(:todo, project: project_1, author: john_doe, user: john_doe, target: alert) } let_it_be(:merge_request_todo) { create(:todo, project: project_1, author: author_2, user: john_doe, target: merge_request) } let_it_be(:pending_1) { create(:todo, :mentioned, project: project_1, author: author_1, user: john_doe, target: issue) } let_it_be(:pending_2) { create(:todo, project: project_2, author: author_2, user: john_doe, target: issue) } @@ -67,7 +69,7 @@ RSpec.describe API::Todos do expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers expect(json_response).to be_an Array - expect(json_response.length).to eq(4) + expect(json_response.length).to eq(5) expect(json_response[0]['id']).to eq(pending_3.id) expect(json_response[0]['project']).to be_a Hash expect(json_response[0]['author']).to be_a Hash @@ -95,6 +97,10 @@ RSpec.describe API::Todos do expect(json_response[3]['target']['merge_requests_count']).to be_nil expect(json_response[3]['target']['upvotes']).to eq(1) expect(json_response[3]['target']['downvotes']).to eq(0) + + expect(json_response[4]['target_type']).to eq('AlertManagement::Alert') + expect(json_response[4]['target']['iid']).to eq(alert.iid) + expect(json_response[4]['target']['title']).to eq(alert.title) end context "when current user does not have access to one of the TODO's target" do @@ -105,7 +111,7 @@ RSpec.describe API::Todos do get api('/todos', john_doe) - expect(json_response.count).to eq(4) + expect(json_response.count).to eq(5) expect(json_response.map { |t| t['id'] }).not_to include(no_access_todo.id, pending_4.id) end end @@ -163,7 +169,7 @@ RSpec.describe API::Todos do expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers expect(json_response).to be_an Array - expect(json_response.length).to eq(3) + expect(json_response.length).to eq(4) end end diff --git a/spec/requests/api/topics_spec.rb b/spec/requests/api/topics_spec.rb new file mode 100644 index 00000000000..a5746a4022e --- /dev/null +++ b/spec/requests/api/topics_spec.rb @@ -0,0 +1,217 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe API::Topics do + include WorkhorseHelpers + + let_it_be(:topic_1) { create(:topic, name: 'Git', total_projects_count: 1) } + 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(:admin) { create(:user, :admin) } + let_it_be(:user) { create(:user) } + + let(:file) { fixture_file_upload('spec/fixtures/dk.png') } + + describe 'GET /topics', :aggregate_failures do + it 'returns topics ordered by total_projects_count' do + get api('/topics') + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.length).to eq(3) + + expect(json_response[0]['id']).to eq(topic_3.id) + expect(json_response[0]['name']).to eq('other-topic') + expect(json_response[0]['total_projects_count']).to eq(3) + + expect(json_response[1]['id']).to eq(topic_2.id) + expect(json_response[1]['name']).to eq('GitLab') + expect(json_response[1]['total_projects_count']).to eq(2) + + expect(json_response[2]['id']).to eq(topic_1.id) + expect(json_response[2]['name']).to eq('Git') + expect(json_response[2]['total_projects_count']).to eq(1) + end + + context 'with search' do + using RSpec::Parameterized::TableSyntax + + where(:search, :result) do + '' | %w[other-topic GitLab Git] + 'g' | %w[] + 'gi' | %w[] + 'git' | %w[Git GitLab] + 'x' | %w[] + 0 | %w[] + end + + with_them do + it 'returns filtered topics' do + get api('/topics'), params: { search: search } + + expect(json_response.map { |t| t['name'] }).to eq(result) + end + end + end + + context 'with pagination' do + using RSpec::Parameterized::TableSyntax + + where(:params, :result) do + { page: 0 } | %w[other-topic GitLab Git] + { page: 1 } | %w[other-topic GitLab Git] + { page: 2 } | %w[] + { per_page: 1 } | %w[other-topic] + { per_page: 2 } | %w[other-topic GitLab] + { per_page: 3 } | %w[other-topic GitLab Git] + { page: 0, per_page: 1 } | %w[other-topic] + { page: 0, per_page: 2 } | %w[other-topic GitLab] + { page: 1, per_page: 1 } | %w[other-topic] + { page: 1, per_page: 2 } | %w[other-topic GitLab] + { page: 2, per_page: 1 } | %w[GitLab] + { page: 2, per_page: 2 } | %w[Git] + { page: 3, per_page: 1 } | %w[Git] + { page: 3, per_page: 2 } | %w[] + { page: 4, per_page: 1 } | %w[] + { page: 4, per_page: 2 } | %w[] + end + + with_them do + it 'returns paginated topics' do + get api('/topics'), params: params + + expect(json_response.map { |t| t['name'] }).to eq(result) + end + end + end + end + + describe 'GET /topic/:id', :aggregate_failures do + it 'returns topic' do + get api("/topics/#{topic_2.id}") + + expect(response).to have_gitlab_http_status(:ok) + + expect(json_response['id']).to eq(topic_2.id) + expect(json_response['name']).to eq('GitLab') + expect(json_response['total_projects_count']).to eq(2) + end + + it 'returns 404 for non existing id' do + get api("/topics/#{non_existing_record_id}") + + expect(response).to have_gitlab_http_status(:not_found) + end + + it 'returns 400 for invalid `id` parameter' do + get api('/topics/invalid') + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['error']).to eql('id is invalid') + end + end + + describe 'POST /topics', :aggregate_failures do + context 'as administrator' do + it 'creates a topic' do + post api('/topics/', admin), params: { name: 'my-topic' } + + expect(response).to have_gitlab_http_status(:created) + expect(json_response['name']).to eq('my-topic') + expect(Projects::Topic.find(json_response['id']).name).to eq('my-topic') + end + + it 'creates a topic with avatar and description' do + workhorse_form_with_file( + api('/topics/', admin), + file_key: :avatar, + params: { name: 'my-topic', description: 'my description...', avatar: file } + ) + + expect(response).to have_gitlab_http_status(:created) + expect(json_response['description']).to eq('my description...') + expect(json_response['avatar_url']).to end_with('dk.png') + end + + it 'returns 400 if name is missing' do + post api('/topics/', admin) + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['error']).to eql('name is missing') + end + end + + context 'as normal user' do + it 'returns 403 Forbidden' do + post api('/topics/', user), params: { name: 'my-topic' } + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + + context 'as anonymous' do + it 'returns 401 Unauthorized' do + post api('/topics/'), params: { name: 'my-topic' } + + expect(response).to have_gitlab_http_status(:unauthorized) + end + end + end + + describe 'PUT /topics', :aggregate_failures do + context 'as administrator' do + it 'updates a topic' do + put api("/topics/#{topic_3.id}", admin), params: { name: 'my-topic' } + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['name']).to eq('my-topic') + expect(topic_3.reload.name).to eq('my-topic') + end + + it 'updates a topic with avatar and description' do + workhorse_form_with_file( + api("/topics/#{topic_3.id}", admin), + method: :put, + file_key: :avatar, + params: { description: 'my description...', avatar: file } + ) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['description']).to eq('my description...') + expect(json_response['avatar_url']).to end_with('dk.png') + end + + it 'returns 404 for non existing id' do + put 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 + put 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 + put 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 + put 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/users_spec.rb b/spec/requests/api/users_spec.rb index fb01845b63a..b93df2f3bae 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -1464,6 +1464,7 @@ RSpec.describe API::Users do credit_card_expiration_year: expiration_year, credit_card_expiration_month: 1, credit_card_holder_name: 'John Smith', + credit_card_type: 'AmericanExpress', credit_card_mask_number: '1111' } end @@ -1495,6 +1496,7 @@ RSpec.describe API::Users do credit_card_validated_at: credit_card_validated_time, expiration_date: Date.new(expiration_year, 1, 31), last_digits: 1111, + network: 'AmericanExpress', holder_name: 'John Smith' ) end @@ -1904,7 +1906,8 @@ RSpec.describe API::Users do expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers expect(json_response).to be_an Array - expect(json_response.first['email']).to eq(email.email) + expect(json_response.first['email']).to eq(user.email) + expect(json_response.second['email']).to eq(email.email) end it "returns a 404 for invalid ID" do @@ -2486,7 +2489,8 @@ RSpec.describe API::Users do expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers expect(json_response).to be_an Array - expect(json_response.first["email"]).to eq(email.email) + expect(json_response.first['email']).to eq(user.email) + expect(json_response.second['email']).to eq(email.email) end context "scopes" do diff --git a/spec/requests/api/v3/github_spec.rb b/spec/requests/api/v3/github_spec.rb index 255f53e4c7c..6d8ae226ce4 100644 --- a/spec/requests/api/v3/github_spec.rb +++ b/spec/requests/api/v3/github_spec.rb @@ -6,7 +6,7 @@ RSpec.describe API::V3::Github do let_it_be(:user) { create(:user) } let_it_be(:unauthorized_user) { create(:user) } let_it_be(:admin) { create(:user, :admin) } - let_it_be(:project) { create(:project, :repository, creator: user) } + let_it_be_with_reload(:project) { create(:project, :repository, creator: user) } before do project.add_maintainer(user) @@ -506,11 +506,18 @@ RSpec.describe API::V3::Github do describe 'GET /repos/:namespace/:project/commits/:sha' do let(:commit) { project.repository.commit } - let(:commit_id) { commit.id } + + def call_api(commit_id: commit.id) + jira_get v3_api("/repos/#{project.namespace.path}/#{project.path}/commits/#{commit_id}", user) + end + + def response_diff_files(response) + Gitlab::Json.parse(response.body)['files'] + end context 'authenticated' do - it 'returns commit with github format' do - jira_get v3_api("/repos/#{project.namespace.path}/#{project.path}/commits/#{commit_id}", user) + it 'returns commit with github format', :aggregate_failures do + call_api expect(response).to have_gitlab_http_status(:ok) expect(response).to match_response_schema('entities/github/commit') @@ -519,36 +526,130 @@ RSpec.describe API::V3::Github do it 'returns 200 when project path include a dot' do project.update!(path: 'foo.bar') - jira_get v3_api("/repos/#{project.namespace.path}/#{project.path}/commits/#{commit_id}", user) + call_api expect(response).to have_gitlab_http_status(:ok) end - it 'returns 200 when namespace path include a dot' do - group = create(:group, path: 'foo.bar') - project = create(:project, :repository, group: group) - project.add_reporter(user) + context 'when namespace path includes a dot' do + let(:group) { create(:group, path: 'foo.bar') } + let(:project) { create(:project, :repository, group: group) } - jira_get v3_api("/repos/#{group.path}/#{project.path}/commits/#{commit_id}", user) + it 'returns 200 when namespace path include a dot' do + project.add_reporter(user) - expect(response).to have_gitlab_http_status(:ok) + call_api + + expect(response).to have_gitlab_http_status(:ok) + end + end + + context 'when the Gitaly `CommitDiff` RPC times out', :use_clean_rails_memory_store_caching do + let(:commit_diff_args) { [project.repository_storage, :diff_service, :commit_diff, any_args] } + + before do + allow(Gitlab::GitalyClient).to receive(:call) + .and_call_original + end + + it 'handles the error, logs it, and returns empty diff files', :aggregate_failures do + allow(Gitlab::GitalyClient).to receive(:call) + .with(*commit_diff_args) + .and_raise(GRPC::DeadlineExceeded) + + expect(Gitlab::ErrorTracking) + .to receive(:track_exception) + .with an_instance_of(GRPC::DeadlineExceeded) + + call_api + + expect(response).to have_gitlab_http_status(:ok) + expect(response_diff_files(response)).to be_blank + end + + it 'does not handle the error when feature flag is disabled', :aggregate_failures do + stub_feature_flags(api_v3_commits_skip_diff_files: false) + + allow(Gitlab::GitalyClient).to receive(:call) + .with(*commit_diff_args) + .and_raise(GRPC::DeadlineExceeded) + + call_api + + expect(response).to have_gitlab_http_status(:error) + end + + it 'only calls Gitaly once for all attempts within a period of time', :aggregate_failures do + expect(Gitlab::GitalyClient).to receive(:call) + .with(*commit_diff_args) + .once # <- once + .and_raise(GRPC::DeadlineExceeded) + + 3.times do + call_api + + expect(response).to have_gitlab_http_status(:ok) + expect(response_diff_files(response)).to be_blank + end + end + + it 'calls Gitaly again after a period of time', :aggregate_failures do + expect(Gitlab::GitalyClient).to receive(:call) + .with(*commit_diff_args) + .twice # <- twice + .and_raise(GRPC::DeadlineExceeded) + + call_api + + expect(response).to have_gitlab_http_status(:ok) + expect(response_diff_files(response)).to be_blank + + travel_to((described_class::GITALY_TIMEOUT_CACHE_EXPIRY + 1.second).from_now) do + call_api + + expect(response).to have_gitlab_http_status(:ok) + expect(response_diff_files(response)).to be_blank + end + end + + it 'uses a unique cache key, allowing other calls to succeed' do + cache_key = [described_class::GITALY_TIMEOUT_CACHE_KEY, project.id, commit.cache_key].join(':') + Rails.cache.write(cache_key, 1) + + expect(Gitlab::GitalyClient).to receive(:call) + .with(*commit_diff_args) + .once # <- once + + call_api + + expect(response).to have_gitlab_http_status(:ok) + expect(response_diff_files(response)).to be_blank + + call_api(commit_id: commit.parent.id) + + expect(response).to have_gitlab_http_status(:ok) + expect(response_diff_files(response).length).to eq(1) + end end end context 'unauthenticated' do + let(:user) { nil } + it 'returns 401' do - jira_get v3_api("/repos/#{project.namespace.path}/#{project.path}/commits/#{commit_id}", nil) + call_api expect(response).to have_gitlab_http_status(:unauthorized) end end context 'unauthorized' do + let(:user) { unauthorized_user } + it 'returns 404 when lower access level' do - project.add_guest(unauthorized_user) + project.add_guest(user) - jira_get v3_api("/repos/#{project.namespace.path}/#{project.path}/commits/#{commit_id}", - unauthorized_user) + call_api expect(response).to have_gitlab_http_status(:not_found) end |