diff options
Diffstat (limited to 'spec/requests')
155 files changed, 3690 insertions, 1904 deletions
diff --git a/spec/requests/acme_challenges_controller_spec.rb b/spec/requests/acme_challenges_controller_spec.rb deleted file mode 100644 index f37aefed488..00000000000 --- a/spec/requests/acme_challenges_controller_spec.rb +++ /dev/null @@ -1,9 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe AcmeChallengesController, type: :request, feature_category: :pages do - it_behaves_like 'Base action controller' do - subject(:request) { get acme_challenge_path } - end -end diff --git a/spec/requests/admin/users_controller_spec.rb b/spec/requests/admin/users_controller_spec.rb index e525d615b50..2f8025691f4 100644 --- a/spec/requests/admin/users_controller_spec.rb +++ b/spec/requests/admin/users_controller_spec.rb @@ -74,4 +74,54 @@ RSpec.describe Admin::UsersController, :enable_admin_mode, feature_category: :us expect { request }.to change { user.reload.access_locked? }.from(true).to(false) end end + + describe 'PUT #trust' do + subject(:request) { put trust_admin_user_path(user) } + + it 'trusts the user' do + expect { request }.to change { user.reload.trusted? }.from(false).to(true) + end + + context 'when setting trust fails' do + before do + allow_next_instance_of(Users::TrustService) do |instance| + allow(instance).to receive(:execute).and_return({ status: :failed }) + end + end + + it 'displays a flash alert' do + request + + expect(response).to redirect_to(admin_user_path(user)) + expect(flash[:alert]).to eq(s_('Error occurred. User was not updated')) + end + end + end + + describe 'PUT #untrust' do + before do + user.custom_attributes.create!(key: UserCustomAttribute::TRUSTED_BY, value: "placeholder") + end + + subject(:request) { put untrust_admin_user_path(user) } + + it 'trusts the user' do + expect { request }.to change { user.reload.trusted? }.from(true).to(false) + end + + context 'when untrusting fails' do + before do + allow_next_instance_of(Users::UntrustService) do |instance| + allow(instance).to receive(:execute).and_return({ status: :failed }) + end + end + + it 'displays a flash alert' do + request + + expect(response).to redirect_to(admin_user_path(user)) + expect(flash[:alert]).to eq(s_('Error occurred. User was not updated')) + end + end + end end diff --git a/spec/requests/api/badges_spec.rb b/spec/requests/api/badges_spec.rb index 0b340b95b20..92c5e90027b 100644 --- a/spec/requests/api/badges_spec.rb +++ b/spec/requests/api/badges_spec.rb @@ -363,7 +363,7 @@ RSpec.describe API::Badges, feature_category: :groups_and_projects do end describe 'Endpoints' do - %w(project group).each do |source_type| + %w[project group].each do |source_type| it_behaves_like 'GET /:sources/:id/badges', source_type it_behaves_like 'GET /:sources/:id/badges/:badge_id', source_type it_behaves_like 'GET /:sources/:id/badges/render', source_type diff --git a/spec/requests/api/bulk_imports_spec.rb b/spec/requests/api/bulk_imports_spec.rb index d3d4a723616..bbc01b30361 100644 --- a/spec/requests/api/bulk_imports_spec.rb +++ b/spec/requests/api/bulk_imports_spec.rb @@ -394,7 +394,7 @@ RSpec.describe API::BulkImports, feature_category: :importers do expect(response).to have_gitlab_http_status(:ok) expect(json_response.pluck('id')).to contain_exactly(entity_3.id) - expect(json_response.first['failures'].first['exception_class']).to eq(failure_3.exception_class) + expect(json_response.first['failures'].first['exception_message']).to eq(failure_3.exception_message) end it_behaves_like 'disabled feature' @@ -420,4 +420,17 @@ RSpec.describe API::BulkImports, feature_category: :importers do expect(response).to have_gitlab_http_status(:unauthorized) end end + + describe 'GET /bulk_imports/:id/entities/:entity_id/failures' do + let(:request) { get api("/bulk_imports/#{import_2.id}/entities/#{entity_3.id}/failures", user) } + + it 'returns specified entity failures' do + request + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response.first['exception_message']).to eq(failure_3.exception_message) + end + + it_behaves_like 'disabled feature' + end end diff --git a/spec/requests/api/ci/job_artifacts_spec.rb b/spec/requests/api/ci/job_artifacts_spec.rb index 6f4e7fd66ed..b96ba356855 100644 --- a/spec/requests/api/ci/job_artifacts_spec.rb +++ b/spec/requests/api/ci/job_artifacts_spec.rb @@ -14,9 +14,7 @@ RSpec.describe API::Ci::JobArtifacts, feature_category: :build_artifacts do end let_it_be(:pipeline, reload: true) do - create(:ci_pipeline, project: project, - sha: project.commit.id, - ref: project.default_branch) + create(:ci_pipeline, project: project, sha: project.commit.id, ref: project.default_branch) end let(:user) { create(:user) } @@ -179,8 +177,7 @@ RSpec.describe API::Ci::JobArtifacts, feature_category: :build_artifacts do context 'when project is public' do it 'allows to access artifacts' do - project.update_column(:visibility_level, - Gitlab::VisibilityLevel::PUBLIC) + project.update_column(:visibility_level, Gitlab::VisibilityLevel::PUBLIC) project.update_column(:public_builds, true) get_artifact_file(artifact) @@ -193,8 +190,7 @@ RSpec.describe API::Ci::JobArtifacts, feature_category: :build_artifacts do let(:job) { create(:ci_build, :artifacts, :with_private_artifacts_config, pipeline: pipeline) } it 'rejects access to artifacts' do - project.update_column(:visibility_level, - Gitlab::VisibilityLevel::PUBLIC) + project.update_column(:visibility_level, Gitlab::VisibilityLevel::PUBLIC) project.update_column(:public_builds, true) get_artifact_file(artifact) @@ -208,8 +204,7 @@ RSpec.describe API::Ci::JobArtifacts, feature_category: :build_artifacts do end it 'allows access to artifacts' do - project.update_column(:visibility_level, - Gitlab::VisibilityLevel::PUBLIC) + project.update_column(:visibility_level, Gitlab::VisibilityLevel::PUBLIC) project.update_column(:public_builds, true) get_artifact_file(artifact) @@ -221,8 +216,7 @@ RSpec.describe API::Ci::JobArtifacts, feature_category: :build_artifacts do 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(:visibility_level, Gitlab::VisibilityLevel::PUBLIC) project.update_column(:public_builds, false) get_artifact_file(artifact) @@ -233,8 +227,7 @@ RSpec.describe API::Ci::JobArtifacts, feature_category: :build_artifacts do 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(:visibility_level, Gitlab::VisibilityLevel::PRIVATE) project.update_column(:public_builds, true) get_artifact_file(artifact) @@ -254,8 +247,7 @@ RSpec.describe API::Ci::JobArtifacts, feature_category: :build_artifacts 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/) + .to include('Content-Type' => 'application/json', 'Gitlab-Workhorse-Send-Data' => /artifacts-entry/) expect(response.headers.to_h) .not_to include('Gitlab-Workhorse-Detect-Content-Type' => 'true') expect(response.parsed_body).to be_empty @@ -404,10 +396,12 @@ RSpec.describe API::Ci::JobArtifacts, feature_category: :build_artifacts do end before do - stub_object_storage_uploader(config: Gitlab.config.artifacts.object_store, - uploader: JobArtifactUploader, - proxy_download: proxy_download, - cdn: cdn_config) + stub_object_storage_uploader( + config: Gitlab.config.artifacts.object_store, + uploader: JobArtifactUploader, + proxy_download: proxy_download, + cdn: cdn_config + ) allow(Gitlab::ApplicationContext).to receive(:push).and_call_original end @@ -624,10 +618,11 @@ RSpec.describe API::Ci::JobArtifacts, feature_category: :build_artifacts do 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/, - 'Gitlab-Workhorse-Detect-Content-Type' => 'true') + expect(response.headers.to_h).to include( + 'Content-Type' => 'application/json', + 'Gitlab-Workhorse-Send-Data' => /artifacts-entry/, + 'Gitlab-Workhorse-Detect-Content-Type' => 'true' + ) end end @@ -695,10 +690,11 @@ RSpec.describe API::Ci::JobArtifacts, feature_category: :build_artifacts do 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/, - 'Gitlab-Workhorse-Detect-Content-Type' => 'true') + expect(response.headers.to_h).to include( + 'Content-Type' => 'application/json', + 'Gitlab-Workhorse-Send-Data' => /artifacts-entry/, + 'Gitlab-Workhorse-Detect-Content-Type' => 'true' + ) expect(response.parsed_body).to be_empty end end @@ -713,10 +709,11 @@ RSpec.describe API::Ci::JobArtifacts, feature_category: :build_artifacts 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/, - 'Gitlab-Workhorse-Detect-Content-Type' => 'true') + expect(response.headers.to_h).to include( + 'Content-Type' => 'application/json', + 'Gitlab-Workhorse-Send-Data' => /artifacts-entry/, + 'Gitlab-Workhorse-Detect-Content-Type' => 'true' + ) end end @@ -765,8 +762,15 @@ RSpec.describe API::Ci::JobArtifacts, feature_category: :build_artifacts do 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) + create( + :ci_build, + :trace_artifact, + :artifacts, + :success, + project: project, + pipeline: pipeline, + artifacts_expire_at: Time.now + 7.days + ) end it 'keeps artifacts' do diff --git a/spec/requests/api/ci/jobs_spec.rb b/spec/requests/api/ci/jobs_spec.rb index 41e35de189e..382aabd45a1 100644 --- a/spec/requests/api/ci/jobs_spec.rb +++ b/spec/requests/api/ci/jobs_spec.rb @@ -14,9 +14,7 @@ RSpec.describe API::Ci::Jobs, feature_category: :continuous_integration do end let_it_be(:pipeline, reload: true) do - create(:ci_pipeline, project: project, - sha: project.commit.id, - ref: project.default_branch) + create(:ci_pipeline, project: project, sha: project.commit.id, ref: project.default_branch) end let(:user) { create(:user) } @@ -25,10 +23,14 @@ RSpec.describe API::Ci::Jobs, feature_category: :continuous_integration do let(:guest) { create(:project_member, :guest, project: project).user } let(:running_job) do - create(:ci_build, :running, project: project, - user: user, - pipeline: pipeline, - artifacts_expire_at: 1.day.since) + create( + :ci_build, + :running, + project: project, + user: user, + pipeline: pipeline, + artifacts_expire_at: 1.day.since + ) end let!(:job) do @@ -266,7 +268,7 @@ RSpec.describe API::Ci::Jobs, feature_category: :continuous_integration do expect(json_response.dig('project', 'groups')).to match_array([{ 'id' => group.id }]) expect(json_response.dig('user', 'id')).to eq(api_user.id) expect(json_response.dig('user', 'username')).to eq(api_user.username) - expect(json_response.dig('user', 'roles_in_project')).to match_array %w(guest reporter developer) + expect(json_response.dig('user', 'roles_in_project')).to match_array %w[guest reporter developer] expect(json_response).not_to include('environment') end @@ -450,7 +452,7 @@ RSpec.describe API::Ci::Jobs, feature_category: :continuous_integration do end context 'filter project with array of scope elements' do - let(:query) { { scope: %w(pending running) } } + let(:query) { { scope: %w[pending running] } } it do expect(response).to have_gitlab_http_status(:ok) @@ -459,7 +461,7 @@ RSpec.describe API::Ci::Jobs, feature_category: :continuous_integration do end context 'respond 400 when scope contains invalid state' do - let(:query) { { scope: %w(unknown running) } } + let(:query) { { scope: %w[unknown running] } } it { expect(response).to have_gitlab_http_status(:bad_request) } end @@ -789,14 +791,14 @@ RSpec.describe API::Ci::Jobs, feature_category: :continuous_integration do end context 'authorized user' do - context 'user with :update_build persmission' do + context 'user with :cancel_build permission' do it 'cancels running or pending job' do expect(response).to have_gitlab_http_status(:created) expect(project.builds.first.status).to eq('success') end end - context 'user without :update_build permission' do + context 'user without :cancel_build permission' do let(:api_user) { reporter } it 'does not cancel job' do diff --git a/spec/requests/api/ci/pipelines_spec.rb b/spec/requests/api/ci/pipelines_spec.rb index 3544a6dd72a..eef125e1bc3 100644 --- a/spec/requests/api/ci/pipelines_spec.rb +++ b/spec/requests/api/ci/pipelines_spec.rb @@ -13,8 +13,14 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do end let_it_be(:pipeline) do - create(:ci_empty_pipeline, project: project, sha: project.commit.id, - ref: project.default_branch, user: user, name: 'Build pipeline') + create( + :ci_empty_pipeline, + project: project, + sha: project.commit.id, + ref: project.default_branch, + user: user, + name: 'Build pipeline' + ) end before do @@ -357,8 +363,13 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do let(:query) { {} } let(:api_user) { user } let_it_be(:job) do - create(:ci_build, :success, name: 'build', pipeline: pipeline, - artifacts_expire_at: 1.day.since) + create( + :ci_build, + :success, + name: 'build', + pipeline: pipeline, + artifacts_expire_at: 1.day.since + ) end let(:guest) { create(:project_member, :guest, project: project).user } @@ -436,7 +447,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do end context 'filter jobs with array of scope elements' do - let(:query) { { scope: %w(pending running) } } + let(:query) { { scope: %w[pending running] } } it :aggregate_failures do expect(response).to have_gitlab_http_status(:ok) @@ -445,7 +456,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do end context 'respond 400 when scope contains invalid state' do - let(:query) { { scope: %w(unknown running) } } + let(:query) { { scope: %w[unknown running] } } it { expect(response).to have_gitlab_http_status(:bad_request) } end @@ -540,12 +551,14 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do let(:downstream_pipeline) { create(:ci_pipeline) } let!(:pipeline_source) do - create(:ci_sources_pipeline, - source_pipeline: pipeline, - source_project: project, - source_job: bridge, - pipeline: downstream_pipeline, - project: downstream_pipeline.project) + create( + :ci_sources_pipeline, + source_pipeline: pipeline, + source_project: project, + source_job: bridge, + pipeline: downstream_pipeline, + project: downstream_pipeline.project + ) end let(:query) { {} } @@ -615,7 +628,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do end context 'with array of scope elements' do - let(:query) { { scope: %w(pending running) } } + let(:query) { { scope: %w[pending running] } } it :skip_before_request, :aggregate_failures do get api("/projects/#{project.id}/pipelines/#{pipeline.id}/bridges", api_user), params: query @@ -623,14 +636,14 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do expect(response).to have_gitlab_http_status(:ok) expect(json_response).to be_an Array expect(json_response.count).to eq 2 - json_response.each { |r| expect(%w(pending running).include?(r['status'])).to be true } + json_response.each { |r| expect(%w[pending running].include?(r['status'])).to be true } end end end context 'respond 400 when scope contains invalid state' do context 'in an array' do - let(:query) { { scope: %w(unknown running) } } + let(:query) { { scope: %w[unknown running] } } it { expect(response).to have_gitlab_http_status(:bad_request) } end @@ -713,12 +726,14 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do def create_bridge(pipeline, status = :created) create(:ci_bridge, status: status, pipeline: pipeline).tap do |bridge| downstream_pipeline = create(:ci_pipeline) - create(:ci_sources_pipeline, - source_pipeline: pipeline, - source_project: pipeline.project, - source_job: bridge, - pipeline: downstream_pipeline, - project: downstream_pipeline.project) + create( + :ci_sources_pipeline, + source_pipeline: pipeline, + source_project: pipeline.project, + source_job: bridge, + pipeline: downstream_pipeline, + project: downstream_pipeline.project + ) end end end @@ -914,13 +929,24 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do let(:second_branch) { project.repository.branches[2] } let!(:second_pipeline) do - create(:ci_empty_pipeline, project: project, sha: second_branch.target, - ref: second_branch.name, user: user, name: 'Build pipeline') + create( + :ci_empty_pipeline, + project: project, + sha: second_branch.target, + ref: second_branch.name, + user: user, + name: 'Build pipeline' + ) end before do - create(:ci_empty_pipeline, project: project, sha: project.commit.parent.id, - ref: project.default_branch, user: user) + create( + :ci_empty_pipeline, + project: project, + sha: project.commit.parent.id, + ref: project.default_branch, + user: user + ) end context 'default repository branch' do @@ -1107,11 +1133,82 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do end end + describe 'PUT /projects/:id/pipelines/:pipeline_id/name' do + let_it_be(:pipeline_creator) { create(:user) } + let(:pipeline) { create(:ci_pipeline, project: project, user: pipeline_creator) } + let(:name) { 'A new pipeline name' } + + subject(:execute) do + put api("/projects/#{project.id}/pipelines/#{pipeline.id}/metadata", current_user), params: { name: name } + end + + context 'authorized user' do + let(:current_user) { create(:user) } + + before do + project.add_developer(current_user) + end + + it 'renames pipeline when name is valid', :aggregate_failures do + expect { execute }.to change { pipeline.reload.name }.to(name) + expect(response).to have_gitlab_http_status(:ok) + end + + context 'when name is invalid' do + let(:name) { 'a' * 256 } + + it 'does not rename pipeline', :aggregate_failures do + expect { execute }.not_to change { pipeline.reload.name } + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['message']).to eq('Failed to update pipeline - Name is too long (maximum is 255 characters)') + end + end + end + + context 'unauthorized user' do + let(:current_user) { create(:user) } + + context 'when user is not a member' do + it 'does not rename pipeline', :aggregate_failures do + expect { execute }.not_to change { pipeline.reload.name } + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'when user is a member' do + before do + project.add_reporter(current_user) + end + + it 'does not rename pipeline', :aggregate_failures do + expect { execute }.not_to change { pipeline.reload.name } + expect(response).to have_gitlab_http_status(:forbidden) + end + end + end + + context 'when authorized with job token' do + let(:job) { create(:ci_build, :running, pipeline: pipeline, project: project, user: pipeline.user) } + + before do + project.add_developer(pipeline.user) + end + + subject(:execute) do + put api("/projects/#{project.id}/pipelines/#{pipeline.id}/metadata", nil, job_token: job.token), params: { name: name } + end + + it 'renames pipeline when name is valid', :aggregate_failures do + expect { execute }.to change { pipeline.reload.name }.to(name) + expect(response).to have_gitlab_http_status(:ok) + end + end + end + describe 'POST /projects/:id/pipelines/:pipeline_id/retry' do context 'authorized user' do let_it_be(:pipeline) do - create(:ci_pipeline, project: project, sha: project.commit.id, - ref: project.default_branch) + create(:ci_pipeline, project: project, sha: project.commit.id, ref: project.default_branch) end let_it_be(:build) { create(:ci_build, :failed, pipeline: pipeline) } @@ -1156,8 +1253,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do describe 'POST /projects/:id/pipelines/:pipeline_id/cancel' do let_it_be(:pipeline) do - create(:ci_empty_pipeline, project: project, sha: project.commit.id, - ref: project.default_branch) + create(:ci_empty_pipeline, project: project, sha: project.commit.id, ref: project.default_branch) end let_it_be(:build) { create(:ci_build, :running, pipeline: pipeline) } diff --git a/spec/requests/api/ci/resource_groups_spec.rb b/spec/requests/api/ci/resource_groups_spec.rb index 26265aec1dc..809b1c7c3d3 100644 --- a/spec/requests/api/ci/resource_groups_spec.rb +++ b/spec/requests/api/ci/resource_groups_spec.rb @@ -126,9 +126,7 @@ RSpec.describe API::Ci::ResourceGroups, feature_category: :continuous_delivery d context 'when resource group key contains a slash' do let_it_be(:resource_group) { create(:ci_resource_group, project: project, key: 'test/test') } let_it_be(:upcoming_processable) do - create(:ci_processable, - :waiting_for_resource, - resource_group: resource_group) + create(:ci_processable, :waiting_for_resource, resource_group: resource_group) end let(:key) { 'test%2Ftest' } diff --git a/spec/requests/api/ci/runner/jobs_artifacts_spec.rb b/spec/requests/api/ci/runner/jobs_artifacts_spec.rb index 2e0be23ba90..637469411d5 100644 --- a/spec/requests/api/ci/runner/jobs_artifacts_spec.rb +++ b/spec/requests/api/ci/runner/jobs_artifacts_spec.rb @@ -30,8 +30,15 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego describe '/api/v4/jobs' do let(:job) do - create(:ci_build, :artifacts, :extended_options, - pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) + create( + :ci_build, + :artifacts, + :extended_options, + pipeline: pipeline, + name: 'spinach', + stage: 'test', + stage_idx: 0 + ) end describe '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 7f9c9a13311..2a870a25ea6 100644 --- a/spec/requests/api/ci/runner/jobs_request_post_spec.rb +++ b/spec/requests/api/ci/runner/jobs_request_post_spec.rb @@ -24,8 +24,17 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego let(:runner) { create(:ci_runner, :project, projects: [project]) } let(:pipeline) { create(:ci_pipeline, project: project, ref: 'master') } let(:job) do - create(:ci_build, :pending, :queued, :artifacts, :extended_options, - pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) + create( + :ci_build, + :pending, + :queued, + :artifacts, + :extended_options, + pipeline: pipeline, + name: 'spinach', + stage: 'test', + stage_idx: 0 + ) end describe 'POST /api/v4/jobs/request' do @@ -202,12 +211,12 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego let(:expected_steps) do [{ 'name' => 'script', - 'script' => %w(echo), + 'script' => %w[echo], 'timeout' => job.metadata_timeout, 'when' => 'on_success', 'allow_failure' => false }, { 'name' => 'after_script', - 'script' => %w(ls date), + 'script' => %w[ls date], 'timeout' => job.metadata_timeout, 'when' => 'always', 'allow_failure' => true }] @@ -226,7 +235,7 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego let(:expected_artifacts) do [{ 'name' => 'artifacts_file', 'untracked' => false, - 'paths' => %w(out/), + 'paths' => %w[out/], 'when' => 'always', 'expire_in' => '7d', "artifact_type" => "archive", @@ -342,10 +351,11 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego request_job expect(response).to have_gitlab_http_status(:created) - expect(json_response['git_info']['refspecs']) - .to contain_exactly("+refs/pipelines/#{pipeline.id}:refs/pipelines/#{pipeline.id}", - '+refs/tags/*:refs/tags/*', - '+refs/heads/*:refs/remotes/origin/*') + expect(json_response['git_info']['refspecs']).to contain_exactly( + "+refs/pipelines/#{pipeline.id}:refs/pipelines/#{pipeline.id}", + '+refs/tags/*:refs/tags/*', + '+refs/heads/*:refs/remotes/origin/*' + ) end end end @@ -383,10 +393,11 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego request_job expect(response).to have_gitlab_http_status(:created) - expect(json_response['git_info']['refspecs']) - .to contain_exactly("+refs/pipelines/#{pipeline.id}:refs/pipelines/#{pipeline.id}", - '+refs/tags/*:refs/tags/*', - '+refs/heads/*:refs/remotes/origin/*') + expect(json_response['git_info']['refspecs']).to contain_exactly( + "+refs/pipelines/#{pipeline.id}:refs/pipelines/#{pipeline.id}", + '+refs/tags/*:refs/tags/*', + '+refs/heads/*:refs/remotes/origin/*' + ) end end end @@ -461,7 +472,7 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego expect { request_job }.to change { runner.reload.contacted_at } end - %w(version revision platform architecture).each do |param| + %w[version revision platform architecture].each do |param| context "when info parameter '#{param}' is present" do let(:value) { "#{param}_value" } @@ -646,8 +657,16 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego context 'when job has code coverage report' do let(:job) do - create(:ci_build, :pending, :queued, :coverage_report_cobertura, - pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) + create( + :ci_build, + :pending, + :queued, + :coverage_report_cobertura, + pipeline: pipeline, + name: 'spinach', + stage: 'test', + stage_idx: 0 + ) end let(:expected_artifacts) do @@ -788,9 +807,16 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego describe 'time_in_queue_seconds support' do let(:job) do - create(:ci_build, :pending, :queued, pipeline: pipeline, - name: 'spinach', stage: 'test', stage_idx: 0, - queued_at: 60.seconds.ago) + create( + :ci_build, + :pending, + :queued, + pipeline: pipeline, + name: 'spinach', + stage: 'test', + stage_idx: 0, + queued_at: 60.seconds.ago + ) end it 'presents the time_in_queue_seconds info in the payload' do diff --git a/spec/requests/api/ci/runner/jobs_trace_spec.rb b/spec/requests/api/ci/runner/jobs_trace_spec.rb index ee00fc5a793..8c596d2338f 100644 --- a/spec/requests/api/ci/runner/jobs_trace_spec.rb +++ b/spec/requests/api/ci/runner/jobs_trace_spec.rb @@ -23,14 +23,28 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_trace_chunks, feature_catego let(:runner) { create(:ci_runner, :project, projects: [project]) } let(:user) { create(:user) } let(:job) do - create(:ci_build, :artifacts, :extended_options, - pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) + create( + :ci_build, + :artifacts, + :extended_options, + pipeline: pipeline, + name: 'spinach', + stage: 'test', + stage_idx: 0 + ) end describe 'PATCH /api/v4/jobs/:id/trace' do let(:job) do - create(:ci_build, :running, :trace_live, - project: project, user: user, runner_id: runner.id, pipeline: pipeline) + create( + :ci_build, + :running, + :trace_live, + project: project, + user: user, + runner_id: runner.id, + pipeline: pipeline + ) end let(:headers) { { API::Ci::Helpers::Runner::JOB_TOKEN_HEADER => job.token, 'Content-Type' => 'text/plain' } } diff --git a/spec/requests/api/ci/runner/runners_post_spec.rb b/spec/requests/api/ci/runner/runners_post_spec.rb index c5e49e9ac54..1490172d1c3 100644 --- a/spec/requests/api/ci/runner/runners_post_spec.rb +++ b/spec/requests/api/ci/runner/runners_post_spec.rb @@ -48,7 +48,7 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego description: 'server.hostname', maintenance_note: 'Some maintainer notes', run_untagged: false, - tag_list: %w(tag1 tag2), + tag_list: %w[tag1 tag2], locked: true, active: true, access_level: 'ref_protected', @@ -167,7 +167,7 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego allow_any_instance_of(::Ci::Runner).to receive(:cache_attributes) end - %w(name version revision platform architecture).each do |param| + %w[name version revision platform architecture].each do |param| context "when info parameter '#{param}' info is present" do let(:value) { "#{param}_value" } @@ -185,8 +185,8 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego it "sets the runner's ip_address" do post api('/runners'), - params: { token: registration_token }, - headers: { 'X-Forwarded-For' => '123.111.123.111' } + params: { token: registration_token }, + headers: { 'X-Forwarded-For' => '123.111.123.111' } expect(response).to have_gitlab_http_status(:created) expect(::Ci::Runner.last.ip_address).to eq('123.111.123.111') diff --git a/spec/requests/api/ci/runners_spec.rb b/spec/requests/api/ci/runners_spec.rb index 2b2d2e0def8..ba80684e89e 100644 --- a/spec/requests/api/ci/runners_spec.rb +++ b/spec/requests/api/ci/runners_spec.rb @@ -249,6 +249,39 @@ RSpec.describe API::Ci::Runners, :aggregate_failures, feature_category: :runner_ a_hash_including('description' => 'Runner tagged with tag1 and tag2') ] end + + context 'with ci_runner_machines' do + let_it_be(:version_ci_runner) { create(:ci_runner, :project, description: 'Runner with machine') } + let_it_be(:version_ci_runner_machine) { create(:ci_runner_machine, runner: version_ci_runner, version: '15.0.3') } + let_it_be(:version_16_ci_runner) { create(:ci_runner, :project, description: 'Runner with machine version 16') } + let_it_be(:version_16_ci_runner_machine) { create(:ci_runner_machine, runner: version_16_ci_runner, version: '16.0.1') } + + it 'filters runners by version_prefix when prefix is "15.0"' do + get api('/runners/all?version_prefix=15.0', admin, admin_mode: true) + + expect(json_response).to match_array [ + a_hash_including('description' => 'Runner with machine', 'active' => true, 'paused' => false) + ] + end + + it 'filters runners by version_prefix when prefix is "16"' do + get api('/runners/all?version_prefix=16', admin, admin_mode: true) + expect(json_response).to match_array [ + a_hash_including('description' => 'Runner with machine version 16', 'active' => true, 'paused' => false) + ] + end + + it 'filters runners by version_prefix when prefix is "25"' do + get api('/runners/all?version_prefix=25', admin, admin_mode: true) + expect(json_response).to match_array [] + end + + it 'does not filter runners by version_prefix when prefix is invalid ("V15")' do + get api('/runners/all?version_prefix=v15', admin, admin_mode: true) + + expect(response).to have_gitlab_http_status(:bad_request) + end + end end context 'without admin privileges' do @@ -467,13 +500,17 @@ RSpec.describe API::Ci::Runners, :aggregate_failures, feature_category: :runner_ active = shared_runner.active runner_queue_value = shared_runner.ensure_runner_queue_value - update_runner(shared_runner.id, admin, description: "#{description}_updated", - active: !active, - tag_list: ['ruby2.1', 'pgsql', 'mysql'], - run_untagged: 'false', - locked: 'true', - access_level: 'ref_protected', - maximum_timeout: 1234) + update_runner( + shared_runner.id, + admin, + description: "#{description}_updated", + active: !active, + tag_list: ['ruby2.1', 'pgsql', 'mysql'], + run_untagged: 'false', + locked: 'true', + access_level: 'ref_protected', + maximum_timeout: 1234 + ) shared_runner.reload expect(response).to have_gitlab_http_status(:ok) diff --git a/spec/requests/api/ci/triggers_spec.rb b/spec/requests/api/ci/triggers_spec.rb index ff54ba61309..a6e50479963 100644 --- a/spec/requests/api/ci/triggers_spec.rb +++ b/spec/requests/api/ci/triggers_spec.rb @@ -81,7 +81,7 @@ RSpec.describe API::Ci::Triggers, feature_category: :continuous_integration do end it 'validates variables needs to be a map of key-valued strings' do - post api("/projects/#{project.id}/trigger/pipeline"), params: options.merge(variables: { 'TRIGGER_KEY' => %w(1 2) }, ref: 'master') + post api("/projects/#{project.id}/trigger/pipeline"), params: options.merge(variables: { 'TRIGGER_KEY' => %w[1 2] }, ref: 'master') expect(response).to have_gitlab_http_status(:bad_request) expect(json_response['message']).to eq('variables needs to be a map of key-valued strings') diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb index c3a7dbdcdbb..6a112918288 100644 --- a/spec/requests/api/commits_spec.rb +++ b/spec/requests/api/commits_spec.rb @@ -2426,16 +2426,6 @@ RSpec.describe API::Commits, feature_category: :source_code_management do expect(json_response['x509_certificate']['x509_issuer']['crl_url']).to eq(commit.signature.x509_certificate.x509_issuer.crl_url) expect(json_response['commit_source']).to eq('gitaly') end - - context 'with Rugged enabled', :enable_rugged do - it 'returns correct JSON' do - get api(route, current_user) - - expect(response).to have_gitlab_http_status(:ok) - expect(json_response['signature_type']).to eq('X509') - expect(json_response['commit_source']).to eq('gitaly') - end - end end context 'with ssh signed commit' do diff --git a/spec/requests/api/container_repositories_spec.rb b/spec/requests/api/container_repositories_spec.rb index 605fa0d92f6..82c63362166 100644 --- a/spec/requests/api/container_repositories_spec.rb +++ b/spec/requests/api/container_repositories_spec.rb @@ -67,7 +67,7 @@ RSpec.describe API::ContainerRepositories, feature_category: :container_registry let(:url) { "/registry/repositories/#{repository.id}?tags=true" } before do - stub_container_registry_tags(repository: repository.path, tags: %w(rootA latest), with_manifest: true) + stub_container_registry_tags(repository: repository.path, tags: %w[rootA latest], with_manifest: true) end it 'returns a repository and its tags' do @@ -102,7 +102,7 @@ RSpec.describe API::ContainerRepositories, feature_category: :container_registry let(:url) { "/registry/repositories/#{repository.id}?tags_count=true" } before do - stub_container_registry_tags(repository: repository.path, tags: %w(rootA latest), with_manifest: true) + stub_container_registry_tags(repository: repository.path, tags: %w[rootA latest], with_manifest: true) end it 'returns a repository and its tags_count' do diff --git a/spec/requests/api/deployments_spec.rb b/spec/requests/api/deployments_spec.rb index 82ac2eed83d..41c5847e940 100644 --- a/spec/requests/api/deployments_spec.rb +++ b/spec/requests/api/deployments_spec.rb @@ -424,7 +424,7 @@ RSpec.describe API::Deployments, feature_category: :continuous_delivery do ) expect(response).to have_gitlab_http_status(:bad_request) - expect(json_response['message']['status']).to include(%{cannot transition via \"run\"}) + expect(json_response['message']['status']).to include(%(cannot transition via \"run\")) end it 'links merge requests when the deployment status changes to success', :sidekiq_inline do diff --git a/spec/requests/api/geo_spec.rb b/spec/requests/api/geo_spec.rb index 3dec91fd2fa..c394553d14e 100644 --- a/spec/requests/api/geo_spec.rb +++ b/spec/requests/api/geo_spec.rb @@ -14,7 +14,7 @@ RSpec.describe API::Geo, feature_category: :geo_replication do { 'type' => 'object', 'additionalProperties' => false, - 'required' => %w(geo_enabled), + 'required' => %w[geo_enabled], 'properties' => { 'geo_enabled' => { 'type' => 'boolean' } } diff --git a/spec/requests/api/graphql/abuse_report_spec.rb b/spec/requests/api/graphql/abuse_report_spec.rb index 7d0b8b35763..f74b1fb4061 100644 --- a/spec/requests/api/graphql/abuse_report_spec.rb +++ b/spec/requests/api/graphql/abuse_report_spec.rb @@ -2,49 +2,122 @@ require 'spec_helper' -RSpec.describe 'abuse_report', feature_category: :insider_threat do +RSpec.describe 'Querying an Abuse Report', feature_category: :insider_threat do include GraphqlHelpers let_it_be(:current_user) { create(:admin) } - let_it_be(:label) { create(:abuse_report_label, title: 'Uno') } - let_it_be(:report) { create(:abuse_report, labels: [label]) } - - let(:report_gid) { Gitlab::GlobalId.build(report, id: report.id).to_s } - - let(:fields) do - <<~GRAPHQL - labels { - nodes { - id - title - description - color - textColor - } - } - GRAPHQL - end + let_it_be(:abuse_report) { create(:abuse_report) } - let(:arguments) { { id: report_gid } } - let(:query) { graphql_query_for('abuseReport', arguments, fields) } + let(:global_id) { abuse_report.to_gid.to_s } + let(:abuse_report_fields) { all_graphql_fields_for('AbuseReport', max_depth: 2) } + let(:abuse_report_data) { graphql_data['abuseReport'] } + + let(:query) do + graphql_query_for('abuseReport', { 'id' => global_id }, abuse_report_fields) + end before do post_graphql(query, current_user: current_user) end - it_behaves_like 'a working graphql query that returns data' + context 'when the user is an admin' do + it_behaves_like 'a working graphql query that returns data' - it 'returns abuse report with labels' do - expect(graphql_data_at('abuseReport', 'labels', 'nodes', 0)).to match(a_graphql_entity_for(label)) + it 'returns all fields' do + expect(abuse_report_data).to include( + 'id' => global_id, + 'userPermissions' => { + 'readAbuseReport' => true, + 'createNote' => true + } + ) + end end - context 'when current user is not an admin' do - let_it_be(:current_user) { create(:user) } + context 'when the user is not an admin' do + let(:current_user) { create(:user) } + + it 'returns nil' do + expect(abuse_report_data).to be_nil + end + end - it_behaves_like 'a working graphql query' + describe 'labels' do + let_it_be(:abuse_report_label) { create(:abuse_report_label, title: 'Label') } + let_it_be(:abuse_report) { create(:abuse_report, labels: [abuse_report_label]) } + + let(:labels_response) do + graphql_data_at(:abuse_report, :labels, :nodes) + end + + let(:abuse_report_fields) do + <<~GRAPHQL + labels { + nodes { + id + title + description + color + textColor + } + } + GRAPHQL + end + + it 'returns labels' do + expect(labels_response).to contain_exactly( + a_graphql_entity_for(abuse_report_label) + ) + end + end + + describe 'notes' do + let_it_be(:note) { create(:note, noteable: abuse_report, author: current_user) } + + let(:notes_response) do + graphql_data_at(:abuse_report, :notes, :nodes) + end + + let(:abuse_report_fields) do + <<~GRAPHQL + notes { + nodes { + #{all_graphql_fields_for('Note', max_depth: 2)} + } + } + GRAPHQL + end + + it 'returns notes' do + expect(notes_response).to contain_exactly( + a_graphql_entity_for(note) + ) + end + end + + describe 'discussions' do + let_it_be(:discussion) do + create(:discussion_note_on_abuse_report, noteable: abuse_report, author: current_user).to_discussion + end + + let(:discussions_response) do + graphql_data_at(:abuse_report, :discussions, :nodes) + end + + let(:abuse_report_fields) do + <<~GRAPHQL + discussions { + nodes { + #{all_graphql_fields_for('Discussion', max_depth: 2)} + } + } + GRAPHQL + end - it 'does not contain any data' do - expect(graphql_data_at('abuseReportLabel')).to be_nil + it 'returns discussions' do + expect(discussions_response).to contain_exactly( + a_graphql_entity_for(discussion) + ) end end end diff --git a/spec/requests/api/graphql/ci/catalog/resource_spec.rb b/spec/requests/api/graphql/ci/catalog/resource_spec.rb new file mode 100644 index 00000000000..fce773f320b --- /dev/null +++ b/spec/requests/api/graphql/ci/catalog/resource_spec.rb @@ -0,0 +1,341 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Query.ciCatalogResource', feature_category: :pipeline_composition do + include GraphqlHelpers + + let_it_be(:user) { create(:user) } + let_it_be(:namespace) { create(:group) } + + let_it_be(:project) do + create( + :project, :with_avatar, :custom_repo, + name: 'Component Repository', + description: 'A simple component', + namespace: namespace, + star_count: 1, + files: { 'README.md' => '[link](README.md)' } + ) + end + + let_it_be(:resource) { create(:ci_catalog_resource, project: project) } + + let(:query) do + <<~GQL + query { + ciCatalogResource(id: "#{resource.to_global_id}") { + #{all_graphql_fields_for('CiCatalogResource', max_depth: 1)} + } + } + GQL + end + + subject(:post_query) { post_graphql(query, current_user: user) } + + context 'when the current user has permission to read the namespace catalog' do + it 'returns the resource with the expected data' do + namespace.add_developer(user) + + post_query + + expect(graphql_data_at(:ciCatalogResource)).to match( + a_graphql_entity_for( + resource, :name, :description, + icon: project.avatar_path, + webPath: "/#{project.full_path}", + starCount: project.star_count, + forksCount: project.forks_count, + readmeHtml: a_string_including( + "#{project.full_path}/-/blob/#{project.default_branch}/README.md" + ) + ) + ) + end + end + + context 'when the current user does not have permission to read the namespace catalog' do + it 'returns nil' do + namespace.add_guest(user) + + post_query + + expect(graphql_data_at(:ciCatalogResource)).to be_nil + end + end + + describe 'versions' do + before_all do + namespace.add_developer(user) + end + + before do + stub_licensed_features(ci_namespace_catalog: true) + end + + let(:query) do + <<~GQL + query { + ciCatalogResource(id: "#{resource.to_global_id}") { + id + versions { + nodes { + id + tagName + releasedAt + author { + id + name + webUrl + } + } + } + } + } + GQL + end + + context 'when the resource has versions' do + let_it_be(:author) { create(:user, name: 'author') } + + let_it_be(:version1) do + create(:release, project: project, released_at: '2023-01-01T00:00:00Z', author: author) + end + + let_it_be(:version2) do + create(:release, project: project, released_at: '2023-02-01T00:00:00Z', author: author) + end + + it 'returns the resource with the versions data' do + post_query + + expect(graphql_data_at(:ciCatalogResource)).to match( + a_graphql_entity_for(resource) + ) + + expect(graphql_data_at(:ciCatalogResource, :versions, :nodes)).to contain_exactly( + a_graphql_entity_for( + version1, + tagName: version1.tag, + releasedAt: version1.released_at, + author: a_graphql_entity_for(author, :name) + ), + a_graphql_entity_for( + version2, + tagName: version2.tag, + releasedAt: version2.released_at, + author: a_graphql_entity_for(author, :name) + ) + ) + end + end + + context 'when the resource does not have a version' do + it 'returns versions as an empty array' do + post_query + + expect(graphql_data_at(:ciCatalogResource)).to match( + a_graphql_entity_for(resource, versions: { 'nodes' => [] }) + ) + end + end + end + + describe 'latestVersion' do + before_all do + namespace.add_developer(user) + end + + before do + stub_licensed_features(ci_namespace_catalog: true) + end + + let(:query) do + <<~GQL + query { + ciCatalogResource(id: "#{resource.to_global_id}") { + id + latestVersion { + id + tagName + releasedAt + author { + id + name + webUrl + } + } + } + } + GQL + end + + context 'when the resource has versions' do + let_it_be(:author) { create(:user, name: 'author') } + + let_it_be(:latest_version) do + create(:release, project: project, released_at: '2023-02-01T00:00:00Z', author: author) + end + + before_all do + # Previous version of the project + create(:release, project: project, released_at: '2023-01-01T00:00:00Z', author: author) + end + + it 'returns the resource with the latest version data' do + post_query + + expect(graphql_data_at(:ciCatalogResource)).to match( + a_graphql_entity_for( + resource, + latestVersion: a_graphql_entity_for( + latest_version, + tagName: latest_version.tag, + releasedAt: latest_version.released_at, + author: a_graphql_entity_for(author, :name) + ) + ) + ) + end + end + + context 'when the resource does not have a version' do + it 'returns nil' do + post_query + + expect(graphql_data_at(:ciCatalogResource)).to match( + a_graphql_entity_for(resource, latestVersion: nil) + ) + end + end + end + + describe 'rootNamespace' do + before_all do + namespace.add_developer(user) + end + + before do + stub_licensed_features(ci_namespace_catalog: true) + end + + let(:query) do + <<~GQL + query { + ciCatalogResource(id: "#{resource.to_global_id}") { + id + rootNamespace { + id + name + path + } + } + } + GQL + end + + it 'returns the correct root namespace data' do + post_query + + expect(graphql_data_at(:ciCatalogResource)).to match( + a_graphql_entity_for( + resource, + rootNamespace: a_graphql_entity_for(namespace, :name, :path) + ) + ) + end + end + + describe 'openIssuesCount' do + before do + stub_licensed_features(ci_namespace_catalog: true) + end + + context 'when open_issue_count is requested' do + let(:query) do + <<~GQL + query { + ciCatalogResource(id: "#{resource.to_global_id}") { + openIssuesCount + } + } + GQL + end + + it 'returns the correct count' do + create(:issue, :opened, project: project) + create(:issue, :opened, project: project) + + namespace.add_developer(user) + + post_query + + expect(graphql_data_at(:ciCatalogResource)).to match( + a_graphql_entity_for( + open_issues_count: 2 + ) + ) + end + + context 'when open_issue_count is zero' do + it 'returns zero' do + namespace.add_developer(user) + + post_query + + expect(graphql_data_at(:ciCatalogResource)).to match( + a_graphql_entity_for( + open_issues_count: 0 + ) + ) + end + end + end + end + + describe 'openMergeRequestsCount' do + before do + stub_licensed_features(ci_namespace_catalog: true) + end + + context 'when merge_requests_count is requested' do + let(:query) do + <<~GQL + query { + ciCatalogResource(id: "#{resource.to_global_id}") { + openMergeRequestsCount + } + } + GQL + end + + it 'returns the correct count' do + create(:merge_request, :opened, source_project: project) + + namespace.add_developer(user) + + post_query + + expect(graphql_data_at(:ciCatalogResource)).to match( + a_graphql_entity_for( + open_merge_requests_count: 1 + ) + ) + end + + context 'when open merge_requests_count is zero' do + it 'returns zero' do + namespace.add_developer(user) + + post_query + + expect(graphql_data_at(:ciCatalogResource)).to match( + a_graphql_entity_for( + open_merge_requests_count: 0 + ) + ) + end + end + end + end +end diff --git a/spec/requests/api/graphql/ci/catalog/resources_spec.rb b/spec/requests/api/graphql/ci/catalog/resources_spec.rb new file mode 100644 index 00000000000..7c955a1202c --- /dev/null +++ b/spec/requests/api/graphql/ci/catalog/resources_spec.rb @@ -0,0 +1,359 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Query.ciCatalogResources', feature_category: :pipeline_composition do + include GraphqlHelpers + + let_it_be(:user) { create(:user) } + let_it_be(:namespace) { create(:group) } + let_it_be(:project2) { create(:project, namespace: namespace) } + + let_it_be(:project1) do + create( + :project, :with_avatar, :custom_repo, + name: 'Component Repository', + description: 'A simple component', + namespace: namespace, + star_count: 1, + files: { 'README.md' => '**Test**' } + ) + end + + let_it_be(:public_project) do + create( + :project, :with_avatar, :custom_repo, :public, + name: 'Public Component', + description: 'A public component', + files: { 'README.md' => '**Test**' } + ) + end + + let_it_be(:resource1) { create(:ci_catalog_resource, project: project1, latest_released_at: '2023-01-01T00:00:00Z') } + let_it_be(:public_resource) { create(:ci_catalog_resource, project: public_project) } + + let(:query) do + <<~GQL + query { + ciCatalogResources { + nodes { + id + name + description + icon + webPath + latestReleasedAt + starCount + forksCount + readmeHtml + } + } + } + GQL + end + + subject(:post_query) { post_graphql(query, current_user: user) } + + shared_examples 'avoids N+1 queries' do + it do + ctx = { current_user: user } + + control_count = ActiveRecord::QueryRecorder.new do + run_with_clean_state(query, context: ctx) + end + + create(:ci_catalog_resource, project: project2) + + expect do + run_with_clean_state(query, context: ctx) + end.not_to exceed_query_limit(control_count) + end + end + + it_behaves_like 'avoids N+1 queries' + + it 'returns the resources with the expected data' do + namespace.add_developer(user) + + post_query + + expect(graphql_data_at(:ciCatalogResources, :nodes)).to contain_exactly( + a_graphql_entity_for( + resource1, :name, :description, + icon: project1.avatar_path, + webPath: "/#{project1.full_path}", + starCount: project1.star_count, + forksCount: project1.forks_count, + readmeHtml: a_string_including('Test</strong>'), + latestReleasedAt: resource1.latest_released_at + ), + a_graphql_entity_for(public_resource) + ) + end + + describe 'versions' do + before_all do + namespace.add_developer(user) + end + + let(:query) do + <<~GQL + query { + ciCatalogResources { + nodes { + id + versions { + nodes { + id + tagName + releasedAt + author { + id + name + webUrl + } + } + } + } + } + } + GQL + end + + it 'limits the request to 1 resource at a time' do + create(:ci_catalog_resource, project: project2) + + post_query + + expect_graphql_errors_to_include \ + [/"versions" field can be requested only for 1 CiCatalogResource\(s\) at a time./] + end + end + + describe 'latestVersion' do + let_it_be(:author1) { create(:user, name: 'author1') } + let_it_be(:author2) { create(:user, name: 'author2') } + + let_it_be(:latest_version1) do + create(:release, project: project1, released_at: '2023-02-01T00:00:00Z', author: author1) + end + + let_it_be(:latest_version2) do + create(:release, project: public_project, released_at: '2023-02-01T00:00:00Z', author: author2) + end + + let(:query) do + <<~GQL + query { + ciCatalogResources { + nodes { + id + latestVersion { + id + tagName + releasedAt + author { + id + name + webUrl + } + } + } + } + } + GQL + end + + before_all do + namespace.add_developer(user) + + # Previous versions of the projects + create(:release, project: project1, released_at: '2023-01-01T00:00:00Z', author: author1) + create(:release, project: public_project, released_at: '2023-01-01T00:00:00Z', author: author2) + end + + it 'returns all resources with the latest version data' do + post_query + + expect(graphql_data_at(:ciCatalogResources, :nodes)).to contain_exactly( + a_graphql_entity_for( + resource1, + latestVersion: a_graphql_entity_for( + latest_version1, + tagName: latest_version1.tag, + releasedAt: latest_version1.released_at, + author: a_graphql_entity_for(author1, :name) + ) + ), + a_graphql_entity_for( + public_resource, + latestVersion: a_graphql_entity_for( + latest_version2, + tagName: latest_version2.tag, + releasedAt: latest_version2.released_at, + author: a_graphql_entity_for(author2, :name) + ) + ) + ) + end + + # TODO: https://gitlab.com/gitlab-org/gitlab/-/issues/430350 + # it_behaves_like 'avoids N+1 queries' + end + + describe 'rootNamespace' do + before_all do + namespace.add_developer(user) + end + + let(:query) do + <<~GQL + query { + ciCatalogResources { + nodes { + id + rootNamespace { + id + name + path + } + } + } + } + GQL + end + + it 'returns the correct root namespace data' do + post_query + + expect(graphql_data_at(:ciCatalogResources, :nodes)).to contain_exactly( + a_graphql_entity_for( + resource1, + rootNamespace: a_graphql_entity_for(namespace, :name, :path) + ), + a_graphql_entity_for(public_resource, rootNamespace: nil) + ) + end + end + + describe 'openIssuesCount' do + before_all do + namespace.add_developer(user) + end + + before_all do + create(:issue, :opened, project: project1) + create(:issue, :opened, project: project1) + + create(:issue, :opened, project: public_project) + end + + let(:query) do + <<~GQL + query { + ciCatalogResources { + nodes { + openIssuesCount + } + } + } + GQL + end + + it 'returns the correct count' do + post_query + + expect(graphql_data_at(:ciCatalogResources, :nodes)).to contain_exactly( + a_graphql_entity_for(openIssuesCount: 2), + a_graphql_entity_for(openIssuesCount: 1) + ) + end + + it_behaves_like 'avoids N+1 queries' + end + + describe 'openMergeRequestsCount' do + before_all do + namespace.add_developer(user) + end + + before_all do + create(:merge_request, :opened, source_project: project1) + create(:merge_request, :opened, source_project: public_project) + end + + let(:query) do + <<~GQL + query { + ciCatalogResources { + nodes { + openMergeRequestsCount + } + } + } + GQL + end + + it 'returns the correct count' do + post_query + + expect(graphql_data_at(:ciCatalogResources, :nodes)).to contain_exactly( + a_graphql_entity_for(openMergeRequestsCount: 1), + a_graphql_entity_for(openMergeRequestsCount: 1) + ) + end + + it_behaves_like 'avoids N+1 queries' + end + + # TODO: https://gitlab.com/gitlab-org/gitlab/-/issues/429636 + context 'when using `projectPath` (legacy) to fetch resources' do + let(:query) do + <<~GQL + query { + ciCatalogResources(projectPath: "#{project1.full_path}") { + nodes { + #{all_graphql_fields_for('CiCatalogResource', max_depth: 1)} + } + } + } + GQL + end + + context 'when the current user has permission to read the namespace catalog' do + before_all do + namespace.add_developer(user) + end + + it 'returns catalog resources with the expected data' do + resource2 = create(:ci_catalog_resource, project: project2) + _resource_in_another_namespace = create(:ci_catalog_resource) + + post_query + + expect(graphql_data_at(:ciCatalogResources, :nodes)).to contain_exactly( + a_graphql_entity_for(resource1), + a_graphql_entity_for( + resource2, :name, :description, + icon: project2.avatar_path, + webPath: "/#{project2.full_path}", + starCount: project2.star_count, + forksCount: project2.forks_count, + readmeHtml: '', + latestReleasedAt: resource2.latest_released_at + ) + ) + end + + it_behaves_like 'avoids N+1 queries' + end + + context 'when the current user does not have permission to read the namespace catalog' do + it 'returns no resources' do + post_query + + expect(graphql_data_at(:ciCatalogResources, :nodes)).to be_empty + end + end + end +end diff --git a/spec/requests/api/graphql/ci/manual_variables_spec.rb b/spec/requests/api/graphql/ci/manual_variables_spec.rb index 47dccc0deb6..41788881e62 100644 --- a/spec/requests/api/graphql/ci/manual_variables_spec.rb +++ b/spec/requests/api/graphql/ci/manual_variables_spec.rb @@ -90,6 +90,6 @@ RSpec.describe 'Query.project(fullPath).pipelines.jobs.manualVariables', feature variables_data = graphql_data.dig('project', 'pipelines', 'nodes').first .dig('jobs', 'nodes').flat_map { |job| job.dig('manualVariables', 'nodes') } - expect(variables_data.map { |var| var['key'] }).to match_array(%w(MANUAL_TEST_VAR_1 MANUAL_TEST_VAR_2)) + expect(variables_data.map { |var| var['key'] }).to match_array(%w[MANUAL_TEST_VAR_1 MANUAL_TEST_VAR_2]) end end diff --git a/spec/requests/api/graphql/ci/runners_spec.rb b/spec/requests/api/graphql/ci/runners_spec.rb index c5571086700..0e2712d742d 100644 --- a/spec/requests/api/graphql/ci/runners_spec.rb +++ b/spec/requests/api/graphql/ci/runners_spec.rb @@ -35,28 +35,16 @@ RSpec.describe 'Query.runners', feature_category: :runner_fleet do end context 'with filters' do - let(:query) do - %( - query { - runners(type: #{runner_type}, status: #{status}) { - #{fields} - } - } - ) - end - - before do - allow_next_instance_of(::Gitlab::Ci::RunnerUpgradeCheck) do |instance| - allow(instance).to receive(:check_runner_upgrade_suggestion) - end - - post_graphql(query, current_user: current_user) - end - shared_examples 'a working graphql query returning expected runner' do - it_behaves_like 'a working graphql query' + it_behaves_like 'a working graphql query' do + before do + post_graphql(query, current_user: current_user) + end + end it 'returns expected runner' do + post_graphql(query, current_user: current_user) + expect(runners_graphql_data['nodes']).to contain_exactly(a_graphql_entity_for(expected_runner)) end @@ -86,22 +74,63 @@ RSpec.describe 'Query.runners', feature_category: :runner_fleet do end end - context 'runner_type is INSTANCE_TYPE and status is ACTIVE' do - let(:runner_type) { 'INSTANCE_TYPE' } - let(:status) { 'ACTIVE' } + context 'when filtered on type and status' do + let(:query) do + %( + query { + runners(type: #{runner_type}, status: #{status}) { + #{fields} + } + } + ) + end - let!(:expected_runner) { instance_runner } + before do + allow_next_instance_of(::Gitlab::Ci::RunnerUpgradeCheck) do |instance| + allow(instance).to receive(:check_runner_upgrade_suggestion) + end + end - it_behaves_like 'a working graphql query returning expected runner' + context 'runner_type is INSTANCE_TYPE and status is ACTIVE' do + let(:runner_type) { 'INSTANCE_TYPE' } + let(:status) { 'ACTIVE' } + + let!(:expected_runner) { instance_runner } + + 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 - context 'runner_type is PROJECT_TYPE and status is NEVER_CONTACTED' do - let(:runner_type) { 'PROJECT_TYPE' } - let(:status) { 'NEVER_CONTACTED' } + context 'when filtered on version prefix' do + let_it_be(:version_runner) { create(:ci_runner, :project, active: false, description: 'Runner with machine') } + let_it_be(:version_runner_machine) { create(:ci_runner_machine, runner: version_runner, version: '15.11.0') } + + let(:query) do + %( + query { + runners(versionPrefix: "#{version_prefix}") { + #{fields} + } + } + ) + end + + context 'version_prefix is "15."' do + let(:version_prefix) { '15.' } - let!(:expected_runner) { project_runner } + let!(:expected_runner) { version_runner } - it_behaves_like 'a working graphql query returning expected runner' + it_behaves_like 'a working graphql query returning expected runner' + end end end diff --git a/spec/requests/api/graphql/container_repository/container_repository_details_spec.rb b/spec/requests/api/graphql/container_repository/container_repository_details_spec.rb index 118a11851dd..20277c7e27b 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 @@ -313,4 +313,124 @@ RSpec.describe 'container repository details', feature_category: :container_regi end it_behaves_like 'handling graphql network errors with the container registry' + + context 'when list tags API is enabled', :saas do + before do + stub_container_registry_config(enabled: true) + allow(ContainerRegistry::GitlabApiClient).to receive(:supports_gitlab_api?).and_return(true) + + allow_next_instances_of(ContainerRegistry::GitlabApiClient, nil) do |client| + allow(client).to receive(:tags).and_return(response_body) + end + end + + let_it_be(:raw_tags_response) do + [ + { + name: 'latest', + digest: 'sha256:1234567892', + config_digest: 'sha256:3332132331', + media_type: 'application/vnd.oci.image.manifest.v1+json', + size_bytes: 1234567892, + created_at: 10.minutes.ago, + updated_at: 10.minutes.ago + } + ] + end + + let_it_be(:url) { URI('/gitlab/v1/repositories/group1/proj1/tags/list/?before=tag1') } + + let_it_be(:response_body) do + { + pagination: { previous: { uri: url }, next: { uri: url } }, + response_body: ::Gitlab::Json.parse(raw_tags_response.to_json) + } + end + + it_behaves_like 'a working graphql query' do # OK + before do + subject + end + + it 'matches the JSON schema' do + expect(container_repository_details_response).to match_schema('graphql/container_repository_details') + end + end + + context 'with different permissions' do # OK + let_it_be(:user) { create(:user) } + + let(:tags_response) { container_repository_details_response.dig('tags', 'nodes') } + + where(:project_visibility, :role, :access_granted, :can_delete) do + :private | :maintainer | true | true + :private | :developer | true | true + :private | :reporter | true | false + :private | :guest | false | false + :private | :anonymous | false | false + :public | :maintainer | true | true + :public | :developer | true | true + :public | :reporter | true | false + :public | :guest | true | false + :public | :anonymous | true | false + end + + with_them do + before do + project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility.to_s.upcase, false)) + project.add_member(user, role) unless role == :anonymous + end + + it 'return the proper response' do + subject + + if access_granted + expect(tags_response.size).to eq(raw_tags_response.size) + expect(container_repository_details_response.dig('canDelete')).to eq(can_delete) + else + expect(container_repository_details_response).to eq(nil) + end + end + end + end + + context 'querying' 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: ContainerRepositoryID!, $n: String) { + containerRepository(id: $id) { + tags(name: $n) { + edges { + node { + #{all_graphql_fields_for('ContainerRepositoryTag')} + } + } + } + } + } + GQL + end + + it 'returns the tag response', :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 + + it_behaves_like 'handling graphql network errors with the container registry' + end end diff --git a/spec/requests/api/graphql/gitlab_schema_spec.rb b/spec/requests/api/graphql/gitlab_schema_spec.rb index d55a70f503c..060a1b42cb6 100644 --- a/spec/requests/api/graphql/gitlab_schema_spec.rb +++ b/spec/requests/api/graphql/gitlab_schema_spec.rb @@ -113,7 +113,7 @@ RSpec.describe 'GitlabSchema configurations', feature_category: :integrations do context 'regular queries' do subject do - query = graphql_query_for('project', { 'fullPath' => project.full_path }, %w(id name description)) + query = graphql_query_for('project', { 'fullPath' => project.full_path }, %w[id name description]) post_graphql(query) end @@ -125,7 +125,7 @@ RSpec.describe 'GitlabSchema configurations', feature_category: :integrations do subject do queries = [ - { query: graphql_query_for('project', { 'fullPath' => '$fullPath' }, %w(id name description)) }, # Complexity 4 + { query: graphql_query_for('project', { 'fullPath' => '$fullPath' }, %w[id name description]) }, # Complexity 4 { query: graphql_query_for('echo', { 'text' => "$test" }, []), variables: { "test" => "Hello world" } }, # Complexity 1 { query: graphql_query_for('project', { 'fullPath' => project.full_path }, "userPermissions { createIssue }") } # Complexity 3 ] @@ -215,7 +215,7 @@ RSpec.describe 'GitlabSchema configurations', feature_category: :integrations do context "global id's" do it 'uses GlobalID to expose ids' do - post_graphql(graphql_query_for('project', { 'fullPath' => project.full_path }, %w(id)), + post_graphql(graphql_query_for('project', { 'fullPath' => project.full_path }, %w[id]), current_user: project.first_owner) parsed_id = GlobalID.parse(graphql_data['project']['id']) diff --git a/spec/requests/api/graphql/group/container_repositories_spec.rb b/spec/requests/api/graphql/group/container_repositories_spec.rb index 51d12261247..9206ead1534 100644 --- a/spec/requests/api/graphql/group/container_repositories_spec.rb +++ b/spec/requests/api/graphql/group/container_repositories_spec.rb @@ -49,7 +49,7 @@ RSpec.describe 'getting container repositories in a group', feature_category: :s group.add_owner(owner) stub_container_registry_config(enabled: true) container_repositories.each do |repository| - stub_container_registry_tags(repository: repository.path, tags: %w(tag1 tag2 tag3), with_manifest: false) + stub_container_registry_tags(repository: repository.path, tags: %w[tag1 tag2 tag3], with_manifest: false) end end @@ -142,7 +142,7 @@ RSpec.describe 'getting container repositories in a group', feature_category: :s end before do - stub_container_registry_tags(repository: container_repository.path, tags: %w(tag4 tag5 tag6), with_manifest: false) + stub_container_registry_tags(repository: container_repository.path, tags: %w[tag4 tag5 tag6], with_manifest: false) end it 'returns the searched container repository' do diff --git a/spec/requests/api/graphql/group/data_transfer_spec.rb b/spec/requests/api/graphql/group/data_transfer_spec.rb index b7c038afa54..e17074a0247 100644 --- a/spec/requests/api/graphql/group/data_transfer_spec.rb +++ b/spec/requests/api/graphql/group/data_transfer_spec.rb @@ -71,45 +71,21 @@ RSpec.describe 'group data transfers', feature_category: :source_code_management context 'when user has enough permissions' do before do group.add_owner(current_user) + subject end - context 'when data_transfer_monitoring_mock_data is NOT enabled' do - before do - stub_feature_flags(data_transfer_monitoring_mock_data: false) - subject - end - - it 'returns real results' do - expect(response).to have_gitlab_http_status(:ok) + it 'returns real results' do + expect(response).to have_gitlab_http_status(:ok) - expect(egress_data.count).to eq(2) + expect(egress_data.count).to eq(2) - expect(egress_data.first.keys).to match_array( - %w[date totalEgress repositoryEgress artifactsEgress packagesEgress registryEgress] - ) + expect(egress_data.first.keys).to match_array( + %w[date totalEgress repositoryEgress artifactsEgress packagesEgress registryEgress] + ) - expect(egress_data.pluck('repositoryEgress')).to match_array(%w[1 6]) - end - - it_behaves_like 'a working graphql query' + expect(egress_data.pluck('repositoryEgress')).to match_array(%w[1 6]) end - context 'when data_transfer_monitoring_mock_data is enabled' do - before do - stub_feature_flags(data_transfer_monitoring_mock_data: true) - subject - end - - it 'returns mock results' do - expect(response).to have_gitlab_http_status(:ok) - - expect(egress_data.count).to eq(12) - expect(egress_data.first.keys).to match_array( - %w[date totalEgress repositoryEgress artifactsEgress packagesEgress registryEgress] - ) - end - - it_behaves_like 'a working graphql query' - end + it_behaves_like 'a working graphql query' end end diff --git a/spec/requests/api/graphql/group/milestones_spec.rb b/spec/requests/api/graphql/group/milestones_spec.rb index 209588835f2..6063e6d5293 100644 --- a/spec/requests/api/graphql/group/milestones_spec.rb +++ b/spec/requests/api/graphql/group/milestones_spec.rb @@ -136,7 +136,7 @@ RSpec.describe 'Milestones through GroupQuery', feature_category: :team_planning let_it_be(:closed_issue) { create(:issue, :closed, project: project, milestone: milestone) } let(:milestone_query) do - %{ + %( id title description @@ -149,7 +149,7 @@ RSpec.describe 'Milestones through GroupQuery', feature_category: :team_planning projectMilestone groupMilestone subgroupMilestone - } + ) end def post_query @@ -180,12 +180,12 @@ RSpec.describe 'Milestones through GroupQuery', feature_category: :team_planning context 'milestone statistics' do let(:milestone_query) do - %{ + %( stats { totalIssuesCount closedIssuesCount } - } + ) end it 'returns the correct milestone statistics' do diff --git a/spec/requests/api/graphql/merge_requests/codequality_reports_comparer_spec.rb b/spec/requests/api/graphql/merge_requests/codequality_reports_comparer_spec.rb index 2939e9307e9..09a229c2098 100644 --- a/spec/requests/api/graphql/merge_requests/codequality_reports_comparer_spec.rb +++ b/spec/requests/api/graphql/merge_requests/codequality_reports_comparer_spec.rb @@ -54,6 +54,7 @@ RSpec.describe 'Query.project.mergeRequest.codequalityReportsComparer', feature_ let(:codequality_reports_comparer_fields) do <<~QUERY codequalityReportsComparer { + status report { status newErrors { @@ -138,6 +139,7 @@ RSpec.describe 'Query.project.mergeRequest.codequalityReportsComparer', feature_ expect(result).to match( a_hash_including( { + status: 'PARSED', report: { status: 'FAILED', newErrors: [ diff --git a/spec/requests/api/graphql/mutations/admin/sidekiq_queues/delete_jobs_spec.rb b/spec/requests/api/graphql/mutations/admin/sidekiq_queues/delete_jobs_spec.rb index e3a7442ffe6..316b0f3755d 100644 --- a/spec/requests/api/graphql/mutations/admin/sidekiq_queues/delete_jobs_spec.rb +++ b/spec/requests/api/graphql/mutations/admin/sidekiq_queues/delete_jobs_spec.rb @@ -20,7 +20,7 @@ RSpec.describe 'Deleting Sidekiq jobs', :clean_gitlab_redis_queues, feature_cate let(:current_user) { create(:user) } it_behaves_like 'a mutation that returns top-level errors', - errors: ['You must be an admin to use this mutation'] + errors: ['You must be an admin to use this mutation'] end context 'when the user is an admin' do @@ -43,7 +43,7 @@ RSpec.describe 'Deleting Sidekiq jobs', :clean_gitlab_redis_queues, feature_cate raise 'Not enqueued!' if Sidekiq::Queue.new(queue).size.zero? end - it 'returns info about the deleted jobs', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/425824' do + it 'returns info about the deleted jobs' do add_job(admin, [1]) add_job(admin, [2]) add_job(create(:user), [3]) @@ -51,9 +51,7 @@ RSpec.describe 'Deleting Sidekiq jobs', :clean_gitlab_redis_queues, feature_cate post_graphql_mutation(mutation, current_user: admin) expect(mutation_response['errors']).to be_empty - expect(mutation_response['result']).to eq('completed' => true, - 'deletedJobs' => 2, - 'queueSize' => 1) + expect(mutation_response['result']).to eq('completed' => true, 'deletedJobs' => 2, 'queueSize' => 1) end end @@ -61,14 +59,14 @@ RSpec.describe 'Deleting Sidekiq jobs', :clean_gitlab_redis_queues, feature_cate let(:variables) { { queue_name: queue } } it_behaves_like 'a mutation that returns errors in the response', - errors: ['No metadata provided'] + errors: ['No metadata provided'] end context 'when the queue does not exist' do let(:variables) { { user: admin.username, queue_name: 'authorized_projects_2' } } it_behaves_like 'a mutation that returns top-level errors', - errors: ['Queue authorized_projects_2 not found'] + errors: ['Queue authorized_projects_2 not found'] end end end diff --git a/spec/requests/api/graphql/mutations/alert_management/alerts/create_alert_issue_spec.rb b/spec/requests/api/graphql/mutations/alert_management/alerts/create_alert_issue_spec.rb index fbe6d95dfff..f2b516783e5 100644 --- a/spec/requests/api/graphql/mutations/alert_management/alerts/create_alert_issue_spec.rb +++ b/spec/requests/api/graphql/mutations/alert_management/alerts/create_alert_issue_spec.rb @@ -14,21 +14,23 @@ RSpec.describe 'Create an alert issue from an alert', feature_category: :inciden project_path: project.full_path, iid: alert.iid.to_s } - graphql_mutation(:create_alert_issue, variables, - <<~QL - clientMutationId - errors - alert { - iid - issue { - iid - } - } - issue { - iid - title - } - QL + graphql_mutation( + :create_alert_issue, + variables, + <<~QL + clientMutationId + errors + alert { + iid + issue { + iid + } + } + issue { + iid + title + } + QL ) end diff --git a/spec/requests/api/graphql/mutations/award_emojis/toggle_spec.rb b/spec/requests/api/graphql/mutations/award_emojis/toggle_spec.rb index 65a5fb87f9a..e7e23304d81 100644 --- a/spec/requests/api/graphql/mutations/award_emojis/toggle_spec.rb +++ b/spec/requests/api/graphql/mutations/award_emojis/toggle_spec.rb @@ -6,9 +6,11 @@ RSpec.describe 'Toggling an AwardEmoji', feature_category: :shared do include GraphqlHelpers let_it_be(:current_user) { create(:user) } - let_it_be(:project, reload: true) { create(:project) } - let_it_be(:awardable) { create(:note, project: project) } + let_it_be(:group) { create(:group) } + let_it_be(:project, reload: true) { create(:project, group: group) } + let_it_be(:issue_note) { create(:note, project: project) } + let(:awardable) { issue_note } let(:emoji_name) { 'thumbsup' } let(:mutation) do variables = { @@ -36,8 +38,8 @@ RSpec.describe 'Toggling an AwardEmoji', feature_category: :shared do end context 'when the user has permission' do - before do - project.add_developer(current_user) + before_all do + group.add_developer(current_user) end context 'when the given awardable is not an Awardable' do @@ -60,6 +62,33 @@ RSpec.describe 'Toggling an AwardEmoji', feature_category: :shared do end context 'when the given awardable is an Awardable' do + context 'when the awardable is a work item' do + context 'when the work item is associated directly with a group' do + let_it_be(:group_work_item) { create(:work_item, :group_level, namespace: group) } + let(:awardable) { group_work_item } + + context 'when no emoji has been awarded by the current_user yet' do + it 'creates an emoji' do + expect do + post_graphql_mutation(mutation, current_user: current_user) + end.to change { AwardEmoji.count }.by(1) + end + end + + context 'when an emoji has been awarded by the current_user' do + before do + create_award_emoji(current_user) + end + + it 'removes the emoji' do + expect do + post_graphql_mutation(mutation, current_user: current_user) + end.to change { AwardEmoji.count }.by(-1) + end + end + end + end + context 'when no emoji has been awarded by the current_user yet' do # Create an award emoji for another user. This therefore tests that # toggling is correctly scoped to the user's emoji only. diff --git a/spec/requests/api/graphql/mutations/boards/issues/issue_move_list_spec.rb b/spec/requests/api/graphql/mutations/boards/issues/issue_move_list_spec.rb index df64caa1cfb..8e71d77f7bc 100644 --- a/spec/requests/api/graphql/mutations/boards/issues/issue_move_list_spec.rb +++ b/spec/requests/api/graphql/mutations/boards/issues/issue_move_list_spec.rb @@ -131,22 +131,24 @@ RSpec.describe 'Reposition and move issue within board lists', feature_category: end def mutation(additional_params = {}) - graphql_mutation(mutation_name, issue_move_params.merge(additional_params), - <<-QL.strip_heredoc - clientMutationId - issue { - iid, - relativePosition - labels { - edges { - node{ - title - } - } - } - } - errors - QL + graphql_mutation( + mutation_name, + issue_move_params.merge(additional_params), + <<-QL.strip_heredoc + clientMutationId + issue { + iid, + relativePosition + labels { + edges { + node{ + title + } + } + } + } + errors + QL ) end end diff --git a/spec/requests/api/graphql/mutations/ci/catalog/resources/create_spec.rb b/spec/requests/api/graphql/mutations/ci/catalog/resources/create_spec.rb new file mode 100644 index 00000000000..f990cab55f4 --- /dev/null +++ b/spec/requests/api/graphql/mutations/ci/catalog/resources/create_spec.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'CatalogResourcesCreate', feature_category: :pipeline_composition do + include GraphqlHelpers + + let_it_be(:current_user) { create(:user) } + let_it_be(:project) { create(:project, :catalog_resource_with_components) } + + let(:mutation) do + variables = { + project_path: project.full_path + } + graphql_mutation(:catalog_resources_create, variables, + <<-QL.strip_heredoc + errors + QL + ) + end + + context 'when unauthorized' do + it_behaves_like 'a mutation that returns a top-level access error' + end + + context 'when authorized' do + context 'with a valid project' do + before_all do + project.add_owner(current_user) + end + + it 'creates a catalog resource' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(graphql_mutation_response(:catalog_resources_create)['errors']).to be_empty + expect(response).to have_gitlab_http_status(:success) + end + end + end +end diff --git a/spec/requests/api/graphql/mutations/ci/catalog/unpublish_spec.rb b/spec/requests/api/graphql/mutations/ci/catalog/unpublish_spec.rb new file mode 100644 index 00000000000..07465777263 --- /dev/null +++ b/spec/requests/api/graphql/mutations/ci/catalog/unpublish_spec.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'CatalogResourceUnpublish', feature_category: :pipeline_composition do + include GraphqlHelpers + + let_it_be(:current_user) { create(:user) } + let_it_be_with_reload(:resource) { create(:ci_catalog_resource) } + + let(:mutation) do + graphql_mutation( + :catalog_resource_unpublish, + id: resource.to_gid.to_s + ) + end + + subject(:post_query) { post_graphql_mutation(mutation, current_user: current_user) } + + context 'when unauthorized' do + it_behaves_like 'a mutation that returns a top-level access error' + end + + context 'when authorized' do + before_all do + resource.project.add_owner(current_user) + end + + context 'when the catalog resource is in published state' do + it 'updates the state to draft' do + resource.update!(state: :published) + expect(resource.state).to eq('published') + + post_query + + expect(resource.reload.state).to eq('draft') + expect_graphql_errors_to_be_empty + end + end + + context 'when the catalog resource is already in draft state' do + it 'leaves the state as draft' do + expect(resource.state).to eq('draft') + + post_query + + expect(resource.reload.state).to eq('draft') + expect_graphql_errors_to_be_empty + end + end + end +end diff --git a/spec/requests/api/graphql/mutations/ci/pipeline_retry_spec.rb b/spec/requests/api/graphql/mutations/ci/pipeline_retry_spec.rb index e7edc86bea0..70b154946ef 100644 --- a/spec/requests/api/graphql/mutations/ci/pipeline_retry_spec.rb +++ b/spec/requests/api/graphql/mutations/ci/pipeline_retry_spec.rb @@ -13,13 +13,15 @@ RSpec.describe 'PipelineRetry', feature_category: :continuous_integration do variables = { id: pipeline.to_global_id.to_s } - graphql_mutation(:pipeline_retry, variables, - <<-QL - errors - pipeline { - id - } - QL + graphql_mutation( + :pipeline_retry, + variables, + <<-QL + errors + pipeline { + id + } + QL ) end diff --git a/spec/requests/api/graphql/mutations/clusters/agent_tokens/agent_tokens/create_spec.rb b/spec/requests/api/graphql/mutations/clusters/agent_tokens/agent_tokens/create_spec.rb index ef0d44395bf..dd4b015409b 100644 --- a/spec/requests/api/graphql/mutations/clusters/agent_tokens/agent_tokens/create_spec.rb +++ b/spec/requests/api/graphql/mutations/clusters/agent_tokens/agent_tokens/create_spec.rb @@ -23,8 +23,8 @@ RSpec.describe 'Create a new cluster agent token', feature_category: :deployment context 'without user permissions' do it_behaves_like 'a mutation that returns top-level errors', - errors: ["The resource that you are attempting to access does not exist "\ - "or you don't have permission to perform this action"] + errors: ["The resource that you are attempting to access does not exist "\ + "or you don't have permission to perform this action"] it 'does not create a token' do expect { post_graphql_mutation(mutation, current_user: current_user) }.not_to change(Clusters::AgentToken, :count) diff --git a/spec/requests/api/graphql/mutations/clusters/agents/delete_spec.rb b/spec/requests/api/graphql/mutations/clusters/agents/delete_spec.rb index b70a6282a7a..a2a093d63e6 100644 --- a/spec/requests/api/graphql/mutations/clusters/agents/delete_spec.rb +++ b/spec/requests/api/graphql/mutations/clusters/agents/delete_spec.rb @@ -22,8 +22,8 @@ RSpec.describe 'Delete a cluster agent', feature_category: :deployment_managemen context 'without project permissions' do it_behaves_like 'a mutation that returns top-level errors', - errors: ['The resource that you are attempting to access does not exist '\ - 'or you don\'t have permission to perform this action'] + errors: ['The resource that you are attempting to access does not exist '\ + 'or you don\'t have permission to perform this action'] it 'does not delete cluster agent' do expect { cluster_agent.reload }.not_to raise_error diff --git a/spec/requests/api/graphql/mutations/container_registry/protection/rule/create_spec.rb b/spec/requests/api/graphql/mutations/container_registry/protection/rule/create_spec.rb new file mode 100644 index 00000000000..0c708c3dc41 --- /dev/null +++ b/spec/requests/api/graphql/mutations/container_registry/protection/rule/create_spec.rb @@ -0,0 +1,180 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Creating the container registry protection rule', :aggregate_failures, feature_category: :container_registry do + include GraphqlHelpers + + let_it_be(:project) { create(:project) } + let_it_be(:user) { create(:user, maintainer_projects: [project]) } + + let(:container_registry_protection_rule_attributes) do + build_stubbed(:container_registry_protection_rule, project: project) + end + + let(:kwargs) do + { + project_path: project.full_path, + container_path_pattern: container_registry_protection_rule_attributes.container_path_pattern, + push_protected_up_to_access_level: 'MAINTAINER', + delete_protected_up_to_access_level: 'MAINTAINER' + } + end + + let(:mutation) do + graphql_mutation(:create_container_registry_protection_rule, kwargs, + <<~QUERY + containerRegistryProtectionRule { + id + containerPathPattern + } + clientMutationId + errors + QUERY + ) + end + + let(:mutation_response) { graphql_mutation_response(:create_container_registry_protection_rule) } + + subject { post_graphql_mutation(mutation, current_user: user) } + + shared_examples 'a successful response' do + it { subject.tap { expect_graphql_errors_to_be_empty } } + + it do + subject + + expect(mutation_response).to include( + 'errors' => be_blank, + 'containerRegistryProtectionRule' => { + 'id' => be_present, + 'containerPathPattern' => kwargs[:container_path_pattern] + } + ) + end + + it 'creates container registry protection rule in the database' do + expect { subject }.to change { ::ContainerRegistry::Protection::Rule.count }.by(1) + + expect(::ContainerRegistry::Protection::Rule.where(project: project, + container_path_pattern: kwargs[:container_path_pattern])).to exist + end + end + + shared_examples 'an erroneous response' do + it { expect { subject }.not_to change { ::ContainerRegistry::Protection::Rule.count } } + end + + it_behaves_like 'a successful response' + + context 'with invalid input fields `pushProtectedUpToAccessLevel` and `deleteProtectedUpToAccessLevel`' do + let(:kwargs) do + super().merge( + push_protected_up_to_access_level: 'UNKNOWN_ACCESS_LEVEL', + delete_protected_up_to_access_level: 'UNKNOWN_ACCESS_LEVEL' + ) + end + + it_behaves_like 'an erroneous response' + + it { + subject + + expect_graphql_errors_to_include([/pushProtectedUpToAccessLevel/, /deleteProtectedUpToAccessLevel/]) + } + end + + context 'with invalid input field `containerPathPattern`' do + let(:kwargs) do + super().merge(container_path_pattern: '') + end + + it_behaves_like 'an erroneous response' + + it { subject.tap { expect_graphql_errors_to_be_empty } } + + it { + subject.tap do + expect(mutation_response['errors']).to eq ["Container path pattern can't be blank"] + end + } + end + + context 'with existing containers protection rule' do + let_it_be(:existing_container_registry_protection_rule) do + create(:container_registry_protection_rule, project: project, + push_protected_up_to_access_level: Gitlab::Access::DEVELOPER) + end + + context 'when container name pattern is slightly different' do + let(:kwargs) do + # The field `container_path_pattern` is unique; this is why we change the value in a minimum way + super().merge( + container_path_pattern: "#{existing_container_registry_protection_rule.container_path_pattern}-unique" + ) + end + + it_behaves_like 'a successful response' + + it 'adds another container registry protection rule to the database' do + expect { subject }.to change { ::ContainerRegistry::Protection::Rule.count }.from(1).to(2) + end + end + + context 'when field `container_path_pattern` is taken' do + let(:kwargs) do + super().merge(container_path_pattern: existing_container_registry_protection_rule.container_path_pattern, + push_protected_up_to_access_level: 'MAINTAINER') + end + + it_behaves_like 'an erroneous response' + + it { subject.tap { expect_graphql_errors_to_be_empty } } + + it 'returns without error' do + subject + + expect(mutation_response['errors']).to eq ['Container path pattern has already been taken'] + end + + it 'does not create new container protection rules' do + expect(::ContainerRegistry::Protection::Rule.where(project: project, + container_path_pattern: kwargs[:container_path_pattern], + push_protected_up_to_access_level: Gitlab::Access::MAINTAINER)).not_to exist + end + end + end + + context 'when user does not have permission' do + let_it_be(:developer) { create(:user).tap { |u| project.add_developer(u) } } + let_it_be(:reporter) { create(:user).tap { |u| project.add_reporter(u) } } + let_it_be(:guest) { create(:user).tap { |u| project.add_guest(u) } } + let_it_be(:anonymous) { create(:user) } + + where(:user) do + [ref(:developer), ref(:reporter), ref(:guest), ref(:anonymous)] + end + + with_them do + it_behaves_like 'an erroneous response' + + it { subject.tap { expect_graphql_errors_to_include(/you don't have permission to perform this action/) } } + end + end + + context "when feature flag ':container_registry_protected_containers' disabled" do + before do + stub_feature_flags(container_registry_protected_containers: false) + end + + it_behaves_like 'an erroneous response' + + it { subject.tap { expect(::ContainerRegistry::Protection::Rule.where(project: project)).not_to exist } } + + it 'returns error of disabled feature flag' do + subject.tap do + expect_graphql_errors_to_include(/'container_registry_protected_containers' feature flag is disabled/) + end + end + end +end diff --git a/spec/requests/api/graphql/mutations/container_repository/destroy_tags_spec.rb b/spec/requests/api/graphql/mutations/container_repository/destroy_tags_spec.rb index 0cb607e13ec..7ced22890df 100644 --- a/spec/requests/api/graphql/mutations/container_repository/destroy_tags_spec.rb +++ b/spec/requests/api/graphql/mutations/container_repository/destroy_tags_spec.rb @@ -27,7 +27,7 @@ RSpec.describe 'Destroying a container repository tags', feature_category: :cont shared_examples 'destroying the container repository tags' do before do stub_delete_reference_requests(tags) - expect_delete_tag_by_names(tags) + expect_delete_tags(tags) allow_next_instance_of(ContainerRegistry::Client) do |client| allow(client).to receive(:supports_tag_delete?).and_return(true) end diff --git a/spec/requests/api/graphql/mutations/design_management/delete_spec.rb b/spec/requests/api/graphql/mutations/design_management/delete_spec.rb index 7ea32ae6d19..6f421abc489 100644 --- a/spec/requests/api/graphql/mutations/design_management/delete_spec.rb +++ b/spec/requests/api/graphql/mutations/design_management/delete_spec.rb @@ -47,28 +47,28 @@ RSpec.describe "deleting designs", feature_category: :design_management do context 'the designs list is empty' do it_behaves_like 'a failed request' do let(:designs) { [] } - let(:the_error) { a_string_matching %r/no filenames/ } + let(:the_error) { a_string_matching %r{no filenames} } end end 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| double('file', filename: fn) } } - let(:the_error) { a_string_matching %r/filenames were not found/ } + 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 context 'the current user does not have developer access' do it_behaves_like 'a failed request' do let(:current_user) { create(:user) } - let(:the_error) { a_string_matching %r/you don't have permission/ } + let(:the_error) { a_string_matching %r{you don't have permission} } end end context "when the issue does not exist" do it_behaves_like 'a failed request' do let(:variables) { { iid: "1234567890" } } - let(:the_error) { a_string_matching %r/does not exist/ } + let(:the_error) { a_string_matching %r{does not exist} } end end diff --git a/spec/requests/api/graphql/mutations/design_management/upload_spec.rb b/spec/requests/api/graphql/mutations/design_management/upload_spec.rb index 9b42b32c150..82a88a2c593 100644 --- a/spec/requests/api/graphql/mutations/design_management/upload_spec.rb +++ b/spec/requests/api/graphql/mutations/design_management/upload_spec.rb @@ -36,10 +36,11 @@ RSpec.describe "uploading designs", feature_category: :design_management do end it 'returns an error' do - workhorse_post_with_file(api('/', current_user, version: 'graphql'), - params: params, - file_key: '1' - ) + workhorse_post_with_file( + api('/', current_user, version: 'graphql'), + params: params, + file_key: '1' + ) expect(response).to have_attributes( code: eq('400'), diff --git a/spec/requests/api/graphql/mutations/issues/link_alerts_spec.rb b/spec/requests/api/graphql/mutations/issues/link_alerts_spec.rb index 85e21952f47..df6c20d6176 100644 --- a/spec/requests/api/graphql/mutations/issues/link_alerts_spec.rb +++ b/spec/requests/api/graphql/mutations/issues/link_alerts_spec.rb @@ -19,19 +19,21 @@ RSpec.describe 'Link alerts to an incident', feature_category: :incident_managem alert_references: [alert1.to_reference, alert2.details_url] } - graphql_mutation(:issue_link_alerts, variables, - <<-QL.strip_heredoc - clientMutationId - errors - issue { - iid - alertManagementAlerts { - nodes { - iid - } - } - } - QL + graphql_mutation( + :issue_link_alerts, + variables, + <<-QL.strip_heredoc + clientMutationId + errors + issue { + iid + alertManagementAlerts { + nodes { + iid + } + } + } + QL ) end diff --git a/spec/requests/api/graphql/mutations/issues/move_spec.rb b/spec/requests/api/graphql/mutations/issues/move_spec.rb index 7d9579067b6..24188d5341d 100644 --- a/spec/requests/api/graphql/mutations/issues/move_spec.rb +++ b/spec/requests/api/graphql/mutations/issues/move_spec.rb @@ -16,14 +16,16 @@ RSpec.describe 'Moving an issue', feature_category: :team_planning do iid: issue.iid.to_s } - graphql_mutation(:issue_move, variables, - <<-QL.strip_heredoc - clientMutationId - errors - issue { - title - } - QL + graphql_mutation( + :issue_move, + variables, + <<-QL.strip_heredoc + clientMutationId + errors + issue { + title + } + QL ) end diff --git a/spec/requests/api/graphql/mutations/issues/set_confidential_spec.rb b/spec/requests/api/graphql/mutations/issues/set_confidential_spec.rb index c5e6901d8f8..c62995c0b9b 100644 --- a/spec/requests/api/graphql/mutations/issues/set_confidential_spec.rb +++ b/spec/requests/api/graphql/mutations/issues/set_confidential_spec.rb @@ -15,15 +15,17 @@ RSpec.describe 'Setting an issue as confidential', feature_category: :team_plann project_path: project.full_path, iid: issue.iid.to_s } - graphql_mutation(:issue_set_confidential, variables.merge(input), - <<-QL.strip_heredoc - clientMutationId - errors - issue { - iid - confidential - } - QL + graphql_mutation( + :issue_set_confidential, + variables.merge(input), + <<-QL.strip_heredoc + clientMutationId + errors + issue { + iid + confidential + } + QL ) 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 497ae1cc13f..cdab267162e 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 @@ -26,18 +26,20 @@ RSpec.describe 'Setting issues crm contacts', feature_category: :service_desk do contact_ids: contact_ids } - graphql_mutation(:issue_set_crm_contacts, variables, - <<-QL.strip_heredoc - clientMutationId - errors - issue { - customerRelationsContacts { - nodes { - id - } - } - } - QL + graphql_mutation( + :issue_set_crm_contacts, + variables, + <<-QL.strip_heredoc + clientMutationId + errors + issue { + customerRelationsContacts { + nodes { + id + } + } + } + QL ) end diff --git a/spec/requests/api/graphql/mutations/issues/set_due_date_spec.rb b/spec/requests/api/graphql/mutations/issues/set_due_date_spec.rb index 1a5a64e4196..f7c5febe56f 100644 --- a/spec/requests/api/graphql/mutations/issues/set_due_date_spec.rb +++ b/spec/requests/api/graphql/mutations/issues/set_due_date_spec.rb @@ -15,15 +15,17 @@ RSpec.describe 'Setting Due Date of an issue', feature_category: :team_planning project_path: project.full_path, iid: issue.iid.to_s } - graphql_mutation(:issue_set_due_date, variables.merge(input), - <<-QL.strip_heredoc - clientMutationId - errors - issue { - iid - dueDate - } - QL + graphql_mutation( + :issue_set_due_date, + variables.merge(input), + <<-QL.strip_heredoc + clientMutationId + errors + issue { + iid + dueDate + } + QL ) end @@ -68,7 +70,7 @@ RSpec.describe 'Setting Due Date of an issue', feature_category: :team_planning it 'returns an error' do post_graphql_mutation(mutation, current_user: current_user) - expect(graphql_errors).to include(a_hash_including('message' => /Arguments must be provided: dueDate/)) + expect(graphql_errors).to include(a_hash_including('message' => 'issueSetDueDate has the wrong arguments')) end end diff --git a/spec/requests/api/graphql/mutations/issues/set_locked_spec.rb b/spec/requests/api/graphql/mutations/issues/set_locked_spec.rb index a8025894b1e..547ec280150 100644 --- a/spec/requests/api/graphql/mutations/issues/set_locked_spec.rb +++ b/spec/requests/api/graphql/mutations/issues/set_locked_spec.rb @@ -16,15 +16,17 @@ RSpec.describe 'Setting an issue as locked', feature_category: :team_planning do project_path: project.full_path, iid: issue.iid.to_s } - graphql_mutation(:issue_set_locked, variables.merge(input), - <<-QL.strip_heredoc - clientMutationId - errors - issue { - iid - discussionLocked - } - QL + graphql_mutation( + :issue_set_locked, + variables.merge(input), + <<-QL.strip_heredoc + clientMutationId + errors + issue { + iid + discussionLocked + } + QL ) end diff --git a/spec/requests/api/graphql/mutations/issues/set_severity_spec.rb b/spec/requests/api/graphql/mutations/issues/set_severity_spec.rb index 77262c7f64f..d53b938a983 100644 --- a/spec/requests/api/graphql/mutations/issues/set_severity_spec.rb +++ b/spec/requests/api/graphql/mutations/issues/set_severity_spec.rb @@ -17,15 +17,17 @@ RSpec.describe 'Setting severity level of an incident', feature_category: :incid iid: incident.iid.to_s } - graphql_mutation(:issue_set_severity, variables.merge(input), - <<-QL.strip_heredoc - clientMutationId - errors - issue { - iid - severity - } - QL + graphql_mutation( + :issue_set_severity, + variables.merge(input), + <<-QL.strip_heredoc + clientMutationId + errors + issue { + iid + severity + } + QL ) end diff --git a/spec/requests/api/graphql/mutations/issues/unlink_alerts_spec.rb b/spec/requests/api/graphql/mutations/issues/unlink_alerts_spec.rb index 7f6f968b1dd..807afdfb812 100644 --- a/spec/requests/api/graphql/mutations/issues/unlink_alerts_spec.rb +++ b/spec/requests/api/graphql/mutations/issues/unlink_alerts_spec.rb @@ -21,19 +21,21 @@ RSpec.describe 'Unlink alert from an incident', feature_category: :incident_mana alert_id: alert_to_unlink.to_global_id.to_s } - graphql_mutation(:issue_unlink_alert, variables, - <<-QL.strip_heredoc - clientMutationId - errors - issue { - iid - alertManagementAlerts { - nodes { - id - } - } - } - QL + graphql_mutation( + :issue_unlink_alert, + variables, + <<-QL.strip_heredoc + clientMutationId + errors + issue { + iid + alertManagementAlerts { + nodes { + id + } + } + } + QL ) end diff --git a/spec/requests/api/graphql/mutations/merge_requests/reviewer_rereview_spec.rb b/spec/requests/api/graphql/mutations/merge_requests/reviewer_rereview_spec.rb index 7a1b3982111..ec82941b094 100644 --- a/spec/requests/api/graphql/mutations/merge_requests/reviewer_rereview_spec.rb +++ b/spec/requests/api/graphql/mutations/merge_requests/reviewer_rereview_spec.rb @@ -16,11 +16,13 @@ RSpec.describe 'Setting assignees of a merge request', feature_category: :code_r project_path: project.full_path, iid: merge_request.iid.to_s } - graphql_mutation(:merge_request_reviewer_rereview, variables.merge(input), - <<-QL.strip_heredoc - clientMutationId - errors - QL + graphql_mutation( + :merge_request_reviewer_rereview, + variables.merge(input), + <<-QL.strip_heredoc + clientMutationId + errors + QL ) end diff --git a/spec/requests/api/graphql/mutations/merge_requests/set_assignees_spec.rb b/spec/requests/api/graphql/mutations/merge_requests/set_assignees_spec.rb index 4a7d1083f2e..cb7bac771b3 100644 --- a/spec/requests/api/graphql/mutations/merge_requests/set_assignees_spec.rb +++ b/spec/requests/api/graphql/mutations/merge_requests/set_assignees_spec.rb @@ -21,19 +21,21 @@ RSpec.describe 'Setting assignees of a merge request', :assume_throttled, featur project_path: project.full_path, iid: merge_request.iid.to_s } - graphql_mutation(:merge_request_set_assignees, variables.merge(input), - <<-QL.strip_heredoc - clientMutationId - errors - mergeRequest { - id - assignees { - nodes { - username - } - } - } - QL + graphql_mutation( + :merge_request_set_assignees, + variables.merge(input), + <<-QL.strip_heredoc + clientMutationId + errors + mergeRequest { + id + assignees { + nodes { + username + } + } + } + QL ) end diff --git a/spec/requests/api/graphql/mutations/merge_requests/set_draft_spec.rb b/spec/requests/api/graphql/mutations/merge_requests/set_draft_spec.rb index 0c2e2975350..a2c5c235d25 100644 --- a/spec/requests/api/graphql/mutations/merge_requests/set_draft_spec.rb +++ b/spec/requests/api/graphql/mutations/merge_requests/set_draft_spec.rb @@ -15,15 +15,17 @@ RSpec.describe 'Setting Draft status of a merge request', feature_category: :cod project_path: project.full_path, iid: merge_request.iid.to_s } - graphql_mutation(:merge_request_set_draft, variables.merge(input), - <<-QL.strip_heredoc - clientMutationId - errors - mergeRequest { - id - title - } - QL + graphql_mutation( + :merge_request_set_draft, + variables.merge(input), + <<-QL.strip_heredoc + clientMutationId + errors + mergeRequest { + id + title + } + QL ) end diff --git a/spec/requests/api/graphql/mutations/merge_requests/set_labels_spec.rb b/spec/requests/api/graphql/mutations/merge_requests/set_labels_spec.rb index e40a3cf7ce9..4ddd10b1734 100644 --- a/spec/requests/api/graphql/mutations/merge_requests/set_labels_spec.rb +++ b/spec/requests/api/graphql/mutations/merge_requests/set_labels_spec.rb @@ -17,19 +17,21 @@ RSpec.describe 'Setting labels of a merge request' do project_path: project.full_path, iid: merge_request.iid.to_s } - graphql_mutation(:merge_request_set_labels, variables.merge(input), - <<-QL.strip_heredoc - clientMutationId - errors - mergeRequest { - id - labels { - nodes { - id - } - } - } - QL + graphql_mutation( + :merge_request_set_labels, + variables.merge(input), + <<-QL.strip_heredoc + clientMutationId + errors + mergeRequest { + id + labels { + nodes { + id + } + } + } + QL ) end diff --git a/spec/requests/api/graphql/mutations/merge_requests/set_locked_spec.rb b/spec/requests/api/graphql/mutations/merge_requests/set_locked_spec.rb index 73a38adf723..a6ddb9beb5c 100644 --- a/spec/requests/api/graphql/mutations/merge_requests/set_locked_spec.rb +++ b/spec/requests/api/graphql/mutations/merge_requests/set_locked_spec.rb @@ -15,15 +15,17 @@ RSpec.describe 'Setting locked status of a merge request', feature_category: :co project_path: project.full_path, iid: merge_request.iid.to_s } - graphql_mutation(:merge_request_set_locked, variables.merge(input), - <<-QL.strip_heredoc - clientMutationId - errors - mergeRequest { - id - discussionLocked - } - QL + graphql_mutation( + :merge_request_set_locked, + variables.merge(input), + <<-QL.strip_heredoc + clientMutationId + errors + mergeRequest { + id + discussionLocked + } + QL ) end diff --git a/spec/requests/api/graphql/mutations/merge_requests/set_milestone_spec.rb b/spec/requests/api/graphql/mutations/merge_requests/set_milestone_spec.rb index 1898ee5a62d..9debfbd474b 100644 --- a/spec/requests/api/graphql/mutations/merge_requests/set_milestone_spec.rb +++ b/spec/requests/api/graphql/mutations/merge_requests/set_milestone_spec.rb @@ -16,17 +16,19 @@ RSpec.describe 'Setting milestone of a merge request', feature_category: :code_r project_path: project.full_path, iid: merge_request.iid.to_s } - graphql_mutation(:merge_request_set_milestone, variables.merge(input), - <<-QL.strip_heredoc - clientMutationId - errors - mergeRequest { - id - milestone { - id - } - } - QL + graphql_mutation( + :merge_request_set_milestone, + variables.merge(input), + <<-QL.strip_heredoc + clientMutationId + errors + mergeRequest { + id + milestone { + id + } + } + QL ) end diff --git a/spec/requests/api/graphql/mutations/merge_requests/set_reviewers_spec.rb b/spec/requests/api/graphql/mutations/merge_requests/set_reviewers_spec.rb index fd87112be33..c9efba689c2 100644 --- a/spec/requests/api/graphql/mutations/merge_requests/set_reviewers_spec.rb +++ b/spec/requests/api/graphql/mutations/merge_requests/set_reviewers_spec.rb @@ -21,19 +21,21 @@ RSpec.describe 'Setting reviewers of a merge request', :assume_throttled, featur project_path: project.full_path, iid: merge_request.iid.to_s } - graphql_mutation(:merge_request_set_reviewers, variables.merge(input), - <<-QL.strip_heredoc - clientMutationId - errors - mergeRequest { - id - reviewers { - nodes { - username - } - } - } - QL + graphql_mutation( + :merge_request_set_reviewers, + variables.merge(input), + <<-QL.strip_heredoc + clientMutationId + errors + mergeRequest { + id + reviewers { + nodes { + username + } + } + } + QL ) end diff --git a/spec/requests/api/graphql/mutations/merge_requests/set_time_estimate_spec.rb b/spec/requests/api/graphql/mutations/merge_requests/set_time_estimate_spec.rb index 6bc130a97cf..541cdf0660d 100644 --- a/spec/requests/api/graphql/mutations/merge_requests/set_time_estimate_spec.rb +++ b/spec/requests/api/graphql/mutations/merge_requests/set_time_estimate_spec.rb @@ -17,8 +17,11 @@ RSpec.describe 'Setting time estimate of a merge request', feature_category: :co let(:extra_params) { { project_path: project.full_path } } let(:input_params) { input.merge(extra_params) } - let(:mutation) { graphql_mutation(:merge_request_update, input_params, nil, ['productAnalyticsState']) } let(:mutation_response) { graphql_mutation_response(:merge_request_update) } + let(:mutation) do + # exclude codequalityReportsComparer because it's behind a feature flag + graphql_mutation(:merge_request_update, input_params, nil, %w[productAnalyticsState codequalityReportsComparer]) + end context 'when the user is not allowed to update a merge request' do before_all do diff --git a/spec/requests/api/graphql/mutations/merge_requests/update_spec.rb b/spec/requests/api/graphql/mutations/merge_requests/update_spec.rb index 48db23569b6..ef21f77d818 100644 --- a/spec/requests/api/graphql/mutations/merge_requests/update_spec.rb +++ b/spec/requests/api/graphql/mutations/merge_requests/update_spec.rb @@ -12,8 +12,11 @@ RSpec.describe 'Update of an existing merge request', feature_category: :code_re let(:input) { { 'iid' => merge_request.iid.to_s } } let(:extra_params) { { project_path: project.full_path } } let(:input_params) { input.merge(extra_params) } - let(:mutation) { graphql_mutation(:merge_request_update, input_params, nil, ['productAnalyticsState']) } let(:mutation_response) { graphql_mutation_response(:merge_request_update) } + let(:mutation) do + # exclude codequalityReportsComparer because it's behind a feature flag + graphql_mutation(:merge_request_update, input_params, nil, %w[productAnalyticsState codequalityReportsComparer]) + end context 'when the user is not allowed to update the merge request' do it_behaves_like 'a mutation that returns a top-level access error' @@ -28,5 +31,17 @@ RSpec.describe 'Update of an existing merge request', feature_category: :code_re let(:resource) { merge_request } let(:mutation_name) { 'mergeRequestUpdate' } end + + context 'when required arguments are missing' do + let(:input_params) { {} } + + it_behaves_like 'a mutation that returns top-level errors' do + let(:match_errors) do + include(end_with( + 'invalid value for projectPath (Expected value to not be null), iid (Expected value to not be null)' + )) + end + end + end end end diff --git a/spec/requests/api/graphql/mutations/namespace/package_settings/update_spec.rb b/spec/requests/api/graphql/mutations/namespace/package_settings/update_spec.rb index 480e184a60c..738dc3078e7 100644 --- a/spec/requests/api/graphql/mutations/namespace/package_settings/update_spec.rb +++ b/spec/requests/api/graphql/mutations/namespace/package_settings/update_spec.rb @@ -129,26 +129,6 @@ RSpec.describe 'Updating the package settings', feature_category: :package_regis it_behaves_like 'returning a success' it_behaves_like 'rejecting invalid regex' - - context 'when nuget_duplicates_option FF is disabled' do - let(:params) do - { - namespace_path: namespace.full_path, - 'nugetDuplicatesAllowed' => false - } - end - - before do - stub_feature_flags(nuget_duplicates_option: false) - end - - it 'raises an error', :aggregate_failures do - subject - - expect(graphql_errors.size).to eq(1) - expect(graphql_errors.first['message']).to include('feature flag is disabled') - end - end end RSpec.shared_examples 'accepting the mutation request creating the package settings' do diff --git a/spec/requests/api/graphql/mutations/notes/create/note_spec.rb b/spec/requests/api/graphql/mutations/notes/create/note_spec.rb index 37bcdf61d23..33d840cafd7 100644 --- a/spec/requests/api/graphql/mutations/notes/create/note_spec.rb +++ b/spec/requests/api/graphql/mutations/notes/create/note_spec.rb @@ -5,13 +5,15 @@ require 'spec_helper' RSpec.describe 'Adding a Note', feature_category: :team_planning do include GraphqlHelpers - let_it_be(:current_user) { create(:user) } - + let_it_be(:user) { create(:user) } + let_it_be(:group) { create(:group).tap { |g| g.add_developer(developer) } } + let_it_be_with_reload(:project) { create(:project, :repository, group: group) } + let_it_be(:developer) { create(:user).tap { |u| group.add_developer(u) } } let(:noteable) { create(:merge_request, source_project: project, target_project: project) } - let(:project) { create(:project, :repository) } let(:discussion) { nil } let(:head_sha) { nil } let(:body) { 'Body text' } + let(:current_user) { user } let(:mutation) do variables = { noteable_id: GitlabSchema.id_from_object(noteable).to_s, @@ -30,9 +32,7 @@ RSpec.describe 'Adding a Note', feature_category: :team_planning do it_behaves_like 'a Note mutation when the user does not have permission' context 'when the user has permission' do - before do - project.add_developer(current_user) - end + let(:current_user) { developer } it_behaves_like 'a working GraphQL mutation' @@ -78,8 +78,10 @@ RSpec.describe 'Adding a Note', feature_category: :team_planning do end context 'for an issue' do - let(:noteable) { create(:issue, project: project) } + let_it_be_with_reload(:issue) { create(:issue, project: project) } + let(:noteable) { issue } let(:mutation) { graphql_mutation(:create_note, variables) } + let(:variables_extra) { {} } let(:variables) do { noteable_id: GitlabSchema.id_from_object(noteable).to_s, @@ -87,10 +89,6 @@ RSpec.describe 'Adding a Note', feature_category: :team_planning do }.merge(variables_extra) end - before do - project.add_developer(current_user) - end - context 'when using internal param' do let(:variables_extra) { { internal: true } } @@ -104,8 +102,8 @@ RSpec.describe 'Adding a Note', feature_category: :team_planning do end context 'as work item' do - let_it_be(:project) { create(:project) } - let_it_be(:noteable) { create(:work_item, project: project) } + let_it_be_with_reload(:work_item) { create(:work_item, :task, project: project) } + let(:noteable) { work_item } context 'when using internal param' do let(:variables_extra) { { internal: true } } @@ -120,10 +118,8 @@ RSpec.describe 'Adding a Note', feature_category: :team_planning do end context 'without notes widget' do - let(:variables_extra) { {} } - before do - WorkItems::Type.default_by_type(:issue).widget_definitions.find_by_widget_type(:notes) + WorkItems::Type.default_by_type(:task).widget_definitions.find_by_widget_type(:notes) .update!(disabled: true) end @@ -133,10 +129,6 @@ RSpec.describe 'Adding a Note', feature_category: :team_planning do end context 'when body contains quick actions' do - let_it_be(:noteable) { create(:work_item, :task, project: project) } - - let(:variables_extra) { {} } - it_behaves_like 'work item supports labels widget updates via quick actions' it_behaves_like 'work item does not support labels widget updates via quick actions' it_behaves_like 'work item supports assignee widget updates via quick actions' @@ -145,6 +137,13 @@ RSpec.describe 'Adding a Note', feature_category: :team_planning do it_behaves_like 'work item does not support start and due date widget updates via quick actions' it_behaves_like 'work item supports type change via quick actions' end + + context 'when work item is directly associated with a group' do + let_it_be_with_reload(:group_work_item) { create(:work_item, :group_level, :task, namespace: group) } + let(:noteable) { group_work_item } + + it_behaves_like 'a Note mutation that creates a Note' + end end end @@ -152,10 +151,6 @@ RSpec.describe 'Adding a Note', feature_category: :team_planning do let(:head_sha) { noteable.diff_head_sha } let(:body) { '/merge' } - before do - project.add_developer(current_user) - end - # NOTE: Known issue https://gitlab.com/gitlab-org/gitlab/-/issues/346557 it 'returns a nil note and info about the command in errors' do post_graphql_mutation(mutation, current_user: current_user) diff --git a/spec/requests/api/graphql/mutations/notes/update/image_diff_note_spec.rb b/spec/requests/api/graphql/mutations/notes/update/image_diff_note_spec.rb index a5cd3c8b019..3f071a6d987 100644 --- a/spec/requests/api/graphql/mutations/notes/update/image_diff_note_spec.rb +++ b/spec/requests/api/graphql/mutations/notes/update/image_diff_note_spec.rb @@ -37,11 +37,13 @@ RSpec.describe 'Updating an image DiffNote', feature_category: :team_planning do end let!(:diff_note) do - create(:image_diff_note_on_merge_request, - noteable: noteable, - project: noteable.project, - note: original_body, - position: original_position) + create( + :image_diff_note_on_merge_request, + noteable: noteable, + project: noteable.project, + note: original_body, + position: original_position + ) end let(:mutation) do diff --git a/spec/requests/api/graphql/mutations/organizations/create_spec.rb b/spec/requests/api/graphql/mutations/organizations/create_spec.rb new file mode 100644 index 00000000000..ac6b04104ba --- /dev/null +++ b/spec/requests/api/graphql/mutations/organizations/create_spec.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Mutations::Organizations::Create, feature_category: :cell do + include GraphqlHelpers + + let_it_be(:user) { create(:user) } + + let(:mutation) { graphql_mutation(:organization_create, params) } + let(:name) { 'Name' } + let(:path) { 'path' } + let(:params) do + { + name: name, + path: path + } + end + + subject(:create_organization) { post_graphql_mutation(mutation, current_user: current_user) } + + it { expect(described_class).to require_graphql_authorizations(:create_organization) } + + def mutation_response + graphql_mutation_response(:organization_create) + end + + context 'when the user does not have permission' do + let(:current_user) { nil } + + it_behaves_like 'a mutation that returns a top-level access error' + + it 'does not create an organization' do + expect { create_organization }.not_to change { Organizations::Organization.count } + end + end + + context 'when the user has permission' do + let(:current_user) { user } + + context 'when the params are invalid' do + let(:name) { '' } + + it 'returns the validation error' do + create_organization + + expect(mutation_response).to include('errors' => ["Name can't be blank"]) + end + end + + it 'creates an organization' do + expect { create_organization }.to change { Organizations::Organization.count }.by(1) + end + + it 'returns the new organization' do + create_organization + + expect(graphql_data_at(:organization_create, :organization)).to match a_hash_including( + 'name' => name, + 'path' => path + ) + end + end +end diff --git a/spec/requests/api/graphql/mutations/packages/cleanup/policy/update_spec.rb b/spec/requests/api/graphql/mutations/packages/cleanup/policy/update_spec.rb index 2540e06be9a..5843109f356 100644 --- a/spec/requests/api/graphql/mutations/packages/cleanup/policy/update_spec.rb +++ b/spec/requests/api/graphql/mutations/packages/cleanup/policy/update_spec.rb @@ -17,14 +17,16 @@ RSpec.describe 'Updating the packages cleanup policy', feature_category: :packag end let(:mutation) do - graphql_mutation(:update_packages_cleanup_policy, params, - <<~QUERY - packagesCleanupPolicy { - keepNDuplicatedPackageFiles - nextRunAt - } - errors - QUERY + graphql_mutation( + :update_packages_cleanup_policy, + params, + <<~QUERY + packagesCleanupPolicy { + keepNDuplicatedPackageFiles + nextRunAt + } + errors + QUERY ) end diff --git a/spec/requests/api/graphql/mutations/packages/protection/rule/create_spec.rb b/spec/requests/api/graphql/mutations/packages/protection/rule/create_spec.rb index b0c8526fa1c..ae5b6a5af95 100644 --- a/spec/requests/api/graphql/mutations/packages/protection/rule/create_spec.rb +++ b/spec/requests/api/graphql/mutations/packages/protection/rule/create_spec.rb @@ -4,7 +4,6 @@ require 'spec_helper' RSpec.describe 'Creating the packages protection rule', :aggregate_failures, feature_category: :package_registry do include GraphqlHelpers - using RSpec::Parameterized::TableSyntax let_it_be(:project) { create(:project) } let_it_be(:user) { create(:user, maintainer_projects: [project]) } @@ -15,151 +14,162 @@ RSpec.describe 'Creating the packages protection rule', :aggregate_failures, fea { project_path: project.full_path, package_name_pattern: package_protection_rule_attributes.package_name_pattern, - package_type: "NPM", - push_protected_up_to_access_level: "MAINTAINER" + package_type: 'NPM', + push_protected_up_to_access_level: 'MAINTAINER' } end let(:mutation) do graphql_mutation(:create_packages_protection_rule, kwargs, <<~QUERY - clientMutationId + packageProtectionRule { + id + packageNamePattern + packageType + pushProtectedUpToAccessLevel + } errors QUERY ) end - let(:mutation_response) { graphql_mutation_response(:create_packages_protection_rule) } + let(:mutation_response_package_protection_rule) do + graphql_data_at(:createPackagesProtectionRule, :packageProtectionRule) + end - describe 'post graphql mutation' do - subject { post_graphql_mutation(mutation, current_user: user) } + let(:mutation_response_errors) { graphql_data_at(:createPackagesProtectionRule, :errors) } - context 'without existing packages protection rule' do - it 'returns without error' do - subject + subject { post_graphql_mutation(mutation, current_user: user) } - expect_graphql_errors_to_be_empty - end + shared_examples 'a successful response' do + it 'returns without error' do + subject - it 'returns the created packages protection rule' do - expect { subject }.to change { ::Packages::Protection::Rule.count }.by(1) + expect_graphql_errors_to_be_empty + expect(mutation_response_errors).to be_empty + end - expect_graphql_errors_to_be_empty - expect(Packages::Protection::Rule.where(project: project).count).to eq 1 + it 'returns the created packages protection rule' do + subject - expect(Packages::Protection::Rule.where(project: project, - package_name_pattern: kwargs[:package_name_pattern])).to exist - end + expect(mutation_response_package_protection_rule).to include( + 'id' => be_present, + 'packageNamePattern' => kwargs[:package_name_pattern], + 'packageType' => kwargs[:package_type], + 'pushProtectedUpToAccessLevel' => kwargs[:push_protected_up_to_access_level] + ) + end - context 'when invalid fields are given' do - let(:kwargs) do - { - project_path: project.full_path, - package_name_pattern: '', - package_type: 'UNKNOWN_PACKAGE_TYPE', - push_protected_up_to_access_level: 'UNKNOWN_ACCESS_LEVEL' - } - end - - it 'returns error about required argument' do - subject - - expect_graphql_errors_to_include(/was provided invalid value for packageType/) - expect_graphql_errors_to_include(/pushProtectedUpToAccessLevel/) - end - end + it 'creates one package protection rule' do + expect { subject }.to change { ::Packages::Protection::Rule.count }.by(1) + + expect(Packages::Protection::Rule.last).to have_attributes( + project: project, + package_name_pattern: kwargs[:package_name_pattern], + package_type: kwargs[:package_type].downcase, + push_protected_up_to_access_level: kwargs[:push_protected_up_to_access_level].downcase + ) end + end - context 'when user does not have permission' do - let_it_be(:developer) { create(:user).tap { |u| project.add_developer(u) } } - let_it_be(:reporter) { create(:user).tap { |u| project.add_reporter(u) } } - let_it_be(:guest) { create(:user).tap { |u| project.add_guest(u) } } - let_it_be(:anonymous) { create(:user) } + shared_examples 'an erroneous response' do + it 'does not create one package protection rule' do + expect { subject }.not_to change { ::Packages::Protection::Rule.count } + end + end - where(:user) do - [ref(:developer), ref(:reporter), ref(:guest), ref(:anonymous)] - end + it_behaves_like 'a successful response' - with_them do - it 'returns an error' do - expect { subject }.not_to change { ::Packages::Protection::Rule.count } + context 'with invalid kwargs leading to error from graphql' do + let(:kwargs) do + super().merge!( + package_name_pattern: '', + package_type: 'UNKNOWN_PACKAGE_TYPE', + push_protected_up_to_access_level: 'UNKNOWN_ACCESS_LEVEL' + ) + end - expect_graphql_errors_to_include(/you don't have permission to perform this action/) - end - end + it_behaves_like 'an erroneous response' + + it 'returns error about required argument' do + subject + + expect_graphql_errors_to_include(/was provided invalid value for packageType/) + expect_graphql_errors_to_include(/pushProtectedUpToAccessLevel/) end + end - context 'with existing packages protection rule' do - let_it_be(:existing_package_protection_rule) do - create(:package_protection_rule, project: project, push_protected_up_to_access_level: Gitlab::Access::DEVELOPER) - end + context 'with invalid kwargs leading to error from business model' do + let(:kwargs) { super().merge!(package_name_pattern: '') } - context 'when package name pattern is slightly different' do - let(:kwargs) do - { - project_path: project.full_path, - # The field `package_name_pattern` is unique; this is why we change the value in a minimum way - package_name_pattern: "#{existing_package_protection_rule.package_name_pattern}-unique", - package_type: "NPM", - push_protected_up_to_access_level: "DEVELOPER" - } - end - - it 'returns the created packages protection rule' do - expect { subject }.to change { ::Packages::Protection::Rule.count }.by(1) - - expect(Packages::Protection::Rule.where(project: project).count).to eq 2 - expect(Packages::Protection::Rule.where(project: project, - package_name_pattern: kwargs[:package_name_pattern])).to exist - end - - it 'returns without error' do - subject - - expect_graphql_errors_to_be_empty - end - end + it_behaves_like 'an erroneous response' - context 'when field `package_name_pattern` is taken' do - let(:kwargs) do - { - project_path: project.full_path, - package_name_pattern: existing_package_protection_rule.package_name_pattern, - package_type: 'NPM', - push_protected_up_to_access_level: 'MAINTAINER' - } - end - - it 'returns without error' do - subject - - expect(mutation_response).to include 'errors' => ['Package name pattern has already been taken'] - end - - it 'does not create new package protection rules' do - expect { subject }.to change { Packages::Protection::Rule.count }.by(0) - - expect(Packages::Protection::Rule.where(project: project, - package_name_pattern: kwargs[:package_name_pattern], - push_protected_up_to_access_level: Gitlab::Access::MAINTAINER)).not_to exist - end - end + it 'returns an error' do + subject.tap { expect(mutation_response_errors).to include(/Package name pattern can't be blank/) } end + end - context "when feature flag ':packages_protected_packages' disabled" do - before do - stub_feature_flags(packages_protected_packages: false) + context 'with existing packages protection rule' do + let_it_be(:existing_package_protection_rule) do + create(:package_protection_rule, project: project, push_protected_up_to_access_level: :maintainer) + end + + let(:kwargs) { super().merge!(package_name_pattern: existing_package_protection_rule.package_name_pattern) } + + it_behaves_like 'an erroneous response' + + it 'returns an error' do + subject.tap { expect(mutation_response_errors).to include(/Package name pattern has already been taken/) } + end + + context 'when field `package_name_pattern` is different than existing one' do + let(:kwargs) do + # The field `package_name_pattern` is unique; this is why we change the value in a minimum way + super().merge!(package_name_pattern: "#{existing_package_protection_rule.package_name_pattern}-unique") end - it 'does not create any package protection rules' do - expect { subject }.to change { Packages::Protection::Rule.count }.by(0) + it_behaves_like 'a successful response' + end + + context 'when field `push_protected_up_to_access_level` is different than existing one' do + let(:kwargs) { super().merge!(push_protected_up_to_access_level: 'DEVELOPER') } + + it_behaves_like 'an erroneous response' - expect(Packages::Protection::Rule.where(project: project)).not_to exist + it 'returns an error' do + subject.tap { expect(mutation_response_errors).to include(/Package name pattern has already been taken/) } end + end + end - it 'returns error of disabled feature flag' do - subject.tap { expect_graphql_errors_to_include(/'packages_protected_packages' feature flag is disabled/) } + context 'when user does not have permission' do + let_it_be(:anonymous) { create(:user) } + let_it_be(:developer) { create(:user).tap { |u| project.add_developer(u) } } + let_it_be(:guest) { create(:user).tap { |u| project.add_guest(u) } } + let_it_be(:reporter) { create(:user).tap { |u| project.add_reporter(u) } } + + where(:user) do + [ref(:developer), ref(:reporter), ref(:guest), ref(:anonymous)] + end + + with_them do + it_behaves_like 'an erroneous response' + + it 'returns an error' do + subject.tap { expect_graphql_errors_to_include(/you don't have permission to perform this action/) } end end end + + context "when feature flag ':packages_protected_packages' disabled" do + before do + stub_feature_flags(packages_protected_packages: false) + end + + it_behaves_like 'an erroneous response' + + it 'returns error of disabled feature flag' do + subject.tap { expect_graphql_errors_to_include(/'packages_protected_packages' feature flag is disabled/) } + end + end end diff --git a/spec/requests/api/graphql/mutations/packages/protection/rule/delete_spec.rb b/spec/requests/api/graphql/mutations/packages/protection/rule/delete_spec.rb new file mode 100644 index 00000000000..1d94d520674 --- /dev/null +++ b/spec/requests/api/graphql/mutations/packages/protection/rule/delete_spec.rb @@ -0,0 +1,88 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Deleting a package protection rule', :aggregate_failures, feature_category: :package_registry do + include GraphqlHelpers + + let_it_be(:project) { create(:project, :repository) } + let_it_be_with_refind(:package_protection_rule) { create(:package_protection_rule, project: project) } + let_it_be(:current_user) { create(:user, maintainer_projects: [project]) } + + let(:mutation) { graphql_mutation(:delete_packages_protection_rule, input) } + let(:mutation_response) { graphql_mutation_response(:delete_packages_protection_rule) } + let(:input) { { id: package_protection_rule.to_global_id } } + + subject { post_graphql_mutation(mutation, current_user: current_user) } + + shared_examples 'an erroneous reponse' do + it { subject.tap { expect(mutation_response).to be_blank } } + it { expect { subject }.not_to change { ::Packages::Protection::Rule.count } } + end + + it_behaves_like 'a working GraphQL mutation' + + it 'responds with deleted package protection rule' do + subject + + expect(mutation_response).to include( + 'errors' => be_blank, + 'packageProtectionRule' => { + 'id' => package_protection_rule.to_global_id.to_s, + 'packageNamePattern' => package_protection_rule.package_name_pattern, + 'packageType' => package_protection_rule.package_type.upcase, + 'pushProtectedUpToAccessLevel' => package_protection_rule.push_protected_up_to_access_level.upcase + } + ) + end + + it { is_expected.tap { expect_graphql_errors_to_be_empty } } + it { expect { subject }.to change { ::Packages::Protection::Rule.count }.from(1).to(0) } + + context 'with existing package protection rule belonging to other project' do + let_it_be(:package_protection_rule) do + create(:package_protection_rule, package_name_pattern: 'protection_rule_other_project') + end + + it_behaves_like 'an erroneous reponse' + + it { subject.tap { expect_graphql_errors_to_include(/you don't have permission to perform this action/) } } + end + + context 'with deleted package protection rule' do + let!(:package_protection_rule) do + create(:package_protection_rule, project: project, package_name_pattern: 'protection_rule_deleted').destroy! + end + + it_behaves_like 'an erroneous reponse' + + it { subject.tap { expect_graphql_errors_to_include(/you don't have permission to perform this action/) } } + end + + context 'when current_user does not have permission' do + let_it_be(:developer) { create(:user).tap { |u| project.add_developer(u) } } + let_it_be(:reporter) { create(:user).tap { |u| project.add_reporter(u) } } + let_it_be(:guest) { create(:user).tap { |u| project.add_guest(u) } } + let_it_be(:anonymous) { create(:user) } + + where(:current_user) do + [ref(:developer), ref(:reporter), ref(:guest), ref(:anonymous)] + end + + with_them do + it_behaves_like 'an erroneous reponse' + + it { subject.tap { expect_graphql_errors_to_include(/you don't have permission to perform this action/) } } + end + end + + context "when feature flag ':packages_protected_packages' disabled" do + before do + stub_feature_flags(packages_protected_packages: false) + end + + it_behaves_like 'an erroneous reponse' + + it { subject.tap { expect_graphql_errors_to_include(/'packages_protected_packages' feature flag is disabled/) } } + end +end diff --git a/spec/requests/api/graphql/mutations/release_asset_links/update_spec.rb b/spec/requests/api/graphql/mutations/release_asset_links/update_spec.rb index 45028cba3ae..fdd4de865ad 100644 --- a/spec/requests/api/graphql/mutations/release_asset_links/update_spec.rb +++ b/spec/requests/api/graphql/mutations/release_asset_links/update_spec.rb @@ -10,12 +10,14 @@ RSpec.describe 'Updating an existing release asset link', feature_category: :rel let_it_be(:developer) { create(:user).tap { |u| project.add_developer(u) } } let_it_be(:release_link) do - create(:release_link, - release: release, - name: 'link name', - url: 'https://example.com/url', - filepath: '/permanent/path', - link_type: 'package') + create( + :release_link, + release: release, + name: 'link name', + url: 'https://example.com/url', + filepath: '/permanent/path', + link_type: 'package' + ) end let(:current_user) { developer } diff --git a/spec/requests/api/graphql/mutations/snippets/create_spec.rb b/spec/requests/api/graphql/mutations/snippets/create_spec.rb index a6d727ae6d3..7094cb807b2 100644 --- a/spec/requests/api/graphql/mutations/snippets/create_spec.rb +++ b/spec/requests/api/graphql/mutations/snippets/create_spec.rb @@ -42,7 +42,7 @@ RSpec.describe 'Creating a Snippet', feature_category: :source_code_management d let(:current_user) { nil } it_behaves_like 'a mutation that returns top-level errors', - errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR] + errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR] it 'does not create the Snippet' do expect do @@ -122,7 +122,7 @@ RSpec.describe 'Creating a Snippet', feature_category: :source_code_management d let(:project_path) { 'foobar' } it_behaves_like 'a mutation that returns top-level errors', - errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR] + errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR] end context 'when the feature is disabled' do @@ -131,7 +131,7 @@ RSpec.describe 'Creating a Snippet', feature_category: :source_code_management d end it_behaves_like 'a mutation that returns top-level errors', - errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR] + errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR] end it_behaves_like 'snippet edit usage data counters' @@ -169,8 +169,8 @@ RSpec.describe 'Creating a Snippet', feature_category: :source_code_management d end it_behaves_like 'expected files argument', nil, nil - it_behaves_like 'expected files argument', %w(foo bar), %w(foo bar) - it_behaves_like 'expected files argument', 'foo', %w(foo) + it_behaves_like 'expected files argument', %w[foo bar], %w[foo bar] + it_behaves_like 'expected files argument', 'foo', %w[foo] context 'when files has an invalid value' do let(:uploaded_files) { [1] } diff --git a/spec/requests/api/graphql/mutations/snippets/destroy_spec.rb b/spec/requests/api/graphql/mutations/snippets/destroy_spec.rb index c3f818b6627..7b0de7a9fba 100644 --- a/spec/requests/api/graphql/mutations/snippets/destroy_spec.rb +++ b/spec/requests/api/graphql/mutations/snippets/destroy_spec.rb @@ -24,7 +24,7 @@ RSpec.describe 'Destroying a Snippet', feature_category: :source_code_management let(:current_user) { create(:user) } it_behaves_like 'a mutation that returns top-level errors', - errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR] + errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR] it 'does not destroy the Snippet' do expect do @@ -53,7 +53,7 @@ RSpec.describe 'Destroying a Snippet', feature_category: :source_code_management let!(:snippet_gid) { project.to_gid.to_s } it 'returns an error' do - err_message = %["#{snippet_gid}" does not represent an instance of Snippet] + err_message = %("#{snippet_gid}" does not represent an instance of Snippet) post_graphql_mutation(mutation, current_user: current_user) diff --git a/spec/requests/api/graphql/mutations/snippets/mark_as_spam_spec.rb b/spec/requests/api/graphql/mutations/snippets/mark_as_spam_spec.rb index 9a8c027da8a..6fd41437ce4 100644 --- a/spec/requests/api/graphql/mutations/snippets/mark_as_spam_spec.rb +++ b/spec/requests/api/graphql/mutations/snippets/mark_as_spam_spec.rb @@ -40,7 +40,7 @@ RSpec.describe 'Mark snippet as spam', feature_category: :source_code_management let(:current_user) { other_user } it_behaves_like 'a mutation that returns top-level errors', - errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR] + errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR] it_behaves_like 'does not mark the snippet as spam' end diff --git a/spec/requests/api/graphql/mutations/snippets/update_spec.rb b/spec/requests/api/graphql/mutations/snippets/update_spec.rb index 78df78cb2a0..0bc475c7105 100644 --- a/spec/requests/api/graphql/mutations/snippets/update_spec.rb +++ b/spec/requests/api/graphql/mutations/snippets/update_spec.rb @@ -46,7 +46,7 @@ RSpec.describe 'Updating a Snippet', feature_category: :source_code_management d let(:current_user) { create(:user) } it_behaves_like 'a mutation that returns top-level errors', - errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR] + errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR] it 'does not update the Snippet' do expect do @@ -118,13 +118,15 @@ RSpec.describe 'Updating a Snippet', feature_category: :source_code_management d describe 'PersonalSnippet' do let(:snippet) do - create(:personal_snippet, - :private, - :repository, - file_name: original_file_name, - title: original_title, - content: original_content, - description: original_description) + create( + :personal_snippet, + :private, + :repository, + file_name: original_file_name, + title: original_title, + content: original_content, + description: original_description + ) end it_behaves_like 'graphql update actions' @@ -139,15 +141,17 @@ RSpec.describe 'Updating a Snippet', feature_category: :source_code_management d let_it_be(:project) { create(:project, :private) } let(:snippet) do - create(:project_snippet, - :private, - :repository, - project: project, - author: create(:user), - file_name: original_file_name, - title: original_title, - content: original_content, - description: original_description) + create( + :project_snippet, + :private, + :repository, + project: project, + author: create(:user), + file_name: original_file_name, + title: original_title, + content: original_content, + description: original_description + ) end context 'when the author is not a member of the project' do diff --git a/spec/requests/api/graphql/mutations/todos/mark_all_done_spec.rb b/spec/requests/api/graphql/mutations/todos/mark_all_done_spec.rb index c611c6ee2a1..429aa06d9f1 100644 --- a/spec/requests/api/graphql/mutations/todos/mark_all_done_spec.rb +++ b/spec/requests/api/graphql/mutations/todos/mark_all_done_spec.rb @@ -21,12 +21,14 @@ RSpec.describe 'Marking all todos done', feature_category: :team_planning do let(:input) { {} } let(:mutation) do - graphql_mutation(:todos_mark_all_done, input, - <<-QL.strip_heredoc - clientMutationId - todos { id } - errors - QL + graphql_mutation( + :todos_mark_all_done, + input, + <<-QL.strip_heredoc + clientMutationId + todos { id } + errors + QL ) end diff --git a/spec/requests/api/graphql/mutations/todos/mark_done_spec.rb b/spec/requests/api/graphql/mutations/todos/mark_done_spec.rb index 60700d8024c..c09f89ef567 100644 --- a/spec/requests/api/graphql/mutations/todos/mark_done_spec.rb +++ b/spec/requests/api/graphql/mutations/todos/mark_done_spec.rb @@ -19,15 +19,17 @@ RSpec.describe 'Marking todos done', feature_category: :team_planning do let(:input) { { id: todo1.to_global_id.to_s } } let(:mutation) do - graphql_mutation(:todo_mark_done, input, - <<-QL.strip_heredoc - clientMutationId - errors - todo { - id - state - } - QL + graphql_mutation( + :todo_mark_done, + input, + <<-QL.strip_heredoc + clientMutationId + errors + todo { + id + state + } + QL ) end diff --git a/spec/requests/api/graphql/mutations/todos/restore_many_spec.rb b/spec/requests/api/graphql/mutations/todos/restore_many_spec.rb index 9daa243cf8e..4bbfc7b2f1d 100644 --- a/spec/requests/api/graphql/mutations/todos/restore_many_spec.rb +++ b/spec/requests/api/graphql/mutations/todos/restore_many_spec.rb @@ -20,15 +20,17 @@ RSpec.describe 'Restoring many Todos', feature_category: :team_planning do let(:input) { { ids: input_ids } } let(:mutation) do - graphql_mutation(:todo_restore_many, input, - <<-QL.strip_heredoc - clientMutationId - errors - todos { - id - state - } - QL + graphql_mutation( + :todo_restore_many, + input, + <<-QL.strip_heredoc + clientMutationId + errors + todos { + id + state + } + QL ) end diff --git a/spec/requests/api/graphql/mutations/todos/restore_spec.rb b/spec/requests/api/graphql/mutations/todos/restore_spec.rb index 868298763ec..1ebd04432be 100644 --- a/spec/requests/api/graphql/mutations/todos/restore_spec.rb +++ b/spec/requests/api/graphql/mutations/todos/restore_spec.rb @@ -19,15 +19,17 @@ RSpec.describe 'Restoring Todos', feature_category: :team_planning do let(:input) { { id: todo1.to_global_id.to_s } } let(:mutation) do - graphql_mutation(:todo_restore, input, - <<-QL.strip_heredoc - clientMutationId - errors - todo { - id - state - } - QL + graphql_mutation( + :todo_restore, + input, + <<-QL.strip_heredoc + clientMutationId + errors + todo { + id + state + } + QL ) end diff --git a/spec/requests/api/graphql/namespace/root_storage_statistics_spec.rb b/spec/requests/api/graphql/namespace/root_storage_statistics_spec.rb index 7c48f324d24..c8819f1e38f 100644 --- a/spec/requests/api/graphql/namespace/root_storage_statistics_spec.rb +++ b/spec/requests/api/graphql/namespace/root_storage_statistics_spec.rb @@ -6,8 +6,15 @@ RSpec.describe 'rendering namespace statistics', feature_category: :metrics do include GraphqlHelpers let(:namespace) { user.namespace } - let!(:statistics) { create(:namespace_root_storage_statistics, namespace: namespace, packages_size: 5.gigabytes, uploads_size: 3.gigabytes) } let(:user) { create(:user) } + let!(:statistics) do + create( + :namespace_root_storage_statistics, + namespace: namespace, + packages_size: 5.gigabytes, + uploads_size: 3.gigabytes + ) + end let(:query) do graphql_query_for( diff --git a/spec/requests/api/graphql/organizations/organization_query_spec.rb b/spec/requests/api/graphql/organizations/organization_query_spec.rb index d02158382eb..c243e0613ad 100644 --- a/spec/requests/api/graphql/organizations/organization_query_spec.rb +++ b/spec/requests/api/graphql/organizations/organization_query_spec.rb @@ -79,7 +79,10 @@ RSpec.describe 'getting organization information', feature_category: :cell do <<~FIELDS organizationUsers { nodes { - badges + badges { + text + variant + } id user { id @@ -94,7 +97,7 @@ RSpec.describe 'getting organization information', feature_category: :cell do organization_user_node = graphql_data_at(:organization, :organizationUsers, :nodes).first expected_attributes = { - "badges" => ["It's you!"], + "badges" => [{ "text" => "It's you!", "variant" => 'muted' }], "id" => organization_user.to_global_id.to_s, "user" => { "id" => user.to_global_id.to_s } } diff --git a/spec/requests/api/graphql/project/base_service_spec.rb b/spec/requests/api/graphql/project/base_service_spec.rb index b27cddea07b..646ff8dd8a8 100644 --- a/spec/requests/api/graphql/project/base_service_spec.rb +++ b/spec/requests/api/graphql/project/base_service_spec.rb @@ -41,7 +41,7 @@ RSpec.describe 'query Jira service', feature_category: :system_access do it 'retuns list of jira imports' do service_types = services.map { |s| s['type'] } - expect(service_types).to match_array(%w(BugzillaService JiraService RedmineService)) + expect(service_types).to match_array(%w[BugzillaService JiraService RedmineService]) end end end diff --git a/spec/requests/api/graphql/project/container_repositories_spec.rb b/spec/requests/api/graphql/project/container_repositories_spec.rb index 9a40a972256..c86d3bdd14c 100644 --- a/spec/requests/api/graphql/project/container_repositories_spec.rb +++ b/spec/requests/api/graphql/project/container_repositories_spec.rb @@ -46,7 +46,7 @@ RSpec.describe 'getting container repositories in a project', feature_category: before do stub_container_registry_config(enabled: true) container_repositories.each do |repository| - stub_container_registry_tags(repository: repository.path, tags: %w(tag1 tag2 tag3), with_manifest: false) + stub_container_registry_tags(repository: repository.path, tags: %w[tag1 tag2 tag3], with_manifest: false) end end @@ -141,7 +141,7 @@ RSpec.describe 'getting container repositories in a project', feature_category: end before do - stub_container_registry_tags(repository: container_repository.path, tags: %w(tag4 tag5 tag6), with_manifest: false) + stub_container_registry_tags(repository: container_repository.path, tags: %w[tag4 tag5 tag6], with_manifest: false) end it 'returns the searched container repository' do @@ -175,11 +175,11 @@ RSpec.describe 'getting container repositories in a project', feature_category: let_it_be(:container_repository5) { create(:container_repository, name: 'e', project: sort_project) } before do - stub_container_registry_tags(repository: container_repository1.path, tags: %w(tag1 tag1 tag3), with_manifest: false) - stub_container_registry_tags(repository: container_repository2.path, tags: %w(tag4 tag5 tag6), with_manifest: false) - stub_container_registry_tags(repository: container_repository3.path, tags: %w(tag7 tag8), with_manifest: false) - stub_container_registry_tags(repository: container_repository4.path, tags: %w(tag9), with_manifest: false) - stub_container_registry_tags(repository: container_repository5.path, tags: %w(tag10 tag11), with_manifest: false) + stub_container_registry_tags(repository: container_repository1.path, tags: %w[tag1 tag1 tag3], with_manifest: false) + stub_container_registry_tags(repository: container_repository2.path, tags: %w[tag4 tag5 tag6], with_manifest: false) + stub_container_registry_tags(repository: container_repository3.path, tags: %w[tag7 tag8], with_manifest: false) + stub_container_registry_tags(repository: container_repository4.path, tags: %w[tag9], with_manifest: false) + stub_container_registry_tags(repository: container_repository5.path, tags: %w[tag10 tag11], with_manifest: false) end def pagination_query(params) diff --git a/spec/requests/api/graphql/project/data_transfer_spec.rb b/spec/requests/api/graphql/project/data_transfer_spec.rb index aafa8d65eb9..79b2b10419b 100644 --- a/spec/requests/api/graphql/project/data_transfer_spec.rb +++ b/spec/requests/api/graphql/project/data_transfer_spec.rb @@ -68,45 +68,21 @@ RSpec.describe 'project data transfers', feature_category: :source_code_manageme context 'when user has enough permissions' do before do project.add_owner(current_user) + subject end - context 'when data_transfer_monitoring_mock_data is NOT enabled' do - before do - stub_feature_flags(data_transfer_monitoring_mock_data: false) - subject - end - - it 'returns real results' do - expect(response).to have_gitlab_http_status(:ok) + it 'returns real results' do + expect(response).to have_gitlab_http_status(:ok) - expect(egress_data.count).to eq(2) + expect(egress_data.count).to eq(2) - expect(egress_data.first.keys).to match_array( - %w[date totalEgress repositoryEgress artifactsEgress packagesEgress registryEgress] - ) + expect(egress_data.first.keys).to match_array( + %w[date totalEgress repositoryEgress artifactsEgress packagesEgress registryEgress] + ) - expect(egress_data.pluck('repositoryEgress')).to match_array(%w[1 2]) - end - - it_behaves_like 'a working graphql query' + expect(egress_data.pluck('repositoryEgress')).to match_array(%w[1 2]) end - context 'when data_transfer_monitoring_mock_data is enabled' do - before do - stub_feature_flags(data_transfer_monitoring_mock_data: true) - subject - end - - it 'returns mock results' do - expect(response).to have_gitlab_http_status(:ok) - - expect(egress_data.count).to eq(12) - expect(egress_data.first.keys).to match_array( - %w[date totalEgress repositoryEgress artifactsEgress packagesEgress registryEgress] - ) - end - - it_behaves_like 'a working graphql query' - end + it_behaves_like 'a working graphql query' end end diff --git a/spec/requests/api/graphql/project/environments_spec.rb b/spec/requests/api/graphql/project/environments_spec.rb index 3a863bd3d77..94ce6b797cd 100644 --- a/spec/requests/api/graphql/project/environments_spec.rb +++ b/spec/requests/api/graphql/project/environments_spec.rb @@ -150,7 +150,7 @@ RSpec.describe 'Project Environments query', feature_category: :continuous_deliv end describe 'last deployments of environments' do - ::Deployment.statuses.each do |status, _| # rubocop:disable RSpec/UselessDynamicDefinition + ::Deployment.statuses.each_key do |status| # rubocop:disable RSpec/UselessDynamicDefinition -- `status` used in `let_it_be` let_it_be(:"production_#{status}_deployment") do create(:deployment, status.to_sym, environment: production, project: project) end diff --git a/spec/requests/api/graphql/project/error_tracking/sentry_errors_request_spec.rb b/spec/requests/api/graphql/project/error_tracking/sentry_errors_request_spec.rb index 6cbc70022ed..c2910938acf 100644 --- a/spec/requests/api/graphql/project/error_tracking/sentry_errors_request_spec.rb +++ b/spec/requests/api/graphql/project/error_tracking/sentry_errors_request_spec.rb @@ -119,7 +119,7 @@ RSpec.describe 'sentry errors requests', feature_category: :error_tracking do end let(:error_data) { graphql_data.dig('project', 'sentryErrors', 'errors', 'nodes') } - let(:pagination_data) { graphql_data.dig('project', 'sentryErrors', 'errors', 'pageInfo') } + let(:pagination_data) { graphql_data.dig('project', 'sentryErrors', 'errors', 'pageInfo') } it_behaves_like 'a working graphql query' do before do diff --git a/spec/requests/api/graphql/project/issue/design_collection/versions_spec.rb b/spec/requests/api/graphql/project/issue/design_collection/versions_spec.rb index a15e4c1e792..bc56df5b6ff 100644 --- a/spec/requests/api/graphql/project/issue/design_collection/versions_spec.rb +++ b/spec/requests/api/graphql/project/issue/design_collection/versions_spec.rb @@ -73,7 +73,7 @@ RSpec.describe 'Getting versions related to an issue', feature_category: :design post_graphql(query, current_user: current_user) keys = graphql_data.dig(*edges_path).first['node'].keys - expect(keys).to match_array(%w(id sha createdAt author)) + expect(keys).to match_array(%w[id sha createdAt author]) end end diff --git a/spec/requests/api/graphql/project/issue_spec.rb b/spec/requests/api/graphql/project/issue_spec.rb index bc90f9e89e6..8d21a9f0394 100644 --- a/spec/requests/api/graphql/project/issue_spec.rb +++ b/spec/requests/api/graphql/project/issue_spec.rb @@ -108,7 +108,7 @@ RSpec.describe 'Query.project(fullPath).issue(iid)', feature_category: :team_pla let(:object_field_name) { :design } let(:no_argument_error) do - custom_graphql_error(path, a_string_matching(%r/id or filename/)) + custom_graphql_error(path, a_string_matching(%r{id or filename})) end let_it_be(:object_on_other_issue) { create(:design, issue: issue_b) } @@ -134,7 +134,7 @@ RSpec.describe 'Query.project(fullPath).issue(iid)', feature_category: :team_pla it 'raises an error' do post_query - expect(graphql_errors).to include(custom_graphql_error(path, a_string_matching(%r/id or sha/))) + expect(graphql_errors).to include(custom_graphql_error(path, a_string_matching(%r{id or sha}))) end end diff --git a/spec/requests/api/graphql/project/jira_import_spec.rb b/spec/requests/api/graphql/project/jira_import_spec.rb index 25cea0238ef..a1d340b3500 100644 --- a/spec/requests/api/graphql/project/jira_import_spec.rb +++ b/spec/requests/api/graphql/project/jira_import_spec.rb @@ -96,7 +96,7 @@ RSpec.describe 'query Jira import data', feature_category: :importers do total_issue_count = jira_imports.map { |ji| ji.dig('totalIssueCount') } expect(jira_imports.size).to eq 2 - expect(jira_proket_keys).to eq %w(BB AA) + expect(jira_proket_keys).to eq %w[BB AA] expect(usernames).to eq [current_user.username, current_user.username] expect(imported_issues_count).to eq [2, 2] expect(failed_issues_count).to eq [1, 2] diff --git a/spec/requests/api/graphql/project/jira_projects_spec.rb b/spec/requests/api/graphql/project/jira_projects_spec.rb index 3cd689deda5..2859e8a7c99 100644 --- a/spec/requests/api/graphql/project/jira_projects_spec.rb +++ b/spec/requests/api/graphql/project/jira_projects_spec.rb @@ -55,8 +55,8 @@ RSpec.describe 'query Jira projects', feature_category: :integrations do project_ids = jira_projects.map { |jp| jp['projectId'] } expect(jira_projects.size).to eq(2) - expect(project_keys).to eq(%w(EX ABC)) - expect(project_names).to eq(%w(Example Alphabetical)) + expect(project_keys).to eq(%w[EX ABC]) + expect(project_names).to eq(%w[Example Alphabetical]) expect(project_ids).to eq([10000, 10001]) end @@ -69,8 +69,8 @@ RSpec.describe 'query Jira projects', feature_category: :integrations do project_ids = jira_projects.map { |jp| jp['projectId'] } expect(jira_projects.size).to eq(1) - expect(project_keys).to eq(%w(EX)) - expect(project_names).to eq(%w(Example)) + expect(project_keys).to eq(%w[EX]) + expect(project_names).to eq(%w[Example]) expect(project_ids).to eq([10000]) end end diff --git a/spec/requests/api/graphql/project/merge_request_spec.rb b/spec/requests/api/graphql/project/merge_request_spec.rb index c274199e65b..23be9fa5286 100644 --- a/spec/requests/api/graphql/project/merge_request_spec.rb +++ b/spec/requests/api/graphql/project/merge_request_spec.rb @@ -22,7 +22,7 @@ RSpec.describe 'getting merge request information nested in a project', feature_ it_behaves_like 'a working graphql query' do # we exclude Project.pipeline because it needs arguments, - # codequalityReportsComparer because no pipeline exist yet + # codequalityReportsComparer because it is behind a feature flag # and runners because the user is not an admin and therefore has no access let(:excluded) { %w[jobs pipeline runners codequalityReportsComparer] } let(:mr_fields) { all_graphql_fields_for('MergeRequest', excluded: excluded) } diff --git a/spec/requests/api/graphql/project/merge_requests_spec.rb b/spec/requests/api/graphql/project/merge_requests_spec.rb index 543de43bcf3..176a02df0be 100644 --- a/spec/requests/api/graphql/project/merge_requests_spec.rb +++ b/spec/requests/api/graphql/project/merge_requests_spec.rb @@ -48,8 +48,11 @@ RSpec.describe 'getting merge request listings nested in a project', feature_cat end it_behaves_like 'a working graphql query' do + # we exclude codequalityReportsComparer because it is behind feature flag + let(:excluded) { %w[codequalityReportsComparer] } + let(:query) do - query_merge_requests(all_graphql_fields_for('MergeRequest', max_depth: 2)) + query_merge_requests(all_graphql_fields_for('MergeRequest', max_depth: 2, excluded: excluded)) end before do diff --git a/spec/requests/api/graphql/project/release_spec.rb b/spec/requests/api/graphql/project/release_spec.rb index 8d4a39d6b30..8206d076d2e 100644 --- a/spec/requests/api/graphql/project/release_spec.rb +++ b/spec/requests/api/graphql/project/release_spec.rb @@ -36,7 +36,7 @@ RSpec.describe 'Query.project(fullPath).release(tagName)', feature_category: :re let(:path) { path_prefix } let(:release_fields) do - %{ + %( tagName tagPath description @@ -45,7 +45,7 @@ RSpec.describe 'Query.project(fullPath).release(tagName)', feature_category: :re createdAt releasedAt upcomingRelease - } + ) end before do @@ -176,14 +176,14 @@ RSpec.describe 'Query.project(fullPath).release(tagName)', feature_category: :re let(:path) { path_prefix + %w[links] } let(:release_fields) do - query_graphql_field(:links, nil, %{ + query_graphql_field(:links, nil, %( selfUrl openedMergeRequestsUrl mergedMergeRequestsUrl closedMergeRequestsUrl openedIssuesUrl closedIssuesUrl - }) + )) end it 'finds all release links' do @@ -225,7 +225,7 @@ RSpec.describe 'Query.project(fullPath).release(tagName)', feature_category: :re let(:path) { path_prefix } let(:release_fields) do - %{ + %( tagName tagPath description @@ -234,7 +234,7 @@ RSpec.describe 'Query.project(fullPath).release(tagName)', feature_category: :re createdAt releasedAt upcomingRelease - } + ) end before do @@ -358,14 +358,14 @@ RSpec.describe 'Query.project(fullPath).release(tagName)', feature_category: :re let(:path) { path_prefix + %w[links] } let(:release_fields) do - query_graphql_field(:links, nil, %{ + query_graphql_field(:links, nil, %( selfUrl openedMergeRequestsUrl mergedMergeRequestsUrl closedMergeRequestsUrl openedIssuesUrl closedIssuesUrl - }) + )) end it 'finds only selfUrl' do @@ -547,10 +547,10 @@ RSpec.describe 'Query.project(fullPath).release(tagName)', feature_category: :re let(:current_user) { developer } let(:release_fields) do - %{ + %( releasedAt upcomingRelease - } + ) end before do @@ -588,13 +588,13 @@ RSpec.describe 'Query.project(fullPath).release(tagName)', feature_category: :re let_it_be_with_reload(:release) { create(:release, project: project) } let(:release_fields) do - %{ + %( milestones { nodes { title } } - } + ) end let(:actual_milestone_title_order) do diff --git a/spec/requests/api/graphql/project/terraform/state_spec.rb b/spec/requests/api/graphql/project/terraform/state_spec.rb index 1889e7a1064..28f3868c7cf 100644 --- a/spec/requests/api/graphql/project/terraform/state_spec.rb +++ b/spec/requests/api/graphql/project/terraform/state_spec.rb @@ -20,7 +20,7 @@ RSpec.describe 'query a single terraform state', feature_category: :infrastructu query_graphql_field( :terraformState, { name: terraform_state.name }, - %{ + %( id name lockedAt @@ -45,7 +45,7 @@ RSpec.describe 'query a single terraform state', feature_category: :infrastructu lockedByUser { id } - } + ) ) ) end diff --git a/spec/requests/api/graphql/project/terraform/states_spec.rb b/spec/requests/api/graphql/project/terraform/states_spec.rb index 7a789a5d481..d6cf3a52649 100644 --- a/spec/requests/api/graphql/project/terraform/states_spec.rb +++ b/spec/requests/api/graphql/project/terraform/states_spec.rb @@ -14,7 +14,7 @@ RSpec.describe 'query terraform states', feature_category: :infrastructure_as_co graphql_query_for( :project, { fullPath: project.full_path }, - %{ + %( terraformStates { count nodes { @@ -45,7 +45,7 @@ RSpec.describe 'query terraform states', feature_category: :infrastructure_as_co } } } - } + ) ) end diff --git a/spec/requests/api/graphql/project/work_items_spec.rb b/spec/requests/api/graphql/project/work_items_spec.rb index d5d3d6c578f..d0f80bcfebe 100644 --- a/spec/requests/api/graphql/project/work_items_spec.rb +++ b/spec/requests/api/graphql/project/work_items_spec.rb @@ -350,7 +350,9 @@ RSpec.describe 'getting a work item list for a project', feature_category: :team end context 'when fetching work item linked items widget' do - let_it_be(:related_items) { create_list(:work_item, 3, project: project, milestone: milestone1) } + let_it_be(:other_project) { create(:project, :repository, :public, group: group) } + let_it_be(:other_milestone) { create(:milestone, project: other_project) } + let_it_be(:related_items) { create_list(:work_item, 3, project: other_project, milestone: other_milestone) } let(:fields) do <<~GRAPHQL @@ -384,21 +386,24 @@ RSpec.describe 'getting a work item list for a project', feature_category: :team before do create(:work_item_link, source: item1, target: related_items[0], link_type: 'relates_to') + create(:work_item_link, source: item2, target: related_items[0], link_type: 'relates_to') end it 'executes limited number of N+1 queries', :use_sql_query_cache do + post_graphql(query, current_user: current_user) # warm-up + control = ActiveRecord::QueryRecorder.new(skip_cached: false) do post_graphql(query, current_user: current_user) end - create(:work_item_link, source: item1, target: related_items[1], link_type: 'relates_to') - create(:work_item_link, source: item1, target: related_items[2], link_type: 'relates_to') + [item1, item2].each do |item| + create(:work_item_link, source: item, target: related_items[1], link_type: 'relates_to') + create(:work_item_link, source: item, target: related_items[2], link_type: 'relates_to') + end expect_graphql_errors_to_be_empty - # TODO: Fix N+1 queries executed for the linked work item widgets - # https://gitlab.com/gitlab-org/gitlab/-/issues/420605 expect { post_graphql(query, current_user: current_user) } - .not_to exceed_all_query_limit(control).with_threshold(11) + .not_to exceed_all_query_limit(control) end end diff --git a/spec/requests/api/graphql/projects/projects_spec.rb b/spec/requests/api/graphql/projects/projects_spec.rb new file mode 100644 index 00000000000..84b8c2285f0 --- /dev/null +++ b/spec/requests/api/graphql/projects/projects_spec.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'getting a collection of projects', feature_category: :source_code_management do + include GraphqlHelpers + + let_it_be(:current_user) { create(:user) } + let_it_be(:group) { create(:group, name: 'public-group') } + let_it_be(:projects) { create_list(:project, 5, :public, group: group) } + let_it_be(:other_project) { create(:project, :public, group: group) } + + let(:filters) { {} } + + let(:query) do + graphql_query_for( + :projects, + filters, + "nodes {#{all_graphql_fields_for('Project', max_depth: 1, excluded: ['productAnalyticsState'])} }" + ) + end + + before_all do + group.add_developer(current_user) + end + + context 'when providing full_paths filter' do + let(:project_full_paths) { projects.map(&:full_path) } + let(:filters) { { full_paths: project_full_paths } } + + let(:single_project_query) do + graphql_query_for( + :projects, + { full_paths: [project_full_paths.first] }, + "nodes {#{all_graphql_fields_for('Project', max_depth: 1, excluded: ['productAnalyticsState'])} }" + ) + end + + it_behaves_like 'a working graphql query that returns data' do + before do + post_graphql(query, current_user: current_user) + end + end + + it 'avoids N+1 queries', :use_sql_query_cache, :clean_gitlab_redis_cache do + post_graphql(single_project_query, current_user: current_user) + + query_count = ActiveRecord::QueryRecorder.new do + post_graphql(single_project_query, current_user: current_user) + end.count + + # There is an N+1 query for max_member_access_for_user_ids + expect do + post_graphql(query, current_user: current_user) + end.not_to exceed_all_query_limit(query_count + 5) + end + + it 'returns the expected projects' do + post_graphql(query, current_user: current_user) + returned_full_paths = graphql_data_at(:projects, :nodes).pluck('fullPath') + + expect(returned_full_paths).to match_array(project_full_paths) + end + + context 'when users provides more than 50 full_paths' do + let(:filters) { { full_paths: Array.new(51) { other_project.full_path } } } + + it 'returns an error' do + post_graphql(query, current_user: current_user) + + expect(graphql_errors).to contain_exactly( + hash_including('message' => _('You cannot provide more than 50 full_paths')) + ) + end + end + end +end diff --git a/spec/requests/api/graphql/user_spec.rb b/spec/requests/api/graphql/user_spec.rb index 41ee233dfc5..22ebc1be964 100644 --- a/spec/requests/api/graphql/user_spec.rb +++ b/spec/requests/api/graphql/user_spec.rb @@ -113,4 +113,36 @@ RSpec.describe 'User', feature_category: :user_profile do end end end + + describe 'organizations field' do + let_it_be(:organization_user) { create(:organization_user, user: current_user) } + let_it_be(:organization) { organization_user.organization } + let_it_be(:another_organization) { create(:organization) } + let_it_be(:another_user) { create(:user) } + + let(:query) do + graphql_query_for( + :user, + { username: current_user.username.to_s.upcase }, + 'organizations { nodes { path } }' + ) + end + + context 'with permission' do + it 'returns the relevant organization details' do + post_graphql(query, current_user: current_user) + + expect(graphql_data.dig('user', 'organizations', 'nodes').pluck('path')) + .to match_array(organization.path) + end + end + + context 'without permission' do + it 'does not return organization details' do + post_graphql(query, current_user: another_user) + + expect(graphql_data.dig('user', 'organizations', 'nodes')).to be_nil + end + end + end end diff --git a/spec/requests/api/graphql/work_item_spec.rb b/spec/requests/api/graphql/work_item_spec.rb index b8575b25e0a..36a27abd982 100644 --- a/spec/requests/api/graphql/work_item_spec.rb +++ b/spec/requests/api/graphql/work_item_spec.rb @@ -619,6 +619,47 @@ RSpec.describe 'Query.work_item(id)', feature_category: :team_planning do ) end + context 'when inaccessible links are present' do + let_it_be(:no_access_item) { create(:work_item, title: "PRIVATE", project: create(:project, :private)) } + + before do + create(:work_item_link, source: work_item, target: no_access_item, link_type: 'relates_to') + end + + it 'returns only items that the user has access to' do + expect(graphql_dig_at(work_item_data, :widgets, "linkedItems", "nodes", "linkId")) + .to match_array([link1.to_gid.to_s, link2.to_gid.to_s]) + end + end + + context 'when limiting the number of results' do + it_behaves_like 'sorted paginated query' do + include_context 'no sort argument' + + let(:first_param) { 1 } + let(:all_records) { [link1, link2] } + let(:data_path) { ['workItem', 'widgets', "linkedItems", -1] } + + def widget_fields(args) + query_graphql_field( + :widgets, {}, query_graphql_field( + '... on WorkItemWidgetLinkedItems', {}, query_graphql_field( + 'linkedItems', args, "#{page_info} nodes { linkId }" + ) + ) + ) + end + + def pagination_query(params) + graphql_query_for('workItem', { 'id' => global_id }, widget_fields(params)) + end + + def pagination_results_data(nodes) + nodes.map { |item| GlobalID::Locator.locate(item['linkId']) } + end + end + end + context 'when filtering by link type' do let(:work_item_fields) do <<~GRAPHQL @@ -664,20 +705,6 @@ RSpec.describe 'Query.work_item(id)', feature_category: :team_planning do end describe 'notes widget' do - let(:work_item_fields) do - <<~GRAPHQL - id - widgets { - type - ... on WorkItemWidgetNotes { - system: discussions(filter: ONLY_ACTIVITY, first: 10) { nodes { id notes { nodes { id system internal body } } } }, - comments: discussions(filter: ONLY_COMMENTS, first: 10) { nodes { id notes { nodes { id system internal body } } } }, - all_notes: discussions(filter: ALL_NOTES, first: 10) { nodes { id notes { nodes { id system internal body } } } } - } - } - GRAPHQL - end - context 'when fetching award emoji from notes' do let(:work_item_fields) do <<~GRAPHQL @@ -768,6 +795,26 @@ RSpec.describe 'Query.work_item(id)', feature_category: :team_planning do expect { post_graphql(query, current_user: developer) }.not_to exceed_query_limit(control).with_threshold(4) expect_graphql_errors_to_be_empty end + + context 'when work item is associated with a group' do + let_it_be(:group_work_item) { create(:work_item, :group_level, namespace: group) } + let_it_be(:group_work_item_note) { create(:note, noteable: group_work_item, author: developer, project: nil) } + let(:global_id) { group_work_item.to_gid.to_s } + + before_all do + create(:award_emoji, awardable: group_work_item_note, name: 'rocket', user: developer) + end + + it 'returns notes for the group work item' do + all_widgets = graphql_dig_at(work_item_data, :widgets) + notes_widget = all_widgets.find { |x| x['type'] == 'NOTES' } + notes = graphql_dig_at(notes_widget['discussions'], :nodes).flat_map { |d| d['notes']['nodes'] } + + expect(notes).to contain_exactly( + hash_including('body' => group_work_item_note.note) + ) + end + end end end diff --git a/spec/requests/api/group_packages_spec.rb b/spec/requests/api/group_packages_spec.rb index 0b4f6130132..0786815c787 100644 --- a/spec/requests/api/group_packages_spec.rb +++ b/spec/requests/api/group_packages_spec.rb @@ -137,6 +137,29 @@ RSpec.describe API::GroupPackages, feature_category: :package_registry do it_behaves_like 'filters on each package_type', is_project: false + context 'filtering on package_version' do + include_context 'package filter context' + + let!(:package1) { create(:nuget_package, project: project, version: '2.0.4') } + let!(:package2) { create(:nuget_package, project: project) } + + it 'returns the versioned package' do + url = group_filter_url(:version, '2.0.4') + get api(url, user) + + expect(json_response.length).to eq(1) + expect(json_response.first['version']).to eq(package1.version) + end + + it 'include_versionless has no effect' do + url = "/groups/#{group.id}/packages?package_version=2.0.4&include_versionless=true" + get api(url, user) + + expect(json_response.length).to eq(1) + expect(json_response.first['version']).to eq(package1.version) + end + end + context 'does not accept non supported package_type value' do include_context 'package filter context' diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb index 662e11f7cfb..327dfd0a76b 100644 --- a/spec/requests/api/groups_spec.rb +++ b/spec/requests/api/groups_spec.rb @@ -572,7 +572,8 @@ RSpec.describe API::Groups, feature_category: :groups_and_projects do expect(json_response['require_two_factor_authentication']).to eq(group1.require_two_factor_authentication) expect(json_response['two_factor_grace_period']).to eq(group1.two_factor_grace_period) expect(json_response['auto_devops_enabled']).to eq(group1.auto_devops_enabled) - expect(json_response['emails_disabled']).to eq(group1.emails_disabled) + expect(json_response['emails_disabled']).to eq(group1.emails_disabled?) + expect(json_response['emails_enabled']).to eq(group1.emails_enabled?) expect(json_response['mentions_disabled']).to eq(group1.mentions_disabled) expect(json_response['project_creation_level']).to eq('maintainer') expect(json_response['subgroup_creation_level']).to eq('maintainer') @@ -870,6 +871,38 @@ RSpec.describe API::Groups, feature_category: :groups_and_projects do end end + before do + stub_application_setting(update_namespace_name_rate_limit: 1) + end + + it 'increments the update_namespace_name rate limit' do + put api("/groups/#{group1.id}", user1), params: { name: "#{new_group_name}_1" } + + expect(::Gitlab::ApplicationRateLimiter.peek(:update_namespace_name, scope: group1)).to be_falsey + + put api("/groups/#{group1.id}", user1), params: { name: "#{new_group_name}_2" } + + expect(::Gitlab::ApplicationRateLimiter.peek(:update_namespace_name, scope: group1)).to be_truthy + expect(response).to have_gitlab_http_status(:ok) + expect(group1.reload.name).to eq("#{new_group_name}_2") + end + + context 'a name is not passed in' do + it 'does not mark name update throttling' do + expect(::Gitlab::ApplicationRateLimiter).not_to receive(:throttled?) + + put api("/groups/#{group1.id}", user1), params: { path: 'another/path' } + end + end + + context 'an empty name is passed in' do + it 'does not mark name update throttling' do + expect(::Gitlab::ApplicationRateLimiter).not_to receive(:throttled?) + + put api("/groups/#{group1.id}", user1), params: { name: '' } + end + end + context 'when authenticated as the group owner' do it 'updates the group', :aggregate_failures do workhorse_form_with_file( @@ -895,7 +928,8 @@ RSpec.describe API::Groups, feature_category: :groups_and_projects do expect(json_response['require_two_factor_authentication']).to eq(false) expect(json_response['two_factor_grace_period']).to eq(48) expect(json_response['auto_devops_enabled']).to eq(nil) - expect(json_response['emails_disabled']).to eq(nil) + expect(json_response['emails_disabled']).to eq(false) + expect(json_response['emails_enabled']).to eq(true) expect(json_response['mentions_disabled']).to eq(nil) expect(json_response['project_creation_level']).to eq("noone") expect(json_response['subgroup_creation_level']).to eq("maintainer") diff --git a/spec/requests/api/helm_packages_spec.rb b/spec/requests/api/helm_packages_spec.rb index d6afd6f86ff..75f60c59759 100644 --- a/spec/requests/api/helm_packages_spec.rb +++ b/spec/requests/api/helm_packages_spec.rb @@ -101,6 +101,12 @@ RSpec.describe API::HelmPackages, feature_category: :package_registry do end it_behaves_like 'deploy token for package GET requests' + + context 'when format param is not nil' do + let(:url) { "/projects/#{project.id}/packages/helm/stable/charts/#{package.name}-#{package.version}.tgz.prov" } + + it_behaves_like 'rejects helm packages access', :maintainer, :not_found, '{"message":"404 Format prov Not Found"}' + end end describe 'POST /api/v4/projects/:id/packages/helm/api/:channel/charts/authorize' do diff --git a/spec/requests/api/internal/base_spec.rb b/spec/requests/api/internal/base_spec.rb index cf0cd9a2e85..e59633b6d35 100644 --- a/spec/requests/api/internal/base_spec.rb +++ b/spec/requests/api/internal/base_spec.rb @@ -111,12 +111,12 @@ RSpec.describe API::Internal::Base, feature_category: :system_access do it 'returns new recovery codes when the user exists' do allow_any_instance_of(User).to receive(:two_factor_enabled?).and_return(true) allow_any_instance_of(User) - .to receive(:generate_otp_backup_codes!).and_return(%w(119135e5a3ebce8e 34bd7b74adbc8861)) + .to receive(:generate_otp_backup_codes!).and_return(%w[119135e5a3ebce8e 34bd7b74adbc8861]) subject expect(json_response['success']).to be_truthy - expect(json_response['recovery_codes']).to match_array(%w(119135e5a3ebce8e 34bd7b74adbc8861)) + expect(json_response['recovery_codes']).to match_array(%w[119135e5a3ebce8e 34bd7b74adbc8861]) end end @@ -200,7 +200,7 @@ RSpec.describe API::Internal::Base, feature_category: :system_access do params: { key_id: key.id, name: 'newtoken', - scopes: %w(read_api badscope read_repository) + scopes: %w[read_api badscope read_repository] }, headers: gitlab_shell_internal_api_request_header @@ -216,14 +216,14 @@ RSpec.describe API::Internal::Base, feature_category: :system_access do params: { key_id: key.id, name: 'newtoken', - scopes: %w(read_api read_repository), + scopes: %w[read_api read_repository], expires_at: max_pat_access_token_lifetime }, headers: gitlab_shell_internal_api_request_header expect(json_response['success']).to be_truthy expect(json_response['token']).to match(/\A\S{#{token_size}}\z/) - expect(json_response['scopes']).to match_array(%w(read_api read_repository)) + expect(json_response['scopes']).to match_array(%w[read_api read_repository]) expect(json_response['expires_at']).to eq(max_pat_access_token_lifetime.iso8601) end end @@ -236,14 +236,14 @@ RSpec.describe API::Internal::Base, feature_category: :system_access do params: { key_id: key.id, name: 'newtoken', - scopes: %w(read_api read_repository), + scopes: %w[read_api read_repository], expires_at: 365.days.from_now }, headers: gitlab_shell_internal_api_request_header expect(json_response['success']).to be_truthy expect(json_response['token']).to match(/\A\S{#{token_size}}\z/) - expect(json_response['scopes']).to match_array(%w(read_api read_repository)) + expect(json_response['scopes']).to match_array(%w[read_api read_repository]) expect(json_response['expires_at']).to eq(max_pat_access_token_lifetime.iso8601) end end @@ -560,6 +560,20 @@ RSpec.describe API::Internal::Base, feature_category: :system_access do end end + context 'when Gitaly provides a relative_path argument', :request_store do + subject { push(key, project, relative_path: relative_path) } + + let(:relative_path) { 'relative_path' } + + it 'stores relative_path value in RequestStore' do + allow(Gitlab::SafeRequestStore).to receive(:[]=).and_call_original + expect(Gitlab::SafeRequestStore).to receive(:[]=).with(:gitlab_git_relative_path, relative_path) + subject + + expect(response).to have_gitlab_http_status(:ok) + end + end + context "git push with project.wiki" do subject { push(key, project.wiki, env: env.to_json) } @@ -744,6 +758,17 @@ RSpec.describe API::Internal::Base, feature_category: :system_access do expect(json_response["gitaly"]["features"]).to eq('gitaly-feature-mep-mep' => 'false') end end + + context 'with audit event' do + it 'does not send a git streaming audit event' do + expect(::Gitlab::Audit::Auditor).not_to receive(:audit) + + pull(key, project) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response["need_audit"]).to be_falsy + end + end end context "git push" do @@ -757,6 +782,7 @@ RSpec.describe API::Internal::Base, feature_category: :system_access do expect(json_response["gl_project_path"]).to eq(project.full_path) expect(json_response["gl_key_type"]).to eq("key") expect(json_response["gl_key_id"]).to eq(key.id) + expect(json_response["need_audit"]).to be_falsy expect(json_response["gitaly"]).not_to be_nil expect(json_response["gitaly"]["repository"]).not_to be_nil expect(json_response["gitaly"]["repository"]["storage_name"]).to eq(project.repository.gitaly_repository.storage_name) @@ -885,7 +911,7 @@ RSpec.describe API::Internal::Base, feature_category: :system_access do { 'action' => 'geo_proxy_to_primary', 'data' => { - 'api_endpoints' => %w{geo/proxy_git_ssh/info_refs_receive_pack geo/proxy_git_ssh/receive_pack}, + 'api_endpoints' => %w[geo/proxy_git_ssh/info_refs_receive_pack geo/proxy_git_ssh/receive_pack], 'gl_username' => 'testuser', 'primary_repo' => 'http://localhost:3000/testuser/repo.git' } @@ -1515,7 +1541,7 @@ RSpec.describe API::Internal::Base, feature_category: :system_access do describe 'POST /internal/pre_receive' do let(:valid_params) do - { gl_repository: gl_repository } + { gl_repository: gl_repository, user_id: user.id } end it 'decreases the reference counter and returns the result' do @@ -1527,6 +1553,12 @@ RSpec.describe API::Internal::Base, feature_category: :system_access do expect(json_response['reference_counter_increased']).to be(true) end + + it 'sticks to the primary' do + expect(User.sticking).to receive(:find_caught_up_replica).with(:user, user.id) + + post api("/internal/pre_receive"), params: valid_params, headers: gitlab_shell_internal_api_request_header + end end describe 'POST /internal/two_factor_config' do diff --git a/spec/requests/api/internal/pages_spec.rb b/spec/requests/api/internal/pages_spec.rb index 1eeb3404157..7e2a778f433 100644 --- a/spec/requests/api/internal/pages_spec.rb +++ b/spec/requests/api/internal/pages_spec.rb @@ -46,17 +46,7 @@ RSpec.describe API::Internal::Pages, feature_category: :pages do end end - context 'when authenticated' do - before do - project.update_pages_deployment!(create(:pages_deployment, project: project)) - end - - around do |example| - freeze_time do - example.run - end - end - + context 'when authenticated', :freeze_time do context 'when domain does not exist' do it 'responds with 204 no content' do get api('/internal/pages'), headers: auth_header, params: { host: 'any-domain.gitlab.io' } @@ -79,10 +69,6 @@ RSpec.describe API::Internal::Pages, feature_category: :pages do end context 'when there are no pages deployed for the related project' do - before do - project.mark_pages_as_not_deployed - end - it 'responds with 204 No Content' do get api('/internal/pages'), headers: auth_header, params: { host: 'pages.io' } @@ -91,9 +77,7 @@ RSpec.describe API::Internal::Pages, feature_category: :pages do end context 'when there are pages deployed for the related project' do - before do - project.mark_pages_as_deployed - end + let!(:deployment) { create(:pages_deployment, project: project) } it 'domain lookup is case insensitive' do get api('/internal/pages'), headers: auth_header, params: { host: 'Pages.IO' } @@ -110,7 +94,6 @@ RSpec.describe API::Internal::Pages, feature_category: :pages do expect(json_response['certificate']).to eq(pages_domain.certificate) expect(json_response['key']).to eq(pages_domain.key) - deployment = project.pages_metadatum.pages_deployment expect(json_response['lookup_paths']).to eq( [ { @@ -144,10 +127,6 @@ RSpec.describe API::Internal::Pages, feature_category: :pages do end context 'when there are no pages deployed for the related project' do - before do - project.mark_pages_as_not_deployed - end - it 'responds with 204 No Content' do get api('/internal/pages'), headers: auth_header, params: { host: 'unique-domain.example.com' } @@ -156,9 +135,7 @@ RSpec.describe API::Internal::Pages, feature_category: :pages do end context 'when there are pages deployed for the related project' do - before do - project.mark_pages_as_deployed - end + let!(:deployment) { create(:pages_deployment, project: project) } context 'when the unique domain is disabled' do before do @@ -186,7 +163,6 @@ RSpec.describe API::Internal::Pages, feature_category: :pages do expect(response).to have_gitlab_http_status(:ok) expect(response).to match_response_schema('internal/pages/virtual_domain') - deployment = project.pages_metadatum.pages_deployment expect(json_response['lookup_paths']).to eq( [ { @@ -218,10 +194,6 @@ RSpec.describe API::Internal::Pages, feature_category: :pages do end context 'when there are no pages deployed for the related project' do - before do - project.mark_pages_as_not_deployed - end - it 'responds with 204 No Content' do get api('/internal/pages'), headers: auth_header, params: { host: "#{group.path}.gitlab-pages.io" } @@ -232,9 +204,7 @@ RSpec.describe API::Internal::Pages, feature_category: :pages do end context 'when there are pages deployed for the related project' do - before do - project.mark_pages_as_deployed - end + let!(:deployment) { create(:pages_deployment, project: project) } context 'with a regular project' do it 'responds with the correct domain configuration' do @@ -243,7 +213,6 @@ RSpec.describe API::Internal::Pages, feature_category: :pages do expect(response).to have_gitlab_http_status(:ok) expect(response).to match_response_schema('internal/pages/virtual_domain') - deployment = project.pages_metadatum.pages_deployment expect(json_response['lookup_paths']).to eq( [ { @@ -274,7 +243,7 @@ RSpec.describe API::Internal::Pages, feature_category: :pages do 3.times do project = create(:project, group: group) - project.mark_pages_as_deployed + create(:pages_deployment, project: project) end expect { get api('/internal/pages'), headers: auth_header, params: { host: "#{group.path}.gitlab-pages.io" } } @@ -292,7 +261,6 @@ RSpec.describe API::Internal::Pages, feature_category: :pages do expect(response).to have_gitlab_http_status(:ok) expect(response).to match_response_schema('internal/pages/virtual_domain') - deployment = project.pages_metadatum.pages_deployment expect(json_response['lookup_paths']).to eq( [ { diff --git a/spec/requests/api/issues/get_group_issues_spec.rb b/spec/requests/api/issues/get_group_issues_spec.rb index eaa3c46d0ca..0ae65479d5e 100644 --- a/spec/requests/api/issues/get_group_issues_spec.rb +++ b/spec/requests/api/issues/get_group_issues_spec.rb @@ -350,7 +350,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do get api(base_url, admin) expect(response).to have_gitlab_http_status(:ok) - expect(json_response.last.keys).to include(*%w(id iid project_id title description)) + expect(json_response.last.keys).to include(*%w[id iid project_id title description]) expect(json_response.last).not_to have_key('subscribed') end diff --git a/spec/requests/api/issues/get_project_issues_spec.rb b/spec/requests/api/issues/get_project_issues_spec.rb index 137fba66eaa..9e54ec08486 100644 --- a/spec/requests/api/issues/get_project_issues_spec.rb +++ b/spec/requests/api/issues/get_project_issues_spec.rb @@ -530,7 +530,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do get api("#{base_url}/issues", user) expect(response).to have_gitlab_http_status(:ok) - expect(json_response.last.keys).to include(*%w(id iid project_id title description)) + expect(json_response.last.keys).to include(*%w[id iid project_id title description]) expect(json_response.last).not_to have_key('subscribed') end diff --git a/spec/requests/api/issues/issues_spec.rb b/spec/requests/api/issues/issues_spec.rb index af289352778..ed71089c5a9 100644 --- a/spec/requests/api/issues/issues_spec.rb +++ b/spec/requests/api/issues/issues_spec.rb @@ -638,7 +638,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do end it 'returns an empty array if no issue matches labels with labels param as array' do - get api('/issues', user), params: { labels: %w(foo bar) } + get api('/issues', user), params: { labels: %w[foo bar] } expect_paginated_array_response([]) end @@ -914,7 +914,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do end it 'fails to sort with non predefined options' do - %w(milestone abracadabra).each do |sort_opt| + %w[milestone abracadabra].each do |sort_opt| get api('/issues', user), params: { order_by: sort_opt, sort: 'asc' } expect(response).to have_gitlab_http_status(:bad_request) end diff --git a/spec/requests/api/issues/post_projects_issues_spec.rb b/spec/requests/api/issues/post_projects_issues_spec.rb index 1cd20680afb..60142e7e151 100644 --- a/spec/requests/api/issues/post_projects_issues_spec.rb +++ b/spec/requests/api/issues/post_projects_issues_spec.rb @@ -183,7 +183,7 @@ RSpec.describe API::Issues, :aggregate_failures, feature_category: :team_plannin expect(response).to have_gitlab_http_status(:created) expect(json_response['title']).to eq('new issue') expect(json_response['description']).to be_nil - expect(json_response['labels']).to eq(%w(label label2)) + expect(json_response['labels']).to eq(%w[label label2]) expect(json_response['confidential']).to be_falsy expect(json_response['assignee']['name']).to eq(user2.name) expect(json_response['assignees'].first['name']).to eq(user2.name) @@ -191,12 +191,12 @@ RSpec.describe API::Issues, :aggregate_failures, feature_category: :team_plannin it 'creates a new project issue with labels param as array' do post api("/projects/#{project.id}/issues", user), - params: { title: 'new issue', labels: %w(label label2), weight: 3, assignee_ids: [user2.id] } + params: { title: 'new issue', labels: %w[label label2], weight: 3, assignee_ids: [user2.id] } expect(response).to have_gitlab_http_status(:created) expect(json_response['title']).to eq('new issue') expect(json_response['description']).to be_nil - expect(json_response['labels']).to eq(%w(label label2)) + expect(json_response['labels']).to eq(%w[label label2]) expect(json_response['confidential']).to be_falsy expect(json_response['assignee']['name']).to eq(user2.name) expect(json_response['assignees'].first['name']).to eq(user2.name) @@ -391,7 +391,7 @@ RSpec.describe API::Issues, :aggregate_failures, feature_category: :team_plannin it 'cannot create new labels with labels param as array' do expect do - post api("/projects/#{project.id}/issues", non_member), params: { title: 'new issue', labels: %w(label label2) } + post api("/projects/#{project.id}/issues", non_member), params: { title: 'new issue', labels: %w[label label2] } end.not_to change { project.labels.count } end end diff --git a/spec/requests/api/issues/put_projects_issues_spec.rb b/spec/requests/api/issues/put_projects_issues_spec.rb index dbba31cd4d6..070ef6057dd 100644 --- a/spec/requests/api/issues/put_projects_issues_spec.rb +++ b/spec/requests/api/issues/put_projects_issues_spec.rb @@ -359,7 +359,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do it 'updates labels and touches the record with labels param as array', :aggregate_failures do travel_to(2.minutes.from_now) do - put api_for_user, params: { labels: %w(foo bar) } + put api_for_user, params: { labels: %w[foo bar] } end expect(response).to have_gitlab_http_status(:ok) diff --git a/spec/requests/api/maven_packages_spec.rb b/spec/requests/api/maven_packages_spec.rb index 1f841eefff2..578a4821b5e 100644 --- a/spec/requests/api/maven_packages_spec.rb +++ b/spec/requests/api/maven_packages_spec.rb @@ -946,6 +946,22 @@ RSpec.describe API::MavenPackages, feature_category: :package_registry do expect(response).to have_gitlab_http_status(:forbidden) end + context 'with basic auth' do + where(:token_type) do + %i[personal_access_token deploy_token job] + end + + with_them do + let(:token) { send(token_type).token } + + it "authorizes upload with #{params[:token_type]} token" do + authorize_upload({}, headers.merge(basic_auth_header(token_type == :job ? ::Gitlab::Auth::CI_JOB_USER : user.username, token))) + + expect(response).to have_gitlab_http_status(:ok) + end + end + end + def authorize_upload(params = {}, request_headers = headers) put api("/projects/#{project.id}/packages/maven/com/example/my-app/#{version}/maven-metadata.xml/authorize"), params: params, headers: request_headers end @@ -1083,6 +1099,22 @@ RSpec.describe API::MavenPackages, feature_category: :package_registry do expect(response).to have_gitlab_http_status(:forbidden) end + context 'with basic auth' do + where(:token_type) do + %i[personal_access_token deploy_token job] + end + + with_them do + let(:token) { send(token_type).token } + + it "allows upload with #{params[:token_type]} token" do + upload_file(params: params, request_headers: headers.merge(basic_auth_header(token_type == :job ? ::Gitlab::Auth::CI_JOB_USER : user.username, token))) + + expect(response).to have_gitlab_http_status(:ok) + end + end + end + context 'file name is too long' do let(:file_name) { 'a' * (Packages::Maven::FindOrCreatePackageService::MAX_FILE_NAME_LENGTH + 1) } diff --git a/spec/requests/api/members_spec.rb b/spec/requests/api/members_spec.rb index 8dab9d555cf..feb24a4e73f 100644 --- a/spec/requests/api/members_spec.rb +++ b/spec/requests/api/members_spec.rb @@ -826,48 +826,20 @@ RSpec.describe API::Members, feature_category: :groups_and_projects do end end - context 'with admin_group_member FF disabled' do - before do - stub_feature_flags(admin_group_member: false) - end - - it_behaves_like 'POST /:source_type/:id/members', 'project' do - let(:source) { project } - end - - it_behaves_like 'POST /:source_type/:id/members', 'group' do - let(:source) { group } - end - - it_behaves_like 'PUT /:source_type/:id/members/:user_id', 'project' do - let(:source) { project } - end - - it_behaves_like 'PUT /:source_type/:id/members/:user_id', 'group' do - let(:source) { group } - end + it_behaves_like 'POST /:source_type/:id/members', 'project' do + let(:source) { project } end - context 'with admin_group_member FF enabled' do - before do - stub_feature_flags(admin_group_member: true) - end - - it_behaves_like 'POST /:source_type/:id/members', 'project' do - let(:source) { project } - end - - it_behaves_like 'POST /:source_type/:id/members', 'group' do - let(:source) { group } - end + it_behaves_like 'POST /:source_type/:id/members', 'group' do + let(:source) { group } + end - it_behaves_like 'PUT /:source_type/:id/members/:user_id', 'project' do - let(:source) { project } - end + it_behaves_like 'PUT /:source_type/:id/members/:user_id', 'project' do + let(:source) { project } + end - it_behaves_like 'PUT /:source_type/:id/members/:user_id', 'group' do - let(:source) { group } - end + it_behaves_like 'PUT /:source_type/:id/members/:user_id', 'group' do + let(:source) { group } end it_behaves_like 'DELETE /:source_type/:id/members/:user_id', 'project' do diff --git a/spec/requests/api/merge_request_approvals_spec.rb b/spec/requests/api/merge_request_approvals_spec.rb index a1d6abec97e..2de59750273 100644 --- a/spec/requests/api/merge_request_approvals_spec.rb +++ b/spec/requests/api/merge_request_approvals_spec.rb @@ -8,8 +8,6 @@ RSpec.describe API::MergeRequestApprovals, feature_category: :source_code_manage let_it_be(:bot) { create(:user, :project_bot) } let_it_be(:project) { create(:project, :public, :repository, creator: user, namespace: user.namespace) } let_it_be(:approver) { create :user } - let_it_be(:group) { create :group } - let(:merge_request) { create(:merge_request, :simple, author: user, source_project: project) } describe 'GET :id/merge_requests/:merge_request_iid/approvals' do @@ -87,6 +85,28 @@ RSpec.describe API::MergeRequestApprovals, feature_category: :source_code_manage expect(response).to have_gitlab_http_status(:created) end + + it 'calls MergeRequests::UpdateReviewerStateService' do + unapprover = create(:user) + + project.add_developer(approver) + project.add_developer(unapprover) + project.add_developer(create(:user)) + + create(:approval, user: approver, merge_request: merge_request) + create(:approval, user: unapprover, merge_request: merge_request) + + expect_next_instance_of( + MergeRequests::UpdateReviewerStateService, + project: project, current_user: unapprover + ) do |service| + expect(service).to receive(:execute).with(merge_request, "unreviewed") + end + + post api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/unapprove", unapprover) + + expect(response).to have_gitlab_http_status(:created) + end end end diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index 2cf8872cd40..6000fa29dc4 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -229,7 +229,7 @@ RSpec.describe API::MergeRequests, :aggregate_failures, feature_category: :sourc merge_request_closed.id, merge_request.id ]) - expect(json_response.last.keys).to match_array(%w(id iid title web_url created_at description project_id state updated_at)) + expect(json_response.last.keys).to match_array(%w[id iid title web_url created_at description project_id state updated_at]) expect(json_response.last['iid']).to eq(merge_request.iid) expect(json_response.last['title']).to eq(merge_request.title) expect(json_response.last).to have_key('web_url') @@ -2175,7 +2175,7 @@ RSpec.describe API::MergeRequests, :aggregate_failures, feature_category: :sourc expect(response).to have_gitlab_http_status(:created) expect(json_response['title']).to eq('Test merge_request') - expect(json_response['labels']).to eq(%w(label label2)) + expect(json_response['labels']).to eq(%w[label label2]) expect(json_response['milestone']['id']).to eq(milestone.id) expect(json_response['squash']).to be_truthy expect(json_response['force_remove_source_branch']).to be_falsy @@ -2187,11 +2187,11 @@ RSpec.describe API::MergeRequests, :aggregate_failures, feature_category: :sourc end it_behaves_like 'creates merge request with labels' do - let(:labels) { %w(label label2) } + let(:labels) { %w[label label2] } end it_behaves_like 'creates merge request with labels' do - let(:labels) { %w(label label2) } + let(:labels) { %w[label label2] } end it 'creates merge request with special label names' do diff --git a/spec/requests/api/metadata_spec.rb b/spec/requests/api/metadata_spec.rb index b81fe3f51b5..c8cee31db47 100644 --- a/spec/requests/api/metadata_spec.rb +++ b/spec/requests/api/metadata_spec.rb @@ -26,7 +26,7 @@ RSpec.describe API::Metadata, feature_category: :shared do let(:personal_access_token) { create(:personal_access_token, scopes: scopes) } context 'with api scope' do - let(:scopes) { %i(api) } + let(:scopes) { %i[api] } it 'returns the metadata information' do get api(endpoint, personal_access_token: personal_access_token) @@ -42,7 +42,7 @@ RSpec.describe API::Metadata, feature_category: :shared do end context 'with ai_features scope' do - let(:scopes) { %i(ai_features) } + let(:scopes) { %i[ai_features] } it 'returns the metadata information' do get api(endpoint, personal_access_token: personal_access_token) @@ -58,7 +58,7 @@ RSpec.describe API::Metadata, feature_category: :shared do end context 'with read_user scope' do - let(:scopes) { %i(read_user) } + let(:scopes) { %i[read_user] } it 'returns the metadata information' do get api(endpoint, personal_access_token: personal_access_token) @@ -74,7 +74,7 @@ RSpec.describe API::Metadata, feature_category: :shared do end context 'with neither api, ai_features nor read_user scope' do - let(:scopes) { %i(read_repository) } + let(:scopes) { %i[read_repository] } it 'returns authorization error' do get api(endpoint, personal_access_token: personal_access_token) diff --git a/spec/requests/api/ml/mlflow/model_versions_spec.rb b/spec/requests/api/ml/mlflow/model_versions_spec.rb new file mode 100644 index 00000000000..f59888ec70f --- /dev/null +++ b/spec/requests/api/ml/mlflow/model_versions_spec.rb @@ -0,0 +1,86 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe API::Ml::Mlflow::ModelVersions, feature_category: :mlops do + let_it_be(:project) { create(:project) } + let_it_be(:developer) { create(:user).tap { |u| project.add_developer(u) } } + let_it_be(:another_project) { build(:project).tap { |p| p.add_developer(developer) } } + + let_it_be(:name) { 'a-model-name' } + let_it_be(:version) { '0.0.1' } + let_it_be(:model) { create(:ml_models, project: project, name: name) } + let_it_be(:model_version) { create(:ml_model_versions, project: project, model: model, version: version) } + + let_it_be(:tokens) do + { + write: create(:personal_access_token, scopes: %w[read_api api], user: developer), + read: create(:personal_access_token, scopes: %w[read_api], user: developer), + no_access: create(:personal_access_token, scopes: %w[read_user], user: developer), + different_user: create(:personal_access_token, scopes: %w[read_api api], user: build(:user)) + } + end + + let(:current_user) { developer } + let(:access_token) { tokens[:write] } + let(:headers) { { 'Authorization' => "Bearer #{access_token.token}" } } + let(:project_id) { project.id } + let(:default_params) { {} } + let(:params) { default_params } + let(:request) { get api(route), params: params, headers: headers } + let(:json_response) { Gitlab::Json.parse(api_response.body) } + + subject(:api_response) do + request + response + end + + describe 'GET /projects/:id/ml/mlflow/api/2.0/mlflow/model_versions/get' do + let(:route) do + "/projects/#{project_id}/ml/mlflow/api/2.0/mlflow/model_versions/get?name=#{name}&version=#{version}" + end + + it 'returns the model version', :aggregate_failures do + is_expected.to have_gitlab_http_status(:ok) + expect(json_response['model_version']).not_to be_nil + expect(json_response['model_version']['name']).to eq(name) + expect(json_response['model_version']['version']).to eq(version) + end + + describe 'Error States' do + context 'when has access' do + context 'and model name in incorrect' do + let(:route) do + "/projects/#{project_id}/ml/mlflow/api/2.0/mlflow/model_versions/get?name=--&version=#{version}" + end + + it_behaves_like 'MLflow|Not Found - Resource Does Not Exist' + end + + context 'and version in incorrect' do + let(:route) do + "/projects/#{project_id}/ml/mlflow/api/2.0/mlflow/model_versions/get?name=#{name}&version=--" + end + + it_behaves_like 'MLflow|Not Found - Resource Does Not Exist' + end + + context 'when user lacks read_model_registry rights' do + before do + allow(Ability).to receive(:allowed?).and_call_original + allow(Ability).to receive(:allowed?) + .with(current_user, :read_model_registry, project) + .and_return(false) + end + + it "is Not Found" do + is_expected.to have_gitlab_http_status(:not_found) + end + end + end + + it_behaves_like 'MLflow|shared model registry error cases' + it_behaves_like 'MLflow|Requires read_api scope' + end + end +end diff --git a/spec/requests/api/ml/mlflow/registered_models_spec.rb b/spec/requests/api/ml/mlflow/registered_models_spec.rb new file mode 100644 index 00000000000..cd8b0a53ef3 --- /dev/null +++ b/spec/requests/api/ml/mlflow/registered_models_spec.rb @@ -0,0 +1,203 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe API::Ml::Mlflow::RegisteredModels, feature_category: :mlops do + let_it_be(:project) { create(:project) } + let_it_be(:developer) { create(:user).tap { |u| project.add_developer(u) } } + let_it_be(:model) do + create(:ml_models, :with_metadata, project: project) + end + + let_it_be(:tokens) do + { + write: create(:personal_access_token, scopes: %w[read_api api], user: developer), + read: create(:personal_access_token, scopes: %w[read_api], user: developer), + no_access: create(:personal_access_token, scopes: %w[read_user], user: developer), + different_user: create(:personal_access_token, scopes: %w[read_api api], user: build(:user)) + } + end + + let(:current_user) { developer } + let(:access_token) { tokens[:write] } + let(:headers) { { 'Authorization' => "Bearer #{access_token.token}" } } + let(:project_id) { project.id } + let(:default_params) { {} } + let(:params) { default_params } + let(:request) { get api(route), params: params, headers: headers } + let(:json_response) { Gitlab::Json.parse(api_response.body) } + + subject(:api_response) do + request + response + end + + describe 'GET /projects/:id/ml/mlflow/api/2.0/mlflow/registered-models/get' do + let(:model_name) { model.name } + let(:route) { "/projects/#{project_id}/ml/mlflow/api/2.0/mlflow/registered-models/get?name=#{model_name}" } + + it 'returns the model', :aggregate_failures do + is_expected.to have_gitlab_http_status(:ok) + is_expected.to match_response_schema('ml/get_model') + end + + describe 'Error States' do + context 'when has access' do + context 'and model does not exist' do + let(:model_name) { 'foo' } + + it_behaves_like 'MLflow|Not Found - Resource Does Not Exist' + end + + context 'and name is not passed' do + let(:route) { "/projects/#{project_id}/ml/mlflow/api/2.0/mlflow/registered-models/get" } + + it_behaves_like 'MLflow|Not Found - Resource Does Not Exist' + end + end + + it_behaves_like 'MLflow|shared model registry error cases' + it_behaves_like 'MLflow|Requires read_api scope' + end + end + + describe 'POST /projects/:id/ml/mlflow/api/2.0/mlflow/registered-models/create' do + let(:route) do + "/projects/#{project_id}/ml/mlflow/api/2.0/mlflow/registered-models/create" + end + + let(:params) { { name: 'my-model-name' } } + let(:request) { post api(route), params: params, headers: headers } + + it 'creates the model', :aggregate_failures do + is_expected.to have_gitlab_http_status(:ok) + expect(json_response).to include('registered_model') + end + + describe 'Error States' do + context 'when the model name is not passed' do + let(:params) { {} } + + it_behaves_like 'MLflow|Bad Request' + end + + context 'when the model name already exists' do + let(:existing_model) do + create(:ml_models, user: current_user, project: project) + end + + let(:params) { { name: existing_model.name } } + + it "is Bad Request", :aggregate_failures do + is_expected.to have_gitlab_http_status(:bad_request) + + expect(json_response).to include({ 'error_code' => 'RESOURCE_ALREADY_EXISTS' }) + end + end + + context 'when project does not exist' do + let(:route) { "/projects/#{non_existing_record_id}/ml/mlflow/api/2.0/mlflow/registered-models/create" } + + it "is Not Found", :aggregate_failures do + is_expected.to have_gitlab_http_status(:not_found) + + expect(json_response['message']).to eq('404 Project Not Found') + end + end + + # TODO: Ensure consisted error responses https://gitlab.com/gitlab-org/gitlab/-/issues/429731 + context 'when a duplicate tag name is supplied' do + let(:params) do + { name: 'my-model-name', tags: [{ key: 'key1', value: 'value1' }, { key: 'key1', value: 'value2' }] } + end + + it "creates the model with only the second tag", :aggregate_failures do + expect(json_response).to include({ 'error_code' => 'RESOURCE_ALREADY_EXISTS' }) + end + end + + # TODO: Ensure consisted error responses https://gitlab.com/gitlab-org/gitlab/-/issues/429731 + context 'when an empty tag name is supplied' do + let(:params) do + { name: 'my-model-name', tags: [{ key: '', value: 'value1' }, { key: 'key1', value: 'value2' }] } + end + + it "creates the model with only the second tag", :aggregate_failures do + expect(json_response).to include({ 'error_code' => 'RESOURCE_ALREADY_EXISTS' }) + end + end + + it_behaves_like 'MLflow|shared model registry error cases' + it_behaves_like 'MLflow|Requires api scope and write permission' + end + end + + describe 'PATCH /projects/:id/ml/mlflow/api/2.0/mlflow/registered-models/update' do + let(:model_name) { model.name } + let(:model_description) { 'updated model description' } + let(:params) { { name: model_name, description: model_description } } + let(:route) { "/projects/#{project_id}/ml/mlflow/api/2.0/mlflow/registered-models/update" } + let(:request) { patch api(route), params: params, headers: headers } + + it 'returns the updated model', :aggregate_failures do + is_expected.to have_gitlab_http_status(:ok) + is_expected.to match_response_schema('ml/update_model') + expect(json_response["registered_model"]["description"]).to eq(model_description) + end + + describe 'Error States' do + context 'when has access' do + context 'and model does not exist' do + let(:model_name) { 'foo' } + + it_behaves_like 'MLflow|Not Found - Resource Does Not Exist' + end + + context 'and name is not passed' do + let(:params) { { description: model_description } } + + it_behaves_like 'MLflow|Not Found - Resource Does Not Exist' + end + end + + it_behaves_like 'MLflow|shared model registry error cases' + it_behaves_like 'MLflow|Requires api scope and write permission' + end + end + + describe 'POST /projects/:id/ml/mlflow/api/2.0/mlflow/registered-models/get-latest-versions' do + let_it_be(:version1) { create(:ml_model_versions, model: model, created_at: 1.week.ago) } + let_it_be(:version2) { create(:ml_model_versions, model: model, created_at: 1.day.ago) } + + let(:model_name) { model.name } + let(:params) { { name: model_name } } + let(:route) { "/projects/#{project_id}/ml/mlflow/api/2.0/mlflow/registered-models/get-latest-versions" } + let(:request) { post api(route), params: params, headers: headers } + + it 'returns an array with the most recently created model version', :aggregate_failures do + is_expected.to have_gitlab_http_status(:ok) + is_expected.to match_response_schema('ml/get_latest_versions') + expect(json_response["model_versions"][0]["name"]).to eq(model_name) + expect(json_response["model_versions"][0]["version"]).to eq(version2.version) + end + + describe 'Error States' do + context 'when has access' do + context 'and model does not exist' do + let(:model_name) { 'foo' } + + it_behaves_like 'MLflow|Not Found - Resource Does Not Exist' + end + + context 'and name is not passed' do + let(:params) { {} } + + it_behaves_like 'MLflow|Not Found - Resource Does Not Exist' + end + end + + it_behaves_like 'MLflow|shared model registry error cases' + it_behaves_like 'MLflow|Requires read_api scope' + end + end +end diff --git a/spec/requests/api/npm_project_packages_spec.rb b/spec/requests/api/npm_project_packages_spec.rb index 340420e46e0..b5f38698857 100644 --- a/spec/requests/api/npm_project_packages_spec.rb +++ b/spec/requests/api/npm_project_packages_spec.rb @@ -196,7 +196,7 @@ RSpec.describe API::NpmProjectPackages, feature_category: :package_registry do context 'with a job token for a different user' do let_it_be(:other_user) { create(:user) } - let_it_be_with_reload(:other_job) { create(:ci_build, :running, user: other_user) } + let_it_be_with_reload(:other_job) { create(:ci_build, :running, user: other_user, project: project) } let(:headers) { build_token_auth_header(other_job.token) } @@ -245,7 +245,7 @@ RSpec.describe API::NpmProjectPackages, feature_category: :package_registry do context 'with a job token for a different user' do let_it_be(:other_user) { create(:user) } - let_it_be_with_reload(:other_job) { create(:ci_build, :running, user: other_user) } + let_it_be_with_reload(:other_job) { create(:ci_build, :running, user: other_user, project: project) } let(:headers) { build_token_auth_header(other_job.token) } diff --git a/spec/requests/api/pages/pages_spec.rb b/spec/requests/api/pages/pages_spec.rb index aa1869eaa84..74dd1ed4f2b 100644 --- a/spec/requests/api/pages/pages_spec.rb +++ b/spec/requests/api/pages/pages_spec.rb @@ -9,7 +9,6 @@ RSpec.describe API::Pages, feature_category: :pages do before do project.add_maintainer(user) - project.mark_pages_as_deployed end describe 'DELETE /projects/:id/pages' do @@ -17,7 +16,7 @@ RSpec.describe API::Pages, feature_category: :pages do it_behaves_like 'DELETE request permissions for admin mode' do before do - allow(Gitlab.config.pages).to receive(:enabled).and_return(true) + stub_pages_setting(enabled: true) end let(:succes_status_code) { :no_content } @@ -25,7 +24,7 @@ RSpec.describe API::Pages, feature_category: :pages do context 'when Pages is disabled' do before do - allow(Gitlab.config.pages).to receive(:enabled).and_return(false) + stub_pages_setting(enabled: false) end it_behaves_like '404 response' do @@ -35,7 +34,7 @@ RSpec.describe API::Pages, feature_category: :pages do context 'when Pages is enabled' do before do - allow(Gitlab.config.pages).to receive(:enabled).and_return(true) + stub_pages_setting(enabled: true) end context 'when Pages are deployed' do @@ -48,15 +47,11 @@ RSpec.describe API::Pages, feature_category: :pages do it 'removes the pages' do delete api(path, admin, admin_mode: true) - expect(project.reload.pages_metadatum.deployed?).to be(false) + expect(project.reload.pages_deployed?).to be(false) end end context 'when pages are not deployed' do - before do - project.mark_pages_as_not_deployed - end - it 'returns 204' do delete api(path, admin, admin_mode: true) diff --git a/spec/requests/api/personal_access_tokens_spec.rb b/spec/requests/api/personal_access_tokens_spec.rb index 166768ea605..a1d29c4a935 100644 --- a/spec/requests/api/personal_access_tokens_spec.rb +++ b/spec/requests/api/personal_access_tokens_spec.rb @@ -461,6 +461,18 @@ RSpec.describe API::PersonalAccessTokens, :aggregate_failures, feature_category: expect(json_response['expires_at']).to eq((Date.today + 1.week).to_s) end + context 'when expiry is defined' do + it "rotates user's own token", :freeze_time do + expiry_date = Date.today + 1.month + + post(api(path, token.user), params: { expires_at: expiry_date }) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['token']).not_to eq(token.token) + expect(json_response['expires_at']).to eq(expiry_date.to_s) + end + end + context 'without permission' do it 'returns an error message' do another_user = create(:user) diff --git a/spec/requests/api/project_attributes.yml b/spec/requests/api/project_attributes.yml index ec98df22af7..165ea7bf66e 100644 --- a/spec/requests/api/project_attributes.yml +++ b/spec/requests/api/project_attributes.yml @@ -98,6 +98,7 @@ ci_cd_settings: - merge_pipelines_enabled - auto_rollback_enabled - inbound_job_token_scope_enabled + - restrict_pipeline_cancellation_role remapped_attributes: default_git_depth: ci_default_git_depth forward_deployment_enabled: ci_forward_deployment_enabled diff --git a/spec/requests/api/project_container_repositories_spec.rb b/spec/requests/api/project_container_repositories_spec.rb index f51b94bb78e..7797e8e9402 100644 --- a/spec/requests/api/project_container_repositories_spec.rb +++ b/spec/requests/api/project_container_repositories_spec.rb @@ -168,7 +168,7 @@ RSpec.describe API::ProjectContainerRepositories, feature_category: :container_r let(:api_user) { reporter } before do - stub_container_registry_tags(repository: root_repository.path, tags: %w(rootA latest)) + stub_container_registry_tags(repository: root_repository.path, tags: %w[rootA latest]) end it_behaves_like 'a package tracking event', described_class.name, 'list_tags' @@ -177,7 +177,7 @@ RSpec.describe API::ProjectContainerRepositories, feature_category: :container_r subject expect(json_response.length).to eq(2) - expect(json_response.map { |repository| repository['name'] }).to eq %w(latest rootA) + expect(json_response.map { |repository| repository['name'] }).to eq %w[latest rootA] end it 'returns a matching schema' do @@ -362,7 +362,7 @@ RSpec.describe API::ProjectContainerRepositories, feature_category: :container_r let(:api_user) { reporter } before do - stub_container_registry_tags(repository: root_repository.path, tags: %w(rootA), with_manifest: true) + stub_container_registry_tags(repository: root_repository.path, tags: %w[rootA], with_manifest: true) end it 'returns a details of tag' do @@ -408,7 +408,7 @@ RSpec.describe API::ProjectContainerRepositories, feature_category: :container_r context 'when there are multiple tags' do before do - stub_container_registry_tags(repository: root_repository.path, tags: %w(rootA rootB), with_manifest: true) + stub_container_registry_tags(repository: root_repository.path, tags: %w[rootA rootB], with_manifest: true) end it 'properly removes tag' do @@ -427,7 +427,7 @@ RSpec.describe API::ProjectContainerRepositories, feature_category: :container_r context 'when there\'s only one tag' do before do - stub_container_registry_tags(repository: root_repository.path, tags: %w(rootA), with_manifest: true) + stub_container_registry_tags(repository: root_repository.path, tags: %w[rootA], with_manifest: true) end it 'properly removes tag' do diff --git a/spec/requests/api/project_packages_spec.rb b/spec/requests/api/project_packages_spec.rb index 219c748c9a6..2ac9a7d97f1 100644 --- a/spec/requests/api/project_packages_spec.rb +++ b/spec/requests/api/project_packages_spec.rb @@ -197,6 +197,26 @@ RSpec.describe API::ProjectPackages, feature_category: :package_registry do end end + context 'filtering on package_version' do + include_context 'package filter context' + + it 'returns the versioned package' do + url = package_filter_url(:version, '2.0.4') + get api(url, user) + + expect(json_response.length).to eq(1) + expect(json_response.first['version']).to eq(package2.version) + end + + it 'include_versionless has no effect' do + url = "/projects/#{project.id}/packages?package_version=2.0.4&include_versionless=true" + get api(url, user) + + expect(json_response.length).to eq(1) + expect(json_response.first['version']).to eq(package2.version) + end + end + it_behaves_like 'with versionless packages' it_behaves_like 'with status param' it_behaves_like 'does not cause n^2 queries' diff --git a/spec/requests/api/project_repository_storage_moves_spec.rb b/spec/requests/api/project_repository_storage_moves_spec.rb index 96ed3042d00..7b5dc0d5ef8 100644 --- a/spec/requests/api/project_repository_storage_moves_spec.rb +++ b/spec/requests/api/project_repository_storage_moves_spec.rb @@ -8,5 +8,29 @@ RSpec.describe API::ProjectRepositoryStorageMoves, feature_category: :gitaly do let_it_be(:storage_move) { create(:project_repository_storage_move, :scheduled, container: container) } let(:repository_storage_move_factory) { :project_repository_storage_move } let(:bulk_worker_klass) { Projects::ScheduleBulkRepositoryShardMovesWorker } + + context 'when project is hidden' do + let_it_be(:container) { create(:project, :hidden) } + let_it_be(:storage_move) { create(:project_repository_storage_move, :scheduled, container: container) } + + it_behaves_like 'get single container repository storage move' do + let(:container_id) { container.id } + let(:url) { "/projects/#{container_id}/repository_storage_moves/#{repository_storage_move_id}" } + end + + it_behaves_like 'post single container repository storage move' + end + + context 'when project is pending delete' do + let_it_be(:container) { create(:project, pending_delete: true) } + let_it_be(:storage_move) { create(:project_repository_storage_move, :scheduled, container: container) } + + it_behaves_like 'get single container repository storage move' do + let(:container_id) { container.id } + let(:url) { "/projects/#{container_id}/repository_storage_moves/#{repository_storage_move_id}" } + end + + it_behaves_like 'post single container repository storage move' + end end end diff --git a/spec/requests/api/project_templates_spec.rb b/spec/requests/api/project_templates_spec.rb index e1d156afd54..1987d70633b 100644 --- a/spec/requests/api/project_templates_spec.rb +++ b/spec/requests/api/project_templates_spec.rb @@ -69,7 +69,7 @@ RSpec.describe API::ProjectTemplates, feature_category: :source_code_management expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers expect(response).to match_response_schema('public_api/v4/template_list') - expect(json_response.map { |t| t['key'] }).to match_array(%w(bug feature_proposal template_test)) + expect(json_response.map { |t| t['key'] }).to match_array(%w[bug feature_proposal template_test]) end it 'returns merge request templates' do @@ -78,7 +78,7 @@ RSpec.describe API::ProjectTemplates, feature_category: :source_code_management expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers expect(response).to match_response_schema('public_api/v4/template_list') - expect(json_response.map { |t| t['key'] }).to match_array(%w(bug feature_proposal template_test)) + expect(json_response.map { |t| t['key'] }).to match_array(%w[bug feature_proposal template_test]) end it 'returns 400 for an unknown template type' do diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 64e010aa50f..e9319d514aa 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -286,6 +286,32 @@ RSpec.describe API::Projects, :aggregate_failures, feature_category: :groups_and expect(json_response.map { |p| p['id'] }).not_to include(project.id) end + context 'when user requests pending_delete projects' do + before do + project.update!(pending_delete: true) + end + + let(:params) { { include_pending_delete: true } } + + it 'does not return projects marked for deletion' do + get api(path, user), params: params + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to be_an Array + expect(json_response.map { |p| p['id'] }).not_to include(project.id) + end + + context 'when user is an admin' do + it 'returns projects marked for deletion' do + get api(path, admin, admin_mode: true), params: params + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to be_an Array + expect(json_response.map { |p| p['id'] }).to include(project.id) + end + end + end + it 'does not include open_issues_count if issues are disabled' do project.project_feature.update_attribute(:issues_access_level, ProjectFeature::DISABLED) @@ -299,7 +325,7 @@ RSpec.describe API::Projects, :aggregate_failures, feature_category: :groups_and context 'filter by topic (column topic_list)' do before do - project.update!(topic_list: %w(ruby javascript)) + project.update!(topic_list: %w[ruby javascript]) end it 'returns no projects' do @@ -868,7 +894,7 @@ RSpec.describe API::Projects, :aggregate_failures, feature_category: :groups_and context 'sorting' do context 'by project statistics' do - %w(repository_size storage_size wiki_size packages_size).each do |order_by| + %w[repository_size storage_size wiki_size packages_size].each do |order_by| context "sorting by #{order_by}" do before do ProjectStatistics.update_all(order_by => 100) @@ -1400,13 +1426,14 @@ RSpec.describe API::Projects, :aggregate_failures, feature_category: :groups_and it 'disallows creating a project with an import_url that is not reachable' do url = 'http://example.com' endpoint_url = "#{url}/info/refs?service=git-upload-pack" - stub_full_request(endpoint_url, method: :get).to_return({ status: 301, body: '', headers: nil }) + error_response = { status: 301, body: '', headers: nil } + stub_full_request(endpoint_url, method: :get).to_return(error_response) project_params = { import_url: url, path: 'path-project-Foo', name: 'Foo Project' } expect { post api(path, user), params: project_params }.not_to change { Project.count } expect(response).to have_gitlab_http_status(:unprocessable_entity) - expect(json_response['message']).to eq("#{url} is not a valid HTTP Git repository") + expect(json_response['message']).to eq("#{url} endpoint error: #{error_response[:status]}") end it 'creates a project with an import_url that is valid' do @@ -2533,7 +2560,7 @@ RSpec.describe API::Projects, :aggregate_failures, feature_category: :groups_and context 'and the project has a private repository' do let(:project) { create(:project, :repository, :public, :repository_private) } - let(:protected_attributes) { %w(default_branch ci_config_path) } + let(:protected_attributes) { %w[default_branch ci_config_path] } it 'hides protected attributes of private repositories if user is not a member' do get api(path, user) @@ -2782,10 +2809,20 @@ RSpec.describe API::Projects, :aggregate_failures, feature_category: :groups_and expect(json_response['shared_with_groups'][0]['expires_at']).to eq(expires_at.to_s) end - it 'returns a project by path name' do - get api(path, user) - expect(response).to have_gitlab_http_status(:ok) - expect(json_response['name']).to eq(project.name) + context 'when path name is specified' do + it 'returns a project' do + get api("/projects/#{CGI.escape(project.full_path)}", user) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['name']).to eq(project.name) + end + + it 'returns a project using case-insensitive search' do + get api("/projects/#{CGI.escape(project.full_path.swapcase)}", user) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['name']).to eq(project.name) + end end context 'when a project is moved' do @@ -3688,6 +3725,16 @@ RSpec.describe API::Projects, :aggregate_failures, feature_category: :groups_and it_behaves_like '412 response' do subject(:request) { api("/projects/#{project.id}/share/#{group.id}", user) } end + + it "returns an error when link is not destroyed" do + allow(::Projects::GroupLinks::DestroyService).to receive_message_chain(:new, :execute) + .and_return(ServiceResponse.error(message: '404 Not Found', reason: :not_found)) + + delete api("/projects/#{project.id}/share/#{group.id}", user) + + expect(response).to have_gitlab_http_status(:not_found) + expect(json_response['message']).to eq '404 Not Found' + end end it 'returns a 400 when group id is not an integer' do @@ -3893,7 +3940,7 @@ RSpec.describe API::Projects, :aggregate_failures, feature_category: :groups_and expect(Project.find_by(path: project[:path]).analytics_access_level).to eq(ProjectFeature::PRIVATE) end - %i(releases_access_level environments_access_level feature_flags_access_level infrastructure_access_level monitor_access_level model_experiments_access_level).each do |field| + %i[releases_access_level environments_access_level feature_flags_access_level infrastructure_access_level monitor_access_level model_experiments_access_level].each do |field| it "sets #{field}" do put api(path, user), params: { field => 'private' } diff --git a/spec/requests/api/pypi_packages_spec.rb b/spec/requests/api/pypi_packages_spec.rb index 0b2641b062c..9305155d285 100644 --- a/spec/requests/api/pypi_packages_spec.rb +++ b/spec/requests/api/pypi_packages_spec.rb @@ -207,7 +207,22 @@ RSpec.describe API::PypiPackages, feature_category: :package_registry do let(:url) { "/projects/#{project.id}/packages/pypi" } let(:headers) { {} } let(:requires_python) { '>=3.7' } - let(:base_params) { { requires_python: requires_python, version: '1.0.0', name: 'sample-project', sha256_digest: '1' * 64, md5_digest: '1' * 32 } } + let(:base_params) do + { + requires_python: requires_python, + version: '1.0.0', + name: 'sample-project', + sha256_digest: '1' * 64, + md5_digest: '1' * 32, + metadata_version: '2.3', + author_email: 'cschultz@example.com, snoopy@peanuts.com', + description: 'Example description', + description_content_type: 'text/plain', + summary: 'A module for collecting votes from beagles.', + keywords: 'dog,puppy,voting,election' + } + end + let(:params) { base_params.merge(content: temp_file(file_name)) } let(:send_rewritten_field) { true } let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace, user: user, property: 'i_package_pypi_user' } } diff --git a/spec/requests/api/releases_spec.rb b/spec/requests/api/releases_spec.rb index a018b91019b..493dc4e72c6 100644 --- a/spec/requests/api/releases_spec.rb +++ b/spec/requests/api/releases_spec.rb @@ -1492,7 +1492,7 @@ RSpec.describe API::Releases, :aggregate_failures, feature_category: :release_or subject expect(response).to have_gitlab_http_status(:ok) - expect(returned_milestones).to match_array(%w(milestone2 milestone3)) + expect(returned_milestones).to match_array(%w[milestone2 milestone3]) end end diff --git a/spec/requests/api/repositories_spec.rb b/spec/requests/api/repositories_spec.rb index 22239f1d23f..f38a120cc74 100644 --- a/spec/requests/api/repositories_spec.rb +++ b/spec/requests/api/repositories_spec.rb @@ -742,7 +742,7 @@ RSpec.describe API::Repositories, feature_category: :source_code_management do describe 'GET :id/repository/merge_base' do let(:refs) do - %w(304d257dcb821665ab5110318fc58a007bd104ed 0031876facac3f2b2702a0e53a26e89939a42209 570e7b2abdd848b95f2f578043fc23bd6f6fd24d) + %w[304d257dcb821665ab5110318fc58a007bd104ed 0031876facac3f2b2702a0e53a26e89939a42209 570e7b2abdd848b95f2f578043fc23bd6f6fd24d] end subject(:request) do @@ -786,7 +786,7 @@ RSpec.describe API::Repositories, feature_category: :source_code_management do context 'when passing refs that do not exist' do it_behaves_like '400 response' do - let(:refs) { %w(304d257dcb821665ab5110318fc58a007bd104ed missing) } + let(:refs) { %w[304d257dcb821665ab5110318fc58a007bd104ed missing] } let(:current_user) { user } let(:message) { 'Could not find ref: missing' } end @@ -801,7 +801,7 @@ RSpec.describe API::Repositories, feature_category: :source_code_management do end context 'when not enough refs are passed' do - let(:refs) { %w(only-one) } + let(:refs) { %w[only-one] } let(:current_user) { user } it 'renders a bad request error' do diff --git a/spec/requests/api/resource_access_tokens_spec.rb b/spec/requests/api/resource_access_tokens_spec.rb index dcb6572d413..01e02651a64 100644 --- a/spec/requests/api/resource_access_tokens_spec.rb +++ b/spec/requests/api/resource_access_tokens_spec.rb @@ -477,6 +477,7 @@ RSpec.describe API::ResourceAccessTokens, feature_category: :system_access do let_it_be(:token) { create(:personal_access_token, user: project_bot) } let_it_be(:resource_id) { resource.id } let_it_be(:token_id) { token.id } + let(:params) { {} } let(:path) { "/#{source_type}s/#{resource_id}/access_tokens/#{token_id}/rotate" } @@ -485,7 +486,7 @@ RSpec.describe API::ResourceAccessTokens, feature_category: :system_access do resource.add_owner(user) end - subject(:rotate_token) { post api(path, user) } + subject(:rotate_token) { post(api(path, user), params: params) } it "allows owner to rotate token", :freeze_time do rotate_token @@ -495,6 +496,19 @@ RSpec.describe API::ResourceAccessTokens, feature_category: :system_access do expect(json_response['expires_at']).to eq((Date.today + 1.week).to_s) end + context 'when expiry is defined' do + let(:expiry_date) { Date.today + 1.month } + let(:params) { { expires_at: expiry_date } } + + it "allows owner to rotate token", :freeze_time do + rotate_token + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['token']).not_to eq(token.token) + expect(json_response['expires_at']).to eq(expiry_date.to_s) + end + end + context 'without permission' do it 'returns an error message' do another_user = create(:user) diff --git a/spec/requests/api/search_spec.rb b/spec/requests/api/search_spec.rb index 6a57cf52466..4733fdafbfb 100644 --- a/spec/requests/api/search_spec.rb +++ b/spec/requests/api/search_spec.rb @@ -140,14 +140,14 @@ RSpec.describe API::Search, :clean_gitlab_redis_rate_limiting, feature_category: end context 'when DB timeouts occur from global searches', :aggregate_failures do - %w( + %w[ issues merge_requests milestones projects snippet_titles users - ).each do |scope| + ].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' } diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb index 2fdcf710471..5656fda7684 100644 --- a/spec/requests/api/settings_spec.rb +++ b/spec/requests/api/settings_spec.rb @@ -79,7 +79,7 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting, featu expect(json_response['slack_app_secret']).to be_nil expect(json_response['slack_app_signing_secret']).to be_nil expect(json_response['slack_app_verification_token']).to be_nil - expect(json_response['valid_runner_registrars']).to match_array(%w(project group)) + expect(json_response['valid_runner_registrars']).to match_array(%w[project group]) expect(json_response['ci_max_includes']).to eq(150) expect(json_response['allow_account_deletion']).to eq(true) expect(json_response['gitlab_shell_operation_limit']).to eq(600) @@ -261,7 +261,7 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting, featu expect(json_response['max_decompressed_archive_size']).to eq(20000) expect(json_response['max_terraform_state_size_bytes']).to eq(1_000) expect(json_response['disabled_oauth_sign_in_sources']).to eq([]) - expect(json_response['import_sources']).to match_array(%w(github bitbucket)) + expect(json_response['import_sources']).to match_array(%w[github bitbucket]) expect(json_response['wiki_page_max_content_bytes']).to eq(12345) expect(json_response['wiki_asciidoc_allow_uri_includes']).to be(true) expect(json_response['personal_access_token_prefix']).to eq("GL-") @@ -418,12 +418,12 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting, featu end it 'does not allow unrestricted key lengths' do - types = %w(dsa_key_restriction + types = %w[dsa_key_restriction ecdsa_key_restriction ecdsa_sk_key_restriction ed25519_key_restriction ed25519_sk_key_restriction - rsa_key_restriction) + rsa_key_restriction] types.each do |type| put api("/application/settings", admin), params: { type => 0 } @@ -519,7 +519,7 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting, featu context 'EKS integration settings' do let(:attribute_names) { settings.keys.map(&:to_s) } - let(:sensitive_attributes) { %w(eks_secret_access_key) } + let(:sensitive_attributes) { %w[eks_secret_access_key] } let(:exposed_attributes) { attribute_names - sensitive_attributes } let(:settings) do diff --git a/spec/requests/api/tags_spec.rb b/spec/requests/api/tags_spec.rb index 604631bbf7f..6bbd43bfc14 100644 --- a/spec/requests/api/tags_spec.rb +++ b/spec/requests/api/tags_spec.rb @@ -206,7 +206,7 @@ RSpec.describe API::Tags, feature_category: :source_code_management do expect(response).to match_response_schema('public_api/v4/tags') expect(response.headers).to include('Link') tag_names = json_response.map { |x| x['name'] } - expect(tag_names).to match_array(%w(v1.1.0 v1.1.1)) + expect(tag_names).to match_array(%w[v1.1.0 v1.1.1]) end end diff --git a/spec/requests/api/task_completion_status_spec.rb b/spec/requests/api/task_completion_status_spec.rb index c46d6954da3..ae48b0c18cd 100644 --- a/spec/requests/api/task_completion_status_spec.rb +++ b/spec/requests/api/task_completion_status_spec.rb @@ -21,30 +21,30 @@ RSpec.describe 'task completion status response', features: :team_planning do expected_completed_count: 0 }, { - description: %{- [ ] task 1 - - [x] task 2 }, + description: %(- [ ] task 1 + - [x] task 2 ), expected_count: 2, expected_completed_count: 1 }, { - description: %{- [ ] task 1 - - [ ] task 2 }, + description: %(- [ ] task 1 + - [ ] task 2 ), expected_count: 2, expected_completed_count: 0 }, { - description: %{- [x] task 1 - - [x] task 2 }, + description: %(- [x] task 1 + - [x] task 2 ), expected_count: 2, expected_completed_count: 2 }, { - description: %{- [ ] task 1}, + description: %(- [ ] task 1), expected_count: 1, expected_completed_count: 0 }, { - description: %{- [x] task 1}, + description: %(- [x] task 1), expected_count: 1, expected_completed_count: 1 } diff --git a/spec/requests/api/unleash_spec.rb b/spec/requests/api/unleash_spec.rb index 75b26b98228..050be3ae8aa 100644 --- a/spec/requests/api/unleash_spec.rb +++ b/spec/requests/api/unleash_spec.rb @@ -96,7 +96,7 @@ RSpec.describe API::Unleash, feature_category: :feature_flags do end end - %w(/feature_flags/unleash/:project_id/features /feature_flags/unleash/:project_id/client/features).each do |features_endpoint| + %w[/feature_flags/unleash/:project_id/features /feature_flags/unleash/:project_id/client/features].each do |features_endpoint| describe "GET #{features_endpoint}", :use_clean_rails_redis_caching do let(:features_url) { features_endpoint.sub(':project_id', project_id.to_s) } let(:client) { create(:operations_feature_flags_client, project: project) } diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index 7da44266064..76fe72efc64 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -5,6 +5,7 @@ require 'spec_helper' RSpec.describe API::Users, :aggregate_failures, feature_category: :user_profile do include WorkhorseHelpers include KeysetPaginationHelpers + include CryptoHelpers let_it_be(:admin) { create(:admin) } let_it_be(:user, reload: true) { create(:user, username: 'user.withdot') } @@ -226,6 +227,19 @@ RSpec.describe API::Users, :aggregate_failures, feature_category: :user_profile end end + context 'with search parameter' do + let_it_be(:first_user) { create(:user, username: 'a-user') } + let_it_be(:second_user) { create(:user, username: 'a-user2') } + + it 'prioritizes username match' do + get api(path, user, admin_mode: true), params: { search: first_user.username } + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response.first['username']).to eq('a-user') + expect(json_response.second['username']).to eq('a-user2') + end + end + context 'N+1 queries' do before do create_list(:user, 2) @@ -1983,17 +1997,23 @@ RSpec.describe API::Users, :aggregate_failures, feature_category: :user_profile end describe "PUT /user/:id/credit_card_validation" do - let(:credit_card_validated_time) { Time.utc(2020, 1, 1) } + let(:network) { 'American Express' } + let(:holder_name) { 'John Smith' } + let(:last_digits) { '1111' } let(:expiration_year) { Date.today.year + 10 } + let(:expiration_month) { 1 } + let(:expiration_date) { Date.new(expiration_year, expiration_month, -1) } + let(:credit_card_validated_at) { Time.utc(2020, 1, 1) } + let(:path) { "/user/#{user.id}/credit_card_validation" } let(:params) do { - credit_card_validated_at: credit_card_validated_time, + credit_card_validated_at: credit_card_validated_at, credit_card_expiration_year: expiration_year, - credit_card_expiration_month: 1, - credit_card_holder_name: 'John Smith', - credit_card_type: 'AmericanExpress', - credit_card_mask_number: '1111' + credit_card_expiration_month: expiration_month, + credit_card_holder_name: holder_name, + credit_card_type: network, + credit_card_mask_number: last_digits } end @@ -2023,11 +2043,11 @@ RSpec.describe API::Users, :aggregate_failures, feature_category: :user_profile expect(response).to have_gitlab_http_status(:ok) expect(user.credit_card_validation).to have_attributes( - credit_card_validated_at: credit_card_validated_time, - expiration_date: Date.new(expiration_year, 1, 31), - last_digits: 1111, - network: 'AmericanExpress', - holder_name: 'John Smith' + credit_card_validated_at: credit_card_validated_at, + network_hash: sha256(network.downcase), + holder_name_hash: sha256(holder_name.downcase), + last_digits_hash: sha256(last_digits), + expiration_date_hash: sha256(expiration_date.to_s) ) end @@ -4519,7 +4539,7 @@ RSpec.describe API::Users, :aggregate_failures, feature_category: :user_profile describe 'POST /users/:user_id/personal_access_tokens' do let(:name) { 'new pat' } let(:expires_at) { 3.days.from_now.to_date.to_s } - let(:scopes) { %w(api read_user) } + let(:scopes) { %w[api read_user] } let(:path) { "/users/#{user.id}/personal_access_tokens" } let(:params) { { name: name, scopes: scopes, expires_at: expires_at } } diff --git a/spec/requests/api/v3/github_spec.rb b/spec/requests/api/v3/github_spec.rb deleted file mode 100644 index fbda291e901..00000000000 --- a/spec/requests/api/v3/github_spec.rb +++ /dev/null @@ -1,721 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe API::V3::Github, :aggregate_failures, feature_category: :integrations do - let_it_be(:user) { create(:user) } - let_it_be(:unauthorized_user) { create(:user) } - let_it_be(:admin) { create(:user, :admin) } - let_it_be_with_reload(:project) { create(:project, :repository, creator: user) } - - before do - project.add_maintainer(user) if user - end - - describe 'GET /orgs/:namespace/repos' do - let_it_be(:group) { create(:group) } - - it_behaves_like 'a GitHub Enterprise Jira DVCS reversible end of life endpoint' do - subject do - jira_get v3_api("/orgs/#{group.path}/repos", user) - end - end - - it 'logs when the endpoint is hit and `jira_dvcs_end_of_life_amnesty` is enabled' do - expect(Gitlab::JsonLogger).to receive(:info).with( - including( - namespace: group.path, - user_id: user.id, - message: 'Deprecated Jira DVCS endpoint request' - ) - ) - - jira_get v3_api("/orgs/#{group.path}/repos", user) - - stub_feature_flags(jira_dvcs_end_of_life_amnesty: false) - - expect(Gitlab::JsonLogger).not_to receive(:info) - - jira_get v3_api("/orgs/#{group.path}/repos", user) - end - - it 'returns an empty array' do - jira_get v3_api("/orgs/#{group.path}/repos", user) - - expect(response).to have_gitlab_http_status(:ok) - expect(json_response).to eq([]) - end - - it 'returns 200 when namespace path include a dot' do - group = create(:group, path: 'foo.bar') - - jira_get v3_api("/orgs/#{group.path}/repos", user) - - expect(response).to have_gitlab_http_status(:ok) - end - end - - describe 'GET /user/repos' do - it_behaves_like 'a GitHub Enterprise Jira DVCS reversible end of life endpoint' do - subject { jira_get v3_api('/user/repos', user) } - end - - it 'returns an empty array' do - jira_get v3_api('/user/repos', user) - - expect(response).to have_gitlab_http_status(:ok) - expect(json_response).to eq([]) - end - end - - shared_examples_for 'Jira-specific mimicked GitHub endpoints' do - describe 'GET /.../issues/:id/comments' do - let(:merge_request) do - create(:merge_request, source_project: project, target_project: project) - end - - let!(:note) do - create(:note, project: project, noteable: merge_request) - end - - context 'when user has access to the merge request' do - it 'returns an array of notes' do - jira_get v3_api("/repos/#{path}/issues/#{merge_request.id}/comments", user) - - expect(response).to have_gitlab_http_status(:ok) - expect(json_response).to be_an(Array) - expect(json_response.size).to eq(1) - end - end - - context 'when user has no access to the merge request' do - let(:project) { create(:project, :private) } - - before do - project.add_guest(user) - end - - it 'returns 404' do - jira_get v3_api("/repos/#{path}/issues/#{merge_request.id}/comments", user) - - expect(response).to have_gitlab_http_status(:not_found) - end - end - end - - describe 'GET /.../pulls/:id/commits' do - it 'returns an empty array' do - jira_get v3_api("/repos/#{path}/pulls/xpto/commits", user) - - expect(response).to have_gitlab_http_status(:ok) - expect(json_response).to eq([]) - end - end - - describe 'GET /.../pulls/:id/comments' do - it 'returns an empty array' do - jira_get v3_api("/repos/#{path}/pulls/xpto/comments", user) - - expect(response).to have_gitlab_http_status(:ok) - expect(json_response).to eq([]) - end - end - end - - # Here we test that using /-/jira as namespace/project still works, - # since that is how old Jira setups will talk to us - context 'old /-/jira endpoints' do - it_behaves_like 'Jira-specific mimicked GitHub endpoints' do - let(:path) { '-/jira' } - end - - it 'returns an empty Array for events' do - jira_get v3_api('/repos/-/jira/events', user) - - expect(response).to have_gitlab_http_status(:ok) - expect(json_response).to eq([]) - end - end - - context 'new :namespace/:project jira endpoints' do - it_behaves_like 'Jira-specific mimicked GitHub endpoints' do - let(:path) { "#{project.namespace.path}/#{project.path}" } - end - - describe 'GET /users/:username' do - let!(:user1) { create(:user, username: 'jane.porter') } - - it_behaves_like 'a GitHub Enterprise Jira DVCS reversible end of life endpoint' do - subject { jira_get v3_api("/users/#{user.username}", user) } - end - - context 'user exists' do - it 'responds with the expected user' do - jira_get v3_api("/users/#{user.username}", user) - - expect(response).to have_gitlab_http_status(:ok) - expect(response).to match_response_schema('entities/github/user') - end - end - - context 'user does not exist' do - it 'responds with the expected status' do - jira_get v3_api('/users/unknown_user_name', user) - - expect(response).to have_gitlab_http_status(:not_found) - end - end - - context 'no rights to request user lists' do - before do - expect(Ability).to receive(:allowed?).with(unauthorized_user, :read_users_list, :global).and_return(false) - expect(Ability).to receive(:allowed?).at_least(:once).and_call_original - end - - it 'responds with forbidden' do - jira_get v3_api("/users/#{user.username}", unauthorized_user) - - expect(response).to have_gitlab_http_status(:forbidden) - end - end - end - - describe 'GET events' do - include ProjectForksHelper - - let(:group) { create(:group) } - let(:project) { create(:project, :empty_repo, path: 'project.with.dot', group: group) } - let(:events_path) { "/repos/#{group.path}/#{project.path}/events" } - - it_behaves_like 'a GitHub Enterprise Jira DVCS reversible end of life endpoint' do - subject { jira_get v3_api(events_path, user) } - end - - context 'if there are no merge requests' do - it 'returns an empty array' do - jira_get v3_api(events_path, user) - - expect(response).to have_gitlab_http_status(:ok) - expect(json_response).to eq([]) - end - end - - context 'if there is a merge request' do - let!(:merge_request) { create(:merge_request, source_project: project, target_project: project, author: user) } - - it 'returns an event' do - jira_get v3_api(events_path, user) - - expect(response).to have_gitlab_http_status(:ok) - expect(json_response).to be_an(Array) - expect(json_response.size).to eq(1) - end - end - - it 'avoids N+1 queries' do - create(:merge_request, source_project: project) - source_project = fork_project(project, nil, repository: true) - - control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) { jira_get v3_api(events_path, user) }.count - - create_list(:merge_request, 2, :unique_branches, source_project: source_project, target_project: project) - - expect { jira_get v3_api(events_path, user) }.not_to exceed_all_query_limit(control_count) - end - - context 'if there are more merge requests' do - let!(:merge_request) { create(:merge_request, id: 10000, source_project: project, target_project: project, author: user) } - let!(:merge_request2) { create(:merge_request, id: 10001, source_project: project, source_branch: generate(:branch), target_project: project, author: user) } - - it 'returns the expected amount of events' do - jira_get v3_api(events_path, user) - - expect(response).to have_gitlab_http_status(:ok) - expect(json_response).to be_an(Array) - expect(json_response.size).to eq(2) - end - - it 'ensures each event has a unique id' do - jira_get v3_api(events_path, user) - - ids = json_response.map { |event| event['id'] }.uniq - expect(ids.size).to eq(2) - end - end - end - end - - describe 'repo pulls' do - let_it_be(:project2) { create(:project, :repository, creator: user) } - let_it_be(:assignee) { create(:user) } - let_it_be(:assignee2) { create(:user) } - let_it_be(:merge_request) do - create(:merge_request, source_project: project, target_project: project, author: user, assignees: [assignee]) - end - - let_it_be(:merge_request_2) do - create(:merge_request, source_project: project2, target_project: project2, author: user, assignees: [assignee, assignee2]) - end - - before do - project2.add_maintainer(user) - end - - def perform_request - jira_get v3_api(route, user) - end - - describe 'GET /-/jira/pulls' do - let(:route) { '/repos/-/jira/pulls' } - - it_behaves_like 'a GitHub Enterprise Jira DVCS reversible end of life endpoint' do - subject { perform_request } - end - - it 'returns an array of merge requests with github format' do - perform_request - - expect(response).to have_gitlab_http_status(:ok) - expect(json_response).to be_an(Array) - expect(json_response.size).to eq(2) - expect(response).to match_response_schema('entities/github/pull_requests') - end - - it 'returns multiple merge requests without N + 1' do - perform_request - - control_count = ActiveRecord::QueryRecorder.new { perform_request }.count - - project3 = create(:project, :repository, creator: user) - project3.add_maintainer(user) - assignee3 = create(:user) - create(:merge_request, source_project: project3, target_project: project3, author: user, assignees: [assignee3]) - - expect { perform_request }.not_to exceed_query_limit(control_count) - end - end - - describe 'GET /repos/:namespace/:project/pulls' do - let(:route) { "/repos/#{project.namespace.path}/#{project.path}/pulls" } - - it_behaves_like 'a GitHub Enterprise Jira DVCS reversible end of life endpoint' do - subject { perform_request } - end - - it 'returns an array of merge requests for the proper project in github format' do - perform_request - - expect(response).to have_gitlab_http_status(:ok) - expect(json_response).to be_an(Array) - expect(json_response.size).to eq(1) - expect(response).to match_response_schema('entities/github/pull_requests') - end - - it 'returns multiple merge requests without N + 1' do - perform_request - - control_count = ActiveRecord::QueryRecorder.new { perform_request }.count - - create(:merge_request, source_project: project, source_branch: 'fix') - - expect { perform_request }.not_to exceed_query_limit(control_count) - end - end - - describe 'GET /repos/:namespace/:project/pulls/:id' do - it_behaves_like 'a GitHub Enterprise Jira DVCS reversible end of life endpoint' do - subject { jira_get v3_api("/repos/#{project.namespace.path}/#{project.path}/pulls/#{merge_request.id}", user) } - end - - context 'when user has access to the merge requests' do - it 'returns the requested merge request in github format' do - jira_get v3_api("/repos/#{project.namespace.path}/#{project.path}/pulls/#{merge_request.id}", user) - - expect(response).to have_gitlab_http_status(:ok) - expect(response).to match_response_schema('entities/github/pull_request') - end - end - - context 'when user has no access to the merge request' do - it 'returns 404' do - project.add_guest(unauthorized_user) - - jira_get v3_api("/repos/#{project.namespace.path}/#{project.path}/pulls/#{merge_request.id}", unauthorized_user) - - expect(response).to have_gitlab_http_status(:not_found) - end - end - - context 'when instance admin' do - it 'returns the requested merge request in github format' do - jira_get v3_api("/repos/#{project.namespace.path}/#{project.path}/pulls/#{merge_request.id}", admin, admin_mode: true) - - expect(response).to have_gitlab_http_status(:ok) - expect(response).to match_response_schema('entities/github/pull_request') - end - end - end - end - - describe 'GET /users/:namespace/repos' do - let(:group) { create(:group, name: 'foo') } - - def expect_project_under_namespace(projects, namespace, user, admin_mode = false) - jira_get v3_api("/users/#{namespace.path}/repos", user, admin_mode: admin_mode) - - expect(response).to have_gitlab_http_status(:ok) - expect(response).to include_pagination_headers - expect(response).to match_response_schema('entities/github/repositories') - - projects.each do |project| - hash = json_response.find do |hash| - hash['name'] == ::Gitlab::Jira::Dvcs.encode_project_name(project) - end - - raise "Project #{project.full_path} not present in response" if hash.nil? - - expect(hash['owner']['login']).to eq(namespace.path) - end - expect(json_response.size).to eq(projects.size) - end - - it_behaves_like 'a GitHub Enterprise Jira DVCS reversible end of life endpoint' do - subject { jira_get v3_api("/users/#{user.namespace.path}/repos", user) } - end - - context 'group namespace' do - let(:project) { create(:project, group: group) } - let!(:project2) { create(:project, :public, group: group) } - - it 'returns an array of projects belonging to group excluding the ones user is not directly a member of, even when public' do - expect_project_under_namespace([project], group, user) - end - - context 'when instance admin' do - let(:user) { create(:user, :admin) } - - it 'returns an array of projects belonging to group' do - expect_project_under_namespace([project, project2], group, user, true) - end - - context 'with a private group' do - let(:group) { create(:group, :private) } - let!(:project2) { create(:project, :private, group: group) } - - it 'returns an array of projects belonging to group' do - expect_project_under_namespace([project, project2], group, user, true) - end - end - end - end - - context 'nested group namespace' do - let(:group) { create(:group, :nested) } - let!(:parent_group_project) { create(:project, group: group.parent, name: 'parent_group_project') } - let!(:child_group_project) { create(:project, group: group, name: 'child_group_project') } - - before do - group.parent.add_maintainer(user) - end - - it 'returns an array of projects belonging to group with github format' do - expect_project_under_namespace([parent_group_project, child_group_project], group.parent, user) - end - - it 'avoids N+1 queries' do - jira_get v3_api("/users/#{group.parent.path}/repos", user) - - control = ActiveRecord::QueryRecorder.new { jira_get v3_api("/users/#{group.parent.path}/repos", user) } - - new_group = create(:group, parent: group.parent) - create(:project, :repository, group: new_group, creator: user) - - expect { jira_get v3_api("/users/#{group.parent.path}/repos", user) }.not_to exceed_query_limit(control) - expect(response).to have_gitlab_http_status(:ok) - end - end - - context 'user namespace' do - let(:project) { create(:project, namespace: user.namespace) } - - it 'returns an array of projects belonging to user namespace with github format' do - expect_project_under_namespace([project], user.namespace, user) - end - end - - context 'namespace path includes a dot' do - let(:project) { create(:project, group: group) } - let(:group) { create(:group, name: 'foo.bar') } - - before do - group.add_maintainer(user) - end - - it 'returns an array of projects belonging to group with github format' do - expect_project_under_namespace([project], group, user) - end - end - - context 'unauthenticated' do - it 'returns 401' do - jira_get v3_api('/users/foo/repos', nil) - - expect(response).to have_gitlab_http_status(:unauthorized) - end - end - - context 'namespace does not exist' do - it 'responds with not found status' do - jira_get v3_api('/users/noo/repos', user) - - expect(response).to have_gitlab_http_status(:not_found) - end - end - end - - describe 'GET /repos/:namespace/:project/branches' do - context 'authenticated' do - it_behaves_like 'a GitHub Enterprise Jira DVCS reversible end of life endpoint' do - subject { jira_get v3_api("/repos/#{project.namespace.path}/#{project.path}/branches", user) } - end - - context 'updating project feature usage' do - it 'counts Jira Cloud integration as enabled' do - user_agent = 'Jira DVCS Connector Vertigo/4.42.0' - - freeze_time do - jira_get v3_api("/repos/#{project.namespace.path}/#{project.path}/branches", user), user_agent - - expect(project.reload.jira_dvcs_cloud_last_sync_at).to be_like_time(Time.now) - end - end - - it 'counts Jira Server integration as enabled' do - user_agent = 'Jira DVCS Connector/3.2.4' - - freeze_time do - jira_get v3_api("/repos/#{project.namespace.path}/#{project.path}/branches", user), user_agent - - expect(project.reload.jira_dvcs_server_last_sync_at).to be_like_time(Time.now) - end - end - end - - it 'returns an array of project branches with github format' do - jira_get v3_api("/repos/#{project.namespace.path}/#{project.path}/branches", user) - - expect(response).to have_gitlab_http_status(:ok) - expect(response).to include_pagination_headers - expect(json_response).to be_an(Array) - - expect(response).to match_response_schema('entities/github/branches') - end - - it 'returns 200 when project path include a dot' do - project.update!(path: 'foo.bar') - - jira_get v3_api("/repos/#{project.namespace.path}/#{project.path}/branches", user) - - expect(response).to have_gitlab_http_status(:ok) - end - - it 'returns 200 when namespace path include a dot' do - group = create(:group, path: 'foo.bar') - project = create(:project, :repository, group: group) - project.add_reporter(user) - - jira_get v3_api("/repos/#{group.path}/#{project.path}/branches", user) - - expect(response).to have_gitlab_http_status(:ok) - end - - context 'when the project has no repository' do - let_it_be(:project) { create(:project, creator: user) } - - it 'returns an empty collection response' do - jira_get v3_api("/repos/#{project.namespace.path}/#{project.path}/branches", user) - - expect(response).to have_gitlab_http_status(:ok) - expect(json_response).to be_empty - end - end - end - - context 'unauthenticated' do - it 'returns 401' do - jira_get v3_api("/repos/#{project.namespace.path}/#{project.path}/branches", nil) - - expect(response).to have_gitlab_http_status(:unauthorized) - end - end - - context 'unauthorized' do - it 'returns 404 when lower access level' do - project.add_guest(unauthorized_user) - - jira_get v3_api("/repos/#{project.namespace.path}/#{project.path}/branches", unauthorized_user) - - expect(response).to have_gitlab_http_status(:not_found) - end - end - end - - describe 'GET /repos/:namespace/:project/commits/:sha' do - let(:commit) { project.repository.commit } - - def call_api(commit_id: commit.id) - jira_get v3_api("/repos/#{project.namespace.path}/#{project.path}/commits/#{commit_id}", user) - end - - def response_diff_files(response) - Gitlab::Json.parse(response.body)['files'] - end - - context 'authenticated' do - it_behaves_like 'a GitHub Enterprise Jira DVCS reversible end of life endpoint' do - subject { call_api } - end - - it 'returns commit with github format' do - call_api - - expect(response).to have_gitlab_http_status(:ok) - expect(response).to match_response_schema('entities/github/commit') - end - - it 'returns 200 when project path include a dot' do - project.update!(path: 'foo.bar') - - call_api - - expect(response).to have_gitlab_http_status(:ok) - end - - context 'when namespace path includes a dot' do - let(:group) { create(:group, path: 'foo.bar') } - let(:project) { create(:project, :repository, group: group) } - - it 'returns 200 when namespace path include a dot' do - project.add_reporter(user) - - call_api - - expect(response).to have_gitlab_http_status(:ok) - end - end - - context 'when the Gitaly `CommitDiff` RPC times out', :use_clean_rails_memory_store_caching do - let(:commit_diff_args) { [project.repository_storage, :diff_service, :commit_diff, any_args] } - - before do - allow(Gitlab::GitalyClient).to receive(:call) - .and_call_original - end - - it 'handles the error, logs it, and returns empty diff files' do - allow(Gitlab::GitalyClient).to receive(:call) - .with(*commit_diff_args) - .and_raise(GRPC::DeadlineExceeded) - - expect(Gitlab::ErrorTracking) - .to receive(:track_exception) - .with an_instance_of(GRPC::DeadlineExceeded) - - call_api - - expect(response).to have_gitlab_http_status(:ok) - expect(response_diff_files(response)).to be_blank - end - - it 'only calls Gitaly once for all attempts within a period of time' do - expect(Gitlab::GitalyClient).to receive(:call) - .with(*commit_diff_args) - .once # <- once - .and_raise(GRPC::DeadlineExceeded) - - 3.times do - call_api - - expect(response).to have_gitlab_http_status(:ok) - expect(response_diff_files(response)).to be_blank - end - end - - it 'calls Gitaly again after a period of time' do - expect(Gitlab::GitalyClient).to receive(:call) - .with(*commit_diff_args) - .twice # <- twice - .and_raise(GRPC::DeadlineExceeded) - - call_api - - expect(response).to have_gitlab_http_status(:ok) - expect(response_diff_files(response)).to be_blank - - travel_to((described_class::GITALY_TIMEOUT_CACHE_EXPIRY + 1.second).from_now) do - call_api - - expect(response).to have_gitlab_http_status(:ok) - expect(response_diff_files(response)).to be_blank - end - end - - it 'uses a unique cache key, allowing other calls to succeed' do - cache_key = [described_class::GITALY_TIMEOUT_CACHE_KEY, project.id, commit.cache_key].join(':') - Rails.cache.write(cache_key, 1) - - expect(Gitlab::GitalyClient).to receive(:call) - .with(*commit_diff_args) - .once # <- once - - call_api - - expect(response).to have_gitlab_http_status(:ok) - expect(response_diff_files(response)).to be_blank - - call_api(commit_id: commit.parent.id) - - expect(response).to have_gitlab_http_status(:ok) - expect(response_diff_files(response).length).to eq(1) - end - end - end - - context 'unauthenticated' do - let(:user) { nil } - - it 'returns 401' do - call_api - - expect(response).to have_gitlab_http_status(:unauthorized) - end - end - - context 'unauthorized' do - let(:user) { unauthorized_user } - - it 'returns 404 when lower access level' do - project.add_guest(user) - - call_api - - expect(response).to have_gitlab_http_status(:not_found) - end - end - end - - def jira_get(path, user_agent = 'Jira DVCS Connector/3.2.4') - get path, headers: { 'User-Agent' => user_agent } - end - - def v3_api(path, user = nil, personal_access_token: nil, oauth_access_token: nil, admin_mode: false) - api( - path, - user, - version: 'v3', - personal_access_token: personal_access_token, - oauth_access_token: oauth_access_token, - admin_mode: admin_mode - ) - end -end diff --git a/spec/requests/api/vs_code/settings/vs_code_settings_sync_spec.rb b/spec/requests/api/vs_code/settings/vs_code_settings_sync_spec.rb index 1055a8efded..74d19f8533c 100644 --- a/spec/requests/api/vs_code/settings/vs_code_settings_sync_spec.rb +++ b/spec/requests/api/vs_code/settings/vs_code_settings_sync_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' RSpec.describe API::VsCode::Settings::VsCodeSettingsSync, :aggregate_failures, factory_default: :keep, feature_category: :web_ide do + include GrapePathHelpers::NamedRouteMatcher + let_it_be(:user) { create_default(:user) } let_it_be(:user_token) { create(:personal_access_token) } @@ -21,6 +23,14 @@ RSpec.describe API::VsCode::Settings::VsCodeSettingsSync, :aggregate_failures, f end end + shared_examples "returns 400" do + it 'returns 400' do + get api(path, personal_access_token: user_token) + + expect(response).to have_gitlab_http_status(:bad_request) + end + end + describe 'GET /vscode/settings_sync/v1/manifest' do let(:path) { "/vscode/settings_sync/v1/manifest" } @@ -80,6 +90,12 @@ RSpec.describe API::VsCode::Settings::VsCodeSettingsSync, :aggregate_failures, f it_behaves_like "returns 20x when authenticated", :no_content it_behaves_like "returns unauthorized when not authenticated" + context "when resource type is invalid" do + let(:path) { "/vscode/settings_sync/v1/resource/foo/1" } + + it_behaves_like "returns 400" + end + context 'when settings with that type are not present' do it 'returns 204 no content and no content ETag header' do get api(path, personal_access_token: user_token) @@ -102,6 +118,55 @@ RSpec.describe API::VsCode::Settings::VsCodeSettingsSync, :aggregate_failures, f end end + describe 'GET /vscode/settings_sync/v1/resource/:resource_name/' do + let(:path) { "/vscode/settings_sync/v1/resource/settings/" } + + context "when resource type is invalid" do + let(:path) { "/vscode/settings_sync/v1/resource/foo" } + + it_behaves_like "returns 400" + end + + it_behaves_like "returns unauthorized when not authenticated" + it_behaves_like "returns 20x when authenticated", :ok + + context 'when settings with that type are not present' do + it "returns empty array response" do + get api(path, personal_access_token: user_token) + + expect(json_response.length).to eq(0) + end + end + + context 'when settings with that type are present' do + let_it_be(:settings) { create(:vscode_setting, content: '{ "key": "value" }') } + + it 'returns settings with the correct json content' do + get api(path, personal_access_token: user_token) + + setting_type = settings[:setting_type] + uuid = settings[:uuid] + + resource_ref = "/api/v4/vscode/settings_sync/v1/resource/#{setting_type}/#{uuid}" + + expect(json_response.length).to eq(1) + expect(json_response.first['url']).to eq(resource_ref) + expect(json_response.first['created']).to eq(settings.updated_at.to_i) + end + end + + context 'when setting type is machine' do + let(:path) { "/vscode/settings_sync/v1/resource/machines/" } + + it 'created field is nil' do + get api(path, personal_access_token: user_token) + + expect(json_response.length).to eq(1) + expect(json_response.first['created']).to be_nil + end + end + end + describe 'POST /vscode/settings_sync/v1/resource/:resource_name' do let(:path) { "/vscode/settings_sync/v1/resource/settings" } @@ -138,4 +203,28 @@ RSpec.describe API::VsCode::Settings::VsCodeSettingsSync, :aggregate_failures, f expect(response).to have_gitlab_http_status(:bad_request) end end + + describe 'DELETE /vscode/settings_sync/v1/collection' do + let(:path) { "/vscode/settings_sync/v1/collection" } + + subject(:request) do + delete api(path, personal_access_token: user_token) + end + + it 'returns unauthorized when not authenticated' do + delete api(path) + expect(response).to have_gitlab_http_status(:unauthorized) + end + + context 'when user has one or more setting resources' do + before do + create(:vscode_setting, setting_type: 'globalState') + create(:vscode_setting, setting_type: 'extensions') + end + + it 'deletes all user setting resources' do + expect { request }.to change { User.find(user.id).vscode_settings.count }.from(2).to(0) + end + end + end end diff --git a/spec/requests/api/wikis_spec.rb b/spec/requests/api/wikis_spec.rb index 00e38a5bb7e..cd9c5637264 100644 --- a/spec/requests/api/wikis_spec.rb +++ b/spec/requests/api/wikis_spec.rb @@ -31,8 +31,8 @@ RSpec.describe API::Wikis, feature_category: :wiki do let(:project_wiki) { create(:project_wiki, project: project, user: user) } let(:payload) { { content: 'content', format: 'rdoc', title: 'title' } } - let(:expected_keys_with_content) { %w(content format slug title encoding) } - let(:expected_keys_without_content) { %w(format slug title) } + let(:expected_keys_with_content) { %w[content format slug title encoding front_matter] } + let(:expected_keys_without_content) { %w[format slug title] } let(:wiki) { project_wiki } shared_examples_for 'wiki API 404 Project Not Found' do @@ -354,6 +354,18 @@ RSpec.describe API::Wikis, feature_category: :wiki do end include_examples 'wikis API creates wiki page' + + context "with front matter title" do + let(:payload) { { title: 'title', front_matter: { "title" => "title in front matter" }, content: 'content' } } + + it "save front matter" do + post(api(url, user), params: payload) + + expect(response).to have_gitlab_http_status(:created) + expect(json_response['front_matter']).to eq(payload[:front_matter]) + expect(json_response['content']).to include(payload[:front_matter]["title"]) + end + end end context 'when user is maintainer' do @@ -478,6 +490,20 @@ RSpec.describe API::Wikis, feature_category: :wiki do include_examples 'wiki API 404 Wiki Page Not Found' end + + context "with front matter title" do + let(:payload) do + { title: 'new title', front_matter: { "title" => "title in front matter" }, content: 'new content' } + end + + it "save front matter" do + put(api(url, user), params: payload) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['front_matter']).to eq(payload[:front_matter]) + expect(json_response['content']).to include(payload[:front_matter]["title"]) + end + end end context 'when user is maintainer' do diff --git a/spec/requests/application_controller_spec.rb b/spec/requests/application_controller_spec.rb deleted file mode 100644 index 52fdf6bc69e..00000000000 --- a/spec/requests/application_controller_spec.rb +++ /dev/null @@ -1,15 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe ApplicationController, type: :request, feature_category: :shared do - let_it_be(:user) { create(:user) } - - before do - sign_in(user) - end - - it_behaves_like 'Base action controller' do - subject(:request) { get root_path } - end -end diff --git a/spec/requests/chaos_controller_spec.rb b/spec/requests/chaos_controller_spec.rb deleted file mode 100644 index d2ce618b041..00000000000 --- a/spec/requests/chaos_controller_spec.rb +++ /dev/null @@ -1,14 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe ChaosController, type: :request, feature_category: :tooling do - it_behaves_like 'Base action controller' do - before do - # Stub leak_mem so we don't actually leak memory for the base action controller tests. - allow(Gitlab::Chaos).to receive(:leak_mem).with(100, 30.seconds) - end - - subject(:request) { get leakmem_chaos_path } - end -end diff --git a/spec/requests/explore/catalog_controller_spec.rb b/spec/requests/explore/catalog_controller_spec.rb new file mode 100644 index 00000000000..50a2240e040 --- /dev/null +++ b/spec/requests/explore/catalog_controller_spec.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Explore::CatalogController, feature_category: :pipeline_composition do + let_it_be(:user) { create(:user) } + + before do + sign_in(user) + end + + shared_examples 'basic get requests' do |action| + let(:path) do + if action == :index + explore_catalog_index_path + else + explore_catalog_path(id: 1) + end + end + + context 'with FF `global_ci_catalog`' do + before do + stub_feature_flags(global_ci_catalog: true) + end + + it 'responds with 200' do + get path + + expect(response).to have_gitlab_http_status(:ok) + end + end + + context 'without FF `global_ci_catalog`' do + before do + stub_feature_flags(global_ci_catalog: false) + end + + it 'responds with 404' do + get path + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end + + describe 'GET #show' do + it_behaves_like 'basic get requests', :show + end + + describe 'GET #index' do + it_behaves_like 'basic get requests', :index + end +end diff --git a/spec/requests/external_redirect/external_redirect_controller_spec.rb b/spec/requests/external_redirect/external_redirect_controller_spec.rb new file mode 100644 index 00000000000..1b4294f5c4d --- /dev/null +++ b/spec/requests/external_redirect/external_redirect_controller_spec.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe "ExternalRedirect::ExternalRedirectController requests", feature_category: :navigation do + let_it_be(:external_url) { 'https://google.com' } + let_it_be(:external_url_encoded) do + Addressable::URI.encode_component(external_url, Addressable::URI::CharacterClasses::QUERY) + end + + let_it_be(:internal_url) { "#{Gitlab.config.gitlab.url}/foo/bar" } + let_it_be(:internal_url_encoded) do + Addressable::URI.encode_component(internal_url, Addressable::URI::CharacterClasses::QUERY) + end + + let_it_be(:top_nav_partial) { 'layouts/header/_default' } + + context "with valid url param" do + before do + get "/-/external_redirect?url=#{external_url_encoded}" + end + + it "returns 200 and renders URL" do + expect(response).to have_gitlab_http_status(:ok) + expect(response.body).to have_link(text: 'Proceed', href: external_url) + end + + it "does not render nav" do + expect(response).not_to render_template(top_nav_partial) + end + end + + context "with same origin url" do + before do + get "/-/external_redirect?url=#{internal_url_encoded}" + end + + it "redirects" do + expect(response).to redirect_to(internal_url) + end + end + + describe "with invalid url params" do + where(:case_name, :params) do + [ + ["when url is bad", "url=javascript:alert(1)"], + ["when url is empty", "url="], + ["when url param is missing", ""] + ] + end + + with_them do + it "returns 404" do + get "/-/external_redirect?#{params}" + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end +end diff --git a/spec/requests/health_controller_spec.rb b/spec/requests/health_controller_spec.rb index 3ad1d8a75b4..639f6194af9 100644 --- a/spec/requests/health_controller_spec.rb +++ b/spec/requests/health_controller_spec.rb @@ -73,9 +73,7 @@ RSpec.describe HealthController, feature_category: :database do end describe 'GET /-/readiness' do - subject(:request) { get readiness_path, params: params, headers: headers } - - it_behaves_like 'Base action controller' + subject { get '/-/readiness', params: params, headers: headers } shared_context 'endpoint responding with readiness data' do context 'when requesting instance-checks' do diff --git a/spec/requests/jira_authorizations_spec.rb b/spec/requests/jira_authorizations_spec.rb deleted file mode 100644 index 704db7fba08..00000000000 --- a/spec/requests/jira_authorizations_spec.rb +++ /dev/null @@ -1,88 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe 'Jira authorization requests', feature_category: :integrations do - let(:user) { create :user } - let(:application) { create :oauth_application, scopes: 'api' } - let(:redirect_uri) { oauth_jira_dvcs_callback_url(host: "http://www.example.com") } - - def generate_access_grant - create :oauth_access_grant, application: application, resource_owner_id: user.id, redirect_uri: redirect_uri - end - - describe 'POST access_token' do - let(:client_id) { application.uid } - let(:client_secret) { application.secret } - - it 'returns values similar to a POST to /oauth/token' do - post_data = { - client_id: client_id, - client_secret: client_secret - } - - post '/oauth/token', params: post_data.merge({ - code: generate_access_grant.token, - grant_type: 'authorization_code', - redirect_uri: redirect_uri - }) - oauth_response = json_response - oauth_response_access_token, scope, token_type = oauth_response.values_at('access_token', 'scope', 'token_type') - - post '/login/oauth/access_token', params: post_data.merge({ - code: generate_access_grant.token - }) - jira_response = response.body - jira_response_access_token = Rack::Utils.parse_nested_query(jira_response)['access_token'] - - expect(jira_response).to include("scope=#{scope}&token_type=#{token_type}") - expect(oauth_response_access_token).not_to eql(jira_response_access_token) - end - - it_behaves_like 'a GitHub Enterprise Jira DVCS reversible end of life endpoint' do - subject do - post '/login/oauth/access_token', params: { - client_id: client_id, - client_secret: client_secret, - code: generate_access_grant.token - } - end - end - - context 'when authorization fails' do - before do - post '/login/oauth/access_token', params: { - client_id: client_id, - client_secret: client_secret, - code: try(:code) || generate_access_grant.token - } - end - - shared_examples 'an unauthorized request' do - it 'returns 401' do - expect(response).to have_gitlab_http_status(:unauthorized) - end - end - - context 'when client_id is invalid' do - let(:client_id) { "invalid_id" } - - it_behaves_like 'an unauthorized request' - end - - context 'when client_secret is invalid' do - let(:client_secret) { "invalid_secret" } - - it_behaves_like 'an unauthorized request' - end - - context 'when code is invalid' do - let(:code) { "invalid_code" } - - it 'returns bad request' do - expect(response).to have_gitlab_http_status(:bad_request) - end - end - end - end -end diff --git a/spec/requests/jwt_controller_spec.rb b/spec/requests/jwt_controller_spec.rb index 965bead4068..966cc2d6d4e 100644 --- a/spec/requests/jwt_controller_spec.rb +++ b/spec/requests/jwt_controller_spec.rb @@ -61,7 +61,7 @@ RSpec.describe JwtController, feature_category: :system_access do end end - shared_examples 'container registry authenticator' do + context 'authenticating against container registry' do context 'existing service' do subject! { get '/jwt/auth', params: parameters } @@ -124,7 +124,7 @@ RSpec.describe JwtController, feature_category: :system_access do end it 'does not log a user' do - expect(log_data.keys).not_to include(%w(username user_id)) + expect(log_data.keys).not_to include(%w[username user_id]) end end @@ -177,7 +177,7 @@ RSpec.describe JwtController, feature_category: :system_access do end let(:service_parameters) do - ActionController::Parameters.new({ service: service_name, scopes: %w(scope1 scope2) }).permit! + ActionController::Parameters.new({ service: service_name, scopes: %w[scope1 scope2] }).permit! end it { expect(service_class).to have_received(:new).with(nil, user, service_parameters.merge(auth_type: :gitlab_or_ldap)) } @@ -185,6 +185,21 @@ RSpec.describe JwtController, feature_category: :system_access do it_behaves_like 'user logging' end + context 'when passing a space-delimited list of scopes' do + let(:parameters) do + { + service: service_name, + scope: 'scope1 scope2' + } + end + + let(:service_parameters) do + ActionController::Parameters.new({ service: service_name, scopes: %w[scope1 scope2] }).permit! + end + + it { expect(service_class).to have_received(:new).with(nil, user, service_parameters.merge(auth_type: :gitlab_or_ldap)) } + end + context 'when user has 2FA enabled' do let(:user) { create(:user, :two_factor) } @@ -254,40 +269,6 @@ RSpec.describe JwtController, feature_category: :system_access do end end - shared_examples 'parses a space-delimited list of scopes' do |output| - let(:user) { create(:user) } - let(:headers) { { authorization: credentials(user.username, user.password) } } - - subject! { get '/jwt/auth', params: parameters, headers: headers } - - let(:parameters) do - { - service: service_name, - scope: 'scope1 scope2' - } - end - - let(:service_parameters) do - ActionController::Parameters.new({ service: service_name, scopes: output }).permit! - end - - it { expect(service_class).to have_received(:new).with(nil, user, service_parameters.merge(auth_type: :gitlab_or_ldap)) } - end - - context 'authenticating against container registry' do - it_behaves_like 'container registry authenticator' - it_behaves_like 'parses a space-delimited list of scopes', %w(scope1 scope2) - - context 'when jwt_auth_space_delimited_scopes feature flag is disabled' do - before do - stub_feature_flags(jwt_auth_space_delimited_scopes: false) - end - - it_behaves_like 'container registry authenticator' - it_behaves_like 'parses a space-delimited list of scopes', ['scope1 scope2'] - end - end - context 'authenticating against dependency proxy' do let_it_be(:user) { create(:user) } let_it_be(:personal_access_token) { create(:personal_access_token, user: user) } diff --git a/spec/requests/lfs_http_spec.rb b/spec/requests/lfs_http_spec.rb index bc1ba3357a4..9bf77a0f6ca 100644 --- a/spec/requests/lfs_http_spec.rb +++ b/spec/requests/lfs_http_spec.rb @@ -664,8 +664,7 @@ RSpec.describe 'Git LFS API and storage', feature_category: :source_code_managem context 'tries to push to other project' do let(:pipeline) { create(:ci_empty_pipeline, project: other_project) } - # I'm not sure what this tests that is different from the previous test - it_behaves_like 'LFS http 403 response' + it_behaves_like 'LFS http 404 response' end end @@ -1185,8 +1184,7 @@ RSpec.describe 'Git LFS API and storage', feature_category: :source_code_managem context 'tries to push to other project' do let(:pipeline) { create(:ci_empty_pipeline, project: other_project) } - # I'm not sure what this tests that is different from the previous test - it_behaves_like 'LFS http 403 response' + it_behaves_like 'LFS http 404 response' end end diff --git a/spec/requests/lfs_locks_api_spec.rb b/spec/requests/lfs_locks_api_spec.rb index 363a16f014b..a1a713308e0 100644 --- a/spec/requests/lfs_locks_api_spec.rb +++ b/spec/requests/lfs_locks_api_spec.rb @@ -67,7 +67,7 @@ RSpec.describe 'Git LFS File Locking API', feature_category: :source_code_manage expect(response).to have_gitlab_http_status(:conflict) - expect(json_response.keys).to match_array(%w(lock message documentation_url)) + expect(json_response.keys).to match_array(%w[lock message documentation_url]) expect(json_response['message']).to match(/already locked/) end @@ -84,7 +84,7 @@ RSpec.describe 'Git LFS File Locking API', feature_category: :source_code_manage expect(response).to have_gitlab_http_status(:created) - expect(json_response['lock'].keys).to match_array(%w(id path locked_at owner)) + expect(json_response['lock'].keys).to match_array(%w[id path locked_at owner]) end end end @@ -103,7 +103,7 @@ RSpec.describe 'Git LFS File Locking API', feature_category: :source_code_manage expect(response).to have_gitlab_http_status(:ok) expect(json_response['locks'].size).to eq(2) - expect(json_response['locks'].first.keys).to match_array(%w(id path locked_at owner)) + expect(json_response['locks'].first.keys).to match_array(%w[id path locked_at owner]) end end @@ -143,7 +143,7 @@ RSpec.describe 'Git LFS File Locking API', feature_category: :source_code_manage it 'returns the deleted lock' do post_lfs_json url, nil, headers - expect(json_response['lock'].keys).to match_array(%w(id path locked_at owner)) + expect(json_response['lock'].keys).to match_array(%w[id path locked_at owner]) end context 'when a maintainer uses force' do diff --git a/spec/requests/metrics_controller_spec.rb b/spec/requests/metrics_controller_spec.rb deleted file mode 100644 index ce96906e020..00000000000 --- a/spec/requests/metrics_controller_spec.rb +++ /dev/null @@ -1,9 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe MetricsController, type: :request, feature_category: :metrics do - it_behaves_like 'Base action controller' do - subject(:request) { get metrics_path } - end -end diff --git a/spec/requests/oauth/authorizations_controller_spec.rb b/spec/requests/oauth/authorizations_controller_spec.rb index 7887bf52542..257f238d9ef 100644 --- a/spec/requests/oauth/authorizations_controller_spec.rb +++ b/spec/requests/oauth/authorizations_controller_spec.rb @@ -20,10 +20,6 @@ RSpec.describe Oauth::AuthorizationsController, feature_category: :system_access end describe 'GET #new' do - it_behaves_like 'Base action controller' do - subject(:request) { get oauth_authorization_path } - end - context 'when application redirect URI has a custom scheme' do context 'when CSP is disabled' do before do diff --git a/spec/requests/organizations/organizations_controller_spec.rb b/spec/requests/organizations/organizations_controller_spec.rb index fdfeb367739..4bf527f49a8 100644 --- a/spec/requests/organizations/organizations_controller_spec.rb +++ b/spec/requests/organizations/organizations_controller_spec.rb @@ -75,6 +75,12 @@ RSpec.describe Organizations::OrganizationsController, feature_category: :cell d it_behaves_like 'controller action that does not require authentication' end + describe 'GET #users' do + subject(:gitlab_request) { get users_organization_path(organization) } + + it_behaves_like 'controller action that does not require authentication' + end + describe 'GET #new' do subject(:gitlab_request) { get new_organization_path } diff --git a/spec/requests/profiles/comment_templates_controller_spec.rb b/spec/requests/profiles/comment_templates_controller_spec.rb index cdbfbb0a346..d58fc3f19ea 100644 --- a/spec/requests/profiles/comment_templates_controller_spec.rb +++ b/spec/requests/profiles/comment_templates_controller_spec.rb @@ -10,26 +10,14 @@ RSpec.describe Profiles::CommentTemplatesController, feature_category: :user_pro end describe 'GET #index' do - describe 'feature flag disabled' do - before do - stub_feature_flags(saved_replies: false) - - get '/-/profile/comment_templates' - end - - it { expect(response).to have_gitlab_http_status(:not_found) } + before do + get '/-/profile/comment_templates' end - describe 'feature flag enabled' do - before do - get '/-/profile/comment_templates' - end - - it { expect(response).to have_gitlab_http_status(:ok) } + it { expect(response).to have_gitlab_http_status(:ok) } - it 'sets hide search settings ivar' do - expect(assigns(:hide_search_settings)).to eq(true) - end + it 'sets hide search settings ivar' do + expect(assigns(:hide_search_settings)).to eq(true) end end end diff --git a/spec/requests/projects/merge_requests_controller_spec.rb b/spec/requests/projects/merge_requests_controller_spec.rb index 4af8f4fac7f..1033a51cd80 100644 --- a/spec/requests/projects/merge_requests_controller_spec.rb +++ b/spec/requests/projects/merge_requests_controller_spec.rb @@ -187,21 +187,6 @@ RSpec.describe Projects::MergeRequestsController, feature_category: :source_code expect(response).to have_gitlab_http_status(:ok) expect(Gitlab::Json.parse(response.body)['count']['all']).to eq(2) end - - context 'when the FF ci_fix_performance_pipelines_json_endpoint is disabled' do - before do - stub_feature_flags(ci_fix_performance_pipelines_json_endpoint: false) - end - - it 'returns the failed builds' do - get pipelines_project_merge_request_path(project, merge_request, format: :json) - - expect(response).to have_gitlab_http_status(:ok) - expect(Gitlab::Json.parse(response.body)['pipelines'].size).to eq(1) - expect(Gitlab::Json.parse(response.body)['pipelines'][0]['failed_builds_count']).to eq(2) - expect(Gitlab::Json.parse(response.body)['pipelines'][0]['failed_builds'].size).to eq(2) - end - end end private diff --git a/spec/requests/projects/ml/model_versions_controller_spec.rb b/spec/requests/projects/ml/model_versions_controller_spec.rb new file mode 100644 index 00000000000..bd9d798c275 --- /dev/null +++ b/spec/requests/projects/ml/model_versions_controller_spec.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Projects::Ml::ModelVersionsController, feature_category: :mlops do + let_it_be(:project) { create(:project) } + let_it_be(:another_project) { create(:project) } + let_it_be(:user) { project.first_owner } + let_it_be(:model) { create(:ml_models, :with_versions, project: project) } + let_it_be(:version) { model.versions.first } + + let(:model_registry_enabled) { true } + + before do + allow(Ability).to receive(:allowed?).and_call_original + allow(Ability).to receive(:allowed?) + .with(user, :read_model_registry, project) + .and_return(model_registry_enabled) + + sign_in(user) + end + + describe 'show' do + let(:model_id) { model.id } + let(:version_id) { version.id } + let(:request_project) { model.project } + + subject(:show_request) do + show_model_version + response + end + + before do + show_request + end + + it 'renders the template' do + is_expected.to render_template('projects/ml/model_versions/show') + end + + it 'fetches the correct model_version' do + show_request + + expect(assigns(:model)).to eq(model) + expect(assigns(:model_version)).to eq(version) + end + + context 'when version id does not exist' do + let(:version_id) { non_existing_record_id } + + it { is_expected.to have_gitlab_http_status(:not_found) } + end + + context 'when version and model id are correct but project is not' do + let(:request_project) { another_project } + + it { is_expected.to have_gitlab_http_status(:not_found) } + end + + context 'when user does not have access' do + let(:model_registry_enabled) { false } + + it { is_expected.to have_gitlab_http_status(:not_found) } + end + end + + private + + def show_model_version + get project_ml_model_version_path(request_project, model_id, version_id) + end +end diff --git a/spec/requests/projects/ml/models_controller_spec.rb b/spec/requests/projects/ml/models_controller_spec.rb index b4402ad9a27..cda3f777a72 100644 --- a/spec/requests/projects/ml/models_controller_spec.rb +++ b/spec/requests/projects/ml/models_controller_spec.rb @@ -10,14 +10,19 @@ RSpec.describe Projects::Ml::ModelsController, feature_category: :mlops do let_it_be(:model3) { create(:ml_models, project: project) } let_it_be(:model_in_different_project) { create(:ml_models) } - let(:model_registry_enabled) { true } + let(:read_model_registry) { true } + let(:write_model_registry) { true } + let(:params) { {} } before do allow(Ability).to receive(:allowed?).and_call_original allow(Ability).to receive(:allowed?) .with(user, :read_model_registry, project) - .and_return(model_registry_enabled) + .and_return(read_model_registry) + allow(Ability).to receive(:allowed?) + .with(user, :write_model_registry, project) + .and_return(write_model_registry) sign_in(user) end @@ -33,34 +38,59 @@ RSpec.describe Projects::Ml::ModelsController, feature_category: :mlops do end it 'fetches the models using the finder' do - expect(::Projects::Ml::ModelFinder).to receive(:new).with(project).and_call_original + expect(::Projects::Ml::ModelFinder).to receive(:new).with(project, {}).and_call_original index_request end - it 'fetches the correct models' do + it 'fetches the correct variables', :aggregate_failures do + stub_const("Projects::Ml::ModelsController::MAX_MODELS_PER_PAGE", 2) + index_request - expect(assigns(:paginator).records).to match_array([model3, model2, model1]) + page_models = [model3, model2] + all_models = [model3, model2, model1] + + expect(assigns(:paginator).records).to match_array(page_models) + expect(assigns(:model_count)).to be all_models.count end it 'does not perform N+1 sql queries' do + list_models + control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) { list_models } create_list(:ml_model_versions, 2, model: model1) create_list(:ml_model_versions, 2, model: model2) + create_list(:ml_models, 4, project: project) expect { list_models }.not_to exceed_all_query_limit(control_count) end context 'when user does not have access' do - let(:model_registry_enabled) { false } + let(:read_model_registry) { false } it 'renders 404' do is_expected.to have_gitlab_http_status(:not_found) end end + context 'with search params' do + let(:params) { { name: 'some_name', order_by: 'name', sort: 'asc' } } + + it 'passes down params to the finder' do + expect(Projects::Ml::ModelFinder).to receive(:new).and_call_original do |_exp, params| + expect(params.to_h).to include({ + name: 'some_name', + order_by: 'name', + sort: 'asc' + }) + end + + index_request + end + end + describe 'pagination' do before do stub_const("Projects::Ml::ModelsController::MAX_MODELS_PER_PAGE", 2) @@ -116,7 +146,40 @@ RSpec.describe Projects::Ml::ModelsController, feature_category: :mlops do end context 'when user does not have access' do - let(:model_registry_enabled) { false } + let(:read_model_registry) { false } + + it { is_expected.to have_gitlab_http_status(:not_found) } + end + end + + describe 'destroy' do + let(:model_for_deletion) do + create(:ml_models, project: project) + end + + let(:model_id) { model_for_deletion.id } + + subject(:delete_request) do + delete_model + response + end + + it 'deletes the model', :aggregate_failures do + is_expected.to have_gitlab_http_status(:found) + + expect(flash[:notice]).to eq('Model removed') + expect(response).to redirect_to("/#{project.full_path}/-/ml/models") + expect { Ml::Model.find(id: model_id) }.to raise_error(ActiveRecord::RecordNotFound) + end + + context 'when model does not exist' do + let(:model_id) { non_existing_record_id } + + it { is_expected.to have_gitlab_http_status(:not_found) } + end + + describe 'when user does not have write_model_registry rights' do + let(:write_model_registry) { false } it { is_expected.to have_gitlab_http_status(:not_found) } end @@ -131,4 +194,8 @@ RSpec.describe Projects::Ml::ModelsController, feature_category: :mlops do def show_model get project_ml_model_path(request_project, model_id) end + + def delete_model + delete project_ml_model_path(project, model_id) + end end diff --git a/spec/requests/projects/service_desk_controller_spec.rb b/spec/requests/projects/service_desk_controller_spec.rb index 05e48c2c5c7..7d881d8ea62 100644 --- a/spec/requests/projects/service_desk_controller_spec.rb +++ b/spec/requests/projects/service_desk_controller_spec.rb @@ -88,6 +88,16 @@ RSpec.describe Projects::ServiceDeskController, feature_category: :service_desk expect(json_response['issue_template_key']).to eq('service_desk') end + it 'sets add_external_participants_from_cc' do + put project_service_desk_path(project, format: :json), params: { add_external_participants_from_cc: true } + project.reset + + settings = project.service_desk_setting + expect(settings).to be_present + expect(settings.add_external_participants_from_cc).to eq(true) + expect(json_response['add_external_participants_from_cc']).to eq(true) + end + it 'returns an error when update of service desk settings fails' do put project_service_desk_path(project, format: :json), params: { issue_template_key: 'invalid key' } diff --git a/spec/requests/registrations_controller_spec.rb b/spec/requests/registrations_controller_spec.rb index 71f2f347f0d..8b857046a4d 100644 --- a/spec/requests/registrations_controller_spec.rb +++ b/spec/requests/registrations_controller_spec.rb @@ -6,9 +6,7 @@ RSpec.describe RegistrationsController, type: :request, feature_category: :syste describe 'POST #create' do let_it_be(:user_attrs) { build_stubbed(:user).slice(:first_name, :last_name, :username, :email, :password) } - subject(:request) { post user_registration_path, params: { user: user_attrs } } - - it_behaves_like 'Base action controller' + subject(:create_user) { post user_registration_path, params: { user: user_attrs } } context 'when email confirmation is required' do before do @@ -17,7 +15,7 @@ RSpec.describe RegistrationsController, type: :request, feature_category: :syste end it 'redirects to the `users_almost_there_path`', unless: Gitlab.ee? do - request + create_user expect(response).to redirect_to(users_almost_there_path(email: user_attrs[:email])) end diff --git a/spec/requests/search_controller_spec.rb b/spec/requests/search_controller_spec.rb index 37474aee1ee..365b20ad4aa 100644 --- a/spec/requests/search_controller_spec.rb +++ b/spec/requests/search_controller_spec.rb @@ -9,7 +9,6 @@ RSpec.describe SearchController, type: :request, feature_category: :global_searc let_it_be(:projects) { create_list(:project, 5, :public, :repository, :wiki_repo) } before do - stub_feature_flags(super_sidebar_nav_enrolled: false) login_as(user) end diff --git a/spec/requests/sessions_spec.rb b/spec/requests/sessions_spec.rb index 1a925969c5a..3428e607305 100644 --- a/spec/requests/sessions_spec.rb +++ b/spec/requests/sessions_spec.rb @@ -7,10 +7,6 @@ RSpec.describe 'Sessions', feature_category: :system_access do let(:user) { create(:user) } - it_behaves_like 'Base action controller' do - subject(:request) { get new_user_session_path } - end - context 'for authentication', :allow_forgery_protection do it 'logout does not require a csrf token' do login_as(user) diff --git a/spec/requests/users_controller_spec.rb b/spec/requests/users_controller_spec.rb index d4e7dc1542a..da111831c15 100644 --- a/spec/requests/users_controller_spec.rb +++ b/spec/requests/users_controller_spec.rb @@ -600,7 +600,7 @@ RSpec.describe UsersController, feature_category: :user_management do end end - %i(html json).each do |format| + %i[html json].each do |format| context "with format: #{format}" do let(:format) { format } @@ -656,7 +656,7 @@ RSpec.describe UsersController, feature_category: :user_management do end end - %i(html json).each do |format| + %i[html json].each do |format| context "with format: #{format}" do let(:format) { format } |