Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'spec/requests')
-rw-r--r--spec/requests/acme_challenges_controller_spec.rb9
-rw-r--r--spec/requests/api/admin/dictionary_spec.rb18
-rw-r--r--spec/requests/api/ci/job_artifacts_spec.rb44
-rw-r--r--spec/requests/api/ci/pipeline_schedules_spec.rb2
-rw-r--r--spec/requests/api/ci/runner/jobs_request_post_spec.rb57
-rw-r--r--spec/requests/api/ci/runner/jobs_request_yamls_spec.rb64
-rw-r--r--spec/requests/api/ci/runner/runners_delete_spec.rb2
-rw-r--r--spec/requests/api/ci/runner/runners_post_spec.rb2
-rw-r--r--spec/requests/api/ci/runner/runners_reset_spec.rb2
-rw-r--r--spec/requests/api/ci/runner/yamls/README.md15
-rw-r--r--spec/requests/api/ci/runner/yamls/image-basic.yml19
-rw-r--r--spec/requests/api/ci/runner/yamls/image-executor_opts-platform.yml25
-rw-r--r--spec/requests/api/ci/runner/yamls/service-basic.yml23
-rw-r--r--spec/requests/api/ci/runner/yamls/service-executor_opts-platform.yml27
-rw-r--r--spec/requests/api/ci/runner/yamls/service-variables.yml30
-rw-r--r--spec/requests/api/ci/runners_reset_registration_token_spec.rb2
-rw-r--r--spec/requests/api/ci/runners_spec.rb2
-rw-r--r--spec/requests/api/commits_spec.rb28
-rw-r--r--spec/requests/api/deploy_tokens_spec.rb1
-rw-r--r--spec/requests/api/deployments_spec.rb68
-rw-r--r--spec/requests/api/environments_spec.rb67
-rw-r--r--spec/requests/api/events_spec.rb14
-rw-r--r--spec/requests/api/graphql/abuse_report_spec.rb6
-rw-r--r--spec/requests/api/graphql/ci/catalog/resource_spec.rb188
-rw-r--r--spec/requests/api/graphql/ci/catalog/resources_spec.rb76
-rw-r--r--spec/requests/api/graphql/ci/runner_spec.rb2
-rw-r--r--spec/requests/api/graphql/ci/runner_web_url_edge_spec.rb2
-rw-r--r--spec/requests/api/graphql/ci/runners_spec.rb72
-rw-r--r--spec/requests/api/graphql/container_repository/container_repository_details_spec.rb3
-rw-r--r--spec/requests/api/graphql/custom_emoji_query_spec.rb4
-rw-r--r--spec/requests/api/graphql/group/issues_spec.rb36
-rw-r--r--spec/requests/api/graphql/group/work_item_state_counts_spec.rb107
-rw-r--r--spec/requests/api/graphql/group/work_item_types_spec.rb55
-rw-r--r--spec/requests/api/graphql/milestone_spec.rb14
-rw-r--r--spec/requests/api/graphql/mutations/admin/sidekiq_queues/delete_jobs_spec.rb4
-rw-r--r--spec/requests/api/graphql/mutations/branch_rules/update_spec.rb95
-rw-r--r--spec/requests/api/graphql/mutations/ci/catalog/resources/destroy_spec.rb41
-rw-r--r--spec/requests/api/graphql/mutations/ci/catalog/unpublish_spec.rb52
-rw-r--r--spec/requests/api/graphql/mutations/ci/runner/create_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/ci/runners_registration_token/reset_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/container_registry/protection/rule/create_spec.rb26
-rw-r--r--spec/requests/api/graphql/mutations/container_registry/protection/rule/delete_spec.rb102
-rw-r--r--spec/requests/api/graphql/mutations/container_registry/protection/rule/update_spec.rb143
-rw-r--r--spec/requests/api/graphql/mutations/merge_requests/set_assignees_spec.rb4
-rw-r--r--spec/requests/api/graphql/mutations/namespace/package_settings/update_spec.rb11
-rw-r--r--spec/requests/api/graphql/mutations/organizations/create_spec.rb43
-rw-r--r--spec/requests/api/graphql/mutations/organizations/update_spec.rb120
-rw-r--r--spec/requests/api/graphql/mutations/packages/bulk_destroy_spec.rb20
-rw-r--r--spec/requests/api/graphql/mutations/packages/destroy_spec.rb11
-rw-r--r--spec/requests/api/graphql/mutations/packages/protection/rule/delete_spec.rb10
-rw-r--r--spec/requests/api/graphql/mutations/packages/protection/rule/update_spec.rb134
-rw-r--r--spec/requests/api/graphql/mutations/user_preferences/update_spec.rb42
-rw-r--r--spec/requests/api/graphql/mutations/work_items/delete_task_spec.rb79
-rw-r--r--spec/requests/api/graphql/organizations/organization_query_spec.rb9
-rw-r--r--spec/requests/api/graphql/project/alert_management/integrations_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/cluster_agents_spec.rb4
-rw-r--r--spec/requests/api/graphql/project/value_streams_spec.rb105
-rw-r--r--spec/requests/api/graphql/project/work_item_state_counts_spec.rb123
-rw-r--r--spec/requests/api/graphql/project/work_item_types_spec.rb55
-rw-r--r--spec/requests/api/graphql/work_item_spec.rb12
-rw-r--r--spec/requests/api/graphql/work_items_by_reference_spec.rb130
-rw-r--r--spec/requests/api/group_export_spec.rb32
-rw-r--r--spec/requests/api/group_milestones_spec.rb110
-rw-r--r--spec/requests/api/import_github_spec.rb25
-rw-r--r--spec/requests/api/integrations_spec.rb3
-rw-r--r--spec/requests/api/internal/kubernetes_spec.rb117
-rw-r--r--spec/requests/api/issues/issues_spec.rb2
-rw-r--r--spec/requests/api/maven_packages_spec.rb58
-rw-r--r--spec/requests/api/ml/mlflow/model_versions_spec.rb99
-rw-r--r--spec/requests/api/ml/mlflow/registered_models_spec.rb98
-rw-r--r--spec/requests/api/ml_model_packages_spec.rb42
-rw-r--r--spec/requests/api/npm_project_packages_spec.rb8
-rw-r--r--spec/requests/api/nuget_group_packages_spec.rb8
-rw-r--r--spec/requests/api/nuget_project_packages_spec.rb6
-rw-r--r--spec/requests/api/project_attributes.yml2
-rw-r--r--spec/requests/api/project_events_spec.rb45
-rw-r--r--spec/requests/api/project_export_spec.rb32
-rw-r--r--spec/requests/api/project_milestones_spec.rb28
-rw-r--r--spec/requests/api/project_templates_spec.rb15
-rw-r--r--spec/requests/api/projects_spec.rb47
-rw-r--r--spec/requests/api/remote_mirrors_spec.rb2
-rw-r--r--spec/requests/api/settings_spec.rb35
-rw-r--r--spec/requests/api/snippets_spec.rb4
-rw-r--r--spec/requests/api/terraform/modules/v1/packages_spec.rb218
-rw-r--r--spec/requests/api/terraform/modules/v1/project_packages_spec.rb205
-rw-r--r--spec/requests/api/user_runners_spec.rb2
-rw-r--r--spec/requests/api/users_spec.rb36
-rw-r--r--spec/requests/application_controller_spec.rb15
-rw-r--r--spec/requests/chaos_controller_spec.rb14
-rw-r--r--spec/requests/clusters/agents/dashboard_controller_spec.rb28
-rw-r--r--spec/requests/concerns/membership_actions_shared_examples.rb67
-rw-r--r--spec/requests/content_security_policy_spec.rb79
-rw-r--r--spec/requests/explore/catalog_controller_spec.rb54
-rw-r--r--spec/requests/external_redirect/external_redirect_controller_spec.rb5
-rw-r--r--spec/requests/groups/group_members_controller_spec.rb20
-rw-r--r--spec/requests/health_controller_spec.rb8
-rw-r--r--spec/requests/ide_controller_spec.rb122
-rw-r--r--spec/requests/jwks_controller_spec.rb21
-rw-r--r--spec/requests/jwt_controller_spec.rb56
-rw-r--r--spec/requests/legacy_routes_spec.rb56
-rw-r--r--spec/requests/metrics_controller_spec.rb9
-rw-r--r--spec/requests/oauth/authorizations_controller_spec.rb4
-rw-r--r--spec/requests/organizations/organizations_controller_spec.rb47
-rw-r--r--spec/requests/organizations/settings_controller_spec.rb2
-rw-r--r--spec/requests/projects/gcp/artifact_registry/docker_images_controller_spec.rb136
-rw-r--r--spec/requests/projects/gcp/artifact_registry/setup_controller_spec.rb73
-rw-r--r--spec/requests/projects/integrations/shimos_controller_spec.rb37
-rw-r--r--spec/requests/projects/merge_requests/content_spec.rb8
-rw-r--r--spec/requests/projects/ml/candidates_controller_spec.rb20
-rw-r--r--spec/requests/projects/pipelines_controller_spec.rb53
-rw-r--r--spec/requests/projects/project_members_controller_spec.rb23
-rw-r--r--spec/requests/projects/service_desk/custom_email_controller_spec.rb16
-rw-r--r--spec/requests/projects/service_desk_controller_spec.rb31
-rw-r--r--spec/requests/projects/tags_controller_spec.rb19
-rw-r--r--spec/requests/registrations_controller_spec.rb6
-rw-r--r--spec/requests/runner_setup_controller_spec.rb2
-rw-r--r--spec/requests/sessions_spec.rb6
-rw-r--r--spec/requests/user_settings_spec.rb31
-rw-r--r--spec/requests/well_known_routing_spec.rb13
-rw-r--r--spec/requests/well_known_spec.rb55
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&#39;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&#39;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