diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-10-05 16:54:15 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-10-05 16:54:15 +0300 |
commit | be834a25982746ffd85252ff502df42bb88cb9d5 (patch) | |
tree | b4d6a8ba0931e12fac08f05abea33a3b8ec2c8a2 /spec/requests/api | |
parent | ee925a3597f27e92f83a50937a64068109675b3d (diff) |
Add latest changes from gitlab-org/gitlab@13-5-stable-eev13.5.0-rc32
Diffstat (limited to 'spec/requests/api')
39 files changed, 1958 insertions, 326 deletions
diff --git a/spec/requests/api/admin/instance_clusters_spec.rb b/spec/requests/api/admin/instance_clusters_spec.rb index b68541b5d92..9d0661089a9 100644 --- a/spec/requests/api/admin/instance_clusters_spec.rb +++ b/spec/requests/api/admin/instance_clusters_spec.rb @@ -162,6 +162,7 @@ RSpec.describe ::API::Admin::InstanceClusters do name: 'test-instance-cluster', domain: 'domain.example.com', managed: false, + namespace_per_environment: false, platform_kubernetes_attributes: platform_kubernetes_attributes, clusterable: clusterable } @@ -206,6 +207,7 @@ RSpec.describe ::API::Admin::InstanceClusters do 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.namespace_per_environment).to eq(false) expect(platform_kubernetes.api_url).to eq("https://example.com") expect(platform_kubernetes.token).to eq('sample-token') end @@ -235,6 +237,22 @@ RSpec.describe ::API::Admin::InstanceClusters do end end + context 'when namespace_per_environment is not set' do + let(:cluster_params) do + { + name: 'test-cluster', + domain: 'domain.example.com', + platform_kubernetes_attributes: platform_kubernetes_attributes + } + end + + it 'defaults to true' do + cluster_result = Clusters::Cluster.find(json_response['id']) + + expect(cluster_result).to be_namespace_per_environment + end + end + context 'when an instance cluster already exists' do it 'allows user to add multiple clusters' do post api('/admin/clusters/add', admin_user), params: multiple_cluster_params diff --git a/spec/requests/api/ci/runner/jobs_artifacts_spec.rb b/spec/requests/api/ci/runner/jobs_artifacts_spec.rb index 97110b63ff6..71be0c30f5a 100644 --- a/spec/requests/api/ci/runner/jobs_artifacts_spec.rb +++ b/spec/requests/api/ci/runner/jobs_artifacts_spec.rb @@ -227,10 +227,6 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do end context 'authorize uploading of an lsif artifact' do - before do - stub_feature_flags(code_navigation: job.project) - end - it 'adds ProcessLsif header' do authorize_artifacts_with_token_in_headers(artifact_type: :lsif) @@ -249,32 +245,6 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do .to change { Gitlab::UsageDataCounters::HLLRedisCounter.unique_events(tracking_params) } .by(1) end - - context 'code_navigation feature flag is disabled' do - before do - stub_feature_flags(code_navigation: false) - end - - it 'responds with a forbidden error' do - authorize_artifacts_with_token_in_headers(artifact_type: :lsif) - - aggregate_failures do - expect(response).to have_gitlab_http_status(:forbidden) - expect(json_response['ProcessLsif']).to be_falsy - end - end - - it 'does not track code_intelligence usage ping' do - tracking_params = { - event_names: 'i_source_code_code_intelligence', - start_date: Date.yesterday, - end_date: Date.today - } - - expect { authorize_artifacts_with_token_in_headers(artifact_type: :lsif) } - .not_to change { Gitlab::UsageDataCounters::HLLRedisCounter.unique_events(tracking_params) } - end - end end def authorize_artifacts(params = {}, request_headers = headers) diff --git a/spec/requests/api/ci/runner/jobs_put_spec.rb b/spec/requests/api/ci/runner/jobs_put_spec.rb index 183a3b26e00..92d38621105 100644 --- a/spec/requests/api/ci/runner/jobs_put_spec.rb +++ b/spec/requests/api/ci/runner/jobs_put_spec.rb @@ -46,64 +46,59 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do end context 'when status is given' do - it 'mark job as succeeded' do + it 'marks job as succeeded' do update_job(state: 'success') - job.reload - expect(job).to be_success + expect(job.reload).to be_success + expect(response.header).not_to have_key('X-GitLab-Trace-Update-Interval') end - it 'mark job as failed' do + it 'marks job as failed' do update_job(state: 'failed') - job.reload - expect(job).to be_failed + expect(job.reload).to be_failed expect(job).to be_unknown_failure + expect(response.header).not_to have_key('X-GitLab-Trace-Update-Interval') end context 'when failure_reason is script_failure' do before do update_job(state: 'failed', failure_reason: 'script_failure') - job.reload end - it { expect(job).to be_script_failure } + it { expect(job.reload).to be_script_failure } end context 'when failure_reason is runner_system_failure' do before do update_job(state: 'failed', failure_reason: 'runner_system_failure') - job.reload end - it { expect(job).to be_runner_system_failure } + it { expect(job.reload).to be_runner_system_failure } end context 'when failure_reason is unrecognized value' do before do update_job(state: 'failed', failure_reason: 'what_is_this') - job.reload end - it { expect(job).to be_unknown_failure } + it { expect(job.reload).to be_unknown_failure } end context 'when failure_reason is job_execution_timeout' do before do update_job(state: 'failed', failure_reason: 'job_execution_timeout') - job.reload end - it { expect(job).to be_job_execution_timeout } + it { expect(job.reload).to be_job_execution_timeout } end context 'when failure_reason is unmet_prerequisites' do before do update_job(state: 'failed', failure_reason: 'unmet_prerequisites') - job.reload end - it { expect(job).to be_unmet_prerequisites } + it { expect(job.reload).to be_unmet_prerequisites } end context 'when unmigrated live trace chunks exist' do @@ -119,24 +114,21 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do expect(job.pending_state).to be_present expect(response).to have_gitlab_http_status(:accepted) + expect(response.header['X-GitLab-Trace-Update-Interval']).to be > 0 end end context 'when runner retries request after receiving 202' do it 'responds with 202 and then with 200', :sidekiq_inline do - perform_enqueued_jobs do - update_job(state: 'success', checksum: 'crc32:12345678') - end + update_job(state: 'success', checksum: 'crc32:12345678') - expect(job.reload.pending_state).to be_present expect(response).to have_gitlab_http_status(:accepted) + expect(job.reload.pending_state).to be_present - perform_enqueued_jobs do - update_job(state: 'success', checksum: 'crc32:12345678') - end + update_job(state: 'success', checksum: 'crc32:12345678') - expect(job.reload.pending_state).not_to be_present expect(response).to have_gitlab_http_status(:ok) + expect(job.reload.pending_state).not_to be_present end end @@ -149,8 +141,9 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do update_job(state: 'success', checksum: 'crc:12345678') expect(job.reload).to be_success - expect(job.pending_state).not_to be_present + expect(job.pending_state).to be_present expect(response).to have_gitlab_http_status(:ok) + expect(response.header).not_to have_key('X-GitLab-Trace-Update-Interval') end end end diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb index d34244771ad..98c1e0228d4 100644 --- a/spec/requests/api/commits_spec.rb +++ b/spec/requests/api/commits_spec.rb @@ -36,6 +36,13 @@ RSpec.describe API::Commits do end it 'include correct pagination headers' do + get api(route, current_user) + + expect(response).to include_limited_pagination_headers + end + + it 'includes the total headers when the count is not disabled' do + stub_feature_flags(api_commits_without_count: false) commit_count = project.repository.count_commits(ref: 'master').to_s get api(route, current_user) @@ -79,12 +86,10 @@ RSpec.describe API::Commits do it 'include correct pagination headers' do commits = project.repository.commits("master", limit: 2) after = commits.second.created_at - commit_count = project.repository.count_commits(ref: 'master', after: after).to_s get api("/projects/#{project_id}/repository/commits?since=#{after.utc.iso8601}", user) - expect(response).to include_pagination_headers - expect(response.headers['X-Total']).to eq(commit_count) + expect(response).to include_limited_pagination_headers expect(response.headers['X-Page']).to eql('1') end end @@ -109,12 +114,10 @@ RSpec.describe API::Commits do it 'include correct pagination headers' do commits = project.repository.commits("master", limit: 2) before = commits.second.created_at - commit_count = project.repository.count_commits(ref: 'master', before: before).to_s get api("/projects/#{project_id}/repository/commits?until=#{before.utc.iso8601}", user) - expect(response).to include_pagination_headers - expect(response.headers['X-Total']).to eq(commit_count) + expect(response).to include_limited_pagination_headers expect(response.headers['X-Page']).to eql('1') end end @@ -137,49 +140,49 @@ RSpec.describe API::Commits do context "path optional parameter" do it "returns project commits matching provided path parameter" do path = 'files/ruby/popen.rb' - commit_count = project.repository.count_commits(ref: 'master', path: path).to_s get api("/projects/#{project_id}/repository/commits?path=#{path}", user) expect(json_response.size).to eq(3) expect(json_response.first["id"]).to eq("570e7b2abdd848b95f2f578043fc23bd6f6fd24d") - expect(response).to include_pagination_headers - expect(response.headers['X-Total']).to eq(commit_count) + expect(response).to include_limited_pagination_headers end it 'include correct pagination headers' do path = 'files/ruby/popen.rb' - commit_count = project.repository.count_commits(ref: 'master', path: path).to_s get api("/projects/#{project_id}/repository/commits?path=#{path}", user) - expect(response).to include_pagination_headers - expect(response.headers['X-Total']).to eq(commit_count) + expect(response).to include_limited_pagination_headers expect(response.headers['X-Page']).to eql('1') end end context 'all optional parameter' do it 'returns all project commits' do - commit_count = project.repository.count_commits(all: true) + expected_commit_ids = project.repository.commits(nil, all: true, limit: 50).map(&:id) + + get api("/projects/#{project_id}/repository/commits?all=true&per_page=50", user) - get api("/projects/#{project_id}/repository/commits?all=true", user) + commit_ids = json_response.map { |c| c['id'] } - expect(response).to include_pagination_headers - expect(response.headers['X-Total']).to eq(commit_count.to_s) + expect(response).to include_limited_pagination_headers + expect(commit_ids).to eq(expected_commit_ids) expect(response.headers['X-Page']).to eql('1') end end context 'first_parent optional parameter' do it 'returns all first_parent commits' do - commit_count = project.repository.count_commits(ref: SeedRepo::Commit::ID, first_parent: true) + expected_commit_ids = project.repository.commits(SeedRepo::Commit::ID, limit: 50, first_parent: true).map(&:id) - get api("/projects/#{project_id}/repository/commits", user), params: { ref_name: SeedRepo::Commit::ID, first_parent: 'true' } + get api("/projects/#{project_id}/repository/commits?per_page=50", user), params: { ref_name: SeedRepo::Commit::ID, first_parent: 'true' } - expect(response).to include_pagination_headers - expect(commit_count).to eq(12) - expect(response.headers['X-Total']).to eq(commit_count.to_s) + commit_ids = json_response.map { |c| c['id'] } + + expect(response).to include_limited_pagination_headers + expect(expected_commit_ids.size).to eq(12) + expect(commit_ids).to eq(expected_commit_ids) end end @@ -209,11 +212,7 @@ RSpec.describe API::Commits do end it 'returns correct headers' do - commit_count = project.repository.count_commits(ref: ref_name).to_s - - expect(response).to include_pagination_headers - expect(response.headers['X-Total']).to eq(commit_count) - expect(response.headers['X-Page']).to eq('1') + expect(response).to include_limited_pagination_headers expect(response.headers['Link']).to match(/page=1&per_page=5/) expect(response.headers['Link']).to match(/page=2&per_page=5/) end @@ -972,7 +971,7 @@ RSpec.describe API::Commits do refs.concat(project.repository.tag_names_contains(commit_id).map {|name| ['tag', name]}) expect(response).to have_gitlab_http_status(:ok) - expect(response).to include_pagination_headers + expect(response).to include_limited_pagination_headers expect(json_response).to be_an Array expect(json_response.map { |r| [r['type'], r['name']] }.compact).to eq(refs) end @@ -1262,7 +1261,7 @@ RSpec.describe API::Commits do get api(route, current_user) expect(response).to have_gitlab_http_status(:ok) - expect(response).to include_pagination_headers + expect(response).to include_limited_pagination_headers expect(json_response.size).to be >= 1 expect(json_response.first.keys).to include 'diff' end @@ -1276,7 +1275,7 @@ RSpec.describe API::Commits do get api(route, current_user) expect(response).to have_gitlab_http_status(:ok) - expect(response).to include_pagination_headers + expect(response).to include_limited_pagination_headers expect(json_response.size).to be <= 1 end end @@ -1914,7 +1913,7 @@ RSpec.describe API::Commits do get api("/projects/#{project.id}/repository/commits/#{commit.id}/merge_requests", user) expect(response).to have_gitlab_http_status(:ok) - expect(response).to include_pagination_headers + expect(response).to include_limited_pagination_headers expect(json_response.length).to eq(1) expect(json_response[0]['id']).to eq(merged_mr.id) end diff --git a/spec/requests/api/debian_group_packages_spec.rb b/spec/requests/api/debian_group_packages_spec.rb new file mode 100644 index 00000000000..8a05d20fb33 --- /dev/null +++ b/spec/requests/api/debian_group_packages_spec.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true +require 'spec_helper' + +RSpec.describe API::DebianGroupPackages do + include HttpBasicAuthHelpers + include WorkhorseHelpers + + include_context 'Debian repository shared context', :group do + describe 'GET groups/:id/-/packages/debian/dists/*distribution/Release.gpg' do + let(:url) { "/groups/#{group.id}/-/packages/debian/dists/#{distribution}/Release.gpg" } + + it_behaves_like 'Debian group repository GET endpoint', :not_found, nil + end + + describe 'GET groups/:id/-/packages/debian/dists/*distribution/Release' do + let(:url) { "/groups/#{group.id}/-/packages/debian/dists/#{distribution}/Release" } + + it_behaves_like 'Debian group repository GET endpoint', :success, 'TODO Release' + end + + describe 'GET groups/:id/-/packages/debian/dists/*distribution/InRelease' do + let(:url) { "/groups/#{group.id}/-/packages/debian/dists/#{distribution}/InRelease" } + + it_behaves_like 'Debian group repository GET endpoint', :not_found, nil + end + + describe 'GET groups/:id/-/packages/debian/dists/*distribution/:component/binary-:architecture/Packages' do + let(:url) { "/groups/#{group.id}/-/packages/debian/dists/#{distribution}/#{component}/binary-#{architecture}/Packages" } + + it_behaves_like 'Debian group repository GET endpoint', :success, 'TODO Packages' + end + + describe 'GET groups/:id/-/packages/debian/pool/:component/:letter/:source_package/:file_name' do + let(:url) { "/groups/#{group.id}/-/packages/debian/pool/#{component}/#{letter}/#{source_package}/#{package_name}_#{package_version}_#{architecture}.deb" } + + it_behaves_like 'Debian group repository GET endpoint', :success, 'TODO File' + end + end +end diff --git a/spec/requests/api/debian_project_packages_spec.rb b/spec/requests/api/debian_project_packages_spec.rb new file mode 100644 index 00000000000..d2f208d0079 --- /dev/null +++ b/spec/requests/api/debian_project_packages_spec.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true +require 'spec_helper' + +RSpec.describe API::DebianProjectPackages do + include HttpBasicAuthHelpers + include WorkhorseHelpers + + include_context 'Debian repository shared context', :project do + describe 'GET projects/:id/-/packages/debian/dists/*distribution/Release.gpg' do + let(:url) { "/projects/#{project.id}/-/packages/debian/dists/#{distribution}/Release.gpg" } + + it_behaves_like 'Debian project repository GET endpoint', :not_found, nil + end + + describe 'GET projects/:id/-/packages/debian/dists/*distribution/Release' do + let(:url) { "/projects/#{project.id}/-/packages/debian/dists/#{distribution}/Release" } + + it_behaves_like 'Debian project repository GET endpoint', :success, 'TODO Release' + end + + describe 'GET projects/:id/-/packages/debian/dists/*distribution/InRelease' do + let(:url) { "/projects/#{project.id}/-/packages/debian/dists/#{distribution}/InRelease" } + + it_behaves_like 'Debian project repository GET endpoint', :not_found, nil + end + + describe 'GET projects/:id/-/packages/debian/dists/*distribution/:component/binary-:architecture/Packages' do + let(:url) { "/projects/#{project.id}/-/packages/debian/dists/#{distribution}/#{component}/binary-#{architecture}/Packages" } + + it_behaves_like 'Debian project repository GET endpoint', :success, 'TODO Packages' + end + + describe 'GET projects/:id/-/packages/debian/pool/:component/:letter/:source_package/:file_name' do + let(:url) { "/projects/#{project.id}/-/packages/debian/pool/#{component}/#{letter}/#{source_package}/#{package_name}_#{package_version}_#{architecture}.deb" } + + it_behaves_like 'Debian project repository GET endpoint', :success, 'TODO File' + end + + describe 'PUT projects/:id/-/packages/debian/incoming/:file_name' do + let(:method) { :put } + let(:url) { "/projects/#{project.id}/-/packages/debian/incoming/#{file_name}" } + + it_behaves_like 'Debian project repository PUT endpoint', :created, nil + end + end +end diff --git a/spec/requests/api/features_spec.rb b/spec/requests/api/features_spec.rb index 2746e777306..3f443b4f92b 100644 --- a/spec/requests/api/features_spec.rb +++ b/spec/requests/api/features_spec.rb @@ -12,6 +12,8 @@ RSpec.describe API::Features, stub_feature_flags: false do Flipper.register(:perf_team) do |actor| actor.respond_to?(:admin) && actor.admin? end + + skip_feature_flags_yaml_validation end describe 'GET /features' do diff --git a/spec/requests/api/files_spec.rb b/spec/requests/api/files_spec.rb index bb4e88f97f8..f77f127ddc8 100644 --- a/spec/requests/api/files_spec.rb +++ b/spec/requests/api/files_spec.rb @@ -747,7 +747,7 @@ RSpec.describe API::Files do it "updates existing file in project repo with accepts correct last commit id" do last_commit = Gitlab::Git::Commit - .last_for_path(project.repository, 'master', URI.unescape(file_path)) + .last_for_path(project.repository, 'master', Addressable::URI.unencode_component(file_path)) params_with_correct_id = params.merge(last_commit_id: last_commit.id) put api(route(file_path), user), params: params_with_correct_id @@ -757,7 +757,7 @@ RSpec.describe API::Files do it "returns 400 when file path is invalid" do last_commit = Gitlab::Git::Commit - .last_for_path(project.repository, 'master', URI.unescape(file_path)) + .last_for_path(project.repository, 'master', Addressable::URI.unencode_component(file_path)) params_with_correct_id = params.merge(last_commit_id: last_commit.id) put api(route(rouge_file_path), user), params: params_with_correct_id @@ -769,7 +769,7 @@ RSpec.describe API::Files do it_behaves_like 'when path is absolute' do let(:last_commit) do Gitlab::Git::Commit - .last_for_path(project.repository, 'master', URI.unescape(file_path)) + .last_for_path(project.repository, 'master', Addressable::URI.unencode_component(file_path)) end let(:params_with_correct_id) { params.merge(last_commit_id: last_commit.id) } diff --git a/spec/requests/api/generic_packages_spec.rb b/spec/requests/api/generic_packages_spec.rb index ed852fe75c7..2cb686167f1 100644 --- a/spec/requests/api/generic_packages_spec.rb +++ b/spec/requests/api/generic_packages_spec.rb @@ -4,79 +4,432 @@ require 'spec_helper' RSpec.describe API::GenericPackages do let_it_be(:personal_access_token) { create(:personal_access_token) } - let_it_be(:project) { create(:project) } + let_it_be(:project, reload: true) { create(:project) } + 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(:user) { personal_access_token.user } + let(:ci_build) { create(:ci_build, :running, user: user) } - describe 'GET /api/v4/projects/:id/packages/generic/ping' do - let(:user) { personal_access_token.user } - let(:auth_token) { personal_access_token.token } + def auth_header + return {} if user_role == :anonymous + case authenticate_with + when :personal_access_token + personal_access_token_header + when :job_token + job_token_header + when :invalid_personal_access_token + personal_access_token_header('wrong token') + when :invalid_job_token + job_token_header('wrong token') + end + end + + def personal_access_token_header(value = nil) + { Gitlab::Auth::AuthFinders::PRIVATE_TOKEN_HEADER => value || personal_access_token.token } + end + + def job_token_header(value = nil) + { Gitlab::Auth::AuthFinders::JOB_TOKEN_HEADER => value || ci_build.token } + end + + shared_examples 'secure endpoint' do before do project.add_developer(user) end - context 'packages feature is disabled' do - it 'responds with 404 Not Found' do - stub_packages_setting(enabled: false) + it 'rejects malicious request' do + subject - ping(personal_access_token: auth_token) + expect(response).to have_gitlab_http_status(:bad_request) + end + end - expect(response).to have_gitlab_http_status(:not_found) + describe 'PUT /api/v4/projects/:id/packages/generic/:package_name/:package_version/:file_name/authorize' do + context 'with valid project' do + using RSpec::Parameterized::TableSyntax + + where(:project_visibility, :user_role, :member?, :authenticate_with, :expected_status) do + 'PUBLIC' | :developer | true | :personal_access_token | :success + 'PUBLIC' | :guest | true | :personal_access_token | :forbidden + 'PUBLIC' | :developer | true | :invalid_personal_access_token | :unauthorized + 'PUBLIC' | :guest | true | :invalid_personal_access_token | :unauthorized + 'PUBLIC' | :developer | false | :personal_access_token | :forbidden + 'PUBLIC' | :guest | false | :personal_access_token | :forbidden + 'PUBLIC' | :developer | false | :invalid_personal_access_token | :unauthorized + 'PUBLIC' | :guest | false | :invalid_personal_access_token | :unauthorized + 'PUBLIC' | :anonymous | false | :none | :unauthorized + 'PRIVATE' | :developer | true | :personal_access_token | :success + 'PRIVATE' | :guest | true | :personal_access_token | :forbidden + 'PRIVATE' | :developer | true | :invalid_personal_access_token | :unauthorized + 'PRIVATE' | :guest | true | :invalid_personal_access_token | :unauthorized + 'PRIVATE' | :developer | false | :personal_access_token | :not_found + 'PRIVATE' | :guest | false | :personal_access_token | :not_found + 'PRIVATE' | :developer | false | :invalid_personal_access_token | :unauthorized + 'PRIVATE' | :guest | false | :invalid_personal_access_token | :unauthorized + 'PRIVATE' | :anonymous | false | :none | :unauthorized + 'PUBLIC' | :developer | true | :job_token | :success + 'PUBLIC' | :developer | true | :invalid_job_token | :unauthorized + 'PUBLIC' | :developer | false | :job_token | :forbidden + 'PUBLIC' | :developer | false | :invalid_job_token | :unauthorized + 'PRIVATE' | :developer | true | :job_token | :success + 'PRIVATE' | :developer | true | :invalid_job_token | :unauthorized + 'PRIVATE' | :developer | false | :job_token | :not_found + 'PRIVATE' | :developer | false | :invalid_job_token | :unauthorized + end + + with_them do + before do + project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility, false)) + project.send("add_#{user_role}", user) if member? && user_role != :anonymous + end + + it "responds with #{params[:expected_status]}" do + authorize_upload_file(workhorse_header.merge(auth_header)) + + expect(response).to have_gitlab_http_status(expected_status) + end + end + end + + context 'application security' do + using RSpec::Parameterized::TableSyntax + + where(:param_name, :param_value) do + :package_name | 'my-package/../' + :package_name | 'my-package%2f%2e%2e%2f' + :file_name | '../.ssh%2fauthorized_keys' + :file_name | '%2e%2e%2f.ssh%2fauthorized_keys' + end + + with_them do + subject { authorize_upload_file(workhorse_header.merge(personal_access_token_header), param_name => param_value) } + + it_behaves_like 'secure endpoint' end end context 'generic_packages feature flag is disabled' do it 'responds with 404 Not Found' do stub_feature_flags(generic_packages: false) + project.add_developer(user) - ping(personal_access_token: auth_token) + authorize_upload_file(workhorse_header.merge(personal_access_token_header)) expect(response).to have_gitlab_http_status(:not_found) end end - context 'generic_packages feature flag is enabled' do + def authorize_upload_file(request_headers, package_name: 'mypackage', file_name: 'myfile.tar.gz') + url = "/projects/#{project.id}/packages/generic/#{package_name}/0.0.1/#{file_name}/authorize" + + put api(url), headers: request_headers + end + end + + describe 'PUT /api/v4/projects/:id/packages/generic/:package_name/:package_version/:file_name' do + include WorkhorseHelpers + + let(:file_upload) { fixture_file_upload('spec/fixtures/packages/generic/myfile.tar.gz') } + let(:params) { { file: file_upload } } + + context 'authentication' do + using RSpec::Parameterized::TableSyntax + + where(:project_visibility, :user_role, :member?, :authenticate_with, :expected_status) do + 'PUBLIC' | :guest | true | :personal_access_token | :forbidden + 'PUBLIC' | :developer | true | :invalid_personal_access_token | :unauthorized + 'PUBLIC' | :guest | true | :invalid_personal_access_token | :unauthorized + 'PUBLIC' | :developer | false | :personal_access_token | :forbidden + 'PUBLIC' | :guest | false | :personal_access_token | :forbidden + 'PUBLIC' | :developer | false | :invalid_personal_access_token | :unauthorized + 'PUBLIC' | :guest | false | :invalid_personal_access_token | :unauthorized + 'PUBLIC' | :anonymous | false | :none | :unauthorized + 'PRIVATE' | :guest | true | :personal_access_token | :forbidden + 'PRIVATE' | :developer | true | :invalid_personal_access_token | :unauthorized + 'PRIVATE' | :guest | true | :invalid_personal_access_token | :unauthorized + 'PRIVATE' | :developer | false | :personal_access_token | :not_found + 'PRIVATE' | :guest | false | :personal_access_token | :not_found + 'PRIVATE' | :developer | false | :invalid_personal_access_token | :unauthorized + 'PRIVATE' | :guest | false | :invalid_personal_access_token | :unauthorized + 'PRIVATE' | :anonymous | false | :none | :unauthorized + 'PUBLIC' | :developer | true | :invalid_job_token | :unauthorized + 'PUBLIC' | :developer | false | :job_token | :forbidden + 'PUBLIC' | :developer | false | :invalid_job_token | :unauthorized + 'PRIVATE' | :developer | true | :invalid_job_token | :unauthorized + 'PRIVATE' | :developer | false | :job_token | :not_found + 'PRIVATE' | :developer | false | :invalid_job_token | :unauthorized + end + + with_them do + before do + project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility, false)) + project.send("add_#{user_role}", user) if member? && user_role != :anonymous + end + + it "responds with #{params[:expected_status]}" do + headers = workhorse_header.merge(auth_header) + + upload_file(params, headers) + + expect(response).to have_gitlab_http_status(expected_status) + end + end + end + + context 'when user can upload packages and has valid credentials' do before do - stub_feature_flags(generic_packages: true) + project.add_developer(user) end - context 'authenticating using personal access token' do - it 'responds with 200 OK when valid personal access token is provided' do - ping(personal_access_token: auth_token) + it 'creates package and package file when valid personal access token is used' do + headers = workhorse_header.merge(personal_access_token_header) + + expect { upload_file(params, headers) } + .to change { project.packages.generic.count }.by(1) + .and change { Packages::PackageFile.count }.by(1) + + aggregate_failures do + expect(response).to have_gitlab_http_status(:created) - expect(response).to have_gitlab_http_status(:ok) + package = project.packages.generic.last + expect(package.name).to eq('mypackage') + expect(package.version).to eq('0.0.1') + expect(package.build_info).to be_nil + + package_file = package.package_files.last + expect(package_file.file_name).to eq('myfile.tar.gz') end + end + + it 'creates package, package file, and package build info when valid job token is used' do + headers = workhorse_header.merge(job_token_header) + + expect { upload_file(params, headers) } + .to change { project.packages.generic.count }.by(1) + .and change { Packages::PackageFile.count }.by(1) - it 'responds with 401 Unauthorized when invalid personal access token provided' do - ping(personal_access_token: 'invalid-token') + aggregate_failures do + expect(response).to have_gitlab_http_status(:created) - expect(response).to have_gitlab_http_status(:unauthorized) + package = project.packages.generic.last + expect(package.name).to eq('mypackage') + expect(package.version).to eq('0.0.1') + expect(package.build_info.pipeline).to eq(ci_build.pipeline) + + package_file = package.package_files.last + expect(package_file.file_name).to eq('myfile.tar.gz') end end - context 'authenticating using job token' do - it 'responds with 200 OK when valid job token is provided' do - job_token = create(:ci_build, :running, user: user).token + context 'event tracking' do + subject { upload_file(params, workhorse_header.merge(personal_access_token_header)) } + + it_behaves_like 'a gitlab tracking event', described_class.name, 'push_package' + end + + it 'rejects request without a file from workhorse' do + headers = workhorse_header.merge(personal_access_token_header) + upload_file({}, headers) + + expect(response).to have_gitlab_http_status(:bad_request) + end + + it 'rejects request without an auth token' do + upload_file(params, workhorse_header) + + expect(response).to have_gitlab_http_status(:unauthorized) + end + + it 'rejects request without workhorse rewritten fields' do + headers = workhorse_header.merge(personal_access_token_header) + upload_file(params, headers, send_rewritten_field: false) - ping(job_token: job_token) + expect(response).to have_gitlab_http_status(:bad_request) + end - expect(response).to have_gitlab_http_status(:ok) + it 'rejects request if file size is too large' do + allow_next_instance_of(UploadedFile) do |uploaded_file| + allow(uploaded_file).to receive(:size).and_return(project.actual_limits.generic_packages_max_file_size + 1) end - it 'responds with 401 Unauthorized when invalid job token provided' do - ping(job_token: 'invalid-token') + headers = workhorse_header.merge(personal_access_token_header) + upload_file(params, headers) + + expect(response).to have_gitlab_http_status(:bad_request) + end + + it 'rejects request without workhorse header' do + expect(Gitlab::ErrorTracking).to receive(:track_exception).once - expect(response).to have_gitlab_http_status(:unauthorized) + upload_file(params, personal_access_token_header) + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + + context 'application security' do + using RSpec::Parameterized::TableSyntax + + where(:param_name, :param_value) do + :package_name | 'my-package/../' + :package_name | 'my-package%2f%2e%2e%2f' + :file_name | '../.ssh%2fauthorized_keys' + :file_name | '%2e%2e%2f.ssh%2fauthorized_keys' + end + + with_them do + subject { upload_file(params, workhorse_header.merge(personal_access_token_header), param_name => param_value) } + + it_behaves_like 'secure endpoint' + end + end + + def upload_file(params, request_headers, send_rewritten_field: true, package_name: 'mypackage', file_name: 'myfile.tar.gz') + url = "/projects/#{project.id}/packages/generic/#{package_name}/0.0.1/#{file_name}" + + workhorse_finalize( + api(url), + method: :put, + file_key: :file, + params: params, + headers: request_headers, + send_rewritten_field: send_rewritten_field + ) + end + end + + describe 'GET /api/v4/projects/:id/packages/generic/:package_name/:package_version/:file_name' do + using RSpec::Parameterized::TableSyntax + + let_it_be(:package) { create(:generic_package, project: project) } + let_it_be(:package_file) { create(:package_file, :generic, package: package) } + + context 'authentication' do + where(:project_visibility, :user_role, :member?, :authenticate_with, :expected_status) do + 'PUBLIC' | :developer | true | :personal_access_token | :success + 'PUBLIC' | :guest | true | :personal_access_token | :success + 'PUBLIC' | :developer | true | :invalid_personal_access_token | :unauthorized + 'PUBLIC' | :guest | true | :invalid_personal_access_token | :unauthorized + 'PUBLIC' | :developer | false | :personal_access_token | :success + 'PUBLIC' | :guest | false | :personal_access_token | :success + 'PUBLIC' | :developer | false | :invalid_personal_access_token | :unauthorized + 'PUBLIC' | :guest | false | :invalid_personal_access_token | :unauthorized + 'PUBLIC' | :anonymous | false | :none | :unauthorized + 'PRIVATE' | :developer | true | :personal_access_token | :success + 'PRIVATE' | :guest | true | :personal_access_token | :forbidden + 'PRIVATE' | :developer | true | :invalid_personal_access_token | :unauthorized + 'PRIVATE' | :guest | true | :invalid_personal_access_token | :unauthorized + 'PRIVATE' | :developer | false | :personal_access_token | :not_found + 'PRIVATE' | :guest | false | :personal_access_token | :not_found + 'PRIVATE' | :developer | false | :invalid_personal_access_token | :unauthorized + 'PRIVATE' | :guest | false | :invalid_personal_access_token | :unauthorized + 'PRIVATE' | :anonymous | false | :none | :unauthorized + 'PUBLIC' | :developer | true | :job_token | :success + 'PUBLIC' | :developer | true | :invalid_job_token | :unauthorized + 'PUBLIC' | :developer | false | :job_token | :success + 'PUBLIC' | :developer | false | :invalid_job_token | :unauthorized + 'PRIVATE' | :developer | true | :job_token | :success + 'PRIVATE' | :developer | true | :invalid_job_token | :unauthorized + 'PRIVATE' | :developer | false | :job_token | :not_found + 'PRIVATE' | :developer | false | :invalid_job_token | :unauthorized + end + + with_them do + before do + project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility, false)) + project.send("add_#{user_role}", user) if member? && user_role != :anonymous + end + + it "responds with #{params[:expected_status]}" do + download_file(auth_header) + + expect(response).to have_gitlab_http_status(expected_status) end end end - def ping(personal_access_token: nil, job_token: nil) - headers = { - Gitlab::Auth::AuthFinders::PRIVATE_TOKEN_HEADER => personal_access_token.presence, - Gitlab::Auth::AuthFinders::JOB_TOKEN_HEADER => job_token.presence - }.compact + context 'event tracking' do + before do + project.add_developer(user) + end + + subject { download_file(personal_access_token_header) } + + it_behaves_like 'a gitlab tracking event', described_class.name, 'pull_package' + end + + it 'rejects a malicious file name request' do + project.add_developer(user) + + download_file(personal_access_token_header, file_name: '../.ssh%2fauthorized_keys') + + expect(response).to have_gitlab_http_status(:bad_request) + end + + it 'rejects a malicious file name request' do + project.add_developer(user) + + download_file(personal_access_token_header, file_name: '%2e%2e%2f.ssh%2fauthorized_keys') + + expect(response).to have_gitlab_http_status(:bad_request) + end + + it 'rejects a malicious package name request' do + project.add_developer(user) + + download_file(personal_access_token_header, package_name: 'my-package/../') + + expect(response).to have_gitlab_http_status(:bad_request) + end + + it 'rejects a malicious package name request' do + project.add_developer(user) + + download_file(personal_access_token_header, package_name: 'my-package%2f%2e%2e%2f') + + expect(response).to have_gitlab_http_status(:bad_request) + end + + context 'application security' do + using RSpec::Parameterized::TableSyntax + + where(:param_name, :param_value) do + :package_name | 'my-package/../' + :package_name | 'my-package%2f%2e%2e%2f' + :file_name | '../.ssh%2fauthorized_keys' + :file_name | '%2e%2e%2f.ssh%2fauthorized_keys' + end + + with_them do + subject { download_file(personal_access_token_header, param_name => param_value) } + + it_behaves_like 'secure endpoint' + end + end + + it 'responds with 404 Not Found for non existing package' do + project.add_developer(user) + + download_file(personal_access_token_header, package_name: 'no-such-package') + + expect(response).to have_gitlab_http_status(:not_found) + end + + it 'responds with 404 Not Found for non existing package file' do + project.add_developer(user) + + download_file(personal_access_token_header, file_name: 'no-such-file') + + expect(response).to have_gitlab_http_status(:not_found) + end + + def download_file(request_headers, package_name: nil, file_name: nil) + package_name ||= package.name + file_name ||= package_file.file_name + url = "/projects/#{project.id}/packages/generic/#{package_name}/#{package.version}/#{file_name}" - get api('/projects/%d/packages/generic/ping' % project.id), headers: headers + get api(url), headers: request_headers end end end diff --git a/spec/requests/api/graphql/gitlab_schema_spec.rb b/spec/requests/api/graphql/gitlab_schema_spec.rb index ee7dba545be..fe1c7c15de2 100644 --- a/spec/requests/api/graphql/gitlab_schema_spec.rb +++ b/spec/requests/api/graphql/gitlab_schema_spec.rb @@ -190,7 +190,9 @@ RSpec.describe 'GitlabSchema configurations' do variables: {}.to_s, complexity: 181, depth: 13, - duration_s: 7 + duration_s: 7, + used_fields: an_instance_of(Array), + used_deprecated_fields: an_instance_of(Array) } expect_any_instance_of(Gitlab::Graphql::QueryAnalyzers::LoggerAnalyzer).to receive(:duration).and_return(7) diff --git a/spec/requests/api/graphql/mutations/boards/lists/destroy_spec.rb b/spec/requests/api/graphql/mutations/boards/lists/destroy_spec.rb new file mode 100644 index 00000000000..42f690f53ed --- /dev/null +++ b/spec/requests/api/graphql/mutations/boards/lists/destroy_spec.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Mutations::Boards::Lists::Destroy do + include GraphqlHelpers + + let_it_be(:current_user, reload: true) { create(:user) } + let_it_be(:project, reload: true) { create(:project) } + let_it_be(:board) { create(:board, project: project) } + let_it_be(:list) { create(:list, board: board) } + let(:mutation) do + variables = { + list_id: GitlabSchema.id_from_object(list).to_s + } + + graphql_mutation(:destroy_board_list, variables) + end + + subject { post_graphql_mutation(mutation, current_user: current_user) } + + def mutation_response + graphql_mutation_response(:destroy_board_list) + end + + context 'when the user does not have permission' do + it_behaves_like 'a mutation that returns a top-level access error' + + it 'does not destroy the list' do + expect { subject }.not_to change { List.count } + end + end + + context 'when the user has permission' do + before do + project.add_maintainer(current_user) + end + + context 'when given id is not for a list' do + let_it_be(:list) { build_stubbed(:issue, project: project) } + + it 'returns an error' do + subject + + expect(graphql_errors.first['message']).to include('does not represent an instance of List') + end + end + + context 'when everything is ok' do + it 'destroys the list' do + expect { subject }.to change { List.count }.from(2).to(1) + end + + it 'returns an empty list' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(mutation_response).to have_key('list') + expect(mutation_response['list']).to be_nil + end + end + + context 'when the list is not destroyable' do + let_it_be(:list) { create(:list, board: board, list_type: :backlog) } + + it 'does not destroy the list' do + expect { subject }.not_to change { List.count }.from(3) + end + + it 'returns an error and not nil list' do + subject + + expect(mutation_response['errors']).not_to be_empty + expect(mutation_response['list']).not_to be_nil + end + 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 1bb446de708..a708c3fdf1f 100644 --- a/spec/requests/api/graphql/mutations/snippets/create_spec.rb +++ b/spec/requests/api/graphql/mutations/snippets/create_spec.rb @@ -79,18 +79,20 @@ RSpec.describe 'Creating a Snippet' do end shared_examples 'creates snippet' do - it 'returns the created Snippet' do + it 'returns the created Snippet', :aggregate_failures do expect do subject end.to change { Snippet.count }.by(1) + snippet = Snippet.last + created_file_1 = snippet.repository.blob_at('HEAD', file_1[:filePath]) + created_file_2 = snippet.repository.blob_at('HEAD', file_2[:filePath]) + + expect(created_file_1.data).to match(file_1[:content]) + expect(created_file_2.data).to match(file_2[:content]) expect(mutation_response['snippet']['title']).to eq(title) expect(mutation_response['snippet']['description']).to eq(description) expect(mutation_response['snippet']['visibilityLevel']).to eq(visibility_level) - expect(mutation_response['snippet']['blobs'][0]['plainData']).to match(file_1[:content]) - expect(mutation_response['snippet']['blobs'][0]['fileName']).to match(file_1[:file_path]) - expect(mutation_response['snippet']['blobs'][1]['plainData']).to match(file_2[:content]) - expect(mutation_response['snippet']['blobs'][1]['fileName']).to match(file_2[:file_path]) end context 'when action is invalid' do diff --git a/spec/requests/api/graphql/mutations/snippets/update_spec.rb b/spec/requests/api/graphql/mutations/snippets/update_spec.rb index 58ce74b9263..67a9869c001 100644 --- a/spec/requests/api/graphql/mutations/snippets/update_spec.rb +++ b/spec/requests/api/graphql/mutations/snippets/update_spec.rb @@ -73,7 +73,6 @@ RSpec.describe 'Updating a Snippet' do aggregate_failures do expect(blob_to_update.data).to eq updated_content expect(blob_to_delete).to be_nil - expect(blob_in_mutation_response(updated_file)['plainData']).to match(updated_content) expect(mutation_response['snippet']['title']).to eq(updated_title) expect(mutation_response['snippet']['description']).to eq(updated_description) expect(mutation_response['snippet']['visibilityLevel']).to eq('public') @@ -100,7 +99,6 @@ RSpec.describe 'Updating a Snippet' do aggregate_failures do expect(blob_at(updated_file).data).to eq blob_to_update.data expect(blob_at(deleted_file).data).to eq blob_to_delete.data - expect(blob_in_mutation_response(deleted_file)['plainData']).not_to be_nil expect(mutation_response['snippet']['title']).to eq(original_title) expect(mutation_response['snippet']['description']).to eq(original_description) expect(mutation_response['snippet']['visibilityLevel']).to eq('private') @@ -108,10 +106,6 @@ RSpec.describe 'Updating a Snippet' do end end - def blob_in_mutation_response(filename) - mutation_response['snippet']['blobs'].select { |blob| blob['name'] == filename }[0] - end - def blob_at(filename) snippet.repository.blob_at('HEAD', filename) end diff --git a/spec/requests/api/graphql/project/issues_spec.rb b/spec/requests/api/graphql/project/issues_spec.rb index 5d4276f47ca..40fec6ba068 100644 --- a/spec/requests/api/graphql/project/issues_spec.rb +++ b/spec/requests/api/graphql/project/issues_spec.rb @@ -53,16 +53,37 @@ RSpec.describe 'getting an issue list for a project' do context 'when limiting the number of results' do let(:query) do - graphql_query_for( - 'project', - { 'fullPath' => project.full_path }, - "issues(first: 1) { #{fields} }" - ) + <<~GQL + query($path: ID!, $n: Int) { + project(fullPath: $path) { + issues(first: $n) { #{fields} } + } + } + GQL + end + + let(:issue_limit) { 1 } + let(:variables) do + { path: project.full_path, n: issue_limit } end it_behaves_like 'a working graphql query' do before do - post_graphql(query, current_user: current_user) + post_graphql(query, current_user: current_user, variables: variables) + end + + it 'only returns N issues' do + expect(issues_data.size).to eq(issue_limit) + end + end + + context 'no limit is provided' do + let(:issue_limit) { nil } + + it 'returns all issues' do + post_graphql(query, current_user: current_user, variables: variables) + + expect(issues_data.size).to be > 1 end end @@ -71,7 +92,7 @@ RSpec.describe 'getting an issue list for a project' do # Newest first, we only want to see the newest checked expect(Ability).not_to receive(:allowed?).with(current_user, :read_issue, issues.first) - post_graphql(query, current_user: current_user) + post_graphql(query, current_user: current_user, variables: variables) end end diff --git a/spec/requests/api/graphql_spec.rb b/spec/requests/api/graphql_spec.rb index ff1a5aa1540..94a66f54e4d 100644 --- a/spec/requests/api/graphql_spec.rb +++ b/spec/requests/api/graphql_spec.rb @@ -9,7 +9,15 @@ RSpec.describe 'GraphQL' do context 'logging' do shared_examples 'logging a graphql query' do let(:expected_params) do - { query_string: query, variables: variables.to_s, duration_s: anything, depth: 1, complexity: 1 } + { + query_string: query, + variables: variables.to_s, + duration_s: anything, + depth: 1, + complexity: 1, + used_fields: ['Query.echo'], + used_deprecated_fields: [] + } end it 'logs a query with the expected params' do diff --git a/spec/requests/api/group_clusters_spec.rb b/spec/requests/api/group_clusters_spec.rb index 068af1485e2..eb21ae9468c 100644 --- a/spec/requests/api/group_clusters_spec.rb +++ b/spec/requests/api/group_clusters_spec.rb @@ -172,6 +172,7 @@ RSpec.describe API::GroupClusters do name: 'test-cluster', domain: 'domain.example.com', managed: false, + namespace_per_environment: false, platform_kubernetes_attributes: platform_kubernetes_attributes, management_project_id: management_project_id } @@ -206,6 +207,7 @@ RSpec.describe API::GroupClusters do expect(cluster_result.domain).to eq('domain.example.com') expect(cluster_result.managed).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 expect(platform_kubernetes.api_url).to eq(api_url) expect(platform_kubernetes.token).to eq('sample-token') @@ -237,6 +239,22 @@ RSpec.describe API::GroupClusters do end end + context 'when namespace_per_environment is not set' do + let(:cluster_params) do + { + name: 'test-cluster', + domain: 'domain.example.com', + platform_kubernetes_attributes: platform_kubernetes_attributes + } + end + + it 'defaults to true' do + cluster_result = Clusters::Cluster.find(json_response['id']) + + expect(cluster_result).to be_namespace_per_environment + end + end + context 'current user does not have access to management_project_id' do let(:management_project_id) { create(:project).id } diff --git a/spec/requests/api/group_container_repositories_spec.rb b/spec/requests/api/group_container_repositories_spec.rb index 3128becae6d..4b97fad79dd 100644 --- a/spec/requests/api/group_container_repositories_spec.rb +++ b/spec/requests/api/group_container_repositories_spec.rb @@ -44,7 +44,7 @@ RSpec.describe API::GroupContainerRepositories do let(:object) { group } end - it_behaves_like 'a gitlab tracking event', described_class.name, 'list_repositories' + it_behaves_like 'a package tracking event', described_class.name, 'list_repositories' context 'with invalid group id' do let(:url) { "/groups/#{non_existing_record_id}/registry/repositories" } diff --git a/spec/requests/api/group_packages_spec.rb b/spec/requests/api/group_packages_spec.rb index f67cafbd8f5..72ba25c59af 100644 --- a/spec/requests/api/group_packages_spec.rb +++ b/spec/requests/api/group_packages_spec.rb @@ -77,7 +77,7 @@ RSpec.describe API::GroupPackages do it_behaves_like 'returns packages', :group, :owner it_behaves_like 'returns packages', :group, :maintainer it_behaves_like 'returns packages', :group, :developer - it_behaves_like 'rejects packages access', :group, :reporter, :forbidden + it_behaves_like 'returns packages', :group, :reporter it_behaves_like 'rejects packages access', :group, :guest, :forbidden context 'with subgroup' do @@ -88,7 +88,7 @@ RSpec.describe API::GroupPackages do it_behaves_like 'returns packages with subgroups', :group, :owner it_behaves_like 'returns packages with subgroups', :group, :maintainer it_behaves_like 'returns packages with subgroups', :group, :developer - it_behaves_like 'rejects packages access', :group, :reporter, :forbidden + it_behaves_like 'returns packages with subgroups', :group, :reporter it_behaves_like 'rejects packages access', :group, :guest, :forbidden context 'excluding subgroup' do @@ -97,7 +97,7 @@ RSpec.describe API::GroupPackages do it_behaves_like 'returns packages', :group, :owner it_behaves_like 'returns packages', :group, :maintainer it_behaves_like 'returns packages', :group, :developer - it_behaves_like 'rejects packages access', :group, :reporter, :forbidden + it_behaves_like 'returns packages', :group, :reporter it_behaves_like 'rejects packages access', :group, :guest, :forbidden end end diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb index da423e986c3..c7756a4fae5 100644 --- a/spec/requests/api/groups_spec.rb +++ b/spec/requests/api/groups_spec.rb @@ -1391,6 +1391,139 @@ RSpec.describe API::Groups do end end + describe 'GET /groups/:id/descendant_groups' do + let_it_be(:child_group1) { create(:group, parent: group1) } + let_it_be(:private_child_group1) { create(:group, :private, parent: group1) } + let_it_be(:sub_child_group1) { create(:group, parent: child_group1) } + let_it_be(:child_group2) { create(:group, :private, parent: group2) } + let_it_be(:sub_child_group2) { create(:group, :private, parent: child_group2) } + let(:response_groups) { json_response.map { |group| group['name'] } } + + context 'when unauthenticated' do + it 'returns only public descendants' do + get api("/groups/#{group1.id}/descendant_groups") + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.length).to eq(2) + expect(response_groups).to contain_exactly(child_group1.name, sub_child_group1.name) + end + + it 'returns 404 for a private group' do + get api("/groups/#{group2.id}/descendant_groups") + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'when authenticated as user' do + context 'when user is not member of a public group' do + it 'returns no descendants for the public group' do + get api("/groups/#{group1.id}/descendant_groups", user2) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to be_an Array + expect(json_response.length).to eq(0) + end + + context 'when using all_available in request' do + it 'returns public descendants' do + get api("/groups/#{group1.id}/descendant_groups", user2), params: { all_available: true } + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to be_an Array + expect(json_response.length).to eq(2) + expect(response_groups).to contain_exactly(child_group1.name, sub_child_group1.name) + end + end + end + + context 'when user is not member of a private group' do + it 'returns 404 for the private group' do + get api("/groups/#{group2.id}/descendant_groups", user1) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'when user is member of public group' do + before do + group1.add_guest(user2) + end + + it 'returns private descendants' do + get api("/groups/#{group1.id}/descendant_groups", user2) + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.length).to eq(3) + expect(response_groups).to contain_exactly(child_group1.name, sub_child_group1.name, private_child_group1.name) + end + + context 'when using statistics in request' do + it 'does not include statistics' do + get api("/groups/#{group1.id}/descendant_groups", user2), params: { statistics: true } + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to be_an Array + expect(json_response.first).not_to include 'statistics' + end + end + end + + context 'when user is member of private group' do + before do + group2.add_guest(user1) + end + + it 'returns descendants' do + get api("/groups/#{group2.id}/descendant_groups", user1) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to be_an Array + expect(json_response.length).to eq(2) + expect(response_groups).to contain_exactly(child_group2.name, sub_child_group2.name) + end + end + end + + context 'when authenticated as admin' do + it 'returns private descendants of a public group' do + get api("/groups/#{group1.id}/descendant_groups", admin) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to be_an Array + expect(json_response.length).to eq(3) + end + + it 'returns descendants of a private group' do + get api("/groups/#{group2.id}/descendant_groups", admin) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to be_an Array + expect(json_response.length).to eq(2) + end + + it 'does not include statistics by default' do + get api("/groups/#{group1.id}/descendant_groups", admin) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to be_an Array + expect(json_response.first).not_to include('statistics') + end + + it 'includes statistics if requested' do + get api("/groups/#{group1.id}/descendant_groups", admin), params: { statistics: true } + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to be_an Array + expect(json_response.first).to include('statistics') + end + end + end + describe "POST /groups" do it_behaves_like 'group avatar upload' do def make_upload_request diff --git a/spec/requests/api/helpers_spec.rb b/spec/requests/api/helpers_spec.rb index 9c0ea14e3e3..91d10791541 100644 --- a/spec/requests/api/helpers_spec.rb +++ b/spec/requests/api/helpers_spec.rb @@ -9,7 +9,7 @@ RSpec.describe API::Helpers do include described_class include TermsHelper - let(:user) { create(:user) } + let_it_be(:user, reload: true) { create(:user) } let(:admin) { create(:admin) } let(:key) { create(:key, user: user) } @@ -243,6 +243,67 @@ RSpec.describe API::Helpers do end end end + + describe "when authenticating using a job token" do + let_it_be(:job, reload: true) do + create(:ci_build, user: user, status: :running) + end + + let(:route_authentication_setting) { {} } + + before do + allow_any_instance_of(self.class).to receive(:route_authentication_setting) + .and_return(route_authentication_setting) + end + + context 'when route is allowed to be authenticated' do + let(:route_authentication_setting) { { job_token_allowed: true } } + + it "returns a 401 response for an invalid token" do + env[Gitlab::Auth::AuthFinders::JOB_TOKEN_HEADER] = 'invalid token' + + expect { current_user }.to raise_error /401/ + end + + it "returns a 401 response for a job that's not running" do + job.update!(status: :success) + env[Gitlab::Auth::AuthFinders::JOB_TOKEN_HEADER] = job.token + + expect { current_user }.to raise_error /401/ + end + + it "returns a 403 response for a user without access" do + env[Gitlab::Auth::AuthFinders::JOB_TOKEN_HEADER] = job.token + allow_any_instance_of(Gitlab::UserAccess).to receive(:allowed?).and_return(false) + + expect { current_user }.to raise_error /403/ + end + + it 'returns a 403 response for a user who is blocked' do + user.block! + env[Gitlab::Auth::AuthFinders::JOB_TOKEN_HEADER] = job.token + + expect { current_user }.to raise_error /403/ + end + + it "sets current_user" do + env[Gitlab::Auth::AuthFinders::JOB_TOKEN_HEADER] = job.token + + expect(current_user).to eq(user) + end + end + + context 'when route is not allowed to be authenticated' do + let(:route_authentication_setting) { { job_token_allowed: false } } + + it "sets current_user to nil" do + env[Gitlab::Auth::AuthFinders::JOB_TOKEN_HEADER] = job.token + allow_any_instance_of(Gitlab::UserAccess).to receive(:allowed?).and_return(true) + + expect(current_user).to be_nil + end + end + end end describe '.handle_api_exception' do diff --git a/spec/requests/api/internal/base_spec.rb b/spec/requests/api/internal/base_spec.rb index 4a0a7c81781..e13b492ecc8 100644 --- a/spec/requests/api/internal/base_spec.rb +++ b/spec/requests/api/internal/base_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' RSpec.describe API::Internal::Base do + include APIInternalBaseHelpers + let_it_be(:user, reload: true) { create(:user) } let_it_be(:project, reload: true) { create(:project, :repository, :wiki_repo) } let_it_be(:personal_snippet) { create(:personal_snippet, :repository, author: user) } @@ -1207,88 +1209,6 @@ RSpec.describe API::Internal::Base do end end - def gl_repository_for(container) - case container - when ProjectWiki - Gitlab::GlRepository::WIKI.identifier_for_container(container.project) - when Project - Gitlab::GlRepository::PROJECT.identifier_for_container(container) - when Snippet - Gitlab::GlRepository::SNIPPET.identifier_for_container(container) - else - nil - end - end - - def full_path_for(container) - case container - when PersonalSnippet - "snippets/#{container.id}" - when ProjectSnippet - "#{container.project.full_path}/snippets/#{container.id}" - else - container.full_path - end - end - - def pull(key, container, protocol = 'ssh') - post( - api("/internal/allowed"), - params: { - key_id: key.id, - project: full_path_for(container), - gl_repository: gl_repository_for(container), - action: 'git-upload-pack', - secret_token: secret_token, - protocol: protocol - } - ) - end - - def push(key, container, protocol = 'ssh', env: nil, changes: nil) - push_with_path(key, - full_path: full_path_for(container), - gl_repository: gl_repository_for(container), - protocol: protocol, - env: env, - changes: changes) - end - - def push_with_path(key, full_path:, gl_repository: nil, protocol: 'ssh', env: nil, changes: nil) - changes ||= 'd14d6c0abdd253381df51a723d58691b2ee1ab08 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/master' - - params = { - changes: changes, - key_id: key.id, - project: full_path, - action: 'git-receive-pack', - secret_token: secret_token, - protocol: protocol, - env: env - } - params[:gl_repository] = gl_repository if gl_repository - - post( - api("/internal/allowed"), - params: params - ) - end - - def archive(key, container) - post( - api("/internal/allowed"), - params: { - ref: 'master', - key_id: key.id, - project: full_path_for(container), - gl_repository: gl_repository_for(container), - action: 'git-upload-archive', - secret_token: secret_token, - protocol: 'ssh' - } - ) - end - def lfs_auth_project(project) post( api("/internal/lfs_authenticate"), diff --git a/spec/requests/api/internal/lfs_spec.rb b/spec/requests/api/internal/lfs_spec.rb new file mode 100644 index 00000000000..4739ec62992 --- /dev/null +++ b/spec/requests/api/internal/lfs_spec.rb @@ -0,0 +1,93 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe API::Internal::Lfs do + include APIInternalBaseHelpers + + let_it_be(:project) { create(:project) } + let_it_be(:lfs_object) { create(:lfs_object, :with_file) } + let_it_be(:lfs_objects_project) { create(:lfs_objects_project, project: project, lfs_object: lfs_object) } + let_it_be(:gl_repository) { "project-#{project.id}" } + let_it_be(:filename) { lfs_object.file.path } + + let(:secret_token) { Gitlab::Shell.secret_token } + + describe 'GET /internal/lfs' do + let(:valid_params) do + { oid: lfs_object.oid, gl_repository: gl_repository, secret_token: secret_token } + end + + context 'with invalid auth' do + let(:invalid_params) { valid_params.merge!(secret_token: 'invalid_tokne') } + + it 'returns 401' do + get api("/internal/lfs"), params: invalid_params + end + end + + context 'with valid auth' do + context 'LFS in local storage' do + it 'sends the file' do + get api("/internal/lfs"), params: valid_params + + expect(response).to have_gitlab_http_status(:ok) + expect(response.headers['Content-Type']).to eq('application/octet-stream') + expect(response.headers['Content-Length'].to_i).to eq(File.stat(filename).size) + expect(response.body).to eq(File.open(filename, 'rb', &:read)) + end + + # https://www.rubydoc.info/github/rack/rack/master/Rack/Sendfile + it 'delegates sending to Web server' do + get api("/internal/lfs"), params: valid_params, env: { 'HTTP_X_SENDFILE_TYPE' => 'X-Sendfile' } + + expect(response).to have_gitlab_http_status(:ok) + expect(response.headers['Content-Type']).to eq('application/octet-stream') + expect(response.headers['Content-Length'].to_i).to eq(0) + expect(response.headers['X-Sendfile']).to be_present + expect(response.body).to eq("") + end + + it 'retuns 404 for unknown file' do + params = valid_params.merge(oid: SecureRandom.hex) + + get api("/internal/lfs"), params: params + + expect(response).to have_gitlab_http_status(:not_found) + end + + it 'returns 404 if LFS object does not belong to project' do + other_lfs = create(:lfs_object, :with_file) + params = valid_params.merge(oid: other_lfs.oid) + + get api("/internal/lfs"), params: params + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'LFS in object storage' do + let!(:lfs_object2) { create(:lfs_object, :with_file) } + let!(:lfs_objects_project2) { create(:lfs_objects_project, project: project, lfs_object: lfs_object2) } + let(:valid_params) do + { oid: lfs_object2.oid, gl_repository: gl_repository, secret_token: secret_token } + end + + before do + stub_lfs_object_storage(enabled: true) + lfs_object2.file.migrate!(LfsObjectUploader::Store::REMOTE) + end + + it 'notifies Workhorse to send the file' do + get api("/internal/lfs"), params: valid_params + + expect(response).to have_gitlab_http_status(:ok) + expect(response.headers[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("send-url:") + expect(response.headers['Content-Type']).to eq('application/octet-stream') + expect(response.headers['Content-Length'].to_i).to eq(0) + expect(response.body).to eq("") + end + end + end + end +end diff --git a/spec/requests/api/lint_spec.rb b/spec/requests/api/lint_spec.rb index 4c60c8bd2a3..6b5a4b6436a 100644 --- a/spec/requests/api/lint_spec.rb +++ b/spec/requests/api/lint_spec.rb @@ -17,23 +17,52 @@ RSpec.describe API::Lint do expect(json_response['status']).to eq('valid') expect(json_response['errors']).to eq([]) end + + it 'outputs expanded yaml content' do + post api('/ci/lint'), params: { content: yaml_content, include_merged_yaml: true } + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to have_key('merged_yaml') + end end context 'with an invalid .gitlab_ci.yml' do - it 'responds with errors about invalid syntax' do - post api('/ci/lint'), params: { content: 'invalid content' } + context 'with invalid syntax' do + let(:yaml_content) { 'invalid content' } - expect(response).to have_gitlab_http_status(:ok) - expect(json_response['status']).to eq('invalid') - expect(json_response['errors']).to eq(['Invalid configuration format']) + it 'responds with errors about invalid syntax' do + post api('/ci/lint'), params: { content: yaml_content } + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['status']).to eq('invalid') + expect(json_response['errors']).to eq(['Invalid configuration format']) + end + + it 'outputs expanded yaml content' do + post api('/ci/lint'), params: { content: yaml_content, include_merged_yaml: true } + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to have_key('merged_yaml') + end end - it "responds with errors about invalid configuration" do - post api('/ci/lint'), params: { content: '{ image: "ruby:2.7", services: ["postgres"] }' } + context 'with invalid configuration' do + let(:yaml_content) { '{ image: "ruby:2.7", services: ["postgres"] }' } - expect(response).to have_gitlab_http_status(:ok) - expect(json_response['status']).to eq('invalid') - expect(json_response['errors']).to eq(['jobs config should contain at least one visible job']) + it 'responds with errors about invalid configuration' do + post api('/ci/lint'), params: { content: yaml_content } + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['status']).to eq('invalid') + expect(json_response['errors']).to eq(['jobs config should contain at least one visible job']) + end + + it 'outputs expanded yaml content' do + post api('/ci/lint'), params: { content: yaml_content, include_merged_yaml: true } + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to have_key('merged_yaml') + end end end diff --git a/spec/requests/api/maven_packages_spec.rb b/spec/requests/api/maven_packages_spec.rb index 0a23aed109b..a67bc157e5a 100644 --- a/spec/requests/api/maven_packages_spec.rb +++ b/spec/requests/api/maven_packages_spec.rb @@ -36,7 +36,7 @@ RSpec.describe API::MavenPackages do context 'with jar file' do let_it_be(:package_file) { jar_file } - it_behaves_like 'a gitlab tracking event', described_class.name, 'pull_package' + it_behaves_like 'a package tracking event', described_class.name, 'pull_package' end end @@ -571,7 +571,7 @@ RSpec.describe API::MavenPackages do context 'event tracking' do subject { upload_file_with_token(params) } - it_behaves_like 'a gitlab tracking event', described_class.name, 'push_package' + it_behaves_like 'a package tracking event', described_class.name, 'push_package' end it 'creates package and stores package file' do diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index 2757c56e0fe..0e5fa24ad66 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -1140,7 +1140,7 @@ RSpec.describe API::MergeRequests do context 'when a merge request has more than the changes limit' do it "returns a string indicating that more changes were made" do - stub_const('Commit::DIFF_HARD_LIMIT_FILES', 5) + allow(Commit).to receive(:diff_hard_limit_files).and_return(5) merge_request_overflow = create(:merge_request, :simple, author: user, diff --git a/spec/requests/api/npm_packages_spec.rb b/spec/requests/api/npm_packages_spec.rb index 108ea84b7e6..8a3ccd7c6e3 100644 --- a/spec/requests/api/npm_packages_spec.rb +++ b/spec/requests/api/npm_packages_spec.rb @@ -88,12 +88,16 @@ RSpec.describe API::NpmPackages do it_behaves_like 'returning the npm package info' context 'with unknown package' do + subject { get api("/packages/npm/unknown") } + it 'returns a redirect' do - get api("/packages/npm/unknown") + subject expect(response).to have_gitlab_http_status(:found) expect(response.headers['Location']).to eq('https://registry.npmjs.org/unknown') end + + it_behaves_like 'a gitlab tracking event', described_class.name, 'npm_request_forward' end end @@ -193,7 +197,7 @@ RSpec.describe API::NpmPackages do expect(response.media_type).to eq('application/octet-stream') end - it_behaves_like 'a gitlab tracking event', described_class.name, 'pull_package' + it_behaves_like 'a package tracking event', described_class.name, 'pull_package' end context 'private project' do @@ -301,7 +305,7 @@ RSpec.describe API::NpmPackages do context 'with access token' do subject { upload_package_with_token(package_name, params) } - it_behaves_like 'a gitlab tracking event', described_class.name, 'push_package' + it_behaves_like 'a package tracking event', described_class.name, 'push_package' it 'creates npm package with file' do expect { subject } diff --git a/spec/requests/api/project_clusters_spec.rb b/spec/requests/api/project_clusters_spec.rb index ff35e380476..7b37862af74 100644 --- a/spec/requests/api/project_clusters_spec.rb +++ b/spec/requests/api/project_clusters_spec.rb @@ -171,6 +171,7 @@ RSpec.describe API::ProjectClusters do name: 'test-cluster', domain: 'domain.example.com', managed: false, + namespace_per_environment: false, platform_kubernetes_attributes: platform_kubernetes_attributes, management_project_id: management_project_id } @@ -202,6 +203,7 @@ RSpec.describe API::ProjectClusters do expect(cluster_result.domain).to eq('domain.example.com') expect(cluster_result.managed).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 expect(platform_kubernetes.api_url).to eq(api_url) expect(platform_kubernetes.namespace).to eq(namespace) @@ -235,6 +237,22 @@ RSpec.describe API::ProjectClusters do end end + context 'when namespace_per_environment is not set' do + let(:cluster_params) do + { + name: 'test-cluster', + domain: 'domain.example.com', + platform_kubernetes_attributes: platform_kubernetes_attributes + } + end + + it 'defaults to true' do + cluster_result = Clusters::Cluster.find(json_response['id']) + + expect(cluster_result).to be_namespace_per_environment + end + end + context 'current user does not have access to management_project_id' do let(:management_project_id) { create(:project).id } diff --git a/spec/requests/api/project_container_repositories_spec.rb b/spec/requests/api/project_container_repositories_spec.rb index 6cf0619cde4..a0c310a448b 100644 --- a/spec/requests/api/project_container_repositories_spec.rb +++ b/spec/requests/api/project_container_repositories_spec.rb @@ -45,7 +45,7 @@ RSpec.describe API::ProjectContainerRepositories do it_behaves_like 'rejected container repository access', :guest, :forbidden it_behaves_like 'rejected container repository access', :anonymous, :not_found - it_behaves_like 'a gitlab tracking event', described_class.name, 'list_repositories' + it_behaves_like 'a package tracking event', described_class.name, 'list_repositories' it_behaves_like 'returns repositories for allowed users', :reporter, 'project' do let(:object) { project } @@ -57,7 +57,7 @@ RSpec.describe API::ProjectContainerRepositories do it_behaves_like 'rejected container repository access', :developer, :forbidden it_behaves_like 'rejected container repository access', :anonymous, :not_found - it_behaves_like 'a gitlab tracking event', described_class.name, 'delete_repository' + it_behaves_like 'a package tracking event', described_class.name, 'delete_repository' context 'for maintainer' do let(:api_user) { maintainer } @@ -86,7 +86,7 @@ RSpec.describe API::ProjectContainerRepositories do stub_container_registry_tags(repository: root_repository.path, tags: %w(rootA latest)) end - it_behaves_like 'a gitlab tracking event', described_class.name, 'list_tags' + it_behaves_like 'a package tracking event', described_class.name, 'list_tags' it 'returns a list of tags' do subject @@ -114,7 +114,7 @@ RSpec.describe API::ProjectContainerRepositories do it_behaves_like 'rejected container repository access', :developer, :forbidden it_behaves_like 'rejected container repository access', :anonymous, :not_found - it_behaves_like 'a gitlab tracking event', described_class.name, 'delete_tag_bulk' + it_behaves_like 'a package tracking event', described_class.name, 'delete_tag_bulk' end context 'for maintainer' do diff --git a/spec/requests/api/project_packages_spec.rb b/spec/requests/api/project_packages_spec.rb index 2f0d0fc87ec..4c8599d1a20 100644 --- a/spec/requests/api/project_packages_spec.rb +++ b/spec/requests/api/project_packages_spec.rb @@ -23,6 +23,19 @@ RSpec.describe API::ProjectPackages do it_behaves_like 'returns packages', :project, :no_type end + context 'with conan package' do + let!(:conan_package) { create(:conan_package, project: project) } + + it 'uses the conan recipe as the package name' do + subject + + response_conan_package = json_response.find { |package| package['id'] == conan_package.id } + + expect(response_conan_package['name']).to eq(conan_package.conan_recipe) + expect(response_conan_package['conan_package_name']).to eq(conan_package.name) + end + end + context 'project is private' do let(:project) { create(:project, :private) } diff --git a/spec/requests/api/releases_spec.rb b/spec/requests/api/releases_spec.rb index 779ae983886..01ead9eef54 100644 --- a/spec/requests/api/releases_spec.rb +++ b/spec/requests/api/releases_spec.rb @@ -259,7 +259,7 @@ RSpec.describe API::Releases do end it '#collected_at' do - Timecop.freeze(Time.now.round) do + travel_to(Time.now.round) do get api("/projects/#{project.id}/releases/v0.1", maintainer) expect(json_response['evidences'].first['collected_at'].to_datetime.to_i).to be_within(1.minute).of(release.evidences.first.created_at.to_i) @@ -476,7 +476,7 @@ RSpec.describe API::Releases do it 'sets the released_at to the current time if the released_at parameter is not provided' do now = Time.zone.parse('2015-08-25 06:00:00Z') - Timecop.freeze(now) do + travel_to(now) do post api("/projects/#{project.id}/releases", maintainer), params: params expect(project.releases.last.released_at).to eq(now) diff --git a/spec/requests/api/repositories_spec.rb b/spec/requests/api/repositories_spec.rb index 36707f32d04..45bce8c8a5c 100644 --- a/spec/requests/api/repositories_spec.rb +++ b/spec/requests/api/repositories_spec.rb @@ -402,7 +402,9 @@ RSpec.describe API::Repositories do end it "returns an empty string when the diff overflows" do - stub_const('Gitlab::Git::DiffCollection::DEFAULT_LIMITS', { max_files: 2, max_lines: 2 }) + allow(Gitlab::Git::DiffCollection) + .to receive(:default_limits) + .and_return({ max_files: 2, max_lines: 2 }) get api(route, current_user), params: { from: 'master', to: 'feature' } diff --git a/spec/requests/api/search_spec.rb b/spec/requests/api/search_spec.rb index af6731f3015..523f0f72f11 100644 --- a/spec/requests/api/search_spec.rb +++ b/spec/requests/api/search_spec.rb @@ -231,18 +231,6 @@ RSpec.describe API::Search do it_behaves_like 'pagination', scope: :users it_behaves_like 'ping counters', scope: :users - - context 'when users search feature is disabled' do - before do - stub_feature_flags(users_search: false) - - get api(endpoint, user), params: { scope: 'users', search: 'billy' } - end - - it 'returns 400 error' do - expect(response).to have_gitlab_http_status(:bad_request) - end - end end context 'for snippet_titles scope' do @@ -416,18 +404,6 @@ RSpec.describe API::Search do include_examples 'pagination', scope: :users end - - context 'when users search feature is disabled' do - before do - stub_feature_flags(users_search: false) - - get api(endpoint, user), params: { scope: 'users', search: 'billy' } - end - - it 'returns 400 error' do - expect(response).to have_gitlab_http_status(:bad_request) - end - end end context 'for users scope with group path as id' do @@ -589,18 +565,6 @@ RSpec.describe API::Search do include_examples 'pagination', scope: :users end - - context 'when users search feature is disabled' do - before do - stub_feature_flags(users_search: false) - - get api(endpoint, user), params: { scope: 'users', search: 'billy' } - end - - it 'returns 400 error' do - expect(response).to have_gitlab_http_status(:bad_request) - end - end end context 'for notes scope' do diff --git a/spec/requests/api/services_spec.rb b/spec/requests/api/services_spec.rb index 5528a0c094f..63ed57c5045 100644 --- a/spec/requests/api/services_spec.rb +++ b/spec/requests/api/services_spec.rb @@ -264,4 +264,34 @@ RSpec.describe API::Services do expect(json_response['properties']['notify_only_broken_pipelines']).to eq(true) end end + + describe 'Hangouts Chat service' do + let(:service_name) { 'hangouts-chat' } + let(:params) do + { + webhook: 'https://hook.example.com', + branches_to_be_notified: 'default' + } + end + + before do + project.create_hangouts_chat_service( + active: true, + properties: params + ) + end + + it 'accepts branches_to_be_notified for update', :aggregate_failures do + put api("/projects/#{project.id}/services/#{service_name}", user), params: params.merge(branches_to_be_notified: 'all') + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['properties']['branches_to_be_notified']).to eq('all') + end + + it 'only requires the webhook param' do + put api("/projects/#{project.id}/services/#{service_name}", user), params: { webhook: 'https://hook.example.com' } + + expect(response).to have_gitlab_http_status(:ok) + end + end end diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb index ef12f6dbed3..b0face6ec41 100644 --- a/spec/requests/api/settings_spec.rb +++ b/spec/requests/api/settings_spec.rb @@ -413,6 +413,14 @@ RSpec.describe API::Settings, 'Settings' do end end + it 'supports legacy admin_notification_email' do + put api('/application/settings', admin), + params: { admin_notification_email: 'test@example.com' } + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['abuse_notification_email']).to eq('test@example.com') + end + context "missing sourcegraph_url value when sourcegraph_enabled is true" do it "returns a blank parameter error message" do put api("/application/settings", admin), params: { sourcegraph_enabled: true } diff --git a/spec/requests/api/terraform/state_spec.rb b/spec/requests/api/terraform/state_spec.rb index 8d128bd911f..aff41ff5974 100644 --- a/spec/requests/api/terraform/state_spec.rb +++ b/spec/requests/api/terraform/state_spec.rb @@ -18,7 +18,7 @@ RSpec.describe API::Terraform::State do let(:state_path) { "/projects/#{project_id}/terraform/state/#{state_name}" } before do - stub_terraform_state_object_storage(Terraform::StateUploader) + stub_terraform_state_object_storage end describe 'GET /projects/:id/terraform/state/:name' do diff --git a/spec/requests/api/terraform/state_version_spec.rb b/spec/requests/api/terraform/state_version_spec.rb new file mode 100644 index 00000000000..ade0aacf805 --- /dev/null +++ b/spec/requests/api/terraform/state_version_spec.rb @@ -0,0 +1,210 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe API::Terraform::StateVersion do + include HttpBasicAuthHelpers + + let_it_be(:project) { create(:project) } + let_it_be(:developer) { create(:user, developer_projects: [project]) } + let_it_be(:maintainer) { create(:user, maintainer_projects: [project]) } + let_it_be(:user_without_access) { create(:user) } + + let_it_be(:state) { create(:terraform_state, project: project) } + + let!(:versions) { create_list(:terraform_state_version, 3, terraform_state: state) } + + let(:current_user) { maintainer } + let(:auth_header) { user_basic_auth_header(current_user) } + let(:project_id) { project.id } + let(:state_name) { state.name } + let(:version) { versions.last } + let(:version_serial) { version.version } + let(:state_version_path) { "/projects/#{project_id}/terraform/state/#{state_name}/versions/#{version_serial}" } + + describe 'GET /projects/:id/terraform/state/:name/versions/:serial' do + subject(:request) { get api(state_version_path), headers: auth_header } + + context 'with invalid authentication' do + let(:auth_header) { basic_auth_header('bad', 'token') } + + it 'returns unauthorized status' do + request + + expect(response).to have_gitlab_http_status(:unauthorized) + end + end + + context 'with no authentication' do + let(:auth_header) { nil } + + it 'returns unauthorized status' do + request + + expect(response).to have_gitlab_http_status(:unauthorized) + end + end + + context 'personal acceess token authentication' do + context 'with maintainer permissions' do + let(:current_user) { maintainer } + + it 'returns the state contents at the given version' do + request + + expect(response).to have_gitlab_http_status(:ok) + expect(response.body).to eq(version.file.read) + end + + context 'for a project that does not exist' do + let(:project_id) { '0000' } + + it 'returns not found status' do + request + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end + + context 'with developer permissions' do + let(:current_user) { developer } + + it 'returns the state contents at the given version' do + request + + expect(response).to have_gitlab_http_status(:ok) + expect(response.body).to eq(version.file.read) + end + end + + context 'with no permissions' do + let(:current_user) { user_without_access } + + it 'returns not found status' do + request + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end + + context 'job token authentication' do + let(:auth_header) { job_basic_auth_header(job) } + + context 'with maintainer permissions' do + let(:job) { create(:ci_build, status: :running, project: project, user: maintainer) } + + it 'returns the state contents at the given version' do + request + + expect(response).to have_gitlab_http_status(:ok) + expect(response.body).to eq(version.file.read) + end + + it 'returns unauthorized status if the the job is not running' do + job.update!(status: :failed) + request + + expect(response).to have_gitlab_http_status(:unauthorized) + end + + context 'for a project that does not exist' do + let(:project_id) { '0000' } + + it 'returns not found status' do + request + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end + + context 'with developer permissions' do + let(:job) { create(:ci_build, status: :running, project: project, user: developer) } + + it 'returns the state contents at the given version' do + request + + expect(response).to have_gitlab_http_status(:ok) + expect(response.body).to eq(version.file.read) + end + end + + context 'with no permissions' do + let(:current_user) { user_without_access } + let(:job) { create(:ci_build, status: :running, user: current_user) } + + it 'returns not found status' do + request + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end + end + + describe 'DELETE /projects/:id/terraform/state/:name/versions/:serial' do + subject(:request) { delete api(state_version_path), headers: auth_header } + + context 'with invalid authentication' do + let(:auth_header) { basic_auth_header('bad', 'token') } + + it 'returns unauthorized status' do + request + + expect(response).to have_gitlab_http_status(:unauthorized) + end + end + + context 'with no authentication' do + let(:auth_header) { nil } + + it 'returns unauthorized status' do + request + + expect(response).to have_gitlab_http_status(:unauthorized) + end + end + + context 'with maintainer permissions' do + let(:current_user) { maintainer } + + it 'deletes the version' do + expect { request }.to change { Terraform::StateVersion.count }.by(-1) + + expect(response).to have_gitlab_http_status(:no_content) + end + + context 'version does not exist' do + let(:version_serial) { -1 } + + it 'does not delete a version' do + expect { request }.to change { Terraform::StateVersion.count }.by(0) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end + + context 'with developer permissions' do + let(:current_user) { developer } + + it 'returns forbidden status' do + expect { request }.to change { Terraform::StateVersion.count }.by(0) + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + + context 'with no permissions' do + let(:current_user) { user_without_access } + + it 'returns not found status' do + expect { request }.to change { Terraform::StateVersion.count }.by(0) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end +end diff --git a/spec/requests/api/unleash_spec.rb b/spec/requests/api/unleash_spec.rb new file mode 100644 index 00000000000..0b70d62b093 --- /dev/null +++ b/spec/requests/api/unleash_spec.rb @@ -0,0 +1,608 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe API::Unleash do + include FeatureFlagHelpers + + let_it_be(:project, refind: true) { create(:project) } + let(:project_id) { project.id } + let(:params) { } + let(:headers) { } + + shared_examples 'authenticated request' do + context 'when using instance id' do + let(:client) { create(:operations_feature_flags_client, project: project) } + let(:params) { { instance_id: client.token } } + + it 'responds with OK' do + subject + + expect(response).to have_gitlab_http_status(:ok) + end + + context 'when repository is disabled' do + before do + project.project_feature.update!( + repository_access_level: ::ProjectFeature::DISABLED, + merge_requests_access_level: ::ProjectFeature::DISABLED, + builds_access_level: ::ProjectFeature::DISABLED + ) + end + + it 'responds with forbidden' do + subject + + expect(response).to have_gitlab_http_status(:ok) + end + end + + context 'when repository is private' do + before do + project.project_feature.update!( + repository_access_level: ::ProjectFeature::PRIVATE, + merge_requests_access_level: ::ProjectFeature::DISABLED, + builds_access_level: ::ProjectFeature::DISABLED + ) + end + + it 'responds with OK' do + subject + + expect(response).to have_gitlab_http_status(:ok) + end + end + end + + context 'when using header' do + let(:client) { create(:operations_feature_flags_client, project: project) } + let(:headers) { { "UNLEASH-INSTANCEID" => client.token }} + + it 'responds with OK' do + subject + + expect(response).to have_gitlab_http_status(:ok) + end + end + + context 'when using bogus instance id' do + let(:params) { { instance_id: 'token' } } + + it 'responds with unauthorized' do + subject + + expect(response).to have_gitlab_http_status(:unauthorized) + end + end + + context 'when using not existing project' do + let(:project_id) { -5000 } + let(:params) { { instance_id: 'token' } } + + it 'responds with unauthorized' do + subject + + expect(response).to have_gitlab_http_status(:unauthorized) + end + end + end + + shared_examples_for 'support multiple environments' do + let!(:client) { create(:operations_feature_flags_client, project: project) } + let!(:base_headers) { { "UNLEASH-INSTANCEID" => client.token } } + let!(:headers) { base_headers.merge({ "UNLEASH-APPNAME" => "test" }) } + + let!(:feature_flag_1) do + create(:operations_feature_flag, name: "feature_flag_1", project: project, active: true) + end + + let!(:feature_flag_2) do + create(:operations_feature_flag, name: "feature_flag_2", project: project, active: false) + end + + before do + create_scope(feature_flag_1, 'production', false) + create_scope(feature_flag_2, 'review/*', true) + end + + it 'does not have N+1 problem' do + control_count = ActiveRecord::QueryRecorder.new { get api(features_url), headers: headers }.count + + create(:operations_feature_flag, name: "feature_flag_3", project: project, active: true) + + expect { get api(features_url), headers: headers }.not_to exceed_query_limit(control_count) + end + + context 'when app name is staging' do + let(:headers) { base_headers.merge({ "UNLEASH-APPNAME" => "staging" }) } + + it 'returns correct active values' do + subject + + feature_flag_1 = json_response['features'].find { |f| f['name'] == 'feature_flag_1' } + feature_flag_2 = json_response['features'].find { |f| f['name'] == 'feature_flag_2' } + + expect(feature_flag_1['enabled']).to eq(true) + expect(feature_flag_2['enabled']).to eq(false) + end + end + + context 'when app name is production' do + let(:headers) { base_headers.merge({ "UNLEASH-APPNAME" => "production" }) } + + it 'returns correct active values' do + subject + + feature_flag_1 = json_response['features'].find { |f| f['name'] == 'feature_flag_1' } + feature_flag_2 = json_response['features'].find { |f| f['name'] == 'feature_flag_2' } + + expect(feature_flag_1['enabled']).to eq(false) + expect(feature_flag_2['enabled']).to eq(false) + end + end + + context 'when app name is review/patch-1' do + let(:headers) { base_headers.merge({ "UNLEASH-APPNAME" => "review/patch-1" }) } + + it 'returns correct active values' do + subject + + feature_flag_1 = json_response['features'].find { |f| f['name'] == 'feature_flag_1' } + feature_flag_2 = json_response['features'].find { |f| f['name'] == 'feature_flag_2' } + + expect(feature_flag_1['enabled']).to eq(true) + expect(feature_flag_2['enabled']).to eq(false) + end + end + + context 'when app name is empty' do + let(:headers) { base_headers } + + it 'returns empty list' do + subject + + expect(json_response['features'].count).to eq(0) + end + end + end + + %w(/feature_flags/unleash/:project_id/features /feature_flags/unleash/:project_id/client/features).each do |features_endpoint| + describe "GET #{features_endpoint}" do + let(:features_url) { features_endpoint.sub(':project_id', project_id.to_s) } + let(:client) { create(:operations_feature_flags_client, project: project) } + + subject { get api(features_url), params: params, headers: headers } + + it_behaves_like 'authenticated request' + + context 'with version 1 (legacy) feature flags' do + let(:feature_flag) { create(:operations_feature_flag, project: project, name: 'feature1', active: true, version: 1) } + + it_behaves_like 'support multiple environments' + + context 'with a list of feature flags' do + let(:headers) { { "UNLEASH-INSTANCEID" => client.token, "UNLEASH-APPNAME" => "production" } } + let!(:enabled_feature_flag) { create(:operations_feature_flag, project: project, name: 'feature1', active: true, version: 1) } + let!(:disabled_feature_flag) { create(:operations_feature_flag, project: project, name: 'feature2', active: false, version: 1) } + + it 'responds with a list of features' do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['version']).to eq(1) + expect(json_response['features']).not_to be_empty + expect(json_response['features'].map { |f| f['name'] }.sort).to eq(%w[feature1 feature2]) + expect(json_response['features'].sort_by {|f| f['name'] }.map { |f| f['enabled'] }).to eq([true, false]) + end + + it 'matches json schema' do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema('unleash/unleash') + end + end + + it 'returns a feature flag strategy' do + create(:operations_feature_flag_scope, + feature_flag: feature_flag, + environment_scope: 'sandbox', + active: true, + strategies: [{ name: "gradualRolloutUserId", + parameters: { groupId: "default", percentage: "50" } }]) + headers = { "UNLEASH-INSTANCEID" => client.token, "UNLEASH-APPNAME" => "sandbox" } + + get api(features_url), headers: headers + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['features'].first['enabled']).to eq(true) + strategies = json_response['features'].first['strategies'] + expect(strategies).to eq([{ + "name" => "gradualRolloutUserId", + "parameters" => { + "percentage" => "50", + "groupId" => "default" + } + }]) + end + + it 'returns a default strategy for a scope' do + create(:operations_feature_flag_scope, feature_flag: feature_flag, environment_scope: 'sandbox', active: true) + headers = { "UNLEASH-INSTANCEID" => client.token, "UNLEASH-APPNAME" => "sandbox" } + + get api(features_url), headers: headers + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['features'].first['enabled']).to eq(true) + strategies = json_response['features'].first['strategies'] + expect(strategies).to eq([{ "name" => "default", "parameters" => {} }]) + end + + it 'returns multiple strategies for a feature flag' do + create(:operations_feature_flag_scope, + feature_flag: feature_flag, + environment_scope: 'staging', + active: true, + strategies: [{ name: "userWithId", parameters: { userIds: "max,fred" } }, + { name: "gradualRolloutUserId", + parameters: { groupId: "default", percentage: "50" } }]) + headers = { "UNLEASH-INSTANCEID" => client.token, "UNLEASH-APPNAME" => "staging" } + + get api(features_url), headers: headers + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['features'].first['enabled']).to eq(true) + strategies = json_response['features'].first['strategies'].sort_by { |s| s['name'] } + expect(strategies).to eq([{ + "name" => "gradualRolloutUserId", + "parameters" => { + "percentage" => "50", + "groupId" => "default" + } + }, { + "name" => "userWithId", + "parameters" => { + "userIds" => "max,fred" + } + }]) + end + + it 'returns a disabled feature when the flag is disabled' do + flag = create(:operations_feature_flag, project: project, name: 'test_feature', active: false, version: 1) + create(:operations_feature_flag_scope, feature_flag: flag, environment_scope: 'production', active: true) + headers = { "UNLEASH-INSTANCEID" => client.token, "UNLEASH-APPNAME" => "production" } + + get api(features_url), headers: headers + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['features'].first['enabled']).to eq(false) + end + + context "with an inactive scope" do + let!(:scope) { create(:operations_feature_flag_scope, feature_flag: feature_flag, environment_scope: 'production', active: false, strategies: [{ name: "default", parameters: {} }]) } + let(:headers) { { "UNLEASH-INSTANCEID" => client.token, "UNLEASH-APPNAME" => "production" } } + + it 'returns a disabled feature' do + get api(features_url), headers: headers + + expect(response).to have_gitlab_http_status(:ok) + feature_json = json_response['features'].first + expect(feature_json['enabled']).to eq(false) + expect(feature_json['strategies']).to eq([{ 'name' => 'default', 'parameters' => {} }]) + end + end + end + + context 'with version 2 feature flags' do + it 'does not return a flag without any strategies' do + create(:operations_feature_flag, project: project, + name: 'feature1', active: true, version: 2) + + get api(features_url), headers: { 'UNLEASH-INSTANCEID' => client.token, 'UNLEASH-APPNAME' => 'production' } + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['features']).to be_empty + end + + it 'returns a flag with a default strategy' do + feature_flag = create(:operations_feature_flag, project: project, + name: 'feature1', active: true, version: 2) + strategy = create(:operations_strategy, feature_flag: feature_flag, + name: 'default', parameters: {}) + create(:operations_scope, strategy: strategy, environment_scope: 'production') + + get api(features_url), headers: { 'UNLEASH-INSTANCEID' => client.token, 'UNLEASH-APPNAME' => 'production' } + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['features']).to eq([{ + 'name' => 'feature1', + 'enabled' => true, + 'strategies' => [{ + 'name' => 'default', + 'parameters' => {} + }] + }]) + end + + it 'returns a flag with a userWithId strategy' do + feature_flag = create(:operations_feature_flag, project: project, + name: 'feature1', active: true, version: 2) + strategy = create(:operations_strategy, feature_flag: feature_flag, + name: 'userWithId', parameters: { userIds: 'user123,user456' }) + create(:operations_scope, strategy: strategy, environment_scope: 'production') + + get api(features_url), headers: { 'UNLEASH-INSTANCEID' => client.token, 'UNLEASH-APPNAME' => 'production' } + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['features']).to eq([{ + 'name' => 'feature1', + 'enabled' => true, + 'strategies' => [{ + 'name' => 'userWithId', + 'parameters' => { 'userIds' => 'user123,user456' } + }] + }]) + end + + it 'returns a flag with multiple strategies' do + feature_flag = create(:operations_feature_flag, project: project, + name: 'feature1', active: true, version: 2) + strategy_a = create(:operations_strategy, feature_flag: feature_flag, + name: 'userWithId', parameters: { userIds: 'user_a,user_b' }) + strategy_b = create(:operations_strategy, feature_flag: feature_flag, + name: 'gradualRolloutUserId', parameters: { groupId: 'default', percentage: '45' }) + create(:operations_scope, strategy: strategy_a, environment_scope: 'production') + create(:operations_scope, strategy: strategy_b, environment_scope: 'production') + + get api(features_url), headers: { 'UNLEASH-INSTANCEID' => client.token, 'UNLEASH-APPNAME' => 'production' } + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['features'].map { |f| f['name'] }.sort).to eq(['feature1']) + features_json = json_response['features'].map do |feature| + feature.merge(feature.slice('strategies').transform_values { |v| v.sort_by { |s| s['name'] } }) + end + expect(features_json).to eq([{ + 'name' => 'feature1', + 'enabled' => true, + 'strategies' => [{ + 'name' => 'gradualRolloutUserId', + 'parameters' => { 'groupId' => 'default', 'percentage' => '45' } + }, { + 'name' => 'userWithId', + 'parameters' => { 'userIds' => 'user_a,user_b' } + }] + }]) + end + + it 'returns only flags matching the environment scope' do + feature_flag_a = create(:operations_feature_flag, project: project, + name: 'feature1', active: true, version: 2) + strategy_a = create(:operations_strategy, feature_flag: feature_flag_a) + create(:operations_scope, strategy: strategy_a, environment_scope: 'production') + feature_flag_b = create(:operations_feature_flag, project: project, + name: 'feature2', active: true, version: 2) + strategy_b = create(:operations_strategy, feature_flag: feature_flag_b) + create(:operations_scope, strategy: strategy_b, environment_scope: 'staging') + + get api(features_url), headers: { 'UNLEASH-INSTANCEID' => client.token, 'UNLEASH-APPNAME' => 'staging' } + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['features'].map { |f| f['name'] }.sort).to eq(['feature2']) + expect(json_response['features']).to eq([{ + 'name' => 'feature2', + 'enabled' => true, + 'strategies' => [{ + 'name' => 'default', + 'parameters' => {} + }] + }]) + end + + it 'returns only strategies matching the environment scope' do + feature_flag = create(:operations_feature_flag, project: project, + name: 'feature1', active: true, version: 2) + strategy_a = create(:operations_strategy, feature_flag: feature_flag, + name: 'userWithId', parameters: { userIds: 'user2,user8,user4' }) + create(:operations_scope, strategy: strategy_a, environment_scope: 'production') + strategy_b = create(:operations_strategy, feature_flag: feature_flag, + name: 'default', parameters: {}) + create(:operations_scope, strategy: strategy_b, environment_scope: 'staging') + + get api(features_url), headers: { 'UNLEASH-INSTANCEID' => client.token, 'UNLEASH-APPNAME' => 'production' } + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['features']).to eq([{ + 'name' => 'feature1', + 'enabled' => true, + 'strategies' => [{ + 'name' => 'userWithId', + 'parameters' => { 'userIds' => 'user2,user8,user4' } + }] + }]) + end + + it 'returns only flags for the given project' do + project_b = create(:project) + feature_flag_a = create(:operations_feature_flag, project: project, name: 'feature_a', active: true, version: 2) + strategy_a = create(:operations_strategy, feature_flag: feature_flag_a) + create(:operations_scope, strategy: strategy_a, environment_scope: 'sandbox') + feature_flag_b = create(:operations_feature_flag, project: project_b, name: 'feature_b', active: true, version: 2) + strategy_b = create(:operations_strategy, feature_flag: feature_flag_b) + create(:operations_scope, strategy: strategy_b, environment_scope: 'sandbox') + + get api(features_url), headers: { 'UNLEASH-INSTANCEID' => client.token, 'UNLEASH-APPNAME' => 'sandbox' } + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['features']).to eq([{ + 'name' => 'feature_a', + 'enabled' => true, + 'strategies' => [{ + 'name' => 'default', + 'parameters' => {} + }] + }]) + end + + it 'returns all strategies with a matching scope' do + feature_flag = create(:operations_feature_flag, project: project, + name: 'feature1', active: true, version: 2) + strategy_a = create(:operations_strategy, feature_flag: feature_flag, + name: 'userWithId', parameters: { userIds: 'user2,user8,user4' }) + create(:operations_scope, strategy: strategy_a, environment_scope: '*') + strategy_b = create(:operations_strategy, feature_flag: feature_flag, + name: 'default', parameters: {}) + create(:operations_scope, strategy: strategy_b, environment_scope: 'review/*') + strategy_c = create(:operations_strategy, feature_flag: feature_flag, + name: 'gradualRolloutUserId', parameters: { groupId: 'default', percentage: '15' }) + create(:operations_scope, strategy: strategy_c, environment_scope: 'review/patch-1') + + get api(features_url), headers: { 'UNLEASH-INSTANCEID' => client.token, 'UNLEASH-APPNAME' => 'review/patch-1' } + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['features'].first['strategies'].sort_by { |s| s['name'] }).to eq([{ + 'name' => 'default', + 'parameters' => {} + }, { + 'name' => 'gradualRolloutUserId', + 'parameters' => { 'groupId' => 'default', 'percentage' => '15' } + }, { + 'name' => 'userWithId', + 'parameters' => { 'userIds' => 'user2,user8,user4' } + }]) + end + + it 'returns a strategy with more than one matching scope' do + feature_flag = create(:operations_feature_flag, project: project, + name: 'feature1', active: true, version: 2) + strategy = create(:operations_strategy, feature_flag: feature_flag, + name: 'default', parameters: {}) + create(:operations_scope, strategy: strategy, environment_scope: 'production') + create(:operations_scope, strategy: strategy, environment_scope: '*') + + get api(features_url), headers: { 'UNLEASH-INSTANCEID' => client.token, 'UNLEASH-APPNAME' => 'production' } + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['features']).to eq([{ + 'name' => 'feature1', + 'enabled' => true, + 'strategies' => [{ + 'name' => 'default', + 'parameters' => {} + }] + }]) + end + + it 'returns a disabled flag with a matching scope' do + feature_flag = create(:operations_feature_flag, project: project, + name: 'myfeature', active: false, version: 2) + strategy = create(:operations_strategy, feature_flag: feature_flag, + name: 'default', parameters: {}) + create(:operations_scope, strategy: strategy, environment_scope: 'production') + + get api(features_url), headers: { 'UNLEASH-INSTANCEID' => client.token, 'UNLEASH-APPNAME' => 'production' } + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['features']).to eq([{ + 'name' => 'myfeature', + 'enabled' => false, + 'strategies' => [{ + 'name' => 'default', + 'parameters' => {} + }] + }]) + end + + it 'returns a userWithId strategy for a gitlabUserList strategy' do + feature_flag = create(:operations_feature_flag, :new_version_flag, project: project, + name: 'myfeature', active: true) + user_list = create(:operations_feature_flag_user_list, project: project, + name: 'My List', user_xids: 'user1,user2') + strategy = create(:operations_strategy, feature_flag: feature_flag, + name: 'gitlabUserList', parameters: {}, user_list: user_list) + create(:operations_scope, strategy: strategy, environment_scope: 'production') + + get api(features_url), headers: { 'UNLEASH-INSTANCEID' => client.token, 'UNLEASH-APPNAME' => 'production' } + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['features']).to eq([{ + 'name' => 'myfeature', + 'enabled' => true, + 'strategies' => [{ + 'name' => 'userWithId', + 'parameters' => { 'userIds' => 'user1,user2' } + }] + }]) + end + end + + context 'when mixing version 1 and version 2 feature flags' do + it 'returns both types of flags when both match' do + feature_flag_a = create(:operations_feature_flag, project: project, + name: 'feature_a', active: true, version: 2) + strategy = create(:operations_strategy, feature_flag: feature_flag_a, + name: 'userWithId', parameters: { userIds: 'user8' }) + create(:operations_scope, strategy: strategy, environment_scope: 'staging') + feature_flag_b = create(:operations_feature_flag, project: project, + name: 'feature_b', active: true, version: 1) + create(:operations_feature_flag_scope, feature_flag: feature_flag_b, + active: true, strategies: [{ name: 'default', parameters: {} }], environment_scope: 'staging') + + get api(features_url), headers: { 'UNLEASH-INSTANCEID' => client.token, 'UNLEASH-APPNAME' => 'staging' } + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['features'].sort_by {|f| f['name']}).to eq([{ + 'name' => 'feature_a', + 'enabled' => true, + 'strategies' => [{ + 'name' => 'userWithId', + 'parameters' => { 'userIds' => 'user8' } + }] + }, { + 'name' => 'feature_b', + 'enabled' => true, + 'strategies' => [{ + 'name' => 'default', + 'parameters' => {} + }] + }]) + end + + it 'returns legacy flags when only legacy flags match' do + feature_flag_a = create(:operations_feature_flag, project: project, + name: 'feature_a', active: true, version: 2) + strategy = create(:operations_strategy, feature_flag: feature_flag_a, + name: 'userWithId', parameters: { userIds: 'user8' }) + create(:operations_scope, strategy: strategy, environment_scope: 'production') + feature_flag_b = create(:operations_feature_flag, project: project, + name: 'feature_b', active: true, version: 1) + create(:operations_feature_flag_scope, feature_flag: feature_flag_b, + active: true, strategies: [{ name: 'default', parameters: {} }], environment_scope: 'staging') + + get api(features_url), headers: { 'UNLEASH-INSTANCEID' => client.token, 'UNLEASH-APPNAME' => 'staging' } + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['features']).to eq([{ + 'name' => 'feature_b', + 'enabled' => true, + 'strategies' => [{ + 'name' => 'default', + 'parameters' => {} + }] + }]) + end + end + end + end + + describe 'POST /feature_flags/unleash/:project_id/client/register' do + subject { post api("/feature_flags/unleash/#{project_id}/client/register"), params: params, headers: headers } + + it_behaves_like 'authenticated request' + end + + describe 'POST /feature_flags/unleash/:project_id/client/metrics' do + subject { post api("/feature_flags/unleash/#{project_id}/client/metrics"), params: params, headers: headers } + + it_behaves_like 'authenticated request' + end +end diff --git a/spec/requests/api/usage_data_spec.rb b/spec/requests/api/usage_data_spec.rb index 46dd54dcc73..4f4f386e9db 100644 --- a/spec/requests/api/usage_data_spec.rb +++ b/spec/requests/api/usage_data_spec.rb @@ -66,6 +66,10 @@ RSpec.describe API::UsageData do end context 'with unknown event' do + before do + skip_feature_flags_yaml_validation + end + it 'returns status ok' do expect(Gitlab::Redis::HLL).not_to receive(:add) diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index 806b586ef49..72dd22038c9 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -1460,39 +1460,22 @@ RSpec.describe API::Users, :do_not_mock_admin_mode do end describe 'GET /user/:id/gpg_keys' do - context 'when unauthenticated' do - it 'returns authentication error' do - get api("/users/#{user.id}/gpg_keys") + it 'returns 404 for non-existing user' do + get api('/users/0/gpg_keys') - expect(response).to have_gitlab_http_status(:unauthorized) - end + expect(response).to have_gitlab_http_status(:not_found) + expect(json_response['message']).to eq('404 User Not Found') end - context 'when authenticated' do - it 'returns 404 for non-existing user' do - get api('/users/0/gpg_keys', admin) - - expect(response).to have_gitlab_http_status(:not_found) - expect(json_response['message']).to eq('404 User Not Found') - end - - it 'returns 404 error if key not foud' do - delete api("/users/#{user.id}/gpg_keys/#{non_existing_record_id}", admin) - - expect(response).to have_gitlab_http_status(:not_found) - expect(json_response['message']).to eq('404 GPG Key Not Found') - end - - it 'returns array of GPG keys' do - user.gpg_keys << gpg_key + it 'returns array of GPG keys' do + user.gpg_keys << gpg_key - get api("/users/#{user.id}/gpg_keys", admin) + get api("/users/#{user.id}/gpg_keys") - expect(response).to have_gitlab_http_status(:ok) - expect(response).to include_pagination_headers - expect(json_response).to be_an Array - expect(json_response.first['key']).to eq(gpg_key.key) - end + expect(response).to have_gitlab_http_status(:ok) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.first['key']).to eq(gpg_key.key) end end @@ -2308,23 +2291,31 @@ RSpec.describe API::Users, :do_not_mock_admin_mode do end describe 'POST /users/:id/activate' do + subject(:activate) { post api("/users/#{user_id}/activate", api_user) } + + let(:user_id) { user.id } + context 'performed by a non-admin user' do + let(:api_user) { user } + it 'is not authorized to perform the action' do - post api("/users/#{user.id}/activate", user) + activate expect(response).to have_gitlab_http_status(:forbidden) end end context 'performed by an admin user' do + let(:api_user) { admin } + context 'for a deactivated user' do before do user.deactivate - - post api("/users/#{user.id}/activate", admin) end it 'activates a deactivated user' do + activate + expect(response).to have_gitlab_http_status(:created) expect(user.reload.state).to eq('active') end @@ -2333,11 +2324,11 @@ RSpec.describe API::Users, :do_not_mock_admin_mode do context 'for an active user' do before do user.activate - - post api("/users/#{user.id}/activate", admin) end it 'returns 201' do + activate + expect(response).to have_gitlab_http_status(:created) expect(user.reload.state).to eq('active') end @@ -2346,11 +2337,11 @@ RSpec.describe API::Users, :do_not_mock_admin_mode do context 'for a blocked user' do before do user.block - - post api("/users/#{user.id}/activate", admin) end it 'returns 403' do + activate + expect(response).to have_gitlab_http_status(:forbidden) expect(json_response['message']).to eq('403 Forbidden - A blocked user must be unblocked to be activated') expect(user.reload.state).to eq('blocked') @@ -2360,11 +2351,11 @@ RSpec.describe API::Users, :do_not_mock_admin_mode do context 'for a ldap blocked user' do before do user.ldap_block - - post api("/users/#{user.id}/activate", admin) end it 'returns 403' do + activate + expect(response).to have_gitlab_http_status(:forbidden) expect(json_response['message']).to eq('403 Forbidden - A blocked user must be unblocked to be activated') expect(user.reload.state).to eq('ldap_blocked') @@ -2372,8 +2363,10 @@ RSpec.describe API::Users, :do_not_mock_admin_mode do end context 'for a user that does not exist' do + let(:user_id) { 0 } + before do - post api("/users/0/activate", admin) + activate end it_behaves_like '404' |