summaryrefslogtreecommitdiff
path: root/spec/requests
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-12-17 11:59:07 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2020-12-17 11:59:07 +0000
commit8b573c94895dc0ac0e1d9d59cf3e8745e8b539ca (patch)
tree544930fb309b30317ae9797a9683768705d664c4 /spec/requests
parent4b1de649d0168371549608993deac953eb692019 (diff)
downloadgitlab-ce-8b573c94895dc0ac0e1d9d59cf3e8745e8b539ca.tar.gz
Add latest changes from gitlab-org/gitlab@13-7-stable-eev13.7.0-rc42
Diffstat (limited to 'spec/requests')
-rw-r--r--spec/requests/api/admin/instance_clusters_spec.rb13
-rw-r--r--spec/requests/api/api_guard/admin_mode_middleware_spec.rb2
-rw-r--r--spec/requests/api/broadcast_messages_spec.rb4
-rw-r--r--spec/requests/api/ci/runner/jobs_artifacts_spec.rb2
-rw-r--r--spec/requests/api/ci/runner/jobs_put_spec.rb17
-rw-r--r--spec/requests/api/ci/runner/jobs_trace_spec.rb2
-rw-r--r--spec/requests/api/composer_packages_spec.rb13
-rw-r--r--spec/requests/api/discussions_spec.rb31
-rw-r--r--spec/requests/api/feature_flags_spec.rb117
-rw-r--r--spec/requests/api/features_spec.rb178
-rw-r--r--spec/requests/api/go_proxy_spec.rb4
-rw-r--r--spec/requests/api/graphql/boards/board_lists_query_spec.rb16
-rw-r--r--spec/requests/api/graphql/ci/ci_cd_setting_spec.rb50
-rw-r--r--spec/requests/api/graphql/ci/config_spec.rb91
-rw-r--r--spec/requests/api/graphql/ci/job_artifacts_spec.rb52
-rw-r--r--spec/requests/api/graphql/ci/jobs_spec.rb119
-rw-r--r--spec/requests/api/graphql/group/container_repositories_spec.rb22
-rw-r--r--spec/requests/api/graphql/group/group_members_spec.rb80
-rw-r--r--spec/requests/api/graphql/group_query_spec.rb2
-rw-r--r--spec/requests/api/graphql/issue/issue_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/admin/sidekiq_queues/delete_jobs_spec.rb12
-rw-r--r--spec/requests/api/graphql/mutations/award_emojis/add_spec.rb4
-rw-r--r--spec/requests/api/graphql/mutations/award_emojis/toggle_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/container_repository/destroy_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/container_repository/destroy_tags_spec.rb120
-rw-r--r--spec/requests/api/graphql/mutations/environments/canary_ingress/update_spec.rb45
-rw-r--r--spec/requests/api/graphql/mutations/releases/delete_spec.rb132
-rw-r--r--spec/requests/api/graphql/mutations/releases/update_spec.rb255
-rw-r--r--spec/requests/api/graphql/mutations/snippets/create_spec.rb8
-rw-r--r--spec/requests/api/graphql/mutations/snippets/mark_as_spam_spec.rb10
-rw-r--r--spec/requests/api/graphql/namespace/projects_spec.rb44
-rw-r--r--spec/requests/api/graphql/project/container_repositories_spec.rb22
-rw-r--r--spec/requests/api/graphql/project/issue/design_collection/version_spec.rb3
-rw-r--r--spec/requests/api/graphql/project/issue_spec.rb6
-rw-r--r--spec/requests/api/graphql/project/issues_spec.rb28
-rw-r--r--spec/requests/api/graphql/project/merge_request/pipelines_spec.rb63
-rw-r--r--spec/requests/api/graphql/project/merge_requests_spec.rb47
-rw-r--r--spec/requests/api/graphql/project/pipeline_spec.rb49
-rw-r--r--spec/requests/api/graphql/project/project_members_spec.rb121
-rw-r--r--spec/requests/api/graphql/project/project_pipeline_statistics_spec.rb58
-rw-r--r--spec/requests/api/graphql/project/release_spec.rb10
-rw-r--r--spec/requests/api/graphql/project/terraform/states_spec.rb45
-rw-r--r--spec/requests/api/graphql/project_query_spec.rb35
-rw-r--r--spec/requests/api/graphql/user_query_spec.rb142
-rw-r--r--spec/requests/api/graphql/users_spec.rb14
-rw-r--r--spec/requests/api/graphql_spec.rb2
-rw-r--r--spec/requests/api/group_clusters_spec.rb12
-rw-r--r--spec/requests/api/group_import_spec.rb6
-rw-r--r--spec/requests/api/import_github_spec.rb4
-rw-r--r--spec/requests/api/internal/base_spec.rb17
-rw-r--r--spec/requests/api/internal/kubernetes_spec.rb16
-rw-r--r--spec/requests/api/internal/pages_spec.rb52
-rw-r--r--spec/requests/api/jobs_spec.rb53
-rw-r--r--spec/requests/api/merge_requests_spec.rb48
-rw-r--r--spec/requests/api/nuget_packages_spec.rb533
-rw-r--r--spec/requests/api/nuget_project_packages_spec.rb280
-rw-r--r--spec/requests/api/project_clusters_spec.rb12
-rw-r--r--spec/requests/api/project_import_spec.rb6
-rw-r--r--spec/requests/api/project_repository_storage_moves_spec.rb70
-rw-r--r--spec/requests/api/projects_spec.rb14
-rw-r--r--spec/requests/api/services_spec.rb4
-rw-r--r--spec/requests/api/settings_spec.rb25
-rw-r--r--spec/requests/api/usage_data_spec.rb81
-rw-r--r--spec/requests/api/users_spec.rb94
-rw-r--r--spec/requests/api/v3/github_spec.rb4
-rw-r--r--spec/requests/git_http_spec.rb20
-rw-r--r--spec/requests/ide_controller_spec.rb17
-rw-r--r--spec/requests/import/gitlab_groups_controller_spec.rb67
-rw-r--r--spec/requests/import/gitlab_projects_controller_spec.rb53
-rw-r--r--spec/requests/jwt_controller_spec.rb318
-rw-r--r--spec/requests/lfs_http_spec.rb1662
-rw-r--r--spec/requests/projects/cycle_analytics_events_spec.rb143
-rw-r--r--spec/requests/projects/metrics_dashboard_spec.rb4
-rw-r--r--spec/requests/rack_attack_global_spec.rb33
-rw-r--r--spec/requests/self_monitoring_project_spec.rb8
-rw-r--r--spec/requests/whats_new_controller_spec.rb34
76 files changed, 3615 insertions, 2171 deletions
diff --git a/spec/requests/api/admin/instance_clusters_spec.rb b/spec/requests/api/admin/instance_clusters_spec.rb
index 1052080aad4..ab3b6b718e1 100644
--- a/spec/requests/api/admin/instance_clusters_spec.rb
+++ b/spec/requests/api/admin/instance_clusters_spec.rb
@@ -90,6 +90,8 @@ RSpec.describe ::API::Admin::InstanceClusters do
expect(json_response['environment_scope']).to eq('*')
expect(json_response['cluster_type']).to eq('instance_type')
expect(json_response['domain']).to eq('example.com')
+ expect(json_response['enabled']).to be_truthy
+ expect(json_response['managed']).to be_truthy
end
it 'returns kubernetes platform information' do
@@ -163,6 +165,7 @@ RSpec.describe ::API::Admin::InstanceClusters do
name: 'test-instance-cluster',
domain: 'domain.example.com',
managed: false,
+ enabled: false,
namespace_per_environment: false,
platform_kubernetes_attributes: platform_kubernetes_attributes,
clusterable: clusterable
@@ -205,9 +208,9 @@ RSpec.describe ::API::Admin::InstanceClusters do
expect(cluster_result.name).to eq('test-instance-cluster')
expect(cluster_result.domain).to eq('domain.example.com')
expect(cluster_result.environment_scope).to eq('*')
- expect(cluster_result.enabled).to eq(true)
- expect(platform_kubernetes.authorization_type).to eq('rbac')
expect(cluster_result.managed).to be_falsy
+ expect(cluster_result.enabled).to be_falsy
+ expect(platform_kubernetes.authorization_type).to eq('rbac')
expect(cluster_result.namespace_per_environment).to eq(false)
expect(platform_kubernetes.api_url).to eq("https://example.com")
expect(platform_kubernetes.token).to eq('sample-token')
@@ -301,6 +304,8 @@ RSpec.describe ::API::Admin::InstanceClusters do
let(:update_params) do
{
domain: domain,
+ managed: false,
+ enabled: false,
platform_kubernetes_attributes: platform_kubernetes_attributes
}
end
@@ -326,6 +331,8 @@ RSpec.describe ::API::Admin::InstanceClusters do
it 'updates cluster attributes' do
expect(cluster.domain).to eq('new-domain.com')
+ expect(cluster.managed).to be_falsy
+ expect(cluster.enabled).to be_falsy
end
end
@@ -338,6 +345,8 @@ RSpec.describe ::API::Admin::InstanceClusters do
it 'does not update cluster attributes' do
expect(cluster.domain).to eq('old-domain.com')
+ expect(cluster.managed).to be_truthy
+ expect(cluster.enabled).to be_truthy
end
it 'returns validation errors' do
diff --git a/spec/requests/api/api_guard/admin_mode_middleware_spec.rb b/spec/requests/api/api_guard/admin_mode_middleware_spec.rb
index 4b477f829a7..63bcec4b52a 100644
--- a/spec/requests/api/api_guard/admin_mode_middleware_spec.rb
+++ b/spec/requests/api/api_guard/admin_mode_middleware_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::APIGuard::AdminModeMiddleware, :do_not_mock_admin_mode, :request_store do
+RSpec.describe API::APIGuard::AdminModeMiddleware, :request_store do
let(:user) { create(:admin) }
it 'is loaded' do
diff --git a/spec/requests/api/broadcast_messages_spec.rb b/spec/requests/api/broadcast_messages_spec.rb
index b5b6ce106e5..b023ec398a2 100644
--- a/spec/requests/api/broadcast_messages_spec.rb
+++ b/spec/requests/api/broadcast_messages_spec.rb
@@ -112,7 +112,7 @@ RSpec.describe API::BroadcastMessages do
expect(response).to have_gitlab_http_status(:bad_request)
end
- it 'accepts an active dismissable value ' do
+ it 'accepts an active dismissable value' do
attrs = { message: 'new message', dismissable: true }
post api('/broadcast_messages', admin), params: attrs
@@ -197,7 +197,7 @@ RSpec.describe API::BroadcastMessages do
expect(response).to have_gitlab_http_status(:bad_request)
end
- it 'accepts a new dismissable value ' do
+ it 'accepts a new dismissable value' do
attrs = { message: 'new message', dismissable: true }
put api("/broadcast_messages/#{message.id}", admin), params: attrs
diff --git a/spec/requests/api/ci/runner/jobs_artifacts_spec.rb b/spec/requests/api/ci/runner/jobs_artifacts_spec.rb
index 71be0c30f5a..4d8da50f8f0 100644
--- a/spec/requests/api/ci/runner/jobs_artifacts_spec.rb
+++ b/spec/requests/api/ci/runner/jobs_artifacts_spec.rb
@@ -242,7 +242,7 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
}
expect { authorize_artifacts_with_token_in_headers(artifact_type: :lsif) }
- .to change { Gitlab::UsageDataCounters::HLLRedisCounter.unique_events(tracking_params) }
+ .to change { Gitlab::UsageDataCounters::HLLRedisCounter.unique_events(**tracking_params) }
.by(1)
end
end
diff --git a/spec/requests/api/ci/runner/jobs_put_spec.rb b/spec/requests/api/ci/runner/jobs_put_spec.rb
index cbefaa2c321..e9d793d5a22 100644
--- a/spec/requests/api/ci/runner/jobs_put_spec.rb
+++ b/spec/requests/api/ci/runner/jobs_put_spec.rb
@@ -61,6 +61,23 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
expect(response.header).not_to have_key('X-GitLab-Trace-Update-Interval')
end
+ context 'when runner sends an unrecognized field in a payload' do
+ ##
+ # This test case is here to ensure that the API used to communicate
+ # runner with GitLab can evolve.
+ #
+ # In case of adding new features on the Runner side we do not want
+ # GitLab-side to reject requests containing unrecognizable fields in
+ # a payload, because runners can be updated before a new version of
+ # GitLab is installed.
+ #
+ it 'ignores unrecognized fields' do
+ update_job(state: 'success', 'unknown': 'something')
+
+ expect(job.reload).to be_success
+ end
+ end
+
context 'when failure_reason is script_failure' do
before do
update_job(state: 'failed', failure_reason: 'script_failure')
diff --git a/spec/requests/api/ci/runner/jobs_trace_spec.rb b/spec/requests/api/ci/runner/jobs_trace_spec.rb
index 1980c1a9f51..5b7a33d23d8 100644
--- a/spec/requests/api/ci/runner/jobs_trace_spec.rb
+++ b/spec/requests/api/ci/runner/jobs_trace_spec.rb
@@ -135,7 +135,7 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
patch_the_trace
end
- it 'returns Forbidden ' do
+ it 'returns Forbidden' do
expect(response).to have_gitlab_http_status(:forbidden)
end
end
diff --git a/spec/requests/api/composer_packages_spec.rb b/spec/requests/api/composer_packages_spec.rb
index ef4682466d5..06d4a2c6017 100644
--- a/spec/requests/api/composer_packages_spec.rb
+++ b/spec/requests/api/composer_packages_spec.rb
@@ -167,7 +167,8 @@ RSpec.describe API::ComposerPackages do
describe 'GET /api/v4/group/:id/-/packages/composer/*package_name.json' do
let(:package_name) { 'foobar' }
- let(:url) { "/group/#{group.id}/-/packages/composer/#{package_name}.json" }
+ let(:sha) { '$1234' }
+ let(:url) { "/group/#{group.id}/-/packages/composer/#{package_name}#{sha}.json" }
subject { get api(url), headers: headers }
@@ -206,6 +207,16 @@ RSpec.describe API::ComposerPackages do
it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member]
end
end
+
+ context 'without a sha' do
+ let(:sha) { '' }
+
+ include_context 'Composer api group access', 'PRIVATE', :developer, true do
+ include_context 'Composer user type', :developer, true do
+ it_behaves_like 'process Composer api request', :developer, :not_found, true
+ end
+ end
+ end
end
it_behaves_like 'rejects Composer access with unknown group id'
diff --git a/spec/requests/api/discussions_spec.rb b/spec/requests/api/discussions_spec.rb
index 720ea429c2c..bdfc1589c9e 100644
--- a/spec/requests/api/discussions_spec.rb
+++ b/spec/requests/api/discussions_spec.rb
@@ -61,6 +61,37 @@ RSpec.describe API::Discussions do
expect(response).to have_gitlab_http_status(:bad_request)
end
end
+
+ context "when a commit parameter is given" do
+ it "creates the discussion on that commit within the merge request" do
+ # SHAs of "feature" and its parent in spec/support/gitlab-git-test.git
+ mr_commit = '0b4bc9a49b562e85de7cc9e834518ea6828729b9'
+ parent_commit = 'ae73cb07c9eeaf35924a10f713b364d32b2dd34f'
+ file = "files/ruby/feature.rb"
+ line_range = {
+ "start_line_code" => Gitlab::Git.diff_line_code(file, 1, 1),
+ "end_line_code" => Gitlab::Git.diff_line_code(file, 1, 1),
+ "start_line_type" => "text",
+ "end_line_type" => "text"
+ }
+ position = build(
+ :text_diff_position,
+ :added,
+ file: file,
+ new_line: 1,
+ line_range: line_range,
+ base_sha: parent_commit,
+ head_sha: mr_commit,
+ start_sha: parent_commit
+ )
+
+ post api("/projects/#{project.id}/merge_requests/#{noteable['iid']}/discussions", user),
+ params: { body: 'MR discussion on commit', position: position.to_h, commit_id: mr_commit }
+
+ expect(response).to have_gitlab_http_status(:created)
+ expect(json_response['notes'].first['commit_id']).to eq(mr_commit)
+ end
+ end
end
context 'when noteable is a Commit' do
diff --git a/spec/requests/api/feature_flags_spec.rb b/spec/requests/api/feature_flags_spec.rb
index 90d4a7b8b21..dd12648f4dd 100644
--- a/spec/requests/api/feature_flags_spec.rb
+++ b/spec/requests/api/feature_flags_spec.rb
@@ -65,26 +65,6 @@ RSpec.describe API::FeatureFlags do
expect(json_response.map { |f| f['version'] }).to eq(%w[legacy_flag legacy_flag])
end
- it 'does not return the legacy flag version when the feature flag is disabled' do
- stub_feature_flags(feature_flags_new_version: false)
-
- subject
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to match_response_schema('public_api/v4/feature_flags')
- expect(json_response.select { |f| f.key?('version') }).to eq([])
- end
-
- it 'does not return strategies if the new flag is disabled' do
- stub_feature_flags(feature_flags_new_version: false)
-
- subject
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to match_response_schema('public_api/v4/feature_flags')
- expect(json_response.select { |f| f.key?('strategies') }).to eq([])
- end
-
it 'does not have N+1 problem' do
control_count = ActiveRecord::QueryRecorder.new { subject }
@@ -134,16 +114,6 @@ RSpec.describe API::FeatureFlags do
}]
}])
end
-
- it 'does not return a version 2 flag when the feature flag is disabled' do
- stub_feature_flags(feature_flags_new_version: false)
-
- subject
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to match_response_schema('public_api/v4/feature_flags')
- expect(json_response).to eq([])
- end
end
context 'with version 1 and 2 feature flags' do
@@ -159,20 +129,6 @@ RSpec.describe API::FeatureFlags do
expect(response).to match_response_schema('public_api/v4/feature_flags')
expect(json_response.map { |f| f['name'] }).to eq(%w[legacy_flag new_version_flag])
end
-
- it 'returns only version 1 flags when the feature flag is disabled' do
- stub_feature_flags(feature_flags_new_version: false)
- create(:operations_feature_flag, project: project, name: 'legacy_flag')
- feature_flag = create(:operations_feature_flag, :new_version_flag, project: project, name: 'new_version_flag')
- strategy = create(:operations_strategy, feature_flag: feature_flag, name: 'default', parameters: {})
- create(:operations_scope, strategy: strategy, environment_scope: 'production')
-
- subject
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to match_response_schema('public_api/v4/feature_flags')
- expect(json_response.map { |f| f['name'] }).to eq(['legacy_flag'])
- end
end
end
@@ -224,18 +180,6 @@ RSpec.describe API::FeatureFlags do
}]
})
end
-
- it 'returns a 404 when the feature is disabled' do
- stub_feature_flags(feature_flags_new_version: false)
- feature_flag = create(:operations_feature_flag, :new_version_flag, project: project, name: 'feature1')
- strategy = create(:operations_strategy, feature_flag: feature_flag, name: 'default', parameters: {})
- create(:operations_scope, strategy: strategy, environment_scope: 'production')
-
- get api("/projects/#{project.id}/feature_flags/feature1", user)
-
- expect(response).to have_gitlab_http_status(:not_found)
- expect(json_response).to eq({ 'message' => '404 Not found' })
- end
end
end
@@ -290,16 +234,6 @@ RSpec.describe API::FeatureFlags do
expect(json_response['version']).to eq('legacy_flag')
end
- it 'does not return version when new version flags are disabled' do
- stub_feature_flags(feature_flags_new_version: false)
-
- subject
-
- expect(response).to have_gitlab_http_status(:created)
- expect(response).to match_response_schema('public_api/v4/feature_flag')
- expect(json_response.key?('version')).to eq(false)
- end
-
context 'with active set to false in the params for a legacy flag' do
let(:params) do
{
@@ -505,20 +439,6 @@ RSpec.describe API::FeatureFlags do
environment_scope: 'staging'
}])
end
-
- it 'returns a 422 when the feature flag is disabled' do
- stub_feature_flags(feature_flags_new_version: false)
- params = {
- name: 'new-feature',
- version: 'new_version_flag'
- }
-
- post api("/projects/#{project.id}/feature_flags", user), params: params
-
- expect(response).to have_gitlab_http_status(:unprocessable_entity)
- expect(json_response).to eq({ 'message' => 'Version 2 flags are not enabled for this project' })
- expect(project.operations_feature_flags.count).to eq(0)
- end
end
context 'when given invalid parameters' do
@@ -744,16 +664,6 @@ RSpec.describe API::FeatureFlags do
name: 'feature1', description: 'old description')
end
- it 'returns a 404 if the feature is disabled' do
- stub_feature_flags(feature_flags_new_version: false)
- params = { description: 'new description' }
-
- put api("/projects/#{project.id}/feature_flags/feature1", user), params: params
-
- expect(response).to have_gitlab_http_status(:not_found)
- expect(feature_flag.reload.description).to eq('old description')
- end
-
it 'returns a 422' do
params = { description: 'new description' }
@@ -771,16 +681,6 @@ RSpec.describe API::FeatureFlags do
name: 'feature1', description: 'old description')
end
- it 'returns a 404 if the feature is disabled' do
- stub_feature_flags(feature_flags_new_version: false)
- params = { description: 'new description' }
-
- put api("/projects/#{project.id}/feature_flags/feature1", user), params: params
-
- expect(response).to have_gitlab_http_status(:not_found)
- expect(feature_flag.reload.description).to eq('old description')
- end
-
it 'returns a 404 if the feature flag does not exist' do
params = { description: 'new description' }
@@ -1100,15 +1000,6 @@ RSpec.describe API::FeatureFlags do
expect(json_response['version']).to eq('legacy_flag')
end
- it 'does not return version when new version flags are disabled' do
- stub_feature_flags(feature_flags_new_version: false)
-
- subject
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response.key?('version')).to eq(false)
- end
-
context 'with a version 2 feature flag' do
let!(:feature_flag) { create(:operations_feature_flag, :new_version_flag, project: project) }
@@ -1117,14 +1008,6 @@ RSpec.describe API::FeatureFlags do
expect(response).to have_gitlab_http_status(:ok)
end
-
- it 'returns a 404 if the feature is disabled' do
- stub_feature_flags(feature_flags_new_version: false)
-
- expect { subject }.not_to change { Operations::FeatureFlag.count }
-
- expect(response).to have_gitlab_http_status(:not_found)
- end
end
end
end
diff --git a/spec/requests/api/features_spec.rb b/spec/requests/api/features_spec.rb
index 3f443b4f92b..0e163ec2154 100644
--- a/spec/requests/api/features_spec.rb
+++ b/spec/requests/api/features_spec.rb
@@ -6,6 +6,18 @@ RSpec.describe API::Features, stub_feature_flags: false do
let_it_be(:user) { create(:user) }
let_it_be(:admin) { create(:admin) }
+ # Find any `development` feature flag name
+ let(:known_feature_flag) do
+ Feature::Definition.definitions
+ .values.find(&:development?)
+ end
+
+ let(:known_feature_flag_definition_hash) do
+ a_hash_including(
+ 'type' => 'development'
+ )
+ end
+
before do
Feature.reset
Flipper.unregister_groups
@@ -22,12 +34,14 @@ RSpec.describe API::Features, stub_feature_flags: false do
{
'name' => 'feature_1',
'state' => 'on',
- 'gates' => [{ 'key' => 'boolean', 'value' => true }]
+ 'gates' => [{ 'key' => 'boolean', 'value' => true }],
+ 'definition' => nil
},
{
'name' => 'feature_2',
'state' => 'off',
- 'gates' => [{ 'key' => 'boolean', 'value' => false }]
+ 'gates' => [{ 'key' => 'boolean', 'value' => false }],
+ 'definition' => nil
},
{
'name' => 'feature_3',
@@ -35,7 +49,14 @@ RSpec.describe API::Features, stub_feature_flags: false do
'gates' => [
{ 'key' => 'boolean', 'value' => false },
{ 'key' => 'groups', 'value' => ['perf_team'] }
- ]
+ ],
+ 'definition' => nil
+ },
+ {
+ 'name' => known_feature_flag.name,
+ 'state' => 'on',
+ 'gates' => [{ 'key' => 'boolean', 'value' => true }],
+ 'definition' => known_feature_flag_definition_hash
}
]
end
@@ -44,6 +65,7 @@ RSpec.describe API::Features, stub_feature_flags: false do
Feature.enable('feature_1')
Feature.disable('feature_2')
Feature.enable('feature_3', Feature.group(:perf_team))
+ Feature.enable(known_feature_flag.name)
end
it 'returns a 401 for anonymous users' do
@@ -67,7 +89,7 @@ RSpec.describe API::Features, stub_feature_flags: false do
end
describe 'POST /feature' do
- let(:feature_name) { 'my_feature' }
+ let(:feature_name) { known_feature_flag.name }
context 'when the feature does not exist' do
it 'returns a 401 for anonymous users' do
@@ -87,43 +109,55 @@ RSpec.describe API::Features, stub_feature_flags: false do
post api("/features/#{feature_name}", admin), params: { value: 'true' }
expect(response).to have_gitlab_http_status(:created)
- expect(json_response).to eq(
- 'name' => 'my_feature',
+ expect(json_response).to match(
+ 'name' => feature_name,
'state' => 'on',
- 'gates' => [{ 'key' => 'boolean', 'value' => true }])
+ 'gates' => [{ 'key' => 'boolean', 'value' => true }],
+ 'definition' => known_feature_flag_definition_hash
+ )
+ end
+
+ it 'logs the event' do
+ expect(Feature.logger).to receive(:info).once
+
+ post api("/features/#{feature_name}", admin), params: { value: 'true' }
end
it 'creates an enabled feature for the given Flipper group when passed feature_group=perf_team' do
post api("/features/#{feature_name}", admin), params: { value: 'true', feature_group: 'perf_team' }
expect(response).to have_gitlab_http_status(:created)
- expect(json_response).to eq(
- 'name' => 'my_feature',
+ expect(json_response).to match(
+ 'name' => feature_name,
'state' => 'conditional',
'gates' => [
{ 'key' => 'boolean', 'value' => false },
{ 'key' => 'groups', 'value' => ['perf_team'] }
- ])
+ ],
+ 'definition' => known_feature_flag_definition_hash
+ )
end
it 'creates an enabled feature for the given user when passed user=username' do
post api("/features/#{feature_name}", admin), params: { value: 'true', user: user.username }
expect(response).to have_gitlab_http_status(:created)
- expect(json_response).to eq(
- 'name' => 'my_feature',
+ expect(json_response).to match(
+ 'name' => feature_name,
'state' => 'conditional',
'gates' => [
{ 'key' => 'boolean', 'value' => false },
{ 'key' => 'actors', 'value' => ["User:#{user.id}"] }
- ])
+ ],
+ 'definition' => known_feature_flag_definition_hash
+ )
end
it 'creates an enabled feature for the given user and feature group when passed user=username and feature_group=perf_team' do
post api("/features/#{feature_name}", admin), params: { value: 'true', user: user.username, feature_group: 'perf_team' }
expect(response).to have_gitlab_http_status(:created)
- expect(json_response['name']).to eq('my_feature')
+ expect(json_response['name']).to eq(feature_name)
expect(json_response['state']).to eq('conditional')
expect(json_response['gates']).to contain_exactly(
{ 'key' => 'boolean', 'value' => false },
@@ -141,13 +175,15 @@ RSpec.describe API::Features, stub_feature_flags: false do
post api("/features/#{feature_name}", admin), params: { value: 'true', project: project.full_path }
expect(response).to have_gitlab_http_status(:created)
- expect(json_response).to eq(
- 'name' => 'my_feature',
+ expect(json_response).to match(
+ 'name' => feature_name,
'state' => 'conditional',
'gates' => [
{ 'key' => 'boolean', 'value' => false },
{ 'key' => 'actors', 'value' => ["Project:#{project.id}"] }
- ])
+ ],
+ 'definition' => known_feature_flag_definition_hash
+ )
end
end
@@ -156,12 +192,13 @@ RSpec.describe API::Features, stub_feature_flags: false do
post api("/features/#{feature_name}", admin), params: { value: 'true', project: 'mep/to/the/mep/mep' }
expect(response).to have_gitlab_http_status(:created)
- expect(json_response).to eq(
- "name" => "my_feature",
+ expect(json_response).to match(
+ "name" => feature_name,
"state" => "off",
"gates" => [
{ "key" => "boolean", "value" => false }
- ]
+ ],
+ 'definition' => known_feature_flag_definition_hash
)
end
end
@@ -175,13 +212,15 @@ RSpec.describe API::Features, stub_feature_flags: false do
post api("/features/#{feature_name}", admin), params: { value: 'true', group: group.full_path }
expect(response).to have_gitlab_http_status(:created)
- expect(json_response).to eq(
- 'name' => 'my_feature',
+ expect(json_response).to match(
+ 'name' => feature_name,
'state' => 'conditional',
'gates' => [
{ 'key' => 'boolean', 'value' => false },
{ 'key' => 'actors', 'value' => ["Group:#{group.id}"] }
- ])
+ ],
+ 'definition' => known_feature_flag_definition_hash
+ )
end
end
@@ -190,12 +229,13 @@ RSpec.describe API::Features, stub_feature_flags: false do
post api("/features/#{feature_name}", admin), params: { value: 'true', group: 'not/a/group' }
expect(response).to have_gitlab_http_status(:created)
- expect(json_response).to eq(
- "name" => "my_feature",
+ expect(json_response).to match(
+ "name" => feature_name,
"state" => "off",
"gates" => [
{ "key" => "boolean", "value" => false }
- ]
+ ],
+ 'definition' => known_feature_flag_definition_hash
)
end
end
@@ -205,26 +245,30 @@ RSpec.describe API::Features, stub_feature_flags: false do
post api("/features/#{feature_name}", admin), params: { value: '50' }
expect(response).to have_gitlab_http_status(:created)
- expect(json_response).to eq(
- 'name' => 'my_feature',
+ expect(json_response).to match(
+ 'name' => feature_name,
'state' => 'conditional',
'gates' => [
{ 'key' => 'boolean', 'value' => false },
{ 'key' => 'percentage_of_time', 'value' => 50 }
- ])
+ ],
+ 'definition' => known_feature_flag_definition_hash
+ )
end
it 'creates a feature with the given percentage of actors if passed an integer' do
post api("/features/#{feature_name}", admin), params: { value: '50', key: 'percentage_of_actors' }
expect(response).to have_gitlab_http_status(:created)
- expect(json_response).to eq(
- 'name' => 'my_feature',
+ expect(json_response).to match(
+ 'name' => feature_name,
'state' => 'conditional',
'gates' => [
{ 'key' => 'boolean', 'value' => false },
{ 'key' => 'percentage_of_actors', 'value' => 50 }
- ])
+ ],
+ 'definition' => known_feature_flag_definition_hash
+ )
end
end
@@ -238,36 +282,42 @@ RSpec.describe API::Features, stub_feature_flags: false do
post api("/features/#{feature_name}", admin), params: { value: 'true' }
expect(response).to have_gitlab_http_status(:created)
- expect(json_response).to eq(
- 'name' => 'my_feature',
+ expect(json_response).to match(
+ 'name' => feature_name,
'state' => 'on',
- 'gates' => [{ 'key' => 'boolean', 'value' => true }])
+ 'gates' => [{ 'key' => 'boolean', 'value' => true }],
+ 'definition' => known_feature_flag_definition_hash
+ )
end
it 'enables the feature for the given Flipper group when passed feature_group=perf_team' do
post api("/features/#{feature_name}", admin), params: { value: 'true', feature_group: 'perf_team' }
expect(response).to have_gitlab_http_status(:created)
- expect(json_response).to eq(
- 'name' => 'my_feature',
+ expect(json_response).to match(
+ 'name' => feature_name,
'state' => 'conditional',
'gates' => [
{ 'key' => 'boolean', 'value' => false },
{ 'key' => 'groups', 'value' => ['perf_team'] }
- ])
+ ],
+ 'definition' => known_feature_flag_definition_hash
+ )
end
it 'enables the feature for the given user when passed user=username' do
post api("/features/#{feature_name}", admin), params: { value: 'true', user: user.username }
expect(response).to have_gitlab_http_status(:created)
- expect(json_response).to eq(
- 'name' => 'my_feature',
+ expect(json_response).to match(
+ 'name' => feature_name,
'state' => 'conditional',
'gates' => [
{ 'key' => 'boolean', 'value' => false },
{ 'key' => 'actors', 'value' => ["User:#{user.id}"] }
- ])
+ ],
+ 'definition' => known_feature_flag_definition_hash
+ )
end
end
@@ -279,10 +329,12 @@ RSpec.describe API::Features, stub_feature_flags: false do
post api("/features/#{feature_name}", admin), params: { value: 'false' }
expect(response).to have_gitlab_http_status(:created)
- expect(json_response).to eq(
- 'name' => 'my_feature',
+ expect(json_response).to match(
+ 'name' => feature_name,
'state' => 'off',
- 'gates' => [{ 'key' => 'boolean', 'value' => false }])
+ 'gates' => [{ 'key' => 'boolean', 'value' => false }],
+ 'definition' => known_feature_flag_definition_hash
+ )
end
it 'disables the feature for the given Flipper group when passed feature_group=perf_team' do
@@ -292,10 +344,12 @@ RSpec.describe API::Features, stub_feature_flags: false do
post api("/features/#{feature_name}", admin), params: { value: 'false', feature_group: 'perf_team' }
expect(response).to have_gitlab_http_status(:created)
- expect(json_response).to eq(
- 'name' => 'my_feature',
+ expect(json_response).to match(
+ 'name' => feature_name,
'state' => 'off',
- 'gates' => [{ 'key' => 'boolean', 'value' => false }])
+ 'gates' => [{ 'key' => 'boolean', 'value' => false }],
+ 'definition' => known_feature_flag_definition_hash
+ )
end
it 'disables the feature for the given user when passed user=username' do
@@ -305,10 +359,12 @@ RSpec.describe API::Features, stub_feature_flags: false do
post api("/features/#{feature_name}", admin), params: { value: 'false', user: user.username }
expect(response).to have_gitlab_http_status(:created)
- expect(json_response).to eq(
- 'name' => 'my_feature',
+ expect(json_response).to match(
+ 'name' => feature_name,
'state' => 'off',
- 'gates' => [{ 'key' => 'boolean', 'value' => false }])
+ 'gates' => [{ 'key' => 'boolean', 'value' => false }],
+ 'definition' => known_feature_flag_definition_hash
+ )
end
end
@@ -321,13 +377,15 @@ RSpec.describe API::Features, stub_feature_flags: false do
post api("/features/#{feature_name}", admin), params: { value: '30' }
expect(response).to have_gitlab_http_status(:created)
- expect(json_response).to eq(
- 'name' => 'my_feature',
+ expect(json_response).to match(
+ 'name' => feature_name,
'state' => 'conditional',
'gates' => [
{ 'key' => 'boolean', 'value' => false },
{ 'key' => 'percentage_of_time', 'value' => 30 }
- ])
+ ],
+ 'definition' => known_feature_flag_definition_hash
+ )
end
end
@@ -340,13 +398,15 @@ RSpec.describe API::Features, stub_feature_flags: false do
post api("/features/#{feature_name}", admin), params: { value: '74', key: 'percentage_of_actors' }
expect(response).to have_gitlab_http_status(:created)
- expect(json_response).to eq(
- 'name' => 'my_feature',
+ expect(json_response).to match(
+ 'name' => feature_name,
'state' => 'conditional',
'gates' => [
{ 'key' => 'boolean', 'value' => false },
{ 'key' => 'percentage_of_actors', 'value' => 74 }
- ])
+ ],
+ 'definition' => known_feature_flag_definition_hash
+ )
end
end
end
@@ -390,6 +450,12 @@ RSpec.describe API::Features, stub_feature_flags: false do
expect(response).to have_gitlab_http_status(:no_content)
end
+
+ it 'logs the event' do
+ expect(Feature.logger).to receive(:info).once
+
+ delete api("/features/#{feature_name}", admin)
+ end
end
end
end
diff --git a/spec/requests/api/go_proxy_spec.rb b/spec/requests/api/go_proxy_spec.rb
index 9d422ebbce2..d45e24241b2 100644
--- a/spec/requests/api/go_proxy_spec.rb
+++ b/spec/requests/api/go_proxy_spec.rb
@@ -428,7 +428,7 @@ RSpec.describe API::GoProxy do
context 'with a non-existent project' do
def get_resource(user = nil, **params)
- get api("/projects/not%2fa%2fproject/packages/go/#{base}/@v/list", user, params)
+ get api("/projects/not%2fa%2fproject/packages/go/#{base}/@v/list", user, **params)
end
describe 'GET /projects/:id/packages/go/*module_name/@v/list' do
@@ -465,7 +465,7 @@ RSpec.describe API::GoProxy do
end
def get_resource(user = nil, headers: {}, **params)
- get api("/projects/#{project.id}/packages/go/#{module_name}/@v/#{resource}", user, params), headers: headers
+ get api("/projects/#{project.id}/packages/go/#{module_name}/@v/#{resource}", user, **params), headers: headers
end
def fmt_pseudo_version(prefix, commit)
diff --git a/spec/requests/api/graphql/boards/board_lists_query_spec.rb b/spec/requests/api/graphql/boards/board_lists_query_spec.rb
index 5d5b963fed5..cd94ce91071 100644
--- a/spec/requests/api/graphql/boards/board_lists_query_spec.rb
+++ b/spec/requests/api/graphql/boards/board_lists_query_spec.rb
@@ -66,28 +66,22 @@ RSpec.describe 'get board lists' do
describe 'sorting and pagination' do
let_it_be(:current_user) { user }
- let(:data_path) { [board_parent_type, :boards, :edges, 0, :node, :lists] }
+ let(:data_path) { [board_parent_type, :boards, :nodes, 0, :lists] }
- def pagination_query(params, page_info)
+ def pagination_query(params)
graphql_query_for(
board_parent_type,
{ 'fullPath' => board_parent.full_path },
<<~BOARDS
boards(first: 1) {
- edges {
- node {
- #{query_graphql_field('lists', params, "#{page_info} edges { node { id } }")}
- }
+ nodes {
+ #{query_graphql_field(:lists, params, "#{page_info} nodes { id }")}
}
}
BOARDS
)
end
- def pagination_results_data(data)
- data.map { |list| list.dig('node', 'id') }
- end
-
context 'when using default sorting' do
let!(:label_list) { create(:list, board: board, label: label, position: 10) }
let!(:label_list2) { create(:list, board: board, label: label2, position: 2) }
@@ -99,7 +93,7 @@ RSpec.describe 'get board lists' do
it_behaves_like 'sorted paginated query' do
let(:sort_param) { }
let(:first_param) { 2 }
- let(:expected_results) { lists.map { |list| list.to_global_id.to_s } }
+ let(:expected_results) { lists.map { |list| global_id_of(list) } }
end
end
end
diff --git a/spec/requests/api/graphql/ci/ci_cd_setting_spec.rb b/spec/requests/api/graphql/ci/ci_cd_setting_spec.rb
new file mode 100644
index 00000000000..e086ce02942
--- /dev/null
+++ b/spec/requests/api/graphql/ci/ci_cd_setting_spec.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe 'Getting Ci Cd Setting' do
+ include GraphqlHelpers
+
+ let_it_be_with_reload(:project) { create(:project, :repository) }
+ let_it_be(:current_user) { project.owner }
+
+ let(:fields) do
+ <<~QUERY
+ #{all_graphql_fields_for('ProjectCiCdSetting')}
+ QUERY
+ end
+
+ let(:query) do
+ graphql_query_for(
+ 'project',
+ { 'fullPath' => project.full_path },
+ query_graphql_field('ciCdSettings', {}, fields)
+ )
+ end
+
+ let(:settings_data) { graphql_data['project']['ciCdSettings'] }
+
+ context 'without permissions' do
+ let(:user) { create(:user) }
+
+ before do
+ project.add_reporter(user)
+ post_graphql(query, current_user: user)
+ end
+
+ it_behaves_like 'a working graphql query'
+
+ specify { expect(settings_data).to be nil }
+ end
+
+ context 'with project permissions' do
+ before do
+ post_graphql(query, current_user: current_user)
+ end
+
+ it_behaves_like 'a working graphql query'
+
+ specify { expect(settings_data['mergePipelinesEnabled']).to eql project.ci_cd_settings.merge_pipelines_enabled? }
+ specify { expect(settings_data['mergeTrainsEnabled']).to eql project.ci_cd_settings.merge_trains_enabled? }
+ specify { expect(settings_data['project']['id']).to eql "gid://gitlab/Project/#{project.id}" }
+ end
+end
diff --git a/spec/requests/api/graphql/ci/config_spec.rb b/spec/requests/api/graphql/ci/config_spec.rb
new file mode 100644
index 00000000000..b682470e0a1
--- /dev/null
+++ b/spec/requests/api/graphql/ci/config_spec.rb
@@ -0,0 +1,91 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Query.ciConfig' do
+ include GraphqlHelpers
+
+ subject(:post_graphql_query) { post_graphql(query, current_user: user) }
+
+ let(:user) { create(:user) }
+
+ let_it_be(:content) do
+ File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci_includes.yml'))
+ end
+
+ let(:query) do
+ %(
+ query {
+ ciConfig(content: "#{content}") {
+ status
+ errors
+ stages {
+ name
+ groups {
+ name
+ size
+ jobs {
+ name
+ groupName
+ stage
+ needs {
+ name
+ }
+ }
+ }
+ }
+ }
+ }
+ )
+ end
+
+ before do
+ post_graphql_query
+ end
+
+ it_behaves_like 'a working graphql query'
+
+ it 'returns the correct structure' do
+ expect(graphql_data['ciConfig']).to eq(
+ "status" => "VALID",
+ "errors" => [],
+ "stages" =>
+ [
+ {
+ "name" => "build",
+ "groups" =>
+ [
+ {
+ "name" => "rspec",
+ "size" => 2,
+ "jobs" =>
+ [
+ { "name" => "rspec 0 1", "groupName" => "rspec", "stage" => "build", "needs" => [] },
+ { "name" => "rspec 0 2", "groupName" => "rspec", "stage" => "build", "needs" => [] }
+ ]
+ },
+ {
+ "name" => "spinach", "size" => 1, "jobs" =>
+ [
+ { "name" => "spinach", "groupName" => "spinach", "stage" => "build", "needs" => [] }
+ ]
+ }
+ ]
+ },
+ {
+ "name" => "test",
+ "groups" =>
+ [
+ {
+ "name" => "docker",
+ "size" => 1,
+ "jobs" => [
+ { "name" => "docker", "groupName" => "docker", "stage" => "test", "needs" => [{ "name" => "spinach" }, { "name" => "rspec 0 1" }] }
+ ]
+ }
+ ]
+ }
+ ]
+ )
+ end
+end
diff --git a/spec/requests/api/graphql/ci/job_artifacts_spec.rb b/spec/requests/api/graphql/ci/job_artifacts_spec.rb
new file mode 100644
index 00000000000..df6e398fbe5
--- /dev/null
+++ b/spec/requests/api/graphql/ci/job_artifacts_spec.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Query.project(fullPath).pipelines.jobs.artifacts' do
+ include GraphqlHelpers
+
+ let_it_be(:project) { create(:project, :repository, :public) }
+ let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
+ let_it_be(:user) { create(:user) }
+
+ let_it_be(:query) do
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ pipelines {
+ nodes {
+ jobs {
+ nodes {
+ artifacts {
+ nodes {
+ downloadPath
+ fileType
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ )
+ end
+
+ it 'returns the fields for the artifacts' do
+ job = create(:ci_build, pipeline: pipeline)
+ create(:ci_job_artifact, :junit, job: job)
+
+ post_graphql(query, current_user: user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+
+ pipelines_data = graphql_data.dig('project', 'pipelines', 'nodes')
+ jobs_data = pipelines_data.first.dig('jobs', 'nodes')
+ artifact_data = jobs_data.first.dig('artifacts', 'nodes').first
+
+ expect(artifact_data['downloadPath']).to eq(
+ "/#{project.full_path}/-/jobs/#{job.id}/artifacts/download?file_type=junit"
+ )
+ expect(artifact_data['fileType']).to eq('JUNIT')
+ end
+end
diff --git a/spec/requests/api/graphql/ci/jobs_spec.rb b/spec/requests/api/graphql/ci/jobs_spec.rb
index 618705e5f94..19954c4e52f 100644
--- a/spec/requests/api/graphql/ci/jobs_spec.rb
+++ b/spec/requests/api/graphql/ci/jobs_spec.rb
@@ -1,41 +1,44 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe 'Query.project.pipeline.stages.groups.jobs' do
+RSpec.describe 'Query.project.pipeline' do
include GraphqlHelpers
- let(:project) { create(:project, :repository, :public) }
- let(:user) { create(:user) }
- let(:pipeline) do
- pipeline = create(:ci_pipeline, project: project, user: user)
- stage = create(:ci_stage_entity, pipeline: pipeline, name: 'first')
- create(:commit_status, stage_id: stage.id, pipeline: pipeline, name: 'my test job')
-
- pipeline
- end
+ let_it_be(:project) { create(:project, :repository, :public) }
+ let_it_be(:user) { create(:user) }
def first(field)
[field.pluralize, 'nodes', 0]
end
- let(:jobs_graphql_data) { graphql_data.dig(*%w[project pipeline], *first('stage'), *first('group'), 'jobs', 'nodes') }
-
- let(:query) do
- %(
- query {
- project(fullPath: "#{project.full_path}") {
- pipeline(iid: "#{pipeline.iid}") {
- stages {
- nodes {
- name
- groups {
- nodes {
- name
- jobs {
- nodes {
- name
- pipeline {
- id
+ describe '.stages.groups.jobs' do
+ let(:pipeline) do
+ pipeline = create(:ci_pipeline, project: project, user: user)
+ stage = create(:ci_stage_entity, pipeline: pipeline, name: 'first')
+ create(:commit_status, stage_id: stage.id, pipeline: pipeline, name: 'my test job')
+
+ pipeline
+ end
+
+ let(:jobs_graphql_data) { graphql_data.dig(*%w[project pipeline], *first('stage'), *first('group'), 'jobs', 'nodes') }
+
+ let(:query) do
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ pipeline(iid: "#{pipeline.iid}") {
+ stages {
+ nodes {
+ name
+ groups {
+ nodes {
+ name
+ jobs {
+ nodes {
+ name
+ pipeline {
+ id
+ }
}
}
}
@@ -45,17 +48,15 @@ RSpec.describe 'Query.project.pipeline.stages.groups.jobs' do
}
}
}
- }
- )
- end
+ )
+ end
- it 'returns the jobs of a pipeline stage' do
- post_graphql(query, current_user: user)
+ it 'returns the jobs of a pipeline stage' do
+ post_graphql(query, current_user: user)
- expect(jobs_graphql_data).to contain_exactly(a_hash_including('name' => 'my test job'))
- end
+ expect(jobs_graphql_data).to contain_exactly(a_hash_including('name' => 'my test job'))
+ end
- context 'when fetching jobs from the pipeline' do
it 'avoids N+1 queries', :aggregate_failures do
control_count = ActiveRecord::QueryRecorder.new do
post_graphql(query, current_user: user)
@@ -112,4 +113,50 @@ RSpec.describe 'Query.project.pipeline.stages.groups.jobs' do
])
end
end
+
+ describe '.jobs.artifacts' do
+ let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
+
+ let(:query) do
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ pipeline(iid: "#{pipeline.iid}") {
+ jobs {
+ nodes {
+ artifacts {
+ nodes {
+ downloadPath
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ )
+ end
+
+ context 'when the job is a build' do
+ it "returns the build's artifacts" do
+ create(:ci_build, :artifacts, pipeline: pipeline)
+
+ post_graphql(query, current_user: user)
+
+ job_data = graphql_data.dig('project', 'pipeline', 'jobs', 'nodes').first
+ expect(job_data.dig('artifacts', 'nodes').count).to be(2)
+ end
+ end
+
+ context 'when the job is not a build' do
+ it 'returns nil' do
+ create(:ci_bridge, pipeline: pipeline)
+
+ post_graphql(query, current_user: user)
+
+ job_data = graphql_data.dig('project', 'pipeline', 'jobs', 'nodes').first
+ expect(job_data['artifacts']).to be_nil
+ end
+ end
+ end
end
diff --git a/spec/requests/api/graphql/group/container_repositories_spec.rb b/spec/requests/api/graphql/group/container_repositories_spec.rb
index bcf689a5e8f..4aa775eba0f 100644
--- a/spec/requests/api/graphql/group/container_repositories_spec.rb
+++ b/spec/requests/api/graphql/group/container_repositories_spec.rb
@@ -14,7 +14,7 @@ RSpec.describe 'getting container repositories in a group' do
let_it_be(:container_repositories) { [container_repository, container_repositories_delete_scheduled, container_repositories_delete_failed].flatten }
let_it_be(:container_expiration_policy) { project.container_expiration_policy }
- let(:fields) do
+ let(:container_repositories_fields) do
<<~GQL
edges {
node {
@@ -24,17 +24,25 @@ RSpec.describe 'getting container repositories in a group' do
GQL
end
+ let(:fields) do
+ <<~GQL
+ #{query_graphql_field('container_repositories', {}, container_repositories_fields)}
+ containerRepositoriesCount
+ GQL
+ end
+
let(:query) do
graphql_query_for(
'group',
{ 'fullPath' => group.full_path },
- query_graphql_field('container_repositories', {}, fields)
+ fields
)
end
let(:user) { owner }
let(:variables) { {} }
let(:container_repositories_response) { graphql_data.dig('group', 'containerRepositories', 'edges') }
+ let(:container_repositories_count_response) { graphql_data.dig('group', 'containerRepositoriesCount') }
before do
group.add_owner(owner)
@@ -101,7 +109,7 @@ RSpec.describe 'getting container repositories in a group' do
<<~GQL
query($path: ID!, $n: Int) {
group(fullPath: $path) {
- containerRepositories(first: $n) { #{fields} }
+ containerRepositories(first: $n) { #{container_repositories_fields} }
}
}
GQL
@@ -122,7 +130,7 @@ RSpec.describe 'getting container repositories in a group' do
<<~GQL
query($path: ID!, $name: String) {
group(fullPath: $path) {
- containerRepositories(name: $name) { #{fields} }
+ containerRepositories(name: $name) { #{container_repositories_fields} }
}
}
GQL
@@ -143,4 +151,10 @@ RSpec.describe 'getting container repositories in a group' do
expect(container_repositories_response.first.dig('node', 'id')).to eq(container_repository.to_global_id.to_s)
end
end
+
+ it 'returns the total count of container repositories' do
+ subject
+
+ expect(container_repositories_count_response).to eq(container_repositories.size)
+ 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 84b2fd63d46..3554e22cdf2 100644
--- a/spec/requests/api/graphql/group/group_members_spec.rb
+++ b/spec/requests/api/graphql/group/group_members_spec.rb
@@ -5,44 +5,95 @@ require 'spec_helper'
RSpec.describe 'getting group members information' do
include GraphqlHelpers
- let_it_be(:group) { create(:group, :public) }
+ let_it_be(:parent_group) { create(:group, :public) }
let_it_be(:user) { create(:user) }
let_it_be(:user_1) { create(:user, username: 'user') }
let_it_be(:user_2) { create(:user, username: 'test') }
let(:member_data) { graphql_data['group']['groupMembers']['edges'] }
- before do
- [user_1, user_2].each { |user| group.add_guest(user) }
+ before_all do
+ [user_1, user_2].each { |user| parent_group.add_guest(user) }
end
context 'when the request is correct' do
it_behaves_like 'a working graphql query' do
- before do
- fetch_members(user)
+ before_all do
+ fetch_members
end
end
it 'returns group members successfully' do
- fetch_members(user)
+ fetch_members
expect(graphql_errors).to be_nil
- expect_array_response(user_1.to_global_id.to_s, user_2.to_global_id.to_s)
+ expect_array_response(user_1, user_2)
end
it 'returns members that match the search query' do
- fetch_members(user, { search: 'test' })
+ fetch_members(args: { search: 'test' })
expect(graphql_errors).to be_nil
- expect_array_response(user_2.to_global_id.to_s)
+ expect_array_response(user_2)
end
end
- def fetch_members(user = nil, args = {})
- post_graphql(members_query(args), current_user: user)
+ context 'member relations' do
+ let_it_be(:child_group) { create(:group, :public, parent: parent_group) }
+ let_it_be(:grandchild_group) { create(:group, :public, parent: child_group) }
+ let_it_be(:child_user) { create(:user) }
+ let_it_be(:grandchild_user) { create(:user) }
+
+ before_all do
+ child_group.add_guest(child_user)
+ grandchild_group.add_guest(grandchild_user)
+ end
+
+ it 'returns direct members' do
+ fetch_members(group: child_group, args: { relations: [:DIRECT] })
+
+ expect(graphql_errors).to be_nil
+ expect_array_response(child_user)
+ end
+
+ it 'returns direct and inherited members' do
+ fetch_members(group: child_group, args: { relations: [:DIRECT, :INHERITED] })
+
+ expect(graphql_errors).to be_nil
+ expect_array_response(child_user, user_1, user_2)
+ end
+
+ it 'returns direct, inherited, and descendant members' do
+ fetch_members(group: child_group, args: { relations: [:DIRECT, :INHERITED, :DESCENDANTS] })
+
+ expect(graphql_errors).to be_nil
+ expect_array_response(child_user, user_1, user_2, grandchild_user)
+ end
+
+ it 'returns an error for an invalid member relation' do
+ fetch_members(group: child_group, args: { relations: [:OBLIQUE] })
+
+ expect(graphql_errors.first)
+ .to include('path' => %w[query group groupMembers relations],
+ 'message' => a_string_including('invalid value ([OBLIQUE])'))
+ end
+ end
+
+ context 'when unauthenticated' do
+ it 'returns nothing' do
+ fetch_members(current_user: nil)
+
+ expect(graphql_errors).to be_nil
+ expect(response).to have_gitlab_http_status(:success)
+ expect(member_data).to be_empty
+ end
+ end
+
+ def fetch_members(group: parent_group, current_user: user, args: {})
+ post_graphql(members_query(group.full_path, args), current_user: current_user)
end
- def members_query(args = {})
+ def members_query(group_path, args = {})
members_node = <<~NODE
edges {
node {
@@ -54,7 +105,7 @@ RSpec.describe 'getting group members information' do
NODE
graphql_query_for("group",
- { full_path: group.full_path },
+ { full_path: group_path },
[query_graphql_field("groupMembers", args, members_node)]
)
end
@@ -62,6 +113,7 @@ RSpec.describe 'getting group members information' do
def expect_array_response(*items)
expect(response).to have_gitlab_http_status(:success)
expect(member_data).to be_an Array
- expect(member_data.map { |node| node["node"]["user"]["id"] }).to match_array(items)
+ expect(member_data.map { |node| node["node"]["user"]["id"] })
+ .to match_array(items.map { |u| global_id_of(u) })
end
end
diff --git a/spec/requests/api/graphql/group_query_spec.rb b/spec/requests/api/graphql/group_query_spec.rb
index 83180c7d7a5..391bae4cfcf 100644
--- a/spec/requests/api/graphql/group_query_spec.rb
+++ b/spec/requests/api/graphql/group_query_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
# Based on spec/requests/api/groups_spec.rb
# Should follow closely in order to ensure all situations are covered
-RSpec.describe 'getting group information', :do_not_mock_admin_mode do
+RSpec.describe 'getting group information' do
include GraphqlHelpers
include UploadHelpers
diff --git a/spec/requests/api/graphql/issue/issue_spec.rb b/spec/requests/api/graphql/issue/issue_spec.rb
index d7fa680d29b..42c8e0cc9c0 100644
--- a/spec/requests/api/graphql/issue/issue_spec.rb
+++ b/spec/requests/api/graphql/issue/issue_spec.rb
@@ -125,7 +125,7 @@ RSpec.describe 'Query.issue(id)' do
let(:issue_params) { { 'id' => confidential_issue.to_global_id.to_s } }
context 'when the user cannot see confidential issues' do
- it 'returns nil ' do
+ it 'returns nil' do
post_graphql(query, current_user: current_user)
expect(issue_data).to be nil
diff --git a/spec/requests/api/graphql/mutations/admin/sidekiq_queues/delete_jobs_spec.rb b/spec/requests/api/graphql/mutations/admin/sidekiq_queues/delete_jobs_spec.rb
index 4ad35e7f0d1..b8cde32877b 100644
--- a/spec/requests/api/graphql/mutations/admin/sidekiq_queues/delete_jobs_spec.rb
+++ b/spec/requests/api/graphql/mutations/admin/sidekiq_queues/delete_jobs_spec.rb
@@ -6,8 +6,9 @@ RSpec.describe 'Deleting Sidekiq jobs', :clean_gitlab_redis_queues do
include GraphqlHelpers
let_it_be(:admin) { create(:admin) }
+ let(:queue) { 'authorized_projects' }
- let(:variables) { { user: admin.username, queue_name: 'authorized_projects' } }
+ let(:variables) { { user: admin.username, queue_name: queue } }
let(:mutation) { graphql_mutation(:admin_sidekiq_queues_delete_jobs, variables) }
def mutation_response
@@ -26,18 +27,19 @@ RSpec.describe 'Deleting Sidekiq jobs', :clean_gitlab_redis_queues do
context 'valid request' do
around do |example|
- Sidekiq::Queue.new('authorized_projects').clear
+ Sidekiq::Queue.new(queue).clear
Sidekiq::Testing.disable!(&example)
- Sidekiq::Queue.new('authorized_projects').clear
+ Sidekiq::Queue.new(queue).clear
end
def add_job(user, args)
Sidekiq::Client.push(
'class' => 'AuthorizedProjectsWorker',
- 'queue' => 'authorized_projects',
+ 'queue' => queue,
'args' => args,
'meta.user' => user.username
)
+ raise 'Not enqueued!' if Sidekiq::Queue.new(queue).size.zero?
end
it 'returns info about the deleted jobs' do
@@ -55,7 +57,7 @@ RSpec.describe 'Deleting Sidekiq jobs', :clean_gitlab_redis_queues do
end
context 'when no required params are provided' do
- let(:variables) { { queue_name: 'authorized_projects' } }
+ let(:variables) { { queue_name: queue } }
it_behaves_like 'a mutation that returns errors in the response',
errors: ['No metadata provided']
diff --git a/spec/requests/api/graphql/mutations/award_emojis/add_spec.rb b/spec/requests/api/graphql/mutations/award_emojis/add_spec.rb
index 3aaebb5095a..b39062f2e71 100644
--- a/spec/requests/api/graphql/mutations/award_emojis/add_spec.rb
+++ b/spec/requests/api/graphql/mutations/award_emojis/add_spec.rb
@@ -56,10 +56,10 @@ RSpec.describe 'Adding an AwardEmoji' do
it_behaves_like 'a mutation that does not create an AwardEmoji'
it_behaves_like 'a mutation that returns top-level errors',
- errors: ['Cannot award emoji to this resource']
+ errors: ['You cannot award emoji to this resource.']
end
- context 'when the given awardable an Awardable' do
+ context 'when the given awardable is an Awardable' do
it 'creates an emoji' do
expect do
post_graphql_mutation(mutation, current_user: current_user)
diff --git a/spec/requests/api/graphql/mutations/award_emojis/toggle_spec.rb b/spec/requests/api/graphql/mutations/award_emojis/toggle_spec.rb
index 6910ad80a11..170e7ff3b44 100644
--- a/spec/requests/api/graphql/mutations/award_emojis/toggle_spec.rb
+++ b/spec/requests/api/graphql/mutations/award_emojis/toggle_spec.rb
@@ -55,7 +55,7 @@ RSpec.describe 'Toggling an AwardEmoji' do
it_behaves_like 'a mutation that does not create or destroy an AwardEmoji'
it_behaves_like 'a mutation that returns top-level errors',
- errors: ['Cannot award emoji to this resource']
+ errors: ['You cannot award emoji to this resource.']
end
context 'when the given awardable is an Awardable' do
diff --git a/spec/requests/api/graphql/mutations/container_repository/destroy_spec.rb b/spec/requests/api/graphql/mutations/container_repository/destroy_spec.rb
index 645edfc2e43..c4121cfed42 100644
--- a/spec/requests/api/graphql/mutations/container_repository/destroy_spec.rb
+++ b/spec/requests/api/graphql/mutations/container_repository/destroy_spec.rb
@@ -22,7 +22,7 @@ RSpec.describe 'Destroying a container repository' do
GQL
end
- let(:params) { { id: container_repository.to_global_id.to_s } }
+ let(:params) { { id: id } }
let(:mutation) { graphql_mutation(:destroy_container_repository, params, query) }
let(:mutation_response) { graphql_mutation_response(:destroyContainerRepository) }
let(:container_repository_mutation_response) { mutation_response['containerRepository'] }
diff --git a/spec/requests/api/graphql/mutations/container_repository/destroy_tags_spec.rb b/spec/requests/api/graphql/mutations/container_repository/destroy_tags_spec.rb
new file mode 100644
index 00000000000..decb2e7bccc
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/container_repository/destroy_tags_spec.rb
@@ -0,0 +1,120 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Destroying a container repository tags' do
+ include_context 'container repository delete tags service shared context'
+ using RSpec::Parameterized::TableSyntax
+
+ include GraphqlHelpers
+
+ let(:id) { repository.to_global_id.to_s }
+ let(:tags) { %w[A C D E] }
+
+ let(:query) do
+ <<~GQL
+ deletedTagNames
+ errors
+ GQL
+ end
+
+ let(:params) { { id: id, tag_names: tags } }
+ let(:mutation) { graphql_mutation(:destroy_container_repository_tags, params, query) }
+ let(:mutation_response) { graphql_mutation_response(:destroyContainerRepositoryTags) }
+ let(:tag_names_response) { mutation_response['deletedTagNames'] }
+ let(:errors_response) { mutation_response['errors'] }
+
+ shared_examples 'destroying the container repository tags' do
+ before do
+ stub_delete_reference_requests(tags)
+ expect_delete_tag_by_names(tags)
+ allow_next_instance_of(ContainerRegistry::Client) do |client|
+ allow(client).to receive(:supports_tag_delete?).and_return(true)
+ end
+ end
+
+ it 'destroys the container repository tags' do
+ expect(Projects::ContainerRepository::DeleteTagsService)
+ .to receive(:new).and_call_original
+ expect { subject }.to change { ::Packages::Event.count }.by(1)
+
+ expect(tag_names_response).to eq(tags)
+ expect(errors_response).to eq([])
+ end
+
+ it_behaves_like 'returning response status', :success
+ end
+
+ shared_examples 'denying the mutation request' do
+ it 'does not destroy the container repository tags' do
+ expect(Projects::ContainerRepository::DeleteTagsService)
+ .not_to receive(:new)
+
+ expect { subject }.not_to change { ::Packages::Event.count }
+
+ expect(mutation_response).to be_nil
+ end
+
+ it_behaves_like 'returning response status', :success
+ end
+
+ describe 'post graphql mutation' do
+ subject { post_graphql_mutation(mutation, current_user: user) }
+
+ context 'with valid id' do
+ where(:user_role, :shared_examples_name) do
+ :maintainer | 'destroying the container repository tags'
+ :developer | 'destroying the container repository tags'
+ :reporter | 'denying the mutation request'
+ :guest | 'denying the mutation request'
+ :anonymous | 'denying the mutation request'
+ end
+
+ with_them do
+ before do
+ project.send("add_#{user_role}", user) unless user_role == :anonymous
+ end
+
+ it_behaves_like params[:shared_examples_name]
+ end
+ end
+
+ context 'with invalid id' do
+ let(:id) { 'gid://gitlab/ContainerRepository/5555' }
+
+ it_behaves_like 'denying the mutation request'
+ end
+
+ context 'with too many tags' do
+ let(:tags) { Array.new(Mutations::ContainerRepositories::DestroyTags::LIMIT + 1, 'x') }
+
+ it 'returns too many tags error' do
+ expect { subject }.not_to change { ::Packages::Event.count }
+
+ explanation = graphql_errors.dig(0, 'extensions', 'problems', 0, 'explanation')
+ expect(explanation).to eq(Mutations::ContainerRepositories::DestroyTags::TOO_MANY_TAGS_ERROR_MESSAGE)
+ end
+ end
+
+ context 'with service error' do
+ before do
+ project.add_maintainer(user)
+ allow_next_instance_of(Projects::ContainerRepository::DeleteTagsService) do |service|
+ allow(service).to receive(:execute).and_return(message: 'could not delete tags', status: :error)
+ end
+ end
+
+ it 'returns an error' do
+ subject
+
+ expect(tag_names_response).to eq([])
+ expect(errors_response).to eq(['could not delete tags'])
+ end
+
+ it 'does not create a package event' do
+ expect(::Packages::CreateEventService).not_to receive(:new)
+ expect { subject }.not_to change { ::Packages::Event.count }
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/environments/canary_ingress/update_spec.rb b/spec/requests/api/graphql/mutations/environments/canary_ingress/update_spec.rb
new file mode 100644
index 00000000000..f25a49291a6
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/environments/canary_ingress/update_spec.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Update Environment Canary Ingress', :clean_gitlab_redis_cache do
+ include GraphqlHelpers
+ include KubernetesHelpers
+
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:cluster) { create(:cluster, :project, projects: [project]) }
+ let_it_be(:service) { create(:cluster_platform_kubernetes, :configured, cluster: cluster) }
+ let_it_be(:environment) { create(:environment, project: project) }
+ let_it_be(:deployment) { create(:deployment, :success, environment: environment, project: project) }
+ let_it_be(:maintainer) { create(:user) }
+ let_it_be(:developer) { create(:user) }
+ let(:environment_id) { environment.to_global_id.to_s }
+ let(:weight) { 25 }
+ let(:actor) { developer }
+
+ let(:mutation) do
+ graphql_mutation(:environments_canary_ingress_update, id: environment_id, weight: weight)
+ end
+
+ before_all do
+ project.add_maintainer(maintainer)
+ project.add_developer(developer)
+ end
+
+ before do
+ stub_kubeclient_ingresses(environment.deployment_namespace, response: kube_ingresses_response(with_canary: true))
+ end
+
+ context 'when kubernetes accepted the patch request' do
+ before do
+ stub_kubeclient_ingresses(environment.deployment_namespace, method: :patch, resource_path: "/production-auto-deploy")
+ end
+
+ it 'updates successfully' do
+ post_graphql_mutation(mutation, current_user: actor)
+
+ expect(graphql_mutation_response(:environments_canary_ingress_update)['errors'])
+ .to be_empty
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/releases/delete_spec.rb b/spec/requests/api/graphql/mutations/releases/delete_spec.rb
new file mode 100644
index 00000000000..3710f118bf4
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/releases/delete_spec.rb
@@ -0,0 +1,132 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Deleting a release' do
+ include GraphqlHelpers
+ include Presentable
+
+ let_it_be(:public_user) { create(:user) }
+ let_it_be(:guest) { create(:user) }
+ let_it_be(:reporter) { create(:user) }
+ let_it_be(:developer) { create(:user) }
+ let_it_be(:maintainer) { create(:user) }
+ let_it_be(:project) { create(:project, :public, :repository) }
+ let_it_be(:tag_name) { 'v1.1.0' }
+ let_it_be(:release) { create(:release, project: project, tag: tag_name) }
+
+ let(:mutation_name) { :release_delete }
+
+ let(:project_path) { project.full_path }
+ let(:mutation_arguments) do
+ {
+ projectPath: project_path,
+ tagName: tag_name
+ }
+ end
+
+ let(:mutation) do
+ graphql_mutation(mutation_name, mutation_arguments, <<~FIELDS)
+ release {
+ tagName
+ }
+ errors
+ FIELDS
+ end
+
+ let(:delete_release) { post_graphql_mutation(mutation, current_user: current_user) }
+ let(:mutation_response) { graphql_mutation_response(mutation_name)&.with_indifferent_access }
+
+ before do
+ project.add_guest(guest)
+ project.add_reporter(reporter)
+ project.add_developer(developer)
+ project.add_maintainer(maintainer)
+ end
+
+ shared_examples 'unauthorized or not found error' do
+ it 'returns a top-level error with message' do
+ delete_release
+
+ expect(mutation_response).to be_nil
+ expect(graphql_errors.count).to eq(1)
+ expect(graphql_errors.first['message']).to eq("The resource that you are attempting to access does not exist or you don't have permission to perform this action")
+ end
+ end
+
+ context 'when the current user has access to update releases' do
+ let(:current_user) { maintainer }
+
+ it 'deletes the release' do
+ expect { delete_release }.to change { Release.count }.by(-1)
+ end
+
+ it 'returns the deleted release' do
+ delete_release
+
+ expected_release = { tagName: tag_name }.with_indifferent_access
+
+ expect(mutation_response[:release]).to eq(expected_release)
+ end
+
+ it 'does not remove the Git tag associated with the deleted release' do
+ expect { delete_release }.not_to change { Project.find_by_id(project.id).repository.tag_count }
+ end
+
+ it 'returns no errors' do
+ delete_release
+
+ expect(mutation_response[:errors]).to eq([])
+ end
+
+ context 'validation' do
+ context 'when the release does not exist' do
+ let_it_be(:tag_name) { 'not-a-real-release' }
+
+ it 'returns the release as null' do
+ delete_release
+
+ expect(mutation_response[:release]).to be_nil
+ end
+
+ it 'returns an errors-at-data message' do
+ delete_release
+
+ expect(mutation_response[:errors]).to eq(['Release does not exist'])
+ end
+ end
+
+ context 'when the project does not exist' do
+ let(:project_path) { 'not/a/real/path' }
+
+ it_behaves_like 'unauthorized or not found error'
+ end
+ end
+ end
+
+ context "when the current user doesn't have access to update releases" do
+ context 'when the current user is a Developer' do
+ let(:current_user) { developer }
+
+ it_behaves_like 'unauthorized or not found error'
+ end
+
+ context 'when the current user is a Reporter' do
+ let(:current_user) { reporter }
+
+ it_behaves_like 'unauthorized or not found error'
+ end
+
+ context 'when the current user is a Guest' do
+ let(:current_user) { guest }
+
+ it_behaves_like 'unauthorized or not found error'
+ end
+
+ context 'when the current user is a public user' do
+ let(:current_user) { public_user }
+
+ it_behaves_like 'unauthorized or not found error'
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/releases/update_spec.rb b/spec/requests/api/graphql/mutations/releases/update_spec.rb
new file mode 100644
index 00000000000..19320c3393c
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/releases/update_spec.rb
@@ -0,0 +1,255 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Updating an existing release' do
+ include GraphqlHelpers
+ include Presentable
+
+ let_it_be(:public_user) { create(:user) }
+ let_it_be(:guest) { create(:user) }
+ let_it_be(:reporter) { create(:user) }
+ let_it_be(:developer) { create(:user) }
+ let_it_be(:project) { create(:project, :public, :repository) }
+ let_it_be(:milestone_12_3) { create(:milestone, project: project, title: '12.3') }
+ let_it_be(:milestone_12_4) { create(:milestone, project: project, title: '12.4') }
+
+ let_it_be(:tag_name) { 'v1.1.0' }
+ let_it_be(:name) { 'Version 7.12.5'}
+ let_it_be(:description) { 'Release 7.12.5 :rocket:' }
+ let_it_be(:released_at) { '2018-12-10' }
+ let_it_be(:created_at) { '2018-11-05' }
+ 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)
+ end
+
+ let(:mutation_name) { :release_update }
+
+ let(:mutation_arguments) do
+ {
+ projectPath: project.full_path,
+ tagName: tag_name
+ }
+ end
+
+ let(:mutation) do
+ graphql_mutation(mutation_name, mutation_arguments, <<~FIELDS)
+ release {
+ tagName
+ name
+ description
+ releasedAt
+ createdAt
+ milestones {
+ nodes {
+ title
+ }
+ }
+ }
+ errors
+ FIELDS
+ end
+
+ let(:update_release) { post_graphql_mutation(mutation, current_user: current_user) }
+ let(:mutation_response) { graphql_mutation_response(mutation_name)&.with_indifferent_access }
+
+ let(:expected_attributes) do
+ {
+ tagName: tag_name,
+ name: name,
+ description: description,
+ releasedAt: Time.parse(released_at).utc.iso8601,
+ createdAt: Time.parse(created_at).utc.iso8601,
+ milestones: {
+ nodes: milestones.map { |m| { title: m.title } }
+ }
+ }.with_indifferent_access
+ end
+
+ around do |example|
+ freeze_time { example.run }
+ end
+
+ before do
+ project.add_guest(guest)
+ project.add_reporter(reporter)
+ project.add_developer(developer)
+
+ stub_default_url_options(host: 'www.example.com')
+ end
+
+ shared_examples 'no errors' do
+ it 'returns no errors' do
+ update_release
+
+ expect(graphql_errors).not_to be_present
+ end
+ end
+
+ shared_examples 'top-level error with message' do |error_message|
+ it 'returns a top-level error with message' do
+ update_release
+
+ expect(mutation_response).to be_nil
+ expect(graphql_errors.count).to eq(1)
+ expect(graphql_errors.first['message']).to eq(error_message)
+ end
+ end
+
+ shared_examples 'errors-as-data with message' do |error_message|
+ it 'returns an error-as-data with message' do
+ update_release
+
+ expect(mutation_response[:release]).to be_nil
+ expect(mutation_response[:errors].count).to eq(1)
+ expect(mutation_response[:errors].first).to match(error_message)
+ end
+ end
+
+ shared_examples 'updates release fields' do |updates|
+ it_behaves_like 'no errors'
+
+ it 'updates the correct field and returns the release' do
+ update_release
+
+ expect(mutation_response[:release]).to include(expected_attributes.merge(updates).except(:milestones))
+
+ # Right now the milestones are returned in a non-deterministic order.
+ # Because of this, we need to test milestones separately to allow
+ # for them to be returned in any order.
+ # Once https://gitlab.com/gitlab-org/gitlab/-/issues/259012 has been
+ # fixed, this special milestone handling can be removed.
+ expected_milestones = expected_attributes.merge(updates)[:milestones]
+ expect(mutation_response[:release][:milestones][:nodes]).to match_array(expected_milestones[:nodes])
+ end
+ end
+
+ context 'when the current user has access to update releases' do
+ let(:current_user) { developer }
+
+ context 'name' do
+ context 'when a new name is provided' do
+ let(:mutation_arguments) { super().merge(name: 'Updated name') }
+
+ it_behaves_like 'updates release fields', name: 'Updated name'
+ end
+
+ context 'when null is provided' do
+ let(:mutation_arguments) { super().merge(name: nil) }
+
+ it_behaves_like 'updates release fields', name: 'v1.1.0'
+ end
+ end
+
+ context 'description' do
+ context 'when a new description is provided' do
+ let(:mutation_arguments) { super().merge(description: 'Updated description') }
+
+ it_behaves_like 'updates release fields', description: 'Updated description'
+ end
+
+ context 'when null is provided' do
+ let(:mutation_arguments) { super().merge(description: nil) }
+
+ it_behaves_like 'updates release fields', description: nil
+ end
+ end
+
+ context 'releasedAt' do
+ context 'when no time zone is provided' do
+ let(:mutation_arguments) { super().merge(releasedAt: '2015-05-05') }
+
+ it_behaves_like 'updates release fields', releasedAt: Time.parse('2015-05-05').utc.iso8601
+ end
+
+ context 'when a local time zone is provided' do
+ let(:mutation_arguments) { super().merge(releasedAt: Time.parse('2015-05-05').in_time_zone('Hawaii').iso8601) }
+
+ it_behaves_like 'updates release fields', releasedAt: Time.parse('2015-05-05').utc.iso8601
+ end
+
+ context 'when null is provided' do
+ let(:mutation_arguments) { super().merge(releasedAt: nil) }
+
+ it_behaves_like 'top-level error with message', 'if the releasedAt argument is provided, it cannot be null'
+ end
+ end
+
+ context 'milestones' do
+ context 'when a new set of milestones is provided provided' do
+ let(:mutation_arguments) { super().merge(milestones: ['12.3']) }
+
+ it_behaves_like 'updates release fields', milestones: { nodes: [{ title: '12.3' }] }
+ end
+
+ context 'when an empty array is provided' do
+ let(:mutation_arguments) { super().merge(milestones: []) }
+
+ it_behaves_like 'updates release fields', milestones: { nodes: [] }
+ end
+
+ context 'when null is provided' do
+ let(:mutation_arguments) { super().merge(milestones: nil) }
+
+ it_behaves_like 'top-level error with message', 'if the milestones argument is provided, it cannot be null'
+ end
+
+ context 'when a non-existent milestone title is provided' do
+ let(:mutation_arguments) { super().merge(milestones: ['not real']) }
+
+ it_behaves_like 'errors-as-data with message', 'Milestone(s) not found: not real'
+ end
+
+ context 'when a milestone title from a different project is provided' do
+ let(:milestone_in_different_project) { create(:milestone, title: 'milestone in different project') }
+ let(:mutation_arguments) { super().merge(milestones: [milestone_in_different_project.title]) }
+
+ it_behaves_like 'errors-as-data with message', 'Milestone(s) not found: milestone in different project'
+ end
+ end
+
+ context 'validation' do
+ context 'when no updated fields are provided' do
+ it_behaves_like 'errors-as-data with message', 'params is empty'
+ end
+
+ context 'when the tag does not exist' do
+ let(:mutation_arguments) { super().merge(tagName: 'not-a-real-tag') }
+
+ it_behaves_like 'errors-as-data with message', 'Tag does not exist'
+ end
+
+ context 'when the project does not exist' do
+ let(:mutation_arguments) { super().merge(projectPath: 'not/a/real/path') }
+
+ it_behaves_like 'top-level error with message', "The resource that you are attempting to access does not exist or you don't have permission to perform this action"
+ end
+ end
+ end
+
+ context "when the current user doesn't have access to update releases" do
+ expected_error_message = "The resource that you are attempting to access does not exist or you don't have permission to perform this action"
+
+ context 'when the current user is a Reporter' do
+ let(:current_user) { reporter }
+
+ it_behaves_like 'top-level error with message', expected_error_message
+ end
+
+ context 'when the current user is a Guest' do
+ let(:current_user) { guest }
+
+ it_behaves_like 'top-level error with message', expected_error_message
+ end
+
+ context 'when the current user is a public user' do
+ let(:current_user) { public_user }
+
+ it_behaves_like 'top-level error with message', expected_error_message
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/snippets/create_spec.rb b/spec/requests/api/graphql/mutations/snippets/create_spec.rb
index d2fa3cfc24f..fd0dc98a8d3 100644
--- a/spec/requests/api/graphql/mutations/snippets/create_spec.rb
+++ b/spec/requests/api/graphql/mutations/snippets/create_spec.rb
@@ -163,9 +163,15 @@ RSpec.describe 'Creating a Snippet' do
context 'when there are uploaded files' do
shared_examples 'expected files argument' do |file_value, expected_value|
let(:uploaded_files) { file_value }
+ let(:snippet) { build(:snippet) }
+ let(:creation_response) do
+ ::ServiceResponse.error(message: 'urk', payload: { snippet: snippet })
+ end
it do
- expect(::Snippets::CreateService).to receive(:new).with(nil, user, hash_including(files: expected_value))
+ expect(::Snippets::CreateService).to receive(:new)
+ .with(nil, user, hash_including(files: expected_value))
+ .and_return(double(execute: creation_response))
subject
end
diff --git a/spec/requests/api/graphql/mutations/snippets/mark_as_spam_spec.rb b/spec/requests/api/graphql/mutations/snippets/mark_as_spam_spec.rb
index 97e6ae8fda8..4d499310591 100644
--- a/spec/requests/api/graphql/mutations/snippets/mark_as_spam_spec.rb
+++ b/spec/requests/api/graphql/mutations/snippets/mark_as_spam_spec.rb
@@ -2,8 +2,9 @@
require 'spec_helper'
-RSpec.describe 'Mark snippet as spam', :do_not_mock_admin_mode do
+RSpec.describe 'Mark snippet as spam' do
include GraphqlHelpers
+ include AfterNextHelpers
let_it_be(:admin) { create(:admin) }
let_it_be(:other_user) { create(:user) }
@@ -56,11 +57,12 @@ RSpec.describe 'Mark snippet as spam', :do_not_mock_admin_mode do
end
it 'marks snippet as spam' do
- expect_next_instance_of(Spam::MarkAsSpamService) do |instance|
- expect(instance).to receive(:execute)
- end
+ expect_next(Spam::MarkAsSpamService, target: snippet)
+ .to receive(:execute).and_return(true)
post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(graphql_errors).to be_blank
end
end
end
diff --git a/spec/requests/api/graphql/namespace/projects_spec.rb b/spec/requests/api/graphql/namespace/projects_spec.rb
index 03160719389..3e68503b7fb 100644
--- a/spec/requests/api/graphql/namespace/projects_spec.rb
+++ b/spec/requests/api/graphql/namespace/projects_spec.rb
@@ -80,38 +80,34 @@ RSpec.describe 'getting projects' do
end
describe 'sorting and pagination' do
+ let_it_be(:ns) { create(:group) }
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:project_1) { create(:project, name: 'Project', path: 'project', namespace: ns) }
+ let_it_be(:project_2) { create(:project, name: 'Test Project', path: 'test-project', namespace: ns) }
+ let_it_be(:project_3) { create(:project, name: 'Test', path: 'test', namespace: ns) }
+ let_it_be(:project_4) { create(:project, name: 'Test Project Other', path: 'other-test-project', namespace: ns) }
+
let(:data_path) { [:namespace, :projects] }
- def pagination_query(params, page_info)
- graphql_query_for(
- 'namespace',
- { 'fullPath' => subject.full_path },
- <<~QUERY
- projects(includeSubgroups: #{include_subgroups}, search: "#{search}", #{params}) {
- #{page_info} edges {
- node {
- #{all_graphql_fields_for('Project')}
- }
- }
- }
- QUERY
- )
+ let(:ns_args) { { full_path: ns.full_path } }
+ let(:search) { 'test' }
+
+ before do
+ ns.add_owner(current_user)
end
- def pagination_results_data(data)
- data.map { |project| project.dig('node', 'name') }
+ def pagination_query(params)
+ arguments = params.merge(include_subgroups: include_subgroups, search: search)
+ graphql_query_for(:namespace, ns_args, query_graphql_field(:projects, arguments, <<~GQL))
+ #{page_info}
+ nodes { name }
+ GQL
end
context 'when sorting by similarity' do
- let!(:project_1) { create(:project, name: 'Project', path: 'project', namespace: subject) }
- let!(:project_2) { create(:project, name: 'Test Project', path: 'test-project', namespace: subject) }
- let!(:project_3) { create(:project, name: 'Test', path: 'test', namespace: subject) }
- let!(:project_4) { create(:project, name: 'Test Project Other', path: 'other-test-project', namespace: subject) }
- let(:search) { 'test' }
- let(:current_user) { user }
-
it_behaves_like 'sorted paginated query' do
- let(:sort_param) { 'SIMILARITY' }
+ let(:node_path) { %w[name] }
+ let(:sort_param) { :SIMILARITY }
let(:first_param) { 2 }
let(:expected_results) { [project_3.name, project_2.name, project_4.name] }
end
diff --git a/spec/requests/api/graphql/project/container_repositories_spec.rb b/spec/requests/api/graphql/project/container_repositories_spec.rb
index 7e32f54bf1d..6b1c8689515 100644
--- a/spec/requests/api/graphql/project/container_repositories_spec.rb
+++ b/spec/requests/api/graphql/project/container_repositories_spec.rb
@@ -12,7 +12,7 @@ RSpec.describe 'getting container repositories in a project' do
let_it_be(:container_repositories) { [container_repository, container_repositories_delete_scheduled, container_repositories_delete_failed].flatten }
let_it_be(:container_expiration_policy) { project.container_expiration_policy }
- let(:fields) do
+ let(:container_repositories_fields) do
<<~GQL
edges {
node {
@@ -22,17 +22,25 @@ RSpec.describe 'getting container repositories in a project' do
GQL
end
+ let(:fields) do
+ <<~GQL
+ #{query_graphql_field('container_repositories', {}, container_repositories_fields)}
+ containerRepositoriesCount
+ GQL
+ end
+
let(:query) do
graphql_query_for(
'project',
{ 'fullPath' => project.full_path },
- query_graphql_field('container_repositories', {}, fields)
+ fields
)
end
let(:user) { project.owner }
let(:variables) { {} }
let(:container_repositories_response) { graphql_data.dig('project', 'containerRepositories', 'edges') }
+ let(:container_repositories_count_response) { graphql_data.dig('project', 'containerRepositoriesCount') }
before do
stub_container_registry_config(enabled: true)
@@ -100,7 +108,7 @@ RSpec.describe 'getting container repositories in a project' do
<<~GQL
query($path: ID!, $n: Int) {
project(fullPath: $path) {
- containerRepositories(first: $n) { #{fields} }
+ containerRepositories(first: $n) { #{container_repositories_fields} }
}
}
GQL
@@ -121,7 +129,7 @@ RSpec.describe 'getting container repositories in a project' do
<<~GQL
query($path: ID!, $name: String) {
project(fullPath: $path) {
- containerRepositories(name: $name) { #{fields} }
+ containerRepositories(name: $name) { #{container_repositories_fields} }
}
}
GQL
@@ -142,4 +150,10 @@ RSpec.describe 'getting container repositories in a project' do
expect(container_repositories_response.first.dig('node', 'id')).to eq(container_repository.to_global_id.to_s)
end
end
+
+ it 'returns the total count of container repositories' do
+ subject
+
+ expect(container_repositories_count_response).to eq(container_repositories.size)
+ 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 4bce3c7fe0f..f544d78ecbb 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
@@ -42,7 +42,6 @@ RSpec.describe 'Query.project(fullPath).issue(iid).designCollection.version(sha)
describe 'scalar fields' do
let(:path) { path_prefix }
- let(:version_fields) { query_graphql_field(:sha) }
before do
post_query
@@ -50,7 +49,7 @@ RSpec.describe 'Query.project(fullPath).issue(iid).designCollection.version(sha)
{ id: ->(x) { x.to_global_id.to_s }, sha: ->(x) { x.sha } }.each do |field, value|
describe ".#{field}" do
- let(:version_fields) { query_graphql_field(field) }
+ let(:version_fields) { field }
it "retrieves the #{field}" do
expect(data).to match(a_hash_including(field.to_s => value[version]))
diff --git a/spec/requests/api/graphql/project/issue_spec.rb b/spec/requests/api/graphql/project/issue_spec.rb
index 5f368833181..ddf63a8f2c9 100644
--- a/spec/requests/api/graphql/project/issue_spec.rb
+++ b/spec/requests/api/graphql/project/issue_spec.rb
@@ -29,8 +29,8 @@ RSpec.describe 'Query.project(fullPath).issue(iid)' do
let(:design_fields) do
[
- query_graphql_field(:filename),
- query_graphql_field(:project, nil, query_graphql_field(:id))
+ :filename,
+ query_graphql_field(:project, :id)
]
end
@@ -173,7 +173,7 @@ RSpec.describe 'Query.project(fullPath).issue(iid)' do
let(:result_fields) { { 'version' => id_hash(version) } }
let(:object_fields) do
- design_fields + [query_graphql_field(:version, nil, query_graphql_field(:id))]
+ design_fields + [query_graphql_field(:version, :id)]
end
let(:no_argument_error) { missing_required_argument(path, :id) }
diff --git a/spec/requests/api/graphql/project/issues_spec.rb b/spec/requests/api/graphql/project/issues_spec.rb
index 4f27f08bf98..9c915075c42 100644
--- a/spec/requests/api/graphql/project/issues_spec.rb
+++ b/spec/requests/api/graphql/project/issues_spec.rb
@@ -142,16 +142,14 @@ RSpec.describe 'getting an issue list for a project' do
describe 'sorting and pagination' do
let_it_be(:data_path) { [:project, :issues] }
- def pagination_query(params, page_info)
- graphql_query_for(
- 'project',
- { 'fullPath' => sort_project.full_path },
- query_graphql_field('issues', params, "#{page_info} edges { node { iid dueDate} }")
+ def pagination_query(params)
+ graphql_query_for(:project, { full_path: sort_project.full_path },
+ query_graphql_field(:issues, params, "#{page_info} nodes { iid }")
)
end
def pagination_results_data(data)
- data.map { |issue| issue.dig('node', 'iid').to_i }
+ data.map { |issue| issue.dig('iid').to_i }
end
context 'when sorting by due date' do
@@ -164,7 +162,7 @@ RSpec.describe 'getting an issue list for a project' do
context 'when ascending' do
it_behaves_like 'sorted paginated query' do
- let(:sort_param) { 'DUE_DATE_ASC' }
+ let(:sort_param) { :DUE_DATE_ASC }
let(:first_param) { 2 }
let(:expected_results) { [due_issue3.iid, due_issue5.iid, due_issue1.iid, due_issue4.iid, due_issue2.iid] }
end
@@ -172,7 +170,7 @@ RSpec.describe 'getting an issue list for a project' do
context 'when descending' do
it_behaves_like 'sorted paginated query' do
- let(:sort_param) { 'DUE_DATE_DESC' }
+ let(:sort_param) { :DUE_DATE_DESC }
let(:first_param) { 2 }
let(:expected_results) { [due_issue1.iid, due_issue5.iid, due_issue3.iid, due_issue4.iid, due_issue2.iid] }
end
@@ -189,7 +187,7 @@ RSpec.describe 'getting an issue list for a project' do
context 'when ascending' do
it_behaves_like 'sorted paginated query' do
- let(:sort_param) { 'RELATIVE_POSITION_ASC' }
+ let(:sort_param) { :RELATIVE_POSITION_ASC }
let(:first_param) { 2 }
let(:expected_results) { [relative_issue5.iid, relative_issue3.iid, relative_issue1.iid, relative_issue4.iid, relative_issue2.iid] }
end
@@ -209,7 +207,7 @@ RSpec.describe 'getting an issue list for a project' do
context 'when ascending' do
it_behaves_like 'sorted paginated query' do
- let(:sort_param) { 'PRIORITY_ASC' }
+ let(:sort_param) { :PRIORITY_ASC }
let(:first_param) { 2 }
let(:expected_results) { [priority_issue3.iid, priority_issue1.iid, priority_issue2.iid, priority_issue4.iid] }
end
@@ -217,7 +215,7 @@ RSpec.describe 'getting an issue list for a project' do
context 'when descending' do
it_behaves_like 'sorted paginated query' do
- let(:sort_param) { 'PRIORITY_DESC' }
+ let(:sort_param) { :PRIORITY_DESC }
let(:first_param) { 2 }
let(:expected_results) { [priority_issue1.iid, priority_issue3.iid, priority_issue2.iid, priority_issue4.iid] }
end
@@ -236,7 +234,7 @@ RSpec.describe 'getting an issue list for a project' do
context 'when ascending' do
it_behaves_like 'sorted paginated query' do
- let(:sort_param) { 'LABEL_PRIORITY_ASC' }
+ let(:sort_param) { :LABEL_PRIORITY_ASC }
let(:first_param) { 2 }
let(:expected_results) { [label_issue3.iid, label_issue1.iid, label_issue2.iid, label_issue4.iid] }
end
@@ -244,7 +242,7 @@ RSpec.describe 'getting an issue list for a project' do
context 'when descending' do
it_behaves_like 'sorted paginated query' do
- let(:sort_param) { 'LABEL_PRIORITY_DESC' }
+ let(:sort_param) { :LABEL_PRIORITY_DESC }
let(:first_param) { 2 }
let(:expected_results) { [label_issue2.iid, label_issue3.iid, label_issue1.iid, label_issue4.iid] }
end
@@ -261,7 +259,7 @@ RSpec.describe 'getting an issue list for a project' do
context 'when ascending' do
it_behaves_like 'sorted paginated query' do
- let(:sort_param) { 'MILESTONE_DUE_ASC' }
+ let(:sort_param) { :MILESTONE_DUE_ASC }
let(:first_param) { 2 }
let(:expected_results) { [milestone_issue2.iid, milestone_issue3.iid, milestone_issue1.iid] }
end
@@ -269,7 +267,7 @@ RSpec.describe 'getting an issue list for a project' do
context 'when descending' do
it_behaves_like 'sorted paginated query' do
- let(:sort_param) { 'MILESTONE_DUE_DESC' }
+ let(:sort_param) { :MILESTONE_DUE_DESC }
let(:first_param) { 2 }
let(:expected_results) { [milestone_issue3.iid, milestone_issue2.iid, milestone_issue1.iid] }
end
diff --git a/spec/requests/api/graphql/project/merge_request/pipelines_spec.rb b/spec/requests/api/graphql/project/merge_request/pipelines_spec.rb
new file mode 100644
index 00000000000..ac0b18a37d6
--- /dev/null
+++ b/spec/requests/api/graphql/project/merge_request/pipelines_spec.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Query.project.mergeRequests.pipelines' do
+ include GraphqlHelpers
+
+ let_it_be(:project) { create(:project, :public, :repository) }
+ let_it_be(:author) { create(:user) }
+ let_it_be(:merge_requests) do
+ %i[with_diffs with_image_diffs conflict].map do |trait|
+ create(:merge_request, trait, author: author, source_project: project)
+ end
+ end
+
+ describe '.count' do
+ let(:query) do
+ <<~GQL
+ query($path: ID!, $first: Int) {
+ project(fullPath: $path) {
+ mergeRequests(first: $first) {
+ nodes {
+ iid
+ pipelines {
+ count
+ }
+ }
+ }
+ }
+ }
+ GQL
+ end
+
+ def run_query(first = nil)
+ post_graphql(query, current_user: author, variables: { path: project.full_path, first: first })
+ end
+
+ before do
+ merge_requests.each do |mr|
+ shas = mr.all_commits.limit(2).pluck(:sha)
+
+ shas.each do |sha|
+ create(:ci_pipeline, :success, project: project, ref: mr.source_branch, sha: sha)
+ end
+ end
+ end
+
+ it 'produces correct results' do
+ run_query(2)
+
+ p_nodes = graphql_data_at(:project, :merge_requests, :nodes)
+
+ expect(p_nodes).to all(match('iid' => be_present, 'pipelines' => match('count' => 2)))
+ end
+
+ it 'is scalable', :request_store, :use_clean_rails_memory_store_caching do
+ # warm up
+ run_query
+
+ expect { run_query(2) }.to(issue_same_number_of_queries_as { run_query(1) }.ignoring_cached_queries)
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/project/merge_requests_spec.rb b/spec/requests/api/graphql/project/merge_requests_spec.rb
index 2b8d537f9fc..c05a620bb62 100644
--- a/spec/requests/api/graphql/project/merge_requests_spec.rb
+++ b/spec/requests/api/graphql/project/merge_requests_spec.rb
@@ -259,29 +259,19 @@ RSpec.describe 'getting merge request listings nested in a project' do
describe 'sorting and pagination' do
let(:data_path) { [:project, :mergeRequests] }
- def pagination_query(params, page_info)
- graphql_query_for(
- :project,
- { full_path: project.full_path },
+ def pagination_query(params)
+ graphql_query_for(:project, { full_path: project.full_path },
<<~QUERY
mergeRequests(#{params}) {
- #{page_info} edges {
- node {
- id
- }
- }
+ #{page_info} nodes { id }
}
QUERY
)
end
- def pagination_results_data(data)
- data.map { |project| project.dig('node', 'id') }
- end
-
context 'when sorting by merged_at DESC' do
it_behaves_like 'sorted paginated query' do
- let(:sort_param) { 'MERGED_AT_DESC' }
+ let(:sort_param) { :MERGED_AT_DESC }
let(:first_param) { 2 }
let(:expected_results) do
@@ -291,7 +281,7 @@ RSpec.describe 'getting merge request listings nested in a project' do
merge_request_c,
merge_request_e,
merge_request_a
- ].map(&:to_gid).map(&:to_s)
+ ].map { |mr| global_id_of(mr) }
end
before do
@@ -304,33 +294,6 @@ RSpec.describe 'getting merge request listings nested in a project' do
merge_request_b.metrics.update!(merged_at: 1.day.ago)
end
-
- context 'when paginating backwards' do
- let(:params) { 'first: 2, sort: MERGED_AT_DESC' }
- let(:page_info) { 'pageInfo { startCursor endCursor }' }
-
- before do
- post_graphql(pagination_query(params, page_info), current_user: current_user)
- end
-
- it 'paginates backwards correctly' do
- # first page
- first_page_response_data = graphql_dig_at(Gitlab::Json.parse(response.body), :data, *data_path, :edges)
- end_cursor = graphql_dig_at(Gitlab::Json.parse(response.body), :data, :project, :mergeRequests, :pageInfo, :endCursor)
-
- # second page
- params = "first: 2, after: \"#{end_cursor}\", sort: MERGED_AT_DESC"
- post_graphql(pagination_query(params, page_info), current_user: current_user)
- start_cursor = graphql_dig_at(Gitlab::Json.parse(response.body), :data, :project, :mergeRequests, :pageInfo, :start_cursor)
-
- # going back to the first page
-
- params = "last: 2, before: \"#{start_cursor}\", sort: MERGED_AT_DESC"
- post_graphql(pagination_query(params, page_info), current_user: current_user)
- backward_paginated_response_data = graphql_dig_at(Gitlab::Json.parse(response.body), :data, *data_path, :edges)
- expect(first_page_response_data).to eq(backward_paginated_response_data)
- end
- end
end
end
end
diff --git a/spec/requests/api/graphql/project/pipeline_spec.rb b/spec/requests/api/graphql/project/pipeline_spec.rb
index fef0e7e160c..6179b43629b 100644
--- a/spec/requests/api/graphql/project/pipeline_spec.rb
+++ b/spec/requests/api/graphql/project/pipeline_spec.rb
@@ -5,12 +5,12 @@ require 'spec_helper'
RSpec.describe 'getting pipeline information nested in a project' do
include GraphqlHelpers
- let(:project) { create(:project, :repository, :public) }
- let(:pipeline) { create(:ci_pipeline, project: project) }
- let(:current_user) { create(:user) }
+ let!(:project) { create(:project, :repository, :public) }
+ let!(:pipeline) { create(:ci_pipeline, project: project) }
+ let!(:current_user) { create(:user) }
let(:pipeline_graphql_data) { graphql_data['project']['pipeline'] }
- let(:query) do
+ let!(:query) do
graphql_query_for(
'project',
{ 'fullPath' => project.full_path },
@@ -35,4 +35,45 @@ RSpec.describe 'getting pipeline information nested in a project' do
expect(pipeline_graphql_data.dig('configSource')).to eq('UNKNOWN_SOURCE')
end
+
+ context 'batching' do
+ let!(:pipeline2) { create(:ci_pipeline, project: project, user: current_user, builds: [create(:ci_build, :success)]) }
+ let!(:pipeline3) { create(:ci_pipeline, project: project, user: current_user, builds: [create(:ci_build, :success)]) }
+ let!(:query) { build_query_to_find_pipeline_shas(pipeline, pipeline2, pipeline3) }
+
+ it 'executes the finder once' do
+ mock = double(Ci::PipelinesFinder)
+ opts = { iids: [pipeline.iid, pipeline2.iid, pipeline3.iid].map(&:to_s) }
+
+ expect(Ci::PipelinesFinder).to receive(:new).once.with(project, current_user, opts).and_return(mock)
+ expect(mock).to receive(:execute).once.and_return(Ci::Pipeline.none)
+
+ post_graphql(query, current_user: current_user)
+ end
+
+ it 'keeps the queries under the threshold' do
+ control = ActiveRecord::QueryRecorder.new do
+ single_pipeline_query = build_query_to_find_pipeline_shas(pipeline)
+
+ post_graphql(single_pipeline_query, current_user: current_user)
+ end
+
+ aggregate_failures do
+ expect(response).to have_gitlab_http_status(:success)
+ expect do
+ post_graphql(query, current_user: current_user)
+ end.not_to exceed_query_limit(control)
+ end
+ end
+ end
+
+ private
+
+ def build_query_to_find_pipeline_shas(*pipelines)
+ pipeline_fields = pipelines.map.each_with_index do |pipeline, idx|
+ "pipeline#{idx}: pipeline(iid: \"#{pipeline.iid}\") { sha }"
+ end.join(' ')
+
+ graphql_query_for('project', { 'fullPath' => project.full_path }, pipeline_fields)
+ end
end
diff --git a/spec/requests/api/graphql/project/project_members_spec.rb b/spec/requests/api/graphql/project/project_members_spec.rb
new file mode 100644
index 00000000000..cb937432ef7
--- /dev/null
+++ b/spec/requests/api/graphql/project/project_members_spec.rb
@@ -0,0 +1,121 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'getting project members information' do
+ include GraphqlHelpers
+
+ let_it_be(:parent_group) { create(:group, :public) }
+ let_it_be(:parent_project) { create(:project, :public, group: parent_group) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:user_1) { create(:user, username: 'user') }
+ let_it_be(:user_2) { create(:user, username: 'test') }
+
+ let(:member_data) { graphql_data['project']['projectMembers']['edges'] }
+
+ before_all do
+ [user_1, user_2].each { |user| parent_group.add_guest(user) }
+ end
+
+ context 'when the request is correct' do
+ it_behaves_like 'a working graphql query' do
+ before_all do
+ fetch_members(project: parent_project)
+ end
+ end
+
+ it 'returns project members successfully' do
+ fetch_members(project: parent_project)
+
+ expect(graphql_errors).to be_nil
+ expect_array_response(user_1, user_2)
+ end
+
+ it 'returns members that match the search query' do
+ fetch_members(project: parent_project, args: { search: 'test' })
+
+ expect(graphql_errors).to be_nil
+ expect_array_response(user_2)
+ end
+ end
+
+ context 'member relations' do
+ let_it_be(:child_group) { create(:group, :public, parent: parent_group) }
+ let_it_be(:child_project) { create(:project, :public, group: child_group) }
+ let_it_be(:invited_group) { create(:group, :public) }
+ let_it_be(:child_user) { create(:user) }
+ let_it_be(:invited_user) { create(:user) }
+ let_it_be(:group_link) { create(:project_group_link, project: child_project, group: invited_group) }
+
+ before_all do
+ child_project.add_guest(child_user)
+ invited_group.add_guest(invited_user)
+ end
+
+ it 'returns direct members' do
+ fetch_members(project: child_project, args: { relations: [:DIRECT] })
+
+ expect(graphql_errors).to be_nil
+ expect_array_response(child_user)
+ end
+
+ it 'returns invited members plus inherited members' do
+ fetch_members(project: child_project, args: { relations: [:INVITED_GROUPS] })
+
+ expect(graphql_errors).to be_nil
+ expect_array_response(invited_user, user_1, user_2)
+ end
+
+ it 'returns direct, inherited, descendant, and invited members' do
+ fetch_members(project: child_project, args: { relations: [:DIRECT, :INHERITED, :DESCENDANTS, :INVITED_GROUPS] })
+
+ expect(graphql_errors).to be_nil
+ expect_array_response(child_user, user_1, user_2, invited_user)
+ end
+
+ it 'returns an error for an invalid member relation' do
+ fetch_members(project: child_project, args: { relations: [:OBLIQUE] })
+
+ expect(graphql_errors.first)
+ .to include('path' => %w[query project projectMembers relations],
+ 'message' => a_string_including('invalid value ([OBLIQUE])'))
+ end
+ end
+
+ context 'when unauthenticated' do
+ it 'returns members' do
+ fetch_members(current_user: nil, project: parent_project)
+
+ expect(graphql_errors).to be_nil
+ expect_array_response(user_1, user_2)
+ end
+ end
+
+ def fetch_members(project:, current_user: user, args: {})
+ post_graphql(members_query(project.full_path, args), current_user: current_user)
+ end
+
+ def members_query(group_path, args = {})
+ members_node = <<~NODE
+ edges {
+ node {
+ user {
+ id
+ }
+ }
+ }
+ NODE
+
+ graphql_query_for('project',
+ { full_path: group_path },
+ [query_graphql_field('projectMembers', args, members_node)]
+ )
+ end
+
+ def expect_array_response(*items)
+ expect(response).to have_gitlab_http_status(:success)
+ expect(member_data).to be_an Array
+ expect(member_data.map { |node| node['node']['user']['id'] })
+ .to match_array(items.map { |u| global_id_of(u) })
+ end
+end
diff --git a/spec/requests/api/graphql/project/project_pipeline_statistics_spec.rb b/spec/requests/api/graphql/project/project_pipeline_statistics_spec.rb
new file mode 100644
index 00000000000..0f495f3e671
--- /dev/null
+++ b/spec/requests/api/graphql/project/project_pipeline_statistics_spec.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'rendering project pipeline statistics' do
+ include GraphqlHelpers
+
+ let_it_be(:project) { create(:project) }
+ let(:user) { create(:user) }
+
+ let(:fields) do
+ <<~QUERY
+ weekPipelinesTotals
+ weekPipelinesLabels
+ monthPipelinesLabels
+ monthPipelinesTotals
+ yearPipelinesLabels
+ yearPipelinesTotals
+ QUERY
+ end
+
+ let(:query) do
+ graphql_query_for('project',
+ { 'fullPath' => project.full_path },
+ query_graphql_field('pipelineAnalytics', {}, fields))
+ end
+
+ before do
+ project.add_maintainer(user)
+ end
+
+ it_behaves_like 'a working graphql query' do
+ before do
+ post_graphql(query, current_user: user)
+ end
+ end
+
+ it "contains two arrays of 8 elements each for the week pipelines" do
+ post_graphql(query, current_user: user)
+
+ expect(graphql_data_at(:project, :pipelineAnalytics, :weekPipelinesTotals).length).to eq(8)
+ expect(graphql_data_at(:project, :pipelineAnalytics, :weekPipelinesLabels).length).to eq(8)
+ end
+
+ it "contains two arrays of 31 elements each for the month pipelines" do
+ post_graphql(query, current_user: user)
+
+ expect(graphql_data_at(:project, :pipelineAnalytics, :monthPipelinesTotals).length).to eq(31)
+ expect(graphql_data_at(:project, :pipelineAnalytics, :monthPipelinesLabels).length).to eq(31)
+ end
+
+ it "contains two arrays of 13 elements each for the year pipelines" do
+ post_graphql(query, current_user: user)
+
+ expect(graphql_data_at(:project, :pipelineAnalytics, :yearPipelinesTotals).length).to eq(13)
+ expect(graphql_data_at(:project, :pipelineAnalytics, :yearPipelinesLabels).length).to eq(13)
+ end
+end
diff --git a/spec/requests/api/graphql/project/release_spec.rb b/spec/requests/api/graphql/project/release_spec.rb
index 57dbe258ce4..99b15ff00b1 100644
--- a/spec/requests/api/graphql/project/release_spec.rb
+++ b/spec/requests/api/graphql/project/release_spec.rb
@@ -36,7 +36,7 @@ RSpec.describe 'Query.project(fullPath).release(tagName)' do
let(:path) { path_prefix }
let(:release_fields) do
- query_graphql_field(%{
+ %{
tagName
tagPath
description
@@ -45,7 +45,7 @@ RSpec.describe 'Query.project(fullPath).release(tagName)' do
createdAt
releasedAt
upcomingRelease
- })
+ }
end
before do
@@ -233,7 +233,7 @@ RSpec.describe 'Query.project(fullPath).release(tagName)' do
let(:path) { path_prefix }
let(:release_fields) do
- query_graphql_field('description')
+ 'description'
end
before do
@@ -394,10 +394,10 @@ RSpec.describe 'Query.project(fullPath).release(tagName)' do
let(:current_user) { developer }
let(:release_fields) do
- query_graphql_field(%{
+ %{
releasedAt
upcomingRelease
- })
+ }
end
before do
diff --git a/spec/requests/api/graphql/project/terraform/states_spec.rb b/spec/requests/api/graphql/project/terraform/states_spec.rb
index 8b67b549efa..2879530acc5 100644
--- a/spec/requests/api/graphql/project/terraform/states_spec.rb
+++ b/spec/requests/api/graphql/project/terraform/states_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
RSpec.describe 'query terraform states' do
include GraphqlHelpers
+ include ::API::Helpers::RelatedResourcesHelpers
let_it_be(:project) { create(:project) }
let_it_be(:terraform_state) { create(:terraform_state, :with_version, :locked, project: project) }
@@ -23,6 +24,8 @@ RSpec.describe 'query terraform states' do
latestVersion {
id
+ downloadPath
+ serial
createdAt
updatedAt
@@ -50,22 +53,32 @@ RSpec.describe 'query terraform states' do
post_graphql(query, current_user: current_user)
end
- it 'returns terraform state data', :aggregate_failures do
- state = data.dig('nodes', 0)
- version = state['latestVersion']
-
- expect(state['id']).to eq(terraform_state.to_global_id.to_s)
- expect(state['name']).to eq(terraform_state.name)
- expect(state['lockedAt']).to eq(terraform_state.locked_at.iso8601)
- expect(state['createdAt']).to eq(terraform_state.created_at.iso8601)
- expect(state['updatedAt']).to eq(terraform_state.updated_at.iso8601)
- expect(state.dig('lockedByUser', 'id')).to eq(terraform_state.locked_by_user.to_global_id.to_s)
-
- expect(version['id']).to eq(latest_version.to_global_id.to_s)
- expect(version['createdAt']).to eq(latest_version.created_at.iso8601)
- expect(version['updatedAt']).to eq(latest_version.updated_at.iso8601)
- expect(version.dig('createdByUser', 'id')).to eq(latest_version.created_by_user.to_global_id.to_s)
- expect(version.dig('job', 'name')).to eq(latest_version.build.name)
+ it 'returns terraform state data' do
+ download_path = expose_path(
+ api_v4_projects_terraform_state_versions_path(
+ id: project.id,
+ name: terraform_state.name,
+ serial: latest_version.version
+ )
+ )
+
+ expect(data['nodes']).to contain_exactly({
+ 'id' => global_id_of(terraform_state),
+ 'name' => terraform_state.name,
+ 'lockedAt' => terraform_state.locked_at.iso8601,
+ 'createdAt' => terraform_state.created_at.iso8601,
+ 'updatedAt' => terraform_state.updated_at.iso8601,
+ 'lockedByUser' => { 'id' => global_id_of(terraform_state.locked_by_user) },
+ 'latestVersion' => {
+ 'id' => eq(global_id_of(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),
+ 'createdByUser' => { 'id' => eq(global_id_of(latest_version.created_by_user)) },
+ 'job' => { 'name' => eq(latest_version.build.name) }
+ }
+ })
end
it 'returns count of terraform states' do
diff --git a/spec/requests/api/graphql/project_query_spec.rb b/spec/requests/api/graphql/project_query_spec.rb
index 4b8ffb0675c..b29f9ae913f 100644
--- a/spec/requests/api/graphql/project_query_spec.rb
+++ b/spec/requests/api/graphql/project_query_spec.rb
@@ -5,36 +5,34 @@ require 'spec_helper'
RSpec.describe 'getting project information' do
include GraphqlHelpers
- let(:group) { create(:group) }
- let(:project) { create(:project, :repository, group: group) }
- let(:current_user) { create(:user) }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, :repository, group: group) }
+ let_it_be(:current_user) { create(:user) }
+ let(:fields) { all_graphql_fields_for(Project, max_depth: 2, excluded: %w(jiraImports services)) }
let(:query) do
- graphql_query_for(
- 'project',
- { 'fullPath' => project.full_path },
- all_graphql_fields_for('project'.to_s.classify, excluded: %w(jiraImports services))
- )
+ graphql_query_for(:project, { full_path: project.full_path }, fields)
end
context 'when the user has full access to the project' do
let(:full_access_query) do
- graphql_query_for('project', 'fullPath' => project.full_path)
+ graphql_query_for(:project, { full_path: project.full_path },
+ all_graphql_fields_for('Project', max_depth: 2))
end
before do
project.add_maintainer(current_user)
end
- it 'includes the project' do
- post_graphql(query, current_user: current_user)
+ it 'includes the project', :use_clean_rails_memory_store_caching, :request_store do
+ post_graphql(full_access_query, current_user: current_user)
expect(graphql_data['project']).not_to be_nil
end
end
- context 'when the user has access to the project' do
- before do
+ context 'when the user has access to the project', :use_clean_rails_memory_store_caching, :request_store do
+ before_all do
project.add_developer(current_user)
end
@@ -55,10 +53,12 @@ RSpec.describe 'getting project information' do
create(:ci_pipeline, project: project)
end
+ let(:fields) { query_nodes(:pipelines) }
+
it 'is included in the pipelines connection' do
post_graphql(query, current_user: current_user)
- expect(graphql_data['project']['pipelines']['edges'].size).to eq(1)
+ expect(graphql_data_at(:project, :pipelines, :nodes)).to contain_exactly(a_kind_of(Hash))
end
end
@@ -109,7 +109,7 @@ RSpec.describe 'getting project information' do
end
describe 'performance' do
- before do
+ before_all do
project.add_developer(current_user)
mrs = create_list(:merge_request, 10, :closed, :with_head_pipeline,
source_project: project,
@@ -151,8 +151,9 @@ RSpec.describe 'getting project information' do
)))
end
- it 'can lookahead to eliminate N+1 queries', :use_clean_rails_memory_store_caching, :request_store do
- expect { run_query(10) }.to issue_same_number_of_queries_as { run_query(1) }.or_fewer.ignoring_cached_queries
+ it 'can lookahead to eliminate N+1 queries' do
+ baseline = ActiveRecord::QueryRecorder.new { run_query(1) }
+ expect { run_query(10) }.not_to exceed_query_limit(baseline)
end
end
diff --git a/spec/requests/api/graphql/user_query_spec.rb b/spec/requests/api/graphql/user_query_spec.rb
index 8c45a67cb0f..60520906e87 100644
--- a/spec/requests/api/graphql/user_query_spec.rb
+++ b/spec/requests/api/graphql/user_query_spec.rb
@@ -58,9 +58,25 @@ RSpec.describe 'getting user information' do
source_project: project_b, author: user)
end
+ let_it_be(:reviewed_mr) do
+ create(:merge_request, :unique_branches, :unique_author,
+ source_project: project_a, reviewers: [user])
+ end
+
+ let_it_be(:reviewed_mr_b) do
+ create(:merge_request, :unique_branches, :unique_author,
+ source_project: project_b, reviewers: [user])
+ end
+
+ let_it_be(:reviewed_mr_c) do
+ create(:merge_request, :unique_branches, :unique_author,
+ source_project: project_b, reviewers: [user])
+ end
+
let(:current_user) { authorised_user }
let(:authored_mrs) { graphql_data_at(:user, :authored_merge_requests, :nodes) }
let(:assigned_mrs) { graphql_data_at(:user, :assigned_merge_requests, :nodes) }
+ let(:reviewed_mrs) { graphql_data_at(:user, :review_requested_merge_requests, :nodes) }
let(:user_params) { { username: user.username } }
before do
@@ -82,7 +98,8 @@ RSpec.describe 'getting user information' do
'username' => presenter.username,
'webUrl' => presenter.web_url,
'avatarUrl' => presenter.avatar_url,
- 'email' => presenter.public_email
+ 'email' => presenter.public_email,
+ 'publicEmail' => presenter.public_email
))
expect(graphql_data['user']['status']).to match(
@@ -156,6 +173,23 @@ RSpec.describe 'getting user information' do
)
end
end
+
+ context 'filtering by reviewer' do
+ let(:reviewer) { create(:user) }
+ let(:mr_args) { { reviewer_username: reviewer.username } }
+
+ it 'finds the assigned mrs' do
+ assigned_mr_b.reviewers << reviewer
+ assigned_mr_c.reviewers << reviewer
+
+ post_graphql(query, current_user: current_user)
+
+ expect(assigned_mrs).to contain_exactly(
+ a_hash_including('id' => global_id_of(assigned_mr_b)),
+ a_hash_including('id' => global_id_of(assigned_mr_c))
+ )
+ end
+ end
end
context 'the current user does not have access' do
@@ -167,6 +201,95 @@ RSpec.describe 'getting user information' do
end
end
+ describe 'reviewRequestedMergeRequests' do
+ let(:user_fields) do
+ query_graphql_field(:review_requested_merge_requests, mr_args, 'nodes { id }')
+ end
+
+ let(:mr_args) { nil }
+
+ it_behaves_like 'a working graphql query'
+
+ it 'can be found' do
+ expect(reviewed_mrs).to contain_exactly(
+ a_hash_including('id' => global_id_of(reviewed_mr)),
+ a_hash_including('id' => global_id_of(reviewed_mr_b)),
+ a_hash_including('id' => global_id_of(reviewed_mr_c))
+ )
+ end
+
+ context 'applying filters' do
+ context 'filtering by IID without specifying a project' do
+ let(:mr_args) do
+ { iids: [reviewed_mr_b.iid.to_s] }
+ end
+
+ it 'return an argument error that mentions the missing fields' do
+ expect_graphql_errors_to_include(/projectPath/)
+ end
+ end
+
+ context 'filtering by project path and IID' do
+ let(:mr_args) do
+ { project_path: project_b.full_path, iids: [reviewed_mr_b.iid.to_s] }
+ end
+
+ it 'selects the correct MRs' do
+ expect(reviewed_mrs).to contain_exactly(
+ a_hash_including('id' => global_id_of(reviewed_mr_b))
+ )
+ end
+ end
+
+ context 'filtering by project path' do
+ let(:mr_args) do
+ { project_path: project_b.full_path }
+ end
+
+ it 'selects the correct MRs' do
+ expect(reviewed_mrs).to contain_exactly(
+ a_hash_including('id' => global_id_of(reviewed_mr_b)),
+ a_hash_including('id' => global_id_of(reviewed_mr_c))
+ )
+ end
+ end
+
+ context 'filtering by author' do
+ let(:author) { reviewed_mr_b.author }
+ let(:mr_args) { { author_username: author.username } }
+
+ it 'finds the authored mrs' do
+ expect(reviewed_mrs).to contain_exactly(
+ a_hash_including('id' => global_id_of(reviewed_mr_b))
+ )
+ end
+ end
+
+ context 'filtering by assignee' do
+ let(:assignee) { create(:user) }
+ let(:mr_args) { { assignee_username: assignee.username } }
+
+ it 'finds the authored mrs' do
+ reviewed_mr_c.assignees << assignee
+
+ post_graphql(query, current_user: current_user)
+
+ expect(reviewed_mrs).to contain_exactly(
+ a_hash_including('id' => global_id_of(reviewed_mr_c))
+ )
+ end
+ end
+ end
+
+ context 'the current user does not have access' do
+ let(:current_user) { unauthorized_user }
+
+ it 'cannot be found' do
+ expect(reviewed_mrs).to be_empty
+ end
+ end
+ end
+
describe 'authoredMergeRequests' do
let(:user_fields) do
query_graphql_field(:authored_merge_requests, mr_args, 'nodes { id }')
@@ -212,6 +335,23 @@ RSpec.describe 'getting user information' do
end
end
+ context 'filtering by reviewer' do
+ let(:reviewer) { create(:user) }
+ let(:mr_args) { { reviewer_username: reviewer.username } }
+
+ it 'finds the assigned mrs' do
+ authored_mr_b.reviewers << reviewer
+ authored_mr_c.reviewers << reviewer
+
+ post_graphql(query, current_user: current_user)
+
+ expect(authored_mrs).to contain_exactly(
+ a_hash_including('id' => global_id_of(authored_mr_b)),
+ a_hash_including('id' => global_id_of(authored_mr_c))
+ )
+ end
+ end
+
context 'filtering by project path and IID' do
let(:mr_args) do
{ project_path: project_b.full_path, iids: [authored_mr_b.iid.to_s] }
diff --git a/spec/requests/api/graphql/users_spec.rb b/spec/requests/api/graphql/users_spec.rb
index 91ac206676b..72d86c10df1 100644
--- a/spec/requests/api/graphql/users_spec.rb
+++ b/spec/requests/api/graphql/users_spec.rb
@@ -59,20 +59,16 @@ RSpec.describe 'Users' do
describe 'sorting and pagination' do
let_it_be(:data_path) { [:users] }
- def pagination_query(params, page_info)
- graphql_query_for("users", params, "#{page_info} edges { node { id } }")
- end
-
- def pagination_results_data(data)
- data.map { |user| user.dig('node', 'id') }
+ def pagination_query(params)
+ graphql_query_for(:users, params, "#{page_info} nodes { id }")
end
context 'when sorting by created_at' do
- let_it_be(:ascending_users) { [user3, user2, user1, current_user].map(&:to_global_id).map(&:to_s) }
+ let_it_be(:ascending_users) { [user3, user2, user1, current_user].map { |u| global_id_of(u) } }
context 'when ascending' do
it_behaves_like 'sorted paginated query' do
- let(:sort_param) { 'created_asc' }
+ let(:sort_param) { :CREATED_ASC }
let(:first_param) { 1 }
let(:expected_results) { ascending_users }
end
@@ -80,7 +76,7 @@ RSpec.describe 'Users' do
context 'when descending' do
it_behaves_like 'sorted paginated query' do
- let(:sort_param) { 'created_desc' }
+ let(:sort_param) { :CREATED_DESC }
let(:first_param) { 1 }
let(:expected_results) { ascending_users.reverse }
end
diff --git a/spec/requests/api/graphql_spec.rb b/spec/requests/api/graphql_spec.rb
index 5dc8edb87e9..4eaf57a7d35 100644
--- a/spec/requests/api/graphql_spec.rb
+++ b/spec/requests/api/graphql_spec.rb
@@ -105,7 +105,7 @@ RSpec.describe 'GraphQL' do
stub_authentication_activity_metrics(debug: false)
end
- it 'Authenticates users with a PAT' do
+ it 'authenticates users with a PAT' do
expect(authentication_metrics)
.to increment(:user_authenticated_counter)
.and increment(:user_session_override_counter)
diff --git a/spec/requests/api/group_clusters_spec.rb b/spec/requests/api/group_clusters_spec.rb
index eb21ae9468c..f65f9384efa 100644
--- a/spec/requests/api/group_clusters_spec.rb
+++ b/spec/requests/api/group_clusters_spec.rb
@@ -89,6 +89,8 @@ RSpec.describe API::GroupClusters do
expect(json_response['environment_scope']).to eq('*')
expect(json_response['cluster_type']).to eq('group_type')
expect(json_response['domain']).to eq('example.com')
+ expect(json_response['enabled']).to be_truthy
+ expect(json_response['managed']).to be_truthy
end
it 'returns group information' do
@@ -172,6 +174,7 @@ RSpec.describe API::GroupClusters do
name: 'test-cluster',
domain: 'domain.example.com',
managed: false,
+ enabled: false,
namespace_per_environment: false,
platform_kubernetes_attributes: platform_kubernetes_attributes,
management_project_id: management_project_id
@@ -206,6 +209,7 @@ RSpec.describe API::GroupClusters do
expect(cluster_result.name).to eq('test-cluster')
expect(cluster_result.domain).to eq('domain.example.com')
expect(cluster_result.managed).to be_falsy
+ expect(cluster_result.enabled).to be_falsy
expect(cluster_result.management_project_id).to eq management_project_id
expect(cluster_result.namespace_per_environment).to eq(false)
expect(platform_kubernetes.rbac?).to be_truthy
@@ -342,7 +346,9 @@ RSpec.describe API::GroupClusters do
{
domain: domain,
platform_kubernetes_attributes: platform_kubernetes_attributes,
- management_project_id: management_project_id
+ management_project_id: management_project_id,
+ managed: false,
+ enabled: false
}
end
@@ -381,6 +387,8 @@ RSpec.describe API::GroupClusters do
it 'updates cluster attributes' do
expect(cluster.domain).to eq('new-domain.com')
expect(cluster.management_project).to eq(management_project)
+ expect(cluster.managed).to be_falsy
+ expect(cluster.enabled).to be_falsy
end
end
@@ -394,6 +402,8 @@ RSpec.describe API::GroupClusters do
it 'does not update cluster attributes' do
expect(cluster.domain).to eq('old-domain.com')
expect(cluster.management_project).to be_nil
+ expect(cluster.managed).to be_truthy
+ expect(cluster.enabled).to be_truthy
end
it 'returns validation errors' do
diff --git a/spec/requests/api/group_import_spec.rb b/spec/requests/api/group_import_spec.rb
index cb63206fcb8..d8e945baf6a 100644
--- a/spec/requests/api/group_import_spec.rb
+++ b/spec/requests/api/group_import_spec.rb
@@ -264,7 +264,7 @@ RSpec.describe API::GroupImport do
subject
expect(response).to have_gitlab_http_status(:ok)
- expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
+ expect(response.media_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
end
it 'rejects requests that bypassed gitlab-workhorse' do
@@ -285,7 +285,7 @@ RSpec.describe API::GroupImport do
subject
expect(response).to have_gitlab_http_status(:ok)
- expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
+ expect(response.media_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
expect(json_response).not_to have_key('TempPath')
expect(json_response['RemoteObject']).to have_key('ID')
expect(json_response['RemoteObject']).to have_key('GetURL')
@@ -304,7 +304,7 @@ RSpec.describe API::GroupImport do
subject
expect(response).to have_gitlab_http_status(:ok)
- expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
+ expect(response.media_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
expect(json_response['TempPath']).to eq(ImportExportUploader.workhorse_local_upload_path)
expect(json_response['RemoteObject']).to be_nil
end
diff --git a/spec/requests/api/import_github_spec.rb b/spec/requests/api/import_github_spec.rb
index 5bbcb0c1950..d5fed330401 100644
--- a/spec/requests/api/import_github_spec.rb
+++ b/spec/requests/api/import_github_spec.rb
@@ -44,7 +44,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, access_params, type: provider)
+ .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: {
@@ -59,7 +59,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, access_params, type: provider)
+ .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/internal/base_spec.rb b/spec/requests/api/internal/base_spec.rb
index 6fe77727702..e04f63befd0 100644
--- a/spec/requests/api/internal/base_spec.rb
+++ b/spec/requests/api/internal/base_spec.rb
@@ -220,6 +220,8 @@ RSpec.describe API::Internal::Base do
end
it 'returns a token without expiry when the expires_at parameter is missing' do
+ token_size = (PersonalAccessToken.token_prefix || '').size + 20
+
post api('/internal/personal_access_token'),
params: {
secret_token: secret_token,
@@ -229,12 +231,14 @@ RSpec.describe API::Internal::Base do
}
expect(json_response['success']).to be_truthy
- expect(json_response['token']).to match(/\A\S{20}\z/)
+ expect(json_response['token']).to match(/\A\S{#{token_size}}\z/)
expect(json_response['scopes']).to match_array(%w(read_api read_repository))
expect(json_response['expires_at']).to be_nil
end
it 'returns a token with expiry when it receives a valid expires_at parameter' do
+ token_size = (PersonalAccessToken.token_prefix || '').size + 20
+
post api('/internal/personal_access_token'),
params: {
secret_token: secret_token,
@@ -245,7 +249,7 @@ RSpec.describe API::Internal::Base do
}
expect(json_response['success']).to be_truthy
- expect(json_response['token']).to match(/\A\S{20}\z/)
+ expect(json_response['token']).to match(/\A\S{#{token_size}}\z/)
expect(json_response['scopes']).to match_array(%w(read_api read_repository))
expect(json_response['expires_at']).to eq('9001-11-17')
end
@@ -722,8 +726,7 @@ RSpec.describe API::Internal::Base do
'ssh',
{
authentication_abilities: [:read_project, :download_code, :push_code],
- namespace_path: project.namespace.path,
- repository_path: project.path,
+ repository_path: "#{project.full_path}.git",
redirected_path: nil
}
).and_return(access_checker)
@@ -1337,9 +1340,13 @@ RSpec.describe API::Internal::Base do
end
context 'when the OTP is valid' do
- it 'returns success' do
+ it 'registers a new OTP session and returns success' do
allow_any_instance_of(Users::ValidateOtpService).to receive(:execute).with(otp).and_return(status: :success)
+ expect_next_instance_of(::Gitlab::Auth::Otp::SessionEnforcer) do |session_enforcer|
+ expect(session_enforcer).to receive(:update_session).once
+ end
+
subject
expect(json_response['success']).to be_truthy
diff --git a/spec/requests/api/internal/kubernetes_spec.rb b/spec/requests/api/internal/kubernetes_spec.rb
index a532b8e59f2..afff3647b91 100644
--- a/spec/requests/api/internal/kubernetes_spec.rb
+++ b/spec/requests/api/internal/kubernetes_spec.rb
@@ -87,7 +87,7 @@ RSpec.describe API::Internal::Kubernetes do
end
end
- describe "GET /internal/kubernetes/agent_info" do
+ describe 'GET /internal/kubernetes/agent_info' do
def send_request(headers: {}, params: {})
get api('/internal/kubernetes/agent_info'), params: params, headers: headers.reverse_merge(jwt_auth_headers)
end
@@ -137,9 +137,7 @@ RSpec.describe API::Internal::Kubernetes do
include_examples 'agent authentication'
context 'an agent is found' do
- let!(:agent_token) { create(:cluster_agent_token) }
-
- let(:agent) { agent_token.agent }
+ let_it_be(:agent_token) { create(:cluster_agent_token) }
context 'project is public' do
let(:project) { create(:project, :public) }
@@ -186,6 +184,16 @@ RSpec.describe API::Internal::Kubernetes do
expect(response).to have_gitlab_http_status(:not_found)
end
+
+ context 'and agent belongs to project' do
+ let(:agent_token) { create(:cluster_agent_token, agent: create(:cluster_agent, project: project)) }
+
+ it 'returns 200' do
+ send_request(params: { id: project.id }, headers: { 'Authorization' => "Bearer #{agent_token.token}" })
+
+ expect(response).to have_gitlab_http_status(:success)
+ end
+ end
end
context 'project is internal' do
diff --git a/spec/requests/api/internal/pages_spec.rb b/spec/requests/api/internal/pages_spec.rb
index 9a63e2a8ed5..5b970ca605c 100644
--- a/spec/requests/api/internal/pages_spec.rb
+++ b/spec/requests/api/internal/pages_spec.rb
@@ -128,32 +128,38 @@ RSpec.describe API::Internal::Pages do
)
end
- it 'responds with proxy configuration' do
+ it 'responds with 204 because of feature deprecation' do
query_host(serverless_domain.uri.host)
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to match_response_schema('internal/serverless/virtual_domain')
-
- expect(json_response['certificate']).to eq(pages_domain.certificate)
- expect(json_response['key']).to eq(pages_domain.key)
+ expect(response).to have_gitlab_http_status(:no_content)
+ expect(response.body).to be_empty
- expect(json_response['lookup_paths']).to eq(
- [
- {
- 'source' => {
- 'type' => 'serverless',
- 'service' => "test-function.#{project.name}-#{project.id}-#{environment.slug}.#{serverless_domain_cluster.knative.hostname}",
- 'cluster' => {
- 'hostname' => serverless_domain_cluster.knative.hostname,
- 'address' => serverless_domain_cluster.knative.external_ip,
- 'port' => 443,
- 'cert' => serverless_domain_cluster.certificate,
- 'key' => serverless_domain_cluster.key
- }
- }
- }
- ]
- )
+ ##
+ # Serverless serving and reverse proxy to Kubernetes / Knative has
+ # been deprecated and disabled, as per
+ # https://gitlab.com/gitlab-org/gitlab-pages/-/issues/467
+ #
+ # expect(response).to match_response_schema('internal/serverless/virtual_domain')
+ # expect(json_response['certificate']).to eq(pages_domain.certificate)
+ # expect(json_response['key']).to eq(pages_domain.key)
+ #
+ # expect(json_response['lookup_paths']).to eq(
+ # [
+ # {
+ # 'source' => {
+ # 'type' => 'serverless',
+ # 'service' => "test-function.#{project.name}-#{project.id}-#{environment.slug}.#{serverless_domain_cluster.knative.hostname}",
+ # 'cluster' => {
+ # 'hostname' => serverless_domain_cluster.knative.hostname,
+ # 'address' => serverless_domain_cluster.knative.external_ip,
+ # 'port' => 443,
+ # 'cert' => serverless_domain_cluster.certificate,
+ # 'key' => serverless_domain_cluster.key
+ # }
+ # }
+ # }
+ # ]
+ # )
end
end
end
diff --git a/spec/requests/api/jobs_spec.rb b/spec/requests/api/jobs_spec.rb
index c1498e03f76..f8521818845 100644
--- a/spec/requests/api/jobs_spec.rb
+++ b/spec/requests/api/jobs_spec.rb
@@ -3,6 +3,7 @@
require 'spec_helper'
RSpec.describe API::Jobs do
+ using RSpec::Parameterized::TableSyntax
include HttpIOHelpers
let_it_be(:project, reload: true) do
@@ -717,6 +718,58 @@ RSpec.describe API::Jobs do
expect(response).to have_gitlab_http_status(:unauthorized)
end
end
+
+ context 'when ci_debug_trace is set to true' do
+ before_all do
+ create(:ci_instance_variable, key: 'CI_DEBUG_TRACE', value: true)
+ end
+
+ where(:public_builds, :user_project_role, :expected_status) do
+ true | 'developer' | :ok
+ true | 'guest' | :forbidden
+ false | 'developer' | :ok
+ false | 'guest' | :forbidden
+ end
+
+ with_them do
+ before do
+ project.update!(public_builds: public_builds)
+ project.add_role(user, user_project_role)
+
+ get api("/projects/#{project.id}/jobs/#{job.id}/trace", api_user)
+ end
+
+ it 'renders trace to authorized users' do
+ expect(response).to have_gitlab_http_status(expected_status)
+ end
+ end
+
+ context 'with restrict_access_to_build_debug_mode feature disabled' do
+ before do
+ stub_feature_flags(restrict_access_to_build_debug_mode: false)
+ end
+
+ where(:public_builds, :user_project_role, :expected_status) do
+ true | 'developer' | :ok
+ true | 'guest' | :ok
+ false | 'developer' | :ok
+ false | 'guest' | :forbidden
+ end
+
+ with_them do
+ before do
+ project.update!(public_builds: public_builds)
+ project.add_role(user, user_project_role)
+
+ get api("/projects/#{project.id}/jobs/#{job.id}/trace", api_user)
+ end
+
+ it 'renders trace to authorized users' do
+ expect(response).to have_gitlab_http_status(expected_status)
+ end
+ end
+ end
+ end
end
describe 'POST /projects/:id/jobs/:job_id/cancel' do
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index e7005bd3ec5..4339f1dd830 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -1888,6 +1888,54 @@ RSpec.describe API::MergeRequests do
expect(response).to have_gitlab_http_status(:created)
end
end
+
+ describe 'SSE counter' do
+ let(:headers) { {} }
+ let(:params) do
+ {
+ title: 'Test merge_request',
+ source_branch: 'feature_conflict',
+ target_branch: 'master',
+ author_id: user.id,
+ milestone_id: milestone.id,
+ squash: true
+ }
+ end
+
+ subject { post api("/projects/#{project.id}/merge_requests", user), params: params, headers: headers }
+
+ it 'does not increase the SSE counter by default' do
+ expect(Gitlab::UsageDataCounters::EditorUniqueCounter).not_to receive(:track_sse_edit_action)
+
+ subject
+
+ expect(response).to have_gitlab_http_status(:created)
+ end
+
+ context 'when referer is not the SSE' do
+ let(:headers) { { 'HTTP_REFERER' => 'https://gitlab.com' } }
+
+ it 'does not increase the SSE counter by default' do
+ expect(Gitlab::UsageDataCounters::EditorUniqueCounter).not_to receive(:track_sse_edit_action)
+
+ subject
+
+ expect(response).to have_gitlab_http_status(:created)
+ end
+ end
+
+ context 'when referer is the SSE' do
+ let(:headers) { { 'HTTP_REFERER' => project_show_sse_url(project, 'master/README.md') } }
+
+ it 'increases the SSE counter by default' do
+ expect(Gitlab::UsageDataCounters::EditorUniqueCounter).to receive(:track_sse_edit_action).with(author: user)
+
+ subject
+
+ expect(response).to have_gitlab_http_status(:created)
+ end
+ end
+ end
end
describe 'PUT /projects/:id/merge_reuests/:merge_request_iid' do
diff --git a/spec/requests/api/nuget_packages_spec.rb b/spec/requests/api/nuget_packages_spec.rb
deleted file mode 100644
index 62f244c433b..00000000000
--- a/spec/requests/api/nuget_packages_spec.rb
+++ /dev/null
@@ -1,533 +0,0 @@
-# frozen_string_literal: true
-require 'spec_helper'
-
-RSpec.describe API::NugetPackages do
- include WorkhorseHelpers
- include PackagesManagerApiSpecHelpers
- include HttpBasicAuthHelpers
-
- let_it_be(:user) { create(:user) }
- let_it_be(:project, reload: true) { create(:project, :public) }
- 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) }
-
- describe 'GET /api/v4/projects/:id/packages/nuget' do
- let(:url) { "/projects/#{project.id}/packages/nuget/index.json" }
-
- subject { get api(url) }
-
- context 'without the need for a license' do
- context 'with valid project' do
- using RSpec::Parameterized::TableSyntax
-
- context 'personal token' do
- where(:project_visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do
- 'PUBLIC' | :developer | true | true | 'process nuget service index request' | :success
- 'PUBLIC' | :guest | true | true | 'process nuget service index request' | :success
- 'PUBLIC' | :developer | true | false | 'process nuget service index request' | :success
- 'PUBLIC' | :guest | true | false | 'process nuget service index request' | :success
- 'PUBLIC' | :developer | false | true | 'process nuget service index request' | :success
- 'PUBLIC' | :guest | false | true | 'process nuget service index request' | :success
- 'PUBLIC' | :developer | false | false | 'process nuget service index request' | :success
- 'PUBLIC' | :guest | false | false | 'process nuget service index request' | :success
- 'PUBLIC' | :anonymous | false | true | 'process nuget service index request' | :success
- 'PRIVATE' | :developer | true | true | 'process nuget service index request' | :success
- 'PRIVATE' | :guest | true | true | 'rejects nuget packages access' | :forbidden
- 'PRIVATE' | :developer | true | false | 'rejects nuget packages access' | :unauthorized
- 'PRIVATE' | :guest | true | false | 'rejects nuget packages access' | :unauthorized
- 'PRIVATE' | :developer | false | true | 'rejects nuget packages access' | :not_found
- 'PRIVATE' | :guest | false | true | 'rejects nuget packages access' | :not_found
- 'PRIVATE' | :developer | false | false | 'rejects nuget packages access' | :unauthorized
- 'PRIVATE' | :guest | false | false | 'rejects nuget packages access' | :unauthorized
- 'PRIVATE' | :anonymous | false | true | 'rejects nuget 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!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility_level, false))
- end
-
- it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member]
- end
- end
-
- context 'with job token' do
- where(:project_visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do
- 'PUBLIC' | :developer | true | true | 'process nuget service index request' | :success
- 'PUBLIC' | :guest | true | true | 'process nuget service index request' | :success
- 'PUBLIC' | :developer | true | false | 'rejects nuget packages access' | :unauthorized
- 'PUBLIC' | :guest | true | false | 'rejects nuget packages access' | :unauthorized
- 'PUBLIC' | :developer | false | true | 'process nuget service index request' | :success
- 'PUBLIC' | :guest | false | true | 'process nuget service index request' | :success
- 'PUBLIC' | :developer | false | false | 'rejects nuget packages access' | :unauthorized
- 'PUBLIC' | :guest | false | false | 'rejects nuget packages access' | :unauthorized
- 'PUBLIC' | :anonymous | false | true | 'process nuget service index request' | :success
- 'PRIVATE' | :developer | true | true | 'process nuget service index request' | :success
- 'PRIVATE' | :guest | true | true | 'rejects nuget packages access' | :forbidden
- 'PRIVATE' | :developer | true | false | 'rejects nuget packages access' | :unauthorized
- 'PRIVATE' | :guest | true | false | 'rejects nuget packages access' | :unauthorized
- 'PRIVATE' | :developer | false | true | 'rejects nuget packages access' | :not_found
- 'PRIVATE' | :guest | false | true | 'rejects nuget packages access' | :not_found
- 'PRIVATE' | :developer | false | false | 'rejects nuget packages access' | :unauthorized
- 'PRIVATE' | :guest | false | false | 'rejects nuget packages access' | :unauthorized
- 'PRIVATE' | :anonymous | false | true | 'rejects nuget packages access' | :unauthorized
- end
-
- with_them do
- let(:job) { user_token ? create(:ci_build, project: project, user: user, status: :running) : double(token: 'wrong') }
- let(:headers) { user_role == :anonymous ? {} : job_basic_auth_header(job) }
-
- subject { get api(url), headers: headers }
-
- before do
- project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility_level, false))
- end
-
- it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member]
- end
- end
- end
-
- it_behaves_like 'deploy token for package GET requests'
-
- it_behaves_like 'rejects nuget access with unknown project id'
-
- it_behaves_like 'rejects nuget access with invalid project id'
- end
- end
-
- describe 'PUT /api/v4/projects/:id/packages/nuget/authorize' do
- let_it_be(:workhorse_token) { JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256') }
- let_it_be(:workhorse_header) { { 'GitLab-Workhorse' => '1.0', Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER => workhorse_token } }
- let(:url) { "/projects/#{project.id}/packages/nuget/authorize" }
- let(:headers) { {} }
-
- subject { put api(url), headers: headers }
-
- context 'without the need for a license' do
- context 'with valid project' do
- using RSpec::Parameterized::TableSyntax
-
- where(:project_visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do
- 'PUBLIC' | :developer | true | true | 'process nuget workhorse authorization' | :success
- 'PUBLIC' | :guest | true | true | 'rejects nuget packages access' | :forbidden
- 'PUBLIC' | :developer | true | false | 'rejects nuget packages access' | :unauthorized
- 'PUBLIC' | :guest | true | false | 'rejects nuget packages access' | :unauthorized
- 'PUBLIC' | :developer | false | true | 'rejects nuget packages access' | :forbidden
- 'PUBLIC' | :guest | false | true | 'rejects nuget packages access' | :forbidden
- 'PUBLIC' | :developer | false | false | 'rejects nuget packages access' | :unauthorized
- 'PUBLIC' | :guest | false | false | 'rejects nuget packages access' | :unauthorized
- 'PUBLIC' | :anonymous | false | true | 'rejects nuget packages access' | :unauthorized
- 'PRIVATE' | :developer | true | true | 'process nuget workhorse authorization' | :success
- 'PRIVATE' | :guest | true | true | 'rejects nuget packages access' | :forbidden
- 'PRIVATE' | :developer | true | false | 'rejects nuget packages access' | :unauthorized
- 'PRIVATE' | :guest | true | false | 'rejects nuget packages access' | :unauthorized
- 'PRIVATE' | :developer | false | true | 'rejects nuget packages access' | :not_found
- 'PRIVATE' | :guest | false | true | 'rejects nuget packages access' | :not_found
- 'PRIVATE' | :developer | false | false | 'rejects nuget packages access' | :unauthorized
- 'PRIVATE' | :guest | false | false | 'rejects nuget packages access' | :unauthorized
- 'PRIVATE' | :anonymous | false | true | 'rejects nuget packages access' | :unauthorized
- end
-
- with_them do
- let(:token) { user_token ? personal_access_token.token : 'wrong' }
- let(:user_headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) }
- let(:headers) { user_headers.merge(workhorse_header) }
-
- before do
- project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility_level, false))
- end
-
- it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member]
- end
- end
-
- it_behaves_like 'deploy token for package uploads'
-
- it_behaves_like 'rejects nuget access with unknown project id'
-
- it_behaves_like 'rejects nuget access with invalid project id'
- end
- end
-
- describe 'PUT /api/v4/projects/:id/packages/nuget' do
- let(:workhorse_token) { JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256') }
- let(:workhorse_header) { { 'GitLab-Workhorse' => '1.0', Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER => workhorse_token } }
- let_it_be(:file_name) { 'package.nupkg' }
- let(:url) { "/projects/#{project.id}/packages/nuget" }
- let(:headers) { {} }
- let(:params) { { package: temp_file(file_name) } }
- let(:file_key) { :package }
- let(:send_rewritten_field) { true }
-
- subject do
- workhorse_finalize(
- api(url),
- method: :put,
- file_key: file_key,
- params: params,
- headers: headers,
- send_rewritten_field: send_rewritten_field
- )
- end
-
- context 'without the need for a license' do
- context 'with valid project' do
- using RSpec::Parameterized::TableSyntax
-
- where(:project_visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do
- 'PUBLIC' | :developer | true | true | 'process nuget upload' | :created
- 'PUBLIC' | :guest | true | true | 'rejects nuget packages access' | :forbidden
- 'PUBLIC' | :developer | true | false | 'rejects nuget packages access' | :unauthorized
- 'PUBLIC' | :guest | true | false | 'rejects nuget packages access' | :unauthorized
- 'PUBLIC' | :developer | false | true | 'rejects nuget packages access' | :forbidden
- 'PUBLIC' | :guest | false | true | 'rejects nuget packages access' | :forbidden
- 'PUBLIC' | :developer | false | false | 'rejects nuget packages access' | :unauthorized
- 'PUBLIC' | :guest | false | false | 'rejects nuget packages access' | :unauthorized
- 'PUBLIC' | :anonymous | false | true | 'rejects nuget packages access' | :unauthorized
- 'PRIVATE' | :developer | true | true | 'process nuget upload' | :created
- 'PRIVATE' | :guest | true | true | 'rejects nuget packages access' | :forbidden
- 'PRIVATE' | :developer | true | false | 'rejects nuget packages access' | :unauthorized
- 'PRIVATE' | :guest | true | false | 'rejects nuget packages access' | :unauthorized
- 'PRIVATE' | :developer | false | true | 'rejects nuget packages access' | :not_found
- 'PRIVATE' | :guest | false | true | 'rejects nuget packages access' | :not_found
- 'PRIVATE' | :developer | false | false | 'rejects nuget packages access' | :unauthorized
- 'PRIVATE' | :guest | false | false | 'rejects nuget packages access' | :unauthorized
- 'PRIVATE' | :anonymous | false | true | 'rejects nuget packages access' | :unauthorized
- end
-
- with_them do
- let(:token) { user_token ? personal_access_token.token : 'wrong' }
- let(:user_headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) }
- let(:headers) { user_headers.merge(workhorse_header) }
-
- before do
- project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility_level, false))
- end
-
- it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member]
- end
- end
-
- it_behaves_like 'deploy token for package uploads'
-
- it_behaves_like 'rejects nuget access with unknown project id'
-
- it_behaves_like 'rejects nuget access with invalid project id'
-
- context 'file size above maximum limit' do
- let(:headers) { basic_auth_header(deploy_token.username, deploy_token.token).merge(workhorse_header) }
-
- before do
- allow_next_instance_of(UploadedFile) do |uploaded_file|
- allow(uploaded_file).to receive(:size).and_return(project.actual_limits.nuget_max_file_size + 1)
- end
- end
-
- it_behaves_like 'returning response status', :bad_request
- end
- end
- end
-
- describe 'GET /api/v4/projects/:id/packages/nuget/metadata/*package_name/index' do
- include_context 'with expected presenters dependency groups'
-
- let_it_be(:package_name) { 'Dummy.Package' }
- let_it_be(:packages) { create_list(:nuget_package, 5, :with_metadatum, name: package_name, project: project) }
- let_it_be(:tags) { packages.each { |pkg| create(:packages_tag, package: pkg, name: 'test') } }
- let(:url) { "/projects/#{project.id}/packages/nuget/metadata/#{package_name}/index.json" }
-
- subject { get api(url) }
-
- before do
- packages.each { |pkg| create_dependencies_for(pkg) }
- end
-
- context 'without the need for license' do
- context 'with valid project' do
- using RSpec::Parameterized::TableSyntax
-
- where(:project_visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do
- 'PUBLIC' | :developer | true | true | 'process nuget metadata request at package name level' | :success
- 'PUBLIC' | :guest | true | true | 'process nuget metadata request at package name level' | :success
- 'PUBLIC' | :developer | true | false | 'process nuget metadata request at package name level' | :success
- 'PUBLIC' | :guest | true | false | 'process nuget metadata request at package name level' | :success
- 'PUBLIC' | :developer | false | true | 'process nuget metadata request at package name level' | :success
- 'PUBLIC' | :guest | false | true | 'process nuget metadata request at package name level' | :success
- 'PUBLIC' | :developer | false | false | 'process nuget metadata request at package name level' | :success
- 'PUBLIC' | :guest | false | false | 'process nuget metadata request at package name level' | :success
- 'PUBLIC' | :anonymous | false | true | 'process nuget metadata request at package name level' | :success
- 'PRIVATE' | :developer | true | true | 'process nuget metadata request at package name level' | :success
- 'PRIVATE' | :guest | true | true | 'rejects nuget packages access' | :forbidden
- 'PRIVATE' | :developer | true | false | 'rejects nuget packages access' | :unauthorized
- 'PRIVATE' | :guest | true | false | 'rejects nuget packages access' | :unauthorized
- 'PRIVATE' | :developer | false | true | 'rejects nuget packages access' | :not_found
- 'PRIVATE' | :guest | false | true | 'rejects nuget packages access' | :not_found
- 'PRIVATE' | :developer | false | false | 'rejects nuget packages access' | :unauthorized
- 'PRIVATE' | :guest | false | false | 'rejects nuget packages access' | :unauthorized
- 'PRIVATE' | :anonymous | false | true | 'rejects nuget 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!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility_level, false))
- end
-
- it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member]
- end
-
- it_behaves_like 'deploy token for package GET requests'
-
- it_behaves_like 'rejects nuget access with unknown project id'
-
- it_behaves_like 'rejects nuget access with invalid project id'
- end
- end
- end
-
- describe 'GET /api/v4/projects/:id/packages/nuget/metadata/*package_name/*package_version' do
- include_context 'with expected presenters dependency groups'
-
- let_it_be(:package_name) { 'Dummy.Package' }
- let_it_be(:package) { create(:nuget_package, :with_metadatum, name: 'Dummy.Package', project: project) }
- let_it_be(:tag) { create(:packages_tag, package: package, name: 'test') }
- let(:url) { "/projects/#{project.id}/packages/nuget/metadata/#{package_name}/#{package.version}.json" }
-
- subject { get api(url) }
-
- before do
- create_dependencies_for(package)
- end
-
- context 'without the need for a license' do
- context 'with valid project' do
- using RSpec::Parameterized::TableSyntax
-
- where(:project_visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do
- 'PUBLIC' | :developer | true | true | 'process nuget metadata request at package name and package version level' | :success
- 'PUBLIC' | :guest | true | true | 'process nuget metadata request at package name and package version level' | :success
- 'PUBLIC' | :developer | true | false | 'process nuget metadata request at package name and package version level' | :success
- 'PUBLIC' | :guest | true | false | 'process nuget metadata request at package name and package version level' | :success
- 'PUBLIC' | :developer | false | true | 'process nuget metadata request at package name and package version level' | :success
- 'PUBLIC' | :guest | false | true | 'process nuget metadata request at package name and package version level' | :success
- 'PUBLIC' | :developer | false | false | 'process nuget metadata request at package name and package version level' | :success
- 'PUBLIC' | :guest | false | false | 'process nuget metadata request at package name and package version level' | :success
- 'PUBLIC' | :anonymous | false | true | 'process nuget metadata request at package name and package version level' | :success
- 'PRIVATE' | :developer | true | true | 'process nuget metadata request at package name and package version level' | :success
- 'PRIVATE' | :guest | true | true | 'rejects nuget packages access' | :forbidden
- 'PRIVATE' | :developer | true | false | 'rejects nuget packages access' | :unauthorized
- 'PRIVATE' | :guest | true | false | 'rejects nuget packages access' | :unauthorized
- 'PRIVATE' | :developer | false | true | 'rejects nuget packages access' | :not_found
- 'PRIVATE' | :guest | false | true | 'rejects nuget packages access' | :not_found
- 'PRIVATE' | :developer | false | false | 'rejects nuget packages access' | :unauthorized
- 'PRIVATE' | :guest | false | false | 'rejects nuget packages access' | :unauthorized
- 'PRIVATE' | :anonymous | false | true | 'rejects nuget 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!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility_level, false))
- end
-
- it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member]
- end
- end
-
- it_behaves_like 'deploy token for package GET requests'
-
- context 'with invalid package name' do
- let_it_be(:package_name) { 'Unkown' }
-
- it_behaves_like 'rejects nuget packages access', :developer, :not_found
- end
- end
- end
-
- describe 'GET /api/v4/projects/:id/packages/nuget/download/*package_name/index' do
- let_it_be(:package_name) { 'Dummy.Package' }
- let_it_be(:packages) { create_list(:nuget_package, 5, name: package_name, project: project) }
- let(:url) { "/projects/#{project.id}/packages/nuget/download/#{package_name}/index.json" }
-
- subject { get api(url) }
-
- context 'without the need for a license' do
- context 'with valid project' do
- using RSpec::Parameterized::TableSyntax
-
- where(:project_visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do
- 'PUBLIC' | :developer | true | true | 'process nuget download versions request' | :success
- 'PUBLIC' | :guest | true | true | 'process nuget download versions request' | :success
- 'PUBLIC' | :developer | true | false | 'process nuget download versions request' | :success
- 'PUBLIC' | :guest | true | false | 'process nuget download versions request' | :success
- 'PUBLIC' | :developer | false | true | 'process nuget download versions request' | :success
- 'PUBLIC' | :guest | false | true | 'process nuget download versions request' | :success
- 'PUBLIC' | :developer | false | false | 'process nuget download versions request' | :success
- 'PUBLIC' | :guest | false | false | 'process nuget download versions request' | :success
- 'PUBLIC' | :anonymous | false | true | 'process nuget download versions request' | :success
- 'PRIVATE' | :developer | true | true | 'process nuget download versions request' | :success
- 'PRIVATE' | :guest | true | true | 'rejects nuget packages access' | :forbidden
- 'PRIVATE' | :developer | true | false | 'rejects nuget packages access' | :unauthorized
- 'PRIVATE' | :guest | true | false | 'rejects nuget packages access' | :unauthorized
- 'PRIVATE' | :developer | false | true | 'rejects nuget packages access' | :not_found
- 'PRIVATE' | :guest | false | true | 'rejects nuget packages access' | :not_found
- 'PRIVATE' | :developer | false | false | 'rejects nuget packages access' | :unauthorized
- 'PRIVATE' | :guest | false | false | 'rejects nuget packages access' | :unauthorized
- 'PRIVATE' | :anonymous | false | true | 'rejects nuget 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!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility_level, false))
- end
-
- it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member]
- end
- end
-
- it_behaves_like 'deploy token for package GET requests'
-
- it_behaves_like 'rejects nuget access with unknown project id'
-
- it_behaves_like 'rejects nuget access with invalid project id'
- end
- end
-
- describe 'GET /api/v4/projects/:id/packages/nuget/download/*package_name/*package_version/*package_filename' do
- let_it_be(:package_name) { 'Dummy.Package' }
- let_it_be(:package) { create(:nuget_package, project: project, name: package_name) }
-
- let(:url) { "/projects/#{project.id}/packages/nuget/download/#{package.name}/#{package.version}/#{package.name}.#{package.version}.nupkg" }
-
- subject { get api(url) }
-
- context 'without the need for a license' do
- context 'with valid project' do
- using RSpec::Parameterized::TableSyntax
-
- where(:project_visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do
- 'PUBLIC' | :developer | true | true | 'process nuget download content request' | :success
- 'PUBLIC' | :guest | true | true | 'process nuget download content request' | :success
- 'PUBLIC' | :developer | true | false | 'process nuget download content request' | :success
- 'PUBLIC' | :guest | true | false | 'process nuget download content request' | :success
- 'PUBLIC' | :developer | false | true | 'process nuget download content request' | :success
- 'PUBLIC' | :guest | false | true | 'process nuget download content request' | :success
- 'PUBLIC' | :developer | false | false | 'process nuget download content request' | :success
- 'PUBLIC' | :guest | false | false | 'process nuget download content request' | :success
- 'PUBLIC' | :anonymous | false | true | 'process nuget download content request' | :success
- 'PRIVATE' | :developer | true | true | 'process nuget download content request' | :success
- 'PRIVATE' | :guest | true | true | 'rejects nuget packages access' | :forbidden
- 'PRIVATE' | :developer | true | false | 'rejects nuget packages access' | :unauthorized
- 'PRIVATE' | :guest | true | false | 'rejects nuget packages access' | :unauthorized
- 'PRIVATE' | :developer | false | true | 'rejects nuget packages access' | :not_found
- 'PRIVATE' | :guest | false | true | 'rejects nuget packages access' | :not_found
- 'PRIVATE' | :developer | false | false | 'rejects nuget packages access' | :unauthorized
- 'PRIVATE' | :guest | false | false | 'rejects nuget packages access' | :unauthorized
- 'PRIVATE' | :anonymous | false | true | 'rejects nuget 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!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility_level, false))
- end
-
- it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member]
- end
- end
-
- it_behaves_like 'deploy token for package GET requests'
-
- it_behaves_like 'rejects nuget access with unknown project id'
-
- it_behaves_like 'rejects nuget access with invalid project id'
- end
- end
-
- describe 'GET /api/v4/projects/:id/packages/nuget/query' do
- let_it_be(:package_a) { create(:nuget_package, :with_metadatum, name: 'Dummy.PackageA', project: project) }
- let_it_be(:tag) { create(:packages_tag, package: package_a, name: 'test') }
- let_it_be(:packages_b) { create_list(:nuget_package, 5, name: 'Dummy.PackageB', project: project) }
- let_it_be(:packages_c) { create_list(:nuget_package, 5, name: 'Dummy.PackageC', project: project) }
- let_it_be(:package_d) { create(:nuget_package, name: 'Dummy.PackageD', version: '5.0.5-alpha', project: project) }
- let_it_be(:package_e) { create(:nuget_package, name: 'Foo.BarE', project: project) }
- let(:search_term) { 'uMmy' }
- let(:take) { 26 }
- let(:skip) { 0 }
- let(:include_prereleases) { true }
- let(:query_parameters) { { q: search_term, take: take, skip: skip, prerelease: include_prereleases } }
- let(:url) { "/projects/#{project.id}/packages/nuget/query?#{query_parameters.to_query}" }
-
- subject { get api(url) }
-
- context 'without the need for a license' do
- context 'with valid project' do
- using RSpec::Parameterized::TableSyntax
-
- where(:project_visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do
- 'PUBLIC' | :developer | true | true | 'process nuget search request' | :success
- 'PUBLIC' | :guest | true | true | 'process nuget search request' | :success
- 'PUBLIC' | :developer | true | false | 'process nuget search request' | :success
- 'PUBLIC' | :guest | true | false | 'process nuget search request' | :success
- 'PUBLIC' | :developer | false | true | 'process nuget search request' | :success
- 'PUBLIC' | :guest | false | true | 'process nuget search request' | :success
- 'PUBLIC' | :developer | false | false | 'process nuget search request' | :success
- 'PUBLIC' | :guest | false | false | 'process nuget search request' | :success
- 'PUBLIC' | :anonymous | false | true | 'process nuget search request' | :success
- 'PRIVATE' | :developer | true | true | 'process nuget search request' | :success
- 'PRIVATE' | :guest | true | true | 'rejects nuget packages access' | :forbidden
- 'PRIVATE' | :developer | true | false | 'rejects nuget packages access' | :unauthorized
- 'PRIVATE' | :guest | true | false | 'rejects nuget packages access' | :unauthorized
- 'PRIVATE' | :developer | false | true | 'rejects nuget packages access' | :not_found
- 'PRIVATE' | :guest | false | true | 'rejects nuget packages access' | :not_found
- 'PRIVATE' | :developer | false | false | 'rejects nuget packages access' | :unauthorized
- 'PRIVATE' | :guest | false | false | 'rejects nuget packages access' | :unauthorized
- 'PRIVATE' | :anonymous | false | true | 'rejects nuget 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!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility_level, false))
- end
-
- it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member]
- end
- end
-
- it_behaves_like 'deploy token for package GET requests'
-
- it_behaves_like 'rejects nuget access with unknown project id'
-
- it_behaves_like 'rejects nuget access with invalid project id'
- end
- end
-end
diff --git a/spec/requests/api/nuget_project_packages_spec.rb b/spec/requests/api/nuget_project_packages_spec.rb
new file mode 100644
index 00000000000..df1daf39144
--- /dev/null
+++ b/spec/requests/api/nuget_project_packages_spec.rb
@@ -0,0 +1,280 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe API::NugetProjectPackages do
+ include WorkhorseHelpers
+ include PackagesManagerApiSpecHelpers
+ include HttpBasicAuthHelpers
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project, reload: true) { create(:project, :public) }
+ 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) }
+
+ describe 'GET /api/v4/projects/:id/packages/nuget' do
+ it_behaves_like 'handling nuget service requests' do
+ let(:url) { "/projects/#{project.id}/packages/nuget/index.json" }
+ end
+ end
+
+ describe 'GET /api/v4/projects/:id/packages/nuget/metadata/*package_name/index' do
+ it_behaves_like 'handling nuget metadata requests with package name' do
+ let(:url) { "/projects/#{project.id}/packages/nuget/metadata/#{package_name}/index.json" }
+ end
+ end
+
+ describe 'GET /api/v4/projects/:id/packages/nuget/metadata/*package_name/*package_version' do
+ it_behaves_like 'handling nuget metadata requests with package name and package version' do
+ let(:url) { "/projects/#{project.id}/packages/nuget/metadata/#{package_name}/#{package.version}.json" }
+ end
+ end
+
+ describe 'GET /api/v4/projects/:id/packages/nuget/query' do
+ it_behaves_like 'handling nuget search requests' do
+ let(:url) { "/projects/#{project.id}/packages/nuget/query?#{query_parameters.to_query}" }
+ end
+ end
+
+ describe 'PUT /api/v4/projects/:id/packages/nuget/authorize' do
+ let_it_be(:workhorse_token) { JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256') }
+ let_it_be(:workhorse_header) { { 'GitLab-Workhorse' => '1.0', Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER => workhorse_token } }
+ let(:url) { "/projects/#{project.id}/packages/nuget/authorize" }
+ let(:headers) { {} }
+
+ subject { put api(url), headers: headers }
+
+ context 'without the need for a license' do
+ context 'with valid project' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:project_visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do
+ 'PUBLIC' | :developer | true | true | 'process nuget workhorse authorization' | :success
+ 'PUBLIC' | :guest | true | true | 'rejects nuget packages access' | :forbidden
+ 'PUBLIC' | :developer | true | false | 'rejects nuget packages access' | :unauthorized
+ 'PUBLIC' | :guest | true | false | 'rejects nuget packages access' | :unauthorized
+ 'PUBLIC' | :developer | false | true | 'rejects nuget packages access' | :forbidden
+ 'PUBLIC' | :guest | false | true | 'rejects nuget packages access' | :forbidden
+ 'PUBLIC' | :developer | false | false | 'rejects nuget packages access' | :unauthorized
+ 'PUBLIC' | :guest | false | false | 'rejects nuget packages access' | :unauthorized
+ 'PUBLIC' | :anonymous | false | true | 'rejects nuget packages access' | :unauthorized
+ 'PRIVATE' | :developer | true | true | 'process nuget workhorse authorization' | :success
+ 'PRIVATE' | :guest | true | true | 'rejects nuget packages access' | :forbidden
+ 'PRIVATE' | :developer | true | false | 'rejects nuget packages access' | :unauthorized
+ 'PRIVATE' | :guest | true | false | 'rejects nuget packages access' | :unauthorized
+ 'PRIVATE' | :developer | false | true | 'rejects nuget packages access' | :not_found
+ 'PRIVATE' | :guest | false | true | 'rejects nuget packages access' | :not_found
+ 'PRIVATE' | :developer | false | false | 'rejects nuget packages access' | :unauthorized
+ 'PRIVATE' | :guest | false | false | 'rejects nuget packages access' | :unauthorized
+ 'PRIVATE' | :anonymous | false | true | 'rejects nuget packages access' | :unauthorized
+ end
+
+ with_them do
+ let(:token) { user_token ? personal_access_token.token : 'wrong' }
+ let(:user_headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) }
+ let(:headers) { user_headers.merge(workhorse_header) }
+
+ before do
+ project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility_level, false))
+ end
+
+ it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member]
+ end
+ end
+
+ it_behaves_like 'deploy token for package uploads'
+
+ it_behaves_like 'rejects nuget access with unknown project id'
+
+ it_behaves_like 'rejects nuget access with invalid project id'
+ end
+ end
+
+ describe 'PUT /api/v4/projects/:id/packages/nuget' do
+ let(:workhorse_token) { JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256') }
+ let(:workhorse_header) { { 'GitLab-Workhorse' => '1.0', Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER => workhorse_token } }
+ let_it_be(:file_name) { 'package.nupkg' }
+ let(:url) { "/projects/#{project.id}/packages/nuget" }
+ let(:headers) { {} }
+ let(:params) { { package: temp_file(file_name) } }
+ let(:file_key) { :package }
+ let(:send_rewritten_field) { true }
+
+ subject do
+ workhorse_finalize(
+ api(url),
+ method: :put,
+ file_key: file_key,
+ params: params,
+ headers: headers,
+ send_rewritten_field: send_rewritten_field
+ )
+ end
+
+ context 'without the need for a license' do
+ context 'with valid project' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:project_visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do
+ 'PUBLIC' | :developer | true | true | 'process nuget upload' | :created
+ 'PUBLIC' | :guest | true | true | 'rejects nuget packages access' | :forbidden
+ 'PUBLIC' | :developer | true | false | 'rejects nuget packages access' | :unauthorized
+ 'PUBLIC' | :guest | true | false | 'rejects nuget packages access' | :unauthorized
+ 'PUBLIC' | :developer | false | true | 'rejects nuget packages access' | :forbidden
+ 'PUBLIC' | :guest | false | true | 'rejects nuget packages access' | :forbidden
+ 'PUBLIC' | :developer | false | false | 'rejects nuget packages access' | :unauthorized
+ 'PUBLIC' | :guest | false | false | 'rejects nuget packages access' | :unauthorized
+ 'PUBLIC' | :anonymous | false | true | 'rejects nuget packages access' | :unauthorized
+ 'PRIVATE' | :developer | true | true | 'process nuget upload' | :created
+ 'PRIVATE' | :guest | true | true | 'rejects nuget packages access' | :forbidden
+ 'PRIVATE' | :developer | true | false | 'rejects nuget packages access' | :unauthorized
+ 'PRIVATE' | :guest | true | false | 'rejects nuget packages access' | :unauthorized
+ 'PRIVATE' | :developer | false | true | 'rejects nuget packages access' | :not_found
+ 'PRIVATE' | :guest | false | true | 'rejects nuget packages access' | :not_found
+ 'PRIVATE' | :developer | false | false | 'rejects nuget packages access' | :unauthorized
+ 'PRIVATE' | :guest | false | false | 'rejects nuget packages access' | :unauthorized
+ 'PRIVATE' | :anonymous | false | true | 'rejects nuget packages access' | :unauthorized
+ end
+
+ with_them do
+ let(:token) { user_token ? personal_access_token.token : 'wrong' }
+ let(:user_headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) }
+ let(:headers) { user_headers.merge(workhorse_header) }
+
+ before do
+ project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility_level, false))
+ end
+
+ it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member]
+ end
+ end
+
+ it_behaves_like 'deploy token for package uploads'
+
+ it_behaves_like 'rejects nuget access with unknown project id'
+
+ it_behaves_like 'rejects nuget access with invalid project id'
+
+ context 'file size above maximum limit' do
+ let(:headers) { basic_auth_header(deploy_token.username, deploy_token.token).merge(workhorse_header) }
+
+ before do
+ allow_next_instance_of(UploadedFile) do |uploaded_file|
+ allow(uploaded_file).to receive(:size).and_return(project.actual_limits.nuget_max_file_size + 1)
+ end
+ end
+
+ it_behaves_like 'returning response status', :bad_request
+ end
+ end
+ end
+
+ describe 'GET /api/v4/projects/:id/packages/nuget/download/*package_name/index' do
+ let_it_be(:package_name) { 'Dummy.Package' }
+ let_it_be(:packages) { create_list(:nuget_package, 5, name: package_name, project: project) }
+ let(:url) { "/projects/#{project.id}/packages/nuget/download/#{package_name}/index.json" }
+
+ subject { get api(url) }
+
+ context 'without the need for a license' do
+ context 'with valid project' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:project_visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do
+ 'PUBLIC' | :developer | true | true | 'process nuget download versions request' | :success
+ 'PUBLIC' | :guest | true | true | 'process nuget download versions request' | :success
+ 'PUBLIC' | :developer | true | false | 'process nuget download versions request' | :success
+ 'PUBLIC' | :guest | true | false | 'process nuget download versions request' | :success
+ 'PUBLIC' | :developer | false | true | 'process nuget download versions request' | :success
+ 'PUBLIC' | :guest | false | true | 'process nuget download versions request' | :success
+ 'PUBLIC' | :developer | false | false | 'process nuget download versions request' | :success
+ 'PUBLIC' | :guest | false | false | 'process nuget download versions request' | :success
+ 'PUBLIC' | :anonymous | false | true | 'process nuget download versions request' | :success
+ 'PRIVATE' | :developer | true | true | 'process nuget download versions request' | :success
+ 'PRIVATE' | :guest | true | true | 'rejects nuget packages access' | :forbidden
+ 'PRIVATE' | :developer | true | false | 'rejects nuget packages access' | :unauthorized
+ 'PRIVATE' | :guest | true | false | 'rejects nuget packages access' | :unauthorized
+ 'PRIVATE' | :developer | false | true | 'rejects nuget packages access' | :not_found
+ 'PRIVATE' | :guest | false | true | 'rejects nuget packages access' | :not_found
+ 'PRIVATE' | :developer | false | false | 'rejects nuget packages access' | :unauthorized
+ 'PRIVATE' | :guest | false | false | 'rejects nuget packages access' | :unauthorized
+ 'PRIVATE' | :anonymous | false | true | 'rejects nuget 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!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility_level, false))
+ end
+
+ it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member]
+ end
+ end
+
+ it_behaves_like 'deploy token for package GET requests'
+
+ it_behaves_like 'rejects nuget access with unknown project id'
+
+ it_behaves_like 'rejects nuget access with invalid project id'
+ end
+ end
+
+ describe 'GET /api/v4/projects/:id/packages/nuget/download/*package_name/*package_version/*package_filename' do
+ let_it_be(:package_name) { 'Dummy.Package' }
+ let_it_be(:package) { create(:nuget_package, project: project, name: package_name) }
+
+ let(:url) { "/projects/#{project.id}/packages/nuget/download/#{package.name}/#{package.version}/#{package.name}.#{package.version}.nupkg" }
+
+ subject { get api(url) }
+
+ context 'without the need for a license' do
+ context 'with valid project' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:project_visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do
+ 'PUBLIC' | :developer | true | true | 'process nuget download content request' | :success
+ 'PUBLIC' | :guest | true | true | 'process nuget download content request' | :success
+ 'PUBLIC' | :developer | true | false | 'process nuget download content request' | :success
+ 'PUBLIC' | :guest | true | false | 'process nuget download content request' | :success
+ 'PUBLIC' | :developer | false | true | 'process nuget download content request' | :success
+ 'PUBLIC' | :guest | false | true | 'process nuget download content request' | :success
+ 'PUBLIC' | :developer | false | false | 'process nuget download content request' | :success
+ 'PUBLIC' | :guest | false | false | 'process nuget download content request' | :success
+ 'PUBLIC' | :anonymous | false | true | 'process nuget download content request' | :success
+ 'PRIVATE' | :developer | true | true | 'process nuget download content request' | :success
+ 'PRIVATE' | :guest | true | true | 'rejects nuget packages access' | :forbidden
+ 'PRIVATE' | :developer | true | false | 'rejects nuget packages access' | :unauthorized
+ 'PRIVATE' | :guest | true | false | 'rejects nuget packages access' | :unauthorized
+ 'PRIVATE' | :developer | false | true | 'rejects nuget packages access' | :not_found
+ 'PRIVATE' | :guest | false | true | 'rejects nuget packages access' | :not_found
+ 'PRIVATE' | :developer | false | false | 'rejects nuget packages access' | :unauthorized
+ 'PRIVATE' | :guest | false | false | 'rejects nuget packages access' | :unauthorized
+ 'PRIVATE' | :anonymous | false | true | 'rejects nuget 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!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility_level, false))
+ end
+
+ it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member]
+ end
+ end
+
+ it_behaves_like 'deploy token for package GET requests'
+
+ it_behaves_like 'rejects nuget access with unknown project id'
+
+ it_behaves_like 'rejects nuget access with invalid project id'
+ end
+ end
+end
diff --git a/spec/requests/api/project_clusters_spec.rb b/spec/requests/api/project_clusters_spec.rb
index 7b37862af74..f784f677c25 100644
--- a/spec/requests/api/project_clusters_spec.rb
+++ b/spec/requests/api/project_clusters_spec.rb
@@ -88,6 +88,8 @@ RSpec.describe API::ProjectClusters do
expect(json_response['environment_scope']).to eq('*')
expect(json_response['cluster_type']).to eq('project_type')
expect(json_response['domain']).to eq('example.com')
+ expect(json_response['enabled']).to be_truthy
+ expect(json_response['managed']).to be_truthy
end
it 'returns project information' do
@@ -171,6 +173,7 @@ RSpec.describe API::ProjectClusters do
name: 'test-cluster',
domain: 'domain.example.com',
managed: false,
+ enabled: false,
namespace_per_environment: false,
platform_kubernetes_attributes: platform_kubernetes_attributes,
management_project_id: management_project_id
@@ -202,6 +205,7 @@ RSpec.describe API::ProjectClusters do
expect(cluster_result.name).to eq('test-cluster')
expect(cluster_result.domain).to eq('domain.example.com')
expect(cluster_result.managed).to be_falsy
+ expect(cluster_result.enabled).to be_falsy
expect(cluster_result.management_project_id).to eq management_project_id
expect(cluster_result.namespace_per_environment).to eq(false)
expect(platform_kubernetes.rbac?).to be_truthy
@@ -337,7 +341,9 @@ RSpec.describe API::ProjectClusters do
{
domain: 'new-domain.com',
platform_kubernetes_attributes: platform_kubernetes_attributes,
- management_project_id: management_project_id
+ management_project_id: management_project_id,
+ managed: false,
+ enabled: false
}
end
@@ -373,6 +379,8 @@ RSpec.describe API::ProjectClusters do
it 'updates cluster attributes' do
expect(response).to have_gitlab_http_status(:ok)
expect(cluster.domain).to eq('new-domain.com')
+ expect(cluster.managed).to be_falsy
+ expect(cluster.enabled).to be_falsy
expect(cluster.platform_kubernetes.namespace).to eq('new-namespace')
expect(cluster.management_project).to eq(management_project)
end
@@ -384,6 +392,8 @@ RSpec.describe API::ProjectClusters do
it 'does not update cluster attributes' do
expect(response).to have_gitlab_http_status(:bad_request)
expect(cluster.domain).not_to eq('new_domain.com')
+ expect(cluster.managed).to be_truthy
+ expect(cluster.enabled).to be_truthy
expect(cluster.platform_kubernetes.namespace).not_to eq('invalid_namespace')
expect(cluster.management_project).not_to eq(management_project)
end
diff --git a/spec/requests/api/project_import_spec.rb b/spec/requests/api/project_import_spec.rb
index a6ae636996e..8e99d37c84f 100644
--- a/spec/requests/api/project_import_spec.rb
+++ b/spec/requests/api/project_import_spec.rb
@@ -303,7 +303,7 @@ RSpec.describe API::ProjectImport do
subject
expect(response).to have_gitlab_http_status(:ok)
- expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
+ expect(response.media_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
expect(json_response['TempPath']).to eq(ImportExportUploader.workhorse_local_upload_path)
end
@@ -325,7 +325,7 @@ RSpec.describe API::ProjectImport do
subject
expect(response).to have_gitlab_http_status(:ok)
- expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
+ expect(response.media_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
expect(json_response).not_to have_key('TempPath')
expect(json_response['RemoteObject']).to have_key('ID')
expect(json_response['RemoteObject']).to have_key('GetURL')
@@ -344,7 +344,7 @@ RSpec.describe API::ProjectImport do
subject
expect(response).to have_gitlab_http_status(:ok)
- expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
+ expect(response.media_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
expect(json_response['TempPath']).to eq(ImportExportUploader.workhorse_local_upload_path)
expect(json_response['RemoteObject']).to be_nil
end
diff --git a/spec/requests/api/project_repository_storage_moves_spec.rb b/spec/requests/api/project_repository_storage_moves_spec.rb
index ecf4c75b52f..15e69c2aa16 100644
--- a/spec/requests/api/project_repository_storage_moves_spec.rb
+++ b/spec/requests/api/project_repository_storage_moves_spec.rb
@@ -6,8 +6,8 @@ RSpec.describe API::ProjectRepositoryStorageMoves do
include AccessMatchersForRequest
let_it_be(:user) { create(:admin) }
- let_it_be(:project) { create(:project) }
- let_it_be(:storage_move) { create(:project_repository_storage_move, :scheduled, project: project) }
+ let_it_be(:project) { create(:project, :repository).tap { |project| project.track_project_repository } }
+ let_it_be(:storage_move) { create(:project_repository_storage_move, :scheduled, container: project) }
shared_examples 'get single project repository storage move' do
let(:project_repository_storage_move_id) { storage_move.id }
@@ -63,14 +63,14 @@ RSpec.describe API::ProjectRepositoryStorageMoves do
control = ActiveRecord::QueryRecorder.new { get_project_repository_storage_moves }
- create(:project_repository_storage_move, :scheduled, project: project)
+ create(:project_repository_storage_move, :scheduled, container: project)
expect { get_project_repository_storage_moves }.not_to exceed_query_limit(control)
end
it 'returns the most recently created first' do
- storage_move_oldest = create(:project_repository_storage_move, :scheduled, project: project, created_at: 2.days.ago)
- storage_move_middle = create(:project_repository_storage_move, :scheduled, project: project, created_at: 1.day.ago)
+ storage_move_oldest = create(:project_repository_storage_move, :scheduled, container: project, created_at: 2.days.ago)
+ storage_move_middle = create(:project_repository_storage_move, :scheduled, container: project, created_at: 1.day.ago)
get_project_repository_storage_moves
@@ -159,4 +159,64 @@ RSpec.describe API::ProjectRepositoryStorageMoves do
end
end
end
+
+ describe 'POST /project_repository_storage_moves' do
+ let(:source_storage_name) { 'default' }
+ let(:destination_storage_name) { 'test_second_storage' }
+
+ def create_project_repository_storage_moves
+ post api('/project_repository_storage_moves', user), params: {
+ source_storage_name: source_storage_name,
+ destination_storage_name: destination_storage_name
+ }
+ end
+
+ before do
+ stub_storage_settings('test_second_storage' => { 'path' => 'tmp/tests/extra_storage' })
+ end
+
+ it 'schedules the worker' do
+ expect(ProjectScheduleBulkRepositoryShardMovesWorker).to receive(:perform_async).with(source_storage_name, destination_storage_name)
+
+ create_project_repository_storage_moves
+
+ expect(response).to have_gitlab_http_status(:accepted)
+ end
+
+ context 'source_storage_name is invalid' do
+ let(:destination_storage_name) { 'not-a-real-storage' }
+
+ it 'gives an error' do
+ create_project_repository_storage_moves
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+ end
+
+ context 'destination_storage_name is missing' do
+ let(:destination_storage_name) { nil }
+
+ it 'schedules the worker' do
+ expect(ProjectScheduleBulkRepositoryShardMovesWorker).to receive(:perform_async).with(source_storage_name, destination_storage_name)
+
+ create_project_repository_storage_moves
+
+ expect(response).to have_gitlab_http_status(:accepted)
+ end
+ end
+
+ context 'destination_storage_name is invalid' do
+ let(:destination_storage_name) { 'not-a-real-storage' }
+
+ it 'gives an error' do
+ create_project_repository_storage_moves
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+ end
+
+ describe 'normal user' do
+ it { expect { create_project_repository_storage_moves }.to be_denied_for(:user) }
+ end
+ end
end
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index 234ac1778fd..ad5468fb54c 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -254,7 +254,7 @@ RSpec.describe API::Projects do
statistics = json_response.find { |p| p['id'] == project.id }['statistics']
expect(statistics).to be_present
- expect(statistics).to include('commit_count', 'storage_size', 'repository_size', 'wiki_size', 'lfs_objects_size', 'job_artifacts_size', 'snippets_size')
+ expect(statistics).to include('commit_count', 'storage_size', 'repository_size', 'wiki_size', 'lfs_objects_size', 'job_artifacts_size', 'snippets_size', 'packages_size')
end
it "does not include license by default" do
@@ -619,7 +619,7 @@ RSpec.describe API::Projects do
end
context 'sorting by project statistics' do
- %w(repository_size storage_size wiki_size).each do |order_by|
+ %w(repository_size storage_size wiki_size packages_size).each do |order_by|
context "sorting by #{order_by}" do
before do
ProjectStatistics.update_all(order_by => 100)
@@ -873,6 +873,7 @@ RSpec.describe API::Projects do
jobs_enabled: false,
merge_requests_enabled: false,
forking_access_level: 'disabled',
+ analytics_access_level: 'disabled',
wiki_enabled: false,
resolve_outdated_diff_discussions: false,
remove_source_branch_after_merge: true,
@@ -883,7 +884,9 @@ RSpec.describe API::Projects do
only_allow_merge_if_all_discussions_are_resolved: false,
ci_config_path: 'a/custom/path',
merge_method: 'ff'
- })
+ }).tap do |attrs|
+ attrs[:operations_access_level] = 'disabled'
+ end
post api('/projects', user), params: project
@@ -900,6 +903,8 @@ RSpec.describe API::Projects do
expect(project.project_feature.issues_access_level).to eq(ProjectFeature::DISABLED)
expect(project.project_feature.merge_requests_access_level).to eq(ProjectFeature::DISABLED)
expect(project.project_feature.wiki_access_level).to eq(ProjectFeature::DISABLED)
+ expect(project.operations_access_level).to eq(ProjectFeature::DISABLED)
+ expect(project.project_feature.analytics_access_level).to eq(ProjectFeature::DISABLED)
end
it 'creates a project using a template' do
@@ -1579,6 +1584,7 @@ RSpec.describe API::Projects do
expect(json_response['only_allow_merge_if_pipeline_succeeds']).to eq(project.only_allow_merge_if_pipeline_succeeds)
expect(json_response['allow_merge_on_skipped_pipeline']).to eq(project.allow_merge_on_skipped_pipeline)
expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to eq(project.only_allow_merge_if_all_discussions_are_resolved)
+ expect(json_response['operations_access_level']).to be_present
end
end
@@ -1619,8 +1625,10 @@ RSpec.describe API::Projects do
expect(json_response['issues_access_level']).to be_present
expect(json_response['merge_requests_access_level']).to be_present
expect(json_response['forking_access_level']).to be_present
+ expect(json_response['analytics_access_level']).to be_present
expect(json_response['wiki_access_level']).to be_present
expect(json_response['builds_access_level']).to be_present
+ expect(json_response['operations_access_level']).to be_present
expect(json_response).to have_key('emails_disabled')
expect(json_response['resolve_outdated_diff_discussions']).to eq(project.resolve_outdated_diff_discussions)
expect(json_response['remove_source_branch_after_merge']).to be_truthy
diff --git a/spec/requests/api/services_spec.rb b/spec/requests/api/services_spec.rb
index 63ed57c5045..2157e69e7bf 100644
--- a/spec/requests/api/services_spec.rb
+++ b/spec/requests/api/services_spec.rb
@@ -76,7 +76,9 @@ RSpec.describe API::Services do
required_attributes = service_attrs_list.select do |attr|
service_klass.validators_on(attr).any? do |v|
- v.class == ActiveRecord::Validations::PresenceValidator
+ v.class == ActiveRecord::Validations::PresenceValidator &&
+ # exclude presence validators with conditional since those are not really required
+ ![:if, :unless].any? { |cond| v.options.include?(cond) }
end
end
diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb
index 03320549e44..8fb0f8fc51a 100644
--- a/spec/requests/api/settings_spec.rb
+++ b/spec/requests/api/settings_spec.rb
@@ -43,6 +43,7 @@ RSpec.describe API::Settings, 'Settings' do
expect(json_response['spam_check_endpoint_url']).to be_nil
expect(json_response['wiki_page_max_content_bytes']).to be_a(Integer)
expect(json_response['require_admin_approval_after_user_signup']).to eq(true)
+ expect(json_response['personal_access_token_prefix']).to be_nil
end
end
@@ -122,7 +123,8 @@ RSpec.describe API::Settings, 'Settings' do
spam_check_endpoint_url: 'https://example.com/spam_check',
disabled_oauth_sign_in_sources: 'unknown',
import_sources: 'github,bitbucket',
- wiki_page_max_content_bytes: 12345
+ wiki_page_max_content_bytes: 12345,
+ personal_access_token_prefix: "GL-"
}
expect(response).to have_gitlab_http_status(:ok)
@@ -166,6 +168,7 @@ RSpec.describe API::Settings, 'Settings' do
expect(json_response['disabled_oauth_sign_in_sources']).to eq([])
expect(json_response['import_sources']).to match_array(%w(github bitbucket))
expect(json_response['wiki_page_max_content_bytes']).to eq(12345)
+ expect(json_response['personal_access_token_prefix']).to eq("GL-")
end
end
@@ -451,5 +454,25 @@ RSpec.describe API::Settings, 'Settings' do
expect(json_response['error']).to eq('spam_check_endpoint_url is missing')
end
end
+
+ context "personal access token prefix settings" do
+ context "handles validation errors" do
+ it "fails to update the settings with too long prefix" do
+ put api("/application/settings", admin), params: { personal_access_token_prefix: "prefix" * 10 }
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ message = json_response["message"]
+ expect(message["personal_access_token_prefix"]).to include(a_string_matching("is too long"))
+ end
+
+ it "fails to update the settings with invalid characters in the prefix" do
+ put api("/application/settings", admin), params: { personal_access_token_prefix: "éñ" }
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ message = json_response["message"]
+ expect(message["personal_access_token_prefix"]).to include(a_string_matching("can contain only letters of the Base64 alphabet"))
+ end
+ end
+ end
end
end
diff --git a/spec/requests/api/usage_data_spec.rb b/spec/requests/api/usage_data_spec.rb
index 4f4f386e9db..d44f179eed8 100644
--- a/spec/requests/api/usage_data_spec.rb
+++ b/spec/requests/api/usage_data_spec.rb
@@ -5,6 +5,87 @@ require 'spec_helper'
RSpec.describe API::UsageData do
let_it_be(:user) { create(:user) }
+ describe 'POST /usage_data/increment_counter' do
+ let(:endpoint) { '/usage_data/increment_counter' }
+ let(:known_event) { "#{known_event_prefix}_#{known_event_postfix}" }
+ let(:known_event_prefix) { "static_site_editor" }
+ let(:known_event_postfix) { 'commits' }
+ let(:unknown_event) { 'unknown' }
+
+ context 'without CSRF token' do
+ it 'returns forbidden' do
+ stub_feature_flags(usage_data_api: true)
+ allow(Gitlab::RequestForgeryProtection).to receive(:verified?).and_return(false)
+
+ post api(endpoint, user), params: { event: known_event }
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+
+ context 'usage_data_api feature not enabled' do
+ it 'returns not_found' do
+ stub_feature_flags(usage_data_api: false)
+
+ post api(endpoint, user), params: { event: known_event }
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'without authentication' do
+ it 'returns 401 response' do
+ post api(endpoint), params: { event: known_event }
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+ end
+
+ context 'with authentication' do
+ before do
+ stub_feature_flags(usage_data_api: true)
+ stub_feature_flags("usage_data_#{known_event}" => true)
+ stub_application_setting(usage_ping_enabled: true)
+ allow(Gitlab::RequestForgeryProtection).to receive(:verified?).and_return(true)
+ end
+
+ context 'when event is missing from params' do
+ it 'returns bad request' do
+ post api(endpoint, user), params: {}
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+ end
+
+ %w[merge_requests commits].each do |postfix|
+ context 'with correct params' do
+ let(:known_event_postfix) { postfix }
+
+ it 'returns status ok' do
+ expect(Gitlab::UsageDataCounters::BaseCounter).to receive(:count).with(known_event_postfix)
+ post api(endpoint, user), params: { event: known_event }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+ end
+
+ context 'with unknown event' do
+ before do
+ skip_feature_flags_yaml_validation
+ end
+
+ it 'returns status ok' do
+ expect(Gitlab::UsageDataCounters::BaseCounter).not_to receive(:count)
+
+ post api(endpoint, user), params: { event: unknown_event }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+ end
+ end
+
describe 'POST /usage_data/increment_unique_users' do
let(:endpoint) { '/usage_data/increment_unique_users' }
let(:known_event) { 'g_compliance_dashboard' }
diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb
index 98840d6238a..2cd1483f486 100644
--- a/spec/requests/api/users_spec.rb
+++ b/spec/requests/api/users_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Users, :do_not_mock_admin_mode do
+RSpec.describe API::Users do
let_it_be(:admin) { create(:admin) }
let_it_be(:user, reload: true) { create(:user, username: 'user.with.dot') }
let_it_be(:key) { create(:key, user: user) }
@@ -2510,6 +2510,98 @@ RSpec.describe API::Users, :do_not_mock_admin_mode do
end
end
+ context 'approve pending user' do
+ shared_examples '404' do
+ it 'returns 404' do
+ expect(response).to have_gitlab_http_status(:not_found)
+ expect(json_response['message']).to eq('404 User Not Found')
+ end
+ end
+
+ describe 'POST /users/:id/approve' do
+ subject(:approve) { post api("/users/#{user_id}/approve", api_user) }
+
+ let_it_be(:pending_user) { create(:user, :blocked_pending_approval) }
+ let_it_be(:deactivated_user) { create(:user, :deactivated) }
+ let_it_be(:blocked_user) { create(:user, :blocked) }
+
+ context 'performed by a non-admin user' do
+ let(:api_user) { user }
+ let(:user_id) { pending_user.id }
+
+ it 'is not authorized to perform the action' do
+ expect { approve }.not_to change { pending_user.reload.state }
+ expect(response).to have_gitlab_http_status(:forbidden)
+ expect(json_response['message']).to eq('You are not allowed to approve a user')
+ end
+ end
+
+ context 'performed by an admin user' do
+ let(:api_user) { admin }
+
+ context 'for a deactivated user' do
+ let(:user_id) { deactivated_user.id }
+
+ it 'does not approve a deactivated user' do
+ expect { approve }.not_to change { deactivated_user.reload.state }
+ expect(response).to have_gitlab_http_status(:conflict)
+ expect(json_response['message']).to eq('The user you are trying to approve is not pending an approval')
+ end
+ end
+
+ context 'for an pending approval user' do
+ let(:user_id) { pending_user.id }
+
+ it 'returns 201' do
+ expect { approve }.to change { pending_user.reload.state }.to('active')
+ expect(response).to have_gitlab_http_status(:created)
+ expect(json_response['message']).to eq('Success')
+ end
+ end
+
+ context 'for an active user' do
+ let(:user_id) { user.id }
+
+ it 'returns 201' do
+ expect { approve }.not_to change { user.reload.state }
+ expect(response).to have_gitlab_http_status(:conflict)
+ expect(json_response['message']).to eq('The user you are trying to approve is not pending an approval')
+ end
+ end
+
+ context 'for a blocked user' do
+ let(:user_id) { blocked_user.id }
+
+ it 'returns 403' do
+ expect { approve }.not_to change { blocked_user.reload.state }
+ expect(response).to have_gitlab_http_status(:conflict)
+ expect(json_response['message']).to eq('The user you are trying to approve is not pending an approval')
+ end
+ end
+
+ context 'for a ldap blocked user' do
+ let(:user_id) { ldap_blocked_user.id }
+
+ it 'returns 403' do
+ expect { approve }.not_to change { ldap_blocked_user.reload.state }
+ expect(response).to have_gitlab_http_status(:conflict)
+ expect(json_response['message']).to eq('The user you are trying to approve is not pending an approval')
+ end
+ end
+
+ context 'for a user that does not exist' do
+ let(:user_id) { non_existing_record_id }
+
+ before do
+ approve
+ end
+
+ it_behaves_like '404'
+ end
+ end
+ end
+ end
+
describe 'POST /users/:id/block' do
let(:blocked_user) { create(:user, state: 'blocked') }
diff --git a/spec/requests/api/v3/github_spec.rb b/spec/requests/api/v3/github_spec.rb
index 86ddf4a78d8..e7d9ba99743 100644
--- a/spec/requests/api/v3/github_spec.rb
+++ b/spec/requests/api/v3/github_spec.rb
@@ -383,7 +383,7 @@ RSpec.describe API::V3::Github do
it 'counts Jira Cloud integration as enabled' do
user_agent = 'Jira DVCS Connector Vertigo/4.42.0'
- Timecop.freeze do
+ freeze_time do
jira_get v3_api("/repos/#{project.namespace.path}/#{project.path}/branches", user), user_agent
expect(project.reload.jira_dvcs_cloud_last_sync_at).to be_like_time(Time.now)
@@ -393,7 +393,7 @@ RSpec.describe API::V3::Github do
it 'counts Jira Server integration as enabled' do
user_agent = 'Jira DVCS Connector/3.2.4'
- Timecop.freeze do
+ freeze_time do
jira_get v3_api("/repos/#{project.namespace.path}/#{project.path}/branches", user), user_agent
expect(project.reload.jira_dvcs_server_last_sync_at).to be_like_time(Time.now)
diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb
index 32aeeed43b6..bc89dc2fa77 100644
--- a/spec/requests/git_http_spec.rb
+++ b/spec/requests/git_http_spec.rb
@@ -809,12 +809,24 @@ RSpec.describe 'Git HTTP requests' do
context 'administrator' do
let(:user) { create(:admin) }
- it_behaves_like 'can download code only'
+ context 'when admin mode is enabled', :enable_admin_mode do
+ it_behaves_like 'can download code only'
- it 'downloads from other project get status 403' do
- clone_get "#{other_project.full_path}.git", user: 'gitlab-ci-token', password: build.token
+ it 'downloads from other project get status 403' do
+ clone_get "#{other_project.full_path}.git", user: 'gitlab-ci-token', password: build.token
- expect(response).to have_gitlab_http_status(:forbidden)
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+
+ context 'when admin mode is disabled' do
+ it_behaves_like 'can download code only'
+
+ it 'downloads from other project get status 404' do
+ clone_get "#{other_project.full_path}.git", user: 'gitlab-ci-token', password: build.token
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
end
end
diff --git a/spec/requests/ide_controller_spec.rb b/spec/requests/ide_controller_spec.rb
new file mode 100644
index 00000000000..805c1f1d82b
--- /dev/null
+++ b/spec/requests/ide_controller_spec.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe IdeController do
+ let(:user) { create(:user) }
+
+ before do
+ sign_in(user)
+ end
+
+ it 'increases the views counter' do
+ expect(Gitlab::UsageDataCounters::WebIdeCounter).to receive(:increment_views_count)
+
+ get ide_url
+ end
+end
diff --git a/spec/requests/import/gitlab_groups_controller_spec.rb b/spec/requests/import/gitlab_groups_controller_spec.rb
index 4125c5c7c7a..51f1363cf1c 100644
--- a/spec/requests/import/gitlab_groups_controller_spec.rb
+++ b/spec/requests/import/gitlab_groups_controller_spec.rb
@@ -5,6 +5,7 @@ require 'spec_helper'
RSpec.describe Import::GitlabGroupsController do
include WorkhorseHelpers
+ let_it_be(:user) { create(:user) }
let(:import_path) { "#{Dir.tmpdir}/gitlab_groups_controller_spec" }
let(:workhorse_token) { JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256') }
let(:workhorse_headers) do
@@ -28,8 +29,6 @@ RSpec.describe Import::GitlabGroupsController do
describe 'POST create' do
subject(:import_request) { upload_archive(file_upload, workhorse_headers, request_params) }
- let_it_be(:user) { create(:user) }
-
let(:file) { File.join('spec', %w[fixtures group_export.tar.gz]) }
let(:file_upload) { fixture_file_upload(file) }
@@ -194,67 +193,11 @@ RSpec.describe Import::GitlabGroupsController do
end
describe 'POST authorize' do
- let_it_be(:user) { create(:user) }
-
- before do
- login_as(user)
- end
-
- context 'when using a workhorse header' do
- subject(:authorize_request) { post authorize_import_gitlab_group_path, headers: workhorse_headers }
-
- it 'authorizes the request' do
- authorize_request
-
- expect(response).to have_gitlab_http_status :ok
- expect(response.media_type).to eq Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE
- expect(json_response['TempPath']).to eq ImportExportUploader.workhorse_local_upload_path
- end
- end
-
- context 'when the request bypasses gitlab-workhorse' do
- subject(:authorize_request) { post authorize_import_gitlab_group_path }
-
- it 'rejects the request' do
- expect { authorize_request }.to raise_error(JWT::DecodeError)
- end
- end
-
- context 'when direct upload is enabled' do
- subject(:authorize_request) { post authorize_import_gitlab_group_path, headers: workhorse_headers }
+ it_behaves_like 'handle uploads authorize request' do
+ let(:uploader_class) { ImportExportUploader }
+ let(:maximum_size) { Gitlab::CurrentSettings.max_import_size.megabytes }
- before do
- stub_uploads_object_storage(ImportExportUploader, enabled: true, direct_upload: true)
- end
-
- it 'accepts the request and stores the files' do
- authorize_request
-
- expect(response).to have_gitlab_http_status :ok
- expect(response.media_type).to eq Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE
- expect(json_response).not_to have_key 'TempPath'
-
- expect(json_response['RemoteObject'].keys)
- .to include('ID', 'GetURL', 'StoreURL', 'DeleteURL', 'MultipartUpload')
- end
- end
-
- context 'when direct upload is disabled' do
- subject(:authorize_request) { post authorize_import_gitlab_group_path, headers: workhorse_headers }
-
- before do
- stub_uploads_object_storage(ImportExportUploader, enabled: true, direct_upload: false)
- end
-
- it 'handles the local file' do
- authorize_request
-
- expect(response).to have_gitlab_http_status :ok
- expect(response.media_type).to eq Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE
-
- expect(json_response['TempPath']).to eq ImportExportUploader.workhorse_local_upload_path
- expect(json_response['RemoteObject']).to be_nil
- end
+ subject { post authorize_import_gitlab_group_path, headers: workhorse_headers }
end
end
end
diff --git a/spec/requests/import/gitlab_projects_controller_spec.rb b/spec/requests/import/gitlab_projects_controller_spec.rb
index c1ac5a9f2c8..d7d4de21a33 100644
--- a/spec/requests/import/gitlab_projects_controller_spec.rb
+++ b/spec/requests/import/gitlab_projects_controller_spec.rb
@@ -84,56 +84,11 @@ RSpec.describe Import::GitlabProjectsController do
end
describe 'POST authorize' do
- subject { post authorize_import_gitlab_project_path, headers: workhorse_headers }
+ it_behaves_like 'handle uploads authorize request' do
+ let(:uploader_class) { ImportExportUploader }
+ let(:maximum_size) { Gitlab::CurrentSettings.max_import_size.megabytes }
- it 'authorizes importing project with workhorse header' do
- subject
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
- expect(json_response['TempPath']).to eq(ImportExportUploader.workhorse_local_upload_path)
- end
-
- it 'rejects requests that bypassed gitlab-workhorse' do
- workhorse_headers.delete(Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER)
-
- expect { subject }.to raise_error(JWT::DecodeError)
- end
-
- context 'when using remote storage' do
- context 'when direct upload is enabled' do
- before do
- stub_uploads_object_storage(ImportExportUploader, enabled: true, direct_upload: true)
- end
-
- it 'responds with status 200, location of file remote store and object details' do
- subject
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
- expect(json_response).not_to have_key('TempPath')
- expect(json_response['RemoteObject']).to have_key('ID')
- expect(json_response['RemoteObject']).to have_key('GetURL')
- expect(json_response['RemoteObject']).to have_key('StoreURL')
- expect(json_response['RemoteObject']).to have_key('DeleteURL')
- expect(json_response['RemoteObject']).to have_key('MultipartUpload')
- end
- end
-
- context 'when direct upload is disabled' do
- before do
- stub_uploads_object_storage(ImportExportUploader, enabled: true, direct_upload: false)
- end
-
- it 'handles as a local file' do
- subject
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
- expect(json_response['TempPath']).to eq(ImportExportUploader.workhorse_local_upload_path)
- expect(json_response['RemoteObject']).to be_nil
- end
- end
+ subject { post authorize_import_gitlab_project_path, headers: workhorse_headers }
end
end
end
diff --git a/spec/requests/jwt_controller_spec.rb b/spec/requests/jwt_controller_spec.rb
index fe6c0f0a556..e154e691d5f 100644
--- a/spec/requests/jwt_controller_spec.rb
+++ b/spec/requests/jwt_controller_spec.rb
@@ -5,13 +5,13 @@ require 'spec_helper'
RSpec.describe JwtController do
include_context 'parsed logs'
- let(:service) { double(execute: {}) }
- let(:service_class) { double(new: service) }
- let(:service_name) { 'test' }
+ let(:service) { double(execute: {} ) }
+ let(:service_class) { Auth::ContainerRegistryAuthenticationService }
+ let(:service_name) { 'container_registry' }
let(:parameters) { { service: service_name } }
before do
- stub_const('JwtController::SERVICES', service_name => service_class)
+ allow(service_class).to receive(:new).and_return(service)
end
shared_examples 'user logging' do
@@ -22,194 +22,266 @@ RSpec.describe JwtController do
end
end
- context 'existing service' do
- subject! { get '/jwt/auth', params: parameters }
+ context 'authenticating against container registry' do
+ context 'existing service' do
+ subject! { get '/jwt/auth', params: parameters }
- it { expect(response).to have_gitlab_http_status(:ok) }
+ it { expect(response).to have_gitlab_http_status(:ok) }
- context 'returning custom http code' do
- let(:service) { double(execute: { http_status: 505 }) }
+ context 'returning custom http code' do
+ let(:service) { double(execute: { http_status: 505 }) }
- it { expect(response).to have_gitlab_http_status(:http_version_not_supported) }
+ it { expect(response).to have_gitlab_http_status(:http_version_not_supported) }
+ end
end
- end
- context 'when using authenticated request' do
- shared_examples 'rejecting a blocked user' do
- context 'with blocked user' do
- let(:user) { create(:user, :blocked) }
+ context 'when using authenticated request' do
+ shared_examples 'rejecting a blocked user' do
+ context 'with blocked user' do
+ let(:user) { create(:user, :blocked) }
- it 'rejects the request as unauthorized' do
- expect(response).to have_gitlab_http_status(:unauthorized)
- expect(response.body).to include('HTTP Basic: Access denied')
+ it 'rejects the request as unauthorized' do
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ expect(response.body).to include('HTTP Basic: Access denied')
+ end
end
end
- end
- context 'using CI token' do
- let(:user) { create(:user) }
- let(:build) { create(:ci_build, :running, user: user) }
- let(:project) { build.project }
- let(:headers) { { authorization: credentials('gitlab-ci-token', build.token) } }
+ context 'using CI token' do
+ let(:user) { create(:user) }
+ let(:build) { create(:ci_build, :running, user: user) }
+ let(:project) { build.project }
+ let(:headers) { { authorization: credentials('gitlab-ci-token', build.token) } }
- context 'project with enabled CI' do
- subject! { get '/jwt/auth', params: parameters, headers: headers }
-
- it { expect(service_class).to have_received(:new).with(project, user, ActionController::Parameters.new(parameters).permit!) }
+ context 'project with enabled CI' do
+ subject! { get '/jwt/auth', params: parameters, headers: headers }
- it_behaves_like 'user logging'
- end
+ it { expect(service_class).to have_received(:new).with(project, user, ActionController::Parameters.new(parameters).permit!) }
- context 'project with disabled CI' do
- before do
- project.project_feature.update_attribute(:builds_access_level, ProjectFeature::DISABLED)
+ it_behaves_like 'user logging'
end
- subject! { get '/jwt/auth', params: parameters, headers: headers }
+ context 'project with disabled CI' do
+ before do
+ project.project_feature.update_attribute(:builds_access_level, ProjectFeature::DISABLED)
+ end
- it { expect(response).to have_gitlab_http_status(:unauthorized) }
- end
+ subject! { get '/jwt/auth', params: parameters, headers: headers }
- context 'using deploy tokens' do
- let(:deploy_token) { create(:deploy_token, read_registry: true, projects: [project]) }
- let(:headers) { { authorization: credentials(deploy_token.username, deploy_token.token) } }
+ it { expect(response).to have_gitlab_http_status(:unauthorized) }
+ end
- subject! { get '/jwt/auth', params: parameters, headers: headers }
+ context 'using deploy tokens' do
+ let(:deploy_token) { create(:deploy_token, read_registry: true, projects: [project]) }
+ let(:headers) { { authorization: credentials(deploy_token.username, deploy_token.token) } }
- it 'authenticates correctly' do
- expect(response).to have_gitlab_http_status(:ok)
- expect(service_class).to have_received(:new).with(nil, deploy_token, ActionController::Parameters.new(parameters).permit!)
- end
+ subject! { get '/jwt/auth', params: parameters, headers: headers }
- it 'does not log a user' do
- expect(log_data.keys).not_to include(%w(username user_id))
+ it 'authenticates correctly' do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(service_class).to have_received(:new).with(nil, deploy_token, ActionController::Parameters.new(parameters).permit!)
+ end
+
+ it 'does not log a user' do
+ expect(log_data.keys).not_to include(%w(username user_id))
+ end
end
- end
- context 'using personal access tokens' do
- let(:pat) { create(:personal_access_token, user: user, scopes: ['read_registry']) }
- let(:headers) { { authorization: credentials('personal_access_token', pat.token) } }
+ context 'using personal access tokens' do
+ let(:pat) { create(:personal_access_token, user: user, scopes: ['read_registry']) }
+ let(:headers) { { authorization: credentials('personal_access_token', pat.token) } }
- before do
- stub_container_registry_config(enabled: true)
+ before do
+ stub_container_registry_config(enabled: true)
+ end
+
+ subject! { get '/jwt/auth', params: parameters, headers: headers }
+
+ it 'authenticates correctly' do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(service_class).to have_received(:new).with(nil, user, ActionController::Parameters.new(parameters).permit!)
+ end
+
+ it_behaves_like 'rejecting a blocked user'
+ it_behaves_like 'user logging'
end
+ end
+
+ context 'using User login' do
+ let(:user) { create(:user) }
+ let(:headers) { { authorization: credentials(user.username, user.password) } }
subject! { get '/jwt/auth', params: parameters, headers: headers }
- it 'authenticates correctly' do
- expect(response).to have_gitlab_http_status(:ok)
- expect(service_class).to have_received(:new).with(nil, user, ActionController::Parameters.new(parameters).permit!)
- end
+ it { expect(service_class).to have_received(:new).with(nil, user, ActionController::Parameters.new(parameters).permit!) }
it_behaves_like 'rejecting a blocked user'
- it_behaves_like 'user logging'
- end
- end
-
- context 'using User login' do
- let(:user) { create(:user) }
- let(:headers) { { authorization: credentials(user.username, user.password) } }
- subject! { get '/jwt/auth', params: parameters, headers: headers }
+ context 'when passing a flat array of scopes' do
+ # We use this trick to make rails to generate a query_string:
+ # scope=scope1&scope=scope2
+ # It works because :scope and 'scope' are the same as string, but different objects
+ let(:parameters) do
+ {
+ :service => service_name,
+ :scope => 'scope1',
+ 'scope' => 'scope2'
+ }
+ end
- it { expect(service_class).to have_received(:new).with(nil, user, ActionController::Parameters.new(parameters).permit!) }
+ let(:service_parameters) do
+ ActionController::Parameters.new({ service: service_name, scopes: %w(scope1 scope2) }).permit!
+ end
- it_behaves_like 'rejecting a blocked user'
+ it { expect(service_class).to have_received(:new).with(nil, user, service_parameters) }
- context 'when passing a flat array of scopes' do
- # We use this trick to make rails to generate a query_string:
- # scope=scope1&scope=scope2
- # It works because :scope and 'scope' are the same as string, but different objects
- let(:parameters) do
- {
- :service => service_name,
- :scope => 'scope1',
- 'scope' => 'scope2'
- }
+ it_behaves_like 'user logging'
end
- let(:service_parameters) do
- ActionController::Parameters.new({ service: service_name, scopes: %w(scope1 scope2) }).permit!
+ context 'when user has 2FA enabled' do
+ let(:user) { create(:user, :two_factor) }
+
+ context 'without personal token' do
+ it 'rejects the authorization attempt' do
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ expect(response.body).to include('You must use a personal access token with \'api\' scope for Git over HTTP')
+ end
+ end
+
+ context 'with personal token' do
+ let(:access_token) { create(:personal_access_token, user: user) }
+ let(:headers) { { authorization: credentials(user.username, access_token.token) } }
+
+ it 'accepts the authorization attempt' do
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
end
- it { expect(service_class).to have_received(:new).with(nil, user, service_parameters) }
+ it 'does not cause session based checks to be activated' do
+ expect(Gitlab::Session).not_to receive(:with_session)
+
+ get '/jwt/auth', params: parameters, headers: headers
- it_behaves_like 'user logging'
+ expect(response).to have_gitlab_http_status(:ok)
+ end
end
- context 'when user has 2FA enabled' do
- let(:user) { create(:user, :two_factor) }
+ context 'using invalid login' do
+ let(:headers) { { authorization: credentials('invalid', 'password') } }
- context 'without personal token' do
+ context 'when internal auth is enabled' do
it 'rejects the authorization attempt' do
+ get '/jwt/auth', params: parameters, headers: headers
+
expect(response).to have_gitlab_http_status(:unauthorized)
- expect(response.body).to include('You must use a personal access token with \'api\' scope for Git over HTTP')
+ expect(response.body).not_to include('You must use a personal access token with \'api\' scope for Git over HTTP')
end
end
- context 'with personal token' do
- let(:access_token) { create(:personal_access_token, user: user) }
- let(:headers) { { authorization: credentials(user.username, access_token.token) } }
+ context 'when internal auth is disabled' do
+ it 'rejects the authorization attempt with personal access token message' do
+ allow_next_instance_of(ApplicationSetting) do |instance|
+ allow(instance).to receive(:password_authentication_enabled_for_git?) { false }
+ end
+ get '/jwt/auth', params: parameters, headers: headers
- it 'accepts the authorization attempt' do
- expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ expect(response.body).to include('You must use a personal access token with \'api\' scope for Git over HTTP')
end
end
end
+ end
- it 'does not cause session based checks to be activated' do
- expect(Gitlab::Session).not_to receive(:with_session)
-
- get '/jwt/auth', params: parameters, headers: headers
+ context 'when using unauthenticated request' do
+ it 'accepts the authorization attempt' do
+ get '/jwt/auth', params: parameters
expect(response).to have_gitlab_http_status(:ok)
end
+
+ it 'allows read access' do
+ expect(service).to receive(:execute).with(authentication_abilities: Gitlab::Auth.read_only_authentication_abilities)
+
+ get '/jwt/auth', params: parameters
+ end
end
- context 'using invalid login' do
- let(:headers) { { authorization: credentials('invalid', 'password') } }
+ context 'unknown service' do
+ subject! { get '/jwt/auth', params: { service: 'unknown' } }
- context 'when internal auth is enabled' do
- it 'rejects the authorization attempt' do
- get '/jwt/auth', params: parameters, headers: headers
+ it { expect(response).to have_gitlab_http_status(:not_found) }
+ end
- expect(response).to have_gitlab_http_status(:unauthorized)
- expect(response.body).not_to include('You must use a personal access token with \'api\' scope for Git over HTTP')
- end
- end
+ def credentials(login, password)
+ ActionController::HttpAuthentication::Basic.encode_credentials(login, password)
+ end
+ end
- context 'when internal auth is disabled' do
- it 'rejects the authorization attempt with personal access token message' do
- allow_next_instance_of(ApplicationSetting) do |instance|
- allow(instance).to receive(:password_authentication_enabled_for_git?) { false }
- end
- get '/jwt/auth', params: parameters, headers: headers
+ context 'authenticating against dependency proxy' do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:personal_access_token) { create(:personal_access_token, user: user) }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, :private, group: group) }
+ let_it_be(:group_deploy_token) { create(:deploy_token, :group, groups: [group]) }
+ let_it_be(:project_deploy_token) { create(:deploy_token, :project, projects: [project]) }
+ let_it_be(:service_name) { 'dependency_proxy' }
+ let(:headers) { { authorization: credentials(credential_user, credential_password) } }
+ let(:params) { { account: credential_user, client_id: 'docker', offline_token: true, service: service_name } }
+
+ before do
+ stub_config(dependency_proxy: { enabled: true })
+ end
- expect(response).to have_gitlab_http_status(:unauthorized)
- expect(response.body).to include('You must use a personal access token with \'api\' scope for Git over HTTP')
- end
+ subject { get '/jwt/auth', params: params, headers: headers }
+
+ shared_examples 'with valid credentials' do
+ it 'returns token successfully' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(json_response['token']).to be_present
end
end
- end
- context 'when using unauthenticated request' do
- it 'accepts the authorization attempt' do
- get '/jwt/auth', params: parameters
+ context 'with personal access token' do
+ let(:credential_user) { nil }
+ let(:credential_password) { personal_access_token.token }
- expect(response).to have_gitlab_http_status(:ok)
+ it_behaves_like 'with valid credentials'
end
- it 'allows read access' do
- expect(service).to receive(:execute).with(authentication_abilities: Gitlab::Auth.read_only_authentication_abilities)
+ context 'with user credentials token' do
+ let(:credential_user) { user.username }
+ let(:credential_password) { user.password }
- get '/jwt/auth', params: parameters
+ it_behaves_like 'with valid credentials'
end
- end
- context 'unknown service' do
- subject! { get '/jwt/auth', params: { service: 'unknown' } }
+ context 'with group deploy token' do
+ let(:credential_user) { group_deploy_token.username }
+ let(:credential_password) { group_deploy_token.token }
- it { expect(response).to have_gitlab_http_status(:not_found) }
+ it_behaves_like 'with valid credentials'
+ end
+
+ context 'with project deploy token' do
+ let(:credential_user) { project_deploy_token.username }
+ let(:credential_password) { project_deploy_token.token }
+
+ it_behaves_like 'with valid credentials'
+ end
+
+ context 'with invalid credentials' do
+ let(:credential_user) { 'foo' }
+ let(:credential_password) { 'bar' }
+
+ it 'returns unauthorized' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+ end
end
def credentials(login, password)
diff --git a/spec/requests/lfs_http_spec.rb b/spec/requests/lfs_http_spec.rb
index 48d125a37c3..535d511a459 100644
--- a/spec/requests/lfs_http_spec.rb
+++ b/spec/requests/lfs_http_spec.rb
@@ -6,1179 +6,1145 @@ RSpec.describe 'Git LFS API and storage' do
include ProjectForksHelper
include WorkhorseHelpers
- let_it_be(:project, reload: true) { create(:project, :repository) }
- let_it_be(:other_project) { create(:project, :repository) }
+ let_it_be(:project, reload: true) { create(:project, :empty_repo) }
let_it_be(:user) { create(:user) }
- let(:lfs_object) { create(:lfs_object, :with_file) }
- let(:headers) do
- {
- 'Authorization' => authorization,
- 'X-Sendfile-Type' => 'X-Sendfile'
- }.compact
- end
-
- let(:include_workhorse_jwt_header) { true }
- let(:authorization) { }
- let(:pipeline) { create(:ci_empty_pipeline, project: project) }
+ context 'with projects' do
+ it_behaves_like 'LFS http requests' do
+ let_it_be(:other_project, reload: true) { create(:project, :empty_repo) }
- let(:sample_oid) { lfs_object.oid }
- let(:sample_size) { lfs_object.size }
- let(:sample_object) { { 'oid' => sample_oid, 'size' => sample_size } }
- let(:non_existing_object_oid) { '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897' }
- let(:non_existing_object_size) { 1575078 }
- let(:non_existing_object) { { 'oid' => non_existing_object_oid, 'size' => non_existing_object_size } }
- let(:multiple_objects) { [sample_object, non_existing_object] }
+ let(:container) { project }
+ let(:authorize_guest) { project.add_guest(user) }
+ let(:authorize_download) { project.add_reporter(user) }
+ let(:authorize_upload) { project.add_developer(user) }
- let(:lfs_enabled) { true }
+ context 'project specific LFS settings' do
+ let(:body) { upload_body(sample_object) }
- before do
- stub_lfs_setting(enabled: lfs_enabled)
- end
+ before do
+ authorize_upload
+ project.update_attribute(:lfs_enabled, project_lfs_enabled)
- context 'project specific LFS settings' do
- let(:body) { upload_body(sample_object) }
- let(:authorization) { authorize_user }
+ subject
+ end
- before do
- project.add_maintainer(user)
- project.update_attribute(:lfs_enabled, project_lfs_enabled)
+ describe 'LFS disabled in project' do
+ let(:project_lfs_enabled) { false }
- subject
- end
+ context 'when uploading' do
+ subject(:request) { post_lfs_json(batch_url(project), body, headers) }
- describe 'LFS disabled in project' do
- let(:project_lfs_enabled) { false }
+ it_behaves_like 'LFS http 404 response'
+ end
- context 'when uploading' do
- subject { post_lfs_json(batch_url(project), body, headers) }
+ context 'when downloading' do
+ subject(:request) { get(objects_url(project, sample_oid), params: {}, headers: headers) }
- it_behaves_like 'LFS http 404 response'
- end
+ it_behaves_like 'LFS http 404 response'
+ end
+ end
- context 'when downloading' do
- subject { get(objects_url(project, sample_oid), params: {}, headers: headers) }
+ describe 'LFS enabled in project' do
+ let(:project_lfs_enabled) { true }
- it_behaves_like 'LFS http 404 response'
- end
- end
+ context 'when uploading' do
+ subject(:request) { post_lfs_json(batch_url(project), body, headers) }
- describe 'LFS enabled in project' do
- let(:project_lfs_enabled) { true }
+ it_behaves_like 'LFS http 200 response'
+ end
- context 'when uploading' do
- subject { post_lfs_json(batch_url(project), body, headers) }
+ context 'when downloading' do
+ subject(:request) { get(objects_url(project, sample_oid), params: {}, headers: headers) }
- it_behaves_like 'LFS http 200 response'
+ it_behaves_like 'LFS http 200 blob response'
+ end
+ end
end
- context 'when downloading' do
- subject { get(objects_url(project, sample_oid), params: {}, headers: headers) }
+ describe 'when fetching LFS object' do
+ subject(:request) { get objects_url(project, sample_oid), params: {}, headers: headers }
- it_behaves_like 'LFS http 200 blob response'
- end
- end
- end
+ let(:response) { request && super() }
- describe 'when fetching LFS object' do
- let(:update_permissions) { }
- let(:before_get) { }
+ before do
+ project.lfs_objects << lfs_object
+ end
- before do
- project.lfs_objects << lfs_object
- update_permissions
- before_get
+ context 'when LFS uses object storage' do
+ before do
+ authorize_download
+ end
- get objects_url(project, sample_oid), params: {}, headers: headers
- end
+ context 'when proxy download is enabled' do
+ before do
+ stub_lfs_object_storage(proxy_download: true)
+ lfs_object.file.migrate!(LfsObjectUploader::Store::REMOTE)
+ end
- context 'when LFS uses object storage' do
- let(:authorization) { authorize_user }
+ it 'responds with the workhorse send-url' do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.headers[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("send-url:")
+ end
+ end
- let(:update_permissions) do
- project.add_maintainer(user)
- end
+ context 'when proxy download is disabled' do
+ before do
+ stub_lfs_object_storage(proxy_download: false)
+ lfs_object.file.migrate!(LfsObjectUploader::Store::REMOTE)
+ end
- context 'when proxy download is enabled' do
- let(:before_get) do
- stub_lfs_object_storage(proxy_download: true)
- lfs_object.file.migrate!(LfsObjectUploader::Store::REMOTE)
- end
+ it 'responds with redirect' do
+ expect(response).to have_gitlab_http_status(:found)
+ end
- it 'responds with the workhorse send-url' do
- expect(response).to have_gitlab_http_status(:ok)
- expect(response.headers[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("send-url:")
+ it 'responds with the file location' do
+ expect(response.location).to include(lfs_object.reload.file.path)
+ end
+ end
end
- end
- context 'when proxy download is disabled' do
- let(:before_get) do
- stub_lfs_object_storage(proxy_download: false)
- lfs_object.file.migrate!(LfsObjectUploader::Store::REMOTE)
- end
+ context 'when deploy key is authorized' do
+ let_it_be(:key) { create(:deploy_key) }
+ let(:authorization) { authorize_deploy_key }
- it 'responds with redirect' do
- expect(response).to have_gitlab_http_status(:found)
- end
+ before do
+ project.deploy_keys << key
+ end
- it 'responds with the file location' do
- expect(response.location).to include(lfs_object.reload.file.path)
+ it_behaves_like 'LFS http 200 blob response'
end
- end
- end
- context 'when deploy key is authorized' do
- let(:key) { create(:deploy_key) }
- let(:authorization) { authorize_deploy_key }
+ context 'when using a user key (LFSToken)' do
+ let(:authorization) { authorize_user_key }
- let(:update_permissions) do
- project.deploy_keys << key
- end
+ context 'when user allowed' do
+ before do
+ authorize_download
+ end
- it_behaves_like 'LFS http 200 blob response'
- end
+ it_behaves_like 'LFS http 200 blob response'
- context 'when using a user key (LFSToken)' do
- let(:authorization) { authorize_user_key }
+ context 'when user password is expired' do
+ let_it_be(:user) { create(:user, password_expires_at: 1.minute.ago)}
- context 'when user allowed' do
- let(:update_permissions) do
- project.add_maintainer(user)
- end
+ it_behaves_like 'LFS http 401 response'
+ end
- it_behaves_like 'LFS http 200 blob response'
+ context 'when user is blocked' do
+ let_it_be(:user) { create(:user, :blocked)}
- context 'when user password is expired' do
- let(:user) { create(:user, password_expires_at: 1.minute.ago)}
+ it_behaves_like 'LFS http 401 response'
+ end
+ end
- it_behaves_like 'LFS http 401 response'
+ context 'when user not allowed' do
+ it_behaves_like 'LFS http 404 response'
+ end
end
- context 'when user is blocked' do
- let(:user) { create(:user, :blocked)}
+ context 'when build is authorized as' do
+ let(:authorization) { authorize_ci_project }
+ let(:pipeline) { create(:ci_empty_pipeline, project: project) }
+ let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
- it_behaves_like 'LFS http 401 response'
- end
- end
+ shared_examples 'can download LFS only from own projects' do
+ context 'for owned project' do
+ let_it_be(:project) { create(:project, namespace: user.namespace) }
- context 'when user not allowed' do
- it_behaves_like 'LFS http 404 response'
- end
- end
+ it_behaves_like 'LFS http 200 blob response'
+ end
- context 'when build is authorized as' do
- let(:authorization) { authorize_ci_project }
+ context 'for member of project' do
+ before do
+ authorize_download
+ end
- shared_examples 'can download LFS only from own projects' do
- context 'for owned project' do
- let(:project) { create(:project, namespace: user.namespace) }
+ it_behaves_like 'LFS http 200 blob response'
+ end
- it_behaves_like 'LFS http 200 blob response'
- end
+ context 'for other project' do
+ let(:pipeline) { create(:ci_empty_pipeline, project: other_project) }
- context 'for member of project' do
- let(:pipeline) { create(:ci_empty_pipeline, project: project) }
+ it 'rejects downloading code' do
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+
+ context 'administrator', :enable_admin_mode do
+ let_it_be(:user) { create(:admin) }
- let(:update_permissions) do
- project.add_reporter(user)
+ it_behaves_like 'can download LFS only from own projects'
end
- it_behaves_like 'LFS http 200 blob response'
- end
+ context 'regular user' do
+ it_behaves_like 'can download LFS only from own projects'
+ end
- context 'for other project' do
- let(:pipeline) { create(:ci_empty_pipeline, project: other_project) }
+ context 'does not have user' do
+ let(:build) { create(:ci_build, :running, pipeline: pipeline) }
- it 'rejects downloading code' do
- expect(response).to have_gitlab_http_status(:not_found)
+ it_behaves_like 'can download LFS only from own projects'
end
end
end
- context 'administrator' do
- let(:user) { create(:admin) }
- let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
-
- it_behaves_like 'can download LFS only from own projects'
- end
+ describe 'when handling LFS batch request' do
+ subject(:request) { post_lfs_json batch_url(project), body, headers }
- context 'regular user' do
- let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
+ let(:response) { request && super() }
+ let(:lfs_chunked_encoding) { true }
- it_behaves_like 'can download LFS only from own projects'
- end
+ before do
+ stub_feature_flags(lfs_chunked_encoding: lfs_chunked_encoding)
+ project.lfs_objects << lfs_object
+ end
- context 'does not have user' do
- let(:build) { create(:ci_build, :running, pipeline: pipeline) }
+ shared_examples 'process authorization header' do |renew_authorization:|
+ let(:response_authorization) do
+ authorization_in_action(lfs_actions.first)
+ end
- it_behaves_like 'can download LFS only from own projects'
- end
- end
- end
+ if renew_authorization
+ context 'when the authorization comes from a user' do
+ it 'returns a new valid LFS token authorization' do
+ expect(response_authorization).not_to eq(authorization)
+ end
- describe 'when handling LFS batch request' do
- let(:update_lfs_permissions) { }
- let(:update_user_permissions) { }
+ it 'returns a valid token' do
+ username, token = ::Base64.decode64(response_authorization.split(' ', 2).last).split(':', 2)
- before do
- update_lfs_permissions
- update_user_permissions
- post_lfs_json batch_url(project), body, headers
- end
+ expect(username).to eq(user.username)
+ expect(Gitlab::LfsToken.new(user).token_valid?(token)).to be_truthy
+ end
- shared_examples 'process authorization header' do |renew_authorization:|
- let(:response_authorization) do
- authorization_in_action(lfs_actions.first)
- end
+ it 'generates only one new token per each request' do
+ authorizations = lfs_actions.map do |action|
+ authorization_in_action(action)
+ end.compact
- if renew_authorization
- context 'when the authorization comes from a user' do
- it 'returns a new valid LFS token authorization' do
- expect(response_authorization).not_to eq(authorization)
+ expect(authorizations.uniq.count).to eq 1
+ end
+ end
+ else
+ context 'when the authorization comes from a token' do
+ it 'returns the same authorization header' do
+ expect(response_authorization).to eq(authorization)
+ end
+ end
end
- it 'returns a a valid token' do
- username, token = ::Base64.decode64(response_authorization.split(' ', 2).last).split(':', 2)
-
- expect(username).to eq(user.username)
- expect(Gitlab::LfsToken.new(user).token_valid?(token)).to be_truthy
+ def lfs_actions
+ json_response['objects'].map { |a| a['actions'] }.compact
end
- it 'generates only one new token per each request' do
- authorizations = lfs_actions.map do |action|
- authorization_in_action(action)
- end.compact
-
- expect(authorizations.uniq.count).to eq 1
+ def authorization_in_action(action)
+ (action['upload'] || action['download']).dig('header', 'Authorization')
end
end
- else
- context 'when the authorization comes from a token' do
- it 'returns the same authorization header' do
- expect(response_authorization).to eq(authorization)
- end
- end
- end
-
- def lfs_actions
- json_response['objects'].map { |a| a['actions'] }.compact
- end
- def authorization_in_action(action)
- (action['upload'] || action['download']).dig('header', 'Authorization')
- end
- end
+ describe 'download' do
+ let(:body) { download_body(sample_object) }
- describe 'download' do
- let(:body) { download_body(sample_object) }
+ shared_examples 'an authorized request' do |renew_authorization:|
+ context 'when downloading an LFS object that is assigned to our project' do
+ it_behaves_like 'LFS http 200 response'
- shared_examples 'an authorized request' do |renew_authorization:|
- context 'when downloading an LFS object that is assigned to our project' do
- let(:update_lfs_permissions) do
- project.lfs_objects << lfs_object
- end
+ it 'with href to download' do
+ expect(json_response['objects'].first).to include(sample_object)
+ expect(json_response['objects'].first['actions']['download']['href']).to eq(objects_url(project, sample_oid))
+ end
- it_behaves_like 'LFS http 200 response'
+ it_behaves_like 'process authorization header', renew_authorization: renew_authorization
+ end
- it 'with href to download' do
- expect(json_response['objects'].first).to include(sample_object)
- expect(json_response['objects'].first['actions']['download']['href']).to eq(objects_url(project, sample_oid))
- end
+ context 'when downloading an LFS object that is assigned to other project' do
+ before do
+ lfs_object.update!(projects: [other_project])
+ end
- it_behaves_like 'process authorization header', renew_authorization: renew_authorization
- end
+ it_behaves_like 'LFS http 200 response'
- context 'when downloading an LFS object that is assigned to other project' do
- let(:update_lfs_permissions) do
- other_project.lfs_objects << lfs_object
- end
+ it 'with an 404 for specific object' do
+ expect(json_response['objects'].first).to include(sample_object)
+ expect(json_response['objects'].first['error']).to include('code' => 404, 'message' => "Object does not exist on the server or you don't have permissions to access it")
+ end
+ end
- it_behaves_like 'LFS http 200 response'
+ context 'when downloading a LFS object that does not exist' do
+ let(:body) { download_body(non_existing_object) }
- it 'with an 404 for specific object' do
- expect(json_response['objects'].first).to include(sample_object)
- expect(json_response['objects'].first['error']).to include('code' => 404, 'message' => "Object does not exist on the server or you don't have permissions to access it")
- end
- end
+ it_behaves_like 'LFS http 200 response'
- context 'when downloading a LFS object that does not exist' do
- let(:body) { download_body(non_existing_object) }
+ it 'with an 404 for specific object' do
+ expect(json_response['objects'].first).to include(non_existing_object)
+ expect(json_response['objects'].first['error']).to include('code' => 404, 'message' => "Object does not exist on the server or you don't have permissions to access it")
+ end
+ end
- it_behaves_like 'LFS http 200 response'
+ context 'when downloading one existing and one missing LFS object' do
+ let(:body) { download_body(multiple_objects) }
- it 'with an 404 for specific object' do
- expect(json_response['objects'].first).to include(non_existing_object)
- expect(json_response['objects'].first['error']).to include('code' => 404, 'message' => "Object does not exist on the server or you don't have permissions to access it")
- end
- end
+ it_behaves_like 'LFS http 200 response'
- context 'when downloading one new and one existing LFS object' do
- let(:body) { download_body(multiple_objects) }
- let(:update_lfs_permissions) do
- project.lfs_objects << lfs_object
- end
+ it 'responds with download hypermedia link for the existing object' do
+ expect(json_response['objects'].first).to include(sample_object)
+ expect(json_response['objects'].first['actions']['download']).to include('href' => objects_url(project, sample_oid))
+ expect(json_response['objects'].last).to eq({
+ 'oid' => non_existing_object_oid,
+ 'size' => non_existing_object_size,
+ 'error' => {
+ 'code' => 404,
+ 'message' => "Object does not exist on the server or you don't have permissions to access it"
+ }
+ })
+ end
- it_behaves_like 'LFS http 200 response'
+ it_behaves_like 'process authorization header', renew_authorization: renew_authorization
+ end
- it 'responds with download hypermedia link for the new object' do
- expect(json_response['objects'].first).to include(sample_object)
- expect(json_response['objects'].first['actions']['download']).to include('href' => objects_url(project, sample_oid))
- expect(json_response['objects'].last).to eq({
- 'oid' => non_existing_object_oid,
- 'size' => non_existing_object_size,
- 'error' => {
- 'code' => 404,
- 'message' => "Object does not exist on the server or you don't have permissions to access it"
- }
- })
- end
+ context 'when downloading two existing LFS objects' do
+ let(:body) { download_body(multiple_objects) }
+ let(:other_object) { create(:lfs_object, :with_file, oid: non_existing_object_oid, size: non_existing_object_size) }
- it_behaves_like 'process authorization header', renew_authorization: renew_authorization
- end
+ before do
+ project.lfs_objects << other_object
+ end
- context 'when downloading two existing LFS objects' do
- let(:body) { download_body(multiple_objects) }
- let(:other_object) { create(:lfs_object, :with_file, oid: non_existing_object_oid, size: non_existing_object_size) }
- let(:update_lfs_permissions) do
- project.lfs_objects << [lfs_object, other_object]
- end
+ it 'responds with the download hypermedia link for each object' do
+ expect(json_response['objects'].first).to include(sample_object)
+ expect(json_response['objects'].first['actions']['download']).to include('href' => objects_url(project, sample_oid))
- it 'responds with the download hypermedia link for each object' do
- expect(json_response['objects'].first).to include(sample_object)
- expect(json_response['objects'].first['actions']['download']).to include('href' => objects_url(project, sample_oid))
+ expect(json_response['objects'].last).to include(non_existing_object)
+ expect(json_response['objects'].last['actions']['download']).to include('href' => objects_url(project, non_existing_object_oid))
+ end
- expect(json_response['objects'].last).to include(non_existing_object)
- expect(json_response['objects'].last['actions']['download']).to include('href' => objects_url(project, non_existing_object_oid))
+ it_behaves_like 'process authorization header', renew_authorization: renew_authorization
+ end
end
- it_behaves_like 'process authorization header', renew_authorization: renew_authorization
- end
- end
+ context 'when user is authenticated' do
+ before do
+ project.add_role(user, role) if role
+ end
- context 'when user is authenticated' do
- let(:authorization) { authorize_user }
+ it_behaves_like 'an authorized request', renew_authorization: true do
+ let(:role) { :reporter }
+ end
- let(:update_user_permissions) do
- project.add_role(user, role)
- end
+ context 'when user is not a member of the project' do
+ let(:role) { nil }
- it_behaves_like 'an authorized request', renew_authorization: true do
- let(:role) { :reporter }
- end
+ it_behaves_like 'LFS http 404 response'
+ end
- context 'when user is not a member of the project' do
- let(:update_user_permissions) { nil }
+ context 'when user does not have download access' do
+ let(:role) { :guest }
- it_behaves_like 'LFS http 404 response'
- end
+ it_behaves_like 'LFS http 404 response'
+ end
- context 'when user does not have download access' do
- let(:role) { :guest }
+ context 'when user password is expired' do
+ let_it_be(:user) { create(:user, password_expires_at: 1.minute.ago)}
+ let(:role) { :reporter}
- it_behaves_like 'LFS http 404 response'
- end
+ # TODO: This should return a 404 response
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/292006
+ it_behaves_like 'LFS http 200 response'
+ end
- context 'when user password is expired' do
- let(:role) { :reporter}
- let(:user) { create(:user, password_expires_at: 1.minute.ago)}
+ context 'when user is blocked' do
+ let_it_be(:user) { create(:user, :blocked)}
+ let(:role) { :reporter}
- it 'with an 404 for specific object' do
- expect(json_response['objects'].first).to include(sample_object)
- expect(json_response['objects'].first['error']).to include('code' => 404, 'message' => "Object does not exist on the server or you don't have permissions to access it")
+ it_behaves_like 'LFS http 401 response'
+ end
end
- end
- context 'when user is blocked' do
- let(:role) { :reporter}
- let(:user) { create(:user, :blocked)}
+ context 'when using Deploy Tokens' do
+ let(:authorization) { authorize_deploy_token }
- it_behaves_like 'LFS http 401 response'
- end
- end
+ context 'when Deploy Token is not valid' do
+ let(:deploy_token) { create(:deploy_token, projects: [project], read_repository: false) }
- context 'when using Deploy Tokens' do
- let(:authorization) { authorize_deploy_token }
- let(:update_user_permissions) { nil }
- let(:role) { nil }
- let(:update_lfs_permissions) do
- project.lfs_objects << lfs_object
- end
+ it_behaves_like 'LFS http 401 response'
+ end
- context 'when Deploy Token is not valid' do
- let(:deploy_token) { create(:deploy_token, projects: [project], read_repository: false) }
+ context 'when Deploy Token is not related to the project' do
+ let(:deploy_token) { create(:deploy_token, projects: [other_project]) }
- it_behaves_like 'LFS http 401 response'
- end
+ it_behaves_like 'LFS http 401 response'
+ end
- context 'when Deploy Token is not related to the project' do
- let(:deploy_token) { create(:deploy_token, projects: [other_project]) }
+ # TODO: We should fix this test case that causes flakyness by alternating the result of the above test cases.
+ context 'when Deploy Token is valid' do
+ let(:deploy_token) { create(:deploy_token, projects: [project]) }
- it_behaves_like 'LFS http 401 response'
- end
+ it_behaves_like 'an authorized request', renew_authorization: false
+ end
+ end
- # TODO: We should fix this test case that causes flakyness by alternating the result of the above test cases.
- context 'when Deploy Token is valid' do
- let(:deploy_token) { create(:deploy_token, projects: [project]) }
+ context 'when build is authorized as' do
+ let(:authorization) { authorize_ci_project }
- it_behaves_like 'an authorized request', renew_authorization: false
- end
- end
+ shared_examples 'can download LFS only from own projects' do |renew_authorization:|
+ context 'for own project' do
+ let(:pipeline) { create(:ci_empty_pipeline, project: project) }
- context 'when build is authorized as' do
- let(:authorization) { authorize_ci_project }
+ before do
+ authorize_download
+ end
- let(:update_lfs_permissions) do
- project.lfs_objects << lfs_object
- end
+ it_behaves_like 'an authorized request', renew_authorization: renew_authorization
+ end
- shared_examples 'can download LFS only from own projects' do |renew_authorization:|
- context 'for own project' do
- let(:pipeline) { create(:ci_empty_pipeline, project: project) }
+ context 'for other project' do
+ let(:pipeline) { create(:ci_empty_pipeline, project: other_project) }
- let(:update_user_permissions) do
- project.add_reporter(user)
+ it 'rejects downloading code' do
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
end
- it_behaves_like 'an authorized request', renew_authorization: renew_authorization
- end
-
- context 'for other project' do
- let(:pipeline) { create(:ci_empty_pipeline, project: other_project) }
+ context 'administrator', :enable_admin_mode do
+ let_it_be(:user) { create(:admin) }
+ let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
- it 'rejects downloading code' do
- expect(response).to have_gitlab_http_status(:not_found)
+ it_behaves_like 'can download LFS only from own projects', renew_authorization: true
end
- end
- end
- context 'administrator' do
- let(:user) { create(:admin) }
- let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
-
- it_behaves_like 'can download LFS only from own projects', renew_authorization: true
- end
+ context 'regular user' do
+ let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
- context 'regular user' do
- let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
-
- it_behaves_like 'can download LFS only from own projects', renew_authorization: true
- end
+ it_behaves_like 'can download LFS only from own projects', renew_authorization: true
+ end
- context 'does not have user' do
- let(:build) { create(:ci_build, :running, pipeline: pipeline) }
+ context 'does not have user' do
+ let(:build) { create(:ci_build, :running, pipeline: pipeline) }
- it_behaves_like 'can download LFS only from own projects', renew_authorization: false
- end
- end
+ it_behaves_like 'can download LFS only from own projects', renew_authorization: false
+ end
+ end
- context 'when user is not authenticated' do
- describe 'is accessing public project' do
- let(:project) { create(:project, :public) }
+ context 'when user is not authenticated' do
+ let(:authorization) { nil }
- let(:update_lfs_permissions) do
- project.lfs_objects << lfs_object
- end
+ describe 'is accessing public project' do
+ let_it_be(:project) { create(:project, :public) }
- it_behaves_like 'LFS http 200 response'
+ it_behaves_like 'LFS http 200 response'
- it 'returns href to download' do
- expect(json_response).to eq({
- 'objects' => [
- {
- 'oid' => sample_oid,
- 'size' => sample_size,
- 'authenticated' => true,
- 'actions' => {
- 'download' => {
- 'href' => objects_url(project, sample_oid),
- 'header' => {}
+ it 'returns href to download' do
+ expect(json_response).to eq({
+ 'objects' => [
+ {
+ 'oid' => sample_oid,
+ 'size' => sample_size,
+ 'authenticated' => true,
+ 'actions' => {
+ 'download' => {
+ 'href' => objects_url(project, sample_oid),
+ 'header' => {}
+ }
+ }
}
- }
- }
- ]
- })
- end
- end
+ ]
+ })
+ end
+ end
- describe 'is accessing non-public project' do
- let(:update_lfs_permissions) do
- project.lfs_objects << lfs_object
+ describe 'is accessing non-public project' do
+ it_behaves_like 'LFS http 401 response'
+ end
end
-
- it_behaves_like 'LFS http 401 response'
end
- end
- end
-
- describe 'upload' do
- let(:project) { create(:project, :public) }
- let(:body) { upload_body(sample_object) }
- shared_examples 'pushes new LFS objects' do |renew_authorization:|
- let(:sample_size) { 150.megabytes }
- let(:sample_oid) { non_existing_object_oid }
+ describe 'upload' do
+ let_it_be(:project) { create(:project, :public) }
+ let(:body) { upload_body(sample_object) }
- it_behaves_like 'LFS http 200 response'
+ shared_examples 'pushes new LFS objects' do |renew_authorization:|
+ let(:sample_size) { 150.megabytes }
+ let(:sample_oid) { non_existing_object_oid }
- it 'responds with upload hypermedia link' do
- expect(json_response['objects']).to be_kind_of(Array)
- expect(json_response['objects'].first).to include(sample_object)
- expect(json_response['objects'].first['actions']['upload']['href']).to eq(objects_url(project, sample_oid, sample_size))
- expect(json_response['objects'].first['actions']['upload']['header']).to include('Content-Type' => 'application/octet-stream')
- end
-
- it_behaves_like 'process authorization header', renew_authorization: renew_authorization
- end
+ it_behaves_like 'LFS http 200 response'
- describe 'when request is authenticated' do
- describe 'when user has project push access' do
- let(:authorization) { authorize_user }
+ it 'responds with upload hypermedia link' do
+ expect(json_response['objects']).to be_kind_of(Array)
+ expect(json_response['objects'].first).to include(sample_object)
+ expect(json_response['objects'].first['actions']['upload']['href']).to eq(objects_url(project, sample_oid, sample_size))
- let(:update_user_permissions) do
- project.add_developer(user)
- end
+ headers = json_response['objects'].first['actions']['upload']['header']
+ expect(headers['Content-Type']).to eq('application/octet-stream')
+ expect(headers['Transfer-Encoding']).to eq('chunked')
+ end
- context 'when pushing an LFS object that already exists' do
- shared_examples_for 'batch upload with existing LFS object' do
- it_behaves_like 'LFS http 200 response'
+ context 'when lfs_chunked_encoding feature is disabled' do
+ let(:lfs_chunked_encoding) { false }
- it 'responds with links the object to the project' do
+ it 'responds with upload hypermedia link' do
expect(json_response['objects']).to be_kind_of(Array)
expect(json_response['objects'].first).to include(sample_object)
- expect(lfs_object.projects.pluck(:id)).not_to include(project.id)
- expect(lfs_object.projects.pluck(:id)).to include(other_project.id)
expect(json_response['objects'].first['actions']['upload']['href']).to eq(objects_url(project, sample_oid, sample_size))
- expect(json_response['objects'].first['actions']['upload']['header']).to include('Content-Type' => 'application/octet-stream')
- end
- it_behaves_like 'process authorization header', renew_authorization: true
+ headers = json_response['objects'].first['actions']['upload']['header']
+ expect(headers['Content-Type']).to eq('application/octet-stream')
+ expect(headers['Transfer-Encoding']).to be_nil
+ end
end
- let(:update_lfs_permissions) do
- other_project.lfs_objects << lfs_object
- end
+ it_behaves_like 'process authorization header', renew_authorization: renew_authorization
+ end
- context 'in another project' do
- it_behaves_like 'batch upload with existing LFS object'
- end
+ describe 'when request is authenticated' do
+ describe 'when user has project push access' do
+ before do
+ authorize_upload
+ end
- context 'in source of fork project' do
- let(:project) { fork_project(other_project) }
+ context 'when pushing an LFS object that already exists' do
+ shared_examples_for 'batch upload with existing LFS object' do
+ it_behaves_like 'LFS http 200 response'
- it_behaves_like 'batch upload with existing LFS object'
- end
- end
+ it 'responds with links to the object in the project' do
+ expect(json_response['objects']).to be_kind_of(Array)
+ expect(json_response['objects'].first).to include(sample_object)
+ expect(lfs_object.projects.pluck(:id)).not_to include(project.id)
+ expect(lfs_object.projects.pluck(:id)).to include(other_project.id)
+ expect(json_response['objects'].first['actions']['upload']['href']).to eq(objects_url(project, sample_oid, sample_size))
- context 'when pushing a LFS object that does not exist' do
- it_behaves_like 'pushes new LFS objects', renew_authorization: true
- end
+ headers = json_response['objects'].first['actions']['upload']['header']
+ expect(headers['Content-Type']).to eq('application/octet-stream')
+ expect(headers['Transfer-Encoding']).to eq('chunked')
+ end
- context 'when pushing one new and one existing LFS object' do
- let(:body) { upload_body(multiple_objects) }
- let(:update_lfs_permissions) do
- project.lfs_objects << lfs_object
- end
+ it_behaves_like 'process authorization header', renew_authorization: true
+ end
- it_behaves_like 'LFS http 200 response'
+ context 'in another project' do
+ before do
+ lfs_object.update!(projects: [other_project])
+ end
- it 'responds with upload hypermedia link for the new object' do
- expect(json_response['objects']).to be_kind_of(Array)
+ it_behaves_like 'batch upload with existing LFS object'
+ end
- expect(json_response['objects'].first).to include(sample_object)
- expect(json_response['objects'].first).not_to have_key('actions')
+ context 'in source of fork project' do
+ let(:project) { fork_project(other_project) }
- expect(json_response['objects'].last).to include(non_existing_object)
- expect(json_response['objects'].last['actions']['upload']['href']).to eq(objects_url(project, non_existing_object_oid, non_existing_object_size))
- expect(json_response['objects'].last['actions']['upload']['header']).to include('Content-Type' => 'application/octet-stream')
- end
+ before do
+ lfs_object.update!(projects: [other_project])
+ end
- it_behaves_like 'process authorization header', renew_authorization: true
- end
- end
+ it_behaves_like 'batch upload with existing LFS object'
+ end
+ end
- context 'when user does not have push access' do
- let(:authorization) { authorize_user }
+ context 'when pushing a LFS object that does not exist' do
+ it_behaves_like 'pushes new LFS objects', renew_authorization: true
+ end
- it_behaves_like 'LFS http 403 response'
- end
+ context 'when pushing one new and one existing LFS object' do
+ let(:body) { upload_body(multiple_objects) }
- context 'when build is authorized' do
- let(:authorization) { authorize_ci_project }
+ it_behaves_like 'LFS http 200 response'
- context 'build has an user' do
- let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
+ it 'responds with upload hypermedia link for the new object' do
+ expect(json_response['objects']).to be_kind_of(Array)
- context 'tries to push to own project' do
- it_behaves_like 'LFS http 403 response'
- end
+ expect(json_response['objects'].first).to include(sample_object)
+ expect(json_response['objects'].first).not_to have_key('actions')
- context 'tries to push to other project' do
- let(:pipeline) { create(:ci_empty_pipeline, project: other_project) }
+ expect(json_response['objects'].last).to include(non_existing_object)
+ expect(json_response['objects'].last['actions']['upload']['href']).to eq(objects_url(project, non_existing_object_oid, non_existing_object_size))
+
+ headers = json_response['objects'].last['actions']['upload']['header']
+ expect(headers['Content-Type']).to eq('application/octet-stream')
+ expect(headers['Transfer-Encoding']).to eq('chunked')
+ end
+
+ it_behaves_like 'process authorization header', renew_authorization: true
+ end
+ end
- # I'm not sure what this tests that is different from the previous test
+ context 'when user does not have push access' do
it_behaves_like 'LFS http 403 response'
end
- end
- context 'does not have user' do
- let(:build) { create(:ci_build, :running, pipeline: pipeline) }
+ context 'when build is authorized' do
+ let(:authorization) { authorize_ci_project }
+ let(:pipeline) { create(:ci_empty_pipeline, project: project) }
- it_behaves_like 'LFS http 403 response'
- end
- end
+ context 'build has an user' do
+ let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
- context 'when deploy key has project push access' do
- let(:key) { create(:deploy_key) }
- let(:authorization) { authorize_deploy_key }
+ context 'tries to push to own project' do
+ it_behaves_like 'LFS http 403 response'
+ end
- let(:update_user_permissions) do
- project.deploy_keys_projects.create!(deploy_key: key, can_push: true)
- end
+ context 'tries to push to other project' do
+ let(:pipeline) { create(:ci_empty_pipeline, project: other_project) }
- it_behaves_like 'pushes new LFS objects', renew_authorization: false
- end
- end
+ # I'm not sure what this tests that is different from the previous test
+ it_behaves_like 'LFS http 403 response'
+ end
+ end
- context 'when user is not authenticated' do
- context 'when user has push access' do
- let(:update_user_permissions) do
- project.add_maintainer(user)
- end
+ context 'does not have user' do
+ let(:build) { create(:ci_build, :running, pipeline: pipeline) }
- it_behaves_like 'LFS http 401 response'
- end
+ it_behaves_like 'LFS http 403 response'
+ end
+ end
- context 'when user does not have push access' do
- it_behaves_like 'LFS http 401 response'
- end
- end
- end
+ context 'when deploy key has project push access' do
+ let(:key) { create(:deploy_key) }
+ let(:authorization) { authorize_deploy_key }
- describe 'unsupported' do
- let(:authorization) { authorize_user }
- let(:body) { request_body('other', sample_object) }
+ before do
+ project.deploy_keys_projects.create!(deploy_key: key, can_push: true)
+ end
- it_behaves_like 'LFS http 404 response'
- end
- end
+ it_behaves_like 'pushes new LFS objects', renew_authorization: false
+ end
+ end
- describe 'when handling LFS batch request on a read-only GitLab instance' do
- let(:authorization) { authorize_user }
+ context 'when user is not authenticated' do
+ let(:authorization) { nil }
- subject { post_lfs_json(batch_url(project), body, headers) }
+ context 'when user has push access' do
+ before do
+ authorize_upload
+ end
- before do
- allow(Gitlab::Database).to receive(:read_only?) { true }
+ it_behaves_like 'LFS http 401 response'
+ end
- project.add_maintainer(user)
+ context 'when user does not have push access' do
+ it_behaves_like 'LFS http 401 response'
+ end
+ end
+ end
- subject
- end
+ describe 'unsupported' do
+ let(:body) { request_body('other', sample_object) }
- context 'when downloading' do
- let(:body) { download_body(sample_object) }
+ it_behaves_like 'LFS http 404 response'
+ end
+ end
- it_behaves_like 'LFS http 200 response'
- end
+ describe 'when handling LFS batch request on a read-only GitLab instance' do
+ subject { post_lfs_json(batch_url(project), body, headers) }
- context 'when uploading' do
- let(:body) { upload_body(sample_object) }
+ before do
+ allow(Gitlab::Database).to receive(:read_only?) { true }
- it_behaves_like 'LFS http expected response code and message' do
- let(:response_code) { 403 }
- let(:message) { 'You cannot write to this read-only GitLab instance.' }
- end
- end
- end
+ project.add_maintainer(user)
- describe 'when pushing a LFS object' do
- shared_examples 'unauthorized' do
- context 'and request is sent by gitlab-workhorse to authorize the request' do
- before do
- put_authorize
+ subject
end
- it_behaves_like 'LFS http 401 response'
- end
+ context 'when downloading' do
+ let(:body) { download_body(sample_object) }
- context 'and request is sent by gitlab-workhorse to finalize the upload' do
- before do
- put_finalize
+ it_behaves_like 'LFS http 200 response'
end
- it_behaves_like 'LFS http 401 response'
- end
+ context 'when uploading' do
+ let(:body) { upload_body(sample_object) }
- context 'and request is sent with a malformed headers' do
- before do
- put_finalize('/etc/passwd')
+ it_behaves_like 'LFS http expected response code and message' do
+ let(:response_code) { 403 }
+ let(:message) { 'You cannot write to this read-only GitLab instance.' }
+ end
end
-
- it_behaves_like 'LFS http 401 response'
end
- end
- shared_examples 'forbidden' do
- context 'and request is sent by gitlab-workhorse to authorize the request' do
- before do
- put_authorize
- end
+ describe 'when pushing a LFS object' do
+ let(:include_workhorse_jwt_header) { true }
- it_behaves_like 'LFS http 403 response'
- end
+ shared_examples 'unauthorized' do
+ context 'and request is sent by gitlab-workhorse to authorize the request' do
+ before do
+ put_authorize
+ end
- context 'and request is sent by gitlab-workhorse to finalize the upload' do
- before do
- put_finalize
- end
+ it_behaves_like 'LFS http 401 response'
+ end
- it_behaves_like 'LFS http 403 response'
- end
+ context 'and request is sent by gitlab-workhorse to finalize the upload' do
+ before do
+ put_finalize
+ end
- context 'and request is sent with a malformed headers' do
- before do
- put_finalize('/etc/passwd')
+ it_behaves_like 'LFS http 401 response'
+ end
+
+ context 'and request is sent with a malformed headers' do
+ before do
+ put_finalize('/etc/passwd')
+ end
+
+ it_behaves_like 'LFS http 401 response'
+ end
end
- it_behaves_like 'LFS http 403 response'
- end
- end
+ shared_examples 'forbidden' do
+ context 'and request is sent by gitlab-workhorse to authorize the request' do
+ before do
+ put_authorize
+ end
- describe 'to one project' do
- describe 'when user is authenticated' do
- let(:authorization) { authorize_user }
+ it_behaves_like 'LFS http 403 response'
+ end
- describe 'when user has push access to the project' do
- before do
- project.add_developer(user)
+ context 'and request is sent by gitlab-workhorse to finalize the upload' do
+ before do
+ put_finalize
+ end
+
+ it_behaves_like 'LFS http 403 response'
end
- context 'and the request bypassed workhorse' do
- it 'raises an exception' do
- expect { put_authorize(verified: false) }.to raise_error JWT::DecodeError
+ context 'and request is sent with a malformed headers' do
+ before do
+ put_finalize('/etc/passwd')
end
+
+ it_behaves_like 'LFS http 403 response'
end
+ end
- context 'and request is sent by gitlab-workhorse to authorize the request' do
- shared_examples 'a valid response' do
+ describe 'to one project' do
+ describe 'when user is authenticated' do
+ describe 'when user has push access to the project' do
before do
- put_authorize
+ project.add_developer(user)
end
- it_behaves_like 'LFS http 200 workhorse response'
- end
-
- shared_examples 'a local file' do
- it_behaves_like 'a valid response' do
- it 'responds with status 200, location of LFS store and object details' do
- expect(json_response['TempPath']).to eq(LfsObjectUploader.workhorse_local_upload_path)
- expect(json_response['RemoteObject']).to be_nil
- expect(json_response['LfsOid']).to eq(sample_oid)
- expect(json_response['LfsSize']).to eq(sample_size)
+ context 'and the request bypassed workhorse' do
+ it 'raises an exception' do
+ expect { put_authorize(verified: false) }.to raise_error JWT::DecodeError
end
end
- end
- context 'when using local storage' do
- it_behaves_like 'a local file'
- end
+ context 'and request is sent by gitlab-workhorse to authorize the request' do
+ shared_examples 'a valid response' do
+ before do
+ put_authorize
+ end
- context 'when using remote storage' do
- context 'when direct upload is enabled' do
- before do
- stub_lfs_object_storage(enabled: true, direct_upload: true)
+ it_behaves_like 'LFS http 200 workhorse response'
end
- it_behaves_like 'a valid response' do
- it 'responds with status 200, location of LFS remote store and object details' do
- expect(json_response).not_to have_key('TempPath')
- expect(json_response['RemoteObject']).to have_key('ID')
- expect(json_response['RemoteObject']).to have_key('GetURL')
- expect(json_response['RemoteObject']).to have_key('StoreURL')
- expect(json_response['RemoteObject']).to have_key('DeleteURL')
- expect(json_response['RemoteObject']).not_to have_key('MultipartUpload')
- expect(json_response['LfsOid']).to eq(sample_oid)
- expect(json_response['LfsSize']).to eq(sample_size)
+ shared_examples 'a local file' do
+ it_behaves_like 'a valid response' do
+ it 'responds with status 200, location of LFS store and object details' do
+ expect(json_response['TempPath']).to eq(LfsObjectUploader.workhorse_local_upload_path)
+ expect(json_response['RemoteObject']).to be_nil
+ expect(json_response['LfsOid']).to eq(sample_oid)
+ expect(json_response['LfsSize']).to eq(sample_size)
+ end
end
end
- end
- context 'when direct upload is disabled' do
- before do
- stub_lfs_object_storage(enabled: true, direct_upload: false)
+ context 'when using local storage' do
+ it_behaves_like 'a local file'
end
- it_behaves_like 'a local file'
- end
- end
- end
+ context 'when using remote storage' do
+ context 'when direct upload is enabled' do
+ before do
+ stub_lfs_object_storage(enabled: true, direct_upload: true)
+ end
- context 'and request is sent by gitlab-workhorse to finalize the upload' do
- before do
- put_finalize
- end
+ it_behaves_like 'a valid response' do
+ it 'responds with status 200, location of LFS remote store and object details' do
+ expect(json_response).not_to have_key('TempPath')
+ expect(json_response['RemoteObject']).to have_key('ID')
+ expect(json_response['RemoteObject']).to have_key('GetURL')
+ expect(json_response['RemoteObject']).to have_key('StoreURL')
+ expect(json_response['RemoteObject']).to have_key('DeleteURL')
+ expect(json_response['RemoteObject']).not_to have_key('MultipartUpload')
+ expect(json_response['LfsOid']).to eq(sample_oid)
+ expect(json_response['LfsSize']).to eq(sample_size)
+ end
+ end
+ end
- it_behaves_like 'LFS http 200 response'
+ context 'when direct upload is disabled' do
+ before do
+ stub_lfs_object_storage(enabled: true, direct_upload: false)
+ end
- it 'LFS object is linked to the project' do
- expect(lfs_object.projects.pluck(:id)).to include(project.id)
- end
- end
+ it_behaves_like 'a local file'
+ end
+ end
+ end
- context 'and request to finalize the upload is not sent by gitlab-workhorse' do
- it 'fails with a JWT decode error' do
- expect { put_finalize(lfs_tmp_file, verified: false) }.to raise_error(JWT::DecodeError)
- end
- end
+ context 'and request is sent by gitlab-workhorse to finalize the upload' do
+ before do
+ put_finalize
+ end
- context 'and workhorse requests upload finalize for a new LFS object' do
- before do
- lfs_object.destroy!
- end
+ it_behaves_like 'LFS http 200 response'
- context 'with object storage disabled' do
- it "doesn't attempt to migrate file to object storage" do
- expect(ObjectStorage::BackgroundMoveWorker).not_to receive(:perform_async)
+ it 'LFS object is linked to the project' do
+ expect(lfs_object.projects.pluck(:id)).to include(project.id)
+ end
+ end
- put_finalize(with_tempfile: true)
+ context 'and request to finalize the upload is not sent by gitlab-workhorse' do
+ it 'fails with a JWT decode error' do
+ expect { put_finalize(lfs_tmp_file, verified: false) }.to raise_error(JWT::DecodeError)
+ end
end
- end
- context 'with object storage enabled' do
- context 'and direct upload enabled' do
- let!(:fog_connection) do
- stub_lfs_object_storage(direct_upload: true)
+ context 'and workhorse requests upload finalize for a new LFS object' do
+ before do
+ lfs_object.destroy!
end
- let(:tmp_object) do
- fog_connection.directories.new(key: 'lfs-objects').files.create( # rubocop: disable Rails/SaveBang
- key: 'tmp/uploads/12312300',
- body: 'content'
- )
+ context 'with object storage disabled' do
+ it "doesn't attempt to migrate file to object storage" do
+ expect(ObjectStorage::BackgroundMoveWorker).not_to receive(:perform_async)
+
+ put_finalize(with_tempfile: true)
+ end
end
- ['123123', '../../123123'].each do |remote_id|
- context "with invalid remote_id: #{remote_id}" do
- subject do
- put_finalize(remote_object: tmp_object, args: {
- 'file.remote_id' => remote_id
- })
+ context 'with object storage enabled' do
+ context 'and direct upload enabled' do
+ let!(:fog_connection) do
+ stub_lfs_object_storage(direct_upload: true)
end
- it 'responds with status 403' do
- subject
+ let(:tmp_object) do
+ fog_connection.directories.new(key: 'lfs-objects').files.create( # rubocop: disable Rails/SaveBang
+ key: 'tmp/uploads/12312300',
+ body: 'content'
+ )
+ end
+
+ ['123123', '../../123123'].each do |remote_id|
+ context "with invalid remote_id: #{remote_id}" do
+ subject do
+ put_finalize(remote_object: tmp_object, args: {
+ 'file.remote_id' => remote_id
+ })
+ end
+
+ it 'responds with status 403' do
+ subject
- expect(response).to have_gitlab_http_status(:forbidden)
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
end
- end
- end
- context 'with valid remote_id' do
- subject do
- put_finalize(remote_object: tmp_object, args: {
- 'file.remote_id' => '12312300',
- 'file.name' => 'name'
- })
- end
+ context 'with valid remote_id' do
+ subject do
+ put_finalize(remote_object: tmp_object, args: {
+ 'file.remote_id' => '12312300',
+ 'file.name' => 'name'
+ })
+ end
- it 'responds with status 200' do
- subject
+ it 'responds with status 200' do
+ subject
- expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to have_gitlab_http_status(:ok)
- object = LfsObject.find_by_oid(sample_oid)
- expect(object).to be_present
- expect(object.file.read).to eq(tmp_object.body)
- end
+ object = LfsObject.find_by_oid(sample_oid)
+ expect(object).to be_present
+ expect(object.file.read).to eq(tmp_object.body)
+ end
+
+ it 'schedules migration of file to object storage' do
+ subject
+
+ expect(LfsObject.last.projects).to include(project)
+ end
- it 'schedules migration of file to object storage' do
- subject
+ it 'have valid file' do
+ subject
- expect(LfsObject.last.projects).to include(project)
+ expect(LfsObject.last.file_store).to eq(ObjectStorage::Store::REMOTE)
+ expect(LfsObject.last.file).to be_exists
+ end
+ end
end
- it 'have valid file' do
- subject
+ context 'and background upload enabled' do
+ before do
+ stub_lfs_object_storage(background_upload: true)
+ end
+
+ it 'schedules migration of file to object storage' do
+ expect(ObjectStorage::BackgroundMoveWorker).to receive(:perform_async).with('LfsObjectUploader', 'LfsObject', :file, kind_of(Numeric))
- expect(LfsObject.last.file_store).to eq(ObjectStorage::Store::REMOTE)
- expect(LfsObject.last.file).to be_exists
+ put_finalize(with_tempfile: true)
+ end
end
end
end
- context 'and background upload enabled' do
+ context 'without the lfs object' do
before do
- stub_lfs_object_storage(background_upload: true)
+ lfs_object.destroy!
end
- it 'schedules migration of file to object storage' do
- expect(ObjectStorage::BackgroundMoveWorker).to receive(:perform_async).with('LfsObjectUploader', 'LfsObject', :file, kind_of(Numeric))
+ it 'rejects slashes in the tempfile name (path traversal)' do
+ put_finalize('../bar', with_tempfile: true)
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+
+ context 'not sending the workhorse jwt header' do
+ let(:include_workhorse_jwt_header) { false }
- put_finalize(with_tempfile: true)
+ it 'rejects the request' do
+ put_finalize(with_tempfile: true)
+
+ expect(response).to have_gitlab_http_status(:unprocessable_entity)
+ end
end
end
end
- end
- context 'without the lfs object' do
- before do
- lfs_object.destroy!
- end
+ describe 'and user does not have push access' do
+ before do
+ project.add_reporter(user)
+ end
- it 'rejects slashes in the tempfile name (path traversal)' do
- put_finalize('../bar', with_tempfile: true)
- expect(response).to have_gitlab_http_status(:bad_request)
+ it_behaves_like 'forbidden'
end
+ end
- context 'not sending the workhorse jwt header' do
- let(:include_workhorse_jwt_header) { false }
-
- it 'rejects the request' do
- put_finalize(with_tempfile: true)
+ context 'when build is authorized' do
+ let(:authorization) { authorize_ci_project }
+ let(:pipeline) { create(:ci_empty_pipeline, project: project) }
- expect(response).to have_gitlab_http_status(:unprocessable_entity)
- end
- end
- end
- end
+ context 'build has an user' do
+ let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
- describe 'and user does not have push access' do
- before do
- project.add_reporter(user)
- end
+ context 'tries to push to own project' do
+ before do
+ project.add_developer(user)
+ put_authorize
+ end
- it_behaves_like 'forbidden'
- end
- end
+ it_behaves_like 'LFS http 403 response'
+ end
- context 'when build is authorized' do
- let(:authorization) { authorize_ci_project }
+ context 'tries to push to other project' do
+ let(:pipeline) { create(:ci_empty_pipeline, project: other_project) }
- context 'build has an user' do
- let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
+ before do
+ put_authorize
+ end
- context 'tries to push to own project' do
- before do
- project.add_developer(user)
- put_authorize
+ it_behaves_like 'LFS http 404 response'
+ end
end
- it_behaves_like 'LFS http 403 response'
- end
+ context 'does not have user' do
+ let(:build) { create(:ci_build, :running, pipeline: pipeline) }
- context 'tries to push to other project' do
- let(:pipeline) { create(:ci_empty_pipeline, project: other_project) }
+ before do
+ put_authorize
+ end
- before do
- put_authorize
+ it_behaves_like 'LFS http 403 response'
end
-
- it_behaves_like 'LFS http 404 response'
end
- end
- context 'does not have user' do
- let(:build) { create(:ci_build, :running, pipeline: pipeline) }
+ describe 'when using a user key (LFSToken)' do
+ let(:authorization) { authorize_user_key }
- before do
- put_authorize
- end
+ context 'when user allowed' do
+ before do
+ project.add_developer(user)
+ put_authorize
+ end
- it_behaves_like 'LFS http 403 response'
- end
- end
+ it_behaves_like 'LFS http 200 workhorse response'
- describe 'when using a user key (LFSToken)' do
- let(:authorization) { authorize_user_key }
+ context 'when user password is expired' do
+ let_it_be(:user) { create(:user, password_expires_at: 1.minute.ago)}
- context 'when user allowed' do
- before do
- project.add_developer(user)
- put_authorize
- end
+ it_behaves_like 'LFS http 401 response'
+ end
- it_behaves_like 'LFS http 200 workhorse response'
+ context 'when user is blocked' do
+ let_it_be(:user) { create(:user, :blocked)}
- context 'when user password is expired' do
- let(:user) { create(:user, password_expires_at: 1.minute.ago)}
+ it_behaves_like 'LFS http 401 response'
+ end
+ end
- it_behaves_like 'LFS http 401 response'
+ context 'when user not allowed' do
+ before do
+ put_authorize
+ end
+
+ it_behaves_like 'LFS http 404 response'
+ end
end
- context 'when user is blocked' do
- let(:user) { create(:user, :blocked)}
+ context 'for unauthenticated' do
+ let(:authorization) { nil }
- it_behaves_like 'LFS http 401 response'
+ it_behaves_like 'unauthorized'
end
end
- context 'when user not allowed' do
- before do
- put_authorize
- end
+ describe 'to a forked project' do
+ let_it_be(:upstream_project) { create(:project, :public) }
+ let_it_be(:project_owner) { create(:user) }
+ let(:project) { fork_project(upstream_project, project_owner) }
- it_behaves_like 'LFS http 404 response'
- end
- end
+ describe 'when user is authenticated' do
+ describe 'when user has push access to the project' do
+ before do
+ project.add_developer(user)
+ end
- context 'for unauthenticated' do
- it_behaves_like 'unauthorized'
- end
- end
+ context 'and request is sent by gitlab-workhorse to authorize the request' do
+ before do
+ put_authorize
+ end
- describe 'to a forked project' do
- let(:upstream_project) { create(:project, :public) }
- let(:project_owner) { create(:user) }
- let(:project) { fork_project(upstream_project, project_owner) }
+ it_behaves_like 'LFS http 200 workhorse response'
- describe 'when user is authenticated' do
- let(:authorization) { authorize_user }
+ it 'with location of LFS store and object details' do
+ expect(json_response['TempPath']).to eq(LfsObjectUploader.workhorse_local_upload_path)
+ expect(json_response['LfsOid']).to eq(sample_oid)
+ expect(json_response['LfsSize']).to eq(sample_size)
+ end
+ end
- describe 'when user has push access to the project' do
- before do
- project.add_developer(user)
- end
+ context 'and request is sent by gitlab-workhorse to finalize the upload' do
+ before do
+ put_finalize
+ end
- context 'and request is sent by gitlab-workhorse to authorize the request' do
- before do
- put_authorize
- end
+ it_behaves_like 'LFS http 200 response'
- it_behaves_like 'LFS http 200 workhorse response'
+ it 'LFS object is linked to the forked project' do
+ expect(lfs_object.projects.pluck(:id)).to include(project.id)
+ end
+ end
+ end
- it 'with location of LFS store and object details' do
- expect(json_response['TempPath']).to eq(LfsObjectUploader.workhorse_local_upload_path)
- expect(json_response['LfsOid']).to eq(sample_oid)
- expect(json_response['LfsSize']).to eq(sample_size)
+ describe 'and user does not have push access' do
+ it_behaves_like 'forbidden'
end
end
- context 'and request is sent by gitlab-workhorse to finalize the upload' do
+ context 'when build is authorized' do
+ let(:authorization) { authorize_ci_project }
+ let(:pipeline) { create(:ci_empty_pipeline, project: project) }
+
before do
- put_finalize
+ put_authorize
end
- it_behaves_like 'LFS http 200 response'
+ context 'build has an user' do
+ let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
- it 'LFS object is linked to the forked project' do
- expect(lfs_object.projects.pluck(:id)).to include(project.id)
- end
- end
- end
-
- describe 'and user does not have push access' do
- it_behaves_like 'forbidden'
- end
- end
+ context 'tries to push to own project' do
+ it_behaves_like 'LFS http 403 response'
+ end
- context 'when build is authorized' do
- let(:authorization) { authorize_ci_project }
+ context 'tries to push to other project' do
+ let(:pipeline) { create(:ci_empty_pipeline, project: other_project) }
- before do
- put_authorize
- end
+ # I'm not sure what this tests that is different from the previous test
+ it_behaves_like 'LFS http 403 response'
+ end
+ end
- context 'build has an user' do
- let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
+ context 'does not have user' do
+ let(:build) { create(:ci_build, :running, pipeline: pipeline) }
- context 'tries to push to own project' do
- it_behaves_like 'LFS http 403 response'
+ it_behaves_like 'LFS http 403 response'
+ end
end
- context 'tries to push to other project' do
- let(:pipeline) { create(:ci_empty_pipeline, project: other_project) }
+ context 'for unauthenticated' do
+ let(:authorization) { nil }
- # I'm not sure what this tests that is different from the previous test
- it_behaves_like 'LFS http 403 response'
+ it_behaves_like 'unauthorized'
end
- end
- context 'does not have user' do
- let(:build) { create(:ci_build, :running, pipeline: pipeline) }
+ describe 'and second project not related to fork or a source project' do
+ let_it_be(:second_project) { create(:project) }
- it_behaves_like 'LFS http 403 response'
- end
- end
-
- context 'for unauthenticated' do
- it_behaves_like 'unauthorized'
- end
+ before do
+ second_project.add_maintainer(user)
+ upstream_project.lfs_objects << lfs_object
+ end
- describe 'and second project not related to fork or a source project' do
- let(:second_project) { create(:project) }
- let(:authorization) { authorize_user }
+ context 'when pushing the same LFS object to the second project' do
+ before do
+ finalize_headers = headers
+ .merge('X-Gitlab-Lfs-Tmp' => lfs_tmp_file)
+ .merge(workhorse_internal_api_request_header)
- before do
- second_project.add_maintainer(user)
- upstream_project.lfs_objects << lfs_object
- end
+ put objects_url(second_project, sample_oid, sample_size),
+ params: {},
+ headers: finalize_headers
+ end
- context 'when pushing the same LFS object to the second project' do
- before do
- finalize_headers = headers
- .merge('X-Gitlab-Lfs-Tmp' => lfs_tmp_file)
- .merge(workhorse_internal_api_request_header)
+ it_behaves_like 'LFS http 200 response'
- put objects_url(second_project, sample_oid, sample_size),
- params: {},
- headers: finalize_headers
+ it 'links the LFS object to the project' do
+ expect(lfs_object.projects.pluck(:id)).to include(second_project.id, upstream_project.id)
+ end
+ end
end
+ end
- it_behaves_like 'LFS http 200 response'
+ def put_authorize(verified: true)
+ authorize_headers = headers
+ authorize_headers.merge!(workhorse_internal_api_request_header) if verified
- it 'links the LFS object to the project' do
- expect(lfs_object.projects.pluck(:id)).to include(second_project.id, upstream_project.id)
- end
+ put authorize_url(project, sample_oid, sample_size), params: {}, headers: authorize_headers
end
- end
- end
- def put_authorize(verified: true)
- authorize_headers = headers
- authorize_headers.merge!(workhorse_internal_api_request_header) if verified
+ def put_finalize(lfs_tmp = lfs_tmp_file, with_tempfile: false, verified: true, remote_object: nil, args: {})
+ uploaded_file = nil
- put authorize_url(project, sample_oid, sample_size), params: {}, headers: authorize_headers
- end
+ if with_tempfile
+ upload_path = LfsObjectUploader.workhorse_local_upload_path
+ file_path = upload_path + '/' + lfs_tmp if lfs_tmp
- def put_finalize(lfs_tmp = lfs_tmp_file, with_tempfile: false, verified: true, remote_object: nil, args: {})
- uploaded_file = nil
+ FileUtils.mkdir_p(upload_path)
+ FileUtils.touch(file_path)
- if with_tempfile
- upload_path = LfsObjectUploader.workhorse_local_upload_path
- file_path = upload_path + '/' + lfs_tmp if lfs_tmp
+ uploaded_file = UploadedFile.new(file_path, filename: File.basename(file_path))
+ elsif remote_object
+ uploaded_file = fog_to_uploaded_file(remote_object)
+ end
- FileUtils.mkdir_p(upload_path)
- FileUtils.touch(file_path)
+ finalize_headers = headers
+ finalize_headers.merge!(workhorse_internal_api_request_header) if verified
+
+ workhorse_finalize(
+ objects_url(project, sample_oid, sample_size),
+ method: :put,
+ file_key: :file,
+ params: args.merge(file: uploaded_file),
+ headers: finalize_headers,
+ send_rewritten_field: include_workhorse_jwt_header
+ )
+ end
- uploaded_file = UploadedFile.new(file_path, filename: File.basename(file_path))
- elsif remote_object
- uploaded_file = fog_to_uploaded_file(remote_object)
+ def lfs_tmp_file
+ "#{sample_oid}012345678"
+ end
end
-
- finalize_headers = headers
- finalize_headers.merge!(workhorse_internal_api_request_header) if verified
-
- workhorse_finalize(
- objects_url(project, sample_oid, sample_size),
- method: :put,
- file_key: :file,
- params: args.merge(file: uploaded_file),
- headers: finalize_headers,
- send_rewritten_field: include_workhorse_jwt_header
- )
- end
-
- def lfs_tmp_file
- "#{sample_oid}012345678"
- end
- end
-
- context 'with projects' do
- it_behaves_like 'LFS http requests' do
- let(:container) { project }
- let(:authorize_guest) { project.add_guest(user) }
- let(:authorize_download) { project.add_reporter(user) }
- let(:authorize_upload) { project.add_developer(user) }
end
end
diff --git a/spec/requests/projects/cycle_analytics_events_spec.rb b/spec/requests/projects/cycle_analytics_events_spec.rb
index 3f57b8ba67b..f8fa9459467 100644
--- a/spec/requests/projects/cycle_analytics_events_spec.rb
+++ b/spec/requests/projects/cycle_analytics_events_spec.rb
@@ -7,108 +7,115 @@ RSpec.describe 'value stream analytics events' do
let(:project) { create(:project, :repository, public_builds: false) }
let(:issue) { create(:issue, project: project, created_at: 2.days.ago) }
- describe 'GET /:namespace/:project/value_stream_analytics/events/issues' do
- before do
- project.add_developer(user)
+ shared_examples 'value stream analytics events examples' do
+ describe 'GET /:namespace/:project/value_stream_analytics/events/issues' do
+ before do
+ project.add_developer(user)
- 3.times do |count|
- travel_to(Time.now + count.days) do
- create_cycle
+ 3.times do |count|
+ travel_to(Time.now + count.days) do
+ create_cycle
+ end
end
- end
-
- deploy_master(user, project)
-
- login_as(user)
- end
-
- it 'lists the issue events' do
- get project_cycle_analytics_issue_path(project, format: :json)
- first_issue_iid = project.issues.sort_by_attribute(:created_desc).pluck(:iid).first.to_s
+ deploy_master(user, project)
- expect(json_response['events']).not_to be_empty
- expect(json_response['events'].first['iid']).to eq(first_issue_iid)
- end
+ login_as(user)
+ end
- it 'lists the plan events' do
- get project_cycle_analytics_plan_path(project, format: :json)
+ it 'lists the issue events' do
+ get project_cycle_analytics_issue_path(project, format: :json)
- first_issue_iid = project.issues.sort_by_attribute(:created_desc).pluck(:iid).first.to_s
+ first_issue_iid = project.issues.sort_by_attribute(:created_desc).pluck(:iid).first.to_s
- expect(json_response['events']).not_to be_empty
- expect(json_response['events'].first['iid']).to eq(first_issue_iid)
- end
+ expect(json_response['events']).not_to be_empty
+ expect(json_response['events'].first['iid']).to eq(first_issue_iid)
+ end
- it 'lists the code events' do
- get project_cycle_analytics_code_path(project, format: :json)
+ it 'lists the plan events' do
+ get project_cycle_analytics_plan_path(project, format: :json)
- expect(json_response['events']).not_to be_empty
+ first_issue_iid = project.issues.sort_by_attribute(:created_desc).pluck(:iid).first.to_s
- first_mr_iid = project.merge_requests.sort_by_attribute(:created_desc).pluck(:iid).first.to_s
+ expect(json_response['events']).not_to be_empty
+ expect(json_response['events'].first['iid']).to eq(first_issue_iid)
+ end
- expect(json_response['events'].first['iid']).to eq(first_mr_iid)
- end
+ it 'lists the code events' do
+ get project_cycle_analytics_code_path(project, format: :json)
- it 'lists the test events', :sidekiq_might_not_need_inline do
- get project_cycle_analytics_test_path(project, format: :json)
+ expect(json_response['events']).not_to be_empty
- expect(json_response['events']).not_to be_empty
- expect(json_response['events'].first['date']).not_to be_empty
- end
+ first_mr_iid = project.merge_requests.sort_by_attribute(:created_desc).pluck(:iid).first.to_s
- it 'lists the review events' do
- get project_cycle_analytics_review_path(project, format: :json)
+ expect(json_response['events'].first['iid']).to eq(first_mr_iid)
+ end
- first_mr_iid = project.merge_requests.sort_by_attribute(:created_desc).pluck(:iid).first.to_s
+ it 'lists the test events', :sidekiq_inline do
+ get project_cycle_analytics_test_path(project, format: :json)
- expect(json_response['events']).not_to be_empty
- expect(json_response['events'].first['iid']).to eq(first_mr_iid)
- end
+ expect(json_response['events']).not_to be_empty
+ expect(json_response['events'].first['date']).not_to be_empty
+ end
- it 'lists the staging events', :sidekiq_might_not_need_inline do
- get project_cycle_analytics_staging_path(project, format: :json)
+ it 'lists the review events' do
+ get project_cycle_analytics_review_path(project, format: :json)
- expect(json_response['events']).not_to be_empty
- expect(json_response['events'].first['date']).not_to be_empty
- end
+ first_mr_iid = project.merge_requests.sort_by_attribute(:created_desc).pluck(:iid).first.to_s
- context 'specific branch' do
- it 'lists the test events', :sidekiq_might_not_need_inline do
- branch = project.merge_requests.first.source_branch
+ expect(json_response['events']).not_to be_empty
+ expect(json_response['events'].first['iid']).to eq(first_mr_iid)
+ end
- get project_cycle_analytics_test_path(project, format: :json, branch: branch)
+ it 'lists the staging events', :sidekiq_inline do
+ get project_cycle_analytics_staging_path(project, format: :json)
expect(json_response['events']).not_to be_empty
expect(json_response['events'].first['date']).not_to be_empty
end
- end
- context 'with private project and builds' do
- before do
- project.members.last.update(access_level: Gitlab::Access::GUEST)
- end
+ context 'with private project and builds' do
+ before do
+ project.members.last.update(access_level: Gitlab::Access::GUEST)
+ end
- it 'does not list the test events' do
- get project_cycle_analytics_test_path(project, format: :json)
+ it 'does not list the test events' do
+ get project_cycle_analytics_test_path(project, format: :json)
- expect(response).to have_gitlab_http_status(:not_found)
- end
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
- it 'does not list the staging events' do
- get project_cycle_analytics_staging_path(project, format: :json)
+ it 'does not list the staging events' do
+ get project_cycle_analytics_staging_path(project, format: :json)
- expect(response).to have_gitlab_http_status(:not_found)
- end
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
- it 'lists the issue events' do
- get project_cycle_analytics_issue_path(project, format: :json)
+ it 'lists the issue events' do
+ get project_cycle_analytics_issue_path(project, format: :json)
- expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to have_gitlab_http_status(:ok)
+ end
end
end
end
+ describe 'when new_project_level_vsa_backend feature flag is off' do
+ before do
+ stub_feature_flags(new_project_level_vsa_backend: false, thing: project)
+ end
+
+ it_behaves_like 'value stream analytics events examples'
+ end
+
+ describe 'when new_project_level_vsa_backend feature flag is on' do
+ before do
+ stub_feature_flags(new_project_level_vsa_backend: true, thing: project)
+ end
+
+ it_behaves_like 'value stream analytics events examples'
+ end
+
def create_cycle
milestone = create(:milestone, project: project)
issue.update(milestone: milestone)
@@ -123,5 +130,7 @@ RSpec.describe 'value stream analytics events' do
merge_merge_requests_closing_issue(user, project, issue)
ProcessCommitWorker.new.perform(project.id, user.id, mr.commits.last.to_hash)
+
+ mr.metrics.update!(latest_build_started_at: 1.hour.ago, latest_build_finished_at: Time.now)
end
end
diff --git a/spec/requests/projects/metrics_dashboard_spec.rb b/spec/requests/projects/metrics_dashboard_spec.rb
index 0a4100f2bf5..c248463faa3 100644
--- a/spec/requests/projects/metrics_dashboard_spec.rb
+++ b/spec/requests/projects/metrics_dashboard_spec.rb
@@ -70,7 +70,7 @@ RSpec.describe 'Projects::MetricsDashboardController' do
context 'when query param environment does not exist' do
it 'responds with 404' do
- send_request(environment: 99)
+ send_request(environment: non_existing_record_id)
expect(response).to have_gitlab_http_status(:not_found)
end
end
@@ -105,7 +105,7 @@ RSpec.describe 'Projects::MetricsDashboardController' do
context 'when query param environment does not exist' do
it 'responds with 404' do
- send_request(dashboard_path: dashboard_path, environment: 99)
+ send_request(dashboard_path: dashboard_path, environment: non_existing_record_id)
expect(response).to have_gitlab_http_status(:not_found)
end
end
diff --git a/spec/requests/rack_attack_global_spec.rb b/spec/requests/rack_attack_global_spec.rb
index 805ac5a9118..c2e68df2c40 100644
--- a/spec/requests/rack_attack_global_spec.rb
+++ b/spec/requests/rack_attack_global_spec.rb
@@ -106,7 +106,7 @@ RSpec.describe 'Rack Attack global throttles' do
let(:request_jobs_url) { '/api/v4/jobs/request' }
let(:runner) { create(:ci_runner) }
- it 'does not cont as unauthenticated' do
+ it 'does not count as unauthenticated' do
(1 + requests_per_period).times do
post request_jobs_url, params: { token: runner.token }
expect(response).to have_gitlab_http_status(:no_content)
@@ -114,6 +114,17 @@ RSpec.describe 'Rack Attack global throttles' do
end
end
+ context 'when the request is to a health endpoint' do
+ let(:health_endpoint) { '/-/metrics' }
+
+ it 'does not throttle the requests' do
+ (1 + requests_per_period).times do
+ get health_endpoint
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+ end
+
it 'logs RackAttack info into structured logs' do
requests_per_period.times do
get url_that_does_not_require_authentication
@@ -133,6 +144,14 @@ RSpec.describe 'Rack Attack global throttles' do
get url_that_does_not_require_authentication
end
+
+ it_behaves_like 'tracking when dry-run mode is set' do
+ let(:throttle_name) { 'throttle_unauthenticated' }
+
+ def do_request
+ get url_that_does_not_require_authentication
+ end
+ end
end
context 'when the throttle is disabled' do
@@ -231,6 +250,10 @@ RSpec.describe 'Rack Attack global throttles' do
let(:post_params) { { user: { login: 'username', password: 'password' } } }
+ def do_request
+ post protected_path_that_does_not_require_authentication, params: post_params
+ end
+
before do
settings_to_set[:throttle_protected_paths_requests_per_period] = requests_per_period # 1
settings_to_set[:throttle_protected_paths_period_in_seconds] = period_in_seconds # 10_000
@@ -244,7 +267,7 @@ RSpec.describe 'Rack Attack global throttles' do
it 'allows requests over the rate limit' do
(1 + requests_per_period).times do
- post protected_path_that_does_not_require_authentication, params: post_params
+ do_request
expect(response).to have_gitlab_http_status(:ok)
end
end
@@ -258,12 +281,16 @@ RSpec.describe 'Rack Attack global throttles' do
it 'rejects requests over the rate limit' do
requests_per_period.times do
- post protected_path_that_does_not_require_authentication, params: post_params
+ do_request
expect(response).to have_gitlab_http_status(:ok)
end
expect_rejection { post protected_path_that_does_not_require_authentication, params: post_params }
end
+
+ it_behaves_like 'tracking when dry-run mode is set' do
+ let(:throttle_name) { 'throttle_unauthenticated_protected_paths' }
+ end
end
end
diff --git a/spec/requests/self_monitoring_project_spec.rb b/spec/requests/self_monitoring_project_spec.rb
index 5844a27da17..f7227f71b05 100644
--- a/spec/requests/self_monitoring_project_spec.rb
+++ b/spec/requests/self_monitoring_project_spec.rb
@@ -12,7 +12,7 @@ RSpec.describe 'Self-Monitoring project requests' do
it_behaves_like 'not accessible to non-admin users'
- context 'with admin user' do
+ context 'with admin user', :enable_admin_mode do
before do
login_as(admin)
end
@@ -36,7 +36,7 @@ RSpec.describe 'Self-Monitoring project requests' do
it_behaves_like 'not accessible to non-admin users'
- context 'with admin user' do
+ context 'with admin user', :enable_admin_mode do
before do
login_as(admin)
end
@@ -116,7 +116,7 @@ RSpec.describe 'Self-Monitoring project requests' do
it_behaves_like 'not accessible to non-admin users'
- context 'with admin user' do
+ context 'with admin user', :enable_admin_mode do
before do
login_as(admin)
end
@@ -140,7 +140,7 @@ RSpec.describe 'Self-Monitoring project requests' do
it_behaves_like 'not accessible to non-admin users'
- context 'with admin user' do
+ context 'with admin user', :enable_admin_mode do
before do
login_as(admin)
end
diff --git a/spec/requests/whats_new_controller_spec.rb b/spec/requests/whats_new_controller_spec.rb
index c04a6b00a93..8005d38dbb0 100644
--- a/spec/requests/whats_new_controller_spec.rb
+++ b/spec/requests/whats_new_controller_spec.rb
@@ -4,29 +4,31 @@ require 'spec_helper'
RSpec.describe WhatsNewController do
describe 'whats_new_path' do
- context 'with whats_new_drawer feature enabled' do
- let(:fixture_dir_glob) { Dir.glob(File.join('spec', 'fixtures', 'whats_new', '*.yml')) }
+ let(:item) { double(:item) }
+ let(:highlights) { double(:highlight, items: [item], map: [item].map, next_page: 2) }
+ context 'with whats_new_drawer feature enabled' do
before do
stub_feature_flags(whats_new_drawer: true)
- allow(Dir).to receive(:glob).with(Rails.root.join('data', 'whats_new', '*.yml')).and_return(fixture_dir_glob)
end
context 'with no page param' do
it 'responds with paginated data and headers' do
+ allow(ReleaseHighlight).to receive(:paginated).with(page: 1).and_return(highlights)
+ allow(Gitlab::WhatsNew::ItemPresenter).to receive(:present).with(item).and_return(item)
+
get whats_new_path, xhr: true
- expect(response.body).to eq([{ title: "bright and sunshinin' day", release: "01.05" }].to_json)
+ expect(response.body).to eq(highlights.items.to_json)
expect(response.headers['X-Next-Page']).to eq(2)
end
end
context 'with page param' do
- it 'responds with paginated data and headers' do
- get whats_new_path(page: 2), xhr: true
+ it 'passes the page parameter' do
+ expect(ReleaseHighlight).to receive(:paginated).with(page: 2).and_call_original
- expect(response.body).to eq([{ title: 'bright' }].to_json)
- expect(response.headers['X-Next-Page']).to eq(3)
+ get whats_new_path(page: 2), xhr: true
end
it 'returns a 404 if page param is negative' do
@@ -34,13 +36,17 @@ RSpec.describe WhatsNewController do
expect(response).to have_gitlab_http_status(:not_found)
end
+ end
+
+ context 'with version param' do
+ it 'returns items without pagination headers' do
+ allow(ReleaseHighlight).to receive(:for_version).with(version: '42').and_return(highlights)
+ allow(Gitlab::WhatsNew::ItemPresenter).to receive(:present).with(item).and_return(item)
+
+ get whats_new_path(version: 42), xhr: true
- context 'when there are no more paginated results' do
- it 'responds with nil X-Next-Page header' do
- get whats_new_path(page: 3), xhr: true
- expect(response.body).to eq([{ title: "It's gonna be a bright" }].to_json)
- expect(response.headers['X-Next-Page']).to be nil
- end
+ expect(response.body).to eq(highlights.items.to_json)
+ expect(response.headers['X-Next-Page']).to be_nil
end
end
end