diff options
Diffstat (limited to 'spec/requests')
120 files changed, 3687 insertions, 1195 deletions
diff --git a/spec/requests/acme_challenges_controller_spec.rb b/spec/requests/acme_challenges_controller_spec.rb new file mode 100644 index 00000000000..f37aefed488 --- /dev/null +++ b/spec/requests/acme_challenges_controller_spec.rb @@ -0,0 +1,9 @@ +# 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/api/admin/dictionary_spec.rb b/spec/requests/api/admin/dictionary_spec.rb index effd3572423..b35aacd6ba0 100644 --- a/spec/requests/api/admin/dictionary_spec.rb +++ b/spec/requests/api/admin/dictionary_spec.rb @@ -29,29 +29,13 @@ RSpec.describe API::Admin::Dictionary, feature_category: :database do end end - context 'with a malicious table_name' do - it 'returns an error' do - get api("/admin/databases/main/dictionary/tables/%2E%2E%2Fpasswords.yml", admin, admin_mode: true) - - expect(response).to have_gitlab_http_status(:error) - end - end - context 'when the params are correct' do - let(:dictionary_dir) { Rails.root.join('spec/fixtures') } - let(:path_file) { Rails.root.join(dictionary_dir, 'achievements.yml') } - it 'fetches the table dictionary' do - allow(Gitlab::Database::GitlabSchema).to receive(:dictionary_paths).and_return([dictionary_dir]) - - expect(Gitlab::PathTraversal).to receive(:check_allowed_absolute_path_and_path_traversal!).twice.with( - path_file.to_s, [dictionary_dir.to_s]).and_call_original - show_table_dictionary aggregate_failures "testing response" do expect(json_response['table_name']).to eq('achievements') - expect(json_response['feature_categories']).to eq(['feature_category_example']) + expect(json_response['feature_categories']).to eq(['user_profile']) end end end diff --git a/spec/requests/api/ci/job_artifacts_spec.rb b/spec/requests/api/ci/job_artifacts_spec.rb index b96ba356855..a8c09a5191d 100644 --- a/spec/requests/api/ci/job_artifacts_spec.rb +++ b/spec/requests/api/ci/job_artifacts_spec.rb @@ -187,7 +187,7 @@ RSpec.describe API::Ci::JobArtifacts, feature_category: :build_artifacts do end context 'when project is public with artifacts that are non public' do - let(:job) { create(:ci_build, :artifacts, :with_private_artifacts_config, pipeline: pipeline) } + let(:job) { create(:ci_build, :private_artifacts, :with_private_artifacts_config, pipeline: pipeline) } it 'rejects access to artifacts' do project.update_column(:visibility_level, Gitlab::VisibilityLevel::PUBLIC) @@ -197,21 +197,6 @@ RSpec.describe API::Ci::JobArtifacts, feature_category: :build_artifacts do expect(response).to have_gitlab_http_status(:forbidden) end - - context 'with the non_public_artifacts feature flag disabled' do - before do - stub_feature_flags(non_public_artifacts: false) - end - - it 'allows access to artifacts' do - project.update_column(:visibility_level, Gitlab::VisibilityLevel::PUBLIC) - project.update_column(:public_builds, true) - - get_artifact_file(artifact) - - expect(response).to have_gitlab_http_status(:ok) - end - end end context 'when project is public with builds access disabled' do @@ -433,7 +418,7 @@ RSpec.describe API::Ci::JobArtifacts, feature_category: :build_artifacts do context 'when public project guest and artifacts are non public' do let(:api_user) { guest } - let(:job) { create(:ci_build, :artifacts, :with_private_artifacts_config, pipeline: pipeline) } + let(:job) { create(:ci_build, :private_artifacts, :with_private_artifacts_config, pipeline: pipeline) } before do project.update_column(:visibility_level, @@ -445,17 +430,6 @@ RSpec.describe API::Ci::JobArtifacts, feature_category: :build_artifacts do it 'rejects access and hides existence of artifacts' do expect(response).to have_gitlab_http_status(:forbidden) end - - context 'with the non_public_artifacts feature flag disabled' do - before do - stub_feature_flags(non_public_artifacts: false) - get api("/projects/#{project.id}/jobs/#{job.id}/artifacts", api_user) - end - - it 'allows access to artifacts' do - expect(response).to have_gitlab_http_status(:ok) - end - end end it 'does not return job artifacts if not uploaded' do @@ -639,7 +613,7 @@ RSpec.describe API::Ci::JobArtifacts, feature_category: :build_artifacts do end context 'when project is public with non public artifacts' do - let(:job) { create(:ci_build, :artifacts, :with_private_artifacts_config, pipeline: pipeline, user: api_user) } + let(:job) { create(:ci_build, :private_artifacts, :with_private_artifacts_config, pipeline: pipeline, user: api_user) } let(:visibility_level) { Gitlab::VisibilityLevel::PUBLIC } let(:public_builds) { true } @@ -651,18 +625,6 @@ RSpec.describe API::Ci::JobArtifacts, feature_category: :build_artifacts do expect(response.headers.to_h) .not_to include('Gitlab-Workhorse-Send-Data' => /artifacts-entry/) end - - context 'with the non_public_artifacts feature flag disabled' do - before do - stub_feature_flags(non_public_artifacts: false) - end - - it 'allows access to artifacts', :sidekiq_might_not_need_inline do - get_artifact_file(artifact) - - expect(response).to have_gitlab_http_status(:ok) - end - end end context 'when project is private' do diff --git a/spec/requests/api/ci/pipeline_schedules_spec.rb b/spec/requests/api/ci/pipeline_schedules_spec.rb index a4bb379d01c..f534b093b7c 100644 --- a/spec/requests/api/ci/pipeline_schedules_spec.rb +++ b/spec/requests/api/ci/pipeline_schedules_spec.rb @@ -241,7 +241,7 @@ RSpec.describe API::Ci::PipelineSchedules, feature_category: :continuous_integra let(:url) { "/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}/pipelines" } matcher :return_pipeline_schedule_pipelines_successfully do - match_unless_raises do |reponse| + match_unless_raises do |response| expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers expect(response).to match_response_schema('public_api/v4/pipelines') 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 2a870a25ea6..3d6d86335eb 100644 --- a/spec/requests/api/ci/runner/jobs_request_post_spec.rb +++ b/spec/requests/api/ci/runner/jobs_request_post_spec.rb @@ -272,16 +272,19 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego expect(json_response['job_info']).to include(expected_job_info) expect(json_response['git_info']).to eq(expected_git_info) expect(json_response['image']).to eq( - { 'name' => 'image:1.0', 'entrypoint' => '/bin/sh', 'ports' => [], 'pull_policy' => nil } + { 'name' => 'image:1.0', 'entrypoint' => '/bin/sh', 'ports' => [], 'executor_opts' => {}, + 'pull_policy' => nil } ) expect(json_response['services']).to eq( [ { 'name' => 'postgres', 'entrypoint' => nil, 'alias' => nil, 'command' => nil, 'ports' => [], - 'variables' => nil, 'pull_policy' => nil }, - { 'name' => 'docker:stable-dind', 'entrypoint' => '/bin/sh', 'alias' => 'docker', 'command' => 'sleep 30', - 'ports' => [], 'variables' => [], 'pull_policy' => nil }, + 'variables' => nil, 'executor_opts' => {}, 'pull_policy' => nil }, + { 'name' => 'docker:stable-dind', 'entrypoint' => '/bin/sh', 'alias' => 'docker', + 'command' => 'sleep 30', 'ports' => [], 'variables' => [], 'executor_opts' => {}, + 'pull_policy' => nil }, { 'name' => 'mysql:latest', 'entrypoint' => nil, 'alias' => nil, 'command' => nil, 'ports' => [], - 'variables' => [{ 'key' => 'MYSQL_ROOT_PASSWORD', 'value' => 'root123.' }], 'pull_policy' => nil } + 'variables' => [{ 'key' => 'MYSQL_ROOT_PASSWORD', 'value' => 'root123.' }], 'executor_opts' => {}, + 'pull_policy' => nil } ]) expect(json_response['steps']).to eq(expected_steps) expect(json_response['hooks']).to eq(expected_hooks) @@ -920,6 +923,41 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego end end + context 'when image has docker options' do + let(:job) { create(:ci_build, :pending, :queued, pipeline: pipeline, options: options) } + + let(:options) do + { + image: { + name: 'ruby', + executor_opts: { + docker: { + platform: 'amd64' + } + } + } + } + end + + it 'returns the image with docker options' do + request_job + + expect(response).to have_gitlab_http_status(:created) + expect(json_response).to include( + 'id' => job.id, + 'image' => { 'name' => 'ruby', + 'executor_opts' => { + 'docker' => { + 'platform' => 'amd64' + } + }, + 'pull_policy' => nil, + 'entrypoint' => nil, + 'ports' => [] } + ) + end + end + context 'when image has pull_policy' do let(:job) { create(:ci_build, :pending, :queued, pipeline: pipeline, options: options) } @@ -938,7 +976,11 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego expect(response).to have_gitlab_http_status(:created) expect(json_response).to include( 'id' => job.id, - 'image' => { 'name' => 'ruby', 'pull_policy' => ['if-not-present'], 'entrypoint' => nil, 'ports' => [] } + 'image' => { 'name' => 'ruby', + 'executor_opts' => {}, + 'pull_policy' => ['if-not-present'], + 'entrypoint' => nil, + 'ports' => [] } ) end end @@ -962,7 +1004,8 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego expect(json_response).to include( 'id' => job.id, 'services' => [{ 'alias' => nil, 'command' => nil, 'entrypoint' => nil, 'name' => 'postgres:11.9', - 'ports' => [], 'pull_policy' => ['if-not-present'], 'variables' => [] }] + 'ports' => [], 'executor_opts' => {}, 'pull_policy' => ['if-not-present'], + 'variables' => [] }] ) end end diff --git a/spec/requests/api/ci/runner/jobs_request_yamls_spec.rb b/spec/requests/api/ci/runner/jobs_request_yamls_spec.rb new file mode 100644 index 00000000000..f399c3e310e --- /dev/null +++ b/spec/requests/api/ci/runner/jobs_request_yamls_spec.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_category: :continuous_integration do + include StubGitlabCalls + + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project, :repository, shared_runners_enabled: false) } + let_it_be(:runner) { create(:ci_runner, :project, projects: [project]) } + + let(:user_agent) { 'gitlab-runner 9.0.0 (9-0-stable; go1.7.4; linux/amd64)' } + + before_all do + project.add_maintainer(user) + end + + Dir[Rails.root.join("spec/requests/api/ci/runner/yamls/*.yml")].each do |yml_file| + context "for #{File.basename(yml_file)}" do + let(:yaml_content) { YAML.load_file(yml_file) } + let(:gitlab_ci_yml) { yaml_content.fetch("gitlab_ci") } + let(:request_response) { yaml_content.fetch("request_response") } + + it 'runs a job' do + stub_ci_pipeline_yaml_file(YAML.dump(gitlab_ci_yml)) + + pipeline_response = create_pipeline! + expect(pipeline_response).to be_success, pipeline_response.message + expect(pipeline_response.payload).to be_created_successfully + expect(pipeline_response.payload.builds).to be_one + + build = pipeline_response.payload.builds.first + + process_pipeline!(pipeline_response.payload) + expect(build.reload).to be_pending + + request_job(runner.token) + expect(response).to have_gitlab_http_status(:created) + expect(response.headers['Content-Type']).to eq('application/json') + expect(json_response).to include('id' => build.id, 'token' => build.token) + expect(json_response).to include(request_response) + end + end + end + + def create_pipeline! + params = { ref: 'master', + before: '00000000', + after: project.commit.id, + commits: [{ message: 'some commit' }] } + + Ci::CreatePipelineService.new(project, user, params).execute(:push) + end + + def process_pipeline!(pipeline) + PipelineProcessWorker.new.perform(pipeline.id) + end + + def request_job(token, **params) + new_params = params.merge(token: token) + post api('/jobs/request'), params: new_params.to_json, + headers: { 'User-Agent' => user_agent, 'Content-Type': 'application/json' } + end +end diff --git a/spec/requests/api/ci/runner/runners_delete_spec.rb b/spec/requests/api/ci/runner/runners_delete_spec.rb index d1488828bad..61420afd578 100644 --- a/spec/requests/api/ci/runner/runners_delete_spec.rb +++ b/spec/requests/api/ci/runner/runners_delete_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_category: :runner_fleet do +RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_category: :fleet_visibility do include StubGitlabCalls include RedisHelpers include WorkhorseHelpers diff --git a/spec/requests/api/ci/runner/runners_post_spec.rb b/spec/requests/api/ci/runner/runners_post_spec.rb index 1490172d1c3..748efe3cd54 100644 --- a/spec/requests/api/ci/runner/runners_post_spec.rb +++ b/spec/requests/api/ci/runner/runners_post_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_category: :runner_fleet do +RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_category: :fleet_visibility do describe '/api/v4/runners' do describe 'POST /api/v4/runners' do it_behaves_like 'runner migrations backoff' do diff --git a/spec/requests/api/ci/runner/runners_reset_spec.rb b/spec/requests/api/ci/runner/runners_reset_spec.rb index 03cb6238fc1..92de1276dbb 100644 --- a/spec/requests/api/ci/runner/runners_reset_spec.rb +++ b/spec/requests/api/ci/runner/runners_reset_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_category: :runner_fleet do +RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_category: :fleet_visibility do include StubGitlabCalls include RedisHelpers include WorkhorseHelpers diff --git a/spec/requests/api/ci/runner/yamls/README.md b/spec/requests/api/ci/runner/yamls/README.md new file mode 100644 index 00000000000..db8ef51ff9c --- /dev/null +++ b/spec/requests/api/ci/runner/yamls/README.md @@ -0,0 +1,15 @@ +# .gitlab-ci.yml end-to-end tests + +The purpose of this folder is to provide a single job `.gitlab-ci.yml` +that will be validated against end-to-end response that is send to runner. + +This allows to easily test end-to-end all CI job transformation that +and impact on how such job is rendered to be executed by the GitLab Runner. + +```yaml +gitlab_ci: + # .gitlab-ci.yml to stub + +request_response: + # exact payload that is checked as returned by `/api/v4/jobs/request` +``` diff --git a/spec/requests/api/ci/runner/yamls/image-basic.yml b/spec/requests/api/ci/runner/yamls/image-basic.yml new file mode 100644 index 00000000000..0c01dbc6e8b --- /dev/null +++ b/spec/requests/api/ci/runner/yamls/image-basic.yml @@ -0,0 +1,19 @@ +gitlab_ci: + rspec: + image: alpine:latest + script: echo Hello World + +request_response: + image: + name: alpine:latest + entrypoint: null + executor_opts: {} + ports: [] + pull_policy: null + steps: + - name: script + script: ["echo Hello World"] + timeout: 3600 + when: on_success + allow_failure: false + services: [] diff --git a/spec/requests/api/ci/runner/yamls/image-executor_opts-platform.yml b/spec/requests/api/ci/runner/yamls/image-executor_opts-platform.yml new file mode 100644 index 00000000000..62e301f2e9a --- /dev/null +++ b/spec/requests/api/ci/runner/yamls/image-executor_opts-platform.yml @@ -0,0 +1,25 @@ +gitlab_ci: + rspec: + image: + name: alpine:latest + docker: + platform: amd64 + script: echo Hello World + +request_response: + image: + name: alpine:latest + entrypoint: null + executor_opts: + docker: + platform: amd64 + ports: [] + pull_policy: null + steps: + - name: script + script: ["echo Hello World"] + timeout: 3600 + when: on_success + allow_failure: false + services: [] + diff --git a/spec/requests/api/ci/runner/yamls/service-basic.yml b/spec/requests/api/ci/runner/yamls/service-basic.yml new file mode 100644 index 00000000000..5438837c496 --- /dev/null +++ b/spec/requests/api/ci/runner/yamls/service-basic.yml @@ -0,0 +1,23 @@ +gitlab_ci: + rspec: + services: + - docker:dind + script: echo Hello World + +request_response: + image: null + steps: + - name: script + script: ["echo Hello World"] + timeout: 3600 + when: on_success + allow_failure: false + services: + - name: docker:dind + alias: null + command: null + entrypoint: null + executor_opts: {} + ports: [] + pull_policy: null + variables: [] diff --git a/spec/requests/api/ci/runner/yamls/service-executor_opts-platform.yml b/spec/requests/api/ci/runner/yamls/service-executor_opts-platform.yml new file mode 100644 index 00000000000..6483d749c45 --- /dev/null +++ b/spec/requests/api/ci/runner/yamls/service-executor_opts-platform.yml @@ -0,0 +1,27 @@ +gitlab_ci: + rspec: + services: + - name: docker:dind + docker: + platform: amd64 + script: echo Hello World + +request_response: + image: null + steps: + - name: script + script: ["echo Hello World"] + timeout: 3600 + when: on_success + allow_failure: false + services: + - name: docker:dind + alias: null + command: null + entrypoint: null + executor_opts: + docker: + platform: amd64 + ports: [] + pull_policy: null + variables: [] diff --git a/spec/requests/api/ci/runner/yamls/service-variables.yml b/spec/requests/api/ci/runner/yamls/service-variables.yml new file mode 100644 index 00000000000..c8e4dde674b --- /dev/null +++ b/spec/requests/api/ci/runner/yamls/service-variables.yml @@ -0,0 +1,30 @@ +gitlab_ci: + rspec: + services: + - name: docker:dind + variables: + DOCKER_HOST: tcp://docker:2375 + DOCKER_DRIVER: overlay2 + script: echo Hello World + +request_response: + image: null + steps: + - name: script + script: ["echo Hello World"] + timeout: 3600 + when: on_success + allow_failure: false + services: + - name: docker:dind + alias: null + command: null + entrypoint: null + executor_opts: {} + ports: [] + pull_policy: null + variables: + - key: DOCKER_HOST + value: tcp://docker:2375 + - key: DOCKER_DRIVER + value: overlay2 diff --git a/spec/requests/api/ci/runners_reset_registration_token_spec.rb b/spec/requests/api/ci/runners_reset_registration_token_spec.rb index 98edde93e95..0b6a6abf419 100644 --- a/spec/requests/api/ci/runners_reset_registration_token_spec.rb +++ b/spec/requests/api/ci/runners_reset_registration_token_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do +RSpec.describe API::Ci::Runners, feature_category: :fleet_visibility do let_it_be(:admin_mode) { false } subject { post api("#{prefix}/runners/reset_registration_token", user, admin_mode: admin_mode) } diff --git a/spec/requests/api/ci/runners_spec.rb b/spec/requests/api/ci/runners_spec.rb index ba80684e89e..187880e16a4 100644 --- a/spec/requests/api/ci/runners_spec.rb +++ b/spec/requests/api/ci/runners_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe API::Ci::Runners, :aggregate_failures, feature_category: :runner_fleet do +RSpec.describe API::Ci::Runners, :aggregate_failures, feature_category: :fleet_visibility do let_it_be(:admin) { create(:user, :admin) } let_it_be(:user) { create(:user) } let_it_be(:user2) { create(:user) } diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb index 6a112918288..4ec5d195ff8 100644 --- a/spec/requests/api/commits_spec.rb +++ b/spec/requests/api/commits_spec.rb @@ -44,6 +44,30 @@ RSpec.describe API::Commits, feature_category: :source_code_management do expect(response).to include_limited_pagination_headers end + + describe "commit trailers" do + it "doesn't include the commit trailers by default" do + get api(route, current_user), params: { page: 2 } + + commit_with_trailers = json_response.find { |c| c["trailers"].any? } + + expect(commit_with_trailers).to be_nil + expect(json_response.first["trailers"]).to eq({}) + end + + it "does include the commit trailers when specified in the params" do + # Test repo commits with trailers are further down the list, so use a + # higher page number. + get api(route, current_user), params: { page: 2, trailers: true } + + commit_with_trailers = json_response.find { |c| c["trailers"].any? } + + expect(commit_with_trailers["trailers"]).to be_a(Hash) + expect(commit_with_trailers["extended_trailers"]).to be_a(Hash) + expect(commit_with_trailers["trailers"].size).to be > 0 + expect(commit_with_trailers["extended_trailers"].size).to be > 0 + end + end end context 'when unauthenticated', 'and project is public' do @@ -426,6 +450,10 @@ RSpec.describe API::Commits, feature_category: :source_code_management do expect(commit['trailers']).to eq( 'Signed-off-by' => 'Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>' ) + + expect(commit['extended_trailers']).to eq( + 'Signed-off-by' => ['Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>'] + ) end end end diff --git a/spec/requests/api/deploy_tokens_spec.rb b/spec/requests/api/deploy_tokens_spec.rb index c0e36bf03bf..2f215cd5bd1 100644 --- a/spec/requests/api/deploy_tokens_spec.rb +++ b/spec/requests/api/deploy_tokens_spec.rb @@ -395,6 +395,7 @@ RSpec.describe API::DeployTokens, :aggregate_failures, feature_category: :contin expect(json_response['scopes']).to eq(['read_repository']) expect(json_response['username']).to eq('Bar') expect(json_response['expires_at'].to_time.to_i).to eq(expires_time.to_i) + expect(json_response['token']).to match(/gldt-[A-Za-z0-9_-]{20}/) end context 'with no optional params given' do diff --git a/spec/requests/api/deployments_spec.rb b/spec/requests/api/deployments_spec.rb index 41c5847e940..5a8e1649e75 100644 --- a/spec/requests/api/deployments_spec.rb +++ b/spec/requests/api/deployments_spec.rb @@ -161,24 +161,56 @@ RSpec.describe API::Deployments, feature_category: :continuous_delivery do end describe 'GET /projects/:id/deployments/:deployment_id' do - let(:project) { deployment.environment.project } - let!(:deployment) { create(:deployment, :success) } + let_it_be(:deployment_with_bridge) { create(:deployment, :with_bridge, :success) } + let_it_be(:deployment_with_build) { create(:deployment, :success) } context 'as a member of the project' do - it 'returns the projects deployment' do - get api("/projects/#{project.id}/deployments/#{deployment.id}", user) + shared_examples "returns project deployments" do + let(:project) { deployment.environment.project } - expect(response).to have_gitlab_http_status(:ok) - expect(json_response['sha']).to match /\A\h{40}\z/ - expect(json_response['id']).to eq(deployment.id) + it 'returns the expected response' do + get api("/projects/#{project.id}/deployments/#{deployment.id}", user) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['sha']).to match /\A\h{40}\z/ + expect(json_response['id']).to eq(deployment.id) + end + end + + context 'when the deployable is a build' do + it_behaves_like 'returns project deployments' do + let!(:deployment) { deployment_with_build } + end + end + + context 'when the deployable is a bridge' do + it_behaves_like 'returns project deployments' do + let!(:deployment) { deployment_with_bridge } + end end end context 'as non member' do - it 'returns a 404 status code' do - get api("/projects/#{project.id}/deployments/#{deployment.id}", non_member) + shared_examples 'deployment will not be found' do + let(:project) { deployment.environment.project } - expect(response).to have_gitlab_http_status(:not_found) + it 'returns a 404 status code' do + get api("/projects/#{project.id}/deployments/#{deployment.id}", non_member) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'when the deployable is a build' do + it_behaves_like 'deployment will not be found' do + let!(:deployment) { deployment_with_build } + end + end + + context 'when the deployable is a bridge' do + it_behaves_like 'deployment will not be found' do + let!(:deployment) { deployment_with_bridge } + end end end end @@ -229,6 +261,22 @@ RSpec.describe API::Deployments, feature_category: :continuous_delivery do expect(json_response['environment']['name']).to eq('production') end + it 'errors when creating a deployment with an invalid ref', :aggregate_failures do + post( + api("/projects/#{project.id}/deployments", user), + params: { + environment: 'production', + sha: sha, + ref: 'doesnotexist', + tag: false, + status: 'success' + } + ) + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['message']).to eq({ "ref" => ["The branch or tag does not exist"] }) + end + it 'errors when creating a deployment with an invalid name' do post( api("/projects/#{project.id}/deployments", user), diff --git a/spec/requests/api/environments_spec.rb b/spec/requests/api/environments_spec.rb index 498e030da0b..aed97bcfe7c 100644 --- a/spec/requests/api/environments_spec.rb +++ b/spec/requests/api/environments_spec.rb @@ -374,32 +374,71 @@ RSpec.describe API::Environments, feature_category: :continuous_delivery do end describe 'GET /projects/:id/environments/:environment_id' do + let_it_be(:bridge_job) { create(:ci_bridge, :running, project: project, user: user) } + let_it_be(:build_job) { create(:ci_build, :running, project: project, user: user) } + context 'as member of the project' do - it 'returns project environments' do - create(:deployment, :success, project: project, environment: environment) + shared_examples "returns project environments" do + it 'returns expected response' do + create( + :deployment, + :success, + project: project, + environment: environment, + deployable: job + ) + + get api("/projects/#{project.id}/environments/#{environment.id}", user) - get api("/projects/#{project.id}/environments/#{environment.id}", user) + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema('public_api/v4/environment') + expect(json_response['last_deployment']).to be_present + end + end - expect(response).to have_gitlab_http_status(:ok) - expect(response).to match_response_schema('public_api/v4/environment') - expect(json_response['last_deployment']).to be_present + context "when the deployable is a bridge" do + it_behaves_like "returns project environments" do + let(:job) { bridge_job } + end + + # No test for Ci::Bridge JOB-TOKEN auth because it doesn't implement the `.token` method. end - it 'returns 200 HTTP status when using JOB-TOKEN auth' do - job = create(:ci_build, :running, project: project, user: user) + context "when the deployable is a build" do + it_behaves_like "returns project environments" do + let(:job) { build_job } + end - get api("/projects/#{project.id}/environments/#{environment.id}"), - params: { job_token: job.token } + it 'returns 200 HTTP status when using JOB-TOKEN auth' do + get( + api("/projects/#{project.id}/environments/#{environment.id}"), + params: { job_token: build_job.token } + ) - expect(response).to have_gitlab_http_status(:ok) + expect(response).to have_gitlab_http_status(:ok) + end end end context 'as non member' do - it 'returns a 404 status code' do - get api("/projects/#{project.id}/environments/#{environment.id}", non_member) + shared_examples 'environment will not be found' do + it 'returns a 404 status code' do + get api("/projects/#{project.id}/environments/#{environment.id}", non_member) - expect(response).to have_gitlab_http_status(:not_found) + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context "when the deployable is a bridge" do + it_behaves_like "environment will not be found" do + let(:job) { bridge_job } + end + end + + context "when the deployable is a build" do + it_behaves_like "environment will not be found" do + let(:job) { build_job } + end end end end diff --git a/spec/requests/api/events_spec.rb b/spec/requests/api/events_spec.rb index f884aaabb53..9da32e6cd37 100644 --- a/spec/requests/api/events_spec.rb +++ b/spec/requests/api/events_spec.rb @@ -3,13 +3,13 @@ require 'spec_helper' RSpec.describe API::Events, feature_category: :user_profile do - let(:user) { create(:user) } - let(:non_member) { create(:user) } - let(:private_project) { create(:project, :private, creator_id: user.id, namespace: user.namespace) } - let(:closed_issue) { create(:closed_issue, project: private_project, author: user) } - let!(:closed_issue_event) { create(:event, :closed, project: private_project, author: user, target: closed_issue, created_at: Date.new(2016, 12, 30)) } - let(:closed_issue2) { create(:closed_issue, project: private_project, author: non_member) } - let!(:closed_issue_event2) { create(:event, :closed, project: private_project, author: non_member, target: closed_issue2, created_at: Date.new(2016, 12, 30)) } + let_it_be(:user) { create(:user) } + let_it_be(:non_member) { create(:user) } + let_it_be(:private_project) { create(:project, :private, creator_id: user.id, namespace: user.namespace) } + let_it_be(:closed_issue) { create(:closed_issue, project: private_project, author: user) } + let_it_be(:closed_issue_event) { create(:event, :closed, project: private_project, author: user, target: closed_issue, created_at: Date.new(2016, 12, 30)) } + let_it_be(:closed_issue2) { create(:closed_issue, project: private_project, author: non_member) } + let_it_be(:closed_issue_event2) { create(:event, :closed, project: private_project, author: non_member, target: closed_issue2, created_at: Date.new(2016, 12, 30)) } describe 'GET /events' do context 'when unauthenticated' do diff --git a/spec/requests/api/graphql/abuse_report_spec.rb b/spec/requests/api/graphql/abuse_report_spec.rb index f74b1fb4061..8ab0e92d838 100644 --- a/spec/requests/api/graphql/abuse_report_spec.rb +++ b/spec/requests/api/graphql/abuse_report_spec.rb @@ -25,11 +25,7 @@ RSpec.describe 'Querying an Abuse Report', feature_category: :insider_threat do it 'returns all fields' do expect(abuse_report_data).to include( - 'id' => global_id, - 'userPermissions' => { - 'readAbuseReport' => true, - 'createNote' => true - } + 'id' => global_id ) end end diff --git a/spec/requests/api/graphql/ci/catalog/resource_spec.rb b/spec/requests/api/graphql/ci/catalog/resource_spec.rb index fce773f320b..9fe73e7ba45 100644 --- a/spec/requests/api/graphql/ci/catalog/resource_spec.rb +++ b/spec/requests/api/graphql/ci/catalog/resource_spec.rb @@ -15,11 +15,14 @@ RSpec.describe 'Query.ciCatalogResource', feature_category: :pipeline_compositio description: 'A simple component', namespace: namespace, star_count: 1, - files: { 'README.md' => '[link](README.md)' } + files: { + 'README.md' => '[link](README.md)', + 'templates/secret-detection.yml' => "spec:\n inputs:\n website:\n---\nimage: alpine_1" + } ) end - let_it_be(:resource) { create(:ci_catalog_resource, project: project) } + let_it_be(:resource) { create(:ci_catalog_resource, :published, project: project) } let(:query) do <<~GQL @@ -33,10 +36,12 @@ RSpec.describe 'Query.ciCatalogResource', feature_category: :pipeline_compositio subject(:post_query) { post_graphql(query, current_user: user) } + before_all do + namespace.add_developer(user) + end + 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( @@ -45,7 +50,6 @@ RSpec.describe 'Query.ciCatalogResource', feature_category: :pipeline_compositio 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" ) @@ -64,15 +68,94 @@ RSpec.describe 'Query.ciCatalogResource', feature_category: :pipeline_compositio end end - describe 'versions' do - before_all do - namespace.add_developer(user) + describe 'components' do + let(:query) do + <<~GQL + query { + ciCatalogResource(id: "#{resource.to_global_id}") { + id + versions { + nodes { + id + components { + nodes { + id + name + path + inputs { + name + default + required + } + } + } + } + } + } + } + GQL end - before do - stub_licensed_features(ci_namespace_catalog: true) + context 'when the catalog resource has components' do + let_it_be(:inputs) do + { + website: nil, + environment: { + default: 'test' + }, + tags: { + type: 'array' + } + } + end + + let_it_be(:version) do + create(:release, :with_catalog_resource_version, project: project).catalog_resource_version + end + + let_it_be(:components) do + create_list(:ci_catalog_resource_component, 2, version: version, inputs: inputs, path: 'templates/comp.yml') + end + + it 'returns the resource with the component data' do + post_query + + expect(graphql_data_at(:ciCatalogResource)).to match(a_graphql_entity_for(resource)) + + expect(graphql_data_at(:ciCatalogResource, :versions, :nodes, :components, :nodes)).to contain_exactly( + a_graphql_entity_for( + components.first, + name: components.first.name, + path: components.first.path, + inputs: [ + a_graphql_entity_for( + name: 'tags', + default: nil, + required: true + ), + a_graphql_entity_for( + name: 'website', + default: nil, + required: true + ), + a_graphql_entity_for( + name: 'environment', + default: 'test', + required: false + ) + ] + ), + a_graphql_entity_for( + components.last, + name: components.last.name, + path: components.last.path + ) + ) + end end + end + describe 'versions' do let(:query) do <<~GQL query { @@ -82,6 +165,7 @@ RSpec.describe 'Query.ciCatalogResource', feature_category: :pipeline_compositio nodes { id tagName + tagPath releasedAt author { id @@ -99,11 +183,13 @@ RSpec.describe 'Query.ciCatalogResource', feature_category: :pipeline_compositio 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) + create(:release, :with_catalog_resource_version, project: project, released_at: '2023-01-01T00:00:00Z', + author: author).catalog_resource_version end let_it_be(:version2) do - create(:release, project: project, released_at: '2023-02-01T00:00:00Z', author: author) + create(:release, :with_catalog_resource_version, project: project, released_at: '2023-02-01T00:00:00Z', + author: author).catalog_resource_version end it 'returns the resource with the versions data' do @@ -116,13 +202,15 @@ RSpec.describe 'Query.ciCatalogResource', feature_category: :pipeline_compositio expect(graphql_data_at(:ciCatalogResource, :versions, :nodes)).to contain_exactly( a_graphql_entity_for( version1, - tagName: version1.tag, + tagName: version1.name, + tagPath: project_tag_path(project, version1.name), releasedAt: version1.released_at, author: a_graphql_entity_for(author, :name) ), a_graphql_entity_for( version2, - tagName: version2.tag, + tagName: version2.name, + tagPath: project_tag_path(project, version2.name), releasedAt: version2.released_at, author: a_graphql_entity_for(author, :name) ) @@ -142,14 +230,6 @@ RSpec.describe 'Query.ciCatalogResource', feature_category: :pipeline_compositio 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 { @@ -158,6 +238,7 @@ RSpec.describe 'Query.ciCatalogResource', feature_category: :pipeline_compositio latestVersion { id tagName + tagPath releasedAt author { id @@ -174,12 +255,14 @@ RSpec.describe 'Query.ciCatalogResource', feature_category: :pipeline_compositio 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) + create(:release, :with_catalog_resource_version, project: project, released_at: '2023-02-01T00:00:00Z', + author: author).catalog_resource_version end before_all do - # Previous version of the project - create(:release, project: project, released_at: '2023-01-01T00:00:00Z', author: author) + # Previous version of the catalog resource + create(:release, :with_catalog_resource_version, project: project, released_at: '2023-01-01T00:00:00Z', + author: author) end it 'returns the resource with the latest version data' do @@ -190,7 +273,8 @@ RSpec.describe 'Query.ciCatalogResource', feature_category: :pipeline_compositio resource, latestVersion: a_graphql_entity_for( latest_version, - tagName: latest_version.tag, + tagName: latest_version.name, + tagPath: project_tag_path(project, latest_version.name), releasedAt: latest_version.released_at, author: a_graphql_entity_for(author, :name) ) @@ -210,47 +294,7 @@ RSpec.describe 'Query.ciCatalogResource', feature_category: :pipeline_compositio 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 @@ -266,8 +310,6 @@ RSpec.describe 'Query.ciCatalogResource', feature_category: :pipeline_compositio create(:issue, :opened, project: project) create(:issue, :opened, project: project) - namespace.add_developer(user) - post_query expect(graphql_data_at(:ciCatalogResource)).to match( @@ -279,8 +321,6 @@ RSpec.describe 'Query.ciCatalogResource', feature_category: :pipeline_compositio 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( @@ -294,10 +334,6 @@ RSpec.describe 'Query.ciCatalogResource', feature_category: :pipeline_compositio 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 @@ -312,8 +348,6 @@ RSpec.describe 'Query.ciCatalogResource', feature_category: :pipeline_compositio 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( @@ -325,8 +359,6 @@ RSpec.describe 'Query.ciCatalogResource', feature_category: :pipeline_compositio 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( diff --git a/spec/requests/api/graphql/ci/catalog/resources_spec.rb b/spec/requests/api/graphql/ci/catalog/resources_spec.rb index 7c955a1202c..49a3f3be1d7 100644 --- a/spec/requests/api/graphql/ci/catalog/resources_spec.rb +++ b/spec/requests/api/graphql/ci/catalog/resources_spec.rb @@ -29,8 +29,11 @@ RSpec.describe 'Query.ciCatalogResources', feature_category: :pipeline_compositi ) 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_it_be(:resource1) do + create(:ci_catalog_resource, :published, project: project1, latest_released_at: '2023-01-01T00:00:00Z') + end + + let_it_be(:public_resource) { create(:ci_catalog_resource, :published, project: public_project) } let(:query) do <<~GQL @@ -44,7 +47,6 @@ RSpec.describe 'Query.ciCatalogResources', feature_category: :pipeline_compositi webPath latestReleasedAt starCount - forksCount readmeHtml } } @@ -58,11 +60,11 @@ RSpec.describe 'Query.ciCatalogResources', feature_category: :pipeline_compositi it do ctx = { current_user: user } - control_count = ActiveRecord::QueryRecorder.new do + control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do run_with_clean_state(query, context: ctx) end - create(:ci_catalog_resource, project: project2) + create(:ci_catalog_resource, :published, project: project2) expect do run_with_clean_state(query, context: ctx) @@ -83,7 +85,6 @@ RSpec.describe 'Query.ciCatalogResources', feature_category: :pipeline_compositi 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 ), @@ -121,7 +122,7 @@ RSpec.describe 'Query.ciCatalogResources', feature_category: :pipeline_compositi end it 'limits the request to 1 resource at a time' do - create(:ci_catalog_resource, project: project2) + create(:ci_catalog_resource, :published, project: project2) post_query @@ -135,11 +136,13 @@ RSpec.describe 'Query.ciCatalogResources', feature_category: :pipeline_compositi 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) + create(:release, :with_catalog_resource_version, project: project1, released_at: '2023-02-01T00:00:00Z', + author: author1).catalog_resource_version end let_it_be(:latest_version2) do - create(:release, project: public_project, released_at: '2023-02-01T00:00:00Z', author: author2) + create(:release, :with_catalog_resource_version, project: public_project, released_at: '2023-02-01T00:00:00Z', + author: author2).catalog_resource_version end let(:query) do @@ -167,9 +170,11 @@ RSpec.describe 'Query.ciCatalogResources', feature_category: :pipeline_compositi 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) + # Previous versions of the catalog resources + create(:release, :with_catalog_resource_version, project: project1, released_at: '2023-01-01T00:00:00Z', + author: author1) + create(:release, :with_catalog_resource_version, project: public_project, released_at: '2023-01-01T00:00:00Z', + author: author2) end it 'returns all resources with the latest version data' do @@ -180,7 +185,7 @@ RSpec.describe 'Query.ciCatalogResources', feature_category: :pipeline_compositi resource1, latestVersion: a_graphql_entity_for( latest_version1, - tagName: latest_version1.tag, + tagName: latest_version1.name, releasedAt: latest_version1.released_at, author: a_graphql_entity_for(author1, :name) ) @@ -189,7 +194,7 @@ RSpec.describe 'Query.ciCatalogResources', feature_category: :pipeline_compositi public_resource, latestVersion: a_graphql_entity_for( latest_version2, - tagName: latest_version2.tag, + tagName: latest_version2.name, releasedAt: latest_version2.released_at, author: a_graphql_entity_for(author2, :name) ) @@ -197,43 +202,7 @@ RSpec.describe 'Query.ciCatalogResources', feature_category: :pipeline_compositi ) 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 + it_behaves_like 'avoids N+1 queries' end describe 'openIssuesCount' do @@ -326,8 +295,8 @@ RSpec.describe 'Query.ciCatalogResources', feature_category: :pipeline_compositi 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) + resource2 = create(:ci_catalog_resource, :published, project: project2) + _resource_in_another_namespace = create(:ci_catalog_resource, :published) post_query @@ -338,7 +307,6 @@ RSpec.describe 'Query.ciCatalogResources', feature_category: :pipeline_compositi icon: project2.avatar_path, webPath: "/#{project2.full_path}", starCount: project2.star_count, - forksCount: project2.forks_count, readmeHtml: '', latestReleasedAt: resource2.latest_released_at ) diff --git a/spec/requests/api/graphql/ci/runner_spec.rb b/spec/requests/api/graphql/ci/runner_spec.rb index 6f1eb77fa9b..8262640b283 100644 --- a/spec/requests/api/graphql/ci/runner_spec.rb +++ b/spec/requests/api/graphql/ci/runner_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe 'Query.runner(id)', :freeze_time, feature_category: :runner_fleet do +RSpec.describe 'Query.runner(id)', :freeze_time, feature_category: :fleet_visibility do include GraphqlHelpers using RSpec::Parameterized::TableSyntax diff --git a/spec/requests/api/graphql/ci/runner_web_url_edge_spec.rb b/spec/requests/api/graphql/ci/runner_web_url_edge_spec.rb index 76e2dda4ce2..8e3efb67ee5 100644 --- a/spec/requests/api/graphql/ci/runner_web_url_edge_spec.rb +++ b/spec/requests/api/graphql/ci/runner_web_url_edge_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true require 'spec_helper' -RSpec.describe 'RunnerWebUrlEdge', feature_category: :runner_fleet do +RSpec.describe 'RunnerWebUrlEdge', feature_category: :fleet_visibility do include GraphqlHelpers describe 'inside a Query.group' do diff --git a/spec/requests/api/graphql/ci/runners_spec.rb b/spec/requests/api/graphql/ci/runners_spec.rb index 0e2712d742d..0fe14bef778 100644 --- a/spec/requests/api/graphql/ci/runners_spec.rb +++ b/spec/requests/api/graphql/ci/runners_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true require 'spec_helper' -RSpec.describe 'Query.runners', feature_category: :runner_fleet do +RSpec.describe 'Query.runners', feature_category: :fleet_visibility do include GraphqlHelpers let_it_be(:current_user) { create_default(:user, :admin) } @@ -35,17 +35,19 @@ RSpec.describe 'Query.runners', feature_category: :runner_fleet do end context 'with filters' do - shared_examples 'a working graphql query returning expected runner' do + shared_examples 'a working graphql query returning expected runners' do it_behaves_like 'a working graphql query' do before do post_graphql(query, current_user: current_user) end end - it 'returns expected runner' do + it 'returns expected runners' do post_graphql(query, current_user: current_user) - expect(runners_graphql_data['nodes']).to contain_exactly(a_graphql_entity_for(expected_runner)) + expect(runners_graphql_data['nodes']).to contain_exactly( + *Array(expected_runners).map { |expected_runner| a_graphql_entity_for(expected_runner) } + ) end it 'does not execute more queries per runner', :aggregate_failures do @@ -95,24 +97,36 @@ RSpec.describe 'Query.runners', feature_category: :runner_fleet do let(:runner_type) { 'INSTANCE_TYPE' } let(:status) { 'ACTIVE' } - let!(:expected_runner) { instance_runner } + let(:expected_runners) { instance_runner } - it_behaves_like 'a working graphql query returning expected runner' + it_behaves_like 'a working graphql query returning expected runners' 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 } + let(:expected_runners) { project_runner } - it_behaves_like 'a working graphql query returning expected runner' + it_behaves_like 'a working graphql query returning expected runners' end end 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_it_be(:runner_15_10_1) { create_ci_runner(version: '15.10.1') } + + let_it_be(:runner_15_11_0) { create_ci_runner(version: '15.11.0') } + let_it_be(:runner_15_11_1) { create_ci_runner(version: '15.11.1') } + + let_it_be(:runner_16_1_0) { create_ci_runner(version: '16.1.0') } + + let(:fields) do + <<~QUERY + nodes { + id + } + QUERY + end let(:query) do %( @@ -124,12 +138,44 @@ RSpec.describe 'Query.runners', feature_category: :runner_fleet do ) end - context 'version_prefix is "15."' do + context 'when version_prefix is "15."' do let(:version_prefix) { '15.' } - let!(:expected_runner) { version_runner } + it_behaves_like 'a working graphql query returning expected runners' do + let(:expected_runners) { [runner_15_10_1, runner_15_11_0, runner_15_11_1] } + end + end + + context 'when version_prefix is "15.11."' do + let(:version_prefix) { '15.11.' } - it_behaves_like 'a working graphql query returning expected runner' + it_behaves_like 'a working graphql query returning expected runners' do + let(:expected_runners) { [runner_15_11_0, runner_15_11_1] } + end + end + + context 'when version_prefix is "15.11.0"' do + let(:version_prefix) { '15.11.0' } + + it_behaves_like 'a working graphql query returning expected runners' do + let(:expected_runners) { runner_15_11_0 } + end + end + + context 'when version_prefix is not digits' do + let(:version_prefix) { 'a.b' } + + it_behaves_like 'a working graphql query returning expected runners' do + let(:expected_runners) do + [instance_runner, project_runner, runner_15_10_1, runner_15_11_0, runner_15_11_1, runner_16_1_0] + end + end + end + + def create_ci_runner(args = {}, version:) + create(:ci_runner, :project, **args).tap do |runner| + create(:ci_runner_machine, runner: runner, version: version) + end end end end diff --git a/spec/requests/api/graphql/container_repository/container_repository_details_spec.rb b/spec/requests/api/graphql/container_repository/container_repository_details_spec.rb index 20277c7e27b..2acdd509355 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 @@ -11,11 +11,12 @@ RSpec.describe 'container repository details', feature_category: :container_regi let_it_be_with_reload(:project) { create(:project) } let_it_be_with_reload(:container_repository) { create(:container_repository, project: project) } + let(:excluded) { %w[pipeline size agentConfigurations iterations iterationCadences productAnalyticsState] } let(:query) do graphql_query_for( 'containerRepository', { id: container_repository_global_id }, - all_graphql_fields_for('ContainerRepositoryDetails', excluded: %w[pipeline size]) + all_graphql_fields_for('ContainerRepositoryDetails', excluded: excluded, max_depth: 4) ) end diff --git a/spec/requests/api/graphql/custom_emoji_query_spec.rb b/spec/requests/api/graphql/custom_emoji_query_spec.rb index 1858ea831dd..c89ad0002b4 100644 --- a/spec/requests/api/graphql/custom_emoji_query_spec.rb +++ b/spec/requests/api/graphql/custom_emoji_query_spec.rb @@ -35,14 +35,14 @@ RSpec.describe 'getting custom emoji within namespace', feature_category: :share expect(graphql_data['group']['customEmoji']['nodes'].first['name']).to eq(custom_emoji.name) end - it 'returns nil custom emoji when the custom_emoji feature flag is disabled' do + it 'returns empty array when the custom_emoji feature flag is disabled' do stub_feature_flags(custom_emoji: false) post_graphql(custom_emoji_query(group), current_user: current_user) expect(response).to have_gitlab_http_status(:ok) expect(graphql_data['group']).to be_present - expect(graphql_data['group']['customEmoji']).to be_nil + expect(graphql_data['group']['customEmoji']['nodes']).to eq([]) end it 'returns nil group when unauthorised' do diff --git a/spec/requests/api/graphql/group/issues_spec.rb b/spec/requests/api/graphql/group/issues_spec.rb index 95aeed32558..1da6abf3cac 100644 --- a/spec/requests/api/graphql/group/issues_spec.rb +++ b/spec/requests/api/graphql/group/issues_spec.rb @@ -15,6 +15,8 @@ RSpec.describe 'getting an issue list for a group', feature_category: :team_plan let_it_be(:issue2) { create(:issue, project: project2) } let_it_be(:issue3) { create(:issue, project: project3) } + let_it_be(:group_level_issue) { create(:issue, :epic, :group_level, namespace: group1) } + let(:issue1_gid) { issue1.to_global_id.to_s } let(:issue2_gid) { issue2.to_global_id.to_s } let(:issues_data) { graphql_data['group']['issues']['edges'] } @@ -142,6 +144,40 @@ RSpec.describe 'getting an issue list for a group', feature_category: :team_plan end end + context 'when querying epic types' do + let(:query) do + graphql_query_for( + 'group', + { 'fullPath' => group1.full_path }, + "issues(types: [EPIC]) { #{fields} }" + ) + end + + before_all do + group1.add_developer(current_user) + end + + it 'returns group-level epics' do + post_graphql(query, current_user: current_user) + + expect_graphql_errors_to_be_empty + expect(issues_ids).to contain_exactly(group_level_issue.to_global_id.to_s) + end + + context 'when namespace_level_work_items is disabled' do + before do + stub_feature_flags(namespace_level_work_items: false) + end + + it 'returns no epics' do + post_graphql(query, current_user: current_user) + + expect_graphql_errors_to_be_empty + expect(issues_ids).to be_empty + end + end + end + def issues_ids graphql_dig_at(issues_data, :node, :id) end diff --git a/spec/requests/api/graphql/group/work_item_state_counts_spec.rb b/spec/requests/api/graphql/group/work_item_state_counts_spec.rb new file mode 100644 index 00000000000..2ae623c39f2 --- /dev/null +++ b/spec/requests/api/graphql/group/work_item_state_counts_spec.rb @@ -0,0 +1,107 @@ +# frozen_string_literal: true + +require 'spec_helper' +require 'request_store' + +RSpec.describe 'getting Work Item counts by state', feature_category: :portfolio_management do + include GraphqlHelpers + + let_it_be(:current_user) { create(:user) } + let_it_be(:group) { create(:group, :private) } + let_it_be(:work_item_opened1) { create(:work_item, namespace: group) } + let_it_be(:work_item_opened2) { create(:work_item, namespace: group, author: current_user) } + let_it_be(:work_item_closed1) { create(:work_item, :closed, namespace: group) } + let_it_be(:work_item_closed2) { create(:work_item, :closed, namespace: group) } + + let(:params) { {} } + + subject(:query_counts) { post_graphql(query, current_user: current_user) } + + context 'with work items count data' do + let(:work_item_counts) { graphql_data.dig('group', 'workItemStateCounts') } + + context 'with group permissions' do + before_all do + group.add_developer(current_user) + end + + it_behaves_like 'a working graphql query' do + before do + query_counts + end + end + + it 'returns the correct counts for each state' do + query_counts + + expect(work_item_counts).to eq( + 'all' => 4, + 'opened' => 2, + 'closed' => 2 + ) + end + + context 'when filters are provided' do + context 'when filtering by author username' do + let(:params) { { 'authorUsername' => current_user.username } } + + it 'returns the correct counts for each state' do + query_counts + + expect(work_item_counts).to eq( + 'all' => 1, + 'opened' => 1, + 'closed' => 0 + ) + end + end + + context 'when filtering by search' do + let(:params) { { search: 'foo', in: [:TITLE] } } + + it 'returns an error for filters that are not supported' do + query_counts + + expect(graphql_errors).to contain_exactly( + hash_including('message' => 'Searching is not available for work items at the namespace level yet') + ) + end + end + end + + context 'when the namespace_level_work_items feature flag is disabled' do + before do + stub_feature_flags(namespace_level_work_items: false) + end + + it 'does not return work item counts' do + query_counts + + expect_graphql_errors_to_be_empty + expect(work_item_counts).to be_nil + end + end + end + + context 'without group permissions' do + it 'does not return work item counts' do + query_counts + + expect_graphql_errors_to_be_empty + expect(work_item_counts).to be_nil + end + end + end + + def query(args: params) + fields = <<~QUERY + #{all_graphql_fields_for('WorkItemStateCountsType'.classify)} + QUERY + + graphql_query_for( + 'group', + { 'fullPath' => group.full_path }, + query_graphql_field('workItemStateCounts', args, fields) + ) + end +end diff --git a/spec/requests/api/graphql/group/work_item_types_spec.rb b/spec/requests/api/graphql/group/work_item_types_spec.rb index 791c0fb9524..fbebcdad389 100644 --- a/spec/requests/api/graphql/group/work_item_types_spec.rb +++ b/spec/requests/api/graphql/group/work_item_types_spec.rb @@ -5,56 +5,19 @@ require 'spec_helper' RSpec.describe 'getting a list of work item types for a group', feature_category: :team_planning do include GraphqlHelpers - let_it_be(:developer) { create(:user) } let_it_be(:group) { create(:group, :private) } + let_it_be(:developer) { create(:user).tap { |u| group.add_developer(u) } } - before_all do - group.add_developer(developer) - end - - let(:current_user) { developer } - - let(:fields) do - <<~GRAPHQL - workItemTypes{ - nodes { id name iconName } - } - GRAPHQL - end - - let(:query) do - graphql_query_for( - 'group', - { 'fullPath' => group.full_path }, - fields - ) - end - - context 'when user has access to the group' do - before do - post_graphql(query, current_user: current_user) - end + it_behaves_like 'graphql work item type list request spec' do + let(:current_user) { developer } + let(:parent_key) { :group } - it_behaves_like 'a working graphql query' - - it 'returns all default work item types' do - expect(graphql_data.dig('group', 'workItemTypes', 'nodes')).to match_array( - WorkItems::Type.default.map do |type| - hash_including('id' => type.to_global_id.to_s, 'name' => type.name, 'iconName' => type.icon_name) - end + let(:query) do + graphql_query_for( + 'group', + { 'fullPath' => group.full_path }, + query_nodes('WorkItemTypes', work_item_type_fields) ) end end - - context "when user doesn't have access to the group" do - let(:current_user) { create(:user) } - - before do - post_graphql(query, current_user: current_user) - end - - it 'does not return the group' do - expect(graphql_data).to eq('group' => nil) - end - end end diff --git a/spec/requests/api/graphql/milestone_spec.rb b/spec/requests/api/graphql/milestone_spec.rb index 2cea9fd0408..0dc2eabc3e1 100644 --- a/spec/requests/api/graphql/milestone_spec.rb +++ b/spec/requests/api/graphql/milestone_spec.rb @@ -151,4 +151,18 @@ RSpec.describe 'Querying a Milestone', feature_category: :team_planning do end end end + + context 'for common GraphQL/REST' do + it_behaves_like 'group milestones including ancestors and descendants' + + def query_group_milestone_ids(params) + query = graphql_query_for('group', { 'fullPath' => group.full_path }, + query_graphql_field('milestones', params, query_graphql_path([:nodes], :id)) + ) + + post_graphql(query, current_user: current_user) + + graphql_data_at(:group, :milestones, :nodes).pluck('id').map { |gid| GlobalID.parse(gid).model_id.to_i } + end + end end 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 316b0f3755d..808dcefb84d 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 @@ -26,7 +26,7 @@ RSpec.describe 'Deleting Sidekiq jobs', :clean_gitlab_redis_queues, feature_cate context 'when the user is an admin' do let(:current_user) { admin } - context 'valid request' do + context 'when valid request' do around do |example| Sidekiq::Queue.new(queue).clear Sidekiq::Testing.disable!(&example) @@ -40,7 +40,7 @@ RSpec.describe 'Deleting Sidekiq jobs', :clean_gitlab_redis_queues, feature_cate 'args' => args, 'meta.user' => user.username ) - raise 'Not enqueued!' if Sidekiq::Queue.new(queue).size.zero? + raise 'Not enqueued!' if Sidekiq::Queue.new(queue).size.zero? # rubocop:disable Style/ZeroLengthPredicate -- Sidekiq::Queue doesn't implement #blank? or #empty? end it 'returns info about the deleted jobs' do diff --git a/spec/requests/api/graphql/mutations/branch_rules/update_spec.rb b/spec/requests/api/graphql/mutations/branch_rules/update_spec.rb new file mode 100644 index 00000000000..14874bdfaa8 --- /dev/null +++ b/spec/requests/api/graphql/mutations/branch_rules/update_spec.rb @@ -0,0 +1,95 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'BranchRuleUpdate', feature_category: :source_code_management do + include GraphqlHelpers + + let_it_be(:project) { create(:project, :public) } + let_it_be(:user) { create(:user) } + let!(:branch_rule_1) { create(:protected_branch, project: project, name: name_1) } + let!(:branch_rule_2) { create(:protected_branch, project: project, name: name_2) } + let(:name_1) { "name_1" } + let(:name_2) { "name_2" } + let(:new_name) { "new name" } + let(:id) { branch_rule_1.to_global_id } + let(:project_path) { project.full_path } + let(:name) { new_name } + let(:params) do + { + id: id, + project_path: project_path, + name: name + } + end + + let(:mutation) { graphql_mutation(:branch_rule_update, params) } + + subject(:post_mutation) { post_graphql_mutation(mutation, current_user: user) } + + def mutation_response + graphql_mutation_response(:branch_rule_update) + end + + context 'when the user does not have permission' do + before_all do + project.add_developer(user) + end + + it 'does not update the branch rule' do + expect { post_mutation }.not_to change { branch_rule_1 } + end + end + + context 'when the user can update a branch rules' do + let(:current_user) { user } + + before_all do + project.add_maintainer(user) + end + + it 'updates the protected branch' do + post_mutation + + expect(branch_rule_1.reload.name).to eq(new_name) + end + + it 'returns the updated branch rule' do + post_mutation + + expect(mutation_response).to have_key('branchRule') + expect(mutation_response['branchRule']['name']).to eq(new_name) + expect(mutation_response['errors']).to be_empty + end + + context 'when name already exists for the project' do + let(:params) do + { + id: id, + project_path: project_path, + name: name_2 + } + end + + it 'returns an error' do + post_mutation + + expect(mutation_response['errors'].first).to eq('Name has already been taken') + end + end + + context 'when the protected branch cannot be found' do + let(:id) { "gid://gitlab/ProtectedBranch/#{non_existing_record_id}" } + + it_behaves_like 'a mutation that returns top-level errors', + errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR] + end + + context 'when the project cannot be found' do + let(:project_path) { 'not a project path' } + + it_behaves_like 'a mutation that returns top-level errors', + errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR] + end + end +end diff --git a/spec/requests/api/graphql/mutations/ci/catalog/resources/destroy_spec.rb b/spec/requests/api/graphql/mutations/ci/catalog/resources/destroy_spec.rb new file mode 100644 index 00000000000..3b278f973b7 --- /dev/null +++ b/spec/requests/api/graphql/mutations/ci/catalog/resources/destroy_spec.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'CatalogResourceDestroy', 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_it_be(:catalog_resource) { create(:ci_catalog_resource, project: project) } + + let(:mutation) do + variables = { + project_path: project.full_path + } + graphql_mutation(:catalog_resources_destroy, 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 + before do + catalog_resource.project.add_owner(current_user) + end + + it 'destroys the catalog resource' do + expect(project.catalog_resource).to eq(catalog_resource) + + post_graphql_mutation(mutation, current_user: current_user) + + expect(project.reload.catalog_resource).to be_nil + expect_graphql_errors_to_be_empty + 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 deleted file mode 100644 index 07465777263..00000000000 --- a/spec/requests/api/graphql/mutations/ci/catalog/unpublish_spec.rb +++ /dev/null @@ -1,52 +0,0 @@ -# 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/runner/create_spec.rb b/spec/requests/api/graphql/mutations/ci/runner/create_spec.rb index b697b9f73b7..567ef12df2b 100644 --- a/spec/requests/api/graphql/mutations/ci/runner/create_spec.rb +++ b/spec/requests/api/graphql/mutations/ci/runner/create_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe 'RunnerCreate', feature_category: :runner_fleet do +RSpec.describe 'RunnerCreate', feature_category: :fleet_visibility do include GraphqlHelpers let_it_be(:user) { create(:user) } diff --git a/spec/requests/api/graphql/mutations/ci/runners_registration_token/reset_spec.rb b/spec/requests/api/graphql/mutations/ci/runners_registration_token/reset_spec.rb index 752242c3ab3..ef752448966 100644 --- a/spec/requests/api/graphql/mutations/ci/runners_registration_token/reset_spec.rb +++ b/spec/requests/api/graphql/mutations/ci/runners_registration_token/reset_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe 'RunnersRegistrationTokenReset', feature_category: :runner_fleet do +RSpec.describe 'RunnersRegistrationTokenReset', feature_category: :fleet_visibility do include GraphqlHelpers let(:mutation) { graphql_mutation(:runners_registration_token_reset, input) } 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 index 0c708c3dc41..71b8c99c1c0 100644 --- 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 @@ -15,7 +15,7 @@ RSpec.describe 'Creating the container registry protection rule', :aggregate_fai let(:kwargs) do { project_path: project.full_path, - container_path_pattern: container_registry_protection_rule_attributes.container_path_pattern, + repository_path_pattern: container_registry_protection_rule_attributes.repository_path_pattern, push_protected_up_to_access_level: 'MAINTAINER', delete_protected_up_to_access_level: 'MAINTAINER' } @@ -26,7 +26,7 @@ RSpec.describe 'Creating the container registry protection rule', :aggregate_fai <<~QUERY containerRegistryProtectionRule { id - containerPathPattern + repositoryPathPattern } clientMutationId errors @@ -48,7 +48,7 @@ RSpec.describe 'Creating the container registry protection rule', :aggregate_fai 'errors' => be_blank, 'containerRegistryProtectionRule' => { 'id' => be_present, - 'containerPathPattern' => kwargs[:container_path_pattern] + 'repositoryPathPattern' => kwargs[:repository_path_pattern] } ) end @@ -57,7 +57,7 @@ RSpec.describe 'Creating the container registry protection rule', :aggregate_fai 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 + repository_path_pattern: kwargs[:repository_path_pattern])).to exist end end @@ -84,9 +84,9 @@ RSpec.describe 'Creating the container registry protection rule', :aggregate_fai } end - context 'with invalid input field `containerPathPattern`' do + context 'with invalid input field `repositoryPathPattern`' do let(:kwargs) do - super().merge(container_path_pattern: '') + super().merge(repository_path_pattern: '') end it_behaves_like 'an erroneous response' @@ -95,7 +95,7 @@ RSpec.describe 'Creating the container registry protection rule', :aggregate_fai it { subject.tap do - expect(mutation_response['errors']).to eq ["Container path pattern can't be blank"] + expect(mutation_response['errors']).to eq ["Repository path pattern can't be blank"] end } end @@ -108,9 +108,9 @@ RSpec.describe 'Creating the container registry protection rule', :aggregate_fai 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 + # The field `repository_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" + repository_path_pattern: "#{existing_container_registry_protection_rule.repository_path_pattern}-unique" ) end @@ -121,9 +121,9 @@ RSpec.describe 'Creating the container registry protection rule', :aggregate_fai end end - context 'when field `container_path_pattern` is taken' do + context 'when field `repository_path_pattern` is taken' do let(:kwargs) do - super().merge(container_path_pattern: existing_container_registry_protection_rule.container_path_pattern, + super().merge(repository_path_pattern: existing_container_registry_protection_rule.repository_path_pattern, push_protected_up_to_access_level: 'MAINTAINER') end @@ -134,12 +134,12 @@ RSpec.describe 'Creating the container registry protection rule', :aggregate_fai it 'returns without error' do subject - expect(mutation_response['errors']).to eq ['Container path pattern has already been taken'] + expect(mutation_response['errors']).to eq ['Repository 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], + repository_path_pattern: kwargs[:repository_path_pattern], push_protected_up_to_access_level: Gitlab::Access::MAINTAINER)).not_to exist end end diff --git a/spec/requests/api/graphql/mutations/container_registry/protection/rule/delete_spec.rb b/spec/requests/api/graphql/mutations/container_registry/protection/rule/delete_spec.rb new file mode 100644 index 00000000000..dd661c302ff --- /dev/null +++ b/spec/requests/api/graphql/mutations/container_registry/protection/rule/delete_spec.rb @@ -0,0 +1,102 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Deleting a container registry protection rule', :aggregate_failures, feature_category: :container_registry do + include GraphqlHelpers + + let_it_be(:project) { create(:project, :repository) } + let_it_be_with_refind(:container_protection_rule) do + create(:container_registry_protection_rule, project: project) + end + + let_it_be(:current_user) { create(:user, maintainer_projects: [project]) } + + let(:mutation) { graphql_mutation(:delete_container_registry_protection_rule, input) } + let(:mutation_response) { graphql_mutation_response(:delete_container_registry_protection_rule) } + let(:input) { { id: container_protection_rule.to_global_id } } + + subject(:post_graphql_mutation_delete_container_registry_protection_rule) do + post_graphql_mutation(mutation, current_user: current_user) + end + + shared_examples 'an erroneous response' do + it { post_graphql_mutation_delete_container_registry_protection_rule.tap { expect(mutation_response).to be_blank } } + + it do + expect { post_graphql_mutation_delete_container_registry_protection_rule } + .not_to change { ::ContainerRegistry::Protection::Rule.count } + end + end + + it_behaves_like 'a working GraphQL mutation' + + it 'responds with deleted container registry protection rule' do + expect { post_graphql_mutation_delete_container_registry_protection_rule } + .to change { ::ContainerRegistry::Protection::Rule.count }.from(1).to(0) + + expect_graphql_errors_to_be_empty + + expect(mutation_response).to include( + 'errors' => be_blank, + 'containerRegistryProtectionRule' => { + 'id' => container_protection_rule.to_global_id.to_s, + 'repositoryPathPattern' => container_protection_rule.repository_path_pattern, + 'deleteProtectedUpToAccessLevel' => container_protection_rule.delete_protected_up_to_access_level.upcase, + 'pushProtectedUpToAccessLevel' => container_protection_rule.push_protected_up_to_access_level.upcase + } + ) + end + + context 'with existing container registry protection rule belonging to other project' do + let_it_be(:container_protection_rule) do + create(:container_registry_protection_rule, repository_path_pattern: 'protection_rule_other_project') + end + + it_behaves_like 'an erroneous response' + + it { is_expected.tap { expect_graphql_errors_to_include(/you don't have permission to perform this action/) } } + end + + context 'with deleted container registry protection rule' do + let!(:container_protection_rule) do + create(:container_registry_protection_rule, project: project, + repository_path_pattern: 'protection_rule_deleted').destroy! + end + + it_behaves_like 'an erroneous response' + + it { is_expected.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 response' + + it { is_expected.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 do + post_graphql_mutation_delete_container_registry_protection_rule + + expect_graphql_errors_to_include(/'container_registry_protected_containers' feature flag is disabled/) + end + end +end diff --git a/spec/requests/api/graphql/mutations/container_registry/protection/rule/update_spec.rb b/spec/requests/api/graphql/mutations/container_registry/protection/rule/update_spec.rb new file mode 100644 index 00000000000..cd2c8b9f0a2 --- /dev/null +++ b/spec/requests/api/graphql/mutations/container_registry/protection/rule/update_spec.rb @@ -0,0 +1,143 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Updating the container registry protection rule', :aggregate_failures, feature_category: :container_registry do + include GraphqlHelpers + + let_it_be(:project) { create(:project) } + let_it_be_with_reload(:container_registry_protection_rule) do + create(:container_registry_protection_rule, project: project, push_protected_up_to_access_level: :developer) + end + + let_it_be(:current_user) { create(:user, maintainer_projects: [project]) } + + let(:container_registry_protection_rule_attributes) do + build_stubbed(:container_registry_protection_rule, project: project) + end + + let(:mutation) do + graphql_mutation(:update_container_registry_protection_rule, input, + <<~QUERY + containerRegistryProtectionRule { + repositoryPathPattern + deleteProtectedUpToAccessLevel + pushProtectedUpToAccessLevel + } + clientMutationId + errors + QUERY + ) + end + + let(:input) do + { + id: container_registry_protection_rule.to_global_id, + repository_path_pattern: "#{container_registry_protection_rule.repository_path_pattern}-updated", + delete_protected_up_to_access_level: 'OWNER', + push_protected_up_to_access_level: 'MAINTAINER' + } + end + + let(:mutation_response) { graphql_mutation_response(:update_container_registry_protection_rule) } + + subject { post_graphql_mutation(mutation, current_user: current_user) } + + shared_examples 'a successful response' do + it { subject.tap { expect_graphql_errors_to_be_empty } } + + it 'returns the updated container registry protection rule' do + subject + + expect(mutation_response).to include( + 'containerRegistryProtectionRule' => { + 'repositoryPathPattern' => input[:repository_path_pattern], + 'deleteProtectedUpToAccessLevel' => input[:delete_protected_up_to_access_level], + 'pushProtectedUpToAccessLevel' => input[:push_protected_up_to_access_level] + } + ) + end + + it do + subject.tap do + expect(container_registry_protection_rule.reload).to have_attributes( + repository_path_pattern: input[:repository_path_pattern], + push_protected_up_to_access_level: input[:push_protected_up_to_access_level].downcase + ) + end + end + end + + shared_examples 'an erroneous reponse' do + it { subject.tap { expect(mutation_response).to be_blank } } + it { expect { subject }.not_to change { container_registry_protection_rule.reload.updated_at } } + end + + it_behaves_like 'a successful response' + + context 'with other existing container registry protection rule with same repository_path_pattern' do + let_it_be_with_reload(:other_existing_container_registry_protection_rule) do + create(:container_registry_protection_rule, project: project, + repository_path_pattern: "#{container_registry_protection_rule.repository_path_pattern}-other") + end + + let(:input) do + super().merge(repository_path_pattern: other_existing_container_registry_protection_rule.repository_path_pattern) + end + + it { is_expected.tap { expect_graphql_errors_to_be_empty } } + + it 'returns a blank container registry protection rule' do + is_expected.tap { expect(mutation_response['containerRegistryProtectionRule']).to be_blank } + end + + it 'includes error message in response' do + is_expected.tap { expect(mutation_response['errors']).to eq ['Repository path pattern has already been taken'] } + end + end + + context 'with invalid input param `pushProtectedUpToAccessLevel`' do + let(:input) { super().merge(push_protected_up_to_access_level: nil) } + + it_behaves_like 'an erroneous reponse' + + it { is_expected.tap { expect_graphql_errors_to_include(/pushProtectedUpToAccessLevel can't be blank/) } } + end + + context 'with invalid input param `repositoryPathPattern`' do + let(:input) { super().merge(repository_path_pattern: '') } + + it_behaves_like 'an erroneous reponse' + + it { is_expected.tap { expect_graphql_errors_to_include(/repositoryPathPattern can't be blank/) } } + 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 { is_expected.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 reponse' + + it 'returns error of disabled feature flag' do + is_expected.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/merge_requests/set_assignees_spec.rb b/spec/requests/api/graphql/mutations/merge_requests/set_assignees_spec.rb index cb7bac771b3..1bd239ecd87 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 @@ -127,7 +127,7 @@ RSpec.describe 'Setting assignees of a merge request', :assume_throttled, featur context 'when passing append as true' do let(:mode) { Types::MutationOperationModeEnum.enum[:append] } let(:input) { { assignee_usernames: [assignee2.username], operation_mode: mode } } - let(:db_query_limit) { 23 } + let(:db_query_limit) { 25 } before do # In CE, APPEND is a NOOP as you can't have multiple assignees @@ -147,7 +147,7 @@ RSpec.describe 'Setting assignees of a merge request', :assume_throttled, featur end context 'when passing remove as true' do - let(:db_query_limit) { 31 } + let(:db_query_limit) { 33 } let(:mode) { Types::MutationOperationModeEnum.enum[:remove] } let(:input) { { assignee_usernames: [assignee.username], operation_mode: mode } } let(:expected_result) { [] } 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 738dc3078e7..05c1a2d96d9 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 @@ -22,7 +22,8 @@ RSpec.describe 'Updating the package settings', feature_category: :package_regis npm_package_requests_forwarding: true, lock_npm_package_requests_forwarding: true, pypi_package_requests_forwarding: true, - lock_pypi_package_requests_forwarding: true + lock_pypi_package_requests_forwarding: true, + nuget_symbol_server_enabled: true } end @@ -42,6 +43,7 @@ RSpec.describe 'Updating the package settings', feature_category: :package_regis lockNpmPackageRequestsForwarding pypiPackageRequestsForwarding lockPypiPackageRequestsForwarding + nugetSymbolServerEnabled } errors QL @@ -70,6 +72,7 @@ RSpec.describe 'Updating the package settings', feature_category: :package_regis expect(package_settings_response['lockPypiPackageRequestsForwarding']).to eq(params[:lock_pypi_package_requests_forwarding]) expect(package_settings_response['npmPackageRequestsForwarding']).to eq(params[:npm_package_requests_forwarding]) expect(package_settings_response['lockNpmPackageRequestsForwarding']).to eq(params[:lock_npm_package_requests_forwarding]) + expect(package_settings_response['nugetSymbolServerEnabled']).to eq(params[:nuget_symbol_server_enabled]) end end @@ -111,7 +114,8 @@ RSpec.describe 'Updating the package settings', feature_category: :package_regis npm_package_requests_forwarding: nil, lock_npm_package_requests_forwarding: false, pypi_package_requests_forwarding: nil, - lock_pypi_package_requests_forwarding: false + lock_pypi_package_requests_forwarding: false, + nuget_symbol_server_enabled: false }, to: { maven_duplicates_allowed: false, maven_duplicate_exception_regex: 'foo-.*', @@ -124,7 +128,8 @@ RSpec.describe 'Updating the package settings', feature_category: :package_regis npm_package_requests_forwarding: true, lock_npm_package_requests_forwarding: true, pypi_package_requests_forwarding: true, - lock_pypi_package_requests_forwarding: true + lock_pypi_package_requests_forwarding: true, + nuget_symbol_server_enabled: true } it_behaves_like 'returning a success' diff --git a/spec/requests/api/graphql/mutations/organizations/create_spec.rb b/spec/requests/api/graphql/mutations/organizations/create_spec.rb index ac6b04104ba..8ab80685822 100644 --- a/spec/requests/api/graphql/mutations/organizations/create_spec.rb +++ b/spec/requests/api/graphql/mutations/organizations/create_spec.rb @@ -4,20 +4,24 @@ require 'spec_helper' RSpec.describe Mutations::Organizations::Create, feature_category: :cell do include GraphqlHelpers + include WorkhorseHelpers let_it_be(:user) { create(:user) } let(:mutation) { graphql_mutation(:organization_create, params) } let(:name) { 'Name' } let(:path) { 'path' } + let(:description) { nil } + let(:avatar) { fixture_file_upload("spec/fixtures/dk.png") } let(:params) do { name: name, - path: path + path: path, + avatar: avatar } end - subject(:create_organization) { post_graphql_mutation(mutation, current_user: current_user) } + subject(:create_organization) { post_graphql_mutation_with_uploads(mutation, current_user: current_user) } it { expect(described_class).to require_graphql_authorizations(:create_organization) } @@ -27,6 +31,7 @@ RSpec.describe Mutations::Organizations::Create, feature_category: :cell do context 'when the user does not have permission' do let(:current_user) { nil } + let(:avatar) { nil } it_behaves_like 'a mutation that returns a top-level access error' @@ -48,17 +53,35 @@ RSpec.describe Mutations::Organizations::Create, feature_category: :cell do end end - it 'creates an organization' do - expect { create_organization }.to change { Organizations::Organization.count }.by(1) + shared_examples 'creating an organization' do + 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, + 'description' => description + ) + end end - it 'returns the new organization' do - create_organization + context 'with description' do + let(:description) { 'Organization description' } + let(:params) do + { + name: name, + path: path, + description: description + } + end - expect(graphql_data_at(:organization_create, :organization)).to match a_hash_including( - 'name' => name, - 'path' => path - ) + include_examples 'creating an organization' end + + include_examples 'creating an organization' end end diff --git a/spec/requests/api/graphql/mutations/organizations/update_spec.rb b/spec/requests/api/graphql/mutations/organizations/update_spec.rb new file mode 100644 index 00000000000..4e819c280d0 --- /dev/null +++ b/spec/requests/api/graphql/mutations/organizations/update_spec.rb @@ -0,0 +1,120 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Mutations::Organizations::Update, feature_category: :cell do + include GraphqlHelpers + include WorkhorseHelpers + + let_it_be(:user) { create(:user) } + let_it_be_with_reload(:organization) do + create(:organization) { |org| create(:organization_user, organization: org, user: user) } + end + + let(:mutation) { graphql_mutation(:organization_update, params) } + let(:name) { 'Name' } + let(:path) { 'path' } + let(:description) { 'org-description' } + let(:avatar) { nil } + let(:params) do + { + id: organization.to_global_id.to_s, + name: name, + path: path, + description: description, + avatar: avatar + } + end + + subject(:update_organization) { post_graphql_mutation_with_uploads(mutation, current_user: current_user) } + + it { expect(described_class).to require_graphql_authorizations(:admin_organization) } + + def mutation_response + graphql_mutation_response(:organization_update) + 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 update the organization' do + initial_name = organization.name + initial_path = organization.path + + update_organization + organization.reset + + expect(organization.name).to eq(initial_name) + expect(organization.path).to eq(initial_path) + 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 + update_organization + + expect(mutation_response).to include('errors' => ["Name can't be blank"]) + end + end + + context 'when single attribute is update' do + using RSpec::Parameterized::TableSyntax + + where(attribute: %w[name path description]) + + with_them do + let(:value) { "new-#{attribute}" } + let(:attribute_hash) { { attribute => value } } + let(:params) { { id: organization.to_global_id.to_s }.merge(attribute_hash) } + + it 'updates the given field' do + update_organization + + expect(graphql_data_at(:organization_update, :organization)).to match a_hash_including(attribute_hash) + expect(mutation_response['errors']).to be_empty + end + end + end + + it 'returns the updated organization' do + update_organization + + expect(graphql_data_at(:organization_update, :organization)).to match a_hash_including( + 'name' => name, + 'path' => path, + 'description' => description + ) + expect(mutation_response['errors']).to be_empty + end + + context 'with a new avatar' do + let(:filename) { 'spec/fixtures/dk.png' } + let(:avatar) { fixture_file_upload(filename) } + + it 'returns the updated organization' do + update_organization + + expect( + graphql_data_at(:organization_update, :organization) + ).to( + match( + a_hash_including( + 'name' => name, + 'path' => path, + 'description' => description + ) + ) + ) + expect(File.basename(organization.reload.avatar.file.file)).to eq(File.basename(filename)) + expect(mutation_response['errors']).to be_empty + end + end + end +end diff --git a/spec/requests/api/graphql/mutations/packages/bulk_destroy_spec.rb b/spec/requests/api/graphql/mutations/packages/bulk_destroy_spec.rb index d0980a2b43d..084958be1fb 100644 --- a/spec/requests/api/graphql/mutations/packages/bulk_destroy_spec.rb +++ b/spec/requests/api/graphql/mutations/packages/bulk_destroy_spec.rb @@ -38,6 +38,26 @@ RSpec.describe 'Destroying multiple packages', feature_category: :package_regist end it_behaves_like 'returning response status', :success + + context 'when npm package' do + let_it_be_with_reload(:packages1) { create_list(:npm_package, 3, project: project1, name: 'test-package-1') } + let_it_be_with_reload(:packages2) { create_list(:npm_package, 2, project: project2, name: 'test-package-2') } + + it 'enqueues the worker to sync a metadata cache' do + arguments = [] + + expect(Packages::Npm::CreateMetadataCacheWorker) + .to receive(:bulk_perform_async_with_contexts).and_wrap_original do |original_method, *args| + packages = args.first + arguments = packages.map(&args.second[:arguments_proc]).uniq + original_method.call(*args) + end + + mutation_request + + expect(arguments).to contain_exactly([project1.id, 'test-package-1'], [project2.id, 'test-package-2']) + end + end end shared_examples 'denying the mutation request' do diff --git a/spec/requests/api/graphql/mutations/packages/destroy_spec.rb b/spec/requests/api/graphql/mutations/packages/destroy_spec.rb index 86167e7116f..6e0e5bd8aae 100644 --- a/spec/requests/api/graphql/mutations/packages/destroy_spec.rb +++ b/spec/requests/api/graphql/mutations/packages/destroy_spec.rb @@ -35,6 +35,17 @@ RSpec.describe 'Destroying a package', feature_category: :package_registry do .to change { ::Packages::Package.pending_destruction.count }.by(1) end + context 'when npm package' do + let_it_be_with_reload(:package) { create(:npm_package) } + + it 'enqueues the worker to sync a metadata cache' do + expect(Packages::Npm::CreateMetadataCacheWorker) + .to receive(:perform_async).with(project.id, package.name) + + mutation_request + end + end + it_behaves_like 'returning response status', :success 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 index 1d94d520674..6c300f8ce57 100644 --- a/spec/requests/api/graphql/mutations/packages/protection/rule/delete_spec.rb +++ b/spec/requests/api/graphql/mutations/packages/protection/rule/delete_spec.rb @@ -15,7 +15,7 @@ RSpec.describe 'Deleting a package protection rule', :aggregate_failures, featur subject { post_graphql_mutation(mutation, current_user: current_user) } - shared_examples 'an erroneous reponse' do + shared_examples 'an erroneous response' do it { subject.tap { expect(mutation_response).to be_blank } } it { expect { subject }.not_to change { ::Packages::Protection::Rule.count } } end @@ -44,7 +44,7 @@ RSpec.describe 'Deleting a package protection rule', :aggregate_failures, featur create(:package_protection_rule, package_name_pattern: 'protection_rule_other_project') end - it_behaves_like 'an erroneous reponse' + it_behaves_like 'an erroneous response' it { subject.tap { expect_graphql_errors_to_include(/you don't have permission to perform this action/) } } end @@ -54,7 +54,7 @@ RSpec.describe 'Deleting a package protection rule', :aggregate_failures, featur create(:package_protection_rule, project: project, package_name_pattern: 'protection_rule_deleted').destroy! end - it_behaves_like 'an erroneous reponse' + it_behaves_like 'an erroneous response' it { subject.tap { expect_graphql_errors_to_include(/you don't have permission to perform this action/) } } end @@ -70,7 +70,7 @@ RSpec.describe 'Deleting a package protection rule', :aggregate_failures, featur end with_them do - it_behaves_like 'an erroneous reponse' + it_behaves_like 'an erroneous response' it { subject.tap { expect_graphql_errors_to_include(/you don't have permission to perform this action/) } } end @@ -81,7 +81,7 @@ RSpec.describe 'Deleting a package protection rule', :aggregate_failures, featur stub_feature_flags(packages_protected_packages: false) end - it_behaves_like 'an erroneous reponse' + it_behaves_like 'an erroneous response' it { subject.tap { expect_graphql_errors_to_include(/'packages_protected_packages' feature flag is disabled/) } } end diff --git a/spec/requests/api/graphql/mutations/packages/protection/rule/update_spec.rb b/spec/requests/api/graphql/mutations/packages/protection/rule/update_spec.rb new file mode 100644 index 00000000000..efc919062d6 --- /dev/null +++ b/spec/requests/api/graphql/mutations/packages/protection/rule/update_spec.rb @@ -0,0 +1,134 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Updating the packages protection rule', :aggregate_failures, feature_category: :package_registry do + include GraphqlHelpers + + let_it_be(:project) { create(:project) } + let_it_be_with_reload(:package_protection_rule) do + create(:package_protection_rule, project: project, push_protected_up_to_access_level: :developer) + end + + let_it_be(:current_user) { create(:user, maintainer_projects: [project]) } + + let(:package_protection_rule_attributes) { build_stubbed(:package_protection_rule, project: project) } + + let(:mutation) do + graphql_mutation(:update_packages_protection_rule, input, + <<~QUERY + packageProtectionRule { + packageNamePattern + pushProtectedUpToAccessLevel + } + clientMutationId + errors + QUERY + ) + end + + let(:input) do + { + id: package_protection_rule.to_global_id, + package_name_pattern: "#{package_protection_rule.package_name_pattern}-updated", + push_protected_up_to_access_level: 'MAINTAINER' + } + end + + let(:mutation_response) { graphql_mutation_response(:update_packages_protection_rule) } + + subject { post_graphql_mutation(mutation, current_user: current_user) } + + shared_examples 'a successful response' do + it { subject.tap { expect_graphql_errors_to_be_empty } } + + it 'returns the updated package protection rule' do + subject + + expect(mutation_response).to include( + 'packageProtectionRule' => { + 'packageNamePattern' => input[:package_name_pattern], + 'pushProtectedUpToAccessLevel' => input[:push_protected_up_to_access_level] + } + ) + end + + it do + subject.tap do + expect(package_protection_rule.reload).to have_attributes( + package_name_pattern: input[:package_name_pattern], + push_protected_up_to_access_level: input[:push_protected_up_to_access_level].downcase + ) + end + end + end + + shared_examples 'an erroneous response' do + it { subject.tap { expect(mutation_response).to be_blank } } + it { expect { subject }.not_to change { package_protection_rule.reload.updated_at } } + end + + it_behaves_like 'a successful response' + + context 'with other existing package protection rule with same package_name_pattern' do + let_it_be_with_reload(:other_existing_package_protection_rule) do + create(:package_protection_rule, project: project, + package_name_pattern: "#{package_protection_rule.package_name_pattern}-other") + end + + let(:input) { super().merge(package_name_pattern: other_existing_package_protection_rule.package_name_pattern) } + + it { is_expected.tap { expect_graphql_errors_to_be_empty } } + + it 'returns a blank package protection rule' do + is_expected.tap { expect(mutation_response['packageProtectionRule']).to be_blank } + end + + it 'includes error message in response' do + is_expected.tap { expect(mutation_response['errors']).to eq ['Package name pattern has already been taken'] } + end + end + + context 'with invalid input param `pushProtectedUpToAccessLevel`' do + let(:input) { super().merge(push_protected_up_to_access_level: nil) } + + it_behaves_like 'an erroneous response' + + it { is_expected.tap { expect_graphql_errors_to_include(/pushProtectedUpToAccessLevel can't be blank/) } } + end + + context 'with invalid input param `packageNamePattern`' do + let(:input) { super().merge(package_name_pattern: '') } + + it_behaves_like 'an erroneous response' + + it { is_expected.tap { expect_graphql_errors_to_include(/packageNamePattern can't be blank/) } } + 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 { is_expected.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 response' + + it 'returns error of disabled feature flag' do + is_expected.tap { expect_graphql_errors_to_include(/'packages_protected_packages' feature flag is disabled/) } + end + end +end diff --git a/spec/requests/api/graphql/mutations/user_preferences/update_spec.rb b/spec/requests/api/graphql/mutations/user_preferences/update_spec.rb index 65b8083c74f..b1cd3259eeb 100644 --- a/spec/requests/api/graphql/mutations/user_preferences/update_spec.rb +++ b/spec/requests/api/graphql/mutations/user_preferences/update_spec.rb @@ -12,7 +12,8 @@ RSpec.describe Mutations::UserPreferences::Update, feature_category: :user_profi let(:input) do { 'issuesSort' => sort_value, - 'visibilityPipelineIdType' => 'IID' + 'visibilityPipelineIdType' => 'IID', + 'useWebIdeExtensionMarketplace' => true } end @@ -26,19 +27,26 @@ RSpec.describe Mutations::UserPreferences::Update, feature_category: :user_profi expect(response).to have_gitlab_http_status(:success) expect(mutation_response['userPreferences']['issuesSort']).to eq(sort_value) expect(mutation_response['userPreferences']['visibilityPipelineIdType']).to eq('IID') + expect(mutation_response['userPreferences']['useWebIdeExtensionMarketplace']).to eq(true) expect(current_user.user_preference.persisted?).to eq(true) expect(current_user.user_preference.issues_sort).to eq(Types::IssueSortEnum.values[sort_value].value.to_s) expect(current_user.user_preference.visibility_pipeline_id_type).to eq('iid') + expect(current_user.user_preference.use_web_ide_extension_marketplace).to eq(true) end end context 'when user has existing preference' do - before do - current_user.create_user_preference!( + let(:init_user_preference) do + { issues_sort: Types::IssueSortEnum.values['TITLE_DESC'].value, - visibility_pipeline_id_type: 'id' - ) + visibility_pipeline_id_type: 'id', + use_web_ide_extension_marketplace: true + } + end + + before do + current_user.create_user_preference!(init_user_preference) end it 'updates the existing value' do @@ -53,5 +61,29 @@ RSpec.describe Mutations::UserPreferences::Update, feature_category: :user_profi expect(current_user.user_preference.issues_sort).to eq(Types::IssueSortEnum.values[sort_value].value.to_s) expect(current_user.user_preference.visibility_pipeline_id_type).to eq('iid') end + + context 'when input has nil attributes' do + let(:input) do + { + 'issuesSort' => nil, + 'visibilityPipelineIdType' => nil, + 'useWebIdeExtensionMarketplace' => nil + } + end + + it 'updates only nullable attributes' do + post_graphql_mutation(mutation, current_user: current_user) + + current_user.user_preference.reload + + expect(current_user.user_preference).to have_attributes({ + # These are nullable and are exepcted to change + issues_sort: nil, + # These should not have changed + visibility_pipeline_id_type: init_user_preference[:visibility_pipeline_id_type], + use_web_ide_extension_marketplace: init_user_preference[:use_web_ide_extension_marketplace] + }) + end + end end end diff --git a/spec/requests/api/graphql/mutations/work_items/delete_task_spec.rb b/spec/requests/api/graphql/mutations/work_items/delete_task_spec.rb deleted file mode 100644 index b1828de046f..00000000000 --- a/spec/requests/api/graphql/mutations/work_items/delete_task_spec.rb +++ /dev/null @@ -1,79 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe "Delete a task in a work item's description", feature_category: :team_planning do - include GraphqlHelpers - - let_it_be(:project) { create(:project) } - let_it_be(:developer) { create(:user).tap { |user| project.add_developer(user) } } - let_it_be(:task) { create(:work_item, :task, project: project, author: developer) } - let_it_be(:work_item, refind: true) do - create(:work_item, project: project, description: "- [ ] #{task.to_reference}+", lock_version: 3) - end - - before_all do - create(:issue_link, source_id: work_item.id, target_id: task.id) - end - - let(:lock_version) { work_item.lock_version } - let(:input) do - { - 'id' => work_item.to_global_id.to_s, - 'lockVersion' => lock_version, - 'taskData' => { - 'id' => task.to_global_id.to_s, - 'lineNumberStart' => 1, - 'lineNumberEnd' => 1 - } - } - end - - let(:mutation) { graphql_mutation(:workItemDeleteTask, input) } - let(:mutation_response) { graphql_mutation_response(:work_item_delete_task) } - - context 'the user is not allowed to update a work item' do - let(:current_user) { create(:user) } - - it_behaves_like 'a mutation that returns a top-level access error' - end - - context 'when user can update the description but not delete the task' do - let(:current_user) { create(:user).tap { |u| project.add_developer(u) } } - - it_behaves_like 'a mutation that returns a top-level access error' - end - - context 'when user has permissions to remove a task' do - let(:current_user) { developer } - - it 'removes the task from the work item' do - expect do - post_graphql_mutation(mutation, current_user: current_user) - work_item.reload - end.to change(WorkItem, :count).by(-1).and( - change(IssueLink, :count).by(-1) - ).and( - change(work_item, :description).from("- [ ] #{task.to_reference}+").to("- [ ] #{task.title}") - ) - - expect(response).to have_gitlab_http_status(:success) - expect(mutation_response['workItem']).to include('id' => work_item.to_global_id.to_s) - end - - context 'when removing the task fails' do - let(:lock_version) { 2 } - - it 'makes no changes to the DB and returns an error message' do - expect do - post_graphql_mutation(mutation, current_user: current_user) - work_item.reload - end.to not_change(WorkItem, :count).and( - not_change(work_item, :description) - ) - - expect(mutation_response['errors']).to contain_exactly('Stale work item. Check lock version') - end - end - end -end diff --git a/spec/requests/api/graphql/organizations/organization_query_spec.rb b/spec/requests/api/graphql/organizations/organization_query_spec.rb index c243e0613ad..c485e3b170d 100644 --- a/spec/requests/api/graphql/organizations/organization_query_spec.rb +++ b/spec/requests/api/graphql/organizations/organization_query_spec.rb @@ -23,7 +23,8 @@ RSpec.describe 'getting organization information', feature_category: :cell do let_it_be(:organization_user) { create(:organization_user) } let_it_be(:organization) { organization_user.organization } let_it_be(:user) { organization_user.user } - let_it_be(:public_group) { create(:group, name: 'public-group', organization: organization) } + let_it_be(:parent_group) { create(:group, name: 'parent-group', organization: organization) } + let_it_be(:public_group) { create(:group, name: 'public-group', parent: parent_group, organization: organization) } let_it_be(:other_group) { create(:group, name: 'other-group', organization: organization) } let_it_be(:outside_organization_group) { create(:group) } @@ -74,6 +75,12 @@ RSpec.describe 'getting organization information', feature_category: :cell do end end + it 'does not return ancestors of authorized groups' do + request_organization + + expect(groups.pluck('id')).not_to include(parent_group.to_global_id.to_s) + end + context 'when requesting organization user' do let(:organization_fields) do <<~FIELDS diff --git a/spec/requests/api/graphql/project/alert_management/integrations_spec.rb b/spec/requests/api/graphql/project/alert_management/integrations_spec.rb index e48db541e1f..c4d3a217027 100644 --- a/spec/requests/api/graphql/project/alert_management/integrations_spec.rb +++ b/spec/requests/api/graphql/project/alert_management/integrations_spec.rb @@ -67,7 +67,7 @@ RSpec.describe 'getting Alert Management Integrations', feature_category: :incid 'name' => 'Prometheus', 'active' => prometheus_integration.manual_configuration?, 'token' => project_alerting_setting.token, - 'url' => "http://localhost/#{project.full_path}/prometheus/alerts/notify.json", + 'url' => "http://#{Gitlab.config.gitlab.host}/#{project.full_path}/prometheus/alerts/notify.json", 'apiUrl' => prometheus_integration.api_url ) ] diff --git a/spec/requests/api/graphql/project/cluster_agents_spec.rb b/spec/requests/api/graphql/project/cluster_agents_spec.rb index 181f21001ea..104f4f41cba 100644 --- a/spec/requests/api/graphql/project/cluster_agents_spec.rb +++ b/spec/requests/api/graphql/project/cluster_agents_spec.rb @@ -22,7 +22,7 @@ RSpec.describe 'Project.cluster_agents', feature_category: :deployment_managemen end before do - allow(Gitlab::Kas::Client).to receive(:new).and_return(double(get_connected_agents: [])) + allow(Gitlab::Kas::Client).to receive(:new).and_return(double(get_connected_agents_by_agent_ids: [])) end it 'can retrieve cluster agents' do @@ -87,7 +87,7 @@ RSpec.describe 'Project.cluster_agents', feature_category: :deployment_managemen let(:cluster_agents_fields) { [:id, query_nodes(:connections, [:connection_id, :connected_at, metadata_fields])] } before do - allow(Gitlab::Kas::Client).to receive(:new).and_return(double(get_connected_agents: [connected_agent])) + allow(Gitlab::Kas::Client).to receive(:new).and_return(double(get_connected_agents_by_agent_ids: [connected_agent])) end it 'can retrieve connections and agent metadata' do diff --git a/spec/requests/api/graphql/project/value_streams_spec.rb b/spec/requests/api/graphql/project/value_streams_spec.rb new file mode 100644 index 00000000000..01e937c1e47 --- /dev/null +++ b/spec/requests/api/graphql/project/value_streams_spec.rb @@ -0,0 +1,105 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Project.value_streams', feature_category: :value_stream_management do + include GraphqlHelpers + + let_it_be(:project) { create(:project) } + let_it_be(:user) { create(:user) } + + let(:query) do + <<~QUERY + query($fullPath: ID!) { + project(fullPath: $fullPath) { + valueStreams { + nodes { + name + stages { + name + startEventIdentifier + endEventIdentifier + } + } + } + } + } + QUERY + end + + context 'when user has permissions to read value streams' do + let(:expected_value_stream) do + { + 'project' => { + 'valueStreams' => { + 'nodes' => [ + { + 'name' => 'default', + 'stages' => expected_stages + } + ] + } + } + } + end + + let(:expected_stages) do + [ + { + 'name' => 'issue', + 'startEventIdentifier' => 'ISSUE_CREATED', + 'endEventIdentifier' => 'ISSUE_STAGE_END' + }, + { + 'name' => 'plan', + 'startEventIdentifier' => 'PLAN_STAGE_START', + 'endEventIdentifier' => 'ISSUE_FIRST_MENTIONED_IN_COMMIT' + }, + { + 'name' => 'code', + 'startEventIdentifier' => 'CODE_STAGE_START', + 'endEventIdentifier' => 'MERGE_REQUEST_CREATED' + }, + { + 'name' => 'test', + 'startEventIdentifier' => 'MERGE_REQUEST_LAST_BUILD_STARTED', + 'endEventIdentifier' => 'MERGE_REQUEST_LAST_BUILD_FINISHED' + }, + { + 'name' => 'review', + 'startEventIdentifier' => 'MERGE_REQUEST_CREATED', + 'endEventIdentifier' => 'MERGE_REQUEST_MERGED' + }, + { + 'name' => 'staging', + 'startEventIdentifier' => 'MERGE_REQUEST_MERGED', + 'endEventIdentifier' => 'MERGE_REQUEST_FIRST_DEPLOYED_TO_PRODUCTION' + } + ] + end + + before_all do + project.add_guest(user) + end + + before do + post_graphql(query, current_user: user, variables: { fullPath: project.full_path }) + end + + it_behaves_like 'a working graphql query' + + it 'returns only `default` value stream' do + expect(graphql_data).to eq(expected_value_stream) + end + end + + context 'when user does not have permission to read value streams' do + before do + post_graphql(query, current_user: user, variables: { fullPath: project.full_path }) + end + + it 'returns nil' do + expect(graphql_data_at(:project, :valueStreams)).to be_nil + end + end +end diff --git a/spec/requests/api/graphql/project/work_item_state_counts_spec.rb b/spec/requests/api/graphql/project/work_item_state_counts_spec.rb new file mode 100644 index 00000000000..d13204a36b7 --- /dev/null +++ b/spec/requests/api/graphql/project/work_item_state_counts_spec.rb @@ -0,0 +1,123 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'getting Work Item counts by state', feature_category: :portfolio_management do + include GraphqlHelpers + + let_it_be(:current_user) { create(:user) } + let_it_be(:group) { create(:group, :private) } + let_it_be(:project) { create(:project, :repository, :private, group: group) } + let_it_be(:work_item_opened1) { create(:work_item, project: project, title: 'Foo') } + let_it_be(:work_item_opened2) { create(:work_item, project: project, author: current_user) } + let_it_be(:work_item_closed) { create(:work_item, :closed, project: project, description: 'Bar') } + + let(:params) { {} } + + subject(:query_counts) { post_graphql(query, current_user: current_user) } + + context 'with work items count data' do + let(:work_item_counts) { graphql_data.dig('project', 'workItemStateCounts') } + + context 'with project permissions' do + before_all do + group.add_developer(current_user) + end + + it_behaves_like 'a working graphql query' do + before do + query_counts + end + end + + it 'returns the correct counts for each state' do + query_counts + + expect(work_item_counts).to eq( + 'all' => 3, + 'opened' => 2, + 'closed' => 1 + ) + end + + context 'when other work items are present in the group' do + it 'only returns counts for work items in the current project' do + other_project = create(:project, :repository, group: group) + create(:work_item, project: other_project) + query_counts + + expect(work_item_counts).to eq( + 'all' => 3, + 'opened' => 2, + 'closed' => 1 + ) + end + end + + context 'when filters are provided' do + context 'when filtering by author username' do + let(:params) { { 'authorUsername' => current_user.username } } + + it 'returns the correct counts for each status' do + query_counts + + expect(work_item_counts).to eq( + 'all' => 1, + 'opened' => 1, + 'closed' => 0 + ) + end + end + + context 'when searching in title' do + let(:params) { { search: 'Foo', in: [:TITLE] } } + + it 'returns the correct counts for each status' do + query_counts + + expect(work_item_counts).to eq( + 'all' => 1, + 'opened' => 1, + 'closed' => 0 + ) + end + end + + context 'when searching in description' do + let(:params) { { search: 'Bar', in: [:DESCRIPTION] } } + + it 'returns the correct counts for each status' do + query_counts + + expect(work_item_counts).to eq( + 'all' => 1, + 'opened' => 0, + 'closed' => 1 + ) + end + end + end + end + + context 'without project permissions' do + it 'does not return work item counts' do + query_counts + + expect_graphql_errors_to_be_empty + expect(work_item_counts).to be_nil + end + end + end + + def query(args: params) + fields = <<~QUERY + #{all_graphql_fields_for('WorkItemStateCountsType'.classify)} + QUERY + + graphql_query_for( + 'project', + { 'fullPath' => project.full_path }, + query_graphql_field('workItemStateCounts', args, fields) + ) + end +end diff --git a/spec/requests/api/graphql/project/work_item_types_spec.rb b/spec/requests/api/graphql/project/work_item_types_spec.rb index c31a260c4b8..086db983760 100644 --- a/spec/requests/api/graphql/project/work_item_types_spec.rb +++ b/spec/requests/api/graphql/project/work_item_types_spec.rb @@ -5,56 +5,19 @@ require 'spec_helper' RSpec.describe 'getting a list of work item types for a project', feature_category: :team_planning do include GraphqlHelpers - let_it_be(:developer) { create(:user) } let_it_be(:project) { create(:project) } + let_it_be(:developer) { create(:user).tap { |u| project.add_developer(u) } } - before_all do - project.add_developer(developer) - end - - let(:current_user) { developer } - - let(:fields) do - <<~GRAPHQL - workItemTypes{ - nodes { id name iconName } - } - GRAPHQL - end - - let(:query) do - graphql_query_for( - 'project', - { 'fullPath' => project.full_path }, - fields - ) - end - - context 'when user has access to the project' do - before do - post_graphql(query, current_user: current_user) - end + it_behaves_like 'graphql work item type list request spec' do + let(:current_user) { developer } + let(:parent_key) { :project } - it_behaves_like 'a working graphql query' - - it 'returns all default work item types' do - expect(graphql_data.dig('project', 'workItemTypes', 'nodes')).to match_array( - WorkItems::Type.default.map do |type| - hash_including('id' => type.to_global_id.to_s, 'name' => type.name, 'iconName' => type.icon_name) - end + let(:query) do + graphql_query_for( + 'project', + { 'fullPath' => project.full_path }, + query_nodes('WorkItemTypes', work_item_type_fields) ) end end - - context "when user doesn't have access to the project" do - let(:current_user) { create(:user) } - - before do - post_graphql(query, current_user: current_user) - end - - it 'does not return the project' do - expect(graphql_data).to eq('project' => nil) - end - end end diff --git a/spec/requests/api/graphql/work_item_spec.rb b/spec/requests/api/graphql/work_item_spec.rb index 36a27abd982..fe77b7ae736 100644 --- a/spec/requests/api/graphql/work_item_spec.rb +++ b/spec/requests/api/graphql/work_item_spec.rb @@ -104,6 +104,18 @@ RSpec.describe 'Query.work_item(id)', feature_category: :team_planning do end end + context 'when querying work item type information' do + include_context 'with work item types request context' + + let(:work_item_fields) { "workItemType { #{work_item_type_fields} }" } + + it 'returns work item type information' do + expect(work_item_data['workItemType']).to match( + expected_work_item_type_response(work_item.work_item_type).first + ) + end + end + context 'when querying widgets' do describe 'description widget' do let(:work_item_fields) do diff --git a/spec/requests/api/graphql/work_items_by_reference_spec.rb b/spec/requests/api/graphql/work_items_by_reference_spec.rb new file mode 100644 index 00000000000..ad2303a81e7 --- /dev/null +++ b/spec/requests/api/graphql/work_items_by_reference_spec.rb @@ -0,0 +1,130 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'find work items by reference', feature_category: :portfolio_management do + include GraphqlHelpers + + let_it_be(:current_user) { create(:user) } + let_it_be(:project) { create(:project, :repository, :public) } + let_it_be(:group2) { create(:group, :public) } + let_it_be(:project2) { create(:project, :repository, :public, group: group2) } + let_it_be(:private_project2) { create(:project, :repository, :private, group: group2) } + let_it_be(:work_item) { create(:work_item, :task, project: project2) } + let_it_be(:private_work_item) { create(:work_item, :task, project: private_project2) } + + let(:references) { [work_item.to_reference(full: true), private_work_item.to_reference(full: true)] } + + shared_examples 'response with matching work items' do + it 'returns accessible work item' do + post_graphql(query, current_user: current_user) + + expected_items = items.map { |item| a_graphql_entity_for(item) } + expect(graphql_data_at('workItemsByReference', 'nodes')).to match(expected_items) + end + end + + context 'when user has access only to public work items' do + it_behaves_like 'a working graphql query that returns data' do + before do + post_graphql(query, current_user: current_user) + end + end + + it_behaves_like 'response with matching work items' do + let(:items) { [work_item] } + end + + it 'avoids N+1 queries', :use_sql_query_cache do + post_graphql(query, current_user: current_user) # warm up + + control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do + post_graphql(query, current_user: current_user) + end + expect(graphql_data_at('workItemsByReference', 'nodes').size).to eq(1) + + extra_work_items = create_list(:work_item, 2, :task, project: project2) + refs = references + extra_work_items.map { |item| item.to_reference(full: true) } + + expect do + post_graphql(query(refs: refs), current_user: current_user) + end.not_to exceed_all_query_limit(control_count) + expect(graphql_data_at('workItemsByReference', 'nodes').size).to eq(3) + end + end + + context 'when user has access to work items in private project' do + before_all do + private_project2.add_guest(current_user) + end + + it_behaves_like 'response with matching work items' do + let(:items) { [private_work_item, work_item] } + end + end + + context 'when refs includes links' do + let_it_be(:work_item_with_url) { create(:work_item, :task, project: project2) } + let(:references) { [work_item.to_reference(full: true), Gitlab::UrlBuilder.build(work_item_with_url)] } + + it_behaves_like 'response with matching work items' do + let(:items) { [work_item_with_url, work_item] } + end + end + + context 'when refs includes a short reference present in the context project' do + let_it_be(:same_project_work_item) { create(:work_item, :task, project: project) } + let(:references) { ["##{same_project_work_item.iid}"] } + + it_behaves_like 'response with matching work items' do + let(:items) { [same_project_work_item] } + end + end + + context 'when user cannot access context namespace' do + it 'returns error' do + post_graphql(query(namespace_path: private_project2.full_path), current_user: current_user) + + expect(graphql_data_at('workItemsByReference')).to be_nil + expect(graphql_errors).to contain_exactly(a_hash_including( + 'message' => a_string_including("you don't have permission to perform this action"), + 'path' => %w[workItemsByReference] + )) + end + end + + context 'when the context is a group' do + it 'returns empty result' do + group2.add_guest(current_user) + post_graphql(query(namespace_path: group2.full_path), current_user: current_user) + + expect_graphql_errors_to_be_empty + expect(graphql_data_at('workItemsByReference', 'nodes')).to be_empty + end + end + + context 'when there are more than the max allowed references' do + let(:references_limit) { ::Resolvers::WorkItemReferencesResolver::REFERENCES_LIMIT } + let(:references) { (0..references_limit).map { |n| "##{n}" } } + let(:error_msg) do + "Number of references exceeds the limit. " \ + "Please provide no more than #{references_limit} references at the same time." + end + + it 'returns an error message' do + post_graphql(query, current_user: current_user) + + expect_graphql_errors_to_include(error_msg) + end + end + + def query(namespace_path: project.full_path, refs: references) + fields = <<~GRAPHQL + nodes { + #{all_graphql_fields_for('WorkItem', max_depth: 2)} + } + GRAPHQL + + graphql_query_for('workItemsByReference', { contextNamespacePath: namespace_path, refs: refs }, fields) + end +end diff --git a/spec/requests/api/group_export_spec.rb b/spec/requests/api/group_export_spec.rb index d0f7c000544..c48ade1cb8b 100644 --- a/spec/requests/api/group_export_spec.rb +++ b/spec/requests/api/group_export_spec.rb @@ -311,6 +311,8 @@ RSpec.describe API::GroupExport, feature_category: :importers do expect(response).to have_gitlab_http_status(:ok) expect(json_response.pluck('relation')).to contain_exactly('labels', 'milestones', 'badges') expect(json_response.pluck('status')).to contain_exactly(-1, 0, 1) + expect(json_response.pluck('batched')).to all(eq(false)) + expect(json_response.pluck('batches_count')).to all(eq(0)) end context 'when relation is specified' do @@ -322,6 +324,36 @@ RSpec.describe API::GroupExport, feature_category: :importers do expect(json_response['status']).to eq(0) end end + + context 'when there is a batched export' do + let_it_be(:batched_export) do + create(:bulk_import_export, :started, :batched, group: group, relation: 'boards', batches_count: 1) + end + + let_it_be(:batch) { create(:bulk_import_export_batch, objects_count: 5, export: batched_export) } + + it 'returns a list of batched relation export statuses' do + get api(status_path, user) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to include( + hash_including( + 'relation' => batched_export.relation, + 'batched' => true, + 'batches_count' => 1, + 'batches' => contain_exactly( + { + 'batch_number' => 1, + 'error' => nil, + 'objects_count' => batch.objects_count, + 'status' => batch.status, + 'updated_at' => batch.updated_at.as_json + } + ) + ) + ) + end + end end context 'when bulk import is disabled' do diff --git a/spec/requests/api/group_milestones_spec.rb b/spec/requests/api/group_milestones_spec.rb index 2f05b0fcf21..82a4311f7d0 100644 --- a/spec/requests/api/group_milestones_spec.rb +++ b/spec/requests/api/group_milestones_spec.rb @@ -30,75 +30,103 @@ RSpec.describe API::GroupMilestones, feature_category: :team_planning do it_behaves_like 'group and project milestones', "/groups/:id/milestones" describe 'GET /groups/:id/milestones' do - let_it_be(:ancestor_group) { create(:group, :private) } - let_it_be(:ancestor_group_milestone) { create(:milestone, group: ancestor_group, updated_at: 2.days.ago) } + context 'for REST only' do + let_it_be(:ancestor_group) { create(:group, :private) } + let_it_be(:ancestor_group_milestone) { create(:milestone, group: ancestor_group, updated_at: 2.days.ago) } - before_all do - group.update!(parent: ancestor_group) - end + before_all do + group.update!(parent: ancestor_group) + end - context 'when include_parent_milestones is true' do - let(:params) { { include_parent_milestones: true } } + context 'when include_ancestors is true' do + let(:params) { { include_ancestors: true } } - context 'when user has access to ancestor groups' do - let(:milestones) { [ancestor_group_milestone, milestone, closed_milestone] } + context 'when user has access to ancestor groups' do + let(:milestones) { [ancestor_group_milestone, milestone, closed_milestone] } - before do - ancestor_group.add_guest(user) - group.add_guest(user) - end + before do + ancestor_group.add_guest(user) + group.add_guest(user) + end - it_behaves_like 'listing all milestones' + it_behaves_like 'listing all milestones' - context 'when iids param is present' do - let(:params) { { include_parent_milestones: true, iids: [milestone.iid] } } + context 'when deprecated include_parent_milestones is true' do + let(:params) { { include_parent_milestones: true } } - it_behaves_like 'listing all milestones' - end + it_behaves_like 'listing all milestones' + end - context 'when updated_before param is present' do - let(:params) { { updated_before: 1.day.ago.iso8601, include_parent_milestones: true } } + context 'when both include_parent_milestones and include_ancestors are specified' do + let(:params) { { include_ancestors: true, include_parent_milestones: true } } - it_behaves_like 'listing all milestones' do - let(:milestones) { [ancestor_group_milestone, milestone] } + it 'returns 400' do + get api(route, user), params: params + + expect(response).to have_gitlab_http_status(:bad_request) + end + end + + context 'when iids param is present' do + let(:params) { { include_ancestors: true, iids: [milestone.iid] } } + + it_behaves_like 'listing all milestones' + end + + context 'when updated_before param is present' do + let(:params) { { updated_before: 1.day.ago.iso8601, include_ancestors: true } } + + it_behaves_like 'listing all milestones' do + let(:milestones) { [ancestor_group_milestone, milestone] } + end + end + + context 'when updated_after param is present' do + let(:params) { { updated_after: 1.day.ago.iso8601, include_ancestors: true } } + + it_behaves_like 'listing all milestones' do + let(:milestones) { [closed_milestone] } + end end end - context 'when updated_after param is present' do - let(:params) { { updated_after: 1.day.ago.iso8601, include_parent_milestones: true } } + context 'when user has no access to ancestor groups' do + let(:user) { create(:user) } + + before do + group.add_guest(user) + end it_behaves_like 'listing all milestones' do - let(:milestones) { [closed_milestone] } + let(:milestones) { [milestone, closed_milestone] } end end end - context 'when user has no access to ancestor groups' do - let(:user) { create(:user) } - - before do - group.add_guest(user) - end + context 'when updated_before param is present' do + let(:params) { { updated_before: 1.day.ago.iso8601 } } it_behaves_like 'listing all milestones' do - let(:milestones) { [milestone, closed_milestone] } + let(:milestones) { [milestone] } end end - end - context 'when updated_before param is present' do - let(:params) { { updated_before: 1.day.ago.iso8601 } } + context 'when updated_after param is present' do + let(:params) { { updated_after: 1.day.ago.iso8601 } } - it_behaves_like 'listing all milestones' do - let(:milestones) { [milestone] } + it_behaves_like 'listing all milestones' do + let(:milestones) { [closed_milestone] } + end end end - context 'when updated_after param is present' do - let(:params) { { updated_after: 1.day.ago.iso8601 } } + context 'for common GraphQL/REST' do + it_behaves_like 'group milestones including ancestors and descendants' + + def query_group_milestone_ids(params) + get api(route, current_user), params: params - it_behaves_like 'listing all milestones' do - let(:milestones) { [closed_milestone] } + json_response.pluck('id') end end end diff --git a/spec/requests/api/import_github_spec.rb b/spec/requests/api/import_github_spec.rb index 9a42b11dc76..f555f39ff74 100644 --- a/spec/requests/api/import_github_spec.rb +++ b/spec/requests/api/import_github_spec.rb @@ -5,8 +5,7 @@ require 'spec_helper' RSpec.describe API::ImportGithub, feature_category: :importers do let(:token) { "asdasd12345" } let(:provider) { :github } - let(:access_params) { { github_access_token: token, additional_access_tokens: additional_access_tokens } } - let(:additional_access_tokens) { nil } + let(:access_params) { { github_access_token: token } } let(:provider_username) { user.username } let(:provider_user) { double('provider', login: provider_username).as_null_object } let(:provider_repo) do @@ -134,28 +133,6 @@ RSpec.describe API::ImportGithub, feature_category: :importers do expect(response).to have_gitlab_http_status(:bad_request) end end - - context 'when additional access tokens are provided' do - let(:additional_access_tokens) { 'token1,token2' } - - it 'returns 201' do - expected_access_params = { github_access_token: token, additional_access_tokens: %w[token1 token2] } - - expect(Gitlab::LegacyGithubImport::ProjectCreator) - .to receive(:new) - .with(provider_repo, provider_repo[:name], user.namespace, user, type: provider, **expected_access_params) - .and_return(double(execute: project)) - - post api("/import/github", user), params: { - target_namespace: user.namespace_path, - personal_access_token: token, - repo_id: non_existing_record_id, - additional_access_tokens: 'token1,token2' - } - - expect(response).to have_gitlab_http_status(:created) - end - end end describe "POST /import/github/cancel" do diff --git a/spec/requests/api/integrations_spec.rb b/spec/requests/api/integrations_spec.rb index d8ac9d5abf7..4696be07045 100644 --- a/spec/requests/api/integrations_spec.rb +++ b/spec/requests/api/integrations_spec.rb @@ -46,10 +46,9 @@ RSpec.describe API::Integrations, feature_category: :integrations do where(:integration) do # The Project Integrations API supports all integrations except: # - The GitLab Slack Application integration, as it must be installed via the UI. - # - Shimo and ZenTao integrations, as new integrations are blocked from being created. + # - ZenTao integration, as new integration is blocked from being created. unavailable_integration_names = [ Integrations::GitlabSlackApplication.to_param, - Integrations::Shimo.to_param, Integrations::Zentao.to_param ] diff --git a/spec/requests/api/internal/kubernetes_spec.rb b/spec/requests/api/internal/kubernetes_spec.rb index 551ed0babf1..5ef041881b9 100644 --- a/spec/requests/api/internal/kubernetes_spec.rb +++ b/spec/requests/api/internal/kubernetes_spec.rb @@ -27,18 +27,6 @@ RSpec.describe API::Internal::Kubernetes, feature_category: :deployment_manageme expect(response).to have_gitlab_http_status(:unauthorized) end end - - context 'kubernetes_agent_internal_api feature flag disabled' do - before do - stub_feature_flags(kubernetes_agent_internal_api: false) - end - - it 'returns 404' do - send_request - - expect(response).to have_gitlab_http_status(:not_found) - end - end end shared_examples 'agent authentication' do @@ -134,15 +122,17 @@ RSpec.describe API::Internal::Kubernetes, feature_category: :deployment_manageme k8s_api_proxy_requests_via_user_access: 44, k8s_api_proxy_requests_via_pat_access: 45 } + users = create_list(:user, 3) + user_ids = users.map(&:id) << users[0].id unique_counters = { - agent_users_using_ci_tunnel: [10, 999, 777, 10], - k8s_api_proxy_requests_unique_users_via_ci_access: [10, 999, 777, 10], - k8s_api_proxy_requests_unique_agents_via_ci_access: [10, 999, 777, 10], - k8s_api_proxy_requests_unique_users_via_user_access: [10, 999, 777, 10], - k8s_api_proxy_requests_unique_agents_via_user_access: [10, 999, 777, 10], - k8s_api_proxy_requests_unique_users_via_pat_access: [10, 999, 777, 10], - k8s_api_proxy_requests_unique_agents_via_pat_access: [10, 999, 777, 10], - flux_git_push_notified_unique_projects: [10, 999, 777, 10] + agent_users_using_ci_tunnel: user_ids, + k8s_api_proxy_requests_unique_users_via_ci_access: user_ids, + k8s_api_proxy_requests_unique_agents_via_ci_access: user_ids, + k8s_api_proxy_requests_unique_users_via_user_access: user_ids, + k8s_api_proxy_requests_unique_agents_via_user_access: user_ids, + k8s_api_proxy_requests_unique_users_via_pat_access: user_ids, + k8s_api_proxy_requests_unique_agents_via_pat_access: user_ids, + flux_git_push_notified_unique_projects: user_ids } expected_counters = { kubernetes_agent_gitops_sync: request_count * counters[:gitops_sync], @@ -172,6 +162,87 @@ RSpec.describe API::Internal::Kubernetes, feature_category: :deployment_manageme end end + describe 'POST /internal/kubernetes/agent_events', :clean_gitlab_redis_shared_state do + def send_request(headers: {}, params: {}) + post api('/internal/kubernetes/agent_events'), params: params, headers: headers.reverse_merge(jwt_auth_headers) + end + + include_examples 'authorization' + include_examples 'error handling' + + context 'is authenticated for an agent' do + let!(:agent_token) { create(:cluster_agent_token) } + + context 'when events are valid' do + let(:request_count) { 2 } + let(:users) { create_list(:user, 3).index_by(&:id) } + let(:projects) { create_list(:project, 3).index_by(&:id) } + let(:events) do + user_ids = users.keys + project_ids = projects.keys + event_data = Array.new(3) do |i| + { user_id: user_ids[i], project_id: project_ids[i] } + end + { + k8s_api_proxy_requests_unique_users_via_ci_access: event_data, + k8s_api_proxy_requests_unique_users_via_user_access: event_data, + k8s_api_proxy_requests_unique_users_via_pat_access: event_data + } + end + + it 'tracks events and returns no_content', :aggregate_failures do + events.each do |event_name, event_list| + event_list.each do |event| + expect(Gitlab::InternalEvents).to receive(:track_event) + .with(event_name.to_s, user: users[event[:user_id]], project: projects[event[:project_id]]) + .exactly(request_count).times + end + end + + request_count.times do + send_request(params: { events: events }) + end + + expect(response).to have_gitlab_http_status(:no_content) + end + end + + context 'when events are empty' do + let(:events) do + { + k8s_api_proxy_requests_unique_users_via_ci_access: [], + k8s_api_proxy_requests_unique_users_via_user_access: [], + k8s_api_proxy_requests_unique_users_via_pat_access: [] + } + end + + it 'returns no_content for empty events' do + expect(Gitlab::InternalEvents).not_to receive(:track_event) + send_request(params: { events: events }) + + expect(response).to have_gitlab_http_status(:no_content) + end + end + + context 'when events have non-integer values' do + let(:events) do + { + k8s_api_proxy_requests_unique_users_via_ci_access: [ + { user_id: 'string', project_id: 111 } + ] + } + end + + it 'returns 400 for non-integer values' do + expect(Gitlab::InternalEvents).not_to receive(:track_event) + send_request(params: { events: events }) + + expect(response).to have_gitlab_http_status(:bad_request) + end + end + end + end + describe 'POST /internal/kubernetes/agent_configuration' do def send_request(headers: {}, params: {}) post api('/internal/kubernetes/agent_configuration'), params: params, headers: headers.reverse_merge(jwt_auth_headers) @@ -254,8 +325,7 @@ RSpec.describe API::Internal::Kubernetes, feature_category: :deployment_manageme 'agent_name' => agent.name, 'gitaly_info' => a_hash_including( 'address' => match(/\.socket$/), - 'token' => 'secret', - 'features' => Feature::Gitaly.server_feature_flags + 'token' => 'secret' ), 'gitaly_repository' => a_hash_including( 'storage_name' => project.repository_storage, @@ -297,8 +367,7 @@ RSpec.describe API::Internal::Kubernetes, feature_category: :deployment_manageme 'project_id' => project.id, 'gitaly_info' => a_hash_including( 'address' => match(/\.socket$/), - 'token' => 'secret', - 'features' => Feature::Gitaly.server_feature_flags + 'token' => 'secret' ), 'gitaly_repository' => a_hash_including( 'storage_name' => project.repository_storage, diff --git a/spec/requests/api/issues/issues_spec.rb b/spec/requests/api/issues/issues_spec.rb index ed71089c5a9..44d5d61ffd2 100644 --- a/spec/requests/api/issues/issues_spec.rb +++ b/spec/requests/api/issues/issues_spec.rb @@ -20,7 +20,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do let_it_be(:milestone) { create(:milestone, title: '1.0.0', project: project) } let_it_be(:empty_milestone) { create(:milestone, title: '2.0.0', project: project) } - let_it_be(:task) { create(:issue, :task, author: user, project: project) } + let_it_be(:objective) { create(:issue, :objective, author: user, project: project) } let_it_be(:closed_issue) do create :closed_issue, diff --git a/spec/requests/api/maven_packages_spec.rb b/spec/requests/api/maven_packages_spec.rb index 578a4821b5e..2110e4a077d 100644 --- a/spec/requests/api/maven_packages_spec.rb +++ b/spec/requests/api/maven_packages_spec.rb @@ -43,7 +43,7 @@ RSpec.describe API::MavenPackages, feature_category: :package_registry do project.add_developer(user) end - shared_examples 'handling groups and subgroups for' do |shared_example_name, visibilities: { public: :redirect }| + shared_examples 'handling groups and subgroups for' do |shared_example_name, shared_example_args = {}, visibilities: { public: :redirect }| context 'within a group' do visibilities.each do |visibility, not_found_response| context "that is #{visibility}" do @@ -51,7 +51,7 @@ RSpec.describe API::MavenPackages, feature_category: :package_registry do group.update!(visibility_level: Gitlab::VisibilityLevel.level_value(visibility.to_s)) end - it_behaves_like shared_example_name, not_found_response + it_behaves_like shared_example_name, not_found_response, shared_example_args end end end @@ -70,7 +70,7 @@ RSpec.describe API::MavenPackages, feature_category: :package_registry do group.update!(visibility_level: Gitlab::VisibilityLevel.level_value(visibility.to_s)) end - it_behaves_like shared_example_name, not_found_response + it_behaves_like shared_example_name, not_found_response, shared_example_args end end end @@ -621,7 +621,15 @@ RSpec.describe API::MavenPackages, feature_category: :package_registry do it_behaves_like 'rejecting request with invalid params' - it_behaves_like 'handling groups and subgroups for', 'getting a file for a group', visibilities: { internal: :unauthorized, public: :redirect } + it_behaves_like 'handling groups and subgroups for', 'getting a file for a group', visibilities: { internal: :unauthorized, public: :unauthorized } + + context 'when the FF maven_remove_permissions_check_from_finder disabled' do + before do + stub_feature_flags(maven_remove_permissions_check_from_finder: false) + end + + it_behaves_like 'handling groups and subgroups for', 'getting a file for a group', visibilities: { internal: :unauthorized, public: :redirect } + end end context 'private project' do @@ -631,7 +639,7 @@ RSpec.describe API::MavenPackages, feature_category: :package_registry do subject { download_file_with_token(file_name: package_file.file_name) } - shared_examples 'getting a file for a group' do |not_found_response| + shared_examples 'getting a file for a group' do |not_found_response, download_denied_status: :forbidden| it_behaves_like 'tracking the file download event' it_behaves_like 'bumping the package last downloaded at field' it_behaves_like 'successfully returning the file' @@ -641,7 +649,7 @@ RSpec.describe API::MavenPackages, feature_category: :package_registry do subject - expect(response).to have_gitlab_http_status(:redirect) + expect(response).to have_gitlab_http_status(download_denied_status) end it 'denies download when no private token' do @@ -682,7 +690,43 @@ RSpec.describe API::MavenPackages, feature_category: :package_registry do end end - it_behaves_like 'handling groups and subgroups for', 'getting a file for a group', visibilities: { private: :unauthorized, internal: :unauthorized, public: :redirect } + context 'with the duplicate packages in the two projects' do + let_it_be(:recent_project) { create(:project, :private, namespace: group) } + + let!(:package_dup) { create(:maven_package, project: recent_project, name: package.name, version: package.version) } + + before do + group.add_guest(user) + project.add_developer(user) + end + + context 'when user does not have enough permission for the recent project' do + it 'tries to download the recent package' do + subject + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + + context 'when the FF maven_remove_permissions_check_from_finder disabled' do + before do + stub_feature_flags(maven_remove_permissions_check_from_finder: false) + end + + it_behaves_like 'bumping the package last downloaded at field' + it_behaves_like 'successfully returning the file' + end + end + + it_behaves_like 'handling groups and subgroups for', 'getting a file for a group', visibilities: { private: :unauthorized, internal: :unauthorized, public: :unauthorized } + + context 'when the FF maven_remove_permissions_check_from_finder disabled' do + before do + stub_feature_flags(maven_remove_permissions_check_from_finder: false) + end + + it_behaves_like 'handling groups and subgroups for', 'getting a file for a group', { download_denied_status: :redirect }, visibilities: { private: :unauthorized, internal: :unauthorized, public: :redirect } + end context 'with a reporter from a subgroup accessing the root group' do let_it_be(:root_group) { create(:group, :private) } diff --git a/spec/requests/api/ml/mlflow/model_versions_spec.rb b/spec/requests/api/ml/mlflow/model_versions_spec.rb index f59888ec70f..e62bccf1507 100644 --- a/spec/requests/api/ml/mlflow/model_versions_spec.rb +++ b/spec/requests/api/ml/mlflow/model_versions_spec.rb @@ -35,9 +35,9 @@ RSpec.describe API::Ml::Mlflow::ModelVersions, feature_category: :mlops do response end - describe 'GET /projects/:id/ml/mlflow/api/2.0/mlflow/model_versions/get' do + 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}" + "/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 @@ -51,7 +51,7 @@ RSpec.describe API::Ml::Mlflow::ModelVersions, feature_category: :mlops 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}" + "/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' @@ -59,7 +59,7 @@ RSpec.describe API::Ml::Mlflow::ModelVersions, feature_category: :mlops do context 'and version in incorrect' do let(:route) do - "/projects/#{project_id}/ml/mlflow/api/2.0/mlflow/model_versions/get?name=#{name}&version=--" + "/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' @@ -79,8 +79,95 @@ RSpec.describe API::Ml::Mlflow::ModelVersions, feature_category: :mlops do end end - it_behaves_like 'MLflow|shared model registry error cases' - it_behaves_like 'MLflow|Requires read_api scope' + it_behaves_like 'MLflow|an authenticated resource' + it_behaves_like 'MLflow|a read-only model registry resource' + end + end + + describe 'UPDATE /projects/:id/ml/mlflow/api/2.0/mlflow/model-versions/update' do + let(:params) { { name: name, version: version, description: 'description-text' } } + let(:request) { patch api(route), params: params, headers: headers } + + let(:route) do + "/projects/#{project_id}/ml/mlflow/api/2.0/mlflow/model-versions/update" + 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(:params) { { name: 'invalid-name', version: version, description: 'description-text' } } + + it 'throws error 400' do + is_expected.to have_gitlab_http_status(:bad_request) + end + end + + context 'and version in incorrect' do + let(:params) { { name: name, version: 'invalid-version', description: 'description-text' } } + + it 'throws error 400' do + is_expected.to have_gitlab_http_status(:bad_request) + end + end + + context 'when user lacks write_model_registry rights' do + before do + allow(Ability).to receive(:allowed?).and_call_original + allow(Ability).to receive(:allowed?) + .with(current_user, :write_model_registry, project) + .and_return(false) + end + + it "is Not Found" do + is_expected.to have_gitlab_http_status(:unauthorized) + end + end + end + + it_behaves_like 'MLflow|an authenticated resource' + it_behaves_like 'MLflow|a read/write model registry resource' + end + end + + describe 'POST /projects/:id/ml/mlflow/api/2.0/mlflow/model_versions/create' do + let(:model_name) { model.name } + let(:route) do + "/projects/#{project_id}/ml/mlflow/api/2.0/mlflow/model-versions/create" + end + + let(:params) { { name: model_name, description: 'description-text' } } + let(:request) { post api(route), params: params, headers: headers } + + it 'returns the model', :aggregate_failures do + is_expected.to have_gitlab_http_status(:ok) + is_expected.to match_response_schema('ml/get_model_version') + end + + it 'increments the version if a model version already exists' do + create(:ml_model_versions, model: model, version: '1.0.0') + + is_expected.to have_gitlab_http_status(:ok) + expect(json_response["model_version"]["version"]).to eq('2.0.0') + 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 + end + + it_behaves_like 'MLflow|an authenticated resource' + it_behaves_like 'MLflow|a read/write model registry resource' 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 index cd8b0a53ef3..09cf765b0b3 100644 --- a/spec/requests/api/ml/mlflow/registered_models_spec.rb +++ b/spec/requests/api/ml/mlflow/registered_models_spec.rb @@ -56,8 +56,8 @@ RSpec.describe API::Ml::Mlflow::RegisteredModels, feature_category: :mlops do end end - it_behaves_like 'MLflow|shared model registry error cases' - it_behaves_like 'MLflow|Requires read_api scope' + it_behaves_like 'MLflow|an authenticated resource' + it_behaves_like 'MLflow|a read-only model registry resource' end end @@ -78,7 +78,7 @@ RSpec.describe API::Ml::Mlflow::RegisteredModels, feature_category: :mlops do context 'when the model name is not passed' do let(:params) { {} } - it_behaves_like 'MLflow|Bad Request' + it_behaves_like 'MLflow|an invalid request' end context 'when the model name already exists' do @@ -127,8 +127,8 @@ RSpec.describe API::Ml::Mlflow::RegisteredModels, feature_category: :mlops do end end - it_behaves_like 'MLflow|shared model registry error cases' - it_behaves_like 'MLflow|Requires api scope and write permission' + it_behaves_like 'MLflow|an authenticated resource' + it_behaves_like 'MLflow|a read/write model registry resource' end end @@ -160,8 +160,8 @@ RSpec.describe API::Ml::Mlflow::RegisteredModels, feature_category: :mlops do end end - it_behaves_like 'MLflow|shared model registry error cases' - it_behaves_like 'MLflow|Requires api scope and write permission' + it_behaves_like 'MLflow|an authenticated resource' + it_behaves_like 'MLflow|a read/write model registry resource' end end @@ -196,8 +196,88 @@ RSpec.describe API::Ml::Mlflow::RegisteredModels, feature_category: :mlops do end end - it_behaves_like 'MLflow|shared model registry error cases' - it_behaves_like 'MLflow|Requires read_api scope' + it_behaves_like 'MLflow|an authenticated resource' + it_behaves_like 'MLflow|a read-only model registry resource' + end + end + + describe 'DELETE /projects/:id/ml/mlflow/api/2.0/mlflow/registered-models/delete' do + let(:model_name) { model.name } + let(:params) { { name: model_name } } + let(:route) { "/projects/#{project_id}/ml/mlflow/api/2.0/mlflow/registered-models/delete" } + let(:request) { delete api(route), params: params, headers: headers } + + it 'returns a success response', :aggregate_failures do + is_expected.to have_gitlab_http_status(:ok) + expect(json_response).to eq({}) + end + + describe 'Error States' do + context 'when destroy fails' do + it 'returns an error' do + allow(Ml::DestroyModelService).to receive_message_chain(:new, :execute).and_return(false) + + is_expected.to have_gitlab_http_status(:bad_request) + expect(json_response["message"]).to eq("Model could not be deleted") + end + end + + 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|an authenticated resource' + it_behaves_like 'MLflow|a read/write model registry resource' + end + end + + describe 'GET /projects/:id/ml/mlflow/api/2.0/mlflow/registered-models/search' do + let_it_be(:model2) do + create(:ml_models, :with_metadata, project: project) + end + + let(:route) { "/projects/#{project_id}/ml/mlflow/api/2.0/mlflow/registered-models/search" } + + it 'returns all the models', :aggregate_failures do + is_expected.to have_gitlab_http_status(:ok) + is_expected.to match_response_schema('ml/list_models') + expect(json_response["registered_models"].count).to be(2) + end + + context "with a valid filter supplied" do + let(:filter) { "name='#{model2.name}'" } + let(:route) { "/projects/#{project_id}/ml/mlflow/api/2.0/mlflow/registered-models/search?filter=#{filter}" } + + it 'returns only the models for the given filter' do + is_expected.to have_gitlab_http_status(:ok) + expect(json_response["registered_models"].count).to be(1) + end + end + + context "with an invalid filter supplied" do + let(:filter) { "description='foo'" } + let(:route) { "/projects/#{project_id}/ml/mlflow/api/2.0/mlflow/registered-models/search?filter=#{filter}" } + + it 'returns an error' do + is_expected.to have_gitlab_http_status(:bad_request) + + expect(json_response).to include({ 'error_code' => 'INVALID_PARAMETER_VALUE' }) + end + end + + describe 'Error States' do + it_behaves_like 'MLflow|an authenticated resource' + it_behaves_like 'MLflow|a read-only model registry resource' end end end diff --git a/spec/requests/api/ml_model_packages_spec.rb b/spec/requests/api/ml_model_packages_spec.rb index 3166298b430..894127cac78 100644 --- a/spec/requests/api/ml_model_packages_spec.rb +++ b/spec/requests/api/ml_model_packages_spec.rb @@ -16,6 +16,8 @@ RSpec.describe ::API::MlModelPackages, feature_category: :mlops do let_it_be(:deploy_token) { create(:deploy_token, read_package_registry: true, write_package_registry: true) } let_it_be(:project_deploy_token) { create(:project_deploy_token, deploy_token: deploy_token, project: project) } let_it_be(:another_project, reload: true) { create(:project) } + let_it_be(:model) { create(:ml_models, user: project.owner, project: project) } + let_it_be(:model_version) { create(:ml_model_versions, :with_package, model: model, version: '0.1.0') } let_it_be(:tokens) do { @@ -70,10 +72,6 @@ RSpec.describe ::API::MlModelPackages, feature_category: :mlops do :private | :guest | false | :job_token | true | :not_found :private | :developer | false | :job_token | false | :unauthorized :private | :guest | false | :job_token | false | :unauthorized - :public | :developer | true | :deploy_token | true | :success - :public | :developer | true | :deploy_token | false | :unauthorized - :private | :developer | true | :deploy_token | true | :success - :private | :developer | true | :deploy_token | false | :unauthorized end # :visibility, :user_role, :member, :token_type, :valid_token, :expected_status @@ -112,10 +110,6 @@ RSpec.describe ::API::MlModelPackages, feature_category: :mlops do :private | :guest | false | :job_token | true | :not_found :private | :developer | false | :job_token | false | :unauthorized :private | :guest | false | :job_token | false | :unauthorized - :public | :developer | true | :deploy_token | true | :success - :public | :developer | true | :deploy_token | false | :unauthorized - :private | :developer | true | :deploy_token | true | :success - :private | :developer | true | :deploy_token | false | :unauthorized end # rubocop:enable Metrics/AbcSize end @@ -128,14 +122,15 @@ RSpec.describe ::API::MlModelPackages, feature_category: :mlops do include_context 'ml model authorize permissions table' let(:token) { tokens[:personal_access_token] } - let(:user_headers) { { 'HTTP_AUTHORIZATION' => token } } + let(:user_headers) { { 'Authorization' => "Bearer #{token}" } } let(:headers) { user_headers.merge(workhorse_headers) } let(:request) { authorize_upload_file(headers) } - let(:model_name) { 'my_package' } + let(:model_name) { model_version.name } + let(:version) { model_version.version } let(:file_name) { 'myfile.tar.gz' } subject(:api_response) do - url = "/projects/#{project.id}/packages/ml_models/#{model_name}/0.0.1/#{file_name}/authorize" + url = "/projects/#{project.id}/packages/ml_models/#{model_name}/#{version}/#{file_name}/authorize" put api(url), headers: headers @@ -149,7 +144,7 @@ RSpec.describe ::API::MlModelPackages, feature_category: :mlops do with_them do let(:token) { valid_token ? tokens[token_type] : 'invalid-token123' } - let(:user_headers) { user_role == :anonymous ? {} : { 'HTTP_AUTHORIZATION' => token } } + let(:user_headers) { user_role == :anonymous ? {} : { 'Authorization' => "Bearer #{token}" } } before do project.update_column(:visibility_level, Gitlab::VisibilityLevel.level_value(visibility.to_s)) @@ -183,15 +178,16 @@ RSpec.describe ::API::MlModelPackages, feature_category: :mlops do let_it_be(:file_name) { 'model.md5' } let(:token) { tokens[:personal_access_token] } - let(:user_headers) { { 'HTTP_AUTHORIZATION' => token } } + let(:user_headers) { { 'Authorization' => "Bearer #{token}" } } let(:headers) { user_headers.merge(workhorse_headers) } let(:params) { { file: temp_file(file_name) } } let(:file_key) { :file } let(:send_rewritten_field) { true } - let(:model_name) { 'my_package' } + let(:model_name) { model_version.name } + let(:version) { model_version.version } subject(:api_response) do - url = "/projects/#{project.id}/packages/ml_models/#{model_name}/0.0.1/#{file_name}" + url = "/projects/#{project.id}/packages/ml_models/#{model_name}/#{version}/#{file_name}" workhorse_finalize( api(url), @@ -219,7 +215,7 @@ RSpec.describe ::API::MlModelPackages, feature_category: :mlops do with_them do let(:token) { valid_token ? tokens[token_type] : 'invalid-token123' } - let(:user_headers) { user_role == :anonymous ? {} : { 'HTTP_AUTHORIZATION' => token } } + let(:user_headers) { user_role == :anonymous ? {} : { 'Authorization' => "Bearer #{token}" } } before do project.update_column(:visibility_level, Gitlab::VisibilityLevel.level_value(visibility.to_s)) @@ -233,25 +229,27 @@ RSpec.describe ::API::MlModelPackages, feature_category: :mlops do end it_behaves_like 'Endpoint not found if read_model_registry not available' + it_behaves_like 'Endpoint not found if write_model_registry not available' + it_behaves_like 'Not found when model version does not exist' end end describe 'GET /api/v4/projects/:project_id/packages/ml_models/:model_name/:model_version/:file_name' do include_context 'ml model authorize permissions table' - let_it_be(:package) { create(:ml_model_package, project: project, name: 'model', version: '0.0.1') } + let_it_be(:package) { model_version.package } let_it_be(:package_file) { create(:package_file, :generic, package: package, file_name: 'model.md5') } - let(:model_name) { package.name } - let(:model_version) { package.version } + let(:model_name) { model_version.name } + let(:version) { model_version.version } let(:file_name) { package_file.file_name } let(:token) { tokens[:personal_access_token] } - let(:user_headers) { { 'HTTP_AUTHORIZATION' => token } } + let(:user_headers) { { 'Authorization' => "Bearer #{token}" } } let(:headers) { user_headers.merge(workhorse_headers) } subject(:api_response) do - url = "/projects/#{project.id}/packages/ml_models/#{model_name}/#{model_version}/#{file_name}" + url = "/projects/#{project.id}/packages/ml_models/#{model_name}/#{version}/#{file_name}" get api(url), headers: headers @@ -265,7 +263,7 @@ RSpec.describe ::API::MlModelPackages, feature_category: :mlops do with_them do let(:token) { valid_token ? tokens[token_type] : 'invalid-token123' } - let(:user_headers) { user_role == :anonymous ? {} : { 'HTTP_AUTHORIZATION' => token } } + let(:user_headers) { user_role == :anonymous ? {} : { 'Authorization' => "Bearer #{token}" } } before do project.update_column(:visibility_level, Gitlab::VisibilityLevel.level_value(visibility.to_s)) diff --git a/spec/requests/api/npm_project_packages_spec.rb b/spec/requests/api/npm_project_packages_spec.rb index b5f38698857..17cb5cf893e 100644 --- a/spec/requests/api/npm_project_packages_spec.rb +++ b/spec/requests/api/npm_project_packages_spec.rb @@ -54,14 +54,6 @@ RSpec.describe API::NpmProjectPackages, feature_category: :package_registry do it_behaves_like 'does not enqueue a worker to sync a metadata cache' - context 'when npm_metadata_cache disabled' do - before do - stub_feature_flags(npm_metadata_cache: false) - end - - it_behaves_like 'generates metadata response "on-the-fly"' - end - context 'when metadata cache file does not exist' do before do FileUtils.rm_rf(npm_metadata_cache.file.path) diff --git a/spec/requests/api/nuget_group_packages_spec.rb b/spec/requests/api/nuget_group_packages_spec.rb index 92eb869b871..4a763b3bbda 100644 --- a/spec/requests/api/nuget_group_packages_spec.rb +++ b/spec/requests/api/nuget_group_packages_spec.rb @@ -188,6 +188,14 @@ RSpec.describe API::NugetGroupPackages, feature_category: :package_registry do end end + describe 'GET /api/v4/groups/:id/-/packages/nuget/token/*token/symbolfiles/*file_name/*signature/*file_name' do + it_behaves_like 'nuget symbol file endpoint' do + let(:url) do + "/groups/#{target.id}/-/packages/nuget/symbolfiles/#{filename}/#{signature}/#{filename}" + end + end + end + def update_visibility_to(visibility) project.update!(visibility_level: visibility) subgroup.update!(visibility_level: visibility) diff --git a/spec/requests/api/nuget_project_packages_spec.rb b/spec/requests/api/nuget_project_packages_spec.rb index a116be84b3e..8252fc1c4cd 100644 --- a/spec/requests/api/nuget_project_packages_spec.rb +++ b/spec/requests/api/nuget_project_packages_spec.rb @@ -419,6 +419,12 @@ RSpec.describe API::NugetProjectPackages, feature_category: :package_registry do end end + describe 'GET /api/v4/projects/:id/packages/nuget/symbolfiles/*file_name/*signature/*file_name' do + it_behaves_like 'nuget symbol file endpoint' do + let(:url) { "/projects/#{target.id}/packages/nuget/symbolfiles/#{filename}/#{signature}/#{filename}" } + end + end + def update_visibility_to(visibility) project.update!(visibility_level: visibility) end diff --git a/spec/requests/api/project_attributes.yml b/spec/requests/api/project_attributes.yml index 165ea7bf66e..bab5bd2b6ac 100644 --- a/spec/requests/api/project_attributes.yml +++ b/spec/requests/api/project_attributes.yml @@ -151,6 +151,7 @@ project_setting: - legacy_open_source_license_available - prevent_merge_without_jira_issue - only_allow_merge_if_all_status_checks_passed + - allow_merge_without_pipeline - warn_about_potentially_unwanted_characters - previous_default_branch - project_id @@ -195,5 +196,6 @@ build_service_desk_setting: # service_desk_setting - encrypted_custom_email_smtp_password_iv - custom_email_smtp_password - add_external_participants_from_cc + - reopen_issue_on_external_participant_note remapped_attributes: project_key: service_desk_address diff --git a/spec/requests/api/project_events_spec.rb b/spec/requests/api/project_events_spec.rb index f904cd8fd6c..52a6093c4c8 100644 --- a/spec/requests/api/project_events_spec.rb +++ b/spec/requests/api/project_events_spec.rb @@ -3,11 +3,11 @@ require 'spec_helper' RSpec.describe API::ProjectEvents, feature_category: :user_profile do - let(:user) { create(:user) } - let(:non_member) { create(:user) } - let(:private_project) { create(:project, :private, creator_id: user.id, namespace: user.namespace) } - let(:closed_issue) { create(:closed_issue, project: private_project, author: user) } - let!(:closed_issue_event) { create(:event, project: private_project, author: user, target: closed_issue, action: :closed, created_at: Date.new(2016, 12, 30)) } + let_it_be(:user) { create(:user) } + let_it_be(:non_member) { create(:user) } + let_it_be(:private_project) { create(:project, :private, creator_id: user.id, namespace: user.namespace) } + let_it_be(:closed_issue) { create(:closed_issue, project: private_project, author: user) } + let_it_be(:closed_issue_event) { create(:closed_issue_event, project: private_project, author: user, target: closed_issue, created_at: Date.new(2016, 12, 30)) } describe 'GET /projects/:id/events' do context 'when unauthenticated ' do @@ -27,11 +27,11 @@ RSpec.describe API::ProjectEvents, feature_category: :user_profile do end context 'with inaccessible events' do - let(:public_project) { create(:project, :public, creator_id: user.id, namespace: user.namespace) } - let(:confidential_issue) { create(:closed_issue, confidential: true, project: public_project, author: user) } - let!(:confidential_event) { create(:event, project: public_project, author: user, target: confidential_issue, action: :closed) } - let(:public_issue) { create(:closed_issue, project: public_project, author: user) } - let!(:public_event) { create(:event, project: public_project, author: user, target: public_issue, action: :closed) } + let_it_be(:public_project) { create(:project, :public, creator_id: user.id, namespace: user.namespace) } + let_it_be(:confidential_issue) { create(:closed_issue, :confidential, project: public_project, author: user) } + let_it_be(:confidential_event) { create(:closed_issue_event, project: public_project, author: user, target: confidential_issue) } + let_it_be(:public_issue) { create(:closed_issue, project: public_project, author: user) } + let_it_be(:public_event) { create(:closed_issue_event, project: public_project, author: user, target: public_issue) } it 'returns only accessible events' do get api("/projects/#{public_project.id}/events", non_member) @@ -124,23 +124,34 @@ RSpec.describe API::ProjectEvents, feature_category: :user_profile do end context 'when exists some events' do - let(:merge_request1) { create(:merge_request, :closed, author: user, assignees: [user], source_project: private_project, title: 'Test') } - let(:merge_request2) { create(:merge_request, :closed, author: user, assignees: [user], source_project: private_project, title: 'Test') } + let_it_be(:merge_request1) { create(:closed_merge_request, author: user, assignees: [user], source_project: private_project) } + let_it_be(:merge_request2) { create(:closed_merge_request, author: user, assignees: [user], source_project: private_project) } + + let_it_be(:token) { create(:personal_access_token, user: user) } before do create_event(merge_request1) end it 'avoids N+1 queries' do - control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do - get api("/projects/#{private_project.id}/events", user), params: { target_type: :merge_request } - end.count + # Warmup, e.g. users#last_activity_on + get api("/projects/#{private_project.id}/events", personal_access_token: token), params: { target_type: :merge_request } + + control = ActiveRecord::QueryRecorder.new(skip_cached: false) do + get api("/projects/#{private_project.id}/events", personal_access_token: token), params: { target_type: :merge_request } + end create_event(merge_request2) expect do - get api("/projects/#{private_project.id}/events", user), params: { target_type: :merge_request } - end.not_to exceed_all_query_limit(control_count) + get api("/projects/#{private_project.id}/events", personal_access_token: token), params: { target_type: :merge_request } + end.to issue_same_number_of_queries_as(control).with_threshold(1) + # The extra threshold is because we need to fetch `project` for the 2nd + # event. This is because in `app/policies/issuable_policy.rb`, we fetch + # the `project` for the `target` for the `event`. It is non-trivial to + # re-use the original `project` object from `lib/api/project_events.rb` + # + # https://gitlab.com/gitlab-org/gitlab/-/issues/432823 expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers diff --git a/spec/requests/api/project_export_spec.rb b/spec/requests/api/project_export_spec.rb index 22729e068da..6d5591d7500 100644 --- a/spec/requests/api/project_export_spec.rb +++ b/spec/requests/api/project_export_spec.rb @@ -688,6 +688,8 @@ RSpec.describe API::ProjectExport, :aggregate_failures, :clean_gitlab_redis_cach expect(response).to have_gitlab_http_status(:ok) expect(json_response.pluck('relation')).to contain_exactly('labels', 'milestones', 'project_badges') expect(json_response.pluck('status')).to contain_exactly(-1, 0, 1) + expect(json_response.pluck('batched')).to all(eq(false)) + expect(json_response.pluck('batches_count')).to all(eq(0)) end context 'when relation is specified' do @@ -699,6 +701,36 @@ RSpec.describe API::ProjectExport, :aggregate_failures, :clean_gitlab_redis_cach expect(json_response['status']).to eq(0) end end + + context 'when there is a batched export' do + let_it_be(:batched_export) do + create(:bulk_import_export, :started, :batched, project: project, relation: 'issues', batches_count: 1) + end + + let_it_be(:batch) { create(:bulk_import_export_batch, objects_count: 5, export: batched_export) } + + it 'returns a list of batched relation export statuses' do + get api(status_path, user) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to include( + hash_including( + 'relation' => batched_export.relation, + 'batched' => true, + 'batches_count' => 1, + 'batches' => contain_exactly( + { + 'batch_number' => 1, + 'error' => nil, + 'objects_count' => batch.objects_count, + 'status' => batch.status, + 'updated_at' => batch.updated_at.as_json + } + ) + ) + ) + end + end end context 'with bulk_import is disabled' do diff --git a/spec/requests/api/project_milestones_spec.rb b/spec/requests/api/project_milestones_spec.rb index 978ac28ef73..e4b579b96cc 100644 --- a/spec/requests/api/project_milestones_spec.rb +++ b/spec/requests/api/project_milestones_spec.rb @@ -40,11 +40,27 @@ RSpec.describe API::ProjectMilestones, feature_category: :team_planning do it_behaves_like 'listing all milestones' - context 'when include_parent_milestones is true' do + context 'when include_ancestors is true' do + let(:params) { { include_ancestors: true } } + + it_behaves_like 'listing all milestones' + end + + context 'when deprecated include_parent_milestones is true' do let(:params) { { include_parent_milestones: true } } it_behaves_like 'listing all milestones' end + + context 'when both include_parent_milestones and include_ancestors are specified' do + let(:params) { { include_ancestors: true, include_parent_milestones: true } } + + it 'returns 400' do + get api(route, user), params: params + + expect(response).to have_gitlab_http_status(:bad_request) + end + end end context 'when project parent is a group' do @@ -52,14 +68,14 @@ RSpec.describe API::ProjectMilestones, feature_category: :team_planning do project.update!(namespace: group) end - context 'when include_parent_milestones is true' do - let(:params) { { include_parent_milestones: true } } + context 'when include_ancestors is true' do + let(:params) { { include_ancestors: true } } let(:milestones) { [group_milestone, ancestor_group_milestone, milestone, closed_milestone] } it_behaves_like 'listing all milestones' context 'when iids param is present' do - let(:params) { { include_parent_milestones: true, iids: [group_milestone.iid] } } + let(:params) { { include_ancestors: true, iids: [group_milestone.iid] } } it_behaves_like 'listing all milestones' end @@ -75,7 +91,7 @@ RSpec.describe API::ProjectMilestones, feature_category: :team_planning do end context 'when updated_before param is present' do - let(:params) { { updated_before: 12.hours.ago.iso8601, include_parent_milestones: true } } + let(:params) { { updated_before: 12.hours.ago.iso8601, include_ancestors: true } } it_behaves_like 'listing all milestones' do let(:milestones) { [group_milestone, ancestor_group_milestone, milestone] } @@ -83,7 +99,7 @@ RSpec.describe API::ProjectMilestones, feature_category: :team_planning do end context 'when updated_after param is present' do - let(:params) { { updated_after: 2.days.ago.iso8601, include_parent_milestones: true } } + let(:params) { { updated_after: 2.days.ago.iso8601, include_ancestors: true } } it_behaves_like 'listing all milestones' do let(:milestones) { [ancestor_group_milestone, closed_milestone] } diff --git a/spec/requests/api/project_templates_spec.rb b/spec/requests/api/project_templates_spec.rb index 1987d70633b..920fbe5f174 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 (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 (test)]) end it 'returns 400 for an unknown template type' do @@ -171,6 +171,17 @@ RSpec.describe API::ProjectTemplates, feature_category: :source_code_management expect(json_response['content']).to eq('something valid') end + context 'when issue template uses parentheses' do + it 'returns a specific issue template' do + get api("/projects/#{private_project.id}/templates/issues/(test)", developer) + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema('public_api/v4/template') + expect(json_response['name']).to eq('(test)') + expect(json_response['content']).to eq('parentheses') + end + end + it 'returns a specific merge request template' do get api("/projects/#{public_project.id}/templates/merge_requests/feature_proposal") diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index e9319d514aa..b8e029385e3 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -1313,6 +1313,7 @@ RSpec.describe API::Projects, :aggregate_failures, feature_category: :groups_and attrs[:merge_requests_access_level] = 'disabled' attrs[:issues_access_level] = 'disabled' attrs[:model_experiments_access_level] = 'disabled' + attrs[:model_registry_access_level] = 'disabled' end post api(path, user), params: project @@ -1323,7 +1324,7 @@ RSpec.describe API::Projects, :aggregate_failures, feature_category: :groups_and next if %i[ has_external_issue_tracker has_external_wiki issues_enabled merge_requests_enabled wiki_enabled storage_version container_registry_access_level releases_access_level environments_access_level feature_flags_access_level - infrastructure_access_level monitor_access_level model_experiments_access_level + infrastructure_access_level monitor_access_level model_experiments_access_level model_registry_access_level ].include?(k) expect(json_response[k.to_s]).to eq(v) @@ -2852,16 +2853,6 @@ RSpec.describe API::Projects, :aggregate_failures, feature_category: :groups_and expect(response).to have_gitlab_http_status(:not_found) end end - - context 'when api_redirect_moved_projects is disabled' do - it 'returns a 404 error' do - stub_feature_flags(api_redirect_moved_projects: false) - - perform_request - - expect(response).to have_gitlab_http_status(:not_found) - end - end end it 'returns a 404 error if not found' do @@ -3667,11 +3658,15 @@ RSpec.describe API::Projects, :aggregate_failures, feature_category: :groups_and expect(json_response['error']).to eq 'group_access does not have a valid value' end - it "returns a 400 error when the project-group share is created with an OWNER access level" do - post api(path, user), params: { group_id: group.id, group_access: Gitlab::Access::OWNER } + it 'returns a 403 when a maintainer tries to create a link with OWNER access' do + user = create(:user) + project.add_maintainer(user) - expect(response).to have_gitlab_http_status(:bad_request) - expect(json_response['error']).to eq 'group_access does not have a valid value' + expect do + post api(path, user), params: { group_id: group.id, group_access: Gitlab::Access::OWNER } + end.to not_change { project.reload.project_group_links.count } + + expect(response).to have_gitlab_http_status(:forbidden) end it "returns a 409 error when link is not saved" do @@ -3700,11 +3695,12 @@ RSpec.describe API::Projects, :aggregate_failures, feature_category: :groups_and context 'for a valid group' do let_it_be(:group) { create(:group, :private) } let_it_be(:group_user) { create(:user) } + let(:group_access) { Gitlab::Access::DEVELOPER } before do group.add_developer(group_user) - create(:project_group_link, group: group, project: project) + create(:project_group_link, group: group, project: project, group_access: group_access) end it 'returns 204 when deleting a group share' do @@ -3735,6 +3731,21 @@ RSpec.describe API::Projects, :aggregate_failures, feature_category: :groups_and expect(response).to have_gitlab_http_status(:not_found) expect(json_response['message']).to eq '404 Not Found' end + + context 'when a MAINTAINER tries to destroy a link with OWNER access' do + let(:group_access) { Gitlab::Access::OWNER } + + it 'returns 403' do + user = create(:user) + project.add_maintainer(user) + + expect do + delete api("/projects/#{project.id}/share/#{group.id}", user) + end.to not_change { project.reload.project_group_links.count } + + expect(response).to have_gitlab_http_status(:forbidden) + end + end end it 'returns a 400 when group id is not an integer' do @@ -3940,7 +3951,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 model_registry_access_level].each do |field| it "sets #{field}" do put api(path, user), params: { field => 'private' } @@ -4465,7 +4476,7 @@ RSpec.describe API::Projects, :aggregate_failures, feature_category: :groups_and end it 'returns 200 when repository storage has changed' do - stub_storage_settings('test_second_storage' => { 'path' => TestEnv::SECOND_STORAGE_PATH }) + stub_storage_settings('test_second_storage' => {}) expect do Sidekiq::Testing.fake! do diff --git a/spec/requests/api/remote_mirrors_spec.rb b/spec/requests/api/remote_mirrors_spec.rb index 3da1760e319..7d37d73a361 100644 --- a/spec/requests/api/remote_mirrors_spec.rb +++ b/spec/requests/api/remote_mirrors_spec.rb @@ -50,7 +50,7 @@ RSpec.describe API::RemoteMirrors, feature_category: :source_code_management do let(:route) { "/projects/#{project.id}/remote_mirrors" } shared_examples 'creates a remote mirror' do - it 'creates a remote mirror and returns reponse' do + it 'creates a remote mirror and returns response' do project.add_maintainer(user) post api(route, user), params: params diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb index 5656fda7684..4e24689c17a 100644 --- a/spec/requests/api/settings_spec.rb +++ b/spec/requests/api/settings_spec.rb @@ -8,6 +8,12 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting, featu let_it_be(:admin) { create(:admin) } describe "GET /application/settings" do + before do + # Testing config file config/gitlab.yml becomes SSOT for this API + # see https://gitlab.com/gitlab-org/gitlab/-/issues/426091#note_1675160909 + stub_storage_settings({ 'default' => {}, 'custom' => {} }) + end + it "returns application settings" do get api("/application/settings", admin) @@ -15,7 +21,7 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting, featu expect(json_response).to be_an Hash expect(json_response['default_projects_limit']).to eq(42) expect(json_response['password_authentication_enabled_for_web']).to be_truthy - expect(json_response['repository_storages_weighted']).to eq({ 'default' => 100 }) + expect(json_response['repository_storages_weighted']).to eq({ 'default' => 100, 'custom' => 0 }) expect(json_response['password_authentication_enabled']).to be_truthy expect(json_response['plantuml_enabled']).to be_falsey expect(json_response['plantuml_url']).to be_nil @@ -87,6 +93,7 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting, featu expect(json_response['default_branch_protection_defaults']).to be_kind_of(Hash) expect(json_response['max_login_attempts']).to be_nil expect(json_response['failed_login_attempts_unlock_period_in_minutes']).to be_nil + expect(json_response['bulk_import_concurrent_pipeline_batch_limit']).to eq(25) end end @@ -109,7 +116,7 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting, featu } expect(response).to have_gitlab_http_status(:ok) - expect(json_response['repository_storages_weighted']).to eq({ 'custom' => 75 }) + expect(json_response['repository_storages_weighted']).to eq({ 'default' => 0, 'custom' => 75 }) end context "repository_storages_weighted value is outside a 0-100 range" do @@ -131,7 +138,7 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting, featu default_projects_limit: 3, default_project_creation: 2, password_authentication_enabled_for_web: false, - repository_storages_weighted: { 'custom' => 100 }, + repository_storages_weighted: { 'default' => 100, 'custom' => 0 }, plantuml_enabled: true, plantuml_url: 'http://plantuml.example.com', diagramsnet_enabled: false, @@ -196,6 +203,7 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting, featu jira_connect_proxy_url: 'http://example.com', bulk_import_enabled: false, bulk_import_max_download_file_size: 1, + bulk_import_concurrent_pipeline_batch_limit: 2, allow_runner_registration_token: true, user_defaults_to_private_profile: true, default_syntax_highlighting_theme: 2, @@ -205,7 +213,8 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting, featu allow_account_deletion: false, gitlab_shell_operation_limit: 500, namespace_aggregation_schedule_lease_duration_in_seconds: 400, - max_import_remote_file_size: 2 + max_import_remote_file_size: 2, + security_txt_content: nil } expect(response).to have_gitlab_http_status(:ok) @@ -213,7 +222,7 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting, featu expect(json_response['default_projects_limit']).to eq(3) expect(json_response['default_project_creation']).to eq(::Gitlab::Access::DEVELOPER_MAINTAINER_PROJECT_ACCESS) expect(json_response['password_authentication_enabled_for_web']).to be_falsey - expect(json_response['repository_storages_weighted']).to eq({ 'custom' => 100 }) + expect(json_response['repository_storages_weighted']).to eq({ 'default' => 100, 'custom' => 0 }) expect(json_response['plantuml_enabled']).to be_truthy expect(json_response['plantuml_url']).to eq('http://plantuml.example.com') expect(json_response['diagramsnet_enabled']).to be_falsey @@ -288,6 +297,8 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting, featu expect(json_response['namespace_aggregation_schedule_lease_duration_in_seconds']).to be(400) expect(json_response['max_import_remote_file_size']).to be(2) expect(json_response['bulk_import_max_download_file_size']).to be(1) + expect(json_response['security_txt_content']).to be(nil) + expect(json_response['bulk_import_concurrent_pipeline_batch_limit']).to be(2) end end @@ -1062,5 +1073,19 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting, featu expect(json_response['failed_login_attempts_unlock_period_in_minutes']).to eq(30) end end + + context 'security txt settings' do + let(:content) { "Contact: foo@acme.com" } + + it 'updates the settings' do + put( + api("/application/settings", admin), + params: { security_txt_content: content } + ) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['security_txt_content']).to eq(content) + end + end end end diff --git a/spec/requests/api/snippets_spec.rb b/spec/requests/api/snippets_spec.rb index 0b97bb5c443..b43f98e5323 100644 --- a/spec/requests/api/snippets_spec.rb +++ b/spec/requests/api/snippets_spec.rb @@ -115,8 +115,8 @@ RSpec.describe API::Snippets, :aggregate_failures, factory_default: :keep, featu public_snippet.id, public_snippet_other.id) expect(json_response.map { |snippet| snippet['web_url'] }).to contain_exactly( - "http://localhost/-/snippets/#{public_snippet.id}", - "http://localhost/-/snippets/#{public_snippet_other.id}") + "http://#{Gitlab.config.gitlab.host}/-/snippets/#{public_snippet.id}", + "http://#{Gitlab.config.gitlab.host}/-/snippets/#{public_snippet_other.id}") expect(json_response[0]['files'].first).to eq snippet_blob_file(public_snippet_other.blobs.first) expect(json_response[1]['files'].first).to eq snippet_blob_file(public_snippet.blobs.first) end diff --git a/spec/requests/api/terraform/modules/v1/packages_spec.rb b/spec/requests/api/terraform/modules/v1/packages_spec.rb index f479ca25f3c..949acdb17e1 100644 --- a/spec/requests/api/terraform/modules/v1/packages_spec.rb +++ b/spec/requests/api/terraform/modules/v1/packages_spec.rb @@ -3,31 +3,9 @@ require 'spec_helper' RSpec.describe API::Terraform::Modules::V1::Packages, feature_category: :package_registry do - include PackagesManagerApiSpecHelpers - include WorkhorseHelpers + include_context 'for terraform modules api setup' using RSpec::Parameterized::TableSyntax - let_it_be_with_reload(:group) { create(:group) } - let_it_be_with_reload(:project) { create(:project, namespace: group) } - let_it_be(:package) { create(:terraform_module_package, project: project) } - let_it_be(:personal_access_token) { create(:personal_access_token) } - let_it_be(:user) { personal_access_token.user } - let_it_be(:job) { create(:ci_build, :running, user: user, project: project) } - let_it_be(:deploy_token) { create(:deploy_token, read_package_registry: true, write_package_registry: true) } - let_it_be(:project_deploy_token) { create(:project_deploy_token, deploy_token: deploy_token, project: project) } - - let(:headers) { {} } - let(:token) { tokens[token_type] } - - let(:tokens) do - { - personal_access_token: personal_access_token.token, - deploy_token: deploy_token.token, - job_token: job.token, - invalid: 'invalid-token123' - } - end - describe 'GET /api/v4/packages/terraform/modules/v1/:module_namespace/:module_name/:module_system/versions' do let(:url) { api("/packages/terraform/modules/v1/#{group.path}/#{package.name}/versions") } let(:headers) { { 'Authorization' => "Bearer #{tokens[:job_token]}" } } @@ -456,198 +434,4 @@ RSpec.describe API::Terraform::Modules::V1::Packages, feature_category: :package end end end - - describe 'PUT /api/v4/projects/:project_id/packages/terraform/modules/:module_name/:module_system/:module_version/file/authorize' do - include_context 'workhorse headers' - - let(:url) { api("/projects/#{project.id}/packages/terraform/modules/mymodule/mysystem/1.0.0/file/authorize") } - let(:headers) { {} } - - subject { put(url, headers: headers) } - - context 'with valid project' do - where(:visibility, :user_role, :member, :token_header, :token_type, :shared_examples_name, :expected_status) do - :public | :developer | true | 'PRIVATE-TOKEN' | :personal_access_token | 'process terraform module workhorse authorization' | :success - :public | :guest | true | 'PRIVATE-TOKEN' | :personal_access_token | 'rejects terraform module packages access' | :forbidden - :public | :developer | true | 'PRIVATE-TOKEN' | :invalid | 'rejects terraform module packages access' | :unauthorized - :public | :guest | true | 'PRIVATE-TOKEN' | :invalid | 'rejects terraform module packages access' | :unauthorized - :public | :developer | false | 'PRIVATE-TOKEN' | :personal_access_token | 'rejects terraform module packages access' | :forbidden - :public | :guest | false | 'PRIVATE-TOKEN' | :personal_access_token | 'rejects terraform module packages access' | :forbidden - :public | :developer | false | 'PRIVATE-TOKEN' | :invalid | 'rejects terraform module packages access' | :unauthorized - :public | :guest | false | 'PRIVATE-TOKEN' | :invalid | 'rejects terraform module packages access' | :unauthorized - :public | :anonymous | false | nil | nil | 'rejects terraform module packages access' | :unauthorized - :private | :developer | true | 'PRIVATE-TOKEN' | :personal_access_token | 'process terraform module workhorse authorization' | :success - :private | :guest | true | 'PRIVATE-TOKEN' | :personal_access_token | 'rejects terraform module packages access' | :forbidden - :private | :developer | true | 'PRIVATE-TOKEN' | :invalid | 'rejects terraform module packages access' | :unauthorized - :private | :guest | true | 'PRIVATE-TOKEN' | :invalid | 'rejects terraform module packages access' | :unauthorized - :private | :developer | false | 'PRIVATE-TOKEN' | :personal_access_token | 'rejects terraform module packages access' | :not_found - :private | :guest | false | 'PRIVATE-TOKEN' | :personal_access_token | 'rejects terraform module packages access' | :not_found - :private | :developer | false | 'PRIVATE-TOKEN' | :invalid | 'rejects terraform module packages access' | :unauthorized - :private | :guest | false | 'PRIVATE-TOKEN' | :invalid | 'rejects terraform module packages access' | :unauthorized - :private | :anonymous | false | nil | nil | 'rejects terraform module packages access' | :unauthorized - :public | :developer | true | 'JOB-TOKEN' | :job_token | 'process terraform module workhorse authorization' | :success - :public | :guest | true | 'JOB-TOKEN' | :job_token | 'rejects terraform module packages access' | :forbidden - :public | :developer | true | 'JOB-TOKEN' | :invalid | 'rejects terraform module packages access' | :unauthorized - :public | :guest | true | 'JOB-TOKEN' | :invalid | 'rejects terraform module packages access' | :unauthorized - :public | :developer | false | 'JOB-TOKEN' | :job_token | 'rejects terraform module packages access' | :forbidden - :public | :guest | false | 'JOB-TOKEN' | :job_token | 'rejects terraform module packages access' | :forbidden - :public | :developer | false | 'JOB-TOKEN' | :invalid | 'rejects terraform module packages access' | :unauthorized - :public | :guest | false | 'JOB-TOKEN' | :invalid | 'rejects terraform module packages access' | :unauthorized - :private | :developer | true | 'JOB-TOKEN' | :job_token | 'process terraform module workhorse authorization' | :success - :private | :guest | true | 'JOB-TOKEN' | :job_token | 'rejects terraform module packages access' | :forbidden - :private | :developer | true | 'JOB-TOKEN' | :invalid | 'rejects terraform module packages access' | :unauthorized - :private | :guest | true | 'JOB-TOKEN' | :invalid | 'rejects terraform module packages access' | :unauthorized - :private | :developer | false | 'JOB-TOKEN' | :job_token | 'rejects terraform module packages access' | :not_found - :private | :guest | false | 'JOB-TOKEN' | :job_token | 'rejects terraform module packages access' | :not_found - :private | :developer | false | 'JOB-TOKEN' | :invalid | 'rejects terraform module packages access' | :unauthorized - :private | :guest | false | 'JOB-TOKEN' | :invalid | 'rejects terraform module packages access' | :unauthorized - :public | :developer | true | 'DEPLOY-TOKEN' | :deploy_token | 'process terraform module workhorse authorization' | :success - :public | :developer | true | 'DEPLOY-TOKEN' | :invalid | 'rejects terraform module packages access' | :unauthorized - :private | :developer | true | 'DEPLOY-TOKEN' | :deploy_token | 'process terraform module workhorse authorization' | :success - :private | :developer | true | 'DEPLOY-TOKEN' | :invalid | 'rejects terraform module packages access' | :unauthorized - end - - with_them do - let(:headers) { user_headers.merge(workhorse_headers) } - let(:user_headers) { user_role == :anonymous ? {} : { token_header => token } } - - before do - project.update!(visibility: visibility.to_s) - end - - it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member] - end - end - end - - describe 'PUT /api/v4/projects/:project_id/packages/terraform/modules/:module_name/:module_system/:module_version/file' do - include_context 'workhorse headers' - - let_it_be(:file_name) { 'module-system-v1.0.0.tgz' } - - let(:url) { "/projects/#{project.id}/packages/terraform/modules/mymodule/mysystem/1.0.0/file" } - let(:headers) { {} } - let(:params) { { file: temp_file(file_name) } } - let(:file_key) { :file } - let(:send_rewritten_field) { true } - - subject do - workhorse_finalize( - api(url), - method: :put, - file_key: file_key, - params: params, - headers: headers, - send_rewritten_field: send_rewritten_field - ) - end - - context 'with valid project' do - where(:visibility, :user_role, :member, :token_header, :token_type, :shared_examples_name, :expected_status) do - :public | :developer | true | 'PRIVATE-TOKEN' | :personal_access_token | 'process terraform module upload' | :created - :public | :guest | true | 'PRIVATE-TOKEN' | :personal_access_token | 'rejects terraform module packages access' | :forbidden - :public | :developer | true | 'PRIVATE-TOKEN' | :invalid | 'rejects terraform module packages access' | :unauthorized - :public | :guest | true | 'PRIVATE-TOKEN' | :invalid | 'rejects terraform module packages access' | :unauthorized - :public | :developer | false | 'PRIVATE-TOKEN' | :personal_access_token | 'rejects terraform module packages access' | :forbidden - :public | :guest | false | 'PRIVATE-TOKEN' | :personal_access_token | 'rejects terraform module packages access' | :forbidden - :public | :developer | false | 'PRIVATE-TOKEN' | :invalid | 'rejects terraform module packages access' | :unauthorized - :public | :guest | false | 'PRIVATE-TOKEN' | :invalid | 'rejects terraform module packages access' | :unauthorized - :public | :anonymous | false | nil | nil | 'rejects terraform module packages access' | :unauthorized - :private | :developer | true | 'PRIVATE-TOKEN' | :personal_access_token | 'process terraform module upload' | :created - :private | :guest | true | 'PRIVATE-TOKEN' | :personal_access_token | 'rejects terraform module packages access' | :forbidden - :private | :developer | true | 'PRIVATE-TOKEN' | :invalid | 'rejects terraform module packages access' | :unauthorized - :private | :guest | true | 'PRIVATE-TOKEN' | :invalid | 'rejects terraform module packages access' | :unauthorized - :private | :developer | false | 'PRIVATE-TOKEN' | :personal_access_token | 'rejects terraform module packages access' | :not_found - :private | :guest | false | 'PRIVATE-TOKEN' | :personal_access_token | 'rejects terraform module packages access' | :not_found - :private | :developer | false | 'PRIVATE-TOKEN' | :invalid | 'rejects terraform module packages access' | :unauthorized - :private | :guest | false | 'PRIVATE-TOKEN' | :invalid | 'rejects terraform module packages access' | :unauthorized - :private | :anonymous | false | nil | nil | 'rejects terraform module packages access' | :unauthorized - :public | :developer | true | 'JOB-TOKEN' | :job_token | 'process terraform module upload' | :created - :public | :guest | true | 'JOB-TOKEN' | :job_token | 'rejects terraform module packages access' | :forbidden - :public | :developer | true | 'JOB-TOKEN' | :invalid | 'rejects terraform module packages access' | :unauthorized - :public | :guest | true | 'JOB-TOKEN' | :invalid | 'rejects terraform module packages access' | :unauthorized - :public | :developer | false | 'JOB-TOKEN' | :job_token | 'rejects terraform module packages access' | :forbidden - :public | :guest | false | 'JOB-TOKEN' | :job_token | 'rejects terraform module packages access' | :forbidden - :public | :developer | false | 'JOB-TOKEN' | :invalid | 'rejects terraform module packages access' | :unauthorized - :public | :guest | false | 'JOB-TOKEN' | :invalid | 'rejects terraform module packages access' | :unauthorized - :private | :developer | true | 'JOB-TOKEN' | :job_token | 'process terraform module upload' | :created - :private | :guest | true | 'JOB-TOKEN' | :job_token | 'rejects terraform module packages access' | :forbidden - :private | :developer | true | 'JOB-TOKEN' | :invalid | 'rejects terraform module packages access' | :unauthorized - :private | :guest | true | 'JOB-TOKEN' | :invalid | 'rejects terraform module packages access' | :unauthorized - :private | :developer | false | 'JOB-TOKEN' | :job_token | 'rejects terraform module packages access' | :not_found - :private | :guest | false | 'JOB-TOKEN' | :job_token | 'rejects terraform module packages access' | :not_found - :private | :developer | false | 'JOB-TOKEN' | :invalid | 'rejects terraform module packages access' | :unauthorized - :private | :guest | false | 'JOB-TOKEN' | :invalid | 'rejects terraform module packages access' | :unauthorized - :public | :developer | true | 'DEPLOY-TOKEN' | :deploy_token | 'process terraform module upload' | :created - :public | :developer | true | 'DEPLOY-TOKEN' | :invalid | 'rejects terraform module packages access' | :unauthorized - :private | :developer | true | 'DEPLOY-TOKEN' | :deploy_token | 'process terraform module upload' | :created - :private | :developer | true | 'DEPLOY-TOKEN' | :invalid | 'rejects terraform module packages access' | :unauthorized - end - - with_them do - let(:user_headers) { user_role == :anonymous ? {} : { token_header => token } } - let(:headers) { user_headers.merge(workhorse_headers) } - let(:snowplow_gitlab_standard_context) do - { project: project, namespace: project.namespace, user: snowplow_user, property: 'i_package_terraform_module_user' } - end - - let(:snowplow_user) do - case token_type - when :deploy_token - deploy_token - when :job_token - job.user - else - user - end - end - - before do - project.update!(visibility: visibility.to_s) - end - - it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member] - end - - context 'failed package file save' do - let(:user_headers) { { 'PRIVATE-TOKEN' => personal_access_token.token } } - let(:headers) { user_headers.merge(workhorse_headers) } - - before do - project.add_developer(user) - end - - it 'does not create package record', :aggregate_failures do - allow(Packages::CreatePackageFileService).to receive(:new).and_raise(StandardError) - - expect { subject } - .to change { project.packages.count }.by(0) - .and change { Packages::PackageFile.count }.by(0) - expect(response).to have_gitlab_http_status(:error) - end - - context 'with an existing package' do - let_it_be_with_reload(:existing_package) { create(:terraform_module_package, name: 'mymodule/mysystem', version: '1.0.0', project: project) } - - it 'does not create a new package' do - expect { subject } - .to change { project.packages.count }.by(0) - .and change { Packages::PackageFile.count }.by(0) - expect(response).to have_gitlab_http_status(:forbidden) - end - - context 'marked as pending_destruction' do - it 'does create a new package' do - existing_package.pending_destruction! - - expect { subject } - .to change { project.packages.count }.by(1) - .and change { Packages::PackageFile.count }.by(1) - expect(response).to have_gitlab_http_status(:created) - end - end - end - end - end - end end diff --git a/spec/requests/api/terraform/modules/v1/project_packages_spec.rb b/spec/requests/api/terraform/modules/v1/project_packages_spec.rb new file mode 100644 index 00000000000..1f3b2283d59 --- /dev/null +++ b/spec/requests/api/terraform/modules/v1/project_packages_spec.rb @@ -0,0 +1,205 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe API::Terraform::Modules::V1::ProjectPackages, feature_category: :package_registry do + include_context 'for terraform modules api setup' + using RSpec::Parameterized::TableSyntax + + describe 'PUT /api/v4/projects/:project_id/packages/terraform/modules/:module_name/:module_system/:module_version/file/authorize' do + include_context 'workhorse headers' + + let(:url) { api("/projects/#{project.id}/packages/terraform/modules/mymodule/mysystem/1.0.0/file/authorize") } + let(:headers) { {} } + + subject { put(url, headers: headers) } + + context 'with valid project' do + where(:visibility, :user_role, :member, :token_header, :token_type, :shared_examples_name, :expected_status) do + :public | :developer | true | 'PRIVATE-TOKEN' | :personal_access_token | 'process terraform module workhorse authorization' | :success + :public | :guest | true | 'PRIVATE-TOKEN' | :personal_access_token | 'rejects terraform module packages access' | :forbidden + :public | :developer | true | 'PRIVATE-TOKEN' | :invalid | 'rejects terraform module packages access' | :unauthorized + :public | :guest | true | 'PRIVATE-TOKEN' | :invalid | 'rejects terraform module packages access' | :unauthorized + :public | :developer | false | 'PRIVATE-TOKEN' | :personal_access_token | 'rejects terraform module packages access' | :forbidden + :public | :guest | false | 'PRIVATE-TOKEN' | :personal_access_token | 'rejects terraform module packages access' | :forbidden + :public | :developer | false | 'PRIVATE-TOKEN' | :invalid | 'rejects terraform module packages access' | :unauthorized + :public | :guest | false | 'PRIVATE-TOKEN' | :invalid | 'rejects terraform module packages access' | :unauthorized + :public | :anonymous | false | nil | nil | 'rejects terraform module packages access' | :unauthorized + :private | :developer | true | 'PRIVATE-TOKEN' | :personal_access_token | 'process terraform module workhorse authorization' | :success + :private | :guest | true | 'PRIVATE-TOKEN' | :personal_access_token | 'rejects terraform module packages access' | :forbidden + :private | :developer | true | 'PRIVATE-TOKEN' | :invalid | 'rejects terraform module packages access' | :unauthorized + :private | :guest | true | 'PRIVATE-TOKEN' | :invalid | 'rejects terraform module packages access' | :unauthorized + :private | :developer | false | 'PRIVATE-TOKEN' | :personal_access_token | 'rejects terraform module packages access' | :not_found + :private | :guest | false | 'PRIVATE-TOKEN' | :personal_access_token | 'rejects terraform module packages access' | :not_found + :private | :developer | false | 'PRIVATE-TOKEN' | :invalid | 'rejects terraform module packages access' | :unauthorized + :private | :guest | false | 'PRIVATE-TOKEN' | :invalid | 'rejects terraform module packages access' | :unauthorized + :private | :anonymous | false | nil | nil | 'rejects terraform module packages access' | :unauthorized + :public | :developer | true | 'JOB-TOKEN' | :job_token | 'process terraform module workhorse authorization' | :success + :public | :guest | true | 'JOB-TOKEN' | :job_token | 'rejects terraform module packages access' | :forbidden + :public | :developer | true | 'JOB-TOKEN' | :invalid | 'rejects terraform module packages access' | :unauthorized + :public | :guest | true | 'JOB-TOKEN' | :invalid | 'rejects terraform module packages access' | :unauthorized + :public | :developer | false | 'JOB-TOKEN' | :job_token | 'rejects terraform module packages access' | :forbidden + :public | :guest | false | 'JOB-TOKEN' | :job_token | 'rejects terraform module packages access' | :forbidden + :public | :developer | false | 'JOB-TOKEN' | :invalid | 'rejects terraform module packages access' | :unauthorized + :public | :guest | false | 'JOB-TOKEN' | :invalid | 'rejects terraform module packages access' | :unauthorized + :private | :developer | true | 'JOB-TOKEN' | :job_token | 'process terraform module workhorse authorization' | :success + :private | :guest | true | 'JOB-TOKEN' | :job_token | 'rejects terraform module packages access' | :forbidden + :private | :developer | true | 'JOB-TOKEN' | :invalid | 'rejects terraform module packages access' | :unauthorized + :private | :guest | true | 'JOB-TOKEN' | :invalid | 'rejects terraform module packages access' | :unauthorized + :private | :developer | false | 'JOB-TOKEN' | :job_token | 'rejects terraform module packages access' | :not_found + :private | :guest | false | 'JOB-TOKEN' | :job_token | 'rejects terraform module packages access' | :not_found + :private | :developer | false | 'JOB-TOKEN' | :invalid | 'rejects terraform module packages access' | :unauthorized + :private | :guest | false | 'JOB-TOKEN' | :invalid | 'rejects terraform module packages access' | :unauthorized + :public | :developer | true | 'DEPLOY-TOKEN' | :deploy_token | 'process terraform module workhorse authorization' | :success + :public | :developer | true | 'DEPLOY-TOKEN' | :invalid | 'rejects terraform module packages access' | :unauthorized + :private | :developer | true | 'DEPLOY-TOKEN' | :deploy_token | 'process terraform module workhorse authorization' | :success + :private | :developer | true | 'DEPLOY-TOKEN' | :invalid | 'rejects terraform module packages access' | :unauthorized + end + + with_them do + let(:headers) { user_headers.merge(workhorse_headers) } + let(:user_headers) { user_role == :anonymous ? {} : { token_header => token } } + + before do + project.update!(visibility: visibility.to_s) + end + + it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member] + end + end + end + + describe 'PUT /api/v4/projects/:project_id/packages/terraform/modules/:module_name/:module_system/:module_version/file' do + include_context 'workhorse headers' + + let_it_be(:file_name) { 'module-system-v1.0.0.tgz' } + + let(:url) { "/projects/#{project.id}/packages/terraform/modules/mymodule/mysystem/1.0.0/file" } + let(:headers) { {} } + let(:params) { { file: temp_file(file_name) } } + let(:file_key) { :file } + let(:send_rewritten_field) { true } + + subject(:api_request) do + workhorse_finalize( + api(url), + method: :put, + file_key: file_key, + params: params, + headers: headers, + send_rewritten_field: send_rewritten_field + ) + end + + context 'with valid project' do + where(:visibility, :user_role, :member, :token_header, :token_type, :shared_examples_name, :expected_status) do + :public | :developer | true | 'PRIVATE-TOKEN' | :personal_access_token | 'process terraform module upload' | :created + :public | :guest | true | 'PRIVATE-TOKEN' | :personal_access_token | 'rejects terraform module packages access' | :forbidden + :public | :developer | true | 'PRIVATE-TOKEN' | :invalid | 'rejects terraform module packages access' | :unauthorized + :public | :guest | true | 'PRIVATE-TOKEN' | :invalid | 'rejects terraform module packages access' | :unauthorized + :public | :developer | false | 'PRIVATE-TOKEN' | :personal_access_token | 'rejects terraform module packages access' | :forbidden + :public | :guest | false | 'PRIVATE-TOKEN' | :personal_access_token | 'rejects terraform module packages access' | :forbidden + :public | :developer | false | 'PRIVATE-TOKEN' | :invalid | 'rejects terraform module packages access' | :unauthorized + :public | :guest | false | 'PRIVATE-TOKEN' | :invalid | 'rejects terraform module packages access' | :unauthorized + :public | :anonymous | false | nil | nil | 'rejects terraform module packages access' | :unauthorized + :private | :developer | true | 'PRIVATE-TOKEN' | :personal_access_token | 'process terraform module upload' | :created + :private | :guest | true | 'PRIVATE-TOKEN' | :personal_access_token | 'rejects terraform module packages access' | :forbidden + :private | :developer | true | 'PRIVATE-TOKEN' | :invalid | 'rejects terraform module packages access' | :unauthorized + :private | :guest | true | 'PRIVATE-TOKEN' | :invalid | 'rejects terraform module packages access' | :unauthorized + :private | :developer | false | 'PRIVATE-TOKEN' | :personal_access_token | 'rejects terraform module packages access' | :not_found + :private | :guest | false | 'PRIVATE-TOKEN' | :personal_access_token | 'rejects terraform module packages access' | :not_found + :private | :developer | false | 'PRIVATE-TOKEN' | :invalid | 'rejects terraform module packages access' | :unauthorized + :private | :guest | false | 'PRIVATE-TOKEN' | :invalid | 'rejects terraform module packages access' | :unauthorized + :private | :anonymous | false | nil | nil | 'rejects terraform module packages access' | :unauthorized + :public | :developer | true | 'JOB-TOKEN' | :job_token | 'process terraform module upload' | :created + :public | :guest | true | 'JOB-TOKEN' | :job_token | 'rejects terraform module packages access' | :forbidden + :public | :developer | true | 'JOB-TOKEN' | :invalid | 'rejects terraform module packages access' | :unauthorized + :public | :guest | true | 'JOB-TOKEN' | :invalid | 'rejects terraform module packages access' | :unauthorized + :public | :developer | false | 'JOB-TOKEN' | :job_token | 'rejects terraform module packages access' | :forbidden + :public | :guest | false | 'JOB-TOKEN' | :job_token | 'rejects terraform module packages access' | :forbidden + :public | :developer | false | 'JOB-TOKEN' | :invalid | 'rejects terraform module packages access' | :unauthorized + :public | :guest | false | 'JOB-TOKEN' | :invalid | 'rejects terraform module packages access' | :unauthorized + :private | :developer | true | 'JOB-TOKEN' | :job_token | 'process terraform module upload' | :created + :private | :guest | true | 'JOB-TOKEN' | :job_token | 'rejects terraform module packages access' | :forbidden + :private | :developer | true | 'JOB-TOKEN' | :invalid | 'rejects terraform module packages access' | :unauthorized + :private | :guest | true | 'JOB-TOKEN' | :invalid | 'rejects terraform module packages access' | :unauthorized + :private | :developer | false | 'JOB-TOKEN' | :job_token | 'rejects terraform module packages access' | :not_found + :private | :guest | false | 'JOB-TOKEN' | :job_token | 'rejects terraform module packages access' | :not_found + :private | :developer | false | 'JOB-TOKEN' | :invalid | 'rejects terraform module packages access' | :unauthorized + :private | :guest | false | 'JOB-TOKEN' | :invalid | 'rejects terraform module packages access' | :unauthorized + :public | :developer | true | 'DEPLOY-TOKEN' | :deploy_token | 'process terraform module upload' | :created + :public | :developer | true | 'DEPLOY-TOKEN' | :invalid | 'rejects terraform module packages access' | :unauthorized + :private | :developer | true | 'DEPLOY-TOKEN' | :deploy_token | 'process terraform module upload' | :created + :private | :developer | true | 'DEPLOY-TOKEN' | :invalid | 'rejects terraform module packages access' | :unauthorized + end + + with_them do + let(:user_headers) { user_role == :anonymous ? {} : { token_header => token } } + let(:headers) { user_headers.merge(workhorse_headers) } + let(:snowplow_gitlab_standard_context) do + { project: project, namespace: project.namespace, user: snowplow_user, + property: 'i_package_terraform_module_user' } + end + + let(:snowplow_user) do + case token_type + when :deploy_token + deploy_token + when :job_token + job.user + else + user + end + end + + before do + project.update!(visibility: visibility.to_s) + end + + it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member] + end + + context 'when failed package file save' do + let(:user_headers) { { 'PRIVATE-TOKEN' => personal_access_token.token } } + let(:headers) { user_headers.merge(workhorse_headers) } + + before do + project.add_developer(user) + end + + it 'does not create package record', :aggregate_failures do + allow(Packages::CreatePackageFileService).to receive(:new).and_raise(StandardError) + + expect { api_request } + .to change { project.packages.count }.by(0) + .and change { Packages::PackageFile.count }.by(0) + expect(response).to have_gitlab_http_status(:error) + end + + context 'with an existing package' do + let_it_be_with_reload(:existing_package) do + create(:terraform_module_package, name: 'mymodule/mysystem', version: '1.0.0', project: project) + end + + it 'does not create a new package' do + expect { api_request } + .to change { project.packages.count }.by(0) + .and change { Packages::PackageFile.count }.by(0) + expect(response).to have_gitlab_http_status(:forbidden) + end + + context 'when marked as pending_destruction' do + it 'does create a new package' do + existing_package.pending_destruction! + + expect { api_request } + .to change { project.packages.count }.by(1) + .and change { Packages::PackageFile.count }.by(1) + expect(response).to have_gitlab_http_status(:created) + end + end + end + end + end + end +end diff --git a/spec/requests/api/user_runners_spec.rb b/spec/requests/api/user_runners_spec.rb index 0e40dcade19..412b2c48f3f 100644 --- a/spec/requests/api/user_runners_spec.rb +++ b/spec/requests/api/user_runners_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe API::UserRunners, :aggregate_failures, feature_category: :runner_fleet do +RSpec.describe API::UserRunners, :aggregate_failures, feature_category: :fleet_visibility do let_it_be(:admin) { create(:admin) } let_it_be(:user, reload: true) { create(:user, username: 'user.withdot') } diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index b73ae2d33eb..86c4e04ef71 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -182,6 +182,7 @@ RSpec.describe API::Users, :aggregate_failures, feature_category: :user_profile expect(json_response.first).not_to have_key('note') expect(json_response.first).not_to have_key('namespace_id') expect(json_response.first).not_to have_key('created_by') + expect(json_response.first).not_to have_key('email_reset_offered_at') end end @@ -194,6 +195,7 @@ RSpec.describe API::Users, :aggregate_failures, feature_category: :user_profile expect(json_response.first).not_to have_key('note') expect(json_response.first).not_to have_key('namespace_id') expect(json_response.first).not_to have_key('created_by') + expect(json_response.first).not_to have_key('email_reset_offered_at') end end @@ -203,6 +205,7 @@ RSpec.describe API::Users, :aggregate_failures, feature_category: :user_profile expect(response).to have_gitlab_http_status(:success) expect(json_response.first).to have_key('note') + expect(json_response.first).to have_key('email_reset_offered_at') expect(json_response.first['note']).to eq '2018-11-05 | 2FA removed | user requested | www.gitlab.com' end @@ -2966,6 +2969,39 @@ RSpec.describe API::Users, :aggregate_failures, feature_category: :user_profile end end + describe "PUT /user/preferences" do + let(:path) { '/user/preferences' } + + context "when unauthenticated" do + it "returns authentication error" do + put api(path) + expect(response).to have_gitlab_http_status(:unauthorized) + end + end + + context "when authenticated" do + it "updates user preferences" do + user.user_preference.view_diffs_file_by_file = false + user.user_preference.show_whitespace_in_diffs = true + user.save! + + put api(path, user), params: { + view_diffs_file_by_file: true, + show_whitespace_in_diffs: false + } + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response["view_diffs_file_by_file"]).to eq(true) + expect(json_response["show_whitespace_in_diffs"]).to eq(false) + + user.reload + + expect(json_response["view_diffs_file_by_file"]).to eq(user.view_diffs_file_by_file) + expect(json_response["show_whitespace_in_diffs"]).to eq(user.show_whitespace_in_diffs) + end + end + end + describe "GET /user/keys" do subject(:request) { get api(path, user) } diff --git a/spec/requests/application_controller_spec.rb b/spec/requests/application_controller_spec.rb new file mode 100644 index 00000000000..52fdf6bc69e --- /dev/null +++ b/spec/requests/application_controller_spec.rb @@ -0,0 +1,15 @@ +# 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 new file mode 100644 index 00000000000..d2ce618b041 --- /dev/null +++ b/spec/requests/chaos_controller_spec.rb @@ -0,0 +1,14 @@ +# 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/clusters/agents/dashboard_controller_spec.rb b/spec/requests/clusters/agents/dashboard_controller_spec.rb index c3c16d9b385..bc7c964d47b 100644 --- a/spec/requests/clusters/agents/dashboard_controller_spec.rb +++ b/spec/requests/clusters/agents/dashboard_controller_spec.rb @@ -3,13 +3,35 @@ require 'spec_helper' RSpec.describe Clusters::Agents::DashboardController, feature_category: :deployment_management do + let(:user) { create(:user) } + let(:stub_ff) { true } + + describe 'GET index' do + before do + allow(::Gitlab::Kas).to receive(:enabled?).and_return(true) + stub_feature_flags(k8s_dashboard: stub_ff) + sign_in(user) + get kubernetes_dashboard_index_path + end + + it 'returns ok and renders view' do + expect(response).to have_gitlab_http_status(:ok) + end + + context 'with k8s_dashboard feature flag disabled' do + let(:stub_ff) { false } + + it 'returns not found' do + expect(response).to have_gitlab_http_status(:not_found) + end + end + end + describe 'GET show' do let_it_be(:organization) { create(:group) } let_it_be(:agent_management_project) { create(:project, group: organization) } let_it_be(:agent) { create(:cluster_agent, project: agent_management_project) } let_it_be(:deployment_project) { create(:project, group: organization) } - let(:user) { create(:user) } - let(:stub_ff) { true } before do allow(::Gitlab::Kas).to receive(:enabled?).and_return(true) @@ -37,7 +59,7 @@ RSpec.describe Clusters::Agents::DashboardController, feature_category: :deploym ).to be_present end - it 'returns not found' do + it 'returns ok' do expect(response).to have_gitlab_http_status(:ok) end diff --git a/spec/requests/concerns/membership_actions_shared_examples.rb b/spec/requests/concerns/membership_actions_shared_examples.rb new file mode 100644 index 00000000000..6e0b0d5c0a3 --- /dev/null +++ b/spec/requests/concerns/membership_actions_shared_examples.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'request_accessable' do + context 'when not signed in' do + it 'redirects to sign in page' do + request + + expect(response).to redirect_to(new_user_session_path) + end + end + + context 'when signed in' do + before do + sign_in(user) + end + + it 'redirects back to group members page and displays the relevant notice' do + request + + expect(response).to redirect_to(membershipable_path) + expect(flash[:notice]).to eq(_('Your request for access has been queued for review.')) + end + + context 'when something goes wrong' do + before do + group_member = build(:group_member) + request_access_service = instance_double(Members::RequestAccessService) + allow(Members::RequestAccessService).to receive(:new).and_return(request_access_service) + allow(request_access_service).to receive(:execute).and_return(group_member) + allow(group_member).to receive_message_chain(:errors, :full_messages, :to_sentence).and_return('Error') + end + + it 'redirects back to group members page and displays the relevant notice' do + request + + expect(response).to redirect_to(membershipable_path) + expect(flash[:alert]).to eq(_('Your request for access could not be processed: Error')) + end + end + + context 'when already a member' do + before do + membershipable.add_developer(user) + end + + it 'redirects back to group members page and displays the relevant notice' do + request + + expect(response).to redirect_to(membershipable_path) + expect(flash[:notice]).to eq(_('You already have access.')) + end + end + + context 'when a pending access request exists' do + before do + membershipable.request_access(user) + end + + it 'redirects back to group members page and displays the relevant notice' do + request + + expect(response).to redirect_to(membershipable_path) + expect(flash[:notice]).to eq(_('You have already requested access.')) + end + end + end +end diff --git a/spec/requests/content_security_policy_spec.rb b/spec/requests/content_security_policy_spec.rb deleted file mode 100644 index 3ce7e33d88a..00000000000 --- a/spec/requests/content_security_policy_spec.rb +++ /dev/null @@ -1,79 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -# The AnonymousController doesn't support setting the CSP -# This is why an arbitrary test request was chosen instead -# of testing in application_controller_spec. -RSpec.describe 'Content Security Policy', feature_category: :application_instrumentation do - let(:snowplow_host) { 'snowplow.example.com' } - let(:vite_origin) { "#{ViteRuby.instance.config.host}:#{ViteRuby.instance.config.port}" } - - shared_examples 'snowplow is not in the CSP' do - it 'does not add the snowplow collector hostname to the CSP' do - get explore_root_url - - expect(response).to have_gitlab_http_status(:ok) - expect(response.headers['Content-Security-Policy']).not_to include(snowplow_host) - end - end - - describe 'GET #explore' do - context 'snowplow is enabled' do - before do - stub_application_setting(snowplow_enabled: true, snowplow_collector_hostname: snowplow_host) - end - - it 'adds the snowplow collector hostname to the CSP' do - get explore_root_url - - expect(response).to have_gitlab_http_status(:ok) - expect(response.headers['Content-Security-Policy']).to include(snowplow_host) - end - end - - context 'snowplow is enabled but host is not configured' do - before do - stub_application_setting(snowplow_enabled: true) - end - - it_behaves_like 'snowplow is not in the CSP' - end - - context 'snowplow is disabled' do - before do - stub_application_setting(snowplow_enabled: false, snowplow_collector_hostname: snowplow_host) - end - - it_behaves_like 'snowplow is not in the CSP' - end - - context 'when vite enabled during development', - quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/424334' do - before do - stub_rails_env('development') - stub_feature_flags(vite: true) - - get explore_root_url - end - - it 'adds vite csp' do - expect(response).to have_gitlab_http_status(:ok) - expect(response.headers['Content-Security-Policy']).to include(vite_origin) - end - end - - context 'when vite disabled' do - before do - stub_feature_flags(vite: false) - - get explore_root_url - end - - it "doesn't add vite csp" do - expect(response).to have_gitlab_http_status(:ok) - expect(response.headers['Content-Security-Policy']).not_to include(vite_origin) - end - end - end -end diff --git a/spec/requests/explore/catalog_controller_spec.rb b/spec/requests/explore/catalog_controller_spec.rb index 50a2240e040..e75b0bba5a6 100644 --- a/spec/requests/explore/catalog_controller_spec.rb +++ b/spec/requests/explore/catalog_controller_spec.rb @@ -3,8 +3,16 @@ require 'spec_helper' RSpec.describe Explore::CatalogController, feature_category: :pipeline_composition do + let_it_be(:namespace) { create(:group) } + let_it_be(:project) { create(:project, namespace: namespace) } + let_it_be(:catalog_resource) { create(:ci_catalog_resource, :published, project: project) } + let_it_be(:user) { create(:user) } + before_all do + catalog_resource.project.add_reporter(user) + end + before do sign_in(user) end @@ -14,40 +22,48 @@ RSpec.describe Explore::CatalogController, feature_category: :pipeline_compositi if action == :index explore_catalog_index_path else - explore_catalog_path(id: 1) + explore_catalog_path(catalog_resource) 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 + it 'responds with 200' do + get path - expect(response).to have_gitlab_http_status(:ok) - end + 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 + describe 'GET #show' do + it_behaves_like 'basic get requests', :show - it 'responds with 404' do - get path + context 'when rendering a draft catalog resource' do + it 'returns not found error' do + draft_catalog_resource = create(:ci_catalog_resource, state: :draft) + + get explore_catalog_path(draft_catalog_resource) expect(response).to have_gitlab_http_status(:not_found) end end - end - describe 'GET #show' do - it_behaves_like 'basic get requests', :show + context 'when rendering a published catalog resource' do + it 'returns success response' do + get explore_catalog_path(catalog_resource) + + expect(response).to have_gitlab_http_status(:success) + end + end end describe 'GET #index' do + let(:subject) { get explore_catalog_index_path } + it_behaves_like 'basic get requests', :index + + it_behaves_like 'internal event tracking' do + let(:namespace) { user.namespace } + let(:project) { nil } + let(:event) { 'unique_users_visiting_ci_catalog' } + end end end diff --git a/spec/requests/external_redirect/external_redirect_controller_spec.rb b/spec/requests/external_redirect/external_redirect_controller_spec.rb index 1b4294f5c4d..881acbd97ac 100644 --- a/spec/requests/external_redirect/external_redirect_controller_spec.rb +++ b/spec/requests/external_redirect/external_redirect_controller_spec.rb @@ -45,7 +45,10 @@ RSpec.describe "ExternalRedirect::ExternalRedirectController requests", feature_ [ ["when url is bad", "url=javascript:alert(1)"], ["when url is empty", "url="], - ["when url param is missing", ""] + ["when url param is missing", ""], + ["when url points to self", "url=http://www.example.com/-/external_redirect?url=#{external_url_encoded}"], + ["when url points to self encoded", + "url=http%3A%2F%2Fwww.example.com/-/external_redirect?url=#{external_url_encoded}"] ] end diff --git a/spec/requests/groups/group_members_controller_spec.rb b/spec/requests/groups/group_members_controller_spec.rb new file mode 100644 index 00000000000..2147090ef51 --- /dev/null +++ b/spec/requests/groups/group_members_controller_spec.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +require 'spec_helper' + +require_relative '../concerns/membership_actions_shared_examples' + +RSpec.describe Groups::GroupMembersController, feature_category: :groups_and_projects do + let_it_be(:user) { create(:user) } + let_it_be(:membershipable) { create(:group, :public) } + + let(:membershipable_path) { group_path(membershipable) } + + describe 'GET /groups/*group_id/-/group_members/request_access' do + subject(:request) do + get request_access_group_group_members_path(group_id: membershipable) + end + + it_behaves_like 'request_accessable' + end +end diff --git a/spec/requests/health_controller_spec.rb b/spec/requests/health_controller_spec.rb index 639f6194af9..5fb2115aac3 100644 --- a/spec/requests/health_controller_spec.rb +++ b/spec/requests/health_controller_spec.rb @@ -73,7 +73,9 @@ RSpec.describe HealthController, feature_category: :database do end describe 'GET /-/readiness' do - subject { get '/-/readiness', params: params, headers: headers } + subject(:request) { get readiness_path, params: params, headers: headers } + + it_behaves_like 'Base action controller' shared_context 'endpoint responding with readiness data' do context 'when requesting instance-checks' do @@ -219,7 +221,6 @@ RSpec.describe HealthController, feature_category: :database do stub_remote_addr(whitelisted_ip) end - it_behaves_like 'endpoint not querying database' it_behaves_like 'endpoint responding with readiness data' context 'when requesting all checks' do @@ -236,7 +237,6 @@ RSpec.describe HealthController, feature_category: :database do stub_remote_addr(not_whitelisted_ip) end - it_behaves_like 'endpoint not querying database' it_behaves_like 'endpoint not found' end @@ -273,7 +273,6 @@ RSpec.describe HealthController, feature_category: :database do stub_remote_addr(whitelisted_ip) end - it_behaves_like 'endpoint not querying database' it_behaves_like 'endpoint responding with liveness data' end @@ -282,7 +281,6 @@ RSpec.describe HealthController, feature_category: :database do stub_remote_addr(not_whitelisted_ip) end - it_behaves_like 'endpoint not querying database' it_behaves_like 'endpoint not found' context 'accessed with valid token' do diff --git a/spec/requests/ide_controller_spec.rb b/spec/requests/ide_controller_spec.rb index 4131f1d26ec..20d890fadbf 100644 --- a/spec/requests/ide_controller_spec.rb +++ b/spec/requests/ide_controller_spec.rb @@ -17,8 +17,6 @@ RSpec.describe IdeController, feature_category: :web_ide do let_it_be(:creator) { project.creator } let_it_be(:other_user) { create(:user) } - let_it_be(:top_nav_partial) { 'layouts/header/_default' } - let(:user) { creator } before do @@ -156,28 +154,70 @@ RSpec.describe IdeController, feature_category: :web_ide do end end - # This indirectly tests that `minimal: true` was passed to the fullscreen layout - describe 'layout' do - where(:ff_state, :expect_top_nav) do - false | true - true | false + describe 'legacy Web IDE' do + before do + stub_feature_flags(vscode_web_ide: false) end - with_them do - before do - stub_feature_flags(vscode_web_ide: ff_state) + it 'uses application layout' do + subject - subject - end + expect(response).to render_template('layouts/application') + end - it 'handles rendering top nav' do - if expect_top_nav - expect(response).to render_template(top_nav_partial) - else - expect(response).not_to render_template(top_nav_partial) - end - end + it 'does not create oauth application' do + expect(Doorkeeper::Application).not_to receive(:new) + + subject + + expect(web_ide_oauth_application).to be_nil + end + end + + describe 'vscode IDE' do + before do + stub_feature_flags(vscode_web_ide: true) + end + + it 'uses fullscreen layout' do + subject + + expect(response).to render_template('layouts/fullscreen') + end + end + + describe 'with web_ide_oauth flag off' do + before do + stub_feature_flags(web_ide_oauth: false) end + + it 'does not create oauth application' do + expect(Doorkeeper::Application).not_to receive(:new) + + subject + + expect(web_ide_oauth_application).to be_nil + end + end + + it 'ensures web_ide_oauth_application' do + expect(Doorkeeper::Application).to receive(:new).and_call_original + + subject + + expect(web_ide_oauth_application).not_to be_nil + expect(web_ide_oauth_application[:name]).to eq('GitLab Web IDE') + end + + it 'when web_ide_oauth_application already exists, does not create new one' do + existing_app = create(:oauth_application, owner_id: nil, owner_type: nil) + + stub_application_setting({ web_ide_oauth_application: existing_app }) + expect(Doorkeeper::Application).not_to receive(:new) + + subject + + expect(web_ide_oauth_application).to eq(existing_app) end end @@ -201,4 +241,48 @@ RSpec.describe IdeController, feature_category: :web_ide do end end end + + describe '#oauth_redirect', :aggregate_failures do + subject(:oauth_redirect) { get '/-/ide/oauth_redirect' } + + it 'with no web_ide_oauth_application, returns not_found' do + oauth_redirect + + expect(response).to have_gitlab_http_status(:not_found) + end + + context 'with web_ide_oauth_application set' do + before do + stub_application_setting({ + web_ide_oauth_application: create(:oauth_application, owner_id: nil, owner_type: nil) + }) + end + + it 'returns ok and renders view' do + oauth_redirect + + expect(response).to have_gitlab_http_status(:ok) + end + + it 'with vscode_web_ide flag off, returns not_found' do + stub_feature_flags(vscode_web_ide: false) + + oauth_redirect + + expect(response).to have_gitlab_http_status(:not_found) + end + + it 'with web_ide_oauth flag off, returns not_found' do + stub_feature_flags(web_ide_oauth: false) + + oauth_redirect + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end + + def web_ide_oauth_application + ::Gitlab::CurrentSettings.current_application_settings.web_ide_oauth_application + end end diff --git a/spec/requests/jwks_controller_spec.rb b/spec/requests/jwks_controller_spec.rb index f756c1758e4..3dc3ed68311 100644 --- a/spec/requests/jwks_controller_spec.rb +++ b/spec/requests/jwks_controller_spec.rb @@ -55,5 +55,26 @@ RSpec.describe JwksController, feature_category: :system_access do end end end + + it 'has cache control header' do + get jwks_url + + expect(response).to have_gitlab_http_status(:ok) + expect(response.headers['Cache-Control']).to include('max-age=86400', 'public', 'must-revalidate', 'no-transform') + end + + context 'when cache_control_headers_for_openid_jwks feature flag is disabled' do + before do + stub_feature_flags(cache_control_headers_for_openid_jwks: false) + end + + it 'does not have cache control header' do + get jwks_url + + expect(response).to have_gitlab_http_status(:ok) + expect(response.headers['Cache-Control']).not_to include('max-age=86400', 'public', + 'no-transform') + end + end end end diff --git a/spec/requests/jwt_controller_spec.rb b/spec/requests/jwt_controller_spec.rb index 966cc2d6d4e..956c0e06cda 100644 --- a/spec/requests/jwt_controller_spec.rb +++ b/spec/requests/jwt_controller_spec.rb @@ -92,7 +92,7 @@ RSpec.describe JwtController, feature_category: :system_access do context 'project with enabled CI' do subject! { get '/jwt/auth', params: parameters, headers: headers } - it { expect(service_class).to have_received(:new).with(project, user, ActionController::Parameters.new(parameters.merge(auth_type: :build)).permit!) } + it { expect(service_class).to have_received(:new).with(project, user, ActionController::Parameters.new(parameters.merge(auth_type: :build, raw_token: build.token)).permit!) } it_behaves_like 'user logging' end @@ -119,7 +119,7 @@ RSpec.describe JwtController, feature_category: :system_access do .with( nil, nil, - ActionController::Parameters.new(parameters.merge(deploy_token: deploy_token, auth_type: :deploy_token)).permit! + ActionController::Parameters.new(parameters.merge(deploy_token: deploy_token, auth_type: :deploy_token, raw_token: deploy_token.token)).permit! ) end @@ -144,7 +144,7 @@ RSpec.describe JwtController, feature_category: :system_access do .with( nil, user, - ActionController::Parameters.new(parameters.merge(auth_type: :personal_access_token)).permit! + ActionController::Parameters.new(parameters.merge(auth_type: :personal_access_token, raw_token: pat.token)).permit! ) end @@ -160,7 +160,7 @@ RSpec.describe JwtController, feature_category: :system_access do subject! { get '/jwt/auth', params: parameters, headers: headers } - it { expect(service_class).to have_received(:new).with(nil, user, ActionController::Parameters.new(parameters.merge(auth_type: :gitlab_or_ldap)).permit!) } + it { expect(service_class).to have_received(:new).with(nil, user, ActionController::Parameters.new(parameters.merge(auth_type: :gitlab_or_ldap, raw_token: user.password)).permit!) } it_behaves_like 'rejecting a blocked user' @@ -180,7 +180,7 @@ RSpec.describe JwtController, feature_category: :system_access 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)) } + it { expect(service_class).to have_received(:new).with(nil, user, service_parameters.merge(auth_type: :gitlab_or_ldap, raw_token: user.password)) } it_behaves_like 'user logging' end @@ -197,7 +197,7 @@ RSpec.describe JwtController, feature_category: :system_access 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)) } + it { expect(service_class).to have_received(:new).with(nil, user, service_parameters.merge(auth_type: :gitlab_or_ldap, raw_token: user.password)) } end context 'when user has 2FA enabled' do @@ -274,6 +274,8 @@ RSpec.describe JwtController, feature_category: :system_access do let_it_be(:personal_access_token) { create(:personal_access_token, user: user) } let_it_be(:group) { create(:group) } let_it_be(:project) { create(:project, :private, group: group) } + let_it_be(:bot_user) { create(:user, :project_bot) } + let_it_be(:group_access_token) { create(:personal_access_token, :dependency_proxy_scopes, user: bot_user) } let_it_be(:group_deploy_token) { create(:deploy_token, :group, :dependency_proxy_scopes) } let_it_be(:gdeploy_token) { create(:group_deploy_token, deploy_token: group_deploy_token, group: group) } let_it_be(:project_deploy_token) { create(:deploy_token, :project, :dependency_proxy_scopes) } @@ -313,6 +315,48 @@ RSpec.describe JwtController, feature_category: :system_access do it_behaves_like 'with valid credentials' end + context 'with group access token' do + let(:credential_user) { group_access_token.user.username } + let(:credential_password) { group_access_token.token } + + context 'with the required scopes' do + it_behaves_like 'with valid credentials' + it_behaves_like 'a token that expires today' + + context 'revoked' do + before do + group_access_token.update!(revoked: true) + end + + it_behaves_like 'returning response status', :unauthorized + end + + context 'expired' do + before do + group_access_token.update!(expires_at: Date.yesterday) + end + + it_behaves_like 'returning response status', :unauthorized + end + end + + context 'without the required scopes' do + before do + group_access_token.update!(scopes: [::Gitlab::Auth::READ_REPOSITORY_SCOPE]) + end + + it_behaves_like 'returning response status', :forbidden + + context 'packages_dependency_proxy_containers_scope_check disabled' do + before do + stub_feature_flags(packages_dependency_proxy_containers_scope_check: false) + end + + it_behaves_like 'with valid credentials' + end + end + end + context 'with group deploy token' do let(:credential_user) { group_deploy_token.username } let(:credential_password) { group_deploy_token.token } diff --git a/spec/requests/legacy_routes_spec.rb b/spec/requests/legacy_routes_spec.rb new file mode 100644 index 00000000000..537ad4054a1 --- /dev/null +++ b/spec/requests/legacy_routes_spec.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe "Legacy routes", type: :request, feature_category: :system_access do + let(:user) { create(:user) } + let(:token) { create(:personal_access_token, user: user) } + + before do + login_as(user) + end + + it "/-/profile/audit_log" do + get "/-/profile/audit_log" + expect(response).to redirect_to('/-/user_settings/authentication_log') + end + + it "/-/profile/active_sessions" do + get "/-/profile/active_sessions" + expect(response).to redirect_to('/-/user_settings/active_sessions') + end + + it "/-/profile/personal_access_tokens" do + get "/-/profile/personal_access_tokens" + expect(response).to redirect_to('/-/user_settings/personal_access_tokens') + + get "/-/profile/personal_access_tokens?name=GitLab+Dangerbot&scopes=api" + expect(response).to redirect_to('/-/user_settings/personal_access_tokens?name=GitLab+Dangerbot&scopes=api') + end + + it "/-/profile/personal_access_tokens/:id/revoke" do + put "/-/profile/personal_access_tokens/#{token.id}/revoke" + expect(token.reload).to be_revoked + end + + it "/-/profile/applications" do + get "/-/profile/applications" + expect(response).to redirect_to('/-/user_settings/applications') + end + + it "/-/profile/password/new" do + get "/-/profile/password/new" + expect(response).to redirect_to('/-/user_settings/password/new') + + get "/-/profile/password/new?abc=xyz" + expect(response).to redirect_to('/-/user_settings/password/new?abc=xyz') + end + + it "/-/profile/password/edit" do + get "/-/profile/password/edit" + expect(response).to redirect_to('/-/user_settings/password/edit') + + get "/-/profile/password/edit?abc=xyz" + expect(response).to redirect_to('/-/user_settings/password/edit?abc=xyz') + end +end diff --git a/spec/requests/metrics_controller_spec.rb b/spec/requests/metrics_controller_spec.rb new file mode 100644 index 00000000000..ce96906e020 --- /dev/null +++ b/spec/requests/metrics_controller_spec.rb @@ -0,0 +1,9 @@ +# 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 257f238d9ef..7887bf52542 100644 --- a/spec/requests/oauth/authorizations_controller_spec.rb +++ b/spec/requests/oauth/authorizations_controller_spec.rb @@ -20,6 +20,10 @@ 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 4bf527f49a8..bfd0603eb3d 100644 --- a/spec/requests/organizations/organizations_controller_spec.rb +++ b/spec/requests/organizations/organizations_controller_spec.rb @@ -11,13 +11,6 @@ RSpec.describe Organizations::OrganizationsController, feature_category: :cell d sign_in(user) end - context 'with no association to an organization' do - let_it_be(:user) { create(:user) } - - it_behaves_like 'organization - successful response' - it_behaves_like 'organization - action disabled by `ui_for_organizations` feature flag' - end - context 'as as admin', :enable_admin_mode do let_it_be(:user) { create(:admin) } @@ -54,6 +47,40 @@ RSpec.describe Organizations::OrganizationsController, feature_category: :cell d it_behaves_like 'when the user is signed in' end + shared_examples 'controller action that requires authentication by an organization user' do + it_behaves_like 'controller action that requires authentication' + + context 'when the user is signed in' do + before do + sign_in(user) + end + + context 'with no association to an organization' do + let_it_be(:user) { create(:user) } + + it_behaves_like 'organization - not found response' + it_behaves_like 'organization - action disabled by `ui_for_organizations` feature flag' + end + end + end + + shared_examples 'controller action that requires authentication by any user' do + it_behaves_like 'controller action that requires authentication' + + context 'when the user is signed in' do + before do + sign_in(user) + end + + context 'with no association to an organization' do + let_it_be(:user) { create(:user) } + + it_behaves_like 'organization - successful response' + it_behaves_like 'organization - action disabled by `ui_for_organizations` feature flag' + end + end + end + shared_examples 'controller action that does not require authentication' do context 'when the user is not logged in' do it_behaves_like 'organization - successful response' @@ -78,18 +105,18 @@ RSpec.describe Organizations::OrganizationsController, feature_category: :cell d describe 'GET #users' do subject(:gitlab_request) { get users_organization_path(organization) } - it_behaves_like 'controller action that does not require authentication' + it_behaves_like 'controller action that requires authentication by an organization user' end describe 'GET #new' do subject(:gitlab_request) { get new_organization_path } - it_behaves_like 'controller action that requires authentication' + it_behaves_like 'controller action that requires authentication by any user' end describe 'GET #index' do subject(:gitlab_request) { get organizations_path } - it_behaves_like 'controller action that requires authentication' + it_behaves_like 'controller action that requires authentication by any user' end end diff --git a/spec/requests/organizations/settings_controller_spec.rb b/spec/requests/organizations/settings_controller_spec.rb index 77048b04b0c..1d98e598159 100644 --- a/spec/requests/organizations/settings_controller_spec.rb +++ b/spec/requests/organizations/settings_controller_spec.rb @@ -46,7 +46,7 @@ RSpec.describe Organizations::SettingsController, feature_category: :cell do create :organization_user, organization: organization, user: user end - it_behaves_like 'organization - not found response' + it_behaves_like 'organization - successful response' it_behaves_like 'organization - action disabled by `ui_for_organizations` feature flag' end end diff --git a/spec/requests/projects/gcp/artifact_registry/docker_images_controller_spec.rb b/spec/requests/projects/gcp/artifact_registry/docker_images_controller_spec.rb new file mode 100644 index 00000000000..d571999feb0 --- /dev/null +++ b/spec/requests/projects/gcp/artifact_registry/docker_images_controller_spec.rb @@ -0,0 +1,136 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Projects::Gcp::ArtifactRegistry::DockerImagesController, feature_category: :container_registry do + let_it_be(:project) { create(:project, :private) } + + let(:user) { project.owner } + let(:gcp_project_id) { 'gcp_project_id' } + let(:gcp_location) { 'gcp_location' } + let(:gcp_ar_repository) { 'gcp_ar_repository' } + let(:gcp_wlif_url) { 'gcp_wlif_url' } + + describe '#index' do + let(:service_response) { ServiceResponse.success(payload: dummy_client_payload) } + let(:service_double) do + instance_double('Integrations::GoogleCloudPlatform::ArtifactRegistry::ListDockerImagesService') + end + + subject(:get_index_page) do + get( + project_gcp_artifact_registry_docker_images_path( + project, + gcp_project_id: gcp_project_id, + gcp_location: gcp_location, + gcp_ar_repository: gcp_ar_repository, + gcp_wlif_url: gcp_wlif_url + ) + ) + end + + before do + allow_next_instance_of(Integrations::GoogleCloudPlatform::ArtifactRegistry::ListDockerImagesService) do |service| + allow(service).to receive(:execute).and_return(service_response) + end + end + + shared_examples 'returning the error message' do |message| + it 'displays an error message' do + sign_in(user) + + get_index_page + + expect(response).to have_gitlab_http_status(:success) + expect(response.body).to include(message) + end + end + + context 'when on saas', :saas do + it 'returns the images' do + sign_in(user) + + get_index_page + + expect(response).to have_gitlab_http_status(:success) + expect(response.body).to include('image@sha256:6a') + expect(response.body).to include('tag1') + expect(response.body).to include('tag2') + expect(response.body).to include('Prev') + expect(response.body).to include('Next') + end + + context 'when the service returns an error response' do + let(:service_response) { ServiceResponse.error(message: 'boom') } + + it_behaves_like 'returning the error message', 'boom' + end + + %i[gcp_project_id gcp_location gcp_ar_repository gcp_wlif_url].each do |field| + context "when a gcp parameter #{field} is missing" do + let(field) { nil } + + it 'redirects to setup page' do + sign_in(user) + + get_index_page + + expect(response).to redirect_to new_project_gcp_artifact_registry_setup_path(project) + end + end + end + + context 'with the feature flag disabled' do + before do + stub_feature_flags(gcp_technical_demo: false) + end + + it_behaves_like 'returning the error message', 'Feature flag disabled' + end + + context 'with non private project' do + before do + allow_next_found_instance_of(Project) do |project| + allow(project).to receive(:private?).and_return(false) + end + end + + it_behaves_like 'returning the error message', 'Can only run on private projects' + end + + context 'with unauthorized user' do + let_it_be(:user) { create(:user) } + + it 'returns success' do + sign_in(user) + + get_index_page + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end + + context 'when not on saas' do + it_behaves_like 'returning the error message', "Can't run here" + end + + def dummy_client_payload + { + images: [ + { + built_at: '2023-11-30T23:23:11.980068941Z', + media_type: 'application/vnd.docker.distribution.manifest.v2+json', + name: 'projects/project/locations/location/repositories/repo/dockerImages/image@sha256:6a', + size_bytes: 2827903, + tags: %w[tag1 tag2], + updated_at: '2023-12-07T11:48:50.840751Z', + uploaded_at: '2023-12-07T11:48:47.598511Z', + uri: 'location.pkg.dev/project/repo/image@sha256:6a' + } + ], + next_page_token: 'next_page_token' + } + end + end +end diff --git a/spec/requests/projects/gcp/artifact_registry/setup_controller_spec.rb b/spec/requests/projects/gcp/artifact_registry/setup_controller_spec.rb new file mode 100644 index 00000000000..20d7969a05f --- /dev/null +++ b/spec/requests/projects/gcp/artifact_registry/setup_controller_spec.rb @@ -0,0 +1,73 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Projects::Gcp::ArtifactRegistry::SetupController, feature_category: :container_registry do + let_it_be(:project) { create(:project, :private) } + + let(:user) { project.owner } + + describe '#new' do + subject(:get_setup_page) { get(new_project_gcp_artifact_registry_setup_path(project)) } + + shared_examples 'returning the error message' do |message| + it 'displays an error message' do + sign_in(user) + + get_setup_page + + expect(response).to have_gitlab_http_status(:success) + expect(response.body).to include(message) + end + end + + context 'when on saas', :saas do + it 'returns the setup page' do + sign_in(user) + + get_setup_page + + expect(response).to have_gitlab_http_status(:success) + expect(response.body).to include('Google Project ID') + expect(response.body).to include('Google Project Location') + expect(response.body).to include('Artifact Registry Repository Name') + expect(response.body).to include('Worflow Identity Federation url') + expect(response.body).to include('Setup') + end + + context 'with the feature flag disabled' do + before do + stub_feature_flags(gcp_technical_demo: false) + end + + it_behaves_like 'returning the error message', 'Feature flag disabled' + end + + context 'with non private project' do + before do + allow_next_found_instance_of(Project) do |project| + allow(project).to receive(:private?).and_return(false) + end + end + + it_behaves_like 'returning the error message', 'Can only run on private projects' + end + + context 'with unauthorized user' do + let_it_be(:user) { create(:user) } + + it 'returns success' do + sign_in(user) + + get_setup_page + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end + + context 'when not on saas' do + it_behaves_like 'returning the error message', "Can't run here" + end + end +end diff --git a/spec/requests/projects/integrations/shimos_controller_spec.rb b/spec/requests/projects/integrations/shimos_controller_spec.rb deleted file mode 100644 index bd7af0bb4ac..00000000000 --- a/spec/requests/projects/integrations/shimos_controller_spec.rb +++ /dev/null @@ -1,37 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe ::Projects::Integrations::ShimosController, feature_category: :integrations do - let_it_be(:project) { create(:project) } - let_it_be(:user) { create(:user, developer_projects: [project]) } - let_it_be(:shimo_integration) { create(:shimo_integration, project: project) } - - before do - sign_in(user) - end - - describe 'GET #show' do - context 'when Shimo integration is inactive' do - before do - shimo_integration.update!(active: false) - end - - it 'returns 404 status' do - get project_integrations_shimo_path(project) - - expect(response).to have_gitlab_http_status(:not_found) - end - end - - context 'when Shimo integration is active' do - it 'renders the "show" template' do - get project_integrations_shimo_path(project) - - expect(response).to have_gitlab_http_status(:ok) - expect(response).to render_template(:show) - expect(response.body).to include shimo_integration.external_wiki_url - end - end - end -end diff --git a/spec/requests/projects/merge_requests/content_spec.rb b/spec/requests/projects/merge_requests/content_spec.rb index 54066756f3e..1ecad609416 100644 --- a/spec/requests/projects/merge_requests/content_spec.rb +++ b/spec/requests/projects/merge_requests/content_spec.rb @@ -29,13 +29,5 @@ RSpec.describe 'merge request content spec', feature_category: :code_review_work describe 'GET cached_widget' do it_behaves_like 'cached widget request' - - context 'with non_public_artifacts disabled' do - before do - stub_feature_flags(non_public_artifacts: false) - end - - it_behaves_like 'cached widget request' - end end end diff --git a/spec/requests/projects/ml/candidates_controller_spec.rb b/spec/requests/projects/ml/candidates_controller_spec.rb index 78f31be26d1..0263f2d79b5 100644 --- a/spec/requests/projects/ml/candidates_controller_spec.rb +++ b/spec/requests/projects/ml/candidates_controller_spec.rb @@ -51,13 +51,7 @@ RSpec.describe Projects::Ml::CandidatesController, feature_category: :mlops do end describe 'GET show' do - let(:can_read_build) { true } - before do - allow(Ability).to receive(:allowed?) - .with(user, :read_build, candidate.ci_build) - .and_return(can_read_build) - show_candidate end @@ -74,20 +68,6 @@ RSpec.describe Projects::Ml::CandidatesController, feature_category: :mlops do expect { show_candidate }.not_to exceed_all_query_limit(control_count) end - context 'when user has permission to read the build' do - it 'includes ci build info' do - expect(assigns[:include_ci_info]).to eq(true) - end - end - - context 'when user has no permission to read the build' do - let(:can_read_build) { false } - - it 'sets include_ci_job to false' do - expect(assigns[:include_ci_info]).to eq(false) - end - end - it_behaves_like '404 if candidate does not exist' it_behaves_like 'requires read_model_experiments' end diff --git a/spec/requests/projects/pipelines_controller_spec.rb b/spec/requests/projects/pipelines_controller_spec.rb index 7bdb66755db..aa3fefdef14 100644 --- a/spec/requests/projects/pipelines_controller_spec.rb +++ b/spec/requests/projects/pipelines_controller_spec.rb @@ -75,6 +75,59 @@ RSpec.describe Projects::PipelinesController, feature_category: :continuous_inte expect(response).to have_gitlab_http_status(:ok) end + context 'when pipeline_stage_set_last_modified is disabled' do + before do + stub_feature_flags(pipeline_stage_set_last_modified: false) + end + + it 'does not set Last-Modified' do + create(:ci_build, :retried, :failed, pipeline: pipeline, stage: 'build') + + request_build_stage + + expect(response).to have_gitlab_http_status(:ok) + expect(response.headers['Last-Modified']).to be_nil + expect(response.headers['Cache-Control']).to eq('max-age=0, private, must-revalidate') + end + end + + context 'when pipeline_stage_set_last_modified is enabled' do + before do + stub_feature_flags(pipeline_stage_set_last_modified: true) + stage.statuses.update_all(updated_at: status_timestamp) + end + + let(:last_modified) { DateTime.parse(response.headers['Last-Modified']).utc } + let(:cache_control) { response.headers['Cache-Control'] } + let(:expected_cache_control) { 'max-age=0, private, must-revalidate' } + + context 'when status.updated_at is before stage.updated' do + let(:stage) { pipeline.stage('build') } + let(:status_timestamp) { stage.updated_at - 10.minutes } + + it 'sets correct Last-Modified of stage.updated_at' do + request_build_stage + + expect(response).to have_gitlab_http_status(:ok) + expect(last_modified).to be_within(1.second).of stage.updated_at + expect(cache_control).to eq(expected_cache_control) + end + end + + context 'when status.updated_at is after stage.updated' do + let(:stage) { pipeline.stage('build') } + let(:status_timestamp) { stage.updated_at + 10.minutes } + + it 'sets correct Last-Modified of max(status.updated_at)' do + request_build_stage + + expect(response).to have_gitlab_http_status(:ok) + expect(last_modified).to be_within(1.second).of status_timestamp + expect(cache_control).to eq(expected_cache_control) + end + end + end + context 'with retried builds' do it 'does not execute N+1 queries' do create(:ci_build, :retried, :failed, pipeline: pipeline, stage: 'build') diff --git a/spec/requests/projects/project_members_controller_spec.rb b/spec/requests/projects/project_members_controller_spec.rb new file mode 100644 index 00000000000..8ab6f521766 --- /dev/null +++ b/spec/requests/projects/project_members_controller_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require 'spec_helper' + +require_relative '../concerns/membership_actions_shared_examples' + +RSpec.describe Projects::ProjectMembersController, feature_category: :groups_and_projects do + let_it_be(:user) { create(:user) } + let_it_be(:membershipable) { create(:project, :public, namespace: create(:group, :public)) } + + let(:membershipable_path) { project_path(membershipable) } + + describe 'GET /*namespace_id/:project_id/-/project_members/request_access' do + subject(:request) do + get request_access_namespace_project_project_members_path( + namespace_id: membershipable.namespace, + project_id: membershipable + ) + end + + it_behaves_like 'request_accessable' + end +end diff --git a/spec/requests/projects/service_desk/custom_email_controller_spec.rb b/spec/requests/projects/service_desk/custom_email_controller_spec.rb index 8ce238ab99c..8d1f61f3f63 100644 --- a/spec/requests/projects/service_desk/custom_email_controller_spec.rb +++ b/spec/requests/projects/service_desk/custom_email_controller_spec.rb @@ -74,16 +74,6 @@ RSpec.describe Projects::ServiceDesk::CustomEmailController, feature_category: : end end - shared_examples 'a controller with disabled feature flag with status' do |status| - context 'when feature flag service_desk_custom_email is disabled' do - before do - stub_feature_flags(service_desk_custom_email: false) - end - - it_behaves_like 'a controller that responds with status', status - end - end - shared_examples 'a deletable resource' do describe 'DELETE custom email' do let(:perform_request) { delete custom_email_path } @@ -98,9 +88,6 @@ RSpec.describe Projects::ServiceDesk::CustomEmailController, feature_category: : sign_in(user) end - # because CustomEmailController check_feature_flag_enabled responds - it_behaves_like 'a controller with disabled feature flag with status', :not_found - describe 'GET custom email' do let(:perform_request) { get custom_email_path } @@ -364,7 +351,6 @@ RSpec.describe Projects::ServiceDesk::CustomEmailController, feature_category: : # because Projects::ApplicationController :authenticate_user! responds # with redirect to login page it_behaves_like 'a controller that responds with status', :found - it_behaves_like 'a controller with disabled feature flag with status', :found end context 'with illegitimate user signed in' do @@ -374,7 +360,5 @@ RSpec.describe Projects::ServiceDesk::CustomEmailController, feature_category: : end it_behaves_like 'a controller that responds with status', :not_found - # because CustomEmailController check_feature_flag_enabled responds - it_behaves_like 'a controller with disabled feature flag with status', :not_found end end diff --git a/spec/requests/projects/service_desk_controller_spec.rb b/spec/requests/projects/service_desk_controller_spec.rb index 7d881d8ea62..1a8104dd669 100644 --- a/spec/requests/projects/service_desk_controller_spec.rb +++ b/spec/requests/projects/service_desk_controller_spec.rb @@ -78,24 +78,25 @@ RSpec.describe Projects::ServiceDeskController, feature_category: :service_desk expect(response).to have_gitlab_http_status(:ok) end - it 'sets issue_template_key' do - put project_service_desk_path(project, format: :json), params: { issue_template_key: 'service_desk' } + it 'sets attributes', :aggregate_failures do + put project_service_desk_path(project, format: :json), params: { + issue_template_key: 'service_desk', + reopen_issue_on_external_participant_note: true, + add_external_participants_from_cc: true + } settings = project.service_desk_setting expect(settings).to be_present - expect(settings.issue_template_key).to eq('service_desk') - expect(json_response['template_file_missing']).to eq(false) - 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) + expect(settings).to have_attributes( + issue_template_key: 'service_desk', + reopen_issue_on_external_participant_note: true, + add_external_participants_from_cc: true + ) + expect(json_response).to include( + 'issue_template_key' => 'service_desk', + 'reopen_issue_on_external_participant_note' => true, + 'add_external_participants_from_cc' => true + ) end it 'returns an error when update of service desk settings fails' do diff --git a/spec/requests/projects/tags_controller_spec.rb b/spec/requests/projects/tags_controller_spec.rb index c0b0b1728c2..97cc3a5a0df 100644 --- a/spec/requests/projects/tags_controller_spec.rb +++ b/spec/requests/projects/tags_controller_spec.rb @@ -24,4 +24,23 @@ RSpec.describe Projects::TagsController, feature_category: :source_code_manageme end end end + + describe '#show' do + let_it_be(:project) { create(:project, :repository, :public) } + let_it_be(:user) { create(:user) } + + before do + sign_in(user) + end + + context 'with x509 signature' do + let(:tag_name) { 'v1.1.1' } + + it 'displays a signature badge' do + get project_tags_path(project, id: tag_name) + + expect(response.body).to include('Unverified') + end + end + end end diff --git a/spec/requests/registrations_controller_spec.rb b/spec/requests/registrations_controller_spec.rb index 8b857046a4d..71f2f347f0d 100644 --- a/spec/requests/registrations_controller_spec.rb +++ b/spec/requests/registrations_controller_spec.rb @@ -6,7 +6,9 @@ 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(:create_user) { post user_registration_path, params: { user: user_attrs } } + subject(:request) { post user_registration_path, params: { user: user_attrs } } + + it_behaves_like 'Base action controller' context 'when email confirmation is required' do before do @@ -15,7 +17,7 @@ RSpec.describe RegistrationsController, type: :request, feature_category: :syste end it 'redirects to the `users_almost_there_path`', unless: Gitlab.ee? do - create_user + request expect(response).to redirect_to(users_almost_there_path(email: user_attrs[:email])) end diff --git a/spec/requests/runner_setup_controller_spec.rb b/spec/requests/runner_setup_controller_spec.rb index 8d75b9e81b7..ae52bd71b3b 100644 --- a/spec/requests/runner_setup_controller_spec.rb +++ b/spec/requests/runner_setup_controller_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe RunnerSetupController, feature_category: :runner_fleet do +RSpec.describe RunnerSetupController, feature_category: :fleet_visibility do let(:user) { create(:user) } before do diff --git a/spec/requests/sessions_spec.rb b/spec/requests/sessions_spec.rb index 3428e607305..337f358d394 100644 --- a/spec/requests/sessions_spec.rb +++ b/spec/requests/sessions_spec.rb @@ -7,6 +7,10 @@ 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) @@ -41,7 +45,7 @@ RSpec.describe 'Sessions', feature_category: :system_access do post user_session_path(user: { login: user.username, password: user.password }) - expect(response).to redirect_to(activity_group_path(member.source)) + expect(response).to redirect_to(group_path(member.source)) end end diff --git a/spec/requests/user_settings_spec.rb b/spec/requests/user_settings_spec.rb new file mode 100644 index 00000000000..8298edc9ad0 --- /dev/null +++ b/spec/requests/user_settings_spec.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe "UserSettings", type: :request, feature_category: :system_access do + let(:user) { create(:user) } + + describe 'GET authentication_log' do + let(:auth_event) { create(:authentication_event, user: user) } + + it 'tracks search event', :snowplow do + sign_in(user) + + get '/-/user_settings/authentication_log' + + expect_snowplow_event( + category: 'UserSettings::UserSettingsController', + action: 'search_audit_event', + user: user + ) + end + + it 'loads page correctly' do + sign_in(user) + + get '/-/user_settings/authentication_log' + + expect(response).to have_gitlab_http_status(:success) + end + end +end diff --git a/spec/requests/well_known_routing_spec.rb b/spec/requests/well_known_routing_spec.rb deleted file mode 100644 index d4e77a06953..00000000000 --- a/spec/requests/well_known_routing_spec.rb +++ /dev/null @@ -1,13 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe 'well-known URLs', feature_category: :system_access do - describe '/.well-known/change-password' do - it 'redirects to edit profile password path' do - get('/.well-known/change-password') - - expect(response).to redirect_to(edit_profile_password_path) - end - end -end diff --git a/spec/requests/well_known_spec.rb b/spec/requests/well_known_spec.rb new file mode 100644 index 00000000000..6236acac3ab --- /dev/null +++ b/spec/requests/well_known_spec.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'well-known URLs', feature_category: :shared do + describe '/.well-known/change-password', feature_category: :system_access do + it 'redirects to edit profile password path' do + get('/.well-known/change-password') + + expect(response).to redirect_to(edit_user_settings_password_path) + end + end + + describe '/.well-known/security.txt', feature_category: :compliance_management do + let(:action) { get('/.well-known/security.txt') } + + context 'for an authenticated user' do + before do + sign_in(create(:user)) + end + + it 'renders when a security txt is configured' do + stub_application_setting security_txt_content: 'HELLO' + action + expect(response.body).to eq('HELLO') + end + + it 'returns a 404 when a security txt is blank' do + stub_application_setting security_txt_content: '' + action + expect(response).to have_gitlab_http_status(:not_found) + end + + it 'returns a 404 when a security txt is nil' do + stub_application_setting security_txt_content: nil + action + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'for an unauthenticated user' do + it 'renders when a security txt is configured' do + stub_application_setting security_txt_content: 'HELLO' + action + expect(response.body).to eq('HELLO') + end + + it 'redirects to sign in' do + stub_application_setting security_txt_content: '' + action + expect(response).to redirect_to(new_user_session_path) + end + end + end +end |