diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-09-16 00:09:35 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-09-16 00:09:35 +0300 |
commit | 7dc8bd3c16a6f8367fdee691711d3313e2efc3c6 (patch) | |
tree | 86108a4c223fd6f0d0c461446e243c413723d1f0 /spec/requests/api | |
parent | f784f7d3b19fe80834240bde23d1300accb01118 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec/requests/api')
-rw-r--r-- | spec/requests/api/conan_instance_packages_spec.rb | 152 | ||||
-rw-r--r-- | spec/requests/api/conan_packages_spec.rb | 986 | ||||
-rw-r--r-- | spec/requests/api/conan_project_packages_spec.rb | 152 | ||||
-rw-r--r-- | spec/requests/api/projects_spec.rb | 83 |
4 files changed, 343 insertions, 1030 deletions
diff --git a/spec/requests/api/conan_instance_packages_spec.rb b/spec/requests/api/conan_instance_packages_spec.rb new file mode 100644 index 00000000000..817530f0bad --- /dev/null +++ b/spec/requests/api/conan_instance_packages_spec.rb @@ -0,0 +1,152 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe API::ConanInstancePackages do + include_context 'conan api setup' + + describe 'GET /api/v4/packages/conan/v1/ping' do + let_it_be(:url) { '/packages/conan/v1/ping' } + + it_behaves_like 'conan ping endpoint' + end + + describe 'GET /api/v4/packages/conan/v1/conans/search' do + let_it_be(:url) { '/packages/conan/v1/conans/search' } + + it_behaves_like 'conan search endpoint' + end + + describe 'GET /api/v4/packages/conan/v1/users/authenticate' do + let_it_be(:url) { '/packages/conan/v1/users/authenticate' } + + it_behaves_like 'conan authenticate endpoint' + end + + describe 'GET /api/v4/packages/conan/v1/users/check_credentials' do + let_it_be(:url) { "/packages/conan/v1/users/check_credentials" } + + it_behaves_like 'conan check_credentials endpoint' + end + + context 'recipe endpoints' do + include_context 'conan recipe endpoints' + + let(:project_id) { 9999 } + let(:url_prefix) { "#{Settings.gitlab.base_url}/api/v4" } + + describe 'GET /api/v4/packages/conan/v1/conans/:package_name/package_version/:package_username/:package_channel' do + let(:recipe_path) { package.conan_recipe_path } + let(:url) { "/packages/conan/v1/conans/#{recipe_path}" } + + it_behaves_like 'recipe snapshot endpoint' + end + + describe 'GET /api/v4/packages/conan/v1/conans/:package_name/package_version/:package_username/:package_channel/packages/:conan_package_reference' do + let(:recipe_path) { package.conan_recipe_path } + let(:url) { "/packages/conan/v1/conans/#{recipe_path}/packages/#{conan_package_reference}" } + + it_behaves_like 'package snapshot endpoint' + end + + describe 'GET /api/v4/packages/conan/v1/conans/:package_name/package_version/:package_username/:package_channel/digest' do + subject { get api("/packages/conan/v1/conans/#{recipe_path}/digest"), headers: headers } + + it_behaves_like 'recipe download_urls endpoint' + end + + describe 'GET /api/v4/packages/conan/v1/conans/:package_name/package_version/:package_username/:package_channel/packages/:conan_package_reference/download_urls' do + subject { get api("/packages/conan/v1/conans/#{recipe_path}/packages/#{conan_package_reference}/download_urls"), headers: headers } + + it_behaves_like 'package download_urls endpoint' + end + + describe 'GET /api/v4/packages/conan/v1/conans/:package_name/package_version/:package_username/:package_channel/download_urls' do + subject { get api("/packages/conan/v1/conans/#{recipe_path}/download_urls"), headers: headers } + + it_behaves_like 'recipe download_urls endpoint' + end + + describe 'GET /api/v4/packages/conan/v1/conans/:package_name/package_version/:package_username/:package_channel/packages/:conan_package_reference/digest' do + subject { get api("/packages/conan/v1/conans/#{recipe_path}/packages/#{conan_package_reference}/digest"), headers: headers } + + it_behaves_like 'package download_urls endpoint' + end + + describe 'POST /api/v4/packages/conan/v1/conans/:package_name/package_version/:package_username/:package_channel/upload_urls' do + subject { post api("/packages/conan/v1/conans/#{recipe_path}/upload_urls"), params: params.to_json, headers: headers } + + it_behaves_like 'recipe upload_urls endpoint' + end + + describe 'POST /api/v4/packages/conan/v1/conans/:package_name/package_version/:package_username/:package_channel/packages/:conan_package_reference/upload_urls' do + subject { post api("/packages/conan/v1/conans/#{recipe_path}/packages/123456789/upload_urls"), params: params.to_json, headers: headers } + + it_behaves_like 'package upload_urls endpoint' + end + + describe 'DELETE /api/v4/packages/conan/v1/conans/:package_name/package_version/:package_username/:package_channel' do + subject { delete api("/packages/conan/v1/conans/#{recipe_path}"), headers: headers} + + it_behaves_like 'delete package endpoint' + end + end + + context 'file download endpoints' do + include_context 'conan file download endpoints' + + describe 'GET /api/v4/packages/conan/v1/files/:package_name/package_version/:package_username/:package_channel/ +:recipe_revision/export/:file_name' do + subject do + get api("/packages/conan/v1/files/#{recipe_path}/#{metadata.recipe_revision}/export/#{recipe_file.file_name}"), + headers: headers + end + + it_behaves_like 'recipe file download endpoint' + it_behaves_like 'project not found by recipe' + end + + describe 'GET /api/v4/packages/conan/v1/files/:package_name/package_version/:package_username/:package_channel/ +:recipe_revision/package/:conan_package_reference/:package_revision/:file_name' do + subject do + get api("/packages/conan/v1/files/#{recipe_path}/#{metadata.recipe_revision}/package/#{metadata.conan_package_reference}/#{metadata.package_revision}/#{package_file.file_name}"), + headers: headers + end + + it_behaves_like 'package file download endpoint' + it_behaves_like 'project not found by recipe' + end + end + + context 'file upload endpoints' do + include_context 'conan file upload endpoints' + + describe 'PUT /api/v4/packages/conan/v1/files/:package_name/package_version/:package_username/:package_channel/:recipe_revision/export/:file_name/authorize' do + let(:file_name) { 'conanfile.py' } + + subject { put api("/packages/conan/v1/files/#{recipe_path}/0/export/#{file_name}/authorize"), headers: headers_with_token } + + it_behaves_like 'workhorse authorize endpoint' + end + + describe 'PUT /api/v4/packages/conan/v1/files/:package_name/package_version/:package_username/:package_channel/:recipe_revision/export/:conan_package_reference/:package_revision/:file_name/authorize' do + let(:file_name) { 'conaninfo.txt' } + + subject { put api("/packages/conan/v1/files/#{recipe_path}/0/package/123456789/0/#{file_name}/authorize"), headers: headers_with_token } + + it_behaves_like 'workhorse authorize endpoint' + end + + describe 'PUT /api/v4/packages/conan/v1/files/:package_name/package_version/:package_username/:package_channel/:recipe_revision/export/:file_name' do + let(:url) { "/api/v4/packages/conan/v1/files/#{recipe_path}/0/export/#{file_name}" } + + it_behaves_like 'workhorse recipe file upload endpoint' + end + + describe 'PUT /api/v4/packages/conan/v1/files/:package_name/package_version/:package_username/:package_channel/:recipe_revision/export/:conan_package_reference/:package_revision/:file_name' do + let(:url) { "/api/v4/packages/conan/v1/files/#{recipe_path}/0/package/123456789/0/#{file_name}" } + + it_behaves_like 'workhorse package file upload endpoint' + end + end +end diff --git a/spec/requests/api/conan_packages_spec.rb b/spec/requests/api/conan_packages_spec.rb deleted file mode 100644 index ab37c361618..00000000000 --- a/spec/requests/api/conan_packages_spec.rb +++ /dev/null @@ -1,986 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe API::ConanPackages do - include WorkhorseHelpers - include HttpBasicAuthHelpers - include PackagesManagerApiSpecHelpers - - let(:package) { create(:conan_package) } - let_it_be(:personal_access_token) { create(:personal_access_token) } - let_it_be(:user) { personal_access_token.user } - let(:project) { package.project } - - let(:base_secret) { SecureRandom.base64(64) } - let(:auth_token) { personal_access_token.token } - let(:job) { create(:ci_build, user: user, status: :running) } - let(:job_token) { job.token } - let(:deploy_token) { create(:deploy_token, read_package_registry: true, write_package_registry: true) } - let(:project_deploy_token) { create(:project_deploy_token, deploy_token: deploy_token, project: project) } - - let(:headers) do - { 'HTTP_AUTHORIZATION' => ActionController::HttpAuthentication::Basic.encode_credentials('foo', auth_token) } - end - - let(:jwt_secret) do - OpenSSL::HMAC.hexdigest( - OpenSSL::Digest::SHA256.new, - base_secret, - Gitlab::ConanToken::HMAC_KEY - ) - end - - before do - project.add_developer(user) - allow(Settings).to receive(:attr_encrypted_db_key_base).and_return(base_secret) - end - - describe 'GET /api/v4/packages/conan/v1/ping' do - it 'responds with 401 Unauthorized when no token provided' do - get api('/packages/conan/v1/ping') - - expect(response).to have_gitlab_http_status(:unauthorized) - end - - it 'responds with 200 OK when valid token is provided' do - jwt = build_jwt(personal_access_token) - get api('/packages/conan/v1/ping'), headers: build_token_auth_header(jwt.encoded) - - expect(response).to have_gitlab_http_status(:ok) - expect(response.headers['X-Conan-Server-Capabilities']).to eq("") - end - - it 'responds with 200 OK when valid job token is provided' do - jwt = build_jwt_from_job(job) - get api('/packages/conan/v1/ping'), headers: build_token_auth_header(jwt.encoded) - - expect(response).to have_gitlab_http_status(:ok) - expect(response.headers['X-Conan-Server-Capabilities']).to eq("") - end - - it 'responds with 200 OK when valid deploy token is provided' do - jwt = build_jwt_from_deploy_token(deploy_token) - get api('/packages/conan/v1/ping'), headers: build_token_auth_header(jwt.encoded) - - expect(response).to have_gitlab_http_status(:ok) - expect(response.headers['X-Conan-Server-Capabilities']).to eq("") - end - - it 'responds with 401 Unauthorized when invalid access token ID is provided' do - jwt = build_jwt(double(id: 12345), user_id: personal_access_token.user_id) - get api('/packages/conan/v1/ping'), headers: build_token_auth_header(jwt.encoded) - - expect(response).to have_gitlab_http_status(:unauthorized) - end - - it 'responds with 401 Unauthorized when invalid user is provided' do - jwt = build_jwt(personal_access_token, user_id: 12345) - get api('/packages/conan/v1/ping'), headers: build_token_auth_header(jwt.encoded) - - expect(response).to have_gitlab_http_status(:unauthorized) - end - - it 'responds with 401 Unauthorized when the provided JWT is signed with different secret' do - jwt = build_jwt(personal_access_token, secret: SecureRandom.base64(32)) - get api('/packages/conan/v1/ping'), headers: build_token_auth_header(jwt.encoded) - - expect(response).to have_gitlab_http_status(:unauthorized) - end - - it 'responds with 401 Unauthorized when invalid JWT is provided' do - get api('/packages/conan/v1/ping'), headers: build_token_auth_header('invalid-jwt') - - expect(response).to have_gitlab_http_status(:unauthorized) - end - - it 'responds with 401 Unauthorized when the job is not running' do - job.update!(status: :failed) - jwt = build_jwt_from_job(job) - get api('/packages/conan/v1/ping'), headers: build_token_auth_header(jwt.encoded) - - expect(response).to have_gitlab_http_status(:unauthorized) - end - - context 'packages feature disabled' do - it 'responds with 404 Not Found' do - stub_packages_setting(enabled: false) - get api('/packages/conan/v1/ping') - - expect(response).to have_gitlab_http_status(:not_found) - end - end - end - - describe 'GET /api/v4/packages/conan/v1/conans/search' do - before do - project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC) - - get api('/packages/conan/v1/conans/search'), headers: headers, params: params - end - - subject { json_response['results'] } - - context 'returns packages with a matching name' do - let(:params) { { q: package.conan_recipe } } - - it { is_expected.to contain_exactly(package.conan_recipe) } - end - - context 'returns packages using a * wildcard' do - let(:params) { { q: "#{package.name[0, 3]}*" } } - - it { is_expected.to contain_exactly(package.conan_recipe) } - end - - context 'does not return non-matching packages' do - let(:params) { { q: "foo" } } - - it { is_expected.to be_blank } - end - end - - describe 'GET /api/v4/packages/conan/v1/users/authenticate' do - subject { get api('/packages/conan/v1/users/authenticate'), headers: headers } - - context 'when using invalid token' do - let(:auth_token) { 'invalid_token' } - - it 'responds with 401' do - subject - - expect(response).to have_gitlab_http_status(:unauthorized) - end - end - - context 'when valid JWT access token is provided' do - it 'responds with 200' do - subject - - expect(response).to have_gitlab_http_status(:ok) - end - - it 'token has valid validity time' do - freeze_time do - subject - - payload = JSONWebToken::HMACToken.decode( - response.body, jwt_secret).first - expect(payload['access_token']).to eq(personal_access_token.id) - expect(payload['user_id']).to eq(personal_access_token.user_id) - - duration = payload['exp'] - payload['iat'] - expect(duration).to eq(1.hour) - end - end - end - - context 'with valid job token' do - let(:auth_token) { job_token } - - it 'responds with 200' do - subject - - expect(response).to have_gitlab_http_status(:ok) - end - end - - context 'with valid deploy token' do - let(:auth_token) { deploy_token.token } - - it 'responds with 200' do - subject - - expect(response).to have_gitlab_http_status(:ok) - end - end - end - - describe 'GET /api/v4/packages/conan/v1/users/check_credentials' do - it 'responds with a 200 OK with PAT' do - get api('/packages/conan/v1/users/check_credentials'), headers: headers - - expect(response).to have_gitlab_http_status(:ok) - end - - context 'with job token' do - let(:auth_token) { job_token } - - it 'responds with a 200 OK with job token' do - get api('/packages/conan/v1/users/check_credentials'), headers: headers - - expect(response).to have_gitlab_http_status(:ok) - end - end - - context 'with deploy token' do - let(:auth_token) { deploy_token.token } - - it 'responds with a 200 OK with job token' do - get api('/packages/conan/v1/users/check_credentials'), headers: headers - - expect(response).to have_gitlab_http_status(:ok) - end - end - - it 'responds with a 401 Unauthorized when an invalid token is used' do - get api('/packages/conan/v1/users/check_credentials'), headers: build_token_auth_header('invalid-token') - - expect(response).to have_gitlab_http_status(:unauthorized) - end - end - - shared_examples 'rejects invalid recipe' do - context 'with invalid recipe path' do - let(:recipe_path) { '../../foo++../..' } - - it 'returns 400' do - subject - - expect(response).to have_gitlab_http_status(:bad_request) - end - end - end - - shared_examples 'rejects invalid file_name' do |invalid_file_name| - let(:file_name) { invalid_file_name } - - context 'with invalid file_name' do - it 'returns 400' do - subject - - expect(response).to have_gitlab_http_status(:bad_request) - end - end - end - - shared_examples 'rejects recipe for invalid project' do - context 'with invalid recipe path' do - let(:recipe_path) { 'aa/bb/not-existing-project/ccc' } - - it 'returns forbidden' do - subject - - expect(response).to have_gitlab_http_status(:forbidden) - end - end - end - - shared_examples 'rejects recipe for not found package' do - context 'with invalid recipe path' do - let(:recipe_path) do - 'aa/bb/%{project}/ccc' % { project: ::Packages::Conan::Metadatum.package_username_from(full_path: project.full_path) } - end - - it 'returns not found' do - subject - - expect(response).to have_gitlab_http_status(:not_found) - end - end - end - - shared_examples 'empty recipe for not found package' do - context 'with invalid recipe url' do - let(:recipe_path) do - 'aa/bb/%{project}/ccc' % { project: ::Packages::Conan::Metadatum.package_username_from(full_path: project.full_path) } - end - - it 'returns not found' do - allow(::Packages::Conan::PackagePresenter).to receive(:new) - .with( - nil, - user, - project, - any_args - ).and_return(presenter) - allow(presenter).to receive(:recipe_snapshot) { {} } - allow(presenter).to receive(:package_snapshot) { {} } - - subject - - expect(response).to have_gitlab_http_status(:ok) - expect(response.body).to eq("{}") - end - end - end - - shared_examples 'not selecting a package with the wrong type' do - context 'with a nuget package with same name and version' do - let(:conan_username) { ::Packages::Conan::Metadatum.package_username_from(full_path: project.full_path) } - let(:wrong_package) { create(:nuget_package, name: "wrong", version: '1.0.0', project: project) } - let(:recipe_path) { "#{wrong_package.name}/#{wrong_package.version}/#{conan_username}/foo" } - - it 'calls the presenter with a nil package' do - expect(::Packages::Conan::PackagePresenter).to receive(:new) - .with(nil, user, project, any_args) - - subject - end - end - end - - shared_examples 'recipe download_urls' do - let(:recipe_path) { package.conan_recipe_path } - - it 'returns the download_urls for the recipe files' do - expected_response = { - 'conanfile.py' => "#{Settings.gitlab.base_url}/api/v4/packages/conan/v1/files/#{package.conan_recipe_path}/0/export/conanfile.py", - 'conanmanifest.txt' => "#{Settings.gitlab.base_url}/api/v4/packages/conan/v1/files/#{package.conan_recipe_path}/0/export/conanmanifest.txt" - } - - allow(presenter).to receive(:recipe_urls) { expected_response } - - subject - - expect(json_response).to eq(expected_response) - end - - it_behaves_like 'not selecting a package with the wrong type' - end - - shared_examples 'package download_urls' do - let(:recipe_path) { package.conan_recipe_path } - - it 'returns the download_urls for the package files' do - expected_response = { - 'conaninfo.txt' => "#{Settings.gitlab.base_url}/api/v4/packages/conan/v1/files/#{package.conan_recipe_path}/0/package/123456789/0/conaninfo.txt", - 'conanmanifest.txt' => "#{Settings.gitlab.base_url}/api/v4/packages/conan/v1/files/#{package.conan_recipe_path}/0/package/123456789/0/conanmanifest.txt", - 'conan_package.tgz' => "#{Settings.gitlab.base_url}/api/v4/packages/conan/v1/files/#{package.conan_recipe_path}/0/package/123456789/0/conan_package.tgz" - } - - allow(presenter).to receive(:package_urls) { expected_response } - - subject - - expect(json_response).to eq(expected_response) - end - - it_behaves_like 'not selecting a package with the wrong type' - end - - context 'recipe endpoints' do - let(:jwt) { build_jwt(personal_access_token) } - let(:headers) { build_token_auth_header(jwt.encoded) } - let(:conan_package_reference) { '123456789' } - let(:presenter) { double('::Packages::Conan::PackagePresenter') } - - before do - allow(::Packages::Conan::PackagePresenter).to receive(:new) - .with(package, user, package.project, any_args) - .and_return(presenter) - end - - shared_examples 'rejects invalid upload_url params' do - context 'with unaccepted json format' do - let(:params) { %w[foo bar] } - - it 'returns 400' do - subject - - expect(response).to have_gitlab_http_status(:bad_request) - end - end - end - - shared_examples 'successful response when using Unicorn' do - context 'on Unicorn', :unicorn do - it 'returns successfully' do - subject - - expect(response).to have_gitlab_http_status(:ok) - end - end - end - - describe 'GET /api/v4/packages/conan/v1/conans/:package_name/package_version/:package_username/:package_channel' do - let(:recipe_path) { package.conan_recipe_path } - - subject { get api("/packages/conan/v1/conans/#{recipe_path}"), headers: headers } - - it_behaves_like 'rejects invalid recipe' - it_behaves_like 'rejects recipe for invalid project' - it_behaves_like 'empty recipe for not found package' - - context 'with existing package' do - it 'returns a hash of files with their md5 hashes' do - expected_response = { - 'conanfile.py' => 'md5hash1', - 'conanmanifest.txt' => 'md5hash2' - } - - allow(presenter).to receive(:recipe_snapshot) { expected_response } - - subject - - expect(json_response).to eq(expected_response) - end - end - end - - describe 'GET /api/v4/packages/conan/v1/conans/:package_name/package_version/:package_username/:package_channel/packages/:conan_package_reference' do - let(:recipe_path) { package.conan_recipe_path } - - subject { get api("/packages/conan/v1/conans/#{recipe_path}/packages/#{conan_package_reference}"), headers: headers } - - it_behaves_like 'rejects invalid recipe' - it_behaves_like 'rejects recipe for invalid project' - it_behaves_like 'empty recipe for not found package' - - context 'with existing package' do - it 'returns a hash of md5 values for the files' do - expected_response = { - 'conaninfo.txt' => "md5hash1", - 'conanmanifest.txt' => "md5hash2", - 'conan_package.tgz' => "md5hash3" - } - - allow(presenter).to receive(:package_snapshot) { expected_response } - - subject - - expect(json_response).to eq(expected_response) - end - end - end - - describe 'GET /api/v4/packages/conan/v1/conans/:package_name/package_version/:package_username/:package_channel/digest' do - subject { get api("/packages/conan/v1/conans/#{recipe_path}/digest"), headers: headers } - - it_behaves_like 'rejects invalid recipe' - it_behaves_like 'rejects recipe for invalid project' - it_behaves_like 'recipe download_urls' - end - - describe 'GET /api/v4/packages/conan/v1/conans/:package_name/package_version/:package_username/:package_channel/packages/:conan_package_reference/download_urls' do - subject { get api("/packages/conan/v1/conans/#{recipe_path}/packages/#{conan_package_reference}/download_urls"), headers: headers } - - it_behaves_like 'rejects invalid recipe' - it_behaves_like 'rejects recipe for invalid project' - it_behaves_like 'package download_urls' - end - - describe 'GET /api/v4/packages/conan/v1/conans/:package_name/package_version/:package_username/:package_channel/download_urls' do - subject { get api("/packages/conan/v1/conans/#{recipe_path}/download_urls"), headers: headers } - - it_behaves_like 'rejects invalid recipe' - it_behaves_like 'rejects recipe for invalid project' - it_behaves_like 'recipe download_urls' - end - - describe 'GET /api/v4/packages/conan/v1/conans/:package_name/package_version/:package_username/:package_channel/packages/:conan_package_reference/digest' do - subject { get api("/packages/conan/v1/conans/#{recipe_path}/packages/#{conan_package_reference}/digest"), headers: headers } - - it_behaves_like 'rejects invalid recipe' - it_behaves_like 'rejects recipe for invalid project' - it_behaves_like 'package download_urls' - end - - describe 'POST /api/v4/packages/conan/v1/conans/:package_name/package_version/:package_username/:package_channel/upload_urls' do - let(:recipe_path) { package.conan_recipe_path } - - let(:params) do - { 'conanfile.py': 24, - 'conanmanifest.txt': 123 } - end - - subject { post api("/packages/conan/v1/conans/#{recipe_path}/upload_urls"), params: params.to_json, headers: headers } - - it_behaves_like 'rejects invalid recipe' - it_behaves_like 'rejects invalid upload_url params' - it_behaves_like 'successful response when using Unicorn' - - it 'returns a set of upload urls for the files requested' do - subject - - expected_response = { - 'conanfile.py': "#{Settings.gitlab.base_url}/api/v4/packages/conan/v1/files/#{package.conan_recipe_path}/0/export/conanfile.py", - 'conanmanifest.txt': "#{Settings.gitlab.base_url}/api/v4/packages/conan/v1/files/#{package.conan_recipe_path}/0/export/conanmanifest.txt" - } - - expect(response.body).to eq(expected_response.to_json) - end - - context 'with conan_sources and conan_export files' do - let(:params) do - { 'conan_sources.tgz': 345, - 'conan_export.tgz': 234, - 'conanmanifest.txt': 123 } - end - - it 'returns upload urls for the additional files' do - subject - - expected_response = { - 'conan_sources.tgz': "#{Settings.gitlab.base_url}/api/v4/packages/conan/v1/files/#{package.conan_recipe_path}/0/export/conan_sources.tgz", - 'conan_export.tgz': "#{Settings.gitlab.base_url}/api/v4/packages/conan/v1/files/#{package.conan_recipe_path}/0/export/conan_export.tgz", - 'conanmanifest.txt': "#{Settings.gitlab.base_url}/api/v4/packages/conan/v1/files/#{package.conan_recipe_path}/0/export/conanmanifest.txt" - } - - expect(response.body).to eq(expected_response.to_json) - end - end - - context 'with an invalid file' do - let(:params) do - { 'invalid_file.txt': 10, - 'conanmanifest.txt': 123 } - end - - it 'does not return the invalid file as an upload_url' do - subject - - expected_response = { - 'conanmanifest.txt': "#{Settings.gitlab.base_url}/api/v4/packages/conan/v1/files/#{package.conan_recipe_path}/0/export/conanmanifest.txt" - } - - expect(response.body).to eq(expected_response.to_json) - end - end - end - - describe 'POST /api/v4/packages/conan/v1/conans/:package_name/package_version/:package_username/:package_channel/packages/:conan_package_reference/upload_urls' do - let(:recipe_path) { package.conan_recipe_path } - - let(:params) do - { 'conaninfo.txt': 24, - 'conanmanifest.txt': 123, - 'conan_package.tgz': 523 } - end - - subject { post api("/packages/conan/v1/conans/#{recipe_path}/packages/123456789/upload_urls"), params: params.to_json, headers: headers } - - it_behaves_like 'rejects invalid recipe' - it_behaves_like 'rejects invalid upload_url params' - it_behaves_like 'successful response when using Unicorn' - - it 'returns a set of upload urls for the files requested' do - expected_response = { - 'conaninfo.txt': "#{Settings.gitlab.base_url}/api/v4/packages/conan/v1/files/#{package.conan_recipe_path}/0/package/123456789/0/conaninfo.txt", - 'conanmanifest.txt': "#{Settings.gitlab.base_url}/api/v4/packages/conan/v1/files/#{package.conan_recipe_path}/0/package/123456789/0/conanmanifest.txt", - 'conan_package.tgz': "#{Settings.gitlab.base_url}/api/v4/packages/conan/v1/files/#{package.conan_recipe_path}/0/package/123456789/0/conan_package.tgz" - } - - subject - - expect(response.body).to eq(expected_response.to_json) - end - - context 'with invalid files' do - let(:params) do - { 'conaninfo.txt': 24, - 'invalid_file.txt': 10 } - end - - it 'returns upload urls only for the valid requested files' do - expected_response = { - 'conaninfo.txt': "#{Settings.gitlab.base_url}/api/v4/packages/conan/v1/files/#{package.conan_recipe_path}/0/package/123456789/0/conaninfo.txt" - } - - subject - - expect(response.body).to eq(expected_response.to_json) - end - end - end - - describe 'DELETE /api/v4/packages/conan/v1/conans/:package_name/package_version/:package_username/:package_channel' do - let(:recipe_path) { package.conan_recipe_path } - - subject { delete api("/packages/conan/v1/conans/#{recipe_path}"), headers: headers} - - it_behaves_like 'rejects invalid recipe' - - it 'returns unauthorized for users without valid permission' do - subject - - expect(response).to have_gitlab_http_status(:forbidden) - end - - context 'with delete permissions' do - before do - project.add_maintainer(user) - end - - it_behaves_like 'a gitlab tracking event', described_class.name, 'delete_package' - - it 'deletes a package' do - expect { subject }.to change { Packages::Package.count }.from(2).to(1) - end - end - end - end - - context 'file endpoints' do - let(:jwt) { build_jwt(personal_access_token) } - let(:headers) { build_token_auth_header(jwt.encoded) } - let(:recipe_path) { package.conan_recipe_path } - - shared_examples 'denies download with no token' do - context 'with no private token' do - let(:headers) { {} } - - it 'returns 400' do - subject - - expect(response).to have_gitlab_http_status(:unauthorized) - end - end - end - - shared_examples 'a public project with packages' do - it 'returns the file' do - subject - - expect(response).to have_gitlab_http_status(:ok) - expect(response.media_type).to eq('application/octet-stream') - end - end - - shared_examples 'an internal project with packages' do - before do - project.team.truncate - project.update!(visibility_level: Gitlab::VisibilityLevel::INTERNAL) - end - - it_behaves_like 'denies download with no token' - - it 'returns the file' do - subject - - expect(response).to have_gitlab_http_status(:ok) - expect(response.media_type).to eq('application/octet-stream') - end - end - - shared_examples 'a private project with packages' do - before do - project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE) - end - - it_behaves_like 'denies download with no token' - - it 'returns the file' do - subject - - expect(response).to have_gitlab_http_status(:ok) - expect(response.media_type).to eq('application/octet-stream') - end - - it 'denies download when not enough permissions' do - project.add_guest(user) - - subject - - expect(response).to have_gitlab_http_status(:forbidden) - end - end - - shared_examples 'a project is not found' do - let(:recipe_path) { 'not/package/for/project' } - - it 'returns forbidden' do - subject - - expect(response).to have_gitlab_http_status(:forbidden) - end - end - - describe 'GET /api/v4/packages/conan/v1/files/:package_name/package_version/:package_username/:package_channel/ -:recipe_revision/export/:file_name' do - let(:recipe_file) { package.package_files.find_by(file_name: 'conanfile.py') } - let(:metadata) { recipe_file.conan_file_metadatum } - - subject do - get api("/packages/conan/v1/files/#{recipe_path}/#{metadata.recipe_revision}/export/#{recipe_file.file_name}"), - headers: headers - end - - it_behaves_like 'a public project with packages' - it_behaves_like 'an internal project with packages' - it_behaves_like 'a private project with packages' - it_behaves_like 'a project is not found' - end - - describe 'GET /api/v4/packages/conan/v1/files/:package_name/package_version/:package_username/:package_channel/ -:recipe_revision/package/:conan_package_reference/:package_revision/:file_name' do - let(:package_file) { package.package_files.find_by(file_name: 'conaninfo.txt') } - let(:metadata) { package_file.conan_file_metadatum } - - subject do - get api("/packages/conan/v1/files/#{recipe_path}/#{metadata.recipe_revision}/package/#{metadata.conan_package_reference}/#{metadata.package_revision}/#{package_file.file_name}"), - headers: headers - end - - it_behaves_like 'a public project with packages' - it_behaves_like 'an internal project with packages' - it_behaves_like 'a private project with packages' - it_behaves_like 'a project is not found' - - context 'tracking the conan_package.tgz download' do - let(:package_file) { package.package_files.find_by(file_name: ::Packages::Conan::FileMetadatum::PACKAGE_BINARY) } - - it_behaves_like 'a gitlab tracking event', described_class.name, 'pull_package' - end - end - end - - context 'file uploads' do - let(:jwt) { build_jwt(personal_access_token) } - 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(:headers_with_token) { build_token_auth_header(jwt.encoded).merge(workhorse_header) } - let(:recipe_path) { "foo/bar/#{project.full_path.tr('/', '+')}/baz"} - - shared_examples 'uploads a package file' do - context 'file size above maximum limit' do - before do - params['file.size'] = project.actual_limits.conan_max_file_size + 1 - end - - it 'handles as a local file' do - subject - - expect(response).to have_gitlab_http_status(:bad_request) - end - end - - context 'with object storage disabled' do - context 'without a file from workhorse' do - let(:params) { { file: nil } } - - it 'rejects the request' do - subject - - expect(response).to have_gitlab_http_status(:bad_request) - end - end - - context 'with a file' do - it_behaves_like 'package workhorse uploads' - end - - context 'without a token' do - it 'rejects request without a token' do - headers_with_token.delete('HTTP_AUTHORIZATION') - - subject - - expect(response).to have_gitlab_http_status(:unauthorized) - end - end - - context 'when params from workhorse are correct' do - it 'creates package and stores package file' do - expect { subject } - .to change { project.packages.count }.by(1) - .and change { Packages::PackageFile.count }.by(1) - - expect(response).to have_gitlab_http_status(:ok) - - package_file = project.packages.last.package_files.reload.last - expect(package_file.file_name).to eq(params[:file].original_filename) - end - - it "doesn't attempt to migrate file to object storage" do - expect(ObjectStorage::BackgroundMoveWorker).not_to receive(:perform_async) - - subject - end - end - end - - context 'with object storage enabled' do - context 'and direct upload enabled' do - let!(:fog_connection) do - stub_package_file_object_storage(direct_upload: true) - end - - let(:tmp_object) do - fog_connection.directories.new(key: 'packages').files.create( - key: "tmp/uploads/#{file_name}", - body: 'content' - ) - end - - let(:fog_file) { fog_to_uploaded_file(tmp_object) } - - ['123123', '../../123123'].each do |remote_id| - context "with invalid remote_id: #{remote_id}" do - let(:params) do - { - file: fog_file, - 'file.remote_id' => remote_id - } - end - - it 'responds with status 403' do - subject - - expect(response).to have_gitlab_http_status(:forbidden) - end - end - end - - context 'with valid remote_id' do - let(:params) do - { - file: fog_file, - 'file.remote_id' => file_name - } - end - - it 'creates package and stores package file' do - expect { subject } - .to change { project.packages.count }.by(1) - .and change { Packages::PackageFile.count }.by(1) - - expect(response).to have_gitlab_http_status(:ok) - - package_file = project.packages.last.package_files.reload.last - expect(package_file.file_name).to eq(params[:file].original_filename) - expect(package_file.file.read).to eq('content') - end - end - end - - it_behaves_like 'background upload schedules a file migration' - end - end - - shared_examples 'workhorse authorization' do - it 'authorizes posting package with a valid token' do - subject - - expect(response).to have_gitlab_http_status(:ok) - expect(response.media_type).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE) - end - - it 'rejects request without a valid token' do - headers_with_token['HTTP_AUTHORIZATION'] = 'foo' - - subject - - expect(response).to have_gitlab_http_status(:unauthorized) - end - - it 'rejects request without a valid permission' do - project.add_guest(user) - - subject - - expect(response).to have_gitlab_http_status(:forbidden) - end - - it 'rejects requests that bypassed gitlab-workhorse' do - headers_with_token.delete(Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER) - - subject - - expect(response).to have_gitlab_http_status(:forbidden) - end - - context 'when using remote storage' do - context 'when direct upload is enabled' do - before do - stub_package_file_object_storage(enabled: true, direct_upload: true) - end - - it 'responds with status 200, location of package remote store and object details' do - subject - - expect(response).to have_gitlab_http_status(:ok) - expect(response.media_type).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE) - expect(json_response).not_to have_key('TempPath') - expect(json_response['RemoteObject']).to have_key('ID') - expect(json_response['RemoteObject']).to have_key('GetURL') - expect(json_response['RemoteObject']).to have_key('StoreURL') - expect(json_response['RemoteObject']).to have_key('DeleteURL') - expect(json_response['RemoteObject']).not_to have_key('MultipartUpload') - end - end - - context 'when direct upload is disabled' do - before do - stub_package_file_object_storage(enabled: true, direct_upload: false) - end - - it 'handles as a local file' do - subject - - expect(response).to have_gitlab_http_status(:ok) - expect(response.media_type).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE) - expect(json_response['TempPath']).to eq(::Packages::PackageFileUploader.workhorse_local_upload_path) - expect(json_response['RemoteObject']).to be_nil - end - end - end - end - - describe 'PUT /api/v4/packages/conan/v1/files/:package_name/package_version/:package_username/:package_channel/:recipe_revision/export/:file_name/authorize' do - let(:file_name) { 'conanfile.py' } - - subject { put api("/packages/conan/v1/files/#{recipe_path}/0/export/#{file_name}/authorize"), headers: headers_with_token } - - it_behaves_like 'rejects invalid recipe' - it_behaves_like 'rejects invalid file_name', 'conanfile.py.git%2fgit-upload-pack' - it_behaves_like 'workhorse authorization' - end - - describe 'PUT /api/v4/packages/conan/v1/files/:package_name/package_version/:package_username/:package_channel/:recipe_revision/export/:conan_package_reference/:package_revision/:file_name/authorize' do - let(:file_name) { 'conaninfo.txt' } - - subject { put api("/packages/conan/v1/files/#{recipe_path}/0/package/123456789/0/#{file_name}/authorize"), headers: headers_with_token } - - it_behaves_like 'rejects invalid recipe' - it_behaves_like 'rejects invalid file_name', 'conaninfo.txttest' - it_behaves_like 'workhorse authorization' - end - - describe 'PUT /api/v4/packages/conan/v1/files/:package_name/package_version/:package_username/:package_channel/:recipe_revision/export/:file_name' do - let(:file_name) { 'conanfile.py' } - let(:params) { { file: temp_file(file_name) } } - - subject do - workhorse_finalize( - "/api/v4/packages/conan/v1/files/#{recipe_path}/0/export/#{file_name}", - method: :put, - file_key: :file, - params: params, - send_rewritten_field: true, - headers: headers_with_token - ) - end - - it_behaves_like 'rejects invalid recipe' - it_behaves_like 'rejects invalid file_name', 'conanfile.py.git%2fgit-upload-pack' - it_behaves_like 'uploads a package file' - end - - describe 'PUT /api/v4/packages/conan/v1/files/:package_name/package_version/:package_username/:package_channel/:recipe_revision/export/:conan_package_reference/:package_revision/:file_name' do - let(:file_name) { 'conaninfo.txt' } - let(:params) { { file: temp_file(file_name) } } - - subject do - workhorse_finalize( - "/api/v4/packages/conan/v1/files/#{recipe_path}/0/package/123456789/0/#{file_name}", - method: :put, - file_key: :file, - params: params, - headers: headers_with_token, - send_rewritten_field: true - ) - end - - it_behaves_like 'rejects invalid recipe' - it_behaves_like 'rejects invalid file_name', 'conaninfo.txttest' - it_behaves_like 'uploads a package file' - - context 'tracking the conan_package.tgz upload' do - let(:file_name) { ::Packages::Conan::FileMetadatum::PACKAGE_BINARY } - - it_behaves_like 'a gitlab tracking event', described_class.name, 'push_package' - end - end - end -end diff --git a/spec/requests/api/conan_project_packages_spec.rb b/spec/requests/api/conan_project_packages_spec.rb new file mode 100644 index 00000000000..fefaf9790b1 --- /dev/null +++ b/spec/requests/api/conan_project_packages_spec.rb @@ -0,0 +1,152 @@ +# frozen_string_literal: true +require 'spec_helper' + +RSpec.describe API::ConanProjectPackages do + include_context 'conan api setup' + + let(:project_id) { project.id } + + describe 'GET /api/v4/projects/:id/packages/conan/v1/ping' do + let(:url) { "/projects/#{project.id}/packages/conan/v1/ping" } + + it_behaves_like 'conan ping endpoint' + end + + describe 'GET /api/v4/projects/:id/packages/conan/v1/conans/search' do + let(:url) { "/projects/#{project.id}/packages/conan/v1/conans/search" } + + it_behaves_like 'conan search endpoint' + end + + describe 'GET /api/v4/projects/:id/packages/conan/v1/users/authenticate' do + let(:url) { "/projects/#{project.id}/packages/conan/v1/users/authenticate" } + + it_behaves_like 'conan authenticate endpoint' + end + + describe 'GET /api/v4/projects/:id/packages/conan/v1/users/check_credentials' do + let(:url) { "/projects/#{project.id}/packages/conan/v1/users/check_credentials" } + + it_behaves_like 'conan check_credentials endpoint' + end + + context 'recipe endpoints' do + include_context 'conan recipe endpoints' + + let(:url_prefix) { "#{Settings.gitlab.base_url}/api/v4/projects/#{project_id}" } + + describe 'GET /api/v4/projects/:id/packages/conan/v1/conans/:package_name/package_version/:package_username/:package_channel' do + let(:recipe_path) { package.conan_recipe_path } + let(:url) { "/projects/#{project_id}/packages/conan/v1/conans/#{recipe_path}" } + + it_behaves_like 'recipe snapshot endpoint' + end + + describe 'GET /api/v4/projects/:id/packages/conan/v1/conans/:package_name/package_version/:package_username/:package_channel/packages/:conan_package_reference' do + let(:recipe_path) { package.conan_recipe_path } + let(:url) { "/projects/#{project_id}/packages/conan/v1/conans/#{recipe_path}/packages/#{conan_package_reference}" } + + it_behaves_like 'package snapshot endpoint' + end + + describe 'GET /api/v4/projects/:id/packages/conan/v1/conans/:package_name/package_version/:package_username/:package_channel/digest' do + subject { get api("/projects/#{project_id}/packages/conan/v1/conans/#{recipe_path}/digest"), headers: headers } + + it_behaves_like 'recipe download_urls endpoint' + end + + describe 'GET /api/v4/projects/:id/packages/conan/v1/conans/:package_name/package_version/:package_username/:package_channel/packages/:conan_package_reference/download_urls' do + subject { get api("/projects/#{project_id}/packages/conan/v1/conans/#{recipe_path}/packages/#{conan_package_reference}/download_urls"), headers: headers } + + it_behaves_like 'package download_urls endpoint' + end + + describe 'GET /api/v4/projects/:id/packages/conan/v1/conans/:package_name/package_version/:package_username/:package_channel/download_urls' do + subject { get api("/projects/#{project_id}/packages/conan/v1/conans/#{recipe_path}/download_urls"), headers: headers } + + it_behaves_like 'recipe download_urls endpoint' + end + + describe 'GET /api/v4/projects/:id/packages/conan/v1/conans/:package_name/package_version/:package_username/:package_channel/packages/:conan_package_reference/digest' do + subject { get api("/projects/#{project_id}/packages/conan/v1/conans/#{recipe_path}/packages/#{conan_package_reference}/digest"), headers: headers } + + it_behaves_like 'package download_urls endpoint' + end + + describe 'POST /api/v4/projects/:id/packages/conan/v1/conans/:package_name/package_version/:package_username/:package_channel/upload_urls' do + subject { post api("/projects/#{project_id}/packages/conan/v1/conans/#{recipe_path}/upload_urls"), params: params.to_json, headers: headers } + + it_behaves_like 'recipe upload_urls endpoint' + end + + describe 'POST /api/v4/projects/:id/packages/conan/v1/conans/:package_name/package_version/:package_username/:package_channel/packages/:conan_package_reference/upload_urls' do + subject { post api("/projects/#{project_id}/packages/conan/v1/conans/#{recipe_path}/packages/123456789/upload_urls"), params: params.to_json, headers: headers } + + it_behaves_like 'package upload_urls endpoint' + end + + describe 'DELETE /api/v4/projects/:id/packages/conan/v1/conans/:package_name/package_version/:package_username/:package_channel' do + subject { delete api("/projects/#{project_id}/packages/conan/v1/conans/#{recipe_path}"), headers: headers} + + it_behaves_like 'delete package endpoint' + end + end + + context 'file download endpoints' do + include_context 'conan file download endpoints' + + describe 'GET /api/v4/projects/:id/packages/conan/v1/files/:package_name/package_version/:package_username/:package_channel/ +:recipe_revision/export/:file_name' do + subject do + get api("/projects/#{project_id}/packages/conan/v1/files/#{recipe_path}/#{metadata.recipe_revision}/export/#{recipe_file.file_name}"), + headers: headers + end + + it_behaves_like 'recipe file download endpoint' + it_behaves_like 'project not found by project id' + end + + describe 'GET /api/v4/projects/:id/packages/conan/v1/files/:package_name/package_version/:package_username/:package_channel/ +:recipe_revision/package/:conan_package_reference/:package_revision/:file_name' do + subject do + get api("/projects/#{project_id}/packages/conan/v1/files/#{recipe_path}/#{metadata.recipe_revision}/package/#{metadata.conan_package_reference}/#{metadata.package_revision}/#{package_file.file_name}"), + headers: headers + end + + it_behaves_like 'package file download endpoint' + it_behaves_like 'project not found by project id' + end + end + + context 'file upload endpoints' do + include_context 'conan file upload endpoints' + + describe 'PUT /api/v4/projects/:id/packages/conan/v1/files/:package_name/package_version/:package_username/:package_channel/:recipe_revision/export/:file_name/authorize' do + let(:file_name) { 'conanfile.py' } + + subject { put api("/projects/#{project_id}/packages/conan/v1/files/#{recipe_path}/0/export/#{file_name}/authorize"), headers: headers_with_token } + + it_behaves_like 'workhorse authorize endpoint' + end + + describe 'PUT /api/v4/projects/:id/packages/conan/v1/files/:package_name/package_version/:package_username/:package_channel/:recipe_revision/export/:conan_package_reference/:package_revision/:file_name/authorize' do + let(:file_name) { 'conaninfo.txt' } + + subject { put api("/projects/#{project_id}/packages/conan/v1/files/#{recipe_path}/0/package/123456789/0/#{file_name}/authorize"), headers: headers_with_token } + + it_behaves_like 'workhorse authorize endpoint' + end + + describe 'PUT /api/v4/projects/:id/packages/conan/v1/files/:package_name/package_version/:package_username/:package_channel/:recipe_revision/export/:file_name' do + let(:url) { "/api/v4/projects/#{project_id}/packages/conan/v1/files/#{recipe_path}/0/export/#{file_name}" } + + it_behaves_like 'workhorse recipe file upload endpoint' + end + + describe 'PUT /api/v4/projects/:id/packages/conan/v1/files/:package_name/package_version/:package_username/:package_channel/:recipe_revision/export/:conan_package_reference/:package_revision/:file_name' do + let(:url) { "/api/v4/projects/#{project_id}/packages/conan/v1/files/#{recipe_path}/0/package/123456789/0/#{file_name}" } + + it_behaves_like 'workhorse package file upload endpoint' + end + end +end diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index df95835ae94..b4705c4cc55 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -49,15 +49,15 @@ end RSpec.describe API::Projects do include ProjectForksHelper - let(:user) { create(:user) } - let(:user2) { create(:user) } - let(:user3) { create(:user) } - let(:admin) { create(:admin) } - let(:project) { create(:project, :repository, namespace: user.namespace) } - let(:project2) { create(:project, namespace: user.namespace) } - let(:project_member) { create(:project_member, :developer, user: user3, project: project) } - let(:user4) { create(:user, username: 'user.with.dot') } - let(:project3) do + let_it_be(:user) { create(:user) } + let_it_be(:user2) { create(:user) } + let_it_be(:user3) { create(:user) } + let_it_be(:admin) { create(:admin) } + let_it_be(:project, reload: true) { create(:project, :repository, namespace: user.namespace) } + let_it_be(:project2, reload: true) { create(:project, namespace: user.namespace) } + let_it_be(:project_member) { create(:project_member, :developer, user: user3, project: project) } + let_it_be(:user4) { create(:user, username: 'user.with.dot') } + let_it_be(:project3, reload: true) do create(:project, :private, :repository, @@ -71,14 +71,14 @@ RSpec.describe API::Projects do snippets_enabled: false) end - let(:project_member2) do + let_it_be(:project_member2) do create(:project_member, user: user4, project: project3, access_level: ProjectMember::MAINTAINER) end - let(:project4) do + let_it_be(:project4, reload: true) do create(:project, name: 'third_project', path: 'third_project', @@ -86,6 +86,8 @@ RSpec.describe API::Projects do namespace: user4.namespace) end + let(:user_projects) { [public_project, project, project2, project3] } + shared_context 'with language detection' do let(:ruby) { create(:programming_language, name: 'Ruby') } let(:javascript) { create(:programming_language, name: 'JavaScript') } @@ -146,14 +148,7 @@ RSpec.describe API::Projects do end end - let!(:public_project) { create(:project, :public, name: 'public_project') } - - before do - project - project2 - project3 - project4 - end + let_it_be(:public_project) { create(:project, :public, name: 'public_project') } context 'when unauthenticated' do it_behaves_like 'projects response' do @@ -171,7 +166,7 @@ RSpec.describe API::Projects do it_behaves_like 'projects response' do let(:filter) { {} } let(:current_user) { user } - let(:projects) { [public_project, project, project2, project3] } + let(:projects) { user_projects } end it_behaves_like 'projects response without N + 1 queries' do @@ -257,7 +252,7 @@ RSpec.describe API::Projects do expect(response).to include_pagination_headers expect(json_response).to be_an Array - statistics = json_response.first['statistics'] + statistics = json_response.find { |p| p['id'] == project.id }['statistics'] expect(statistics).to be_present expect(statistics).to include('commit_count', 'storage_size', 'repository_size', 'wiki_size', 'lfs_objects_size', 'job_artifacts_size', 'snippets_size') end @@ -386,14 +381,14 @@ RSpec.describe API::Projects do it_behaves_like 'projects response' do let(:filter) { { id_after: project2.id } } let(:current_user) { user } - let(:projects) { [public_project, project, project2, project3].select { |p| p.id > project2.id } } + let(:projects) { user_projects.select { |p| p.id > project2.id } } end context 'regression: empty string is ignored' do it_behaves_like 'projects response' do let(:filter) { { id_after: '' } } let(:current_user) { user } - let(:projects) { [public_project, project, project2, project3] } + let(:projects) { user_projects } end end end @@ -402,14 +397,14 @@ RSpec.describe API::Projects do it_behaves_like 'projects response' do let(:filter) { { id_before: project2.id } } let(:current_user) { user } - let(:projects) { [public_project, project, project2, project3].select { |p| p.id < project2.id } } + let(:projects) { user_projects.select { |p| p.id < project2.id } } end context 'regression: empty string is ignored' do it_behaves_like 'projects response' do let(:filter) { { id_before: '' } } let(:current_user) { user } - let(:projects) { [public_project, project, project2, project3] } + let(:projects) { user_projects } end end end @@ -418,7 +413,7 @@ RSpec.describe API::Projects do it_behaves_like 'projects response' do let(:filter) { { id_before: project2.id, id_after: public_project.id } } let(:current_user) { user } - let(:projects) { [public_project, project, project2, project3].select { |p| p.id < project2.id && p.id > public_project.id } } + let(:projects) { user_projects.select { |p| p.id < project2.id && p.id > public_project.id } } end end @@ -481,7 +476,7 @@ RSpec.describe API::Projects do expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers expect(json_response).to be_an Array - expect(json_response.first['id']).to eq(project3.id) + expect(json_response.map { |p| p['id'] }).to eq(user_projects.map(&:id).sort.reverse) end end @@ -501,7 +496,6 @@ RSpec.describe API::Projects do let(:public_project) { create(:project, :public) } before do - project_member user3.update!(starred_projects: [project, project2, project3, public_project]) end @@ -642,7 +636,6 @@ RSpec.describe API::Projects do context 'non-admin user' do let(:current_user) { user } - let(:projects) { [public_project, project, project2, project3] } it 'returns projects ordered normally' do get api('/projects', current_user), params: { order_by: order_by } @@ -650,7 +643,7 @@ RSpec.describe API::Projects do expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers expect(json_response).to be_an Array - expect(json_response.map { |project| project['id'] }).to eq(projects.map(&:id).reverse) + expect(json_response.map { |project| project['id'] }).to eq(user_projects.map(&:id).sort.reverse) end end end @@ -686,7 +679,8 @@ RSpec.describe API::Projects do context 'with keyset pagination' do let(:current_user) { user } - let(:projects) { [public_project, project, project2, project3] } + let(:first_project_id) { user_projects.map(&:id).min } + let(:last_project_id) { user_projects.map(&:id).max } context 'headers and records' do let(:params) { { pagination: 'keyset', order_by: :id, sort: :asc, per_page: 1 } } @@ -696,11 +690,11 @@ RSpec.describe API::Projects do expect(response.header).to include('Links') expect(response.header['Links']).to include('pagination=keyset') - expect(response.header['Links']).to include("id_after=#{public_project.id}") + expect(response.header['Links']).to include("id_after=#{first_project_id}") expect(response.header).to include('Link') expect(response.header['Link']).to include('pagination=keyset') - expect(response.header['Link']).to include("id_after=#{public_project.id}") + expect(response.header['Link']).to include("id_after=#{first_project_id}") end it 'contains only the first project with per_page = 1' do @@ -708,7 +702,7 @@ RSpec.describe API::Projects do expect(response).to have_gitlab_http_status(:ok) expect(json_response).to be_an Array - expect(json_response.map { |p| p['id'] }).to contain_exactly(public_project.id) + expect(json_response.map { |p| p['id'] }).to contain_exactly(first_project_id) end it 'still includes a link if the end has reached and there is no more data after this page' do @@ -752,11 +746,11 @@ RSpec.describe API::Projects do expect(response.header).to include('Links') expect(response.header['Links']).to include('pagination=keyset') - expect(response.header['Links']).to include("id_before=#{project3.id}") + expect(response.header['Links']).to include("id_before=#{last_project_id}") expect(response.header).to include('Link') expect(response.header['Link']).to include('pagination=keyset') - expect(response.header['Link']).to include("id_before=#{project3.id}") + expect(response.header['Link']).to include("id_before=#{last_project_id}") end it 'contains only the last project with per_page = 1' do @@ -764,7 +758,7 @@ RSpec.describe API::Projects do expect(response).to have_gitlab_http_status(:ok) expect(json_response).to be_an Array - expect(json_response.map { |p| p['id'] }).to contain_exactly(project3.id) + expect(json_response.map { |p| p['id'] }).to contain_exactly(last_project_id) end end @@ -793,7 +787,7 @@ RSpec.describe API::Projects do ids += Gitlab::Json.parse(response.body).map { |p| p['id'] } end - expect(ids).to contain_exactly(*projects.map(&:id)) + expect(ids).to contain_exactly(*user_projects.map(&:id)) end end end @@ -814,7 +808,7 @@ RSpec.describe API::Projects do .to change { Project.count }.by(1) expect(response).to have_gitlab_http_status(:created) - project = Project.first + project = Project.last expect(project.name).to eq('Foo Project') expect(project.path).to eq('foo-project') @@ -825,7 +819,7 @@ RSpec.describe API::Projects do .to change { Project.count }.by(1) expect(response).to have_gitlab_http_status(:created) - project = Project.first + project = Project.last expect(project.name).to eq('foo_project') expect(project.path).to eq('foo_project') @@ -836,7 +830,7 @@ RSpec.describe API::Projects do .to change { Project.count }.by(1) expect(response).to have_gitlab_http_status(:created) - project = Project.first + project = Project.last expect(project.name).to eq('Foo Project') expect(project.path).to eq('path-project-Foo') @@ -1985,7 +1979,8 @@ RSpec.describe API::Projects do context 'when authenticated' do context 'valid request' do it_behaves_like 'project users response' do - let(:current_user) { user } + let(:project) { project4 } + let(:current_user) { user4 } end end @@ -2011,8 +2006,8 @@ RSpec.describe API::Projects do get api("/projects/#{project.id}/users?skip_users=#{user.id}", user) expect(response).to have_gitlab_http_status(:ok) - expect(json_response.size).to eq(1) - expect(json_response[0]['id']).to eq(other_user.id) + expect(json_response.size).to eq(2) + expect(json_response.map { |m| m['id'] }).not_to include(user.id) end end end |