Commit 16775e71 authored by Steve Abrams's avatar Steve Abrams Committed by Andreas Brandl

Conan snapshot and manifest API endpoints

Conan endpoints for returning 'snapshot' and 'manifest'
data to the conan CLI. This follows the v1 conan API.

PackagesConanFileMetadata is created to store metadata for
conan package files.
parent 16020d79
# frozen_string_literal: true
class CreatePackagesConanFileMetadata < ActiveRecord::Migration[5.2]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def change
create_table :packages_conan_file_metadata do |t|
t.references :package_file, index: { unique: true }, null: false, foreign_key: { to_table: :packages_package_files, on_delete: :cascade }, type: :bigint
t.timestamps_with_timezone
t.string "recipe_revision", null: false, default: "0", limit: 255
t.string "package_revision", limit: 255
t.string "conan_package_reference", limit: 255
t.integer "conan_file_type", limit: 2, null: false
end
end
end
# frozen_string_literal: true
class CreatePackagesConanMetadata < ActiveRecord::Migration[5.2]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def change
create_table :packages_conan_metadata do |t|
t.references :package, index: { unique: true }, null: false, foreign_key: { to_table: :packages_packages, on_delete: :cascade }, type: :bigint
t.timestamps_with_timezone
t.string "package_username", null: false, limit: 255
t.string "package_channel", null: false, limit: 255
end
end
end
......@@ -2653,6 +2653,26 @@ ActiveRecord::Schema.define(version: 2019_10_29_191901) do
t.index ["project_id", "token_encrypted"], name: "index_feature_flags_clients_on_project_id_and_token_encrypted", unique: true
end
create_table "packages_conan_file_metadata", force: :cascade do |t|
t.bigint "package_file_id", null: false
t.datetime_with_timezone "created_at", null: false
t.datetime_with_timezone "updated_at", null: false
t.string "recipe_revision", limit: 255, default: "0", null: false
t.string "package_revision", limit: 255
t.string "conan_package_reference", limit: 255
t.integer "conan_file_type", limit: 2, null: false
t.index ["package_file_id"], name: "index_packages_conan_file_metadata_on_package_file_id", unique: true
end
create_table "packages_conan_metadata", force: :cascade do |t|
t.bigint "package_id", null: false
t.datetime_with_timezone "created_at", null: false
t.datetime_with_timezone "updated_at", null: false
t.string "package_username", limit: 255, null: false
t.string "package_channel", limit: 255, null: false
t.index ["package_id"], name: "index_packages_conan_metadata_on_package_id", unique: true
end
create_table "packages_maven_metadata", force: :cascade do |t|
t.bigint "package_id", null: false
t.datetime_with_timezone "created_at", null: false
......@@ -4326,6 +4346,8 @@ ActiveRecord::Schema.define(version: 2019_10_29_191901) do
add_foreign_key "operations_feature_flag_scopes", "operations_feature_flags", column: "feature_flag_id", on_delete: :cascade
add_foreign_key "operations_feature_flags", "projects", on_delete: :cascade
add_foreign_key "operations_feature_flags_clients", "projects", on_delete: :cascade
add_foreign_key "packages_conan_file_metadata", "packages_package_files", column: "package_file_id", on_delete: :cascade
add_foreign_key "packages_conan_metadata", "packages_packages", column: "package_id", on_delete: :cascade
add_foreign_key "packages_maven_metadata", "packages_packages", column: "package_id", name: "fk_be88aed360", on_delete: :cascade
add_foreign_key "packages_package_files", "packages_packages", column: "package_id", name: "fk_86f0f182f8", on_delete: :cascade
add_foreign_key "packages_package_metadata", "packages_packages", column: "package_id", on_delete: :cascade
......
......@@ -2,15 +2,15 @@
module Packages
class ConanPackageFinder
attr_reader :query, :current_user
attr_reader :current_user, :query
def initialize(query, current_user)
@query = query
def initialize(current_user, params)
@current_user = current_user
@query = params[:query]
end
def execute
packages_for_current_user.with_name_like(query).order_name_asc
packages_for_current_user.with_name_like(query).order_name_asc if query
end
private
......
# frozen_string_literal: true
class Packages::ConanFileMetadatum < ApplicationRecord
belongs_to :package_file, inverse_of: :conan_file_metadatum
validates :package_file, presence: true
validates :recipe_revision,
presence: true,
format: { with: Gitlab::Regex.conan_revision_regex }
validates :package_revision, absence: true, if: :recipe_file?
validates :package_revision, format: { with: Gitlab::Regex.conan_revision_regex }, if: :package_file?
validates :conan_package_reference, absence: true, if: :recipe_file?
validates :conan_package_reference, format: { with: Gitlab::Regex.conan_package_reference_regex }, if: :package_file?
enum conan_file_type: { recipe_file: 1, package_file: 2 }
RECIPE_FILES = %w[conanfile.py conanmanifest.txt].freeze
PACKAGE_FILES = %w[conaninfo.txt conanmanifest.txt conan_package.tgz].freeze
end
# frozen_string_literal: true
class Packages::ConanMetadatum < ApplicationRecord
belongs_to :package, inverse_of: :conan_metadatum
validates :package, presence: true
validates :package_username,
presence: true,
format: { with: Gitlab::Regex.conan_recipe_component_regex }
validates :package_channel,
presence: true,
format: { with: Gitlab::Regex.conan_recipe_component_regex }
def recipe
"#{package.name}/#{package.version}@#{package_username}/#{package_channel}"
end
def recipe_path
recipe.tr('@', '/')
end
def self.package_username_from(full_path:)
full_path.tr('/', '+')
end
def self.full_path_from(package_username:)
package_username.tr('+', '/')
end
end
......@@ -5,10 +5,14 @@ class Packages::Package < ApplicationRecord
belongs_to :project
# package_files must be destroyed by ruby code in order to properly remove carrierwave uploads and update project statistics
has_many :package_files, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_one :conan_metadatum, inverse_of: :package
has_one :maven_metadatum, inverse_of: :package
accepts_nested_attributes_for :conan_metadatum
accepts_nested_attributes_for :maven_metadatum
delegate :recipe, :recipe_path, to: :conan_metadatum, prefix: :conan
validates :project, presence: true
validates :name,
......
......@@ -8,12 +8,17 @@ class Packages::PackageFile < ApplicationRecord
belongs_to :package
has_one :conan_file_metadatum, inverse_of: :package_file
accepts_nested_attributes_for :conan_file_metadatum
validates :package, presence: true
validates :file, presence: true
validates :file_name, presence: true
scope :recent, -> { order(id: :desc) }
scope :with_files_stored_locally, -> { where(file_store: ::Packages::PackageFileUploader::Store::LOCAL) }
scope :with_conan_file_metadata, -> { includes(:conan_file_metadatum) }
mount_uploader :file, Packages::PackageFileUploader
......
# frozen_string_literal: true
class ConanPackagePresenter
include API::Helpers::RelatedResourcesHelpers
include Gitlab::Utils::StrongMemoize
def initialize(recipe, user, project)
@recipe = recipe
@user = user
@project = project
end
def recipe_urls
map_package_files do |package_file|
build_recipe_file_url(package_file) if package_file.conan_file_metadatum.recipe_file?
end
end
def recipe_snapshot
map_package_files do |package_file|
package_file.file_md5 if package_file.conan_file_metadatum.recipe_file?
end
end
def package_urls
map_package_files do |package_file|
build_package_file_url(package_file) if package_file.conan_file_metadatum.package_file?
end
end
def package_snapshot
map_package_files do |package_file|
package_file.file_md5 if package_file.conan_file_metadatum.package_file?
end
end
private
def build_recipe_file_url(package_file)
expose_url(
api_v4_packages_conan_v1_files_export_path(
package_name: package.name,
package_version: package.version,
package_username: package.conan_metadatum.package_username,
package_channel: package.conan_metadatum.package_channel,
recipe_revision: package_file.conan_file_metadatum.recipe_revision,
file_name: package_file.file_name
)
)
end
def build_package_file_url(package_file)
expose_url(
api_v4_packages_conan_v1_files_package_path(
package_name: package.name,
package_version: package.version,
package_username: package.conan_metadatum.package_username,
package_channel: package.conan_metadatum.package_channel,
recipe_revision: package_file.conan_file_metadatum.recipe_revision,
conan_package_reference: package_file.conan_file_metadatum.conan_package_reference,
package_revision: package_file.conan_file_metadatum.package_revision,
file_name: package_file.file_name
)
)
end
def map_package_files
package_files.to_a.map do |package_file|
[package_file.file_name, yield(package_file)]
end.to_h.compact
end
def package_files
return unless package
@package_files ||= package.package_files.with_conan_file_metadata
end
def package
strong_memoize(:package) do
name, version = @recipe.split('@')[0].split('/')
@project.packages.with_name(name).with_version(version).order_created.last
end
end
end
......@@ -23,6 +23,8 @@ module Packages
def search_results
return [] if wildcard_query?
return search_for_single_package(sanitized_query) if params[:query].include?(RECIPE_SEPARATOR)
search_packages(build_query)
end
......@@ -31,11 +33,9 @@ module Packages
end
def build_query
sanitized_query = sanitize_sql_like(params[:query].delete(WILDCARD))
return "#{sanitized_query}%" if params[:query].end_with?(WILDCARD)
return sanitized_query if sanitized_query.include?(RECIPE_SEPARATOR)
"#{sanitized_query}/%"
sanitized_query
end
def feature_available?
......@@ -43,7 +43,21 @@ module Packages
end
def search_packages(query)
Packages::ConanPackageFinder.new(query, current_user).execute.pluck_names
Packages::ConanPackageFinder.new(current_user, query: query).execute.map(&:conan_recipe)
end
def search_for_single_package(query)
name, version, username, _ = query.split(/[@\/]/)
full_path = Packages::ConanMetadatum.full_path_from(package_username: username)
project = Project.find_by_full_path(full_path)
return unless current_user.can?(:read_package, project)
result = project.packages.with_name(name).with_version(version).order_created.last
[result&.conan_recipe].compact
end
def sanitized_query
@sanitized_query ||= sanitize_sql_like(params[:query].delete(WILDCARD))
end
end
end
......
......@@ -4,36 +4,29 @@ module API
class ConanPackages < Grape::API
helpers ::API::Helpers::PackagesHelpers
PACKAGE_REQUIREMENTS = {
package_name: API::NO_SLASH_URL_PART_REGEX,
package_version: API::NO_SLASH_URL_PART_REGEX,
package_username: API::NO_SLASH_URL_PART_REGEX,
package_channel: API::NO_SLASH_URL_PART_REGEX
}.freeze
FILE_NAME_REQUIREMENTS = {
file_name: Gitlab::Regex.conan_file_name_regex
}.freeze
PACKAGE_COMPONENT_REGEX = Gitlab::Regex.conan_recipe_component_regex
before do
not_found! unless Feature.enabled?(:conan_package_registry)
require_packages_enabled!
# Personal access token will be extracted from Bearer or Basic authorization
# in the overriden find_personal_access_token helper
# in the overridden find_personal_access_token helper
authenticate!
end
namespace 'packages/conan/v1/users/' do
format :txt
desc 'Authenticate user against conan CLI' do
detail 'This feature was introduced in GitLab 12.2'
end
get 'authenticate' do
token = ::Gitlab::ConanToken.from_personal_access_token(access_token)
token.to_jwt
end
desc 'Check for valid user credentials per conan CLI' do
detail 'This feature was introduced in GitLab 12.3'
end
get 'check_credentials' do
authenticate!
:ok
end
end
namespace 'packages/conan/v1/' do
namespace 'packages/conan/v1' do
desc 'Ping the Conan API' do
detail 'This feature was introduced in GitLab 12.2'
end
......@@ -42,7 +35,7 @@ module API
end
desc 'Search for packages' do
detail 'This feature was introduced in GitLab 12.3'
detail 'This feature was introduced in GitLab 12.4'
end
params do
requires :q, type: String, desc: 'Search query'
......@@ -51,91 +44,220 @@ module API
service = ::Packages::Conan::SearchService.new(current_user, query: params[:q]).execute
service.payload
end
end
namespace 'packages/conan/v1/conans/*url_recipe' do
before do
render_api_error!("Invalid recipe", 400) unless valid_url_recipe?(params[:url_recipe])
namespace 'users' do
format :txt
desc 'Authenticate user against conan CLI' do
detail 'This feature was introduced in GitLab 12.2'
end
get 'authenticate' do
token = ::Gitlab::ConanToken.from_personal_access_token(access_token)
token.to_jwt
end
desc 'Check for valid user credentials per conan CLI' do
detail 'This feature was introduced in GitLab 12.4'
end
get 'check_credentials' do
authenticate!
:ok
end
end
params do
requires :url_recipe, type: String, desc: 'Package recipe'
requires :package_name, type: String, regexp: PACKAGE_COMPONENT_REGEX, desc: 'Package name'
requires :package_version, type: String, regexp: PACKAGE_COMPONENT_REGEX, desc: 'Package version'
requires :package_username, type: String, regexp: PACKAGE_COMPONENT_REGEX, desc: 'Package username'
requires :package_channel, type: String, regexp: PACKAGE_COMPONENT_REGEX, desc: 'Package channel'
end
namespace 'conans/:package_name/:package_version/:package_username/:package_channel', requirements: PACKAGE_REQUIREMENTS do
# Get the snapshot
#
# the snapshot is a hash of { filename: md5 hash }
# md5 hash is the has of that file. This hash is used to diff the files existing on the client
# to determine which client files need to be uploaded if no recipe exists the snapshot is empty
desc 'Package Snapshot' do
detail 'This feature was introduced in GitLab 12.5'
end
get 'packages/:conan_package_reference' do
authorize!(:read_package, project)
# Get the recipe manifest
# returns the download urls for the existing recipe in the registry
#
# the manifest is a hash of { filename: url }
# where the url is the download url for the file
desc 'Package Digest' do
detail 'This feature was introduced in GitLab 12.3'
end
get 'packages/:package_id/digest' do
render_api_error!("No recipe manifest found", 404)
end
presenter = ConanPackagePresenter.new(recipe, current_user, project)
desc 'Recipe Digest' do
detail 'This feature was introduced in GitLab 12.3'
end
get 'digest' do
render_api_error!("No recipe manifest found", 404)
end
present presenter, with: EE::API::Entities::ConanPackage::ConanPackageSnapshot
end
desc 'Recipe Snapshot' do
detail 'This feature was introduced in GitLab 12.5'
end
get do
authorize!(:read_package, project)
presenter = ConanPackagePresenter.new(recipe, current_user, project)
present presenter, with: EE::API::Entities::ConanPackage::ConanRecipeSnapshot
end
# Get the manifest
# returns the download urls for the existing recipe in the registry
#
# the manifest is a hash of { filename: url }
# where the url is the download url for the file
desc 'Package Digest' do
detail 'This feature was introduced in GitLab 12.5'
end
get 'packages/:conan_package_reference/digest' do
authorize!(:read_package, project)
presenter = ConanPackagePresenter.new(recipe, current_user, project)
# Get the upload urls
#
# request body contains { filename: filesize } where the filename is the
# name of the file the conan client is requesting to upload
#
# returns { filename: url }
# where the url is the upload url for the file that the conan client will use
desc 'Package Upload Urls' do
detail 'This feature was introduced in GitLab 12.3'
render_api_error!("No recipe manifest found", 404) if presenter.package_urls.empty?
present presenter, with: EE::API::Entities::ConanPackage::ConanPackageManifest
end
desc 'Recipe Digest' do
detail 'This feature was introduced in GitLab 12.5'
end
get 'digest' do
authorize!(:read_package, project)
presenter = ConanPackagePresenter.new(recipe, current_user, project)
render_api_error!("No recipe manifest found", 404) if presenter.recipe_urls.empty?
present presenter, with: EE::API::Entities::ConanPackage::ConanRecipeManifest
end
# Get the upload urls
#
# request body contains { filename: filesize } where the filename is the
# name of the file the conan client is requesting to upload
#
# returns { filename: url }
# where the url is the upload url for the file that the conan client will use
desc 'Package Upload Urls' do
detail 'This feature was introduced in GitLab 12.4'
end
params do
requires :conan_package_reference, type: String, desc: 'Conan package ID'
end
post 'packages/:conan_package_reference/upload_urls' do
authorize!(:read_package, project)
status 200
upload_urls = package_upload_urls(::Packages::ConanFileMetadatum::PACKAGE_FILES)
present upload_urls, with: EE::API::Entities::ConanPackage::ConanUploadUrls
end
desc 'Recipe Upload Urls' do
detail 'This feature was introduced in GitLab 12.4'
end
post 'upload_urls' do
authorize!(:read_package, project)
status 200
upload_urls = recipe_upload_urls(::Packages::ConanFileMetadatum::RECIPE_FILES)
present upload_urls, with: EE::API::Entities::ConanPackage::ConanUploadUrls
end
end
params do
requires :package_id, type: String, desc: 'Conan package ID'
end
post 'packages/:package_id/upload_urls' do
status 200
{
'conaninfo.txt': "#{base_file_url}/#{params[:url_recipe]}/-/0/package/#{params[:package_id]}/0/conaninfo.txt",
'conanmanifest.txt': "#{base_file_url}/#{params[:url_recipe]}/-/0/package/#{params[:package_id]}/0/conanmanifest.txt",
'conan_package.tgz': "#{base_file_url}/#{params[:url_recipe]}/-/0/package/#{params[:package_id]}/0/conan_package.tgz"
}
requires :package_name, type: String, regexp: PACKAGE_COMPONENT_REGEX, desc: 'Package name'
requires :package_version, type: String, regexp: PACKAGE_COMPONENT_REGEX, desc: 'Package version'
requires :package_username, type: String, regexp: PACKAGE_COMPONENT_REGEX, desc: 'Package username'
requires :package_channel, type: String, regexp: PACKAGE_COMPONENT_REGEX, desc: 'Package channel'
requires :recipe_revision, type: String, desc: 'Conan Recipe Revision'
end
namespace 'files/:package_name/:package_version/:package_username/:package_channel/:recipe_revision' do
before do
authenticate_non_get!
end
params do
requires :file_name, type: String, desc: 'Package file name'
end
desc 'Download recipe files' do
detail 'This feature was introduced in GitLab 12.5'
end
get 'export/:file_name' do
not_found!
end
desc 'Recipe Upload Urls' do
detail 'This feature was introduced in GitLab 12.3'
params do
requires :conan_package_reference, type: String, desc: 'Conan Package ID'
requires :package_revision, type: String, desc: 'Conan Package Revision'
requires :file_name, type: String, desc: 'Package file name'
end
desc 'Download package files' do
detail 'This feature was introduced in GitLab 12.5'
end
get 'package/:conan_package_reference/:package_revision/:file_name' do
not_found!
end
end
post 'upload_urls' do
status 200
{
'conanfile.py': "#{base_file_url}/#{params[:url_recipe]}/-/0/export/conanfile.py",
'conanmanifest.txt': "#{base_file_url}/#{params[:url_recipe]}/-/0/export/conanmanifest.txt"
}
end
helpers do
include Gitlab::Utils::StrongMemoize
include ::API::Helpers::RelatedResourcesHelpers
def recipe_upload_urls(file_names)
{ upload_urls: Hash[
file_names.collect do |file_name|
[file_name, recipe_file_upload_url(file_name)]
end
] }
end
# Get the recipe snapshot
#
# the snapshot is a hash of { filename: md5 hash }
# md5 hash is the has of that file. This hash is used to diff the files existing on the client
# to determine which client files need to be uploaded if no recipe exists the snapshot is empty
desc 'Recipe Snapshot' do
detail 'This feature was introduced in GitLab 12.3'
def package_upload_urls(file_names)
{ upload_urls: Hash[
file_names.collect do |file_name|
[file_name, package_file_upload_url(file_name)]
end
] }
end
get '/' do
{}
def package_file_upload_url(file_name)
expose_url(
api_v4_packages_conan_v1_files_package_path(
package_name: params[:package_name],
package_version: params[:package_version],
package_username: params[:package_username],
package_channel: params[:package_channel],
recipe_revision: '0',
conan_package_reference: params[:conan_package_reference],
package_revision: '0',
file_name: file_name
)
)
end
desc 'Package Snapshot' do
detail 'This feature was introduced in GitLab 12.3'
def recipe_file_upload_url(file_name)
expose_url(
api_v4_packages_conan_v1_files_export_path(
package_name: params[:package_name],
package_version: params[:package_version],
package_username: params[:package_username],
package_channel: params[:package_channel],
recipe_revision: '0',
file_name: file_name
)
)
end
get 'packages/:package_id' do
{}
def recipe
"%{package_name}/%{package_version}@%{package_username}/%{package_channel}" % params.symbolize_keys
end
end
helpers do
def base_file_url
"#{::Settings.gitlab.base_url}/api/v4/packages/conan/v1/files"
def project
strong_memoize(:project) do
full_path = ::Packages::ConanMetadatum.full_path_from(package_username: params[:package_username])
Project.find_by_full_path(full_path)
end
end
def find_personal_access_token
......@@ -167,10 +289,6 @@ module API
PersonalAccessToken.find_by_token(token)
end
def valid_url_recipe?(recipe_url)
recipe_url =~ %r{\A(([\w](\.|\+|-)?)*(\/?)){4}\z}
end
end
end
end
......@@ -808,6 +808,28 @@ module EE
end
end
module ConanPackage
class ConanPackageManifest < Grape::Entity
expose :package_urls, merge: true
end
class ConanPackageSnapshot < Grape::Entity
expose :package_snapshot, merge: true
end
class ConanRecipeManifest < Grape::Entity
expose :recipe_urls, merge: true
end
class ConanRecipeSnapshot < Grape::Entity
expose :recipe_snapshot, merge: true
end
class ConanUploadUrls < Grape::Entity
expose :upload_urls, merge: true
end
end
class NpmPackage < Grape::Entity
expose :name
expose :versions
......
......@@ -6,6 +6,23 @@ module EE
extend ActiveSupport::Concern
class_methods do
def conan_file_name_regex
@conan_file_name_regex ||=
%r{\A#{(::Packages::ConanFileMetadatum::RECIPE_FILES + ::Packages::ConanFileMetadatum::PACKAGE_FILES).join("|")}\z}.freeze
end
def conan_package_reference_regex
@conan_package_reference_regex ||= %r{\A[A-Za-z0-9]+\z}.freeze
end
def conan_revision_regex
@conan_revision_regex ||= %r{\A0\z}.freeze
end
def conan_recipe_component_regex
@conan_recipe_component_regex ||= %r{\A(\w[.+-]?)+\z}.freeze
end
def package_name_regex
@package_name_regex ||= %r{\A\@?(([\w\-\.\+]*)\/)*([\w\-\.]+)@?(([\w\-\.\+]*)\/)*([\w\-\.]*)\z}.freeze
end
......
......@@ -31,15 +31,98 @@ FactoryBot.define do
end
factory :conan_package do
sequence(:name) { |n| "package-#{n}/1.0.0@#{project.full_path.tr('/', '+')}/stable"}
conan_metadatum
after :build do |package|
package.conan_metadatum.package_username = Packages::ConanMetadatum.package_username_from(
full_path: package.project.full_path
)
end
sequence(:name) { |n| "package-#{n}" }
version { '1.0.0' }
package_type { 'conan' }
after :create do |package|
create :conan_package_file, :conan_recipe_file, package: package
create :conan_package_file, :conan_recipe_manifest, package: package
create :conan_package_file, :conan_package_info, package: package
create :conan_package_file, :conan_package_manifest, package: package
create :conan_package_file, :conan_package, package: package
end
end
end
factory :package_file, class: Packages::PackageFile do
package
factory :conan_package_file do
trait(:conan_recipe_file) do
after :create do |package_file|
create :conan_file_metadatum, :recipe_file, package_file: package_file
end
file { fixture_file_upload('ee/spec/fixtures/conan/recipe_conanfile.py') }
file_name { 'recipe_conanfile.py' }
file_sha1 { 'be93151dc23ac34a82752444556fe79b32c7a1ad' }
file_md5 { '12345abcde' }
file_type { 'py' }
size { 400.kilobytes }
end
trait(:conan_recipe_manifest) do
after :create do |package_file|
create :conan_file_metadatum, :recipe_file, package_file: package_file
end
file { fixture_file_upload('ee/spec/fixtures/conan/recipe_conanmanifest.txt') }
file_name { 'recipe_conanmanifest.txt' }
file_sha1 { 'be93151dc23ac34a82752444556fe79b32c7a1ad' }
file_md5 { '12345abcde' }
file_type { 'txt' }
size { 400.kilobytes }
end
trait(:conan_package_manifest) do
after :create do |package_file|
create :conan_file_metadatum, :package_file, package_file: package_file
end
file { fixture_file_upload('ee/spec/fixtures/conan/package_conanmanifest.txt') }
file_name { 'package_conanmanifest.txt' }
file_sha1 { 'be93151dc23ac34a82752444556fe79b32c7a1ad' }
file_md5 { '12345abcde' }
file_type { 'txt' }
size { 400.kilobytes }
end
trait(:conan_package_info) do
after :create do |package_file|
create :conan_file_metadatum, :package_file, package_file: package_file
end
file { fixture_file_upload('ee/spec/fixtures/conan/package_conaninfo.txt') }
file_name { 'package_conaninfo.txt' }
file_sha1 { 'be93151dc23ac34a82752444556fe79b32c7a1ad' }
file_md5 { '12345abcde' }
file_type { 'txt' }
size { 400.kilobytes }
end
trait(:conan_package) do
after :create do |package_file|
create :conan_file_metadatum, :package_file, package_file: package_file
end
file { fixture_file_upload('ee/spec/fixtures/conan/conan_package.tgz') }
file_name { 'conan_package.tgz' }
file_sha1 { 'be93151dc23ac34a82752444556fe79b32c7a1ad' }
file_md5 { '12345abcde' }
file_type { 'tgz' }
size { 400.kilobytes }
end
end
trait(:jar) do
file { fixture_file_upload('ee/spec/fixtures/maven/my-app-1.0-20180724.124855-1.jar') }
file_name { 'my-app-1.0-20180724.124855-1.jar' }
......@@ -84,4 +167,25 @@ FactoryBot.define do
app_name { 'my-app' }
app_version { '1.0-SNAPSHOT' }
end
factory :conan_metadatum, class: Packages::ConanMetadatum do
package
package_username { 'username' }
package_channel { 'stable' }
end
factory :conan_file_metadatum, class: Packages::ConanFileMetadatum do
package_file
recipe_revision { '0' }
trait(:recipe_file) do
conan_file_type { 'recipe_file' }
end
trait(:package_file) do
conan_file_type { 'package_file' }
package_revision { '0' }
conan_package_reference { '123456789' }
end
end
end
......@@ -2,13 +2,14 @@
require 'spec_helper'
describe Packages::ConanPackageFinder do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :public) }
describe '#execute' do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :public) }
let!(:conan_package) { create(:conan_package, project: project) }
let!(:conan_package2) { create(:conan_package, project: project) }
subject { described_class.new(query, user).execute }
subject { described_class.new(user, query: query).execute }
context 'packages that are not visible to user' do
let!(:non_visible_project) { create(:project, :private) }
......
[settings]
arch=x86_64
build_type=Release
compiler=apple-clang
compiler.libcxx=libc++
compiler.version=10.0
os=Macos
[requires]
[options]
shared=False
[full_settings]
arch=x86_64
build_type=Release
compiler=apple-clang
compiler.libcxx=libc++
compiler.version=10.0
os=Macos
[full_requires]
[full_options]
shared=False
[recipe_hash]
b4b91125b36b40a7076a98310588f820
[env]
1565723794
conaninfo.txt: 2774ebe649804c1cd9430f26ab0ead14
include/hello.h: 8727846905bd09baecf8bdc1edb1f46e
lib/libhello.a: 7f2aaa8b6f3bc316bba59e47b6a0bd43
from conans import ConanFile, CMake, tools
class HelloConan(ConanFile):
name = "Hello"
version = "0.1"
license = "<Put the package license here>"
author = "<Put your name here> <And your email here>"
url = "<Package recipe repository url here, for issues about the package>"
description = "<Description of Hello here>"
topics = ("<Put some tag here>", "<here>", "<and here>")
settings = "os", "compiler", "build_type", "arch"
options = {"shared": [True, False]}
default_options = "shared=False"
generators = "cmake"
def source(self):
self.run("git clone https://github.com/conan-io/hello.git")
# This small hack might be useful to guarantee proper /MT /MD linkage
# in MSVC if the packaged project doesn't have variables to set it
# properly
tools.replace_in_file("hello/CMakeLists.txt", "PROJECT(HelloWorld)",
'''PROJECT(HelloWorld)
include(${CMAKE_BINARY_DIR}/conanbuildinfo.cmake)
conan_basic_setup()''')
def build(self):
cmake = CMake(self)
cmake.configure(source_folder="hello")
cmake.build()
# Explicit way:
# self.run('cmake %s/hello %s'
# % (self.source_folder, cmake.command_line))
# self.run("cmake --build . %s" % cmake.build_config)
def package(self):
self.copy("*.h", dst="include", src="hello")
self.copy("*hello.lib", dst="lib", keep_path=False)
self.copy("*.dll", dst="bin", keep_path=False)
self.copy("*.so", dst="lib", keep_path=False)
self.copy("*.dylib", dst="lib", keep_path=False)
self.copy("*.a", dst="lib", keep_path=False)
def package_info(self):
self.cpp_info.libs = ["hello"]
1565723790
conanfile.py: 7c042b95312cc4c4ee89199dc51aebf9
......@@ -3,6 +3,45 @@
require 'spec_helper'
describe Gitlab::Regex do
describe '.conan_file_name_regex' do
subject { described_class.conan_file_name_regex }
it { is_expected.to match('conanfile.py') }
it { is_expected.to match('conan_package.tgz') }
it { is_expected.not_to match('foo.txt') }
it { is_expected.not_to match('!!()()') }
end
describe '.conan_package_reference_regex' do
subject { described_class.conan_package_reference_regex }
it { is_expected.to match('123456789') }
it { is_expected.to match('asdf1234') }
it { is_expected.not_to match('@foo') }
it { is_expected.not_to match('0/pack+age/1@1/0') }
it { is_expected.not_to match('!!()()') }
end
describe '.conan_revision_regex' do
subject { described_class.conan_revision_regex }
it { is_expected.to match('0') }
it { is_expected.not_to match('foo') }
it { is_expected.not_to match('!!()()') }
end
describe '.conan_recipe_component_regex' do
subject { described_class.conan_recipe_component_regex }
it { is_expected.to match('foobar') }
it { is_expected.to match('foo_bar') }
it { is_expected.to match('foo+bar') }
it { is_expected.to match('1.0.0') }
it { is_expected.not_to match('foo@bar') }
it { is_expected.not_to match('foo/bar') }
it { is_expected.not_to match('!!()()') }
end
describe '.feature_flag_regex' do
subject { described_class.feature_flag_regex }
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Packages::ConanFileMetadatum, type: :model do
describe 'relationships' do
it { is_expected.to belong_to(:package_file) }
end
describe 'validations' do
let(:package_file) do
create(:package_file,
file: fixture_file_upload('ee/spec/fixtures/conan/recipe_conanfile.py'),
file_name: 'recipe_conanfile.py')
end
it { is_expected.to validate_presence_of(:package_file) }
it { is_expected.to validate_presence_of(:recipe_revision) }
describe '#recipe_revision' do
it { is_expected.to allow_value("0").for(:recipe_revision) }
it { is_expected.not_to allow_value(nil).for(:recipe_revision) }
end
describe '#package_revision_for_package_file' do
context 'recipe file' do
let(:conan_file_metadatum) { build(:conan_file_metadatum, :recipe_file, package_file: package_file) }
it 'is valid with empty value' do
conan_file_metadatum.package_revision = nil
expect(conan_file_metadatum).to be_valid
end
it 'is invalid with value' do
conan_file_metadatum.package_revision = '0'
expect(conan_file_metadatum).to be_invalid
end
end
context 'package file' do
let(:conan_file_metadatum) { build(:conan_file_metadatum, :package_file, package_file: package_file) }
it 'is valid with default value' do
conan_file_metadatum.package_revision = '0'
expect(conan_file_metadatum).to be_valid
end
it 'is invalid with non-default value' do
conan_file_metadatum.package_revision = 'foo'
expect(conan_file_metadatum).to be_invalid
end
end
end
describe '#conan_package_reference_for_package_file' do
context 'recipe file' do
let(:conan_file_metadatum) { build(:conan_file_metadatum, :recipe_file, package_file: package_file) }
it 'is valid with empty value' do
conan_file_metadatum.conan_package_reference = nil
expect(conan_file_metadatum).to be_valid
end
it 'is invalid with value' do
conan_file_metadatum.conan_package_reference = '123456789'
expect(conan_file_metadatum).to be_invalid
end
end
context 'package file' do
let(:conan_file_metadatum) { build(:conan_file_metadatum, :package_file, package_file: package_file) }
it 'is valid with acceptable value' do
conan_file_metadatum.conan_package_reference = '123456asdf'
expect(conan_file_metadatum).to be_valid
end
it 'is invalid with invalid value' do
conan_file_metadatum.conan_package_reference = 'foo@bar'
expect(conan_file_metadatum).to be_invalid
end
it 'is invalid when nil' do
conan_file_metadatum.conan_package_reference = nil
expect(conan_file_metadatum).to be_invalid
end
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Packages::ConanMetadatum, type: :model do
describe 'relationships' do
it { is_expected.to belong_to(:package) }
end
describe 'validations' do
it { is_expected.to validate_presence_of(:package) }
it { is_expected.to validate_presence_of(:package_username) }
it { is_expected.to validate_presence_of(:package_channel) }
describe '#package_username' do
it { is_expected.to allow_value("my-package+username").for(:package_username) }
it { is_expected.not_to allow_value("my/package").for(:package_username) }
it { is_expected.not_to allow_value("my(package)").for(:package_username) }
it { is_expected.not_to allow_value("my@package").for(:package_username) }
end
describe '#package_channel' do
it { is_expected.to allow_value("beta").for(:package_channel) }
it { is_expected.to allow_value("stable+1.0").for(:package_channel) }
it { is_expected.not_to allow_value("my/channel").for(:package_channel) }
it { is_expected.not_to allow_value("my(channel)").for(:package_channel) }
it { is_expected.not_to allow_value("my@channel").for(:package_channel) }
end
end
describe '#recipe' do
let(:package) { create(:conan_package) }
it 'returns the recipe' do
expect(package.conan_recipe).to eq("#{package.name}/#{package.version}@#{package.conan_metadatum.package_username}/#{package.conan_metadatum.package_channel}")
end
end
describe '#recipe_url' do
let(:package) { create(:conan_package) }
it 'returns the recipe url' do
expect(package.conan_recipe_path).to eq("#{package.name}/#{package.version}/#{package.conan_metadatum.package_username}/#{package.conan_metadatum.package_channel}")
end
end
describe '.package_username_from' do
let(:full_path) { 'foo/bar/baz-buz' }
it 'returns the username formatted package path' do
expect(described_class.package_username_from(full_path: full_path)).to eq('foo+bar+baz-buz')
end
end
describe '.full_path_from' do
let(:username) { 'foo+bar+baz-buz' }
it 'returns the username formatted package path' do
expect(described_class.full_path_from(package_username: username)).to eq('foo/bar/baz-buz')
end
end
end
......@@ -4,6 +4,7 @@ require 'spec_helper'
RSpec.describe Packages::PackageFile, type: :model do
describe 'relationships' do
it { is_expected.to belong_to(:package) }
it { is_expected.to have_one(:conan_file_metadatum) }
end
describe 'validations' do
......
# frozen_string_literal: true
require 'spec_helper'
describe ConanPackagePresenter do
let(:user) { create(:user) }
let(:project) { create(:project) }
describe '#recipe_urls' do
subject { described_class.new(recipe, user, project).recipe_urls }
context 'no existing package' do
let(:recipe) { "my-pkg/v1.0.0/#{project.full_path}/stable" }
it { is_expected.to be_empty }
end
context 'existing package' do
let(:package) { create(:conan_package, project: project) }
let(:recipe) { package.conan_recipe }
let(:expected_result) do
{
"recipe_conanfile.py" => "#{Settings.build_base_gitlab_url}/api/v4/packages/conan/v1/files/#{package.conan_recipe_path}/0/export/recipe_conanfile.py",
"recipe_conanmanifest.txt" => "#{Settings.build_base_gitlab_url}/api/v4/packages/conan/v1/files/#{package.conan_recipe_path}/0/export/recipe_conanmanifest.txt"
}
end
it { is_expected.to eq(expected_result) }
end
end
describe '#recipe_snapshot' do
subject { described_class.new(recipe, user, project).recipe_snapshot }
context 'no existing package' do
let(:recipe) { "my-pkg/v1.0.0/#{project.full_path}/stable" }
it { is_expected.to be_empty }
end
context 'existing package' do
let(:package) { create(:conan_package, project: project) }
let(:recipe) { package.conan_recipe }
let(:expected_result) do
{
"recipe_conanfile.py" => '12345abcde',
"recipe_conanmanifest.txt" => '12345abcde'
}
end
it { is_expected.to eq(expected_result) }
end
end
describe '#package_urls' do
subject { described_class.new(recipe, user, project).package_urls }
context 'no existing package' do
let(:recipe) { "my-pkg/v1.0.0/#{project.full_path}/stable" }
it { is_expected.to be_empty }
end
context 'existing package' do
let(:package) { create(:conan_package, project: project) }
let(:recipe) { package.conan_recipe }
let(:expected_result) do
{
"package_conaninfo.txt" => "#{Settings.build_base_gitlab_url}/api/v4/packages/conan/v1/files/#{package.conan_recipe_path}/0/package/123456789/0/package_conaninfo.txt",
"package_conanmanifest.txt" => "#{Settings.build_base_gitlab_url}/api/v4/packages/conan/v1/files/#{package.conan_recipe_path}/0/package/123456789/0/package_conanmanifest.txt",
"conan_package.tgz" => "#{Settings.build_base_gitlab_url}/api/v4/packages/conan/v1/files/#{package.conan_recipe_path}/0/package/123456789/0/conan_package.tgz"
}
end
it { is_expected.to eq(expected_result) }
end
end
describe '#package_snapshot' do
subject { described_class.new(recipe, user, project).package_snapshot }
context 'no existing package' do
let(:recipe) { "my-pkg/v1.0.0/#{project.full_path}/stable" }
it { is_expected.to be_empty }
end
context 'existing package' do
let(:package) { create(:conan_package, project: project) }
let(:recipe) { package.conan_recipe }
let(:expected_result) do
{
"package_conaninfo.txt" => '12345abcde',
"package_conanmanifest.txt" => '12345abcde',
"conan_package.tgz" => '12345abcde'
}
end
it { is_expected.to eq(expected_result) }
end
end
end
......@@ -2,10 +2,16 @@
require 'spec_helper'
describe API::ConanPackages do
let_it_be(: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(:personal_access_token) { create(:personal_access_token) }
let(:auth_token) { personal_access_token.token }
let(:headers) do
{ 'HTTP_AUTHORIZATION' => ActionController::HttpAuthentication::Basic.encode_credentials('foo', personal_access_token.token) }
{ 'HTTP_AUTHORIZATION' => ActionController::HttpAuthentication::Basic.encode_credentials('foo', auth_token) }
end
let(:jwt_secret) do
......@@ -17,21 +23,11 @@ describe API::ConanPackages do
end
before do
project.add_developer(user)
stub_licensed_features(packages: true)
allow(Settings).to receive(:attr_encrypted_db_key_base).and_return(base_secret)
end
def build_jwt(personal_access_token, secret: jwt_secret, user_id: nil)
JSONWebToken::HMACToken.new(secret).tap do |jwt|
jwt['pat'] = personal_access_token.id
jwt['u'] = user_id || personal_access_token.user_id
end
end
def build_auth_headers(token)
{ 'HTTP_AUTHORIZATION' => "Bearer #{token}" }
end
describe 'GET /api/v4/packages/conan/v1/ping' do
context 'feature flag disabled' do
before do
......@@ -99,53 +95,66 @@ describe API::ConanPackages do
end
describe 'GET /api/v4/packages/conan/v1/conans/search' do
let(:project) { create(:project, :public) }
let(:package) { create(:conan_package, project: project) }
before do
project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
get api('/packages/conan/v1/conans/search'), headers: headers, params: params
end
subject { JSON.parse(response.body)['results'] }
subject { json_response['results'] }
context 'returns packages with a matching name' do
let(:params) { { q: package.name } }
let(:params) { { q: package.conan_recipe } }
it { is_expected.to contain_exactly(package.name) }
it { is_expected.to contain_exactly(package.conan_recipe) }
end
context 'returns packages using a * wildcard' do
let(:params) {{ q: "#{package.name[0, 3]}*" }}
let(:params) { { q: "#{package.name[0, 3]}*" } }
it { is_expected.to contain_exactly(package.name) }
it { is_expected.to contain_exactly(package.conan_recipe) }
end
context 'does not return non-matching packages' do
let(:params) {{ q: "foo" }}
let(:params) { { q: "foo" } }
it { is_expected.to be_blank }
end
end
describe 'GET /api/v4/packages/conan/v1/users/authenticate' do
it 'responds with 401 Unauthorized when invalid token is provided' do
headers = { 'HTTP_AUTHORIZATION' => ActionController::HttpAuthentication::Basic.encode_credentials('foo', 'wrong-token') }
get api('/packages/conan/v1/users/authenticate'), headers: headers
subject { get api('/packages/conan/v1/users/authenticate'), headers: headers }
expect(response).to have_gitlab_http_status(401)
context 'when using invalid token' do
let(:auth_token) { 'invalid_token' }
it 'responds with 401' do
subject
expect(response).to have_gitlab_http_status(401)
end
end
it 'responds with 200 OK and JWT when valid access token is provided' do
get api('/packages/conan/v1/users/authenticate'), headers: headers
context 'when valid JWT access token is provided' do
it 'responds with 200' do
subject
expect(response).to have_gitlab_http_status(200)
expect(response).to have_gitlab_http_status(200)
end
payload = JSONWebToken::HMACToken.decode(response.body, jwt_secret).first
expect(payload['pat']).to eq(personal_access_token.id)
expect(payload['u']).to eq(personal_access_token.user_id)
it 'token has valid validity time' do
Timecop.freeze do
subject
payload = JSONWebToken::HMACToken.decode(
response.body, jwt_secret).first
expect(payload['pat']).to eq(personal_access_token.id)
expect(payload['u']).to eq(personal_access_token.user_id)
duration = payload['exp'] - payload['iat']
expect(duration).to eq(1.hour)
duration = payload['exp'] - payload['iat']
expect(duration).to eq(1.hour)
end
end
end
end
......@@ -163,9 +172,10 @@ describe API::ConanPackages do
end
end
shared_examples 'rejected invalid recipe' do
context 'with invalid recipe url' do
let(:recipe) { '../../foo++../..' }
shared_examples 'rejects invalid recipe' do
context 'with invalid recipe path' do
let(:recipe_path) { '../../foo++../..' }
it 'returns 400' do
subject
......@@ -174,103 +184,295 @@ describe API::ConanPackages do
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::ConanMetadatum.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::ConanMetadatum.package_username_from(full_path: project.full_path) }
end
it 'returns not found' do
allow(ConanPackagePresenter).to receive(:new)
.with(
'aa/bb@%{project}/ccc' % { project: ::Packages::ConanMetadatum.package_username_from(full_path: project.full_path) },
user,
project
).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
context 'recipe endpoints' do
let(:jwt) { build_jwt(personal_access_token) }
let(:headers) { build_auth_headers(jwt.encoded) }
let(:recipe) { 'my-package-name/1.0/username/channel' }
let(:package_id) { '123456789' }
let(:presenter) { double('ConanPackagePresenter') }
before do
allow(ConanPackagePresenter).to receive(:new)
.with(package.conan_recipe, user, package.project)
.and_return(presenter)
end
describe 'GET /api/v4/packages/conan/v1/conans/*recipe' do
subject { get api("/packages/conan/v1/conans/#{recipe}"), headers: headers }
describe 'GET /api/v4/packages/conan/v1/conans/:package_name/package_version/:package_username/:package_channel' do
let(:recipe_path) { package.conan_recipe_path }
it_behaves_like 'rejected invalid recipe'
subject { get api("/packages/conan/v1/conans/#{recipe_path}"), headers: headers }
it 'responds with an empty response' do
subject
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'
}
expect(response.body).to be {}
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/*recipe/digest' do
subject { get api("/packages/conan/v1/conans/#{recipe}/digest"), headers: headers }
describe 'GET /api/v4/packages/conan/v1/conans/:package_name/package_version/:package_username/:package_channel/packages/:package_id' do
let(:recipe_path) { package.conan_recipe_path }
it_behaves_like 'rejected invalid recipe'
subject { get api("/packages/conan/v1/conans/#{recipe_path}/packages/#{package_id}"), headers: headers }
it 'responds with a 404' do
subject
it_behaves_like 'rejects invalid recipe'
it_behaves_like 'rejects recipe for invalid project'
it_behaves_like 'empty recipe for not found package'
expect(response).to have_gitlab_http_status(404)
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'
context 'with existing package' do
let(:recipe_path) { package.conan_recipe_path }
it 'returns the download urls for each package file' 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
end
end
describe 'GET /api/v4/packages/conan/v1/conans/*recipe/upload_urls' do
describe 'GET /api/v4/packages/conan/v1/conans/:package_name/package_version/:package_username/:package_channel/packages/:package_id/digest' do
subject { get api("/packages/conan/v1/conans/#{recipe_path}/packages/#{package_id}/digest"), headers: headers }
it_behaves_like 'rejects invalid recipe'
it_behaves_like 'rejects recipe for invalid project'
context 'with existing package' do
let(:recipe_path) { package.conan_recipe_path }
it 'returns the download urls for the 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
end
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,
"conanmanifext.txt": 123 }
end
subject { post api("/packages/conan/v1/conans/#{recipe}/upload_urls"), params: params, headers: headers }
it_behaves_like 'rejected invalid recipe'
subject { post api("/packages/conan/v1/conans/#{recipe_path}/upload_urls"), params: params, headers: headers }
it_behaves_like 'rejects invalid recipe'
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/#{recipe}/-/0/export/conanfile.py",
'conanmanifest.txt': "#{Settings.gitlab.base_url}/api/v4/packages/conan/v1/files/#{recipe}/-/0/export/conanmanifest.txt"
'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
expect(response.body).to eq(expected_response.to_json)
end
end
describe 'GET /api/v4/packages/conan/v1/conans/*recipe/packages/:package_id' do
subject { get api("/packages/conan/v1/conans/#{recipe}/packages/123456789"), headers: headers }
describe 'POST /api/v4/packages/conan/v1/conans/:package_name/package_version/:package_username/:package_channel/packages/:package_id/upload_urls' do
let(:recipe_path) { package.conan_recipe_path }
it_behaves_like 'rejected invalid recipe'
let(:params) do
{ "conaninfo.txt": 24,
"conanmanifext.txt": 123,
"conan_package.tgz": 523 }
end
subject { post api("/packages/conan/v1/conans/#{recipe_path}/packages/123456789/upload_urls"), params: params, headers: headers }
it_behaves_like 'rejects invalid recipe'
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"
}
it 'responds with an empty response' do
subject
expect(response.body).to be {}
expect(response.body).to eq(expected_response.to_json)
end
end
end
describe 'GET /api/v4/packages/conan/v1/conans/*recipe/packages/:package_id/digest' do
subject { get api("/packages/conan/v1/conans/#{recipe}/packages/123456789/digest"), headers: headers }
context 'file endpoints' do
let(:jwt) { build_jwt(personal_access_token) }
let(:headers) { build_auth_headers(jwt.encoded) }
let(:package_file_tgz) { package.package_files.find_by(file_type: 'tgz') }
let(:metadata) { package_file_tgz.conan_file_metadatum }
it_behaves_like 'rejected invalid recipe'
describe 'GET /api/v4/packages/conan/v1/files/:package_name/package_version/:package_username/:package_channel/
:recipe_revision/export/:file_name' do
let(:recipe_path) { package.conan_recipe_path }
it 'responds with a 404' do
subject
subject do
get api("/packages/conan/v1/files/#{recipe_path}/#{metadata.recipe_revision}/export/#{file_name}"),
headers: headers
end
expect(response).to have_gitlab_http_status(404)
context 'invalid file' do
let(:file_name) { 'badfile.txt' }
it 'returns 404 not found' do
subject
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'valid file' do
let(:file_name) { package_file_tgz.file_name }
it 'returns forbidden' do
subject
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
describe 'GET /api/v4/packages/conan/v1/conans/*recipe/packages/:package_id/upload_urls' do
let(:params) do
{ "conaninfo.txt": 24,
"conanmanifext.txt": 123,
"conan_package.tgz": 523 }
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(:recipe_path) { package.conan_recipe_path }
subject do
get api("/packages/conan/v1/files/#{recipe_path}/#{metadata.recipe_revision}/package/" \
"#{metadata.conan_package_reference}/#{metadata.package_revision}/#{file_name}"),
headers: headers
end
context 'valid recipe' do
subject { post api("/packages/conan/v1/conans/#{recipe}/packages/123456789/upload_urls"), params: params, headers: headers }
context 'invalid file' do
let(:file_name) { 'badfile.txt' }
it_behaves_like 'rejected invalid recipe'
it 'returns 404 not found' do
subject
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/#{recipe}/-/0/package/123456789/0/conaninfo.txt",
'conanmanifest.txt': "#{Settings.gitlab.base_url}/api/v4/packages/conan/v1/files/#{recipe}/-/0/package/123456789/0/conanmanifest.txt",
'conan_package.tgz': "#{Settings.gitlab.base_url}/api/v4/packages/conan/v1/files/#{recipe}/-/0/package/123456789/0/conan_package.tgz"
}
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'valid file' do
let(:file_name) { package_file_tgz.file_name }
it 'returns forbidden' do
subject
expect(response.body).to eq expected_response.to_json
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
end
def build_jwt(personal_access_token, secret: jwt_secret, user_id: nil)
JSONWebToken::HMACToken.new(secret).tap do |jwt|
jwt['pat'] = personal_access_token.id
jwt['u'] = user_id || personal_access_token.user_id
end
end
def build_auth_headers(token)
{ 'HTTP_AUTHORIZATION' => "Bearer #{token}" }
end
end
......@@ -10,6 +10,10 @@ describe Packages::Conan::SearchService do
subject { described_class.new(user, query: query) }
before do
project.add_developer(user)
end
describe '#execute' do
context 'feature unavailable' do
let(:query) { '' }
......@@ -34,7 +38,7 @@ describe Packages::Conan::SearchService do
result = subject.execute
expect(result.status).to eq :success
expect(result.payload).to eq(results: [conan_package.name, conan_package2.name])
expect(result.payload).to eq(results: [conan_package.conan_recipe, conan_package2.conan_recipe])
end
end
......@@ -50,24 +54,24 @@ describe Packages::Conan::SearchService do
end
context 'with no wildcard' do
let(:query) { conan_package.name.split('/').first }
let(:query) { conan_package.name }
it 'makes a search using the beginning of the recipe' do
result = subject.execute
expect(result.status).to eq :success
expect(result.payload).to eq(results: [conan_package.name])
expect(result.payload).to eq(results: [conan_package.conan_recipe])
end
end
context 'with full recipe match' do
let(:query) { conan_package.name }
let(:query) { conan_package.conan_recipe }
it 'makes an exact search' do
result = subject.execute
expect(result.status).to eq :success
expect(result.payload).to eq(results: [conan_package.name])
expect(result.payload).to eq(results: [conan_package.conan_recipe])
end
end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment