From aee0a117a889461ce8ced6fcf73207fe017f1d99 Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Mon, 20 Dec 2021 13:37:47 +0000 Subject: Add latest changes from gitlab-org/gitlab@14-6-stable-ee --- spec/requests/api/admin/plan_limits_spec.rb | 7 +- spec/requests/api/ci/job_artifacts_spec.rb | 661 +++++++++++++++++++++ spec/requests/api/ci/jobs_spec.rb | 605 +------------------ spec/requests/api/ci/pipelines_spec.rb | 5 +- spec/requests/api/ci/runner/jobs_artifacts_spec.rb | 12 +- .../api/ci/runner/jobs_request_post_spec.rb | 4 +- spec/requests/api/ci/runner/runners_post_spec.rb | 64 +- spec/requests/api/ci/runners_spec.rb | 29 +- spec/requests/api/commit_statuses_spec.rb | 34 +- spec/requests/api/commits_spec.rb | 4 +- spec/requests/api/composer_packages_spec.rb | 14 + spec/requests/api/conan_project_packages_spec.rb | 5 +- spec/requests/api/dependency_proxy_spec.rb | 22 - spec/requests/api/error_tracking/collector_spec.rb | 12 +- .../graphql/boards/board_list_issues_query_spec.rb | 46 +- .../api/graphql/boards/board_list_query_spec.rb | 33 +- .../api/graphql/boards/board_lists_query_spec.rb | 8 +- spec/requests/api/graphql/ci/jobs_spec.rb | 95 +-- spec/requests/api/graphql/ci/pipelines_spec.rb | 12 +- spec/requests/api/graphql/ci/runner_spec.rb | 50 +- spec/requests/api/graphql/ci/runners_spec.rb | 9 + .../container_repository_details_spec.rb | 82 +++ .../api/graphql/current_user/todos_query_spec.rb | 2 +- .../mutations/design_management/delete_spec.rb | 2 +- .../mutations/issues/set_crm_contacts_spec.rb | 16 +- .../graphql/mutations/user_callouts/create_spec.rb | 2 +- spec/requests/api/graphql/packages/package_spec.rb | 87 ++- .../api/graphql/project/cluster_agents_spec.rb | 35 +- spec/requests/api/graphql/project/jobs_spec.rb | 56 ++ spec/requests/api/graphql/project/pipeline_spec.rb | 42 ++ spec/requests/api/graphql/project_query_spec.rb | 34 ++ spec/requests/api/groups_spec.rb | 140 ++++- spec/requests/api/import_github_spec.rb | 6 +- spec/requests/api/invitations_spec.rb | 18 - .../requests/api/issues/get_project_issues_spec.rb | 2 +- spec/requests/api/labels_spec.rb | 16 + spec/requests/api/markdown_golden_master_spec.rb | 9 + spec/requests/api/members_spec.rb | 31 - spec/requests/api/project_import_spec.rb | 2 +- spec/requests/api/projects_spec.rb | 23 + spec/requests/api/repositories_spec.rb | 69 ++- spec/requests/api/search_spec.rb | 17 + spec/requests/api/settings_spec.rb | 9 - spec/requests/api/terraform/state_spec.rb | 10 + spec/requests/api/todos_spec.rb | 2 +- spec/requests/api/topics_spec.rb | 40 +- spec/requests/api/v3/github_spec.rb | 12 - 47 files changed, 1587 insertions(+), 908 deletions(-) create mode 100644 spec/requests/api/ci/job_artifacts_spec.rb create mode 100644 spec/requests/api/graphql/project/jobs_spec.rb create mode 100644 spec/requests/api/markdown_golden_master_spec.rb (limited to 'spec/requests/api') diff --git a/spec/requests/api/admin/plan_limits_spec.rb b/spec/requests/api/admin/plan_limits_spec.rb index f497227789a..03642ad617e 100644 --- a/spec/requests/api/admin/plan_limits_spec.rb +++ b/spec/requests/api/admin/plan_limits_spec.rb @@ -25,6 +25,7 @@ RSpec.describe API::Admin::PlanLimits, 'PlanLimits' do expect(json_response).to be_an Hash expect(json_response['conan_max_file_size']).to eq(Plan.default.actual_limits.conan_max_file_size) expect(json_response['generic_packages_max_file_size']).to eq(Plan.default.actual_limits.generic_packages_max_file_size) + expect(json_response['helm_max_file_size']).to eq(Plan.default.actual_limits.helm_max_file_size) expect(json_response['maven_max_file_size']).to eq(Plan.default.actual_limits.maven_max_file_size) expect(json_response['npm_max_file_size']).to eq(Plan.default.actual_limits.npm_max_file_size) expect(json_response['nuget_max_file_size']).to eq(Plan.default.actual_limits.nuget_max_file_size) @@ -45,6 +46,7 @@ RSpec.describe API::Admin::PlanLimits, 'PlanLimits' do expect(json_response).to be_an Hash expect(json_response['conan_max_file_size']).to eq(Plan.default.actual_limits.conan_max_file_size) expect(json_response['generic_packages_max_file_size']).to eq(Plan.default.actual_limits.generic_packages_max_file_size) + expect(json_response['helm_max_file_size']).to eq(Plan.default.actual_limits.helm_max_file_size) expect(json_response['maven_max_file_size']).to eq(Plan.default.actual_limits.maven_max_file_size) expect(json_response['npm_max_file_size']).to eq(Plan.default.actual_limits.npm_max_file_size) expect(json_response['nuget_max_file_size']).to eq(Plan.default.actual_limits.nuget_max_file_size) @@ -84,6 +86,7 @@ RSpec.describe API::Admin::PlanLimits, 'PlanLimits' do 'plan_name': 'default', 'conan_max_file_size': 10, 'generic_packages_max_file_size': 20, + 'helm_max_file_size': 25, 'maven_max_file_size': 30, 'npm_max_file_size': 40, 'nuget_max_file_size': 50, @@ -95,6 +98,7 @@ RSpec.describe API::Admin::PlanLimits, 'PlanLimits' do expect(json_response).to be_an Hash expect(json_response['conan_max_file_size']).to eq(10) expect(json_response['generic_packages_max_file_size']).to eq(20) + expect(json_response['helm_max_file_size']).to eq(25) expect(json_response['maven_max_file_size']).to eq(30) expect(json_response['npm_max_file_size']).to eq(40) expect(json_response['nuget_max_file_size']).to eq(50) @@ -129,6 +133,7 @@ RSpec.describe API::Admin::PlanLimits, 'PlanLimits' do 'plan_name': 'default', 'conan_max_file_size': 'a', 'generic_packages_max_file_size': 'b', + 'helm_max_file_size': 'h', 'maven_max_file_size': 'c', 'npm_max_file_size': 'd', 'nuget_max_file_size': 'e', @@ -140,8 +145,8 @@ RSpec.describe API::Admin::PlanLimits, 'PlanLimits' do expect(json_response['error']).to include( 'conan_max_file_size is invalid', 'generic_packages_max_file_size is invalid', + 'helm_max_file_size is invalid', 'maven_max_file_size is invalid', - 'generic_packages_max_file_size is invalid', 'npm_max_file_size is invalid', 'nuget_max_file_size is invalid', 'pypi_max_file_size is invalid', diff --git a/spec/requests/api/ci/job_artifacts_spec.rb b/spec/requests/api/ci/job_artifacts_spec.rb new file mode 100644 index 00000000000..585fab33708 --- /dev/null +++ b/spec/requests/api/ci/job_artifacts_spec.rb @@ -0,0 +1,661 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe API::Ci::JobArtifacts do + include HttpBasicAuthHelpers + include DependencyProxyHelpers + + include HttpIOHelpers + + let_it_be(:project, reload: true) do + create(:project, :repository, public_builds: false) + end + + let_it_be(:pipeline, reload: true) do + create(:ci_pipeline, project: project, + sha: project.commit.id, + ref: project.default_branch) + end + + let(:user) { create(:user) } + let(:api_user) { user } + let(:reporter) { create(:project_member, :reporter, project: project).user } + let(:guest) { create(:project_member, :guest, project: project).user } + + let!(:job) do + create(:ci_build, :success, :tags, pipeline: pipeline, + artifacts_expire_at: 1.day.since) + end + + before do + project.add_developer(user) + end + + shared_examples 'returns unauthorized' do + it 'returns unauthorized' do + expect(response).to have_gitlab_http_status(:unauthorized) + end + end + + describe 'DELETE /projects/:id/jobs/:job_id/artifacts' do + let!(:job) { create(:ci_build, :artifacts, pipeline: pipeline, user: api_user) } + + before do + delete api("/projects/#{project.id}/jobs/#{job.id}/artifacts", api_user) + end + + context 'when user is anonymous' do + let(:api_user) { nil } + + it 'does not delete artifacts' do + expect(job.job_artifacts.size).to eq 2 + end + + it 'returns status 401 (unauthorized)' do + expect(response).to have_gitlab_http_status(:unauthorized) + end + end + + context 'with developer' do + it 'does not delete artifacts' do + expect(job.job_artifacts.size).to eq 2 + end + + it 'returns status 403 (forbidden)' do + expect(response).to have_gitlab_http_status(:forbidden) + end + end + + context 'with authorized user' do + let(:maintainer) { create(:project_member, :maintainer, project: project).user } + let!(:api_user) { maintainer } + + it 'deletes artifacts' do + expect(job.job_artifacts.size).to eq 0 + end + + it 'returns status 204 (no content)' do + expect(response).to have_gitlab_http_status(:no_content) + end + end + end + + describe 'GET /projects/:id/jobs/:job_id/artifacts/:artifact_path' do + context 'when job has artifacts' do + let(:job) { create(:ci_build, :artifacts, pipeline: pipeline) } + + let(:artifact) do + 'other_artifacts_0.1.2/another-subdirectory/banana_sample.gif' + end + + context 'when user is anonymous' do + let(:api_user) { nil } + + context 'when project is public' do + it 'allows to access artifacts' do + project.update_column(:visibility_level, + Gitlab::VisibilityLevel::PUBLIC) + project.update_column(:public_builds, true) + + get_artifact_file(artifact) + + expect(response).to have_gitlab_http_status(:ok) + end + end + + context 'when project is public with artifacts that are non public' do + let(:job) { create(:ci_build, :artifacts, :non_public_artifacts, pipeline: pipeline) } + + it 'rejects access to artifacts' do + project.update_column(:visibility_level, + Gitlab::VisibilityLevel::PUBLIC) + project.update_column(:public_builds, true) + + get_artifact_file(artifact) + + expect(response).to have_gitlab_http_status(:forbidden) + end + + context 'with the non_public_artifacts feature flag disabled' do + before do + stub_feature_flags(non_public_artifacts: false) + end + + it 'allows access to artifacts' do + project.update_column(:visibility_level, + Gitlab::VisibilityLevel::PUBLIC) + project.update_column(:public_builds, true) + + get_artifact_file(artifact) + + expect(response).to have_gitlab_http_status(:ok) + end + end + end + + context 'when project is public with builds access disabled' do + it 'rejects access to artifacts' do + project.update_column(:visibility_level, + Gitlab::VisibilityLevel::PUBLIC) + project.update_column(:public_builds, false) + + get_artifact_file(artifact) + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + + context 'when project is private' do + it 'rejects access and hides existence of artifacts' do + project.update_column(:visibility_level, + Gitlab::VisibilityLevel::PRIVATE) + project.update_column(:public_builds, true) + + get_artifact_file(artifact) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end + + context 'when user is authorized' do + it 'returns a specific artifact file for a valid path' do + expect(Gitlab::Workhorse) + .to receive(:send_artifacts_entry) + .and_call_original + + get_artifact_file(artifact) + + expect(response).to have_gitlab_http_status(:ok) + expect(response.headers.to_h) + .to include('Content-Type' => 'application/json', + 'Gitlab-Workhorse-Send-Data' => /artifacts-entry/) + expect(response.parsed_body).to be_empty + end + + context 'when artifacts are locked' do + it 'allows access to expired artifact' do + pipeline.artifacts_locked! + job.update!(artifacts_expire_at: Time.now - 7.days) + + get_artifact_file(artifact) + + expect(response).to have_gitlab_http_status(:ok) + end + end + end + end + + context 'when job does not have artifacts' do + it 'does not return job artifact file' do + get_artifact_file('some/artifact') + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + def get_artifact_file(artifact_path) + get api("/projects/#{project.id}/jobs/#{job.id}/" \ + "artifacts/#{artifact_path}", api_user) + end + end + + describe 'GET /projects/:id/jobs/:job_id/artifacts' do + shared_examples 'downloads artifact' 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 + + it 'returns specific job artifacts' do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(response.headers.to_h).to include(download_headers) + expect(response.body).to match_file(job.artifacts_file.file.file) + end + end + + context 'normal authentication' do + context 'job with artifacts' do + context 'when artifacts are stored locally' do + let(:job) { create(:ci_build, :artifacts, pipeline: pipeline) } + + subject { get api("/projects/#{project.id}/jobs/#{job.id}/artifacts", api_user) } + + context 'authorized user' do + it_behaves_like 'downloads artifact' + end + + context 'when job token is used' do + let(:other_job) { create(:ci_build, :running, user: user) } + + subject { get api("/projects/#{project.id}/jobs/#{job.id}/artifacts", job_token: other_job.token) } + + before do + stub_licensed_features(cross_project_pipelines: true) + end + + it_behaves_like 'downloads artifact' + + context 'when job token scope is enabled' do + before do + other_job.project.ci_cd_settings.update!(job_token_scope_enabled: true) + end + + it 'does not allow downloading artifacts' do + subject + + expect(response).to have_gitlab_http_status(:not_found) + end + + context 'when project is added to the job token scope' do + let!(:link) { create(:ci_job_token_project_scope_link, source_project: other_job.project, target_project: job.project) } + + it_behaves_like 'downloads artifact' + end + end + end + + context 'unauthorized user' do + let(:api_user) { nil } + + it 'does not return specific job artifacts' do + subject + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end + + context 'when artifacts are stored remotely' do + let(:proxy_download) { false } + let(:job) { create(:ci_build, pipeline: pipeline) } + let(:artifact) { create(:ci_job_artifact, :archive, :remote_store, job: job) } + + before do + stub_artifacts_object_storage(proxy_download: proxy_download) + + artifact + job.reload + + get api("/projects/#{project.id}/jobs/#{job.id}/artifacts", api_user) + end + + context 'when proxy download is enabled' do + let(:proxy_download) { true } + + it 'responds with the workhorse send-url' do + expect(response.headers[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("send-url:") + end + end + + context 'when proxy download is disabled' do + it 'returns location redirect' do + expect(response).to have_gitlab_http_status(:found) + end + end + + context 'authorized user' do + it 'returns the file remote URL' do + expect(response).to redirect_to(artifact.file.url) + end + end + + context 'unauthorized user' do + let(:api_user) { nil } + + it 'does not return specific job artifacts' do + expect(response).to have_gitlab_http_status(:not_found) + end + end + end + + context 'when public project guest and artifacts are non public' do + let(:api_user) { guest } + let(:job) { create(:ci_build, :artifacts, :non_public_artifacts, pipeline: pipeline) } + + before do + project.update_column(:visibility_level, + Gitlab::VisibilityLevel::PUBLIC) + project.update_column(:public_builds, true) + get api("/projects/#{project.id}/jobs/#{job.id}/artifacts", api_user) + end + + it 'rejects access and hides existence of artifacts' do + expect(response).to have_gitlab_http_status(:forbidden) + end + + context 'with the non_public_artifacts feature flag disabled' do + before do + stub_feature_flags(non_public_artifacts: false) + get api("/projects/#{project.id}/jobs/#{job.id}/artifacts", api_user) + end + + it 'allows access to artifacts' do + expect(response).to have_gitlab_http_status(:ok) + end + end + end + + it 'does not return job artifacts if not uploaded' do + get api("/projects/#{project.id}/jobs/#{job.id}/artifacts", api_user) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end + end + + describe 'GET /projects/:id/artifacts/:ref_name/download?job=name' do + let(:api_user) { reporter } + let(:job) { create(:ci_build, :artifacts, pipeline: pipeline, user: api_user) } + + before do + stub_artifacts_object_storage + job.success + end + + def get_for_ref(ref = pipeline.ref, job_name = job.name) + get api("/projects/#{project.id}/jobs/artifacts/#{ref}/download", api_user), params: { job: job_name } + end + + context 'when not logged in' do + let(:api_user) { nil } + + before do + get_for_ref + end + + it 'does not find a resource in a private project' do + expect(project).to be_private + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'when logging as guest' do + let(:api_user) { guest } + + before do + get_for_ref + end + + it 'gives 403' do + expect(response).to have_gitlab_http_status(:forbidden) + end + end + + context 'non-existing job' do + shared_examples 'not found' do + it { expect(response).to have_gitlab_http_status(:not_found) } + end + + context 'has no such ref' do + before do + get_for_ref('TAIL') + end + + it_behaves_like 'not found' + end + + context 'has no such job' do + before do + get_for_ref(pipeline.ref, 'NOBUILD') + end + + it_behaves_like 'not found' + end + end + + context 'find proper job' do + let(:job_with_artifacts) { job } + + shared_examples 'a valid file' do + context 'when artifacts are stored locally', :sidekiq_might_not_need_inline do + let(:download_headers) do + { 'Content-Transfer-Encoding' => 'binary', + 'Content-Disposition' => + %Q(attachment; filename="#{job_with_artifacts.artifacts_file.filename}"; filename*=UTF-8''#{job.artifacts_file.filename}) } + end + + it { expect(response).to have_gitlab_http_status(:ok) } + it { expect(response.headers.to_h).to include(download_headers) } + end + + context 'when artifacts are stored remotely' do + let(:job) { create(:ci_build, pipeline: pipeline, user: api_user) } + let!(:artifact) { create(:ci_job_artifact, :archive, :remote_store, job: job) } + + before do + job.reload + + get api("/projects/#{project.id}/jobs/#{job.id}/artifacts", api_user) + end + + it 'returns location redirect' do + expect(response).to have_gitlab_http_status(:found) + end + end + end + + context 'with regular branch' do + before do + pipeline.reload + pipeline.update!(ref: 'master', + sha: project.commit('master').sha) + + get_for_ref('master') + end + + it_behaves_like 'a valid file' + end + + context 'with branch name containing slash' do + before do + pipeline.reload + pipeline.update!(ref: 'improve/awesome', sha: project.commit('improve/awesome').sha) + get_for_ref('improve/awesome') + end + + it_behaves_like 'a valid file' + end + + context 'with job name in a child pipeline' do + let(:child_pipeline) { create(:ci_pipeline, child_of: pipeline) } + let!(:child_job) { create(:ci_build, :artifacts, :success, name: 'rspec', pipeline: child_pipeline) } + let(:job_with_artifacts) { child_job } + + before do + get_for_ref('master', child_job.name) + end + + it_behaves_like 'a valid file' + end + end + end + + describe 'GET id/jobs/artifacts/:ref_name/raw/*artifact_path?job=name' do + context 'when job has artifacts' do + let(:job) { create(:ci_build, :artifacts, pipeline: pipeline, user: api_user) } + let(:artifact) { 'other_artifacts_0.1.2/another-subdirectory/banana_sample.gif' } + let(:visibility_level) { Gitlab::VisibilityLevel::PUBLIC } + let(:public_builds) { true } + + before do + stub_artifacts_object_storage + job.success + + project.update!(visibility_level: visibility_level, + public_builds: public_builds) + + get_artifact_file(artifact) + end + + context 'when user is anonymous' do + let(:api_user) { nil } + + context 'when project is public' do + let(:visibility_level) { Gitlab::VisibilityLevel::PUBLIC } + let(:public_builds) { true } + + it 'allows to access artifacts', :sidekiq_might_not_need_inline do + expect(response).to have_gitlab_http_status(:ok) + expect(response.headers.to_h) + .to include('Content-Type' => 'application/json', + 'Gitlab-Workhorse-Send-Data' => /artifacts-entry/) + end + end + + context 'when project is public with builds access disabled' do + let(:visibility_level) { Gitlab::VisibilityLevel::PUBLIC } + let(:public_builds) { false } + + it 'rejects access to artifacts' do + expect(response).to have_gitlab_http_status(:forbidden) + expect(json_response).to have_key('message') + expect(response.headers.to_h) + .not_to include('Gitlab-Workhorse-Send-Data' => /artifacts-entry/) + end + end + + context 'when project is public with non public artifacts' do + let(:job) { create(:ci_build, :artifacts, :non_public_artifacts, pipeline: pipeline, user: api_user) } + let(:visibility_level) { Gitlab::VisibilityLevel::PUBLIC } + let(:public_builds) { true } + + it 'rejects access and hides existence of artifacts', :sidekiq_might_not_need_inline do + get_artifact_file(artifact) + + expect(response).to have_gitlab_http_status(:forbidden) + expect(json_response).to have_key('message') + expect(response.headers.to_h) + .not_to include('Gitlab-Workhorse-Send-Data' => /artifacts-entry/) + end + + context 'with the non_public_artifacts feature flag disabled' do + before do + stub_feature_flags(non_public_artifacts: false) + end + + it 'allows access to artifacts', :sidekiq_might_not_need_inline do + get_artifact_file(artifact) + + expect(response).to have_gitlab_http_status(:ok) + end + end + end + + context 'when project is private' do + let(:visibility_level) { Gitlab::VisibilityLevel::PRIVATE } + let(:public_builds) { true } + + it 'rejects access and hides existence of artifacts' do + expect(response).to have_gitlab_http_status(:not_found) + expect(json_response).to have_key('message') + expect(response.headers.to_h) + .not_to include('Gitlab-Workhorse-Send-Data' => /artifacts-entry/) + end + end + end + + context 'when user is authorized' do + let(:visibility_level) { Gitlab::VisibilityLevel::PRIVATE } + let(:public_builds) { true } + + it 'returns a specific artifact file for a valid path', :sidekiq_might_not_need_inline do + expect(Gitlab::Workhorse) + .to receive(:send_artifacts_entry) + .and_call_original + + get_artifact_file(artifact) + + expect(response).to have_gitlab_http_status(:ok) + expect(response.headers.to_h) + .to include('Content-Type' => 'application/json', + 'Gitlab-Workhorse-Send-Data' => /artifacts-entry/) + expect(response.parsed_body).to be_empty + end + end + + context 'with branch name containing slash' do + before do + pipeline.reload + pipeline.update!(ref: 'improve/awesome', + sha: project.commit('improve/awesome').sha) + end + + it 'returns a specific artifact file for a valid path', :sidekiq_might_not_need_inline do + get_artifact_file(artifact, 'improve/awesome') + + expect(response).to have_gitlab_http_status(:ok) + expect(response.headers.to_h) + .to include('Content-Type' => 'application/json', + 'Gitlab-Workhorse-Send-Data' => /artifacts-entry/) + end + end + + context 'non-existing job' do + shared_examples 'not found' do + it { expect(response).to have_gitlab_http_status(:not_found) } + end + + context 'has no such ref' do + before do + get_artifact_file('some/artifact', 'wrong-ref') + end + + it_behaves_like 'not found' + end + + context 'has no such job' do + before do + get_artifact_file('some/artifact', pipeline.ref, 'wrong-job-name') + end + + it_behaves_like 'not found' + end + end + end + + context 'when job does not have artifacts' do + let(:job) { create(:ci_build, pipeline: pipeline, user: api_user) } + + it 'does not return job artifact file' do + get_artifact_file('some/artifact') + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + def get_artifact_file(artifact_path, ref = pipeline.ref, job_name = job.name) + get api("/projects/#{project.id}/jobs/artifacts/#{ref}/raw/#{artifact_path}", api_user), params: { job: job_name } + end + end + + describe 'POST /projects/:id/jobs/:job_id/artifacts/keep' do + before do + post api("/projects/#{project.id}/jobs/#{job.id}/artifacts/keep", user) + end + + context 'artifacts did not expire' do + let(:job) do + create(:ci_build, :trace_artifact, :artifacts, :success, + project: project, pipeline: pipeline, artifacts_expire_at: Time.now + 7.days) + end + + it 'keeps artifacts' do + expect(response).to have_gitlab_http_status(:ok) + expect(job.reload.artifacts_expire_at).to be_nil + end + end + + context 'no artifacts' do + let(:job) { create(:ci_build, project: project, pipeline: pipeline) } + + it 'responds with not found' do + expect(response).to have_gitlab_http_status(:not_found) + end + end + end +end diff --git a/spec/requests/api/ci/jobs_spec.rb b/spec/requests/api/ci/jobs_spec.rb index 410020b68cd..7c85cbc31a5 100644 --- a/spec/requests/api/ci/jobs_spec.rb +++ b/spec/requests/api/ci/jobs_spec.rb @@ -428,584 +428,41 @@ RSpec.describe API::Ci::Jobs do end end - context 'when trace artifact record exists with no stored file', :skip_before_request do - before do - create(:ci_job_artifact, :unarchived_trace_artifact, job: job, project: job.project) - end - - it 'returns no artifacts nor trace data' do + context 'when job succeeded' do + it 'does not return failure_reason' do get api("/projects/#{project.id}/jobs/#{job.id}", api_user) - expect(response).to have_gitlab_http_status(:ok) - expect(json_response['artifacts']).to be_an Array - expect(json_response['artifacts'].size).to eq(1) - expect(json_response['artifacts'][0]['file_type']).to eq('trace') - expect(json_response['artifacts'][0]['filename']).to eq('job.log') - end - end - end - - describe 'DELETE /projects/:id/jobs/:job_id/artifacts' do - let!(:job) { create(:ci_build, :artifacts, pipeline: pipeline, user: api_user) } - - before do - delete api("/projects/#{project.id}/jobs/#{job.id}/artifacts", api_user) - end - - context 'when user is anonymous' do - let(:api_user) { nil } - - it 'does not delete artifacts' do - expect(job.job_artifacts.size).to eq 2 - end - - it 'returns status 401 (unauthorized)' do - expect(response).to have_gitlab_http_status(:unauthorized) - end - end - - context 'with developer' do - it 'does not delete artifacts' do - expect(job.job_artifacts.size).to eq 2 - end - - it 'returns status 403 (forbidden)' do - expect(response).to have_gitlab_http_status(:forbidden) - end - end - - context 'with authorized user' do - let(:maintainer) { create(:project_member, :maintainer, project: project).user } - let!(:api_user) { maintainer } - - it 'deletes artifacts' do - expect(job.job_artifacts.size).to eq 0 - end - - it 'returns status 204 (no content)' do - expect(response).to have_gitlab_http_status(:no_content) + expect(json_response).not_to include('failure_reason') end end - end - - describe 'GET /projects/:id/jobs/:job_id/artifacts/:artifact_path' do - context 'when job has artifacts' do - let(:job) { create(:ci_build, :artifacts, pipeline: pipeline) } - - let(:artifact) do - 'other_artifacts_0.1.2/another-subdirectory/banana_sample.gif' - end - - context 'when user is anonymous' do - let(:api_user) { nil } - - context 'when project is public' do - it 'allows to access artifacts' do - project.update_column(:visibility_level, - Gitlab::VisibilityLevel::PUBLIC) - project.update_column(:public_builds, true) - - get_artifact_file(artifact) - - expect(response).to have_gitlab_http_status(:ok) - end - end - - context 'when project is public with artifacts that are non public' do - let(:job) { create(:ci_build, :artifacts, :non_public_artifacts, pipeline: pipeline) } - - it 'rejects access to artifacts' do - project.update_column(:visibility_level, - Gitlab::VisibilityLevel::PUBLIC) - project.update_column(:public_builds, true) - - get_artifact_file(artifact) - - expect(response).to have_gitlab_http_status(:forbidden) - end - - context 'with the non_public_artifacts feature flag disabled' do - before do - stub_feature_flags(non_public_artifacts: false) - end - - it 'allows access to artifacts' do - project.update_column(:visibility_level, - Gitlab::VisibilityLevel::PUBLIC) - project.update_column(:public_builds, true) - - get_artifact_file(artifact) - - expect(response).to have_gitlab_http_status(:ok) - end - end - end - - context 'when project is public with builds access disabled' do - it 'rejects access to artifacts' do - project.update_column(:visibility_level, - Gitlab::VisibilityLevel::PUBLIC) - project.update_column(:public_builds, false) - - get_artifact_file(artifact) - - expect(response).to have_gitlab_http_status(:forbidden) - end - end - - context 'when project is private' do - it 'rejects access and hides existence of artifacts' do - project.update_column(:visibility_level, - Gitlab::VisibilityLevel::PRIVATE) - project.update_column(:public_builds, true) - - get_artifact_file(artifact) - - expect(response).to have_gitlab_http_status(:not_found) - end - end - end - context 'when user is authorized' do - it 'returns a specific artifact file for a valid path' do - expect(Gitlab::Workhorse) - .to receive(:send_artifacts_entry) - .and_call_original - - get_artifact_file(artifact) - - expect(response).to have_gitlab_http_status(:ok) - expect(response.headers.to_h) - .to include('Content-Type' => 'application/json', - 'Gitlab-Workhorse-Send-Data' => /artifacts-entry/) - expect(response.parsed_body).to be_empty - end - - context 'when artifacts are locked' do - it 'allows access to expired artifact' do - pipeline.artifacts_locked! - job.update!(artifacts_expire_at: Time.now - 7.days) - - get_artifact_file(artifact) - - expect(response).to have_gitlab_http_status(:ok) - end - end - end - end - - context 'when job does not have artifacts' do - it 'does not return job artifact file' do - get_artifact_file('some/artifact') - - expect(response).to have_gitlab_http_status(:not_found) - end - end - - def get_artifact_file(artifact_path) - get api("/projects/#{project.id}/jobs/#{job.id}/" \ - "artifacts/#{artifact_path}", api_user) - end - end - - describe 'GET /projects/:id/jobs/:job_id/artifacts' do - shared_examples 'downloads artifact' 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 - - it 'returns specific job artifacts' do - expect(response).to have_gitlab_http_status(:ok) - expect(response.headers.to_h).to include(download_headers) - expect(response.body).to match_file(job.artifacts_file.file.file) - end - end - - context 'normal authentication' do - context 'job with artifacts' do - context 'when artifacts are stored locally' do - let(:job) { create(:ci_build, :artifacts, pipeline: pipeline) } - - before do - get api("/projects/#{project.id}/jobs/#{job.id}/artifacts", api_user) - end - - context 'authorized user' do - it_behaves_like 'downloads artifact' - end - - context 'unauthorized user' do - let(:api_user) { nil } - - it 'does not return specific job artifacts' do - expect(response).to have_gitlab_http_status(:not_found) - end - end - end - - context 'when artifacts are stored remotely' do - let(:proxy_download) { false } - let(:job) { create(:ci_build, pipeline: pipeline) } - let(:artifact) { create(:ci_job_artifact, :archive, :remote_store, job: job) } - - before do - stub_artifacts_object_storage(proxy_download: proxy_download) - - artifact - job.reload - - get api("/projects/#{project.id}/jobs/#{job.id}/artifacts", api_user) - end - - context 'when proxy download is enabled' do - let(:proxy_download) { true } - - it 'responds with the workhorse send-url' do - expect(response.headers[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("send-url:") - end - end - - context 'when proxy download is disabled' do - it 'returns location redirect' do - expect(response).to have_gitlab_http_status(:found) - end - end - - context 'authorized user' do - it 'returns the file remote URL' do - expect(response).to redirect_to(artifact.file.url) - end - end - - context 'unauthorized user' do - let(:api_user) { nil } - - it 'does not return specific job artifacts' do - expect(response).to have_gitlab_http_status(:not_found) - end - end - end - - context 'when public project guest and artifacts are non public' do - let(:api_user) { guest } - let(:job) { create(:ci_build, :artifacts, :non_public_artifacts, pipeline: pipeline) } - - before do - project.update_column(:visibility_level, - Gitlab::VisibilityLevel::PUBLIC) - project.update_column(:public_builds, true) - get api("/projects/#{project.id}/jobs/#{job.id}/artifacts", api_user) - end - - it 'rejects access and hides existence of artifacts' do - expect(response).to have_gitlab_http_status(:forbidden) - end - - context 'with the non_public_artifacts feature flag disabled' do - before do - stub_feature_flags(non_public_artifacts: false) - get api("/projects/#{project.id}/jobs/#{job.id}/artifacts", api_user) - end - - it 'allows access to artifacts' do - expect(response).to have_gitlab_http_status(:ok) - end - end - end - - it 'does not return job artifacts if not uploaded' do - get api("/projects/#{project.id}/jobs/#{job.id}/artifacts", api_user) - - expect(response).to have_gitlab_http_status(:not_found) - end - end - end - end - - describe 'GET /projects/:id/artifacts/:ref_name/download?job=name' do - let(:api_user) { reporter } - let(:job) { create(:ci_build, :artifacts, pipeline: pipeline, user: api_user) } - - before do - stub_artifacts_object_storage - job.success - end - - def get_for_ref(ref = pipeline.ref, job_name = job.name) - get api("/projects/#{project.id}/jobs/artifacts/#{ref}/download", api_user), params: { job: job_name } - end - - context 'when not logged in' do - let(:api_user) { nil } - - before do - get_for_ref - end - - it 'does not find a resource in a private project' do - expect(project).to be_private - expect(response).to have_gitlab_http_status(:not_found) - end - end - - context 'when logging as guest' do - let(:api_user) { guest } - - before do - get_for_ref - end - - it 'gives 403' do - expect(response).to have_gitlab_http_status(:forbidden) - end - end - - context 'non-existing job' do - shared_examples 'not found' do - it { expect(response).to have_gitlab_http_status(:not_found) } - end - - context 'has no such ref' do - before do - get_for_ref('TAIL') - end - - it_behaves_like 'not found' - end - - context 'has no such job' do - before do - get_for_ref(pipeline.ref, 'NOBUILD') - end - - it_behaves_like 'not found' - end - end - - context 'find proper job' do - let(:job_with_artifacts) { job } - - shared_examples 'a valid file' do - context 'when artifacts are stored locally', :sidekiq_might_not_need_inline do - let(:download_headers) do - { 'Content-Transfer-Encoding' => 'binary', - 'Content-Disposition' => - %Q(attachment; filename="#{job_with_artifacts.artifacts_file.filename}"; filename*=UTF-8''#{job.artifacts_file.filename}) } - end - - it { expect(response).to have_gitlab_http_status(:ok) } - it { expect(response.headers.to_h).to include(download_headers) } - end - - context 'when artifacts are stored remotely' do - let(:job) { create(:ci_build, pipeline: pipeline, user: api_user) } - let!(:artifact) { create(:ci_job_artifact, :archive, :remote_store, job: job) } - - before do - job.reload - - get api("/projects/#{project.id}/jobs/#{job.id}/artifacts", api_user) - end - - it 'returns location redirect' do - expect(response).to have_gitlab_http_status(:found) - end - end - end - - context 'with regular branch' do - before do - pipeline.reload - pipeline.update!(ref: 'master', - sha: project.commit('master').sha) - - get_for_ref('master') - end - - it_behaves_like 'a valid file' - end - - context 'with branch name containing slash' do - before do - pipeline.reload - pipeline.update!(ref: 'improve/awesome', sha: project.commit('improve/awesome').sha) - get_for_ref('improve/awesome') - end - - it_behaves_like 'a valid file' + context 'when job failed' do + let(:job) do + create(:ci_build, :failed, :tags, pipeline: pipeline) end - context 'with job name in a child pipeline' do - let(:child_pipeline) { create(:ci_pipeline, child_of: pipeline) } - let!(:child_job) { create(:ci_build, :artifacts, :success, name: 'rspec', pipeline: child_pipeline) } - let(:job_with_artifacts) { child_job } - - before do - get_for_ref('master', child_job.name) - end + it 'returns failure_reason' do + get api("/projects/#{project.id}/jobs/#{job.id}", api_user) - it_behaves_like 'a valid file' + expect(json_response).to include('failure_reason') end end - end - - describe 'GET id/jobs/artifacts/:ref_name/raw/*artifact_path?job=name' do - context 'when job has artifacts' do - let(:job) { create(:ci_build, :artifacts, pipeline: pipeline, user: api_user) } - let(:artifact) { 'other_artifacts_0.1.2/another-subdirectory/banana_sample.gif' } - let(:visibility_level) { Gitlab::VisibilityLevel::PUBLIC } - let(:public_builds) { true } + context 'when trace artifact record exists with no stored file', :skip_before_request do before do - stub_artifacts_object_storage - job.success - - project.update!(visibility_level: visibility_level, - public_builds: public_builds) - - get_artifact_file(artifact) - end - - context 'when user is anonymous' do - let(:api_user) { nil } - - context 'when project is public' do - let(:visibility_level) { Gitlab::VisibilityLevel::PUBLIC } - let(:public_builds) { true } - - it 'allows to access artifacts', :sidekiq_might_not_need_inline do - expect(response).to have_gitlab_http_status(:ok) - expect(response.headers.to_h) - .to include('Content-Type' => 'application/json', - 'Gitlab-Workhorse-Send-Data' => /artifacts-entry/) - end - end - - context 'when project is public with builds access disabled' do - let(:visibility_level) { Gitlab::VisibilityLevel::PUBLIC } - let(:public_builds) { false } - - it 'rejects access to artifacts' do - expect(response).to have_gitlab_http_status(:forbidden) - expect(json_response).to have_key('message') - expect(response.headers.to_h) - .not_to include('Gitlab-Workhorse-Send-Data' => /artifacts-entry/) - end - end - - context 'when project is public with non public artifacts' do - let(:job) { create(:ci_build, :artifacts, :non_public_artifacts, pipeline: pipeline, user: api_user) } - let(:visibility_level) { Gitlab::VisibilityLevel::PUBLIC } - let(:public_builds) { true } - - it 'rejects access and hides existence of artifacts', :sidekiq_might_not_need_inline do - get_artifact_file(artifact) - - expect(response).to have_gitlab_http_status(:forbidden) - expect(json_response).to have_key('message') - expect(response.headers.to_h) - .not_to include('Gitlab-Workhorse-Send-Data' => /artifacts-entry/) - end - - context 'with the non_public_artifacts feature flag disabled' do - before do - stub_feature_flags(non_public_artifacts: false) - end - - it 'allows access to artifacts', :sidekiq_might_not_need_inline do - get_artifact_file(artifact) - - expect(response).to have_gitlab_http_status(:ok) - end - end - end - - context 'when project is private' do - let(:visibility_level) { Gitlab::VisibilityLevel::PRIVATE } - let(:public_builds) { true } - - it 'rejects access and hides existence of artifacts' do - expect(response).to have_gitlab_http_status(:not_found) - expect(json_response).to have_key('message') - expect(response.headers.to_h) - .not_to include('Gitlab-Workhorse-Send-Data' => /artifacts-entry/) - end - end - end - - context 'when user is authorized' do - let(:visibility_level) { Gitlab::VisibilityLevel::PRIVATE } - let(:public_builds) { true } - - it 'returns a specific artifact file for a valid path', :sidekiq_might_not_need_inline do - expect(Gitlab::Workhorse) - .to receive(:send_artifacts_entry) - .and_call_original - - get_artifact_file(artifact) - - expect(response).to have_gitlab_http_status(:ok) - expect(response.headers.to_h) - .to include('Content-Type' => 'application/json', - 'Gitlab-Workhorse-Send-Data' => /artifacts-entry/) - expect(response.parsed_body).to be_empty - end - end - - context 'with branch name containing slash' do - before do - pipeline.reload - pipeline.update!(ref: 'improve/awesome', - sha: project.commit('improve/awesome').sha) - end - - it 'returns a specific artifact file for a valid path', :sidekiq_might_not_need_inline do - get_artifact_file(artifact, 'improve/awesome') - - expect(response).to have_gitlab_http_status(:ok) - expect(response.headers.to_h) - .to include('Content-Type' => 'application/json', - 'Gitlab-Workhorse-Send-Data' => /artifacts-entry/) - end - end - - context 'non-existing job' do - shared_examples 'not found' do - it { expect(response).to have_gitlab_http_status(:not_found) } - end - - context 'has no such ref' do - before do - get_artifact_file('some/artifact', 'wrong-ref') - end - - it_behaves_like 'not found' - end - - context 'has no such job' do - before do - get_artifact_file('some/artifact', pipeline.ref, 'wrong-job-name') - end - - it_behaves_like 'not found' - end + create(:ci_job_artifact, :unarchived_trace_artifact, job: job, project: job.project) end - end - context 'when job does not have artifacts' do - let(:job) { create(:ci_build, pipeline: pipeline, user: api_user) } - - it 'does not return job artifact file' do - get_artifact_file('some/artifact') + it 'returns no artifacts nor trace data' do + get api("/projects/#{project.id}/jobs/#{job.id}", api_user) - expect(response).to have_gitlab_http_status(:not_found) + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['artifacts']).to be_an Array + expect(json_response['artifacts'].size).to eq(1) + expect(json_response['artifacts'][0]['file_type']).to eq('trace') + expect(json_response['artifacts'][0]['filename']).to eq('job.log') end end - - def get_artifact_file(artifact_path, ref = pipeline.ref, job_name = job.name) - get api("/projects/#{project.id}/jobs/artifacts/#{ref}/raw/#{artifact_path}", api_user), params: { job: job_name } - end end describe 'GET /projects/:id/jobs/:job_id/trace' do @@ -1249,32 +706,6 @@ RSpec.describe API::Ci::Jobs do end end - describe 'POST /projects/:id/jobs/:job_id/artifacts/keep' do - before do - post api("/projects/#{project.id}/jobs/#{job.id}/artifacts/keep", user) - end - - context 'artifacts did not expire' do - let(:job) do - create(:ci_build, :trace_artifact, :artifacts, :success, - project: project, pipeline: pipeline, artifacts_expire_at: Time.now + 7.days) - end - - it 'keeps artifacts' do - expect(response).to have_gitlab_http_status(:ok) - expect(job.reload.artifacts_expire_at).to be_nil - end - end - - context 'no artifacts' do - let(:job) { create(:ci_build, project: project, pipeline: pipeline) } - - it 'responds with not found' do - expect(response).to have_gitlab_http_status(:not_found) - end - end - end - describe 'POST /projects/:id/jobs/:job_id/play' do before do post api("/projects/#{project.id}/jobs/#{job.id}/play", api_user) diff --git a/spec/requests/api/ci/pipelines_spec.rb b/spec/requests/api/ci/pipelines_spec.rb index 7ae350885f4..13838cffd76 100644 --- a/spec/requests/api/ci/pipelines_spec.rb +++ b/spec/requests/api/ci/pipelines_spec.rb @@ -33,6 +33,7 @@ RSpec.describe API::Ci::Pipelines do expect(json_response).to be_an Array expect(json_response.first['sha']).to match(/\A\h{40}\z/) expect(json_response.first['id']).to eq pipeline.id + expect(json_response.first['iid']).to eq pipeline.iid expect(json_response.first['web_url']).to be_present end @@ -40,7 +41,7 @@ RSpec.describe API::Ci::Pipelines do it 'includes pipeline source' do get api("/projects/#{project.id}/pipelines", user) - expect(json_response.first.keys).to contain_exactly(*%w[id project_id sha ref status web_url created_at updated_at source]) + expect(json_response.first.keys).to contain_exactly(*%w[id iid project_id sha ref status web_url created_at updated_at source]) end end @@ -840,7 +841,7 @@ RSpec.describe API::Ci::Pipelines do it 'exposes the coverage' do get api("/projects/#{project.id}/pipelines/#{pipeline.id}", user) - expect(json_response["coverage"].to_i).to eq(30) + expect(json_response["coverage"]).to eq('30.00') 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 index 195aac2e5f0..f627f207d98 100644 --- a/spec/requests/api/ci/runner/jobs_artifacts_spec.rb +++ b/spec/requests/api/ci/runner/jobs_artifacts_spec.rb @@ -131,8 +131,8 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do let(:send_request) { subject } end - it 'updates runner info' do - expect { subject }.to change { runner.reload.contacted_at } + it "doesn't update runner info" do + expect { subject }.not_to change { runner.reload.contacted_at } end shared_examples 'authorizes local file' do @@ -280,8 +280,8 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do end end - it 'updates runner info' do - expect { upload_artifacts(file_upload, headers_with_token) }.to change { runner.reload.contacted_at } + it "doesn't update runner info" do + expect { upload_artifacts(file_upload, headers_with_token) }.not_to change { runner.reload.contacted_at } end context 'when the artifact is too large' do @@ -812,8 +812,8 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do let(:send_request) { download_artifact } end - it 'updates runner info' do - expect { download_artifact }.to change { runner.reload.contacted_at } + it "doesn't update runner info" do + expect { download_artifact }.not_to change { runner.reload.contacted_at } end context 'when job has artifacts' do diff --git a/spec/requests/api/ci/runner/jobs_request_post_spec.rb b/spec/requests/api/ci/runner/jobs_request_post_spec.rb index fdf1a278d4c..68f7581bf06 100644 --- a/spec/requests/api/ci/runner/jobs_request_post_spec.rb +++ b/spec/requests/api/ci/runner/jobs_request_post_spec.rb @@ -833,8 +833,8 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do let(:expected_params) { { project: project.full_path, client_id: "runner/#{runner.id}" } } end - it_behaves_like 'not executing any extra queries for the application context', 2 do - # Extra queries: Project, Route + it_behaves_like 'not executing any extra queries for the application context', 3 do + # Extra queries: Project, Route, RunnerProject let(:subject_proc) { proc { request_job } } end end diff --git a/spec/requests/api/ci/runner/runners_post_spec.rb b/spec/requests/api/ci/runner/runners_post_spec.rb index b3a7d591c93..a51d8b458f8 100644 --- a/spec/requests/api/ci/runner/runners_post_spec.rb +++ b/spec/requests/api/ci/runner/runners_post_spec.rb @@ -98,33 +98,14 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do before do create(:ci_runner, runner_type: :project_type, projects: [project], contacted_at: 1.second.ago) create(:plan_limits, :default_plan, ci_registered_project_runners: 1) - - skip_default_enabled_yaml_check - stub_feature_flags(ci_runner_limits_override: ci_runner_limits_override) - end - - context 'with ci_runner_limits_override FF disabled' do - let(:ci_runner_limits_override) { false } - - it 'does not create runner' do - request - - expect(response).to have_gitlab_http_status(:bad_request) - expect(json_response['message']).to include('runner_projects.base' => ['Maximum number of ci registered project runners (1) exceeded']) - expect(project.runners.reload.size).to eq(1) - end end - context 'with ci_runner_limits_override FF enabled' do - let(:ci_runner_limits_override) { true } - - it 'creates runner' do - request + it 'does not create runner' do + request - expect(response).to have_gitlab_http_status(:created) - expect(json_response['message']).to be_nil - expect(project.runners.reload.size).to eq(2) - end + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['message']).to include('runner_projects.base' => ['Maximum number of ci registered project runners (1) exceeded']) + expect(project.runners.reload.size).to eq(1) end end @@ -132,9 +113,6 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do before do create(:ci_runner, runner_type: :project_type, projects: [project], created_at: 14.months.ago, contacted_at: 13.months.ago) create(:plan_limits, :default_plan, ci_registered_project_runners: 1) - - skip_default_enabled_yaml_check - stub_feature_flags(ci_runner_limits_override: false) end it 'creates runner' do @@ -204,33 +182,14 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do before do create(:ci_runner, runner_type: :group_type, groups: [group], contacted_at: nil, created_at: 1.month.ago) create(:plan_limits, :default_plan, ci_registered_group_runners: 1) - - skip_default_enabled_yaml_check - stub_feature_flags(ci_runner_limits_override: ci_runner_limits_override) - end - - context 'with ci_runner_limits_override FF disabled' do - let(:ci_runner_limits_override) { false } - - it 'does not create runner' do - request - - expect(response).to have_gitlab_http_status(:bad_request) - expect(json_response['message']).to include('runner_namespaces.base' => ['Maximum number of ci registered group runners (1) exceeded']) - expect(group.runners.reload.size).to eq(1) - end end - context 'with ci_runner_limits_override FF enabled' do - let(:ci_runner_limits_override) { true } - - it 'creates runner' do - request + it 'does not create runner' do + request - expect(response).to have_gitlab_http_status(:created) - expect(json_response['message']).to be_nil - expect(group.runners.reload.size).to eq(2) - end + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['message']).to include('runner_namespaces.base' => ['Maximum number of ci registered group runners (1) exceeded']) + expect(group.runners.reload.size).to eq(1) end end @@ -239,9 +198,6 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do create(:ci_runner, runner_type: :group_type, groups: [group], created_at: 4.months.ago, contacted_at: 3.months.ago) create(:ci_runner, runner_type: :group_type, groups: [group], contacted_at: nil, created_at: 4.months.ago) create(:plan_limits, :default_plan, ci_registered_group_runners: 1) - - skip_default_enabled_yaml_check - stub_feature_flags(ci_runner_limits_override: false) end it 'creates runner' do diff --git a/spec/requests/api/ci/runners_spec.rb b/spec/requests/api/ci/runners_spec.rb index 6879dfc9572..6ca380a3cb9 100644 --- a/spec/requests/api/ci/runners_spec.rb +++ b/spec/requests/api/ci/runners_spec.rb @@ -254,6 +254,7 @@ RSpec.describe API::Ci::Runners do expect(response).to have_gitlab_http_status(:ok) expect(json_response['description']).to eq(shared_runner.description) expect(json_response['maximum_timeout']).to be_nil + expect(json_response['status']).to eq("not_connected") end end @@ -1101,31 +1102,13 @@ RSpec.describe API::Ci::Runners do context 'when it exceeds the application limits' do before do create(:plan_limits, :default_plan, ci_registered_project_runners: 1) - - skip_default_enabled_yaml_check - stub_feature_flags(ci_runner_limits_override: ci_runner_limits_override) end - context 'with ci_runner_limits_override FF disabled' do - let(:ci_runner_limits_override) { false } - - it 'does not enable specific runner' do - expect do - post api("/projects/#{project.id}/runners", admin), params: { runner_id: new_project_runner.id } - end.not_to change { project.runners.count } - expect(response).to have_gitlab_http_status(:bad_request) - end - end - - context 'with ci_runner_limits_override FF enabled' do - let(:ci_runner_limits_override) { true } - - it 'enables specific runner' do - expect do - post api("/projects/#{project.id}/runners", admin), params: { runner_id: new_project_runner.id } - end.to change { project.runners.count } - expect(response).to have_gitlab_http_status(:created) - end + it 'does not enable specific runner' do + expect do + post api("/projects/#{project.id}/runners", admin), params: { runner_id: new_project_runner.id } + end.not_to change { project.runners.count } + expect(response).to have_gitlab_http_status(:bad_request) end end end diff --git a/spec/requests/api/commit_statuses_spec.rb b/spec/requests/api/commit_statuses_spec.rb index 47bc3eb74a6..39be28d7427 100644 --- a/spec/requests/api/commit_statuses_spec.rb +++ b/spec/requests/api/commit_statuses_spec.rb @@ -14,8 +14,19 @@ RSpec.describe API::CommitStatuses do let(:get_url) { "/projects/#{project.id}/repository/commits/#{sha}/statuses" } context 'ci commit exists' do - let!(:master) { project.ci_pipelines.create!(source: :push, sha: commit.id, ref: 'master', protected: false) } - let!(:develop) { project.ci_pipelines.create!(source: :push, sha: commit.id, ref: 'develop', protected: false) } + let!(:master) do + project.ci_pipelines.build(source: :push, sha: commit.id, ref: 'master', protected: false).tap do |p| + p.ensure_project_iid! # Necessary to avoid cross-database modification error + p.save! + end + end + + let!(:develop) do + project.ci_pipelines.build(source: :push, sha: commit.id, ref: 'develop', protected: false).tap do |p| + p.ensure_project_iid! # Necessary to avoid cross-database modification error + p.save! + end + end context "reporter user" do let(:statuses_id) { json_response.map { |status| status['id'] } } @@ -131,7 +142,7 @@ RSpec.describe API::CommitStatuses do %w[pending running success failed canceled].each do |status| context "for #{status}" do context 'when pipeline for sha does not exists' do - it 'creates commit status' do + it 'creates commit status and sets pipeline iid' do post api(post_url, developer), params: { state: status } expect(response).to have_gitlab_http_status(:created) @@ -145,6 +156,8 @@ RSpec.describe API::CommitStatuses do if status == 'failed' expect(CommitStatus.find(json_response['id'])).to be_api_failure end + + expect(::Ci::Pipeline.last.iid).not_to be_nil end end end @@ -308,8 +321,19 @@ RSpec.describe API::CommitStatuses do end context 'when a pipeline id is specified' do - let!(:first_pipeline) { project.ci_pipelines.create!(source: :push, sha: commit.id, ref: 'master', status: 'created') } - let!(:other_pipeline) { project.ci_pipelines.create!(source: :push, sha: commit.id, ref: 'master', status: 'created') } + let!(:first_pipeline) do + project.ci_pipelines.build(source: :push, sha: commit.id, ref: 'master', status: 'created').tap do |p| + p.ensure_project_iid! # Necessary to avoid cross-database modification error + p.save! + end + end + + let!(:other_pipeline) do + project.ci_pipelines.build(source: :push, sha: commit.id, ref: 'master', status: 'created').tap do |p| + p.ensure_project_iid! # Necessary to avoid cross-database modification error + p.save! + end + end subject do post api(post_url, developer), params: { diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb index 1d76c281dee..1e587480fd9 100644 --- a/spec/requests/api/commits_spec.rb +++ b/spec/requests/api/commits_spec.rb @@ -377,11 +377,11 @@ RSpec.describe API::Commits do end context 'when using warden' do - it 'increments usage counters', :clean_gitlab_redis_shared_state do + it 'increments usage counters', :clean_gitlab_redis_sessions do session_id = Rack::Session::SessionId.new('6919a6f1bb119dd7396fadc38fd18d0d') session_hash = { 'warden.user.user.key' => [[user.id], user.encrypted_password[0, 29]] } - Gitlab::Redis::SharedState.with do |redis| + Gitlab::Redis::Sessions.with do |redis| redis.set("session:gitlab:#{session_id.private_id}", Marshal.dump(session_hash)) end diff --git a/spec/requests/api/composer_packages_spec.rb b/spec/requests/api/composer_packages_spec.rb index e75725cacba..21b4634ce25 100644 --- a/spec/requests/api/composer_packages_spec.rb +++ b/spec/requests/api/composer_packages_spec.rb @@ -9,6 +9,10 @@ RSpec.describe API::ComposerPackages do let_it_be(:personal_access_token) { create(:personal_access_token, user: user) } let_it_be(:package_name) { 'package-name' } let_it_be(:project, reload: true) { create(:project, :custom_repo, files: { 'composer.json' => { name: package_name }.to_json }, group: group) } + let_it_be(:deploy_token_for_project) { create(:deploy_token, read_package_registry: true, write_package_registry: true) } + let_it_be(:project_deploy_token) { create(:project_deploy_token, deploy_token: deploy_token_for_project, project: project) } + let_it_be(:deploy_token_for_group) { create(:deploy_token, :group, read_package_registry: true, write_package_registry: true) } + let_it_be(:group_deploy_token) { create(:group_deploy_token, deploy_token: deploy_token_for_group, group: group) } let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace, user: user } } let(:headers) { {} } @@ -92,6 +96,8 @@ RSpec.describe API::ComposerPackages do group.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE) end + it_behaves_like 'Composer access with deploy tokens' + 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 @@ -162,6 +168,8 @@ RSpec.describe API::ComposerPackages do it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member] end end + + it_behaves_like 'Composer access with deploy tokens' end it_behaves_like 'rejects Composer access with unknown group id' @@ -219,6 +227,8 @@ RSpec.describe API::ComposerPackages do end end end + + it_behaves_like 'Composer access with deploy tokens' end it_behaves_like 'rejects Composer access with unknown group id' @@ -265,6 +275,8 @@ RSpec.describe API::ComposerPackages do it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member] end end + + it_behaves_like 'Composer access with deploy tokens' end it_behaves_like 'rejects Composer access with unknown group id' @@ -308,6 +320,8 @@ RSpec.describe API::ComposerPackages do it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member] end end + + it_behaves_like 'Composer publish with deploy tokens' end it_behaves_like 'rejects Composer access with unknown project id' diff --git a/spec/requests/api/conan_project_packages_spec.rb b/spec/requests/api/conan_project_packages_spec.rb index da054ed2e96..c108f2efaaf 100644 --- a/spec/requests/api/conan_project_packages_spec.rb +++ b/spec/requests/api/conan_project_packages_spec.rb @@ -1,10 +1,11 @@ # frozen_string_literal: true require 'spec_helper' -RSpec.describe API::ConanProjectPackages, quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/326194' do +RSpec.describe API::ConanProjectPackages do include_context 'conan api setup' let(:project_id) { project.id } + let(:snowplow_standard_context_params) { { user: user, project: project, namespace: project.namespace } } describe 'GET /api/v4/projects/:id/packages/conan/v1/ping' do let(:url) { "/projects/#{project.id}/packages/conan/v1/ping" } @@ -92,7 +93,7 @@ RSpec.describe API::ConanProjectPackages, quarantine: 'https://gitlab.com/gitlab end end - context 'file download endpoints' do + context 'file download endpoints', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/326194' do include_context 'conan file download endpoints' describe 'GET /api/v4/projects/:id/packages/conan/v1/files/:package_name/package_version/:package_username/:package_channel/ diff --git a/spec/requests/api/dependency_proxy_spec.rb b/spec/requests/api/dependency_proxy_spec.rb index 2837d1c02c4..067852ef1e9 100644 --- a/spec/requests/api/dependency_proxy_spec.rb +++ b/spec/requests/api/dependency_proxy_spec.rb @@ -3,8 +3,6 @@ require 'spec_helper' RSpec.describe API::DependencyProxy, api: true do - include ExclusiveLeaseHelpers - let_it_be(:user) { create(:user) } let_it_be(:blob) { create(:dependency_proxy_blob )} let_it_be(:group, reload: true) { blob.group } @@ -20,11 +18,8 @@ RSpec.describe API::DependencyProxy, api: true do shared_examples 'responding to purge requests' do context 'with feature available and enabled' do - let_it_be(:lease_key) { "dependency_proxy:delete_group_blobs:#{group.id}" } - context 'an admin user' do it 'deletes the blobs and returns no content' do - stub_exclusive_lease(lease_key, timeout: 1.hour) expect(PurgeDependencyProxyCacheWorker).to receive(:perform_async) subject @@ -32,23 +27,6 @@ RSpec.describe API::DependencyProxy, api: true do expect(response).to have_gitlab_http_status(:accepted) expect(response.body).to eq('202') end - - context 'called multiple times in one hour', :clean_gitlab_redis_shared_state do - it 'returns 409 with an error message' do - stub_exclusive_lease_taken(lease_key, timeout: 1.hour) - - subject - - expect(response).to have_gitlab_http_status(:conflict) - expect(response.body).to include('This request has already been made.') - end - - it 'executes service only for the first time' do - expect(PurgeDependencyProxyCacheWorker).to receive(:perform_async).once - - 2.times { subject } - end - end end context 'a non-admin' do diff --git a/spec/requests/api/error_tracking/collector_spec.rb b/spec/requests/api/error_tracking/collector_spec.rb index 21e2849fef0..573da862b57 100644 --- a/spec/requests/api/error_tracking/collector_spec.rb +++ b/spec/requests/api/error_tracking/collector_spec.rb @@ -24,7 +24,7 @@ RSpec.describe API::ErrorTracking::Collector do end RSpec.shared_examples 'successful request' do - it 'writes to the database and returns OK' do + it 'writes to the database and returns OK', :aggregate_failures do expect { subject }.to change { ErrorTracking::ErrorEvent.count }.by(1) expect(response).to have_gitlab_http_status(:ok) @@ -40,6 +40,8 @@ RSpec.describe API::ErrorTracking::Collector do subject { post api(url), params: params, headers: headers } + it_behaves_like 'successful request' + context 'error tracking feature is disabled' do before do setting.update!(enabled: false) @@ -109,8 +111,6 @@ RSpec.describe API::ErrorTracking::Collector do it_behaves_like 'successful request' end - - it_behaves_like 'successful request' end describe "POST /error_tracking/collector/api/:id/store" do @@ -165,6 +165,12 @@ RSpec.describe API::ErrorTracking::Collector do it_behaves_like 'successful request' end + context 'body contains nullbytes' do + let_it_be(:raw_event) { fixture_file('error_tracking/parsed_event_nullbytes.json') } + + it_behaves_like 'successful request' + end + context 'sentry_key as param and empty headers' do let(:url) { "/error_tracking/collector/api/#{project.id}/store?sentry_key=#{sentry_key}" } let(:headers) { {} } diff --git a/spec/requests/api/graphql/boards/board_list_issues_query_spec.rb b/spec/requests/api/graphql/boards/board_list_issues_query_spec.rb index 241c658441b..6324db0be4a 100644 --- a/spec/requests/api/graphql/boards/board_list_issues_query_spec.rb +++ b/spec/requests/api/graphql/boards/board_list_issues_query_spec.rb @@ -16,6 +16,7 @@ RSpec.describe 'get board lists' do let(:params) { '' } let(:board) { } + let(:confidential) { false } 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] } @@ -30,7 +31,7 @@ RSpec.describe 'get board lists' do nodes { lists { nodes { - issues(filters: {labelName: "#{label2.title}"}, first: 3) { + issues(filters: {labelName: "#{label2.title}", confidential: #{confidential}}, first: 3) { count nodes { #{all_graphql_fields_for('issues'.classify)} @@ -57,14 +58,15 @@ RSpec.describe 'get board lists' do 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, label2], relative_position: 9) } - let!(:issue2) { create(:issue, project: issue_project, labels: [label, label2], relative_position: 2) } - let!(:issue3) { create(:issue, project: issue_project, labels: [label, label2], relative_position: nil) } - let!(:issue4) { create(:issue, project: issue_project, labels: [label], relative_position: 9) } - let!(:issue5) { create(:issue, project: issue_project, labels: [label2], relative_position: 432) } - let!(:issue6) { create(:issue, project: issue_project, labels: [label, label2], relative_position: nil) } + let_it_be(:board) { create(:board, resource_parent: board_parent) } + let_it_be(:label_list) { create(:list, board: board, label: label, position: 10) } + let_it_be(:issue1) { create(:issue, project: issue_project, labels: [label, label2], relative_position: 9) } + let_it_be(:issue2) { create(:issue, project: issue_project, labels: [label, label2], relative_position: 2) } + let_it_be(:issue3) { create(:issue, project: issue_project, labels: [label, label2], relative_position: nil) } + let_it_be(:issue4) { create(:issue, project: issue_project, labels: [label], relative_position: 9) } + let_it_be(:issue5) { create(:issue, project: issue_project, labels: [label2], relative_position: 432) } + let_it_be(:issue6) { create(:issue, project: issue_project, labels: [label, label2], relative_position: nil) } + let_it_be(:issue7) { create(:issue, project: issue_project, labels: [label, label2], relative_position: 5, confidential: true) } context 'when the user does not have access to the board' do it 'returns nil' do @@ -90,23 +92,33 @@ RSpec.describe 'get board lists' do expect(issue_id).not_to include(issue6.id) expect(issue3.relative_position).to be_nil end + + context 'when filtering by confidential' do + let(:confidential) { true } + + it 'returns matching issue' do + expect(issue_titles).to match_array([issue7.title]) + expect(issue_relative_positions).not_to include(nil) + end + end end end describe 'for a project' do - let(:board_parent) { project } - let(:label) { project_label } - let(:label2) { project_label2 } - let(:issue_project) { project } + let_it_be(:board_parent) { project } + let_it_be(:label) { project_label } + let_it_be(:label2) { project_label2 } + let_it_be(: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) } + let_it_be(:board_parent) { group } + let_it_be(:label) { group_label } + let_it_be(:label2) { group_label2 } + + let_it_be(:issue_project) { create(:project, :private, group: group) } before do allow(board_parent).to receive(:multiple_issue_boards_available?).and_return(false) diff --git a/spec/requests/api/graphql/boards/board_list_query_spec.rb b/spec/requests/api/graphql/boards/board_list_query_spec.rb index dec7ca715f2..f01f7e87f10 100644 --- a/spec/requests/api/graphql/boards/board_list_query_spec.rb +++ b/spec/requests/api/graphql/boards/board_list_query_spec.rb @@ -12,6 +12,7 @@ RSpec.describe 'Querying a Board list' do let_it_be(:list) { create(:list, board: board, label: label) } let_it_be(:issue1) { create(:issue, project: project, labels: [label]) } let_it_be(:issue2) { create(:issue, project: project, labels: [label], assignees: [current_user]) } + let_it_be(:issue3) { create(:issue, project: project, labels: [label], confidential: true) } let(:filters) { {} } let(:query) do @@ -37,19 +38,33 @@ RSpec.describe 'Querying a Board list' do it { is_expected.to include({ 'issuesCount' => 2, 'title' => list.title }) } - context 'with matching issue filters' do - let(:filters) { { assigneeUsername: current_user.username } } + describe 'issue filters' do + context 'with matching assignee username issue filters' do + let(:filters) { { assigneeUsername: current_user.username } } - it 'filters issues metadata' do - is_expected.to include({ 'issuesCount' => 1, 'title' => list.title }) + it 'filters issues metadata' do + is_expected.to include({ 'issuesCount' => 1, 'title' => list.title }) + end end - end - context 'with unmatching issue filters' do - let(:filters) { { assigneeUsername: 'foo' } } + context 'with unmatching assignee username issue filters' do + let(:filters) { { assigneeUsername: 'foo' } } + + it 'filters issues metadata' do + is_expected.to include({ 'issuesCount' => 0, 'title' => list.title }) + end + end + + context 'when filtering by confidential' do + let(:filters) { { confidential: true } } + + before_all do + project.add_developer(current_user) + end - it 'filters issues metadata' do - is_expected.to include({ 'issuesCount' => 0, 'title' => list.title }) + it 'filters issues metadata' do + is_expected.to include({ 'issuesCount' => 1, 'title' => list.title }) + end end 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 ace8c59e82d..e8fb9daa43b 100644 --- a/spec/requests/api/graphql/boards/board_lists_query_spec.rb +++ b/spec/requests/api/graphql/boards/board_lists_query_spec.rb @@ -109,9 +109,15 @@ RSpec.describe 'get board lists' do it 'returns the correct list with issue count for matching issue filters' do label_list = create(:list, board: board, label: label, position: 10) create(:issue, project: project, labels: [label, label2]) + create(:issue, project: project, labels: [label, label2], confidential: true) create(:issue, project: project, labels: [label]) - post_graphql(query(id: global_id_of(label_list), issueFilters: { labelName: label2.title }), current_user: user) + post_graphql( + query( + id: global_id_of(label_list), + issueFilters: { labelName: label2.title, confidential: false } + ), current_user: user + ) aggregate_failures do list_node = lists_data[0]['node'] diff --git a/spec/requests/api/graphql/ci/jobs_spec.rb b/spec/requests/api/graphql/ci/jobs_spec.rb index b2f4801a083..3a1df3525ef 100644 --- a/spec/requests/api/graphql/ci/jobs_spec.rb +++ b/spec/requests/api/graphql/ci/jobs_spec.rb @@ -14,8 +14,8 @@ RSpec.describe 'Query.project.pipeline' do describe '.stages.groups.jobs' do let(:pipeline) do pipeline = create(:ci_pipeline, project: project, user: user) - stage = create(:ci_stage_entity, project: project, pipeline: pipeline, name: 'first') - create(:ci_build, stage_id: stage.id, pipeline: pipeline, name: 'my test job') + stage = create(:ci_stage_entity, project: project, pipeline: pipeline, name: 'first', position: 1) + create(:ci_build, stage_id: stage.id, pipeline: pipeline, name: 'my test job', scheduling_type: :stage) pipeline end @@ -44,13 +44,23 @@ RSpec.describe 'Query.project.pipeline' do name jobs { nodes { - detailedStatus { - id - } name needs { nodes { #{all_graphql_fields_for('CiBuildNeed')} } } + previousStageJobsOrNeeds { + nodes { + ... on CiBuildNeed { + #{all_graphql_fields_for('CiBuildNeed')} + } + ... on CiJob { + #{all_graphql_fields_for('CiJob')} + } + } + } + detailedStatus { + id + } pipeline { id } @@ -62,58 +72,61 @@ RSpec.describe 'Query.project.pipeline' do FIELDS end - context 'when there are build needs' do - before do - pipeline.statuses.each do |build| - create_list(:ci_build_need, 2, build: build) - end - end - - it 'reports the build needs' do - post_graphql(query, current_user: user) - - expect(jobs_graphql_data).to contain_exactly a_hash_including( - 'needs' => a_hash_including( - 'nodes' => contain_exactly( - a_hash_including('name' => String), - a_hash_including('name' => String) - ) - ) - ) - end - 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 - describe 'performance' do + context 'when there is more than one stage and job needs' do before do build_stage = create(:ci_stage_entity, position: 2, name: 'build', project: project, pipeline: pipeline) test_stage = create(:ci_stage_entity, position: 3, name: 'test', project: project, 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') - end - it 'can find the first stage' do - post_graphql(query, current_user: user, variables: first_n.with(1)) + create(:ci_build, pipeline: pipeline, name: 'docker 1 2', scheduling_type: :stage, stage: build_stage, stage_idx: build_stage.position) + create(:ci_build, pipeline: pipeline, name: 'docker 2 2', stage: build_stage, stage_idx: build_stage.position, scheduling_type: :dag) + create(:ci_build, pipeline: pipeline, name: 'rspec 1 2', scheduling_type: :stage, stage: test_stage, stage_idx: test_stage.position) + test_job = create(:ci_build, pipeline: pipeline, name: 'rspec 2 2', scheduling_type: :dag, stage: test_stage, stage_idx: test_stage.position) - expect(jobs_graphql_data).to contain_exactly(a_hash_including('name' => 'my test job')) + create(:ci_build_need, build: test_job, name: 'my test job') end - it 'reports the build needs and previous stages with no duplicates', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/346433' do + it 'reports the build needs and execution requirements', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/347290' do post_graphql(query, current_user: user) expect(jobs_graphql_data).to contain_exactly( - a_hash_including('name' => 'my test job'), - a_hash_including('name' => 'docker 1 2'), - a_hash_including('name' => 'docker 2 2'), - a_hash_including('name' => 'rspec 1 2'), - a_hash_including('name' => 'rspec 2 2') + a_hash_including( + 'name' => 'my test job', + 'needs' => { 'nodes' => [] }, + 'previousStageJobsOrNeeds' => { 'nodes' => [] } + ), + a_hash_including( + 'name' => 'docker 1 2', + 'needs' => { 'nodes' => [] }, + 'previousStageJobsOrNeeds' => { 'nodes' => [ + a_hash_including( 'name' => 'my test job' ) + ] } + ), + a_hash_including( + 'name' => 'docker 2 2', + 'needs' => { 'nodes' => [] }, + 'previousStageJobsOrNeeds' => { 'nodes' => [] } + ), + a_hash_including( + 'name' => 'rspec 1 2', + 'needs' => { 'nodes' => [] }, + 'previousStageJobsOrNeeds' => { 'nodes' => [ + a_hash_including('name' => 'docker 1 2'), + a_hash_including('name' => 'docker 2 2') + ] } + ), + a_hash_including( + 'name' => 'rspec 2 2', + 'needs' => { 'nodes' => [a_hash_including('name' => 'my test job')] }, + 'previousStageJobsOrNeeds' => { 'nodes' => [ + a_hash_including('name' => 'my test job' ) + ] } + ) ) end diff --git a/spec/requests/api/graphql/ci/pipelines_spec.rb b/spec/requests/api/graphql/ci/pipelines_spec.rb index 1f47f678898..95ddd0250e7 100644 --- a/spec/requests/api/graphql/ci/pipelines_spec.rb +++ b/spec/requests/api/graphql/ci/pipelines_spec.rb @@ -79,12 +79,13 @@ RSpec.describe 'Query.project(fullPath).pipelines' do create(:ci_build, pipeline: pipeline, stage_id: other_stage.id, name: 'linux: [baz]') end - it 'is null if the user is a guest' do + it 'is present if the user has guest access' do project.add_guest(user) - post_graphql(query, current_user: user, variables: first_n.with(1)) + post_graphql(query, current_user: user) - expect(graphql_data_at(:project, :pipelines, :nodes)).to contain_exactly a_hash_including('stages' => be_nil) + expect(graphql_data_at(:project, :pipelines, :nodes, :stages, :nodes, :name)) + .to contain_exactly(eq(stage.name), eq(other_stage.name)) end it 'is present if the user has reporter access' do @@ -113,12 +114,13 @@ RSpec.describe 'Query.project(fullPath).pipelines' do wrap_fields(query_graphql_path(query_path, :name)) end - it 'is empty if the user is a guest' do + it 'is present if the user has guest access' do project.add_guest(user) post_graphql(query, current_user: user) - expect(graphql_data_at(:project, :pipelines, :nodes, :stages, :nodes, :groups)).to be_empty + expect(graphql_data_at(:project, :pipelines, :nodes, :stages, :nodes, :groups, :nodes, :name)) + .to contain_exactly('linux', 'linux') end it 'is present if the user has reporter access' do diff --git a/spec/requests/api/graphql/ci/runner_spec.rb b/spec/requests/api/graphql/ci/runner_spec.rb index ab53ff654e9..98d3a3b1c51 100644 --- a/spec/requests/api/graphql/ci/runner_spec.rb +++ b/spec/requests/api/graphql/ci/runner_spec.rb @@ -63,7 +63,7 @@ RSpec.describe 'Query.runner(id)' do 'revision' => runner.revision, 'locked' => false, 'active' => runner.active, - 'status' => runner.status.to_s.upcase, + 'status' => runner.status('14.5').to_s.upcase, 'maximumTimeout' => runner.maximum_timeout, 'accessLevel' => runner.access_level.to_s.upcase, 'runUntagged' => runner.run_untagged, @@ -221,6 +221,54 @@ RSpec.describe 'Query.runner(id)' do end end + describe 'for runner with status' do + let_it_be(:stale_runner) { create(:ci_runner, description: 'Stale runner 1', created_at: 3.months.ago) } + let_it_be(:never_contacted_instance_runner) { create(:ci_runner, description: 'Missing runner 1', created_at: 1.month.ago, contacted_at: nil) } + + let(:status_fragment) do + %( + status + legacyStatusWithExplicitVersion: status(legacyMode: "14.5") + newStatus: status(legacyMode: null) + ) + end + + let(:query) do + %( + query { + staleRunner: runner(id: "#{stale_runner.to_global_id}") { #{status_fragment} } + pausedRunner: runner(id: "#{inactive_instance_runner.to_global_id}") { #{status_fragment} } + neverContactedInstanceRunner: runner(id: "#{never_contacted_instance_runner.to_global_id}") { #{status_fragment} } + } + ) + end + + it 'retrieves status fields with expected values' do + post_graphql(query, current_user: user) + + stale_runner_data = graphql_data_at(:stale_runner) + expect(stale_runner_data).to match a_hash_including( + 'status' => 'NOT_CONNECTED', + 'legacyStatusWithExplicitVersion' => 'NOT_CONNECTED', + 'newStatus' => 'STALE' + ) + + paused_runner_data = graphql_data_at(:paused_runner) + expect(paused_runner_data).to match a_hash_including( + 'status' => 'PAUSED', + 'legacyStatusWithExplicitVersion' => 'PAUSED', + 'newStatus' => 'OFFLINE' + ) + + never_contacted_instance_runner_data = graphql_data_at(:never_contacted_instance_runner) + expect(never_contacted_instance_runner_data).to match a_hash_including( + 'status' => 'NOT_CONNECTED', + 'legacyStatusWithExplicitVersion' => 'NOT_CONNECTED', + 'newStatus' => 'NEVER_CONTACTED' + ) + end + end + describe 'for multiple runners' do let_it_be(:project1) { create(:project, :test_repo) } let_it_be(:project2) { create(:project, :test_repo) } diff --git a/spec/requests/api/graphql/ci/runners_spec.rb b/spec/requests/api/graphql/ci/runners_spec.rb index 51a07e60e15..267dd1b5e6f 100644 --- a/spec/requests/api/graphql/ci/runners_spec.rb +++ b/spec/requests/api/graphql/ci/runners_spec.rb @@ -62,6 +62,15 @@ RSpec.describe 'Query.runners' do it_behaves_like 'a working graphql query returning expected runner' end + + context 'runner_type is PROJECT_TYPE and status is NEVER_CONTACTED' do + let(:runner_type) { 'PROJECT_TYPE' } + let(:status) { 'NEVER_CONTACTED' } + + let!(:expected_runner) { project_runner } + + it_behaves_like 'a working graphql query returning expected runner' + end end describe 'pagination' do diff --git a/spec/requests/api/graphql/container_repository/container_repository_details_spec.rb b/spec/requests/api/graphql/container_repository/container_repository_details_spec.rb index d93afcc0f33..802ab847b3d 100644 --- a/spec/requests/api/graphql/container_repository/container_repository_details_spec.rb +++ b/spec/requests/api/graphql/container_repository/container_repository_details_spec.rb @@ -30,6 +30,14 @@ RSpec.describe 'container repository details' do subject { post_graphql(query, current_user: user, variables: variables) } + shared_examples 'returning an invalid value error' do + it 'returns an error' do + subject + + expect(graphql_errors.first.dig('message')).to match(/invalid value/) + end + end + it_behaves_like 'a working graphql query' do before do subject @@ -138,6 +146,80 @@ RSpec.describe 'container repository details' do end end + context 'sorting the tags' do + let(:sort) { 'NAME_DESC' } + let(:tags_response) { container_repository_details_response.dig('tags', 'edges') } + let(:variables) do + { id: container_repository_global_id, n: sort } + end + + let(:query) do + <<~GQL + query($id: ID!, $n: ContainerRepositoryTagSort) { + containerRepository(id: $id) { + tags(sort: $n) { + edges { + node { + #{all_graphql_fields_for('ContainerRepositoryTag')} + } + } + } + } + } + GQL + end + + it 'sorts the tags', :aggregate_failures do + subject + + expect(tags_response.first.dig('node', 'name')).to eq('tag5') + expect(tags_response.last.dig('node', 'name')).to eq('latest') + end + + context 'invalid sort' do + let(:sort) { 'FOO_ASC' } + + it_behaves_like 'returning an invalid value error' + end + end + + context 'filtering by name' do + let(:name) { 'l' } + let(:tags_response) { container_repository_details_response.dig('tags', 'edges') } + let(:variables) do + { id: container_repository_global_id, n: name } + end + + let(:query) do + <<~GQL + query($id: ID!, $n: String) { + containerRepository(id: $id) { + tags(name: $n) { + edges { + node { + #{all_graphql_fields_for('ContainerRepositoryTag')} + } + } + } + } + } + GQL + end + + it 'sorts the tags', :aggregate_failures do + subject + + expect(tags_response.size).to eq(1) + expect(tags_response.first.dig('node', 'name')).to eq('latest') + end + + context 'invalid filter' do + let(:name) { 1 } + + it_behaves_like 'returning an invalid value error' + end + end + context 'with tags with a manifest containing nil fields' do let(:tags_response) { container_repository_details_response.dig('tags', 'nodes') } let(:errors) { container_repository_details_response.dig('errors') } diff --git a/spec/requests/api/graphql/current_user/todos_query_spec.rb b/spec/requests/api/graphql/current_user/todos_query_spec.rb index 981b10a7467..5a45f0db518 100644 --- a/spec/requests/api/graphql/current_user/todos_query_spec.rb +++ b/spec/requests/api/graphql/current_user/todos_query_spec.rb @@ -69,7 +69,7 @@ RSpec.describe 'Query current user todos' do QUERY end - it 'avoids N+1 queries', :request_store do + it 'avoids N+1 queries', :request_store, quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/338671' do control = ActiveRecord::QueryRecorder.new { post_graphql(query, current_user: current_user) } project2 = create(:project) diff --git a/spec/requests/api/graphql/mutations/design_management/delete_spec.rb b/spec/requests/api/graphql/mutations/design_management/delete_spec.rb index 1dffb86b344..1f43f113e65 100644 --- a/spec/requests/api/graphql/mutations/design_management/delete_spec.rb +++ b/spec/requests/api/graphql/mutations/design_management/delete_spec.rb @@ -53,7 +53,7 @@ RSpec.describe "deleting designs" do context 'the designs list contains filenames we cannot find' do it_behaves_like 'a failed request' do - let(:designs) { %w/foo bar baz/.map { |fn| instance_double('file', filename: fn) } } + let(:designs) { %w/foo bar baz/.map { |fn| double('file', filename: fn) } } let(:the_error) { a_string_matching %r/filenames were not found/ } end end diff --git a/spec/requests/api/graphql/mutations/issues/set_crm_contacts_spec.rb b/spec/requests/api/graphql/mutations/issues/set_crm_contacts_spec.rb index 3da702c55d7..2da69509ad6 100644 --- a/spec/requests/api/graphql/mutations/issues/set_crm_contacts_spec.rb +++ b/spec/requests/api/graphql/mutations/issues/set_crm_contacts_spec.rb @@ -12,7 +12,7 @@ RSpec.describe 'Setting issues crm contacts' do let(:issue) { create(:issue, project: project) } let(:operation_mode) { Types::MutationOperationModeEnum.default_mode } - let(:crm_contact_ids) { [global_id_of(contacts[1]), global_id_of(contacts[2])] } + let(:contact_ids) { [global_id_of(contacts[1]), global_id_of(contacts[2])] } let(:does_not_exist_or_no_permission) { "The resource that you are attempting to access does not exist or you don't have permission to perform this action" } let(:mutation) do @@ -20,7 +20,7 @@ RSpec.describe 'Setting issues crm contacts' do project_path: issue.project.full_path, iid: issue.iid.to_s, operation_mode: operation_mode, - crm_contact_ids: crm_contact_ids + contact_ids: contact_ids } graphql_mutation(:issue_set_crm_contacts, variables, @@ -83,7 +83,7 @@ RSpec.describe 'Setting issues crm contacts' do end context 'append' do - let(:crm_contact_ids) { [global_id_of(contacts[3])] } + let(:contact_ids) { [global_id_of(contacts[3])] } let(:operation_mode) { Types::MutationOperationModeEnum.enum[:append] } it 'updates the issue with correct contacts' do @@ -95,7 +95,7 @@ RSpec.describe 'Setting issues crm contacts' do end context 'remove' do - let(:crm_contact_ids) { [global_id_of(contacts[0])] } + let(:contact_ids) { [global_id_of(contacts[0])] } let(:operation_mode) { Types::MutationOperationModeEnum.enum[:remove] } it 'updates the issue with correct contacts' do @@ -107,7 +107,7 @@ RSpec.describe 'Setting issues crm contacts' do end context 'when the contact does not exist' do - let(:crm_contact_ids) { ["gid://gitlab/CustomerRelations::Contact/#{non_existing_record_id}"] } + let(:contact_ids) { ["gid://gitlab/CustomerRelations::Contact/#{non_existing_record_id}"] } it 'returns expected error' do post_graphql_mutation(mutation, current_user: user) @@ -120,7 +120,7 @@ RSpec.describe 'Setting issues crm contacts' do context 'when the contact belongs to a different group' do let(:group2) { create(:group) } let(:contact) { create(:contact, group: group2) } - let(:crm_contact_ids) { [global_id_of(contact)] } + let(:contact_ids) { [global_id_of(contact)] } before do group2.add_reporter(user) @@ -137,7 +137,7 @@ RSpec.describe 'Setting issues crm contacts' do context 'when attempting to add more than 6' do let(:operation_mode) { Types::MutationOperationModeEnum.enum[:append] } let(:gid) { global_id_of(contacts[0]) } - let(:crm_contact_ids) { [gid, gid, gid, gid, gid, gid, gid] } + let(:contact_ids) { [gid, gid, gid, gid, gid, gid, gid] } it 'returns expected error' do post_graphql_mutation(mutation, current_user: user) @@ -149,7 +149,7 @@ RSpec.describe 'Setting issues crm contacts' do context 'when trying to remove non-existent contact' do let(:operation_mode) { Types::MutationOperationModeEnum.enum[:remove] } - let(:crm_contact_ids) { ["gid://gitlab/CustomerRelations::Contact/#{non_existing_record_id}"] } + let(:contact_ids) { ["gid://gitlab/CustomerRelations::Contact/#{non_existing_record_id}"] } it 'raises expected error' do post_graphql_mutation(mutation, current_user: user) diff --git a/spec/requests/api/graphql/mutations/user_callouts/create_spec.rb b/spec/requests/api/graphql/mutations/user_callouts/create_spec.rb index 716983f01d2..28a46583d2a 100644 --- a/spec/requests/api/graphql/mutations/user_callouts/create_spec.rb +++ b/spec/requests/api/graphql/mutations/user_callouts/create_spec.rb @@ -7,7 +7,7 @@ RSpec.describe 'Create a user callout' do let_it_be(:current_user) { create(:user) } - let(:feature_name) { ::UserCallout.feature_names.each_key.first } + let(:feature_name) { ::Users::Callout.feature_names.each_key.first } let(:input) do { diff --git a/spec/requests/api/graphql/packages/package_spec.rb b/spec/requests/api/graphql/packages/package_spec.rb index 83ea9ff4dc8..a9019a7611a 100644 --- a/spec/requests/api/graphql/packages/package_spec.rb +++ b/spec/requests/api/graphql/packages/package_spec.rb @@ -4,7 +4,7 @@ require 'spec_helper' RSpec.describe 'package details' do include GraphqlHelpers - let_it_be(:project) { create(:project) } + let_it_be_with_reload(:project) { create(:project) } let_it_be(:composer_package) { create(:composer_package, project: project) } let_it_be(:composer_json) { { name: 'name', type: 'type', license: 'license', version: 1 } } let_it_be(:composer_metadatum) do @@ -68,7 +68,7 @@ RSpec.describe 'package details' do subject expect(graphql_data_at(:package, :versions, :nodes, :version)).to be_present - expect(graphql_data_at(:package, :versions, :nodes, :versions, :nodes)).to be_empty + expect(graphql_data_at(:package, :versions, :nodes, :versions, :nodes)).to eq [nil, nil] end end end @@ -96,4 +96,87 @@ RSpec.describe 'package details' do expect(graphql_data_at(:b)).to be(nil) end end + + context 'with unauthorized user' do + let_it_be(:user) { create(:user) } + + before do + project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE) + end + + it 'returns no packages' do + subject + + expect(graphql_data_at(:package)).to be_nil + end + end + + context 'pipelines field', :aggregate_failures do + let(:pipelines) { create_list(:ci_pipeline, 6, project: project) } + let(:pipeline_gids) { pipelines.sort_by(&:id).map(&:to_gid).map(&:to_s).reverse } + + before do + composer_package.pipelines = pipelines + composer_package.save! + end + + def run_query(args) + pipelines_nodes = <<~QUERY + nodes { + id + } + pageInfo { + startCursor + endCursor + } + QUERY + + query = graphql_query_for(:package, { id: package_global_id }, query_graphql_field("pipelines", args, pipelines_nodes)) + post_graphql(query, current_user: user) + end + + it 'loads the second page with pagination first correctly' do + run_query(first: 2) + pipeline_ids = graphql_data.dig('package', 'pipelines', 'nodes').pluck('id') + + expect(pipeline_ids).to eq(pipeline_gids[0..1]) + + cursor = graphql_data.dig('package', 'pipelines', 'pageInfo', 'endCursor') + + run_query(first: 2, after: cursor) + + pipeline_ids = graphql_data.dig('package', 'pipelines', 'nodes').pluck('id') + + expect(pipeline_ids).to eq(pipeline_gids[2..3]) + end + + it 'loads the second page with pagination last correctly' do + run_query(last: 2) + pipeline_ids = graphql_data.dig('package', 'pipelines', 'nodes').pluck('id') + + expect(pipeline_ids).to eq(pipeline_gids[4..5]) + + cursor = graphql_data.dig('package', 'pipelines', 'pageInfo', 'startCursor') + + run_query(last: 2, before: cursor) + + pipeline_ids = graphql_data.dig('package', 'pipelines', 'nodes').pluck('id') + + expect(pipeline_ids).to eq(pipeline_gids[2..3]) + end + + context 'with unauthorized user' do + let_it_be(:user) { create(:user) } + + before do + project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE) + end + + it 'returns no packages' do + run_query(first: 2) + + expect(graphql_data_at(:package)).to be_nil + end + end + end end diff --git a/spec/requests/api/graphql/project/cluster_agents_spec.rb b/spec/requests/api/graphql/project/cluster_agents_spec.rb index dc7254dd552..585126f3849 100644 --- a/spec/requests/api/graphql/project/cluster_agents_spec.rb +++ b/spec/requests/api/graphql/project/cluster_agents_spec.rb @@ -7,7 +7,7 @@ RSpec.describe 'Project.cluster_agents' do let_it_be(:project) { create(:project, :public) } let_it_be(:current_user) { create(:user, maintainer_projects: [project]) } - let_it_be(:agents) { create_list(:cluster_agent, 5, project: project) } + let_it_be(:agents) { create_list(:cluster_agent, 3, project: project) } let(:first) { var('Int') } let(:cluster_agents_fields) { nil } @@ -105,4 +105,37 @@ RSpec.describe 'Project.cluster_agents' do }) end end + + context 'selecting activity events' do + let_it_be(:token) { create(:cluster_agent_token, agent: agents.first) } + let_it_be(:event) { create(:agent_activity_event, agent: agents.first, agent_token: token, user: current_user) } + + let(:cluster_agents_fields) { [:id, query_nodes(:activity_events, of: 'ClusterAgentActivityEvent', max_depth: 2)] } + + it 'retrieves activity event details' do + post_graphql(query, current_user: current_user) + + response = graphql_data_at(:project, :cluster_agents, :nodes, :activity_events, :nodes).first + + expect(response).to include({ + 'kind' => event.kind, + 'level' => event.level, + 'recordedAt' => event.recorded_at.iso8601, + 'agentToken' => hash_including('name' => token.name), + 'user' => hash_including('name' => current_user.name) + }) + end + + it 'preloads associations to prevent N+1 queries' do + user = create(:user) + token = create(:cluster_agent_token, agent: agents.second) + create(:agent_activity_event, agent: agents.second, agent_token: token, user: user) + + post_graphql(query, current_user: current_user) + + expect do + post_graphql(query, current_user: current_user) + end.to issue_same_number_of_queries_as { post_graphql(query, current_user: current_user, variables: [first.with(1)]) } + end + end end diff --git a/spec/requests/api/graphql/project/jobs_spec.rb b/spec/requests/api/graphql/project/jobs_spec.rb new file mode 100644 index 00000000000..1a823ede9ac --- /dev/null +++ b/spec/requests/api/graphql/project/jobs_spec.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true +require 'spec_helper' + +RSpec.describe 'Query.project.jobs' do + include GraphqlHelpers + + let_it_be(:project) { create(:project, :repository, :public) } + let_it_be(:user) { create(:user) } + + let(:pipeline) do + create(:ci_pipeline, project: project, user: user) + end + + let(:query) do + <<~QUERY + { + project(fullPath: "#{project.full_path}") { + jobs { + nodes { + name + previousStageJobsAndNeeds { + nodes { + name + } + } + } + } + } + } + QUERY + end + + it 'does not generate N+1 queries', :request_store, :use_sql_query_cache do + build_stage = create(:ci_stage_entity, position: 1, name: 'build', project: project, pipeline: pipeline) + test_stage = create(:ci_stage_entity, position: 2, name: 'test', project: project, pipeline: pipeline) + create(:ci_build, pipeline: pipeline, stage_idx: build_stage.position, name: 'docker 1 2', stage: build_stage) + create(:ci_build, pipeline: pipeline, stage_idx: build_stage.position, name: 'docker 2 2', stage: build_stage) + create(:ci_build, pipeline: pipeline, stage_idx: test_stage.position, name: 'rspec 1 2', stage: test_stage) + test_job = create(:ci_build, pipeline: pipeline, stage_idx: test_stage.position, name: 'rspec 2 2', stage: test_stage) + create(:ci_build_need, build: test_job, name: 'docker 1 2') + + post_graphql(query, current_user: user) + + control = ActiveRecord::QueryRecorder.new(skip_cached: false) do + post_graphql(query, current_user: user) + end + + create(:ci_build, name: 'test-a', stage: test_stage, stage_idx: test_stage.position, pipeline: pipeline) + test_b_job = create(:ci_build, name: 'test-b', stage: test_stage, stage_idx: test_stage.position, pipeline: pipeline) + create(:ci_build_need, build: test_b_job, name: 'docker 2 2') + + expect do + post_graphql(query, current_user: user) + end.not_to exceed_all_query_limit(control) + end +end diff --git a/spec/requests/api/graphql/project/pipeline_spec.rb b/spec/requests/api/graphql/project/pipeline_spec.rb index d46ef313563..73e02e2a4b1 100644 --- a/spec/requests/api/graphql/project/pipeline_spec.rb +++ b/spec/requests/api/graphql/project/pipeline_spec.rb @@ -273,6 +273,48 @@ RSpec.describe 'getting pipeline information nested in a project' do end end + context 'N+1 queries on pipeline jobs' do + let(:pipeline) { create(:ci_pipeline, project: project) } + + let(:fields) do + <<~FIELDS + jobs { + nodes { + previousStageJobsAndNeeds { + nodes { + name + } + } + } + } + FIELDS + end + + it 'does not generate N+1 queries', :request_store, :use_sql_query_cache do + build_stage = create(:ci_stage_entity, position: 1, name: 'build', project: project, pipeline: pipeline) + test_stage = create(:ci_stage_entity, position: 2, name: 'test', project: project, pipeline: pipeline) + create(:ci_build, pipeline: pipeline, stage_idx: build_stage.position, name: 'docker 1 2', stage: build_stage) + create(:ci_build, pipeline: pipeline, stage_idx: build_stage.position, name: 'docker 2 2', stage: build_stage) + create(:ci_build, pipeline: pipeline, stage_idx: test_stage.position, name: 'rspec 1 2', stage: test_stage) + test_job = create(:ci_build, pipeline: pipeline, stage_idx: test_stage.position, name: 'rspec 2 2', stage: test_stage) + create(:ci_build_need, build: test_job, name: 'docker 1 2') + + post_graphql(query, current_user: current_user) + + control = ActiveRecord::QueryRecorder.new(skip_cached: false) do + post_graphql(query, current_user: current_user) + end + + create(:ci_build, name: 'test-a', stage: test_stage, stage_idx: test_stage.position, pipeline: pipeline) + test_b_job = create(:ci_build, name: 'test-b', stage: test_stage, stage_idx: test_stage.position, pipeline: pipeline) + create(:ci_build_need, build: test_b_job, name: 'docker 2 2') + + expect do + post_graphql(query, current_user: current_user) + end.not_to exceed_all_query_limit(control) + end + end + context 'N+1 queries on stages jobs' do let(:depth) { 5 } let(:fields) do diff --git a/spec/requests/api/graphql/project_query_spec.rb b/spec/requests/api/graphql/project_query_spec.rb index e44a7efb354..310a8e9fa33 100644 --- a/spec/requests/api/graphql/project_query_spec.rb +++ b/spec/requests/api/graphql/project_query_spec.rb @@ -143,6 +143,40 @@ RSpec.describe 'getting project information' do end end + context 'when the user has guest access' do + context 'when the project has public pipelines' do + before do + pipeline = create(:ci_pipeline, project: project) + create(:ci_build, project: project, pipeline: pipeline, name: 'a test job') + project.add_guest(current_user) + end + + it 'shows all jobs' do + query = <<~GQL + query { + project(fullPath: "#{project.full_path}") { + jobs { + nodes { + name + stage { + name + } + } + } + } + } + GQL + + post_graphql(query, current_user: current_user) + + expect(graphql_data_at(:project, :jobs, :nodes)).to contain_exactly({ + 'name' => 'a test job', + 'stage' => { 'name' => 'test' } + }) + end + end + end + context 'when the user does not have access to the project' do it 'returns an empty field' do post_graphql(query, current_user: current_user) diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb index 75f5a974d22..d226bb07c73 100644 --- a/spec/requests/api/groups_spec.rb +++ b/spec/requests/api/groups_spec.rb @@ -174,18 +174,6 @@ RSpec.describe API::Groups do 'Remaining records can be retrieved using keyset pagination.' ) end - - context 'when the feature flag `keyset_pagination_for_groups_api` is disabled' do - before do - stub_feature_flags(keyset_pagination_for_groups_api: false) - end - - it 'returns successful response' do - get api('/groups'), params: { page: 3000, per_page: 25 } - - expect(response).to have_gitlab_http_status(:ok) - end - end end context 'on making requests below the allowed offset pagination threshold' do @@ -247,24 +235,6 @@ RSpec.describe API::Groups do expect(records.size).to eq(1) expect(records.first['id']).to eq(group_2.id) end - - context 'when the feature flag `keyset_pagination_for_groups_api` is disabled' do - before do - stub_feature_flags(keyset_pagination_for_groups_api: false) - end - - it 'ignores the keyset pagination params and performs offset pagination' do - get api('/groups'), params: { pagination: 'keyset', per_page: 1 } - - expect(response).to have_gitlab_http_status(:ok) - records = json_response - expect(records.size).to eq(1) - expect(records.first['id']).to eq(group_1.id) - - params_for_next_page = params_for_next_page(response) - expect(params_for_next_page).not_to include('cursor') - end - end end context 'on making requests with unsupported ordering structure' do @@ -1973,6 +1943,116 @@ RSpec.describe API::Groups do end end + describe 'POST /groups/:id/transfer' do + let_it_be(:user) { create(:user) } + let_it_be_with_reload(:new_parent_group) { create(:group, :private) } + let_it_be_with_reload(:group) { create(:group, :nested, :private) } + + before do + new_parent_group.add_owner(user) + group.add_owner(user) + end + + def make_request(user) + post api("/groups/#{group.id}/transfer", user), params: params + end + + context 'when promoting a subgroup to a root group' do + shared_examples_for 'promotes the subgroup to a root group' do + it 'returns success' do + make_request(user) + + expect(response).to have_gitlab_http_status(:created) + expect(json_response['parent_id']).to be_nil + end + end + + context 'when no group_id is specified' do + let(:params) {} + + it_behaves_like 'promotes the subgroup to a root group' + end + + context 'when group_id is specified as blank' do + let(:params) { { group_id: '' } } + + it_behaves_like 'promotes the subgroup to a root group' + end + + context 'when the group is already a root group' do + let(:group) { create(:group) } + let(:params) { { group_id: '' } } + + it 'returns error' do + make_request(user) + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['message']).to eq('Transfer failed: Group is already a root group.') + end + end + end + + context 'when transferring a subgroup to a different group' do + let(:params) { { group_id: new_parent_group.id } } + + context 'when the user does not have admin rights to the group being transferred' do + it 'forbids the operation' do + developer_user = create(:user) + group.add_developer(developer_user) + + make_request(developer_user) + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + + context 'when the user does not have access to the new parent group' do + it 'fails with 404' do + user_without_access_to_new_parent_group = create(:user) + group.add_owner(user_without_access_to_new_parent_group) + + make_request(user_without_access_to_new_parent_group) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'when the ID of a non-existent group is mentioned as the new parent group' do + let(:params) { { group_id: non_existing_record_id } } + + it 'fails with 404' do + make_request(user) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'when the transfer fails due to an error' do + before do + expect_next_instance_of(::Groups::TransferService) do |service| + expect(service).to receive(:proceed_to_transfer).and_raise(Gitlab::UpdatePathError, 'namespace directory cannot be moved') + end + end + + it 'returns error' do + make_request(user) + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['message']).to eq('Transfer failed: namespace directory cannot be moved') + end + end + + context 'when the transfer succceds' do + it 'returns success' do + make_request(user) + + expect(response).to have_gitlab_http_status(:created) + expect(json_response['parent_id']).to eq(new_parent_group.id) + end + end + end + end + it_behaves_like 'custom attributes endpoints', 'groups' do let(:attributable) { group1 } let(:other_attributable) { group2 } diff --git a/spec/requests/api/import_github_spec.rb b/spec/requests/api/import_github_spec.rb index d5fed330401..f0c4fcc4f29 100644 --- a/spec/requests/api/import_github_spec.rb +++ b/spec/requests/api/import_github_spec.rb @@ -11,12 +11,12 @@ RSpec.describe API::ImportGithub do let(:user) { create(:user) } let(:project) { create(:project) } let(:provider_username) { user.username } - let(:provider_user) { OpenStruct.new(login: provider_username) } + let(:provider_user) { double('provider', login: provider_username) } let(:provider_repo) do - OpenStruct.new( + double('provider', name: 'vim', full_name: "#{provider_username}/vim", - owner: OpenStruct.new(login: provider_username) + owner: double('provider', login: provider_username) ) end diff --git a/spec/requests/api/invitations_spec.rb b/spec/requests/api/invitations_spec.rb index cba4256adc5..702e6ef0a2a 100644 --- a/spec/requests/api/invitations_spec.rb +++ b/spec/requests/api/invitations_spec.rb @@ -152,25 +152,7 @@ RSpec.describe API::Invitations do end end - context 'with areas_of_focus', :snowplow do - it 'tracks the areas_of_focus from params' do - post invitations_url(source, maintainer), - params: { email: email, access_level: Member::DEVELOPER, areas_of_focus: 'Other' } - - expect_snowplow_event( - category: 'Members::InviteService', - action: 'area_of_focus', - label: 'Other', - property: source.members.last.id.to_s - ) - end - end - context 'with tasks_to_be_done and tasks_project_id in the params' do - before do - stub_experiments(invite_members_for_task: true) - end - let(:project_id) { source_type == 'project' ? source.id : create(:project, namespace: source).id } context 'when there is 1 invitation' do diff --git a/spec/requests/api/issues/get_project_issues_spec.rb b/spec/requests/api/issues/get_project_issues_spec.rb index 07fa1d40f7b..9948e13e9ae 100644 --- a/spec/requests/api/issues/get_project_issues_spec.rb +++ b/spec/requests/api/issues/get_project_issues_spec.rb @@ -873,7 +873,7 @@ RSpec.describe API::Issues do end it 'returns 404 if the issue is confidential' do - post api("/projects/#{project.id}/issues/#{confidential_issue.iid}/participants", non_member) + get api("/projects/#{project.id}/issues/#{confidential_issue.iid}/participants", non_member) expect(response).to have_gitlab_http_status(:not_found) end diff --git a/spec/requests/api/labels_spec.rb b/spec/requests/api/labels_spec.rb index 4b6868f42bc..db9d72245b3 100644 --- a/spec/requests/api/labels_spec.rb +++ b/spec/requests/api/labels_spec.rb @@ -589,6 +589,15 @@ RSpec.describe API::Labels do expect(response).to have_gitlab_http_status(:forbidden) end + it 'returns 403 if reporter promotes label' do + reporter = create(:user) + project.add_reporter(reporter) + + put api("/projects/#{project.id}/labels/promote", reporter), params: { name: label1.name } + + expect(response).to have_gitlab_http_status(:forbidden) + end + it 'returns 404 if label does not exist' do put api("/projects/#{project.id}/labels/promote", user), params: { name: 'unknown' } @@ -601,6 +610,13 @@ RSpec.describe API::Labels do expect(response).to have_gitlab_http_status(:bad_request) expect(json_response['error']).to eq('name is missing') end + + it 'returns 400 if project does not have a group' do + project = create(:project, creator_id: user.id, namespace: user.namespace) + put api("/projects/#{project.id}/labels/promote", user), params: { name: label1.name } + + expect(response).to have_gitlab_http_status(:bad_request) + end end describe "POST /projects/:id/labels/:label_id/subscribe" do diff --git a/spec/requests/api/markdown_golden_master_spec.rb b/spec/requests/api/markdown_golden_master_spec.rb new file mode 100644 index 00000000000..4fa946de342 --- /dev/null +++ b/spec/requests/api/markdown_golden_master_spec.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require 'spec_helper' + +# See spec/fixtures/markdown/markdown_golden_master_examples.yml for documentation on how this spec works. +RSpec.describe API::Markdown, 'Golden Master' do + markdown_yml_file_path = File.expand_path('../../fixtures/markdown/markdown_golden_master_examples.yml', __dir__) + include_context 'API::Markdown Golden Master shared context', markdown_yml_file_path +end diff --git a/spec/requests/api/members_spec.rb b/spec/requests/api/members_spec.rb index 7f4345faabb..02061bb8ab6 100644 --- a/spec/requests/api/members_spec.rb +++ b/spec/requests/api/members_spec.rb @@ -387,38 +387,7 @@ RSpec.describe API::Members do end end - context 'with areas_of_focus considerations', :snowplow do - let(:user_id) { stranger.id } - - context 'when areas_of_focus is present in params' do - it 'tracks the areas_of_focus' do - post api("/#{source_type.pluralize}/#{source.id}/members", maintainer), - params: { user_id: user_id, access_level: Member::DEVELOPER, areas_of_focus: 'Other' } - - expect_snowplow_event( - category: 'Members::CreateService', - action: 'area_of_focus', - label: 'Other', - property: source.members.last.id.to_s - ) - end - end - - context 'when areas_of_focus is not present in params' do - it 'does not track the areas_of_focus' do - post api("/#{source_type.pluralize}/#{source.id}/members", maintainer), - params: { user_id: user_id, access_level: Member::DEVELOPER } - - expect_no_snowplow_event(category: 'Members::CreateService', action: 'area_of_focus') - end - end - end - context 'with tasks_to_be_done and tasks_project_id in the params' do - before do - stub_experiments(invite_members_for_task: true) - end - let(:project_id) { source_type == 'project' ? source.id : create(:project, namespace: source).id } context 'when there is 1 user to add' do diff --git a/spec/requests/api/project_import_spec.rb b/spec/requests/api/project_import_spec.rb index 097d374640c..3ed08afd57d 100644 --- a/spec/requests/api/project_import_spec.rb +++ b/spec/requests/api/project_import_spec.rb @@ -47,7 +47,7 @@ RSpec.describe API::ProjectImport do it 'executes a limited number of queries' do control_count = ActiveRecord::QueryRecorder.new { subject }.count - expect(control_count).to be <= 101 + expect(control_count).to be <= 104 end it 'schedules an import using a namespace' do diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index cc546cbcda1..79dbbd20d83 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -1160,6 +1160,15 @@ RSpec.describe API::Projects do expect(response).to have_gitlab_http_status(:forbidden) end + it 'allows creating a project without an import_url when git import source is disabled', :aggregate_failures do + stub_application_setting(import_sources: nil) + project_params = { path: 'path-project-Foo' } + + expect { post api('/projects', user), params: project_params }.to change { Project.count }.by(1) + + expect(response).to have_gitlab_http_status(:created) + end + it 'disallows creating a project with an import_url that is not reachable', :aggregate_failures do url = 'http://example.com' endpoint_url = "#{url}/info/refs?service=git-upload-pack" @@ -1504,6 +1513,20 @@ RSpec.describe API::Projects do expect(json_response.map { |project| project['id'] }).to contain_exactly(private_project1.id) end + context 'and using an admin to search', :enable_admin_mode, :aggregate_errors do + it 'returns users projects when authenticated as admin' do + private_project1 = create(:project, :private, name: 'private_project1', creator_id: user4.id, namespace: user4.namespace) + + # min_access_level does not make any difference when admins search for a user's projects + get api("/users/#{user4.id}/projects/", admin), params: { min_access_level: 30 } + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.map { |project| project['id'] }).to contain_exactly(project4.id, private_project1.id, public_project.id) + end + end + context 'and using the programming language filter' do include_context 'with language detection' diff --git a/spec/requests/api/repositories_spec.rb b/spec/requests/api/repositories_spec.rb index f3146480be2..21a8622e08d 100644 --- a/spec/requests/api/repositories_spec.rb +++ b/spec/requests/api/repositories_spec.rb @@ -731,6 +731,71 @@ RSpec.describe API::Repositories do end end + describe 'GET /projects/:id/repository/changelog' do + it 'generates the changelog for a version' do + spy = instance_spy(Repositories::ChangelogService) + release_notes = 'Release notes' + + allow(Repositories::ChangelogService) + .to receive(:new) + .with( + project, + user, + version: '1.0.0', + from: 'foo', + to: 'bar', + date: DateTime.new(2020, 1, 1), + trailer: 'Foo' + ) + .and_return(spy) + + expect(spy).to receive(:execute).with(commit_to_changelog: false).and_return(release_notes) + + get( + api("/projects/#{project.id}/repository/changelog", user), + params: { + version: '1.0.0', + from: 'foo', + to: 'bar', + date: '2020-01-01', + trailer: 'Foo' + } + ) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['notes']).to eq(release_notes) + end + + it 'supports leaving out the from and to attribute' do + spy = instance_spy(Repositories::ChangelogService) + + allow(Repositories::ChangelogService) + .to receive(:new) + .with( + project, + user, + version: '1.0.0', + date: DateTime.new(2020, 1, 1), + trailer: 'Foo' + ) + .and_return(spy) + + expect(spy).to receive(:execute).with(commit_to_changelog: false) + + get( + api("/projects/#{project.id}/repository/changelog", user), + params: { + version: '1.0.0', + date: '2020-01-01', + trailer: 'Foo' + } + ) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['notes']).to be_present + end + end + describe 'POST /projects/:id/repository/changelog' do it 'generates the changelog for a version' do spy = instance_spy(Repositories::ChangelogService) @@ -751,7 +816,7 @@ RSpec.describe API::Repositories do ) .and_return(spy) - allow(spy).to receive(:execute) + allow(spy).to receive(:execute).with(commit_to_changelog: true) post( api("/projects/#{project.id}/repository/changelog", user), @@ -787,7 +852,7 @@ RSpec.describe API::Repositories do ) .and_return(spy) - expect(spy).to receive(:execute) + expect(spy).to receive(:execute).with(commit_to_changelog: true) post( api("/projects/#{project.id}/repository/changelog", user), diff --git a/spec/requests/api/search_spec.rb b/spec/requests/api/search_spec.rb index 8012892a571..b75fe11b06d 100644 --- a/spec/requests/api/search_spec.rb +++ b/spec/requests/api/search_spec.rb @@ -122,6 +122,23 @@ RSpec.describe API::Search do end end + context 'when DB timeouts occur from global searches', :aggregate_errors do + %w( + issues + merge_requests + milestones + projects + snippet_titles + users + ).each do |scope| + it "returns a 408 error if search with scope: #{scope} times out" do + allow(SearchService).to receive(:new).and_raise ActiveRecord::QueryCanceled + get api(endpoint, user), params: { scope: scope, search: 'awesome' } + expect(response).to have_gitlab_http_status(:request_timeout) + end + end + end + context 'when scope is not supported' do it 'returns 400 error' do get api(endpoint, user), params: { scope: 'unsupported', search: 'awesome' } diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb index 641c6a2cd91..7e940d52a41 100644 --- a/spec/requests/api/settings_spec.rb +++ b/spec/requests/api/settings_spec.rb @@ -523,15 +523,6 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting do end end - context "missing spam_check_api_key value when spam_check_endpoint_enabled is true" do - it "returns a blank parameter error message" do - put api("/application/settings", admin), params: { spam_check_endpoint_enabled: true, spam_check_endpoint_url: "https://example.com/spam_check" } - - expect(response).to have_gitlab_http_status(:bad_request) - expect(json_response['error']).to eq('spam_check_api_key is missing') - end - end - context "overly long spam_check_api_key" do it "fails to update the settings with too long spam_check_api_key" do put api("/application/settings", admin), params: { spam_check_api_key: "0123456789" * 500 } diff --git a/spec/requests/api/terraform/state_spec.rb b/spec/requests/api/terraform/state_spec.rb index 5d2635126e8..24f38b04348 100644 --- a/spec/requests/api/terraform/state_spec.rb +++ b/spec/requests/api/terraform/state_spec.rb @@ -152,6 +152,16 @@ RSpec.describe API::Terraform::State do expect(response).to have_gitlab_http_status(:ok) expect(Gitlab::Json.parse(response.body)).to be_empty end + + context 'when serial already exists' do + let(:params) { { 'instance': 'example-instance', 'serial': state.latest_version.version } } + + it 'returns unprocessable entity' do + request + + expect(response).to have_gitlab_http_status(:unprocessable_entity) + end + end end context 'without body' do diff --git a/spec/requests/api/todos_spec.rb b/spec/requests/api/todos_spec.rb index c6b4f50afae..0944bfb6ba6 100644 --- a/spec/requests/api/todos_spec.rb +++ b/spec/requests/api/todos_spec.rb @@ -380,7 +380,7 @@ RSpec.describe API::Todos do end end - describe 'POST :id/issuable_type/:issueable_id/todo' do + describe 'POST :id/issuable_type/:issuable_id/todo' do context 'for an issue' do let_it_be(:issuable) do create(:issue, :confidential, project: project_1) diff --git a/spec/requests/api/topics_spec.rb b/spec/requests/api/topics_spec.rb index a5746a4022e..70eee8a1af9 100644 --- a/spec/requests/api/topics_spec.rb +++ b/spec/requests/api/topics_spec.rb @@ -5,15 +5,15 @@ require 'spec_helper' RSpec.describe API::Topics do include WorkhorseHelpers - let_it_be(:topic_1) { create(:topic, name: 'Git', total_projects_count: 1) } + let_it_be(:file) { fixture_file_upload('spec/fixtures/dk.png') } + + let_it_be(:topic_1) { create(:topic, name: 'Git', total_projects_count: 1, avatar: file) } let_it_be(:topic_2) { create(:topic, name: 'GitLab', total_projects_count: 2) } let_it_be(:topic_3) { create(:topic, name: 'other-topic', total_projects_count: 3) } let_it_be(:admin) { create(:user, :admin) } let_it_be(:user) { create(:user) } - let(:file) { fixture_file_upload('spec/fixtures/dk.png') } - describe 'GET /topics', :aggregate_failures do it 'returns topics ordered by total_projects_count' do get api('/topics') @@ -184,6 +184,14 @@ RSpec.describe API::Topics do expect(json_response['avatar_url']).to end_with('dk.png') end + it 'keeps avatar when updating other fields' do + put api("/topics/#{topic_1.id}", admin), params: { name: 'my-topic' } + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['name']).to eq('my-topic') + expect(topic_1.reload.avatar_url).not_to be_nil + end + it 'returns 404 for non existing id' do put api("/topics/#{non_existing_record_id}", admin), params: { name: 'my-topic' } @@ -196,6 +204,32 @@ RSpec.describe API::Topics do expect(response).to have_gitlab_http_status(:bad_request) expect(json_response['error']).to eql('id is invalid') end + + context 'with blank avatar' do + it 'removes avatar' do + put api("/topics/#{topic_1.id}", admin), params: { avatar: '' } + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['avatar_url']).to be_nil + expect(topic_3.reload.avatar_url).to be_nil + end + + it 'removes avatar besides other changes' do + put api("/topics/#{topic_1.id}", admin), params: { name: 'new-topic-name', avatar: '' } + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['name']).to eq('new-topic-name') + expect(json_response['avatar_url']).to be_nil + expect(topic_1.reload.avatar_url).to be_nil + end + + it 'does not remove avatar in case of other errors' do + put api("/topics/#{topic_1.id}", admin), params: { name: topic_2.name, avatar: '' } + + expect(response).to have_gitlab_http_status(:bad_request) + expect(topic_1.reload.avatar_url).not_to be_nil + end + end end context 'as normal user' do diff --git a/spec/requests/api/v3/github_spec.rb b/spec/requests/api/v3/github_spec.rb index 6d8ae226ce4..838948132dd 100644 --- a/spec/requests/api/v3/github_spec.rb +++ b/spec/requests/api/v3/github_spec.rb @@ -567,18 +567,6 @@ RSpec.describe API::V3::Github do expect(response_diff_files(response)).to be_blank end - it 'does not handle the error when feature flag is disabled', :aggregate_failures do - stub_feature_flags(api_v3_commits_skip_diff_files: false) - - allow(Gitlab::GitalyClient).to receive(:call) - .with(*commit_diff_args) - .and_raise(GRPC::DeadlineExceeded) - - call_api - - expect(response).to have_gitlab_http_status(:error) - end - it 'only calls Gitaly once for all attempts within a period of time', :aggregate_failures do expect(Gitlab::GitalyClient).to receive(:call) .with(*commit_diff_args) -- cgit v1.2.3