diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-12-17 11:59:07 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-12-17 11:59:07 +0000 |
commit | 8b573c94895dc0ac0e1d9d59cf3e8745e8b539ca (patch) | |
tree | 544930fb309b30317ae9797a9683768705d664c4 /spec/requests | |
parent | 4b1de649d0168371549608993deac953eb692019 (diff) | |
download | gitlab-ce-8b573c94895dc0ac0e1d9d59cf3e8745e8b539ca.tar.gz |
Add latest changes from gitlab-org/gitlab@13-7-stable-eev13.7.0-rc42
Diffstat (limited to 'spec/requests')
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 |