Commit 16472b24 authored by David Fernandez's avatar David Fernandez

Add nuget metadata service endpoints

See https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource
Add relevant presenters
Add nuget package finder
parent 2ce5ef22
# frozen_string_literal: true
module Packages
module Nuget
class PackageFinder
MAX_PACKAGES_COUNT = 50
def initialize(project, package_name:, package_version: nil)
@project = project
@package_name = package_name
@package_version = package_version
end
def execute
packages.limit_recent(MAX_PACKAGES_COUNT)
end
private
def packages
result = @project.packages
.nuget
.with_name(@package_name)
result = result.with_version(@package_version) if @package_version.present?
result
end
end
end
end
...@@ -47,6 +47,7 @@ class Packages::Package < ApplicationRecord ...@@ -47,6 +47,7 @@ class Packages::Package < ApplicationRecord
end end
scope :preload_files, -> { preload(:package_files) } scope :preload_files, -> { preload(:package_files) }
scope :last_of_each_version, -> { where(id: all.select('MAX(id) AS id').group(:version)) } scope :last_of_each_version, -> { where(id: all.select('MAX(id) AS id').group(:version)) }
scope :limit_recent, ->(limit) { order_created_desc.limit(limit) }
# Sorting # Sorting
scope :order_created, -> { reorder('created_at ASC') } scope :order_created, -> { reorder('created_at ASC') }
......
# frozen_string_literal: true
module Packages
module Nuget
class PackageMetadataPresenter
include API::Helpers::Packages::Nuget::MetadataPresenterHelpers
def initialize(package)
@package = package
end
def json_url
json_url_for(@package)
end
def archive_url
archive_url_for(@package)
end
def catalog_entry
catalog_entry_for(@package)
end
end
end
end
# frozen_string_literal: true
module Packages
module Nuget
class PackagesMetadataPresenter
include API::Helpers::Packages::Nuget::MetadataPresenterHelpers
include Gitlab::Utils::StrongMemoize
attr_reader :packages
COUNT = 1.freeze
def initialize(packages)
@packages = packages
end
def count
COUNT
end
def items
[summary]
end
private
def summary
{
json_url: json_url,
lower_version: lower_version,
upper_version: upper_version,
packages_count: @packages.count,
packages: @packages.map { |pkg| metadata_for(pkg) }
}
end
def metadata_for(package)
{
json_url: json_url_for(package),
archive_url: archive_url_for(package),
catalog_entry: catalog_entry_for(package)
}
end
def json_url
json_url_for(@packages.first)
end
def lower_version
sorted_versions.first
end
def upper_version
sorted_versions.last
end
def sorted_versions
strong_memoize(:sorted_versions) do
versions = packages.map(&:version).compact
VersionSorter.sort(versions)
end
end
end
end
end
...@@ -56,13 +56,28 @@ module Packages ...@@ -56,13 +56,28 @@ module Packages
full_path = case service_type full_path = case service_type
when :download when :download
"#{base_path}/download" # TODO NUGET API: replace with grape path helper when download endpoint is implemented api_v4_projects_packages_nuget_download_package_name_package_version_package_filename_path(
{
id: project.id,
package_name: nil,
package_version: nil,
package_filename: nil
},
true
)
when :search when :search
"#{base_path}/query" # TODO NUGET API: replace with grape path helper when query endpoint is implemented "#{base_path}/query"
when :metadata when :metadata
"#{base_path}/metadata" # TODO NUGET API: replace with grape path helper when metadata endpoint is implemented api_v4_projects_packages_nuget_metadata_package_name_package_version_path(
{
id: project.id,
package_name: nil,
package_version: nil
},
true
)
when :publish when :publish
base_path # TODO NUGET API: replace with grape path helper when publish endpoint is implemented base_path
end end
expose_url(full_path) expose_url(full_path)
......
# frozen_string_literal: true
module API
module Helpers
module Packages
module Nuget
module MetadataPresenterHelpers
include ::API::Helpers::RelatedResourcesHelpers
include ::API::Helpers::PackagesHelpers
BLANK_STRING = ''
EMPTY_ARRAY = [].freeze
private
def json_url_for(package)
path = api_v4_projects_packages_nuget_metadata_package_name_package_version_path(
{
id: package.project.id,
package_name: package.name,
package_version: package.version,
format: '.json'
},
true
)
expose_url(path)
end
def archive_url_for(package)
path = api_v4_projects_packages_nuget_download_package_name_package_version_package_filename_path(
{
id: package.project.id,
package_name: package.name,
package_version: package.version,
package_filename: package.package_files.last&.file_name
},
true
)
expose_url(path)
end
def catalog_entry_for(package)
{
json_url: json_url_for(package),
authors: BLANK_STRING,
dependencies: EMPTY_ARRAY,
package_name: package.name,
package_version: package.version,
archive_url: archive_url_for(package),
summary: BLANK_STRING
}
end
def base_path_for(package)
api_v4_projects_packages_nuget_path(id: package.project.id)
end
end
end
end
end
end
...@@ -84,7 +84,7 @@ module API ...@@ -84,7 +84,7 @@ module API
end end
# https://docs.microsoft.com/en-us/nuget/api/package-publish-resource # https://docs.microsoft.com/en-us/nuget/api/package-publish-resource
desc 'The NuGet Package Content endpoint' do desc 'The NuGet Package Publish endpoint' do
detail 'This feature was introduced in GitLab 12.6' detail 'This feature was introduced in GitLab 12.6'
end end
params do params do
...@@ -118,6 +118,64 @@ module API ...@@ -118,6 +118,64 @@ module API
put 'authorize' do put 'authorize' do
authorize_workhorse!(subject: authorized_user_project, has_length: false) authorize_workhorse!(subject: authorized_user_project, has_length: false)
end end
params do
requires :package_name, type: String, desc: 'The NuGet package name', regexp: API::NO_SLASH_URL_PART_REGEX
end
namespace '/metadata/*package_name' do
before do
authorize_read_package!(authorized_user_project)
end
# https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource
desc 'The NuGet Metadata Service - Package name level' do
detail 'This feature was introduced in GitLab 12.8'
end
get 'index', format: :json do
packages = ::Packages::Nuget::PackageFinder.new(authorized_user_project, package_name: params[:package_name])
.execute
not_found!('Packages') unless packages.exists?
present ::Packages::Nuget::PackagesMetadataPresenter.new(packages),
with: EE::API::Entities::Nuget::PackagesMetadata
end
desc 'The NuGet Metadata Service - Package name and version level' do
detail 'This feature was introduced in GitLab 12.8'
end
params do
requires :package_version, type: String, desc: 'The NuGet package version', regexp: API::NO_SLASH_URL_PART_REGEX
end
get '*package_version', format: :json do
package = ::Packages::Nuget::PackageFinder
.new(authorized_user_project, package_name: params[:package_name], package_version: params[:package_version])
.execute
.first
not_found!('Package') unless package
present ::Packages::Nuget::PackageMetadataPresenter.new(package),
with: EE::API::Entities::Nuget::PackageMetadata
end
end
# https://docs.microsoft.com/en-us/nuget/api/package-base-address-resource
desc 'The NuGet Content Service' do
detail 'This feature was introduced in GitLab 12.8'
end
params do
requires :package_name, type: String, desc: 'The NuGet package name', regexp: API::NO_SLASH_URL_PART_REGEX
requires :package_version, type: String, desc: 'The NuGet package version', regexp: API::NO_SLASH_URL_PART_REGEX
end
namespace '/download/*package_name/*package_version' do
params do
requires :package_filename, type: String, desc: 'The NuGet package filename', regexp: API::NO_SLASH_URL_PART_REGEX
end
get '*package_filename' do
not_found!('package not found') # TODO NUGET API: not implemented yet.
end
end
end end
end end
end end
......
...@@ -878,6 +878,35 @@ module EE ...@@ -878,6 +878,35 @@ module EE
expose :version expose :version
expose :resources expose :resources
end end
class PackageMetadataCatalogEntry < Grape::Entity
expose :json_url, as: :@id
expose :authors
expose :dependencies, as: :dependencyGroups
expose :package_name, as: :id
expose :package_version, as: :version
expose :archive_url, as: :packageContent
expose :summary
end
class PackageMetadata < Grape::Entity
expose :json_url, as: :@id
expose :archive_url, as: :packageContent
expose :catalog_entry, as: :catalogEntry, using: EE::API::Entities::Nuget::PackageMetadataCatalogEntry
end
class PackagesMetadataItem < Grape::Entity
expose :json_url, as: :@id
expose :lower_version, as: :lower
expose :upper_version, as: :upper
expose :packages_count, as: :count
expose :packages, as: :items, using: EE::API::Entities::Nuget::PackageMetadata
end
class PackagesMetadata < Grape::Entity
expose :count
expose :items, using: EE::API::Entities::Nuget::PackagesMetadataItem
end
end end
class NpmPackage < Grape::Entity class NpmPackage < Grape::Entity
......
...@@ -38,7 +38,7 @@ FactoryBot.define do ...@@ -38,7 +38,7 @@ FactoryBot.define do
factory :nuget_package do factory :nuget_package do
sequence(:name) { |n| "NugetPackage#{n}"} sequence(:name) { |n| "NugetPackage#{n}"}
version { '1.0.0' } sequence(:version) { |n| "1.0.#{n}" }
package_type { :nuget } package_type { :nuget }
after :create do |package| after :create do |package|
......
# frozen_string_literal: true
require 'spec_helper'
describe Packages::Nuget::PackageFinder do
let_it_be(:package1) { create(:nuget_package) }
let_it_be(:package2) { create(:nuget_package, name: package1.name, version: '2.0.0', project: package1.project) }
let_it_be(:project) { package1.project }
let(:package_name) { package1.name }
let(:package_version) { nil }
describe '#execute!' do
subject { described_class.new(project, package_name: package_name, package_version: package_version).execute }
it { is_expected.to match_array([package1, package2]) }
context 'with unknown package name' do
let(:package_name) { 'foobar' }
it { is_expected.to be_empty }
end
context 'with valid version' do
let(:package_version) { '2.0.0' }
it { is_expected.to match_array([package2]) }
end
context 'with unknown version' do
let(:package_version) { 'foobar' }
it { is_expected.to be_empty }
end
end
end
{
"type": "object",
"required": ["@id", "packageContent", "catalogEntry"],
"properties": {
"@id": { "type": "string" },
"packageContent": { "type": "string" },
"catalogEntry": {
"type": "object",
"required": ["@id", "authors", "dependencyGroups", "id", "packageContent", "summary", "version"],
"properties": {
"@id": { "type": "string" },
"authors": { "const": "" },
"dependencyGroups": { "const": [] },
"id": { "type": "string" },
"packageContent": { "type": "string" },
"summary": { "const": "" },
"version": { "type": "string" }
}
}
}
}
{
"type": "object",
"required": ["count", "items"],
"properties": {
"count": { "const": 0 },
"items": {
"type": "array",
"items": {
"type": "object",
"required": ["lower", "upper", "count", "items"],
"properties": {
"lower": { "type": "string" },
"upper": { "type": "string" },
"count": { "type": "integer" },
"items": {
"type": "array",
"items": {
"type": "object",
"required": ["@id", "packageContent", "catalogEntry"],
"properties": {
"@id": { "type": "string" },
"packageContent": { "type": "string" },
"catalogEntry": {
"type": "object",
"required": ["@id", "authors", "dependencyGroups", "id", "packageContent", "summary", "version"],
"properties": {
"@id": { "type": "string" },
"authors": { "const": "" },
"dependencyGroups": { "const": [] },
"id": { "type": "string" },
"packageContent": { "type": "string" },
"summary": { "const": "" },
"version": { "type": "string" }
}
}
}
}
}
}
}
}
}
}
...@@ -155,4 +155,14 @@ RSpec.describe Packages::Package, type: :model do ...@@ -155,4 +155,14 @@ RSpec.describe Packages::Package, type: :model do
it { is_expected.to eq([package2, package3]) } it { is_expected.to eq([package2, package3]) }
end end
end end
describe '.limit_recent' do
let!(:package1) { create(:nuget_package) }
let!(:package2) { create(:nuget_package) }
let!(:package3) { create(:nuget_package) }
subject { described_class.limit_recent(2) }
it { is_expected.to match_array([package3, package2]) }
end
end end
# frozen_string_literal: true
require 'spec_helper'
describe Packages::Nuget::PackageMetadataPresenter do
let_it_be(:package) { create(:nuget_package) }
let_it_be(:presenter) { described_class.new(package) }
describe '#json_url' do
let_it_be(:expected_suffix) { "/api/v4/projects/#{package.project_id}/packages/nuget/metadata/#{package.name}/#{package.version}.json" }
subject { presenter.json_url }
it { is_expected.to end_with(expected_suffix) }
end
describe '#archive_url' do
let_it_be(:expected_suffix) { "/api/v4/projects/#{package.project_id}/packages/nuget/download/#{package.name}/#{package.version}/#{package.package_files.last.file_name}" }
subject { presenter.archive_url }
it { is_expected.to end_with(expected_suffix) }
end
describe '#catalog_entry' do
subject { presenter.catalog_entry }
it 'returns an entry structure' do
entry = subject
expect(entry).to be_a Hash
%i[json_url archive_url].each { |field| expect(entry[field]).not_to be_blank }
%i[authors summary].each { |field| expect(entry[field]).to be_blank }
expect(entry[:dependencies]).to eq []
expect(entry[:package_name]).to eq package.name
expect(entry[:package_version]).to eq package.version
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Packages::Nuget::PackagesMetadataPresenter do
let_it_be(:packages) { create_list(:nuget_package, 5, name: 'Dummy.Package') }
let_it_be(:presenter) { described_class.new(packages) }
describe '#count' do
subject { presenter.count }
it {is_expected.to eq 1}
end
describe '#items' do
subject { presenter.items }
it 'returns an array' do
items = subject
expect(items).to be_a Array
expect(items.size).to eq 1
end
it 'returns a summary structure' do
item = subject.first
expect(item).to be_a Hash
%i[json_url lower_version upper_version].each { |field| expect(item[field]).not_to be_blank }
expect(item[:packages_count]).to eq packages.count
expect(item[:packages]).to be_a Array
expect(item[:packages].size).to eq packages.count
end
it 'returns the catalog entries' do
item = subject.first
item[:packages].each do |pkg|
expect(pkg).to be_a Hash
%i[json_url archive_url catalog_entry].each { |field| expect(pkg[field]).not_to be_blank }
catalog_entry = pkg[:catalog_entry]
%i[json_url archive_url package_name package_version].each { |field| expect(catalog_entry[field]).not_to be_blank }
%i[authors summary].each { |field| expect(catalog_entry[field]).to be_blank }
expect(catalog_entry[:dependencies]).to eq []
end
end
end
end
This diff is collapsed.
...@@ -18,6 +18,26 @@ RSpec.shared_examples 'rejects nuget packages access' do |user_type, status, add ...@@ -18,6 +18,26 @@ RSpec.shared_examples 'rejects nuget packages access' do |user_type, status, add
end end
end end
RSpec.shared_examples 'rejects nuget packages access with packages features disabled' do
context 'with packages features disabled' do
before do
stub_licensed_features(packages: false)
end
it_behaves_like 'rejects nuget packages access', :anonymous, :forbidden
end
end
RSpec.shared_examples 'rejects nuget packages access with feature flag disabled' do
context 'with feature flag disabled' do
before do
stub_feature_flags(nuget_package_registry: { enabled: false, thing: project })
end
it_behaves_like 'rejects nuget packages access', :anonymous, :not_found
end
end
RSpec.shared_examples 'process nuget service index request' do |user_type, status, add_member = true| RSpec.shared_examples 'process nuget service index request' do |user_type, status, add_member = true|
context "for user type #{user_type}" do context "for user type #{user_type}" do
before do before do
...@@ -47,6 +67,64 @@ RSpec.shared_examples 'process nuget service index request' do |user_type, statu ...@@ -47,6 +67,64 @@ RSpec.shared_examples 'process nuget service index request' do |user_type, statu
end end
end end
RSpec.shared_examples 'process nuget metadata request at package name level' do |user_type, status, add_member = true|
context "for user type #{user_type}" do
before do
project.send("add_#{user_type}", user) if add_member && user_type != :anonymous
end
it_behaves_like 'returning response status', status
it 'returns a valid json response' do
subject
expect(response.content_type.to_s).to eq('application/json')
expect(json_response).to be_a(Hash)
end
it 'returns a valid nuget packages metadata json' do
subject
expect(json_response).to match_schema('public_api/v4/packages/nuget/packages_metadata', dir: 'ee')
end
context 'with invalid format' do
let(:url) { "/projects/#{project.id}/packages/nuget/metadata/#{package_name}/index.xls" }
it_behaves_like 'rejects nuget packages access', :anonymous, :not_found
end
end
end
RSpec.shared_examples 'process nuget metadata request at package name and package version level' do |user_type, status, add_member = true|
context "for user type #{user_type}" do
before do
project.send("add_#{user_type}", user) if add_member && user_type != :anonymous
end
it_behaves_like 'returning response status', status
it 'returns a valid json response' do
subject
expect(response.content_type.to_s).to eq('application/json')
expect(json_response).to be_a(Hash)
end
it 'returns a valid nuget package metadata json' do
subject
expect(json_response).to match_schema('public_api/v4/packages/nuget/package_metadata', dir: 'ee')
end
context 'with invalid format' do
let(:url) { "/projects/#{project.id}/packages/nuget/metadata/#{package_name}/#{package.version}.xls" }
it_behaves_like 'rejects nuget packages access', :anonymous, :not_found
end
end
end
RSpec.shared_examples 'process nuget workhorse authorization' do |user_type, status, add_member = true| RSpec.shared_examples 'process nuget workhorse authorization' do |user_type, status, add_member = true|
context "for user type #{user_type}" do context "for user type #{user_type}" do
before do before do
...@@ -78,7 +156,7 @@ RSpec.shared_examples 'process nuget workhorse authorization' do |user_type, sta ...@@ -78,7 +156,7 @@ RSpec.shared_examples 'process nuget workhorse authorization' do |user_type, sta
end end
RSpec.shared_examples 'process nuget upload' do |user_type, status, add_member = true| RSpec.shared_examples 'process nuget upload' do |user_type, status, add_member = true|
shared_examples 'creates nuget package files' do RSpec.shared_examples 'creates nuget package files' do
it 'creates package files' do it 'creates package files' do
expect(::Packages::Nuget::ExtractionWorker).to receive(:perform_async).once expect(::Packages::Nuget::ExtractionWorker).to receive(:perform_async).once
expect { subject } expect { subject }
......
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