Commit 0eba4b31 authored by Michael Kozono's avatar Michael Kozono

Merge branch '299282-rubygems-dependency-endpoint' into 'master'

Rubygems dependencies endpoint

See merge request gitlab-org/gitlab!55917
parents fda95cf4 914dd34b
# frozen_string_literal: true
module Packages
module Rubygems
class DependencyResolverService < BaseService
include Gitlab::Utils::StrongMemoize
DEFAULT_PLATFORM = 'ruby'
def execute
return ServiceResponse.error(message: "forbidden", http_status: :forbidden) unless Ability.allowed?(current_user, :read_package, project)
return ServiceResponse.error(message: "#{gem_name} not found", http_status: :not_found) if packages.empty?
payload = packages.map do |package|
dependencies = package.dependency_links.map do |link|
[link.dependency.name, link.dependency.version_pattern]
end
{
name: gem_name,
number: package.version,
platform: DEFAULT_PLATFORM,
dependencies: dependencies
}
end
ServiceResponse.success(payload: payload)
end
private
def packages
strong_memoize(:packages) do
project.packages.with_name(gem_name)
end
end
def gem_name
params[:gem_name]
end
end
end
end
......@@ -26,7 +26,7 @@ module API
before do
require_packages_enabled!
authenticate!
authenticate_non_get!
not_found! unless Feature.enabled?(:rubygem_packages, user_project)
end
......@@ -118,11 +118,24 @@ module API
detail 'This feature was introduced in GitLab 13.9'
end
params do
optional :gems, type: String, desc: 'Comma delimited gem names'
optional :gems, type: Array[String], coerce_with: ::API::Validations::Types::CommaSeparatedToArray.coerce, desc: 'Comma delimited gem names'
end
get 'dependencies' do
# To be implemented in https://gitlab.com/gitlab-org/gitlab/-/issues/299282
not_found!
authorize_read_package!
if params[:gems].blank?
status :ok
else
results = params[:gems].map do |gem_name|
service_result = Packages::Rubygems::DependencyResolverService.new(user_project, current_user, gem_name: gem_name).execute
render_api_error!(service_result.message, service_result.http_status) if service_result.error?
service_result.payload
end
content_type 'application/octet-stream'
Marshal.dump(results.flatten)
end
end
end
end
......
......@@ -277,6 +277,10 @@ FactoryBot.define do
factory :packages_dependency, class: 'Packages::Dependency' do
sequence(:name) { |n| "@test/package-#{n}"}
sequence(:version_pattern) { |n| "~6.2.#{n}" }
trait(:rubygems) do
sequence(:name) { |n| "gem-dependency-#{n}"}
end
end
factory :packages_dependency_link, class: 'Packages::DependencyLink' do
......@@ -289,6 +293,11 @@ FactoryBot.define do
link.nuget_metadatum = build(:nuget_dependency_link_metadatum)
end
end
trait(:rubygems) do
package { association(:rubygems_package) }
dependency { association(:packages_dependency, :rubygems) }
end
end
factory :nuget_dependency_link_metadatum, class: 'Packages::Nuget::DependencyLinkMetadatum' do
......
......@@ -44,7 +44,7 @@ RSpec.describe API::RubygemPackages do
end
shared_examples 'without authentication' do
it_behaves_like 'returning response status', :unauthorized
it_behaves_like 'returning response status', :not_found
end
shared_examples 'with authentication' do
......@@ -276,10 +276,65 @@ RSpec.describe API::RubygemPackages do
end
describe 'GET /api/v4/projects/:project_id/packages/rubygems/api/v1/dependencies' do
let_it_be(:package) { create(:rubygems_package, project: project) }
let(:url) { api("/projects/#{project.id}/packages/rubygems/api/v1/dependencies") }
subject { get(url, headers: headers) }
subject { get(url, headers: headers, params: params) }
it_behaves_like 'an unimplemented route'
context 'with valid project' do
where(:visibility, :user_role, :member, :token_type, :valid_token, :shared_examples_name, :expected_status) do
:public | :developer | true | :personal_access_token | true | 'dependency endpoint success' | :success
:public | :guest | true | :personal_access_token | true | 'dependency endpoint success' | :success
:public | :developer | true | :personal_access_token | false | 'rejects rubygems packages access' | :unauthorized
:public | :guest | true | :personal_access_token | false | 'rejects rubygems packages access' | :unauthorized
:public | :developer | false | :personal_access_token | true | 'dependency endpoint success' | :success
:public | :guest | false | :personal_access_token | true | 'dependency endpoint success' | :success
:public | :developer | false | :personal_access_token | false | 'rejects rubygems packages access' | :unauthorized
:public | :guest | false | :personal_access_token | false | 'rejects rubygems packages access' | :unauthorized
:public | :anonymous | false | :personal_access_token | true | 'dependency endpoint success' | :success
:private | :developer | true | :personal_access_token | true | 'dependency endpoint success' | :success
:private | :guest | true | :personal_access_token | true | 'rejects rubygems packages access' | :forbidden
:private | :developer | true | :personal_access_token | false | 'rejects rubygems packages access' | :unauthorized
:private | :guest | true | :personal_access_token | false | 'rejects rubygems packages access' | :unauthorized
:private | :developer | false | :personal_access_token | true | 'rejects rubygems packages access' | :not_found
:private | :guest | false | :personal_access_token | true | 'rejects rubygems packages access' | :not_found
:private | :developer | false | :personal_access_token | false | 'rejects rubygems packages access' | :unauthorized
:private | :guest | false | :personal_access_token | false | 'rejects rubygems packages access' | :unauthorized
:private | :anonymous | false | :personal_access_token | true | 'rejects rubygems packages access' | :not_found
:public | :developer | true | :job_token | true | 'dependency endpoint success' | :success
:public | :guest | true | :job_token | true | 'dependency endpoint success' | :success
:public | :developer | true | :job_token | false | 'rejects rubygems packages access' | :unauthorized
:public | :guest | true | :job_token | false | 'rejects rubygems packages access' | :unauthorized
:public | :developer | false | :job_token | true | 'dependency endpoint success' | :success
:public | :guest | false | :job_token | true | 'dependency endpoint success' | :success
:public | :developer | false | :job_token | false | 'rejects rubygems packages access' | :unauthorized
:public | :guest | false | :job_token | false | 'rejects rubygems packages access' | :unauthorized
:private | :developer | true | :job_token | true | 'dependency endpoint success' | :success
:private | :guest | true | :job_token | true | 'rejects rubygems packages access' | :forbidden
:private | :developer | true | :job_token | false | 'rejects rubygems packages access' | :unauthorized
:private | :guest | true | :job_token | false | 'rejects rubygems packages access' | :unauthorized
:private | :developer | false | :job_token | true | 'rejects rubygems packages access' | :not_found
:private | :guest | false | :job_token | true | 'rejects rubygems packages access' | :not_found
:private | :developer | false | :job_token | false | 'rejects rubygems packages access' | :unauthorized
:private | :guest | false | :job_token | false | 'rejects rubygems packages access' | :unauthorized
:public | :developer | true | :deploy_token | true | 'dependency endpoint success' | :success
:public | :developer | true | :deploy_token | false | 'rejects rubygems packages access' | :unauthorized
:private | :developer | true | :deploy_token | true | 'dependency endpoint success' | :success
:private | :developer | true | :deploy_token | false | 'rejects rubygems packages access' | :unauthorized
end
with_them do
let(:token) { valid_token ? tokens[token_type] : 'invalid-token123' }
let(:headers) { user_role == :anonymous ? {} : { 'HTTP_AUTHORIZATION' => token } }
let(:params) { {} }
before do
project.update!(visibility: visibility.to_s)
end
it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member]
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Packages::Rubygems::DependencyResolverService do
let_it_be(:project) { create(:project, :private) }
let_it_be(:package) { create(:package, project: project) }
let_it_be(:user) { create(:user) }
let(:gem_name) { package.name }
let(:service) { described_class.new(project, user, gem_name: gem_name) }
describe '#execute' do
subject { service.execute }
context 'user without access' do
it 'returns a service error' do
expect(subject.error?).to be(true)
expect(subject.message).to eq('forbidden')
end
end
context 'user with access' do
before do
project.add_developer(user)
end
context 'when no package is found' do
let(:gem_name) { nil }
it 'returns a service error', :aggregate_failures do
expect(subject.error?).to be(true)
expect(subject.message).to eq("#{gem_name} not found")
end
end
context 'package without dependencies' do
it 'returns an empty dependencies array' do
expected_result = [{
name: package.name,
number: package.version,
platform: described_class::DEFAULT_PLATFORM,
dependencies: []
}]
expect(subject.payload).to eq(expected_result)
end
end
context 'package with dependencies' do
let(:dependency_link) { create(:packages_dependency_link, :rubygems, package: package)}
let(:dependency_link2) { create(:packages_dependency_link, :rubygems, package: package)}
let(:dependency_link3) { create(:packages_dependency_link, :rubygems, package: package)}
it 'returns a set of dependencies' do
expected_result = [{
name: package.name,
number: package.version,
platform: described_class::DEFAULT_PLATFORM,
dependencies: [
[dependency_link.dependency.name, dependency_link.dependency.version_pattern],
[dependency_link2.dependency.name, dependency_link2.dependency.version_pattern],
[dependency_link3.dependency.name, dependency_link3.dependency.version_pattern]
]
}]
expect(subject.payload).to eq(expected_result)
end
end
context 'package with multiple versions' do
let(:dependency_link) { create(:packages_dependency_link, :rubygems, package: package)}
let(:dependency_link2) { create(:packages_dependency_link, :rubygems, package: package)}
let(:dependency_link3) { create(:packages_dependency_link, :rubygems, package: package)}
let(:package2) { create(:package, project: project, name: package.name, version: '9.9.9') }
let(:dependency_link4) { create(:packages_dependency_link, :rubygems, package: package2)}
it 'returns a set of dependencies' do
expected_result = [{
name: package.name,
number: package.version,
platform: described_class::DEFAULT_PLATFORM,
dependencies: [
[dependency_link.dependency.name, dependency_link.dependency.version_pattern],
[dependency_link2.dependency.name, dependency_link2.dependency.version_pattern],
[dependency_link3.dependency.name, dependency_link3.dependency.version_pattern]
]
}, {
name: package2.name,
number: package2.version,
platform: described_class::DEFAULT_PLATFORM,
dependencies: [
[dependency_link4.dependency.name, dependency_link4.dependency.version_pattern]
]
}]
expect(subject.payload).to eq(expected_result)
end
end
end
end
end
......@@ -128,3 +128,50 @@ RSpec.shared_examples 'process rubygems upload' do |user_type, status, add_membe
end
end
end
RSpec.shared_examples 'dependency endpoint success' 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
raise 'Status is not :success' if status != :success
context 'with no params', :aggregate_failures do
it 'returns empty' do
subject
expect(response.body).to eq('200')
expect(response).to have_gitlab_http_status(status)
end
end
context 'with gems params' do
let(:params) { { gems: 'foo,bar' } }
let(:expected_response) { Marshal.dump(%w(result result)) }
it 'returns successfully', :aggregate_failures do
service_result = double('DependencyResolverService', execute: ServiceResponse.success(payload: 'result'))
expect(Packages::Rubygems::DependencyResolverService).to receive(:new).with(project, anything, gem_name: 'foo').and_return(service_result)
expect(Packages::Rubygems::DependencyResolverService).to receive(:new).with(project, anything, gem_name: 'bar').and_return(service_result)
subject
expect(response.body).to eq(expected_response) # rubocop:disable Security/MarshalLoad
expect(response).to have_gitlab_http_status(status)
end
it 'rejects if the service fails', :aggregate_failures do
service_result = double('DependencyResolverService', execute: ServiceResponse.error(message: 'rejected', http_status: :bad_request))
expect(Packages::Rubygems::DependencyResolverService).to receive(:new).with(project, anything, gem_name: 'foo').and_return(service_result)
subject
expect(response.body).to match(/rejected/)
expect(response).to have_gitlab_http_status(:bad_request)
end
end
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