spec/requests/api/conan/v2/project_packages_spec.rb (475 lines of code) (raw):
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe API::Conan::V2::ProjectPackages, feature_category: :package_registry do
include_context 'with conan api setup'
let_it_be_with_reload(:package) { create(:conan_package, project: project) }
let(:project_id) { project.id }
let(:url) { "/projects/#{project_id}/packages/conan/v2/conans/#{url_suffix}" }
shared_examples 'conan package revisions feature flag check' do
before do
stub_feature_flags(conan_package_revisions_support: false)
end
it_behaves_like 'returning response status with message', status: :not_found,
message: "404 'conan_package_revisions_support' feature flag is disabled Not Found"
end
shared_examples 'package without recipe_revision returns revision not found' do
let_it_be(:package) { create(:conan_package, project: project, without_revisions: true) }
it_behaves_like 'returning response status with message', status: :not_found,
message: '404 Revision Not Found'
end
shared_examples 'triggers an internal event' do |event:|
it 'triggers an internal event' do
expect { request }
.to trigger_internal_events(event)
.with(user: user, project: project, property: 'user', label: 'conan', category: 'InternalEventTracking')
end
end
shared_examples 'recipe revision deletion' do
it_behaves_like 'triggers an internal event', event: 'delete_recipe_revision_from_registry'
it 'deletes the package with specific revision' do
request
expect(response).to have_gitlab_http_status(:no_content)
expect(package.conan_recipe_revisions).to match_array([aditional_recipe_revision])
expect(package.package_files.where(id: revision_package_files_ids)).to all(be_pending_destruction)
end
context 'with only one revision' do
let_it_be_with_reload(:package) { create(:conan_package, project: project) }
let_it_be(:recipe_revision) { package.conan_recipe_revisions.first.revision }
it_behaves_like 'triggers an internal event', event: 'delete_package_from_registry'
it_behaves_like 'returning response status', :no_content
it { expect { request }.to change { ::Packages::Package.pending_destruction.count }.by(1) }
end
end
shared_examples 'get file list' do |expected_file_list, not_found_err:|
subject(:api_request) { get api(url), headers: headers }
it_behaves_like 'enforcing read_packages job token policy' do
subject(:request) { api_request }
end
it_behaves_like 'conan FIPS mode'
it_behaves_like 'accept get request on private project with access to package registry for everyone'
it { is_expected.to have_request_urgency(:low) }
it 'returns the file list' do
api_request
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to eq(expected_file_list)
end
context 'when the recipe revision files are not found' do
# This is a non-existent revision
let(:recipe_revision) { 'da39a3ee5e6b4b0d3255bfef95601890afd80709' }
it_behaves_like 'returning response status with message', status: :not_found,
message: not_found_err
end
context 'when the package is not found' do
# This is a non-existent revision
let(:recipe_path) { 'test/9.0.0/namespace1+project-1/stable' }
it_behaves_like 'returning response status with message', status: :not_found, message: '404 Package Not Found'
end
context 'when the limit is reached' do
before do
stub_const("#{described_class}::MAX_FILES_COUNT", 1)
end
it 'limits the number of files to MAX_FILES_COUNT' do
api_request
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['files'].size).to eq(1)
end
end
end
shared_examples 'returns 404 when resource does not exist' do
it 'returns 404' do
request
expect(response).to have_gitlab_http_status(:not_found)
expect(json_response['message']).to eq('404 Revision Not Found')
end
end
shared_examples 'returns empty package revisions list when resource does not exist' do
it 'returns empty package revisions list' do
subject
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['packageReference']).to eq(package_reference)
expect(json_response['revisions']).to be_empty
end
end
describe 'GET /api/v4/projects/:id/packages/conan/v2/users/authenticate' do
let(:url) { "/projects/#{project.id}/packages/conan/v2/users/authenticate" }
it_behaves_like 'conan authenticate endpoint'
end
describe 'GET /api/v4/projects/:id/packages/conan/v2/users/check_credentials' do
let(:url) { "/projects/#{project.id}/packages/conan/v2/users/check_credentials" }
it_behaves_like 'conan check_credentials endpoint'
it_behaves_like 'conan package revisions feature flag check' do
subject { get api(url), headers: headers }
end
end
describe 'GET /api/v4/projects/:id/packages/conan/v2/conans/search' do
let(:url_suffix) { "search" }
let(:params) { { q: package.conan_recipe } }
subject { get api(url), params: params }
it_behaves_like 'conan search endpoint'
it_behaves_like 'conan FIPS mode'
it_behaves_like 'conan search endpoint with access to package registry for everyone'
it_behaves_like 'conan package revisions feature flag check'
end
describe 'GET /api/v4/projects/:id/packages/conan/v2/conans/:package_name/:package_version/:package_username/' \
':package_channel/revisions/:recipe_revision/files' do
let_it_be(:additional_recipe_revision) { create(:conan_recipe_revision, package: package) }
let_it_be(:additional_recipe_files) do
create(:conan_package_file, :conan_recipe_file, package: package,
conan_recipe_revision: additional_recipe_revision, file_name: 'additional_conanfile.py')
end
let(:recipe_revision) { package.conan_recipe_revisions.first.revision }
let(:recipe_path) { package.conan_recipe_path }
let(:url_suffix) { "#{recipe_path}/revisions/#{recipe_revision}/files" }
let(:url) { "/projects/#{project_id}/packages/conan/v2/conans/#{url_suffix}" }
it_behaves_like 'get file list',
{ 'files' => { 'conanfile.py' => {}, 'conanmanifest.txt' => {} } },
not_found_err: '404 Recipe files Not Found'
end
describe 'GET /api/v4/projects/:id/packages/conan/v2/conans/:package_name/:package_version/:package_username/' \
':package_channel/revisions/:recipe_revision/files/:file_name' do
include_context 'for conan file download endpoints'
let(:file_name) { recipe_file.file_name }
let(:recipe_revision) { recipe_file_metadata.recipe_revision_value }
let(:url_suffix) { "#{recipe_path}/revisions/#{recipe_revision}/files/#{file_name}" }
subject(:request) { get api(url), headers: headers }
it_behaves_like 'conan package revisions feature flag check'
it_behaves_like 'packages feature check'
it_behaves_like 'recipe file download endpoint'
it_behaves_like 'accept get request on private project with access to package registry for everyone'
it_behaves_like 'project not found by project id'
it_behaves_like 'enforcing job token policies', :read_packages,
allow_public_access_for_enabled_project_features: :package_registry do
let(:headers) { job_basic_auth_header(target_job) }
end
describe 'parameter validation for recipe file endpoints' do
using RSpec::Parameterized::TableSyntax
let(:url_suffix) { "#{url_recipe_path}/revisions/#{url_recipe_revision}/files/#{url_file_name}" }
# rubocop:disable Layout/LineLength -- Avoid formatting to keep one-line table syntax
where(:error, :url_recipe_path, :url_recipe_revision, :url_file_name) do
/package_name/ | 'pac$kage-1/1.0.0/namespace1+project-1/stable' | ref(:recipe_revision) | ref(:file_name)
/package_version/ | 'package-1/1.0.$/namespace1+project-1/stable' | ref(:recipe_revision) | ref(:file_name)
/package_username/ | 'package-1/1.0.0/name$pace1+project-1/stable' | ref(:recipe_revision) | ref(:file_name)
/package_channel/ | 'package-1/1.0.0/namespace1+project-1/$table' | ref(:recipe_revision) | ref(:file_name)
/recipe_revision/ | ref(:recipe_path) | 'invalid_revi$ion' | ref(:file_name)
/recipe_revision/ | ref(:recipe_path) | Packages::Conan::FileMetadatum::DEFAULT_REVISION | ref(:file_name)
/file_name/ | ref(:recipe_path) | ref(:recipe_revision) | 'invalid_file.txt'
end
# rubocop:enable Layout/LineLength
with_them do
it_behaves_like 'returning response status with error', status: :bad_request, error: params[:error]
end
end
end
describe 'GET /api/v4/projects/:id/packages/conan/v2/conans/:package_name/:package_version/:package_username/' \
':package_channel/revisions/:recipe_revision/packages/:conan_package_reference/revisions/:package_revision/' \
'files/:file_name' do
include_context 'for conan file download endpoints'
let(:file_name) { package_file.file_name }
let(:recipe_revision) { package_file_metadata.recipe_revision_value }
let(:package_revision) { package_file_metadata.package_revision_value }
let(:conan_package_reference) { package_file_metadata.package_reference_value }
let(:url_suffix) do
"#{recipe_path}/revisions/#{recipe_revision}/packages/#{conan_package_reference}/revisions/#{package_revision}/" \
"files/#{file_name}"
end
subject(:request) { get api(url), headers: headers }
it_behaves_like 'conan package revisions feature flag check'
it_behaves_like 'packages feature check'
it_behaves_like 'package file download endpoint'
it_behaves_like 'accept get request on private project with access to package registry for everyone'
it_behaves_like 'project not found by project id'
it_behaves_like 'enforcing job token policies', :read_packages,
allow_public_access_for_enabled_project_features: :package_registry do
let(:headers) { job_basic_auth_header(target_job) }
end
describe 'parameter validation for package file endpoints' do
using RSpec::Parameterized::TableSyntax
let(:url_suffix) do
"#{recipe_path}/revisions/#{recipe_revision}/packages/#{url_package_reference}/revisions/" \
"#{url_package_revision}/files/#{url_file_name}"
end
# rubocop:disable Layout/LineLength -- Avoid formatting to keep one-line table syntax
where(:error, :url_package_reference, :url_package_revision, :url_file_name) do
/conan_package_reference/ | 'invalid_package_reference$' | ref(:package_revision) | ref(:file_name)
/package_revision/ | ref(:conan_package_reference) | 'invalid_package_revi$ion' | ref(:file_name)
/package_revision/ | ref(:conan_package_reference) | Packages::Conan::FileMetadatum::DEFAULT_REVISION | ref(:file_name)
/file_name/ | ref(:conan_package_reference) | ref(:package_revision) | 'invalid_file.txt'
end
# rubocop:enable Layout/LineLength
with_them do
it_behaves_like 'returning response status with error', status: :bad_request, error: params[:error]
end
end
end
context 'with file upload endpoints' do
include_context 'for conan file upload endpoints'
let(:file_name) { 'conanfile.py' }
let(:recipe_revision) { OpenSSL::Digest.hexdigest('MD5', 'valid_recipe_revision') }
let(:conan_package_reference) { OpenSSL::Digest.hexdigest('SHA1', 'valid_package_reference') }
let(:package_revision) { OpenSSL::Digest.hexdigest('MD5', 'valid_package_revision') }
describe 'PUT /api/v4/projects/:id/packages/conan/v2/conans/:package_name/:package_version/:package_username/' \
':package_channel/revisions/:recipe_revision/files/:file_name' do
let(:url_suffix) { "#{recipe_path}/revisions/#{recipe_revision}/files/#{file_name}" }
subject(:request) { put api(url), headers: headers_with_token }
it_behaves_like 'conan package revisions feature flag check'
it_behaves_like 'packages feature check'
it_behaves_like 'workhorse recipe file upload endpoint', revision: true
end
describe 'PUT /api/v4/projects/:id/packages/conan/v2/conans/:package_name/:package_version/:package_username/' \
':package_channel/revisions/:recipe_revision/files/:file_name/authorize' do
let(:url_suffix) { "#{recipe_path}/revisions/#{recipe_revision}/files/#{file_name}/authorize" }
subject(:request) do
put api(url),
headers: headers_with_token
end
it_behaves_like 'conan package revisions feature flag check'
it_behaves_like 'packages feature check'
it_behaves_like 'workhorse authorize endpoint'
end
describe 'PUT /api/v4/projects/:id/packages/conan/v2/conans/:package_name/:package_version/:package_username/' \
':package_channel/revisions/:recipe_revision/packages/:conan_package_reference/revisions/:package_revision/' \
'files/:file_name' do
let(:file_name) { 'conaninfo.txt' }
let(:url_suffix) do
"#{recipe_path}/revisions/#{recipe_revision}/packages/#{conan_package_reference}/revisions/" \
"#{package_revision}/files/#{file_name}"
end
subject(:request) { put api(url), headers: headers_with_token }
it_behaves_like 'conan package revisions feature flag check'
it_behaves_like 'packages feature check'
it_behaves_like 'workhorse package file upload endpoint', revision: true
end
describe 'PUT /api/v4/projects/:id/packages/conan/v2/conans/:package_name/:package_version/:package_username/' \
':package_channel/revisions/:recipe_revision/packages/:conan_package_reference/revisions/:package_revision/' \
'files/:file_name/authorize' do
let(:file_name) { 'conaninfo.txt' }
let(:url_suffix) do
"#{recipe_path}/revisions/#{recipe_revision}/packages/#{conan_package_reference}/revisions/" \
"#{package_revision}/files/#{file_name}/authorize"
end
subject(:request) { put api(url), headers: headers_with_token }
it_behaves_like 'conan package revisions feature flag check'
it_behaves_like 'packages feature check'
it_behaves_like 'workhorse authorize endpoint'
end
end
describe 'GET /api/v4/projects/:id/packages/conan/v2/conans/:package_name/:package_version/:package_username' \
'/:package_channel/latest' do
let(:recipe_path) { package.conan_recipe_path }
let(:url_suffix) { "#{recipe_path}/latest" }
subject(:request) { get api(url), headers: headers }
it 'returns the latest revision' do
request
expect(response).to have_gitlab_http_status(:ok)
recipe_revision = package.conan_recipe_revisions.first
expect(json_response['revision']).to eq(recipe_revision.revision)
expect(json_response['time']).to eq(recipe_revision.created_at.iso8601(3))
end
context 'when package has no revisions' do
let_it_be(:package) { create(:conan_package, project: project, without_revisions: true) }
it 'returns 404' do
request
expect(response).to have_gitlab_http_status(:not_found)
expect(json_response['message']).to eq('404 Revision Not Found')
end
end
it_behaves_like 'conan package revisions feature flag check'
it_behaves_like 'enforcing read_packages job token policy'
it_behaves_like 'accept get request on private project with access to package registry for everyone'
it_behaves_like 'conan FIPS mode'
it_behaves_like 'package not found'
it_behaves_like 'project not found by project id'
it_behaves_like 'package without recipe_revision returns revision not found'
end
describe 'DELETE /api/v4/projects/:id/packages/conan/v2/conans/:package_name/package_version/:package_username/' \
':package_channel/revisions/:recipe_revision' do
let_it_be_with_reload(:package) { create(:conan_package, project: project) }
let_it_be(:recipe_revision) { package.conan_recipe_revisions.first.revision }
let_it_be(:revision_package_files_ids) { package.conan_recipe_revisions.first.package_files.ids }
let_it_be(:additional_recipe_revision) { create(:conan_recipe_revision, package: package) }
let(:recipe_path) { package.conan_recipe_path }
let(:url_suffix) { "#{recipe_path}/revisions/#{recipe_revision}" }
subject(:request) { delete api(url), headers: headers }
it_behaves_like 'conan package revisions feature flag check'
it_behaves_like 'packages feature check'
it_behaves_like 'conan FIPS mode'
it_behaves_like 'rejects invalid recipe'
it_behaves_like 'project not found by project id'
it_behaves_like 'returning response status with message', status: :forbidden,
message: '403 Forbidden'
context 'with delete permissions' do
before do
project.add_maintainer(user)
end
it_behaves_like 'triggers an internal event', event: 'delete_recipe_revision_from_registry'
it_behaves_like 'package not found'
it_behaves_like 'package without recipe_revision returns revision not found'
it_behaves_like 'handling empty values for username and channel', success_status: :ok
it 'deletes the package with specific revision' do
expect { request }.to change { package.conan_recipe_revisions.count }.by(-1)
expect(response).to have_gitlab_http_status(:ok)
expect(package.conan_recipe_revisions).to match_array([additional_recipe_revision])
expect(package.package_files.where(id: revision_package_files_ids)).to all(be_pending_destruction)
end
context 'with only one revision' do
let_it_be_with_reload(:package) { create(:conan_package, project: project) }
let_it_be(:recipe_revision) { package.conan_recipe_revisions.first.revision }
it_behaves_like 'triggers an internal event', event: 'delete_package_from_registry'
it_behaves_like 'returning response status', :ok
it { expect { request }.to change { ::Packages::Package.pending_destruction.count }.by(1) }
end
context 'with non-existing recipe revision' do
let_it_be(:recipe_revision) { OpenSSL::Digest.hexdigest('MD5', 'non_existing') }
it_behaves_like 'returning response status with message', status: :not_found,
message: '404 Revision Not Found'
end
context 'when the number of files to delete is greater than the maximum allowed' do
before do
stub_const("#{described_class}::MAX_FILES_COUNT", 1)
end
it_behaves_like 'returning response status with message', status: :unprocessable_entity,
message: "Cannot delete more than 1 files"
end
end
end
describe 'GET /api/v4/projects/:id/packages/conan/v2/conans/:package_name/:package_version/:package_username' \
'/:package_channel/revisions' do
let(:recipe_path) { package.conan_recipe_path }
let(:url_suffix) { "#{recipe_path}/revisions" }
let_it_be(:revision1) { package.conan_recipe_revisions.first }
let_it_be(:revision2) { create(:conan_recipe_revision, package: package) }
subject(:request) { get api(url), headers: headers }
it 'returns the reference and a list of revisions in descending order' do
request
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['reference']).to eq(package.conan_recipe)
expect(json_response['revisions']).to eq([
{
'revision' => revision2.revision,
'time' => revision2.created_at.iso8601(3)
},
{
'revision' => revision1.revision,
'time' => revision1.created_at.iso8601(3)
}
])
end
it_behaves_like 'conan package revisions feature flag check'
it_behaves_like 'packages feature check'
it_behaves_like 'enforcing read_packages job token policy'
it_behaves_like 'accept get request on private project with access to package registry for everyone'
it_behaves_like 'conan FIPS mode'
it_behaves_like 'package not found'
it_behaves_like 'project not found by project id'
end
describe 'GET /api/v4/projects/:id/packages/conan/v2/conans/:package_name/:package_version/:package_username' \
'/:package_channel/revisions/:recipe_revision/packages/:conan_package_reference/revisions' do
let_it_be(:conan_package_reference) { package.conan_package_references.first.reference }
let_it_be(:revision1) { package.conan_package_revisions.first }
let_it_be(:revision2) { create(:conan_package_revision, package: package) }
let(:recipe_path) { package.conan_recipe_path }
let(:recipe_revision) { package.conan_recipe_revisions.first.revision }
let(:package_reference) { "#{package.conan_recipe}##{recipe_revision}:#{conan_package_reference}" }
let(:url_suffix) { "#{recipe_path}/revisions/#{recipe_revision}/packages/#{conan_package_reference}/revisions" }
subject(:api_request) { get api(url), headers: headers }
it 'returns the reference and a list of revisions in descending order' do
api_request
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['packageReference']).to eq(package_reference)
expect(json_response['revisions']).to eq([
{
'revision' => revision2.revision,
'time' => revision2.created_at.iso8601(3)
},
{
'revision' => revision1.revision,
'time' => revision1.created_at.iso8601(3)
}
])
end
it { is_expected.to have_request_urgency(:low) }
context 'when recipe revision does not exist' do
let(:recipe_revision) { OpenSSL::Digest.hexdigest('MD5', 'nonexistent-revision') }
it_behaves_like 'returns empty package revisions list when resource does not exist'
end
context 'when package reference does not exist' do
let(:conan_package_reference) { OpenSSL::Digest.hexdigest('SHA1', 'nonexistent-reference') }
it_behaves_like 'returns empty package revisions list when resource does not exist'
end
context 'when the max revisions count is reached' do
before do
stub_const("#{described_class}::MAX_PACKAGE_REVISIONS_COUNT", 1)
end
it 'limits the number of files to MAX_PACKAGE_REVISIONS_COUNT' do
api_request
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['revisions'].size).to eq(1)
end
end
it_behaves_like 'enforcing read_packages job token policy' do
subject(:request) { api_request }
end
it_behaves_like 'conan package revisions feature flag check'
it_behaves_like 'packages feature check'
it_behaves_like 'accept get request on private project with access to package registry for everyone'
it_behaves_like 'conan FIPS mode'
it_behaves_like 'package not found'
it_behaves_like 'project not found by project id'
end
describe 'GET /api/v4/projects/:id/packages/conan/v2/conans/:package_name/:package_version/:package_username' \
'/:package_channel/revisions/:recipe_revision/packages/:conan_package_reference/latest' do
let(:recipe_path) { package.conan_recipe_path }
let(:recipe_revision) { package.conan_recipe_revisions.first.revision }
let(:conan_package_reference) { package.conan_package_references.first.reference }
let(:url_suffix) { "#{recipe_path}/revisions/#{recipe_revision}/packages/#{conan_package_reference}/latest" }
subject(:request) { get api(url), headers: headers }
it 'returns the latest revision' do
request
expect(response).to have_gitlab_http_status(:ok)
package_revision = package.conan_package_revisions.first
expect(json_response['revision']).to eq(package_revision.revision)
expect(json_response['time']).to eq(package_revision.created_at.iso8601(3))
end
context 'when recipe revision does not exist' do
let(:recipe_revision) { OpenSSL::Digest.hexdigest('MD5', 'nonexistent-revision') }
it_behaves_like 'returns 404 when resource does not exist'
end
context 'when package reference does not exist' do
let(:conan_package_reference) { OpenSSL::Digest.hexdigest('SHA1', 'nonexistent-reference') }
it_behaves_like 'returns 404 when resource does not exist'
end
it_behaves_like 'enforcing read_packages job token policy'
it_behaves_like 'accept get request on private project with access to package registry for everyone'
it_behaves_like 'conan FIPS mode'
it_behaves_like 'package not found'
it_behaves_like 'project not found by project id'
end
describe 'GET /api/v4/projects/:id/packages/conan/v2/conans/:package_name/:package_version/:package_username/' \
':package_channel/revisions/:recipe_revision/packages/:conan_package_reference/revisions/:package_revision/' \
'files' do
let_it_be(:additional_package_reference) { create(:conan_package_reference, package: package) }
let_it_be(:additional_package_revision) { create(:conan_package_revision, package: package) }
let_it_be(:additional_package_file_1) do
create(:conan_package_file, :conan_package, package: package,
conan_package_revision: additional_package_revision, file_name: 'additional_conan_package_1.tgz')
end
let_it_be(:additional_package_file_2) do
create(:conan_package_file, :conan_package, package: package,
conan_package_reference: additional_package_reference, file_name: 'additional_conan_package_2.tgz')
end
let(:recipe_revision) { package.conan_recipe_revisions.first.revision }
let(:recipe_path) { package.conan_recipe_path }
let(:package_revision) { package.conan_package_revisions.first.revision }
let(:url) { "/projects/#{project_id}/packages/conan/v2/conans/#{url_suffix}" }
let(:url_suffix) do
"#{recipe_path}/revisions/#{recipe_revision}/packages/#{conan_package_reference}/revisions/" \
"#{package_revision}/files"
end
it_behaves_like 'get file list',
{ 'files' => { 'conan_package.tgz' => {}, 'conaninfo.txt' => {}, 'conanmanifest.txt' => {} } },
not_found_err: '404 Package files Not Found'
end
describe 'GET /api/v4/projects/:id/packages/conan/v2/conans/:package_name/:package_version/:package_username' \
'/:package_channel/search' do
let(:recipe_path) { package.conan_recipe_path }
let(:url_suffix) { "#{recipe_path}/search" }
subject(:request) { get api(url), headers: headers }
it_behaves_like 'GET package references metadata endpoint'
it_behaves_like 'accept get request on private project with access to package registry for everyone'
it_behaves_like 'project not found by project id'
it_behaves_like 'conan package revisions feature flag check'
end
end