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/api')
-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
86 files changed, 2798 insertions, 939 deletions
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) }