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:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-09-20 02:18:09 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2022-09-20 02:18:09 +0300
commit6ed4ec3e0b1340f96b7c043ef51d1b33bbe85fde (patch)
treedc4d20fe6064752c0bd323187252c77e0a89144b /spec/requests/api
parent9868dae7fc0655bd7ce4a6887d4e6d487690eeed (diff)
Add latest changes from gitlab-org/gitlab@15-4-stable-eev15.4.0-rc42
Diffstat (limited to 'spec/requests/api')
-rw-r--r--spec/requests/api/admin/batched_background_migrations_spec.rb230
-rw-r--r--spec/requests/api/branches_spec.rb141
-rw-r--r--spec/requests/api/ci/job_artifacts_spec.rb12
-rw-r--r--spec/requests/api/ci/jobs_spec.rb26
-rw-r--r--spec/requests/api/ci/runner/jobs_request_post_spec.rb83
-rw-r--r--spec/requests/api/ci/runners_spec.rb38
-rw-r--r--spec/requests/api/commit_statuses_spec.rb20
-rw-r--r--spec/requests/api/commits_spec.rb616
-rw-r--r--spec/requests/api/conan_instance_packages_spec.rb6
-rw-r--r--spec/requests/api/conan_project_packages_spec.rb6
-rw-r--r--spec/requests/api/debian_group_packages_spec.rb40
-rw-r--r--spec/requests/api/debian_project_packages_spec.rb40
-rw-r--r--spec/requests/api/deployments_spec.rb9
-rw-r--r--spec/requests/api/feature_flags_spec.rb8
-rw-r--r--spec/requests/api/files_spec.rb715
-rw-r--r--spec/requests/api/generic_packages_spec.rb18
-rw-r--r--spec/requests/api/graphql/ci/config_spec.rb6
-rw-r--r--spec/requests/api/graphql/ci/config_variables_spec.rb93
-rw-r--r--spec/requests/api/graphql/ci/group_variables_spec.rb12
-rw-r--r--spec/requests/api/graphql/ci/instance_variables_spec.rb2
-rw-r--r--spec/requests/api/graphql/ci/jobs_spec.rb31
-rw-r--r--spec/requests/api/graphql/ci/project_variables_spec.rb4
-rw-r--r--spec/requests/api/graphql/ci/runner_spec.rb163
-rw-r--r--spec/requests/api/graphql/ci/runners_spec.rb18
-rw-r--r--spec/requests/api/graphql/custom_emoji_query_spec.rb12
-rw-r--r--spec/requests/api/graphql/environments/deployments_query_spec.rb487
-rw-r--r--spec/requests/api/graphql/group/group_members_spec.rb9
-rw-r--r--spec/requests/api/graphql/group/packages_spec.rb2
-rw-r--r--spec/requests/api/graphql/group/work_item_types_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/boards/issues/issue_move_list_spec.rb14
-rw-r--r--spec/requests/api/graphql/mutations/branches/create_spec.rb89
-rw-r--r--spec/requests/api/graphql/mutations/ci/job/destroy_spec.rb54
-rw-r--r--spec/requests/api/graphql/mutations/ci/job_artifact/destroy_spec.rb47
-rw-r--r--spec/requests/api/graphql/mutations/custom_emoji/create_spec.rb14
-rw-r--r--spec/requests/api/graphql/mutations/custom_emoji/destroy_spec.rb14
-rw-r--r--spec/requests/api/graphql/mutations/incident_management/timeline_event/promote_from_note_spec.rb3
-rw-r--r--spec/requests/api/graphql/mutations/merge_requests/set_assignees_spec.rb10
-rw-r--r--spec/requests/api/graphql/mutations/releases/update_spec.rb11
-rw-r--r--spec/requests/api/graphql/packages/composer_spec.rb2
-rw-r--r--spec/requests/api/graphql/packages/conan_spec.rb2
-rw-r--r--spec/requests/api/graphql/packages/helm_spec.rb2
-rw-r--r--spec/requests/api/graphql/packages/maven_spec.rb6
-rw-r--r--spec/requests/api/graphql/packages/nuget_spec.rb2
-rw-r--r--spec/requests/api/graphql/packages/package_spec.rb15
-rw-r--r--spec/requests/api/graphql/packages/pypi_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/branch_protections/merge_access_levels_spec.rb109
-rw-r--r--spec/requests/api/graphql/project/branch_protections/push_access_levels_spec.rb109
-rw-r--r--spec/requests/api/graphql/project/branch_rules/branch_protection_spec.rb60
-rw-r--r--spec/requests/api/graphql/project/branch_rules_spec.rb122
-rw-r--r--spec/requests/api/graphql/project/deployment_spec.rb51
-rw-r--r--spec/requests/api/graphql/project/environments_spec.rb133
-rw-r--r--spec/requests/api/graphql/project/issue/design_collection/version_spec.rb10
-rw-r--r--spec/requests/api/graphql/project/issues_spec.rb26
-rw-r--r--spec/requests/api/graphql/project/job_spec.rb54
-rw-r--r--spec/requests/api/graphql/project/merge_request_spec.rb20
-rw-r--r--spec/requests/api/graphql/project/pipeline_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/terraform/state_spec.rb16
-rw-r--r--spec/requests/api/graphql/project/terraform/states_spec.rb18
-rw-r--r--spec/requests/api/graphql/project/work_items_spec.rb63
-rw-r--r--spec/requests/api/graphql/query_spec.rb4
-rw-r--r--spec/requests/api/graphql/work_item_spec.rb19
-rw-r--r--spec/requests/api/group_export_spec.rb1
-rw-r--r--spec/requests/api/groups_spec.rb136
-rw-r--r--spec/requests/api/import_github_spec.rb10
-rw-r--r--spec/requests/api/integrations/slack/events_spec.rb112
-rw-r--r--spec/requests/api/internal/base_spec.rb201
-rw-r--r--spec/requests/api/internal/lfs_spec.rb26
-rw-r--r--spec/requests/api/issues/get_group_issues_spec.rb8
-rw-r--r--spec/requests/api/markdown_snapshot_spec.rb4
-rw-r--r--spec/requests/api/maven_packages_spec.rb160
-rw-r--r--spec/requests/api/merge_requests_spec.rb187
-rw-r--r--spec/requests/api/ml/mlflow_spec.rb366
-rw-r--r--spec/requests/api/namespaces_spec.rb8
-rw-r--r--spec/requests/api/npm_project_packages_spec.rb3
-rw-r--r--spec/requests/api/personal_access_tokens/self_revocation_spec.rb69
-rw-r--r--spec/requests/api/personal_access_tokens_spec.rb58
-rw-r--r--spec/requests/api/project_attributes.yml2
-rw-r--r--spec/requests/api/project_import_spec.rb2
-rw-r--r--spec/requests/api/project_packages_spec.rb13
-rw-r--r--spec/requests/api/project_snippets_spec.rb3
-rw-r--r--spec/requests/api/projects_spec.rb190
-rw-r--r--spec/requests/api/releases_spec.rb218
-rw-r--r--spec/requests/api/resource_access_tokens_spec.rb60
-rw-r--r--spec/requests/api/resource_state_events_spec.rb83
-rw-r--r--spec/requests/api/rpm_project_packages_spec.rb250
-rw-r--r--spec/requests/api/search_spec.rb40
-rw-r--r--spec/requests/api/settings_spec.rb14
-rw-r--r--spec/requests/api/snippets_spec.rb2
-rw-r--r--spec/requests/api/suggestions_spec.rb15
-rw-r--r--spec/requests/api/tags_spec.rb78
-rw-r--r--spec/requests/api/topics_spec.rb62
-rw-r--r--spec/requests/api/unleash_spec.rb94
-rw-r--r--spec/requests/api/usage_data_queries_spec.rb32
-rw-r--r--spec/requests/api/usage_data_spec.rb4
-rw-r--r--spec/requests/api/users_spec.rb122
95 files changed, 5243 insertions, 1342 deletions
diff --git a/spec/requests/api/admin/batched_background_migrations_spec.rb b/spec/requests/api/admin/batched_background_migrations_spec.rb
new file mode 100644
index 00000000000..c99b21c0c27
--- /dev/null
+++ b/spec/requests/api/admin/batched_background_migrations_spec.rb
@@ -0,0 +1,230 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe API::Admin::BatchedBackgroundMigrations do
+ let(:admin) { create(:admin) }
+ let(:unauthorized_user) { create(:user) }
+
+ describe 'GET /admin/batched_background_migrations/:id' do
+ let!(:migration) { create(:batched_background_migration, :paused) }
+ let(:database) { :main }
+
+ subject(:show_migration) do
+ get api("/admin/batched_background_migrations/#{migration.id}", admin), params: { database: database }
+ end
+
+ it 'fetches the batched background migration' do
+ show_migration
+
+ aggregate_failures "testing response" do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['id']).to eq(migration.id)
+ expect(json_response['status']).to eq('paused')
+ expect(json_response['job_class_name']).to eq(migration.job_class_name)
+ expect(json_response['progress']).to be_zero
+ end
+ end
+
+ context 'when the batched background migration does not exist' do
+ let(:params) { { database: database } }
+
+ it 'returns 404' do
+ put api("/admin/batched_background_migrations/#{non_existing_record_id}", admin), params: params
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'when multiple database is enabled' do
+ before do
+ skip_if_multiple_databases_not_setup
+ end
+
+ let(:ci_model) { Ci::ApplicationRecord }
+ let(:database) { :ci }
+
+ it 'uses the correct connection' do
+ expect(Gitlab::Database::SharedModel).to receive(:using_connection).with(ci_model.connection).and_yield
+
+ show_migration
+ end
+ end
+
+ context 'when authenticated as a non-admin user' do
+ it 'returns 403' do
+ get api("/admin/batched_background_migrations/#{migration.id}", unauthorized_user)
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+ end
+
+ describe 'GET /admin/batched_background_migrations' do
+ let!(:migration) { create(:batched_background_migration) }
+
+ context 'when is an admin user' do
+ it 'returns batched background migrations' do
+ get api('/admin/batched_background_migrations', admin)
+
+ aggregate_failures "testing response" do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response.count).to eq(1)
+ expect(json_response.first['id']).to eq(migration.id)
+ expect(json_response.first['job_class_name']).to eq(migration.job_class_name)
+ expect(json_response.first['table_name']).to eq(migration.table_name)
+ expect(json_response.first['status']).to eq(migration.status_name.to_s)
+ expect(json_response.first['progress']).to be_zero
+ end
+ end
+
+ context 'when multiple database is enabled', :add_ci_connection do
+ let(:database) { :ci }
+ let(:schema) { :gitlab_ci }
+ let(:ci_model) { Ci::ApplicationRecord }
+
+ context 'when CI database is provided' do
+ let(:db_config) { instance_double(ActiveRecord::DatabaseConfigurations::HashConfig, name: 'fake_db') }
+ let(:default_model) { ActiveRecord::Base }
+ let(:base_models) { { 'fake_db' => default_model, 'ci' => ci_model }.with_indifferent_access }
+
+ it "uses CI database connection" do
+ allow(Gitlab::Database).to receive(:db_config_for_connection).and_return(db_config)
+ allow(Gitlab::Database).to receive(:database_base_models).and_return(base_models)
+
+ expect(Gitlab::Database::SharedModel).to receive(:using_connection).with(ci_model.connection).and_yield
+
+ get api('/admin/batched_background_migrations', admin), params: { database: :ci }
+ end
+
+ it 'returns CI database records' do
+ # If we only have one DB we'll see both migrations
+ skip_if_multiple_databases_not_setup
+
+ ci_database_migration = Gitlab::Database::SharedModel.using_connection(ci_model.connection) do
+ create(:batched_background_migration, :active, gitlab_schema: schema)
+ end
+
+ get api('/admin/batched_background_migrations', admin), params: { database: :ci }
+
+ aggregate_failures "testing response" do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response.count).to eq(1)
+ expect(json_response.first['id']).to eq(ci_database_migration.id)
+ expect(json_response.first['job_class_name']).to eq(ci_database_migration.job_class_name)
+ expect(json_response.first['table_name']).to eq(ci_database_migration.table_name)
+ expect(json_response.first['status']).to eq(ci_database_migration.status_name.to_s)
+ expect(json_response.first['progress']).to be_zero
+ end
+ end
+ end
+ end
+ end
+
+ context 'when authenticated as a non-admin user' do
+ it 'returns 403' do
+ get api('/admin/batched_background_migrations', unauthorized_user)
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+ end
+
+ describe 'PUT /admin/batched_background_migrations/:id/resume' do
+ let!(:migration) { create(:batched_background_migration, :paused) }
+ let(:database) { :main }
+
+ subject(:resume) do
+ put api("/admin/batched_background_migrations/#{migration.id}/resume", admin), params: { database: database }
+ end
+
+ it 'pauses the batched background migration' do
+ resume
+
+ aggregate_failures "testing response" do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['id']).to eq(migration.id)
+ expect(json_response['status']).to eq('active')
+ end
+ end
+
+ context 'when the batched background migration does not exist' do
+ let(:params) { { database: database } }
+
+ it 'returns 404' do
+ put api("/admin/batched_background_migrations/#{non_existing_record_id}/resume", admin), params: params
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'when multiple database is enabled' do
+ let(:ci_model) { Ci::ApplicationRecord }
+ let(:database) { :ci }
+
+ before do
+ skip_if_multiple_databases_not_setup
+ end
+
+ it 'uses the correct connection' do
+ expect(Gitlab::Database::SharedModel).to receive(:using_connection).with(ci_model.connection).and_yield
+
+ resume
+ end
+ end
+
+ context 'when authenticated as a non-admin user' do
+ it 'returns 403' do
+ put api("/admin/batched_background_migrations/#{migration.id}/resume", unauthorized_user)
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+ end
+
+ describe 'PUT /admin/batched_background_migrations/:id/pause' do
+ let!(:migration) { create(:batched_background_migration, :active) }
+
+ it 'pauses the batched background migration' do
+ put api("/admin/batched_background_migrations/#{migration.id}/pause", admin), params: { database: :main }
+
+ aggregate_failures "testing response" do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['id']).to eq(migration.id)
+ expect(json_response['status']).to eq('paused')
+ end
+ end
+
+ context 'when the batched background migration does not exist' do
+ let(:params) { { database: :main } }
+
+ it 'returns 404' do
+ put api("/admin/batched_background_migrations/#{non_existing_record_id}/pause", admin), params: params
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'when multiple database is enabled' do
+ let(:ci_model) { Ci::ApplicationRecord }
+
+ before do
+ skip_if_multiple_databases_not_setup
+ end
+
+ it 'uses the correct connection' do
+ expect(Gitlab::Database::SharedModel).to receive(:using_connection).with(ci_model.connection).and_yield
+
+ put api("/admin/batched_background_migrations/#{migration.id}/pause", admin), params: { database: :ci }
+ end
+ end
+
+ context 'when authenticated as a non-admin user' do
+ it 'returns 403' do
+ put api("/admin/batched_background_migrations/#{non_existing_record_id}/pause", unauthorized_user)
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/branches_spec.rb b/spec/requests/api/branches_spec.rb
index cc696d76a02..f7539e13b80 100644
--- a/spec/requests/api/branches_spec.rb
+++ b/spec/requests/api/branches_spec.rb
@@ -211,6 +211,68 @@ RSpec.describe API::Branches do
end
it_behaves_like 'repository branches'
+
+ context 'caching' do
+ it 'caches the query' do
+ get api(route), params: { per_page: 1 }
+
+ expect(API::Entities::Branch).not_to receive(:represent)
+
+ get api(route), params: { per_page: 1 }
+ end
+
+ context 'when increase_branch_cache_expiry is enabled' do
+ it 'uses the cache up to 60 minutes' do
+ time_of_request = Time.current
+
+ get api(route), params: { per_page: 1 }
+
+ travel_to time_of_request + 59.minutes do
+ expect(API::Entities::Branch).not_to receive(:represent)
+
+ get api(route), params: { per_page: 1 }
+ end
+ end
+
+ it 'requests for new value after 60 minutes' do
+ get api(route), params: { per_page: 1 }
+
+ travel_to 61.minutes.from_now do
+ expect(API::Entities::Branch).to receive(:represent)
+
+ get api(route), params: { per_page: 1 }
+ end
+ end
+ end
+
+ context 'when increase_branch_cache_expiry is disabled' do
+ before do
+ stub_feature_flags(increase_branch_cache_expiry: false)
+ end
+
+ it 'uses the cache up to 10 minutes' do
+ time_of_request = Time.current
+
+ get api(route), params: { per_page: 1 }
+
+ travel_to time_of_request + 9.minutes do
+ expect(API::Entities::Branch).not_to receive(:represent)
+
+ get api(route), params: { per_page: 1 }
+ end
+ end
+
+ it 'requests for new value after 10 minutes' do
+ get api(route), params: { per_page: 1 }
+
+ travel_to 11.minutes.from_now do
+ expect(API::Entities::Branch).to receive(:represent)
+
+ get api(route), params: { per_page: 1 }
+ end
+ end
+ end
+ end
end
context 'when unauthenticated', 'and project is private' do
@@ -586,13 +648,36 @@ RSpec.describe API::Branches do
let(:route) { "/projects/#{project_id}/repository/branches/#{branch_name}/unprotect" }
shared_examples_for 'repository unprotected branch' do
- it 'unprotects a single branch' do
- put api(route, current_user)
+ context 'when branch is protected' do
+ let!(:protected_branch) { create(:protected_branch, project: project, name: protected_branch_name) }
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to match_response_schema('public_api/v4/branch')
- expect(json_response['name']).to eq(CGI.unescape(branch_name))
- expect(json_response['protected']).to eq(false)
+ it 'unprotects a single branch' do
+ expect_next_instance_of(::ProtectedBranches::DestroyService, project, current_user) do |instance|
+ expect(instance).to receive(:execute).with(protected_branch).and_call_original
+ end
+
+ put api(route, current_user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to match_response_schema('public_api/v4/branch')
+ expect(json_response['name']).to eq(CGI.unescape(branch_name))
+ expect(json_response['protected']).to eq(false)
+
+ expect { protected_branch.reload }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+ end
+
+ context 'when branch is not protected' do
+ it 'returns a single branch response' do
+ expect(::ProtectedBranches::DestroyService).not_to receive(:new)
+
+ put api(route, current_user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to match_response_schema('public_api/v4/branch')
+ expect(json_response['name']).to eq(CGI.unescape(branch_name))
+ expect(json_response['protected']).to eq(false)
+ end
end
context 'when branch does not exist' do
@@ -637,40 +722,40 @@ RSpec.describe API::Branches do
context 'when authenticated', 'as a maintainer' do
let(:current_user) { user }
+ let(:protected_branch_name) { branch_name }
- context "when a protected branch doesn't already exist" do
- it_behaves_like 'repository unprotected branch'
+ it_behaves_like 'repository unprotected branch'
- context 'when branch contains a dot' do
- let(:branch_name) { branch_with_dot }
+ context 'when branch contains a dot' do
+ let(:branch_name) { branch_with_dot }
- it_behaves_like 'repository unprotected branch'
- end
+ it_behaves_like 'repository unprotected branch'
+ end
- context 'when branch contains a slash' do
- let(:branch_name) { branch_with_slash }
+ context 'when branch contains a slash' do
+ let(:branch_name) { branch_with_slash }
- it_behaves_like '404 response' do
- let(:request) { put api(route, current_user) }
- end
+ it_behaves_like '404 response' do
+ let(:request) { put api(route, current_user) }
end
+ end
- context 'when branch contains an escaped slash' do
- let(:branch_name) { CGI.escape(branch_with_slash) }
+ context 'when branch contains an escaped slash' do
+ let(:branch_name) { CGI.escape(branch_with_slash) }
+ let(:protected_branch_name) { branch_with_slash }
- it_behaves_like 'repository unprotected branch'
- end
+ it_behaves_like 'repository unprotected branch'
+ end
- context 'requesting with the escaped project full path' do
- let(:project_id) { CGI.escape(project.full_path) }
+ context 'requesting with the escaped project full path' do
+ let(:project_id) { CGI.escape(project.full_path) }
- it_behaves_like 'repository unprotected branch'
+ it_behaves_like 'repository unprotected branch'
- context 'when branch contains a dot' do
- let(:branch_name) { branch_with_dot }
+ context 'when branch contains a dot' do
+ let(:branch_name) { branch_with_dot }
- it_behaves_like 'repository unprotected branch'
- end
+ it_behaves_like 'repository unprotected branch'
end
end
end
diff --git a/spec/requests/api/ci/job_artifacts_spec.rb b/spec/requests/api/ci/job_artifacts_spec.rb
index 2fa1ffb4974..0fb11bf98d2 100644
--- a/spec/requests/api/ci/job_artifacts_spec.rb
+++ b/spec/requests/api/ci/job_artifacts_spec.rb
@@ -24,8 +24,7 @@ RSpec.describe API::Ci::JobArtifacts do
let(:guest) { create(:project_member, :guest, project: project).user }
let!(:job) do
- create(:ci_build, :success, :tags, pipeline: pipeline,
- artifacts_expire_at: 1.day.since)
+ create(:ci_build, :success, :tags, pipeline: pipeline, artifacts_expire_at: 1.day.since)
end
before do
@@ -535,8 +534,7 @@ RSpec.describe API::Ci::JobArtifacts do
context 'with regular branch' do
before do
pipeline.reload
- pipeline.update!(ref: 'master',
- sha: project.commit('master').sha)
+ pipeline.update!(ref: 'master', sha: project.commit('master').sha)
get_for_ref('master')
end
@@ -579,8 +577,7 @@ RSpec.describe API::Ci::JobArtifacts do
stub_artifacts_object_storage
job.success
- project.update!(visibility_level: visibility_level,
- public_builds: public_builds)
+ project.update!(visibility_level: visibility_level, public_builds: public_builds)
get_artifact_file(artifact)
end
@@ -676,8 +673,7 @@ RSpec.describe API::Ci::JobArtifacts do
context 'with branch name containing slash' do
before do
pipeline.reload
- pipeline.update!(ref: 'improve/awesome',
- sha: project.commit('improve/awesome').sha)
+ pipeline.update!(ref: 'improve/awesome', sha: project.commit('improve/awesome').sha)
end
it 'returns a specific artifact file for a valid path', :sidekiq_might_not_need_inline do
diff --git a/spec/requests/api/ci/jobs_spec.rb b/spec/requests/api/ci/jobs_spec.rb
index 57828e50320..b8983e9632e 100644
--- a/spec/requests/api/ci/jobs_spec.rb
+++ b/spec/requests/api/ci/jobs_spec.rb
@@ -32,8 +32,7 @@ RSpec.describe API::Ci::Jobs do
end
let!(:job) do
- create(:ci_build, :success, :tags, pipeline: pipeline,
- artifacts_expire_at: 1.day.since)
+ create(:ci_build, :success, :tags, pipeline: pipeline, artifacts_expire_at: 1.day.since)
end
before do
@@ -94,9 +93,13 @@ RSpec.describe API::Ci::Jobs do
let(:params_with_token) { {} }
end
+ def perform_request
+ get api('/job'), headers: headers_with_token, params: params_with_token
+ end
+
before do |example|
unless example.metadata[:skip_before_request]
- get api('/job'), headers: headers_with_token, params: params_with_token
+ perform_request
end
end
@@ -125,6 +128,15 @@ RSpec.describe API::Ci::Jobs do
expect(json_response['finished_at']).to be_nil
end
+ it 'avoids N+1 queries', :skip_before_request do
+ control_count = ActiveRecord::QueryRecorder.new { perform_request }.count
+
+ running_job = create(:ci_build, :running, project: project, user: user, pipeline: pipeline, artifacts_expire_at: 1.day.since)
+ running_job.save!
+
+ expect { perform_request }.not_to exceed_query_limit(control_count)
+ end
+
it_behaves_like 'returns common pipeline data' do
let(:jobx) { running_job }
end
@@ -237,6 +249,10 @@ RSpec.describe API::Ci::Jobs do
it 'includes environment slug' do
expect(json_response.dig('environment', 'slug')).to eq('production')
end
+
+ it 'includes environment tier' do
+ expect(json_response.dig('environment', 'tier')).to eq('production')
+ end
end
context 'when non-deployment environment action' do
@@ -248,6 +264,10 @@ RSpec.describe API::Ci::Jobs do
it 'includes environment slug' do
expect(json_response.dig('environment', 'slug')).to eq('review')
end
+
+ it 'includes environment tier' do
+ expect(json_response.dig('environment', 'tier')).to eq('development')
+ end
end
context 'when passing the token as params' do
diff --git a/spec/requests/api/ci/runner/jobs_request_post_spec.rb b/spec/requests/api/ci/runner/jobs_request_post_spec.rb
index cd58251cfcc..b33b97f90d7 100644
--- a/spec/requests/api/ci/runner/jobs_request_post_spec.rb
+++ b/spec/requests/api/ci/runner/jobs_request_post_spec.rb
@@ -17,11 +17,12 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
end
describe '/api/v4/jobs' do
- let(:group) { create(:group, :nested) }
+ let_it_be(:group) { create(:group, :nested) }
+ let_it_be(:user) { create(:user) }
+
let(:project) { create(:project, namespace: group, shared_runners_enabled: false) }
- let(:pipeline) { create(:ci_pipeline, project: project, ref: 'master') }
let(:runner) { create(:ci_runner, :project, projects: [project]) }
- let(:user) { create(:user) }
+ let(:pipeline) { create(:ci_pipeline, project: project, ref: 'master') }
let(:job) do
create(:ci_build, :pending, :queued, :artifacts, :extended_options,
pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0)
@@ -145,7 +146,7 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
let(:expected_job_info) do
{ 'id' => job.id,
'name' => job.name,
- 'stage' => job.stage,
+ 'stage' => job.stage_name,
'project_id' => job.project.id,
'project_name' => job.project.name }
end
@@ -354,6 +355,9 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
end
context 'when GIT_DEPTH is not specified and there is no default git depth for the project' do
+ let(:project) { create(:project, namespace: group, shared_runners_enabled: false) }
+ let(:runner) { create(:ci_runner, :project, projects: [project]) }
+
before do
project.update!(ci_default_git_depth: nil)
end
@@ -411,7 +415,8 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
context 'when job is made for merge request' do
let(:pipeline) { create(:ci_pipeline, source: :merge_request_event, project: project, ref: 'feature', merge_request: merge_request) }
let!(:job) { create(:ci_build, :pending, :queued, pipeline: pipeline, name: 'spinach', ref: 'feature', stage: 'test', stage_idx: 0) }
- let(:merge_request) { create(:merge_request) }
+
+ let_it_be(:merge_request) { create(:merge_request) }
it 'sets branch as ref_type' do
request_job
@@ -546,9 +551,12 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
let!(:job) { create(:ci_build, :pending, :queued, :tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) }
let!(:job2) { create(:ci_build, :pending, :queued, :tag, pipeline: pipeline, name: 'rubocop', stage: 'test', stage_idx: 0) }
let!(:test_job) do
- create(:ci_build, :pending, :queued, pipeline: pipeline, name: 'deploy',
- stage: 'deploy', stage_idx: 1,
- options: { script: ['bash'], dependencies: [job2.name] })
+ create(:ci_build, :pending, :queued,
+ pipeline: pipeline,
+ name: 'deploy',
+ stage: 'deploy',
+ stage_idx: 1,
+ options: { script: ['bash'], dependencies: [job2.name] })
end
before do
@@ -570,9 +578,12 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
let!(:job) { create(:ci_build, :pending, :queued, :tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) }
let!(:job2) { create(:ci_build, :pending, :queued, :tag, pipeline: pipeline, name: 'rubocop', stage: 'test', stage_idx: 0) }
let!(:empty_dependencies_job) do
- create(:ci_build, :pending, :queued, pipeline: pipeline, name: 'empty_dependencies_job',
- stage: 'deploy', stage_idx: 1,
- options: { script: ['bash'], dependencies: [] })
+ create(:ci_build, :pending, :queued,
+ pipeline: pipeline,
+ name: 'empty_dependencies_job',
+ stage: 'deploy',
+ stage_idx: 1,
+ options: { script: ['bash'], dependencies: [] })
end
before do
@@ -722,7 +733,9 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
describe 'timeout support' do
context 'when project specifies job timeout' do
- let(:project) { create(:project, shared_runners_enabled: false, build_timeout: 1234) }
+ let_it_be(:project) { create(:project, shared_runners_enabled: false, build_timeout: 1234) }
+
+ let(:runner) { create(:ci_runner, :project, projects: [project]) }
it 'contains info about timeout taken from project' do
request_job
@@ -827,22 +840,6 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
'image' => { 'name' => 'ruby', 'pull_policy' => ['if-not-present'], 'entrypoint' => nil, 'ports' => [] }
)
end
-
- context 'when the FF ci_docker_image_pull_policy is disabled' do
- before do
- stub_feature_flags(ci_docker_image_pull_policy: false)
- end
-
- it 'returns the image without pull policy' do
- request_job
-
- expect(response).to have_gitlab_http_status(:created)
- expect(json_response).to include(
- 'id' => job.id,
- 'image' => { 'name' => 'ruby', 'entrypoint' => nil, 'ports' => [] }
- )
- end
- end
end
context 'when service has pull_policy' do
@@ -867,31 +864,17 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
'ports' => [], 'pull_policy' => ['if-not-present'], 'variables' => [] }]
)
end
-
- context 'when the FF ci_docker_image_pull_policy is disabled' do
- before do
- stub_feature_flags(ci_docker_image_pull_policy: false)
- end
-
- it 'returns the service without pull policy' do
- request_job
-
- expect(response).to have_gitlab_http_status(:created)
- expect(json_response).to include(
- 'id' => job.id,
- 'services' => [{ 'alias' => nil, 'command' => nil, 'entrypoint' => nil, 'name' => 'postgres:11.9',
- 'ports' => [], 'variables' => [] }]
- )
- end
- end
end
describe 'a job with excluded artifacts' do
context 'when excluded paths are defined' do
let(:job) do
- create(:ci_build, :pending, :queued, pipeline: pipeline, name: 'test',
- stage: 'deploy', stage_idx: 1,
- options: { artifacts: { paths: ['abc'], exclude: ['cde'] } })
+ create(:ci_build, :pending, :queued,
+ pipeline: pipeline,
+ name: 'test',
+ stage: 'deploy',
+ stage_idx: 1,
+ options: { artifacts: { paths: ['abc'], exclude: ['cde'] } })
end
context 'when a runner supports this feature' do
@@ -950,8 +933,8 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
end
context 'when the runner is of group type' do
- let(:group) { create(:group) }
- let(:runner) { create(:ci_runner, :group, groups: [group]) }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:runner) { create(:ci_runner, :group, groups: [group]) }
it_behaves_like 'storing arguments in the application context for the API' do
let(:expected_params) { { root_namespace: group.full_path_components.first, client_id: "runner/#{runner.id}" } }
diff --git a/spec/requests/api/ci/runners_spec.rb b/spec/requests/api/ci/runners_spec.rb
index 31b85a0b1d6..fa1f713e757 100644
--- a/spec/requests/api/ci/runners_spec.rb
+++ b/spec/requests/api/ci/runners_spec.rb
@@ -889,6 +889,44 @@ RSpec.describe API::Ci::Runners do
end
end
+ it 'avoids N+1 DB queries' do
+ get api("/runners/#{shared_runner.id}/jobs", admin)
+
+ control = ActiveRecord::QueryRecorder.new do
+ get api("/runners/#{shared_runner.id}/jobs", admin)
+ end
+
+ create(:ci_build, :failed, runner: shared_runner, project: project)
+
+ expect do
+ get api("/runners/#{shared_runner.id}/jobs", admin)
+ end.not_to exceed_query_limit(control.count)
+ end
+
+ it 'batches loading of commits' do
+ shared_runner = create(:ci_runner, :instance, description: 'Shared runner')
+
+ project_with_repo = create(:project, :repository)
+
+ pipeline = create(:ci_pipeline, project: project_with_repo, sha: 'ddd0f15ae83993f5cb66a927a28673882e99100b')
+ create(:ci_build, :running, runner: shared_runner, project: project_with_repo, pipeline: pipeline)
+
+ pipeline = create(:ci_pipeline, project: project_with_repo, sha: 'c1c67abbaf91f624347bb3ae96eabe3a1b742478')
+ create(:ci_build, :failed, runner: shared_runner, project: project_with_repo, pipeline: pipeline)
+
+ pipeline = create(:ci_pipeline, project: project_with_repo, sha: '1a0b36b3cdad1d2ee32457c102a8c0b7056fa863')
+ create(:ci_build, :failed, runner: shared_runner, project: project_with_repo, pipeline: pipeline)
+
+ expect_next_instance_of(Repository) do |repo|
+ expect(repo).to receive(:commits_by).with(oids: %w[
+ 1a0b36b3cdad1d2ee32457c102a8c0b7056fa863
+ c1c67abbaf91f624347bb3ae96eabe3a1b742478
+ ]).once.and_call_original
+ end
+
+ get api("/runners/#{shared_runner.id}/jobs", admin), params: { per_page: 2, order_by: 'id', sort: 'desc' }
+ end
+
context "when runner doesn't exist" do
it 'returns 404' do
get api('/runners/0/jobs', admin)
diff --git a/spec/requests/api/commit_statuses_spec.rb b/spec/requests/api/commit_statuses_spec.rb
index 39be28d7427..dc5d9620dc4 100644
--- a/spec/requests/api/commit_statuses_spec.rb
+++ b/spec/requests/api/commit_statuses_spec.rb
@@ -478,6 +478,26 @@ RSpec.describe API::CommitStatuses do
.to include 'has already been taken'
end
end
+
+ context 'with partitions' do
+ let(:current_partition_id) { 123 }
+
+ before do
+ allow(Ci::Pipeline)
+ .to receive(:current_partition_value) { current_partition_id }
+ end
+
+ it 'creates records in the current partition' do
+ expect { post api(post_url, developer), params: { state: 'running' } }
+ .to change(CommitStatus, :count).by(1)
+ .and change(Ci::Pipeline, :count).by(1)
+
+ status = CommitStatus.find(json_response['id'])
+
+ expect(status.partition_id).to eq(current_partition_id)
+ expect(status.pipeline.partition_id).to eq(current_partition_id)
+ end
+ end
end
context 'reporter user' do
diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb
index 68fe45cd026..8a08d5203fd 100644
--- a/spec/requests/api/commits_spec.rb
+++ b/spec/requests/api/commits_spec.rb
@@ -7,14 +7,17 @@ RSpec.describe API::Commits do
include ProjectForksHelper
include SessionHelpers
- let(:user) { create(:user) }
- let(:guest) { create(:user).tap { |u| project.add_guest(u) } }
- let(:developer) { create(:user).tap { |u| project.add_developer(u) } }
- let(:project) { create(:project, :repository, creator: user, path: 'my.project') }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project, :repository, creator: user, path: 'my.project') }
+ let_it_be(:guest) { create(:user).tap { |u| project.add_guest(u) } }
+ let_it_be(:developer) { create(:user).tap { |u| project.add_developer(u) } }
+
let(:branch_with_dot) { project.repository.find_branch('ends-with.json') }
let(:branch_with_slash) { project.repository.find_branch('improve/awesome') }
let(:project_id) { project.id }
let(:current_user) { nil }
+ let(:group) { create(:group, :public) }
+ let(:inherited_guest) { create(:user).tap { |u| group.add_guest(u) } }
before do
project.add_maintainer(user)
@@ -44,7 +47,7 @@ RSpec.describe API::Commits do
end
context 'when unauthenticated', 'and project is public' do
- let(:project) { create(:project, :public, :repository) }
+ let_it_be(:project) { create(:project, :public, :repository) }
it_behaves_like 'project commits'
end
@@ -56,311 +59,340 @@ RSpec.describe API::Commits do
end
end
- context 'when authenticated', 'as a maintainer' do
- let(:current_user) { user }
+ context 'when authenticated' do
+ context 'when user is a direct project member' do
+ context 'and user is a maintainer' do
+ let(:current_user) { user }
- it_behaves_like 'project commits'
+ it_behaves_like 'project commits'
- context "since optional parameter" do
- it "returns project commits since provided parameter" do
- commits = project.repository.commits("master", limit: 2)
- after = commits.second.created_at
+ context "since optional parameter" do
+ it "returns project commits since provided parameter" do
+ commits = project.repository.commits("master", limit: 2)
+ after = commits.second.created_at
- get api("/projects/#{project_id}/repository/commits?since=#{after.utc.iso8601}", user)
+ get api("/projects/#{project_id}/repository/commits?since=#{after.utc.iso8601}", user)
- expect(json_response.size).to eq 2
- expect(json_response.first["id"]).to eq(commits.first.id)
- expect(json_response.second["id"]).to eq(commits.second.id)
- end
+ expect(json_response.size).to eq 2
+ expect(json_response.first["id"]).to eq(commits.first.id)
+ expect(json_response.second["id"]).to eq(commits.second.id)
+ end
- it 'include correct pagination headers' do
- commits = project.repository.commits("master", limit: 2)
- after = commits.second.created_at
+ it 'include correct pagination headers' do
+ commits = project.repository.commits("master", limit: 2)
+ after = commits.second.created_at
- get api("/projects/#{project_id}/repository/commits?since=#{after.utc.iso8601}", user)
+ get api("/projects/#{project_id}/repository/commits?since=#{after.utc.iso8601}", user)
- expect(response).to include_limited_pagination_headers
- expect(response.headers['X-Page']).to eql('1')
- end
- end
+ expect(response).to include_limited_pagination_headers
+ expect(response.headers['X-Page']).to eql('1')
+ end
+ end
- context "until optional parameter" do
- it "returns project commits until provided parameter" do
- commits = project.repository.commits("master", limit: 20)
- before = commits.second.created_at
+ context "until optional parameter" do
+ it "returns project commits until provided parameter" do
+ commits = project.repository.commits("master", limit: 20)
+ before = commits.second.created_at
- get api("/projects/#{project_id}/repository/commits?until=#{before.utc.iso8601}", user)
+ get api("/projects/#{project_id}/repository/commits?until=#{before.utc.iso8601}", user)
- if commits.size == 20
- expect(json_response.size).to eq(20)
- else
- expect(json_response.size).to eq(commits.size - 1)
- end
+ if commits.size == 20
+ expect(json_response.size).to eq(20)
+ else
+ expect(json_response.size).to eq(commits.size - 1)
+ end
- expect(json_response.first["id"]).to eq(commits.second.id)
- expect(json_response.second["id"]).to eq(commits.third.id)
- end
+ expect(json_response.first["id"]).to eq(commits.second.id)
+ expect(json_response.second["id"]).to eq(commits.third.id)
+ end
- it 'include correct pagination headers' do
- commits = project.repository.commits("master", limit: 2)
- before = commits.second.created_at
+ it 'include correct pagination headers' do
+ commits = project.repository.commits("master", limit: 2)
+ before = commits.second.created_at
- get api("/projects/#{project_id}/repository/commits?until=#{before.utc.iso8601}", user)
+ get api("/projects/#{project_id}/repository/commits?until=#{before.utc.iso8601}", user)
- expect(response).to include_limited_pagination_headers
- expect(response.headers['X-Page']).to eql('1')
- end
- end
+ expect(response).to include_limited_pagination_headers
+ expect(response.headers['X-Page']).to eql('1')
+ end
+ end
- context "invalid xmlschema date parameters" do
- it "returns an invalid parameter error message" do
- get api("/projects/#{project_id}/repository/commits?since=invalid-date", user)
+ context "invalid xmlschema date parameters" do
+ it "returns an invalid parameter error message" do
+ get api("/projects/#{project_id}/repository/commits?since=invalid-date", user)
- expect(response).to have_gitlab_http_status(:bad_request)
- expect(json_response['error']).to eq('since is invalid')
- end
- end
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['error']).to eq('since is invalid')
+ end
+ end
- context "with empty ref_name parameter" do
- let(:route) { "/projects/#{project_id}/repository/commits?ref_name=" }
+ context "with empty ref_name parameter" do
+ let(:route) { "/projects/#{project_id}/repository/commits?ref_name=" }
- it_behaves_like 'project commits'
- end
+ it_behaves_like 'project commits'
+ end
- context 'when repository does not exist' do
- let(:project) { create(:project, creator: user, path: 'my.project') }
+ context 'when repository does not exist' do
+ let(:project) { create(:project, creator: user, path: 'my.project') }
- it_behaves_like '404 response' do
- let(:request) { get api(route, current_user) }
- let(:message) { '404 Repository Not Found' }
- end
- end
+ it_behaves_like '404 response' do
+ let(:request) { get api(route, current_user) }
+ let(:message) { '404 Repository Not Found' }
+ end
+ end
- context "path optional parameter" do
- it "returns project commits matching provided path parameter" do
- path = 'files/ruby/popen.rb'
+ context "path optional parameter" do
+ it "returns project commits matching provided path parameter" do
+ path = 'files/ruby/popen.rb'
- get api("/projects/#{project_id}/repository/commits?path=#{path}", user)
+ get api("/projects/#{project_id}/repository/commits?path=#{path}", user)
- expect(json_response.size).to eq(3)
- expect(json_response.first["id"]).to eq("570e7b2abdd848b95f2f578043fc23bd6f6fd24d")
- expect(response).to include_limited_pagination_headers
- end
+ expect(json_response.size).to eq(3)
+ expect(json_response.first["id"]).to eq("570e7b2abdd848b95f2f578043fc23bd6f6fd24d")
+ expect(response).to include_limited_pagination_headers
+ end
- it 'include correct pagination headers' do
- path = 'files/ruby/popen.rb'
+ it 'include correct pagination headers' do
+ path = 'files/ruby/popen.rb'
- get api("/projects/#{project_id}/repository/commits?path=#{path}", user)
+ get api("/projects/#{project_id}/repository/commits?path=#{path}", user)
- expect(response).to include_limited_pagination_headers
- expect(response.headers['X-Page']).to eql('1')
- end
- end
+ expect(response).to include_limited_pagination_headers
+ expect(response.headers['X-Page']).to eql('1')
+ end
+ end
- context 'all optional parameter' do
- it 'returns all project commits' do
- expected_commit_ids = project.repository.commits(nil, all: true, limit: 50).map(&:id)
+ context 'all optional parameter' do
+ it 'returns all project commits' do
+ expected_commit_ids = project.repository.commits(nil, all: true, limit: 50).map(&:id)
- get api("/projects/#{project_id}/repository/commits?all=true&per_page=50", user)
+ get api("/projects/#{project_id}/repository/commits?all=true&per_page=50", user)
- commit_ids = json_response.map { |c| c['id'] }
+ commit_ids = json_response.map { |c| c['id'] }
- expect(response).to include_limited_pagination_headers
- expect(commit_ids).to eq(expected_commit_ids)
- expect(response.headers['X-Page']).to eql('1')
- end
- end
+ expect(response).to include_limited_pagination_headers
+ expect(commit_ids).to eq(expected_commit_ids)
+ expect(response.headers['X-Page']).to eql('1')
+ end
+ end
- context 'first_parent optional parameter' do
- it 'returns all first_parent commits' do
- expected_commit_ids = project.repository.commits(SeedRepo::Commit::ID, limit: 50, first_parent: true).map(&:id)
+ context 'first_parent optional parameter' do
+ it 'returns all first_parent commits' do
+ expected_commit_ids = project.repository.commits(SeedRepo::Commit::ID, limit: 50, first_parent: true).map(&:id)
- get api("/projects/#{project_id}/repository/commits?per_page=50", user), params: { ref_name: SeedRepo::Commit::ID, first_parent: 'true' }
+ get api("/projects/#{project_id}/repository/commits?per_page=50", user), params: { ref_name: SeedRepo::Commit::ID, first_parent: 'true' }
- commit_ids = json_response.map { |c| c['id'] }
+ commit_ids = json_response.map { |c| c['id'] }
- expect(response).to include_limited_pagination_headers
- expect(expected_commit_ids.size).to eq(12)
- expect(commit_ids).to eq(expected_commit_ids)
- end
- end
+ expect(response).to include_limited_pagination_headers
+ expect(expected_commit_ids.size).to eq(12)
+ expect(commit_ids).to eq(expected_commit_ids)
+ end
+ end
- context 'with_stats optional parameter' do
- let(:project) { create(:project, :public, :repository) }
+ context 'with_stats optional parameter' do
+ let(:project) { create(:project, :public, :repository) }
- it_behaves_like 'project commits', schema: 'public_api/v4/commits_with_stats' do
- let(:route) { "/projects/#{project_id}/repository/commits?with_stats=true" }
+ it_behaves_like 'project commits', schema: 'public_api/v4/commits_with_stats' do
+ let(:route) { "/projects/#{project_id}/repository/commits?with_stats=true" }
- it 'include commits details' do
- commit = project.repository.commit
- get api(route, current_user)
+ it 'include commits details' do
+ commit = project.repository.commit
+ get api(route, current_user)
- expect(json_response.first['stats']['additions']).to eq(commit.stats.additions)
- expect(json_response.first['stats']['deletions']).to eq(commit.stats.deletions)
- expect(json_response.first['stats']['total']).to eq(commit.stats.total)
+ expect(json_response.first['stats']['additions']).to eq(commit.stats.additions)
+ expect(json_response.first['stats']['deletions']).to eq(commit.stats.deletions)
+ expect(json_response.first['stats']['total']).to eq(commit.stats.total)
+ end
+ end
end
- end
- end
- context 'with pagination params' do
- let(:page) { 1 }
- let(:per_page) { 5 }
- let(:ref_name) { 'master' }
- let(:request) do
- get api("/projects/#{project_id}/repository/commits?page=#{page}&per_page=#{per_page}&ref_name=#{ref_name}", user)
- end
+ context 'with pagination params' do
+ let(:page) { 1 }
+ let(:per_page) { 5 }
+ let(:ref_name) { 'master' }
+ let(:request) do
+ get api("/projects/#{project_id}/repository/commits?page=#{page}&per_page=#{per_page}&ref_name=#{ref_name}", user)
+ end
- it 'returns correct headers' do
- request
+ it 'returns correct headers' do
+ request
- expect(response).to include_limited_pagination_headers
- expect(response.headers['Link']).to match(/page=1&per_page=5/)
- expect(response.headers['Link']).to match(/page=2&per_page=5/)
- end
+ expect(response).to include_limited_pagination_headers
+ expect(response.headers['Link']).to match(/page=1&per_page=5/)
+ expect(response.headers['Link']).to match(/page=2&per_page=5/)
+ end
- context 'viewing the first page' do
- it 'returns the first 5 commits' do
- request
+ context 'viewing the first page' do
+ it 'returns the first 5 commits' do
+ request
- commit = project.repository.commit
+ commit = project.repository.commit
- expect(json_response.size).to eq(per_page)
- expect(json_response.first['id']).to eq(commit.id)
- expect(response.headers['X-Page']).to eq('1')
- end
- end
+ expect(json_response.size).to eq(per_page)
+ expect(json_response.first['id']).to eq(commit.id)
+ expect(response.headers['X-Page']).to eq('1')
+ end
+ end
- context 'viewing the third page' do
- let(:page) { 3 }
+ context 'viewing the third page' do
+ let(:page) { 3 }
- it 'returns the third 5 commits' do
- request
+ it 'returns the third 5 commits' do
+ request
- commit = project.repository.commits('HEAD', limit: per_page, offset: (page - 1) * per_page).first
+ commit = project.repository.commits('HEAD', limit: per_page, offset: (page - 1) * per_page).first
- expect(json_response.size).to eq(per_page)
- expect(json_response.first['id']).to eq(commit.id)
- expect(response.headers['X-Page']).to eq('3')
- end
- end
+ expect(json_response.size).to eq(per_page)
+ expect(json_response.first['id']).to eq(commit.id)
+ expect(response.headers['X-Page']).to eq('3')
+ end
+ end
- context 'when pagination params are invalid' do
- let_it_be(:project) { create(:project, :repository) }
+ context 'when pagination params are invalid' do
+ let_it_be(:project) { create(:project, :repository) }
- using RSpec::Parameterized::TableSyntax
+ using RSpec::Parameterized::TableSyntax
- where(:page, :per_page, :error_message) do
- 0 | nil | 'page does not have a valid value'
- -1 | nil | 'page does not have a valid value'
- 'a' | nil | 'page is invalid'
- nil | 0 | 'per_page does not have a valid value'
- nil | -1 | 'per_page does not have a valid value'
- nil | 'a' | 'per_page is invalid'
- end
+ where(:page, :per_page, :error_message) do
+ 0 | nil | 'page does not have a valid value'
+ -1 | nil | 'page does not have a valid value'
+ 'a' | nil | 'page is invalid'
+ nil | 0 | 'per_page does not have a valid value'
+ nil | -1 | 'per_page does not have a valid value'
+ nil | 'a' | 'per_page is invalid'
+ end
- with_them do
- it 'returns 400 response' do
- request
+ with_them do
+ it 'returns 400 response' do
+ request
- expect(response).to have_gitlab_http_status(:bad_request)
- expect(json_response['error']).to eq(error_message)
- end
- end
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['error']).to eq(error_message)
+ end
+ end
- context 'when FF is off' do
- before do
- stub_feature_flags(only_positive_pagination_values: false)
- end
+ context 'when FF is off' do
+ before do
+ stub_feature_flags(only_positive_pagination_values: false)
+ end
- where(:page, :per_page, :error_message, :status) do
- 0 | nil | nil | :success
- -10 | nil | nil | :internal_server_error
- 'a' | nil | 'page is invalid' | :bad_request
- nil | 0 | 'per_page has a value not allowed' | :bad_request
- nil | -1 | nil | :success
- nil | 'a' | 'per_page is invalid' | :bad_request
- end
+ where(:page, :per_page, :error_message, :status) do
+ 0 | nil | nil | :success
+ -10 | nil | nil | :internal_server_error
+ 'a' | nil | 'page is invalid' | :bad_request
+ nil | 0 | 'per_page has a value not allowed' | :bad_request
+ nil | -1 | nil | :success
+ nil | 'a' | 'per_page is invalid' | :bad_request
+ end
- with_them do
- it 'returns a response' do
- request
+ with_them do
+ it 'returns a response' do
+ request
- expect(response).to have_gitlab_http_status(status)
+ expect(response).to have_gitlab_http_status(status)
- if error_message
- expect(json_response['error']).to eq(error_message)
+ if error_message
+ expect(json_response['error']).to eq(error_message)
+ end
+ end
end
end
end
end
- end
- end
- context 'with order parameter' do
- let(:route) { "/projects/#{project_id}/repository/commits?ref_name=0031876&per_page=6&order=#{order}" }
+ context 'with order parameter' do
+ let(:route) { "/projects/#{project_id}/repository/commits?ref_name=0031876&per_page=6&order=#{order}" }
- context 'set to topo' do
- let(:order) { 'topo' }
+ context 'set to topo' do
+ let(:order) { 'topo' }
- # git log --graph -n 6 --pretty=format:"%h" --topo-order 0031876
- # * 0031876
- # |\
- # | * 48ca272
- # | * 335bc94
- # * | bf6e164
- # * | 9d526f8
- # |/
- # * 1039376
- it 'returns project commits ordered by topo order' do
- commits = project.repository.commits("0031876", limit: 6, order: 'topo')
+ # git log --graph -n 6 --pretty=format:"%h" --topo-order 0031876
+ # * 0031876
+ # |\
+ # | * 48ca272
+ # | * 335bc94
+ # * | bf6e164
+ # * | 9d526f8
+ # |/
+ # * 1039376
+ it 'returns project commits ordered by topo order' do
+ commits = project.repository.commits("0031876", limit: 6, order: 'topo')
- get api(route, current_user)
+ get api(route, current_user)
- expect(json_response.size).to eq(6)
- expect(json_response.map { |entry| entry["id"] }).to eq(commits.map(&:id))
- end
- end
+ expect(json_response.size).to eq(6)
+ expect(json_response.map { |entry| entry["id"] }).to eq(commits.map(&:id))
+ end
+ end
+
+ context 'set to default' do
+ let(:order) { 'default' }
+
+ # git log --graph -n 6 --pretty=format:"%h" --date-order 0031876
+ # * 0031876
+ # |\
+ # * | bf6e164
+ # | * 48ca272
+ # * | 9d526f8
+ # | * 335bc94
+ # |/
+ # * 1039376
+ it 'returns project commits ordered by default order' do
+ commits = project.repository.commits("0031876", limit: 6, order: 'default')
+
+ get api(route, current_user)
+
+ expect(json_response.size).to eq(6)
+ expect(json_response.map { |entry| entry["id"] }).to eq(commits.map(&:id))
+ end
+ end
- context 'set to default' do
- let(:order) { 'default' }
+ context 'set to an invalid parameter' do
+ let(:order) { 'invalid' }
- # git log --graph -n 6 --pretty=format:"%h" --date-order 0031876
- # * 0031876
- # |\
- # * | bf6e164
- # | * 48ca272
- # * | 9d526f8
- # | * 335bc94
- # |/
- # * 1039376
- it 'returns project commits ordered by default order' do
- commits = project.repository.commits("0031876", limit: 6, order: 'default')
+ it_behaves_like '400 response' do
+ let(:request) { get api(route, current_user) }
+ end
+ end
+ end
- get api(route, current_user)
+ context 'with the optional trailers parameter' do
+ it 'includes the Git trailers' do
+ get api("/projects/#{project_id}/repository/commits?ref_name=6d394385cf567f80a8fd85055db1ab4c5295806f&trailers=true", current_user)
- expect(json_response.size).to eq(6)
- expect(json_response.map { |entry| entry["id"] }).to eq(commits.map(&:id))
+ commit = json_response[0]
+
+ expect(commit['trailers']).to eq(
+ 'Signed-off-by' => 'Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>'
+ )
+ end
end
end
+ end
- context 'set to an invalid parameter' do
- let(:order) { 'invalid' }
+ context 'when user is an inherited member from the group' do
+ context 'when project is public with private repository' do
+ let(:project) { create(:project, :public, :repository, :repository_private, group: group) }
- it_behaves_like '400 response' do
- let(:request) { get api(route, current_user) }
+ context 'and user is a guest' do
+ let(:current_user) { inherited_guest }
+
+ it_behaves_like 'project commits'
end
end
- end
- context 'with the optional trailers parameter' do
- it 'includes the Git trailers' do
- get api("/projects/#{project_id}/repository/commits?ref_name=6d394385cf567f80a8fd85055db1ab4c5295806f&trailers=true", current_user)
+ context 'when project is private' do
+ let(:project) { create(:project, :private, :repository, group: group) }
- commit = json_response[0]
+ context 'and user is a guest' do
+ let(:current_user) { inherited_guest }
- expect(commit['trailers']).to eq(
- 'Signed-off-by' => 'Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>'
- )
+ it_behaves_like '404 response' do
+ let(:request) { get api(route) }
+ let(:message) { '404 Project Not Found' }
+ end
+ end
end
end
end
@@ -382,6 +414,9 @@ RSpec.describe API::Commits do
end
describe 'create' do
+ let_it_be(:sequencer) { FactoryBot::Sequence.new(:new_file_path) { |n| "files/test/#{n}.rb" } }
+
+ let(:new_file_path) { sequencer.next }
let(:message) { 'Created a new file with a very very looooooooooooooooooooooooooooooooooooooooooooooong commit message' }
let(:invalid_c_params) do
{
@@ -404,7 +439,7 @@ RSpec.describe API::Commits do
actions: [
{
action: 'create',
- file_path: 'foo/bar/baz.txt',
+ file_path: new_file_path,
content: 'puts 8'
}
]
@@ -418,7 +453,7 @@ RSpec.describe API::Commits do
actions: [
{
action: 'create',
- file_path: 'foo/bar/baz.txt',
+ file_path: new_file_path,
content: 'puts 🦊'
}
]
@@ -466,11 +501,57 @@ RSpec.describe API::Commits do
end
context 'a new file in project repo' do
- before do
- post api(url, user), params: valid_c_params
+ context 'when user is a direct project member' do
+ before do
+ post api(url, user), params: valid_c_params
+ end
+
+ it_behaves_like 'successfully creates the commit'
end
- it_behaves_like "successfully creates the commit"
+ context 'when user is an inherited member from the group' do
+ context 'when project is public with private repository' do
+ let(:project) { create(:project, :public, :repository, :repository_private, group: group) }
+
+ context 'and user is a guest' do
+ it_behaves_like '403 response' do
+ let(:request) { post api(url, inherited_guest), params: valid_c_params }
+ let(:message) { '403 Forbidden' }
+ end
+ end
+ end
+
+ context 'when project is private' do
+ let(:project) { create(:project, :private, :repository, group: group) }
+
+ context 'and user is a guest' do
+ it_behaves_like '403 response' do
+ let(:request) { post api(url, inherited_guest), params: valid_c_params }
+ let(:message) { '403 Forbidden' }
+ end
+ end
+ end
+ end
+ end
+
+ context 'when repository is empty' do
+ let!(:project) { create(:project, :empty_repo) }
+
+ context 'when params are valid' do
+ before do
+ post api(url, user), params: valid_c_params
+ end
+
+ it_behaves_like "successfully creates the commit"
+ end
+
+ context 'when branch name is invalid' do
+ before do
+ post api(url, user), params: valid_c_params.merge(branch: 'wrong:name')
+ end
+
+ it { expect(response).to have_gitlab_http_status(:bad_request) }
+ end
end
context 'a new file with utf8 chars in project repo' do
@@ -882,6 +963,7 @@ RSpec.describe API::Commits do
end
describe 'multiple operations' do
+ let(:project) { create(:project, :repository, creator: user, path: 'my.project') }
let(:message) { 'Multiple actions' }
let(:invalid_mo_params) do
{
@@ -951,17 +1033,11 @@ RSpec.describe API::Commits do
}
end
- it 'are committed as one in project repo' do
+ it 'is committed as one in project repo and includes stats' do
post api(url, user), params: valid_mo_params
expect(response).to have_gitlab_http_status(:created)
expect(json_response['title']).to eq(message)
- end
-
- it 'includes the commit stats' do
- post api(url, user), params: valid_mo_params
-
- expect(response).to have_gitlab_http_status(:created)
expect(json_response).to include 'stats'
end
@@ -1047,7 +1123,8 @@ RSpec.describe API::Commits do
end
describe 'GET /projects/:id/repository/commits/:sha/refs' do
- let(:project) { create(:project, :public, :repository) }
+ let_it_be(:project) { create(:project, :public, :repository) }
+
let(:tag) { project.repository.find_tag('v1.1.0') }
let(:commit_id) { tag.dereferenced_target.id }
let(:route) { "/projects/#{project_id}/repository/commits/#{commit_id}/refs" }
@@ -1062,6 +1139,8 @@ RSpec.describe API::Commits do
end
context 'when repository is disabled' do
+ let(:project) { create(:project, :repository, creator: user, path: 'my.project') }
+
include_context 'disabled repository'
it_behaves_like '404 response' do
@@ -1151,6 +1230,8 @@ RSpec.describe API::Commits do
end
context 'when repository is disabled' do
+ let(:project) { create(:project, :repository, creator: user, path: 'my.project') }
+
include_context 'disabled repository'
it_behaves_like '404 response' do
@@ -1192,8 +1273,14 @@ RSpec.describe API::Commits do
end
shared_examples_for 'ref with unaccessible pipeline' do
- let!(:pipeline) do
- create(:ci_empty_pipeline, project: project, status: :created, source: :push, ref: 'master', sha: commit.sha, protected: false)
+ let(:pipeline) do
+ create(:ci_empty_pipeline,
+ project: project,
+ status: :created,
+ source: :push,
+ ref: 'master',
+ sha: commit.sha,
+ protected: false)
end
it 'does not include last_pipeline' do
@@ -1231,7 +1318,7 @@ RSpec.describe API::Commits do
end
context 'when unauthenticated', 'and project is public' do
- let(:project) { create(:project, :public, :repository) }
+ let_it_be_with_reload(:project) { create(:project, :public, :repository) }
it_behaves_like 'ref commit'
it_behaves_like 'ref with pipeline'
@@ -1261,6 +1348,7 @@ RSpec.describe API::Commits do
context 'when builds are disabled' do
before do
project
+ .reload
.project_feature
.update!(builds_access_level: ProjectFeature::DISABLED)
end
@@ -1312,7 +1400,7 @@ RSpec.describe API::Commits do
context 'with private builds' do
before do
- project.project_feature.update!(builds_access_level: ProjectFeature::PRIVATE)
+ project.reload.project_feature.update!(builds_access_level: ProjectFeature::PRIVATE)
end
it_behaves_like 'ref with pipeline'
@@ -1338,8 +1426,8 @@ RSpec.describe API::Commits do
end
context 'when authenticated', 'as non_member and project is public' do
- let(:current_user) { create(:user) }
- let(:project) { create(:project, :public, :repository) }
+ let_it_be(:current_user) { create(:user) }
+ let_it_be_with_reload(:project) { create(:project, :public, :repository) }
it_behaves_like 'ref with pipeline'
@@ -1392,6 +1480,8 @@ RSpec.describe API::Commits do
end
context 'when repository is disabled' do
+ let(:project) { create(:project, :repository, creator: user, path: 'my.project') }
+
include_context 'disabled repository'
it_behaves_like '404 response' do
@@ -1401,7 +1491,7 @@ RSpec.describe API::Commits do
end
context 'when unauthenticated', 'and project is public' do
- let(:project) { create(:project, :public, :repository) }
+ let_it_be(:project) { create(:project, :public, :repository) }
it_behaves_like 'ref diff'
end
@@ -1491,6 +1581,8 @@ RSpec.describe API::Commits do
end
context 'when repository is disabled' do
+ let(:project) { create(:project, :repository, creator: user, path: 'my.project') }
+
include_context 'disabled repository'
it_behaves_like '404 response' do
@@ -1500,7 +1592,7 @@ RSpec.describe API::Commits do
end
context 'when unauthenticated', 'and project is public' do
- let(:project) { create(:project, :public, :repository) }
+ let_it_be(:project) { create(:project, :public, :repository) }
it_behaves_like 'ref comments'
end
@@ -1589,6 +1681,7 @@ RSpec.describe API::Commits do
end
describe 'POST :id/repository/commits/:sha/cherry_pick' do
+ let(:project) { create(:project, :repository, creator: user, path: 'my.project') }
let(:commit) { project.commit('7d3b0f7cff5f37573aea97cebfd5692ea1689924') }
let(:commit_id) { commit.id }
let(:branch) { 'master' }
@@ -1626,6 +1719,8 @@ RSpec.describe API::Commits do
end
context 'when repository is disabled' do
+ let(:project) { create(:project, :repository, creator: user, path: 'my.project') }
+
include_context 'disabled repository'
it_behaves_like '404 response' do
@@ -1635,7 +1730,7 @@ RSpec.describe API::Commits do
end
context 'when unauthenticated', 'and project is public' do
- let(:project) { create(:project, :public, :repository) }
+ let_it_be(:project) { create(:project, :public, :repository) }
it_behaves_like '403 response' do
let(:request) { post api(route), params: { branch: 'master' } }
@@ -1774,6 +1869,7 @@ RSpec.describe API::Commits do
end
describe 'POST :id/repository/commits/:sha/revert' do
+ let(:project) { create(:project, :repository, creator: user, path: 'my.project') }
let(:commit_id) { 'b83d6e391c22777fca1ed3012fce84f633d7fed0' }
let(:commit) { project.commit(commit_id) }
let(:branch) { 'master' }
@@ -1814,7 +1910,7 @@ RSpec.describe API::Commits do
end
context 'when unauthenticated', 'and project is public' do
- let(:project) { create(:project, :public, :repository) }
+ let_it_be(:project) { create(:project, :public, :repository) }
it_behaves_like '403 response' do
let(:request) { post api(route), params: { branch: branch } }
@@ -1921,6 +2017,7 @@ RSpec.describe API::Commits do
end
describe 'POST /projects/:id/repository/commits/:sha/comments' do
+ let(:project) { create(:project, :repository, :private) }
let(:commit) { project.repository.commit }
let(:commit_id) { commit.id }
let(:note) { 'My comment' }
@@ -1941,6 +2038,8 @@ RSpec.describe API::Commits do
end
context 'when repository is disabled' do
+ let(:project) { create(:project, :repository, creator: user, path: 'my.project') }
+
include_context 'disabled repository'
it_behaves_like '404 response' do
@@ -1950,7 +2049,7 @@ RSpec.describe API::Commits do
end
context 'when unauthenticated', 'and project is public' do
- let(:project) { create(:project, :public, :repository) }
+ let_it_be(:project) { create(:project, :public, :repository) }
it_behaves_like '400 response' do
let(:request) { post api(route), params: { note: 'My comment' } }
@@ -1970,12 +2069,13 @@ RSpec.describe API::Commits do
it_behaves_like 'ref new comment'
it 'returns the inline comment' do
- post api(route, current_user), params: { note: 'My comment', path: project.repository.commit.raw_diffs.first.new_path, line: 1, line_type: 'new' }
+ path = project.repository.commit.raw_diffs.first.new_path
+ post api(route, current_user), params: { note: 'My comment', path: path, line: 1, line_type: 'new' }
expect(response).to have_gitlab_http_status(:created)
expect(response).to match_response_schema('public_api/v4/commit_note')
expect(json_response['note']).to eq('My comment')
- expect(json_response['path']).to eq(project.repository.commit.raw_diffs.first.new_path)
+ expect(json_response['path']).to eq(path)
expect(json_response['line']).to eq(1)
expect(json_response['line_type']).to eq('new')
end
@@ -2050,7 +2150,8 @@ RSpec.describe API::Commits do
end
describe 'GET /projects/:id/repository/commits/:sha/merge_requests' do
- let(:project) { create(:project, :repository, :private) }
+ let_it_be(:project) { create(:project, :repository, :private) }
+
let(:merged_mr) { create(:merge_request, source_project: project, source_branch: 'master', target_branch: 'feature') }
let(:commit) { merged_mr.merge_request_diff.commits.last }
@@ -2082,7 +2183,8 @@ RSpec.describe API::Commits do
end
context 'public project' do
- let(:project) { create(:project, :repository, :public, :merge_requests_private) }
+ let_it_be(:project) { create(:project, :repository, :public, :merge_requests_private) }
+
let(:non_member) { create(:user) }
it 'responds 403 when only members are allowed to read merge requests' do
diff --git a/spec/requests/api/conan_instance_packages_spec.rb b/spec/requests/api/conan_instance_packages_spec.rb
index e4747e0eb99..b343e0cfc97 100644
--- a/spec/requests/api/conan_instance_packages_spec.rb
+++ b/spec/requests/api/conan_instance_packages_spec.rb
@@ -103,8 +103,7 @@ RSpec.describe API::ConanInstancePackages do
context 'file download endpoints' do
include_context 'conan file download endpoints'
- describe 'GET /api/v4/packages/conan/v1/files/:package_name/package_version/:package_username/:package_channel/
-:recipe_revision/export/:file_name' do
+ describe 'GET /api/v4/packages/conan/v1/files/:package_name/package_version/:package_username/:package_channel/:recipe_revision/export/:file_name' do
subject do
get api("/packages/conan/v1/files/#{recipe_path}/#{metadata.recipe_revision}/export/#{recipe_file.file_name}"),
headers: headers
@@ -114,8 +113,7 @@ RSpec.describe API::ConanInstancePackages do
it_behaves_like 'project not found by recipe'
end
- describe 'GET /api/v4/packages/conan/v1/files/:package_name/package_version/:package_username/:package_channel/
-:recipe_revision/package/:conan_package_reference/:package_revision/:file_name' do
+ describe 'GET /api/v4/packages/conan/v1/files/:package_name/package_version/:package_username/:package_channel/:recipe_revision/package/:conan_package_reference/:package_revision/:file_name' do
subject do
get api("/packages/conan/v1/files/#{recipe_path}/#{metadata.recipe_revision}/package/#{metadata.conan_package_reference}/#{metadata.package_revision}/#{package_file.file_name}"),
headers: headers
diff --git a/spec/requests/api/conan_project_packages_spec.rb b/spec/requests/api/conan_project_packages_spec.rb
index 48e36b55a68..4e6af9942ef 100644
--- a/spec/requests/api/conan_project_packages_spec.rb
+++ b/spec/requests/api/conan_project_packages_spec.rb
@@ -102,8 +102,7 @@ RSpec.describe API::ConanProjectPackages do
context 'file download endpoints', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/326194' do
include_context 'conan file download endpoints'
- describe 'GET /api/v4/projects/:id/packages/conan/v1/files/:package_name/package_version/:package_username/:package_channel/
-:recipe_revision/export/:file_name' do
+ describe 'GET /api/v4/projects/:id/packages/conan/v1/files/:package_name/package_version/:package_username/:package_channel/:recipe_revision/export/:file_name' do
subject do
get api("/projects/#{project_id}/packages/conan/v1/files/#{recipe_path}/#{metadata.recipe_revision}/export/#{recipe_file.file_name}"),
headers: headers
@@ -113,8 +112,7 @@ RSpec.describe API::ConanProjectPackages do
it_behaves_like 'project not found by project id'
end
- describe 'GET /api/v4/projects/:id/packages/conan/v1/files/:package_name/package_version/:package_username/:package_channel/
-:recipe_revision/package/:conan_package_reference/:package_revision/:file_name' do
+ describe 'GET /api/v4/projects/:id/packages/conan/v1/files/:package_name/package_version/:package_username/:package_channel/:recipe_revision/package/:conan_package_reference/:package_revision/:file_name' do
subject do
get api("/projects/#{project_id}/packages/conan/v1/files/#{recipe_path}/#{metadata.recipe_revision}/package/#{metadata.conan_package_reference}/#{metadata.package_revision}/#{package_file.file_name}"),
headers: headers
diff --git a/spec/requests/api/debian_group_packages_spec.rb b/spec/requests/api/debian_group_packages_spec.rb
index d881d4350fb..9dbb75becf8 100644
--- a/spec/requests/api/debian_group_packages_spec.rb
+++ b/spec/requests/api/debian_group_packages_spec.rb
@@ -36,12 +36,42 @@ RSpec.describe API::DebianGroupPackages do
it_behaves_like 'Debian packages read endpoint', 'GET', :success, /Description: This is an incomplete Packages file/
end
+ describe 'GET groups/:id/-/packages/debian/dists/*distribution/:component/binary-:architecture/by-hash/SHA256/:file_sha256' do
+ let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution.codename}/#{component.name}/binary-#{architecture.name}/by-hash/SHA256/#{component_file_older_sha256.file_sha256}" }
+
+ it_behaves_like 'Debian packages read endpoint', 'GET', :success, /^Other SHA256$/
+ end
+
+ describe 'GET groups/:id/-/packages/debian/dists/*distribution/:component/source/Sources' do
+ let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution.codename}/#{component.name}/source/Sources" }
+
+ it_behaves_like 'Debian packages read endpoint', 'GET', :success, /Description: This is an incomplete Sources file/
+ end
+
+ describe 'GET groups/:id/-/packages/debian/dists/*distribution/:component/source/by-hash/SHA256/:file_sha256' do
+ let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution.codename}/#{component.name}/source/by-hash/SHA256/#{component_file_sources_older_sha256.file_sha256}" }
+
+ it_behaves_like 'Debian packages read endpoint', 'GET', :success, /^Other SHA256$/
+ end
+
+ describe 'GET groups/:id/-/packages/debian/dists/*distribution/:component/debian-installer/binary-:architecture/Packages' do
+ let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution.codename}/#{component.name}/debian-installer/binary-#{architecture.name}/Packages" }
+
+ it_behaves_like 'Debian packages read endpoint', 'GET', :success, /Description: This is an incomplete D-I Packages file/
+ end
+
+ describe 'GET groups/:id/-/packages/debian/dists/*distribution/:component/debian-installer/binary-:architecture/by-hash/SHA256/:file_sha256' do
+ let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution.codename}/#{component.name}/debian-installer/binary-#{architecture.name}/by-hash/SHA256/#{component_file_di_older_sha256.file_sha256}" }
+
+ it_behaves_like 'Debian packages read endpoint', 'GET', :success, /^Other SHA256$/
+ end
+
describe 'GET groups/:id/-/packages/debian/pool/:codename/:project_id/:letter/:package_name/:package_version/:file_name' do
+ using RSpec::Parameterized::TableSyntax
+
let(:url) { "/groups/#{container.id}/-/packages/debian/pool/#{package.debian_distribution.codename}/#{project.id}/#{letter}/#{package.name}/#{package.version}/#{file_name}" }
let(:file_name) { params[:file_name] }
- using RSpec::Parameterized::TableSyntax
-
where(:file_name, :success_body) do
'sample_1.2.3~alpha2.tar.xz' | /^.7zXZ/
'sample_1.2.3~alpha2.dsc' | /^Format: 3.0 \(native\)/
@@ -53,6 +83,12 @@ RSpec.describe API::DebianGroupPackages do
with_them do
it_behaves_like 'Debian packages read endpoint', 'GET', :success, params[:success_body]
+
+ context 'for bumping last downloaded at' do
+ include_context 'Debian repository access', :public, :developer, :basic do
+ it_behaves_like 'bumping the package last downloaded at field'
+ end
+ end
end
end
end
diff --git a/spec/requests/api/debian_project_packages_spec.rb b/spec/requests/api/debian_project_packages_spec.rb
index bd68bf912e1..6bef669cb3a 100644
--- a/spec/requests/api/debian_project_packages_spec.rb
+++ b/spec/requests/api/debian_project_packages_spec.rb
@@ -36,12 +36,42 @@ RSpec.describe API::DebianProjectPackages do
it_behaves_like 'Debian packages read endpoint', 'GET', :success, /Description: This is an incomplete Packages file/
end
+ describe 'GET projects/:id/packages/debian/dists/*distribution/:component/binary-:architecture/by-hash/SHA256/:file_sha256' do
+ let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/#{component.name}/binary-#{architecture.name}/by-hash/SHA256/#{component_file_older_sha256.file_sha256}" }
+
+ it_behaves_like 'Debian packages read endpoint', 'GET', :success, /^Other SHA256$/
+ end
+
+ describe 'GET projects/:id/packages/debian/dists/*distribution/source/Sources' do
+ let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/#{component.name}/source/Sources" }
+
+ it_behaves_like 'Debian packages read endpoint', 'GET', :success, /Description: This is an incomplete Sources file/
+ end
+
+ describe 'GET projects/:id/packages/debian/dists/*distribution/source/by-hash/SHA256/:file_sha256' do
+ let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/#{component.name}/source/by-hash/SHA256/#{component_file_sources_older_sha256.file_sha256}" }
+
+ it_behaves_like 'Debian packages read endpoint', 'GET', :success, /^Other SHA256$/
+ end
+
+ describe 'GET projects/:id/packages/debian/dists/*distribution/:component/debian-installer/binary-:architecture/Packages' do
+ let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/#{component.name}/debian-installer/binary-#{architecture.name}/Packages" }
+
+ it_behaves_like 'Debian packages read endpoint', 'GET', :success, /Description: This is an incomplete D-I Packages file/
+ end
+
+ describe 'GET projects/:id/packages/debian/dists/*distribution/:component/debian-installer/binary-:architecture/by-hash/SHA256/:file_sha256' do
+ let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/#{component.name}/debian-installer/binary-#{architecture.name}/by-hash/SHA256/#{component_file_di_older_sha256.file_sha256}" }
+
+ it_behaves_like 'Debian packages read endpoint', 'GET', :success, /^Other SHA256$/
+ end
+
describe 'GET projects/:id/packages/debian/pool/:codename/:letter/:package_name/:package_version/:file_name' do
+ using RSpec::Parameterized::TableSyntax
+
let(:url) { "/projects/#{container.id}/packages/debian/pool/#{package.debian_distribution.codename}/#{letter}/#{package.name}/#{package.version}/#{file_name}" }
let(:file_name) { params[:file_name] }
- using RSpec::Parameterized::TableSyntax
-
where(:file_name, :success_body) do
'sample_1.2.3~alpha2.tar.xz' | /^.7zXZ/
'sample_1.2.3~alpha2.dsc' | /^Format: 3.0 \(native\)/
@@ -53,6 +83,12 @@ RSpec.describe API::DebianProjectPackages do
with_them do
it_behaves_like 'Debian packages read endpoint', 'GET', :success, params[:success_body]
+
+ context 'for bumping last downloaded at' do
+ include_context 'Debian repository access', :public, :developer, :basic do
+ it_behaves_like 'bumping the package last downloaded at field'
+ end
+ end
end
end
diff --git a/spec/requests/api/deployments_spec.rb b/spec/requests/api/deployments_spec.rb
index 24c3ee59c18..24e0e5d3180 100644
--- a/spec/requests/api/deployments_spec.rb
+++ b/spec/requests/api/deployments_spec.rb
@@ -14,9 +14,10 @@ RSpec.describe API::Deployments do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:production) { create(:environment, :production, project: project) }
let_it_be(:staging) { create(:environment, :staging, project: project) }
- let_it_be(:deployment_1) { create(:deployment, :success, project: project, environment: production, ref: 'master', created_at: Time.now, updated_at: Time.now) }
- let_it_be(:deployment_2) { create(:deployment, :success, project: project, environment: staging, ref: 'master', created_at: 1.day.ago, updated_at: 2.hours.ago) }
- let_it_be(:deployment_3) { create(:deployment, :success, project: project, environment: staging, ref: 'master', created_at: 2.days.ago, updated_at: 1.hour.ago) }
+ let_it_be(:build) { create(:ci_build, :success, project: project) }
+ let_it_be(:deployment_1) { create(:deployment, :success, project: project, environment: production, deployable: build, ref: 'master', created_at: Time.now, updated_at: Time.now) }
+ let_it_be(:deployment_2) { create(:deployment, :success, project: project, environment: staging, deployable: build, ref: 'master', created_at: 1.day.ago, updated_at: 2.hours.ago) }
+ let_it_be(:deployment_3) { create(:deployment, :success, project: project, environment: staging, deployable: build, ref: 'master', created_at: 2.days.ago, updated_at: 1.hour.ago) }
def perform_request(params = {})
get api("/projects/#{project.id}/deployments", user), params: params
@@ -104,7 +105,7 @@ RSpec.describe API::Deployments do
control_count = ActiveRecord::QueryRecorder.new { perform_request }.count
- create(:deployment, :success, project: project, iid: 21, ref: 'master')
+ create(:deployment, :success, project: project, deployable: build, iid: 21, ref: 'master')
expect { perform_request }.not_to exceed_query_limit(control_count)
end
diff --git a/spec/requests/api/feature_flags_spec.rb b/spec/requests/api/feature_flags_spec.rb
index a1aedc1d6b2..bf7eec167f5 100644
--- a/spec/requests/api/feature_flags_spec.rb
+++ b/spec/requests/api/feature_flags_spec.rb
@@ -365,8 +365,8 @@ RSpec.describe API::FeatureFlags do
describe 'PUT /projects/:id/feature_flags/:name' do
context 'with a version 2 feature flag' do
let!(:feature_flag) do
- create(:operations_feature_flag, :new_version_flag, project: project, active: true,
- name: 'feature1', description: 'old description')
+ create(:operations_feature_flag, :new_version_flag,
+ project: project, active: true, name: 'feature1', description: 'old description')
end
it 'returns a 404 if the feature flag does not exist' do
@@ -591,8 +591,8 @@ RSpec.describe API::FeatureFlags do
it 'deletes a feature flag strategy' do
strategy_a = create(:operations_strategy, feature_flag: feature_flag, name: 'default', parameters: {})
- strategy_b = create(:operations_strategy, feature_flag: feature_flag,
- name: 'userWithId', parameters: { userIds: 'userA,userB' })
+ strategy_b = create(:operations_strategy,
+ feature_flag: feature_flag, name: 'userWithId', parameters: { userIds: 'userA,userB' })
params = {
strategies: [{
id: strategy_a.id,
diff --git a/spec/requests/api/files_spec.rb b/spec/requests/api/files_spec.rb
index 06d22e7e218..e95a626b4aa 100644
--- a/spec/requests/api/files_spec.rb
+++ b/spec/requests/api/files_spec.rb
@@ -5,13 +5,21 @@ require 'spec_helper'
RSpec.describe API::Files do
include RepoHelpers
- let(:user) { create(:user) }
+ let_it_be(:group) { create(:group, :public) }
+ let_it_be_with_refind(:user) { create(:user) }
+ let_it_be(:inherited_guest) { create(:user) }
+ let_it_be(:inherited_reporter) { create(:user) }
+ let_it_be(:inherited_developer) { create(:user) }
+
let!(:project) { create(:project, :repository, namespace: user.namespace ) }
let(:guest) { create(:user) { |u| project.add_guest(u) } }
- let(:file_path) { "files%2Fruby%2Fpopen%2Erb" }
- let(:executable_file_path) { "files%2Fexecutables%2Fls" }
- let(:rouge_file_path) { "%2e%2e%2f" }
- let(:absolute_path) { "%2Fetc%2Fpasswd.rb" }
+ let(:file_path) { 'files%2Fruby%2Fpopen%2Erb' }
+ let(:file_name) { 'popen.rb' }
+ let(:last_commit_id) { '570e7b2abdd848b95f2f578043fc23bd6f6fd24d' }
+ let(:content_sha256) { 'c440cd09bae50c4632cc58638ad33c6aa375b6109d811e76a9cc3a613c1e8887' }
+ let(:executable_file_path) { 'files%2Fexecutables%2Fls' }
+ let(:invalid_file_path) { '%2e%2e%2f' }
+ let(:absolute_path) { '%2Fetc%2Fpasswd.rb' }
let(:invalid_file_message) { 'file_path should be a valid file path' }
let(:params) do
{
@@ -46,6 +54,12 @@ RSpec.describe API::Files do
fake_class.new
end
+ before_all do
+ group.add_guest(inherited_guest)
+ group.add_reporter(inherited_reporter)
+ group.add_developer(inherited_developer)
+ end
+
before do
project.add_developer(user)
end
@@ -70,8 +84,10 @@ RSpec.describe API::Files do
expect(helper.headers).to eq({ 'X-Gitlab-Test' => '1' })
end
- it 'raises exception if value is an Enumerable' do
- expect { helper.set_http_headers(test: [1]) }.to raise_error(ArgumentError)
+ context 'when value is an Enumerable' do
+ it 'raises an exception' do
+ expect { helper.set_http_headers(test: [1]) }.to raise_error(ArgumentError)
+ end
end
end
@@ -87,12 +103,12 @@ RSpec.describe API::Files do
end
end
- describe "HEAD /projects/:id/repository/files/:file_path" do
+ describe 'HEAD /projects/:id/repository/files/:file_path' do
shared_examples_for 'repository files' do
let(:options) { {} }
it 'returns 400 when file path is invalid' do
- head api(route(rouge_file_path), current_user, **options), params: params
+ head api(route(invalid_file_path), current_user, **options), params: params
expect(response).to have_gitlab_http_status(:bad_request)
end
@@ -106,16 +122,16 @@ RSpec.describe API::Files do
expect(response).to have_gitlab_http_status(:ok)
expect(response.headers['X-Gitlab-File-Path']).to eq(CGI.unescape(file_path))
- expect(response.headers['X-Gitlab-File-Name']).to eq('popen.rb')
- expect(response.headers['X-Gitlab-Last-Commit-Id']).to eq('570e7b2abdd848b95f2f578043fc23bd6f6fd24d')
- expect(response.headers['X-Gitlab-Content-Sha256']).to eq('c440cd09bae50c4632cc58638ad33c6aa375b6109d811e76a9cc3a613c1e8887')
+ expect(response.headers['X-Gitlab-File-Name']).to eq(file_name)
+ expect(response.headers['X-Gitlab-Last-Commit-Id']).to eq(last_commit_id)
+ expect(response.headers['X-Gitlab-Content-Sha256']).to eq(content_sha256)
end
it 'caches sha256 of the content', :use_clean_rails_redis_caching do
head api(route(file_path), current_user, **options), params: params
expect(Rails.cache.fetch("blob_content_sha256:#{project.full_path}:#{response.headers['X-Gitlab-Blob-Id']}"))
- .to eq('c440cd09bae50c4632cc58638ad33c6aa375b6109d811e76a9cc3a613c1e8887')
+ .to eq(content_sha256)
expect_next_instance_of(Gitlab::Git::Blob) do |instance|
expect(instance).not_to receive(:load_all_data!)
@@ -126,8 +142,8 @@ RSpec.describe API::Files do
it 'returns file by commit sha' do
# This file is deleted on HEAD
- file_path = "files%2Fjs%2Fcommit%2Ejs%2Ecoffee"
- params[:ref] = "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9"
+ file_path = 'files%2Fjs%2Fcommit%2Ejs%2Ecoffee'
+ params[:ref] = '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9'
head api(route(file_path), current_user, **options), params: params
@@ -137,15 +153,15 @@ RSpec.describe API::Files do
end
context 'when mandatory params are not given' do
- it "responds with a 400 status" do
- head api(route("any%2Ffile"), current_user, **options)
+ it 'responds with a 400 status' do
+ head api(route('any%2Ffile'), current_user, **options)
expect(response).to have_gitlab_http_status(:bad_request)
end
end
context 'when file_path does not exist' do
- it "responds with a 404 status" do
+ it 'responds with a 404 status' do
params[:ref] = 'master'
head api(route('app%2Fmodels%2Fapplication%2Erb'), current_user, **options), params: params
@@ -157,7 +173,7 @@ RSpec.describe API::Files do
context 'when file_path does not exist' do
include_context 'disabled repository'
- it "responds with a 403 status" do
+ it 'responds with a 403 status' do
head api(route(file_path), current_user, **options), params: params
expect(response).to have_gitlab_http_status(:forbidden)
@@ -165,20 +181,22 @@ RSpec.describe API::Files do
end
end
- context 'when unauthenticated', 'and project is public' do
- it_behaves_like 'repository files' do
- let(:project) { create(:project, :public, :repository) }
- let(:current_user) { nil }
+ context 'when unauthenticated' do
+ context 'and project is public' do
+ it_behaves_like 'repository files' do
+ let(:project) { create(:project, :public, :repository) }
+ let(:current_user) { nil }
+ end
end
- end
- context 'when unauthenticated', 'and project is private' do
- it "responds with a 404 status" do
- current_user = nil
+ context 'and project is private' do
+ it 'responds with a 404 status' do
+ current_user = nil
- head api(route(file_path), current_user), params: params
+ head api(route(file_path), current_user), params: params
- expect(response).to have_gitlab_http_status(:not_found)
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
end
end
@@ -190,25 +208,41 @@ RSpec.describe API::Files do
end
end
- context 'when authenticated', 'as a developer' do
- it_behaves_like 'repository files' do
- let(:current_user) { user }
+ context 'when authenticated' do
+ context 'and user is a developer' do
+ it_behaves_like 'repository files' do
+ let(:current_user) { user }
+ end
end
- end
- context 'when authenticated', 'as a guest' do
- it_behaves_like '403 response' do
- let(:request) { head api(route(file_path), guest), params: params }
+ context 'and user is a guest' do
+ it_behaves_like '403 response' do
+ let(:request) { head api(route(file_path), guest), params: params }
+ end
end
end
end
- describe "GET /projects/:id/repository/files/:file_path" do
- shared_examples_for 'repository files' do
- let(:options) { {} }
+ describe 'GET /projects/:id/repository/files/:file_path' do
+ let(:options) { {} }
+
+ shared_examples 'returns non-executable file attributes as json' do
+ specify do
+ get api(route(file_path), api_user, **options), params: params
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['file_path']).to eq(CGI.unescape(file_path))
+ expect(json_response['file_name']).to eq(file_name)
+ expect(json_response['last_commit_id']).to eq(last_commit_id)
+ expect(json_response['content_sha256']).to eq(content_sha256)
+ expect(json_response['execute_filemode']).to eq(false)
+ expect(Base64.decode64(json_response['content']).lines.first).to eq("require 'fileutils'\n")
+ end
+ end
+ shared_examples_for 'repository files' do
it 'returns 400 for invalid file path' do
- get api(route(rouge_file_path), api_user, **options), params: params
+ get api(route(invalid_file_path), api_user, **options), params: params
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['error']).to eq(invalid_file_message)
@@ -218,17 +252,7 @@ RSpec.describe API::Files do
subject { get api(route(absolute_path), api_user, **options), params: params }
end
- it 'returns file attributes as json' do
- get api(route(file_path), api_user, **options), params: params
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response['file_path']).to eq(CGI.unescape(file_path))
- expect(json_response['file_name']).to eq('popen.rb')
- expect(json_response['last_commit_id']).to eq('570e7b2abdd848b95f2f578043fc23bd6f6fd24d')
- expect(json_response['content_sha256']).to eq('c440cd09bae50c4632cc58638ad33c6aa375b6109d811e76a9cc3a613c1e8887')
- expect(json_response['execute_filemode']).to eq(false)
- expect(Base64.decode64(json_response['content']).lines.first).to eq("require 'fileutils'\n")
- end
+ it_behaves_like 'returns non-executable file attributes as json'
context 'for executable file' do
it 'returns file attributes as json' do
@@ -247,7 +271,7 @@ RSpec.describe API::Files do
end
it 'returns json when file has txt extension' do
- file_path = "bar%2Fbranch-test.txt"
+ file_path = 'bar%2Fbranch-test.txt'
get api(route(file_path), api_user, **options), params: params
@@ -277,8 +301,8 @@ RSpec.describe API::Files do
it 'returns file by commit sha' do
# This file is deleted on HEAD
- file_path = "files%2Fjs%2Fcommit%2Ejs%2Ecoffee"
- params[:ref] = "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9"
+ file_path = 'files%2Fjs%2Fcommit%2Ejs%2Ecoffee'
+ params[:ref] = '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9'
get api(route(file_path), api_user, **options), params: params
@@ -289,9 +313,9 @@ RSpec.describe API::Files do
end
it 'returns raw file info' do
- url = route(file_path) + "/raw"
+ url = route(file_path) + '/raw'
expect_to_send_git_blob(api(url, api_user, **options), params)
- expect(headers[Gitlab::Workhorse::DETECT_HEADER]).to eq "true"
+ expect(headers[Gitlab::Workhorse::DETECT_HEADER]).to eq 'true'
end
it 'returns blame file info' do
@@ -303,16 +327,16 @@ RSpec.describe API::Files do
end
it 'sets inline content disposition by default' do
- url = route(file_path) + "/raw"
+ url = route(file_path) + '/raw'
get api(url, api_user, **options), params: params
- expect(headers['Content-Disposition']).to eq(%q(inline; filename="popen.rb"; filename*=UTF-8''popen.rb))
+ expect(headers['Content-Disposition']).to eq(%(inline; filename="#{file_name}"; filename*=UTF-8''#{file_name}))
end
context 'when mandatory params are not given' do
it_behaves_like '400 response' do
- let(:request) { get api(route("any%2Ffile"), current_user, **options) }
+ let(:request) { get api(route('any%2Ffile'), current_user, **options) }
end
end
@@ -334,40 +358,96 @@ RSpec.describe API::Files do
end
end
- context 'when unauthenticated', 'and project is public' do
- it_behaves_like 'repository files' do
- let(:project) { create(:project, :public, :repository) }
- let(:current_user) { nil }
- let(:api_user) { nil }
+ context 'when unauthenticated' do
+ context 'and project is public' do
+ it_behaves_like 'repository files' do
+ let(:project) { create(:project, :public, :repository) }
+ let(:current_user) { nil }
+ let(:api_user) { nil }
+ end
end
- end
- context 'when PATs are used' do
- it_behaves_like 'repository files' do
- let(:token) { create(:personal_access_token, scopes: ['read_repository'], user: user) }
- let(:current_user) { user }
- let(:api_user) { nil }
- let(:options) { { personal_access_token: token } }
+ context 'and project is private' do
+ it_behaves_like '404 response' do
+ let(:request) { get api(route(file_path)), params: params }
+ let(:message) { '404 Project Not Found' }
+ end
end
end
- context 'when unauthenticated', 'and project is private' do
- it_behaves_like '404 response' do
- let(:request) { get api(route(file_path)), params: params }
- let(:message) { '404 Project Not Found' }
- end
- end
+ context 'when authenticated' do
+ context 'and user is a direct project member' do
+ context 'and project is private' do
+ context 'and user is a developer' do
+ it_behaves_like 'repository files' do
+ let(:current_user) { user }
+ let(:api_user) { user }
+ end
+
+ context 'and PATs are used' do
+ it_behaves_like 'repository files' do
+ let(:token) { create(:personal_access_token, scopes: ['read_repository'], user: user) }
+ let(:current_user) { user }
+ let(:api_user) { nil }
+ let(:options) { { personal_access_token: token } }
+ end
+ end
+ end
- context 'when authenticated', 'as a developer' do
- it_behaves_like 'repository files' do
- let(:current_user) { user }
- let(:api_user) { user }
+ context 'and user is a guest' do
+ it_behaves_like '403 response' do
+ let(:request) { get api(route(file_path), guest), params: params }
+ end
+ end
+ end
end
end
- context 'when authenticated', 'as a guest' do
- it_behaves_like '403 response' do
- let(:request) { get api(route(file_path), guest), params: params }
+ context 'when authenticated' do
+ context 'and user is an inherited member from the group' do
+ context 'when project is public with private repository' do
+ let_it_be(:project) { create(:project, :public, :repository, :repository_private, group: group) }
+
+ context 'and user is a guest' do
+ it_behaves_like 'returns non-executable file attributes as json' do
+ let(:api_user) { inherited_guest }
+ end
+ end
+
+ context 'and user is a reporter' do
+ it_behaves_like 'returns non-executable file attributes as json' do
+ let(:api_user) { inherited_reporter }
+ end
+ end
+
+ context 'and user is a developer' do
+ it_behaves_like 'returns non-executable file attributes as json' do
+ let(:api_user) { inherited_developer }
+ end
+ end
+ end
+
+ context 'when project is private' do
+ let_it_be(:project) { create(:project, :private, :repository, group: group) }
+
+ context 'and user is a guest' do
+ it_behaves_like '403 response' do
+ let(:request) { get api(route(file_path), inherited_guest), params: params }
+ end
+ end
+
+ context 'and user is a reporter' do
+ it_behaves_like 'returns non-executable file attributes as json' do
+ let(:api_user) { inherited_reporter }
+ end
+ end
+
+ context 'and user is a developer' do
+ it_behaves_like 'returns non-executable file attributes as json' do
+ let(:api_user) { inherited_developer }
+ end
+ end
+ end
end
end
end
@@ -406,11 +486,10 @@ RSpec.describe API::Files do
expect(response).to have_gitlab_http_status(:ok)
expect(response.headers['X-Gitlab-File-Path']).to eq(CGI.unescape(file_path))
- expect(response.headers['X-Gitlab-File-Name']).to eq('popen.rb')
- expect(response.headers['X-Gitlab-Last-Commit-Id']).to eq('570e7b2abdd848b95f2f578043fc23bd6f6fd24d')
- expect(response.headers['X-Gitlab-Content-Sha256'])
- .to eq('c440cd09bae50c4632cc58638ad33c6aa375b6109d811e76a9cc3a613c1e8887')
- expect(response.headers['X-Gitlab-Execute-Filemode']).to eq("false")
+ expect(response.headers['X-Gitlab-File-Name']).to eq(file_name)
+ expect(response.headers['X-Gitlab-Last-Commit-Id']).to eq(last_commit_id)
+ expect(response.headers['X-Gitlab-Content-Sha256']).to eq(content_sha256)
+ expect(response.headers['X-Gitlab-Execute-Filemode']).to eq('false')
end
context 'for executable file' do
@@ -424,13 +503,13 @@ RSpec.describe API::Files do
expect(response.headers['X-Gitlab-Last-Commit-Id']).to eq('6b8dc4a827797aa025ff6b8f425e583858a10d4f')
expect(response.headers['X-Gitlab-Content-Sha256'])
.to eq('2c74b1181ef780dfb692c030d3a0df6e0b624135c38a9344e56b9f80007b6191')
- expect(response.headers['X-Gitlab-Execute-Filemode']).to eq("true")
+ expect(response.headers['X-Gitlab-Execute-Filemode']).to eq('true')
end
end
end
it 'returns 400 when file path is invalid' do
- get api(route(rouge_file_path) + '/blame', current_user), params: params
+ get api(route(invalid_file_path) + '/blame', current_user), params: params
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['error']).to eq(invalid_file_message)
@@ -573,29 +652,33 @@ RSpec.describe API::Files do
end
end
- context 'when unauthenticated', 'and project is public' do
- it_behaves_like 'repository blame files' do
- let(:project) { create(:project, :public, :repository) }
- let(:current_user) { nil }
+ context 'when unauthenticated' do
+ context 'and project is public' do
+ it_behaves_like 'repository blame files' do
+ let(:project) { create(:project, :public, :repository) }
+ let(:current_user) { nil }
+ end
end
- end
- context 'when unauthenticated', 'and project is private' do
- it_behaves_like '404 response' do
- let(:request) { get api(route(file_path)), params: params }
- let(:message) { '404 Project Not Found' }
+ context 'and project is private' do
+ it_behaves_like '404 response' do
+ let(:request) { get api(route(file_path)), params: params }
+ let(:message) { '404 Project Not Found' }
+ end
end
end
- context 'when authenticated', 'as a developer' do
- it_behaves_like 'repository blame files' do
- let(:current_user) { user }
+ context 'when authenticated' do
+ context 'and user is a developer' do
+ it_behaves_like 'repository blame files' do
+ let(:current_user) { user }
+ end
end
- end
- context 'when authenticated', 'as a guest' do
- it_behaves_like '403 response' do
- let(:request) { get api(route(file_path) + '/blame', guest), params: params }
+ context 'and user is a guest' do
+ it_behaves_like '403 response' do
+ let(:request) { get api(route(file_path) + '/blame', guest), params: params }
+ end
end
end
@@ -614,10 +697,10 @@ RSpec.describe API::Files do
end
end
- describe "GET /projects/:id/repository/files/:file_path/raw" do
+ describe 'GET /projects/:id/repository/files/:file_path/raw' do
shared_examples_for 'repository raw files' do
it 'returns 400 when file path is invalid' do
- get api(route(rouge_file_path) + "/raw", current_user), params: params
+ get api(route(invalid_file_path) + '/raw', current_user), params: params
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['error']).to eq(invalid_file_message)
@@ -628,7 +711,7 @@ RSpec.describe API::Files do
end
it 'returns raw file info' do
- url = route(file_path) + "/raw"
+ url = route(file_path) + '/raw'
expect_to_send_git_blob(api(url, current_user), params)
end
@@ -639,39 +722,39 @@ RSpec.describe API::Files do
end
it 'returns response :ok', :aggregate_failures do
- url = route(file_path) + "/raw"
+ url = route(file_path) + '/raw'
expect_to_send_git_blob(api(url, current_user), {})
end
end
it 'returns raw file info for files with dots' do
- url = route('.gitignore') + "/raw"
+ url = route('.gitignore') + '/raw'
expect_to_send_git_blob(api(url, current_user), params)
end
it 'returns file by commit sha' do
# This file is deleted on HEAD
- file_path = "files%2Fjs%2Fcommit%2Ejs%2Ecoffee"
- params[:ref] = "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9"
+ file_path = 'files%2Fjs%2Fcommit%2Ejs%2Ecoffee'
+ params[:ref] = '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9'
- expect_to_send_git_blob(api(route(file_path) + "/raw", current_user), params)
+ expect_to_send_git_blob(api(route(file_path) + '/raw', current_user), params)
end
it 'sets no-cache headers' do
- url = route('.gitignore') + "/raw"
+ url = route('.gitignore') + '/raw'
expect_to_send_git_blob(api(url, current_user), params)
- expect(response.headers["Cache-Control"]).to eq("max-age=0, private, must-revalidate, no-store, no-cache")
- expect(response.headers["Pragma"]).to eq("no-cache")
- expect(response.headers["Expires"]).to eq("Fri, 01 Jan 1990 00:00:00 GMT")
+ expect(response.headers['Cache-Control']).to eq('max-age=0, private, must-revalidate, no-store, no-cache')
+ expect(response.headers['Pragma']).to eq('no-cache')
+ expect(response.headers['Expires']).to eq('Fri, 01 Jan 1990 00:00:00 GMT')
end
context 'when mandatory params are not given' do
it_behaves_like '400 response' do
- let(:request) { get api(route("any%2Ffile"), current_user) }
+ let(:request) { get api(route('any%2Ffile'), current_user) }
end
end
@@ -693,29 +776,33 @@ RSpec.describe API::Files do
end
end
- context 'when unauthenticated', 'and project is public' do
- it_behaves_like 'repository raw files' do
- let(:project) { create(:project, :public, :repository) }
- let(:current_user) { nil }
+ context 'when unauthenticated' do
+ context 'and project is public' do
+ it_behaves_like 'repository raw files' do
+ let(:project) { create(:project, :public, :repository) }
+ let(:current_user) { nil }
+ end
end
- end
- context 'when unauthenticated', 'and project is private' do
- it_behaves_like '404 response' do
- let(:request) { get api(route(file_path)), params: params }
- let(:message) { '404 Project Not Found' }
+ context 'and project is private' do
+ it_behaves_like '404 response' do
+ let(:request) { get api(route(file_path)), params: params }
+ let(:message) { '404 Project Not Found' }
+ end
end
end
- context 'when authenticated', 'as a developer' do
- it_behaves_like 'repository raw files' do
- let(:current_user) { user }
+ context 'when authenticated' do
+ context 'and user is a developer' do
+ it_behaves_like 'repository raw files' do
+ let(:current_user) { user }
+ end
end
- end
- context 'when authenticated', 'as a guest' do
- it_behaves_like '403 response' do
- let(:request) { get api(route(file_path), guest), params: params }
+ context 'and user is a guest' do
+ it_behaves_like '403 response' do
+ let(:request) { get api(route(file_path), guest), params: params }
+ end
end
end
@@ -724,139 +811,205 @@ RSpec.describe API::Files do
token = create(:personal_access_token, scopes: ['read_repository'], user: user)
# This file is deleted on HEAD
- file_path = "files%2Fjs%2Fcommit%2Ejs%2Ecoffee"
- params[:ref] = "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9"
- url = api(route(file_path) + "/raw", personal_access_token: token)
+ file_path = 'files%2Fjs%2Fcommit%2Ejs%2Ecoffee'
+ params[:ref] = '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9'
+ url = api(route(file_path) + '/raw', personal_access_token: token)
expect_to_send_git_blob(url, params)
end
end
end
- describe "POST /projects/:id/repository/files/:file_path" do
- let!(:file_path) { "new_subfolder%2Fnewfile%2Erb" }
+ describe 'POST /projects/:id/repository/files/:file_path' do
+ let!(:file_path) { 'new_subfolder%2Fnewfile%2Erb' }
+
let(:params) do
{
- branch: "master",
- content: "puts 8",
- commit_message: "Added newfile"
+ branch: 'master',
+ content: 'puts 8',
+ commit_message: 'Added newfile'
}
end
let(:executable_params) do
{
- branch: "master",
- content: "puts 8",
- commit_message: "Added newfile",
+ branch: 'master',
+ content: 'puts 8',
+ commit_message: 'Added newfile',
execute_filemode: true
}
end
- it 'returns 400 when file path is invalid' do
- post api(route(rouge_file_path), user), params: params
+ shared_examples 'creates a new file in the project repo' do
+ specify do
+ post api(route(file_path), current_user), params: params
- expect(response).to have_gitlab_http_status(:bad_request)
- expect(json_response['error']).to eq(invalid_file_message)
+ expect(response).to have_gitlab_http_status(:created)
+ expect(json_response['file_path']).to eq(CGI.unescape(file_path))
+ last_commit = project.repository.commit.raw
+ expect(last_commit.author_email).to eq(current_user.email)
+ expect(last_commit.author_name).to eq(current_user.name)
+ expect(project.repository.blob_at_branch(params[:branch], CGI.unescape(file_path)).executable?).to eq(false)
+ end
end
- it_behaves_like 'when path is absolute' do
- subject { post api(route(absolute_path), user), params: params }
- end
+ context 'when authenticated', 'as a direct project member' do
+ context 'when project is private' do
+ context 'and user is a developer' do
+ it 'returns 400 when file path is invalid' do
+ post api(route(invalid_file_path), user), params: params
- it "creates a new file in project repo" do
- post api(route(file_path), user), params: params
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['error']).to eq(invalid_file_message)
+ end
- expect(response).to have_gitlab_http_status(:created)
- expect(json_response["file_path"]).to eq(CGI.unescape(file_path))
- last_commit = project.repository.commit.raw
- expect(last_commit.author_email).to eq(user.email)
- expect(last_commit.author_name).to eq(user.name)
- expect(project.repository.blob_at_branch(params[:branch], CGI.unescape(file_path)).executable?).to eq(false)
- end
+ it_behaves_like 'when path is absolute' do
+ subject { post api(route(absolute_path), user), params: params }
+ end
- it "creates a new executable file in project repo" do
- post api(route(file_path), user), params: executable_params
+ it_behaves_like 'creates a new file in the project repo' do
+ let(:current_user) { user }
+ end
- expect(response).to have_gitlab_http_status(:created)
- expect(json_response["file_path"]).to eq(CGI.unescape(file_path))
- last_commit = project.repository.commit.raw
- expect(last_commit.author_email).to eq(user.email)
- expect(last_commit.author_name).to eq(user.name)
- expect(project.repository.blob_at_branch(params[:branch], CGI.unescape(file_path)).executable?).to eq(true)
- end
+ it 'creates a new executable file in project repo' do
+ post api(route(file_path), user), params: executable_params
- it "returns a 400 bad request if no mandatory params given" do
- post api(route("any%2Etxt"), user)
+ expect(response).to have_gitlab_http_status(:created)
+ expect(json_response['file_path']).to eq(CGI.unescape(file_path))
+ last_commit = project.repository.commit.raw
+ expect(last_commit.author_email).to eq(user.email)
+ expect(last_commit.author_name).to eq(user.name)
+ expect(project.repository.blob_at_branch(params[:branch], CGI.unescape(file_path)).executable?).to eq(true)
+ end
- expect(response).to have_gitlab_http_status(:bad_request)
- end
+ context 'when no mandatory params given' do
+ it 'returns a 400 bad request' do
+ post api(route('any%2Etxt'), user)
- it 'returns a 400 bad request if the commit message is empty' do
- params[:commit_message] = ''
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+ end
- post api(route(file_path), user), params: params
+ context 'when the commit message is empty' do
+ before do
+ params[:commit_message] = ''
+ end
- expect(response).to have_gitlab_http_status(:bad_request)
- end
+ it 'returns a 400 bad request' do
+ post api(route(file_path), user), params: params
- it "returns a 400 if editor fails to create file" do
- allow_next_instance_of(Repository) do |instance|
- allow(instance).to receive(:create_file).and_raise(Gitlab::Git::CommitError, 'Cannot create file')
- end
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+ end
- post api(route("any%2Etxt"), user), params: params
+ context 'when editor fails to create file' do
+ before do
+ allow_next_instance_of(Repository) do |instance|
+ allow(instance).to receive(:create_file).and_raise(Gitlab::Git::CommitError, 'Cannot create file')
+ end
+ end
- expect(response).to have_gitlab_http_status(:bad_request)
- end
+ it 'returns a 400 bad request' do
+ post api(route('any%2Etxt'), user), params: params
- context 'with PATs' do
- it 'returns 403 with `read_repository` scope' do
- token = create(:personal_access_token, scopes: ['read_repository'], user: user)
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+ end
- post api(route(file_path), personal_access_token: token), params: params
+ context 'and PATs are used' do
+ it 'returns 403 with `read_repository` scope' do
+ token = create(:personal_access_token, scopes: ['read_repository'], user: user)
- expect(response).to have_gitlab_http_status(:forbidden)
- end
+ post api(route(file_path), personal_access_token: token), params: params
- it 'returns 201 with `api` scope' do
- token = create(:personal_access_token, scopes: ['api'], user: user)
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
- post api(route(file_path), personal_access_token: token), params: params
+ it 'returns 201 with `api` scope' do
+ token = create(:personal_access_token, scopes: ['api'], user: user)
- expect(response).to have_gitlab_http_status(:created)
- end
- end
+ post api(route(file_path), personal_access_token: token), params: params
- context "when specifying an author" do
- it "creates a new file with the specified author" do
- params.merge!(author_email: author_email, author_name: author_name)
+ expect(response).to have_gitlab_http_status(:created)
+ end
+ end
- post api(route("new_file_with_author%2Etxt"), user), params: params
+ context 'and the repo is empty' do
+ let!(:project) { create(:project_empty_repo, namespace: user.namespace ) }
- expect(response).to have_gitlab_http_status(:created)
- expect(response.media_type).to eq('application/json')
- last_commit = project.repository.commit.raw
- expect(last_commit.author_email).to eq(author_email)
- expect(last_commit.author_name).to eq(author_name)
+ it_behaves_like 'creates a new file in the project repo' do
+ let(:current_user) { user }
+ let(:file_path) { 'newfile%2Erb' }
+ end
+ end
+
+ context 'when specifying an author' do
+ it 'creates a new file with the specified author' do
+ params.merge!(author_email: author_email, author_name: author_name)
+
+ post api(route('new_file_with_author%2Etxt'), user), params: params
+
+ expect(response).to have_gitlab_http_status(:created)
+ expect(response.media_type).to eq('application/json')
+ last_commit = project.repository.commit.raw
+ expect(last_commit.author_email).to eq(author_email)
+ expect(last_commit.author_name).to eq(author_name)
+ end
+ end
+ end
end
end
- context 'when the repo is empty' do
- let!(:project) { create(:project_empty_repo, namespace: user.namespace ) }
+ context 'when authenticated' do
+ context 'and user is an inherited member from the group' do
+ context 'when project is public with private repository' do
+ let_it_be(:project) { create(:project, :public, :repository, :repository_private, group: group) }
- it "creates a new file in project repo" do
- post api(route("newfile%2Erb"), user), params: params
+ context 'and user is a guest' do
+ it_behaves_like '403 response' do
+ let(:request) { post api(route(file_path), inherited_guest), params: params }
+ end
+ end
- expect(response).to have_gitlab_http_status(:created)
- expect(json_response['file_path']).to eq('newfile.rb')
- last_commit = project.repository.commit.raw
- expect(last_commit.author_email).to eq(user.email)
- expect(last_commit.author_name).to eq(user.name)
+ context 'and user is a reporter' do
+ it_behaves_like '403 response' do
+ let(:request) { post api(route(file_path), inherited_reporter), params: params }
+ end
+ end
+
+ context 'and user is a developer' do
+ it_behaves_like 'creates a new file in the project repo' do
+ let(:current_user) { inherited_developer }
+ end
+ end
+ end
+
+ context 'when project is private' do
+ let_it_be(:project) { create(:project, :private, :repository, group: group) }
+
+ context 'and user is a guest' do
+ it_behaves_like '403 response' do
+ let(:request) { post api(route(file_path), inherited_guest), params: params }
+ end
+ end
+
+ context 'and user is a reporter' do
+ it_behaves_like '403 response' do
+ let(:request) { post api(route(file_path), inherited_reporter), params: params }
+ end
+ end
+
+ context 'and user is a developer' do
+ it_behaves_like 'creates a new file in the project repo' do
+ let(:current_user) { inherited_developer }
+ end
+ end
+ end
end
end
end
- describe "PUT /projects/:id/repository/files" do
+ describe 'PUT /projects/:id/repository/files' do
let(:params) do
{
branch: 'master',
@@ -865,7 +1018,7 @@ RSpec.describe API::Files do
}
end
- it "updates existing file in project repo" do
+ it 'updates existing file in project repo' do
put api(route(file_path), user), params: params
expect(response).to have_gitlab_http_status(:ok)
@@ -875,42 +1028,58 @@ RSpec.describe API::Files do
expect(last_commit.author_name).to eq(user.name)
end
- it 'returns a 400 bad request if the commit message is empty' do
- params[:commit_message] = ''
+ context 'when the commit message is empty' do
+ before do
+ params[:commit_message] = ''
+ end
- put api(route(file_path), user), params: params
+ it 'returns a 400 bad request' do
+ put api(route(file_path), user), params: params
- expect(response).to have_gitlab_http_status(:bad_request)
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
end
- it "returns a 400 bad request if update existing file with stale last commit id" do
- params_with_stale_id = params.merge(last_commit_id: 'stale')
+ context 'when updating an existing file with stale last commit id' do
+ let(:params_with_stale_id) { params.merge(last_commit_id: 'stale') }
- put api(route(file_path), user), params: params_with_stale_id
+ it 'returns a 400 bad request' do
+ put api(route(file_path), user), params: params_with_stale_id
- expect(response).to have_gitlab_http_status(:bad_request)
- expect(json_response['message']).to eq(_('You are attempting to update a file that has changed since you started editing it.'))
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['message']).to eq(_('You are attempting to update a file that has changed since you started editing it.'))
+ end
end
- it "updates existing file in project repo with accepts correct last commit id" do
- last_commit = Gitlab::Git::Commit
- .last_for_path(project.repository, 'master', Addressable::URI.unencode_component(file_path))
- params_with_correct_id = params.merge(last_commit_id: last_commit.id)
+ context 'with correct last commit id' do
+ let(:last_commit) do
+ Gitlab::Git::Commit
+ .last_for_path(project.repository, 'master', Addressable::URI.unencode_component(file_path))
+ end
- put api(route(file_path), user), params: params_with_correct_id
+ let(:params_with_correct_id) { params.merge(last_commit_id: last_commit.id) }
- expect(response).to have_gitlab_http_status(:ok)
+ it 'updates existing file in project repo' do
+ put api(route(file_path), user), params: params_with_correct_id
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
end
- it "returns 400 when file path is invalid" do
- last_commit = Gitlab::Git::Commit
- .last_for_path(project.repository, 'master', Addressable::URI.unencode_component(file_path))
- params_with_correct_id = params.merge(last_commit_id: last_commit.id)
+ context 'when file path is invalid' do
+ let(:last_commit) do
+ Gitlab::Git::Commit
+ .last_for_path(project.repository, 'master', Addressable::URI.unencode_component(file_path))
+ end
- put api(route(rouge_file_path), user), params: params_with_correct_id
+ let(:params_with_correct_id) { params.merge(last_commit_id: last_commit.id) }
- expect(response).to have_gitlab_http_status(:bad_request)
- expect(json_response['error']).to eq(invalid_file_message)
+ it 'returns a 400 bad request' do
+ put api(route(invalid_file_path), user), params: params_with_correct_id
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['error']).to eq(invalid_file_message)
+ end
end
it_behaves_like 'when path is absolute' do
@@ -924,15 +1093,17 @@ RSpec.describe API::Files do
subject { put api(route(absolute_path), user), params: params_with_correct_id }
end
- it "returns a 400 bad request if no params given" do
- put api(route(file_path), user)
+ context 'when no params given' do
+ it 'returns a 400 bad request' do
+ put api(route(file_path), user)
- expect(response).to have_gitlab_http_status(:bad_request)
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
end
- context "when specifying an author" do
- it "updates a file with the specified author" do
- params.merge!(author_email: author_email, author_name: author_name, content: "New content")
+ context 'when specifying an author' do
+ it 'updates a file with the specified author' do
+ params.merge!(author_email: author_email, author_name: author_name, content: 'New content')
put api(route(file_path), user), params: params
@@ -982,7 +1153,7 @@ RSpec.describe API::Files do
end
end
- describe "DELETE /projects/:id/repository/files" do
+ describe 'DELETE /projects/:id/repository/files' do
let(:params) do
{
branch: 'master',
@@ -991,7 +1162,7 @@ RSpec.describe API::Files do
end
it 'returns 400 when file path is invalid' do
- delete api(route(rouge_file_path), user), params: params
+ delete api(route(invalid_file_path), user), params: params
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['error']).to eq(invalid_file_message)
@@ -1001,38 +1172,48 @@ RSpec.describe API::Files do
subject { delete api(route(absolute_path), user), params: params }
end
- it "deletes existing file in project repo" do
+ it 'deletes existing file in project repo' do
delete api(route(file_path), user), params: params
expect(response).to have_gitlab_http_status(:no_content)
end
- it "returns a 400 bad request if no params given" do
- delete api(route(file_path), user)
+ context 'when no params given' do
+ it 'returns a 400 bad request' do
+ delete api(route(file_path), user)
- expect(response).to have_gitlab_http_status(:bad_request)
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
end
- it 'returns a 400 bad request if the commit message is empty' do
- params[:commit_message] = ''
+ context 'when the commit message is empty' do
+ before do
+ params[:commit_message] = ''
+ end
- delete api(route(file_path), user), params: params
+ it 'returns a 400 bad request' do
+ delete api(route(file_path), user), params: params
- expect(response).to have_gitlab_http_status(:bad_request)
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
end
- it "returns a 400 if fails to delete file" do
- allow_next_instance_of(Repository) do |instance|
- allow(instance).to receive(:delete_file).and_raise(Gitlab::Git::CommitError, 'Cannot delete file')
+ context 'when fails to delete file' do
+ before do
+ allow_next_instance_of(Repository) do |instance|
+ allow(instance).to receive(:delete_file).and_raise(Gitlab::Git::CommitError, 'Cannot delete file')
+ end
end
- delete api(route(file_path), user), params: params
+ it 'returns a 400 bad request' do
+ delete api(route(file_path), user), params: params
- expect(response).to have_gitlab_http_status(:bad_request)
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
end
- context "when specifying an author" do
- it "removes a file with the specified author" do
+ context 'when specifying an author' do
+ it 'removes a file with the specified author' do
params.merge!(author_email: author_email, author_name: author_name)
delete api(route(file_path), user), params: params
@@ -1042,7 +1223,7 @@ RSpec.describe API::Files do
end
end
- describe "POST /projects/:id/repository/files with binary file" do
+ describe 'POST /projects/:id/repository/files with binary file' do
let(:file_path) { 'test%2Ebin' }
let(:put_params) do
{
@@ -1063,7 +1244,7 @@ RSpec.describe API::Files do
post api(route(file_path), user), params: put_params
end
- it "remains unchanged" do
+ it 'remains unchanged' do
get api(route(file_path), user), params: get_params
expect(response).to have_gitlab_http_status(:ok)
diff --git a/spec/requests/api/generic_packages_spec.rb b/spec/requests/api/generic_packages_spec.rb
index 3a5c6103781..823eafab734 100644
--- a/spec/requests/api/generic_packages_spec.rb
+++ b/spec/requests/api/generic_packages_spec.rb
@@ -572,6 +572,12 @@ RSpec.describe API::GenericPackages do
expect(response).to have_gitlab_http_status(expected_status)
end
+
+ if params[:expected_status] == :success
+ it_behaves_like 'bumping the package last downloaded at field' do
+ subject { download_file(auth_header) }
+ end
+ end
end
where(:authenticate_with, :expected_status) do
@@ -587,6 +593,12 @@ RSpec.describe API::GenericPackages do
expect(response).to have_gitlab_http_status(expected_status)
end
+
+ if params[:expected_status] == :success
+ it_behaves_like 'bumping the package last downloaded at field' do
+ subject { download_file(deploy_token_auth_header) }
+ end
+ end
end
end
@@ -608,6 +620,12 @@ RSpec.describe API::GenericPackages do
expect(response).to have_gitlab_http_status(expected_status)
end
+
+ if params[:expected_status] == :success
+ it_behaves_like 'bumping the package last downloaded at field' do
+ subject { download_file(personal_access_token_header) }
+ end
+ end
end
end
diff --git a/spec/requests/api/graphql/ci/config_spec.rb b/spec/requests/api/graphql/ci/config_spec.rb
index 5f8a895b16e..960fda80dd9 100644
--- a/spec/requests/api/graphql/ci/config_spec.rb
+++ b/spec/requests/api/graphql/ci/config_spec.rb
@@ -173,7 +173,7 @@ RSpec.describe 'Query.ciConfig' do
{
"name" => "docker",
"size" => 1,
- "jobs" =>
+ "jobs" =>
{
"nodes" => [
{
@@ -206,7 +206,7 @@ RSpec.describe 'Query.ciConfig' do
{
"name" => "deploy_job",
"size" => 1,
- "jobs" =>
+ "jobs" =>
{
"nodes" => [
{
@@ -332,7 +332,7 @@ RSpec.describe 'Query.ciConfig' do
"only" => { "refs" => %w[branches tags] },
"when" => "on_success",
"tags" => [],
- "needs" => { "nodes" => [] } }
+ "needs" => { "nodes" => [] } }
]
}
}
diff --git a/spec/requests/api/graphql/ci/config_variables_spec.rb b/spec/requests/api/graphql/ci/config_variables_spec.rb
new file mode 100644
index 00000000000..2b5a5d0dc93
--- /dev/null
+++ b/spec/requests/api/graphql/ci/config_variables_spec.rb
@@ -0,0 +1,93 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Query.project(fullPath).ciConfigVariables(sha)' do
+ include GraphqlHelpers
+ include ReactiveCachingHelpers
+
+ let_it_be(:content) do
+ File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml'))
+ end
+
+ let_it_be(:project) { create(:project, :custom_repo, :public, files: { '.gitlab-ci.yml' => content }) }
+ let_it_be(:user) { create(:user) }
+
+ let(:service) { Ci::ListConfigVariablesService.new(project, user) }
+ let(:sha) { project.repository.commit.sha }
+
+ let(:query) do
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ ciConfigVariables(sha: "#{sha}") {
+ key
+ value
+ description
+ }
+ }
+ }
+ )
+ end
+
+ context 'when the user has the correct permissions' do
+ before do
+ project.add_maintainer(user)
+ allow(Ci::ListConfigVariablesService)
+ .to receive(:new)
+ .and_return(service)
+ end
+
+ context 'when the cache is not empty' do
+ before do
+ synchronous_reactive_cache(service)
+ end
+
+ it 'returns the CI variables for the config' do
+ expect(service)
+ .to receive(:execute)
+ .with(sha)
+ .and_call_original
+
+ post_graphql(query, current_user: user)
+
+ expect(graphql_data.dig('project', 'ciConfigVariables')).to contain_exactly(
+ {
+ 'key' => 'DB_NAME',
+ 'value' => 'postgres',
+ 'description' => nil
+ },
+ {
+ 'key' => 'ENVIRONMENT_VAR',
+ 'value' => 'env var value',
+ 'description' => 'env var description'
+ }
+ )
+ end
+ end
+
+ context 'when the cache is empty' do
+ it 'returns nothing' do
+ post_graphql(query, current_user: user)
+
+ expect(graphql_data.dig('project', 'ciConfigVariables')).to be_nil
+ end
+ end
+ end
+
+ context 'when the user is not authorized' do
+ before do
+ project.add_guest(user)
+ allow(Ci::ListConfigVariablesService)
+ .to receive(:new)
+ .and_return(service)
+ synchronous_reactive_cache(service)
+ end
+
+ it 'returns nothing' do
+ post_graphql(query, current_user: user)
+
+ expect(graphql_data.dig('project', 'ciConfigVariables')).to be_nil
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/ci/group_variables_spec.rb b/spec/requests/api/graphql/ci/group_variables_spec.rb
index 5ea6646ec2c..7baf26c7648 100644
--- a/spec/requests/api/graphql/ci/group_variables_spec.rb
+++ b/spec/requests/api/graphql/ci/group_variables_spec.rb
@@ -13,6 +13,7 @@ RSpec.describe 'Query.group(fullPath).ciVariables' do
query {
group(fullPath: "#{group.full_path}") {
ciVariables {
+ limit
nodes {
id
key
@@ -35,11 +36,18 @@ RSpec.describe 'Query.group(fullPath).ciVariables' do
end
it "returns the group's CI variables" do
- variable = create(:ci_group_variable, group: group, key: 'TEST_VAR', value: 'test',
- masked: false, protected: true, raw: true, environment_scope: 'staging')
+ variable = create(:ci_group_variable,
+ group: group,
+ key: 'TEST_VAR',
+ value: 'test',
+ masked: false,
+ protected: true,
+ raw: true,
+ environment_scope: 'staging')
post_graphql(query, current_user: user)
+ expect(graphql_data.dig('group', 'ciVariables', 'limit')).to be(200)
expect(graphql_data.dig('group', 'ciVariables', 'nodes')).to contain_exactly({
'id' => variable.to_global_id.to_s,
'key' => 'TEST_VAR',
diff --git a/spec/requests/api/graphql/ci/instance_variables_spec.rb b/spec/requests/api/graphql/ci/instance_variables_spec.rb
index c5c88697bf4..cd6b2de98a1 100644
--- a/spec/requests/api/graphql/ci/instance_variables_spec.rb
+++ b/spec/requests/api/graphql/ci/instance_variables_spec.rb
@@ -29,7 +29,7 @@ RSpec.describe 'Query.ciVariables' do
it "returns the instance's CI variables" do
variable = create(:ci_instance_variable, key: 'TEST_VAR', value: 'test',
- masked: false, protected: true, raw: true)
+ masked: false, protected: true, raw: true)
post_graphql(query, current_user: user)
diff --git a/spec/requests/api/graphql/ci/jobs_spec.rb b/spec/requests/api/graphql/ci/jobs_spec.rb
index 8c4ab13fc35..fa8fb1d54aa 100644
--- a/spec/requests/api/graphql/ci/jobs_spec.rb
+++ b/spec/requests/api/graphql/ci/jobs_spec.rb
@@ -335,4 +335,35 @@ RSpec.describe 'Query.project.pipeline' do
end
end
end
+
+ context 'when querying jobs for multiple projects' do
+ let(:query) do
+ %(
+ query {
+ projects {
+ nodes {
+ jobs {
+ nodes {
+ name
+ }
+ }
+ }
+ }
+ }
+ )
+ end
+
+ before do
+ create_list(:project, 2).each do |project|
+ project.add_developer(user)
+ create(:ci_build, project: project)
+ end
+ end
+
+ it 'returns an error' do
+ post_graphql(query, current_user: user)
+
+ expect_graphql_errors_to_include [/"jobs" field can be requested only for 1 Project\(s\) at a time./]
+ end
+ end
end
diff --git a/spec/requests/api/graphql/ci/project_variables_spec.rb b/spec/requests/api/graphql/ci/project_variables_spec.rb
index e61f146b24c..d49a4a7e768 100644
--- a/spec/requests/api/graphql/ci/project_variables_spec.rb
+++ b/spec/requests/api/graphql/ci/project_variables_spec.rb
@@ -13,6 +13,7 @@ RSpec.describe 'Query.project(fullPath).ciVariables' do
query {
project(fullPath: "#{project.full_path}") {
ciVariables {
+ limit
nodes {
id
key
@@ -36,10 +37,11 @@ RSpec.describe 'Query.project(fullPath).ciVariables' do
it "returns the project's CI variables" do
variable = create(:ci_variable, project: project, key: 'TEST_VAR', value: 'test',
- masked: false, protected: true, raw: true, environment_scope: 'production')
+ masked: false, protected: true, raw: true, environment_scope: 'production')
post_graphql(query, current_user: user)
+ expect(graphql_data.dig('project', 'ciVariables', 'limit')).to be(200)
expect(graphql_data.dig('project', 'ciVariables', 'nodes')).to contain_exactly({
'id' => variable.to_global_id.to_s,
'key' => 'TEST_VAR',
diff --git a/spec/requests/api/graphql/ci/runner_spec.rb b/spec/requests/api/graphql/ci/runner_spec.rb
index e17a83d8e47..bd90753f9ad 100644
--- a/spec/requests/api/graphql/ci/runner_spec.rb
+++ b/spec/requests/api/graphql/ci/runner_spec.rb
@@ -9,24 +9,53 @@ RSpec.describe 'Query.runner(id)' do
let_it_be(:group) { create(:group) }
let_it_be(:active_instance_runner) do
- create(:ci_runner, :instance, description: 'Runner 1', contacted_at: 2.hours.ago,
- active: true, version: 'adfe156', revision: 'a', locked: true, ip_address: '127.0.0.1', maximum_timeout: 600,
- access_level: 0, tag_list: %w[tag1 tag2], run_untagged: true, executor_type: :custom,
- maintenance_note: '**Test maintenance note**')
+ create(:ci_runner, :instance,
+ description: 'Runner 1',
+ contacted_at: 2.hours.ago,
+ active: true,
+ version: 'adfe156',
+ revision: 'a',
+ locked: true,
+ ip_address: '127.0.0.1',
+ maximum_timeout: 600,
+ access_level: 0,
+ tag_list: %w[tag1 tag2],
+ run_untagged: true,
+ executor_type: :custom,
+ maintenance_note: '**Test maintenance note**')
end
let_it_be(:inactive_instance_runner) do
- create(:ci_runner, :instance, description: 'Runner 2', contacted_at: 1.day.ago, active: false,
- version: 'adfe157', revision: 'b', ip_address: '10.10.10.10', access_level: 1, run_untagged: true)
+ create(:ci_runner, :instance,
+ description: 'Runner 2',
+ contacted_at: 1.day.ago,
+ active: false,
+ version: 'adfe157',
+ revision: 'b',
+ ip_address: '10.10.10.10',
+ access_level: 1,
+ run_untagged: true)
end
let_it_be(:active_group_runner) do
- create(:ci_runner, :group, groups: [group], description: 'Group runner 1', contacted_at: 2.hours.ago,
- active: true, version: 'adfe156', revision: 'a', locked: true, ip_address: '127.0.0.1', maximum_timeout: 600,
- access_level: 0, tag_list: %w[tag1 tag2], run_untagged: true, executor_type: :shell)
+ create(:ci_runner, :group,
+ groups: [group],
+ description: 'Group runner 1',
+ contacted_at: 2.hours.ago,
+ active: true,
+ version: 'adfe156',
+ revision: 'a',
+ locked: true,
+ ip_address: '127.0.0.1',
+ maximum_timeout: 600,
+ access_level: 0,
+ tag_list: %w[tag1 tag2],
+ run_untagged: true,
+ executor_type: :shell)
end
- let_it_be(:active_project_runner) { create(:ci_runner, :project) }
+ let_it_be(:project1) { create(:project) }
+ let_it_be(:active_project_runner) { create(:ci_runner, :project, projects: [project1]) }
shared_examples 'runner details fetch' do
let(:query) do
@@ -159,8 +188,16 @@ RSpec.describe 'Query.runner(id)' do
with_them do
let(:project_runner) do
- create(:ci_runner, :project, description: 'Runner 3', contacted_at: 1.day.ago, active: false, locked: is_locked,
- version: 'adfe157', revision: 'b', ip_address: '10.10.10.10', access_level: 1, run_untagged: true)
+ create(:ci_runner, :project,
+ description: 'Runner 3',
+ contacted_at: 1.day.ago,
+ active: false,
+ locked: is_locked,
+ version: 'adfe157',
+ revision: 'b',
+ ip_address: '10.10.10.10',
+ access_level: 1,
+ run_untagged: true)
end
let(:query) do
@@ -187,7 +224,6 @@ RSpec.describe 'Query.runner(id)' do
end
describe 'ownerProject' do
- let_it_be(:project1) { create(:project) }
let_it_be(:project2) { create(:project) }
let_it_be(:runner1) { create(:ci_runner, :project, projects: [project2, project1]) }
let_it_be(:runner2) { create(:ci_runner, :project, projects: [project1, project2]) }
@@ -301,7 +337,6 @@ RSpec.describe 'Query.runner(id)' do
end
describe 'for multiple runners' do
- let_it_be(:project1) { create(:project, :test_repo) }
let_it_be(:project2) { create(:project, :test_repo) }
let_it_be(:project_runner1) { create(:ci_runner, :project, projects: [project1, project2], description: 'Runner 1') }
let_it_be(:project_runner2) { create(:ci_runner, :project, projects: [], description: 'Runner 2') }
@@ -394,6 +429,8 @@ RSpec.describe 'Query.runner(id)' do
'jobs' => nil, # returning jobs not allowed for more than 1 runner (see RunnerJobsResolver)
'projectCount' => nil,
'projects' => nil)
+
+ expect_graphql_errors_to_include [/"jobs" field can be requested only for 1 CiRunner\(s\) at a time./]
end
end
end
@@ -472,8 +509,8 @@ RSpec.describe 'Query.runner(id)' do
<<~QUERY
{
instance_runner1: #{runner_query(active_instance_runner)}
- project_runner1: #{runner_query(active_project_runner)}
group_runner1: #{runner_query(active_group_runner)}
+ project_runner1: #{runner_query(active_project_runner)}
}
QUERY
end
@@ -493,12 +530,13 @@ RSpec.describe 'Query.runner(id)' do
it 'does not execute more queries per runner', :aggregate_failures do
# warm-up license cache and so on:
- post_graphql(double_query, current_user: user)
+ personal_access_token = create(:personal_access_token, user: user)
+ args = { current_user: user, token: { personal_access_token: personal_access_token } }
+ post_graphql(double_query, **args)
- control = ActiveRecord::QueryRecorder.new { post_graphql(single_query, current_user: user) }
+ control = ActiveRecord::QueryRecorder.new { post_graphql(single_query, **args) }
- expect { post_graphql(double_query, current_user: user) }
- .not_to exceed_query_limit(control)
+ expect { post_graphql(double_query, **args) }.not_to exceed_query_limit(control)
expect(graphql_data.count).to eq 6
expect(graphql_data).to match(
@@ -528,4 +566,91 @@ RSpec.describe 'Query.runner(id)' do
))
end
end
+
+ describe 'sorting and pagination' do
+ let(:query) do
+ <<~GQL
+ query($id: CiRunnerID!, $projectSearchTerm: String, $n: Int, $cursor: String) {
+ runner(id: $id) {
+ #{fields}
+ }
+ }
+ GQL
+ end
+
+ before do
+ post_graphql(query, current_user: user, variables: variables)
+ end
+
+ context 'with project search term' do
+ let_it_be(:project1) { create(:project, description: 'abc') }
+ let_it_be(:project2) { create(:project, description: 'def') }
+ let_it_be(:project_runner) do
+ create(:ci_runner, :project, projects: [project1, project2])
+ end
+
+ let(:variables) { { id: project_runner.to_global_id.to_s, n: n, project_search_term: search_term } }
+
+ let(:fields) do
+ <<~QUERY
+ projects(search: $projectSearchTerm, first: $n, after: $cursor) {
+ count
+ nodes {
+ id
+ }
+ pageInfo {
+ hasPreviousPage
+ startCursor
+ endCursor
+ hasNextPage
+ }
+ }
+ QUERY
+ end
+
+ let(:projects_data) { graphql_data_at('runner', 'projects') }
+
+ context 'set to empty string' do
+ let(:search_term) { '' }
+
+ context 'with n = 1' do
+ let(:n) { 1 }
+
+ it_behaves_like 'a working graphql query'
+
+ it 'returns paged result' do
+ expect(projects_data).not_to be_nil
+ expect(projects_data['count']).to eq 2
+ expect(projects_data['pageInfo']['hasNextPage']).to eq true
+ end
+ end
+
+ context 'with n = 2' do
+ let(:n) { 2 }
+
+ it 'returns non-paged result' do
+ expect(projects_data).not_to be_nil
+ expect(projects_data['count']).to eq 2
+ expect(projects_data['pageInfo']['hasNextPage']).to eq false
+ end
+ end
+ end
+
+ context 'set to partial match' do
+ let(:search_term) { 'def' }
+
+ context 'with n = 1' do
+ let(:n) { 1 }
+
+ it_behaves_like 'a working graphql query'
+
+ it 'returns paged result with no additional pages' do
+ expect(projects_data).not_to be_nil
+ expect(projects_data['count']).to eq 1
+ expect(projects_data['pageInfo']['hasNextPage']).to eq false
+ end
+ end
+ end
+ end
+ end
end
diff --git a/spec/requests/api/graphql/ci/runners_spec.rb b/spec/requests/api/graphql/ci/runners_spec.rb
index 749f6839cb5..3054b866812 100644
--- a/spec/requests/api/graphql/ci/runners_spec.rb
+++ b/spec/requests/api/graphql/ci/runners_spec.rb
@@ -69,15 +69,6 @@ RSpec.describe 'Query.runners' do
it_behaves_like 'a working graphql query returning expected runner'
end
-
- context 'runner_type is PROJECT_TYPE and status is NEVER_CONTACTED' do
- let(:runner_type) { 'PROJECT_TYPE' }
- let(:status) { 'NEVER_CONTACTED' }
-
- let!(:expected_runner) { project_runner }
-
- it_behaves_like 'a working graphql query returning expected runner'
- end
end
describe 'pagination' do
@@ -141,8 +132,13 @@ RSpec.describe 'Group.runners' do
describe 'edges' do
let_it_be(:runner) do
- create(:ci_runner, :group, active: false, version: 'def', revision: '456',
- description: 'Project runner', groups: [group], ip_address: '127.0.0.1')
+ create(:ci_runner, :group,
+ active: false,
+ version: 'def',
+ revision: '456',
+ description: 'Project runner',
+ groups: [group],
+ ip_address: '127.0.0.1')
end
let(:query) do
diff --git a/spec/requests/api/graphql/custom_emoji_query_spec.rb b/spec/requests/api/graphql/custom_emoji_query_spec.rb
index 13b7a22e791..5dd5ad117b0 100644
--- a/spec/requests/api/graphql/custom_emoji_query_spec.rb
+++ b/spec/requests/api/graphql/custom_emoji_query_spec.rb
@@ -35,7 +35,17 @@ RSpec.describe 'getting custom emoji within namespace' do
expect(graphql_data['group']['customEmoji']['nodes'].first['name']).to eq(custom_emoji.name)
end
- it 'returns nil when unauthorised' do
+ it 'returns nil custom emoji 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
+ end
+
+ it 'returns nil group when unauthorised' do
user = create(:user)
post_graphql(custom_emoji_query(group), current_user: user)
diff --git a/spec/requests/api/graphql/environments/deployments_query_spec.rb b/spec/requests/api/graphql/environments/deployments_query_spec.rb
new file mode 100644
index 00000000000..6da00057449
--- /dev/null
+++ b/spec/requests/api/graphql/environments/deployments_query_spec.rb
@@ -0,0 +1,487 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Environments Deployments query' do
+ include GraphqlHelpers
+
+ let_it_be(:project) { create(:project, :private, :repository) }
+ let_it_be(:environment) { create(:environment, project: project) }
+ let_it_be(:developer) { create(:user).tap { |u| project.add_developer(u) } }
+ let_it_be(:guest) { create(:user).tap { |u| project.add_guest(u) } }
+
+ let(:user) { developer }
+
+ subject { GitlabSchema.execute(query, context: { current_user: user }).as_json }
+
+ context 'when there are deployments in the environment' do
+ let_it_be(:finished_deployment_old) do
+ create(:deployment, :success, environment: environment, project: project, finished_at: 2.days.ago)
+ end
+
+ let_it_be(:finished_deployment_new) do
+ create(:deployment, :success, environment: environment, project: project, finished_at: 1.day.ago)
+ end
+
+ let_it_be(:upcoming_deployment_old) do
+ create(:deployment, :created, environment: environment, project: project, created_at: 2.hours.ago)
+ end
+
+ let_it_be(:upcoming_deployment_new) do
+ create(:deployment, :created, environment: environment, project: project, created_at: 1.hour.ago)
+ end
+
+ let_it_be(:other_environment) { create(:environment, project: project) }
+ let_it_be(:other_deployment) { create(:deployment, :success, environment: other_environment, project: project) }
+
+ let(:query) do
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ environment(name: "#{environment.name}") {
+ deployments {
+ nodes {
+ id
+ iid
+ ref
+ tag
+ sha
+ createdAt
+ updatedAt
+ finishedAt
+ status
+ }
+ }
+ }
+ }
+ }
+ )
+ end
+
+ it 'returns all deployments of the environment' do
+ deployments = subject.dig('data', 'project', 'environment', 'deployments', 'nodes')
+
+ expect(deployments.count).to eq(4)
+ end
+
+ context 'when query last deployment' do
+ let(:query) do
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ environment(name: "#{environment.name}") {
+ deployments(statuses: [SUCCESS], orderBy: { finishedAt: DESC }, first: 1) {
+ nodes {
+ iid
+ }
+ }
+ }
+ }
+ }
+ )
+ end
+
+ it 'returns deployment' do
+ deployments = subject.dig('data', 'project', 'environment', 'deployments', 'nodes')
+
+ expect(deployments.count).to eq(1)
+ expect(deployments[0]['iid']).to eq(finished_deployment_new.iid.to_s)
+ end
+ end
+
+ context 'when query latest upcoming deployment' do
+ let(:query) do
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ environment(name: "#{environment.name}") {
+ deployments(statuses: [CREATED RUNNING BLOCKED], orderBy: { createdAt: DESC }, first: 1) {
+ nodes {
+ iid
+ }
+ }
+ }
+ }
+ }
+ )
+ end
+
+ it 'returns deployment' do
+ deployments = subject.dig('data', 'project', 'environment', 'deployments', 'nodes')
+
+ expect(deployments.count).to eq(1)
+ expect(deployments[0]['iid']).to eq(upcoming_deployment_new.iid.to_s)
+ end
+ end
+
+ context 'when query finished deployments in descending order' do
+ let(:query) do
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ environment(name: "#{environment.name}") {
+ deployments(statuses: [SUCCESS FAILED CANCELED], orderBy: { finishedAt: DESC }) {
+ nodes {
+ iid
+ }
+ }
+ }
+ }
+ }
+ )
+ end
+
+ it 'returns deployments' do
+ deployments = subject.dig('data', 'project', 'environment', 'deployments', 'nodes')
+
+ expect(deployments.count).to eq(2)
+ expect(deployments[0]['iid']).to eq(finished_deployment_new.iid.to_s)
+ expect(deployments[1]['iid']).to eq(finished_deployment_old.iid.to_s)
+ end
+ end
+
+ context 'when query finished deployments in ascending order' do
+ let(:query) do
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ environment(name: "#{environment.name}") {
+ deployments(statuses: [SUCCESS FAILED CANCELED], orderBy: { finishedAt: ASC }) {
+ nodes {
+ iid
+ }
+ }
+ }
+ }
+ }
+ )
+ end
+
+ it 'returns deployments' do
+ deployments = subject.dig('data', 'project', 'environment', 'deployments', 'nodes')
+
+ expect(deployments.count).to eq(2)
+ expect(deployments[0]['iid']).to eq(finished_deployment_old.iid.to_s)
+ expect(deployments[1]['iid']).to eq(finished_deployment_new.iid.to_s)
+ end
+ end
+
+ context 'when query upcoming deployments in descending order' do
+ let(:query) do
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ environment(name: "#{environment.name}") {
+ deployments(statuses: [CREATED RUNNING BLOCKED], orderBy: { createdAt: DESC }) {
+ nodes {
+ iid
+ }
+ }
+ }
+ }
+ }
+ )
+ end
+
+ it 'returns deployments' do
+ deployments = subject.dig('data', 'project', 'environment', 'deployments', 'nodes')
+
+ expect(deployments.count).to eq(2)
+ expect(deployments[0]['iid']).to eq(upcoming_deployment_new.iid.to_s)
+ expect(deployments[1]['iid']).to eq(upcoming_deployment_old.iid.to_s)
+ end
+ end
+
+ context 'when query upcoming deployments in ascending order' do
+ let(:query) do
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ environment(name: "#{environment.name}") {
+ deployments(statuses: [CREATED RUNNING BLOCKED], orderBy: { createdAt: ASC }) {
+ nodes {
+ iid
+ }
+ }
+ }
+ }
+ }
+ )
+ end
+
+ it 'returns deployments' do
+ deployments = subject.dig('data', 'project', 'environment', 'deployments', 'nodes')
+
+ expect(deployments.count).to eq(2)
+ expect(deployments[0]['iid']).to eq(upcoming_deployment_old.iid.to_s)
+ expect(deployments[1]['iid']).to eq(upcoming_deployment_new.iid.to_s)
+ end
+ end
+
+ context 'when query last deployments of multiple environments' do
+ let(:query) do
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ environments {
+ nodes {
+ name
+ deployments(statuses: [SUCCESS], orderBy: { finishedAt: DESC }, first: 1) {
+ nodes {
+ iid
+ }
+ }
+ }
+ }
+ }
+ }
+ )
+ end
+
+ it 'returns an error for preventing N+1 queries' do
+ expect(subject['errors'][0]['message'])
+ .to include('"deployments" field can be requested only for 1 Environment(s) at a time.')
+ end
+ end
+
+ context 'when query finished and upcoming deployments together' do
+ let(:query) do
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ environment(name: "#{environment.name}") {
+ deployments(statuses: [CREATED SUCCESS]) {
+ nodes {
+ iid
+ }
+ }
+ }
+ }
+ }
+ )
+ end
+
+ it 'raises an error' do
+ expect { subject }.to raise_error(DeploymentsFinder::InefficientQueryError)
+ end
+ end
+
+ context 'when multiple orderBy input are specified' do
+ let(:query) do
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ environment(name: "#{environment.name}") {
+ deployments(orderBy: { finishedAt: DESC, createdAt: ASC }) {
+ nodes {
+ iid
+ }
+ }
+ }
+ }
+ }
+ )
+ end
+
+ it 'raises an error' do
+ expect(subject['errors'][0]['message']).to include('orderBy parameter must contain one key-value pair.')
+ end
+ end
+
+ context 'when user is guest' do
+ let(:user) { guest }
+
+ it 'returns nothing' do
+ expect(subject['data']['project']['environment']).to be_nil
+ end
+ end
+
+ shared_examples_for 'avoids N+1 database queries' do
+ it 'does not increase the query count' do
+ create_deployments
+
+ baseline = ActiveRecord::QueryRecorder.new do
+ run_with_clean_state(query, context: { current_user: user })
+ end
+
+ create_deployments
+
+ multi = ActiveRecord::QueryRecorder.new do
+ run_with_clean_state(query, context: { current_user: user })
+ end
+
+ expect(multi).not_to exceed_query_limit(baseline)
+ end
+
+ def create_deployments
+ create_list(:deployment, 3, environment: environment, project: project).each do |deployment|
+ deployment.user = create(:user).tap { |u| project.add_developer(u) }
+ deployment.deployable =
+ create(:ci_build, project: project, environment: environment.name, deployment: deployment,
+ user: deployment.user)
+
+ deployment.save!
+ end
+ end
+ end
+
+ context 'when requesting commits of deployments' do
+ let(:query) do
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ environment(name: "#{environment.name}") {
+ deployments {
+ nodes {
+ iid
+ commit {
+ author {
+ avatarUrl
+ name
+ webPath
+ }
+ fullTitle
+ webPath
+ sha
+ }
+ }
+ }
+ }
+ }
+ }
+ )
+ end
+
+ it_behaves_like 'avoids N+1 database queries'
+
+ it 'returns commits of deployments' do
+ deployments = subject.dig('data', 'project', 'environment', 'deployments', 'nodes')
+
+ deployments.each do |deployment|
+ deployment_in_record = project.deployments.find_by_iid(deployment['iid'])
+
+ expect(deployment_in_record.sha).to eq(deployment['commit']['sha'])
+ end
+ end
+ end
+
+ context 'when requesting triggerers of deployments' do
+ let(:query) do
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ environment(name: "#{environment.name}") {
+ deployments {
+ nodes {
+ iid
+ triggerer {
+ id
+ avatarUrl
+ name
+ webPath
+ }
+ }
+ }
+ }
+ }
+ }
+ )
+ end
+
+ it_behaves_like 'avoids N+1 database queries'
+
+ it 'returns triggerers of deployments' do
+ deployments = subject.dig('data', 'project', 'environment', 'deployments', 'nodes')
+
+ deployments.each do |deployment|
+ deployment_in_record = project.deployments.find_by_iid(deployment['iid'])
+
+ expect(deployment_in_record.deployed_by.name).to eq(deployment['triggerer']['name'])
+ end
+ end
+ end
+
+ context 'when requesting jobs of deployments' do
+ let(:query) do
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ environment(name: "#{environment.name}") {
+ deployments {
+ nodes {
+ iid
+ job {
+ id
+ status
+ name
+ webPath
+ }
+ }
+ }
+ }
+ }
+ }
+ )
+ end
+
+ it_behaves_like 'avoids N+1 database queries'
+
+ it 'returns jobs of deployments' do
+ deployments = subject.dig('data', 'project', 'environment', 'deployments', 'nodes')
+
+ deployments.each do |deployment|
+ deployment_in_record = project.deployments.find_by_iid(deployment['iid'])
+
+ expect(deployment_in_record.build.to_global_id.to_s).to eq(deployment['job']['id'])
+ end
+ end
+ end
+
+ describe 'sorting and pagination' do
+ let(:data_path) { [:project, :environment, :deployments] }
+ let(:current_user) { user }
+
+ def pagination_query(params)
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ environment(name: "#{environment.name}") {
+ deployments(statuses: [SUCCESS], #{params}) {
+ nodes {
+ iid
+ }
+ pageInfo {
+ startCursor
+ endCursor
+ hasNextPage
+ hasPreviousPage
+ }
+ }
+ }
+ }
+ }
+ )
+ end
+
+ def pagination_results_data(nodes)
+ nodes.map { |deployment| deployment['iid'].to_i }
+ end
+
+ context 'when sorting by finished_at in ascending order' do
+ it_behaves_like 'sorted paginated query' do
+ let(:sort_argument) { graphql_args(orderBy: { finishedAt: :ASC }) }
+ let(:first_param) { 2 }
+ let(:all_records) { [finished_deployment_old.iid, finished_deployment_new.iid] }
+ end
+ end
+
+ context 'when sorting by finished_at in descending order' do
+ it_behaves_like 'sorted paginated query' do
+ let(:sort_argument) { graphql_args(orderBy: { finishedAt: :DESC }) }
+ let(:first_param) { 2 }
+ let(:all_records) { [finished_deployment_new.iid, finished_deployment_old.iid] }
+ end
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/group/group_members_spec.rb b/spec/requests/api/graphql/group/group_members_spec.rb
index bab8d5b770c..5f8becc0726 100644
--- a/spec/requests/api/graphql/group/group_members_spec.rb
+++ b/spec/requests/api/graphql/group/group_members_spec.rb
@@ -156,13 +156,20 @@ RSpec.describe 'getting group members information' do
expect_array_response(child_user)
end
- it 'returns invited members plus inherited members' do
+ it 'returns invited members and inherited members of a shared group' do
fetch_members(group: child_group, args: { relations: [:DIRECT, :INHERITED, :SHARED_FROM_GROUPS] })
expect(graphql_errors).to be_nil
expect_array_response(invited_user, user_1, user_2, child_user)
end
+ it 'returns invited members and inherited members of an ancestor of a shared group' do
+ fetch_members(group: grandchild_group, args: { relations: [:DIRECT, :INHERITED, :SHARED_FROM_GROUPS] })
+
+ expect(graphql_errors).to be_nil
+ expect_array_response(grandchild_user, invited_user, user_1, user_2, child_user)
+ end
+
it 'returns direct and inherited members' do
fetch_members(group: child_group, args: { relations: [:DIRECT, :INHERITED] })
diff --git a/spec/requests/api/graphql/group/packages_spec.rb b/spec/requests/api/graphql/group/packages_spec.rb
index adee556db3a..cf8736db5af 100644
--- a/spec/requests/api/graphql/group/packages_spec.rb
+++ b/spec/requests/api/graphql/group/packages_spec.rb
@@ -39,7 +39,7 @@ RSpec.describe 'getting a package list for a group' do
it 'returns an error for the second group and data for the first' do
expect(a_packages_names).to contain_exactly(group_one_package.name)
- expect_graphql_errors_to_include [/Packages can be requested only for one group at a time/]
+ expect_graphql_errors_to_include [/"packages" field can be requested only for 1 Group\(s\) at a time./]
expect(graphql_data_at(:b, :packages)).to be(nil)
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 a33e3ae5427..d6b0673e4f8 100644
--- a/spec/requests/api/graphql/group/work_item_types_spec.rb
+++ b/spec/requests/api/graphql/group/work_item_types_spec.rb
@@ -46,7 +46,7 @@ RSpec.describe 'getting a list of work item types for a group' do
end
end
- context "when user doesn't have acces to the group" do
+ context "when user doesn't have access to the group" do
let(:current_user) { create(:user) }
before do
diff --git a/spec/requests/api/graphql/mutations/boards/issues/issue_move_list_spec.rb b/spec/requests/api/graphql/mutations/boards/issues/issue_move_list_spec.rb
index 46ec22e7ef8..06093e9f7c2 100644
--- a/spec/requests/api/graphql/mutations/boards/issues/issue_move_list_spec.rb
+++ b/spec/requests/api/graphql/mutations/boards/issues/issue_move_list_spec.rb
@@ -100,6 +100,20 @@ RSpec.describe 'Reposition and move issue within board lists' do
expect(response_issue['labels']['edges'][0]['node']['title']).to eq(testing.title)
end
end
+
+ context 'when moving an issue using position_in_list' do
+ let(:issue_move_params) { { from_list_id: list1.id, to_list_id: list2.id, position_in_list: 0 } }
+
+ it 'repositions an issue' do
+ post_graphql_mutation(mutation(params), current_user: current_user)
+
+ expect(response).to have_gitlab_http_status(:success)
+ response_issue = json_response['data'][mutation_result_identifier]['issue']
+ expect(response_issue['iid']).to eq(issue1.iid.to_s)
+ expect(response_issue['labels']['edges'][0]['node']['title']).to eq(testing.title)
+ expect(response_issue['relativePosition']).to be < existing_issue1.relative_position
+ end
+ end
end
context 'when user has no access to resources' do
diff --git a/spec/requests/api/graphql/mutations/branches/create_spec.rb b/spec/requests/api/graphql/mutations/branches/create_spec.rb
index 6a098002963..9ee2f41e8fc 100644
--- a/spec/requests/api/graphql/mutations/branches/create_spec.rb
+++ b/spec/requests/api/graphql/mutations/branches/create_spec.rb
@@ -5,26 +5,18 @@ require 'spec_helper'
RSpec.describe 'Creation of a new branch' do
include GraphqlHelpers
+ let_it_be(:group) { create(:group, :public) }
let_it_be(:current_user) { create(:user) }
- let_it_be(:project) { create(:project, :public, :empty_repo) }
let(:input) { { project_path: project.full_path, name: new_branch, ref: ref } }
- let(:new_branch) { 'new_branch' }
+ let(:new_branch) { "new_branch_#{SecureRandom.hex(4)}" }
let(:ref) { 'master' }
let(:mutation) { graphql_mutation(:create_branch, input) }
let(:mutation_response) { graphql_mutation_response(:create_branch) }
- context 'the user is not allowed to create a branch' do
- it_behaves_like 'a mutation that returns a top-level access error'
- end
-
- context 'when user has permissions to create a branch' do
- before do
- project.add_developer(current_user)
- end
-
- it 'creates a new branch' do
+ shared_examples 'creates a new branch' do
+ specify do
post_graphql_mutation(mutation, current_user: current_user)
expect(response).to have_gitlab_http_status(:success)
@@ -33,14 +25,75 @@ RSpec.describe 'Creation of a new branch' do
'commit' => a_hash_including('id')
)
end
+ end
+
+ context 'when project is public' do
+ let_it_be(:project) { create(:project, :public, :empty_repo) }
+
+ context 'when user is not allowed to create a branch' do
+ it_behaves_like 'a mutation that returns a top-level access error'
+ end
+
+ context 'when user is a direct project member' do
+ context 'and user is a developer' do
+ before do
+ project.add_developer(current_user)
+ end
+
+ it_behaves_like 'creates a new branch'
+
+ context 'when ref is not correct' do
+ err_msg = 'Failed to create branch \'another_branch\': invalid reference name \'unknown\''
+ let(:new_branch) { 'another_branch' }
+ let(:ref) { 'unknown' }
+
+ it_behaves_like 'a mutation that returns errors in the response', errors: [err_msg]
+ end
+ end
+ end
+
+ context 'when user is an inherited member from the group' do
+ context 'when project has a private repository' do
+ let_it_be(:project) { create(:project, :public, :empty_repo, :repository_private, group: group) }
+
+ context 'and user is a guest' do
+ before do
+ group.add_guest(current_user)
+ end
+
+ it_behaves_like 'a mutation that returns a top-level access error'
+ end
+
+ context 'and user is a developer' do
+ before do
+ group.add_developer(current_user)
+ end
+
+ it_behaves_like 'creates a new branch'
+ end
+ end
+ end
+ end
+
+ context 'when project is private' do
+ let_it_be(:project) { create(:project, :private, :empty_repo, group: group) }
+
+ context 'when user is an inherited member from the group' do
+ context 'and user is a guest' do
+ before do
+ group.add_guest(current_user)
+ end
+
+ it_behaves_like 'a mutation that returns a top-level access error'
+ end
- context 'when ref is not correct' do
- err_msg = 'Failed to create branch \'another_branch\': invalid reference name \'unknown\''
- let(:new_branch) { 'another_branch' }
- let(:ref) { 'unknown' }
+ context 'and user is a developer' do
+ before do
+ group.add_developer(current_user)
+ end
- it_behaves_like 'a mutation that returns errors in the response',
- errors: [err_msg]
+ it_behaves_like 'creates a new branch'
+ end
end
end
end
diff --git a/spec/requests/api/graphql/mutations/ci/job/destroy_spec.rb b/spec/requests/api/graphql/mutations/ci/job/destroy_spec.rb
new file mode 100644
index 00000000000..5855eb6bb51
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/ci/job/destroy_spec.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'JobArtifactsDestroy' do
+ include GraphqlHelpers
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:job) { create(:ci_build) }
+
+ let(:mutation) do
+ variables = {
+ id: job.to_global_id.to_s
+ }
+ graphql_mutation(:job_artifacts_destroy, variables, <<~FIELDS)
+ job {
+ name
+ }
+ destroyedArtifactsCount
+ errors
+ FIELDS
+ end
+
+ before do
+ create(:ci_job_artifact, :archive, job: job)
+ create(:ci_job_artifact, :junit, job: job)
+ end
+
+ it 'returns an error if the user is not allowed to destroy the job artifacts' do
+ post_graphql_mutation(mutation, current_user: user)
+
+ expect(graphql_errors).not_to be_empty
+ expect(job.reload.job_artifacts.count).to be(2)
+ end
+
+ it 'destroys the job artifacts and returns the expected data' do
+ job.project.add_maintainer(user)
+ expected_data = {
+ 'jobArtifactsDestroy' => {
+ 'errors' => [],
+ 'destroyedArtifactsCount' => 2,
+ 'job' => {
+ 'name' => job.name
+ }
+ }
+ }
+
+ post_graphql_mutation(mutation, current_user: user)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(graphql_data).to eq(expected_data)
+ expect(job.reload.job_artifacts.count).to be(0)
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/ci/job_artifact/destroy_spec.rb b/spec/requests/api/graphql/mutations/ci/job_artifact/destroy_spec.rb
new file mode 100644
index 00000000000..a5ec9ea343d
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/ci/job_artifact/destroy_spec.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'ArtifactDestroy' do
+ include GraphqlHelpers
+
+ let(:user) { create(:user) }
+ let(:artifact) { create(:ci_job_artifact) }
+
+ let(:mutation) do
+ variables = {
+ id: artifact.to_global_id.to_s
+ }
+ graphql_mutation(:artifact_destroy, variables, 'errors')
+ end
+
+ it 'returns an error if the user is not allowed to destroy the artifact' do
+ post_graphql_mutation(mutation, current_user: user)
+
+ expect(graphql_errors).not_to be_empty
+ end
+
+ context 'when the user is allowed to destroy the artifact' do
+ before do
+ artifact.job.project.add_maintainer(user)
+ end
+
+ it 'destroys the artifact' do
+ post_graphql_mutation(mutation, current_user: user)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect { artifact.reload }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+
+ it 'returns error if destory fails' do
+ allow_next_found_instance_of(Ci::JobArtifact) do |instance|
+ allow(instance).to receive(:destroy).and_return(false)
+ allow(instance).to receive_message_chain(:errors, :full_messages).and_return(['cannot be removed'])
+ end
+
+ post_graphql_mutation(mutation, current_user: user)
+
+ expect(graphql_data_at(:artifact_destroy, :errors)).to contain_exactly('cannot be removed')
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/custom_emoji/create_spec.rb b/spec/requests/api/graphql/mutations/custom_emoji/create_spec.rb
index c91437fa355..66facdebe78 100644
--- a/spec/requests/api/graphql/mutations/custom_emoji/create_spec.rb
+++ b/spec/requests/api/graphql/mutations/custom_emoji/create_spec.rb
@@ -39,5 +39,19 @@ RSpec.describe 'Creation of a new Custom Emoji' do
expect(gql_response['customEmoji']['name']).to eq(attributes[:name])
expect(gql_response['customEmoji']['url']).to eq(attributes[:url])
end
+
+ context 'when the custom_emoji feature flag is disabled' do
+ before do
+ stub_feature_flags(custom_emoji: false)
+ end
+
+ it 'does nothing and returns and error' do
+ expect do
+ post_graphql_mutation(mutation, current_user: current_user)
+ end.to not_change(CustomEmoji, :count)
+
+ expect_graphql_errors_to_include('Custom emoji feature is disabled')
+ end
+ end
end
end
diff --git a/spec/requests/api/graphql/mutations/custom_emoji/destroy_spec.rb b/spec/requests/api/graphql/mutations/custom_emoji/destroy_spec.rb
index 07fd57a2cee..7d25206e617 100644
--- a/spec/requests/api/graphql/mutations/custom_emoji/destroy_spec.rb
+++ b/spec/requests/api/graphql/mutations/custom_emoji/destroy_spec.rb
@@ -68,6 +68,20 @@ RSpec.describe 'Deletion of custom emoji' do
end
it_behaves_like 'deletes custom emoji'
+
+ context 'when the custom_emoji feature flag is disabled' do
+ before do
+ stub_feature_flags(custom_emoji: false)
+ end
+
+ it_behaves_like 'does not delete custom emoji'
+
+ it 'returns an error' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect_graphql_errors_to_include('Custom emoji feature is disabled')
+ end
+ end
end
end
end
diff --git a/spec/requests/api/graphql/mutations/incident_management/timeline_event/promote_from_note_spec.rb b/spec/requests/api/graphql/mutations/incident_management/timeline_event/promote_from_note_spec.rb
index 9272e218172..85eaec90f47 100644
--- a/spec/requests/api/graphql/mutations/incident_management/timeline_event/promote_from_note_spec.rb
+++ b/spec/requests/api/graphql/mutations/incident_management/timeline_event/promote_from_note_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
RSpec.describe 'Promote an incident timeline event from a comment' do
include GraphqlHelpers
+ include NotesHelper
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) }
@@ -53,7 +54,7 @@ RSpec.describe 'Promote an incident timeline event from a comment' do
'promotedFromNote' => {
'id' => comment.to_global_id.to_s
},
- 'note' => comment.note,
+ 'note' => "@#{comment.author.username} [commented](#{noteable_note_url(comment)}): '#{comment.note}'",
'action' => 'comment',
'editable' => true,
'occurredAt' => comment.created_at.iso8601
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 608b36e4f15..8cec5867aca 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
@@ -93,6 +93,16 @@ RSpec.describe 'Setting assignees of a merge request', :assume_throttled do
expect(response).to have_gitlab_http_status(:success)
expect(mutation_assignee_nodes).to match_array(expected_result)
end
+
+ it 'triggers webhooks', :sidekiq_inline do
+ hook = create(:project_hook, merge_requests_events: true, project: merge_request.project)
+
+ expect(WebHookWorker).to receive(:perform_async).with(hook.id, anything, 'merge_request_hooks', anything)
+
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(response).to have_gitlab_http_status(:success)
+ end
end
context 'when passing an empty list of assignees' do
diff --git a/spec/requests/api/graphql/mutations/releases/update_spec.rb b/spec/requests/api/graphql/mutations/releases/update_spec.rb
index 33d4e57904c..240db764f40 100644
--- a/spec/requests/api/graphql/mutations/releases/update_spec.rb
+++ b/spec/requests/api/graphql/mutations/releases/update_spec.rb
@@ -22,9 +22,14 @@ RSpec.describe 'Updating an existing release' do
let_it_be(:milestones) { [milestone_12_3, milestone_12_4] }
let_it_be(:release) do
- create(:release, project: project, tag: tag_name, name: name,
- description: description, released_at: Time.parse(released_at).utc,
- created_at: Time.parse(created_at).utc, milestones: milestones)
+ create(:release,
+ project: project,
+ tag: tag_name,
+ name: name,
+ description: description,
+ released_at: Time.parse(released_at).utc,
+ created_at: Time.parse(created_at).utc,
+ milestones: milestones)
end
let(:mutation_name) { :release_update }
diff --git a/spec/requests/api/graphql/packages/composer_spec.rb b/spec/requests/api/graphql/packages/composer_spec.rb
index 9830623ede8..89c01d44771 100644
--- a/spec/requests/api/graphql/packages/composer_spec.rb
+++ b/spec/requests/api/graphql/packages/composer_spec.rb
@@ -5,7 +5,7 @@ RSpec.describe 'package details' do
include GraphqlHelpers
include_context 'package details setup'
- let_it_be(:package) { create(:composer_package, project: project) }
+ let_it_be(:package) { create(:composer_package, :last_downloaded_at, project: project) }
let_it_be(:composer_json) { { name: 'name', type: 'type', license: 'license', version: 1 } }
let_it_be(:composer_metadatum) do
# we are forced to manually create the metadatum, without using the factory to force the sha to be a string
diff --git a/spec/requests/api/graphql/packages/conan_spec.rb b/spec/requests/api/graphql/packages/conan_spec.rb
index 5bd5a71bbeb..7ad85edecef 100644
--- a/spec/requests/api/graphql/packages/conan_spec.rb
+++ b/spec/requests/api/graphql/packages/conan_spec.rb
@@ -5,7 +5,7 @@ RSpec.describe 'conan package details' do
include GraphqlHelpers
include_context 'package details setup'
- let_it_be(:package) { create(:conan_package, project: project) }
+ let_it_be(:package) { create(:conan_package, :last_downloaded_at, project: project) }
let(:metadata) { query_graphql_fragment('ConanMetadata') }
let(:package_files_metadata) { query_graphql_fragment('ConanFileMetadata') }
diff --git a/spec/requests/api/graphql/packages/helm_spec.rb b/spec/requests/api/graphql/packages/helm_spec.rb
index 1675b8faa23..79a589e2dc2 100644
--- a/spec/requests/api/graphql/packages/helm_spec.rb
+++ b/spec/requests/api/graphql/packages/helm_spec.rb
@@ -5,7 +5,7 @@ RSpec.describe 'helm package details' do
include GraphqlHelpers
include_context 'package details setup'
- let_it_be(:package) { create(:helm_package, project: project) }
+ let_it_be(:package) { create(:helm_package, :last_downloaded_at, project: project) }
let(:package_files_metadata) { query_graphql_fragment('HelmFileMetadata') }
diff --git a/spec/requests/api/graphql/packages/maven_spec.rb b/spec/requests/api/graphql/packages/maven_spec.rb
index 9d59a922660..b7f39efcf73 100644
--- a/spec/requests/api/graphql/packages/maven_spec.rb
+++ b/spec/requests/api/graphql/packages/maven_spec.rb
@@ -5,7 +5,7 @@ RSpec.describe 'maven package details' do
include GraphqlHelpers
include_context 'package details setup'
- let_it_be(:package) { create(:maven_package, project: project) }
+ let_it_be(:package) { create(:maven_package, :last_downloaded_at, project: project) }
let(:metadata) { query_graphql_fragment('MavenMetadata') }
@@ -31,7 +31,9 @@ RSpec.describe 'maven package details' do
context 'a versionless maven package' do
let_it_be(:maven_metadatum) { create(:maven_metadatum, app_version: nil) }
- let_it_be(:package) { create(:maven_package, project: project, version: nil, maven_metadatum: maven_metadatum) }
+ let_it_be(:package) do
+ create(:maven_package, :last_downloaded_at, project: project, version: nil, maven_metadatum: maven_metadatum)
+ end
subject { post_graphql(query, current_user: user) }
diff --git a/spec/requests/api/graphql/packages/nuget_spec.rb b/spec/requests/api/graphql/packages/nuget_spec.rb
index 87cffc67ce5..7de132d1574 100644
--- a/spec/requests/api/graphql/packages/nuget_spec.rb
+++ b/spec/requests/api/graphql/packages/nuget_spec.rb
@@ -5,7 +5,7 @@ RSpec.describe 'nuget package details' do
include GraphqlHelpers
include_context 'package details setup'
- let_it_be(:package) { create(:nuget_package, :with_metadatum, project: project) }
+ let_it_be(:package) { create(:nuget_package, :last_downloaded_at, :with_metadatum, project: project) }
let_it_be(:dependency_link) { create(:packages_dependency_link, :with_nuget_metadatum, package: package) }
let(:metadata) { query_graphql_fragment('NugetMetadata') }
diff --git a/spec/requests/api/graphql/packages/package_spec.rb b/spec/requests/api/graphql/packages/package_spec.rb
index c28b37db5af..e9f82d66775 100644
--- a/spec/requests/api/graphql/packages/package_spec.rb
+++ b/spec/requests/api/graphql/packages/package_spec.rb
@@ -6,8 +6,8 @@ RSpec.describe 'package details' do
let_it_be_with_reload(:group) { create(:group) }
let_it_be_with_reload(:project) { create(:project, group: group) }
+ let_it_be_with_reload(:composer_package) { create(:composer_package, :last_downloaded_at, project: project) }
let_it_be(:user) { create(:user) }
- let_it_be(:composer_package) { create(:composer_package, project: project) }
let_it_be(:composer_json) { { name: 'name', type: 'type', license: 'license', version: 1 } }
let_it_be(:composer_metadatum) do
# we are forced to manually create the metadatum, without using the factory to force the sha to be a string
@@ -65,6 +65,17 @@ RSpec.describe 'package details' do
end
end
+ context 'with package without last_downloaded_at' do
+ before do
+ composer_package.update!(last_downloaded_at: nil)
+ subject
+ end
+
+ it 'matches the JSON schema' do
+ expect(package_details).to match_schema('graphql/packages/package_details')
+ end
+ end
+
context 'with package files pending destruction' do
let_it_be(:package_file) { create(:package_file, package: composer_package) }
let_it_be(:package_file_pending_destruction) { create(:package_file, :pending_destruction, package: composer_package) }
@@ -97,7 +108,7 @@ RSpec.describe 'package details' do
expect(graphql_data_at(:a, :name)).to eq(composer_package.name)
- expect_graphql_errors_to_include [/Package details can be requested only for one package at a time/]
+ expect_graphql_errors_to_include [/"package" field can be requested only for 1 Query\(s\) at a time./]
expect(graphql_data_at(:b)).to be(nil)
end
end
diff --git a/spec/requests/api/graphql/packages/pypi_spec.rb b/spec/requests/api/graphql/packages/pypi_spec.rb
index 0cc5bd2e3b2..c0e589f3597 100644
--- a/spec/requests/api/graphql/packages/pypi_spec.rb
+++ b/spec/requests/api/graphql/packages/pypi_spec.rb
@@ -5,7 +5,7 @@ RSpec.describe 'pypi package details' do
include GraphqlHelpers
include_context 'package details setup'
- let_it_be(:package) { create(:pypi_package, project: project) }
+ let_it_be(:package) { create(:pypi_package, :last_downloaded_at, project: project) }
let(:metadata) { query_graphql_fragment('PypiMetadata') }
diff --git a/spec/requests/api/graphql/project/branch_protections/merge_access_levels_spec.rb b/spec/requests/api/graphql/project/branch_protections/merge_access_levels_spec.rb
new file mode 100644
index 00000000000..cb5006ec8e4
--- /dev/null
+++ b/spec/requests/api/graphql/project/branch_protections/merge_access_levels_spec.rb
@@ -0,0 +1,109 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'getting merge access levels for a branch protection' do
+ include GraphqlHelpers
+
+ let_it_be(:current_user) { create(:user) }
+
+ let(:merge_access_level_data) { merge_access_levels_data[0] }
+
+ let(:merge_access_levels_data) do
+ graphql_data_at('project',
+ 'branchRules',
+ 'nodes',
+ 0,
+ 'branchProtection',
+ 'mergeAccessLevels',
+ 'nodes')
+ end
+
+ let(:project) { protected_branch.project }
+
+ let(:merge_access_levels_count) { protected_branch.merge_access_levels.size }
+
+ let(:variables) { { path: project.full_path } }
+
+ let(:fields) { all_graphql_fields_for('MergeAccessLevel') }
+
+ let(:query) do
+ <<~GQL
+ query($path: ID!) {
+ project(fullPath: $path) {
+ branchRules(first: 1) {
+ nodes {
+ branchProtection {
+ mergeAccessLevels {
+ nodes {
+ #{fields}
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ GQL
+ end
+
+ context 'when the user does not have read_protected_branch abilities' do
+ let_it_be(:protected_branch) { create(:protected_branch) }
+
+ before do
+ project.add_guest(current_user)
+ post_graphql(query, current_user: current_user, variables: variables)
+ end
+
+ it_behaves_like 'a working graphql query'
+
+ it { expect(merge_access_levels_data).not_to be_present }
+ end
+
+ shared_examples 'merge access request' do
+ let(:merge_access) { protected_branch.merge_access_levels.first }
+
+ before do
+ project.add_maintainer(current_user)
+ post_graphql(query, current_user: current_user, variables: variables)
+ end
+
+ it_behaves_like 'a working graphql query'
+
+ it 'returns all merge access levels' do
+ expect(merge_access_levels_data.size).to eq(merge_access_levels_count)
+ end
+
+ it 'includes access_level' do
+ expect(merge_access_level_data['accessLevel'])
+ .to eq(merge_access.access_level)
+ end
+
+ it 'includes access_level_description' do
+ expect(merge_access_level_data['accessLevelDescription'])
+ .to eq(merge_access.humanize)
+ end
+ end
+
+ context 'when the user does have read_protected_branch abilities' do
+ let(:merge_access) { protected_branch.merge_access_levels.first }
+
+ context 'when no one has access' do
+ let_it_be(:protected_branch) { create(:protected_branch, :no_one_can_merge) }
+
+ it_behaves_like 'merge access request'
+ end
+
+ context 'when developers have access' do
+ let_it_be(:protected_branch) { create(:protected_branch, :developers_can_merge) }
+
+ it_behaves_like 'merge access request'
+ end
+
+ context 'when maintainers have access' do
+ let_it_be(:protected_branch) { create(:protected_branch, :maintainers_can_merge) }
+
+ it_behaves_like 'merge access request'
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/project/branch_protections/push_access_levels_spec.rb b/spec/requests/api/graphql/project/branch_protections/push_access_levels_spec.rb
new file mode 100644
index 00000000000..59f9c7d61cb
--- /dev/null
+++ b/spec/requests/api/graphql/project/branch_protections/push_access_levels_spec.rb
@@ -0,0 +1,109 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'getting push access levels for a branch protection' do
+ include GraphqlHelpers
+
+ let_it_be(:current_user) { create(:user) }
+
+ let(:push_access_level_data) { push_access_levels_data[0] }
+
+ let(:push_access_levels_data) do
+ graphql_data_at('project',
+ 'branchRules',
+ 'nodes',
+ 0,
+ 'branchProtection',
+ 'pushAccessLevels',
+ 'nodes')
+ end
+
+ let(:project) { protected_branch.project }
+
+ let(:push_access_levels_count) { protected_branch.push_access_levels.size }
+
+ let(:variables) { { path: project.full_path } }
+
+ let(:fields) { all_graphql_fields_for('PushAccessLevel'.classify) }
+
+ let(:query) do
+ <<~GQL
+ query($path: ID!) {
+ project(fullPath: $path) {
+ branchRules(first: 1) {
+ nodes {
+ branchProtection {
+ pushAccessLevels {
+ nodes {
+ #{fields}
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ GQL
+ end
+
+ context 'when the user does not have read_protected_branch abilities' do
+ let_it_be(:protected_branch) { create(:protected_branch) }
+
+ before do
+ project.add_guest(current_user)
+ post_graphql(query, current_user: current_user, variables: variables)
+ end
+
+ it_behaves_like 'a working graphql query'
+
+ it { expect(push_access_levels_data).not_to be_present }
+ end
+
+ shared_examples 'push access request' do
+ let(:push_access) { protected_branch.push_access_levels.first }
+
+ before do
+ project.add_maintainer(current_user)
+ post_graphql(query, current_user: current_user, variables: variables)
+ end
+
+ it_behaves_like 'a working graphql query'
+
+ it 'returns all push access levels' do
+ expect(push_access_levels_data.size).to eq(push_access_levels_count)
+ end
+
+ it 'includes access_level' do
+ expect(push_access_level_data['accessLevel'])
+ .to eq(push_access.access_level)
+ end
+
+ it 'includes access_level_description' do
+ expect(push_access_level_data['accessLevelDescription'])
+ .to eq(push_access.humanize)
+ end
+ end
+
+ context 'when the user does have read_protected_branch abilities' do
+ let(:push_access) { protected_branch.push_access_levels.first }
+
+ context 'when no one has access' do
+ let_it_be(:protected_branch) { create(:protected_branch, :no_one_can_push) }
+
+ it_behaves_like 'push access request'
+ end
+
+ context 'when developers have access' do
+ let_it_be(:protected_branch) { create(:protected_branch, :developers_can_push) }
+
+ it_behaves_like 'push access request'
+ end
+
+ context 'when maintainers have access' do
+ let_it_be(:protected_branch) { create(:protected_branch, :maintainers_can_push) }
+
+ it_behaves_like 'push access request'
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/project/branch_rules/branch_protection_spec.rb b/spec/requests/api/graphql/project/branch_rules/branch_protection_spec.rb
new file mode 100644
index 00000000000..8a3f546ef95
--- /dev/null
+++ b/spec/requests/api/graphql/project/branch_rules/branch_protection_spec.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'getting branch protection for a branch rule' do
+ include GraphqlHelpers
+
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:branch_rule) { create(:protected_branch) }
+ let_it_be(:project) { branch_rule.project }
+
+ let(:branch_protection_data) do
+ graphql_data_at('project', 'branchRules', 'nodes', 0, 'branchProtection')
+ end
+
+ let(:variables) { { path: project.full_path } }
+
+ let(:fields) { all_graphql_fields_for('BranchProtection') }
+
+ let(:query) do
+ <<~GQL
+ query($path: ID!) {
+ project(fullPath: $path) {
+ branchRules(first: 1) {
+ nodes {
+ branchProtection {
+ #{fields}
+ }
+ }
+ }
+ }
+ }
+ GQL
+ end
+
+ context 'when the user does not have read_protected_branch abilities' do
+ before do
+ project.add_guest(current_user)
+ post_graphql(query, current_user: current_user, variables: variables)
+ end
+
+ it_behaves_like 'a working graphql query'
+
+ it { expect(branch_protection_data).not_to be_present }
+ end
+
+ context 'when the user does have read_protected_branch abilities' do
+ before do
+ project.add_maintainer(current_user)
+ post_graphql(query, current_user: current_user, variables: variables)
+ end
+
+ it_behaves_like 'a working graphql query'
+
+ it 'includes allow_force_push' do
+ expect(branch_protection_data['allowForcePush']).to be_in([true, false])
+ expect(branch_protection_data['allowForcePush']).to eq(branch_rule.allow_force_push)
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/project/branch_rules_spec.rb b/spec/requests/api/graphql/project/branch_rules_spec.rb
new file mode 100644
index 00000000000..70fb37941e2
--- /dev/null
+++ b/spec/requests/api/graphql/project/branch_rules_spec.rb
@@ -0,0 +1,122 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'getting list of branch rules for a project' do
+ include GraphqlHelpers
+
+ let_it_be(:project) { create(:project, :repository, :public) }
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:branch_name_a) { 'branch_name_a' }
+ let_it_be(:branch_name_b) { 'wildcard-*' }
+ let_it_be(:branch_rules) { [branch_rule_a, branch_rule_b] }
+
+ let_it_be(:branch_rule_a) do
+ create(:protected_branch, project: project, name: branch_name_a)
+ end
+
+ let_it_be(:branch_rule_b) do
+ create(:protected_branch, project: project, name: branch_name_b)
+ end
+
+ let(:branch_rules_data) { graphql_data_at('project', 'branchRules', 'edges') }
+ let(:variables) { { path: project.full_path } }
+
+ let(:fields) do
+ <<~QUERY
+ pageInfo {
+ hasNextPage
+ hasPreviousPage
+ }
+ edges {
+ cursor
+ node {
+ #{all_graphql_fields_for('branch_rules'.classify)}
+ }
+ }
+ QUERY
+ end
+
+ let(:query) do
+ <<~GQL
+ query($path: ID!, $n: Int, $cursor: String) {
+ project(fullPath: $path) {
+ branchRules(first: $n, after: $cursor) { #{fields} }
+ }
+ }
+ GQL
+ end
+
+ context 'when the user does not have read_protected_branch abilities' do
+ before do
+ project.add_guest(current_user)
+ post_graphql(query, current_user: current_user, variables: variables)
+ end
+
+ it_behaves_like 'a working graphql query'
+
+ it { expect(branch_rules_data).to be_empty }
+ end
+
+ context 'when the user does have read_protected_branch abilities' do
+ before do
+ project.add_maintainer(current_user)
+ post_graphql(query, current_user: current_user, variables: variables)
+ end
+
+ it_behaves_like 'a working graphql query'
+
+ it 'includes a name' do
+ expect(branch_rules_data.dig(0, 'node', 'name')).to be_present
+ end
+
+ it 'includes created_at and updated_at' do
+ expect(branch_rules_data.dig(0, 'node', 'createdAt')).to be_present
+ expect(branch_rules_data.dig(1, 'node', 'updatedAt')).to be_present
+ end
+
+ context 'when limiting the number of results' do
+ let(:branch_rule_limit) { 1 }
+ let(:variables) { { path: project.full_path, n: branch_rule_limit } }
+ let(:next_variables) do
+ { path: project.full_path, n: branch_rule_limit, cursor: last_cursor }
+ end
+
+ it_behaves_like 'a working graphql query' do
+ it 'only returns N branch_rules' do
+ expect(branch_rules_data.size).to eq(branch_rule_limit)
+ expect(has_next_page).to be_truthy
+ expect(has_prev_page).to be_falsey
+ post_graphql(query, current_user: current_user, variables: next_variables)
+ expect(branch_rules_data.size).to eq(branch_rule_limit)
+ expect(has_next_page).to be_falsey
+ expect(has_prev_page).to be_truthy
+ end
+ end
+
+ context 'when no limit is provided' do
+ let(:branch_rule_limit) { nil }
+
+ it 'returns all branch_rules' do
+ expect(branch_rules_data.size).to eq(branch_rules.size)
+ end
+ end
+ end
+ end
+
+ def pagination_info
+ graphql_data_at('project', 'branchRules', 'pageInfo')
+ end
+
+ def has_next_page
+ pagination_info['hasNextPage']
+ end
+
+ def has_prev_page
+ pagination_info['hasPreviousPage']
+ end
+
+ def last_cursor
+ branch_rules_data.last['cursor']
+ end
+end
diff --git a/spec/requests/api/graphql/project/deployment_spec.rb b/spec/requests/api/graphql/project/deployment_spec.rb
new file mode 100644
index 00000000000..e5ef7bcafbf
--- /dev/null
+++ b/spec/requests/api/graphql/project/deployment_spec.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Project Deployment query' do
+ let_it_be(:project) { create(:project, :private, :repository) }
+ let_it_be(:developer) { create(:user).tap { |u| project.add_developer(u) } }
+ let_it_be(:guest) { create(:user).tap { |u| project.add_guest(u) } }
+ let_it_be(:environment) { create(:environment, project: project) }
+ let_it_be(:deployment) { create(:deployment, environment: environment, project: project) }
+
+ subject { GitlabSchema.execute(query, context: { current_user: user }).as_json }
+
+ let(:user) { developer }
+
+ let(:query) do
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ deployment(iid: #{deployment.iid}) {
+ id
+ iid
+ ref
+ tag
+ sha
+ createdAt
+ updatedAt
+ finishedAt
+ status
+ }
+ }
+ }
+ )
+ end
+
+ it 'returns the deployment of the project' do
+ deployment_data = subject.dig('data', 'project', 'deployment')
+
+ expect(deployment_data['iid']).to eq(deployment.iid.to_s)
+ end
+
+ context 'when user is guest' do
+ let(:user) { guest }
+
+ it 'returns nothing' do
+ deployment_data = subject.dig('data', 'project', 'deployment')
+
+ expect(deployment_data).to be_nil
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/project/environments_spec.rb b/spec/requests/api/graphql/project/environments_spec.rb
new file mode 100644
index 00000000000..e5b6aebbf2c
--- /dev/null
+++ b/spec/requests/api/graphql/project/environments_spec.rb
@@ -0,0 +1,133 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Project Environments query' do
+ include GraphqlHelpers
+
+ let_it_be(:project) { create(:project, :private, :repository) }
+ let_it_be_with_refind(:production) { create(:environment, :production, project: project) }
+ let_it_be_with_refind(:staging) { create(:environment, :staging, project: project) }
+ let_it_be(:developer) { create(:user).tap { |u| project.add_developer(u) } }
+
+ subject { post_graphql(query, current_user: user) }
+
+ let(:user) { developer }
+
+ let(:query) do
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ environment(name: "#{production.name}") {
+ slug
+ createdAt
+ updatedAt
+ autoStopAt
+ autoDeleteAt
+ tier
+ environmentType
+ }
+ }
+ }
+ )
+ end
+
+ it 'returns the specified fields of the environment', :aggregate_failures do
+ production.update!(auto_stop_at: 1.day.ago, auto_delete_at: 2.days.ago, environment_type: 'review')
+
+ subject
+
+ environment_data = graphql_data.dig('project', 'environment')
+ expect(environment_data['slug']).to eq(production.slug)
+ expect(environment_data['createdAt']).to eq(production.created_at.iso8601)
+ expect(environment_data['updatedAt']).to eq(production.updated_at.iso8601)
+ expect(environment_data['autoStopAt']).to eq(production.auto_stop_at.iso8601)
+ expect(environment_data['autoDeleteAt']).to eq(production.auto_delete_at.iso8601)
+ expect(environment_data['tier']).to eq(production.tier.upcase)
+ expect(environment_data['environmentType']).to eq(production.environment_type)
+ end
+
+ describe 'last deployments of environments' do
+ ::Deployment.statuses.each do |status, _|
+ let_it_be(:"production_#{status}_deployment") do
+ create(:deployment, status.to_sym, environment: production, project: project)
+ end
+
+ let_it_be(:"staging_#{status}_deployment") do
+ create(:deployment, status.to_sym, environment: staging, project: project)
+ end
+ end
+
+ let(:query) do
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ environments {
+ nodes {
+ name
+ lastSuccessDeployment: lastDeployment(status: SUCCESS) {
+ iid
+ }
+ lastRunningDeployment: lastDeployment(status: RUNNING) {
+ iid
+ }
+ lastBlockedDeployment: lastDeployment(status: BLOCKED) {
+ iid
+ }
+ }
+ }
+ }
+ }
+ )
+ end
+
+ it 'returns all last deployments of the environment' do
+ subject
+
+ environments_data = graphql_data_at(:project, :environments, :nodes)
+
+ environments_data.each do |environment_data|
+ name = environment_data['name']
+ success_deployment = public_send(:"#{name}_success_deployment")
+ running_deployment = public_send(:"#{name}_running_deployment")
+ blocked_deployment = public_send(:"#{name}_blocked_deployment")
+
+ expect(environment_data['lastSuccessDeployment']['iid']).to eq(success_deployment.iid.to_s)
+ expect(environment_data['lastRunningDeployment']['iid']).to eq(running_deployment.iid.to_s)
+ expect(environment_data['lastBlockedDeployment']['iid']).to eq(blocked_deployment.iid.to_s)
+ end
+ end
+
+ it 'executes the same number of queries in single environment and multiple environments' do
+ single_environment_query =
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ environment(name: "#{production.name}") {
+ name
+ lastSuccessDeployment: lastDeployment(status: SUCCESS) {
+ iid
+ }
+ lastRunningDeployment: lastDeployment(status: RUNNING) {
+ iid
+ }
+ lastBlockedDeployment: lastDeployment(status: BLOCKED) {
+ iid
+ }
+ }
+ }
+ }
+ )
+
+ baseline = ActiveRecord::QueryRecorder.new do
+ run_with_clean_state(single_environment_query, context: { current_user: user })
+ end
+
+ multi = ActiveRecord::QueryRecorder.new do
+ run_with_clean_state(query, context: { current_user: user })
+ end
+
+ expect(multi).not_to exceed_query_limit(baseline)
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/project/issue/design_collection/version_spec.rb b/spec/requests/api/graphql/project/issue/design_collection/version_spec.rb
index 8cda61f0628..0444ce43c22 100644
--- a/spec/requests/api/graphql/project/issue/design_collection/version_spec.rb
+++ b/spec/requests/api/graphql/project/issue/design_collection/version_spec.rb
@@ -11,14 +11,14 @@ RSpec.describe 'Query.project(fullPath).issue(iid).designCollection.version(sha)
let_it_be(:developer) { create(:user) }
let_it_be(:stranger) { create(:user) }
let_it_be(:old_version) do
- create(:design_version, issue: issue,
- created_designs: create_list(:design, 3, issue: issue))
+ create(:design_version, issue: issue, created_designs: create_list(:design, 3, issue: issue))
end
let_it_be(:version) do
- create(:design_version, issue: issue,
- modified_designs: old_version.designs,
- created_designs: create_list(:design, 2, issue: issue))
+ create(:design_version,
+ issue: issue,
+ modified_designs: old_version.designs,
+ created_designs: create_list(:design, 2, issue: issue))
end
let(:current_user) { developer }
diff --git a/spec/requests/api/graphql/project/issues_spec.rb b/spec/requests/api/graphql/project/issues_spec.rb
index 596e023a027..28282860416 100644
--- a/spec/requests/api/graphql/project/issues_spec.rb
+++ b/spec/requests/api/graphql/project/issues_spec.rb
@@ -27,14 +27,6 @@ RSpec.describe 'getting an issue list for a project' do
QUERY
end
- let(:query) do
- graphql_query_for(
- 'project',
- { 'fullPath' => project.full_path },
- query_graphql_field('issues', issue_filter_params, fields)
- )
- end
-
it_behaves_like 'a working graphql query' do
before do
post_graphql(query, current_user: current_user)
@@ -89,6 +81,14 @@ RSpec.describe 'getting an issue list for a project' do
end
end
+ context 'when filtering by search' do
+ it_behaves_like 'query with a search term' do
+ let(:issuable_data) { issues_data }
+ let(:user) { current_user }
+ let_it_be(:issuable) { create(:issue, project: project, description: 'bar') }
+ end
+ end
+
context 'when limiting the number of results' do
let(:query) do
<<~GQL
@@ -301,7 +301,7 @@ RSpec.describe 'getting an issue list for a project' do
let_it_be(:relative_issue5) { create(:issue, project: sort_project, relative_position: 500) }
context 'when ascending' do
- it_behaves_like 'sorted paginated query' do
+ it_behaves_like 'sorted paginated query', is_reversible: true do
let(:sort_param) { :RELATIVE_POSITION_ASC }
let(:first_param) { 2 }
let(:all_records) do
@@ -679,4 +679,12 @@ RSpec.describe 'getting an issue list for a project' do
def issues_ids
graphql_dig_at(issues_data, :node, :id)
end
+
+ def query(params = issue_filter_params)
+ graphql_query_for(
+ 'project',
+ { 'fullPath' => project.full_path },
+ query_graphql_field('issues', params, fields)
+ )
+ end
end
diff --git a/spec/requests/api/graphql/project/job_spec.rb b/spec/requests/api/graphql/project/job_spec.rb
new file mode 100644
index 00000000000..6edd4cf753f
--- /dev/null
+++ b/spec/requests/api/graphql/project/job_spec.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Query.project.job' do
+ include GraphqlHelpers
+
+ let_it_be(:project) { create(:project) }
+ let_it_be(:user) { create(:user) }
+
+ let(:job) { create(:ci_build, project: project, name: 'GQL test job') }
+
+ let(:query) do
+ <<~QUERY
+ {
+ project(fullPath: "#{project.full_path}") {
+ job(id: "#{job.to_global_id}") {
+ name
+ }
+ }
+ }
+ QUERY
+ end
+
+ context 'when the user can read jobs on the project' do
+ before do
+ project.add_developer(user)
+ end
+
+ it 'returns the job that matches the given ID' do
+ post_graphql(query, current_user: user)
+
+ expect(graphql_data.dig('project', 'job', 'name')).to eq('GQL test job')
+ end
+
+ context 'when no job matches the given ID' do
+ let(:job) { create(:ci_build, project: create(:project), name: 'Job from another project') }
+
+ it 'returns null' do
+ post_graphql(query, current_user: user)
+
+ expect(graphql_data.dig('project', 'job')).to be_nil
+ end
+ end
+ end
+
+ context 'when the user cannot read jobs on the project' do
+ it 'returns null' do
+ post_graphql(query, current_user: user)
+
+ expect(graphql_data.dig('project', 'job')).to be_nil
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/project/merge_request_spec.rb b/spec/requests/api/graphql/project/merge_request_spec.rb
index d2f34080be3..6a59df81405 100644
--- a/spec/requests/api/graphql/project/merge_request_spec.rb
+++ b/spec/requests/api/graphql/project/merge_request_spec.rb
@@ -365,7 +365,7 @@ RSpec.describe 'getting merge request information nested in a project' do
expect(interaction_data).to contain_exactly a_hash_including(
'canMerge' => false,
'canUpdate' => can_update,
- 'reviewState' => attention_requested,
+ 'reviewState' => unreviewed,
'reviewed' => false,
'approved' => false
)
@@ -398,8 +398,8 @@ RSpec.describe 'getting merge request information nested in a project' do
describe 'scalability' do
let_it_be(:other_users) { create_list(:user, 3) }
- let(:attention_requested) do
- { 'reviewState' => 'ATTENTION_REQUESTED' }
+ let(:unreviewed) do
+ { 'reviewState' => 'UNREVIEWED' }
end
let(:reviewed) do
@@ -425,15 +425,15 @@ RSpec.describe 'getting merge request information nested in a project' do
other_users.each do |user|
assign_user(user)
- merge_request.merge_request_reviewers.find_or_create_by!(reviewer: user, state: :attention_requested)
+ merge_request.merge_request_reviewers.find_or_create_by!(reviewer: user)
end
expect { post_graphql(query) }.not_to exceed_query_limit(baseline)
expect(interaction_data).to contain_exactly(
- include(attention_requested),
- include(attention_requested),
- include(attention_requested),
+ include(unreviewed),
+ include(unreviewed),
+ include(unreviewed),
include(reviewed)
)
end
@@ -462,17 +462,17 @@ RSpec.describe 'getting merge request information nested in a project' do
it_behaves_like 'when requesting information about MR interactions' do
let(:field) { :reviewers }
- let(:attention_requested) { 'ATTENTION_REQUESTED' }
+ let(:unreviewed) { 'UNREVIEWED' }
let(:can_update) { false }
def assign_user(user)
- merge_request.merge_request_reviewers.create!(reviewer: user, state: :attention_requested)
+ merge_request.merge_request_reviewers.create!(reviewer: user)
end
end
it_behaves_like 'when requesting information about MR interactions' do
let(:field) { :assignees }
- let(:attention_requested) { nil }
+ let(:unreviewed) { nil }
let(:can_update) { true } # assignees can update MRs
def assign_user(user)
diff --git a/spec/requests/api/graphql/project/pipeline_spec.rb b/spec/requests/api/graphql/project/pipeline_spec.rb
index 08c6a2d9927..41915d3cdee 100644
--- a/spec/requests/api/graphql/project/pipeline_spec.rb
+++ b/spec/requests/api/graphql/project/pipeline_spec.rb
@@ -111,7 +111,7 @@ RSpec.describe 'getting pipeline information nested in a project' do
name: build_job.name,
pipeline: pipeline,
stage_idx: 0,
- stage: build_job.stage)
+ stage: build_job.stage_name)
end
let(:fields) do
diff --git a/spec/requests/api/graphql/project/terraform/state_spec.rb b/spec/requests/api/graphql/project/terraform/state_spec.rb
index 8f2d2cffef2..5e207ec0963 100644
--- a/spec/requests/api/graphql/project/terraform/state_spec.rb
+++ b/spec/requests/api/graphql/project/terraform/state_spec.rb
@@ -60,17 +60,17 @@ RSpec.describe 'query a single terraform state' do
expect(data).to match a_graphql_entity_for(
terraform_state,
:name,
- 'lockedAt' => terraform_state.locked_at.iso8601,
- 'createdAt' => terraform_state.created_at.iso8601,
- 'updatedAt' => terraform_state.updated_at.iso8601,
- 'lockedByUser' => a_graphql_entity_for(terraform_state.locked_by_user),
+ 'lockedAt' => terraform_state.locked_at.iso8601,
+ 'createdAt' => terraform_state.created_at.iso8601,
+ 'updatedAt' => terraform_state.updated_at.iso8601,
+ 'lockedByUser' => a_graphql_entity_for(terraform_state.locked_by_user),
'latestVersion' => a_graphql_entity_for(
latest_version,
- 'serial' => eq(latest_version.version),
- 'createdAt' => eq(latest_version.created_at.iso8601),
- 'updatedAt' => eq(latest_version.updated_at.iso8601),
+ 'serial' => eq(latest_version.version),
+ 'createdAt' => eq(latest_version.created_at.iso8601),
+ 'updatedAt' => eq(latest_version.updated_at.iso8601),
'createdByUser' => a_graphql_entity_for(latest_version.created_by_user),
- 'job' => { 'name' => eq(latest_version.build.name) }
+ 'job' => { 'name' => eq(latest_version.build.name) }
)
)
end
diff --git a/spec/requests/api/graphql/project/terraform/states_spec.rb b/spec/requests/api/graphql/project/terraform/states_spec.rb
index a7ec6f69776..cc3660bcc6b 100644
--- a/spec/requests/api/graphql/project/terraform/states_spec.rb
+++ b/spec/requests/api/graphql/project/terraform/states_spec.rb
@@ -64,18 +64,18 @@ RSpec.describe 'query terraform states' do
expect(data['nodes']).to contain_exactly a_graphql_entity_for(
terraform_state, :name,
- 'lockedAt' => terraform_state.locked_at.iso8601,
- 'createdAt' => terraform_state.created_at.iso8601,
- 'updatedAt' => terraform_state.updated_at.iso8601,
- 'lockedByUser' => a_graphql_entity_for(terraform_state.locked_by_user),
+ 'lockedAt' => terraform_state.locked_at.iso8601,
+ 'createdAt' => terraform_state.created_at.iso8601,
+ 'updatedAt' => terraform_state.updated_at.iso8601,
+ 'lockedByUser' => a_graphql_entity_for(terraform_state.locked_by_user),
'latestVersion' => a_graphql_entity_for(
latest_version,
- 'serial' => eq(latest_version.version),
- 'downloadPath' => eq(download_path),
- 'createdAt' => eq(latest_version.created_at.iso8601),
- 'updatedAt' => eq(latest_version.updated_at.iso8601),
+ 'serial' => eq(latest_version.version),
+ 'downloadPath' => eq(download_path),
+ 'createdAt' => eq(latest_version.created_at.iso8601),
+ 'updatedAt' => eq(latest_version.updated_at.iso8601),
'createdByUser' => a_graphql_entity_for(latest_version.created_by_user),
- 'job' => { 'name' => eq(latest_version.build.name) }
+ 'job' => { 'name' => eq(latest_version.build.name) }
)
)
end
diff --git a/spec/requests/api/graphql/project/work_items_spec.rb b/spec/requests/api/graphql/project/work_items_spec.rb
index 6ef28392b8b..69f8d1cac74 100644
--- a/spec/requests/api/graphql/project/work_items_spec.rb
+++ b/spec/requests/api/graphql/project/work_items_spec.rb
@@ -10,7 +10,10 @@ RSpec.describe 'getting an work item list for a project' do
let_it_be(:current_user) { create(:user) }
let_it_be(:item1) { create(:work_item, project: project, discussion_locked: true, title: 'item1') }
- let_it_be(:item2) { create(:work_item, project: project, title: 'item2') }
+ let_it_be(:item2) do
+ create(:work_item, project: project, title: 'item2', last_edited_by: current_user, last_edited_at: 1.day.ago)
+ end
+
let_it_be(:confidential_item) { create(:work_item, confidential: true, project: project, title: 'item3') }
let_it_be(:other_item) { create(:work_item) }
@@ -27,14 +30,6 @@ RSpec.describe 'getting an work item list for a project' do
QUERY
end
- let(:query) do
- graphql_query_for(
- 'project',
- { 'fullPath' => project.full_path },
- query_graphql_field('workItems', item_filter_params, fields)
- )
- end
-
it_behaves_like 'a working graphql query' do
before do
post_graphql(query, current_user: current_user)
@@ -83,6 +78,48 @@ RSpec.describe 'getting an work item list for a project' do
end
end
+ context 'when fetching description edit information' do
+ let(:fields) do
+ <<~GRAPHQL
+ nodes {
+ widgets {
+ type
+ ... on WorkItemWidgetDescription {
+ edited
+ lastEditedAt
+ lastEditedBy {
+ webPath
+ username
+ }
+ }
+ }
+ }
+ GRAPHQL
+ end
+
+ it 'avoids N+1 queries' do
+ post_graphql(query, current_user: current_user) # warm-up
+
+ control = ActiveRecord::QueryRecorder.new do
+ post_graphql(query, current_user: current_user)
+ end
+ expect_graphql_errors_to_be_empty
+
+ create_list(:work_item, 3, :last_edited_by_user, last_edited_at: 1.week.ago, project: project)
+
+ expect_graphql_errors_to_be_empty
+ expect { post_graphql(query, current_user: current_user) }.not_to exceed_query_limit(control)
+ end
+ end
+
+ context 'when filtering by search' do
+ it_behaves_like 'query with a search term' do
+ let(:issuable_data) { items_data }
+ let(:user) { current_user }
+ let_it_be(:issuable) { create(:work_item, project: project, description: 'bar') }
+ end
+ end
+
describe 'sorting and pagination' do
let(:data_path) { [:project, :work_items] }
@@ -118,4 +155,12 @@ RSpec.describe 'getting an work item list for a project' do
def item_ids
graphql_dig_at(items_data, :node, :id)
end
+
+ def query(params = item_filter_params)
+ graphql_query_for(
+ 'project',
+ { 'fullPath' => project.full_path },
+ query_graphql_field('workItems', params, fields)
+ )
+ end
end
diff --git a/spec/requests/api/graphql/query_spec.rb b/spec/requests/api/graphql/query_spec.rb
index 4aa9c4b8254..359c599cd3a 100644
--- a/spec/requests/api/graphql/query_spec.rb
+++ b/spec/requests/api/graphql/query_spec.rb
@@ -108,8 +108,8 @@ RSpec.describe 'Query' do
design_at_version,
'filename' => design_at_version.design.filename,
'version' => a_graphql_entity_for(version, :sha),
- 'design' => a_graphql_entity_for(design),
- 'issue' => { 'title' => issue.title, 'iid' => issue.iid.to_s },
+ 'design' => a_graphql_entity_for(design),
+ 'issue' => { 'title' => issue.title, 'iid' => issue.iid.to_s },
'project' => a_graphql_entity_for(project, :full_path)
)
end
diff --git a/spec/requests/api/graphql/work_item_spec.rb b/spec/requests/api/graphql/work_item_spec.rb
index 34644e5893a..e4bb4109c76 100644
--- a/spec/requests/api/graphql/work_item_spec.rb
+++ b/spec/requests/api/graphql/work_item_spec.rb
@@ -14,7 +14,10 @@ RSpec.describe 'Query.work_item(id)' do
project: project,
description: '- List item',
start_date: Date.today,
- due_date: 1.week.from_now
+ due_date: 1.week.from_now,
+ created_at: 1.week.ago,
+ last_edited_at: 1.day.ago,
+ last_edited_by: guest
)
end
@@ -67,6 +70,12 @@ RSpec.describe 'Query.work_item(id)' do
... on WorkItemWidgetDescription {
description
descriptionHtml
+ edited
+ lastEditedBy {
+ webPath
+ username
+ }
+ lastEditedAt
}
}
GRAPHQL
@@ -79,7 +88,13 @@ RSpec.describe 'Query.work_item(id)' do
hash_including(
'type' => 'DESCRIPTION',
'description' => work_item.description,
- 'descriptionHtml' => ::MarkupHelper.markdown_field(work_item, :description, {})
+ 'descriptionHtml' => ::MarkupHelper.markdown_field(work_item, :description, {}),
+ 'edited' => true,
+ 'lastEditedAt' => work_item.last_edited_at.iso8601,
+ 'lastEditedBy' => {
+ 'webPath' => "/#{guest.full_path}",
+ 'username' => guest.username
+ }
)
)
)
diff --git a/spec/requests/api/group_export_spec.rb b/spec/requests/api/group_export_spec.rb
index bda46f85140..83c34204c78 100644
--- a/spec/requests/api/group_export_spec.rb
+++ b/spec/requests/api/group_export_spec.rb
@@ -34,6 +34,7 @@ RSpec.describe API::GroupExport do
before do
allow_next_instance_of(Gitlab::ApplicationRateLimiter::BaseStrategy) do |strategy|
allow(strategy).to receive(:increment).and_return(0)
+ allow(strategy).to receive(:read).and_return(0)
end
upload.export_file = fixture_file_upload('spec/fixtures/group_export.tar.gz', "`/tar.gz")
diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb
index bc37f8e4655..6169bc9b2a2 100644
--- a/spec/requests/api/groups_spec.rb
+++ b/spec/requests/api/groups_spec.rb
@@ -5,6 +5,7 @@ require 'spec_helper'
RSpec.describe API::Groups do
include GroupAPIHelpers
include UploadHelpers
+ include WorkhorseHelpers
let_it_be(:user1) { create(:user, can_create_group: false) }
let_it_be(:user2) { create(:user) }
@@ -540,9 +541,9 @@ RSpec.describe API::Groups do
# Returns a Hash of visibility_level => Project pairs
def add_projects_to_group(group, share_with: nil)
projects = {
- public: create(:project, :public, namespace: group),
+ public: create(:project, :public, namespace: group),
internal: create(:project, :internal, namespace: group),
- private: create(:project, :private, namespace: group)
+ private: create(:project, :private, namespace: group)
}
if share_with
@@ -872,21 +873,31 @@ RSpec.describe API::Groups do
group_param = {
avatar: fixture_file_upload(file_path)
}
- put api("/groups/#{group1.id}", user1), params: group_param
+ workhorse_form_with_file(
+ api("/groups/#{group1.id}", user1),
+ method: :put,
+ file_key: :avatar,
+ params: group_param
+ )
end
end
context 'when authenticated as the group owner' do
it 'updates the group' do
- put api("/groups/#{group1.id}", user1), params: {
- name: new_group_name,
- request_access_enabled: true,
- project_creation_level: "noone",
- subgroup_creation_level: "maintainer",
- default_branch_protection: ::Gitlab::Access::MAINTAINER_PROJECT_ACCESS,
- prevent_sharing_groups_outside_hierarchy: true,
- avatar: fixture_file_upload(file_path)
- }
+ workhorse_form_with_file(
+ api("/groups/#{group1.id}", user1),
+ method: :put,
+ file_key: :avatar,
+ params: {
+ name: new_group_name,
+ request_access_enabled: true,
+ project_creation_level: "noone",
+ subgroup_creation_level: "maintainer",
+ default_branch_protection: ::Gitlab::Access::MAINTAINER_PROJECT_ACCESS,
+ prevent_sharing_groups_outside_hierarchy: true,
+ avatar: fixture_file_upload(file_path)
+ }
+ )
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['name']).to eq(new_group_name)
@@ -912,6 +923,16 @@ RSpec.describe API::Groups do
expect(json_response['prevent_sharing_groups_outside_hierarchy']).to eq(true)
end
+ it 'removes the group avatar' do
+ put api("/groups/#{group1.id}", user1), params: { avatar: '' }
+
+ aggregate_failures "testing response" do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['avatar_url']).to be_nil
+ expect(group1.reload.avatar_url).to be_nil
+ end
+ end
+
it 'does not update visibility_level if it is restricted' do
stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::INTERNAL])
@@ -1787,7 +1808,12 @@ RSpec.describe API::Groups do
attrs[:avatar] = fixture_file_upload(file_path)
end
- post api("/groups", user3), params: params
+ workhorse_form_with_file(
+ api('/groups', user3),
+ method: :post,
+ file_key: :avatar,
+ params: params
+ )
end
end
@@ -2029,6 +2055,90 @@ RSpec.describe API::Groups do
end
end
+ describe 'GET /groups/:id/transfer_locations' do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:source_group) { create(:group, :private) }
+
+ let(:params) { {} }
+
+ subject(:request) do
+ get api("/groups/#{source_group.id}/transfer_locations", user), params: params
+ end
+
+ context 'when the user has rights to transfer the group' do
+ let_it_be(:guest_group) { create(:group) }
+ let_it_be(:maintainer_group) { create(:group, name: 'maintainer group', path: 'maintainer-group') }
+ let_it_be(:owner_group_1) { create(:group, name: 'owner group', path: 'owner-group') }
+ let_it_be(:owner_group_2) { create(:group, name: 'gitlab group', path: 'gitlab-group') }
+ let_it_be(:shared_with_group_where_direct_owner_as_owner) { create(:group) }
+
+ before do
+ source_group.add_owner(user)
+ guest_group.add_guest(user)
+ maintainer_group.add_maintainer(user)
+ owner_group_1.add_owner(user)
+ owner_group_2.add_owner(user)
+ create(:group_group_link, :owner,
+ shared_with_group: owner_group_1,
+ shared_group: shared_with_group_where_direct_owner_as_owner
+ )
+ end
+
+ it 'returns 200' do
+ request
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to include_pagination_headers
+ end
+
+ it 'only includes groups where the user has permissions to transfer a group to' do
+ request
+
+ expect(group_ids_from_response).to contain_exactly(
+ owner_group_1.id,
+ owner_group_2.id,
+ shared_with_group_where_direct_owner_as_owner.id
+ )
+ end
+
+ context 'with search' do
+ let(:params) { { search: 'gitlab' } }
+
+ it 'includes groups where the user has permissions to transfer a group to, matching the search term' do
+ request
+
+ expect(group_ids_from_response).to contain_exactly(owner_group_2.id)
+ end
+ end
+
+ def group_ids_from_response
+ json_response.map { |group| group['id'] }
+ end
+ end
+
+ context 'when the user does not have permissions to transfer the group' do
+ before do
+ source_group.add_developer(user)
+ end
+
+ it 'returns 403' do
+ request
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+
+ context 'for an anonymous user' do
+ let_it_be(:user) { nil }
+
+ it 'returns 404' do
+ request
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+
describe 'POST /groups/:id/transfer' do
let_it_be(:user) { create(:user) }
let_it_be_with_reload(:new_parent_group) { create(:group, :private) }
diff --git a/spec/requests/api/import_github_spec.rb b/spec/requests/api/import_github_spec.rb
index 7de72de3940..d2fa3dabe69 100644
--- a/spec/requests/api/import_github_spec.rb
+++ b/spec/requests/api/import_github_spec.rb
@@ -13,15 +13,15 @@ RSpec.describe API::ImportGithub do
let(:provider_username) { user.username }
let(:provider_user) { double('provider', login: provider_username) }
let(:provider_repo) do
- double('provider',
+ {
name: 'vim',
full_name: "#{provider_username}/vim",
owner: double('provider', login: provider_username),
description: 'provider',
private: false,
clone_url: 'https://fake.url/vim.git',
- has_wiki?: true
- )
+ has_wiki: true
+ }
end
before do
@@ -48,7 +48,7 @@ RSpec.describe API::ImportGithub do
it 'returns 201 response when the project is imported successfully' do
allow(Gitlab::LegacyGithubImport::ProjectCreator)
- .to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, type: provider, **access_params)
+ .to receive(:new).with(provider_repo, provider_repo[:name], user.namespace, user, type: provider, **access_params)
.and_return(double(execute: project))
post api("/import/github", user), params: {
@@ -63,7 +63,7 @@ RSpec.describe API::ImportGithub do
it 'returns 201 response when the project is imported successfully from GHE' do
allow(Gitlab::LegacyGithubImport::ProjectCreator)
- .to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, type: provider, **access_params)
+ .to receive(:new).with(provider_repo, provider_repo[:name], user.namespace, user, type: provider, **access_params)
.and_return(double(execute: project))
post api("/import/github", user), params: {
diff --git a/spec/requests/api/integrations/slack/events_spec.rb b/spec/requests/api/integrations/slack/events_spec.rb
deleted file mode 100644
index 176e9eded31..00000000000
--- a/spec/requests/api/integrations/slack/events_spec.rb
+++ /dev/null
@@ -1,112 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe API::Integrations::Slack::Events do
- describe 'POST /integrations/slack/events' do
- let(:params) { {} }
- let(:headers) do
- {
- ::API::Integrations::Slack::Request::VERIFICATION_TIMESTAMP_HEADER => Time.current.to_i.to_s,
- ::API::Integrations::Slack::Request::VERIFICATION_SIGNATURE_HEADER => 'mock_verified_signature'
- }
- end
-
- before do
- allow(ActiveSupport::SecurityUtils).to receive(:secure_compare) do |signature|
- signature == 'mock_verified_signature'
- end
-
- stub_application_setting(slack_app_signing_secret: 'mock_key')
- end
-
- subject { post api('/integrations/slack/events'), params: params, headers: headers }
-
- shared_examples 'an unauthorized request' do
- specify do
- subject
-
- expect(response).to have_gitlab_http_status(:unauthorized)
- end
- end
-
- shared_examples 'a successful request that generates a tracked error' do
- specify do
- expect(Gitlab::ErrorTracking).to receive(:track_exception).once
-
- subject
-
- expect(response).to have_gitlab_http_status(:no_content)
- expect(response.body).to be_empty
- end
- end
-
- context 'when the slack_app_signing_secret setting is not set' do
- before do
- stub_application_setting(slack_app_signing_secret: nil)
- end
-
- it_behaves_like 'an unauthorized request'
- end
-
- context 'when the timestamp header has expired' do
- before do
- headers[::API::Integrations::Slack::Request::VERIFICATION_TIMESTAMP_HEADER] = 5.minutes.ago.to_i.to_s
- end
-
- it_behaves_like 'an unauthorized request'
- end
-
- context 'when the timestamp header is missing' do
- before do
- headers.delete(::API::Integrations::Slack::Request::VERIFICATION_TIMESTAMP_HEADER)
- end
-
- it_behaves_like 'an unauthorized request'
- end
-
- context 'when the signature header is missing' do
- before do
- headers.delete(::API::Integrations::Slack::Request::VERIFICATION_SIGNATURE_HEADER)
- end
-
- it_behaves_like 'an unauthorized request'
- end
-
- context 'when the signature is not verified' do
- before do
- headers[::API::Integrations::Slack::Request::VERIFICATION_SIGNATURE_HEADER] = 'unverified_signature'
- end
-
- it_behaves_like 'an unauthorized request'
- end
-
- context 'when type param is missing' do
- it_behaves_like 'a successful request that generates a tracked error'
- end
-
- context 'when type param is unknown' do
- let(:params) do
- { type: 'unknown_type' }
- end
-
- it_behaves_like 'a successful request that generates a tracked error'
- end
-
- context 'when type param is url_verification' do
- let(:params) do
- {
- type: 'url_verification',
- challenge: '3eZbrw1aBm2rZgRNFdxV2595E9CY3gmdALWMmHkvFXO7tYXAYM8P'
- }
- end
-
- it 'responds in-request with the challenge' do
- subject
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response).to eq({ 'challenge' => '3eZbrw1aBm2rZgRNFdxV2595E9CY3gmdALWMmHkvFXO7tYXAYM8P' })
- end
- end
- end
-end
diff --git a/spec/requests/api/internal/base_spec.rb b/spec/requests/api/internal/base_spec.rb
index e100684018a..1f6c241b3f5 100644
--- a/spec/requests/api/internal/base_spec.rb
+++ b/spec/requests/api/internal/base_spec.rb
@@ -3,6 +3,7 @@
require 'spec_helper'
RSpec.describe API::Internal::Base do
+ include GitlabShellHelpers
include APIInternalBaseHelpers
let_it_be(:user, reload: true) { create(:user) }
@@ -17,10 +18,14 @@ RSpec.describe API::Internal::Base do
let(:snippet_changes) { "#{TestEnv::BRANCH_SHA['snippet/single-file']} #{TestEnv::BRANCH_SHA['snippet/edit-file']} refs/heads/snippet/edit-file" }
describe "GET /internal/check" do
+ def perform_request(headers: gitlab_shell_internal_api_request_header)
+ get api("/internal/check"), headers: headers
+ end
+
it do
expect_any_instance_of(Redis).to receive(:ping).and_return('PONG')
- get api("/internal/check"), params: { secret_token: secret_token }
+ perform_request
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['api_version']).to eq(API::API.version)
@@ -30,24 +35,57 @@ RSpec.describe API::Internal::Base do
it 'returns false for field `redis` when redis is unavailable' do
expect_any_instance_of(Redis).to receive(:ping).and_raise(Errno::ENOENT)
- get api("/internal/check"), params: { secret_token: secret_token }
+ perform_request
expect(json_response['redis']).to be(false)
end
context 'authenticating' do
- it 'authenticates using a header' do
- get api("/internal/check"),
- headers: { API::Helpers::GITLAB_SHARED_SECRET_HEADER => Base64.encode64(secret_token) }
+ it 'authenticates using a jwt token in a header' do
+ perform_request
expect(response).to have_gitlab_http_status(:ok)
end
- it 'returns 401 when no credentials provided' do
- get(api("/internal/check"))
+ it 'returns 401 when jwt token is expired' do
+ headers = gitlab_shell_internal_api_request_header
+
+ travel_to(2.minutes.since) do
+ perform_request(headers: headers)
+ end
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+
+ it 'returns 401 when jwt issuer is not Gitlab-Shell' do
+ perform_request(headers: gitlab_shell_internal_api_request_header(issuer: "gitlab-workhorse"))
expect(response).to have_gitlab_http_status(:unauthorized)
end
+
+ it 'returns 401 when jwt token is not provided, even if plain secret is provided' do
+ perform_request(headers: { API::Helpers::GITLAB_SHARED_SECRET_HEADER => Base64.encode64(secret_token) })
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+
+ context 'when gitlab_shell_jwt_token is disabled' do
+ before do
+ stub_feature_flags(gitlab_shell_jwt_token: false)
+ end
+
+ it 'authenticates using a header' do
+ perform_request(headers: { API::Helpers::GITLAB_SHARED_SECRET_HEADER => Base64.encode64(secret_token) })
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+
+ it 'returns 401 when no credentials provided' do
+ get(api("/internal/check"))
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+ end
end
end
@@ -56,10 +94,8 @@ RSpec.describe API::Internal::Base do
subject do
post api('/internal/two_factor_recovery_codes'),
- params: {
- secret_token: secret_token,
- key_id: key_id
- }
+ params: { key_id: key_id },
+ headers: gitlab_shell_internal_api_request_header
end
it_behaves_like 'actor key validations'
@@ -105,10 +141,8 @@ RSpec.describe API::Internal::Base do
subject do
post api('/internal/personal_access_token'),
- params: {
- secret_token: secret_token,
- key_id: key_id
- }
+ params: { key_id: key_id },
+ headers: gitlab_shell_internal_api_request_header
end
it_behaves_like 'actor key validations'
@@ -126,10 +160,8 @@ RSpec.describe API::Internal::Base do
it 'returns an error message when given an non existent user' do
post api('/internal/personal_access_token'),
- params: {
- secret_token: secret_token,
- user_id: 0
- }
+ params: { user_id: 0 },
+ headers: gitlab_shell_internal_api_request_header
expect(json_response['success']).to be_falsey
expect(json_response['message']).to eq("Could not find the given user")
@@ -137,10 +169,8 @@ RSpec.describe API::Internal::Base do
it 'returns an error message when no name parameter is received' do
post api('/internal/personal_access_token'),
- params: {
- secret_token: secret_token,
- key_id: key.id
- }
+ params: { key_id: key.id },
+ headers: gitlab_shell_internal_api_request_header
expect(json_response['success']).to be_falsey
expect(json_response['message']).to eq("No token name specified")
@@ -148,11 +178,8 @@ RSpec.describe API::Internal::Base do
it 'returns an error message when no scopes parameter is received' do
post api('/internal/personal_access_token'),
- params: {
- secret_token: secret_token,
- key_id: key.id,
- name: 'newtoken'
- }
+ params: { key_id: key.id, name: 'newtoken' },
+ headers: gitlab_shell_internal_api_request_header
expect(json_response['success']).to be_falsey
expect(json_response['message']).to eq("No token scopes specified")
@@ -161,12 +188,12 @@ RSpec.describe API::Internal::Base do
it 'returns an error message when expires_at contains an invalid date' do
post api('/internal/personal_access_token'),
params: {
- secret_token: secret_token,
- key_id: key.id,
+ key_id: key.id,
name: 'newtoken',
scopes: ['api'],
expires_at: 'invalid-date'
- }
+ },
+ headers: gitlab_shell_internal_api_request_header
expect(json_response['success']).to be_falsey
expect(json_response['message']).to eq("Invalid token expiry date: 'invalid-date'")
@@ -175,11 +202,11 @@ RSpec.describe API::Internal::Base do
it 'returns an error message when it receives an invalid scope' do
post api('/internal/personal_access_token'),
params: {
- secret_token: secret_token,
- key_id: key.id,
+ key_id: key.id,
name: 'newtoken',
scopes: %w(read_api badscope read_repository)
- }
+ },
+ headers: gitlab_shell_internal_api_request_header
expect(json_response['success']).to be_falsey
expect(json_response['message']).to match(/\AInvalid scope: 'badscope'. Valid scopes are: /)
@@ -190,11 +217,11 @@ RSpec.describe API::Internal::Base do
post api('/internal/personal_access_token'),
params: {
- secret_token: secret_token,
- key_id: key.id,
+ key_id: key.id,
name: 'newtoken',
scopes: %w(read_api read_repository)
- }
+ },
+ headers: gitlab_shell_internal_api_request_header
expect(json_response['success']).to be_truthy
expect(json_response['token']).to match(/\A\S{#{token_size}}\z/)
@@ -207,12 +234,12 @@ RSpec.describe API::Internal::Base do
post api('/internal/personal_access_token'),
params: {
- secret_token: secret_token,
- key_id: key.id,
+ key_id: key.id,
name: 'newtoken',
scopes: %w(read_api read_repository),
expires_at: '9001-11-17'
- }
+ },
+ headers: gitlab_shell_internal_api_request_header
expect(json_response['success']).to be_truthy
expect(json_response['token']).to match(/\A\S{#{token_size}}\z/)
@@ -309,7 +336,7 @@ RSpec.describe API::Internal::Base do
describe "GET /internal/discover" do
it "finds a user by key id" do
- get(api("/internal/discover"), params: { key_id: key.id, secret_token: secret_token })
+ get(api("/internal/discover"), params: { key_id: key.id }, headers: gitlab_shell_internal_api_request_header)
expect(response).to have_gitlab_http_status(:ok)
@@ -317,7 +344,7 @@ RSpec.describe API::Internal::Base do
end
it "finds a user by username" do
- get(api("/internal/discover"), params: { username: user.username, secret_token: secret_token })
+ get(api("/internal/discover"), params: { username: user.username }, headers: gitlab_shell_internal_api_request_header)
expect(response).to have_gitlab_http_status(:ok)
@@ -325,7 +352,7 @@ RSpec.describe API::Internal::Base do
end
it 'responds successfully when a user is not found' do
- get(api('/internal/discover'), params: { username: 'noone', secret_token: secret_token })
+ get(api('/internal/discover'), params: { username: 'noone' }, headers: gitlab_shell_internal_api_request_header)
expect(response).to have_gitlab_http_status(:ok)
@@ -333,7 +360,7 @@ RSpec.describe API::Internal::Base do
end
it 'response successfully when passing invalid params' do
- get(api('/internal/discover'), params: { nothing: 'to find a user', secret_token: secret_token })
+ get(api('/internal/discover'), params: { nothing: 'to find a user' }, headers: gitlab_shell_internal_api_request_header)
expect(response).to have_gitlab_http_status(:ok)
@@ -344,7 +371,7 @@ RSpec.describe API::Internal::Base do
describe "GET /internal/authorized_keys" do
context "using an existing key" do
it "finds the key" do
- get(api('/internal/authorized_keys'), params: { key: key.key.split[1], secret_token: secret_token })
+ get(api('/internal/authorized_keys'), params: { key: key.key.split[1] }, headers: gitlab_shell_internal_api_request_header)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['id']).to eq(key.id)
@@ -352,7 +379,7 @@ RSpec.describe API::Internal::Base do
end
it 'exposes the comment of the key as a simple identifier of username + hostname' do
- get(api('/internal/authorized_keys'), params: { key: key.key.split[1], secret_token: secret_token })
+ get(api('/internal/authorized_keys'), params: { key: key.key.split[1] }, headers: gitlab_shell_internal_api_request_header)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['key']).to include("#{key.user_name} (#{Gitlab.config.gitlab.host})")
@@ -360,13 +387,13 @@ RSpec.describe API::Internal::Base do
end
it "returns 404 with a partial key" do
- get(api('/internal/authorized_keys'), params: { key: key.key.split[1][0...-3], secret_token: secret_token })
+ get(api('/internal/authorized_keys'), params: { key: key.key.split[1][0...-3] }, headers: gitlab_shell_internal_api_request_header)
expect(response).to have_gitlab_http_status(:not_found)
end
it "returns 404 with an not valid base64 string" do
- get(api('/internal/authorized_keys'), params: { key: "whatever!", secret_token: secret_token })
+ get(api('/internal/authorized_keys'), params: { key: "whatever!" }, headers: gitlab_shell_internal_api_request_header)
expect(response).to have_gitlab_http_status(:not_found)
end
@@ -609,9 +636,9 @@ RSpec.describe API::Internal::Base do
project: full_path_for(project),
gl_repository: gl_repository_for(project),
action: 'git-upload-pack',
- secret_token: secret_token,
protocol: 'ssh'
- }
+ },
+ headers: gitlab_shell_internal_api_request_header
)
end
end
@@ -994,9 +1021,9 @@ RSpec.describe API::Internal::Base do
key_id: key.id,
project: 'project/does-not-exist.git',
action: 'git-upload-pack',
- secret_token: secret_token,
protocol: 'ssh'
- }
+ },
+ headers: gitlab_shell_internal_api_request_header
)
expect(response).to have_gitlab_http_status(:not_found)
@@ -1170,9 +1197,9 @@ RSpec.describe API::Internal::Base do
key_id: key.id,
project: project.full_path,
gl_repository: gl_repository,
- secret_token: secret_token,
protocol: 'ssh'
- })
+ }, headers: gitlab_shell_internal_api_request_header
+ )
expect(response).to have_gitlab_http_status(:unauthorized)
end
@@ -1285,7 +1312,6 @@ RSpec.describe API::Internal::Base do
let(:valid_params) do
{
gl_repository: gl_repository,
- secret_token: secret_token,
identifier: identifier,
changes: changes,
push_options: push_options
@@ -1296,7 +1322,7 @@ RSpec.describe API::Internal::Base do
"#{Gitlab::Git::BLANK_SHA} 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/#{branch_name}"
end
- subject { post api('/internal/post_receive'), params: valid_params }
+ subject { post api('/internal/post_receive'), params: valid_params, headers: gitlab_shell_internal_api_request_header }
before do
project.add_developer(user)
@@ -1397,7 +1423,7 @@ RSpec.describe API::Internal::Base do
describe 'POST /internal/pre_receive' do
let(:valid_params) do
- { gl_repository: gl_repository, secret_token: secret_token }
+ { gl_repository: gl_repository }
end
it 'decreases the reference counter and returns the result' do
@@ -1405,7 +1431,7 @@ RSpec.describe API::Internal::Base do
.and_return(reference_counter)
expect(reference_counter).to receive(:increase).and_return(true)
- post api("/internal/pre_receive"), params: valid_params
+ post api("/internal/pre_receive"), params: valid_params, headers: gitlab_shell_internal_api_request_header
expect(json_response['reference_counter_increased']).to be(true)
end
@@ -1420,10 +1446,8 @@ RSpec.describe API::Internal::Base do
subject do
post api('/internal/two_factor_config'),
- params: {
- secret_token: secret_token,
- key_id: key_id
- }
+ params: { key_id: key_id },
+ headers: gitlab_shell_internal_api_request_header
end
it_behaves_like 'actor key validations'
@@ -1478,27 +1502,6 @@ RSpec.describe API::Internal::Base do
end
end
- describe 'POST /internal/two_factor_otp_check' do
- let(:key_id) { key.id }
- let(:otp) { '123456' }
-
- subject do
- post api('/internal/two_factor_otp_check'),
- params: {
- secret_token: secret_token,
- key_id: key_id,
- otp_attempt: otp
- }
- end
-
- it 'is not available' do
- subject
-
- expect(json_response['success']).to be_falsey
- expect(json_response['message']).to eq 'Feature is not available'
- end
- end
-
describe 'POST /internal/two_factor_manual_otp_check' do
let(:key_id) { key.id }
let(:otp) { '123456' }
@@ -1509,7 +1512,8 @@ RSpec.describe API::Internal::Base do
secret_token: secret_token,
key_id: key_id,
otp_attempt: otp
- }
+ },
+ headers: gitlab_shell_internal_api_request_header
end
it 'is not available' do
@@ -1530,7 +1534,8 @@ RSpec.describe API::Internal::Base do
secret_token: secret_token,
key_id: key_id,
otp_attempt: otp
- }
+ },
+ headers: gitlab_shell_internal_api_request_header
end
it 'is not available' do
@@ -1551,7 +1556,8 @@ RSpec.describe API::Internal::Base do
secret_token: secret_token,
key_id: key_id,
otp_attempt: otp
- }
+ },
+ headers: gitlab_shell_internal_api_request_header
end
it 'is not available' do
@@ -1571,7 +1577,8 @@ RSpec.describe API::Internal::Base do
secret_token: secret_token,
key_id: key_id,
otp_attempt: otp
- }
+ },
+ headers: gitlab_shell_internal_api_request_header
end
it 'is not available' do
@@ -1584,32 +1591,24 @@ RSpec.describe API::Internal::Base do
def lfs_auth_project(project)
post(
api("/internal/lfs_authenticate"),
- params: {
- secret_token: secret_token,
- project: project.full_path
- }
+ params: { project: project.full_path },
+ headers: gitlab_shell_internal_api_request_header
)
end
def lfs_auth_key(key_id, project)
post(
api("/internal/lfs_authenticate"),
- params: {
- key_id: key_id,
- secret_token: secret_token,
- project: project.full_path
- }
+ params: { key_id: key_id, project: project.full_path },
+ headers: gitlab_shell_internal_api_request_header
)
end
def lfs_auth_user(user_id, project)
post(
api("/internal/lfs_authenticate"),
- params: {
- user_id: user_id,
- secret_token: secret_token,
- project: project.full_path
- }
+ params: { user_id: user_id, project: project.full_path },
+ headers: gitlab_shell_internal_api_request_header
)
end
end
diff --git a/spec/requests/api/internal/lfs_spec.rb b/spec/requests/api/internal/lfs_spec.rb
index 4739ec62992..9eb48db5bd5 100644
--- a/spec/requests/api/internal/lfs_spec.rb
+++ b/spec/requests/api/internal/lfs_spec.rb
@@ -3,6 +3,7 @@
require 'spec_helper'
RSpec.describe API::Internal::Lfs do
+ include GitlabShellHelpers
include APIInternalBaseHelpers
let_it_be(:project) { create(:project) }
@@ -11,25 +12,23 @@ RSpec.describe API::Internal::Lfs do
let_it_be(:gl_repository) { "project-#{project.id}" }
let_it_be(:filename) { lfs_object.file.path }
- let(:secret_token) { Gitlab::Shell.secret_token }
-
describe 'GET /internal/lfs' do
let(:valid_params) do
- { oid: lfs_object.oid, gl_repository: gl_repository, secret_token: secret_token }
+ { oid: lfs_object.oid, gl_repository: gl_repository }
end
context 'with invalid auth' do
- let(:invalid_params) { valid_params.merge!(secret_token: 'invalid_tokne') }
-
it 'returns 401' do
- get api("/internal/lfs"), params: invalid_params
+ get api("/internal/lfs"),
+ params: valid_params,
+ headers: gitlab_shell_internal_api_request_header(issuer: 'gitlab-workhorse')
end
end
context 'with valid auth' do
context 'LFS in local storage' do
it 'sends the file' do
- get api("/internal/lfs"), params: valid_params
+ get api("/internal/lfs"), params: valid_params, headers: gitlab_shell_internal_api_request_header
expect(response).to have_gitlab_http_status(:ok)
expect(response.headers['Content-Type']).to eq('application/octet-stream')
@@ -39,7 +38,10 @@ RSpec.describe API::Internal::Lfs do
# https://www.rubydoc.info/github/rack/rack/master/Rack/Sendfile
it 'delegates sending to Web server' do
- get api("/internal/lfs"), params: valid_params, env: { 'HTTP_X_SENDFILE_TYPE' => 'X-Sendfile' }
+ get api("/internal/lfs"),
+ params: valid_params,
+ env: { 'HTTP_X_SENDFILE_TYPE' => 'X-Sendfile' },
+ headers: gitlab_shell_internal_api_request_header
expect(response).to have_gitlab_http_status(:ok)
expect(response.headers['Content-Type']).to eq('application/octet-stream')
@@ -51,7 +53,7 @@ RSpec.describe API::Internal::Lfs do
it 'retuns 404 for unknown file' do
params = valid_params.merge(oid: SecureRandom.hex)
- get api("/internal/lfs"), params: params
+ get api("/internal/lfs"), params: params, headers: gitlab_shell_internal_api_request_header
expect(response).to have_gitlab_http_status(:not_found)
end
@@ -60,7 +62,7 @@ RSpec.describe API::Internal::Lfs do
other_lfs = create(:lfs_object, :with_file)
params = valid_params.merge(oid: other_lfs.oid)
- get api("/internal/lfs"), params: params
+ get api("/internal/lfs"), params: params, headers: gitlab_shell_internal_api_request_header
expect(response).to have_gitlab_http_status(:not_found)
end
@@ -70,7 +72,7 @@ RSpec.describe API::Internal::Lfs do
let!(:lfs_object2) { create(:lfs_object, :with_file) }
let!(:lfs_objects_project2) { create(:lfs_objects_project, project: project, lfs_object: lfs_object2) }
let(:valid_params) do
- { oid: lfs_object2.oid, gl_repository: gl_repository, secret_token: secret_token }
+ { oid: lfs_object2.oid, gl_repository: gl_repository }
end
before do
@@ -79,7 +81,7 @@ RSpec.describe API::Internal::Lfs do
end
it 'notifies Workhorse to send the file' do
- get api("/internal/lfs"), params: valid_params
+ get api("/internal/lfs"), params: valid_params, headers: gitlab_shell_internal_api_request_header
expect(response).to have_gitlab_http_status(:ok)
expect(response.headers[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("send-url:")
diff --git a/spec/requests/api/issues/get_group_issues_spec.rb b/spec/requests/api/issues/get_group_issues_spec.rb
index 3663a82891c..5c06214316b 100644
--- a/spec/requests/api/issues/get_group_issues_spec.rb
+++ b/spec/requests/api/issues/get_group_issues_spec.rb
@@ -465,10 +465,10 @@ RSpec.describe API::Issues do
context 'with archived projects' do
let_it_be(:archived_issue) do
- create(
- :issue, author: user, assignees: [user],
- project: create(:project, :public, :archived, creator_id: user.id, namespace: group)
- )
+ create(:issue,
+ author: user,
+ assignees: [user],
+ project: create(:project, :public, :archived, creator_id: user.id, namespace: group))
end
it 'returns only non archived projects issues' do
diff --git a/spec/requests/api/markdown_snapshot_spec.rb b/spec/requests/api/markdown_snapshot_spec.rb
index 1270efdfd6f..f2019172a54 100644
--- a/spec/requests/api/markdown_snapshot_spec.rb
+++ b/spec/requests/api/markdown_snapshot_spec.rb
@@ -5,7 +5,5 @@ require 'spec_helper'
# See https://docs.gitlab.com/ee/development/gitlab_flavored_markdown/specification_guide/#markdown-snapshot-testing
# for documentation on this spec.
RSpec.describe API::Markdown, 'Snapshot' do
- # noinspection RubyMismatchedArgumentType (ignore RBS type warning: __dir__ can be nil, but 2nd argument can't be nil)
- glfm_specification_dir = File.expand_path('../../../glfm_specification', __dir__)
- include_context 'with API::Markdown Snapshot shared context', glfm_specification_dir
+ include_context 'with API::Markdown Snapshot shared context'
end
diff --git a/spec/requests/api/maven_packages_spec.rb b/spec/requests/api/maven_packages_spec.rb
index 1b378788b6a..d7cc6991ef4 100644
--- a/spec/requests/api/maven_packages_spec.rb
+++ b/spec/requests/api/maven_packages_spec.rb
@@ -2,6 +2,7 @@
require 'spec_helper'
RSpec.describe API::MavenPackages do
+ using RSpec::Parameterized::TableSyntax
include WorkhorseHelpers
include_context 'workhorse headers'
@@ -40,15 +41,15 @@ RSpec.describe API::MavenPackages do
project.add_developer(user)
end
- shared_examples 'handling groups and subgroups for' do |shared_example_name, visibilities: %i[public]|
+ shared_examples 'handling groups and subgroups for' do |shared_example_name, visibilities: { public: :redirect }|
context 'within a group' do
- visibilities.each do |visibility|
+ visibilities.each do |visibility, not_found_response|
context "that is #{visibility}" do
before do
group.update!(visibility_level: Gitlab::VisibilityLevel.level_value(visibility.to_s))
end
- it_behaves_like shared_example_name
+ it_behaves_like shared_example_name, not_found_response
end
end
end
@@ -60,20 +61,20 @@ RSpec.describe API::MavenPackages do
move_project_to_namespace(subgroup)
end
- visibilities.each do |visibility|
+ visibilities.each do |visibility, not_found_response|
context "that is #{visibility}" do
before do
subgroup.update!(visibility_level: Gitlab::VisibilityLevel.level_value(visibility.to_s))
group.update!(visibility_level: Gitlab::VisibilityLevel.level_value(visibility.to_s))
end
- it_behaves_like shared_example_name
+ it_behaves_like shared_example_name, not_found_response
end
end
end
end
- shared_examples 'handling groups, subgroups and user namespaces for' do |shared_example_name, visibilities: %i[public]|
+ shared_examples 'handling groups, subgroups and user namespaces for' do |shared_example_name, visibilities: { public: :redirect }|
it_behaves_like 'handling groups and subgroups for', shared_example_name, visibilities: visibilities
context 'within a user namespace' do
@@ -103,16 +104,6 @@ RSpec.describe API::MavenPackages do
end
end
- shared_examples 'rejecting the request for non existing maven path' do |expected_status: :not_found|
- it 'rejects the request' do
- expect(::Packages::Maven::PackageFinder).not_to receive(:new)
-
- subject
-
- expect(response).to have_gitlab_http_status(expected_status)
- end
- end
-
shared_examples 'processing HEAD requests' do |instance_level: false|
subject { head api(url) }
@@ -162,7 +153,7 @@ RSpec.describe API::MavenPackages do
context 'with a non existing maven path' do
let(:path) { 'foo/bar/1.2.3' }
- it_behaves_like 'rejecting the request for non existing maven path', expected_status: instance_level ? :forbidden : :not_found
+ it_behaves_like 'returning response status', instance_level ? :forbidden : :redirect
end
end
end
@@ -238,12 +229,66 @@ RSpec.describe API::MavenPackages do
end
end
+ shared_examples 'forwarding package requests' do
+ context 'request forwarding' do
+ include_context 'dependency proxy helpers context'
+
+ subject { download_file(file_name: package_name) }
+
+ shared_examples 'redirecting the request' do
+ it_behaves_like 'returning response status', :redirect
+ end
+
+ shared_examples 'package not found' do
+ it_behaves_like 'returning response status', :not_found
+ end
+
+ where(:forward, :package_in_project, :shared_examples_name) do
+ true | true | 'successfully returning the file'
+ true | false | 'redirecting the request'
+ false | true | 'successfully returning the file'
+ false | false | 'package not found'
+ end
+
+ with_them do
+ let(:package_name) { package_in_project ? package_file.file_name : 'foo' }
+
+ before do
+ allow_fetch_application_setting(attribute: 'maven_package_requests_forwarding', return_value: forward)
+ end
+
+ it_behaves_like params[:shared_examples_name]
+ end
+
+ context 'with maven_central_request_forwarding disabled' do
+ where(:forward, :package_in_project, :shared_examples_name) do
+ true | true | 'successfully returning the file'
+ true | false | 'package not found'
+ false | true | 'successfully returning the file'
+ false | false | 'package not found'
+ end
+
+ with_them do
+ let(:package_name) { package_in_project ? package_file.file_name : 'foo' }
+
+ before do
+ stub_feature_flags(maven_central_request_forwarding: false)
+ allow_fetch_application_setting(attribute: 'maven_package_requests_forwarding', return_value: forward)
+ end
+
+ it_behaves_like params[:shared_examples_name]
+ end
+ end
+ end
+ end
+
describe 'GET /api/v4/packages/maven/*path/:file_name' do
context 'a public project' do
subject { download_file(file_name: package_file.file_name) }
shared_examples 'getting a file' do
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'
it_behaves_like 'file download in FIPS mode'
@@ -258,7 +303,16 @@ RSpec.describe API::MavenPackages do
context 'with a non existing maven path' do
subject { download_file(file_name: package_file.file_name, path: 'foo/bar/1.2.3') }
- it_behaves_like 'rejecting the request for non existing maven path', expected_status: :forbidden
+ it_behaves_like 'returning response status', :forbidden
+ end
+
+ it 'returns not found when a package is not found' do
+ finder = double('finder', execute: nil)
+ expect(::Packages::Maven::PackageFinder).to receive(:new).and_return(finder)
+
+ subject
+
+ expect(response).to have_gitlab_http_status(:not_found)
end
end
@@ -275,7 +329,7 @@ RSpec.describe API::MavenPackages do
shared_examples 'getting a file' do
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'
it 'denies download when no private token' do
@@ -285,17 +339,16 @@ RSpec.describe API::MavenPackages do
end
it_behaves_like 'downloads with a job token'
-
it_behaves_like 'downloads with a deploy token'
context 'with a non existing maven path' do
subject { download_file_with_token(file_name: package_file.file_name, path: 'foo/bar/1.2.3') }
- it_behaves_like 'rejecting the request for non existing maven path', expected_status: :forbidden
+ it_behaves_like 'returning response status', :forbidden
end
end
- it_behaves_like 'handling groups, subgroups and user namespaces for', 'getting a file', visibilities: %i[public internal]
+ it_behaves_like 'handling groups, subgroups and user namespaces for', 'getting a file', visibilities: { public: :redirect, internal: :not_found }
end
context 'private project' do
@@ -307,7 +360,7 @@ RSpec.describe API::MavenPackages do
shared_examples 'getting a file' do
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'
it 'denies download when not enough permissions' do
@@ -327,7 +380,6 @@ RSpec.describe API::MavenPackages do
end
it_behaves_like 'downloads with a job token'
-
it_behaves_like 'downloads with a deploy token'
it 'does not allow download by a unauthorized deploy token with same id as a user with access' do
@@ -350,11 +402,11 @@ RSpec.describe API::MavenPackages do
context 'with a non existing maven path' do
subject { download_file_with_token(file_name: package_file.file_name, path: 'foo/bar/1.2.3') }
- it_behaves_like 'rejecting the request for non existing maven path', expected_status: :forbidden
+ it_behaves_like 'returning response status', :forbidden
end
end
- it_behaves_like 'handling groups, subgroups and user namespaces for', 'getting a file', visibilities: %i[public internal private]
+ it_behaves_like 'handling groups, subgroups and user namespaces for', 'getting a file', visibilities: { public: :redirect, internal: :not_found, private: :not_found }
end
context 'project name is different from a package name' do
@@ -409,11 +461,14 @@ RSpec.describe API::MavenPackages do
group.add_developer(user)
end
+ it_behaves_like 'forwarding package requests'
+
context 'a public project' do
subject { download_file(file_name: package_file.file_name) }
shared_examples 'getting a file for a group' do
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'
it_behaves_like 'file download in FIPS mode'
@@ -428,7 +483,7 @@ RSpec.describe API::MavenPackages do
context 'with a non existing maven path' do
subject { download_file(file_name: package_file.file_name, path: 'foo/bar/1.2.3') }
- it_behaves_like 'rejecting the request for non existing maven path'
+ it_behaves_like 'returning response status', :redirect
end
end
@@ -443,29 +498,28 @@ RSpec.describe API::MavenPackages do
subject { download_file_with_token(file_name: package_file.file_name) }
- shared_examples 'getting a file for a group' do
+ shared_examples 'getting a file for a group' do |not_found_response|
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'
- it 'denies download when no private token' do
+ it 'forwards download when no private token' do
download_file(file_name: package_file.file_name)
- expect(response).to have_gitlab_http_status(:not_found)
+ expect(response).to have_gitlab_http_status(not_found_response)
end
it_behaves_like 'downloads with a job token'
-
it_behaves_like 'downloads with a deploy token'
context 'with a non existing maven path' do
subject { download_file_with_token(file_name: package_file.file_name, path: 'foo/bar/1.2.3') }
- it_behaves_like 'rejecting the request for non existing maven path'
+ it_behaves_like 'returning response status', :redirect
end
end
- it_behaves_like 'handling groups and subgroups for', 'getting a file for a group', visibilities: %i[internal public]
+ it_behaves_like 'handling groups and subgroups for', 'getting a file for a group', visibilities: { internal: :not_found, public: :redirect }
end
context 'private project' do
@@ -475,9 +529,9 @@ RSpec.describe API::MavenPackages do
subject { download_file_with_token(file_name: package_file.file_name) }
- shared_examples 'getting a file for a group' do
+ shared_examples 'getting a file for a group' do |not_found_response|
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'
it 'denies download when not enough permissions' do
@@ -485,23 +539,22 @@ RSpec.describe API::MavenPackages do
subject
- expect(response).to have_gitlab_http_status(:not_found)
+ expect(response).to have_gitlab_http_status(:redirect)
end
it 'denies download when no private token' do
download_file(file_name: package_file.file_name)
- expect(response).to have_gitlab_http_status(:not_found)
+ expect(response).to have_gitlab_http_status(not_found_response)
end
it_behaves_like 'downloads with a job token'
-
it_behaves_like 'downloads with a deploy token'
context 'with a non existing maven path' do
subject { download_file_with_token(file_name: package_file.file_name, path: 'foo/bar/1.2.3') }
- it_behaves_like 'rejecting the request for non existing maven path'
+ it_behaves_like 'returning response status', :redirect
end
context 'with group deploy token' do
@@ -521,12 +574,12 @@ RSpec.describe API::MavenPackages do
context 'with a non existing maven path' do
subject { download_file_with_token(file_name: package_file.file_name, path: 'foo/bar/1.2.3', request_headers: group_deploy_token_headers) }
- it_behaves_like 'rejecting the request for non existing maven path'
+ it_behaves_like 'returning response status', :redirect
end
end
end
- it_behaves_like 'handling groups and subgroups for', 'getting a file for a group', visibilities: %i[private internal public]
+ it_behaves_like 'handling groups and subgroups for', 'getting a file for a group', visibilities: { private: :not_found, internal: :not_found, public: :redirect }
context 'with a reporter from a subgroup accessing the root group' do
let_it_be(:root_group) { create(:group, :private) }
@@ -544,7 +597,7 @@ RSpec.describe API::MavenPackages do
context 'with a non existing maven path' do
subject { download_file_with_token(file_name: package_file.file_name, path: 'foo/bar/1.2.3', request_headers: headers_with_token, group_id: root_group.id) }
- it_behaves_like 'rejecting the request for non existing maven path'
+ it_behaves_like 'returning response status', :redirect
end
end
end
@@ -640,12 +693,14 @@ RSpec.describe API::MavenPackages do
it_behaves_like 'successfully returning the file'
it_behaves_like 'file download in FIPS mode'
- it 'returns sha1 of the file' do
- download_file(file_name: package_file.file_name + '.sha1')
+ %w[sha1 md5].each do |format|
+ it "returns #{format} of the file" do
+ download_file(file_name: package_file.file_name + ".#{format}")
- expect(response).to have_gitlab_http_status(:ok)
- expect(response.media_type).to eq('text/plain')
- expect(response.body).to eq(package_file.file_sha1)
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.media_type).to eq('text/plain')
+ expect(response.body).to eq(package_file.send("file_#{format}".to_sym))
+ end
end
context 'when the repository is disabled' do
@@ -664,7 +719,7 @@ RSpec.describe API::MavenPackages do
context 'with a non existing maven path' do
subject { download_file(file_name: package_file.file_name, path: 'foo/bar/1.2.3') }
- it_behaves_like 'rejecting the request for non existing maven path'
+ it_behaves_like 'returning response status', :redirect
end
end
@@ -676,7 +731,7 @@ RSpec.describe API::MavenPackages do
subject { download_file_with_token(file_name: package_file.file_name) }
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'
it 'denies download when not enough permissions' do
@@ -694,16 +749,17 @@ RSpec.describe API::MavenPackages do
end
it_behaves_like 'downloads with a job token'
-
it_behaves_like 'downloads with a deploy token'
context 'with a non existing maven path' do
subject { download_file_with_token(file_name: package_file.file_name, path: 'foo/bar/1.2.3') }
- it_behaves_like 'rejecting the request for non existing maven path'
+ it_behaves_like 'returning response status', :redirect
end
end
+ it_behaves_like 'forwarding package requests'
+
def download_file(file_name:, params: {}, request_headers: headers, path: maven_metadatum.path)
get api("/projects/#{project.id}/packages/maven/" \
"#{path}/#{file_name}"), params: params, headers: request_headers
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index 2a03ae89389..9d153286d14 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -9,6 +9,7 @@ RSpec.describe API::MergeRequests do
let_it_be(:user) { create(:user) }
let_it_be(:user2) { create(:user) }
let_it_be(:admin) { create(:user, :admin) }
+ let_it_be(:bot) { create(:user, :project_bot) }
let_it_be(:project) { create(:project, :public, :repository, creator: user, namespace: user.namespace, only_allow_merge_if_pipeline_succeeds: false) }
let(:milestone1) { create(:milestone, title: '0.9', project: project) }
@@ -1022,6 +1023,22 @@ RSpec.describe API::MergeRequests do
it_behaves_like 'a non-cached MergeRequest api request', 1
end
+ context 'when the assignees change' do
+ before do
+ merge_request.assignees << create(:user)
+ end
+
+ it_behaves_like 'a non-cached MergeRequest api request', 1
+ end
+
+ context 'when the reviewers change' do
+ before do
+ merge_request.reviewers << create(:user)
+ end
+
+ it_behaves_like 'a non-cached MergeRequest api request', 1
+ end
+
context 'when another user requests' do
before do
sign_in(user2)
@@ -1120,6 +1137,44 @@ RSpec.describe API::MergeRequests do
end.not_to exceed_query_limit(control)
end
end
+
+ context 'when user is an inherited member from the group' do
+ let_it_be(:group) { create(:group) }
+
+ shared_examples 'user cannot view merge requests' do
+ it 'returns 403 forbidden' do
+ get api("/projects/#{group_project.id}/merge_requests", inherited_user)
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+
+ context 'and user is a guest' do
+ let_it_be(:inherited_user) { create(:user) }
+
+ before_all do
+ group.add_guest(inherited_user)
+ end
+
+ context 'when project is public with private merge requests' do
+ let(:group_project) do
+ create(:project,
+ :public,
+ :repository,
+ group: group,
+ merge_requests_access_level: ProjectFeature::DISABLED)
+ end
+
+ it_behaves_like 'user cannot view merge requests'
+ end
+
+ context 'when project is private' do
+ let(:group_project) { create(:project, :private, :repository, group: group) }
+
+ it_behaves_like 'user cannot view merge requests'
+ end
+ end
+ end
end
describe "GET /groups/:id/merge_requests" do
@@ -1528,7 +1583,6 @@ RSpec.describe API::MergeRequests do
expect(json_response.last['user']['name']).to eq(reviewer.name)
expect(json_response.last['user']['username']).to eq(reviewer.username)
expect(json_response.last['state']).to eq('unreviewed')
- expect(json_response.last['updated_state_by']).to be_nil
expect(json_response.last['created_at']).to be_present
end
@@ -2219,6 +2273,59 @@ RSpec.describe API::MergeRequests do
expect(response).to have_gitlab_http_status(:created)
end
end
+
+ context 'when user is an inherited member from the group' do
+ let_it_be(:group) { create(:group) }
+
+ shared_examples 'user cannot create merge requests' do
+ it 'returns 403 forbidden' do
+ post api("/projects/#{group_project.id}/merge_requests", inherited_user), params: params
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+
+ context 'and user is a guest' do
+ let_it_be(:inherited_user) { create(:user) }
+ let_it_be(:params) do
+ {
+ title: 'Test merge request',
+ source_branch: 'feature_conflict',
+ target_branch: 'master',
+ author_id: inherited_user.id
+ }
+ end
+
+ before_all do
+ group.add_guest(inherited_user)
+ end
+
+ context 'when project is public with private merge requests' do
+ let(:group_project) do
+ create(:project,
+ :public,
+ :repository,
+ group: group,
+ merge_requests_access_level: ProjectFeature::DISABLED,
+ only_allow_merge_if_pipeline_succeeds: false)
+ end
+
+ it_behaves_like 'user cannot create merge requests'
+ end
+
+ context 'when project is private' do
+ let(:group_project) do
+ create(:project,
+ :private,
+ :repository,
+ group: group,
+ only_allow_merge_if_pipeline_succeeds: false)
+ end
+
+ it_behaves_like 'user cannot create merge requests'
+ end
+ end
+ end
end
describe 'PUT /projects/:id/merge_requests/:merge_request_iid' do
@@ -2247,6 +2354,16 @@ RSpec.describe API::MergeRequests do
expect(merge_request.notes.system.last.note).to include("assigned to #{user2.to_reference}")
end
+
+ it 'triggers webhooks', :sidekiq_inline do
+ hook = create(:project_hook, merge_requests_events: true, project: merge_request.project)
+
+ expect(WebHookWorker).to receive(:perform_async).with(hook.id, anything, 'merge_request_hooks', anything)
+
+ put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user), params: params
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
end
context 'when assignee_id=user2.id' do
@@ -3373,7 +3490,8 @@ RSpec.describe API::MergeRequests do
context 'when merge request branch does not allow force push' do
before do
- create(:protected_branch, project: project, name: merge_request.source_branch, allow_force_push: false)
+ create_params = { name: merge_request.source_branch, allow_force_push: false, merge_access_levels_attributes: [{ access_level: Gitlab::Access::DEVELOPER }] }
+ ProtectedBranches::CreateService.new(project, project.first_owner, create_params).execute
end
it 'returns 403' do
@@ -3413,6 +3531,71 @@ RSpec.describe API::MergeRequests do
end
end
+ describe 'PUT :id/merge_requests/:merge_request_iid/reset_approvals' do
+ before do
+ merge_request.approvals.create!(user: user2)
+ create(:project_member, :maintainer, user: bot, source: project)
+ end
+
+ context 'when reset_approvals can be performed' do
+ it 'clears approvals of the merge_request' do
+ put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/reset_approvals", bot)
+
+ merge_request.reload
+ expect(response).to have_gitlab_http_status(:accepted)
+ expect(merge_request.approvals).to be_empty
+ end
+
+ it 'for users with bot role' do
+ put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/reset_approvals", bot)
+
+ expect(response).to have_gitlab_http_status(:accepted)
+ end
+
+ context 'for users with non-bot roles' do
+ let(:human_user) { create(:user) }
+
+ [:add_owner, :add_maintainer, :add_developer, :add_guest].each do |role_method|
+ it 'returns 401' do
+ project.send(role_method, human_user)
+
+ put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/reset_approvals", human_user)
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+ end
+ end
+
+ context 'for bot-users from external namespaces' do
+ let_it_be(:external_bot) { create(:user, :project_bot) }
+
+ context 'external group bot-user' do
+ before do
+ create(:group_member, :maintainer, user: external_bot, source: create(:group))
+ end
+
+ it 'returns 401' do
+ put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/reset_approvals", external_bot)
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+ end
+
+ context 'external project bot-user' do
+ before do
+ create(:project_member, :maintainer, user: external_bot, source: create(:project))
+ end
+
+ it 'returns 401' do
+ put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/reset_approvals", external_bot)
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+ end
+ end
+ end
+ end
+
describe 'Time tracking' do
let!(:issuable) { create(:merge_request, :simple, author: user, assignees: [user], source_project: project, target_project: project, source_branch: 'markdown', title: "Test", created_at: base_time) }
diff --git a/spec/requests/api/ml/mlflow_spec.rb b/spec/requests/api/ml/mlflow_spec.rb
new file mode 100644
index 00000000000..4e7091a5b0f
--- /dev/null
+++ b/spec/requests/api/ml/mlflow_spec.rb
@@ -0,0 +1,366 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require 'mime/types'
+
+RSpec.describe API::Ml::Mlflow do
+ include SessionHelpers
+ include ApiHelpers
+ include HttpBasicAuthHelpers
+
+ let_it_be(:project) { create(:project, :private) }
+ let_it_be(:developer) { create(:user).tap { |u| project.add_developer(u) } }
+ let_it_be(:experiment) do
+ create(:ml_experiments, user: project.creator, project: project)
+ end
+
+ let_it_be(:candidate) do
+ create(:ml_candidates, user: experiment.user, start_time: 1234, experiment: experiment)
+ end
+
+ let_it_be(:another_candidate) do
+ create(:ml_candidates,
+ experiment: create(:ml_experiments, project: create(:project)))
+ end
+
+ let(:current_user) { developer }
+ let(:ff_value) { true }
+ let(:scopes) { %w[read_api api] }
+ let(:headers) do
+ { 'Authorization' => "Bearer #{create(:personal_access_token, scopes: scopes, user: current_user).token}" }
+ end
+
+ let(:params) { {} }
+ let(:request) { get api(route), params: params, headers: headers }
+
+ before do
+ stub_feature_flags(ml_experiment_tracking: ff_value)
+
+ request
+ end
+
+ shared_examples 'Not Found' do |message|
+ it "is Not Found" do
+ expect(response).to have_gitlab_http_status(:not_found)
+
+ expect(json_response['message']).to eq(message) if message.present?
+ end
+ end
+
+ shared_examples 'Not Found - Resource Does Not Exist' do
+ it "is Resource Does Not Exist" do
+ expect(response).to have_gitlab_http_status(:not_found)
+
+ expect(json_response).to include({ "error_code" => 'RESOURCE_DOES_NOT_EXIST' })
+ end
+ end
+
+ shared_examples 'Requires api scope' do
+ context 'when user has access but token has wrong scope' do
+ let(:scopes) { %w[read_api] }
+
+ it { expect(response).to have_gitlab_http_status(:forbidden) }
+ end
+ end
+
+ shared_examples 'Requires read_api scope' do
+ context 'when user has access but token has wrong scope' do
+ let(:scopes) { %w[read_user] }
+
+ it { expect(response).to have_gitlab_http_status(:forbidden) }
+ end
+ end
+
+ shared_examples 'Bad Request' do |error_code = nil|
+ it "is Bad Request" do
+ expect(response).to have_gitlab_http_status(:bad_request)
+
+ expect(json_response).to include({ 'error_code' => error_code }) if error_code.present?
+ end
+ end
+
+ shared_examples 'shared error cases' do
+ context 'when not authenticated' do
+ let(:headers) { {} }
+
+ it "is Unauthorized" do
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+ end
+
+ context 'when user does not have access' do
+ let(:current_user) { create(:user) }
+
+ it_behaves_like 'Not Found'
+ end
+
+ context 'when ff is disabled' do
+ let(:ff_value) { false }
+
+ it_behaves_like 'Not Found'
+ end
+ end
+
+ describe 'GET /projects/:id/ml/mflow/api/2.0/mlflow/get' do
+ let(:experiment_iid) { experiment.iid.to_s }
+ let(:route) { "/projects/#{project.id}/ml/mflow/api/2.0/mlflow/experiments/get?experiment_id=#{experiment_iid}" }
+
+ it 'returns the experiment' do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to match_response_schema('ml/get_experiment')
+ expect(json_response).to include({
+ 'experiment' => {
+ 'experiment_id' => experiment_iid,
+ 'name' => experiment.name,
+ 'lifecycle_stage' => 'active',
+ 'artifact_location' => 'not_implemented'
+ }
+ })
+ end
+
+ describe 'Error States' do
+ context 'when has access' do
+ context 'and experiment does not exist' do
+ let(:experiment_iid) { non_existing_record_iid.to_s }
+
+ it_behaves_like 'Not Found - Resource Does Not Exist'
+ end
+
+ context 'and experiment_id is not passed' do
+ let(:route) { "/projects/#{project.id}/ml/mflow/api/2.0/mlflow/experiments/get" }
+
+ it_behaves_like 'Not Found - Resource Does Not Exist'
+ end
+ end
+
+ it_behaves_like 'shared error cases'
+ it_behaves_like 'Requires read_api scope'
+ end
+ end
+
+ describe 'GET /projects/:id/ml/mflow/api/2.0/mlflow/experiments/get-by-name' do
+ let(:experiment_name) { experiment.name }
+ let(:route) do
+ "/projects/#{project.id}/ml/mflow/api/2.0/mlflow/experiments/get-by-name?experiment_name=#{experiment_name}"
+ end
+
+ it 'returns the experiment' do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to match_response_schema('ml/get_experiment')
+ expect(json_response).to include({
+ 'experiment' => {
+ 'experiment_id' => experiment.iid.to_s,
+ 'name' => experiment_name,
+ 'lifecycle_stage' => 'active',
+ 'artifact_location' => 'not_implemented'
+ }
+ })
+ end
+
+ describe 'Error States' do
+ context 'when has access but experiment does not exist' do
+ let(:experiment_name) { "random_experiment" }
+
+ it_behaves_like 'Not Found - Resource Does Not Exist'
+ end
+
+ context 'when has access but experiment_name is not passed' do
+ let(:route) { "/projects/#{project.id}/ml/mflow/api/2.0/mlflow/experiments/get-by-name" }
+
+ it_behaves_like 'Not Found - Resource Does Not Exist'
+ end
+
+ it_behaves_like 'shared error cases'
+ it_behaves_like 'Requires read_api scope'
+ end
+ end
+
+ describe 'POST /projects/:id/ml/mflow/api/2.0/mlflow/experiments/create' do
+ let(:route) do
+ "/projects/#{project.id}/ml/mflow/api/2.0/mlflow/experiments/create"
+ end
+
+ let(:params) { { name: 'new_experiment' } }
+ let(:request) { post api(route), params: params, headers: headers }
+
+ it 'creates the experiment' do
+ expect(response).to have_gitlab_http_status(:created)
+ expect(json_response).to include('experiment_id' )
+ end
+
+ describe 'Error States' do
+ context 'when experiment name is not passed' do
+ let(:params) { {} }
+
+ it_behaves_like 'Bad Request'
+ end
+
+ context 'when experiment name already exists' do
+ let(:existing_experiment) do
+ create(:ml_experiments, user: current_user, project: project)
+ end
+
+ let(:params) { { name: existing_experiment.name } }
+
+ it_behaves_like 'Bad Request', 'RESOURCE_ALREADY_EXISTS'
+ end
+
+ context 'when project does not exist' do
+ let(:route) { "/projects/#{non_existing_record_id}/ml/mflow/api/2.0/mlflow/experiments/create" }
+
+ it_behaves_like 'Not Found', '404 Project Not Found'
+ end
+
+ it_behaves_like 'shared error cases'
+ it_behaves_like 'Requires api scope'
+ end
+ end
+
+ describe 'Runs' do
+ describe 'POST /projects/:id/ml/mflow/api/2.0/mlflow/runs/create' do
+ let(:route) do
+ "/projects/#{project.id}/ml/mflow/api/2.0/mlflow/runs/create"
+ end
+
+ let(:params) { { experiment_id: experiment.iid.to_s, start_time: Time.now.to_i } }
+ let(:request) { post api(route), params: params, headers: headers }
+
+ it 'creates the run' do
+ expected_properties = {
+ 'experiment_id' => params[:experiment_id],
+ 'user_id' => current_user.id.to_s,
+ 'start_time' => params[:start_time],
+ 'artifact_uri' => 'not_implemented',
+ 'status' => "RUNNING",
+ 'lifecycle_stage' => "active"
+ }
+
+ expect(response).to have_gitlab_http_status(:created)
+ expect(response).to match_response_schema('ml/run')
+ expect(json_response['run']).to include('info' => hash_including(**expected_properties), 'data' => {})
+ end
+
+ describe 'Error States' do
+ context 'when experiment id is not passed' do
+ let(:params) { {} }
+
+ it_behaves_like 'Bad Request'
+ end
+
+ context 'when experiment id does not exist' do
+ let(:params) { { experiment_id: non_existing_record_iid.to_s } }
+
+ it_behaves_like 'Not Found - Resource Does Not Exist'
+ end
+
+ it_behaves_like 'shared error cases'
+ it_behaves_like 'Requires api scope'
+ end
+ end
+
+ describe 'GET /projects/:id/ml/mflow/api/2.0/mlflow/runs/get' do
+ let_it_be(:route) do
+ "/projects/#{project.id}/ml/mflow/api/2.0/mlflow/runs/get"
+ end
+
+ let_it_be(:candidate) { create(:ml_candidates, user: experiment.user, start_time: 1234, experiment: experiment) }
+
+ let(:params) { { 'run_id' => candidate.iid } }
+
+ it 'gets the run' do
+ expected_properties = {
+ 'experiment_id' => candidate.experiment.iid.to_s,
+ 'user_id' => candidate.user.id.to_s,
+ 'start_time' => candidate.start_time,
+ 'artifact_uri' => 'not_implemented',
+ 'status' => "RUNNING",
+ 'lifecycle_stage' => "active"
+ }
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(response).to match_response_schema('ml/run')
+ expect(json_response['run']).to include('info' => hash_including(**expected_properties), 'data' => {})
+ end
+
+ describe 'Error States' do
+ context 'when run id is not passed' do
+ let(:params) { {} }
+
+ it_behaves_like 'Not Found - Resource Does Not Exist'
+ end
+
+ context 'when run id does not exist' do
+ let(:params) { { run_id: non_existing_record_iid.to_s } }
+
+ it_behaves_like 'Not Found - Resource Does Not Exist'
+ end
+
+ context 'when run id exists but does not belong to project' do
+ let(:params) { { run_id: another_candidate.iid.to_s } }
+
+ it_behaves_like 'Not Found - Resource Does Not Exist'
+ end
+
+ it_behaves_like 'shared error cases'
+ it_behaves_like 'Requires read_api scope'
+ end
+ end
+ end
+
+ describe 'POST /projects/:id/ml/mflow/api/2.0/mlflow/runs/update' do
+ let(:route) { "/projects/#{project.id}/ml/mflow/api/2.0/mlflow/runs/update" }
+ let(:params) { { run_id: candidate.iid.to_s, status: 'FAILED', end_time: Time.now.to_i } }
+ let(:request) { post api(route), params: params, headers: headers }
+
+ it 'updates the run' do
+ expected_properties = {
+ 'experiment_id' => candidate.experiment.iid.to_s,
+ 'user_id' => candidate.user.id.to_s,
+ 'start_time' => candidate.start_time,
+ 'end_time' => params[:end_time],
+ 'artifact_uri' => 'not_implemented',
+ 'status' => 'FAILED',
+ 'lifecycle_stage' => 'active'
+ }
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(response).to match_response_schema('ml/update_run')
+ expect(json_response).to include('run_info' => hash_including(**expected_properties))
+ end
+
+ describe 'Error States' do
+ context 'when run id is not passed' do
+ let(:params) { {} }
+
+ it_behaves_like 'Not Found - Resource Does Not Exist'
+ end
+
+ context 'when run id does not exist' do
+ let(:params) { { run_id: non_existing_record_iid.to_s } }
+
+ it_behaves_like 'Not Found - Resource Does Not Exist'
+ end
+
+ context 'when run id exists but does not belong to project' do
+ let(:params) { { run_id: another_candidate.iid.to_s } }
+
+ it_behaves_like 'Not Found - Resource Does Not Exist'
+ end
+
+ context 'when run id exists but status in invalid' do
+ let(:params) { { run_id: candidate.iid.to_s, status: 'YOLO', end_time: Time.now.to_i } }
+
+ it_behaves_like 'Bad Request'
+ end
+
+ context 'when run id exists but end_time is invalid' do
+ let(:params) { { run_id: candidate.iid.to_s, status: 'FAILED', end_time: 's' } }
+
+ it_behaves_like 'Bad Request'
+ end
+
+ it_behaves_like 'shared error cases'
+ it_behaves_like 'Requires api scope'
+ end
+ end
+end
diff --git a/spec/requests/api/namespaces_spec.rb b/spec/requests/api/namespaces_spec.rb
index 09b87f41b82..ab39c29653f 100644
--- a/spec/requests/api/namespaces_spec.rb
+++ b/spec/requests/api/namespaces_spec.rb
@@ -285,6 +285,14 @@ RSpec.describe API::Namespaces do
end
context 'when authenticated' do
+ it_behaves_like 'rate limited endpoint', rate_limit_key: :namespace_exists do
+ let(:current_user) { user }
+
+ def request
+ get api("/namespaces/#{namespace1.path}/exists", current_user)
+ end
+ end
+
it 'returns JSON indicating the namespace exists and a suggestion' do
get api("/namespaces/#{namespace1.path}/exists", user)
diff --git a/spec/requests/api/npm_project_packages_spec.rb b/spec/requests/api/npm_project_packages_spec.rb
index 3bcffac2760..bdcd6e7278d 100644
--- a/spec/requests/api/npm_project_packages_spec.rb
+++ b/spec/requests/api/npm_project_packages_spec.rb
@@ -63,6 +63,7 @@ RSpec.describe API::NpmProjectPackages do
it_behaves_like 'successfully downloads the file'
it_behaves_like 'a package tracking event', 'API::NpmPackages', 'pull_package'
+ it_behaves_like 'bumping the package last downloaded at field'
end
context 'with job token' do
@@ -70,12 +71,14 @@ RSpec.describe API::NpmProjectPackages do
it_behaves_like 'successfully downloads the file'
it_behaves_like 'a package tracking event', 'API::NpmPackages', 'pull_package'
+ it_behaves_like 'bumping the package last downloaded at field'
end
end
context 'a public project' do
it_behaves_like 'successfully downloads the file'
it_behaves_like 'a package tracking event', 'API::NpmPackages', 'pull_package'
+ it_behaves_like 'bumping the package last downloaded at field'
context 'with a job token for a different user' do
let_it_be(:other_user) { create(:user) }
diff --git a/spec/requests/api/personal_access_tokens/self_revocation_spec.rb b/spec/requests/api/personal_access_tokens/self_revocation_spec.rb
new file mode 100644
index 00000000000..f829b39cc1e
--- /dev/null
+++ b/spec/requests/api/personal_access_tokens/self_revocation_spec.rb
@@ -0,0 +1,69 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe API::PersonalAccessTokens::SelfRevocation do
+ let_it_be(:current_user) { create(:user) }
+
+ describe 'DELETE /personal_access_tokens/self' do
+ let(:path) { '/personal_access_tokens/self' }
+ let(:token) { create(:personal_access_token, user: current_user) }
+
+ subject(:delete_token) { delete api(path, personal_access_token: token) }
+
+ shared_examples 'revoking token succeeds' do
+ it 'revokes token' do
+ delete_token
+
+ expect(response).to have_gitlab_http_status(:no_content)
+ expect(token.reload).to be_revoked
+ end
+ end
+
+ shared_examples 'revoking token denied' do |status|
+ it 'cannot revoke token' do
+ delete_token
+
+ expect(response).to have_gitlab_http_status(status)
+ end
+ end
+
+ context 'when current_user is an administrator', :enable_admin_mode do
+ let(:current_user) { create(:admin) }
+
+ it_behaves_like 'revoking token succeeds'
+
+ context 'with impersonated token' do
+ let(:token) { create(:personal_access_token, :impersonation, user: current_user) }
+
+ it_behaves_like 'revoking token succeeds'
+ end
+ end
+
+ context 'when current_user is not an administrator' do
+ let(:current_user) { create(:user) }
+
+ it_behaves_like 'revoking token succeeds'
+
+ context 'with impersonated token' do
+ let(:token) { create(:personal_access_token, :impersonation, user: current_user) }
+
+ it_behaves_like 'revoking token denied', :bad_request
+ end
+
+ context 'with already revoked token' do
+ let(:token) { create(:personal_access_token, :revoked, user: current_user) }
+
+ it_behaves_like 'revoking token denied', :unauthorized
+ end
+ end
+
+ Gitlab::Auth.all_available_scopes.each do |scope|
+ context "with a '#{scope}' scoped token" do
+ let(:token) { create(:personal_access_token, scopes: [scope], user: current_user) }
+
+ it_behaves_like 'revoking token succeeds'
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/personal_access_tokens_spec.rb b/spec/requests/api/personal_access_tokens_spec.rb
index 8d8998cfdd6..37b5a594f2a 100644
--- a/spec/requests/api/personal_access_tokens_spec.rb
+++ b/spec/requests/api/personal_access_tokens_spec.rb
@@ -75,6 +75,7 @@ RSpec.describe API::PersonalAccessTokens do
describe 'GET /personal_access_tokens/:id' do
let_it_be(:user_token) { create(:personal_access_token, user: current_user) }
+ let_it_be(:user_read_only_token) { create(:personal_access_token, scopes: ['read_repository'], user: current_user) }
let_it_be(:user_token_path) { "/personal_access_tokens/#{user_token.id}" }
let_it_be(:invalid_path) { "/personal_access_tokens/#{non_existing_record_id}" }
@@ -125,53 +126,11 @@ RSpec.describe API::PersonalAccessTokens do
expect(response).to have_gitlab_http_status(:unauthorized)
end
- end
- end
-
- describe 'DELETE /personal_access_tokens/self' do
- let(:path) { '/personal_access_tokens/self' }
- let(:token) { create(:personal_access_token, user: current_user) }
-
- subject { delete api(path, current_user, personal_access_token: token) }
-
- shared_examples 'revoking token succeeds' do
- it 'revokes token' do
- subject
-
- expect(response).to have_gitlab_http_status(:no_content)
- expect(token.reload).to be_revoked
- end
- end
- shared_examples 'revoking token denied' do |status|
- it 'cannot revoke token' do
- subject
+ it 'fails to return own PAT by id with read_repository token' do
+ get api(user_token_path, current_user, personal_access_token: user_read_only_token)
- expect(response).to have_gitlab_http_status(status)
- end
- end
-
- context 'when current_user is an administrator', :enable_admin_mode do
- let(:current_user) { create(:admin) }
-
- it_behaves_like 'revoking token succeeds'
- end
-
- context 'when current_user is not an administrator' do
- let(:current_user) { create(:user) }
-
- it_behaves_like 'revoking token succeeds'
-
- context 'with impersonated token' do
- let(:token) { create(:personal_access_token, :impersonation, user: current_user) }
-
- it_behaves_like 'revoking token denied', :bad_request
- end
-
- context 'with already revoked token' do
- let(:token) { create(:personal_access_token, :revoked, user: current_user) }
-
- it_behaves_like 'revoking token denied', :unauthorized
+ expect(response).to have_gitlab_http_status(:forbidden)
end
end
end
@@ -183,6 +142,9 @@ RSpec.describe API::PersonalAccessTokens do
let_it_be(:admin_user) { create(:admin) }
let_it_be(:admin_token) { create(:personal_access_token, user: admin_user) }
let_it_be(:admin_path) { "/personal_access_tokens/#{admin_token.id}" }
+ let_it_be(:admin_read_only_token) do
+ create(:personal_access_token, scopes: ['read_repository'], user: admin_user)
+ end
it 'revokes a different users token' do
delete api(path, admin_user)
@@ -196,6 +158,12 @@ RSpec.describe API::PersonalAccessTokens do
expect(response).to have_gitlab_http_status(:no_content)
end
+
+ it 'fails to revoke a different user token using a readonly scope' do
+ delete api(path, personal_access_token: admin_read_only_token)
+
+ expect(token1.reload.revoked?).to be false
+ end
end
context 'when current_user is not an administrator' do
diff --git a/spec/requests/api/project_attributes.yml b/spec/requests/api/project_attributes.yml
index 670035187cb..1335fa02aaf 100644
--- a/spec/requests/api/project_attributes.yml
+++ b/spec/requests/api/project_attributes.yml
@@ -154,11 +154,13 @@ project_setting:
- project_id
- push_rule_id
- show_default_award_emojis
+ - show_diff_preview_in_email
- updated_at
- cve_id_request_enabled
- mr_default_target_self
- target_platforms
- selective_code_owner_removals
+ - show_diff_preview_in_email
build_service_desk_setting: # service_desk_setting
unexposed_attributes:
diff --git a/spec/requests/api/project_import_spec.rb b/spec/requests/api/project_import_spec.rb
index afe5a7d4a21..401db766589 100644
--- a/spec/requests/api/project_import_spec.rb
+++ b/spec/requests/api/project_import_spec.rb
@@ -47,7 +47,7 @@ RSpec.describe API::ProjectImport, :aggregate_failures do
it 'executes a limited number of queries' do
control_count = ActiveRecord::QueryRecorder.new { subject }.count
- expect(control_count).to be <= 109
+ expect(control_count).to be <= 110
end
it 'schedules an import using a namespace' do
diff --git a/spec/requests/api/project_packages_spec.rb b/spec/requests/api/project_packages_spec.rb
index 7a05da8e13f..00d295b3490 100644
--- a/spec/requests/api/project_packages_spec.rb
+++ b/spec/requests/api/project_packages_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe API::ProjectPackages do
let_it_be(:project) { create(:project, :public) }
let(:user) { create(:user) }
- let!(:package1) { create(:npm_package, project: project, version: '3.1.0', name: "@#{project.root_namespace.path}/foo1") }
+ let!(:package1) { create(:npm_package, :last_downloaded_at, project: project, version: '3.1.0', name: "@#{project.root_namespace.path}/foo1") }
let(:package_url) { "/projects/#{project.id}/packages/#{package1.id}" }
let!(:package2) { create(:nuget_package, project: project, version: '2.0.4') }
let!(:another_package) { create(:npm_package) }
@@ -272,6 +272,17 @@ RSpec.describe API::ProjectPackages do
it_behaves_like 'returns package', :project, :no_type
it_behaves_like 'returns package', :project, :guest
end
+
+ context 'with a package without last_downloaded_at' do
+ let(:package_url) { "/projects/#{project.id}/packages/#{package2.id}" }
+
+ it 'returns 200 and the package information' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to match_response_schema(single_package_schema)
+ end
+ end
end
context 'project is private' do
diff --git a/spec/requests/api/project_snippets_spec.rb b/spec/requests/api/project_snippets_spec.rb
index 72519ed1683..6e2dd6e76a9 100644
--- a/spec/requests/api/project_snippets_spec.rb
+++ b/spec/requests/api/project_snippets_spec.rb
@@ -256,6 +256,7 @@ RSpec.describe API::ProjectSnippets do
allow_next_instance_of(Spam::AkismetService) do |instance|
allow(instance).to receive(:spam?).and_return(true)
end
+ stub_feature_flags(allow_possible_spam: false)
project.add_developer(user)
end
@@ -311,6 +312,8 @@ RSpec.describe API::ProjectSnippets do
allow_next_instance_of(Spam::AkismetService) do |instance|
allow(instance).to receive(:spam?).and_return(true)
end
+
+ stub_feature_flags(allow_possible_spam: false)
end
context 'when the snippet is private' do
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index 94688833d88..7ad1ce0ede9 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -48,6 +48,7 @@ end
RSpec.describe API::Projects do
include ProjectForksHelper
+ include WorkhorseHelpers
include StubRequests
let_it_be(:user) { create(:user) }
@@ -1249,9 +1250,10 @@ RSpec.describe API::Projects do
stub_application_setting(import_sources: nil)
endpoint_url = "#{url}/info/refs?service=git-upload-pack"
- stub_full_request(endpoint_url, method: :get).to_return({ status: 200,
- body: '001e# service=git-upload-pack',
- headers: { 'Content-Type': 'application/x-git-upload-pack-advertisement' } })
+ stub_full_request(endpoint_url, method: :get).to_return(
+ { status: 200,
+ body: '001e# service=git-upload-pack',
+ headers: { 'Content-Type': 'application/x-git-upload-pack-advertisement' } })
project_params = { import_url: url, path: 'path-project-Foo', name: 'Foo Project' }
expect { post api('/projects', user), params: project_params }
@@ -1348,7 +1350,12 @@ RSpec.describe API::Projects do
it 'uploads avatar for project a project' do
project = attributes_for(:project, avatar: fixture_file_upload('spec/fixtures/banana_sample.gif', 'image/gif'))
- post api('/projects', user), params: project
+ workhorse_form_with_file(
+ api('/projects', user),
+ method: :post,
+ file_key: :avatar,
+ params: project
+ )
project_id = json_response['id']
expect(json_response['avatar_url']).to eq("http://localhost/uploads/-/system/project/avatar/#{project_id}/banana_sample.gif")
@@ -1924,8 +1931,6 @@ RSpec.describe API::Projects do
end
describe "POST /projects/:id/uploads/authorize" do
- include WorkhorseHelpers
-
let(:headers) { workhorse_internal_api_request_header.merge({ 'HTTP_GITLAB_WORKHORSE' => 1 }) }
context 'with authorized user' do
@@ -3583,18 +3588,77 @@ RSpec.describe API::Projects do
end
end
- it 'updates avatar' do
- project_param = {
- avatar: fixture_file_upload('spec/fixtures/banana_sample.gif',
- 'image/gif')
- }
+ context 'with changes to the avatar' do
+ let_it_be(:avatar_file) { fixture_file_upload('spec/fixtures/banana_sample.gif', 'image/gif') }
+ let_it_be(:alternate_avatar_file) { fixture_file_upload('spec/fixtures/rails_sample.png', 'image/png') }
+ let_it_be(:project_with_avatar, reload: true) do
+ create(:project,
+ :private,
+ :repository,
+ name: 'project-with-avatar',
+ creator_id: user.id,
+ namespace: user.namespace,
+ avatar: avatar_file)
+ end
- put api("/projects/#{project3.id}", user), params: project_param
+ it 'uploads avatar to project without an avatar' do
+ workhorse_form_with_file(
+ api("/projects/#{project3.id}", user),
+ method: :put,
+ file_key: :avatar,
+ params: { avatar: avatar_file }
+ )
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response['avatar_url']).to eq('http://localhost/uploads/'\
- '-/system/project/avatar/'\
- "#{project3.id}/banana_sample.gif")
+ aggregate_failures "testing response" do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['avatar_url']).to eq('http://localhost/uploads/'\
+ '-/system/project/avatar/'\
+ "#{project3.id}/banana_sample.gif")
+ end
+ end
+
+ it 'uploads and changes avatar to project with an avatar' do
+ workhorse_form_with_file(
+ api("/projects/#{project_with_avatar.id}", user),
+ method: :put,
+ file_key: :avatar,
+ params: { avatar: alternate_avatar_file }
+ )
+
+ aggregate_failures "testing response" do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['avatar_url']).to eq('http://localhost/uploads/'\
+ '-/system/project/avatar/'\
+ "#{project_with_avatar.id}/rails_sample.png")
+ end
+ end
+
+ it 'uploads and changes avatar to project among other changes' do
+ workhorse_form_with_file(
+ api("/projects/#{project_with_avatar.id}", user),
+ method: :put,
+ file_key: :avatar,
+ params: { description: 'changed description', avatar: avatar_file }
+ )
+
+ aggregate_failures "testing response" do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['description']).to eq('changed description')
+ expect(json_response['avatar_url']).to eq('http://localhost/uploads/'\
+ '-/system/project/avatar/'\
+ "#{project_with_avatar.id}/banana_sample.gif")
+ end
+ end
+
+ it 'removes avatar from project with an avatar' do
+ put api("/projects/#{project_with_avatar.id}", user), params: { avatar: '' }
+
+ aggregate_failures "testing response" do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['avatar_url']).to be_nil
+ expect(project_with_avatar.reload.avatar_url).to be_nil
+ end
+ end
end
it 'updates auto_devops_deploy_strategy' do
@@ -4645,6 +4709,100 @@ RSpec.describe API::Projects do
end
end
+ describe 'GET /projects/:id/transfer_locations' do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:source_group) { create(:group) }
+ let_it_be(:project) { create(:project, group: source_group) }
+
+ let(:params) { {} }
+
+ subject(:request) do
+ get api("/projects/#{project.id}/transfer_locations", user), params: params
+ end
+
+ context 'when the user has rights to transfer the project' do
+ let_it_be(:guest_group) { create(:group) }
+ let_it_be(:maintainer_group) { create(:group, name: 'maintainer group', path: 'maintainer-group') }
+ let_it_be(:owner_group) { create(:group, name: 'owner group', path: 'owner-group') }
+
+ before do
+ source_group.add_owner(user)
+ guest_group.add_guest(user)
+ maintainer_group.add_maintainer(user)
+ owner_group.add_owner(user)
+ end
+
+ it 'returns 200' do
+ request
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to include_pagination_headers
+ end
+
+ it 'includes groups where the user has permissions to transfer a project to' do
+ request
+
+ expect(project_ids_from_response).to include(maintainer_group.id, owner_group.id)
+ end
+
+ it 'does not include groups where the user doesn not have permissions to transfer a project' do
+ request
+
+ expect(project_ids_from_response).not_to include(guest_group.id)
+ end
+
+ context 'with search' do
+ let(:params) { { search: 'maintainer' } }
+
+ it 'includes groups where the user has permissions to transfer a project to' do
+ request
+
+ expect(project_ids_from_response).to contain_exactly(maintainer_group.id)
+ end
+ end
+
+ context 'group shares' do
+ let_it_be(:shared_to_owner_group) { create(:group) }
+ let_it_be(:shared_to_guest_group) { create(:group) }
+
+ before do
+ create(:group_group_link, :owner,
+ shared_with_group: owner_group,
+ shared_group: shared_to_owner_group
+ )
+
+ create(:group_group_link, :guest,
+ shared_with_group: guest_group,
+ shared_group: shared_to_guest_group
+ )
+ end
+
+ it 'only includes groups arising from group shares where the user has permission to transfer a project to' do
+ request
+
+ expect(project_ids_from_response).to include(shared_to_owner_group.id)
+ expect(project_ids_from_response).not_to include(shared_to_guest_group.id)
+ end
+ end
+
+ def project_ids_from_response
+ json_response.map { |project| project['id'] }
+ end
+ end
+
+ context 'when the user does not have permissions to transfer the project' do
+ before do
+ source_group.add_developer(user)
+ end
+
+ it 'returns 403' do
+ request
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+ end
+
describe 'GET /projects/:id/storage' do
context 'when unauthenticated' do
it 'does not return project storage data' do
diff --git a/spec/requests/api/releases_spec.rb b/spec/requests/api/releases_spec.rb
index 1d9e3a6c887..754b77af60e 100644
--- a/spec/requests/api/releases_spec.rb
+++ b/spec/requests/api/releases_spec.rb
@@ -573,6 +573,224 @@ RSpec.describe API::Releases do
end
end
+ describe 'GET /projects/:id/releases/:tag_name/downloads/*file_path' do
+ let!(:release) { create(:release, project: project, tag: 'v0.1', author: maintainer) }
+ let!(:link) { create(:release_link, release: release, url: "#{url}#{filepath}", filepath: filepath) }
+ let(:filepath) { '/bin/bigfile.exe' }
+ let(:url) { 'https://google.com/-/jobs/140463678/artifacts/download' }
+
+ context 'with an invalid release tag' do
+ it 'returns 404 for maintater' do
+ get api("/projects/#{project.id}/releases/v0.2/downloads#{filepath}", maintainer)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ expect(json_response['message']).to eq('404 Not Found')
+ end
+
+ it 'returns project not found for no user' do
+ get api("/projects/#{project.id}/releases/v0.2/downloads#{filepath}", nil)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ expect(json_response['message']).to eq('404 Project Not Found')
+ end
+
+ it 'returns forbidden for guest' do
+ get api("/projects/#{project.id}/releases/v0.2/downloads#{filepath}", guest)
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+
+ context 'with a valid release tag' do
+ context 'when filepath is provided' do
+ context 'when filepath exists' do
+ it 'redirects to the file download URL' do
+ get api("/projects/#{project.id}/releases/v0.1/downloads#{filepath}", maintainer)
+
+ expect(response).to redirect_to("#{url}#{filepath}")
+ end
+
+ it 'redirects to the file download URL when using JOB-TOKEN auth' do
+ job = create(:ci_build, :running, project: project, user: maintainer)
+
+ get api("/projects/#{project.id}/releases/v0.1/downloads#{filepath}"), params: { job_token: job.token }
+
+ expect(response).to redirect_to("#{url}#{filepath}")
+ end
+
+ context 'when user is a guest' do
+ it 'responds 403 Forbidden' do
+ get api("/projects/#{project.id}/releases/v0.1/downloads#{filepath}", guest)
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+
+ context 'when project is public' do
+ let(:project) { create(:project, :repository, :public) }
+
+ it 'responds 200 OK' do
+ get api("/projects/#{project.id}/releases/v0.1/downloads#{filepath}", guest)
+
+ expect(response).to redirect_to("#{url}#{filepath}")
+ end
+ end
+ end
+ end
+
+ context 'when filepath does not exists' do
+ it 'returns 404 for maintater' do
+ get api("/projects/#{project.id}/releases/v0.1/downloads/bin/not_existing.exe", maintainer)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ expect(json_response['message']).to eq('404 Not found')
+ end
+
+ it 'returns project not found for no user' do
+ get api("/projects/#{project.id}/releases/v0.1/downloads/bin/not_existing.exe", nil)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ expect(json_response['message']).to eq('404 Project Not Found')
+ end
+
+ it 'returns forbidden for guest' do
+ get api("/projects/#{project.id}/releases/v0.1/downloads/bin/not_existing.exe", guest)
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+ end
+
+ context 'when filepath is not provided' do
+ it 'returns 404 for maintater' do
+ get api("/projects/#{project.id}/releases/v0.1/downloads", maintainer)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+
+ it 'returns project not found for no user' do
+ get api("/projects/#{project.id}/releases/v0.1/downloads", nil)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+
+ it 'returns forbidden for guest' do
+ get api("/projects/#{project.id}/releases/v0.1/downloads", guest)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+ end
+
+ describe 'GET /projects/:id/releases/permalink/latest' do
+ context 'when there is no release' do
+ it 'returns not found' do
+ get api("/projects/#{project.id}/releases/permalink/latest", maintainer)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+
+ it 'returns not found when using JOB-TOKEN auth' do
+ job = create(:ci_build, :running, project: project, user: maintainer)
+
+ get api("/projects/#{project.id}/releases/permalink/latest"), params: { job_token: job.token }
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'when there are more than one release' do
+ let!(:release_a) do
+ create(:release,
+ project: project,
+ tag: 'v0.1',
+ author: maintainer,
+ description: 'This is v0.1',
+ released_at: 3.days.ago)
+ end
+
+ let!(:release_b) do
+ create(:release,
+ project: project,
+ tag: 'v0.2',
+ author: maintainer,
+ description: 'This is v0.2',
+ released_at: 2.days.ago)
+ end
+
+ it 'redirects to the latest release tag' do
+ get api("/projects/#{project.id}/releases/permalink/latest", maintainer)
+
+ uri = URI(response.header["Location"])
+
+ expect(response).to have_gitlab_http_status(:redirect)
+ expect(uri.path).to eq("/api/v4/projects/#{project.id}/releases/#{release_b.tag}")
+ end
+
+ it 'redirects to the latest release tag when using JOB-TOKEN auth' do
+ job = create(:ci_build, :running, project: project, user: maintainer)
+
+ get api("/projects/#{project.id}/releases/permalink/latest"), params: { job_token: job.token }
+
+ uri = URI(response.header["Location"])
+
+ expect(response).to have_gitlab_http_status(:redirect)
+ expect(uri.path).to eq("/api/v4/projects/#{project.id}/releases/#{release_b.tag}")
+ end
+
+ context 'when there are query parameters present' do
+ it 'includes the query params on the redirection' do
+ get api("/projects/#{project.id}/releases/permalink/latest", maintainer), params: { include_html_description: true, other_param: "aaa" }
+
+ uri = URI(response.header["Location"])
+ query_params = Rack::Utils.parse_nested_query(uri.query)
+
+ expect(response).to have_gitlab_http_status(:redirect)
+ expect(uri.path).to eq("/api/v4/projects/#{project.id}/releases/#{release_b.tag}")
+ expect(query_params).to include({
+ "include_html_description" => "true",
+ "other_param" => "aaa"
+ })
+ end
+
+ it 'discards the `order_by` query param' do
+ get api("/projects/#{project.id}/releases/permalink/latest", maintainer), params: { order_by: 'something', other_param: "aaa" }
+
+ uri = URI(response.header["Location"])
+ query_params = Rack::Utils.parse_nested_query(uri.query)
+
+ expect(response).to have_gitlab_http_status(:redirect)
+ expect(uri.path).to eq("/api/v4/projects/#{project.id}/releases/#{release_b.tag}")
+ expect(query_params).to include({
+ "other_param" => "aaa"
+ })
+ expect(query_params).not_to include({
+ "order_by" => "something"
+ })
+ end
+ end
+
+ context 'when downloading a release asset' do
+ it 'redirects to the right endpoint keeping the suffix_path' do
+ get api("/projects/#{project.id}/releases/permalink/latest/downloads/bin/example.exe", maintainer)
+
+ uri = URI(response.header["Location"])
+
+ expect(response).to have_gitlab_http_status(:redirect)
+ expect(uri.path).to eq("/api/v4/projects/#{project.id}/releases/#{release_b.tag}/downloads/bin/example.exe")
+ end
+
+ it 'returns error when there is path traversal in suffix path' do
+ get api("/projects/#{project.id}/releases/permalink/latest/downloads/bin/../../../../../../../password.txt", maintainer)
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+
+ expect(json_response['error']).to eq('suffix_path should be a valid file path')
+ end
+ end
+ end
+ end
+
describe 'POST /projects/:id/releases' do
let(:params) do
{
diff --git a/spec/requests/api/resource_access_tokens_spec.rb b/spec/requests/api/resource_access_tokens_spec.rb
index 369a8c1b0ab..d9a12e7e148 100644
--- a/spec/requests/api/resource_access_tokens_spec.rb
+++ b/spec/requests/api/resource_access_tokens_spec.rb
@@ -243,27 +243,65 @@ RSpec.describe API::ResourceAccessTokens do
end
context "when the user has valid permissions" do
- it "deletes the #{source_type} access token from the #{source_type}" do
- delete_token
+ context 'when user_destroy_with_limited_execution_time_worker is enabled' do
+ it "deletes the #{source_type} access token from the #{source_type}" do
+ delete_token
- expect(response).to have_gitlab_http_status(:no_content)
- expect(User.exists?(project_bot.id)).to be_falsy
- end
+ expect(response).to have_gitlab_http_status(:no_content)
+ expect(
+ Users::GhostUserMigration.where(user: project_bot,
+ initiator_user: user)
+ ).to be_exists
+ end
- context "when using #{source_type} access token to DELETE other #{source_type} access token" do
- let_it_be(:other_project_bot) { create(:user, :project_bot) }
- let_it_be(:other_token) { create(:personal_access_token, user: other_project_bot) }
- let_it_be(:token_id) { other_token.id }
+ context "when using #{source_type} access token to DELETE other #{source_type} access token" do
+ let_it_be(:other_project_bot) { create(:user, :project_bot) }
+ let_it_be(:other_token) { create(:personal_access_token, user: other_project_bot) }
+ let_it_be(:token_id) { other_token.id }
+
+ before do
+ resource.add_maintainer(other_project_bot)
+ end
+
+ it "deletes the #{source_type} access token from the #{source_type}" do
+ delete_token
+ expect(response).to have_gitlab_http_status(:no_content)
+ expect(
+ Users::GhostUserMigration.where(user: other_project_bot,
+ initiator_user: user)
+ ).to be_exists
+ end
+ end
+ end
+
+ context 'when user_destroy_with_limited_execution_time_worker is disabled' do
before do
- resource.add_maintainer(other_project_bot)
+ stub_feature_flags(user_destroy_with_limited_execution_time_worker: false)
end
it "deletes the #{source_type} access token from the #{source_type}" do
delete_token
expect(response).to have_gitlab_http_status(:no_content)
- expect(User.exists?(other_project_bot.id)).to be_falsy
+ expect(User.exists?(project_bot.id)).to be_falsy
+ end
+
+ context "when using #{source_type} access token to DELETE other #{source_type} access token" do
+ let_it_be(:other_project_bot) { create(:user, :project_bot) }
+ let_it_be(:other_token) { create(:personal_access_token, user: other_project_bot) }
+ let_it_be(:token_id) { other_token.id }
+
+ before do
+ resource.add_maintainer(other_project_bot)
+ end
+
+ it "deletes the #{source_type} access token from the #{source_type}" do
+ delete_token
+
+ expect(response).to have_gitlab_http_status(:no_content)
+ expect(User.exists?(other_project_bot.id)).to be_falsy
+ end
end
end
diff --git a/spec/requests/api/resource_state_events_spec.rb b/spec/requests/api/resource_state_events_spec.rb
index 46ca9874395..5f756bc6c63 100644
--- a/spec/requests/api/resource_state_events_spec.rb
+++ b/spec/requests/api/resource_state_events_spec.rb
@@ -6,87 +6,8 @@ RSpec.describe API::ResourceStateEvents do
let_it_be(:user) { create(:user) }
let_it_be(:project, reload: true) { create(:project, :public, namespace: user.namespace) }
- before_all do
- project.add_developer(user)
- end
-
- shared_examples 'resource_state_events API' do |parent_type, eventable_type, id_name|
- describe "GET /#{parent_type}/:id/#{eventable_type}/:noteable_id/resource_state_events" do
- let!(:event) { create_event }
-
- it "returns an array of resource state events" do
- url = "/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}/resource_state_events"
- get api(url, user)
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- expect(json_response.first['id']).to eq(event.id)
- expect(json_response.first['state']).to eq(event.state.to_s)
- end
-
- it "returns a 404 error when eventable id not found" do
- get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{non_existing_record_id}/resource_state_events", user)
-
- expect(response).to have_gitlab_http_status(:not_found)
- end
-
- it "returns 404 when not authorized" do
- parent.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
- private_user = create(:user)
-
- get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}/resource_state_events", private_user)
-
- expect(response).to have_gitlab_http_status(:not_found)
- end
- end
-
- describe "GET /#{parent_type}/:id/#{eventable_type}/:noteable_id/resource_state_events/:event_id" do
- let!(:event) { create_event }
-
- it "returns a resource state event by id" do
- get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}/resource_state_events/#{event.id}", user)
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response['id']).to eq(event.id)
- expect(json_response['state']).to eq(event.state.to_s)
- end
-
- it "returns 404 when not authorized" do
- parent.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
- private_user = create(:user)
-
- get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}/resource_state_events/#{event.id}", private_user)
-
- expect(response).to have_gitlab_http_status(:not_found)
- end
-
- it "returns a 404 error if resource state event not found" do
- get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}/resource_state_events/#{non_existing_record_id}", user)
-
- expect(response).to have_gitlab_http_status(:not_found)
- end
- end
-
- describe 'pagination' do
- # https://gitlab.com/gitlab-org/gitlab/-/issues/220192
- it 'returns the second page' do
- create_event
- event2 = create_event
-
- get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}/resource_state_events?page=2&per_page=1", user)
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to include_pagination_headers
- expect(response.headers['X-Total']).to eq '2'
- expect(json_response.count).to eq(1)
- expect(json_response.first['id']).to eq(event2.id)
- end
- end
-
- def create_event(state: :opened)
- create(:resource_state_event, eventable.class.name.underscore => eventable, state: state)
- end
+ before do
+ parent.add_developer(user)
end
context 'when eventable is an Issue' do
diff --git a/spec/requests/api/rpm_project_packages_spec.rb b/spec/requests/api/rpm_project_packages_spec.rb
new file mode 100644
index 00000000000..6a646c26fd2
--- /dev/null
+++ b/spec/requests/api/rpm_project_packages_spec.rb
@@ -0,0 +1,250 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe API::RpmProjectPackages do
+ include HttpBasicAuthHelpers
+ include WorkhorseHelpers
+
+ include_context 'workhorse headers'
+
+ using RSpec::Parameterized::TableSyntax
+
+ let_it_be_with_reload(:project) { create(:project, :public) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:personal_access_token) { create(:personal_access_token, user: user) }
+ 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(:job) { create(:ci_build, :running, user: user, project: project) }
+
+ let(:headers) { {} }
+ let(:package_name) { 'rpm-package.0-1.x86_64.rpm' }
+ let(:package_file_id) { 1 }
+
+ shared_examples 'rejects rpm packages access' do |status|
+ it_behaves_like 'returning response status', status
+
+ if status == :unauthorized
+ it 'has the correct response header' do
+ subject
+
+ expect(response.headers['WWW-Authenticate']).to eq 'Basic realm="GitLab Packages Registry"'
+ end
+ end
+ end
+
+ shared_examples 'process rpm packages upload/download' do |status|
+ it_behaves_like 'returning response status', status
+ end
+
+ shared_examples 'a deploy token for RPM requests' do
+ context 'with deploy token headers' do
+ before do
+ project.update_column(:visibility_level, Gitlab::VisibilityLevel::PRIVATE)
+ end
+
+ let(:headers) { basic_auth_header(deploy_token.username, deploy_token.token) }
+
+ context 'when token is valid' do
+ it_behaves_like 'returning response status', :not_found
+ end
+
+ context 'when token is invalid' do
+ let(:headers) { basic_auth_header(deploy_token.username, 'bar') }
+
+ it_behaves_like 'returning response status', :unauthorized
+ end
+ end
+ end
+
+ shared_examples 'a job token for RPM requests' do
+ context 'with job token headers' do
+ let(:headers) { basic_auth_header(::Gitlab::Auth::CI_JOB_USER, job.token) }
+
+ before do
+ project.update_column(:visibility_level, Gitlab::VisibilityLevel::PRIVATE)
+ project.add_developer(user)
+ end
+
+ context 'with valid token' do
+ it_behaves_like 'returning response status', :not_found
+ end
+
+ context 'with invalid token' do
+ let(:headers) { basic_auth_header(::Gitlab::Auth::CI_JOB_USER, 'bar') }
+
+ it_behaves_like 'returning response status', :unauthorized
+ end
+
+ context 'with invalid user' do
+ let(:headers) { basic_auth_header('foo', job.token) }
+
+ it_behaves_like 'returning response status', :unauthorized
+ end
+ end
+ end
+
+ shared_examples 'a user token for RPM requests' do
+ context 'with valid project' do
+ where(:visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do
+ 'PUBLIC' | :developer | true | true | 'process rpm packages upload/download' | :not_found
+ 'PUBLIC' | :guest | true | true | 'process rpm packages upload/download' | :forbidden
+ 'PUBLIC' | :developer | true | false | 'rejects rpm packages access' | :unauthorized
+ 'PUBLIC' | :guest | true | false | 'rejects rpm packages access' | :unauthorized
+ 'PUBLIC' | :developer | false | true | 'process rpm packages upload/download' | :not_found
+ 'PUBLIC' | :guest | false | true | 'process rpm packages upload/download' | :not_found
+ 'PUBLIC' | :developer | false | false | 'rejects rpm packages access' | :unauthorized
+ 'PUBLIC' | :guest | false | false | 'rejects rpm packages access' | :unauthorized
+ 'PUBLIC' | :anonymous | false | true | 'process rpm packages upload/download' | :unauthorized
+ 'PRIVATE' | :developer | true | true | 'process rpm packages upload/download' | :not_found
+ 'PRIVATE' | :guest | true | true | 'rejects rpm packages access' | :forbidden
+ 'PRIVATE' | :developer | true | false | 'rejects rpm packages access' | :unauthorized
+ 'PRIVATE' | :guest | true | false | 'rejects rpm packages access' | :unauthorized
+ 'PRIVATE' | :developer | false | true | 'rejects rpm packages access' | :not_found
+ 'PRIVATE' | :guest | false | true | 'rejects rpm packages access' | :not_found
+ 'PRIVATE' | :developer | false | false | 'rejects rpm packages access' | :unauthorized
+ 'PRIVATE' | :guest | false | false | 'rejects rpm packages access' | :unauthorized
+ 'PRIVATE' | :anonymous | false | true | 'rejects rpm packages access' | :unauthorized
+ end
+
+ with_them do
+ let(:token) { user_token ? personal_access_token.token : 'wrong' }
+ let(:headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) }
+
+ subject { get api(url), headers: headers }
+
+ before do
+ project.update_column(:visibility_level, Gitlab::VisibilityLevel.level_value(visibility_level))
+ project.send("add_#{user_role}", user) if member && user_role != :anonymous
+ end
+
+ it_behaves_like params[:shared_examples_name], params[:expected_status]
+ end
+ end
+ end
+
+ describe 'GET /api/v4/projects/:project_id/packages/rpm/repodata/:filename' do
+ let(:url) { "/projects/#{project.id}/packages/rpm/repodata/#{package_name}" }
+
+ subject { get api(url), headers: headers }
+
+ it_behaves_like 'a job token for RPM requests'
+ it_behaves_like 'a deploy token for RPM requests'
+ it_behaves_like 'a user token for RPM requests'
+ end
+
+ describe 'GET /api/v4/projects/:id/packages/rpm/:package_file_id/:filename' do
+ let(:url) { "/projects/#{project.id}/packages/rpm/#{package_file_id}/#{package_name}" }
+
+ subject { get api(url), headers: headers }
+
+ it_behaves_like 'a job token for RPM requests'
+ it_behaves_like 'a deploy token for RPM requests'
+ it_behaves_like 'a user token for RPM requests'
+ end
+
+ describe 'POST /api/v4/projects/:project_id/packages/rpm' do
+ let(:url) { "/projects/#{project.id}/packages/rpm" }
+ let(:file_upload) { fixture_file_upload('spec/fixtures/packages/rpm/hello-0.0.1-1.fc29.x86_64.rpm') }
+
+ subject { post api(url), params: { file: file_upload }, headers: headers }
+
+ context 'with user token' do
+ context 'with valid project' do
+ where(:visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do
+ 'PUBLIC' | :developer | true | true | 'process rpm packages upload/download' | :not_found
+ 'PUBLIC' | :guest | true | true | 'rejects rpm packages access' | :forbidden
+ 'PUBLIC' | :developer | true | false | 'rejects rpm packages access' | :unauthorized
+ 'PUBLIC' | :guest | true | false | 'rejects rpm packages access' | :unauthorized
+ 'PUBLIC' | :developer | false | true | 'rejects rpm packages access' | :not_found
+ 'PUBLIC' | :guest | false | true | 'rejects rpm packages access' | :not_found
+ 'PUBLIC' | :developer | false | false | 'rejects rpm packages access' | :unauthorized
+ 'PUBLIC' | :guest | false | false | 'rejects rpm packages access' | :unauthorized
+ 'PUBLIC' | :anonymous | false | true | 'rejects rpm packages access' | :unauthorized
+ 'PRIVATE' | :developer | true | true | 'process rpm packages upload/download' | :not_found
+ 'PRIVATE' | :guest | true | true | 'rejects rpm packages access' | :forbidden
+ 'PRIVATE' | :developer | true | false | 'rejects rpm packages access' | :unauthorized
+ 'PRIVATE' | :guest | true | false | 'rejects rpm packages access' | :unauthorized
+ 'PRIVATE' | :developer | false | true | 'rejects rpm packages access' | :not_found
+ 'PRIVATE' | :guest | false | true | 'rejects rpm packages access' | :not_found
+ 'PRIVATE' | :developer | false | false | 'rejects rpm packages access' | :unauthorized
+ 'PRIVATE' | :guest | false | false | 'rejects rpm packages access' | :unauthorized
+ 'PRIVATE' | :anonymous | false | true | 'rejects rpm packages access' | :unauthorized
+ end
+
+ with_them do
+ let(:token) { user_token ? personal_access_token.token : 'wrong' }
+ let(:headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) }
+
+ before do
+ project.update_column(:visibility_level, Gitlab::VisibilityLevel.level_value(visibility_level))
+ project.send("add_#{user_role}", user) if member && user_role != :anonymous
+ end
+
+ it_behaves_like params[:shared_examples_name], params[:expected_status]
+ end
+ end
+
+ context 'when user can upload file' do
+ before do
+ project.add_developer(user)
+ end
+
+ let(:headers) { basic_auth_header(user.username, personal_access_token.token).merge(workhorse_headers) }
+
+ context 'when file size too large' do
+ before do
+ allow_next_instance_of(UploadedFile) do |uploaded_file|
+ allow(uploaded_file).to receive(:size).and_return(project.actual_limits.rpm_max_file_size + 1)
+ end
+ end
+
+ it 'returns an error' do
+ upload_file(params: { file: file_upload }, request_headers: headers)
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(response.body).to match(/File is too large/)
+ end
+ end
+ end
+
+ def upload_file(params: {}, request_headers: headers)
+ url = "/projects/#{project.id}/packages/rpm"
+ workhorse_finalize(
+ api(url),
+ method: :post,
+ file_key: :file,
+ params: params,
+ headers: request_headers,
+ send_rewritten_field: true
+ )
+ end
+ end
+
+ it_behaves_like 'a deploy token for RPM requests'
+ it_behaves_like 'a job token for RPM requests'
+ end
+
+ describe 'POST /api/v4/projects/:project_id/packages/rpm/authorize' do
+ let(:url) { api("/projects/#{project.id}/packages/rpm/authorize") }
+
+ subject { post(url, headers: headers) }
+
+ it_behaves_like 'returning response status', :not_found
+
+ context 'when feature flag is disabled' do
+ before do
+ stub_feature_flags(rpm_packages: false)
+ end
+
+ it_behaves_like 'returning response status', :not_found
+ end
+
+ context 'when package feature is disabled' do
+ before do
+ stub_config(packages: { enabled: false })
+ end
+
+ it_behaves_like 'returning response status', :not_found
+ end
+ end
+end
diff --git a/spec/requests/api/search_spec.rb b/spec/requests/api/search_spec.rb
index 6034d26f1d2..05f38aff6ab 100644
--- a/spec/requests/api/search_spec.rb
+++ b/spec/requests/api/search_spec.rb
@@ -9,6 +9,7 @@ RSpec.describe API::Search do
let_it_be(:repo_project) { create(:project, :public, :repository, group: group) }
before do
+ allow(Gitlab::ApplicationRateLimiter).to receive(:threshold).and_return(0)
allow(Gitlab::ApplicationRateLimiter).to receive(:threshold).with(:search_rate_limit).and_return(1000)
allow(Gitlab::ApplicationRateLimiter).to receive(:threshold).with(:search_rate_limit_unauthenticated).and_return(1000)
end
@@ -351,6 +352,43 @@ RSpec.describe API::Search do
end
end
+ it 'increments the custom search sli apdex' do
+ expect(Gitlab::Metrics::GlobalSearchSlis).to receive(:record_apdex).with(
+ elapsed: a_kind_of(Numeric),
+ search_scope: 'issues',
+ search_type: 'basic',
+ search_level: 'global'
+ )
+
+ get api(endpoint, user), params: { scope: 'issues', search: 'john doe' }
+ end
+
+ it 'increments the custom search sli error rate with error false if no error occurred' do
+ expect(Gitlab::Metrics::GlobalSearchSlis).to receive(:record_error_rate).with(
+ error: false,
+ search_scope: 'issues',
+ search_type: 'basic',
+ search_level: 'global'
+ )
+
+ get api(endpoint, user), params: { scope: 'issues', search: 'john doe' }
+ end
+
+ it 'increments the custom search sli error rate with error true if an error occurred' do
+ allow_next_instance_of(SearchService) do |service|
+ allow(service).to receive(:search_results).and_raise(ActiveRecord::QueryCanceled)
+ end
+
+ expect(Gitlab::Metrics::GlobalSearchSlis).to receive(:record_error_rate).with(
+ error: true,
+ search_scope: 'issues',
+ search_type: 'basic',
+ search_level: 'global'
+ )
+
+ get api(endpoint, user), params: { scope: 'issues', search: 'john doe' }
+ end
+
it 'sets global search information for logging' do
expect(Gitlab::Instrumentation::GlobalSearchApi).to receive(:set_information).with(
type: 'basic',
@@ -618,7 +656,7 @@ RSpec.describe API::Search do
context 'when requesting basic search' do
it 'passes the parameter to search service' do
- expect(SearchService).to receive(:new).with(user, hash_including(basic_search: 'true'))
+ expect(SearchService).to receive(:new).with(user, hash_including(basic_search: 'true')).twice
get api(endpoint, user), params: { scope: 'issues', search: 'awesome', basic_search: 'true' }
end
diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb
index 6f0d5827a80..315c76c8ac3 100644
--- a/spec/requests/api/settings_spec.rb
+++ b/spec/requests/api/settings_spec.rb
@@ -56,6 +56,10 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting do
expect(json_response['project_runner_token_expiration_interval']).to be_nil
expect(json_response['max_export_size']).to eq(0)
expect(json_response['pipeline_limit_per_project_user_sha']).to eq(0)
+ expect(json_response['delete_inactive_projects']).to be(false)
+ expect(json_response['inactive_projects_delete_after_months']).to eq(2)
+ expect(json_response['inactive_projects_min_size_mb']).to eq(0)
+ expect(json_response['inactive_projects_send_warning_email_after_months']).to eq(1)
end
end
@@ -148,7 +152,11 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting do
user_deactivation_emails_enabled: false,
admin_mode: true,
suggest_pipeline_enabled: false,
- users_get_by_id_limit: 456
+ users_get_by_id_limit: 456,
+ delete_inactive_projects: true,
+ inactive_projects_delete_after_months: 24,
+ inactive_projects_min_size_mb: 10,
+ inactive_projects_send_warning_email_after_months: 12
}
expect(response).to have_gitlab_http_status(:ok)
@@ -205,6 +213,10 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting do
expect(json_response['user_deactivation_emails_enabled']).to be(false)
expect(json_response['suggest_pipeline_enabled']).to be(false)
expect(json_response['users_get_by_id_limit']).to eq(456)
+ expect(json_response['delete_inactive_projects']).to be(true)
+ expect(json_response['inactive_projects_delete_after_months']).to eq(24)
+ expect(json_response['inactive_projects_min_size_mb']).to eq(10)
+ expect(json_response['inactive_projects_send_warning_email_after_months']).to eq(12)
end
end
diff --git a/spec/requests/api/snippets_spec.rb b/spec/requests/api/snippets_spec.rb
index 0dd6e484e8d..031bcb612f4 100644
--- a/spec/requests/api/snippets_spec.rb
+++ b/spec/requests/api/snippets_spec.rb
@@ -340,6 +340,7 @@ RSpec.describe API::Snippets, factory_default: :keep do
allow_next_instance_of(Spam::AkismetService) do |instance|
allow(instance).to receive(:spam?).and_return(true)
end
+ stub_feature_flags(allow_possible_spam: false)
end
context 'when the snippet is private' do
@@ -405,6 +406,7 @@ RSpec.describe API::Snippets, factory_default: :keep do
allow_next_instance_of(Spam::AkismetService) do |instance|
allow(instance).to receive(:spam?).and_return(true)
end
+ stub_feature_flags(allow_possible_spam: false)
end
context 'when the snippet is private' do
diff --git a/spec/requests/api/suggestions_spec.rb b/spec/requests/api/suggestions_spec.rb
index 7f53d379af5..2393a268693 100644
--- a/spec/requests/api/suggestions_spec.rb
+++ b/spec/requests/api/suggestions_spec.rb
@@ -34,15 +34,14 @@ RSpec.describe API::Suggestions do
end
let(:diff_note2) do
- create(:diff_note_on_merge_request, noteable: merge_request,
- position: position2,
- project: project)
+ create(:diff_note_on_merge_request, noteable: merge_request, position: position2, project: project)
end
let(:suggestion) do
- create(:suggestion, note: diff_note,
- from_content: " raise RuntimeError, \"System commands must be given as an array of strings\"\n",
- to_content: " raise RuntimeError, 'Explosion'\n # explosion?")
+ create(:suggestion,
+ note: diff_note,
+ from_content: " raise RuntimeError, \"System commands must be given as an array of strings\"\n",
+ to_content: " raise RuntimeError, 'Explosion'\n # explosion?")
end
let(:unappliable_suggestion) do
@@ -119,8 +118,8 @@ RSpec.describe API::Suggestions do
describe "PUT /suggestions/batch_apply" do
let(:suggestion2) do
create(:suggestion, note: diff_note2,
- from_content: " \"PWD\" => path\n",
- to_content: " *** FOO ***\n")
+ from_content: " \"PWD\" => path\n",
+ to_content: " *** FOO ***\n")
end
let(:url) { "/suggestions/batch_apply" }
diff --git a/spec/requests/api/tags_spec.rb b/spec/requests/api/tags_spec.rb
index e81e9e0bf2f..b62fbaead6f 100644
--- a/spec/requests/api/tags_spec.rb
+++ b/spec/requests/api/tags_spec.rb
@@ -17,10 +17,6 @@ RSpec.describe API::Tags do
end
describe 'GET /projects/:id/repository/tags', :use_clean_rails_memory_store_caching do
- before do
- stub_feature_flags(tag_list_keyset_pagination: false)
- end
-
let(:route) { "/projects/#{project_id}/repository/tags" }
context 'sorting' do
@@ -59,6 +55,18 @@ RSpec.describe API::Tags do
expect(json_response.map { |tag| tag['name'] }).to eq(ordered_by_name)
end
+
+ it 'sorts by version in ascending order when requested' do
+ repository = project.repository
+ repository.add_tag(user, 'v1.2.0', repository.commit.id)
+ repository.add_tag(user, 'v1.10.0', repository.commit.id)
+
+ get api("#{route}?order_by=version&sort=asc", current_user)
+
+ ordered_by_version = VersionSorter.sort(project.repository.tags.map { |tag| tag.name })
+
+ expect(json_response.map { |tag| tag['name'] }).to eq(ordered_by_version)
+ end
end
context 'searching' do
@@ -154,50 +162,44 @@ RSpec.describe API::Tags do
end
end
- context 'with keyset pagination on', :aggregate_errors do
- before do
- stub_feature_flags(tag_list_keyset_pagination: true)
- end
-
- context 'with keyset pagination option' do
- let(:base_params) { { pagination: 'keyset' } }
+ context 'with keyset pagination option', :aggregate_errors do
+ let(:base_params) { { pagination: 'keyset' } }
- context 'with gitaly pagination params' do
- context 'with high limit' do
- let(:params) { base_params.merge(per_page: 100) }
+ context 'with gitaly pagination params' do
+ context 'with high limit' do
+ let(:params) { base_params.merge(per_page: 100) }
- it 'returns all repository tags' do
- get api(route, user), params: params
+ it 'returns all repository tags' do
+ get api(route, user), params: params
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to match_response_schema('public_api/v4/tags')
- expect(response.headers).not_to include('Link')
- tag_names = json_response.map { |x| x['name'] }
- expect(tag_names).to match_array(project.repository.tag_names)
- end
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to match_response_schema('public_api/v4/tags')
+ expect(response.headers).not_to include('Link')
+ tag_names = json_response.map { |x| x['name'] }
+ expect(tag_names).to match_array(project.repository.tag_names)
end
+ end
- context 'with low limit' do
- let(:params) { base_params.merge(per_page: 2) }
+ context 'with low limit' do
+ let(:params) { base_params.merge(per_page: 2) }
- it 'returns limited repository tags' do
- get api(route, user), params: params
+ it 'returns limited repository tags' do
+ get api(route, user), params: params
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to match_response_schema('public_api/v4/tags')
- expect(response.headers).to include('Link')
- tag_names = json_response.map { |x| x['name'] }
- expect(tag_names).to match_array(%w(v1.1.0 v1.1.1))
- end
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to match_response_schema('public_api/v4/tags')
+ expect(response.headers).to include('Link')
+ tag_names = json_response.map { |x| x['name'] }
+ expect(tag_names).to match_array(%w(v1.1.0 v1.1.1))
end
+ end
- context 'with missing page token' do
- let(:params) { base_params.merge(page_token: 'unknown') }
+ context 'with missing page token' do
+ let(:params) { base_params.merge(page_token: 'unknown') }
- it_behaves_like '422 response' do
- let(:request) { get api(route, user), params: params }
- let(:message) { 'Invalid page token: refs/tags/unknown' }
- end
+ it_behaves_like '422 response' do
+ let(:request) { get api(route, user), params: params }
+ let(:message) { 'Invalid page token: refs/tags/unknown' }
end
end
end
diff --git a/spec/requests/api/topics_spec.rb b/spec/requests/api/topics_spec.rb
index 72221e3fb6a..1ad6f876fab 100644
--- a/spec/requests/api/topics_spec.rb
+++ b/spec/requests/api/topics_spec.rb
@@ -317,4 +317,66 @@ RSpec.describe API::Topics do
end
end
end
+
+ describe 'POST /topics/merge', :aggregate_failures do
+ context 'as administrator' do
+ let_it_be(:api_url) { api('/topics/merge', admin) }
+
+ it 'merge topics' do
+ post api_url, params: { source_topic_id: topic_3.id, target_topic_id: topic_2.id }
+
+ expect(response).to have_gitlab_http_status(:created)
+ expect { topic_2.reload }.not_to raise_error
+ expect { topic_3.reload }.to raise_error(ActiveRecord::RecordNotFound)
+ expect(json_response['id']).to eq(topic_2.id)
+ expect(json_response['total_projects_count']).to eq(topic_2.total_projects_count)
+ end
+
+ it 'returns 404 for non existing source topic id' do
+ post api_url, params: { source_topic_id: non_existing_record_id, target_topic_id: topic_2.id }
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+
+ it 'returns 404 for non existing target topic id' do
+ post api_url, params: { source_topic_id: topic_3.id, target_topic_id: non_existing_record_id }
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+
+ it 'returns 400 for identical topic ids' do
+ post api_url, params: { source_topic_id: topic_2.id, target_topic_id: topic_2.id }
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['message']).to eql('The source topic and the target topic are identical.')
+ end
+
+ it 'returns 400 if merge failed' do
+ allow_next_found_instance_of(Projects::Topic) do |topic|
+ allow(topic).to receive(:destroy!).and_raise(ActiveRecord::RecordNotDestroyed)
+ end
+
+ post api_url, params: { source_topic_id: topic_3.id, target_topic_id: topic_2.id }
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['message']).to eql('Topics could not be merged!')
+ end
+ end
+
+ context 'as normal user' do
+ it 'returns 403 Forbidden' do
+ post api('/topics/merge', user), params: { source_topic_id: topic_3.id, target_topic_id: topic_2.id }
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+
+ context 'as anonymous' do
+ it 'returns 401 Unauthorized' do
+ post api('/topics/merge'), params: { source_topic_id: topic_3.id, target_topic_id: topic_2.id }
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+ end
+ end
end
diff --git a/spec/requests/api/unleash_spec.rb b/spec/requests/api/unleash_spec.rb
index 3ee895d9421..51c567309b7 100644
--- a/spec/requests/api/unleash_spec.rb
+++ b/spec/requests/api/unleash_spec.rb
@@ -218,8 +218,7 @@ RSpec.describe API::Unleash do
context 'with version 2 feature flags' do
it 'does not return a flag without any strategies' do
- create(:operations_feature_flag, project: project,
- name: 'feature1', active: true, version: 2)
+ create(:operations_feature_flag, project: project, name: 'feature1', active: true, version: 2)
get api(features_url), headers: { 'UNLEASH-INSTANCEID' => client.token, 'UNLEASH-APPNAME' => 'production' }
@@ -228,10 +227,8 @@ RSpec.describe API::Unleash do
end
it 'returns a flag with a default strategy' do
- feature_flag = create(:operations_feature_flag, project: project,
- name: 'feature1', active: true, version: 2)
- strategy = create(:operations_strategy, feature_flag: feature_flag,
- name: 'default', parameters: {})
+ feature_flag = create(:operations_feature_flag, project: project, name: 'feature1', active: true, version: 2)
+ strategy = create(:operations_strategy, feature_flag: feature_flag, name: 'default', parameters: {})
create(:operations_scope, strategy: strategy, environment_scope: 'production')
get api(features_url), headers: { 'UNLEASH-INSTANCEID' => client.token, 'UNLEASH-APPNAME' => 'production' }
@@ -248,10 +245,9 @@ RSpec.describe API::Unleash do
end
it 'returns a flag with a userWithId strategy' do
- feature_flag = create(:operations_feature_flag, project: project,
- name: 'feature1', active: true, version: 2)
- strategy = create(:operations_strategy, feature_flag: feature_flag,
- name: 'userWithId', parameters: { userIds: 'user123,user456' })
+ feature_flag = create(:operations_feature_flag, project: project, name: 'feature1', active: true, version: 2)
+ strategy = create(:operations_strategy,
+ feature_flag: feature_flag, name: 'userWithId', parameters: { userIds: 'user123,user456' })
create(:operations_scope, strategy: strategy, environment_scope: 'production')
get api(features_url), headers: { 'UNLEASH-INSTANCEID' => client.token, 'UNLEASH-APPNAME' => 'production' }
@@ -268,12 +264,13 @@ RSpec.describe API::Unleash do
end
it 'returns a flag with multiple strategies' do
- feature_flag = create(:operations_feature_flag, project: project,
- name: 'feature1', active: true, version: 2)
- strategy_a = create(:operations_strategy, feature_flag: feature_flag,
- name: 'userWithId', parameters: { userIds: 'user_a,user_b' })
- strategy_b = create(:operations_strategy, feature_flag: feature_flag,
- name: 'gradualRolloutUserId', parameters: { groupId: 'default', percentage: '45' })
+ feature_flag = create(:operations_feature_flag, project: project, name: 'feature1', active: true, version: 2)
+ strategy_a = create(:operations_strategy,
+ feature_flag: feature_flag, name: 'userWithId', parameters: { userIds: 'user_a,user_b' })
+ strategy_b = create(:operations_strategy,
+ feature_flag: feature_flag,
+ name: 'gradualRolloutUserId',
+ parameters: { groupId: 'default', percentage: '45' })
create(:operations_scope, strategy: strategy_a, environment_scope: 'production')
create(:operations_scope, strategy: strategy_b, environment_scope: 'production')
@@ -298,12 +295,12 @@ RSpec.describe API::Unleash do
end
it 'returns only flags matching the environment scope' do
- feature_flag_a = create(:operations_feature_flag, project: project,
- name: 'feature1', active: true, version: 2)
+ feature_flag_a = create(:operations_feature_flag,
+ project: project, name: 'feature1', active: true, version: 2)
strategy_a = create(:operations_strategy, feature_flag: feature_flag_a)
create(:operations_scope, strategy: strategy_a, environment_scope: 'production')
- feature_flag_b = create(:operations_feature_flag, project: project,
- name: 'feature2', active: true, version: 2)
+ feature_flag_b = create(:operations_feature_flag,
+ project: project, name: 'feature2', active: true, version: 2)
strategy_b = create(:operations_strategy, feature_flag: feature_flag_b)
create(:operations_scope, strategy: strategy_b, environment_scope: 'staging')
@@ -322,13 +319,11 @@ RSpec.describe API::Unleash do
end
it 'returns only strategies matching the environment scope' do
- feature_flag = create(:operations_feature_flag, project: project,
- name: 'feature1', active: true, version: 2)
- strategy_a = create(:operations_strategy, feature_flag: feature_flag,
- name: 'userWithId', parameters: { userIds: 'user2,user8,user4' })
+ feature_flag = create(:operations_feature_flag, project: project, name: 'feature1', active: true, version: 2)
+ strategy_a = create(:operations_strategy,
+ feature_flag: feature_flag, name: 'userWithId', parameters: { userIds: 'user2,user8,user4' })
create(:operations_scope, strategy: strategy_a, environment_scope: 'production')
- strategy_b = create(:operations_strategy, feature_flag: feature_flag,
- name: 'default', parameters: {})
+ strategy_b = create(:operations_strategy, feature_flag: feature_flag, name: 'default', parameters: {})
create(:operations_scope, strategy: strategy_b, environment_scope: 'staging')
get api(features_url), headers: { 'UNLEASH-INSTANCEID' => client.token, 'UNLEASH-APPNAME' => 'production' }
@@ -346,10 +341,12 @@ RSpec.describe API::Unleash do
it 'returns only flags for the given project' do
project_b = create(:project)
- feature_flag_a = create(:operations_feature_flag, project: project, name: 'feature_a', active: true, version: 2)
+ feature_flag_a = create(:operations_feature_flag,
+ project: project, name: 'feature_a', active: true, version: 2)
strategy_a = create(:operations_strategy, feature_flag: feature_flag_a)
create(:operations_scope, strategy: strategy_a, environment_scope: 'sandbox')
- feature_flag_b = create(:operations_feature_flag, project: project_b, name: 'feature_b', active: true, version: 2)
+ feature_flag_b = create(:operations_feature_flag,
+ project: project_b, name: 'feature_b', active: true, version: 2)
strategy_b = create(:operations_strategy, feature_flag: feature_flag_b)
create(:operations_scope, strategy: strategy_b, environment_scope: 'sandbox')
@@ -367,16 +364,16 @@ RSpec.describe API::Unleash do
end
it 'returns all strategies with a matching scope' do
- feature_flag = create(:operations_feature_flag, project: project,
- name: 'feature1', active: true, version: 2)
- strategy_a = create(:operations_strategy, feature_flag: feature_flag,
- name: 'userWithId', parameters: { userIds: 'user2,user8,user4' })
+ feature_flag = create(:operations_feature_flag, project: project, name: 'feature1', active: true, version: 2)
+ strategy_a = create(:operations_strategy,
+ feature_flag: feature_flag, name: 'userWithId', parameters: { userIds: 'user2,user8,user4' })
create(:operations_scope, strategy: strategy_a, environment_scope: '*')
- strategy_b = create(:operations_strategy, feature_flag: feature_flag,
- name: 'default', parameters: {})
+ strategy_b = create(:operations_strategy, feature_flag: feature_flag, name: 'default', parameters: {})
create(:operations_scope, strategy: strategy_b, environment_scope: 'review/*')
- strategy_c = create(:operations_strategy, feature_flag: feature_flag,
- name: 'gradualRolloutUserId', parameters: { groupId: 'default', percentage: '15' })
+ strategy_c = create(:operations_strategy,
+ feature_flag: feature_flag,
+ name: 'gradualRolloutUserId',
+ parameters: { groupId: 'default', percentage: '15' })
create(:operations_scope, strategy: strategy_c, environment_scope: 'review/patch-1')
get api(features_url), headers: { 'UNLEASH-INSTANCEID' => client.token, 'UNLEASH-APPNAME' => 'review/patch-1' }
@@ -395,10 +392,8 @@ RSpec.describe API::Unleash do
end
it 'returns a strategy with more than one matching scope' do
- feature_flag = create(:operations_feature_flag, project: project,
- name: 'feature1', active: true, version: 2)
- strategy = create(:operations_strategy, feature_flag: feature_flag,
- name: 'default', parameters: {})
+ feature_flag = create(:operations_feature_flag, project: project, name: 'feature1', active: true, version: 2)
+ strategy = create(:operations_strategy, feature_flag: feature_flag, name: 'default', parameters: {})
create(:operations_scope, strategy: strategy, environment_scope: 'production')
create(:operations_scope, strategy: strategy, environment_scope: '*')
@@ -416,10 +411,9 @@ RSpec.describe API::Unleash do
end
it 'returns a disabled flag with a matching scope' do
- feature_flag = create(:operations_feature_flag, project: project,
- name: 'myfeature', active: false, version: 2)
- strategy = create(:operations_strategy, feature_flag: feature_flag,
- name: 'default', parameters: {})
+ feature_flag = create(:operations_feature_flag,
+ project: project, name: 'myfeature', active: false, version: 2)
+ strategy = create(:operations_strategy, feature_flag: feature_flag, name: 'default', parameters: {})
create(:operations_scope, strategy: strategy, environment_scope: 'production')
get api(features_url), headers: { 'UNLEASH-INSTANCEID' => client.token, 'UNLEASH-APPNAME' => 'production' }
@@ -436,12 +430,12 @@ RSpec.describe API::Unleash do
end
it 'returns a userWithId strategy for a gitlabUserList strategy' do
- feature_flag = create(:operations_feature_flag, :new_version_flag, project: project,
- name: 'myfeature', active: true)
- user_list = create(:operations_feature_flag_user_list, project: project,
- name: 'My List', user_xids: 'user1,user2')
- strategy = create(:operations_strategy, feature_flag: feature_flag,
- name: 'gitlabUserList', parameters: {}, user_list: user_list)
+ feature_flag = create(:operations_feature_flag, :new_version_flag,
+ project: project, name: 'myfeature', active: true)
+ user_list = create(:operations_feature_flag_user_list,
+ project: project, name: 'My List', user_xids: 'user1,user2')
+ strategy = create(:operations_strategy,
+ feature_flag: feature_flag, name: 'gitlabUserList', parameters: {}, user_list: user_list)
create(:operations_scope, strategy: strategy, environment_scope: 'production')
get api(features_url), headers: { 'UNLEASH-INSTANCEID' => client.token, 'UNLEASH-APPNAME' => 'production' }
diff --git a/spec/requests/api/usage_data_queries_spec.rb b/spec/requests/api/usage_data_queries_spec.rb
index 69a8d865a59..6ce03954246 100644
--- a/spec/requests/api/usage_data_queries_spec.rb
+++ b/spec/requests/api/usage_data_queries_spec.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
+require 'rake_helper'
RSpec.describe API::UsageDataQueries do
include UsageDataHelpers
@@ -64,5 +65,36 @@ RSpec.describe API::UsageDataQueries do
expect(response).to have_gitlab_http_status(:forbidden)
end
end
+
+ context 'when querying sql metrics' do
+ let(:file) { Rails.root.join('tmp', 'test', 'sql_metrics_queries.json') }
+
+ before do
+ Rake.application.rake_require 'tasks/gitlab/usage_data'
+
+ run_rake_task('gitlab:usage_data:generate_sql_metrics_queries')
+ end
+
+ after do
+ FileUtils.rm_rf(file)
+ end
+
+ it 'matches the generated query' do
+ Timecop.freeze(2021, 1, 1) do
+ get api(endpoint, admin)
+ end
+
+ data = Gitlab::Json.parse(File.read(file))
+
+ expect(
+ json_response['counts_monthly'].except('aggregated_metrics')
+ ).to eq(data['counts_monthly'].except('aggregated_metrics'))
+
+ expect(json_response['counts']).to eq(data['counts'])
+ expect(json_response['active_user_count']).to eq(data['active_user_count'])
+ expect(json_response['usage_activity_by_stage']).to eq(data['usage_activity_by_stage'])
+ expect(json_response['usage_activity_by_stage_monthly']).to eq(data['usage_activity_by_stage_monthly'])
+ end
+ end
end
end
diff --git a/spec/requests/api/usage_data_spec.rb b/spec/requests/api/usage_data_spec.rb
index ea50c404d92..d532fb6c168 100644
--- a/spec/requests/api/usage_data_spec.rb
+++ b/spec/requests/api/usage_data_spec.rb
@@ -138,7 +138,9 @@ RSpec.describe API::UsageData do
context 'with correct params' do
it 'returns status ok' do
- expect(Gitlab::Redis::HLL).to receive(:add)
+ expect(Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:track).with(anything, known_event, anything)
+ # allow other events to also get triggered
+ allow(Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:track)
post api(endpoint, user), params: { event: known_event }
diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb
index 26238a87209..96e23337411 100644
--- a/spec/requests/api/users_spec.rb
+++ b/spec/requests/api/users_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe API::Users do
+ include WorkhorseHelpers
+
let_it_be(:admin) { create(:admin) }
let_it_be(:user, reload: true) { create(:user, username: 'user.withdot') }
let_it_be(:key) { create(:key, user: user) }
@@ -116,7 +118,7 @@ RSpec.describe API::Users do
end
it "returns a 403 if the target user is an admin" do
- expect(TwoFactor::DestroyService).to receive(:new).never
+ expect(TwoFactor::DestroyService).not_to receive(:new)
expect do
patch api("/users/#{admin_with_2fa.id}/disable_two_factor", admin)
@@ -127,7 +129,7 @@ RSpec.describe API::Users do
end
it "returns a 404 if the target user cannot be found" do
- expect(TwoFactor::DestroyService).to receive(:new).never
+ expect(TwoFactor::DestroyService).not_to receive(:new)
patch api("/users/#{non_existing_record_id}/disable_two_factor", admin)
@@ -1180,6 +1182,22 @@ RSpec.describe API::Users do
expect(new_user.user_preference.view_diffs_file_by_file?).to eq(true)
end
+ it "creates user with avatar" do
+ workhorse_form_with_file(
+ api('/users', admin),
+ method: :post,
+ file_key: :avatar,
+ params: attributes_for(:user, avatar: fixture_file_upload('spec/fixtures/banana_sample.gif', 'image/gif'))
+ )
+
+ expect(response).to have_gitlab_http_status(:created)
+
+ new_user = User.find_by(id: json_response['id'])
+
+ expect(new_user).not_to eq(nil)
+ expect(json_response['avatar_url']).to include(new_user.avatar_path)
+ end
+
it "does not create user with invalid email" do
post api('/users', admin),
params: {
@@ -1478,7 +1496,12 @@ RSpec.describe API::Users do
end
it 'updates user with avatar' do
- put api("/users/#{user.id}", admin), params: { avatar: fixture_file_upload('spec/fixtures/banana_sample.gif', 'image/gif') }
+ workhorse_form_with_file(
+ api("/users/#{user.id}", admin),
+ method: :put,
+ file_key: :avatar,
+ params: { avatar: fixture_file_upload('spec/fixtures/banana_sample.gif', 'image/gif') }
+ )
user.reload
@@ -2479,14 +2502,32 @@ RSpec.describe API::Users do
describe "DELETE /users/:id" do
let_it_be(:issue) { create(:issue, author: user) }
- it "deletes user", :sidekiq_inline do
- namespace_id = user.namespace.id
+ context 'user deletion' do
+ context 'when user_destroy_with_limited_execution_time_worker is enabled' do
+ it "deletes user", :sidekiq_inline do
+ perform_enqueued_jobs { delete api("/users/#{user.id}", admin) }
- perform_enqueued_jobs { delete api("/users/#{user.id}", admin) }
+ expect(response).to have_gitlab_http_status(:no_content)
+ expect(Users::GhostUserMigration.where(user: user,
+ initiator_user: admin)).to be_exists
+ end
+ end
- expect(response).to have_gitlab_http_status(:no_content)
- expect { User.find(user.id) }.to raise_error ActiveRecord::RecordNotFound
- expect { Namespace.find(namespace_id) }.to raise_error ActiveRecord::RecordNotFound
+ context 'when user_destroy_with_limited_execution_time_worker is disabled' do
+ before do
+ stub_feature_flags(user_destroy_with_limited_execution_time_worker: false)
+ end
+
+ it "deletes user", :sidekiq_inline do
+ namespace_id = user.namespace.id
+
+ perform_enqueued_jobs { delete api("/users/#{user.id}", admin) }
+
+ expect(response).to have_gitlab_http_status(:no_content)
+ expect { User.find(user.id) }.to raise_error ActiveRecord::RecordNotFound
+ expect { Namespace.find(namespace_id) }.to raise_error ActiveRecord::RecordNotFound
+ end
+ end
end
context "sole owner of a group" do
@@ -2550,22 +2591,55 @@ RSpec.describe API::Users do
expect(response).to have_gitlab_http_status(:not_found)
end
- context "hard delete disabled" do
- it "moves contributions to the ghost user", :sidekiq_might_not_need_inline do
- perform_enqueued_jobs { delete api("/users/#{user.id}", admin) }
+ context 'hard delete' do
+ context 'when user_destroy_with_limited_execution_time_worker is enabled' do
+ context "hard delete disabled" do
+ it "moves contributions to the ghost user", :sidekiq_might_not_need_inline do
+ perform_enqueued_jobs { delete api("/users/#{user.id}", admin) }
- expect(response).to have_gitlab_http_status(:no_content)
- expect(issue.reload).to be_persisted
- expect(issue.author.ghost?).to be_truthy
+ expect(response).to have_gitlab_http_status(:no_content)
+ expect(issue.reload).to be_persisted
+ expect(Users::GhostUserMigration.where(user: user,
+ initiator_user: admin,
+ hard_delete: false)).to be_exists
+ end
+ end
+
+ context "hard delete enabled" do
+ it "removes contributions", :sidekiq_might_not_need_inline do
+ perform_enqueued_jobs { delete api("/users/#{user.id}?hard_delete=true", admin) }
+
+ expect(response).to have_gitlab_http_status(:no_content)
+ expect(Users::GhostUserMigration.where(user: user,
+ initiator_user: admin,
+ hard_delete: true)).to be_exists
+ end
+ end
end
- end
- context "hard delete enabled" do
- it "removes contributions", :sidekiq_might_not_need_inline do
- perform_enqueued_jobs { delete api("/users/#{user.id}?hard_delete=true", admin) }
+ context 'when user_destroy_with_limited_execution_time_worker is disabled' do
+ before do
+ stub_feature_flags(user_destroy_with_limited_execution_time_worker: false)
+ end
- expect(response).to have_gitlab_http_status(:no_content)
- expect(Issue.exists?(issue.id)).to be_falsy
+ context "hard delete disabled" do
+ it "moves contributions to the ghost user", :sidekiq_might_not_need_inline do
+ perform_enqueued_jobs { delete api("/users/#{user.id}", admin) }
+
+ expect(response).to have_gitlab_http_status(:no_content)
+ expect(issue.reload).to be_persisted
+ expect(issue.author.ghost?).to be_truthy
+ end
+ end
+
+ context "hard delete enabled" do
+ it "removes contributions", :sidekiq_might_not_need_inline do
+ perform_enqueued_jobs { delete api("/users/#{user.id}?hard_delete=true", admin) }
+
+ expect(response).to have_gitlab_http_status(:no_content)
+ expect(Issue.exists?(issue.id)).to be_falsy
+ end
+ end
end
end
end
@@ -3238,7 +3312,7 @@ RSpec.describe API::Users do
let(:user) { create(:user, **activity) }
context 'with no recent activity' do
- let(:activity) { { last_activity_on: ::User::MINIMUM_INACTIVE_DAYS.next.days.ago } }
+ let(:activity) { { last_activity_on: Gitlab::CurrentSettings.deactivate_dormant_users_period.next.days.ago } }
it 'deactivates an active user' do
deactivate
@@ -3249,13 +3323,13 @@ RSpec.describe API::Users do
end
context 'with recent activity' do
- let(:activity) { { last_activity_on: ::User::MINIMUM_INACTIVE_DAYS.pred.days.ago } }
+ let(:activity) { { last_activity_on: Gitlab::CurrentSettings.deactivate_dormant_users_period.pred.days.ago } }
it 'does not deactivate an active user' do
deactivate
expect(response).to have_gitlab_http_status(:forbidden)
- expect(json_response['message']).to eq("403 Forbidden - The user you are trying to deactivate has been active in the past #{::User::MINIMUM_INACTIVE_DAYS} days and cannot be deactivated")
+ expect(json_response['message']).to eq("403 Forbidden - The user you are trying to deactivate has been active in the past #{Gitlab::CurrentSettings.deactivate_dormant_users_period} days and cannot be deactivated")
expect(user.reload.state).to eq('active')
end
end