diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-08-20 21:42:06 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-08-20 21:42:06 +0300 |
commit | 6e4e1050d9dba2b7b2523fdd1768823ab85feef4 (patch) | |
tree | 78be5963ec075d80116a932011d695dd33910b4e /spec/requests/api | |
parent | 1ce776de4ae122aba3f349c02c17cebeaa8ecf07 (diff) |
Add latest changes from gitlab-org/gitlab@13-3-stable-ee
Diffstat (limited to 'spec/requests/api')
88 files changed, 5273 insertions, 3310 deletions
diff --git a/spec/requests/api/admin/ci/variables_spec.rb b/spec/requests/api/admin/ci/variables_spec.rb index 812ee93ad21..f89964411f8 100644 --- a/spec/requests/api/admin/ci/variables_spec.rb +++ b/spec/requests/api/admin/ci/variables_spec.rb @@ -110,20 +110,19 @@ RSpec.describe ::API::Admin::Ci::Variables do expect(response).to have_gitlab_http_status(:bad_request) end - it 'does not allow values above 700 characters' do + it 'does not allow values above 10,000 characters' do too_long_message = <<~MESSAGE.strip - The encrypted value of the provided variable exceeds 1024 bytes. \ - Variables over 700 characters risk exceeding the limit. + The value of the provided variable exceeds the 10000 character limit MESSAGE expect do post api('/admin/ci/variables', admin), - params: { key: 'too_long', value: SecureRandom.hex(701) } + params: { key: 'too_long', value: SecureRandom.hex(10_001) } end.not_to change { ::Ci::InstanceVariable.count } expect(response).to have_gitlab_http_status(:bad_request) expect(json_response).to match('message' => - a_hash_including('encrypted_value' => [too_long_message])) + a_hash_including('value' => [too_long_message])) end end diff --git a/spec/requests/api/branches_spec.rb b/spec/requests/api/branches_spec.rb index 46acd92803f..4b9b82b3a5b 100644 --- a/spec/requests/api/branches_spec.rb +++ b/spec/requests/api/branches_spec.rb @@ -39,9 +39,11 @@ RSpec.describe API::Branches do end context 'with branch_list_keyset_pagination feature off' do - context 'with legacy pagination params' do + let(:base_params) { {} } + + context 'with offset pagination params' do it 'returns the repository branches' do - get api(route, current_user), params: { per_page: 100 } + get api(route, current_user), params: base_params.merge(per_page: 100) expect(response).to have_gitlab_http_status(:ok) expect(response).to match_response_schema('public_api/v4/branches') @@ -53,7 +55,7 @@ RSpec.describe API::Branches do it 'determines only a limited number of merged branch names' do expect(API::Entities::Branch).to receive(:represent).with(anything, has_up_to_merged_branch_names_count(2)).and_call_original - get api(route, current_user), params: { per_page: 2 } + get api(route, current_user), params: base_params.merge(per_page: 2) expect(response).to have_gitlab_http_status(:ok) expect(json_response.count).to eq 2 @@ -64,7 +66,7 @@ RSpec.describe API::Branches do it 'merge status matches reality on paginated input' do expected_first_branch_name = project.repository.branches_sorted_by('name')[20].name - get api(route, current_user), params: { per_page: 20, page: 2 } + get api(route, current_user), params: base_params.merge(per_page: 20, page: 2) expect(response).to have_gitlab_http_status(:ok) expect(json_response.count).to eq 20 @@ -74,11 +76,11 @@ RSpec.describe API::Branches do end end - context 'with gitaly pagination params ' do + context 'with gitaly pagination params' do it 'merge status matches reality on paginated input' do expected_first_branch_name = project.repository.branches_sorted_by('name').first.name - get api(route, current_user), params: { per_page: 20, page_token: 'feature' } + get api(route, current_user), params: base_params.merge(per_page: 20, page_token: 'feature') expect(response).to have_gitlab_http_status(:ok) expect(json_response.count).to eq 20 @@ -91,52 +93,58 @@ RSpec.describe API::Branches do context 'with branch_list_keyset_pagination feature on' do before do - stub_feature_flags(branch_list_keyset_pagination: true) + stub_feature_flags(branch_list_keyset_pagination: project) end - context 'with gitaly pagination params ' do - it 'returns the repository branches' do - get api(route, current_user), params: { per_page: 100 } + context 'with keyset pagination option' do + let(:base_params) { { pagination: 'keyset' } } - expect(response).to have_gitlab_http_status(:ok) - expect(response).to match_response_schema('public_api/v4/branches') - branch_names = json_response.map { |x| x['name'] } - expect(branch_names).to match_array(project.repository.branch_names) - end + context 'with gitaly pagination params ' do + it 'returns the repository branches' do + get api(route, current_user), params: base_params.merge(per_page: 100) - it 'determines only a limited number of merged branch names' do - expect(API::Entities::Branch).to receive(:represent).with(anything, has_up_to_merged_branch_names_count(2)).and_call_original + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema('public_api/v4/branches') + expect(response.headers).not_to include('Link', 'Links') + branch_names = json_response.map { |x| x['name'] } + expect(branch_names).to match_array(project.repository.branch_names) + end - get api(route, current_user), params: { per_page: 2 } + it 'determines only a limited number of merged branch names' do + expect(API::Entities::Branch).to receive(:represent).with(anything, has_up_to_merged_branch_names_count(2)).and_call_original - expect(response).to have_gitlab_http_status(:ok) - expect(json_response.count).to eq 2 + get api(route, current_user), params: base_params.merge(per_page: 2) - check_merge_status(json_response) - end + expect(response).to have_gitlab_http_status(:ok) + expect(response.headers).to include('Link', 'Links') + expect(json_response.count).to eq 2 - it 'merge status matches reality on paginated input' do - expected_first_branch_name = project.repository.branches_sorted_by('name').drop_while { |b| b.name <= 'feature' }.first.name + check_merge_status(json_response) + end - get api(route, current_user), params: { per_page: 20, page_token: 'feature' } + it 'merge status matches reality on paginated input' do + expected_first_branch_name = project.repository.branches_sorted_by('name').drop_while { |b| b.name <= 'feature' }.first.name - expect(response).to have_gitlab_http_status(:ok) - expect(json_response.count).to eq 20 - expect(json_response.first['name']).to eq(expected_first_branch_name) + get api(route, current_user), params: base_params.merge(per_page: 20, page_token: 'feature') - check_merge_status(json_response) + expect(response).to have_gitlab_http_status(:ok) + expect(json_response.count).to eq 20 + expect(json_response.first['name']).to eq(expected_first_branch_name) + + check_merge_status(json_response) + end end - end - context 'with legacy pagination params' do - it 'ignores legacy pagination params' do - expected_first_branch_name = project.repository.branches_sorted_by('name').first.name - get api(route, current_user), params: { per_page: 20, page: 2 } + context 'with offset pagination params' do + it 'ignores legacy pagination params' do + expected_first_branch_name = project.repository.branches_sorted_by('name').first.name + get api(route, current_user), params: base_params.merge(per_page: 20, page: 2) - expect(response).to have_gitlab_http_status(:ok) - expect(json_response.first['name']).to eq(expected_first_branch_name) + expect(response).to have_gitlab_http_status(:ok) + expect(json_response.first['name']).to eq(expected_first_branch_name) - check_merge_status(json_response) + check_merge_status(json_response) + end end end end diff --git a/spec/requests/api/ci/pipelines_spec.rb b/spec/requests/api/ci/pipelines_spec.rb index c9ca806e2c4..111bc933ea4 100644 --- a/spec/requests/api/ci/pipelines_spec.rb +++ b/spec/requests/api/ci/pipelines_spec.rb @@ -438,7 +438,7 @@ RSpec.describe API::Ci::Pipelines do expect(response).to match_response_schema('public_api/v4/pipeline/detail') end - it 'returns project pipelines' do + it 'returns project pipeline' do get api("/projects/#{project.id}/pipelines/#{pipeline.id}", user) expect(response).to have_gitlab_http_status(:ok) @@ -475,6 +475,20 @@ RSpec.describe API::Ci::Pipelines do expect(json_response['id']).to be nil end end + + context 'when config source is not ci' do + let(:non_ci_config_source) { ::Ci::PipelineEnums.non_ci_config_source_values.first } + let(:pipeline_not_ci) do + create(:ci_pipeline, config_source: non_ci_config_source, project: project) + end + + it 'returns the specified pipeline' do + get api("/projects/#{project.id}/pipelines/#{pipeline_not_ci.id}", user) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['sha']).to eq(pipeline_not_ci.sha) + end + end end describe 'GET /projects/:id/pipelines/latest' do @@ -721,55 +735,36 @@ RSpec.describe API::Ci::Pipelines do let(:pipeline) { create(:ci_pipeline, project: project) } - context 'when feature is enabled' do - before do - stub_feature_flags(junit_pipeline_view: true) - end - - context 'when pipeline does not have a test report' do - it 'returns an empty test report' do - subject - - expect(response).to have_gitlab_http_status(:ok) - expect(json_response['total_count']).to eq(0) - end - end - - context 'when pipeline has a test report' do - let(:pipeline) { create(:ci_pipeline, :with_test_reports, project: project) } - - it 'returns the test report' do - subject + context 'when pipeline does not have a test report' do + it 'returns an empty test report' do + subject - expect(response).to have_gitlab_http_status(:ok) - expect(json_response['total_count']).to eq(4) - end + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['total_count']).to eq(0) end + end - context 'when pipeline has corrupt test reports' do - before do - job = create(:ci_build, pipeline: pipeline) - create(:ci_job_artifact, :junit_with_corrupted_data, job: job, project: project) - end + context 'when pipeline has a test report' do + let(:pipeline) { create(:ci_pipeline, :with_test_reports, project: project) } - it 'returns a suite_error' do - subject + it 'returns the test report' do + subject - expect(response).to have_gitlab_http_status(:ok) - expect(json_response['test_suites'].first['suite_error']).to eq('JUnit XML parsing failed: 1:1: FATAL: Document is empty') - end + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['total_count']).to eq(4) end end - context 'when feature is disabled' do + context 'when pipeline has corrupt test reports' do before do - stub_feature_flags(junit_pipeline_view: false) + create(:ci_build, :broken_test_reports, name: 'rspec', pipeline: pipeline) end - it 'renders empty response' do + it 'returns a suite_error' do subject - expect(response).to have_gitlab_http_status(:not_found) + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['test_suites'].first['suite_error']).to eq('JUnit XML parsing failed: 1:1: FATAL: Document is empty') end end end diff --git a/spec/requests/api/ci/runner/jobs_artifacts_spec.rb b/spec/requests/api/ci/runner/jobs_artifacts_spec.rb new file mode 100644 index 00000000000..e5c60bb539b --- /dev/null +++ b/spec/requests/api/ci/runner/jobs_artifacts_spec.rb @@ -0,0 +1,901 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do + include StubGitlabCalls + include RedisHelpers + include WorkhorseHelpers + + let(:registration_token) { 'abcdefg123456' } + + before do + stub_feature_flags(ci_enable_live_trace: true) + stub_gitlab_calls + stub_application_setting(runners_registration_token: registration_token) + allow_any_instance_of(::Ci::Runner).to receive(:cache_attributes) + end + + describe '/api/v4/jobs' do + let(:root_namespace) { create(:namespace) } + let(:namespace) { create(:namespace, parent: root_namespace) } + let(:project) { create(:project, namespace: namespace, shared_runners_enabled: false) } + let(:pipeline) { create(:ci_pipeline, project: project, ref: 'master') } + let(:runner) { create(:ci_runner, :project, projects: [project]) } + let(:user) { create(:user) } + let(:job) do + create(:ci_build, :artifacts, :extended_options, + pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) + end + + describe 'artifacts' do + let(:job) { create(:ci_build, :pending, user: user, project: project, pipeline: pipeline, runner_id: runner.id) } + let(:jwt) { JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256') } + let(:headers) { { 'GitLab-Workhorse' => '1.0', Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER => jwt } } + let(:headers_with_token) { headers.merge(API::Helpers::Runner::JOB_TOKEN_HEADER => job.token) } + let(:file_upload) { fixture_file_upload('spec/fixtures/banana_sample.gif', 'image/gif') } + let(:file_upload2) { fixture_file_upload('spec/fixtures/dk.png', 'image/gif') } + + before do + stub_artifacts_object_storage + job.run! + end + + shared_examples_for 'rejecting artifacts that are too large' do + let(:filesize) { 100.megabytes.to_i } + let(:sample_max_size) { (filesize / 1.megabyte) - 10 } # Set max size to be smaller than file size to trigger error + + shared_examples_for 'failed request' do + it 'responds with payload too large error' do + send_request + + expect(response).to have_gitlab_http_status(:payload_too_large) + end + end + + context 'based on plan limit setting' do + let(:application_max_size) { sample_max_size + 100 } + let(:limit_name) { "#{Ci::JobArtifact::PLAN_LIMIT_PREFIX}archive" } + + before do + create(:plan_limits, :default_plan, limit_name => sample_max_size) + stub_application_setting(max_artifacts_size: application_max_size) + end + + it_behaves_like 'failed request' + end + + context 'based on application setting' do + before do + stub_application_setting(max_artifacts_size: sample_max_size) + end + + it_behaves_like 'failed request' + end + + context 'based on root namespace setting' do + let(:application_max_size) { sample_max_size + 10 } + + before do + stub_application_setting(max_artifacts_size: application_max_size) + root_namespace.update!(max_artifacts_size: sample_max_size) + end + + it_behaves_like 'failed request' + end + + context 'based on child namespace setting' do + let(:application_max_size) { sample_max_size + 10 } + let(:root_namespace_max_size) { sample_max_size + 10 } + + before do + stub_application_setting(max_artifacts_size: application_max_size) + root_namespace.update!(max_artifacts_size: root_namespace_max_size) + namespace.update!(max_artifacts_size: sample_max_size) + end + + it_behaves_like 'failed request' + end + + context 'based on project setting' do + let(:application_max_size) { sample_max_size + 10 } + let(:root_namespace_max_size) { sample_max_size + 10 } + let(:child_namespace_max_size) { sample_max_size + 10 } + + before do + stub_application_setting(max_artifacts_size: application_max_size) + root_namespace.update!(max_artifacts_size: root_namespace_max_size) + namespace.update!(max_artifacts_size: child_namespace_max_size) + project.update!(max_artifacts_size: sample_max_size) + end + + it_behaves_like 'failed request' + end + end + + describe 'POST /api/v4/jobs/:id/artifacts/authorize' do + context 'when using token as parameter' do + context 'and the artifact is too large' do + it_behaves_like 'rejecting artifacts that are too large' do + let(:success_code) { :ok } + let(:send_request) { authorize_artifacts_with_token_in_params(filesize: filesize) } + end + end + + context 'posting artifacts to running job' do + subject do + authorize_artifacts_with_token_in_params + end + + it_behaves_like 'API::CI::Runner application context metadata', '/api/:version/jobs/:id/artifacts/authorize' do + let(:send_request) { subject } + end + + it 'updates runner info' do + expect { subject }.to change { runner.reload.contacted_at } + end + + shared_examples 'authorizes local file' do + it 'succeeds' do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(response.media_type).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE) + expect(json_response['TempPath']).to eq(JobArtifactUploader.workhorse_local_upload_path) + expect(json_response['RemoteObject']).to be_nil + end + end + + context 'when using local storage' do + it_behaves_like 'authorizes local file' + end + + context 'when using remote storage' do + context 'when direct upload is enabled' do + before do + stub_artifacts_object_storage(enabled: true, direct_upload: true) + end + + it 'succeeds' do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(response.media_type).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE) + expect(json_response).not_to have_key('TempPath') + expect(json_response['RemoteObject']).to have_key('ID') + expect(json_response['RemoteObject']).to have_key('GetURL') + expect(json_response['RemoteObject']).to have_key('StoreURL') + expect(json_response['RemoteObject']).to have_key('DeleteURL') + expect(json_response['RemoteObject']).to have_key('MultipartUpload') + end + end + + context 'when direct upload is disabled' do + before do + stub_artifacts_object_storage(enabled: true, direct_upload: false) + end + + it_behaves_like 'authorizes local file' + end + end + end + end + + context 'when using token as header' do + it 'authorizes posting artifacts to running job' do + authorize_artifacts_with_token_in_headers + + expect(response).to have_gitlab_http_status(:ok) + expect(response.media_type).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE) + expect(json_response['TempPath']).not_to be_nil + end + + it 'fails to post too large artifact' do + stub_application_setting(max_artifacts_size: 0) + + authorize_artifacts_with_token_in_headers(filesize: 100) + + expect(response).to have_gitlab_http_status(:payload_too_large) + end + end + + context 'when using runners token' do + it 'fails to authorize artifacts posting' do + authorize_artifacts(token: job.project.runners_token) + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + + it 'reject requests that did not go through gitlab-workhorse' do + headers.delete(Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER) + + authorize_artifacts + + expect(response).to have_gitlab_http_status(:forbidden) + end + + context 'authorization token is invalid' do + it 'responds with forbidden' do + authorize_artifacts(token: 'invalid', filesize: 100 ) + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + + context 'authorize uploading of an lsif artifact' do + before do + stub_feature_flags(code_navigation: job.project) + end + + it 'adds ProcessLsif header' do + authorize_artifacts_with_token_in_headers(artifact_type: :lsif) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['ProcessLsif']).to be_truthy + end + + it 'adds ProcessLsifReferences header' do + authorize_artifacts_with_token_in_headers(artifact_type: :lsif) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['ProcessLsifReferences']).to be_truthy + end + + context 'code_navigation feature flag is disabled' do + it 'responds with a forbidden error' do + stub_feature_flags(code_navigation: false) + authorize_artifacts_with_token_in_headers(artifact_type: :lsif) + + aggregate_failures do + expect(response).to have_gitlab_http_status(:forbidden) + expect(json_response['ProcessLsif']).to be_falsy + expect(json_response['ProcessLsifReferences']).to be_falsy + end + end + end + + context 'code_navigation_references feature flag is disabled' do + it 'sets ProcessLsifReferences header to false' do + stub_feature_flags(code_navigation_references: false) + authorize_artifacts_with_token_in_headers(artifact_type: :lsif) + + aggregate_failures do + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['ProcessLsif']).to be_truthy + expect(json_response['ProcessLsifReferences']).to be_falsy + end + end + end + end + + def authorize_artifacts(params = {}, request_headers = headers) + post api("/jobs/#{job.id}/artifacts/authorize"), params: params, headers: request_headers + end + + def authorize_artifacts_with_token_in_params(params = {}, request_headers = headers) + params = params.merge(token: job.token) + authorize_artifacts(params, request_headers) + end + + def authorize_artifacts_with_token_in_headers(params = {}, request_headers = headers_with_token) + authorize_artifacts(params, request_headers) + end + end + + describe 'POST /api/v4/jobs/:id/artifacts' do + it_behaves_like 'API::CI::Runner application context metadata', '/api/:version/jobs/:id/artifacts' do + let(:send_request) do + upload_artifacts(file_upload, headers_with_token) + end + end + + it 'updates runner info' do + expect { upload_artifacts(file_upload, headers_with_token) }.to change { runner.reload.contacted_at } + end + + context 'when the artifact is too large' do + it_behaves_like 'rejecting artifacts that are too large' do + # This filesize validation also happens in non remote stored files, + # it's just that it's hard to stub the filesize in other cases to be + # more than a megabyte. + let!(:fog_connection) do + stub_artifacts_object_storage(direct_upload: true) + end + + let(:file_upload) { fog_to_uploaded_file(object) } + let(:success_code) { :created } + + let(:object) do + fog_connection.directories.new(key: 'artifacts').files.create( # rubocop:disable Rails/SaveBang + key: 'tmp/uploads/12312300', + body: 'content' + ) + end + + let(:send_request) do + upload_artifacts(file_upload, headers_with_token, 'file.remote_id' => '12312300') + end + + before do + allow(object).to receive(:content_length).and_return(filesize) + end + end + end + + context 'when artifacts are being stored inside of tmp path' do + before do + # by configuring this path we allow to pass temp file from any path + allow(JobArtifactUploader).to receive(:workhorse_upload_path).and_return('/') + end + + context 'when job has been erased' do + let(:job) { create(:ci_build, erased_at: Time.now) } + + before do + upload_artifacts(file_upload, headers_with_token) + end + + it 'responds with forbidden' do + upload_artifacts(file_upload, headers_with_token) + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + + context 'when job is running' do + shared_examples 'successful artifacts upload' do + it 'updates successfully' do + expect(response).to have_gitlab_http_status(:created) + end + end + + context 'when uses accelerated file post' do + context 'for file stored locally' do + before do + upload_artifacts(file_upload, headers_with_token) + end + + it_behaves_like 'successful artifacts upload' + end + + context 'for file stored remotely' do + let!(:fog_connection) do + stub_artifacts_object_storage(direct_upload: true) + end + + let(:object) do + fog_connection.directories.new(key: 'artifacts').files.create( # rubocop:disable Rails/SaveBang + key: 'tmp/uploads/12312300', + body: 'content' + ) + end + + let(:file_upload) { fog_to_uploaded_file(object) } + + before do + upload_artifacts(file_upload, headers_with_token, 'file.remote_id' => remote_id) + end + + context 'when valid remote_id is used' do + let(:remote_id) { '12312300' } + + it_behaves_like 'successful artifacts upload' + end + + context 'when invalid remote_id is used' do + let(:remote_id) { 'invalid id' } + + it 'responds with bad request' do + expect(response).to have_gitlab_http_status(:internal_server_error) + expect(json_response['message']).to eq("Missing file") + end + end + end + end + + context 'when using runners token' do + it 'responds with forbidden' do + upload_artifacts(file_upload, headers.merge(API::Helpers::Runner::JOB_TOKEN_HEADER => job.project.runners_token)) + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + end + + context 'when artifacts post request does not contain file' do + it 'fails to post artifacts without file' do + post api("/jobs/#{job.id}/artifacts"), params: {}, headers: headers_with_token + + expect(response).to have_gitlab_http_status(:bad_request) + end + end + + context 'GitLab Workhorse is not configured' do + it 'fails to post artifacts without GitLab-Workhorse' do + post api("/jobs/#{job.id}/artifacts"), params: { token: job.token }, headers: {} + + expect(response).to have_gitlab_http_status(:bad_request) + end + end + + context 'Is missing GitLab Workhorse token headers' do + let(:jwt) { JWT.encode({ 'iss' => 'invalid-header' }, Gitlab::Workhorse.secret, 'HS256') } + + it 'fails to post artifacts without GitLab-Workhorse' do + expect(Gitlab::ErrorTracking).to receive(:track_exception).once + + upload_artifacts(file_upload, headers_with_token) + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + + context 'when setting an expire date' do + let(:default_artifacts_expire_in) {} + let(:post_data) do + { file: file_upload, + expire_in: expire_in } + end + + before do + stub_application_setting(default_artifacts_expire_in: default_artifacts_expire_in) + + upload_artifacts(file_upload, headers_with_token, post_data) + end + + context 'when an expire_in is given' do + let(:expire_in) { '7 days' } + + it 'updates when specified' do + expect(response).to have_gitlab_http_status(:created) + expect(job.reload.artifacts_expire_at).to be_within(5.minutes).of(7.days.from_now) + end + end + + context 'when no expire_in is given' do + let(:expire_in) { nil } + + it 'ignores if not specified' do + expect(response).to have_gitlab_http_status(:created) + expect(job.reload.artifacts_expire_at).to be_nil + end + + context 'with application default' do + context 'when default is 5 days' do + let(:default_artifacts_expire_in) { '5 days' } + + it 'sets to application default' do + expect(response).to have_gitlab_http_status(:created) + expect(job.reload.artifacts_expire_at).to be_within(5.minutes).of(5.days.from_now) + end + end + + context 'when default is 0' do + let(:default_artifacts_expire_in) { '0' } + + it 'does not set expire_in' do + expect(response).to have_gitlab_http_status(:created) + expect(job.reload.artifacts_expire_at).to be_nil + end + end + + context 'when value is never' do + let(:expire_in) { 'never' } + let(:default_artifacts_expire_in) { '5 days' } + + it 'does not set expire_in' do + expect(response).to have_gitlab_http_status(:created) + expect(job.reload.artifacts_expire_at).to be_nil + end + end + end + end + end + + context 'posts artifacts file and metadata file' do + let!(:artifacts) { file_upload } + let!(:artifacts_sha256) { Digest::SHA256.file(artifacts.path).hexdigest } + let!(:metadata) { file_upload2 } + let!(:metadata_sha256) { Digest::SHA256.file(metadata.path).hexdigest } + + let(:stored_artifacts_file) { job.reload.artifacts_file } + let(:stored_metadata_file) { job.reload.artifacts_metadata } + let(:stored_artifacts_size) { job.reload.artifacts_size } + let(:stored_artifacts_sha256) { job.reload.job_artifacts_archive.file_sha256 } + let(:stored_metadata_sha256) { job.reload.job_artifacts_metadata.file_sha256 } + let(:file_keys) { post_data.keys } + let(:send_rewritten_field) { true } + + before do + workhorse_finalize_with_multiple_files( + api("/jobs/#{job.id}/artifacts"), + method: :post, + file_keys: file_keys, + params: post_data, + headers: headers_with_token, + send_rewritten_field: send_rewritten_field + ) + end + + context 'when posts data accelerated by workhorse is correct' do + let(:post_data) { { file: artifacts, metadata: metadata } } + + it 'stores artifacts and artifacts metadata' do + expect(response).to have_gitlab_http_status(:created) + expect(stored_artifacts_file.filename).to eq(artifacts.original_filename) + expect(stored_metadata_file.filename).to eq(metadata.original_filename) + expect(stored_artifacts_size).to eq(artifacts.size) + expect(stored_artifacts_sha256).to eq(artifacts_sha256) + expect(stored_metadata_sha256).to eq(metadata_sha256) + end + end + + context 'with a malicious file.path param' do + let(:post_data) { {} } + let(:tmp_file) { Tempfile.new('crafted.file.path') } + let(:url) { "/jobs/#{job.id}/artifacts?file.path=#{tmp_file.path}" } + + it 'rejects the request' do + expect(response).to have_gitlab_http_status(:bad_request) + expect(stored_artifacts_size).to be_nil + end + end + + context 'when workhorse header is missing' do + let(:post_data) { { file: artifacts, metadata: metadata } } + let(:send_rewritten_field) { false } + + it 'rejects the request' do + expect(response).to have_gitlab_http_status(:bad_request) + expect(stored_artifacts_size).to be_nil + end + end + + context 'when there is no artifacts file in post data' do + let(:post_data) do + { metadata: metadata } + end + + it 'is expected to respond with bad request' do + expect(response).to have_gitlab_http_status(:bad_request) + end + + it 'does not store metadata' do + expect(stored_metadata_file).to be_nil + end + end + end + + context 'when artifact_type is archive' do + context 'when artifact_format is zip' do + let(:params) { { artifact_type: :archive, artifact_format: :zip } } + + it 'stores junit test report' do + upload_artifacts(file_upload, headers_with_token, params) + + expect(response).to have_gitlab_http_status(:created) + expect(job.reload.job_artifacts_archive).not_to be_nil + end + end + + context 'when artifact_format is gzip' do + let(:params) { { artifact_type: :archive, artifact_format: :gzip } } + + it 'returns an error' do + upload_artifacts(file_upload, headers_with_token, params) + + expect(response).to have_gitlab_http_status(:bad_request) + expect(job.reload.job_artifacts_archive).to be_nil + end + end + end + + context 'when artifact_type is junit' do + context 'when artifact_format is gzip' do + let(:file_upload) { fixture_file_upload('spec/fixtures/junit/junit.xml.gz') } + let(:params) { { artifact_type: :junit, artifact_format: :gzip } } + + it 'stores junit test report' do + upload_artifacts(file_upload, headers_with_token, params) + + expect(response).to have_gitlab_http_status(:created) + expect(job.reload.job_artifacts_junit).not_to be_nil + end + end + + context 'when artifact_format is raw' do + let(:file_upload) { fixture_file_upload('spec/fixtures/junit/junit.xml.gz') } + let(:params) { { artifact_type: :junit, artifact_format: :raw } } + + it 'returns an error' do + upload_artifacts(file_upload, headers_with_token, params) + + expect(response).to have_gitlab_http_status(:bad_request) + expect(job.reload.job_artifacts_junit).to be_nil + end + end + end + + context 'when artifact_type is metrics_referee' do + context 'when artifact_format is gzip' do + let(:file_upload) { fixture_file_upload('spec/fixtures/referees/metrics_referee.json.gz') } + let(:params) { { artifact_type: :metrics_referee, artifact_format: :gzip } } + + it 'stores metrics_referee data' do + upload_artifacts(file_upload, headers_with_token, params) + + expect(response).to have_gitlab_http_status(:created) + expect(job.reload.job_artifacts_metrics_referee).not_to be_nil + end + end + + context 'when artifact_format is raw' do + let(:file_upload) { fixture_file_upload('spec/fixtures/referees/metrics_referee.json.gz') } + let(:params) { { artifact_type: :metrics_referee, artifact_format: :raw } } + + it 'returns an error' do + upload_artifacts(file_upload, headers_with_token, params) + + expect(response).to have_gitlab_http_status(:bad_request) + expect(job.reload.job_artifacts_metrics_referee).to be_nil + end + end + end + + context 'when artifact_type is network_referee' do + context 'when artifact_format is gzip' do + let(:file_upload) { fixture_file_upload('spec/fixtures/referees/network_referee.json.gz') } + let(:params) { { artifact_type: :network_referee, artifact_format: :gzip } } + + it 'stores network_referee data' do + upload_artifacts(file_upload, headers_with_token, params) + + expect(response).to have_gitlab_http_status(:created) + expect(job.reload.job_artifacts_network_referee).not_to be_nil + end + end + + context 'when artifact_format is raw' do + let(:file_upload) { fixture_file_upload('spec/fixtures/referees/network_referee.json.gz') } + let(:params) { { artifact_type: :network_referee, artifact_format: :raw } } + + it 'returns an error' do + upload_artifacts(file_upload, headers_with_token, params) + + expect(response).to have_gitlab_http_status(:bad_request) + expect(job.reload.job_artifacts_network_referee).to be_nil + end + end + end + + context 'when artifact_type is dotenv' do + context 'when artifact_format is gzip' do + let(:file_upload) { fixture_file_upload('spec/fixtures/build.env.gz') } + let(:params) { { artifact_type: :dotenv, artifact_format: :gzip } } + + it 'stores dotenv file' do + upload_artifacts(file_upload, headers_with_token, params) + + expect(response).to have_gitlab_http_status(:created) + expect(job.reload.job_artifacts_dotenv).not_to be_nil + end + + it 'parses dotenv file' do + expect do + upload_artifacts(file_upload, headers_with_token, params) + end.to change { job.job_variables.count }.from(0).to(2) + end + + context 'when parse error happens' do + let(:file_upload) { fixture_file_upload('spec/fixtures/ci_build_artifacts_metadata.gz') } + + it 'returns an error' do + upload_artifacts(file_upload, headers_with_token, params) + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['message']).to eq('Invalid Format') + end + end + end + + context 'when artifact_format is raw' do + let(:file_upload) { fixture_file_upload('spec/fixtures/build.env.gz') } + let(:params) { { artifact_type: :dotenv, artifact_format: :raw } } + + it 'returns an error' do + upload_artifacts(file_upload, headers_with_token, params) + + expect(response).to have_gitlab_http_status(:bad_request) + expect(job.reload.job_artifacts_dotenv).to be_nil + end + end + end + end + + context 'when artifacts already exist for the job' do + let(:params) do + { + artifact_type: :archive, + artifact_format: :zip, + 'file.sha256' => uploaded_sha256 + } + end + + let(:existing_sha256) { '0' * 64 } + + let!(:existing_artifact) do + create(:ci_job_artifact, :archive, file_sha256: existing_sha256, job: job) + end + + context 'when sha256 is the same of the existing artifact' do + let(:uploaded_sha256) { existing_sha256 } + + it 'ignores the new artifact' do + upload_artifacts(file_upload, headers_with_token, params) + + expect(response).to have_gitlab_http_status(:created) + expect(job.reload.job_artifacts_archive).to eq(existing_artifact) + end + end + + context 'when sha256 is different than the existing artifact' do + let(:uploaded_sha256) { '1' * 64 } + + it 'logs and returns an error' do + expect(Gitlab::ErrorTracking).to receive(:track_exception) + + upload_artifacts(file_upload, headers_with_token, params) + + expect(response).to have_gitlab_http_status(:bad_request) + expect(job.reload.job_artifacts_archive).to eq(existing_artifact) + end + end + end + + context 'when object storage throws errors' do + let(:params) { { artifact_type: :archive, artifact_format: :zip } } + + it 'does not store artifacts' do + allow_next_instance_of(JobArtifactUploader) do |uploader| + allow(uploader).to receive(:store!).and_raise(Errno::EIO) + end + + upload_artifacts(file_upload, headers_with_token, params) + + expect(response).to have_gitlab_http_status(:service_unavailable) + expect(job.reload.job_artifacts_archive).to be_nil + end + end + + context 'when artifacts are being stored outside of tmp path' do + let(:new_tmpdir) { Dir.mktmpdir } + + before do + # init before overwriting tmp dir + file_upload + + # by configuring this path we allow to pass file from @tmpdir only + # but all temporary files are stored in system tmp directory + allow(Dir).to receive(:tmpdir).and_return(new_tmpdir) + end + + after do + FileUtils.remove_entry(new_tmpdir) + end + + it 'fails to post artifacts for outside of tmp path' do + upload_artifacts(file_upload, headers_with_token) + + expect(response).to have_gitlab_http_status(:bad_request) + end + end + + def upload_artifacts(file, headers = {}, params = {}) + workhorse_finalize( + api("/jobs/#{job.id}/artifacts"), + method: :post, + file_key: :file, + params: params.merge(file: file), + headers: headers, + send_rewritten_field: true + ) + end + end + + describe 'GET /api/v4/jobs/:id/artifacts' do + let(:token) { job.token } + + it_behaves_like 'API::CI::Runner application context metadata', '/api/:version/jobs/:id/artifacts' do + let(:send_request) { download_artifact } + end + + it 'updates runner info' do + expect { download_artifact }.to change { runner.reload.contacted_at } + end + + context 'when job has artifacts' do + let(:job) { create(:ci_build) } + let(:store) { JobArtifactUploader::Store::LOCAL } + + before do + create(:ci_job_artifact, :archive, file_store: store, job: job) + end + + context 'when using job token' do + context 'when artifacts are stored locally' do + let(:download_headers) do + { 'Content-Transfer-Encoding' => 'binary', + 'Content-Disposition' => %q(attachment; filename="ci_build_artifacts.zip"; filename*=UTF-8''ci_build_artifacts.zip) } + end + + before do + download_artifact + end + + it 'download artifacts' do + expect(response).to have_gitlab_http_status(:ok) + expect(response.headers.to_h).to include download_headers + end + end + + context 'when artifacts are stored remotely' do + let(:store) { JobArtifactUploader::Store::REMOTE } + let!(:job) { create(:ci_build) } + + context 'when proxy download is being used' do + before do + download_artifact(direct_download: false) + end + + it 'uses workhorse send-url' do + expect(response).to have_gitlab_http_status(:ok) + expect(response.headers.to_h).to include( + 'Gitlab-Workhorse-Send-Data' => /send-url:/) + end + end + + context 'when direct download is being used' do + before do + download_artifact(direct_download: true) + end + + it 'receive redirect for downloading artifacts' do + expect(response).to have_gitlab_http_status(:found) + expect(response.headers).to include('Location') + end + end + end + end + + context 'when using runnners token' do + let(:token) { job.project.runners_token } + + before do + download_artifact + end + + it 'responds with forbidden' do + expect(response).to have_gitlab_http_status(:forbidden) + end + end + end + + context 'when job does not have artifacts' do + it 'responds with not found' do + download_artifact + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + def download_artifact(params = {}, request_headers = headers) + params = params.merge(token: token) + job.reload + + get api("/jobs/#{job.id}/artifacts"), params: params, headers: request_headers + end + end + end + end +end diff --git a/spec/requests/api/ci/runner/jobs_put_spec.rb b/spec/requests/api/ci/runner/jobs_put_spec.rb new file mode 100644 index 00000000000..025747f2f0c --- /dev/null +++ b/spec/requests/api/ci/runner/jobs_put_spec.rb @@ -0,0 +1,196 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do + include StubGitlabCalls + include RedisHelpers + include WorkhorseHelpers + + let(:registration_token) { 'abcdefg123456' } + + before do + stub_feature_flags(ci_enable_live_trace: true) + stub_gitlab_calls + stub_application_setting(runners_registration_token: registration_token) + allow_any_instance_of(::Ci::Runner).to receive(:cache_attributes) + end + + describe '/api/v4/jobs' do + let(:root_namespace) { create(:namespace) } + let(:namespace) { create(:namespace, parent: root_namespace) } + let(:project) { create(:project, namespace: namespace, shared_runners_enabled: false) } + let(:pipeline) { create(:ci_pipeline, project: project, ref: 'master') } + let(:runner) { create(:ci_runner, :project, projects: [project]) } + let(:user) { create(:user) } + let(:job) do + create(:ci_build, :artifacts, :extended_options, + pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) + end + + describe 'PUT /api/v4/jobs/:id' do + let(:job) do + create(:ci_build, :pending, :trace_live, pipeline: pipeline, project: project, user: user, runner_id: runner.id) + end + + before do + job.run! + end + + it_behaves_like 'API::CI::Runner application context metadata', '/api/:version/jobs/:id' do + let(:send_request) { update_job(state: 'success') } + end + + it 'updates runner info' do + expect { update_job(state: 'success') }.to change { runner.reload.contacted_at } + end + + context 'when status is given' do + it 'mark job as succeeded' do + update_job(state: 'success') + + job.reload + expect(job).to be_success + end + + it 'mark job as failed' do + update_job(state: 'failed') + + job.reload + expect(job).to be_failed + expect(job).to be_unknown_failure + end + + context 'when failure_reason is script_failure' do + before do + update_job(state: 'failed', failure_reason: 'script_failure') + job.reload + end + + it { expect(job).to be_script_failure } + end + + context 'when failure_reason is runner_system_failure' do + before do + update_job(state: 'failed', failure_reason: 'runner_system_failure') + job.reload + end + + it { expect(job).to be_runner_system_failure } + end + + context 'when failure_reason is unrecognized value' do + before do + update_job(state: 'failed', failure_reason: 'what_is_this') + job.reload + end + + it { expect(job).to be_unknown_failure } + end + + context 'when failure_reason is job_execution_timeout' do + before do + update_job(state: 'failed', failure_reason: 'job_execution_timeout') + job.reload + end + + it { expect(job).to be_job_execution_timeout } + end + + context 'when failure_reason is unmet_prerequisites' do + before do + update_job(state: 'failed', failure_reason: 'unmet_prerequisites') + job.reload + end + + it { expect(job).to be_unmet_prerequisites } + end + end + + context 'when trace is given' do + it 'creates a trace artifact' do + allow(BuildFinishedWorker).to receive(:perform_async).with(job.id) do + ArchiveTraceWorker.new.perform(job.id) + end + + update_job(state: 'success', trace: 'BUILD TRACE UPDATED') + + job.reload + expect(response).to have_gitlab_http_status(:ok) + expect(job.trace.raw).to eq 'BUILD TRACE UPDATED' + expect(job.job_artifacts_trace.open.read).to eq 'BUILD TRACE UPDATED' + end + + context 'when concurrent update of trace is happening' do + before do + job.trace.write('wb') do + update_job(state: 'success', trace: 'BUILD TRACE UPDATED') + end + end + + it 'returns that operation conflicts' do + expect(response).to have_gitlab_http_status(:conflict) + end + end + end + + context 'when no trace is given' do + it 'does not override trace information' do + update_job + + expect(job.reload.trace.raw).to eq 'BUILD TRACE' + end + + context 'when running state is sent' do + it 'updates update_at value' do + expect { update_job_after_time }.to change { job.reload.updated_at } + end + end + + context 'when other state is sent' do + it "doesn't update update_at value" do + expect { update_job_after_time(20.minutes, state: 'success') }.not_to change { job.reload.updated_at } + end + end + end + + context 'when job has been erased' do + let(:job) { create(:ci_build, runner_id: runner.id, erased_at: Time.now) } + + it 'responds with forbidden' do + update_job + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + + context 'when job has already been finished' do + before do + job.trace.set('Job failed') + job.drop!(:script_failure) + end + + it 'does not update job status and job trace' do + update_job(state: 'success', trace: 'BUILD TRACE UPDATED') + + job.reload + expect(response).to have_gitlab_http_status(:forbidden) + expect(response.header['Job-Status']).to eq 'failed' + expect(job.trace.raw).to eq 'Job failed' + expect(job).to be_failed + end + end + + def update_job(token = job.token, **params) + new_params = params.merge(token: token) + put api("/jobs/#{job.id}"), params: new_params + end + + def update_job_after_time(update_interval = 20.minutes, state = 'running') + Timecop.travel(job.updated_at + update_interval) do + update_job(job.token, state: state) + end + end + end + end +end diff --git a/spec/requests/api/ci/runner/jobs_request_post_spec.rb b/spec/requests/api/ci/runner/jobs_request_post_spec.rb new file mode 100644 index 00000000000..4fa95f8ebb2 --- /dev/null +++ b/spec/requests/api/ci/runner/jobs_request_post_spec.rb @@ -0,0 +1,861 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do + include StubGitlabCalls + include RedisHelpers + include WorkhorseHelpers + + let(:registration_token) { 'abcdefg123456' } + + before do + stub_feature_flags(ci_enable_live_trace: true) + stub_gitlab_calls + stub_application_setting(runners_registration_token: registration_token) + allow_any_instance_of(::Ci::Runner).to receive(:cache_attributes) + end + + describe '/api/v4/jobs' do + let(:root_namespace) { create(:namespace) } + let(:namespace) { create(:namespace, parent: root_namespace) } + let(:project) { create(:project, namespace: namespace, shared_runners_enabled: false) } + let(:pipeline) { create(:ci_pipeline, project: project, ref: 'master') } + let(:runner) { create(:ci_runner, :project, projects: [project]) } + let(:user) { create(:user) } + let(:job) do + create(:ci_build, :artifacts, :extended_options, + pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) + end + + describe 'POST /api/v4/jobs/request' do + let!(:last_update) {} + let!(:new_update) { } + let(:user_agent) { 'gitlab-runner 9.0.0 (9-0-stable; go1.7.4; linux/amd64)' } + + before do + job + stub_container_registry_config(enabled: false) + end + + shared_examples 'no jobs available' do + before do + request_job + end + + context 'when runner sends version in User-Agent' do + context 'for stable version' do + it 'gives 204 and set X-GitLab-Last-Update' do + expect(response).to have_gitlab_http_status(:no_content) + expect(response.header).to have_key('X-GitLab-Last-Update') + end + end + + context 'when last_update is up-to-date' do + let(:last_update) { runner.ensure_runner_queue_value } + + it 'gives 204 and set the same X-GitLab-Last-Update' do + expect(response).to have_gitlab_http_status(:no_content) + expect(response.header['X-GitLab-Last-Update']).to eq(last_update) + end + end + + context 'when last_update is outdated' do + let(:last_update) { runner.ensure_runner_queue_value } + let(:new_update) { runner.tick_runner_queue } + + it 'gives 204 and set a new X-GitLab-Last-Update' do + expect(response).to have_gitlab_http_status(:no_content) + expect(response.header['X-GitLab-Last-Update']).to eq(new_update) + end + end + + context 'when beta version is sent' do + let(:user_agent) { 'gitlab-runner 9.0.0~beta.167.g2b2bacc (master; go1.7.4; linux/amd64)' } + + it { expect(response).to have_gitlab_http_status(:no_content) } + end + + context 'when pre-9-0 version is sent' do + let(:user_agent) { 'gitlab-ci-multi-runner 1.6.0 (1-6-stable; go1.6.3; linux/amd64)' } + + it { expect(response).to have_gitlab_http_status(:no_content) } + end + + context 'when pre-9-0 beta version is sent' do + let(:user_agent) { 'gitlab-ci-multi-runner 1.6.0~beta.167.g2b2bacc (master; go1.6.3; linux/amd64)' } + + it { expect(response).to have_gitlab_http_status(:no_content) } + end + end + end + + context 'when no token is provided' do + it 'returns 400 error' do + post api('/jobs/request') + + expect(response).to have_gitlab_http_status(:bad_request) + end + end + + context 'when invalid token is provided' do + it 'returns 403 error' do + post api('/jobs/request'), params: { token: 'invalid' } + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + + context 'when valid token is provided' do + context 'when Runner is not active' do + let(:runner) { create(:ci_runner, :inactive) } + let(:update_value) { runner.ensure_runner_queue_value } + + it 'returns 204 error' do + request_job + + expect(response).to have_gitlab_http_status(:no_content) + expect(response.header['X-GitLab-Last-Update']).to eq(update_value) + end + end + + context 'when jobs are finished' do + before do + job.success + end + + it_behaves_like 'no jobs available' + end + + context 'when other projects have pending jobs' do + before do + job.success + create(:ci_build, :pending) + end + + it_behaves_like 'no jobs available' + end + + context 'when shared runner requests job for project without shared_runners_enabled' do + let(:runner) { create(:ci_runner, :instance) } + + it_behaves_like 'no jobs available' + end + + context 'when there is a pending job' do + let(:expected_job_info) do + { 'name' => job.name, + 'stage' => job.stage, + 'project_id' => job.project.id, + 'project_name' => job.project.name } + end + + let(:expected_git_info) do + { 'repo_url' => job.repo_url, + 'ref' => job.ref, + 'sha' => job.sha, + 'before_sha' => job.before_sha, + 'ref_type' => 'branch', + 'refspecs' => ["+refs/pipelines/#{pipeline.id}:refs/pipelines/#{pipeline.id}", + "+refs/heads/#{job.ref}:refs/remotes/origin/#{job.ref}"], + 'depth' => project.ci_default_git_depth } + end + + let(:expected_steps) do + [{ 'name' => 'script', + 'script' => %w(echo), + 'timeout' => job.metadata_timeout, + 'when' => 'on_success', + 'allow_failure' => false }, + { 'name' => 'after_script', + 'script' => %w(ls date), + 'timeout' => job.metadata_timeout, + 'when' => 'always', + 'allow_failure' => true }] + end + + let(:expected_variables) do + [{ 'key' => 'CI_JOB_NAME', 'value' => 'spinach', 'public' => true, 'masked' => false }, + { 'key' => 'CI_JOB_STAGE', 'value' => 'test', 'public' => true, 'masked' => false }, + { 'key' => 'DB_NAME', 'value' => 'postgres', 'public' => true, 'masked' => false }] + end + + let(:expected_artifacts) do + [{ 'name' => 'artifacts_file', + 'untracked' => false, + 'paths' => %w(out/), + 'when' => 'always', + 'expire_in' => '7d', + "artifact_type" => "archive", + "artifact_format" => "zip" }] + end + + let(:expected_cache) do + [{ 'key' => 'cache_key', + 'untracked' => false, + 'paths' => ['vendor/*'], + 'policy' => 'pull-push' }] + end + + let(:expected_features) { { 'trace_sections' => true } } + + it 'picks a job' do + request_job info: { platform: :darwin } + + expect(response).to have_gitlab_http_status(:created) + expect(response.headers['Content-Type']).to eq('application/json') + expect(response.headers).not_to have_key('X-GitLab-Last-Update') + expect(runner.reload.platform).to eq('darwin') + expect(json_response['id']).to eq(job.id) + expect(json_response['token']).to eq(job.token) + expect(json_response['job_info']).to eq(expected_job_info) + expect(json_response['git_info']).to eq(expected_git_info) + expect(json_response['image']).to eq({ 'name' => 'ruby:2.7', 'entrypoint' => '/bin/sh', 'ports' => [] }) + expect(json_response['services']).to eq([{ 'name' => 'postgres', 'entrypoint' => nil, + 'alias' => nil, 'command' => nil, 'ports' => [] }, + { 'name' => 'docker:stable-dind', 'entrypoint' => '/bin/sh', + 'alias' => 'docker', 'command' => 'sleep 30', 'ports' => [] }]) + expect(json_response['steps']).to eq(expected_steps) + expect(json_response['artifacts']).to eq(expected_artifacts) + expect(json_response['cache']).to eq(expected_cache) + expect(json_response['variables']).to include(*expected_variables) + expect(json_response['features']).to eq(expected_features) + end + + it 'creates persistent ref' do + expect_any_instance_of(::Ci::PersistentRef).to receive(:create_ref) + .with(job.sha, "refs/#{Repository::REF_PIPELINES}/#{job.commit_id}") + + request_job info: { platform: :darwin } + + expect(response).to have_gitlab_http_status(:created) + expect(json_response['id']).to eq(job.id) + end + + context 'when job is made for tag' do + let!(:job) { create(:ci_build, :tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) } + + it 'sets branch as ref_type' do + request_job + + expect(response).to have_gitlab_http_status(:created) + expect(json_response['git_info']['ref_type']).to eq('tag') + end + + context 'when GIT_DEPTH is specified' do + before do + create(:ci_pipeline_variable, key: 'GIT_DEPTH', value: 1, pipeline: pipeline) + end + + it 'specifies refspecs' do + request_job + + expect(response).to have_gitlab_http_status(:created) + expect(json_response['git_info']['refspecs']).to include("+refs/tags/#{job.ref}:refs/tags/#{job.ref}") + end + end + + context 'when a Gitaly exception is thrown during response' do + before do + allow_next_instance_of(Ci::BuildRunnerPresenter) do |instance| + allow(instance).to receive(:artifacts).and_raise(GRPC::DeadlineExceeded) + end + end + + it 'fails the job as a scheduler failure' do + request_job + + expect(response).to have_gitlab_http_status(:no_content) + expect(job.reload.failed?).to be_truthy + expect(job.failure_reason).to eq('scheduler_failure') + expect(job.runner_id).to eq(runner.id) + expect(job.runner_session).to be_nil + end + end + + context 'when GIT_DEPTH is not specified and there is no default git depth for the project' do + before do + project.update!(ci_default_git_depth: nil) + end + + it 'specifies refspecs' do + request_job + + expect(response).to have_gitlab_http_status(:created) + expect(json_response['git_info']['refspecs']) + .to contain_exactly("+refs/pipelines/#{pipeline.id}:refs/pipelines/#{pipeline.id}", + '+refs/tags/*:refs/tags/*', + '+refs/heads/*:refs/remotes/origin/*') + end + end + end + + context 'when job filtered by job_age' do + let!(:job) { create(:ci_build, :tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0, queued_at: 60.seconds.ago) } + + context 'job is queued less than job_age parameter' do + let(:job_age) { 120 } + + it 'gives 204' do + request_job(job_age: job_age) + + expect(response).to have_gitlab_http_status(:no_content) + end + end + + context 'job is queued more than job_age parameter' do + let(:job_age) { 30 } + + it 'picks a job' do + request_job(job_age: job_age) + + expect(response).to have_gitlab_http_status(:created) + end + end + end + + context 'when job is made for branch' do + it 'sets tag as ref_type' do + request_job + + expect(response).to have_gitlab_http_status(:created) + expect(json_response['git_info']['ref_type']).to eq('branch') + end + + context 'when GIT_DEPTH is specified' do + before do + create(:ci_pipeline_variable, key: 'GIT_DEPTH', value: 1, pipeline: pipeline) + end + + it 'specifies refspecs' do + request_job + + expect(response).to have_gitlab_http_status(:created) + expect(json_response['git_info']['refspecs']).to include("+refs/heads/#{job.ref}:refs/remotes/origin/#{job.ref}") + end + end + + context 'when GIT_DEPTH is not specified and there is no default git depth for the project' do + before do + project.update!(ci_default_git_depth: nil) + end + + it 'specifies refspecs' do + request_job + + expect(response).to have_gitlab_http_status(:created) + expect(json_response['git_info']['refspecs']) + .to contain_exactly("+refs/pipelines/#{pipeline.id}:refs/pipelines/#{pipeline.id}", + '+refs/tags/*:refs/tags/*', + '+refs/heads/*:refs/remotes/origin/*') + end + end + end + + context 'when job is for a release' do + let!(:job) { create(:ci_build, :release_options, pipeline: pipeline) } + + context 'when `multi_build_steps` is passed by the runner' do + it 'exposes release info' do + request_job info: { features: { multi_build_steps: true } } + + expect(response).to have_gitlab_http_status(:created) + expect(response.headers).not_to have_key('X-GitLab-Last-Update') + expect(json_response['steps']).to eq([ + { + "name" => "script", + "script" => ["make changelog | tee release_changelog.txt"], + "timeout" => 3600, + "when" => "on_success", + "allow_failure" => false + }, + { + "name" => "release", + "script" => + ["release-cli create --name \"Release $CI_COMMIT_SHA\" --description \"Created using the release-cli $EXTRA_DESCRIPTION\" --tag-name \"release-$CI_COMMIT_SHA\" --ref \"$CI_COMMIT_SHA\""], + "timeout" => 3600, + "when" => "on_success", + "allow_failure" => false + } + ]) + end + end + + context 'when `multi_build_steps` is not passed by the runner' do + it 'drops the job' do + request_job + + expect(response).to have_gitlab_http_status(:no_content) + end + end + end + + context 'when job is made for merge request' do + let(:pipeline) { create(:ci_pipeline, source: :merge_request_event, project: project, ref: 'feature', merge_request: merge_request) } + let!(:job) { create(:ci_build, pipeline: pipeline, name: 'spinach', ref: 'feature', stage: 'test', stage_idx: 0) } + let(:merge_request) { create(:merge_request) } + + it 'sets branch as ref_type' do + request_job + + expect(response).to have_gitlab_http_status(:created) + expect(json_response['git_info']['ref_type']).to eq('branch') + end + + context 'when GIT_DEPTH is specified' do + before do + create(:ci_pipeline_variable, key: 'GIT_DEPTH', value: 1, pipeline: pipeline) + end + + it 'returns the overwritten git depth for merge request refspecs' do + request_job + + expect(response).to have_gitlab_http_status(:created) + expect(json_response['git_info']['depth']).to eq(1) + end + end + end + + it 'updates runner info' do + expect { request_job }.to change { runner.reload.contacted_at } + end + + %w(version revision platform architecture).each do |param| + context "when info parameter '#{param}' is present" do + let(:value) { "#{param}_value" } + + it "updates provided Runner's parameter" do + request_job info: { param => value } + + expect(response).to have_gitlab_http_status(:created) + expect(runner.reload.read_attribute(param.to_sym)).to eq(value) + end + end + end + + it "sets the runner's ip_address" do + post api('/jobs/request'), + params: { token: runner.token }, + headers: { 'User-Agent' => user_agent, 'X-Forwarded-For' => '123.222.123.222' } + + expect(response).to have_gitlab_http_status(:created) + expect(runner.reload.ip_address).to eq('123.222.123.222') + end + + it "handles multiple X-Forwarded-For addresses" do + post api('/jobs/request'), + params: { token: runner.token }, + headers: { 'User-Agent' => user_agent, 'X-Forwarded-For' => '123.222.123.222, 127.0.0.1' } + + expect(response).to have_gitlab_http_status(:created) + expect(runner.reload.ip_address).to eq('123.222.123.222') + end + + context 'when concurrently updating a job' do + before do + expect_any_instance_of(::Ci::Build).to receive(:run!) + .and_raise(ActiveRecord::StaleObjectError.new(nil, nil)) + end + + it 'returns a conflict' do + request_job + + expect(response).to have_gitlab_http_status(:conflict) + expect(response.headers).not_to have_key('X-GitLab-Last-Update') + end + end + + context 'when project and pipeline have multiple jobs' do + let!(:job) { create(:ci_build, :tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) } + let!(:job2) { create(:ci_build, :tag, pipeline: pipeline, name: 'rubocop', stage: 'test', stage_idx: 0) } + let!(:test_job) { create(:ci_build, pipeline: pipeline, name: 'deploy', stage: 'deploy', stage_idx: 1) } + + before do + job.success + job2.success + end + + it 'returns dependent jobs' do + request_job + + expect(response).to have_gitlab_http_status(:created) + expect(json_response['id']).to eq(test_job.id) + expect(json_response['dependencies'].count).to eq(2) + expect(json_response['dependencies']).to include( + { 'id' => job.id, 'name' => job.name, 'token' => job.token }, + { 'id' => job2.id, 'name' => job2.name, 'token' => job2.token }) + end + end + + context 'when pipeline have jobs with artifacts' do + let!(:job) { create(:ci_build, :tag, :artifacts, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) } + let!(:test_job) { create(:ci_build, pipeline: pipeline, name: 'deploy', stage: 'deploy', stage_idx: 1) } + + before do + job.success + end + + it 'returns dependent jobs' do + request_job + + expect(response).to have_gitlab_http_status(:created) + expect(json_response['id']).to eq(test_job.id) + expect(json_response['dependencies'].count).to eq(1) + expect(json_response['dependencies']).to include( + { 'id' => job.id, 'name' => job.name, 'token' => job.token, + 'artifacts_file' => { 'filename' => 'ci_build_artifacts.zip', 'size' => 107464 } }) + end + end + + context 'when explicit dependencies are defined' do + let!(:job) { create(:ci_build, :tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) } + let!(:job2) { create(:ci_build, :tag, pipeline: pipeline, name: 'rubocop', stage: 'test', stage_idx: 0) } + let!(:test_job) do + create(:ci_build, pipeline: pipeline, token: 'test-job-token', name: 'deploy', + stage: 'deploy', stage_idx: 1, + options: { script: ['bash'], dependencies: [job2.name] }) + end + + before do + job.success + job2.success + end + + it 'returns dependent jobs' do + request_job + + expect(response).to have_gitlab_http_status(:created) + expect(json_response['id']).to eq(test_job.id) + expect(json_response['dependencies'].count).to eq(1) + expect(json_response['dependencies'][0]).to include('id' => job2.id, 'name' => job2.name, 'token' => job2.token) + end + end + + context 'when dependencies is an empty array' do + let!(:job) { create(:ci_build, :tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) } + let!(:job2) { create(:ci_build, :tag, pipeline: pipeline, name: 'rubocop', stage: 'test', stage_idx: 0) } + let!(:empty_dependencies_job) do + create(:ci_build, pipeline: pipeline, token: 'test-job-token', name: 'empty_dependencies_job', + stage: 'deploy', stage_idx: 1, + options: { script: ['bash'], dependencies: [] }) + end + + before do + job.success + job2.success + end + + it 'returns an empty array' do + request_job + + expect(response).to have_gitlab_http_status(:created) + expect(json_response['id']).to eq(empty_dependencies_job.id) + expect(json_response['dependencies'].count).to eq(0) + end + end + + context 'when job has no tags' do + before do + job.update!(tags: []) + end + + context 'when runner is allowed to pick untagged jobs' do + before do + runner.update_column(:run_untagged, true) + end + + it 'picks job' do + request_job + + expect(response).to have_gitlab_http_status(:created) + end + end + + context 'when runner is not allowed to pick untagged jobs' do + before do + runner.update_column(:run_untagged, false) + end + + it_behaves_like 'no jobs available' + end + end + + context 'when triggered job is available' do + let(:expected_variables) do + [{ 'key' => 'CI_JOB_NAME', 'value' => 'spinach', 'public' => true, 'masked' => false }, + { 'key' => 'CI_JOB_STAGE', 'value' => 'test', 'public' => true, 'masked' => false }, + { 'key' => 'CI_PIPELINE_TRIGGERED', 'value' => 'true', 'public' => true, 'masked' => false }, + { 'key' => 'DB_NAME', 'value' => 'postgres', 'public' => true, 'masked' => false }, + { 'key' => 'SECRET_KEY', 'value' => 'secret_value', 'public' => false, 'masked' => false }, + { 'key' => 'TRIGGER_KEY_1', 'value' => 'TRIGGER_VALUE_1', 'public' => false, 'masked' => false }] + end + + let(:trigger) { create(:ci_trigger, project: project) } + let!(:trigger_request) { create(:ci_trigger_request, pipeline: pipeline, builds: [job], trigger: trigger) } + + before do + project.variables << ::Ci::Variable.new(key: 'SECRET_KEY', value: 'secret_value') + end + + shared_examples 'expected variables behavior' do + it 'returns variables for triggers' do + request_job + + expect(response).to have_gitlab_http_status(:created) + expect(json_response['variables']).to include(*expected_variables) + end + end + + context 'when variables are stored in trigger_request' do + before do + trigger_request.update_attribute(:variables, { TRIGGER_KEY_1: 'TRIGGER_VALUE_1' } ) + end + + it_behaves_like 'expected variables behavior' + end + + context 'when variables are stored in pipeline_variables' do + before do + create(:ci_pipeline_variable, pipeline: pipeline, key: :TRIGGER_KEY_1, value: 'TRIGGER_VALUE_1') + end + + it_behaves_like 'expected variables behavior' + end + end + + describe 'registry credentials support' do + let(:registry_url) { 'registry.example.com:5005' } + let(:registry_credentials) do + { 'type' => 'registry', + 'url' => registry_url, + 'username' => 'gitlab-ci-token', + 'password' => job.token } + end + + context 'when registry is enabled' do + before do + stub_container_registry_config(enabled: true, host_port: registry_url) + end + + it 'sends registry credentials key' do + request_job + + expect(json_response).to have_key('credentials') + expect(json_response['credentials']).to include(registry_credentials) + end + end + + context 'when registry is disabled' do + before do + stub_container_registry_config(enabled: false, host_port: registry_url) + end + + it 'does not send registry credentials' do + request_job + + expect(json_response).to have_key('credentials') + expect(json_response['credentials']).not_to include(registry_credentials) + end + end + end + + describe 'timeout support' do + context 'when project specifies job timeout' do + let(:project) { create(:project, shared_runners_enabled: false, build_timeout: 1234) } + + it 'contains info about timeout taken from project' do + request_job + + expect(response).to have_gitlab_http_status(:created) + expect(json_response['runner_info']).to include({ 'timeout' => 1234 }) + end + + context 'when runner specifies lower timeout' do + let(:runner) { create(:ci_runner, :project, maximum_timeout: 1000, projects: [project]) } + + it 'contains info about timeout overridden by runner' do + request_job + + expect(response).to have_gitlab_http_status(:created) + expect(json_response['runner_info']).to include({ 'timeout' => 1000 }) + end + end + + context 'when runner specifies bigger timeout' do + let(:runner) { create(:ci_runner, :project, maximum_timeout: 2000, projects: [project]) } + + it 'contains info about timeout not overridden by runner' do + request_job + + expect(response).to have_gitlab_http_status(:created) + expect(json_response['runner_info']).to include({ 'timeout' => 1234 }) + end + end + end + end + end + + describe 'port support' do + let(:job) { create(:ci_build, pipeline: pipeline, options: options) } + + context 'when job image has ports' do + let(:options) do + { + image: { + name: 'ruby', + ports: [80] + }, + services: ['mysql'] + } + end + + it 'returns the image ports' do + request_job + + expect(response).to have_gitlab_http_status(:created) + expect(json_response).to include( + 'id' => job.id, + 'image' => a_hash_including('name' => 'ruby', 'ports' => [{ 'number' => 80, 'protocol' => 'http', 'name' => 'default_port' }]), + 'services' => all(a_hash_including('name' => 'mysql'))) + end + end + + context 'when job services settings has ports' do + let(:options) do + { + image: 'ruby', + services: [ + { + name: 'tomcat', + ports: [{ number: 8081, protocol: 'http', name: 'custom_port' }] + } + ] + } + end + + it 'returns the service ports' do + request_job + + expect(response).to have_gitlab_http_status(:created) + expect(json_response).to include( + 'id' => job.id, + 'image' => a_hash_including('name' => 'ruby'), + 'services' => all(a_hash_including('name' => 'tomcat', 'ports' => [{ 'number' => 8081, 'protocol' => 'http', 'name' => 'custom_port' }]))) + end + end + end + + describe 'a job with excluded artifacts' do + context 'when excluded paths are defined' do + let(:job) do + create(:ci_build, pipeline: pipeline, token: 'test-job-token', name: 'test', + stage: 'deploy', stage_idx: 1, + options: { artifacts: { paths: ['abc'], exclude: ['cde'] } }) + end + + context 'when a runner supports this feature' do + it 'exposes excluded paths when the feature is enabled' do + stub_feature_flags(ci_artifacts_exclude: true) + + request_job info: { features: { artifacts_exclude: true } } + + expect(response).to have_gitlab_http_status(:created) + expect(json_response.dig('artifacts').first).to include('exclude' => ['cde']) + end + + it 'does not expose excluded paths when the feature is disabled' do + stub_feature_flags(ci_artifacts_exclude: false) + + request_job info: { features: { artifacts_exclude: true } } + + expect(response).to have_gitlab_http_status(:created) + expect(json_response.dig('artifacts').first).not_to have_key('exclude') + end + end + + context 'when a runner does not support this feature' do + it 'does not expose the build at all' do + stub_feature_flags(ci_artifacts_exclude: true) + + request_job + + expect(response).to have_gitlab_http_status(:no_content) + end + end + end + + it 'does not expose excluded paths when these are empty' do + request_job + + expect(response).to have_gitlab_http_status(:created) + expect(json_response.dig('artifacts').first).not_to have_key('exclude') + end + end + + def request_job(token = runner.token, **params) + new_params = params.merge(token: token, last_update: last_update) + post api('/jobs/request'), params: new_params.to_json, headers: { 'User-Agent' => user_agent, 'Content-Type': 'application/json' } + end + end + + context 'for web-ide job' do + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project, :repository) } + + let(:runner) { create(:ci_runner, :project, projects: [project]) } + let(:service) { ::Ci::CreateWebIdeTerminalService.new(project, user, ref: 'master').execute } + let(:pipeline) { service[:pipeline] } + let(:build) { pipeline.builds.first } + let(:job) { {} } + let(:config_content) do + 'terminal: { image: ruby, services: [mysql], before_script: [ls], tags: [tag-1], variables: { KEY: value } }' + end + + before do + stub_webide_config_file(config_content) + project.add_maintainer(user) + + pipeline + end + + context 'when runner has matching tag' do + before do + runner.update!(tag_list: ['tag-1']) + end + + it 'successfully picks job' do + request_job + + build.reload + + expect(build).to be_running + expect(build.runner).to eq(runner) + + expect(response).to have_gitlab_http_status(:created) + expect(json_response).to include( + "id" => build.id, + "variables" => include("key" => 'KEY', "value" => 'value', "public" => true, "masked" => false), + "image" => a_hash_including("name" => 'ruby'), + "services" => all(a_hash_including("name" => 'mysql')), + "job_info" => a_hash_including("name" => 'terminal', "stage" => 'terminal')) + end + end + + context 'when runner does not have matching tags' do + it 'does not pick a job' do + request_job + + build.reload + + expect(build).to be_pending + expect(response).to have_gitlab_http_status(:no_content) + end + end + + def request_job(token = runner.token, **params) + post api('/jobs/request'), params: params.merge(token: token) + end + end + end + end +end diff --git a/spec/requests/api/ci/runner/jobs_trace_spec.rb b/spec/requests/api/ci/runner/jobs_trace_spec.rb new file mode 100644 index 00000000000..1980c1a9f51 --- /dev/null +++ b/spec/requests/api/ci/runner/jobs_trace_spec.rb @@ -0,0 +1,292 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do + include StubGitlabCalls + include RedisHelpers + include WorkhorseHelpers + + let(:registration_token) { 'abcdefg123456' } + + before do + stub_feature_flags(ci_enable_live_trace: true) + stub_gitlab_calls + stub_application_setting(runners_registration_token: registration_token) + allow_any_instance_of(::Ci::Runner).to receive(:cache_attributes) + end + + describe '/api/v4/jobs' do + let(:root_namespace) { create(:namespace) } + let(:namespace) { create(:namespace, parent: root_namespace) } + let(:project) { create(:project, namespace: namespace, shared_runners_enabled: false) } + let(:pipeline) { create(:ci_pipeline, project: project, ref: 'master') } + let(:runner) { create(:ci_runner, :project, projects: [project]) } + let(:user) { create(:user) } + let(:job) do + create(:ci_build, :artifacts, :extended_options, + pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) + end + + describe 'PATCH /api/v4/jobs/:id/trace' do + let(:job) do + create(:ci_build, :running, :trace_live, + project: project, user: user, runner_id: runner.id, pipeline: pipeline) + end + + let(:headers) { { API::Helpers::Runner::JOB_TOKEN_HEADER => job.token, 'Content-Type' => 'text/plain' } } + let(:headers_with_range) { headers.merge({ 'Content-Range' => '11-20' }) } + let(:update_interval) { 10.seconds.to_i } + + before do + initial_patch_the_trace + end + + it_behaves_like 'API::CI::Runner application context metadata', '/api/:version/jobs/:id/trace' do + let(:send_request) { patch_the_trace } + end + + it 'updates runner info' do + runner.update!(contacted_at: 1.year.ago) + + expect { patch_the_trace }.to change { runner.reload.contacted_at } + end + + context 'when request is valid' do + it 'gets correct response' do + expect(response).to have_gitlab_http_status(:accepted) + expect(job.reload.trace.raw).to eq 'BUILD TRACE appended' + expect(response.header).to have_key 'Range' + expect(response.header).to have_key 'Job-Status' + expect(response.header).to have_key 'X-GitLab-Trace-Update-Interval' + end + + context 'when job has been updated recently' do + it { expect { patch_the_trace }.not_to change { job.updated_at }} + + it "changes the job's trace" do + patch_the_trace + + expect(job.reload.trace.raw).to eq 'BUILD TRACE appended appended' + end + + context 'when Runner makes a force-patch' do + it { expect { force_patch_the_trace }.not_to change { job.updated_at }} + + it "doesn't change the build.trace" do + force_patch_the_trace + + expect(job.reload.trace.raw).to eq 'BUILD TRACE appended' + end + end + end + + context 'when job was not updated recently' do + let(:update_interval) { 15.minutes.to_i } + + it { expect { patch_the_trace }.to change { job.updated_at } } + + it 'changes the job.trace' do + patch_the_trace + + expect(job.reload.trace.raw).to eq 'BUILD TRACE appended appended' + end + + context 'when Runner makes a force-patch' do + it { expect { force_patch_the_trace }.to change { job.updated_at } } + + it "doesn't change the job.trace" do + force_patch_the_trace + + expect(job.reload.trace.raw).to eq 'BUILD TRACE appended' + end + end + end + + context 'when project for the build has been deleted' do + let(:job) do + create(:ci_build, :running, :trace_live, runner_id: runner.id, pipeline: pipeline) do |job| + job.project.update!(pending_delete: true) + end + end + + it 'responds with forbidden' do + expect(response).to have_gitlab_http_status(:forbidden) + end + end + + context 'when trace is patched' do + before do + patch_the_trace + end + + it 'has valid trace' do + expect(response).to have_gitlab_http_status(:accepted) + expect(job.reload.trace.raw).to eq 'BUILD TRACE appended appended' + end + + context 'when job is cancelled' do + before do + job.cancel + end + + context 'when trace is patched' do + before do + patch_the_trace + end + + it 'returns Forbidden ' do + expect(response).to have_gitlab_http_status(:forbidden) + end + end + end + + context 'when redis data are flushed' do + before do + redis_shared_state_cleanup! + end + + it 'has empty trace' do + expect(job.reload.trace.raw).to eq '' + end + + context 'when we perform partial patch' do + before do + patch_the_trace('hello', headers.merge({ 'Content-Range' => "28-32/5" })) + end + + it 'returns an error' do + expect(response).to have_gitlab_http_status(:range_not_satisfiable) + expect(response.header['Range']).to eq('0-0') + end + end + + context 'when we resend full trace' do + before do + patch_the_trace('BUILD TRACE appended appended hello', headers.merge({ 'Content-Range' => "0-34/35" })) + end + + it 'succeeds with updating trace' do + expect(response).to have_gitlab_http_status(:accepted) + expect(job.reload.trace.raw).to eq 'BUILD TRACE appended appended hello' + end + end + end + end + + context 'when concurrent update of trace is happening' do + before do + job.trace.write('wb') do + patch_the_trace + end + end + + it 'returns that operation conflicts' do + expect(response).to have_gitlab_http_status(:conflict) + end + end + + context 'when the job is canceled' do + before do + job.cancel + patch_the_trace + end + + it 'receives status in header' do + expect(response.header['Job-Status']).to eq 'canceled' + end + end + + context 'when build trace is being watched' do + before do + job.trace.being_watched! + end + + it 'returns X-GitLab-Trace-Update-Interval as 3' do + patch_the_trace + + expect(response).to have_gitlab_http_status(:accepted) + expect(response.header['X-GitLab-Trace-Update-Interval']).to eq('3') + end + end + + context 'when build trace is not being watched' do + it 'returns X-GitLab-Trace-Update-Interval as 30' do + patch_the_trace + + expect(response).to have_gitlab_http_status(:accepted) + expect(response.header['X-GitLab-Trace-Update-Interval']).to eq('30') + end + end + end + + context 'when Runner makes a force-patch' do + before do + force_patch_the_trace + end + + it 'gets correct response' do + expect(response).to have_gitlab_http_status(:accepted) + expect(job.reload.trace.raw).to eq 'BUILD TRACE appended' + expect(response.header).to have_key 'Range' + expect(response.header).to have_key 'Job-Status' + end + end + + context 'when content-range start is too big' do + let(:headers_with_range) { headers.merge({ 'Content-Range' => '15-20/6' }) } + + it 'gets 416 error response with range headers' do + expect(response).to have_gitlab_http_status(:range_not_satisfiable) + expect(response.header).to have_key 'Range' + expect(response.header['Range']).to eq '0-11' + end + end + + context 'when content-range start is too small' do + let(:headers_with_range) { headers.merge({ 'Content-Range' => '8-20/13' }) } + + it 'gets 416 error response with range headers' do + expect(response).to have_gitlab_http_status(:range_not_satisfiable) + expect(response.header).to have_key 'Range' + expect(response.header['Range']).to eq '0-11' + end + end + + context 'when Content-Range header is missing' do + let(:headers_with_range) { headers } + + it { expect(response).to have_gitlab_http_status(:bad_request) } + end + + context 'when job has been errased' do + let(:job) { create(:ci_build, runner_id: runner.id, erased_at: Time.now) } + + it { expect(response).to have_gitlab_http_status(:forbidden) } + end + + def patch_the_trace(content = ' appended', request_headers = nil) + unless request_headers + job.trace.read do |stream| + offset = stream.size + limit = offset + content.length - 1 + request_headers = headers.merge({ 'Content-Range' => "#{offset}-#{limit}" }) + end + end + + Timecop.travel(job.updated_at + update_interval) do + patch api("/jobs/#{job.id}/trace"), params: content, headers: request_headers + job.reload + end + end + + def initial_patch_the_trace + patch_the_trace(' appended', headers_with_range) + end + + def force_patch_the_trace + 2.times { patch_the_trace('') } + end + end + end +end diff --git a/spec/requests/api/ci/runner/runners_delete_spec.rb b/spec/requests/api/ci/runner/runners_delete_spec.rb new file mode 100644 index 00000000000..75960a1a1c0 --- /dev/null +++ b/spec/requests/api/ci/runner/runners_delete_spec.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do + include StubGitlabCalls + include RedisHelpers + include WorkhorseHelpers + + let(:registration_token) { 'abcdefg123456' } + + before do + stub_feature_flags(ci_enable_live_trace: true) + stub_gitlab_calls + stub_application_setting(runners_registration_token: registration_token) + allow_any_instance_of(::Ci::Runner).to receive(:cache_attributes) + end + + describe '/api/v4/runners' do + describe 'DELETE /api/v4/runners' do + context 'when no token is provided' do + it 'returns 400 error' do + delete api('/runners') + + expect(response).to have_gitlab_http_status(:bad_request) + end + end + + context 'when invalid token is provided' do + it 'returns 403 error' do + delete api('/runners'), params: { token: 'invalid' } + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + + context 'when valid token is provided' do + let(:runner) { create(:ci_runner) } + + it 'deletes Runner' do + delete api('/runners'), params: { token: runner.token } + + expect(response).to have_gitlab_http_status(:no_content) + expect(::Ci::Runner.count).to eq(0) + end + + it_behaves_like '412 response' do + let(:request) { api('/runners') } + let(:params) { { token: runner.token } } + end + end + end + end +end diff --git a/spec/requests/api/ci/runner/runners_post_spec.rb b/spec/requests/api/ci/runner/runners_post_spec.rb new file mode 100644 index 00000000000..7c362fae7d2 --- /dev/null +++ b/spec/requests/api/ci/runner/runners_post_spec.rb @@ -0,0 +1,250 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do + include StubGitlabCalls + include RedisHelpers + include WorkhorseHelpers + + let(:registration_token) { 'abcdefg123456' } + + before do + stub_feature_flags(ci_enable_live_trace: true) + stub_gitlab_calls + stub_application_setting(runners_registration_token: registration_token) + allow_any_instance_of(::Ci::Runner).to receive(:cache_attributes) + end + + describe '/api/v4/runners' do + describe 'POST /api/v4/runners' do + context 'when no token is provided' do + it 'returns 400 error' do + post api('/runners') + + expect(response).to have_gitlab_http_status(:bad_request) + end + end + + context 'when invalid token is provided' do + it 'returns 403 error' do + post api('/runners'), params: { token: 'invalid' } + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + + context 'when valid token is provided' do + it 'creates runner with default values' do + post api('/runners'), params: { token: registration_token } + + runner = ::Ci::Runner.first + + expect(response).to have_gitlab_http_status(:created) + expect(json_response['id']).to eq(runner.id) + expect(json_response['token']).to eq(runner.token) + expect(runner.run_untagged).to be true + expect(runner.active).to be true + expect(runner.token).not_to eq(registration_token) + expect(runner).to be_instance_type + end + + context 'when project token is used' do + let(:project) { create(:project) } + + it 'creates project runner' do + post api('/runners'), params: { token: project.runners_token } + + expect(response).to have_gitlab_http_status(:created) + expect(project.runners.size).to eq(1) + runner = ::Ci::Runner.first + expect(runner.token).not_to eq(registration_token) + expect(runner.token).not_to eq(project.runners_token) + expect(runner).to be_project_type + end + end + + context 'when group token is used' do + let(:group) { create(:group) } + + it 'creates a group runner' do + post api('/runners'), params: { token: group.runners_token } + + expect(response).to have_gitlab_http_status(:created) + expect(group.runners.reload.size).to eq(1) + runner = ::Ci::Runner.first + expect(runner.token).not_to eq(registration_token) + expect(runner.token).not_to eq(group.runners_token) + expect(runner).to be_group_type + end + end + end + + context 'when runner description is provided' do + it 'creates runner' do + post api('/runners'), params: { + token: registration_token, + description: 'server.hostname' + } + + expect(response).to have_gitlab_http_status(:created) + expect(::Ci::Runner.first.description).to eq('server.hostname') + end + end + + context 'when runner tags are provided' do + it 'creates runner' do + post api('/runners'), params: { + token: registration_token, + tag_list: 'tag1, tag2' + } + + expect(response).to have_gitlab_http_status(:created) + expect(::Ci::Runner.first.tag_list.sort).to eq(%w(tag1 tag2)) + end + end + + context 'when option for running untagged jobs is provided' do + context 'when tags are provided' do + it 'creates runner' do + post api('/runners'), params: { + token: registration_token, + run_untagged: false, + tag_list: ['tag'] + } + + expect(response).to have_gitlab_http_status(:created) + expect(::Ci::Runner.first.run_untagged).to be false + expect(::Ci::Runner.first.tag_list.sort).to eq(['tag']) + end + end + + context 'when tags are not provided' do + it 'returns 400 error' do + post api('/runners'), params: { + token: registration_token, + run_untagged: false + } + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['message']).to include( + 'tags_list' => ['can not be empty when runner is not allowed to pick untagged jobs']) + end + end + end + + context 'when option for locking Runner is provided' do + it 'creates runner' do + post api('/runners'), params: { + token: registration_token, + locked: true + } + + expect(response).to have_gitlab_http_status(:created) + expect(::Ci::Runner.first.locked).to be true + end + end + + context 'when option for activating a Runner is provided' do + context 'when active is set to true' do + it 'creates runner' do + post api('/runners'), params: { + token: registration_token, + active: true + } + + expect(response).to have_gitlab_http_status(:created) + expect(::Ci::Runner.first.active).to be true + end + end + + context 'when active is set to false' do + it 'creates runner' do + post api('/runners'), params: { + token: registration_token, + active: false + } + + expect(response).to have_gitlab_http_status(:created) + expect(::Ci::Runner.first.active).to be false + end + end + end + + context 'when access_level is provided for Runner' do + context 'when access_level is set to ref_protected' do + it 'creates runner' do + post api('/runners'), params: { + token: registration_token, + access_level: 'ref_protected' + } + + expect(response).to have_gitlab_http_status(:created) + expect(::Ci::Runner.first.ref_protected?).to be true + end + end + + context 'when access_level is set to not_protected' do + it 'creates runner' do + post api('/runners'), params: { + token: registration_token, + access_level: 'not_protected' + } + + expect(response).to have_gitlab_http_status(:created) + expect(::Ci::Runner.first.ref_protected?).to be false + end + end + end + + context 'when maximum job timeout is specified' do + it 'creates runner' do + post api('/runners'), params: { + token: registration_token, + maximum_timeout: 9000 + } + + expect(response).to have_gitlab_http_status(:created) + expect(::Ci::Runner.first.maximum_timeout).to eq(9000) + end + + context 'when maximum job timeout is empty' do + it 'creates runner' do + post api('/runners'), params: { + token: registration_token, + maximum_timeout: '' + } + + expect(response).to have_gitlab_http_status(:created) + expect(::Ci::Runner.first.maximum_timeout).to be_nil + end + end + end + + %w(name version revision platform architecture).each do |param| + context "when info parameter '#{param}' info is present" do + let(:value) { "#{param}_value" } + + it "updates provided Runner's parameter" do + post api('/runners'), params: { + token: registration_token, + info: { param => value } + } + + expect(response).to have_gitlab_http_status(:created) + expect(::Ci::Runner.first.read_attribute(param.to_sym)).to eq(value) + end + end + end + + it "sets the runner's ip_address" do + post api('/runners'), + params: { token: registration_token }, + headers: { 'X-Forwarded-For' => '123.111.123.111' } + + expect(response).to have_gitlab_http_status(:created) + expect(::Ci::Runner.first.ip_address).to eq('123.111.123.111') + end + end + end +end diff --git a/spec/requests/api/ci/runner/runners_verify_post_spec.rb b/spec/requests/api/ci/runner/runners_verify_post_spec.rb new file mode 100644 index 00000000000..e2f5f9b2d68 --- /dev/null +++ b/spec/requests/api/ci/runner/runners_verify_post_spec.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do + include StubGitlabCalls + include RedisHelpers + include WorkhorseHelpers + + let(:registration_token) { 'abcdefg123456' } + + before do + stub_feature_flags(ci_enable_live_trace: true) + stub_gitlab_calls + stub_application_setting(runners_registration_token: registration_token) + allow_any_instance_of(::Ci::Runner).to receive(:cache_attributes) + end + + describe '/api/v4/runners' do + describe 'POST /api/v4/runners/verify' do + let(:runner) { create(:ci_runner) } + + context 'when no token is provided' do + it 'returns 400 error' do + post api('/runners/verify') + + expect(response).to have_gitlab_http_status :bad_request + end + end + + context 'when invalid token is provided' do + it 'returns 403 error' do + post api('/runners/verify'), params: { token: 'invalid-token' } + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + + context 'when valid token is provided' do + it 'verifies Runner credentials' do + post api('/runners/verify'), params: { token: runner.token } + + expect(response).to have_gitlab_http_status(:ok) + end + end + end + end +end diff --git a/spec/requests/api/ci/runner_spec.rb b/spec/requests/api/ci/runner_spec.rb deleted file mode 100644 index c8718309bf2..00000000000 --- a/spec/requests/api/ci/runner_spec.rb +++ /dev/null @@ -1,2474 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do - include StubGitlabCalls - include RedisHelpers - include WorkhorseHelpers - - let(:registration_token) { 'abcdefg123456' } - - before do - stub_feature_flags(ci_enable_live_trace: true) - stub_gitlab_calls - stub_application_setting(runners_registration_token: registration_token) - allow_any_instance_of(::Ci::Runner).to receive(:cache_attributes) - end - - describe '/api/v4/runners' do - describe 'POST /api/v4/runners' do - context 'when no token is provided' do - it 'returns 400 error' do - post api('/runners') - - expect(response).to have_gitlab_http_status(:bad_request) - end - end - - context 'when invalid token is provided' do - it 'returns 403 error' do - post api('/runners'), params: { token: 'invalid' } - - expect(response).to have_gitlab_http_status(:forbidden) - end - end - - context 'when valid token is provided' do - it 'creates runner with default values' do - post api('/runners'), params: { token: registration_token } - - runner = ::Ci::Runner.first - - expect(response).to have_gitlab_http_status(:created) - expect(json_response['id']).to eq(runner.id) - expect(json_response['token']).to eq(runner.token) - expect(runner.run_untagged).to be true - expect(runner.active).to be true - expect(runner.token).not_to eq(registration_token) - expect(runner).to be_instance_type - end - - context 'when project token is used' do - let(:project) { create(:project) } - - it 'creates project runner' do - post api('/runners'), params: { token: project.runners_token } - - expect(response).to have_gitlab_http_status(:created) - expect(project.runners.size).to eq(1) - runner = ::Ci::Runner.first - expect(runner.token).not_to eq(registration_token) - expect(runner.token).not_to eq(project.runners_token) - expect(runner).to be_project_type - end - end - - context 'when group token is used' do - let(:group) { create(:group) } - - it 'creates a group runner' do - post api('/runners'), params: { token: group.runners_token } - - expect(response).to have_gitlab_http_status(:created) - expect(group.runners.reload.size).to eq(1) - runner = ::Ci::Runner.first - expect(runner.token).not_to eq(registration_token) - expect(runner.token).not_to eq(group.runners_token) - expect(runner).to be_group_type - end - end - end - - context 'when runner description is provided' do - it 'creates runner' do - post api('/runners'), params: { - token: registration_token, - description: 'server.hostname' - } - - expect(response).to have_gitlab_http_status(:created) - expect(::Ci::Runner.first.description).to eq('server.hostname') - end - end - - context 'when runner tags are provided' do - it 'creates runner' do - post api('/runners'), params: { - token: registration_token, - tag_list: 'tag1, tag2' - } - - expect(response).to have_gitlab_http_status(:created) - expect(::Ci::Runner.first.tag_list.sort).to eq(%w(tag1 tag2)) - end - end - - context 'when option for running untagged jobs is provided' do - context 'when tags are provided' do - it 'creates runner' do - post api('/runners'), params: { - token: registration_token, - run_untagged: false, - tag_list: ['tag'] - } - - expect(response).to have_gitlab_http_status(:created) - expect(::Ci::Runner.first.run_untagged).to be false - expect(::Ci::Runner.first.tag_list.sort).to eq(['tag']) - end - end - - context 'when tags are not provided' do - it 'returns 400 error' do - post api('/runners'), params: { - token: registration_token, - run_untagged: false - } - - expect(response).to have_gitlab_http_status(:bad_request) - expect(json_response['message']).to include( - 'tags_list' => ['can not be empty when runner is not allowed to pick untagged jobs']) - end - end - end - - context 'when option for locking Runner is provided' do - it 'creates runner' do - post api('/runners'), params: { - token: registration_token, - locked: true - } - - expect(response).to have_gitlab_http_status(:created) - expect(::Ci::Runner.first.locked).to be true - end - end - - context 'when option for activating a Runner is provided' do - context 'when active is set to true' do - it 'creates runner' do - post api('/runners'), params: { - token: registration_token, - active: true - } - - expect(response).to have_gitlab_http_status(:created) - expect(::Ci::Runner.first.active).to be true - end - end - - context 'when active is set to false' do - it 'creates runner' do - post api('/runners'), params: { - token: registration_token, - active: false - } - - expect(response).to have_gitlab_http_status(:created) - expect(::Ci::Runner.first.active).to be false - end - end - end - - context 'when access_level is provided for Runner' do - context 'when access_level is set to ref_protected' do - it 'creates runner' do - post api('/runners'), params: { - token: registration_token, - access_level: 'ref_protected' - } - - expect(response).to have_gitlab_http_status(:created) - expect(::Ci::Runner.first.ref_protected?).to be true - end - end - - context 'when access_level is set to not_protected' do - it 'creates runner' do - post api('/runners'), params: { - token: registration_token, - access_level: 'not_protected' - } - - expect(response).to have_gitlab_http_status(:created) - expect(::Ci::Runner.first.ref_protected?).to be false - end - end - end - - context 'when maximum job timeout is specified' do - it 'creates runner' do - post api('/runners'), params: { - token: registration_token, - maximum_timeout: 9000 - } - - expect(response).to have_gitlab_http_status(:created) - expect(::Ci::Runner.first.maximum_timeout).to eq(9000) - end - - context 'when maximum job timeout is empty' do - it 'creates runner' do - post api('/runners'), params: { - token: registration_token, - maximum_timeout: '' - } - - expect(response).to have_gitlab_http_status(:created) - expect(::Ci::Runner.first.maximum_timeout).to be_nil - end - end - end - - %w(name version revision platform architecture).each do |param| - context "when info parameter '#{param}' info is present" do - let(:value) { "#{param}_value" } - - it "updates provided Runner's parameter" do - post api('/runners'), params: { - token: registration_token, - info: { param => value } - } - - expect(response).to have_gitlab_http_status(:created) - expect(::Ci::Runner.first.read_attribute(param.to_sym)).to eq(value) - end - end - end - - it "sets the runner's ip_address" do - post api('/runners'), - params: { token: registration_token }, - headers: { 'X-Forwarded-For' => '123.111.123.111' } - - expect(response).to have_gitlab_http_status(:created) - expect(::Ci::Runner.first.ip_address).to eq('123.111.123.111') - end - end - - describe 'DELETE /api/v4/runners' do - context 'when no token is provided' do - it 'returns 400 error' do - delete api('/runners') - - expect(response).to have_gitlab_http_status(:bad_request) - end - end - - context 'when invalid token is provided' do - it 'returns 403 error' do - delete api('/runners'), params: { token: 'invalid' } - - expect(response).to have_gitlab_http_status(:forbidden) - end - end - - context 'when valid token is provided' do - let(:runner) { create(:ci_runner) } - - it 'deletes Runner' do - delete api('/runners'), params: { token: runner.token } - - expect(response).to have_gitlab_http_status(:no_content) - expect(::Ci::Runner.count).to eq(0) - end - - it_behaves_like '412 response' do - let(:request) { api('/runners') } - let(:params) { { token: runner.token } } - end - end - end - - describe 'POST /api/v4/runners/verify' do - let(:runner) { create(:ci_runner) } - - context 'when no token is provided' do - it 'returns 400 error' do - post api('/runners/verify') - - expect(response).to have_gitlab_http_status :bad_request - end - end - - context 'when invalid token is provided' do - it 'returns 403 error' do - post api('/runners/verify'), params: { token: 'invalid-token' } - - expect(response).to have_gitlab_http_status(:forbidden) - end - end - - context 'when valid token is provided' do - it 'verifies Runner credentials' do - post api('/runners/verify'), params: { token: runner.token } - - expect(response).to have_gitlab_http_status(:ok) - end - end - end - end - - describe '/api/v4/jobs' do - shared_examples 'application context metadata' do |api_route| - it 'contains correct context metadata' do - # Avoids popping the context from the thread so we can - # check its content after the request. - allow(Labkit::Context).to receive(:pop) - - send_request - - Labkit::Context.with_context do |context| - expected_context = { - 'meta.caller_id' => api_route, - 'meta.user' => job.user.username, - 'meta.project' => job.project.full_path, - 'meta.root_namespace' => job.project.full_path_components.first - } - - expect(context.to_h).to include(expected_context) - end - end - end - - let(:root_namespace) { create(:namespace) } - let(:namespace) { create(:namespace, parent: root_namespace) } - let(:project) { create(:project, namespace: namespace, shared_runners_enabled: false) } - let(:pipeline) { create(:ci_pipeline, project: project, ref: 'master') } - let(:runner) { create(:ci_runner, :project, projects: [project]) } - let(:user) { create(:user) } - let(:job) do - create(:ci_build, :artifacts, :extended_options, - pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) - end - - describe 'POST /api/v4/jobs/request' do - let!(:last_update) {} - let!(:new_update) { } - let(:user_agent) { 'gitlab-runner 9.0.0 (9-0-stable; go1.7.4; linux/amd64)' } - - before do - job - stub_container_registry_config(enabled: false) - end - - shared_examples 'no jobs available' do - before do - request_job - end - - context 'when runner sends version in User-Agent' do - context 'for stable version' do - it 'gives 204 and set X-GitLab-Last-Update' do - expect(response).to have_gitlab_http_status(:no_content) - expect(response.header).to have_key('X-GitLab-Last-Update') - end - end - - context 'when last_update is up-to-date' do - let(:last_update) { runner.ensure_runner_queue_value } - - it 'gives 204 and set the same X-GitLab-Last-Update' do - expect(response).to have_gitlab_http_status(:no_content) - expect(response.header['X-GitLab-Last-Update']).to eq(last_update) - end - end - - context 'when last_update is outdated' do - let(:last_update) { runner.ensure_runner_queue_value } - let(:new_update) { runner.tick_runner_queue } - - it 'gives 204 and set a new X-GitLab-Last-Update' do - expect(response).to have_gitlab_http_status(:no_content) - expect(response.header['X-GitLab-Last-Update']).to eq(new_update) - end - end - - context 'when beta version is sent' do - let(:user_agent) { 'gitlab-runner 9.0.0~beta.167.g2b2bacc (master; go1.7.4; linux/amd64)' } - - it { expect(response).to have_gitlab_http_status(:no_content) } - end - - context 'when pre-9-0 version is sent' do - let(:user_agent) { 'gitlab-ci-multi-runner 1.6.0 (1-6-stable; go1.6.3; linux/amd64)' } - - it { expect(response).to have_gitlab_http_status(:no_content) } - end - - context 'when pre-9-0 beta version is sent' do - let(:user_agent) { 'gitlab-ci-multi-runner 1.6.0~beta.167.g2b2bacc (master; go1.6.3; linux/amd64)' } - - it { expect(response).to have_gitlab_http_status(:no_content) } - end - end - end - - context 'when no token is provided' do - it 'returns 400 error' do - post api('/jobs/request') - - expect(response).to have_gitlab_http_status(:bad_request) - end - end - - context 'when invalid token is provided' do - it 'returns 403 error' do - post api('/jobs/request'), params: { token: 'invalid' } - - expect(response).to have_gitlab_http_status(:forbidden) - end - end - - context 'when valid token is provided' do - context 'when Runner is not active' do - let(:runner) { create(:ci_runner, :inactive) } - let(:update_value) { runner.ensure_runner_queue_value } - - it 'returns 204 error' do - request_job - - expect(response).to have_gitlab_http_status(:no_content) - expect(response.header['X-GitLab-Last-Update']).to eq(update_value) - end - end - - context 'when jobs are finished' do - before do - job.success - end - - it_behaves_like 'no jobs available' - end - - context 'when other projects have pending jobs' do - before do - job.success - create(:ci_build, :pending) - end - - it_behaves_like 'no jobs available' - end - - context 'when shared runner requests job for project without shared_runners_enabled' do - let(:runner) { create(:ci_runner, :instance) } - - it_behaves_like 'no jobs available' - end - - context 'when there is a pending job' do - let(:expected_job_info) do - { 'name' => job.name, - 'stage' => job.stage, - 'project_id' => job.project.id, - 'project_name' => job.project.name } - end - - let(:expected_git_info) do - { 'repo_url' => job.repo_url, - 'ref' => job.ref, - 'sha' => job.sha, - 'before_sha' => job.before_sha, - 'ref_type' => 'branch', - 'refspecs' => ["+refs/pipelines/#{pipeline.id}:refs/pipelines/#{pipeline.id}", - "+refs/heads/#{job.ref}:refs/remotes/origin/#{job.ref}"], - 'depth' => project.ci_default_git_depth } - end - - let(:expected_steps) do - [{ 'name' => 'script', - 'script' => %w(echo), - 'timeout' => job.metadata_timeout, - 'when' => 'on_success', - 'allow_failure' => false }, - { 'name' => 'after_script', - 'script' => %w(ls date), - 'timeout' => job.metadata_timeout, - 'when' => 'always', - 'allow_failure' => true }] - end - - let(:expected_variables) do - [{ 'key' => 'CI_JOB_NAME', 'value' => 'spinach', 'public' => true, 'masked' => false }, - { 'key' => 'CI_JOB_STAGE', 'value' => 'test', 'public' => true, 'masked' => false }, - { 'key' => 'DB_NAME', 'value' => 'postgres', 'public' => true, 'masked' => false }] - end - - let(:expected_artifacts) do - [{ 'name' => 'artifacts_file', - 'untracked' => false, - 'paths' => %w(out/), - 'when' => 'always', - 'expire_in' => '7d', - "artifact_type" => "archive", - "artifact_format" => "zip" }] - end - - let(:expected_cache) do - [{ 'key' => 'cache_key', - 'untracked' => false, - 'paths' => ['vendor/*'], - 'policy' => 'pull-push' }] - end - - let(:expected_features) { { 'trace_sections' => true } } - - it 'picks a job' do - request_job info: { platform: :darwin } - - expect(response).to have_gitlab_http_status(:created) - expect(response.headers['Content-Type']).to eq('application/json') - expect(response.headers).not_to have_key('X-GitLab-Last-Update') - expect(runner.reload.platform).to eq('darwin') - expect(json_response['id']).to eq(job.id) - expect(json_response['token']).to eq(job.token) - expect(json_response['job_info']).to eq(expected_job_info) - expect(json_response['git_info']).to eq(expected_git_info) - expect(json_response['image']).to eq({ 'name' => 'ruby:2.7', 'entrypoint' => '/bin/sh', 'ports' => [] }) - expect(json_response['services']).to eq([{ 'name' => 'postgres', 'entrypoint' => nil, - 'alias' => nil, 'command' => nil, 'ports' => [] }, - { 'name' => 'docker:stable-dind', 'entrypoint' => '/bin/sh', - 'alias' => 'docker', 'command' => 'sleep 30', 'ports' => [] }]) - expect(json_response['steps']).to eq(expected_steps) - expect(json_response['artifacts']).to eq(expected_artifacts) - expect(json_response['cache']).to eq(expected_cache) - expect(json_response['variables']).to include(*expected_variables) - expect(json_response['features']).to eq(expected_features) - end - - it 'creates persistent ref' do - expect_any_instance_of(::Ci::PersistentRef).to receive(:create_ref) - .with(job.sha, "refs/#{Repository::REF_PIPELINES}/#{job.commit_id}") - - request_job info: { platform: :darwin } - - expect(response).to have_gitlab_http_status(:created) - expect(json_response['id']).to eq(job.id) - end - - context 'when job is made for tag' do - let!(:job) { create(:ci_build, :tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) } - - it 'sets branch as ref_type' do - request_job - - expect(response).to have_gitlab_http_status(:created) - expect(json_response['git_info']['ref_type']).to eq('tag') - end - - context 'when GIT_DEPTH is specified' do - before do - create(:ci_pipeline_variable, key: 'GIT_DEPTH', value: 1, pipeline: pipeline) - end - - it 'specifies refspecs' do - request_job - - expect(response).to have_gitlab_http_status(:created) - expect(json_response['git_info']['refspecs']).to include("+refs/tags/#{job.ref}:refs/tags/#{job.ref}") - end - end - - context 'when a Gitaly exception is thrown during response' do - before do - allow_next_instance_of(Ci::BuildRunnerPresenter) do |instance| - allow(instance).to receive(:artifacts).and_raise(GRPC::DeadlineExceeded) - end - end - - it 'fails the job as a scheduler failure' do - request_job - - expect(response).to have_gitlab_http_status(:no_content) - expect(job.reload.failed?).to be_truthy - expect(job.failure_reason).to eq('scheduler_failure') - expect(job.runner_id).to eq(runner.id) - expect(job.runner_session).to be_nil - end - end - - context 'when GIT_DEPTH is not specified and there is no default git depth for the project' do - before do - project.update!(ci_default_git_depth: nil) - end - - it 'specifies refspecs' do - request_job - - expect(response).to have_gitlab_http_status(:created) - expect(json_response['git_info']['refspecs']) - .to contain_exactly("+refs/pipelines/#{pipeline.id}:refs/pipelines/#{pipeline.id}", - '+refs/tags/*:refs/tags/*', - '+refs/heads/*:refs/remotes/origin/*') - end - end - end - - context 'when job filtered by job_age' do - let!(:job) { create(:ci_build, :tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0, queued_at: 60.seconds.ago) } - - context 'job is queued less than job_age parameter' do - let(:job_age) { 120 } - - it 'gives 204' do - request_job(job_age: job_age) - - expect(response).to have_gitlab_http_status(:no_content) - end - end - - context 'job is queued more than job_age parameter' do - let(:job_age) { 30 } - - it 'picks a job' do - request_job(job_age: job_age) - - expect(response).to have_gitlab_http_status(:created) - end - end - end - - context 'when job is made for branch' do - it 'sets tag as ref_type' do - request_job - - expect(response).to have_gitlab_http_status(:created) - expect(json_response['git_info']['ref_type']).to eq('branch') - end - - context 'when GIT_DEPTH is specified' do - before do - create(:ci_pipeline_variable, key: 'GIT_DEPTH', value: 1, pipeline: pipeline) - end - - it 'specifies refspecs' do - request_job - - expect(response).to have_gitlab_http_status(:created) - expect(json_response['git_info']['refspecs']).to include("+refs/heads/#{job.ref}:refs/remotes/origin/#{job.ref}") - end - end - - context 'when GIT_DEPTH is not specified and there is no default git depth for the project' do - before do - project.update!(ci_default_git_depth: nil) - end - - it 'specifies refspecs' do - request_job - - expect(response).to have_gitlab_http_status(:created) - expect(json_response['git_info']['refspecs']) - .to contain_exactly("+refs/pipelines/#{pipeline.id}:refs/pipelines/#{pipeline.id}", - '+refs/tags/*:refs/tags/*', - '+refs/heads/*:refs/remotes/origin/*') - end - end - end - - context 'when job is for a release' do - let!(:job) { create(:ci_build, :release_options, pipeline: pipeline) } - - context 'when `multi_build_steps` is passed by the runner' do - it 'exposes release info' do - request_job info: { features: { multi_build_steps: true } } - - expect(response).to have_gitlab_http_status(:created) - expect(response.headers).not_to have_key('X-GitLab-Last-Update') - expect(json_response['steps']).to eq([ - { - "name" => "script", - "script" => ["make changelog | tee release_changelog.txt"], - "timeout" => 3600, - "when" => "on_success", - "allow_failure" => false - }, - { - "name" => "release", - "script" => - ["release-cli create --name \"Release $CI_COMMIT_SHA\" --description \"Created using the release-cli $EXTRA_DESCRIPTION\" --tag-name \"release-$CI_COMMIT_SHA\" --ref \"$CI_COMMIT_SHA\""], - "timeout" => 3600, - "when" => "on_success", - "allow_failure" => false - } - ]) - end - end - - context 'when `multi_build_steps` is not passed by the runner' do - it 'drops the job' do - request_job - - expect(response).to have_gitlab_http_status(:no_content) - end - end - end - - context 'when job is made for merge request' do - let(:pipeline) { create(:ci_pipeline, source: :merge_request_event, project: project, ref: 'feature', merge_request: merge_request) } - let!(:job) { create(:ci_build, pipeline: pipeline, name: 'spinach', ref: 'feature', stage: 'test', stage_idx: 0) } - let(:merge_request) { create(:merge_request) } - - it 'sets branch as ref_type' do - request_job - - expect(response).to have_gitlab_http_status(:created) - expect(json_response['git_info']['ref_type']).to eq('branch') - end - - context 'when GIT_DEPTH is specified' do - before do - create(:ci_pipeline_variable, key: 'GIT_DEPTH', value: 1, pipeline: pipeline) - end - - it 'returns the overwritten git depth for merge request refspecs' do - request_job - - expect(response).to have_gitlab_http_status(:created) - expect(json_response['git_info']['depth']).to eq(1) - end - end - end - - it 'updates runner info' do - expect { request_job }.to change { runner.reload.contacted_at } - end - - %w(version revision platform architecture).each do |param| - context "when info parameter '#{param}' is present" do - let(:value) { "#{param}_value" } - - it "updates provided Runner's parameter" do - request_job info: { param => value } - - expect(response).to have_gitlab_http_status(:created) - expect(runner.reload.read_attribute(param.to_sym)).to eq(value) - end - end - end - - it "sets the runner's ip_address" do - post api('/jobs/request'), - params: { token: runner.token }, - headers: { 'User-Agent' => user_agent, 'X-Forwarded-For' => '123.222.123.222' } - - expect(response).to have_gitlab_http_status(:created) - expect(runner.reload.ip_address).to eq('123.222.123.222') - end - - it "handles multiple X-Forwarded-For addresses" do - post api('/jobs/request'), - params: { token: runner.token }, - headers: { 'User-Agent' => user_agent, 'X-Forwarded-For' => '123.222.123.222, 127.0.0.1' } - - expect(response).to have_gitlab_http_status(:created) - expect(runner.reload.ip_address).to eq('123.222.123.222') - end - - context 'when concurrently updating a job' do - before do - expect_any_instance_of(::Ci::Build).to receive(:run!) - .and_raise(ActiveRecord::StaleObjectError.new(nil, nil)) - end - - it 'returns a conflict' do - request_job - - expect(response).to have_gitlab_http_status(:conflict) - expect(response.headers).not_to have_key('X-GitLab-Last-Update') - end - end - - context 'when project and pipeline have multiple jobs' do - let!(:job) { create(:ci_build, :tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) } - let!(:job2) { create(:ci_build, :tag, pipeline: pipeline, name: 'rubocop', stage: 'test', stage_idx: 0) } - let!(:test_job) { create(:ci_build, pipeline: pipeline, name: 'deploy', stage: 'deploy', stage_idx: 1) } - - before do - job.success - job2.success - end - - it 'returns dependent jobs' do - request_job - - expect(response).to have_gitlab_http_status(:created) - expect(json_response['id']).to eq(test_job.id) - expect(json_response['dependencies'].count).to eq(2) - expect(json_response['dependencies']).to include( - { 'id' => job.id, 'name' => job.name, 'token' => job.token }, - { 'id' => job2.id, 'name' => job2.name, 'token' => job2.token }) - end - end - - context 'when pipeline have jobs with artifacts' do - let!(:job) { create(:ci_build, :tag, :artifacts, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) } - let!(:test_job) { create(:ci_build, pipeline: pipeline, name: 'deploy', stage: 'deploy', stage_idx: 1) } - - before do - job.success - end - - it 'returns dependent jobs' do - request_job - - expect(response).to have_gitlab_http_status(:created) - expect(json_response['id']).to eq(test_job.id) - expect(json_response['dependencies'].count).to eq(1) - expect(json_response['dependencies']).to include( - { 'id' => job.id, 'name' => job.name, 'token' => job.token, - 'artifacts_file' => { 'filename' => 'ci_build_artifacts.zip', 'size' => 107464 } }) - end - end - - context 'when explicit dependencies are defined' do - let!(:job) { create(:ci_build, :tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) } - let!(:job2) { create(:ci_build, :tag, pipeline: pipeline, name: 'rubocop', stage: 'test', stage_idx: 0) } - let!(:test_job) do - create(:ci_build, pipeline: pipeline, token: 'test-job-token', name: 'deploy', - stage: 'deploy', stage_idx: 1, - options: { script: ['bash'], dependencies: [job2.name] }) - end - - before do - job.success - job2.success - end - - it 'returns dependent jobs' do - request_job - - expect(response).to have_gitlab_http_status(:created) - expect(json_response['id']).to eq(test_job.id) - expect(json_response['dependencies'].count).to eq(1) - expect(json_response['dependencies'][0]).to include('id' => job2.id, 'name' => job2.name, 'token' => job2.token) - end - end - - context 'when dependencies is an empty array' do - let!(:job) { create(:ci_build, :tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) } - let!(:job2) { create(:ci_build, :tag, pipeline: pipeline, name: 'rubocop', stage: 'test', stage_idx: 0) } - let!(:empty_dependencies_job) do - create(:ci_build, pipeline: pipeline, token: 'test-job-token', name: 'empty_dependencies_job', - stage: 'deploy', stage_idx: 1, - options: { script: ['bash'], dependencies: [] }) - end - - before do - job.success - job2.success - end - - it 'returns an empty array' do - request_job - - expect(response).to have_gitlab_http_status(:created) - expect(json_response['id']).to eq(empty_dependencies_job.id) - expect(json_response['dependencies'].count).to eq(0) - end - end - - context 'when job has no tags' do - before do - job.update(tags: []) - end - - context 'when runner is allowed to pick untagged jobs' do - before do - runner.update_column(:run_untagged, true) - end - - it 'picks job' do - request_job - - expect(response).to have_gitlab_http_status(:created) - end - end - - context 'when runner is not allowed to pick untagged jobs' do - before do - runner.update_column(:run_untagged, false) - end - - it_behaves_like 'no jobs available' - end - end - - context 'when triggered job is available' do - let(:expected_variables) do - [{ 'key' => 'CI_JOB_NAME', 'value' => 'spinach', 'public' => true, 'masked' => false }, - { 'key' => 'CI_JOB_STAGE', 'value' => 'test', 'public' => true, 'masked' => false }, - { 'key' => 'CI_PIPELINE_TRIGGERED', 'value' => 'true', 'public' => true, 'masked' => false }, - { 'key' => 'DB_NAME', 'value' => 'postgres', 'public' => true, 'masked' => false }, - { 'key' => 'SECRET_KEY', 'value' => 'secret_value', 'public' => false, 'masked' => false }, - { 'key' => 'TRIGGER_KEY_1', 'value' => 'TRIGGER_VALUE_1', 'public' => false, 'masked' => false }] - end - - let(:trigger) { create(:ci_trigger, project: project) } - let!(:trigger_request) { create(:ci_trigger_request, pipeline: pipeline, builds: [job], trigger: trigger) } - - before do - project.variables << ::Ci::Variable.new(key: 'SECRET_KEY', value: 'secret_value') - end - - shared_examples 'expected variables behavior' do - it 'returns variables for triggers' do - request_job - - expect(response).to have_gitlab_http_status(:created) - expect(json_response['variables']).to include(*expected_variables) - end - end - - context 'when variables are stored in trigger_request' do - before do - trigger_request.update_attribute(:variables, { TRIGGER_KEY_1: 'TRIGGER_VALUE_1' } ) - end - - it_behaves_like 'expected variables behavior' - end - - context 'when variables are stored in pipeline_variables' do - before do - create(:ci_pipeline_variable, pipeline: pipeline, key: :TRIGGER_KEY_1, value: 'TRIGGER_VALUE_1') - end - - it_behaves_like 'expected variables behavior' - end - end - - describe 'registry credentials support' do - let(:registry_url) { 'registry.example.com:5005' } - let(:registry_credentials) do - { 'type' => 'registry', - 'url' => registry_url, - 'username' => 'gitlab-ci-token', - 'password' => job.token } - end - - context 'when registry is enabled' do - before do - stub_container_registry_config(enabled: true, host_port: registry_url) - end - - it 'sends registry credentials key' do - request_job - - expect(json_response).to have_key('credentials') - expect(json_response['credentials']).to include(registry_credentials) - end - end - - context 'when registry is disabled' do - before do - stub_container_registry_config(enabled: false, host_port: registry_url) - end - - it 'does not send registry credentials' do - request_job - - expect(json_response).to have_key('credentials') - expect(json_response['credentials']).not_to include(registry_credentials) - end - end - end - - describe 'timeout support' do - context 'when project specifies job timeout' do - let(:project) { create(:project, shared_runners_enabled: false, build_timeout: 1234) } - - it 'contains info about timeout taken from project' do - request_job - - expect(response).to have_gitlab_http_status(:created) - expect(json_response['runner_info']).to include({ 'timeout' => 1234 }) - end - - context 'when runner specifies lower timeout' do - let(:runner) { create(:ci_runner, :project, maximum_timeout: 1000, projects: [project]) } - - it 'contains info about timeout overridden by runner' do - request_job - - expect(response).to have_gitlab_http_status(:created) - expect(json_response['runner_info']).to include({ 'timeout' => 1000 }) - end - end - - context 'when runner specifies bigger timeout' do - let(:runner) { create(:ci_runner, :project, maximum_timeout: 2000, projects: [project]) } - - it 'contains info about timeout not overridden by runner' do - request_job - - expect(response).to have_gitlab_http_status(:created) - expect(json_response['runner_info']).to include({ 'timeout' => 1234 }) - end - end - end - end - end - - describe 'port support' do - let(:job) { create(:ci_build, pipeline: pipeline, options: options) } - - context 'when job image has ports' do - let(:options) do - { - image: { - name: 'ruby', - ports: [80] - }, - services: ['mysql'] - } - end - - it 'returns the image ports' do - request_job - - expect(response).to have_gitlab_http_status(:created) - expect(json_response).to include( - 'id' => job.id, - 'image' => a_hash_including('name' => 'ruby', 'ports' => [{ 'number' => 80, 'protocol' => 'http', 'name' => 'default_port' }]), - 'services' => all(a_hash_including('name' => 'mysql'))) - end - end - - context 'when job services settings has ports' do - let(:options) do - { - image: 'ruby', - services: [ - { - name: 'tomcat', - ports: [{ number: 8081, protocol: 'http', name: 'custom_port' }] - } - ] - } - end - - it 'returns the service ports' do - request_job - - expect(response).to have_gitlab_http_status(:created) - expect(json_response).to include( - 'id' => job.id, - 'image' => a_hash_including('name' => 'ruby'), - 'services' => all(a_hash_including('name' => 'tomcat', 'ports' => [{ 'number' => 8081, 'protocol' => 'http', 'name' => 'custom_port' }]))) - end - end - end - - describe 'a job with excluded artifacts' do - context 'when excluded paths are defined' do - let(:job) do - create(:ci_build, pipeline: pipeline, token: 'test-job-token', name: 'test', - stage: 'deploy', stage_idx: 1, - options: { artifacts: { paths: ['abc'], exclude: ['cde'] } }) - end - - context 'when a runner supports this feature' do - it 'exposes excluded paths when the feature is enabled' do - stub_feature_flags(ci_artifacts_exclude: true) - - request_job info: { features: { artifacts_exclude: true } } - - expect(response).to have_gitlab_http_status(:created) - expect(json_response.dig('artifacts').first).to include('exclude' => ['cde']) - end - - it 'does not expose excluded paths when the feature is disabled' do - stub_feature_flags(ci_artifacts_exclude: false) - - request_job info: { features: { artifacts_exclude: true } } - - expect(response).to have_gitlab_http_status(:created) - expect(json_response.dig('artifacts').first).not_to have_key('exclude') - end - end - - context 'when a runner does not support this feature' do - it 'does not expose the build at all' do - stub_feature_flags(ci_artifacts_exclude: true) - - request_job - - expect(response).to have_gitlab_http_status(:no_content) - end - end - end - - it 'does not expose excluded paths when these are empty' do - request_job - - expect(response).to have_gitlab_http_status(:created) - expect(json_response.dig('artifacts').first).not_to have_key('exclude') - end - end - - def request_job(token = runner.token, **params) - new_params = params.merge(token: token, last_update: last_update) - post api('/jobs/request'), params: new_params.to_json, headers: { 'User-Agent' => user_agent, 'Content-Type': 'application/json' } - end - end - - context 'for web-ide job' do - let_it_be(:user) { create(:user) } - let_it_be(:project) { create(:project, :repository) } - - let(:runner) { create(:ci_runner, :project, projects: [project]) } - let(:service) { ::Ci::CreateWebIdeTerminalService.new(project, user, ref: 'master').execute } - let(:pipeline) { service[:pipeline] } - let(:build) { pipeline.builds.first } - let(:job) { {} } - let(:config_content) do - 'terminal: { image: ruby, services: [mysql], before_script: [ls], tags: [tag-1], variables: { KEY: value } }' - end - - before do - stub_webide_config_file(config_content) - project.add_maintainer(user) - - pipeline - end - - context 'when runner has matching tag' do - before do - runner.update!(tag_list: ['tag-1']) - end - - it 'successfully picks job' do - request_job - - build.reload - - expect(build).to be_running - expect(build.runner).to eq(runner) - - expect(response).to have_gitlab_http_status(:created) - expect(json_response).to include( - "id" => build.id, - "variables" => include("key" => 'KEY', "value" => 'value', "public" => true, "masked" => false), - "image" => a_hash_including("name" => 'ruby'), - "services" => all(a_hash_including("name" => 'mysql')), - "job_info" => a_hash_including("name" => 'terminal', "stage" => 'terminal')) - end - end - - context 'when runner does not have matching tags' do - it 'does not pick a job' do - request_job - - build.reload - - expect(build).to be_pending - expect(response).to have_gitlab_http_status(:no_content) - end - end - - def request_job(token = runner.token, **params) - post api('/jobs/request'), params: params.merge(token: token) - end - end - end - - describe 'PUT /api/v4/jobs/:id' do - let(:job) do - create(:ci_build, :pending, :trace_live, pipeline: pipeline, project: project, user: user, runner_id: runner.id) - end - - before do - job.run! - end - - it_behaves_like 'application context metadata', '/api/:version/jobs/:id' do - let(:send_request) { update_job(state: 'success') } - end - - it 'updates runner info' do - expect { update_job(state: 'success') }.to change { runner.reload.contacted_at } - end - - context 'when status is given' do - it 'mark job as succeeded' do - update_job(state: 'success') - - job.reload - expect(job).to be_success - end - - it 'mark job as failed' do - update_job(state: 'failed') - - job.reload - expect(job).to be_failed - expect(job).to be_unknown_failure - end - - context 'when failure_reason is script_failure' do - before do - update_job(state: 'failed', failure_reason: 'script_failure') - job.reload - end - - it { expect(job).to be_script_failure } - end - - context 'when failure_reason is runner_system_failure' do - before do - update_job(state: 'failed', failure_reason: 'runner_system_failure') - job.reload - end - - it { expect(job).to be_runner_system_failure } - end - - context 'when failure_reason is unrecognized value' do - before do - update_job(state: 'failed', failure_reason: 'what_is_this') - job.reload - end - - it { expect(job).to be_unknown_failure } - end - - context 'when failure_reason is job_execution_timeout' do - before do - update_job(state: 'failed', failure_reason: 'job_execution_timeout') - job.reload - end - - it { expect(job).to be_job_execution_timeout } - end - - context 'when failure_reason is unmet_prerequisites' do - before do - update_job(state: 'failed', failure_reason: 'unmet_prerequisites') - job.reload - end - - it { expect(job).to be_unmet_prerequisites } - end - end - - context 'when trace is given' do - it 'creates a trace artifact' do - allow(BuildFinishedWorker).to receive(:perform_async).with(job.id) do - ArchiveTraceWorker.new.perform(job.id) - end - - update_job(state: 'success', trace: 'BUILD TRACE UPDATED') - - job.reload - expect(response).to have_gitlab_http_status(:ok) - expect(job.trace.raw).to eq 'BUILD TRACE UPDATED' - expect(job.job_artifacts_trace.open.read).to eq 'BUILD TRACE UPDATED' - end - - context 'when concurrent update of trace is happening' do - before do - job.trace.write('wb') do - update_job(state: 'success', trace: 'BUILD TRACE UPDATED') - end - end - - it 'returns that operation conflicts' do - expect(response).to have_gitlab_http_status(:conflict) - end - end - end - - context 'when no trace is given' do - it 'does not override trace information' do - update_job - - expect(job.reload.trace.raw).to eq 'BUILD TRACE' - end - - context 'when running state is sent' do - it 'updates update_at value' do - expect { update_job_after_time }.to change { job.reload.updated_at } - end - end - - context 'when other state is sent' do - it "doesn't update update_at value" do - expect { update_job_after_time(20.minutes, state: 'success') }.not_to change { job.reload.updated_at } - end - end - end - - context 'when job has been erased' do - let(:job) { create(:ci_build, runner_id: runner.id, erased_at: Time.now) } - - it 'responds with forbidden' do - update_job - - expect(response).to have_gitlab_http_status(:forbidden) - end - end - - context 'when job has already been finished' do - before do - job.trace.set('Job failed') - job.drop!(:script_failure) - end - - it 'does not update job status and job trace' do - update_job(state: 'success', trace: 'BUILD TRACE UPDATED') - - job.reload - expect(response).to have_gitlab_http_status(:forbidden) - expect(response.header['Job-Status']).to eq 'failed' - expect(job.trace.raw).to eq 'Job failed' - expect(job).to be_failed - end - end - - def update_job(token = job.token, **params) - new_params = params.merge(token: token) - put api("/jobs/#{job.id}"), params: new_params - end - - def update_job_after_time(update_interval = 20.minutes, state = 'running') - Timecop.travel(job.updated_at + update_interval) do - update_job(job.token, state: state) - end - end - end - - describe 'PATCH /api/v4/jobs/:id/trace' do - let(:job) do - create(:ci_build, :running, :trace_live, - project: project, user: user, runner_id: runner.id, pipeline: pipeline) - end - let(:headers) { { API::Helpers::Runner::JOB_TOKEN_HEADER => job.token, 'Content-Type' => 'text/plain' } } - let(:headers_with_range) { headers.merge({ 'Content-Range' => '11-20' }) } - let(:update_interval) { 10.seconds.to_i } - - before do - initial_patch_the_trace - end - - it_behaves_like 'application context metadata', '/api/:version/jobs/:id/trace' do - let(:send_request) { patch_the_trace } - end - - it 'updates runner info' do - runner.update!(contacted_at: 1.year.ago) - - expect { patch_the_trace }.to change { runner.reload.contacted_at } - end - - context 'when request is valid' do - it 'gets correct response' do - expect(response).to have_gitlab_http_status(:accepted) - expect(job.reload.trace.raw).to eq 'BUILD TRACE appended' - expect(response.header).to have_key 'Range' - expect(response.header).to have_key 'Job-Status' - expect(response.header).to have_key 'X-GitLab-Trace-Update-Interval' - end - - context 'when job has been updated recently' do - it { expect { patch_the_trace }.not_to change { job.updated_at }} - - it "changes the job's trace" do - patch_the_trace - - expect(job.reload.trace.raw).to eq 'BUILD TRACE appended appended' - end - - context 'when Runner makes a force-patch' do - it { expect { force_patch_the_trace }.not_to change { job.updated_at }} - - it "doesn't change the build.trace" do - force_patch_the_trace - - expect(job.reload.trace.raw).to eq 'BUILD TRACE appended' - end - end - end - - context 'when job was not updated recently' do - let(:update_interval) { 15.minutes.to_i } - - it { expect { patch_the_trace }.to change { job.updated_at } } - - it 'changes the job.trace' do - patch_the_trace - - expect(job.reload.trace.raw).to eq 'BUILD TRACE appended appended' - end - - context 'when Runner makes a force-patch' do - it { expect { force_patch_the_trace }.to change { job.updated_at } } - - it "doesn't change the job.trace" do - force_patch_the_trace - - expect(job.reload.trace.raw).to eq 'BUILD TRACE appended' - end - end - end - - context 'when project for the build has been deleted' do - let(:job) do - create(:ci_build, :running, :trace_live, runner_id: runner.id, pipeline: pipeline) do |job| - job.project.update(pending_delete: true) - end - end - - it 'responds with forbidden' do - expect(response).to have_gitlab_http_status(:forbidden) - end - end - - context 'when trace is patched' do - before do - patch_the_trace - end - - it 'has valid trace' do - expect(response).to have_gitlab_http_status(:accepted) - expect(job.reload.trace.raw).to eq 'BUILD TRACE appended appended' - end - - context 'when job is cancelled' do - before do - job.cancel - end - - context 'when trace is patched' do - before do - patch_the_trace - end - - it 'returns Forbidden ' do - expect(response).to have_gitlab_http_status(:forbidden) - end - end - end - - context 'when redis data are flushed' do - before do - redis_shared_state_cleanup! - end - - it 'has empty trace' do - expect(job.reload.trace.raw).to eq '' - end - - context 'when we perform partial patch' do - before do - patch_the_trace('hello', headers.merge({ 'Content-Range' => "28-32/5" })) - end - - it 'returns an error' do - expect(response).to have_gitlab_http_status(:range_not_satisfiable) - expect(response.header['Range']).to eq('0-0') - end - end - - context 'when we resend full trace' do - before do - patch_the_trace('BUILD TRACE appended appended hello', headers.merge({ 'Content-Range' => "0-34/35" })) - end - - it 'succeeds with updating trace' do - expect(response).to have_gitlab_http_status(:accepted) - expect(job.reload.trace.raw).to eq 'BUILD TRACE appended appended hello' - end - end - end - end - - context 'when concurrent update of trace is happening' do - before do - job.trace.write('wb') do - patch_the_trace - end - end - - it 'returns that operation conflicts' do - expect(response).to have_gitlab_http_status(:conflict) - end - end - - context 'when the job is canceled' do - before do - job.cancel - patch_the_trace - end - - it 'receives status in header' do - expect(response.header['Job-Status']).to eq 'canceled' - end - end - - context 'when build trace is being watched' do - before do - job.trace.being_watched! - end - - it 'returns X-GitLab-Trace-Update-Interval as 3' do - patch_the_trace - - expect(response).to have_gitlab_http_status(:accepted) - expect(response.header['X-GitLab-Trace-Update-Interval']).to eq('3') - end - end - - context 'when build trace is not being watched' do - it 'returns X-GitLab-Trace-Update-Interval as 30' do - patch_the_trace - - expect(response).to have_gitlab_http_status(:accepted) - expect(response.header['X-GitLab-Trace-Update-Interval']).to eq('30') - end - end - end - - context 'when Runner makes a force-patch' do - before do - force_patch_the_trace - end - - it 'gets correct response' do - expect(response).to have_gitlab_http_status(:accepted) - expect(job.reload.trace.raw).to eq 'BUILD TRACE appended' - expect(response.header).to have_key 'Range' - expect(response.header).to have_key 'Job-Status' - end - end - - context 'when content-range start is too big' do - let(:headers_with_range) { headers.merge({ 'Content-Range' => '15-20/6' }) } - - it 'gets 416 error response with range headers' do - expect(response).to have_gitlab_http_status(:range_not_satisfiable) - expect(response.header).to have_key 'Range' - expect(response.header['Range']).to eq '0-11' - end - end - - context 'when content-range start is too small' do - let(:headers_with_range) { headers.merge({ 'Content-Range' => '8-20/13' }) } - - it 'gets 416 error response with range headers' do - expect(response).to have_gitlab_http_status(:range_not_satisfiable) - expect(response.header).to have_key 'Range' - expect(response.header['Range']).to eq '0-11' - end - end - - context 'when Content-Range header is missing' do - let(:headers_with_range) { headers } - - it { expect(response).to have_gitlab_http_status(:bad_request) } - end - - context 'when job has been errased' do - let(:job) { create(:ci_build, runner_id: runner.id, erased_at: Time.now) } - - it { expect(response).to have_gitlab_http_status(:forbidden) } - end - - def patch_the_trace(content = ' appended', request_headers = nil) - unless request_headers - job.trace.read do |stream| - offset = stream.size - limit = offset + content.length - 1 - request_headers = headers.merge({ 'Content-Range' => "#{offset}-#{limit}" }) - end - end - - Timecop.travel(job.updated_at + update_interval) do - patch api("/jobs/#{job.id}/trace"), params: content, headers: request_headers - job.reload - end - end - - def initial_patch_the_trace - patch_the_trace(' appended', headers_with_range) - end - - def force_patch_the_trace - 2.times { patch_the_trace('') } - end - end - - describe 'artifacts' do - let(:job) { create(:ci_build, :pending, user: user, project: project, pipeline: pipeline, runner_id: runner.id) } - let(:jwt) { JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256') } - let(:headers) { { 'GitLab-Workhorse' => '1.0', Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER => jwt } } - let(:headers_with_token) { headers.merge(API::Helpers::Runner::JOB_TOKEN_HEADER => job.token) } - let(:file_upload) { fixture_file_upload('spec/fixtures/banana_sample.gif', 'image/gif') } - let(:file_upload2) { fixture_file_upload('spec/fixtures/dk.png', 'image/gif') } - - before do - stub_artifacts_object_storage - job.run! - end - - shared_examples_for 'rejecting artifacts that are too large' do - let(:filesize) { 100.megabytes.to_i } - let(:sample_max_size) { (filesize / 1.megabyte) - 10 } # Set max size to be smaller than file size to trigger error - - shared_examples_for 'failed request' do - it 'responds with payload too large error' do - send_request - - expect(response).to have_gitlab_http_status(:payload_too_large) - end - end - - context 'based on plan limit setting' do - let(:application_max_size) { sample_max_size + 100 } - let(:limit_name) { "#{Ci::JobArtifact::PLAN_LIMIT_PREFIX}archive" } - - before do - create(:plan_limits, :default_plan, limit_name => sample_max_size) - stub_application_setting(max_artifacts_size: application_max_size) - end - - context 'and feature flag ci_max_artifact_size_per_type is enabled' do - before do - stub_feature_flags(ci_max_artifact_size_per_type: true) - end - - it_behaves_like 'failed request' - end - - context 'and feature flag ci_max_artifact_size_per_type is disabled' do - before do - stub_feature_flags(ci_max_artifact_size_per_type: false) - end - - it 'bases of project closest setting' do - send_request - - expect(response).to have_gitlab_http_status(success_code) - end - end - end - - context 'based on application setting' do - before do - stub_application_setting(max_artifacts_size: sample_max_size) - end - - it_behaves_like 'failed request' - end - - context 'based on root namespace setting' do - let(:application_max_size) { sample_max_size + 10 } - - before do - stub_application_setting(max_artifacts_size: application_max_size) - root_namespace.update!(max_artifacts_size: sample_max_size) - end - - it_behaves_like 'failed request' - end - - context 'based on child namespace setting' do - let(:application_max_size) { sample_max_size + 10 } - let(:root_namespace_max_size) { sample_max_size + 10 } - - before do - stub_application_setting(max_artifacts_size: application_max_size) - root_namespace.update!(max_artifacts_size: root_namespace_max_size) - namespace.update!(max_artifacts_size: sample_max_size) - end - - it_behaves_like 'failed request' - end - - context 'based on project setting' do - let(:application_max_size) { sample_max_size + 10 } - let(:root_namespace_max_size) { sample_max_size + 10 } - let(:child_namespace_max_size) { sample_max_size + 10 } - - before do - stub_application_setting(max_artifacts_size: application_max_size) - root_namespace.update!(max_artifacts_size: root_namespace_max_size) - namespace.update!(max_artifacts_size: child_namespace_max_size) - project.update!(max_artifacts_size: sample_max_size) - end - - it_behaves_like 'failed request' - end - end - - describe 'POST /api/v4/jobs/:id/artifacts/authorize' do - context 'when using token as parameter' do - context 'and the artifact is too large' do - it_behaves_like 'rejecting artifacts that are too large' do - let(:success_code) { :ok } - let(:send_request) { authorize_artifacts_with_token_in_params(filesize: filesize) } - end - end - - context 'posting artifacts to running job' do - subject do - authorize_artifacts_with_token_in_params - end - - it_behaves_like 'application context metadata', '/api/:version/jobs/:id/artifacts/authorize' do - let(:send_request) { subject } - end - - it 'updates runner info' do - expect { subject }.to change { runner.reload.contacted_at } - end - - shared_examples 'authorizes local file' do - it 'succeeds' do - subject - - expect(response).to have_gitlab_http_status(:ok) - expect(response.media_type).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE) - expect(json_response['TempPath']).to eq(JobArtifactUploader.workhorse_local_upload_path) - expect(json_response['RemoteObject']).to be_nil - end - end - - context 'when using local storage' do - it_behaves_like 'authorizes local file' - end - - context 'when using remote storage' do - context 'when direct upload is enabled' do - before do - stub_artifacts_object_storage(enabled: true, direct_upload: true) - end - - it 'succeeds' do - subject - - expect(response).to have_gitlab_http_status(:ok) - expect(response.media_type).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE) - expect(json_response).not_to have_key('TempPath') - expect(json_response['RemoteObject']).to have_key('ID') - expect(json_response['RemoteObject']).to have_key('GetURL') - expect(json_response['RemoteObject']).to have_key('StoreURL') - expect(json_response['RemoteObject']).to have_key('DeleteURL') - expect(json_response['RemoteObject']).to have_key('MultipartUpload') - end - end - - context 'when direct upload is disabled' do - before do - stub_artifacts_object_storage(enabled: true, direct_upload: false) - end - - it_behaves_like 'authorizes local file' - end - end - end - end - - context 'when using token as header' do - it 'authorizes posting artifacts to running job' do - authorize_artifacts_with_token_in_headers - - expect(response).to have_gitlab_http_status(:ok) - expect(response.media_type).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE) - expect(json_response['TempPath']).not_to be_nil - end - - it 'fails to post too large artifact' do - stub_application_setting(max_artifacts_size: 0) - - authorize_artifacts_with_token_in_headers(filesize: 100) - - expect(response).to have_gitlab_http_status(:payload_too_large) - end - end - - context 'when using runners token' do - it 'fails to authorize artifacts posting' do - authorize_artifacts(token: job.project.runners_token) - - expect(response).to have_gitlab_http_status(:forbidden) - end - end - - it 'reject requests that did not go through gitlab-workhorse' do - headers.delete(Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER) - - authorize_artifacts - - expect(response).to have_gitlab_http_status(:forbidden) - end - - context 'authorization token is invalid' do - it 'responds with forbidden' do - authorize_artifacts(token: 'invalid', filesize: 100 ) - - expect(response).to have_gitlab_http_status(:forbidden) - end - end - - context 'authorize uploading of an lsif artifact' do - before do - stub_feature_flags(code_navigation: job.project) - end - - it 'adds ProcessLsif header' do - authorize_artifacts_with_token_in_headers(artifact_type: :lsif) - - expect(response).to have_gitlab_http_status(:ok) - expect(json_response['ProcessLsif']).to be_truthy - end - - it 'adds ProcessLsifReferences header' do - authorize_artifacts_with_token_in_headers(artifact_type: :lsif) - - expect(response).to have_gitlab_http_status(:ok) - expect(json_response['ProcessLsifReferences']).to be_truthy - end - - context 'code_navigation feature flag is disabled' do - it 'responds with a forbidden error' do - stub_feature_flags(code_navigation: false) - authorize_artifacts_with_token_in_headers(artifact_type: :lsif) - - aggregate_failures do - expect(response).to have_gitlab_http_status(:forbidden) - expect(json_response['ProcessLsif']).to be_falsy - expect(json_response['ProcessLsifReferences']).to be_falsy - end - end - end - - context 'code_navigation_references feature flag is disabled' do - it 'sets ProcessLsifReferences header to false' do - stub_feature_flags(code_navigation_references: false) - authorize_artifacts_with_token_in_headers(artifact_type: :lsif) - - aggregate_failures do - expect(response).to have_gitlab_http_status(:ok) - expect(json_response['ProcessLsif']).to be_truthy - expect(json_response['ProcessLsifReferences']).to be_falsy - end - end - end - end - - def authorize_artifacts(params = {}, request_headers = headers) - post api("/jobs/#{job.id}/artifacts/authorize"), params: params, headers: request_headers - end - - def authorize_artifacts_with_token_in_params(params = {}, request_headers = headers) - params = params.merge(token: job.token) - authorize_artifacts(params, request_headers) - end - - def authorize_artifacts_with_token_in_headers(params = {}, request_headers = headers_with_token) - authorize_artifacts(params, request_headers) - end - end - - describe 'POST /api/v4/jobs/:id/artifacts' do - it_behaves_like 'application context metadata', '/api/:version/jobs/:id/artifacts' do - let(:send_request) do - upload_artifacts(file_upload, headers_with_token) - end - end - - it 'updates runner info' do - expect { upload_artifacts(file_upload, headers_with_token) }.to change { runner.reload.contacted_at } - end - - context 'when the artifact is too large' do - it_behaves_like 'rejecting artifacts that are too large' do - # This filesize validation also happens in non remote stored files, - # it's just that it's hard to stub the filesize in other cases to be - # more than a megabyte. - let!(:fog_connection) do - stub_artifacts_object_storage(direct_upload: true) - end - let(:object) do - fog_connection.directories.new(key: 'artifacts').files.create( - key: 'tmp/uploads/12312300', - body: 'content' - ) - end - let(:file_upload) { fog_to_uploaded_file(object) } - let(:send_request) do - upload_artifacts(file_upload, headers_with_token, 'file.remote_id' => '12312300') - end - let(:success_code) { :created } - - before do - allow(object).to receive(:content_length).and_return(filesize) - end - end - end - - context 'when artifacts are being stored inside of tmp path' do - before do - # by configuring this path we allow to pass temp file from any path - allow(JobArtifactUploader).to receive(:workhorse_upload_path).and_return('/') - end - - context 'when job has been erased' do - let(:job) { create(:ci_build, erased_at: Time.now) } - - before do - upload_artifacts(file_upload, headers_with_token) - end - - it 'responds with forbidden' do - upload_artifacts(file_upload, headers_with_token) - - expect(response).to have_gitlab_http_status(:forbidden) - end - end - - context 'when job is running' do - shared_examples 'successful artifacts upload' do - it 'updates successfully' do - expect(response).to have_gitlab_http_status(:created) - end - end - - context 'when uses accelerated file post' do - context 'for file stored locally' do - before do - upload_artifacts(file_upload, headers_with_token) - end - - it_behaves_like 'successful artifacts upload' - end - - context 'for file stored remotely' do - let!(:fog_connection) do - stub_artifacts_object_storage(direct_upload: true) - end - let(:object) do - fog_connection.directories.new(key: 'artifacts').files.create( - key: 'tmp/uploads/12312300', - body: 'content' - ) - end - let(:file_upload) { fog_to_uploaded_file(object) } - - before do - upload_artifacts(file_upload, headers_with_token, 'file.remote_id' => remote_id) - end - - context 'when valid remote_id is used' do - let(:remote_id) { '12312300' } - - it_behaves_like 'successful artifacts upload' - end - - context 'when invalid remote_id is used' do - let(:remote_id) { 'invalid id' } - - it 'responds with bad request' do - expect(response).to have_gitlab_http_status(:internal_server_error) - expect(json_response['message']).to eq("Missing file") - end - end - end - end - - context 'when using runners token' do - it 'responds with forbidden' do - upload_artifacts(file_upload, headers.merge(API::Helpers::Runner::JOB_TOKEN_HEADER => job.project.runners_token)) - - expect(response).to have_gitlab_http_status(:forbidden) - end - end - end - - context 'when artifacts post request does not contain file' do - it 'fails to post artifacts without file' do - post api("/jobs/#{job.id}/artifacts"), params: {}, headers: headers_with_token - - expect(response).to have_gitlab_http_status(:bad_request) - end - end - - context 'GitLab Workhorse is not configured' do - it 'fails to post artifacts without GitLab-Workhorse' do - post api("/jobs/#{job.id}/artifacts"), params: { token: job.token }, headers: {} - - expect(response).to have_gitlab_http_status(:bad_request) - end - end - - context 'Is missing GitLab Workhorse token headers' do - let(:jwt) { JWT.encode({ 'iss' => 'invalid-header' }, Gitlab::Workhorse.secret, 'HS256') } - - it 'fails to post artifacts without GitLab-Workhorse' do - expect(Gitlab::ErrorTracking).to receive(:track_exception).once - - upload_artifacts(file_upload, headers_with_token) - - expect(response).to have_gitlab_http_status(:forbidden) - end - end - - context 'when setting an expire date' do - let(:default_artifacts_expire_in) {} - let(:post_data) do - { file: file_upload, - expire_in: expire_in } - end - - before do - stub_application_setting(default_artifacts_expire_in: default_artifacts_expire_in) - - upload_artifacts(file_upload, headers_with_token, post_data) - end - - context 'when an expire_in is given' do - let(:expire_in) { '7 days' } - - it 'updates when specified' do - expect(response).to have_gitlab_http_status(:created) - expect(job.reload.artifacts_expire_at).to be_within(5.minutes).of(7.days.from_now) - end - end - - context 'when no expire_in is given' do - let(:expire_in) { nil } - - it 'ignores if not specified' do - expect(response).to have_gitlab_http_status(:created) - expect(job.reload.artifacts_expire_at).to be_nil - end - - context 'with application default' do - context 'when default is 5 days' do - let(:default_artifacts_expire_in) { '5 days' } - - it 'sets to application default' do - expect(response).to have_gitlab_http_status(:created) - expect(job.reload.artifacts_expire_at).to be_within(5.minutes).of(5.days.from_now) - end - end - - context 'when default is 0' do - let(:default_artifacts_expire_in) { '0' } - - it 'does not set expire_in' do - expect(response).to have_gitlab_http_status(:created) - expect(job.reload.artifacts_expire_at).to be_nil - end - end - end - end - end - - context 'posts artifacts file and metadata file' do - let!(:artifacts) { file_upload } - let!(:artifacts_sha256) { Digest::SHA256.file(artifacts.path).hexdigest } - let!(:metadata) { file_upload2 } - let!(:metadata_sha256) { Digest::SHA256.file(metadata.path).hexdigest } - - let(:stored_artifacts_file) { job.reload.artifacts_file } - let(:stored_metadata_file) { job.reload.artifacts_metadata } - let(:stored_artifacts_size) { job.reload.artifacts_size } - let(:stored_artifacts_sha256) { job.reload.job_artifacts_archive.file_sha256 } - let(:stored_metadata_sha256) { job.reload.job_artifacts_metadata.file_sha256 } - let(:file_keys) { post_data.keys } - let(:send_rewritten_field) { true } - - before do - workhorse_finalize_with_multiple_files( - api("/jobs/#{job.id}/artifacts"), - method: :post, - file_keys: file_keys, - params: post_data, - headers: headers_with_token, - send_rewritten_field: send_rewritten_field - ) - end - - context 'when posts data accelerated by workhorse is correct' do - let(:post_data) { { file: artifacts, metadata: metadata } } - - it 'stores artifacts and artifacts metadata' do - expect(response).to have_gitlab_http_status(:created) - expect(stored_artifacts_file.filename).to eq(artifacts.original_filename) - expect(stored_metadata_file.filename).to eq(metadata.original_filename) - expect(stored_artifacts_size).to eq(artifacts.size) - expect(stored_artifacts_sha256).to eq(artifacts_sha256) - expect(stored_metadata_sha256).to eq(metadata_sha256) - end - end - - context 'with a malicious file.path param' do - let(:post_data) { {} } - let(:tmp_file) { Tempfile.new('crafted.file.path') } - let(:url) { "/jobs/#{job.id}/artifacts?file.path=#{tmp_file.path}" } - - it 'rejects the request' do - expect(response).to have_gitlab_http_status(:bad_request) - expect(stored_artifacts_size).to be_nil - end - end - - context 'when workhorse header is missing' do - let(:post_data) { { file: artifacts, metadata: metadata } } - let(:send_rewritten_field) { false } - - it 'rejects the request' do - expect(response).to have_gitlab_http_status(:bad_request) - expect(stored_artifacts_size).to be_nil - end - end - - context 'when there is no artifacts file in post data' do - let(:post_data) do - { metadata: metadata } - end - - it 'is expected to respond with bad request' do - expect(response).to have_gitlab_http_status(:bad_request) - end - - it 'does not store metadata' do - expect(stored_metadata_file).to be_nil - end - end - end - - context 'when artifact_type is archive' do - context 'when artifact_format is zip' do - let(:params) { { artifact_type: :archive, artifact_format: :zip } } - - it 'stores junit test report' do - upload_artifacts(file_upload, headers_with_token, params) - - expect(response).to have_gitlab_http_status(:created) - expect(job.reload.job_artifacts_archive).not_to be_nil - end - end - - context 'when artifact_format is gzip' do - let(:params) { { artifact_type: :archive, artifact_format: :gzip } } - - it 'returns an error' do - upload_artifacts(file_upload, headers_with_token, params) - - expect(response).to have_gitlab_http_status(:bad_request) - expect(job.reload.job_artifacts_archive).to be_nil - end - end - end - - context 'when artifact_type is junit' do - context 'when artifact_format is gzip' do - let(:file_upload) { fixture_file_upload('spec/fixtures/junit/junit.xml.gz') } - let(:params) { { artifact_type: :junit, artifact_format: :gzip } } - - it 'stores junit test report' do - upload_artifacts(file_upload, headers_with_token, params) - - expect(response).to have_gitlab_http_status(:created) - expect(job.reload.job_artifacts_junit).not_to be_nil - end - end - - context 'when artifact_format is raw' do - let(:file_upload) { fixture_file_upload('spec/fixtures/junit/junit.xml.gz') } - let(:params) { { artifact_type: :junit, artifact_format: :raw } } - - it 'returns an error' do - upload_artifacts(file_upload, headers_with_token, params) - - expect(response).to have_gitlab_http_status(:bad_request) - expect(job.reload.job_artifacts_junit).to be_nil - end - end - end - - context 'when artifact_type is metrics_referee' do - context 'when artifact_format is gzip' do - let(:file_upload) { fixture_file_upload('spec/fixtures/referees/metrics_referee.json.gz') } - let(:params) { { artifact_type: :metrics_referee, artifact_format: :gzip } } - - it 'stores metrics_referee data' do - upload_artifacts(file_upload, headers_with_token, params) - - expect(response).to have_gitlab_http_status(:created) - expect(job.reload.job_artifacts_metrics_referee).not_to be_nil - end - end - - context 'when artifact_format is raw' do - let(:file_upload) { fixture_file_upload('spec/fixtures/referees/metrics_referee.json.gz') } - let(:params) { { artifact_type: :metrics_referee, artifact_format: :raw } } - - it 'returns an error' do - upload_artifacts(file_upload, headers_with_token, params) - - expect(response).to have_gitlab_http_status(:bad_request) - expect(job.reload.job_artifacts_metrics_referee).to be_nil - end - end - end - - context 'when artifact_type is network_referee' do - context 'when artifact_format is gzip' do - let(:file_upload) { fixture_file_upload('spec/fixtures/referees/network_referee.json.gz') } - let(:params) { { artifact_type: :network_referee, artifact_format: :gzip } } - - it 'stores network_referee data' do - upload_artifacts(file_upload, headers_with_token, params) - - expect(response).to have_gitlab_http_status(:created) - expect(job.reload.job_artifacts_network_referee).not_to be_nil - end - end - - context 'when artifact_format is raw' do - let(:file_upload) { fixture_file_upload('spec/fixtures/referees/network_referee.json.gz') } - let(:params) { { artifact_type: :network_referee, artifact_format: :raw } } - - it 'returns an error' do - upload_artifacts(file_upload, headers_with_token, params) - - expect(response).to have_gitlab_http_status(:bad_request) - expect(job.reload.job_artifacts_network_referee).to be_nil - end - end - end - - context 'when artifact_type is dotenv' do - context 'when artifact_format is gzip' do - let(:file_upload) { fixture_file_upload('spec/fixtures/build.env.gz') } - let(:params) { { artifact_type: :dotenv, artifact_format: :gzip } } - - it 'stores dotenv file' do - upload_artifacts(file_upload, headers_with_token, params) - - expect(response).to have_gitlab_http_status(:created) - expect(job.reload.job_artifacts_dotenv).not_to be_nil - end - - it 'parses dotenv file' do - expect do - upload_artifacts(file_upload, headers_with_token, params) - end.to change { job.job_variables.count }.from(0).to(2) - end - - context 'when parse error happens' do - let(:file_upload) { fixture_file_upload('spec/fixtures/ci_build_artifacts_metadata.gz') } - - it 'returns an error' do - upload_artifacts(file_upload, headers_with_token, params) - - expect(response).to have_gitlab_http_status(:bad_request) - expect(json_response['message']).to eq('Invalid Format') - end - end - end - - context 'when artifact_format is raw' do - let(:file_upload) { fixture_file_upload('spec/fixtures/build.env.gz') } - let(:params) { { artifact_type: :dotenv, artifact_format: :raw } } - - it 'returns an error' do - upload_artifacts(file_upload, headers_with_token, params) - - expect(response).to have_gitlab_http_status(:bad_request) - expect(job.reload.job_artifacts_dotenv).to be_nil - end - end - end - end - - context 'when artifacts already exist for the job' do - let(:params) do - { - artifact_type: :archive, - artifact_format: :zip, - 'file.sha256' => uploaded_sha256 - } - end - - let(:existing_sha256) { '0' * 64 } - - let!(:existing_artifact) do - create(:ci_job_artifact, :archive, file_sha256: existing_sha256, job: job) - end - - context 'when sha256 is the same of the existing artifact' do - let(:uploaded_sha256) { existing_sha256 } - - it 'ignores the new artifact' do - upload_artifacts(file_upload, headers_with_token, params) - - expect(response).to have_gitlab_http_status(:created) - expect(job.reload.job_artifacts_archive).to eq(existing_artifact) - end - end - - context 'when sha256 is different than the existing artifact' do - let(:uploaded_sha256) { '1' * 64 } - - it 'logs and returns an error' do - expect(Gitlab::ErrorTracking).to receive(:track_exception) - - upload_artifacts(file_upload, headers_with_token, params) - - expect(response).to have_gitlab_http_status(:bad_request) - expect(job.reload.job_artifacts_archive).to eq(existing_artifact) - end - end - end - - context 'when object storage throws errors' do - let(:params) { { artifact_type: :archive, artifact_format: :zip } } - - it 'does not store artifacts' do - allow_next_instance_of(JobArtifactUploader) do |uploader| - allow(uploader).to receive(:store!).and_raise(Errno::EIO) - end - - upload_artifacts(file_upload, headers_with_token, params) - - expect(response).to have_gitlab_http_status(:service_unavailable) - expect(job.reload.job_artifacts_archive).to be_nil - end - end - - context 'when artifacts are being stored outside of tmp path' do - let(:new_tmpdir) { Dir.mktmpdir } - - before do - # init before overwriting tmp dir - file_upload - - # by configuring this path we allow to pass file from @tmpdir only - # but all temporary files are stored in system tmp directory - allow(Dir).to receive(:tmpdir).and_return(new_tmpdir) - end - - after do - FileUtils.remove_entry(new_tmpdir) - end - - it 'fails to post artifacts for outside of tmp path' do - upload_artifacts(file_upload, headers_with_token) - - expect(response).to have_gitlab_http_status(:bad_request) - end - end - - def upload_artifacts(file, headers = {}, params = {}) - workhorse_finalize( - api("/jobs/#{job.id}/artifacts"), - method: :post, - file_key: :file, - params: params.merge(file: file), - headers: headers, - send_rewritten_field: true - ) - end - end - - describe 'GET /api/v4/jobs/:id/artifacts' do - let(:token) { job.token } - - it_behaves_like 'application context metadata', '/api/:version/jobs/:id/artifacts' do - let(:send_request) { download_artifact } - end - - it 'updates runner info' do - expect { download_artifact }.to change { runner.reload.contacted_at } - end - - context 'when job has artifacts' do - let(:job) { create(:ci_build) } - let(:store) { JobArtifactUploader::Store::LOCAL } - - before do - create(:ci_job_artifact, :archive, file_store: store, job: job) - end - - context 'when using job token' do - context 'when artifacts are stored locally' do - let(:download_headers) do - { 'Content-Transfer-Encoding' => 'binary', - 'Content-Disposition' => %q(attachment; filename="ci_build_artifacts.zip"; filename*=UTF-8''ci_build_artifacts.zip) } - end - - before do - download_artifact - end - - it 'download artifacts' do - expect(response).to have_gitlab_http_status(:ok) - expect(response.headers.to_h).to include download_headers - end - end - - context 'when artifacts are stored remotely' do - let(:store) { JobArtifactUploader::Store::REMOTE } - let!(:job) { create(:ci_build) } - - context 'when proxy download is being used' do - before do - download_artifact(direct_download: false) - end - - it 'uses workhorse send-url' do - expect(response).to have_gitlab_http_status(:ok) - expect(response.headers.to_h).to include( - 'Gitlab-Workhorse-Send-Data' => /send-url:/) - end - end - - context 'when direct download is being used' do - before do - download_artifact(direct_download: true) - end - - it 'receive redirect for downloading artifacts' do - expect(response).to have_gitlab_http_status(:found) - expect(response.headers).to include('Location') - end - end - end - end - - context 'when using runnners token' do - let(:token) { job.project.runners_token } - - before do - download_artifact - end - - it 'responds with forbidden' do - expect(response).to have_gitlab_http_status(:forbidden) - end - end - end - - context 'when job does not have artifacts' do - it 'responds with not found' do - download_artifact - - expect(response).to have_gitlab_http_status(:not_found) - end - end - - def download_artifact(params = {}, request_headers = headers) - params = params.merge(token: token) - job.reload - - get api("/jobs/#{job.id}/artifacts"), params: params, headers: request_headers - end - end - end - end -end diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb index 724e3177173..21ff0a94db9 100644 --- a/spec/requests/api/commits_spec.rb +++ b/spec/requests/api/commits_spec.rb @@ -329,6 +329,7 @@ RSpec.describe API::Commits do ] } end + let(:valid_c_params) do { branch: 'master', @@ -342,6 +343,7 @@ RSpec.describe API::Commits do ] } end + let(:valid_utf8_c_params) do { branch: 'master', @@ -621,6 +623,7 @@ RSpec.describe API::Commits do ] } end + let(:valid_d_params) do { branch: 'markdown', @@ -664,6 +667,7 @@ RSpec.describe API::Commits do ] } end + let(:valid_m_params) do { branch: 'feature', @@ -708,6 +712,7 @@ RSpec.describe API::Commits do ] } end + let(:valid_u_params) do { branch: 'master', @@ -819,6 +824,7 @@ RSpec.describe API::Commits do ] } end + let(:valid_mo_params) do { branch: 'master', @@ -1462,6 +1468,16 @@ RSpec.describe API::Commits do expect(json_response['author_name']).to eq(commit.author_name) expect(json_response['committer_name']).to eq(user.name) end + + it 'supports dry-run without applying changes' do + head = project.commit(branch) + + post api(route, current_user), params: { branch: branch, dry_run: true } + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to eq("dry_run" => "success") + expect(project.commit(branch)).to eq(head) + end end context 'when repository is disabled' do @@ -1533,6 +1549,14 @@ RSpec.describe API::Commits do expect(json_response['error_code']).to eq 'empty' end + + it 'includes an additional dry_run error field when enabled' do + post api(route, current_user), params: { branch: 'markdown', dry_run: true } + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['error_code']).to eq 'empty' + expect(json_response['dry_run']).to eq 'error' + end end context 'when ref contains a dot' do @@ -1623,6 +1647,16 @@ RSpec.describe API::Commits do expect(json_response['committer_name']).to eq(user.name) expect(json_response['parent_ids']).to contain_exactly(commit_id) end + + it 'supports dry-run without applying changes' do + head = project.commit(branch) + + post api(route, current_user), params: { branch: branch, dry_run: true } + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to eq("dry_run" => "success") + expect(project.commit(branch)).to eq(head) + end end context 'when repository is disabled' do @@ -1704,6 +1738,18 @@ RSpec.describe API::Commits do expect(response).to have_gitlab_http_status(:bad_request) expect(json_response['error_code']).to eq 'empty' end + + it 'includes an additional dry_run error field when enabled' do + # First one actually reverts + post api(route, current_user), params: { branch: 'markdown' } + + # Second one is redundant and should be empty + post api(route, current_user), params: { branch: 'markdown', dry_run: true } + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['error_code']).to eq 'empty' + expect(json_response['dry_run']).to eq 'error' + end end end diff --git a/spec/requests/api/composer_packages_spec.rb b/spec/requests/api/composer_packages_spec.rb index d756a7700f6..f5b8ebb545b 100644 --- a/spec/requests/api/composer_packages_spec.rb +++ b/spec/requests/api/composer_packages_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' RSpec.describe API::ComposerPackages do - include PackagesManagerApiSpecHelpers + include HttpBasicAuthHelpers let_it_be(:user) { create(:user) } let_it_be(:group, reload: true) { create(:group, :public) } @@ -11,47 +11,88 @@ RSpec.describe API::ComposerPackages do let_it_be(:project, reload: true) { create(:project, :custom_repo, files: { 'composer.json' => { name: package_name }.to_json }, group: group) } let(:headers) { {} } + using RSpec::Parameterized::TableSyntax + describe 'GET /api/v4/group/:id/-/packages/composer/packages' do let(:url) { "/group/#{group.id}/-/packages/composer/packages.json" } subject { get api(url), headers: headers } - context 'without the need for a license' do - context 'with valid project' do - let!(:package) { create(:composer_package, :with_metadatum, project: project) } + context 'with valid project' do + let!(:package) { create(:composer_package, :with_metadatum, project: project) } - using RSpec::Parameterized::TableSyntax + context 'with a public group' do + before do + group.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC) + end - where(:project_visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do - 'PUBLIC' | :developer | true | true | 'Composer package index' | :success - 'PUBLIC' | :guest | true | true | 'Composer package index' | :success - 'PUBLIC' | :developer | true | false | 'Composer package index' | :success - 'PUBLIC' | :guest | true | false | 'Composer package index' | :success - 'PUBLIC' | :developer | false | true | 'Composer package index' | :success - 'PUBLIC' | :guest | false | true | 'Composer package index' | :success - 'PUBLIC' | :developer | false | false | 'Composer package index' | :success - 'PUBLIC' | :guest | false | false | 'Composer package index' | :success - 'PUBLIC' | :anonymous | false | true | 'Composer package index' | :success - 'PRIVATE' | :developer | true | true | 'Composer package index' | :success - 'PRIVATE' | :guest | true | true | 'Composer package index' | :success - 'PRIVATE' | :developer | true | false | 'process Composer api request' | :not_found - 'PRIVATE' | :guest | true | false | 'process Composer api request' | :not_found - 'PRIVATE' | :developer | false | true | 'process Composer api request' | :not_found - 'PRIVATE' | :guest | false | true | 'process Composer api request' | :not_found - 'PRIVATE' | :developer | false | false | 'process Composer api request' | :not_found - 'PRIVATE' | :guest | false | false | 'process Composer api request' | :not_found - 'PRIVATE' | :anonymous | false | true | 'process Composer api request' | :not_found + where(:project_visibility_level, :user_role, :member, :user_token, :include_package) do + 'PUBLIC' | :developer | true | true | :include_package + 'PUBLIC' | :developer | true | false | :include_package + 'PUBLIC' | :developer | false | false | :include_package + 'PUBLIC' | :developer | false | true | :include_package + 'PUBLIC' | :guest | true | true | :include_package + 'PUBLIC' | :guest | true | false | :include_package + 'PUBLIC' | :guest | false | true | :include_package + 'PUBLIC' | :guest | false | false | :include_package + 'PUBLIC' | :anonymous | false | true | :include_package + 'PRIVATE' | :developer | true | true | :include_package + 'PRIVATE' | :developer | true | false | :does_not_include_package + 'PRIVATE' | :developer | false | true | :does_not_include_package + 'PRIVATE' | :developer | false | false | :does_not_include_package + 'PRIVATE' | :guest | true | true | :does_not_include_package + 'PRIVATE' | :guest | true | false | :does_not_include_package + 'PRIVATE' | :guest | false | true | :does_not_include_package + 'PRIVATE' | :guest | false | false | :does_not_include_package + 'PRIVATE' | :anonymous | false | true | :does_not_include_package end with_them do - include_context 'Composer api group access', params[:project_visibility_level], params[:user_role], params[:user_token] do - it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member] + include_context 'Composer api project access', params[:project_visibility_level], params[:user_role], params[:user_token] do + it_behaves_like 'Composer package index', params[:user_role], :success, params[:member], params[:include_package] end end end - it_behaves_like 'rejects Composer access with unknown group id' + context 'with a private group' do + before do + group.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE) + end + + context 'with access to the api' do + where(:project_visibility_level, :user_role, :member, :user_token, :include_package) do + 'PRIVATE' | :developer | true | true | :include_package + 'PRIVATE' | :guest | true | true | :does_not_include_package + end + + with_them do + include_context 'Composer api project access', params[:project_visibility_level], params[:user_role], params[:user_token] do + it_behaves_like 'Composer package index', params[:user_role], :success, params[:member], params[:include_package] + end + end + end + + context 'without access to the api' do + where(:project_visibility_level, :user_role, :member, :user_token) do + 'PRIVATE' | :developer | true | false + 'PRIVATE' | :developer | false | true + 'PRIVATE' | :developer | false | false + 'PRIVATE' | :guest | true | false + 'PRIVATE' | :guest | false | true + 'PRIVATE' | :guest | false | false + 'PRIVATE' | :anonymous | false | true + end + + with_them do + include_context 'Composer api project access', params[:project_visibility_level], params[:user_role], params[:user_token] do + it_behaves_like 'process Composer api request', params[:user_role], :not_found, params[:member] + end + end + end + end end + + it_behaves_like 'rejects Composer access with unknown group id' end describe 'GET /api/v4/group/:id/-/packages/composer/p/:sha.json' do @@ -61,40 +102,36 @@ RSpec.describe API::ComposerPackages do subject { get api(url), headers: headers } - context 'without the need for a license' do - context 'with valid project' do - using RSpec::Parameterized::TableSyntax - - where(:project_visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do - 'PUBLIC' | :developer | true | true | 'Composer provider index' | :success - 'PUBLIC' | :guest | true | true | 'Composer provider index' | :success - 'PUBLIC' | :developer | true | false | 'Composer provider index' | :success - 'PUBLIC' | :guest | true | false | 'Composer provider index' | :success - 'PUBLIC' | :developer | false | true | 'Composer provider index' | :success - 'PUBLIC' | :guest | false | true | 'Composer provider index' | :success - 'PUBLIC' | :developer | false | false | 'Composer provider index' | :success - 'PUBLIC' | :guest | false | false | 'Composer provider index' | :success - 'PUBLIC' | :anonymous | false | true | 'Composer provider index' | :success - 'PRIVATE' | :developer | true | true | 'Composer provider index' | :success - 'PRIVATE' | :guest | true | true | 'Composer empty provider index' | :success - 'PRIVATE' | :developer | true | false | 'process Composer api request' | :not_found - 'PRIVATE' | :guest | true | false | 'process Composer api request' | :not_found - 'PRIVATE' | :developer | false | true | 'process Composer api request' | :not_found - 'PRIVATE' | :guest | false | true | 'process Composer api request' | :not_found - 'PRIVATE' | :developer | false | false | 'process Composer api request' | :not_found - 'PRIVATE' | :guest | false | false | 'process Composer api request' | :not_found - 'PRIVATE' | :anonymous | false | true | 'process Composer api request' | :not_found - end + context 'with valid project' do + where(:project_visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do + 'PUBLIC' | :developer | true | true | 'Composer provider index' | :success + 'PUBLIC' | :developer | true | false | 'Composer provider index' | :success + 'PUBLIC' | :developer | false | true | 'Composer provider index' | :success + 'PUBLIC' | :developer | false | false | 'Composer provider index' | :success + 'PUBLIC' | :guest | true | true | 'Composer provider index' | :success + 'PUBLIC' | :guest | true | false | 'Composer provider index' | :success + 'PUBLIC' | :guest | false | true | 'Composer provider index' | :success + 'PUBLIC' | :guest | false | false | 'Composer provider index' | :success + 'PUBLIC' | :anonymous | false | true | 'Composer provider index' | :success + 'PRIVATE' | :developer | true | true | 'Composer provider index' | :success + 'PRIVATE' | :developer | true | false | 'process Composer api request' | :not_found + 'PRIVATE' | :developer | false | true | 'process Composer api request' | :not_found + 'PRIVATE' | :developer | false | false | 'process Composer api request' | :not_found + 'PRIVATE' | :guest | true | true | 'Composer empty provider index' | :success + 'PRIVATE' | :guest | true | false | 'process Composer api request' | :not_found + 'PRIVATE' | :guest | false | true | 'process Composer api request' | :not_found + 'PRIVATE' | :guest | false | false | 'process Composer api request' | :not_found + 'PRIVATE' | :anonymous | false | true | 'process Composer api request' | :not_found + end - with_them do - include_context 'Composer api group access', params[:project_visibility_level], params[:user_role], params[:user_token] do - it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member] - end + with_them do + include_context 'Composer api group access', params[:project_visibility_level], params[:user_role], params[:user_token] do + it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member] end end - - it_behaves_like 'rejects Composer access with unknown group id' end + + it_behaves_like 'rejects Composer access with unknown group id' end describe 'GET /api/v4/group/:id/-/packages/composer/*package_name.json' do @@ -103,48 +140,44 @@ RSpec.describe API::ComposerPackages do subject { get api(url), headers: headers } - context 'without the need for a license' do - context 'with no packages' do - include_context 'Composer user type', :developer, true do - it_behaves_like 'returning response status', :not_found - end + context 'with no packages' do + include_context 'Composer user type', :developer, true do + it_behaves_like 'returning response status', :not_found end + end - context 'with valid project' do - using RSpec::Parameterized::TableSyntax - - let!(:package) { create(:composer_package, :with_metadatum, name: package_name, project: project) } - - where(:project_visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do - 'PUBLIC' | :developer | true | true | 'Composer package api request' | :success - 'PUBLIC' | :guest | true | true | 'Composer package api request' | :success - 'PUBLIC' | :developer | true | false | 'Composer package api request' | :success - 'PUBLIC' | :guest | true | false | 'Composer package api request' | :success - 'PUBLIC' | :developer | false | true | 'Composer package api request' | :success - 'PUBLIC' | :guest | false | true | 'Composer package api request' | :success - 'PUBLIC' | :developer | false | false | 'Composer package api request' | :success - 'PUBLIC' | :guest | false | false | 'Composer package api request' | :success - 'PUBLIC' | :anonymous | false | true | 'Composer package api request' | :success - 'PRIVATE' | :developer | true | true | 'Composer package api request' | :success - 'PRIVATE' | :guest | true | true | 'process Composer api request' | :not_found - 'PRIVATE' | :developer | true | false | 'process Composer api request' | :not_found - 'PRIVATE' | :guest | true | false | 'process Composer api request' | :not_found - 'PRIVATE' | :developer | false | true | 'process Composer api request' | :not_found - 'PRIVATE' | :guest | false | true | 'process Composer api request' | :not_found - 'PRIVATE' | :developer | false | false | 'process Composer api request' | :not_found - 'PRIVATE' | :guest | false | false | 'process Composer api request' | :not_found - 'PRIVATE' | :anonymous | false | true | 'process Composer api request' | :not_found - end + context 'with valid project' do + let!(:package) { create(:composer_package, :with_metadatum, name: package_name, project: project) } + + where(:project_visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do + 'PUBLIC' | :developer | true | true | 'Composer package api request' | :success + 'PUBLIC' | :developer | true | false | 'Composer package api request' | :success + 'PUBLIC' | :developer | false | true | 'Composer package api request' | :success + 'PUBLIC' | :developer | false | false | 'Composer package api request' | :success + 'PUBLIC' | :guest | true | true | 'Composer package api request' | :success + 'PUBLIC' | :guest | true | false | 'Composer package api request' | :success + 'PUBLIC' | :guest | false | true | 'Composer package api request' | :success + 'PUBLIC' | :guest | false | false | 'Composer package api request' | :success + 'PUBLIC' | :anonymous | false | true | 'Composer package api request' | :success + 'PRIVATE' | :developer | true | true | 'Composer package api request' | :success + 'PRIVATE' | :developer | true | false | 'process Composer api request' | :not_found + 'PRIVATE' | :developer | false | true | 'process Composer api request' | :not_found + 'PRIVATE' | :developer | false | false | 'process Composer api request' | :not_found + 'PRIVATE' | :guest | true | true | 'process Composer api request' | :not_found + 'PRIVATE' | :guest | true | false | 'process Composer api request' | :not_found + 'PRIVATE' | :guest | false | true | 'process Composer api request' | :not_found + 'PRIVATE' | :guest | false | false | 'process Composer api request' | :not_found + 'PRIVATE' | :anonymous | false | true | 'process Composer api request' | :not_found + end - with_them do - include_context 'Composer api group access', params[:project_visibility_level], params[:user_role], params[:user_token] do - it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member] - end + with_them do + include_context 'Composer api group access', params[:project_visibility_level], params[:user_role], params[:user_token] do + it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member] end end - - it_behaves_like 'rejects Composer access with unknown group id' end + + it_behaves_like 'rejects Composer access with unknown group id' end describe 'POST /api/v4/projects/:id/packages/composer' do @@ -158,44 +191,40 @@ RSpec.describe API::ComposerPackages do subject { post api(url), headers: headers, params: params } shared_examples 'composer package publish' do - context 'without the need for a license' do - context 'with valid project' do - using RSpec::Parameterized::TableSyntax - - where(:project_visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do - 'PUBLIC' | :developer | true | true | 'Composer package creation' | :created - 'PUBLIC' | :guest | true | true | 'process Composer api request' | :forbidden - 'PUBLIC' | :developer | true | false | 'process Composer api request' | :unauthorized - 'PUBLIC' | :guest | true | false | 'process Composer api request' | :unauthorized - 'PUBLIC' | :developer | false | true | 'process Composer api request' | :forbidden - 'PUBLIC' | :guest | false | true | 'process Composer api request' | :forbidden - 'PUBLIC' | :developer | false | false | 'process Composer api request' | :unauthorized - 'PUBLIC' | :guest | false | false | 'process Composer api request' | :unauthorized - 'PUBLIC' | :anonymous | false | true | 'process Composer api request' | :unauthorized - 'PRIVATE' | :developer | true | true | 'Composer package creation' | :created - 'PRIVATE' | :guest | true | true | 'process Composer api request' | :forbidden - 'PRIVATE' | :developer | true | false | 'process Composer api request' | :unauthorized - 'PRIVATE' | :guest | true | false | 'process Composer api request' | :unauthorized - 'PRIVATE' | :developer | false | true | 'process Composer api request' | :not_found - 'PRIVATE' | :guest | false | true | 'process Composer api request' | :not_found - 'PRIVATE' | :developer | false | false | 'process Composer api request' | :unauthorized - 'PRIVATE' | :guest | false | false | 'process Composer api request' | :unauthorized - 'PRIVATE' | :anonymous | false | true | 'process Composer api request' | :unauthorized - end + context 'with valid project' do + where(:project_visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do + 'PUBLIC' | :developer | true | true | 'Composer package creation' | :created + 'PUBLIC' | :developer | true | false | 'process Composer api request' | :unauthorized + 'PUBLIC' | :developer | false | true | 'process Composer api request' | :forbidden + 'PUBLIC' | :developer | false | false | 'process Composer api request' | :unauthorized + 'PUBLIC' | :guest | true | true | 'process Composer api request' | :forbidden + 'PUBLIC' | :guest | true | false | 'process Composer api request' | :unauthorized + 'PUBLIC' | :guest | false | true | 'process Composer api request' | :forbidden + 'PUBLIC' | :guest | false | false | 'process Composer api request' | :unauthorized + 'PUBLIC' | :anonymous | false | true | 'process Composer api request' | :unauthorized + 'PRIVATE' | :developer | true | true | 'Composer package creation' | :created + 'PRIVATE' | :developer | true | false | 'process Composer api request' | :unauthorized + 'PRIVATE' | :developer | false | true | 'process Composer api request' | :not_found + 'PRIVATE' | :developer | false | false | 'process Composer api request' | :unauthorized + 'PRIVATE' | :guest | true | true | 'process Composer api request' | :forbidden + 'PRIVATE' | :guest | true | false | 'process Composer api request' | :unauthorized + 'PRIVATE' | :guest | false | true | 'process Composer api request' | :not_found + 'PRIVATE' | :guest | false | false | 'process Composer api request' | :unauthorized + 'PRIVATE' | :anonymous | false | true | 'process Composer api request' | :unauthorized + end - with_them do - include_context 'Composer api project access', params[:project_visibility_level], params[:user_role], params[:user_token] do - it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member] - end + with_them do + include_context 'Composer api project access', params[:project_visibility_level], params[:user_role], params[:user_token] do + it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member] end end - - it_behaves_like 'rejects Composer access with unknown project id' end + + it_behaves_like 'rejects Composer access with unknown project id' end context 'with no tag or branch params' do - let(:headers) { build_basic_auth_header(user.username, personal_access_token.token) } + let(:headers) { basic_auth_header(user.username, personal_access_token.token) } it_behaves_like 'process Composer api request', :developer, :bad_request end @@ -209,7 +238,7 @@ RSpec.describe API::ComposerPackages do context 'with a non existing tag' do let(:params) { { tag: 'non-existing-tag' } } - let(:headers) { build_basic_auth_header(user.username, personal_access_token.token) } + let(:headers) { basic_auth_header(user.username, personal_access_token.token) } it_behaves_like 'process Composer api request', :developer, :not_found end @@ -224,7 +253,7 @@ RSpec.describe API::ComposerPackages do context 'with a non existing branch' do let(:params) { { branch: 'non-existing-branch' } } - let(:headers) { build_basic_auth_header(user.username, personal_access_token.token) } + let(:headers) { basic_auth_header(user.username, personal_access_token.token) } it_behaves_like 'process Composer api request', :developer, :not_found end @@ -238,65 +267,61 @@ RSpec.describe API::ComposerPackages do subject { get api(url), headers: headers, params: params } - context 'without the need for a license' do - context 'with valid project' do - let!(:package) { create(:composer_package, :with_metadatum, name: package_name, project: project) } - - context 'when the sha does not match the package name' do - let(:sha) { '123' } + context 'with valid project' do + let!(:package) { create(:composer_package, :with_metadatum, name: package_name, project: project) } - it_behaves_like 'process Composer api request', :anonymous, :not_found - end + context 'when the sha does not match the package name' do + let(:sha) { '123' } - context 'when the package name does not match the sha' do - let(:branch) { project.repository.find_branch('master') } - let(:sha) { branch.target } - let(:url) { "/projects/#{project.id}/packages/composer/archives/unexisting-package-name.zip" } + it_behaves_like 'process Composer api request', :anonymous, :not_found + end - it_behaves_like 'process Composer api request', :anonymous, :not_found - end + context 'when the package name does not match the sha' do + let(:branch) { project.repository.find_branch('master') } + let(:sha) { branch.target } + let(:url) { "/projects/#{project.id}/packages/composer/archives/unexisting-package-name.zip" } - context 'with a match package name and sha' do - let(:branch) { project.repository.find_branch('master') } - let(:sha) { branch.target } - - using RSpec::Parameterized::TableSyntax - - where(:project_visibility_level, :user_role, :member, :user_token, :expected_status) do - 'PUBLIC' | :developer | true | true | :success - 'PUBLIC' | :guest | true | true | :success - 'PUBLIC' | :developer | true | false | :success - 'PUBLIC' | :guest | true | false | :success - 'PUBLIC' | :developer | false | true | :success - 'PUBLIC' | :guest | false | true | :success - 'PUBLIC' | :developer | false | false | :success - 'PUBLIC' | :guest | false | false | :success - 'PUBLIC' | :anonymous | false | true | :success - 'PRIVATE' | :developer | true | true | :success - 'PRIVATE' | :guest | true | true | :success - 'PRIVATE' | :developer | true | false | :success - 'PRIVATE' | :guest | true | false | :success - 'PRIVATE' | :developer | false | true | :success - 'PRIVATE' | :guest | false | true | :success - 'PRIVATE' | :developer | false | false | :success - 'PRIVATE' | :guest | false | false | :success - 'PRIVATE' | :anonymous | false | true | :success - end + it_behaves_like 'process Composer api request', :anonymous, :not_found + end - with_them do - let(:token) { user_token ? personal_access_token.token : 'wrong' } - let(:headers) { user_role == :anonymous ? {} : build_basic_auth_header(user.username, token) } + context 'with a match package name and sha' do + let(:branch) { project.repository.find_branch('master') } + let(:sha) { branch.target } + + where(:project_visibility_level, :user_role, :member, :user_token, :expected_status) do + 'PUBLIC' | :developer | true | true | :success + 'PUBLIC' | :developer | true | false | :success + 'PUBLIC' | :developer | false | true | :success + 'PUBLIC' | :developer | false | false | :success + 'PUBLIC' | :guest | true | true | :success + 'PUBLIC' | :guest | true | false | :success + 'PUBLIC' | :guest | false | true | :success + 'PUBLIC' | :guest | false | false | :success + 'PUBLIC' | :anonymous | false | true | :success + 'PRIVATE' | :developer | true | true | :success + 'PRIVATE' | :developer | true | false | :success + 'PRIVATE' | :developer | false | true | :success + 'PRIVATE' | :developer | false | false | :success + 'PRIVATE' | :guest | true | true | :success + 'PRIVATE' | :guest | true | false | :success + 'PRIVATE' | :guest | false | true | :success + 'PRIVATE' | :guest | false | false | :success + 'PRIVATE' | :anonymous | false | true | :success + end - before do - project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility_level, false)) - end + with_them do + let(:token) { user_token ? personal_access_token.token : 'wrong' } + let(:headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) } - it_behaves_like 'process Composer api request', params[:user_role], params[:expected_status], params[:member] + before do + project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility_level, false)) end + + it_behaves_like 'process Composer api request', params[:user_role], params[:expected_status], params[:member] end end - - it_behaves_like 'rejects Composer access with unknown project id' end + + it_behaves_like 'rejects Composer access with unknown project id' end end diff --git a/spec/requests/api/conan_packages_spec.rb b/spec/requests/api/conan_packages_spec.rb index 1d88eaef79c..95798b060f1 100644 --- a/spec/requests/api/conan_packages_spec.rb +++ b/spec/requests/api/conan_packages_spec.rb @@ -3,6 +3,7 @@ require 'spec_helper' RSpec.describe API::ConanPackages do include WorkhorseHelpers + include HttpBasicAuthHelpers include PackagesManagerApiSpecHelpers let(:package) { create(:conan_package) } @@ -330,6 +331,18 @@ RSpec.describe API::ConanPackages do .and_return(presenter) end + shared_examples 'rejects invalid upload_url params' do + context 'with unaccepted json format' do + let(:params) { %w[foo bar] } + + it 'returns 400' do + subject + + expect(response).to have_gitlab_http_status(:bad_request) + end + end + end + describe 'GET /api/v4/packages/conan/v1/conans/:package_name/package_version/:package_username/:package_channel' do let(:recipe_path) { package.conan_recipe_path } @@ -417,13 +430,14 @@ RSpec.describe API::ConanPackages do let(:recipe_path) { package.conan_recipe_path } let(:params) do - { "conanfile.py": 24, - "conanmanifext.txt": 123 } + { 'conanfile.py': 24, + 'conanmanifest.txt': 123 } end - subject { post api("/packages/conan/v1/conans/#{recipe_path}/upload_urls"), params: params, headers: headers } + subject { post api("/packages/conan/v1/conans/#{recipe_path}/upload_urls"), params: params.to_json, headers: headers } it_behaves_like 'rejects invalid recipe' + it_behaves_like 'rejects invalid upload_url params' it 'returns a set of upload urls for the files requested' do subject @@ -435,20 +449,58 @@ RSpec.describe API::ConanPackages do expect(response.body).to eq(expected_response.to_json) end + + context 'with conan_sources and conan_export files' do + let(:params) do + { 'conan_sources.tgz': 345, + 'conan_export.tgz': 234, + 'conanmanifest.txt': 123 } + end + + it 'returns upload urls for the additional files' do + subject + + expected_response = { + 'conan_sources.tgz': "#{Settings.gitlab.base_url}/api/v4/packages/conan/v1/files/#{package.conan_recipe_path}/0/export/conan_sources.tgz", + 'conan_export.tgz': "#{Settings.gitlab.base_url}/api/v4/packages/conan/v1/files/#{package.conan_recipe_path}/0/export/conan_export.tgz", + 'conanmanifest.txt': "#{Settings.gitlab.base_url}/api/v4/packages/conan/v1/files/#{package.conan_recipe_path}/0/export/conanmanifest.txt" + } + + expect(response.body).to eq(expected_response.to_json) + end + end + + context 'with an invalid file' do + let(:params) do + { 'invalid_file.txt': 10, + 'conanmanifest.txt': 123 } + end + + it 'does not return the invalid file as an upload_url' do + subject + + expected_response = { + 'conanmanifest.txt': "#{Settings.gitlab.base_url}/api/v4/packages/conan/v1/files/#{package.conan_recipe_path}/0/export/conanmanifest.txt" + } + + expect(response.body).to eq(expected_response.to_json) + end + end end describe 'POST /api/v4/packages/conan/v1/conans/:package_name/package_version/:package_username/:package_channel/packages/:conan_package_reference/upload_urls' do let(:recipe_path) { package.conan_recipe_path } let(:params) do - { "conaninfo.txt": 24, - "conanmanifext.txt": 123, - "conan_package.tgz": 523 } + { 'conaninfo.txt': 24, + 'conanmanifest.txt': 123, + 'conan_package.tgz': 523 } end - subject { post api("/packages/conan/v1/conans/#{recipe_path}/packages/123456789/upload_urls"), params: params, headers: headers } + subject { post api("/packages/conan/v1/conans/#{recipe_path}/packages/123456789/upload_urls"), params: params.to_json, headers: headers } it_behaves_like 'rejects invalid recipe' + it_behaves_like 'rejects invalid upload_url params' it 'returns a set of upload urls for the files requested' do expected_response = { @@ -461,6 +513,23 @@ RSpec.describe API::ConanPackages do expect(response.body).to eq(expected_response.to_json) end + + context 'with invalid files' do + let(:params) do + { 'conaninfo.txt': 24, + 'invalid_file.txt': 10 } + end + + it 'returns upload urls only for the valid requested files' do + expected_response = { + 'conaninfo.txt': "#{Settings.gitlab.base_url}/api/v4/packages/conan/v1/files/#{package.conan_recipe_path}/0/package/123456789/0/conaninfo.txt" + } + + subject + + expect(response.body).to eq(expected_response.to_json) + end + end end describe 'DELETE /api/v4/packages/conan/v1/conans/:package_name/package_version/:package_username/:package_channel' do diff --git a/spec/requests/api/deploy_keys_spec.rb b/spec/requests/api/deploy_keys_spec.rb index 81cef653770..591d994fec9 100644 --- a/spec/requests/api/deploy_keys_spec.rb +++ b/spec/requests/api/deploy_keys_spec.rb @@ -165,6 +165,7 @@ RSpec.describe API::DeployKeys do let(:deploy_keys_project) do create(:deploy_keys_project, project: project, deploy_key: deploy_key) end + let(:extra_params) { { title: 'new title', can_push: true } } it 'updates the title of the deploy key' do diff --git a/spec/requests/api/files_spec.rb b/spec/requests/api/files_spec.rb index b50f63ed67c..d7571ad4bff 100644 --- a/spec/requests/api/files_spec.rb +++ b/spec/requests/api/files_spec.rb @@ -10,12 +10,14 @@ RSpec.describe API::Files do let(:guest) { create(:user) { |u| project.add_guest(u) } } let(:file_path) { "files%2Fruby%2Fpopen%2Erb" } let(:rouge_file_path) { "%2e%2e%2f" } + let(:absolute_path) { "%2Fetc%2Fpasswd.rb" } let(:invalid_file_message) { 'file_path should be a valid file path' } let(:params) do { ref: 'master' } end + let(:author_email) { 'user@example.org' } let(:author_name) { 'John Doe' } @@ -57,6 +59,18 @@ RSpec.describe API::Files do end end + shared_examples 'when path is absolute' do + it 'returns 400 when file path is absolute' do + subject + + expect(response).to have_gitlab_http_status(:bad_request) + + if response.body.present? + expect(json_response['error']).to eq(invalid_file_message) + end + end + end + describe "HEAD /projects/:id/repository/files/:file_path" do shared_examples_for 'repository files' do it 'returns 400 when file path is invalid' do @@ -65,6 +79,10 @@ RSpec.describe API::Files do expect(response).to have_gitlab_http_status(:bad_request) end + it_behaves_like 'when path is absolute' do + subject { head api(route(absolute_path), current_user), params: params } + end + it 'returns file attributes in headers' do head api(route(file_path), current_user), params: params @@ -165,6 +183,10 @@ RSpec.describe API::Files do expect(json_response['error']).to eq(invalid_file_message) end + it_behaves_like 'when path is absolute' do + subject { get api(route(absolute_path), api_user), params: params } + end + it 'returns file attributes as json' do get api(route(file_path), api_user), params: params @@ -350,6 +372,10 @@ RSpec.describe API::Files do expect(json_response['error']).to eq(invalid_file_message) end + it_behaves_like 'when path is absolute' do + subject { get api(route(absolute_path) + '/blame', current_user), params: params } + end + it 'returns blame file attributes as json' do get api(route(file_path) + '/blame', current_user), params: params @@ -473,6 +499,10 @@ RSpec.describe API::Files do expect(json_response['error']).to eq(invalid_file_message) end + it_behaves_like 'when path is absolute' do + subject { get api(route(absolute_path) + '/raw', current_user), params: params } + end + it 'returns raw file info' do url = route(file_path) + "/raw" expect(Gitlab::Workhorse).to receive(:send_git_blob) @@ -597,6 +627,10 @@ RSpec.describe API::Files do expect(json_response['error']).to eq(invalid_file_message) end + it_behaves_like 'when path is absolute' do + subject { post api(route(absolute_path), user), params: params } + end + it "creates a new file in project repo" do post api(route(file_path), user), params: params @@ -735,6 +769,17 @@ RSpec.describe API::Files do expect(json_response['error']).to eq(invalid_file_message) end + it_behaves_like 'when path is absolute' do + let(:last_commit) do + Gitlab::Git::Commit + .last_for_path(project.repository, 'master', URI.unescape(file_path)) + end + + let(:params_with_correct_id) { params.merge(last_commit_id: last_commit.id) } + + subject { put api(route(absolute_path), user), params: params_with_correct_id } + end + it "returns a 400 bad request if no params given" do put api(route(file_path), user) @@ -770,6 +815,10 @@ RSpec.describe API::Files do expect(json_response['error']).to eq(invalid_file_message) end + it_behaves_like 'when path is absolute' do + subject { delete api(route(absolute_path), user), params: params } + end + it "deletes existing file in project repo" do delete api(route(file_path), user), params: params @@ -821,6 +870,7 @@ RSpec.describe API::Files do encoding: 'base64' } end + let(:get_params) do { ref: 'master' diff --git a/spec/requests/api/go_proxy_spec.rb b/spec/requests/api/go_proxy_spec.rb index 91e455dac19..2d7e319b0be 100644 --- a/spec/requests/api/go_proxy_spec.rb +++ b/spec/requests/api/go_proxy_spec.rb @@ -4,6 +4,7 @@ require 'spec_helper' RSpec.describe API::GoProxy do include PackagesManagerApiSpecHelpers + include HttpBasicAuthHelpers let_it_be(:user) { create :user } let_it_be(:project) { create :project_empty_repo, creator: user, path: 'my-go-lib' } @@ -108,6 +109,7 @@ RSpec.describe API::GoProxy do project.repository.commit_by(oid: sha) end + let(:resource) { "#{version}.info" } it_behaves_like 'an unavailable resource' @@ -386,7 +388,7 @@ RSpec.describe API::GoProxy do end it 'returns ok with a personal access token and basic authentication' do - get_resource(headers: build_basic_auth_header(user.username, pa_token.token)) + get_resource(headers: basic_auth_header(user.username, pa_token.token)) expect(response).to have_gitlab_http_status(:ok) end diff --git a/spec/requests/api/graphql/boards/board_list_issues_query_spec.rb b/spec/requests/api/graphql/boards/board_list_issues_query_spec.rb new file mode 100644 index 00000000000..ae1abb50a40 --- /dev/null +++ b/spec/requests/api/graphql/boards/board_list_issues_query_spec.rb @@ -0,0 +1,101 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'get board lists' do + include GraphqlHelpers + + let_it_be(:user) { create(:user) } + let_it_be(:unauth_user) { create(:user) } + let_it_be(:project) { create(:project, creator_id: user.id, namespace: user.namespace ) } + let_it_be(:group) { create(:group, :private) } + let_it_be(:project_label) { create(:label, project: project, name: 'Development') } + let_it_be(:project_label2) { create(:label, project: project, name: 'Testing') } + let_it_be(:group_label) { create(:group_label, group: group, name: 'Development') } + let_it_be(:group_label2) { create(:group_label, group: group, name: 'Testing') } + + let(:params) { '' } + let(:board) { } + let(:board_parent_type) { board_parent.class.to_s.downcase } + let(:board_data) { graphql_data[board_parent_type]['boards']['nodes'][0] } + let(:lists_data) { board_data['lists']['nodes'][0] } + let(:issues_data) { lists_data['issues']['nodes'] } + + def query(list_params = params) + graphql_query_for( + board_parent_type, + { 'fullPath' => board_parent.full_path }, + <<~BOARDS + boards(first: 1) { + nodes { + lists { + nodes { + issues { + count + nodes { + #{all_graphql_fields_for('issues'.classify)} + } + } + } + } + } + } + BOARDS + ) + end + + def issue_titles + issues_data.map { |i| i['title'] } + end + + shared_examples 'group and project board list issues query' do + let!(:board) { create(:board, resource_parent: board_parent) } + let!(:label_list) { create(:list, board: board, label: label, position: 10) } + let!(:issue1) { create(:issue, project: issue_project, labels: [label], relative_position: 9) } + let!(:issue2) { create(:issue, project: issue_project, labels: [label], relative_position: 2) } + let!(:issue3) { create(:issue, project: issue_project, labels: [label], relative_position: 9) } + let!(:issue4) { create(:issue, project: issue_project, labels: [label2], relative_position: 432) } + + context 'when the user does not have access to the board' do + it 'returns nil' do + post_graphql(query, current_user: unauth_user) + + expect(graphql_data[board_parent_type]).to be_nil + end + end + + context 'when user can read the board' do + before do + board_parent.add_reporter(user) + end + + it 'can access the issues' do + post_graphql(query("id: \"#{global_id_of(label_list)}\""), current_user: user) + + expect(issue_titles).to eq([issue2.title, issue3.title, issue1.title]) + end + end + end + + describe 'for a project' do + let(:board_parent) { project } + let(:label) { project_label } + let(:label2) { project_label2 } + let(:issue_project) { project } + + it_behaves_like 'group and project board list issues query' + end + + describe 'for a group' do + let(:board_parent) { group } + let(:label) { group_label } + let(:label2) { group_label2 } + let(:issue_project) { create(:project, :private, group: group) } + + before do + allow(board_parent).to receive(:multiple_issue_boards_available?).and_return(false) + end + + it_behaves_like 'group and project board list issues query' + end +end diff --git a/spec/requests/api/graphql/boards/board_lists_query_spec.rb b/spec/requests/api/graphql/boards/board_lists_query_spec.rb index 8a89590c85a..0838900eaba 100644 --- a/spec/requests/api/graphql/boards/board_lists_query_spec.rb +++ b/spec/requests/api/graphql/boards/board_lists_query_spec.rb @@ -105,6 +105,20 @@ RSpec.describe 'get board lists' do end end end + + context 'when querying for a single list' do + before do + board_parent.add_reporter(user) + end + + it 'finds the correct list' do + label_list = create(:list, board: board, label: label, position: 10) + + post_graphql(query("id: \"#{global_id_of(label_list)}\""), current_user: user) + + expect(lists_data[0]['node']['title']).to eq label_list.title + end + end end describe 'for a project' do diff --git a/spec/requests/api/graphql/ci/groups_spec.rb b/spec/requests/api/graphql/ci/groups_spec.rb new file mode 100644 index 00000000000..9e81358a152 --- /dev/null +++ b/spec/requests/api/graphql/ci/groups_spec.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true +require 'spec_helper' + +RSpec.describe 'Query.project.pipeline.stages.groups' do + include GraphqlHelpers + + let(:project) { create(:project, :repository, :public) } + let(:user) { create(:user) } + let(:pipeline) { create(:ci_pipeline, project: project, user: user) } + let(:group_graphql_data) { graphql_data.dig('project', 'pipeline', 'stages', 'nodes', 0, 'groups', 'nodes') } + + let(:params) { {} } + + let(:fields) do + <<~QUERY + nodes { + #{all_graphql_fields_for('CiGroup')} + } + QUERY + end + + let(:query) do + %( + query { + project(fullPath: "#{project.full_path}") { + pipeline(iid: "#{pipeline.iid}") { + stages { + nodes { + groups { + #{fields} + } + } + } + } + } + } + ) + end + + before do + create(:commit_status, pipeline: pipeline, name: 'rspec 0 2') + create(:commit_status, pipeline: pipeline, name: 'rspec 0 1') + create(:commit_status, pipeline: pipeline, name: 'spinach 0 1') + post_graphql(query, current_user: user) + end + + it_behaves_like 'a working graphql query' + + it 'returns a array of jobs belonging to a pipeline' do + expect(group_graphql_data.map { |g| g.slice('name', 'size') }).to eq([ + { 'name' => 'rspec', 'size' => 2 }, + { 'name' => 'spinach', 'size' => 1 } + ]) + end +end diff --git a/spec/requests/api/graphql/ci/jobs_spec.rb b/spec/requests/api/graphql/ci/jobs_spec.rb new file mode 100644 index 00000000000..7d416f4720b --- /dev/null +++ b/spec/requests/api/graphql/ci/jobs_spec.rb @@ -0,0 +1,93 @@ +# frozen_string_literal: true +require 'spec_helper' + +RSpec.describe 'Query.project.pipeline.stages.groups.jobs' do + include GraphqlHelpers + + let(:project) { create(:project, :repository, :public) } + let(:user) { create(:user) } + let(:pipeline) do + pipeline = create(:ci_pipeline, project: project, user: user) + stage = create(:ci_stage_entity, pipeline: pipeline, name: 'first') + create(:commit_status, stage_id: stage.id, pipeline: pipeline, name: 'my test job') + + pipeline + end + + def first(field) + [field.pluralize, 'nodes', 0] + end + + let(:jobs_graphql_data) { graphql_data.dig(*%w[project pipeline], *first('stage'), *first('group'), 'jobs', 'nodes') } + + let(:query) do + %( + query { + project(fullPath: "#{project.full_path}") { + pipeline(iid: "#{pipeline.iid}") { + stages { + nodes { + name + groups { + nodes { + name + jobs { + nodes { + name + } + } + } + } + } + } + } + } + } + ) + end + + it 'returns the jobs of a pipeline stage' do + post_graphql(query, current_user: user) + + expect(jobs_graphql_data).to contain_exactly(a_hash_including('name' => 'my test job')) + end + + context 'when fetching jobs from the pipeline' do + it 'avoids N+1 queries' do + control_count = ActiveRecord::QueryRecorder.new do + post_graphql(query, current_user: user) + end + + build_stage = create(:ci_stage_entity, name: 'build', pipeline: pipeline) + test_stage = create(:ci_stage_entity, name: 'test', pipeline: pipeline) + create(:commit_status, pipeline: pipeline, stage_id: build_stage.id, name: 'docker 1 2') + create(:commit_status, pipeline: pipeline, stage_id: build_stage.id, name: 'docker 2 2') + create(:commit_status, pipeline: pipeline, stage_id: test_stage.id, name: 'rspec 1 2') + create(:commit_status, pipeline: pipeline, stage_id: test_stage.id, name: 'rspec 2 2') + + expect do + post_graphql(query, current_user: user) + end.not_to exceed_query_limit(control_count) + + expect(response).to have_gitlab_http_status(:ok) + + build_stage = graphql_data.dig('project', 'pipeline', 'stages', 'nodes').find do |stage| + stage['name'] == 'build' + end + test_stage = graphql_data.dig('project', 'pipeline', 'stages', 'nodes').find do |stage| + stage['name'] == 'test' + end + docker_group = build_stage.dig('groups', 'nodes').first + rspec_group = test_stage.dig('groups', 'nodes').first + + expect(docker_group['name']).to eq('docker') + expect(rspec_group['name']).to eq('rspec') + + docker_jobs = docker_group.dig('jobs', 'nodes') + rspec_jobs = rspec_group.dig('jobs', 'nodes') + + expect(docker_jobs).to eq([{ 'name' => 'docker 1 2' }, { 'name' => 'docker 2 2' }]) + expect(rspec_jobs).to eq([{ 'name' => 'rspec 1 2' }, { 'name' => 'rspec 2 2' }]) + end + end +end diff --git a/spec/requests/api/graphql/ci/stages_spec.rb b/spec/requests/api/graphql/ci/stages_spec.rb new file mode 100644 index 00000000000..cd48a24b9c8 --- /dev/null +++ b/spec/requests/api/graphql/ci/stages_spec.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true +require 'spec_helper' + +RSpec.describe 'Query.project.pipeline.stages' do + include GraphqlHelpers + + let(:project) { create(:project, :repository, :public) } + let(:user) { create(:user) } + let(:pipeline) { create(:ci_pipeline, project: project, user: user) } + let(:stage_graphql_data) { graphql_data['project']['pipeline']['stages'] } + + let(:params) { {} } + + let(:fields) do + <<~QUERY + nodes { + #{all_graphql_fields_for('CiStage')} + } + QUERY + end + + let(:query) do + %( + query { + project(fullPath: "#{project.full_path}") { + pipeline(iid: "#{pipeline.iid}") { + stages { + #{fields} + } + } + } + } + ) + end + + before do + create(:ci_stage_entity, pipeline: pipeline, name: 'deploy') + post_graphql(query, current_user: user) + end + + it_behaves_like 'a working graphql query' + + it 'returns the stage of a pipeline' do + expect(stage_graphql_data['nodes'].first['name']).to eq('deploy') + end +end diff --git a/spec/requests/api/graphql/issue_status_counts_spec.rb b/spec/requests/api/graphql/issue_status_counts_spec.rb new file mode 100644 index 00000000000..3d8817c3bc5 --- /dev/null +++ b/spec/requests/api/graphql/issue_status_counts_spec.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true +require 'spec_helper' + +RSpec.describe 'getting Issue counts by status' do + include GraphqlHelpers + + let_it_be(:project) { create(:project, :repository) } + let_it_be(:current_user) { create(:user) } + let_it_be(:issue_opened) { create(:issue, project: project) } + let_it_be(:issue_closed) { create(:issue, :closed, project: project) } + let_it_be(:other_project_issue) { create(:issue) } + let(:params) { {} } + + let(:fields) do + <<~QUERY + #{all_graphql_fields_for('IssueStatusCountsType'.classify)} + QUERY + end + + let(:query) do + graphql_query_for( + 'project', + { 'fullPath' => project.full_path }, + query_graphql_field('issueStatusCounts', params, fields) + ) + end + + context 'with issue count data' do + let(:issue_counts) { graphql_data.dig('project', 'issueStatusCounts') } + + context 'without project permissions' do + let(:user) { create(:user) } + + before do + post_graphql(query, current_user: current_user) + end + + it_behaves_like 'a working graphql query' + it { expect(issue_counts).to be nil } + end + + context 'with project permissions' do + before do + project.add_developer(current_user) + post_graphql(query, current_user: current_user) + end + + it_behaves_like 'a working graphql query' + it 'returns the correct counts for each status' do + expect(issue_counts).to eq( + 'all' => 2, + 'opened' => 1, + 'closed' => 1 + ) + end + end + end +end diff --git a/spec/requests/api/graphql/metrics/dashboard/annotations_spec.rb b/spec/requests/api/graphql/metrics/dashboard/annotations_spec.rb index c47920087dc..ca5a9165760 100644 --- a/spec/requests/api/graphql/metrics/dashboard/annotations_spec.rb +++ b/spec/requests/api/graphql/metrics/dashboard/annotations_spec.rb @@ -30,22 +30,22 @@ RSpec.describe 'Getting Metrics Dashboard Annotations' do let(:query) do %( - query { - project(fullPath:"#{project.full_path}") { - environments(name: "#{environment.name}") { - nodes { - metricsDashboard(path: "#{path}"){ - annotations(#{args}){ - nodes { - #{fields} - } - } + query { + project(fullPath: "#{project.full_path}") { + environments(name: "#{environment.name}") { + nodes { + metricsDashboard(path: "#{path}") { + annotations(#{args}) { + nodes { + #{fields} } } } } } - ) + } + } + ) end before do diff --git a/spec/requests/api/graphql/milestone_spec.rb b/spec/requests/api/graphql/milestone_spec.rb new file mode 100644 index 00000000000..59de116fa2b --- /dev/null +++ b/spec/requests/api/graphql/milestone_spec.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Querying a Milestone' do + include GraphqlHelpers + + let_it_be(:current_user) { create(:user) } + let_it_be(:project) { create(:project) } + let_it_be(:milestone) { create(:milestone, project: project) } + + let(:query) do + graphql_query_for('milestone', { id: milestone.to_global_id.to_s }, 'title') + end + + subject { graphql_data['milestone'] } + + before do + post_graphql(query, current_user: current_user) + end + + context 'when the user has access to the milestone' do + before_all do + project.add_guest(current_user) + end + + it_behaves_like 'a working graphql query' + + it { is_expected.to include('title' => milestone.name) } + end + + context 'when the user does not have access to the milestone' do + it_behaves_like 'a working graphql query' + + it { is_expected.to be_nil } + end + + context 'when ID argument is missing' do + let(:query) do + graphql_query_for('milestone', {}, 'title') + end + + it 'raises an exception' do + expect(graphql_errors).to include(a_hash_including('message' => "Field 'milestone' is missing required arguments: id")) + end + end +end diff --git a/spec/requests/api/graphql/mutations/boards/issues/issue_move_list_spec.rb b/spec/requests/api/graphql/mutations/boards/issues/issue_move_list_spec.rb new file mode 100644 index 00000000000..e24ab0b07f2 --- /dev/null +++ b/spec/requests/api/graphql/mutations/boards/issues/issue_move_list_spec.rb @@ -0,0 +1,109 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Reposition and move issue within board lists' do + include GraphqlHelpers + + let_it_be(:group) { create(:group, :private) } + let_it_be(:project) { create(:project, group: group) } + let_it_be(:board) { create(:board, group: group) } + let_it_be(:user) { create(:user) } + let_it_be(:development) { create(:label, project: project, name: 'Development') } + let_it_be(:testing) { create(:label, project: project, name: 'Testing') } + let_it_be(:list1) { create(:list, board: board, label: development, position: 0) } + let_it_be(:list2) { create(:list, board: board, label: testing, position: 1) } + let_it_be(:existing_issue1) { create(:labeled_issue, project: project, labels: [testing], relative_position: 10) } + let_it_be(:existing_issue2) { create(:labeled_issue, project: project, labels: [testing], relative_position: 50) } + let_it_be(:issue1) { create(:labeled_issue, project: project, labels: [development]) } + + let(:mutation_class) { Mutations::Boards::Issues::IssueMoveList } + let(:mutation_name) { mutation_class.graphql_name } + let(:mutation_result_identifier) { mutation_name.camelize(:lower) } + let(:current_user) { user } + let(:params) { { board_id: board.to_global_id.to_s, project_path: project.full_path, iid: issue1.iid.to_s } } + let(:issue_move_params) do + { + from_list_id: list1.id, + to_list_id: list2.id + } + end + + before_all do + group.add_maintainer(user) + end + + shared_examples 'returns an error' do + it 'fails with error' do + message = "The resource that you are attempting to access does not exist or you don't have "\ + "permission to perform this action" + + post_graphql_mutation(mutation(params), current_user: current_user) + + expect(graphql_errors).to include(a_hash_including('message' => message)) + end + end + + context 'when user has access to resources' do + context 'when repositioning an issue' do + let(:issue_move_params) { { move_after_id: existing_issue1.id, move_before_id: existing_issue2.id } } + + it 'repositions an issue' do + post_graphql_mutation(mutation(params), current_user: current_user) + + expect(response).to have_gitlab_http_status(:success) + response_issue = json_response['data'][mutation_result_identifier]['issue'] + expect(response_issue['iid']).to eq(issue1.iid.to_s) + expect(response_issue['relativePosition']).to be > existing_issue1.relative_position + expect(response_issue['relativePosition']).to be < existing_issue2.relative_position + end + end + + context 'when moving an issue to a different list' do + let(:issue_move_params) { { from_list_id: list1.id, to_list_id: list2.id } } + + it 'moves issue to a different list' do + post_graphql_mutation(mutation(params), current_user: current_user) + + expect(response).to have_gitlab_http_status(:success) + response_issue = json_response['data'][mutation_result_identifier]['issue'] + expect(response_issue['iid']).to eq(issue1.iid.to_s) + expect(response_issue['labels']['edges'][0]['node']['title']).to eq(testing.title) + end + end + end + + context 'when user has no access to resources' do + context 'the user is not allowed to update the issue' do + let(:current_user) { create(:user) } + + it_behaves_like 'returns an error' + end + + context 'when the user can not read board' do + let(:board) { create(:board, group: create(:group, :private)) } + + it_behaves_like 'returns an error' + end + end + + def mutation(additional_params = {}) + graphql_mutation(mutation_name, issue_move_params.merge(additional_params), + <<-QL.strip_heredoc + clientMutationId + issue { + iid, + relativePosition + labels { + edges { + node{ + title + } + } + } + } + errors + QL + ) + end +end diff --git a/spec/requests/api/graphql/mutations/boards/lists/update_spec.rb b/spec/requests/api/graphql/mutations/boards/lists/update_spec.rb new file mode 100644 index 00000000000..8a6d2cb3994 --- /dev/null +++ b/spec/requests/api/graphql/mutations/boards/lists/update_spec.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Update of an existing board list' do + include GraphqlHelpers + + let_it_be(:current_user) { create(:user) } + let_it_be(:group) { create(:group, :private) } + let_it_be(:board) { create(:board, group: group) } + let_it_be(:list) { create(:list, board: board, position: 0) } + let_it_be(:list2) { create(:list, board: board) } + let_it_be(:input) { { list_id: list.to_global_id.to_s, position: 1, collapsed: true } } + let(:mutation) { graphql_mutation(:update_board_list, input) } + let(:mutation_response) { graphql_mutation_response(:update_board_list) } + + context 'the user is not allowed to read board lists' do + it_behaves_like 'a mutation that returns top-level errors', + errors: ['The resource that you are attempting to access does not exist or you don\'t have permission to perform this action'] + end + + before do + list.update_preferences_for(current_user, collapsed: false) + end + + context 'when user has permissions to admin board lists' do + before do + group.add_reporter(current_user) + end + + it 'updates the list position and collapsed state' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(response).to have_gitlab_http_status(:success) + expect(mutation_response['list']).to include( + 'position' => 1, + 'collapsed' => true + ) + end + end + + context 'when user has permissions to read board lists' do + before do + group.add_guest(current_user) + end + + it 'updates the list collapsed state but not the list position' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(response).to have_gitlab_http_status(:success) + expect(mutation_response['list']).to include( + 'position' => 0, + 'collapsed' => true + ) + end + end +end diff --git a/spec/requests/api/graphql/mutations/container_expiration_policy/update_spec.rb b/spec/requests/api/graphql/mutations/container_expiration_policy/update_spec.rb index bc1b42d68e6..7bef812bfec 100644 --- a/spec/requests/api/graphql/mutations/container_expiration_policy/update_spec.rb +++ b/spec/requests/api/graphql/mutations/container_expiration_policy/update_spec.rb @@ -18,6 +18,7 @@ RSpec.describe 'Updating the container expiration policy' do older_than: 'FOURTEEN_DAYS' } end + let(:mutation) do graphql_mutation(:update_container_expiration_policy, params, <<~QL @@ -32,6 +33,7 @@ RSpec.describe 'Updating the container expiration policy' do QL ) end + let(:mutation_response) { graphql_mutation_response(:update_container_expiration_policy) } let(:container_expiration_policy_response) { mutation_response['containerExpirationPolicy'] } diff --git a/spec/requests/api/graphql/mutations/design_management/move_spec.rb b/spec/requests/api/graphql/mutations/design_management/move_spec.rb new file mode 100644 index 00000000000..dd121ec733e --- /dev/null +++ b/spec/requests/api/graphql/mutations/design_management/move_spec.rb @@ -0,0 +1,122 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "moving designs" do + include GraphqlHelpers + include DesignManagementTestHelpers + + let_it_be(:issue) { create(:issue) } + let_it_be(:designs) { create_list(:design, 3, :with_versions, :with_relative_position, issue: issue) } + let_it_be(:developer) { create(:user, developer_projects: [issue.project]) } + + let(:user) { developer } + + let(:current_design) { designs.first } + let(:previous_design) { designs.second } + let(:next_design) { designs.third } + let(:mutation_name) { :design_management_move } + + let(:mutation) do + input = { + id: current_design.to_global_id.to_s, + previous: previous_design&.to_global_id&.to_s, + next: next_design&.to_global_id&.to_s + }.compact + + graphql_mutation(mutation_name, input, <<~FIELDS) + errors + designCollection { + designs { + nodes { + filename + } + } + } + FIELDS + end + + let(:move_designs) { post_graphql_mutation(mutation, current_user: user) } + let(:mutation_response) { graphql_mutation_response(mutation_name) } + + before do + enable_design_management + designs.each(&:reset) + issue.reset + end + + shared_examples 'a successful move' do + it 'does not error, and reports the current order' do + move_designs + + expect(graphql_errors).not_to be_present + + expect(mutation_response).to eq( + 'errors' => [], + 'designCollection' => { + 'designs' => { + 'nodes' => new_order.map { |d| { 'filename' => d.filename } } + } + } + ) + end + end + + context 'the user is not allowed to move designs' do + let(:user) { create(:user) } + + it 'returns an error' do + move_designs + + expect(graphql_errors).to be_present + end + end + + context 'the neighbors do not have positions' do + let!(:previous_design) { create(:design, :with_versions, issue: issue) } + let!(:next_design) { create(:design, :with_versions, issue: issue) } + + let(:new_order) do + [ + designs.second, + designs.third, + previous_design, current_design, next_design + ] + end + + it_behaves_like 'a successful move' + + it 'maintains the correct order in the presence of other unpositioned designs' do + other_design = create(:design, :with_versions, issue: issue) + + move_designs + moved_designs = mutation_response.dig('designCollection', 'designs', 'nodes') + + expect(moved_designs.map { |d| d['filename'] }) + .to eq([*new_order.map(&:filename), other_design.filename]) + end + end + + context 'moving a design between two others' do + let(:new_order) { [designs.second, designs.first, designs.third] } + + it_behaves_like 'a successful move' + end + + context 'moving a design to the start' do + let(:current_design) { designs.last } + let(:next_design) { designs.first } + let(:previous_design) { nil } + let(:new_order) { [designs.last, designs.first, designs.second] } + + it_behaves_like 'a successful move' + end + + context 'moving a design to the end' do + let(:current_design) { designs.first } + let(:next_design) { nil } + let(:previous_design) { designs.last } + let(:new_order) { [designs.second, designs.third, designs.first] } + + it_behaves_like 'a successful move' + end +end diff --git a/spec/requests/api/graphql/mutations/discussions/toggle_resolve_spec.rb b/spec/requests/api/graphql/mutations/discussions/toggle_resolve_spec.rb index e83da830935..457c37e900b 100644 --- a/spec/requests/api/graphql/mutations/discussions/toggle_resolve_spec.rb +++ b/spec/requests/api/graphql/mutations/discussions/toggle_resolve_spec.rb @@ -10,9 +10,11 @@ RSpec.describe 'Toggling the resolve status of a discussion' do let(:discussion) do create(:diff_note_on_merge_request, noteable: noteable, project: project).to_discussion end + let(:mutation) do graphql_mutation(:discussion_toggle_resolve, { id: discussion.to_global_id.to_s, resolve: true }) end + let(:mutation_response) { graphql_mutation_response(:discussion_toggle_resolve) } context 'when the user does not have permission' do 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 3dd1225db5a..b3c9b9d4995 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 @@ -49,13 +49,13 @@ RSpec.describe 'Setting Due Date of an issue' do expect(mutation_response['issue']['dueDate']).to eq(2.days.since.to_date.to_s) end - context 'when passing due date without a date value' do + context 'when the due date value is not a valid time' do let(:input) { { due_date: 'test' } } - it 'returns internal server error' do + it 'returns a coercion error' do post_graphql_mutation(mutation, current_user: current_user) - expect(graphql_errors).to include(a_hash_including('message' => 'Internal server error')) + expect(graphql_errors).to include(a_hash_including('message' => /provided invalid value for dueDate/)) end end end diff --git a/spec/requests/api/graphql/mutations/issues/set_subscription_spec.rb b/spec/requests/api/graphql/mutations/issues/set_subscription_spec.rb new file mode 100644 index 00000000000..1edc1e0553b --- /dev/null +++ b/spec/requests/api/graphql/mutations/issues/set_subscription_spec.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Setting subscribed status of an issue' do + include GraphqlHelpers + + it_behaves_like 'a subscribable resource api' do + let_it_be(:resource) { create(:issue) } + let(:mutation_name) { :issue_set_subscription } + end +end diff --git a/spec/requests/api/graphql/mutations/issues/update_spec.rb b/spec/requests/api/graphql/mutations/issues/update_spec.rb new file mode 100644 index 00000000000..fd983c683be --- /dev/null +++ b/spec/requests/api/graphql/mutations/issues/update_spec.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Update of an existing issue' do + include GraphqlHelpers + + let_it_be(:current_user) { create(:user) } + let_it_be(:project) { create(:project, :public) } + let_it_be(:issue) { create(:issue, project: project) } + let(:input) do + { + project_path: project.full_path, + iid: issue.iid.to_s, + locked: true + } + end + + let(:mutation) { graphql_mutation(:update_issue, input) } + let(:mutation_response) { graphql_mutation_response(:update_issue) } + + context 'the user is not allowed to update issue' do + it_behaves_like 'a mutation that returns top-level errors', + errors: ['The resource that you are attempting to access does not exist or you don\'t have permission to perform this action'] + end + + context 'when user has permissions to update issue' do + before do + project.add_developer(current_user) + end + + it 'updates the issue' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(response).to have_gitlab_http_status(:success) + expect(mutation_response['issue']).to include( + 'discussionLocked' => true + ) + end + end +end diff --git a/spec/requests/api/graphql/mutations/merge_requests/create_spec.rb b/spec/requests/api/graphql/mutations/merge_requests/create_spec.rb index d4ac639e226..9297ca054c7 100644 --- a/spec/requests/api/graphql/mutations/merge_requests/create_spec.rb +++ b/spec/requests/api/graphql/mutations/merge_requests/create_spec.rb @@ -15,6 +15,7 @@ RSpec.describe 'Creation of a new merge request' do target_branch: target_branch } end + let(:title) { 'MergeRequest' } let(:source_branch) { 'new_branch' } let(:target_branch) { 'master' } diff --git a/spec/requests/api/graphql/mutations/merge_requests/set_subscription_spec.rb b/spec/requests/api/graphql/mutations/merge_requests/set_subscription_spec.rb index 6b3035fbf48..d90faa605c0 100644 --- a/spec/requests/api/graphql/mutations/merge_requests/set_subscription_spec.rb +++ b/spec/requests/api/graphql/mutations/merge_requests/set_subscription_spec.rb @@ -5,59 +5,8 @@ require 'spec_helper' RSpec.describe 'Setting subscribed status of a merge request' do include GraphqlHelpers - let(:current_user) { create(:user) } - let(:merge_request) { create(:merge_request) } - let(:project) { merge_request.project } - let(:input) { { subscribed_state: true } } - - let(:mutation) do - variables = { - project_path: project.full_path, - iid: merge_request.iid.to_s - } - graphql_mutation(:merge_request_set_subscription, variables.merge(input), - <<-QL.strip_heredoc - clientMutationId - errors - mergeRequest { - id - subscribed - } - QL - ) - end - - def mutation_response - graphql_mutation_response(:merge_request_set_subscription)['mergeRequest']['subscribed'] - end - - before do - project.add_developer(current_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 - - it 'marks the merge request as WIP' do - post_graphql_mutation(mutation, current_user: current_user) - - expect(response).to have_gitlab_http_status(:success) - expect(mutation_response).to eq(true) - end - - context 'when passing subscribe false as input' do - let(:input) { { subscribed_state: false } } - - it 'unmarks the merge request as subscribed' do - merge_request.subscribe(current_user, project) - - post_graphql_mutation(mutation, current_user: current_user) - - expect(response).to have_gitlab_http_status(:success) - expect(mutation_response).to eq(false) - end + it_behaves_like 'a subscribable resource api' do + let_it_be(:resource) { create(:merge_request) } + let(:mutation_name) { :merge_request_set_subscription } end end diff --git a/spec/requests/api/graphql/mutations/notes/update/image_diff_note_spec.rb b/spec/requests/api/graphql/mutations/notes/update/image_diff_note_spec.rb index f7be671e5f3..463a872d95d 100644 --- a/spec/requests/api/graphql/mutations/notes/update/image_diff_note_spec.rb +++ b/spec/requests/api/graphql/mutations/notes/update/image_diff_note_spec.rb @@ -33,6 +33,7 @@ RSpec.describe 'Updating an image DiffNote' do y: updated_y } end + let!(:diff_note) do create(:image_diff_note_on_merge_request, noteable: noteable, @@ -40,6 +41,7 @@ RSpec.describe 'Updating an image DiffNote' do note: original_body, position: original_position) end + let(:mutation) do variables = { id: GitlabSchema.id_from_object(diff_note).to_s, diff --git a/spec/requests/api/graphql/mutations/notes/update/note_spec.rb b/spec/requests/api/graphql/mutations/notes/update/note_spec.rb index 38378310d9f..0d93afe9434 100644 --- a/spec/requests/api/graphql/mutations/notes/update/note_spec.rb +++ b/spec/requests/api/graphql/mutations/notes/update/note_spec.rb @@ -8,11 +8,9 @@ RSpec.describe 'Updating a Note' do let!(:note) { create(:note, note: original_body) } let(:original_body) { 'Initial body text' } let(:updated_body) { 'Updated body text' } + let(:params) { { body: updated_body, confidential: true } } let(:mutation) do - variables = { - id: GitlabSchema.id_from_object(note).to_s, - body: updated_body - } + variables = params.merge(id: GitlabSchema.id_from_object(note).to_s) graphql_mutation(:update_note, variables) end @@ -31,6 +29,7 @@ RSpec.describe 'Updating a Note' do post_graphql_mutation(mutation, current_user: current_user) expect(note.reload.note).to eq(original_body) + expect(note.confidential).to be_falsey end end @@ -43,12 +42,40 @@ RSpec.describe 'Updating a Note' do post_graphql_mutation(mutation, current_user: current_user) expect(note.reload.note).to eq(updated_body) + expect(note.confidential).to be_truthy end it 'returns the updated Note' do post_graphql_mutation(mutation, current_user: current_user) expect(mutation_response['note']['body']).to eq(updated_body) + expect(mutation_response['note']['confidential']).to be_truthy + end + + context 'when only confidential param is present' do + let(:params) { { confidential: true } } + + it 'updates only the note confidentiality' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(note.reload.note).to eq(original_body) + expect(note.confidential).to be_truthy + end + end + + context 'when only body param is present' do + let(:params) { { body: updated_body } } + + before do + note.update_column(:confidential, true) + end + + it 'updates only the note body' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(note.reload.note).to eq(updated_body) + expect(note.confidential).to be_truthy + end end context 'when there are ActiveRecord validation errors' do @@ -60,12 +87,14 @@ RSpec.describe 'Updating a Note' do post_graphql_mutation(mutation, current_user: current_user) expect(note.reload.note).to eq(original_body) + expect(note.confidential).to be_falsey end - it 'returns the Note with its original body' do + it 'returns the original Note' do post_graphql_mutation(mutation, current_user: current_user) expect(mutation_response['note']['body']).to eq(original_body) + expect(mutation_response['note']['confidential']).to be_falsey end end diff --git a/spec/requests/api/graphql/mutations/snippets/create_spec.rb b/spec/requests/api/graphql/mutations/snippets/create_spec.rb index e2474e1bcce..56a5f4907c1 100644 --- a/spec/requests/api/graphql/mutations/snippets/create_spec.rb +++ b/spec/requests/api/graphql/mutations/snippets/create_spec.rb @@ -149,7 +149,7 @@ RSpec.describe 'Creating a Snippet' do visibility_level: visibility_level, project_path: project_path, title: title, - files: actions + blob_actions: actions } end diff --git a/spec/requests/api/graphql/mutations/snippets/update_spec.rb b/spec/requests/api/graphql/mutations/snippets/update_spec.rb index 3b2f9dc0f19..3f39c0ab851 100644 --- a/spec/requests/api/graphql/mutations/snippets/update_spec.rb +++ b/spec/requests/api/graphql/mutations/snippets/update_spec.rb @@ -26,6 +26,7 @@ RSpec.describe 'Updating a Snippet' do title: updated_title } end + let(:mutation) do graphql_mutation(:update_snippet, mutation_vars) end @@ -157,7 +158,7 @@ RSpec.describe 'Updating a Snippet' do let(:mutation_vars) do { id: snippet_gid, - files: [ + blob_actions: [ { action: :update, filePath: updated_file, content: updated_content }, { action: :delete, filePath: deleted_file } ] diff --git a/spec/requests/api/graphql/project/alert_management/alert/todos_spec.rb b/spec/requests/api/graphql/project/alert_management/alert/todos_spec.rb new file mode 100644 index 00000000000..3a9077061ad --- /dev/null +++ b/spec/requests/api/graphql/project/alert_management/alert/todos_spec.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'getting Alert Management Alert Assignees' do + include GraphqlHelpers + + let_it_be(:project) { create(:project) } + let_it_be(:current_user) { create(:user) } + let_it_be(:alert) { create(:alert_management_alert, project: project) } + let_it_be(:other_alert) { create(:alert_management_alert, project: project) } + let_it_be(:todo) { create(:todo, :pending, target: alert, user: current_user, project: project) } + let_it_be(:other_todo) { create(:todo, :pending, target: other_alert, user: current_user, project: project) } + + let(:fields) do + <<~QUERY + nodes { + iid + todos { + nodes { + id + } + } + } + QUERY + end + + let(:graphql_query) do + graphql_query_for( + 'project', + { 'fullPath' => project.full_path }, + query_graphql_field('alertManagementAlerts', {}, fields) + ) + end + + let(:gql_alerts) { graphql_data.dig('project', 'alertManagementAlerts', 'nodes') } + let(:gql_todos) { gql_alerts.map { |gql_alert| [gql_alert['iid'], gql_alert['todos']['nodes']] }.to_h } + let(:gql_alert_todo) { gql_todos[alert.iid.to_s].first } + let(:gql_other_alert_todo) { gql_todos[other_alert.iid.to_s].first } + + before do + project.add_developer(current_user) + end + + it 'includes the correct metrics dashboard url' do + post_graphql(graphql_query, current_user: current_user) + + expect(gql_alert_todo['id']).to eq(todo.to_global_id.to_s) + expect(gql_other_alert_todo['id']).to eq(other_todo.to_global_id.to_s) + end +end diff --git a/spec/requests/api/graphql/project/alert_management/alert_status_counts_spec.rb b/spec/requests/api/graphql/project/alert_management/alert_status_counts_spec.rb index b62215f43fb..9fbf5aaa41f 100644 --- a/spec/requests/api/graphql/project/alert_management/alert_status_counts_spec.rb +++ b/spec/requests/api/graphql/project/alert_management/alert_status_counts_spec.rb @@ -6,8 +6,8 @@ RSpec.describe 'getting Alert Management Alert counts by status' do let_it_be(:project) { create(:project, :repository) } let_it_be(:current_user) { create(:user) } - let_it_be(:alert_1) { create(:alert_management_alert, :resolved, project: project) } - let_it_be(:alert_2) { create(:alert_management_alert, project: project) } + let_it_be(:alert_resolved) { create(:alert_management_alert, :resolved, project: project) } + let_it_be(:alert_triggered) { create(:alert_management_alert, project: project) } let_it_be(:other_project_alert) { create(:alert_management_alert) } let(:params) { {} } @@ -58,7 +58,7 @@ RSpec.describe 'getting Alert Management Alert counts by status' do end context 'with search criteria' do - let(:params) { { search: alert_1.title } } + let(:params) { { search: alert_resolved.title } } it_behaves_like 'a working graphql query' it 'returns the correct counts for each status' do diff --git a/spec/requests/api/graphql/project/alert_management/alerts_spec.rb b/spec/requests/api/graphql/project/alert_management/alerts_spec.rb index f050c6873f3..d3a2e6a1deb 100644 --- a/spec/requests/api/graphql/project/alert_management/alerts_spec.rb +++ b/spec/requests/api/graphql/project/alert_management/alerts_spec.rb @@ -4,12 +4,12 @@ require 'spec_helper' RSpec.describe 'getting Alert Management Alerts' do include GraphqlHelpers - let_it_be(:payload) { { 'custom' => { 'alert' => 'payload' } } } + let_it_be(:payload) { { 'custom' => { 'alert' => 'payload' }, 'runbook' => 'runbook' } } let_it_be(:project) { create(:project, :repository) } let_it_be(:current_user) { create(:user) } - let_it_be(:resolved_alert) { create(:alert_management_alert, :all_fields, :resolved, project: project, issue: nil, severity: :low) } - let_it_be(:triggered_alert) { create(:alert_management_alert, :all_fields, project: project, severity: :critical, payload: payload) } - let_it_be(:other_project_alert) { create(:alert_management_alert, :all_fields) } + let_it_be(:resolved_alert) { create(:alert_management_alert, :all_fields, :resolved, project: project, issue: nil, severity: :low).present } + let_it_be(:triggered_alert) { create(:alert_management_alert, :all_fields, project: project, severity: :critical, payload: payload).present } + let_it_be(:other_project_alert) { create(:alert_management_alert, :all_fields).present } let(:params) { {} } @@ -71,10 +71,13 @@ RSpec.describe 'getting Alert Management Alerts' do 'eventCount' => triggered_alert.events, 'startedAt' => triggered_alert.started_at.strftime('%Y-%m-%dT%H:%M:%SZ'), 'endedAt' => nil, - 'details' => { 'custom.alert' => 'payload' }, + 'details' => { 'custom.alert' => 'payload', 'runbook' => 'runbook' }, 'createdAt' => triggered_alert.created_at.strftime('%Y-%m-%dT%H:%M:%SZ'), 'updatedAt' => triggered_alert.updated_at.strftime('%Y-%m-%dT%H:%M:%SZ'), - 'metricsDashboardUrl' => nil + 'metricsDashboardUrl' => nil, + 'detailsUrl' => triggered_alert.details_url, + 'prometheusAlert' => nil, + 'runbook' => 'runbook' ) expect(second_alert).to include( diff --git a/spec/requests/api/graphql/project/container_expiration_policy_spec.rb b/spec/requests/api/graphql/project/container_expiration_policy_spec.rb index b064e4d43e9..dc16847a669 100644 --- a/spec/requests/api/graphql/project/container_expiration_policy_spec.rb +++ b/spec/requests/api/graphql/project/container_expiration_policy_spec.rb @@ -13,6 +13,7 @@ RSpec.describe 'getting a repository in a project' do #{all_graphql_fields_for('container_expiration_policy'.classify)} QUERY end + let(:query) do graphql_query_for( 'project', diff --git a/spec/requests/api/graphql/project/issue/designs/designs_spec.rb b/spec/requests/api/graphql/project/issue/designs/designs_spec.rb index e47c025f8b2..decab900a43 100644 --- a/spec/requests/api/graphql/project/issue/designs/designs_spec.rb +++ b/spec/requests/api/graphql/project/issue/designs/designs_spec.rb @@ -24,12 +24,14 @@ RSpec.describe 'Getting designs related to an issue' do } NODE end + let(:issue) { design.issue } let(:project) { issue.project } let(:query) { make_query } let(:design_collection) do graphql_data_at(:project, :issue, :design_collection) end + let(:design_response) do design_collection.dig('designs', 'edges').first['node'] end @@ -185,6 +187,7 @@ RSpec.describe 'Getting designs related to an issue' do } NODE end + let(:design_response) do design_collection['designs']['edges'] end diff --git a/spec/requests/api/graphql/project/issues_spec.rb b/spec/requests/api/graphql/project/issues_spec.rb index cdfff2f50d4..06e613a09bc 100644 --- a/spec/requests/api/graphql/project/issues_spec.rb +++ b/spec/requests/api/graphql/project/issues_spec.rb @@ -12,6 +12,7 @@ RSpec.describe 'getting an issue list for a project' do [create(:issue, project: project, discussion_locked: true), create(:issue, project: project)] end + let(:fields) do <<~QUERY edges { diff --git a/spec/requests/api/graphql/project/jira_import_spec.rb b/spec/requests/api/graphql/project/jira_import_spec.rb index 814965262b6..1cc30b95162 100644 --- a/spec/requests/api/graphql/project/jira_import_spec.rb +++ b/spec/requests/api/graphql/project/jira_import_spec.rb @@ -53,6 +53,7 @@ RSpec.describe 'query Jira import data' do } ) end + let(:jira_imports) { graphql_data.dig('project', 'jiraImports', 'nodes')} let(:jira_import_status) { graphql_data.dig('project', 'jiraImportStatus')} @@ -106,7 +107,7 @@ RSpec.describe 'query Jira import data' do let(:query) do %( query { - project(fullPath:"#{project.full_path}") { + project(fullPath: "#{project.full_path}") { jiraImports(first: 1) { nodes { jiraProjectKey @@ -132,7 +133,7 @@ RSpec.describe 'query Jira import data' do let(:query) do %( query { - project(fullPath:"#{project.full_path}") { + project(fullPath: "#{project.full_path}") { jiraImports(last: 1) { nodes { jiraProjectKey diff --git a/spec/requests/api/graphql/project/jira_projects_spec.rb b/spec/requests/api/graphql/project/jira_projects_spec.rb index d5f59711ab1..410d5b21505 100644 --- a/spec/requests/api/graphql/project/jira_projects_spec.rb +++ b/spec/requests/api/graphql/project/jira_projects_spec.rb @@ -8,7 +8,7 @@ RSpec.describe 'query Jira projects' do let_it_be(:current_user) { create(:user) } let_it_be(:project) { create(:project) } - include_context 'jira projects request context' + include_context 'Jira projects request context' let(:services) { graphql_data_at(:project, :services, :edges) } let(:jira_projects) { services.first.dig('node', 'projects', 'nodes') } diff --git a/spec/requests/api/graphql/project/merge_requests_spec.rb b/spec/requests/api/graphql/project/merge_requests_spec.rb index e2255fdb048..bb63a5994b0 100644 --- a/spec/requests/api/graphql/project/merge_requests_spec.rb +++ b/spec/requests/api/graphql/project/merge_requests_spec.rb @@ -171,4 +171,43 @@ RSpec.describe 'getting merge request listings nested in a project' do it_behaves_like 'searching with parameters' end + + describe 'fields' do + let(:requested_fields) { nil } + let(:extra_iid_for_second_query) { merge_request_c.iid.to_s } + let(:search_params) { { iids: [merge_request_a.iid.to_s, merge_request_b.iid.to_s] } } + + def execute_query + query = query_merge_requests(requested_fields) + post_graphql(query, current_user: current_user) + end + + context 'when requesting `commit_count`' do + let(:requested_fields) { [:commit_count] } + + it 'exposes `commit_count`' do + merge_request_a.metrics.update!(commits_count: 5) + + execute_query + + expect(results).to include(a_hash_including('commitCount' => 5)) + end + + include_examples 'N+1 query check' + end + + context 'when requesting `merged_at`' do + let(:requested_fields) { [:merged_at] } + + before do + # make the MRs "merged" + [merge_request_a, merge_request_b, merge_request_c].each do |mr| + mr.update_column(:state_id, MergeRequest.available_states[:merged]) + mr.metrics.update_column(:merged_at, Time.now) + end + end + + include_examples 'N+1 query check' + end + end end diff --git a/spec/requests/api/graphql/project/packages_spec.rb b/spec/requests/api/graphql/project/packages_spec.rb index 88f97f9256b..5df98ed1e6b 100644 --- a/spec/requests/api/graphql/project/packages_spec.rb +++ b/spec/requests/api/graphql/project/packages_spec.rb @@ -28,42 +28,40 @@ RSpec.describe 'getting a package list for a project' do ) end - context 'without the need for a license' do - context 'when user has access to the project' do - before do - project.add_reporter(current_user) - post_graphql(query, current_user: current_user) - end + context 'when user has access to the project' do + before do + project.add_reporter(current_user) + post_graphql(query, current_user: current_user) + end - it_behaves_like 'a working graphql query' + it_behaves_like 'a working graphql query' - it 'returns packages successfully' do - expect(packages_data[0]['node']['name']).to eq package.name - end + it 'returns packages successfully' do + expect(packages_data[0]['node']['name']).to eq package.name end + end - context 'when the user does not have access to the project/packages' do - before do - post_graphql(query, current_user: current_user) - end + context 'when the user does not have access to the project/packages' do + before do + post_graphql(query, current_user: current_user) + end - it_behaves_like 'a working graphql query' + it_behaves_like 'a working graphql query' - it 'returns nil' do - expect(graphql_data['project']).to be_nil - end + it 'returns nil' do + expect(graphql_data['project']).to be_nil end + end - context 'when the user is not autenthicated' do - before do - post_graphql(query) - end + context 'when the user is not autenthicated' do + before do + post_graphql(query) + end - it_behaves_like 'a working graphql query' + it_behaves_like 'a working graphql query' - it 'returns nil' do - expect(graphql_data['project']).to be_nil - end + it 'returns nil' do + expect(graphql_data['project']).to be_nil end end end diff --git a/spec/requests/api/graphql/project/pipeline_spec.rb b/spec/requests/api/graphql/project/pipeline_spec.rb index 57b9de25c3d..fef0e7e160c 100644 --- a/spec/requests/api/graphql/project/pipeline_spec.rb +++ b/spec/requests/api/graphql/project/pipeline_spec.rb @@ -29,4 +29,10 @@ RSpec.describe 'getting pipeline information nested in a project' do expect(pipeline_graphql_data).not_to be_nil end + + it 'contains configSource' do + post_graphql(query, current_user: current_user) + + expect(pipeline_graphql_data.dig('configSource')).to eq('UNKNOWN_SOURCE') + end end diff --git a/spec/requests/api/graphql/project/repository_spec.rb b/spec/requests/api/graphql/project/repository_spec.rb index bd719a69647..a4984688557 100644 --- a/spec/requests/api/graphql/project/repository_spec.rb +++ b/spec/requests/api/graphql/project/repository_spec.rb @@ -11,6 +11,7 @@ RSpec.describe 'getting a repository in a project' do #{all_graphql_fields_for('repository'.classify)} QUERY end + let(:query) do graphql_query_for( 'project', diff --git a/spec/requests/api/graphql/project/tree/tree_spec.rb b/spec/requests/api/graphql/project/tree/tree_spec.rb index bce63d57c38..f4cd316da96 100644 --- a/spec/requests/api/graphql/project/tree/tree_spec.rb +++ b/spec/requests/api/graphql/project/tree/tree_spec.rb @@ -15,6 +15,7 @@ RSpec.describe 'getting a tree in a project' do } QUERY end + let(:query) do graphql_query_for( 'project', diff --git a/spec/requests/api/graphql/project_query_spec.rb b/spec/requests/api/graphql/project_query_spec.rb index b115030afbc..c6049e098be 100644 --- a/spec/requests/api/graphql/project_query_spec.rb +++ b/spec/requests/api/graphql/project_query_spec.rb @@ -76,16 +76,16 @@ RSpec.describe 'getting project information' do def run_query(number) q = <<~GQL - query { - project(fullPath: "#{project.full_path}") { - mergeRequests(first: #{number}) { - nodes { - assignees { nodes { username } } - headPipeline { status } + query { + project(fullPath: "#{project.full_path}") { + mergeRequests(first: #{number}) { + nodes { + assignees { nodes { username } } + headPipeline { status } + } } } } - } GQL post_graphql(q, current_user: current_user) diff --git a/spec/requests/api/graphql/user_query_spec.rb b/spec/requests/api/graphql/user_query_spec.rb index 7ba1788a9ef..2f4dc0a9160 100644 --- a/spec/requests/api/graphql/user_query_spec.rb +++ b/spec/requests/api/graphql/user_query_spec.rb @@ -75,7 +75,9 @@ RSpec.describe 'getting user information' do 'name' => presenter.name, 'username' => presenter.username, 'webUrl' => presenter.web_url, - 'avatarUrl' => presenter.avatar_url + 'avatarUrl' => presenter.avatar_url, + 'status' => presenter.status, + 'email' => presenter.email )) end @@ -83,6 +85,7 @@ RSpec.describe 'getting user information' do let(:user_fields) do query_graphql_field(:assigned_merge_requests, mr_args, 'nodes { id }') end + let(:mr_args) { nil } it_behaves_like 'a working graphql query' @@ -145,6 +148,7 @@ RSpec.describe 'getting user information' do let(:user_fields) do query_graphql_field(:authored_merge_requests, mr_args, 'nodes { id }') end + let(:mr_args) { nil } it_behaves_like 'a working graphql query' diff --git a/spec/requests/api/group_import_spec.rb b/spec/requests/api/group_import_spec.rb index ad67f737725..cb63206fcb8 100644 --- a/spec/requests/api/group_import_spec.rb +++ b/spec/requests/api/group_import_spec.rb @@ -217,12 +217,14 @@ RSpec.describe API::GroupImport do let!(:fog_connection) do stub_uploads_object_storage(ImportExportUploader, direct_upload: true) end + let(:tmp_object) do fog_connection.directories.new(key: 'uploads').files.create( key: "tmp/uploads/#{file_name}", body: file_upload ) end + let(:fog_file) { fog_to_uploaded_file(tmp_object) } let(:params) do { diff --git a/spec/requests/api/group_milestones_spec.rb b/spec/requests/api/group_milestones_spec.rb index 2b361f2b503..7ed6e1a295f 100644 --- a/spec/requests/api/group_milestones_spec.rb +++ b/spec/requests/api/group_milestones_spec.rb @@ -3,15 +3,65 @@ require 'spec_helper' RSpec.describe API::GroupMilestones do - let(:user) { create(:user) } - let(:group) { create(:group, :private) } - let(:project) { create(:project, namespace: group) } - let!(:group_member) { create(:group_member, group: group, user: user) } - let!(:closed_milestone) { create(:closed_milestone, group: group, title: 'version1', description: 'closed milestone') } - let!(:milestone) { create(:milestone, group: group, title: 'version2', description: 'open milestone') } - - it_behaves_like 'group and project milestones', "/groups/:id/milestones" do - let(:route) { "/groups/#{group.id}/milestones" } + let_it_be(:user) { create(:user) } + let_it_be(:group) { create(:group, :private) } + let_it_be(:project) { create(:project, namespace: group) } + let_it_be(:group_member) { create(:group_member, group: group, user: user) } + let_it_be(:closed_milestone) { create(:closed_milestone, group: group, title: 'version1', description: 'closed milestone') } + let_it_be(:milestone) { create(:milestone, group: group, title: 'version2', description: 'open milestone') } + let(:route) { "/groups/#{group.id}/milestones" } + + it_behaves_like 'group and project milestones', "/groups/:id/milestones" + + describe 'GET /groups/:id/milestones' do + context 'when include_parent_milestones is true' do + let_it_be(:ancestor_group) { create(:group, :private) } + let_it_be(:ancestor_group_milestone) { create(:milestone, group: ancestor_group) } + let_it_be(:params) { { include_parent_milestones: true } } + + before_all do + group.update(parent: ancestor_group) + end + + shared_examples 'listing all milestones' do + it 'returns correct list of milestones' do + get api(route, user), params: params + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response.size).to eq(milestones.size) + expect(json_response.map { |entry| entry["id"] }).to eq(milestones.map(&:id)) + end + end + + context 'when user has access to ancestor groups' do + let(:milestones) { [ancestor_group_milestone, milestone, closed_milestone] } + + before do + ancestor_group.add_guest(user) + group.add_guest(user) + end + + it_behaves_like 'listing all milestones' + + context 'when iids param is present' do + let_it_be(:params) { { include_parent_milestones: true, iids: [milestone.iid] } } + + it_behaves_like 'listing all milestones' + end + end + + context 'when user has no access to ancestor groups' do + let(:user) { create(:user) } + + before do + group.add_guest(user) + end + + it_behaves_like 'listing all milestones' do + let(:milestones) { [milestone, closed_milestone] } + end + end + end end def setup_for_group diff --git a/spec/requests/api/group_packages_spec.rb b/spec/requests/api/group_packages_spec.rb index 7c7e8da3fb1..e02f6099637 100644 --- a/spec/requests/api/group_packages_spec.rb +++ b/spec/requests/api/group_packages_spec.rb @@ -13,135 +13,133 @@ RSpec.describe API::GroupPackages do let(:url) { "/groups/#{group.id}/packages" } let(:package_schema) { 'public_api/v4/packages/group_packages' } - context 'without the need for a license' do - context 'with sorting' do - let_it_be(:package1) { create(:npm_package, project: project, version: '3.1.0', name: "@#{project.root_namespace.path}/foo1") } - let_it_be(:package2) { create(:nuget_package, project: project, version: '2.0.4') } - let(:package3) { create(:maven_package, project: project, version: '1.1.1', name: 'zzz') } - - before do - travel_to(1.day.ago) do - package3 - end + context 'with sorting' do + let_it_be(:package1) { create(:npm_package, project: project, version: '3.1.0', name: "@#{project.root_namespace.path}/foo1") } + let_it_be(:package2) { create(:nuget_package, project: project, version: '2.0.4') } + let(:package3) { create(:maven_package, project: project, version: '1.1.1', name: 'zzz') } + + before do + travel_to(1.day.ago) do + package3 end + end - context 'without sorting params' do - let(:packages) { [package3, package1, package2] } + context 'without sorting params' do + let(:packages) { [package3, package1, package2] } - it 'sorts by created_at asc' do - subject + it 'sorts by created_at asc' do + subject - expect(json_response.map { |package| package['id'] }).to eq(packages.map(&:id)) - end + expect(json_response.map { |package| package['id'] }).to eq(packages.map(&:id)) end + end - it_behaves_like 'package sorting', 'name' do - let(:packages) { [package1, package2, package3] } - end + it_behaves_like 'package sorting', 'name' do + let(:packages) { [package1, package2, package3] } + end - it_behaves_like 'package sorting', 'created_at' do - let(:packages) { [package3, package1, package2] } - end + it_behaves_like 'package sorting', 'created_at' do + let(:packages) { [package3, package1, package2] } + end - it_behaves_like 'package sorting', 'version' do - let(:packages) { [package3, package2, package1] } - end + it_behaves_like 'package sorting', 'version' do + let(:packages) { [package3, package2, package1] } + end - it_behaves_like 'package sorting', 'type' do - let(:packages) { [package3, package1, package2] } - end + it_behaves_like 'package sorting', 'type' do + let(:packages) { [package3, package1, package2] } + end - it_behaves_like 'package sorting', 'project_path' do - let(:another_project) { create(:project, :public, namespace: group, name: 'project B') } - let!(:package4) { create(:npm_package, project: another_project, version: '3.1.0', name: "@#{project.root_namespace.path}/bar") } + it_behaves_like 'package sorting', 'project_path' do + let(:another_project) { create(:project, :public, namespace: group, name: 'project B') } + let!(:package4) { create(:npm_package, project: another_project, version: '3.1.0', name: "@#{project.root_namespace.path}/bar") } - let(:packages) { [package1, package2, package3, package4] } - end + let(:packages) { [package1, package2, package3, package4] } end + end - context 'with private group' do - let!(:package1) { create(:package, project: project) } - let!(:package2) { create(:package, project: project) } + context 'with private group' do + let!(:package1) { create(:package, project: project) } + let!(:package2) { create(:package, project: project) } - let(:group) { create(:group, :private) } - let(:subgroup) { create(:group, :private, parent: group) } - let(:project) { create(:project, :private, namespace: group) } - let(:subproject) { create(:project, :private, namespace: subgroup) } + let(:group) { create(:group, :private) } + let(:subgroup) { create(:group, :private, parent: group) } + let(:project) { create(:project, :private, namespace: group) } + let(:subproject) { create(:project, :private, namespace: subgroup) } - context 'with unauthenticated user' do - it_behaves_like 'rejects packages access', :group, :no_type, :not_found - end + context 'with unauthenticated user' do + it_behaves_like 'rejects packages access', :group, :no_type, :not_found + end + + context 'with authenticated user' do + subject { get api(url, user) } + + it_behaves_like 'returns packages', :group, :owner + it_behaves_like 'returns packages', :group, :maintainer + it_behaves_like 'returns packages', :group, :developer + it_behaves_like 'rejects packages access', :group, :reporter, :forbidden + it_behaves_like 'rejects packages access', :group, :guest, :forbidden - context 'with authenticated user' do - subject { get api(url, user) } + context 'with subgroup' do + let(:subgroup) { create(:group, :private, parent: group) } + let(:subproject) { create(:project, :private, namespace: subgroup) } + let!(:package3) { create(:npm_package, project: subproject) } - it_behaves_like 'returns packages', :group, :owner - it_behaves_like 'returns packages', :group, :maintainer - it_behaves_like 'returns packages', :group, :developer + it_behaves_like 'returns packages with subgroups', :group, :owner + it_behaves_like 'returns packages with subgroups', :group, :maintainer + it_behaves_like 'returns packages with subgroups', :group, :developer it_behaves_like 'rejects packages access', :group, :reporter, :forbidden it_behaves_like 'rejects packages access', :group, :guest, :forbidden - context 'with subgroup' do - let(:subgroup) { create(:group, :private, parent: group) } - let(:subproject) { create(:project, :private, namespace: subgroup) } - let!(:package3) { create(:npm_package, project: subproject) } + context 'excluding subgroup' do + let(:url) { "/groups/#{group.id}/packages?exclude_subgroups=true" } - it_behaves_like 'returns packages with subgroups', :group, :owner - it_behaves_like 'returns packages with subgroups', :group, :maintainer - it_behaves_like 'returns packages with subgroups', :group, :developer + it_behaves_like 'returns packages', :group, :owner + it_behaves_like 'returns packages', :group, :maintainer + it_behaves_like 'returns packages', :group, :developer it_behaves_like 'rejects packages access', :group, :reporter, :forbidden it_behaves_like 'rejects packages access', :group, :guest, :forbidden - - context 'excluding subgroup' do - let(:url) { "/groups/#{group.id}/packages?exclude_subgroups=true" } - - it_behaves_like 'returns packages', :group, :owner - it_behaves_like 'returns packages', :group, :maintainer - it_behaves_like 'returns packages', :group, :developer - it_behaves_like 'rejects packages access', :group, :reporter, :forbidden - it_behaves_like 'rejects packages access', :group, :guest, :forbidden - end end end end + end - context 'with public group' do - let_it_be(:package1) { create(:package, project: project) } - let_it_be(:package2) { create(:package, project: project) } + context 'with public group' do + let_it_be(:package1) { create(:package, project: project) } + let_it_be(:package2) { create(:package, project: project) } - context 'with unauthenticated user' do - it_behaves_like 'returns packages', :group, :no_type - end + context 'with unauthenticated user' do + it_behaves_like 'returns packages', :group, :no_type + end - context 'with authenticated user' do - subject { get api(url, user) } + context 'with authenticated user' do + subject { get api(url, user) } - it_behaves_like 'returns packages', :group, :owner - it_behaves_like 'returns packages', :group, :maintainer - it_behaves_like 'returns packages', :group, :developer - it_behaves_like 'returns packages', :group, :reporter - it_behaves_like 'returns packages', :group, :guest - end + it_behaves_like 'returns packages', :group, :owner + it_behaves_like 'returns packages', :group, :maintainer + it_behaves_like 'returns packages', :group, :developer + it_behaves_like 'returns packages', :group, :reporter + it_behaves_like 'returns packages', :group, :guest end + end - context 'with pagination params' do - let_it_be(:package1) { create(:package, project: project) } - let_it_be(:package2) { create(:package, project: project) } - let_it_be(:package3) { create(:npm_package, project: project) } - let_it_be(:package4) { create(:npm_package, project: project) } + context 'with pagination params' do + let_it_be(:package1) { create(:package, project: project) } + let_it_be(:package2) { create(:package, project: project) } + let_it_be(:package3) { create(:npm_package, project: project) } + let_it_be(:package4) { create(:npm_package, project: project) } - it_behaves_like 'returns paginated packages' - end + it_behaves_like 'returns paginated packages' + end - it_behaves_like 'filters on each package_type', is_project: false + it_behaves_like 'filters on each package_type', is_project: false - context 'does not accept non supported package_type value' do - include_context 'package filter context' + context 'does not accept non supported package_type value' do + include_context 'package filter context' - let(:url) { group_filter_url(:type, 'foo') } + let(:url) { group_filter_url(:type, 'foo') } - it_behaves_like 'returning response status', :bad_request - end + it_behaves_like 'returning response status', :bad_request end end end diff --git a/spec/requests/api/group_variables_spec.rb b/spec/requests/api/group_variables_spec.rb index c6d6ae1615b..41b013f49ee 100644 --- a/spec/requests/api/group_variables_spec.rb +++ b/spec/requests/api/group_variables_spec.rb @@ -169,6 +169,14 @@ RSpec.describe API::GroupVariables do expect(response).to have_gitlab_http_status(:not_found) end + + it 'responds with 400 if the update fails' do + put api("/groups/#{group.id}/variables/#{variable.key}", user), params: { value: 'shrt', masked: true } + + expect(response).to have_gitlab_http_status(:bad_request) + expect(variable.reload.masked).to eq(false) + expect(json_response['message']).to eq('value' => ['is invalid']) + end end context 'authorized user with invalid permissions' do diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb index fac9f4dfe00..da423e986c3 100644 --- a/spec/requests/api/groups_spec.rb +++ b/spec/requests/api/groups_spec.rb @@ -860,6 +860,66 @@ RSpec.describe API::Groups do end end + context 'with similarity ordering' do + let_it_be(:group_with_projects) { create(:group) } + let_it_be(:project_1) { create(:project, name: 'Project', path: 'project', group: group_with_projects) } + let_it_be(:project_2) { create(:project, name: 'Test Project', path: 'test-project', group: group_with_projects) } + let_it_be(:project_3) { create(:project, name: 'Test', path: 'test', group: group_with_projects) } + + let(:params) { { order_by: 'similarity', search: 'test' } } + + subject { get api("/groups/#{group_with_projects.id}/projects", user1), params: params } + + before do + group_with_projects.add_owner(user1) + end + + it 'returns items based ordered by similarity' do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to include_pagination_headers + expect(json_response.length).to eq(2) + + project_names = json_response.map { |proj| proj['name'] } + expect(project_names).to eq(['Test', 'Test Project']) + end + + context 'when `search` parameter is not given' do + before do + params.delete(:search) + end + + it 'returns items ordered by name' do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to include_pagination_headers + expect(json_response.length).to eq(3) + + project_names = json_response.map { |proj| proj['name'] } + expect(project_names).to eq(['Project', 'Test', 'Test Project']) + end + end + + context 'when `similarity_search` feature flag is off' do + before do + stub_feature_flags(similarity_search: false) + end + + it 'returns items ordered by name' do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to include_pagination_headers + expect(json_response.length).to eq(2) + + project_names = json_response.map { |proj| proj['name'] } + expect(project_names).to eq(['Test', 'Test Project']) + end + end + end + it "returns the group's projects with simple representation" do get api("/groups/#{group1.id}/projects", user1), params: { simple: true } @@ -1012,6 +1072,7 @@ RSpec.describe API::Groups do let!(:project4) do create(:project, namespace: group2, path: 'test_project', visibility_level: Gitlab::VisibilityLevel::PRIVATE) end + let(:path) { "/groups/#{group1.id}/projects/shared" } before do diff --git a/spec/requests/api/helpers_spec.rb b/spec/requests/api/helpers_spec.rb index 12cd5ace84e..fefa7105327 100644 --- a/spec/requests/api/helpers_spec.rb +++ b/spec/requests/api/helpers_spec.rb @@ -24,6 +24,7 @@ RSpec.describe API::Helpers do 'CONTENT_TYPE' => 'text/plain;charset=utf-8' } end + let(:header) { } let(:request) { Grape::Request.new(env)} let(:params) { request.params } diff --git a/spec/requests/api/import_bitbucket_server_spec.rb b/spec/requests/api/import_bitbucket_server_spec.rb index 5828dab3080..dac139064da 100644 --- a/spec/requests/api/import_bitbucket_server_spec.rb +++ b/spec/requests/api/import_bitbucket_server_spec.rb @@ -96,7 +96,7 @@ RSpec.describe API::ImportBitbucketServer do Grape::Endpoint.before_each nil end - it 'returns 400 response due to a blcoked URL' do + it 'returns 400 response due to a blocked URL' do allow(Gitlab::BitbucketServerImport::ProjectCreator) .to receive(:new).with(project_key, repo_slug, anything, project.name, user.namespace, user, anything) .and_return(double(execute: project)) diff --git a/spec/requests/api/import_github_spec.rb b/spec/requests/api/import_github_spec.rb index f026314f7a8..bbfb17fe753 100644 --- a/spec/requests/api/import_github_spec.rb +++ b/spec/requests/api/import_github_spec.rb @@ -22,7 +22,7 @@ RSpec.describe API::ImportGithub do before do Grape::Endpoint.before_each do |endpoint| - allow(endpoint).to receive(:client).and_return(double('client', user: provider_user, repo: provider_repo).as_null_object) + allow(endpoint).to receive(:client).and_return(double('client', user: provider_user, repository: provider_repo).as_null_object) end end diff --git a/spec/requests/api/internal/base_spec.rb b/spec/requests/api/internal/base_spec.rb index 7d219954e9d..873189af397 100644 --- a/spec/requests/api/internal/base_spec.rb +++ b/spec/requests/api/internal/base_spec.rb @@ -120,6 +120,138 @@ RSpec.describe API::Internal::Base do end end + describe 'POST /internal/personal_access_token' do + it 'returns an error message when the key does not exist' do + post api('/internal/personal_access_token'), + params: { + secret_token: secret_token, + key_id: non_existing_record_id + } + + expect(json_response['success']).to be_falsey + expect(json_response['message']).to eq('Could not find the given key') + end + + it 'returns an error message when the key is a deploy key' do + deploy_key = create(:deploy_key) + + post api('/internal/personal_access_token'), + params: { + secret_token: secret_token, + key_id: deploy_key.id + } + + expect(json_response['success']).to be_falsey + expect(json_response['message']).to eq('Deploy keys cannot be used to create personal access tokens') + end + + it 'returns an error message when the user does not exist' do + key_without_user = create(:key, user: nil) + + post api('/internal/personal_access_token'), + params: { + secret_token: secret_token, + key_id: key_without_user.id + } + + expect(json_response['success']).to be_falsey + expect(json_response['message']).to eq('Could not find a user for the given key') + expect(json_response['token']).to be_nil + end + + it 'returns an error message when given an non existent user' do + post api('/internal/personal_access_token'), + params: { + secret_token: secret_token, + user_id: 0 + } + + expect(json_response['success']).to be_falsey + expect(json_response['message']).to eq("Could not find the given user") + end + + it 'returns an error message when no name parameter is received' do + post api('/internal/personal_access_token'), + params: { + secret_token: secret_token, + key_id: key.id + } + + expect(json_response['success']).to be_falsey + expect(json_response['message']).to eq("No token name specified") + end + + it 'returns an error message when no scopes parameter is received' do + post api('/internal/personal_access_token'), + params: { + secret_token: secret_token, + key_id: key.id, + name: 'newtoken' + } + + expect(json_response['success']).to be_falsey + expect(json_response['message']).to eq("No token scopes specified") + end + + it 'returns an error message when expires_at contains an invalid date' do + post api('/internal/personal_access_token'), + params: { + secret_token: secret_token, + key_id: key.id, + name: 'newtoken', + scopes: ['api'], + expires_at: 'invalid-date' + } + + expect(json_response['success']).to be_falsey + expect(json_response['message']).to eq("Invalid token expiry date: 'invalid-date'") + end + + it 'returns an error message when it receives an invalid scope' do + post api('/internal/personal_access_token'), + params: { + secret_token: secret_token, + key_id: key.id, + name: 'newtoken', + scopes: %w(read_api badscope read_repository) + } + + expect(json_response['success']).to be_falsey + expect(json_response['message']).to match(/\AInvalid scope: 'badscope'. Valid scopes are: /) + end + + it 'returns a token without expiry when the expires_at parameter is missing' do + post api('/internal/personal_access_token'), + params: { + secret_token: secret_token, + key_id: key.id, + name: 'newtoken', + scopes: %w(read_api read_repository) + } + + expect(json_response['success']).to be_truthy + expect(json_response['token']).to match(/\A\S{20}\z/) + expect(json_response['scopes']).to match_array(%w(read_api read_repository)) + expect(json_response['expires_at']).to be_nil + end + + it 'returns a token with expiry when it receives a valid expires_at parameter' do + post api('/internal/personal_access_token'), + params: { + secret_token: secret_token, + key_id: key.id, + name: 'newtoken', + scopes: %w(read_api read_repository), + expires_at: '9001-11-17' + } + + expect(json_response['success']).to be_truthy + expect(json_response['token']).to match(/\A\S{20}\z/) + expect(json_response['scopes']).to match_array(%w(read_api read_repository)) + expect(json_response['expires_at']).to eq('9001-11-17') + end + end + describe "POST /internal/lfs_authenticate" do before do project.add_developer(user) @@ -321,6 +453,8 @@ RSpec.describe API::Internal::Base do expect(json_response["status"]).to be_truthy expect(json_response["gl_project_path"]).to eq(project.wiki.full_path) expect(json_response["gl_repository"]).to eq("wiki-#{project.id}") + expect(json_response["gl_key_type"]).to eq("key") + expect(json_response["gl_key_id"]).to eq(key.id) expect(user.reload.last_activity_on).to be_nil end @@ -444,6 +578,8 @@ RSpec.describe API::Internal::Base do expect(json_response["status"]).to be_truthy expect(json_response["gl_repository"]).to eq("project-#{project.id}") expect(json_response["gl_project_path"]).to eq(project.full_path) + expect(json_response["gl_key_type"]).to eq("key") + expect(json_response["gl_key_id"]).to eq(key.id) expect(json_response["gitaly"]).not_to be_nil expect(json_response["gitaly"]["repository"]).not_to be_nil expect(json_response["gitaly"]["repository"]["storage_name"]).to eq(project.repository.gitaly_repository.storage_name) @@ -547,6 +683,7 @@ RSpec.describe API::Internal::Base do } } end + let(:console_messages) { ['informational message'] } let(:custom_action_result) { Gitlab::GitAccessResult::CustomAction.new(payload, console_messages) } @@ -706,6 +843,8 @@ RSpec.describe API::Internal::Base do expect(response).to have_gitlab_http_status(:ok) expect(json_response["status"]).to be_truthy expect(json_response["gitaly"]).not_to be_nil + expect(json_response["gl_key_type"]).to eq("deploy_key") + expect(json_response["gl_key_id"]).to eq(key.id) expect(json_response["gitaly"]["repository"]).not_to be_nil expect(json_response["gitaly"]["repository"]["storage_name"]).to eq(project.repository.gitaly_repository.storage_name) expect(json_response["gitaly"]["repository"]["relative_path"]).to eq(project.repository.gitaly_repository.relative_path) diff --git a/spec/requests/api/internal/kubernetes_spec.rb b/spec/requests/api/internal/kubernetes_spec.rb new file mode 100644 index 00000000000..555ca441fe7 --- /dev/null +++ b/spec/requests/api/internal/kubernetes_spec.rb @@ -0,0 +1,154 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe API::Internal::Kubernetes do + describe "GET /internal/kubernetes/agent_info" do + context 'kubernetes_agent_internal_api feature flag disabled' do + before do + stub_feature_flags(kubernetes_agent_internal_api: false) + end + + it 'returns 404' do + get api('/internal/kubernetes/agent_info') + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + it 'returns 403 if Authorization header not sent' do + get api('/internal/kubernetes/agent_info') + + expect(response).to have_gitlab_http_status(:forbidden) + end + + context 'an agent is found' do + let!(:agent_token) { create(:cluster_agent_token) } + + let(:agent) { agent_token.agent } + let(:project) { agent.project } + + it 'returns expected data', :aggregate_failures do + get api('/internal/kubernetes/agent_info'), headers: { 'Authorization' => "Bearer #{agent_token.token}" } + + expect(response).to have_gitlab_http_status(:success) + + expect(json_response).to match( + a_hash_including( + 'project_id' => project.id, + 'agent_id' => agent.id, + 'agent_name' => agent.name, + 'gitaly_info' => a_hash_including( + 'address' => match(/\.socket$/), + 'token' => 'secret', + 'features' => {} + ), + 'gitaly_repository' => a_hash_including( + 'storage_name' => project.repository_storage, + 'relative_path' => project.disk_path + '.git', + 'gl_repository' => "project-#{project.id}", + 'gl_project_path' => project.full_path + ) + ) + ) + end + end + + context 'no such agent exists' do + it 'returns 404' do + get api('/internal/kubernetes/agent_info'), headers: { 'Authorization' => 'Bearer ABCD' } + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + end + + describe 'GET /internal/kubernetes/project_info' do + context 'kubernetes_agent_internal_api feature flag disabled' do + before do + stub_feature_flags(kubernetes_agent_internal_api: false) + end + + it 'returns 404' do + get api('/internal/kubernetes/project_info') + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + it 'returns 403 if Authorization header not sent' do + get api('/internal/kubernetes/project_info') + + expect(response).to have_gitlab_http_status(:forbidden) + end + + context 'no such agent exists' do + it 'returns 404' do + get api('/internal/kubernetes/project_info'), headers: { 'Authorization' => 'Bearer ABCD' } + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + + context 'an agent is found' do + let!(:agent_token) { create(:cluster_agent_token) } + + let(:agent) { agent_token.agent } + + context 'project is public' do + let(:project) { create(:project, :public) } + + it 'returns expected data', :aggregate_failures do + get api('/internal/kubernetes/project_info'), params: { id: project.id }, headers: { 'Authorization' => "Bearer #{agent_token.token}" } + + expect(response).to have_gitlab_http_status(:success) + + expect(json_response).to match( + a_hash_including( + 'project_id' => project.id, + 'gitaly_info' => a_hash_including( + 'address' => match(/\.socket$/), + 'token' => 'secret', + 'features' => {} + ), + 'gitaly_repository' => a_hash_including( + 'storage_name' => project.repository_storage, + 'relative_path' => project.disk_path + '.git', + 'gl_repository' => "project-#{project.id}", + 'gl_project_path' => project.full_path + ) + ) + ) + end + end + + context 'project is private' do + let(:project) { create(:project, :private) } + + it 'returns 404' do + get api('/internal/kubernetes/project_info'), params: { id: project.id }, headers: { 'Authorization' => "Bearer #{agent_token.token}" } + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'project is internal' do + let(:project) { create(:project, :internal) } + + it 'returns 404' do + get api('/internal/kubernetes/project_info'), params: { id: project.id }, headers: { 'Authorization' => "Bearer #{agent_token.token}" } + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'project does not exist' do + it 'returns 404' do + get api('/internal/kubernetes/project_info'), params: { id: 0 }, headers: { 'Authorization' => "Bearer #{agent_token.token}" } + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end + end +end diff --git a/spec/requests/api/internal/pages_spec.rb b/spec/requests/api/internal/pages_spec.rb index 48fc95b6574..e58eba02132 100644 --- a/spec/requests/api/internal/pages_spec.rb +++ b/spec/requests/api/internal/pages_spec.rb @@ -7,6 +7,7 @@ RSpec.describe API::Internal::Pages do jwt_token = JWT.encode({ 'iss' => 'gitlab-pages' }, Gitlab::Pages.secret, 'HS256') { Gitlab::Pages::INTERNAL_API_REQUEST_HEADER => jwt_token } end + let(:pages_secret) { SecureRandom.random_bytes(Gitlab::Pages::SECRET_LENGTH) } before do diff --git a/spec/requests/api/issues/get_group_issues_spec.rb b/spec/requests/api/issues/get_group_issues_spec.rb index b53fac3679d..b0fbf3bf66d 100644 --- a/spec/requests/api/issues/get_group_issues_spec.rb +++ b/spec/requests/api/issues/get_group_issues_spec.rb @@ -36,6 +36,7 @@ RSpec.describe API::Issues do updated_at: 3.hours.ago, created_at: 1.day.ago end + let!(:group_confidential_issue) do create :issue, :confidential, @@ -45,6 +46,7 @@ RSpec.describe API::Issues do updated_at: 2.hours.ago, created_at: 2.days.ago end + let!(:group_issue) do create :issue, author: user, @@ -56,14 +58,17 @@ RSpec.describe API::Issues do description: issue_description, created_at: 5.days.ago end + let!(:group_label) do create(:label, title: 'group_lbl', color: '#FFAABB', project: group_project) end + let!(:group_label_link) { create(:label_link, label: group_label, target: group_issue) } let!(:group_milestone) { create(:milestone, title: '3.0.0', project: group_project) } let!(:group_empty_milestone) do create(:milestone, title: '4.0.0', project: group_project) end + let!(:group_note) { create(:note_on_issue, author: user, project: group_project, noteable: group_issue) } let(:base_url) { "/groups/#{group.id}/issues" } @@ -246,6 +251,7 @@ RSpec.describe API::Issues do target_project: private_mrs_project, description: "closes #{group_issue.to_reference(private_mrs_project)}") end + let!(:merge_request2) do create(:merge_request, :simple, diff --git a/spec/requests/api/issues/get_project_issues_spec.rb b/spec/requests/api/issues/get_project_issues_spec.rb index 7ff07bf580d..4228ca2d5fd 100644 --- a/spec/requests/api/issues/get_project_issues_spec.rb +++ b/spec/requests/api/issues/get_project_issues_spec.rb @@ -28,6 +28,7 @@ RSpec.describe API::Issues do updated_at: 3.hours.ago, closed_at: 1.hour.ago end + let!(:confidential_issue) do create :issue, :confidential, @@ -37,6 +38,7 @@ RSpec.describe API::Issues do created_at: generate(:past_time), updated_at: 2.hours.ago end + let!(:issue) do create :issue, author: user, @@ -48,6 +50,7 @@ RSpec.describe API::Issues do title: issue_title, description: issue_description end + let_it_be(:label) do create(:label, title: 'label', color: '#FFAABB', project: project) end @@ -69,6 +72,7 @@ RSpec.describe API::Issues do target_project: project, description: "closes #{issue.to_reference}") end + let!(:merge_request2) do create(:merge_request, :simple, @@ -180,12 +184,15 @@ RSpec.describe API::Issues do it 'avoids N+1 queries' do get api("/projects/#{project.id}/issues", user) - create_list(:issue, 3, project: project) + create_list(:issue, 3, project: project, closed_by: user) control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do get api("/projects/#{project.id}/issues", user) end.count + milestone = create(:milestone, project: project) + create(:issue, project: project, milestone: milestone, closed_by: create(:user)) + expect do get api("/projects/#{project.id}/issues", user) end.not_to exceed_all_query_limit(control_count) diff --git a/spec/requests/api/issues/issues_spec.rb b/spec/requests/api/issues/issues_spec.rb index 519bea22501..b638a65d65e 100644 --- a/spec/requests/api/issues/issues_spec.rb +++ b/spec/requests/api/issues/issues_spec.rb @@ -28,6 +28,7 @@ RSpec.describe API::Issues do updated_at: 3.hours.ago, closed_at: 1.hour.ago end + let!(:confidential_issue) do create :issue, :confidential, @@ -37,6 +38,7 @@ RSpec.describe API::Issues do created_at: generate(:past_time), updated_at: 2.hours.ago end + let!(:issue) do create :issue, author: user, @@ -48,6 +50,7 @@ RSpec.describe API::Issues do title: issue_title, description: issue_description end + let_it_be(:label) do create(:label, title: 'label', color: '#FFAABB', project: project) end @@ -384,6 +387,60 @@ RSpec.describe API::Issues do end end + context 'filtering by due date' do + # This date chosen because it is the beginning of a week + near the beginning of a month + let_it_be(:frozen_time) { DateTime.parse('2020-08-03 12:00') } + + let_it_be(:issue2) { create(:issue, project: project, author: user, due_date: frozen_time + 3.days) } + let_it_be(:issue3) { create(:issue, project: project, author: user, due_date: frozen_time + 10.days) } + let_it_be(:issue4) { create(:issue, project: project, author: user, due_date: frozen_time + 34.days) } + let_it_be(:issue5) { create(:issue, project: project, author: user, due_date: frozen_time - 8.days) } + + before do + travel_to(frozen_time) + end + + after do + travel_back + end + + it 'returns them all when argument is empty' do + get api('/issues?due_date=', user) + + expect_paginated_array_response(issue5.id, issue4.id, issue3.id, issue2.id, issue.id, closed_issue.id) + end + + it 'returns issues without due date' do + get api('/issues?due_date=0', user) + + expect_paginated_array_response(issue.id, closed_issue.id) + end + + it 'returns issues due for this week' do + get api('/issues?due_date=week', user) + + expect_paginated_array_response(issue2.id) + end + + it 'returns issues due for this month' do + get api('/issues?due_date=month', user) + + expect_paginated_array_response(issue3.id, issue2.id) + end + + it 'returns issues that are due previous two weeks and next month' do + get api('/issues?due_date=next_month_and_previous_two_weeks', user) + + expect_paginated_array_response(issue5.id, issue4.id, issue3.id, issue2.id) + end + + it 'returns issues that are overdue' do + get api('/issues?due_date=overdue', user) + + expect_paginated_array_response(issue5.id) + end + end + context 'filter by labels or label_name param' do context 'N+1' do let(:label_b) { create(:label, title: 'foo', project: project) } @@ -807,6 +864,7 @@ RSpec.describe API::Issues do target_project: private_mrs_project, description: "closes #{issue.to_reference(private_mrs_project)}") end + let!(:merge_request2) do create(:merge_request, :simple, diff --git a/spec/requests/api/issues/post_projects_issues_spec.rb b/spec/requests/api/issues/post_projects_issues_spec.rb index e2f1bb2cd1a..a7fe4d4509a 100644 --- a/spec/requests/api/issues/post_projects_issues_spec.rb +++ b/spec/requests/api/issues/post_projects_issues_spec.rb @@ -27,6 +27,7 @@ RSpec.describe API::Issues do updated_at: 3.hours.ago, closed_at: 1.hour.ago end + let!(:confidential_issue) do create :issue, :confidential, @@ -36,6 +37,7 @@ RSpec.describe API::Issues do created_at: generate(:past_time), updated_at: 2.hours.ago end + let!(:issue) do create :issue, author: user, @@ -47,6 +49,7 @@ RSpec.describe API::Issues do title: issue_title, description: issue_description end + let_it_be(:label) do create(:label, title: 'label', color: '#FFAABB', project: project) end diff --git a/spec/requests/api/jobs_spec.rb b/spec/requests/api/jobs_spec.rb index 53c57931d36..77d5a4f26a8 100644 --- a/spec/requests/api/jobs_spec.rb +++ b/spec/requests/api/jobs_spec.rb @@ -239,6 +239,18 @@ RSpec.describe API::Jobs do end end + context 'when config source not ci' do + let(:non_ci_config_source) { ::Ci::PipelineEnums.non_ci_config_source_values.first } + let(:pipeline) do + create(:ci_pipeline, config_source: non_ci_config_source, project: project) + end + + it 'returns the specified pipeline' do + expect(response).to have_gitlab_http_status(:ok) + expect(json_response[0]['pipeline']['sha']).to eq(pipeline.sha.to_s) + end + end + it 'avoids N+1 queries' do control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do get api("/projects/#{project.id}/pipelines/#{pipeline.id}/jobs", api_user), params: query diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index 68f1a0f1ba1..d4c05b4b198 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -1551,25 +1551,33 @@ RSpec.describe API::MergeRequests do it "returns 422 when source_branch equals target_branch" do post api("/projects/#{project.id}/merge_requests", user), params: { title: "Test merge_request", source_branch: "master", target_branch: "master", author: user } + expect(response).to have_gitlab_http_status(:unprocessable_entity) + expect(json_response['message']).to eq(["You can't use same project/branch for source and target"]) end it "returns 400 when source_branch is missing" do post api("/projects/#{project.id}/merge_requests", user), params: { title: "Test merge_request", target_branch: "master", author: user } + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['error']).to eq('source_branch is missing') end it "returns 400 when target_branch is missing" do post api("/projects/#{project.id}/merge_requests", user), params: { title: "Test merge_request", source_branch: "markdown", author: user } + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['error']).to eq('target_branch is missing') end it "returns 400 when title is missing" do post api("/projects/#{project.id}/merge_requests", user), params: { target_branch: 'master', source_branch: 'markdown' } + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['error']).to eq('title is missing') end context 'with existing MR' do @@ -1594,7 +1602,9 @@ RSpec.describe API::MergeRequests do author: user } end.to change { MergeRequest.count }.by(0) + expect(response).to have_gitlab_http_status(:conflict) + expect(json_response['message']).to eq(["Another open merge request already exists for this source branch: !5"]) end end diff --git a/spec/requests/api/notes_spec.rb b/spec/requests/api/notes_spec.rb index 1510d31a1a6..ca4ebd3689f 100644 --- a/spec/requests/api/notes_spec.rb +++ b/spec/requests/api/notes_spec.rb @@ -55,6 +55,7 @@ RSpec.describe API::Notes do create(:project, namespace: private_user.namespace) .tap { |p| p.add_maintainer(private_user) } end + let(:private_issue) { create(:issue, project: private_project) } let(:ext_proj) { create(:project, :public) } diff --git a/spec/requests/api/notification_settings_spec.rb b/spec/requests/api/notification_settings_spec.rb index 73cb4948524..7b4a58e63da 100644 --- a/spec/requests/api/notification_settings_spec.rb +++ b/spec/requests/api/notification_settings_spec.rb @@ -70,12 +70,13 @@ RSpec.describe API::NotificationSettings do describe "PUT /projects/:id/notification_settings" do it "updates project level notification settings for the current user" do - put api("/projects/#{project.id}/notification_settings", user), params: { level: 'custom', new_note: true } + put api("/projects/#{project.id}/notification_settings", user), params: { level: 'custom', new_note: true, moved_project: true } expect(response).to have_gitlab_http_status(:ok) expect(json_response['level']).to eq(user.reload.notification_settings_for(project).level) expect(json_response['events']['new_note']).to be_truthy expect(json_response['events']['new_issue']).to be_falsey + expect(json_response['events']['moved_project']).to be_truthy end end diff --git a/spec/requests/api/npm_packages_spec.rb b/spec/requests/api/npm_packages_spec.rb index 98a1ca978a8..94647123df0 100644 --- a/spec/requests/api/npm_packages_spec.rb +++ b/spec/requests/api/npm_packages_spec.rb @@ -4,6 +4,7 @@ require 'spec_helper' RSpec.describe API::NpmPackages do include PackagesManagerApiSpecHelpers + include HttpBasicAuthHelpers let_it_be(:user) { create(:user) } let_it_be(:group) { create(:group) } @@ -407,39 +408,37 @@ RSpec.describe API::NpmPackages do subject { get api(url) } - context 'without the need for a license' do - context 'with public project' do - context 'with authenticated user' do - subject { get api(url, personal_access_token: personal_access_token) } + context 'with public project' do + context 'with authenticated user' do + subject { get api(url, personal_access_token: personal_access_token) } - it_behaves_like 'returns package tags', :maintainer - it_behaves_like 'returns package tags', :developer - it_behaves_like 'returns package tags', :reporter - it_behaves_like 'returns package tags', :guest - end + it_behaves_like 'returns package tags', :maintainer + it_behaves_like 'returns package tags', :developer + it_behaves_like 'returns package tags', :reporter + it_behaves_like 'returns package tags', :guest + end - context 'with unauthenticated user' do - it_behaves_like 'returns package tags', :no_type - end + context 'with unauthenticated user' do + it_behaves_like 'returns package tags', :no_type end + end - context 'with private project' do - before do - project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE) - end + context 'with private project' do + before do + project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE) + end - context 'with authenticated user' do - subject { get api(url, personal_access_token: personal_access_token) } + context 'with authenticated user' do + subject { get api(url, personal_access_token: personal_access_token) } - it_behaves_like 'returns package tags', :maintainer - it_behaves_like 'returns package tags', :developer - it_behaves_like 'returns package tags', :reporter - it_behaves_like 'rejects package tags access', :guest, :forbidden - end + it_behaves_like 'returns package tags', :maintainer + it_behaves_like 'returns package tags', :developer + it_behaves_like 'returns package tags', :reporter + it_behaves_like 'rejects package tags access', :guest, :forbidden + end - context 'with unauthenticated user' do - it_behaves_like 'rejects package tags access', :no_type, :forbidden - end + context 'with unauthenticated user' do + it_behaves_like 'rejects package tags access', :no_type, :forbidden end end end @@ -453,39 +452,37 @@ RSpec.describe API::NpmPackages do subject { put api(url), env: { 'api.request.body': version } } - context 'without the need for a license' do - context 'with public project' do - context 'with authenticated user' do - subject { put api(url, personal_access_token: personal_access_token), env: { 'api.request.body': version } } + context 'with public project' do + context 'with authenticated user' do + subject { put api(url, personal_access_token: personal_access_token), env: { 'api.request.body': version } } - it_behaves_like 'create package tag', :maintainer - it_behaves_like 'create package tag', :developer - it_behaves_like 'rejects package tags access', :reporter, :forbidden - it_behaves_like 'rejects package tags access', :guest, :forbidden - end + it_behaves_like 'create package tag', :maintainer + it_behaves_like 'create package tag', :developer + it_behaves_like 'rejects package tags access', :reporter, :forbidden + it_behaves_like 'rejects package tags access', :guest, :forbidden + end - context 'with unauthenticated user' do - it_behaves_like 'rejects package tags access', :no_type, :unauthorized - end + context 'with unauthenticated user' do + it_behaves_like 'rejects package tags access', :no_type, :unauthorized end + end - context 'with private project' do - before do - project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE) - end + context 'with private project' do + before do + project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE) + end - context 'with authenticated user' do - subject { put api(url, personal_access_token: personal_access_token), env: { 'api.request.body': version } } + context 'with authenticated user' do + subject { put api(url, personal_access_token: personal_access_token), env: { 'api.request.body': version } } - it_behaves_like 'create package tag', :maintainer - it_behaves_like 'create package tag', :developer - it_behaves_like 'rejects package tags access', :reporter, :forbidden - it_behaves_like 'rejects package tags access', :guest, :forbidden - end + it_behaves_like 'create package tag', :maintainer + it_behaves_like 'create package tag', :developer + it_behaves_like 'rejects package tags access', :reporter, :forbidden + it_behaves_like 'rejects package tags access', :guest, :forbidden + end - context 'with unauthenticated user' do - it_behaves_like 'rejects package tags access', :no_type, :unauthorized - end + context 'with unauthenticated user' do + it_behaves_like 'rejects package tags access', :no_type, :unauthorized end end end @@ -499,39 +496,37 @@ RSpec.describe API::NpmPackages do subject { delete api(url) } - context 'without the need for a license' do - context 'with public project' do - context 'with authenticated user' do - subject { delete api(url, personal_access_token: personal_access_token) } + context 'with public project' do + context 'with authenticated user' do + subject { delete api(url, personal_access_token: personal_access_token) } - it_behaves_like 'delete package tag', :maintainer - it_behaves_like 'rejects package tags access', :developer, :forbidden - it_behaves_like 'rejects package tags access', :reporter, :forbidden - it_behaves_like 'rejects package tags access', :guest, :forbidden - end + it_behaves_like 'delete package tag', :maintainer + it_behaves_like 'rejects package tags access', :developer, :forbidden + it_behaves_like 'rejects package tags access', :reporter, :forbidden + it_behaves_like 'rejects package tags access', :guest, :forbidden + end - context 'with unauthenticated user' do - it_behaves_like 'rejects package tags access', :no_type, :unauthorized - end + context 'with unauthenticated user' do + it_behaves_like 'rejects package tags access', :no_type, :unauthorized end + end - context 'with private project' do - before do - project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE) - end + context 'with private project' do + before do + project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE) + end - context 'with authenticated user' do - subject { delete api(url, personal_access_token: personal_access_token) } + context 'with authenticated user' do + subject { delete api(url, personal_access_token: personal_access_token) } - it_behaves_like 'delete package tag', :maintainer - it_behaves_like 'rejects package tags access', :developer, :forbidden - it_behaves_like 'rejects package tags access', :reporter, :forbidden - it_behaves_like 'rejects package tags access', :guest, :forbidden - end + it_behaves_like 'delete package tag', :maintainer + it_behaves_like 'rejects package tags access', :developer, :forbidden + it_behaves_like 'rejects package tags access', :reporter, :forbidden + it_behaves_like 'rejects package tags access', :guest, :forbidden + end - context 'with unauthenticated user' do - it_behaves_like 'rejects package tags access', :no_type, :unauthorized - end + context 'with unauthenticated user' do + it_behaves_like 'rejects package tags access', :no_type, :unauthorized end end end diff --git a/spec/requests/api/nuget_packages_spec.rb b/spec/requests/api/nuget_packages_spec.rb index 43aa65d1f76..ab537a61058 100644 --- a/spec/requests/api/nuget_packages_spec.rb +++ b/spec/requests/api/nuget_packages_spec.rb @@ -4,6 +4,7 @@ require 'spec_helper' RSpec.describe API::NugetPackages do include WorkhorseHelpers include PackagesManagerApiSpecHelpers + include HttpBasicAuthHelpers let_it_be(:user) { create(:user) } let_it_be(:project, reload: true) { create(:project, :public) } @@ -20,38 +21,76 @@ RSpec.describe API::NugetPackages do context 'with valid project' do using RSpec::Parameterized::TableSyntax - where(:project_visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do - 'PUBLIC' | :developer | true | true | 'process nuget service index request' | :success - 'PUBLIC' | :guest | true | true | 'process nuget service index request' | :success - 'PUBLIC' | :developer | true | false | 'process nuget service index request' | :success - 'PUBLIC' | :guest | true | false | 'process nuget service index request' | :success - 'PUBLIC' | :developer | false | true | 'process nuget service index request' | :success - 'PUBLIC' | :guest | false | true | 'process nuget service index request' | :success - 'PUBLIC' | :developer | false | false | 'process nuget service index request' | :success - 'PUBLIC' | :guest | false | false | 'process nuget service index request' | :success - 'PUBLIC' | :anonymous | false | true | 'process nuget service index request' | :success - 'PRIVATE' | :developer | true | true | 'process nuget service index request' | :success - 'PRIVATE' | :guest | true | true | 'rejects nuget packages access' | :forbidden - 'PRIVATE' | :developer | true | false | 'rejects nuget packages access' | :unauthorized - 'PRIVATE' | :guest | true | false | 'rejects nuget packages access' | :unauthorized - 'PRIVATE' | :developer | false | true | 'rejects nuget packages access' | :not_found - 'PRIVATE' | :guest | false | true | 'rejects nuget packages access' | :not_found - 'PRIVATE' | :developer | false | false | 'rejects nuget packages access' | :unauthorized - 'PRIVATE' | :guest | false | false | 'rejects nuget packages access' | :unauthorized - 'PRIVATE' | :anonymous | false | true | 'rejects nuget packages access' | :unauthorized - end + context 'personal token' do + where(:project_visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do + 'PUBLIC' | :developer | true | true | 'process nuget service index request' | :success + 'PUBLIC' | :guest | true | true | 'process nuget service index request' | :success + 'PUBLIC' | :developer | true | false | 'process nuget service index request' | :success + 'PUBLIC' | :guest | true | false | 'process nuget service index request' | :success + 'PUBLIC' | :developer | false | true | 'process nuget service index request' | :success + 'PUBLIC' | :guest | false | true | 'process nuget service index request' | :success + 'PUBLIC' | :developer | false | false | 'process nuget service index request' | :success + 'PUBLIC' | :guest | false | false | 'process nuget service index request' | :success + 'PUBLIC' | :anonymous | false | true | 'process nuget service index request' | :success + 'PRIVATE' | :developer | true | true | 'process nuget service index request' | :success + 'PRIVATE' | :guest | true | true | 'rejects nuget packages access' | :forbidden + 'PRIVATE' | :developer | true | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :guest | true | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :developer | false | true | 'rejects nuget packages access' | :not_found + 'PRIVATE' | :guest | false | true | 'rejects nuget packages access' | :not_found + 'PRIVATE' | :developer | false | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :guest | false | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :anonymous | false | true | 'rejects nuget packages access' | :unauthorized + end - with_them do - let(:token) { user_token ? personal_access_token.token : 'wrong' } - let(:headers) { user_role == :anonymous ? {} : build_basic_auth_header(user.username, token) } + with_them do + let(:token) { user_token ? personal_access_token.token : 'wrong' } + let(:headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) } - subject { get api(url), headers: headers } + subject { get api(url), headers: headers } - before do - project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility_level, false)) + before do + project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility_level, false)) + end + + it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member] end + end - it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member] + context 'with job token' do + where(:project_visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do + 'PUBLIC' | :developer | true | true | 'process nuget service index request' | :success + 'PUBLIC' | :guest | true | true | 'process nuget service index request' | :success + 'PUBLIC' | :developer | true | false | 'rejects nuget packages access' | :unauthorized + 'PUBLIC' | :guest | true | false | 'rejects nuget packages access' | :unauthorized + 'PUBLIC' | :developer | false | true | 'process nuget service index request' | :success + 'PUBLIC' | :guest | false | true | 'process nuget service index request' | :success + 'PUBLIC' | :developer | false | false | 'rejects nuget packages access' | :unauthorized + 'PUBLIC' | :guest | false | false | 'rejects nuget packages access' | :unauthorized + 'PUBLIC' | :anonymous | false | true | 'process nuget service index request' | :success + 'PRIVATE' | :developer | true | true | 'process nuget service index request' | :success + 'PRIVATE' | :guest | true | true | 'rejects nuget packages access' | :forbidden + 'PRIVATE' | :developer | true | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :guest | true | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :developer | false | true | 'rejects nuget packages access' | :not_found + 'PRIVATE' | :guest | false | true | 'rejects nuget packages access' | :not_found + 'PRIVATE' | :developer | false | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :guest | false | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :anonymous | false | true | 'rejects nuget packages access' | :unauthorized + end + + with_them do + let(:job) { user_token ? create(:ci_build, project: project, user: user) : double(token: 'wrong') } + let(:headers) { user_role == :anonymous ? {} : job_basic_auth_header(job) } + + subject { get api(url), headers: headers } + + before do + project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility_level, false)) + end + + it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member] + end end end @@ -98,7 +137,7 @@ RSpec.describe API::NugetPackages do with_them do let(:token) { user_token ? personal_access_token.token : 'wrong' } - let(:user_headers) { user_role == :anonymous ? {} : build_basic_auth_header(user.username, token) } + let(:user_headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) } let(:headers) { user_headers.merge(workhorse_header) } before do @@ -165,7 +204,7 @@ RSpec.describe API::NugetPackages do with_them do let(:token) { user_token ? personal_access_token.token : 'wrong' } - let(:user_headers) { user_role == :anonymous ? {} : build_basic_auth_header(user.username, token) } + let(:user_headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) } let(:headers) { user_headers.merge(workhorse_header) } before do @@ -225,7 +264,7 @@ RSpec.describe API::NugetPackages do with_them do let(:token) { user_token ? personal_access_token.token : 'wrong' } - let(:headers) { user_role == :anonymous ? {} : build_basic_auth_header(user.username, token) } + let(:headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) } subject { get api(url), headers: headers } @@ -286,7 +325,7 @@ RSpec.describe API::NugetPackages do with_them do let(:token) { user_token ? personal_access_token.token : 'wrong' } - let(:headers) { user_role == :anonymous ? {} : build_basic_auth_header(user.username, token) } + let(:headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) } subject { get api(url), headers: headers } @@ -342,7 +381,7 @@ RSpec.describe API::NugetPackages do with_them do let(:token) { user_token ? personal_access_token.token : 'wrong' } - let(:headers) { user_role == :anonymous ? {} : build_basic_auth_header(user.username, token) } + let(:headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) } subject { get api(url), headers: headers } @@ -397,7 +436,7 @@ RSpec.describe API::NugetPackages do with_them do let(:token) { user_token ? personal_access_token.token : 'wrong' } - let(:headers) { user_role == :anonymous ? {} : build_basic_auth_header(user.username, token) } + let(:headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) } subject { get api(url), headers: headers } @@ -460,7 +499,7 @@ RSpec.describe API::NugetPackages do with_them do let(:token) { user_token ? personal_access_token.token : 'wrong' } - let(:headers) { user_role == :anonymous ? {} : build_basic_auth_header(user.username, token) } + let(:headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) } subject { get api(url), headers: headers } diff --git a/spec/requests/api/pages_domains_spec.rb b/spec/requests/api/pages_domains_spec.rb index b6838a39257..75183156c9d 100644 --- a/spec/requests/api/pages_domains_spec.rb +++ b/spec/requests/api/pages_domains_spec.rb @@ -17,6 +17,7 @@ RSpec.describe API::PagesDomains do build(:pages_domain, :without_key, :without_certificate, domain: 'www.other-domain.test', auto_ssl_enabled: true) .slice(:domain, :auto_ssl_enabled) end + let(:pages_domain_secure_params) { build(:pages_domain, domain: 'ssl.other-domain.test', project: project).slice(:domain, :certificate, :key) } let(:pages_domain_secure_key_missmatch_params) {build(:pages_domain, :with_trusted_chain, project: project).slice(:domain, :certificate, :key) } let(:pages_domain_secure_missing_chain_params) {build(:pages_domain, :with_missing_chain, project: project).slice(:certificate) } diff --git a/spec/requests/api/performance_bar_spec.rb b/spec/requests/api/performance_bar_spec.rb new file mode 100644 index 00000000000..a4dbb3d17b8 --- /dev/null +++ b/spec/requests/api/performance_bar_spec.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Performance Bar for API requests', :request_store, :clean_gitlab_redis_cache do + context 'with user that has access to the performance bar' do + let_it_be(:admin) { create(:admin) } + + context 'when cookie is set to true' do + before do + cookies[:perf_bar_enabled] = 'true' + end + + it 'stores performance data' do + get api("/users/#{admin.id}", admin) + + expect(Peek.adapter.get(headers['X-Request-Id'])).not_to be_empty + end + end + + context 'when cookie is missing' do + it 'does not store performance data' do + get api("/users/#{admin.id}", admin) + + expect(Peek.adapter.get(headers['X-Request-Id'])).to be_nil + end + end + end + + context 'with user that does not have access to the performance bar' do + let(:user) { create(:user) } + + it 'does not store performance data' do + cookies[:perf_bar_enabled] = 'true' + + get api("/users/#{user.id}", user) + + expect(Peek.adapter.get(headers['X-Request-Id'])).to be_nil + end + end +end diff --git a/spec/requests/api/project_export_spec.rb b/spec/requests/api/project_export_spec.rb index d7ba3b4e158..09d295afbea 100644 --- a/spec/requests/api/project_export_spec.rb +++ b/spec/requests/api/project_export_spec.rb @@ -338,6 +338,16 @@ RSpec.describe API::ProjectExport, :clean_gitlab_redis_cache do end context 'with download strategy' do + before do + Grape::Endpoint.before_each do |endpoint| + allow(endpoint).to receive(:user_project).and_return(project) + end + end + + after do + Grape::Endpoint.before_each nil + end + it 'starts' do expect_any_instance_of(Gitlab::ImportExport::AfterExportStrategies::WebUploadStrategy).not_to receive(:send_file) @@ -345,6 +355,12 @@ RSpec.describe API::ProjectExport, :clean_gitlab_redis_cache do expect(response).to have_gitlab_http_status(:accepted) end + + it 'removes previously exported archive file' do + expect(project).to receive(:remove_exports).once + + post api(path, user) + end end end diff --git a/spec/requests/api/project_hooks_spec.rb b/spec/requests/api/project_hooks_spec.rb index 8ab90e26a51..3b2a7895630 100644 --- a/spec/requests/api/project_hooks_spec.rb +++ b/spec/requests/api/project_hooks_spec.rb @@ -40,6 +40,7 @@ RSpec.describe API::ProjectHooks, 'ProjectHooks' do expect(json_response.first['job_events']).to eq(true) expect(json_response.first['pipeline_events']).to eq(true) expect(json_response.first['wiki_page_events']).to eq(true) + expect(json_response.first['deployment_events']).to eq(true) expect(json_response.first['enable_ssl_verification']).to eq(true) expect(json_response.first['push_events_branch_filter']).to eq('master') end @@ -71,6 +72,7 @@ RSpec.describe API::ProjectHooks, 'ProjectHooks' do expect(json_response['job_events']).to eq(hook.job_events) expect(json_response['pipeline_events']).to eq(hook.pipeline_events) expect(json_response['wiki_page_events']).to eq(hook.wiki_page_events) + expect(json_response['deployment_events']).to eq(true) expect(json_response['enable_ssl_verification']).to eq(hook.enable_ssl_verification) end @@ -92,8 +94,11 @@ RSpec.describe API::ProjectHooks, 'ProjectHooks' do describe "POST /projects/:id/hooks" do it "adds hook to project" do expect do - post api("/projects/#{project.id}/hooks", user), - params: { url: "http://example.com", issues_events: true, confidential_issues_events: true, wiki_page_events: true, job_events: true, push_events_branch_filter: 'some-feature-branch' } + post(api("/projects/#{project.id}/hooks", user), + params: { url: "http://example.com", issues_events: true, + confidential_issues_events: true, wiki_page_events: true, + job_events: true, deployment_events: true, + push_events_branch_filter: 'some-feature-branch' }) end.to change {project.hooks.count}.by(1) expect(response).to have_gitlab_http_status(:created) @@ -108,6 +113,7 @@ RSpec.describe API::ProjectHooks, 'ProjectHooks' do expect(json_response['job_events']).to eq(true) expect(json_response['pipeline_events']).to eq(false) expect(json_response['wiki_page_events']).to eq(true) + expect(json_response['deployment_events']).to eq(true) expect(json_response['enable_ssl_verification']).to eq(true) expect(json_response['push_events_branch_filter']).to eq('some-feature-branch') expect(json_response).not_to include('token') diff --git a/spec/requests/api/project_milestones_spec.rb b/spec/requests/api/project_milestones_spec.rb index b238949ce47..d1e5df66b3f 100644 --- a/spec/requests/api/project_milestones_spec.rb +++ b/spec/requests/api/project_milestones_spec.rb @@ -3,17 +3,68 @@ require 'spec_helper' RSpec.describe API::ProjectMilestones do - let(:user) { create(:user) } - let!(:project) { create(:project, namespace: user.namespace ) } - let!(:closed_milestone) { create(:closed_milestone, project: project, title: 'version1', description: 'closed milestone') } - let!(:milestone) { create(:milestone, project: project, title: 'version2', description: 'open milestone') } + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project, namespace: user.namespace ) } + let_it_be(:closed_milestone) { create(:closed_milestone, project: project, title: 'version1', description: 'closed milestone') } + let_it_be(:milestone) { create(:milestone, project: project, title: 'version2', description: 'open milestone') } + let_it_be(:route) { "/projects/#{project.id}/milestones" } before do project.add_developer(user) end - it_behaves_like 'group and project milestones', "/projects/:id/milestones" do - let(:route) { "/projects/#{project.id}/milestones" } + it_behaves_like 'group and project milestones', "/projects/:id/milestones" + + describe 'GET /projects/:id/milestones' do + context 'when include_parent_milestones is true' do + let_it_be(:ancestor_group) { create(:group, :private) } + let_it_be(:group) { create(:group, :private, parent: ancestor_group) } + let_it_be(:ancestor_group_milestone) { create(:milestone, group: ancestor_group) } + let_it_be(:group_milestone) { create(:milestone, group: group) } + let(:params) { { include_parent_milestones: true } } + + shared_examples 'listing all milestones' do + it 'returns correct list of milestones' do + get api(route, user), params: params + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response.size).to eq(milestones.size) + expect(json_response.map { |entry| entry["id"] }).to eq(milestones.map(&:id)) + end + end + + context 'when project parent is a namespace' do + it_behaves_like 'listing all milestones' do + let(:milestones) { [milestone, closed_milestone] } + end + end + + context 'when project parent is a group' do + let(:milestones) { [group_milestone, ancestor_group_milestone, milestone, closed_milestone] } + + before_all do + project.update(namespace: group) + end + + it_behaves_like 'listing all milestones' + + context 'when iids param is present' do + let(:params) { { include_parent_milestones: true, iids: [group_milestone.iid] } } + + it_behaves_like 'listing all milestones' + end + + context 'when user is not a member of the private project' do + let(:external_user) { create(:user) } + + it 'returns a 404 error' do + get api(route, external_user), params: params + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end + end end describe 'DELETE /projects/:id/milestones/:milestone_id' do @@ -45,10 +96,11 @@ RSpec.describe API::ProjectMilestones do describe 'PUT /projects/:id/milestones/:milestone_id to test observer on close' do it 'creates an activity event when a milestone is closed' do - expect(Event).to receive(:create!) + path = "/projects/#{project.id}/milestones/#{milestone.id}" - put api("/projects/#{project.id}/milestones/#{milestone.id}", user), - params: { state_event: 'close' } + expect do + put api(path, user), params: { state_event: 'close' } + end.to change(Event, :count).by(1) end end diff --git a/spec/requests/api/project_snippets_spec.rb b/spec/requests/api/project_snippets_spec.rb index fbb0e3e109f..9b876edae24 100644 --- a/spec/requests/api/project_snippets_spec.rb +++ b/spec/requests/api/project_snippets_spec.rb @@ -123,16 +123,19 @@ RSpec.describe API::ProjectSnippets do end describe 'POST /projects/:project_id/snippets/' do - let(:params) do + let(:base_params) do { title: 'Test Title', - file_name: 'test.rb', description: 'test description', - content: 'puts "hello world"', visibility: 'public' } end + let(:file_path) { 'file_1.rb' } + let(:file_content) { 'puts "hello world"' } + let(:params) { base_params.merge(file_params) } + let(:file_params) { { files: [{ file_path: file_path, content: file_content }] } } + shared_examples 'project snippet repository actions' do let(:snippet) { ProjectSnippet.find(json_response['id']) } @@ -145,9 +148,9 @@ RSpec.describe API::ProjectSnippets do it 'commit the files to the repository' do subject - blob = snippet.repository.blob_at('master', params[:file_name]) + blob = snippet.repository.blob_at('master', file_path) - expect(blob.data).to eq params[:content] + expect(blob.data).to eq file_content end end @@ -184,63 +187,60 @@ RSpec.describe API::ProjectSnippets do params['visibility'] = 'internal' end + subject { post api("/projects/#{project.id}/snippets/", user), params: params } + it 'creates a new snippet' do - post api("/projects/#{project.id}/snippets/", user), params: params + subject expect(response).to have_gitlab_http_status(:created) snippet = ProjectSnippet.find(json_response['id']) - expect(snippet.content).to eq(params[:content]) + expect(snippet.content).to eq(file_content) expect(snippet.description).to eq(params[:description]) expect(snippet.title).to eq(params[:title]) - expect(snippet.file_name).to eq(params[:file_name]) + expect(snippet.file_name).to eq(file_path) expect(snippet.visibility_level).to eq(Snippet::INTERNAL) end - it_behaves_like 'project snippet repository actions' do - subject { post api("/projects/#{project.id}/snippets/", user), params: params } - end + it_behaves_like 'project snippet repository actions' end - it 'creates a new snippet' do - post api("/projects/#{project.id}/snippets/", admin), params: params + context 'with an admin' do + subject { post api("/projects/#{project.id}/snippets/", admin), params: params } - expect(response).to have_gitlab_http_status(:created) - snippet = ProjectSnippet.find(json_response['id']) - expect(snippet.content).to eq(params[:content]) - expect(snippet.description).to eq(params[:description]) - expect(snippet.title).to eq(params[:title]) - expect(snippet.file_name).to eq(params[:file_name]) - expect(snippet.visibility_level).to eq(Snippet::PUBLIC) - end + it 'creates a new snippet' do + subject - it_behaves_like 'project snippet repository actions' do - subject { post api("/projects/#{project.id}/snippets/", admin), params: params } - end + expect(response).to have_gitlab_http_status(:created) + snippet = ProjectSnippet.find(json_response['id']) + expect(snippet.content).to eq(file_content) + expect(snippet.description).to eq(params[:description]) + expect(snippet.title).to eq(params[:title]) + expect(snippet.file_name).to eq(file_path) + expect(snippet.visibility_level).to eq(Snippet::PUBLIC) + end - it 'returns 400 for missing parameters' do - params.delete(:title) + it_behaves_like 'project snippet repository actions' - post api("/projects/#{project.id}/snippets/", admin), params: params + it 'returns 400 for missing parameters' do + params.delete(:title) - expect(response).to have_gitlab_http_status(:bad_request) - end + subject - it 'returns 400 if content is blank' do - params[:content] = '' + expect(response).to have_gitlab_http_status(:bad_request) + end - post api("/projects/#{project.id}/snippets/", admin), params: params + it_behaves_like 'snippet creation with files parameter' - expect(response).to have_gitlab_http_status(:bad_request) - expect(json_response['error']).to eq 'content is empty' - end + it_behaves_like 'snippet creation without files parameter' - it 'returns 400 if title is blank' do - params[:title] = '' + it 'returns 400 if title is blank' do + params[:title] = '' - post api("/projects/#{project.id}/snippets/", admin), params: params + subject - expect(response).to have_gitlab_http_status(:bad_request) - expect(json_response['error']).to eq 'title is empty' + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['error']).to eq 'title is empty' + end end context 'when save fails because the repository could not be created' do diff --git a/spec/requests/api/project_templates_spec.rb b/spec/requests/api/project_templates_spec.rb index 59b2b09f0bf..d242d49fc1b 100644 --- a/spec/requests/api/project_templates_spec.rb +++ b/spec/requests/api/project_templates_spec.rb @@ -3,8 +3,8 @@ require 'spec_helper' RSpec.describe API::ProjectTemplates do - let_it_be(:public_project) { create(:project, :public, path: 'path.with.dot') } - let_it_be(:private_project) { create(:project, :private) } + let_it_be(:public_project) { create(:project, :public, :repository, create_templates: :merge_request, path: 'path.with.dot') } + let_it_be(:private_project) { create(:project, :private, :repository, create_templates: :issue) } let_it_be(:developer) { create(:user) } let(:url_encoded_path) { "#{public_project.namespace.path}%2F#{public_project.path}" } @@ -62,6 +62,33 @@ RSpec.describe API::ProjectTemplates do expect(json_response).to satisfy_one { |template| template['key'] == 'mit' } end + it 'returns metrics_dashboard_ymls' do + get api("/projects/#{public_project.id}/templates/metrics_dashboard_ymls") + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to include_pagination_headers + expect(response).to match_response_schema('public_api/v4/template_list') + expect(json_response).to satisfy_one { |template| template['key'] == 'Default' } + end + + it 'returns issue templates' do + get api("/projects/#{private_project.id}/templates/issues", developer) + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to include_pagination_headers + expect(response).to match_response_schema('public_api/v4/template_list') + expect(json_response.map {|t| t['key']}).to match_array(%w(bug feature_proposal template_test)) + end + + it 'returns merge request templates' do + get api("/projects/#{public_project.id}/templates/merge_requests") + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to include_pagination_headers + expect(response).to match_response_schema('public_api/v4/template_list') + expect(json_response.map {|t| t['key']}).to match_array(%w(bug feature_proposal template_test)) + end + it 'returns 400 for an unknown template type' do get api("/projects/#{public_project.id}/templates/unknown") @@ -136,6 +163,14 @@ RSpec.describe API::ProjectTemplates do expect(json_response['name']).to eq('Android') end + it 'returns a specific metrics_dashboard_yml' do + get api("/projects/#{public_project.id}/templates/metrics_dashboard_ymls/Default") + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema('public_api/v4/template') + expect(json_response['name']).to eq('Default') + end + it 'returns a specific license' do get api("/projects/#{public_project.id}/templates/licenses/mit") @@ -143,12 +178,42 @@ RSpec.describe API::ProjectTemplates do expect(response).to match_response_schema('public_api/v4/license') end + it 'returns a specific issue template' do + get api("/projects/#{private_project.id}/templates/issues/bug", developer) + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema('public_api/v4/template') + expect(json_response['name']).to eq('bug') + expect(json_response['content']).to eq('something valid') + end + + it 'returns a specific merge request template' do + get api("/projects/#{public_project.id}/templates/merge_requests/feature_proposal") + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema('public_api/v4/template') + expect(json_response['name']).to eq('feature_proposal') + expect(json_response['content']).to eq('feature_proposal') # Content is identical to filename here + end + it 'returns 404 for an unknown specific template' do get api("/projects/#{public_project.id}/templates/licenses/unknown") expect(response).to have_gitlab_http_status(:not_found) end + it 'returns 404 for an unknown issue template' do + get api("/projects/#{public_project.id}/templates/issues/unknown") + + expect(response).to have_gitlab_http_status(:not_found) + end + + it 'returns 404 for an unknown merge request template' do + get api("/projects/#{public_project.id}/templates/merge_requests/unknown") + + expect(response).to have_gitlab_http_status(:not_found) + end + it 'denies access to an anonymous user on a private project' do get api("/projects/#{private_project.id}/templates/licenses/mit") @@ -166,6 +231,10 @@ RSpec.describe API::ProjectTemplates do subject { get api("/projects/#{url_encoded_path}/templates/gitlab_ci_ymls/Android") } end + it_behaves_like 'accepts project paths with dots' do + subject { get api("/projects/#{url_encoded_path}/templates/metrics_dashboard_ymls/Default") } + end + shared_examples 'path traversal attempt' do |template_type| it 'rejects invalid filenames' do get api("/projects/#{public_project.id}/templates/#{template_type}/%2e%2e%2fPython%2ea") diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 76b0c04e32d..46340f86f69 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -70,12 +70,14 @@ RSpec.describe API::Projects do builds_enabled: false, snippets_enabled: false) end + let(:project_member2) do create(:project_member, user: user4, project: project3, access_level: ProjectMember::MAINTAINER) end + let(:project4) do create(:project, name: 'third_project', @@ -386,6 +388,14 @@ RSpec.describe API::Projects do let(:current_user) { user } let(:projects) { [public_project, project, project2, project3].select { |p| p.id > project2.id } } end + + context 'regression: empty string is ignored' do + it_behaves_like 'projects response' do + let(:filter) { { id_after: '' } } + let(:current_user) { user } + let(:projects) { [public_project, project, project2, project3] } + end + end end context 'and using id_before' do @@ -394,6 +404,14 @@ RSpec.describe API::Projects do let(:current_user) { user } let(:projects) { [public_project, project, project2, project3].select { |p| p.id < project2.id } } end + + context 'regression: empty string is ignored' do + it_behaves_like 'projects response' do + let(:filter) { { id_before: '' } } + let(:current_user) { user } + let(:projects) { [public_project, project, project2, project3] } + end + end end context 'and using both id_after and id_before' do @@ -1586,6 +1604,7 @@ RSpec.describe API::Projects do expect(json_response['ci_default_git_depth']).to eq(project.ci_default_git_depth) expect(json_response['merge_method']).to eq(project.merge_method.to_s) expect(json_response['readme_url']).to eq(project.readme_url) + expect(json_response).to have_key 'packages_enabled' end it 'returns a group link with expiration date' do @@ -2339,6 +2358,20 @@ RSpec.describe API::Projects do expect(project_member).to be_persisted end + describe 'updating packages_enabled attribute' do + it 'is enabled by default' do + expect(project.packages_enabled).to be true + end + + it 'disables project packages feature' do + put(api("/projects/#{project.id}", user), params: { packages_enabled: false }) + + expect(response).to have_gitlab_http_status(:ok) + expect(project.reload.packages_enabled).to be false + expect(json_response['packages_enabled']).to eq(false) + end + end + it 'returns 400 when nothing sent' do project_param = {} diff --git a/spec/requests/api/pypi_packages_spec.rb b/spec/requests/api/pypi_packages_spec.rb index b4e83c8caab..e2cfd87b507 100644 --- a/spec/requests/api/pypi_packages_spec.rb +++ b/spec/requests/api/pypi_packages_spec.rb @@ -4,6 +4,7 @@ require 'spec_helper' RSpec.describe API::PypiPackages do include WorkhorseHelpers include PackagesManagerApiSpecHelpers + include HttpBasicAuthHelpers let_it_be(:user) { create(:user) } let_it_be(:project, reload: true) { create(:project, :public) } @@ -17,49 +18,47 @@ RSpec.describe API::PypiPackages do subject { get api(url) } - context 'without the need for a license' do - context 'with valid project' do - using RSpec::Parameterized::TableSyntax - - where(:project_visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do - 'PUBLIC' | :developer | true | true | 'PyPi package versions' | :success - 'PUBLIC' | :guest | true | true | 'PyPi package versions' | :success - 'PUBLIC' | :developer | true | false | 'PyPi package versions' | :success - 'PUBLIC' | :guest | true | false | 'PyPi package versions' | :success - 'PUBLIC' | :developer | false | true | 'PyPi package versions' | :success - 'PUBLIC' | :guest | false | true | 'PyPi package versions' | :success - 'PUBLIC' | :developer | false | false | 'PyPi package versions' | :success - 'PUBLIC' | :guest | false | false | 'PyPi package versions' | :success - 'PUBLIC' | :anonymous | false | true | 'PyPi package versions' | :success - 'PRIVATE' | :developer | true | true | 'PyPi package versions' | :success - 'PRIVATE' | :guest | true | true | 'process PyPi api request' | :forbidden - 'PRIVATE' | :developer | true | false | 'process PyPi api request' | :unauthorized - 'PRIVATE' | :guest | true | false | 'process PyPi api request' | :unauthorized - 'PRIVATE' | :developer | false | true | 'process PyPi api request' | :not_found - 'PRIVATE' | :guest | false | true | 'process PyPi api request' | :not_found - 'PRIVATE' | :developer | false | false | 'process PyPi api request' | :unauthorized - 'PRIVATE' | :guest | false | false | 'process PyPi api request' | :unauthorized - 'PRIVATE' | :anonymous | false | true | 'process PyPi api request' | :unauthorized - end - - with_them do - let(:token) { user_token ? personal_access_token.token : 'wrong' } - let(:headers) { user_role == :anonymous ? {} : build_basic_auth_header(user.username, token) } + context 'with valid project' do + using RSpec::Parameterized::TableSyntax + + where(:project_visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do + 'PUBLIC' | :developer | true | true | 'PyPi package versions' | :success + 'PUBLIC' | :guest | true | true | 'PyPi package versions' | :success + 'PUBLIC' | :developer | true | false | 'PyPi package versions' | :success + 'PUBLIC' | :guest | true | false | 'PyPi package versions' | :success + 'PUBLIC' | :developer | false | true | 'PyPi package versions' | :success + 'PUBLIC' | :guest | false | true | 'PyPi package versions' | :success + 'PUBLIC' | :developer | false | false | 'PyPi package versions' | :success + 'PUBLIC' | :guest | false | false | 'PyPi package versions' | :success + 'PUBLIC' | :anonymous | false | true | 'PyPi package versions' | :success + 'PRIVATE' | :developer | true | true | 'PyPi package versions' | :success + 'PRIVATE' | :guest | true | true | 'process PyPi api request' | :forbidden + 'PRIVATE' | :developer | true | false | 'process PyPi api request' | :unauthorized + 'PRIVATE' | :guest | true | false | 'process PyPi api request' | :unauthorized + 'PRIVATE' | :developer | false | true | 'process PyPi api request' | :not_found + 'PRIVATE' | :guest | false | true | 'process PyPi api request' | :not_found + 'PRIVATE' | :developer | false | false | 'process PyPi api request' | :unauthorized + 'PRIVATE' | :guest | false | false | 'process PyPi api request' | :unauthorized + 'PRIVATE' | :anonymous | false | true | 'process PyPi api request' | :unauthorized + end - subject { get api(url), headers: headers } + with_them do + let(:token) { user_token ? personal_access_token.token : 'wrong' } + let(:headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) } - before do - project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility_level, false)) - end + subject { get api(url), headers: headers } - it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member] + before do + project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility_level, false)) end + + it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member] end + end - it_behaves_like 'deploy token for package GET requests' + it_behaves_like 'deploy token for package GET requests' - it_behaves_like 'rejects PyPI access with unknown project id' - end + it_behaves_like 'rejects PyPI access with unknown project id' end describe 'POST /api/v4/projects/:id/packages/pypi/authorize' do @@ -70,48 +69,46 @@ RSpec.describe API::PypiPackages do subject { post api(url), headers: headers } - context 'without the need for a license' do - context 'with valid project' do - using RSpec::Parameterized::TableSyntax - - where(:project_visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do - 'PUBLIC' | :developer | true | true | 'process PyPi api request' | :success - 'PUBLIC' | :guest | true | true | 'process PyPi api request' | :forbidden - 'PUBLIC' | :developer | true | false | 'process PyPi api request' | :unauthorized - 'PUBLIC' | :guest | true | false | 'process PyPi api request' | :unauthorized - 'PUBLIC' | :developer | false | true | 'process PyPi api request' | :forbidden - 'PUBLIC' | :guest | false | true | 'process PyPi api request' | :forbidden - 'PUBLIC' | :developer | false | false | 'process PyPi api request' | :unauthorized - 'PUBLIC' | :guest | false | false | 'process PyPi api request' | :unauthorized - 'PUBLIC' | :anonymous | false | true | 'process PyPi api request' | :unauthorized - 'PRIVATE' | :developer | true | true | 'process PyPi api request' | :success - 'PRIVATE' | :guest | true | true | 'process PyPi api request' | :forbidden - 'PRIVATE' | :developer | true | false | 'process PyPi api request' | :unauthorized - 'PRIVATE' | :guest | true | false | 'process PyPi api request' | :unauthorized - 'PRIVATE' | :developer | false | true | 'process PyPi api request' | :not_found - 'PRIVATE' | :guest | false | true | 'process PyPi api request' | :not_found - 'PRIVATE' | :developer | false | false | 'process PyPi api request' | :unauthorized - 'PRIVATE' | :guest | false | false | 'process PyPi api request' | :unauthorized - 'PRIVATE' | :anonymous | false | true | 'process PyPi api request' | :unauthorized - end - - with_them do - let(:token) { user_token ? personal_access_token.token : 'wrong' } - let(:user_headers) { user_role == :anonymous ? {} : build_basic_auth_header(user.username, token) } - let(:headers) { user_headers.merge(workhorse_header) } + context 'with valid project' do + using RSpec::Parameterized::TableSyntax + + where(:project_visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do + 'PUBLIC' | :developer | true | true | 'process PyPi api request' | :success + 'PUBLIC' | :guest | true | true | 'process PyPi api request' | :forbidden + 'PUBLIC' | :developer | true | false | 'process PyPi api request' | :unauthorized + 'PUBLIC' | :guest | true | false | 'process PyPi api request' | :unauthorized + 'PUBLIC' | :developer | false | true | 'process PyPi api request' | :forbidden + 'PUBLIC' | :guest | false | true | 'process PyPi api request' | :forbidden + 'PUBLIC' | :developer | false | false | 'process PyPi api request' | :unauthorized + 'PUBLIC' | :guest | false | false | 'process PyPi api request' | :unauthorized + 'PUBLIC' | :anonymous | false | true | 'process PyPi api request' | :unauthorized + 'PRIVATE' | :developer | true | true | 'process PyPi api request' | :success + 'PRIVATE' | :guest | true | true | 'process PyPi api request' | :forbidden + 'PRIVATE' | :developer | true | false | 'process PyPi api request' | :unauthorized + 'PRIVATE' | :guest | true | false | 'process PyPi api request' | :unauthorized + 'PRIVATE' | :developer | false | true | 'process PyPi api request' | :not_found + 'PRIVATE' | :guest | false | true | 'process PyPi api request' | :not_found + 'PRIVATE' | :developer | false | false | 'process PyPi api request' | :unauthorized + 'PRIVATE' | :guest | false | false | 'process PyPi api request' | :unauthorized + 'PRIVATE' | :anonymous | false | true | 'process PyPi api request' | :unauthorized + end - before do - project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility_level, false)) - end + with_them do + let(:token) { user_token ? personal_access_token.token : 'wrong' } + let(:user_headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) } + let(:headers) { user_headers.merge(workhorse_header) } - it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member] + before do + project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility_level, false)) end + + it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member] end + end - it_behaves_like 'deploy token for package uploads' + it_behaves_like 'deploy token for package uploads' - it_behaves_like 'rejects PyPI access with unknown project id' - end + it_behaves_like 'rejects PyPI access with unknown project id' end describe 'POST /api/v4/projects/:id/packages/pypi' do @@ -135,61 +132,59 @@ RSpec.describe API::PypiPackages do ) end - context 'without the need for a license' do - context 'with valid project' do - using RSpec::Parameterized::TableSyntax - - where(:project_visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do - 'PUBLIC' | :developer | true | true | 'PyPi package creation' | :created - 'PUBLIC' | :guest | true | true | 'process PyPi api request' | :forbidden - 'PUBLIC' | :developer | true | false | 'process PyPi api request' | :unauthorized - 'PUBLIC' | :guest | true | false | 'process PyPi api request' | :unauthorized - 'PUBLIC' | :developer | false | true | 'process PyPi api request' | :forbidden - 'PUBLIC' | :guest | false | true | 'process PyPi api request' | :forbidden - 'PUBLIC' | :developer | false | false | 'process PyPi api request' | :unauthorized - 'PUBLIC' | :guest | false | false | 'process PyPi api request' | :unauthorized - 'PUBLIC' | :anonymous | false | true | 'process PyPi api request' | :unauthorized - 'PRIVATE' | :developer | true | true | 'process PyPi api request' | :created - 'PRIVATE' | :guest | true | true | 'process PyPi api request' | :forbidden - 'PRIVATE' | :developer | true | false | 'process PyPi api request' | :unauthorized - 'PRIVATE' | :guest | true | false | 'process PyPi api request' | :unauthorized - 'PRIVATE' | :developer | false | true | 'process PyPi api request' | :not_found - 'PRIVATE' | :guest | false | true | 'process PyPi api request' | :not_found - 'PRIVATE' | :developer | false | false | 'process PyPi api request' | :unauthorized - 'PRIVATE' | :guest | false | false | 'process PyPi api request' | :unauthorized - 'PRIVATE' | :anonymous | false | true | 'process PyPi api request' | :unauthorized - end - - with_them do - let(:token) { user_token ? personal_access_token.token : 'wrong' } - let(:user_headers) { user_role == :anonymous ? {} : build_basic_auth_header(user.username, token) } - let(:headers) { user_headers.merge(workhorse_header) } - - before do - project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility_level, false)) - end - - it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member] - end + context 'with valid project' do + using RSpec::Parameterized::TableSyntax + + where(:project_visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do + 'PUBLIC' | :developer | true | true | 'PyPi package creation' | :created + 'PUBLIC' | :guest | true | true | 'process PyPi api request' | :forbidden + 'PUBLIC' | :developer | true | false | 'process PyPi api request' | :unauthorized + 'PUBLIC' | :guest | true | false | 'process PyPi api request' | :unauthorized + 'PUBLIC' | :developer | false | true | 'process PyPi api request' | :forbidden + 'PUBLIC' | :guest | false | true | 'process PyPi api request' | :forbidden + 'PUBLIC' | :developer | false | false | 'process PyPi api request' | :unauthorized + 'PUBLIC' | :guest | false | false | 'process PyPi api request' | :unauthorized + 'PUBLIC' | :anonymous | false | true | 'process PyPi api request' | :unauthorized + 'PRIVATE' | :developer | true | true | 'process PyPi api request' | :created + 'PRIVATE' | :guest | true | true | 'process PyPi api request' | :forbidden + 'PRIVATE' | :developer | true | false | 'process PyPi api request' | :unauthorized + 'PRIVATE' | :guest | true | false | 'process PyPi api request' | :unauthorized + 'PRIVATE' | :developer | false | true | 'process PyPi api request' | :not_found + 'PRIVATE' | :guest | false | true | 'process PyPi api request' | :not_found + 'PRIVATE' | :developer | false | false | 'process PyPi api request' | :unauthorized + 'PRIVATE' | :guest | false | false | 'process PyPi api request' | :unauthorized + 'PRIVATE' | :anonymous | false | true | 'process PyPi api request' | :unauthorized end - context 'with an invalid package' do - let(:token) { personal_access_token.token } - let(:user_headers) { build_basic_auth_header(user.username, token) } + with_them do + let(:token) { user_token ? personal_access_token.token : 'wrong' } + let(:user_headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) } let(:headers) { user_headers.merge(workhorse_header) } before do - params[:name] = '.$/@!^*' - project.add_developer(user) + project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility_level, false)) end - it_behaves_like 'returning response status', :bad_request + it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member] end + end - it_behaves_like 'deploy token for package uploads' + context 'with an invalid package' do + let(:token) { personal_access_token.token } + let(:user_headers) { basic_auth_header(user.username, token) } + let(:headers) { user_headers.merge(workhorse_header) } + + before do + params[:name] = '.$/@!^*' + project.add_developer(user) + end - it_behaves_like 'rejects PyPI access with unknown project id' + it_behaves_like 'returning response status', :bad_request end + + it_behaves_like 'deploy token for package uploads' + + it_behaves_like 'rejects PyPI access with unknown project id' end describe 'GET /api/v4/projects/:id/packages/pypi/files/:sha256/*file_identifier' do @@ -200,60 +195,58 @@ RSpec.describe API::PypiPackages do subject { get api(url) } - context 'without the need for a license' do - context 'with valid project' do - using RSpec::Parameterized::TableSyntax - - where(:project_visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do - 'PUBLIC' | :developer | true | true | 'PyPi package download' | :success - 'PUBLIC' | :guest | true | true | 'PyPi package download' | :success - 'PUBLIC' | :developer | true | false | 'PyPi package download' | :success - 'PUBLIC' | :guest | true | false | 'PyPi package download' | :success - 'PUBLIC' | :developer | false | true | 'PyPi package download' | :success - 'PUBLIC' | :guest | false | true | 'PyPi package download' | :success - 'PUBLIC' | :developer | false | false | 'PyPi package download' | :success - 'PUBLIC' | :guest | false | false | 'PyPi package download' | :success - 'PUBLIC' | :anonymous | false | true | 'PyPi package download' | :success - 'PRIVATE' | :developer | true | true | 'PyPi package download' | :success - 'PRIVATE' | :guest | true | true | 'PyPi package download' | :success - 'PRIVATE' | :developer | true | false | 'PyPi package download' | :success - 'PRIVATE' | :guest | true | false | 'PyPi package download' | :success - 'PRIVATE' | :developer | false | true | 'PyPi package download' | :success - 'PRIVATE' | :guest | false | true | 'PyPi package download' | :success - 'PRIVATE' | :developer | false | false | 'PyPi package download' | :success - 'PRIVATE' | :guest | false | false | 'PyPi package download' | :success - 'PRIVATE' | :anonymous | false | true | 'PyPi package download' | :success - end - - with_them do - let(:token) { user_token ? personal_access_token.token : 'wrong' } - let(:headers) { user_role == :anonymous ? {} : build_basic_auth_header(user.username, token) } + context 'with valid project' do + using RSpec::Parameterized::TableSyntax + + where(:project_visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do + 'PUBLIC' | :developer | true | true | 'PyPi package download' | :success + 'PUBLIC' | :guest | true | true | 'PyPi package download' | :success + 'PUBLIC' | :developer | true | false | 'PyPi package download' | :success + 'PUBLIC' | :guest | true | false | 'PyPi package download' | :success + 'PUBLIC' | :developer | false | true | 'PyPi package download' | :success + 'PUBLIC' | :guest | false | true | 'PyPi package download' | :success + 'PUBLIC' | :developer | false | false | 'PyPi package download' | :success + 'PUBLIC' | :guest | false | false | 'PyPi package download' | :success + 'PUBLIC' | :anonymous | false | true | 'PyPi package download' | :success + 'PRIVATE' | :developer | true | true | 'PyPi package download' | :success + 'PRIVATE' | :guest | true | true | 'PyPi package download' | :success + 'PRIVATE' | :developer | true | false | 'PyPi package download' | :success + 'PRIVATE' | :guest | true | false | 'PyPi package download' | :success + 'PRIVATE' | :developer | false | true | 'PyPi package download' | :success + 'PRIVATE' | :guest | false | true | 'PyPi package download' | :success + 'PRIVATE' | :developer | false | false | 'PyPi package download' | :success + 'PRIVATE' | :guest | false | false | 'PyPi package download' | :success + 'PRIVATE' | :anonymous | false | true | 'PyPi package download' | :success + end - subject { get api(url), headers: headers } + with_them do + let(:token) { user_token ? personal_access_token.token : 'wrong' } + let(:headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) } - before do - project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility_level, false)) - end + subject { get api(url), headers: headers } - it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member] + before do + project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility_level, false)) end + + it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member] end + end - context 'with deploy token headers' do - let(:headers) { build_basic_auth_header(deploy_token.username, deploy_token.token) } + context 'with deploy token headers' do + let(:headers) { basic_auth_header(deploy_token.username, deploy_token.token) } - context 'valid token' do - it_behaves_like 'returning response status', :success - end + context 'valid token' do + it_behaves_like 'returning response status', :success + end - context 'invalid token' do - let(:headers) { build_basic_auth_header('foo', 'bar') } + context 'invalid token' do + let(:headers) { basic_auth_header('foo', 'bar') } - it_behaves_like 'returning response status', :success - end + it_behaves_like 'returning response status', :success end - - it_behaves_like 'rejects PyPI access with unknown project id' end + + it_behaves_like 'rejects PyPI access with unknown project id' end end diff --git a/spec/requests/api/releases_spec.rb b/spec/requests/api/releases_spec.rb index 5e8353d74c3..a9a92a4d3cd 100644 --- a/spec/requests/api/releases_spec.rb +++ b/spec/requests/api/releases_spec.rb @@ -420,7 +420,17 @@ RSpec.describe API::Releases do { name: 'New release', tag_name: 'v0.1', - description: 'Super nice release' + description: 'Super nice release', + assets: { + links: [ + { + name: 'An example runbook link', + url: 'https://example.com/runbook', + link_type: 'runbook', + filepath: '/permanent/path/to/runbook' + } + ] + } } end @@ -435,9 +445,17 @@ RSpec.describe API::Releases do post api("/projects/#{project.id}/releases", maintainer), params: params end.to change { Release.count }.by(1) - expect(project.releases.last.name).to eq('New release') - expect(project.releases.last.tag).to eq('v0.1') - expect(project.releases.last.description).to eq('Super nice release') + release = project.releases.last + + aggregate_failures do + expect(release.name).to eq('New release') + expect(release.tag).to eq('v0.1') + expect(release.description).to eq('Super nice release') + expect(release.links.last.name).to eq('An example runbook link') + expect(release.links.last.url).to eq('https://example.com/runbook') + expect(release.links.last.link_type).to eq('runbook') + expect(release.links.last.filepath).to eq('/permanent/path/to/runbook') + end end it 'creates a new release without description' do diff --git a/spec/requests/api/snippets_spec.rb b/spec/requests/api/snippets_spec.rb index e676eb94337..4e2f6e108eb 100644 --- a/spec/requests/api/snippets_spec.rb +++ b/spec/requests/api/snippets_spec.rb @@ -84,8 +84,8 @@ RSpec.describe API::Snippets do public_snippet.id, public_snippet_other.id) expect(json_response.map { |snippet| snippet['web_url']} ).to contain_exactly( - "http://localhost/snippets/#{public_snippet.id}", - "http://localhost/snippets/#{public_snippet_other.id}") + "http://localhost/-/snippets/#{public_snippet.id}", + "http://localhost/-/snippets/#{public_snippet_other.id}") expect(json_response[0]['files'].first).to eq snippet_blob_file(public_snippet_other.blobs.first) expect(json_response[1]['files'].first).to eq snippet_blob_file(public_snippet.blobs.first) end @@ -229,13 +229,16 @@ RSpec.describe API::Snippets do let(:base_params) do { title: 'Test Title', - file_name: 'test.rb', description: 'test description', - content: 'puts "hello world"', visibility: 'public' } end - let(:params) { base_params.merge(extra_params) } + + let(:file_path) { 'file_1.rb' } + let(:file_content) { 'puts "hello world"' } + + let(:params) { base_params.merge(file_params, extra_params) } + let(:file_params) { { files: [{ file_path: file_path, content: file_content }] } } let(:extra_params) { {} } subject { post api("/snippets/", user), params: params } @@ -251,7 +254,7 @@ RSpec.describe API::Snippets do expect(response).to have_gitlab_http_status(:created) expect(json_response['title']).to eq(params[:title]) expect(json_response['description']).to eq(params[:description]) - expect(json_response['file_name']).to eq(params[:file_name]) + expect(json_response['file_name']).to eq(file_path) expect(json_response['files']).to eq(snippet.blobs.map { |blob| snippet_blob_file(blob) }) expect(json_response['visibility']).to eq(params[:visibility]) end @@ -265,12 +268,31 @@ RSpec.describe API::Snippets do it 'commit the files to the repository' do subject - blob = snippet.repository.blob_at('master', params[:file_name]) + blob = snippet.repository.blob_at('master', file_path) - expect(blob.data).to eq params[:content] + expect(blob.data).to eq file_content end end + context 'with files parameter' do + it_behaves_like 'snippet creation with files parameter' + + context 'with multiple files' do + let(:file_params) do + { + files: [ + { file_path: 'file_1.rb', content: 'puts "hello world"' }, + { file_path: 'file_2.rb', content: 'puts "hello world 2"' } + ] + } + end + + it_behaves_like 'snippet creation' + end + end + + it_behaves_like 'snippet creation without files parameter' + context 'with restricted visibility settings' do before do stub_application_setting(restricted_visibility_levels: @@ -305,15 +327,6 @@ RSpec.describe API::Snippets do expect(response).to have_gitlab_http_status(:bad_request) end - it 'returns 400 if content is blank' do - params[:content] = '' - - subject - - expect(response).to have_gitlab_http_status(:bad_request) - expect(json_response['error']).to eq 'content is empty' - end - it 'returns 400 if title is blank' do params[:title] = '' diff --git a/spec/requests/api/suggestions_spec.rb b/spec/requests/api/suggestions_spec.rb index 34d3c54d700..78a2688ac5e 100644 --- a/spec/requests/api/suggestions_spec.rb +++ b/spec/requests/api/suggestions_spec.rb @@ -74,7 +74,7 @@ RSpec.describe API::Suggestions do put api(url, user) expect(response).to have_gitlab_http_status(:bad_request) - expect(json_response).to eq({ 'message' => 'A suggestion is not applicable.' }) + expect(json_response).to eq({ 'message' => "Can't apply as this line was changed in a more recent version." }) end end @@ -133,7 +133,7 @@ RSpec.describe API::Suggestions do params: { ids: [suggestion.id, unappliable_suggestion.id] } expect(response).to have_gitlab_http_status(:bad_request) - expect(json_response).to eq({ 'message' => 'A suggestion is not applicable.' }) + expect(json_response).to eq({ 'message' => "Can't apply as this line was changed in a more recent version." }) end end diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index 17f9112c1d5..6c6497a240b 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -64,6 +64,7 @@ RSpec.describe API::Users, :do_not_mock_admin_mode do expect(json_response).to have_key('note') expect(json_response['note']).to eq(user.note) + expect(json_response).to have_key('sign_in_count') end end @@ -72,6 +73,7 @@ RSpec.describe API::Users, :do_not_mock_admin_mode do get api("/users/#{user.id}", user) expect(json_response).not_to have_key('note') + expect(json_response).not_to have_key('sign_in_count') end end end |