Commit 1a0ec496 authored by Ethan Reesor's avatar Ethan Reesor Committed by Ethan Reesor

Improve tests and apply suggestions for Go proxy

See !27746

- Fix rubocop disable comments
- Move `before` block to before tests
- Correct rubocop alerts due to new rules
- Use shared examples to clarify Go proxy spec
- Enable HTTP Basic authentication for Go proxy
  + Support both HTTP basic and normal token header/query var
  + Remove custom `find_project!` helper and use basic auth helpers
- Validate GoModuleVersion type attribute
- Implement testing factories
- Implement specs for untested new classes
- Add a Settings helper for Go URLs
parent e1009191
......@@ -66,6 +66,12 @@ class Settings < Settingslogic
(base_url(gitlab) + [gitlab.relative_url_root]).join('')
end
def build_gitlab_go_url
# "Go package paths are not URLs, and do not include port numbers"
# https://github.com/golang/go/issues/38213#issuecomment-607851460
"#{gitlab.host}#{gitlab.relative_url_root}"
end
def kerberos_protocol
kerberos.https ? "https" : "http"
end
......
......@@ -5,6 +5,8 @@ module Packages
class ModuleFinder
include ::API::Helpers::Packages::Go::ModuleHelpers
GITLAB_GO_URL = (Settings.build_gitlab_go_url + '/').freeze
attr_reader :project, :module_name
def initialize(project, module_name)
......@@ -14,23 +16,17 @@ module Packages
@module_name = module_name
end
# rubocop: disable CodeReuse/ActiveRecord
def execute
return if @module_name.blank?
if @module_name == package_base
Packages::GoModule.new(@project, @module_name, '')
elsif @module_name.start_with?(package_base + '/')
Packages::GoModule.new(@project, @module_name, @module_name[(package_base.length + 1)..])
else
nil
end
end
return if @module_name.blank? || !@module_name.start_with?(GITLAB_GO_URL)
private
module_path = @module_name[GITLAB_GO_URL.length..].split('/')
project_path = project.full_path.split('/')
return unless module_path.take(project_path.length) == project_path
def package_base
@package_base ||= Gitlab::Routing.url_helpers.project_url(@project).split('://', 2)[1]
Packages::GoModule.new(@project, @module_name, module_path.drop(project_path.length).join('/'))
end
# rubocop: enable CodeReuse/ActiveRecord
end
end
end
......@@ -21,14 +21,10 @@ module Packages
def find(target)
case target
when String
unless pseudo_version? target
return mod.versions.filter { |v| v.name == target }.first
end
begin
if pseudo_version? target
find_pseudo_version target
rescue ArgumentError
nil
else
mod.versions.find { |v| v.name == target }
end
when Gitlab::Git::Ref
......
......@@ -3,6 +3,8 @@
class Packages::GoModuleVersion
include ::API::Helpers::Packages::Go::ModuleHelpers
VALID_TYPES = %i[ref commit pseudo].freeze
attr_reader :mod, :type, :ref, :commit
delegate :major, to: :@semver, allow_nil: true
......@@ -12,6 +14,17 @@ class Packages::GoModuleVersion
delegate :build, to: :@semver, allow_nil: true
def initialize(mod, type, commit, name: nil, semver: nil, ref: nil)
raise ArgumentError.new("invalid type '#{type}'") unless VALID_TYPES.include? type
raise ArgumentError.new("mod is required") unless mod
raise ArgumentError.new("commit is required") unless commit
if type == :ref
raise ArgumentError.new("ref is required") unless ref
elsif type == :pseudo
raise ArgumentError.new("name is required") unless name
raise ArgumentError.new("semver is required") unless semver
end
@mod = mod
@type = type
@commit = commit
......
# frozen_string_literal: true
class Packages::SemVer
# basic semver, but bounded (^expr$)
PATTERN = /\A(v?)(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([-.a-z0-9]+))?(?:\+([-.a-z0-9]+))?\z/i.freeze
attr_accessor :major, :minor, :patch, :prerelease, :build
def initialize(major = 0, minor = 0, patch = 0, prerelease = nil, build = nil, prefixed: false)
@major = major
@minor = minor
@patch = patch
@prerelease = prerelease
@build = build
@prefixed = prefixed
end
def prefixed?
@prefixed
end
def ==(other)
self.class == other.class &&
self.major == other.major &&
self.minor == other.minor &&
self.patch == other.patch &&
self.prerelease == other.prerelease &&
self.build == other.build
end
def to_s
s = "#{prefixed? ? 'v' : ''}#{major || 0}.#{minor || 0}.#{patch || 0}"
s += "-#{prerelease}" if prerelease
s += "+#{build}" if build
s
end
def self.match(str, prefixed: false)
m = PATTERN.match(str)
return unless m
return if prefixed == m[1].empty?
m
end
def self.match?(str, prefixed: false)
!match(str, prefixed: prefixed).nil?
end
def self.parse(str, prefixed: false)
m = match str, prefixed: prefixed
return unless m
new(m[2].to_i, m[3].to_i, m[4].to_i, m[5], m[6], prefixed: prefixed)
end
end
# frozen_string_literal: true
module API
class GoProxy < Grape::API
helpers ::API::Helpers::PackagesHelpers
helpers ::API::Helpers::PackagesManagerClientsHelpers
helpers ::API::Helpers::Packages::BasicAuthHelpers
helpers ::API::Helpers::Packages::Go::ModuleHelpers
# basic semver, except case encoded (A => !a)
......@@ -12,15 +13,27 @@ module API
before { require_packages_enabled! }
helpers do
def case_decode(str)
str.gsub(/![[:alpha:]]/) { |s| s[1..].upcase }
# support personal access tokens for HTTP Basic in addition to the usual methods
def find_personal_access_token
pa = find_personal_access_token_from_http_basic_auth
return pa if pa
# copied from Gitlab::Auth::AuthFinders
token =
current_request.params[::Gitlab::Auth::AuthFinders::PRIVATE_TOKEN_PARAM].presence ||
current_request.env[::Gitlab::Auth::AuthFinders::PRIVATE_TOKEN_HEADER].presence ||
parsed_oauth_token
return unless token
# Expiration, revocation and scopes are verified in `validate_access_token!`
PersonalAccessToken.find_by_token(token) || raise(::Gitlab::Auth::UnauthorizedError)
end
def find_module
module_name = case_decode params[:module_name]
bad_request!('Module Name') if module_name.blank?
mod = ::Packages::Go::ModuleFinder.new(user_project, module_name).execute
mod = ::Packages::Go::ModuleFinder.new(authorized_user_project, module_name).execute
not_found! unless mod
......@@ -29,25 +42,14 @@ module API
def find_version
module_version = case_decode params[:module_version]
ver = ::Packages::Go::VersionFinder.new(find_module).find(module_version)
ver = find_module.find_version(module_version)
not_found! unless ver&.valid?
ver
end
def find_project!(id)
project = find_project(id)
ability = job_token_authentication? ? :build_read_project : :read_project
if can?(current_user, ability, project)
project
elsif current_user.nil?
unauthorized!
else
not_found!('Project')
end
rescue ArgumentError
not_found!
end
end
......@@ -58,8 +60,8 @@ module API
route_setting :authentication, job_token_allowed: true
resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
before do
authorize_read_package!
authorize_packages_feature!
authorize_read_package!(authorized_user_project)
authorize_packages_feature!(authorized_user_project)
end
namespace ':id/packages/go/*module_name/@v' do
......
......@@ -8,9 +8,6 @@ module API
# basic semver regex
SEMVER_REGEX = /v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([-.a-z0-9]+))?(?:\+([-.a-z0-9]+))?/i.freeze
# basic semver, but bounded (^expr$)
SEMVER_TAG_REGEX = /^v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([-.a-z0-9]+))?(?:\+([-.a-z0-9]+))?$/i.freeze
# semver, but the prerelease component follows a specific format
PSEUDO_VERSION_REGEX = /^v\d+\.(0\.0-|\d+\.\d+-([^+]*\.)?0\.)\d{14}-[A-Za-z0-9]+(\+[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$/i.freeze
......@@ -25,23 +22,15 @@ module API
def semver?(tag)
return false if tag.dereferenced_target.nil?
SEMVER_TAG_REGEX.match?(tag.name)
::Packages::SemVer.match?(tag.name, prefixed: true)
end
def pseudo_version?(str)
SEMVER_TAG_REGEX.match?(str) && PSEUDO_VERSION_REGEX.match?(str)
::Packages::SemVer.match?(str, prefixed: true) && PSEUDO_VERSION_REGEX.match?(str)
end
def parse_semver(str)
m = SEMVER_TAG_REGEX.match(str)
return unless m
OpenStruct.new(
major: m[1].to_i,
minor: m[2].to_i,
patch: m[3].to_i,
prerelease: m[4],
build: m[5])
::Packages::SemVer.parse(str, prefixed: true)
end
end
end
......
# frozen_string_literal: true
def get_result(op, ret)
raise "#{op} failed: #{ret}" unless ret[:status] == :success
ret[:result]
end
FactoryBot.define do
factory :go_module_commit, class: 'Commit' do
skip_create
transient do
project { raise ArgumentError.new("project is required") }
service { raise ArgumentError.new("this factory cannot be used without specifying a trait") }
tag { nil }
tag_message { nil }
commit do
r = service.execute
raise "operation failed: #{r}" unless r[:status] == :success
commit = project.repository.commit_by(oid: r[:result])
if tag
r = Tags::CreateService.new(project, project.owner).execute(tag, commit.sha, tag_message)
raise "operation failed: #{r}" unless r[:status] == :success
end
commit
end
end
initialize_with do
commit
end
trait :files do
transient do
files { raise ArgumentError.new("files is required") }
message { 'Add files' }
end
service do
Files::MultiService.new(
project,
project.owner,
commit_message: message,
start_branch: project.repository.root_ref || 'master',
branch_name: project.repository.root_ref || 'master',
actions: files.map do |path, content|
{ action: :create, file_path: path, content: content }
end
)
end
end
trait :package do
transient do
path { raise ArgumentError.new("path is required") }
message { 'Add package' }
end
service do
Files::MultiService.new(
project,
project.owner,
commit_message: message,
start_branch: project.repository.root_ref || 'master',
branch_name: project.repository.root_ref || 'master',
actions: [
{ action: :create, file_path: path + '/b.go', content: "package b\nfunc Bye() { println(\"Goodbye world!\") }\n" }
]
)
end
end
trait :module do
transient do
name { nil }
message { 'Add module' }
end
service do
port = ::Gitlab.config.gitlab.port
host = ::Gitlab.config.gitlab.host
domain = case port when 80, 443 then host else "#{host}:#{port}" end
url = "#{domain}/#{project.path_with_namespace}"
if name.nil?
path = ''
else
url += '/' + name
path = name + '/'
end
Files::MultiService.new(
project,
project.owner,
commit_message: message,
start_branch: project.repository.root_ref || 'master',
branch_name: project.repository.root_ref || 'master',
actions: [
{ action: :create, file_path: path + 'go.mod', content: "module #{url}\n" },
{ action: :create, file_path: path + 'a.go', content: "package a\nfunc Hi() { println(\"Hello world!\") }\n" }
]
)
end
end
end
end
# frozen_string_literal: true
def get_result(op, ret)
raise "#{op} failed: #{ret}" unless ret[:status] == :success
ret[:result]
end
FactoryBot.define do
factory :go_module_version, class: 'Packages::GoModuleVersion' do
skip_create
initialize_with do
p = attributes[:params]
s = Packages::SemVer.parse(p.semver, prefixed: true)
raise ArgumentError.new("invalid sematic version: '#{p.semver}''") if !s && p.semver
new(p.mod, p.type, p.commit, name: p.name, semver: s, ref: p.ref)
end
mod { go_module }
type { :commit }
commit { raise ArgumentError.new("commit is required") }
name { nil }
semver { nil }
ref { nil }
params { OpenStruct.new(mod: mod, type: type, commit: commit, name: name, semver: semver, ref: ref) }
trait :tagged do
name { raise ArgumentError.new("name is required") }
ref { mod.project.repository.find_tag(name) }
commit { ref.dereferenced_target }
params { OpenStruct.new(mod: mod, type: :ref, commit: commit, semver: name, ref: ref) }
end
trait :pseudo do
transient do
prefix { raise ArgumentError.new("prefix is required") }
end
type { :pseudo }
name { "#{prefix}#{commit.committed_date.strftime('%Y%m%d%H%M%S')}-#{commit.sha[0..11]}" }
params { OpenStruct.new(mod: mod, type: :pseudo, commit: commit, name: name, semver: name) }
end
end
end
# frozen_string_literal: true
FactoryBot.define do
factory :go_module, class: 'Packages::GoModule' do
initialize_with { new(attributes[:project], attributes[:name], attributes[:path]) }
skip_create
project
path { '' }
name { "#{Settings.build_gitlab_go_url}/#{project.full_path}#{path.empty? ? '' : path}" }
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Packages::Go::ModuleFinder do
let_it_be(:project) { create :project }
let_it_be(:other_project) { create :project }
describe '#execute' do
context 'with module name equal to project name' do
let(:finder) { described_class.new(project, base_url(project)) }
it 'returns a module with empty path' do
mod = finder.execute
expect(mod).not_to be_nil
expect(mod.path).to eq('')
end
end
context 'with module name starting with project name and slash' do
let(:finder) { described_class.new(project, base_url(project) + '/mod') }
it 'returns a module with non-empty path' do
mod = finder.execute
expect(mod).not_to be_nil
expect(mod.path).to eq('mod')
end
end
context 'with a module name not equal to and not starting with project name' do
let(:finder) { described_class.new(project, base_url(other_project)) }
it 'returns nil' do
expect(finder.execute).to be_nil
end
end
end
def base_url(project)
"#{Settings.build_gitlab_go_url}/#{project.full_path}"
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Packages::Go::VersionFinder do
let_it_be(:user) { create :user }
let_it_be(:project) { create :project_empty_repo, creator: user, path: 'my-go-lib' }
let(:finder) { described_class.new mod }
before :all do
create :go_module_commit, :files, project: project, tag: 'v1.0.0', files: { 'README.md' => 'Hi' }
create :go_module_commit, :module, project: project, tag: 'v1.0.1'
create :go_module_commit, :package, project: project, tag: 'v1.0.2', path: 'pkg'
create :go_module_commit, :module, project: project, tag: 'v1.0.3', name: 'mod'
create :go_module_commit, :files, project: project, tag: 'c1', files: { 'y.go' => "package a\n" }
create :go_module_commit, :module, project: project, tag: 'c2', name: 'v2'
create :go_module_commit, :files, project: project, tag: 'v2.0.0', files: { 'v2/x.go' => "package a\n" }
end
shared_examples '#execute' do |*expected|
it "returns #{expected.empty? ? 'nothing' : expected.join(', ')}" do
actual = finder.execute.map { |x| x.name }
expect(actual.to_set).to eq(expected.to_set)
end
end
shared_examples '#find with an invalid argument' do |message|
it "raises an argument exception: #{message}" do
expect { finder.find(target) }.to raise_error(ArgumentError, message)
end
end
describe '#execute' do
context 'for the root module' do
let(:mod) { create :go_module, project: project }
it_behaves_like '#execute', 'v1.0.1', 'v1.0.2', 'v1.0.3'
end
context 'for the package' do
let(:mod) { create :go_module, project: project, path: '/pkg' }
it_behaves_like '#execute'
end
context 'for the submodule' do
let(:mod) { create :go_module, project: project, path: '/mod' }
it_behaves_like '#execute', 'v1.0.3'
end
context 'for the root module v2' do
let(:mod) { create :go_module, project: project, path: '/v2' }
it_behaves_like '#execute', 'v2.0.0'
end
end
describe '#find' do
let(:mod) { create :go_module, project: project }
context 'with a ref' do
it 'returns a ref version' do
ref = project.repository.find_branch 'master'
v = finder.find(ref)
expect(v.type).to eq(:ref)
expect(v.ref).to eq(ref)
end
end
context 'with a semver tag' do
it 'returns a version with a semver' do
v = finder.find(project.repository.find_tag('v1.0.0'))
expect(v.major).to eq(1)
expect(v.minor).to eq(0)
expect(v.patch).to eq(0)
expect(v.prerelease).to be_nil
expect(v.build).to be_nil
end
end
context 'with a semver tag string' do
it 'returns a version with a semver' do
v = finder.find('v1.0.1')
expect(v.major).to eq(1)
expect(v.minor).to eq(0)
expect(v.patch).to eq(1)
expect(v.prerelease).to be_nil
expect(v.build).to be_nil
end
end
context 'with a commit' do
it 'retruns a commit version' do
v = finder.find(project.repository.head_commit)
expect(v.type).to eq(:commit)
end
end
context 'with a pseudo-version' do
it 'returns a pseudo version' do
commit = project.repository.head_commit
pseudo = "v0.0.0-#{commit.committed_date.strftime('%Y%m%d%H%M%S')}-#{commit.sha[0..11]}"
v = finder.find(pseudo)
expect(v.type).to eq(:pseudo)
expect(v.commit).to eq(commit)
expect(v.name).to eq(pseudo)
end
end
context 'with a string that is not a semantic version' do
it 'returns nil' do
expect(finder.find('not-a-semver')).to be_nil
end
end
context 'with a pseudo-version that does not reference a commit' do
it_behaves_like '#find with an invalid argument', 'invalid pseudo-version: unknown commit' do
let(:commit) { project.repository.head_commit }
let(:target) { "v0.0.0-#{commit.committed_date.strftime('%Y%m%d%H%M%S')}-#{'0' * 12}" }
end
end
context 'with a pseudo-version with a short sha' do
it_behaves_like '#find with an invalid argument', 'invalid pseudo-version: revision is shorter than canonical' do
let(:commit) { project.repository.head_commit }
let(:target) { "v0.0.0-#{commit.committed_date.strftime('%Y%m%d%H%M%S')}-#{commit.sha[0..10]}" }
end
end
context 'with a pseudo-version with an invalid timestamp' do
it_behaves_like '#find with an invalid argument', 'invalid pseudo-version: does not match version-control timestamp' do
let(:commit) { project.repository.head_commit }
let(:target) { "v0.0.0-#{'0' * 14}-#{commit.sha[0..11]}" }
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Packages::GoModule, type: :model do
describe '#path_valid?' do
context 'with root path' do
let_it_be(:package) { create(:go_module) }
context 'with major version 0' do
it('returns true') { expect(package.path_valid?(0)).to eq(true) }
end
context 'with major version 1' do
it('returns true') { expect(package.path_valid?(1)).to eq(true) }
end
context 'with major version 2' do
it('returns false') { expect(package.path_valid?(2)).to eq(false) }
end
end
context 'with path ./v2' do
let_it_be(:package) { create(:go_module, path: '/v2') }
context 'with major version 0' do
it('returns false') { expect(package.path_valid?(0)).to eq(false) }
end
context 'with major version 1' do
it('returns false') { expect(package.path_valid?(1)).to eq(false) }
end
context 'with major version 2' do
it('returns true') { expect(package.path_valid?(2)).to eq(true) }
end
end
end
describe '#gomod_valid?' do
let_it_be(:package) { create(:go_module) }
context 'with good gomod' do
it('returns true') { expect(package.gomod_valid?("module #{package.name}")).to eq(true) }
end
context 'with bad gomod' do
it('returns false') { expect(package.gomod_valid?("module #{package.name}/v2")).to eq(false) }
end
context 'with empty gomod' do
it('returns false') { expect(package.gomod_valid?("")).to eq(false) }
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Packages::GoModuleVersion, type: :model do
let_it_be(:user) { create :user }
let_it_be(:project) { create :project_empty_repo, creator: user, path: 'my-go-lib' }
let_it_be(:mod) { create :go_module, project: project }
before :all do
create :go_module_commit, :files, project: project, tag: 'v1.0.0', files: { 'README.md' => 'Hi' }
create :go_module_commit, :module, project: project, tag: 'v1.0.1'
create :go_module_commit, :package, project: project, tag: 'v1.0.2', path: 'pkg'
create :go_module_commit, :module, project: project, tag: 'v1.0.3', name: 'mod'
create :go_module_commit, :files, project: project, files: { 'y.go' => "package a\n" }
create :go_module_commit, :module, project: project, name: 'v2'
create :go_module_commit, :files, project: project, tag: 'v2.0.0', files: { 'v2/x.go' => "package a\n" }
end
describe '#name' do
context 'with ref and name specified' do
let_it_be(:version) { create :go_module_version, mod: mod, name: 'foobar', commit: project.repository.head_commit, ref: project.repository.find_tag('v1.0.0') }
it('returns that name') { expect(version.name).to eq('foobar') }
end
context 'with ref specified and name unspecified' do
let_it_be(:version) { create :go_module_version, mod: mod, commit: project.repository.head_commit, ref: project.repository.find_tag('v1.0.0') }
it('returns the name of the ref') { expect(version.name).to eq('v1.0.0') }
end
context 'with ref and name unspecified' do
let_it_be(:version) { create :go_module_version, mod: mod, commit: project.repository.head_commit }
it('returns nil') { expect(version.name).to eq(nil) }
end
end
describe '#gomod' do
context 'with go.mod missing' do
let_it_be(:version) { create :go_module_version, :tagged, mod: mod, name: 'v1.0.0' }
it('returns nil') { expect(version.gomod).to eq(nil) }
end
context 'with go.mod present' do
let_it_be(:version) { create :go_module_version, :tagged, mod: mod, name: 'v1.0.1' }
it('returns the contents of go.mod') { expect(version.gomod).to eq("module #{mod.name}\n") }
end
end
describe '#files' do
context 'with a root module' do
context 'with an empty module path' do
let_it_be(:version) { create :go_module_version, :tagged, mod: mod, name: 'v1.0.2' }
it('returns all the files') { expect(version.files.map { |x| x.path }.to_set).to eq(Set['README.md', 'go.mod', 'a.go', 'pkg/b.go']) }
end
end
context 'with a root module and a submodule' do
context 'with an empty module path' do
let_it_be(:version) { create :go_module_version, :tagged, mod: mod, name: 'v1.0.3' }
it('returns files excluding the submodule') { expect(version.files.map { |x| x.path }.to_set).to eq(Set['README.md', 'go.mod', 'a.go', 'pkg/b.go']) }
end
context 'with the submodule\'s path' do
let_it_be(:mod) { create :go_module, project: project, path: 'mod' }
let_it_be(:version) { create :go_module_version, :tagged, mod: mod, name: 'v1.0.3' }
it('returns the submodule\'s files') { expect(version.files.map { |x| x.path }.to_set).to eq(Set['mod/go.mod', 'mod/a.go']) }
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Packages::SemVer, type: :model do
shared_examples '#parse with a valid semver' do |str, major, minor, patch, prerelease, build|
context "with #{str}" do
it "returns #{described_class.new(major, minor, patch, prerelease, build, prefixed: true)} with prefix" do
expected = described_class.new(major, minor, patch, prerelease, build, prefixed: true)
expect(described_class.parse('v' + str, prefixed: true)).to eq(expected)
end
it "returns #{described_class.new(major, minor, patch, prerelease, build)} without prefix" do
expected = described_class.new(major, minor, patch, prerelease, build)
expect(described_class.parse(str)).to eq(expected)
end
end
end
shared_examples '#parse with an invalid semver' do |str|
context "with #{str}" do
it 'returns nil with prefix' do
expect(described_class.parse('v' + str, prefixed: true)).to be_nil
end
it 'returns nil without prefix' do
expect(described_class.parse(str)).to be_nil
end
end
end
describe '#parse' do
it_behaves_like '#parse with a valid semver', '1.0.0', 1, 0, 0, nil, nil
it_behaves_like '#parse with a valid semver', '1.0.0-pre', 1, 0, 0, 'pre', nil
it_behaves_like '#parse with a valid semver', '1.0.0+build', 1, 0, 0, nil, 'build'
it_behaves_like '#parse with a valid semver', '1.0.0-pre+build', 1, 0, 0, 'pre', 'build'
it_behaves_like '#parse with an invalid semver', '01.0.0'
it_behaves_like '#parse with an invalid semver', '0.01.0'
it_behaves_like '#parse with an invalid semver', '0.0.01'
it_behaves_like '#parse with an invalid semver', '1.0.0asdf'
end
end
......@@ -3,440 +3,342 @@
require 'spec_helper'
describe API::GoProxy do
let_it_be(:domain) do
port = ::Gitlab.config.gitlab.port
host = ::Gitlab.config.gitlab.host
case port when 80, 443 then host else "#{host}:#{port}" end
end
include EE::PackagesManagerApiSpecHelpers
let_it_be(:user) { create :user }
let_it_be(:project) { create :project_empty_repo, creator: user, path: 'my-go-lib' }
let_it_be(:base) { "#{domain}/#{project.path_with_namespace}" }
let_it_be(:base) { "#{Settings.build_gitlab_go_url}/#{project.full_path}" }
let_it_be(:oauth) { create :oauth_access_token, scopes: 'api', resource_owner: user }
let_it_be(:job) { create :ci_build, user: user }
let_it_be(:pa_token) { create :personal_access_token, user: user }
# rubocop: disable Layout/IndentationConsistency
let_it_be(:modules) do
create_version(1, 0, 0, create_file('README.md', 'Hi', commit_message: 'Add README.md'))
create_version(1, 0, 1, create_module)
create_version(1, 0, 2, create_package('pkg'))
create_version(1, 0, 3, create_module('mod'))
sha1 = create_file('y.go', "package a\n")
sha2 = create_module('v2')
create_version(2, 0, 0, create_file('v2/x.go', "package a\n"))
commits = [
create(:go_module_commit, :files, project: project, tag: 'v1.0.0', files: { 'README.md' => 'Hi' } ),
create(:go_module_commit, :module, project: project, tag: 'v1.0.1' ),
create(:go_module_commit, :package, project: project, tag: 'v1.0.2', path: 'pkg' ),
create(:go_module_commit, :module, project: project, tag: 'v1.0.3', name: 'mod' ),
create(:go_module_commit, :files, project: project, files: { 'y.go' => "package a\n" } ),
create(:go_module_commit, :module, project: project, name: 'v2' ),
create(:go_module_commit, :files, project: project, tag: 'v2.0.0', files: { 'v2/x.go' => "package a\n" })
]
{ sha: [sha1, sha2] }
{ sha: [commits[4].sha, commits[5].sha] }
end
describe 'GET /projects/:id/packages/go/*module_name/@v/list' do
let(:resource) { "list" }
before do
project.add_developer(user)
stub_licensed_features(packages: true)
context 'for the root module' do
let(:module_name) { base }
modules
end
it 'returns v1.0.1, v1.0.2, v1.0.3' do
shared_examples 'an unavailable resource' do
it 'returns not found' do
get_resource(user)
expect_module_version_list('v1.0.1', 'v1.0.2', 'v1.0.3')
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'for the package' do
let(:module_name) { "#{base}/pkg" }
shared_examples 'a module version list resource' do |*versions, path: ''|
let(:module_name) { "#{base}#{path}" }
let(:resource) { "list" }
it 'returns nothing' do
it "returns #{versions.empty? ? 'nothing' : versions.join(', ')}" do
get_resource(user)
expect_module_version_list
expect(response).to have_gitlab_http_status(:ok)
expect(response.body.split("\n").to_set).to eq(versions.to_set)
end
end
context 'for the submodule' do
let(:module_name) { "#{base}/mod" }
it 'returns v1.0.3' do
get_resource(user)
shared_examples 'a missing module version list resource' do |*versions, path: ''|
let(:module_name) { "#{base}#{path}" }
let(:resource) { "list" }
expect_module_version_list('v1.0.3')
end
it_behaves_like 'an unavailable resource'
end
context 'for the root module v2' do
let(:module_name) { "#{base}/v2" }
shared_examples 'a module version information resource' do |version, path: ''|
let(:module_name) { "#{base}#{path}" }
let(:resource) { "#{version}.info" }
it 'returns v2.0.0' do
it "returns information for #{version}" do
get_resource(user)
expect_module_version_list('v2.0.0')
end
time = project.repository.find_tag(version).dereferenced_target.committed_date
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to be_kind_of(Hash)
expect(json_response['Version']).to eq(version)
expect(json_response['Time']).to eq(time.strftime('%Y-%m-%dT%H:%M:%S.%L%:z'))
end
end
describe 'GET /projects/:id/packages/go/*module_name/@v/:module_version.info' do
shared_examples 'a missing module version information resource' do |version, path: ''|
let(:module_name) { "#{base}#{path}" }
let(:resource) { "#{version}.info" }
context 'with the root module v1.0.1' do
let(:module_name) { base }
let(:version) { "v1.0.1" }
it_behaves_like 'an unavailable resource'
end
shared_examples 'a module pseudo-version information resource' do |prefix, path: ''|
let(:module_name) { "#{base}#{path}" }
let(:commit) { project.repository.commit_by(oid: sha) }
let(:version) { fmt_pseudo_version prefix, commit }
let(:resource) { "#{version}.info" }
it 'returns correct information' do
it "returns information for #{prefix}yyyymmddhhmmss-abcdefabcdef" do
get_resource(user)
expect_module_version_info(version)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to be_kind_of(Hash)
expect(json_response['Version']).to eq(version)
expect(json_response['Time']).to eq(commit.committed_date.strftime('%Y-%m-%dT%H:%M:%S.%L%:z'))
end
end
context 'with the submodule v1.0.3' do
let(:module_name) { "#{base}/mod" }
let(:version) { "v1.0.3" }
it 'returns correct information' do
get_resource(user)
shared_examples 'a missing module pseudo-version information resource' do |path: ''|
let(:module_name) { "#{base}#{path}" }
let(:commit) do
raise "tried to reference :commit without defining :sha" unless defined?(sha)
expect_module_version_info(version)
project.repository.commit_by(oid: sha)
end
let(:resource) { "#{version}.info" }
it_behaves_like 'an unavailable resource'
end
context 'with the root module v2.0.0' do
let(:module_name) { "#{base}/v2" }
let(:version) { "v2.0.0" }
shared_examples 'a module file resource' do |version, path: ''|
let(:module_name) { "#{base}#{path}" }
let(:resource) { "#{version}.mod" }
it 'returns correct information' do
it "returns #{path}/go.mod from the repo" do
get_resource(user)
expect_module_version_info(version)
expect(response).to have_gitlab_http_status(:ok)
expect(response.body.split("\n", 2).first).to eq("module #{module_name}")
end
end
context 'with an invalid path' do
let(:module_name) { "#{base}/pkg" }
let(:version) { "v1.0.3" }
shared_examples 'a missing module file resource' do |version, path: ''|
let(:module_name) { "#{base}#{path}" }
let(:resource) { "#{version}.mod" }
it 'returns not found' do
it_behaves_like 'an unavailable resource'
end
shared_examples 'a module archive resource' do |version, entries, path: ''|
let(:module_name) { "#{base}#{path}" }
let(:resource) { "#{version}.zip" }
it "returns an archive of #{path.empty? ? '/' : path} @ #{version} from the repo" do
get_resource(user)
expect(response).to have_gitlab_http_status(:not_found)
expect(response).to have_gitlab_http_status(:ok)
entries = entries.map { |e| "#{module_name}@#{version}/#{e}" }.to_set
actual = Set[]
Zip::InputStream.open(StringIO.new(response.body)) do |zip|
while (entry = zip.get_next_entry)
actual.add(entry.name)
end
end
context 'with an invalid version' do
let(:module_name) { "#{base}/mod" }
let(:version) { "v1.0.1" }
expect(actual).to eq(entries)
end
end
it 'returns not found' do
get_resource(user)
describe 'GET /projects/:id/packages/go/*module_name/@v/list' do
context 'for the root module' do
it_behaves_like 'a module version list resource', 'v1.0.1', 'v1.0.2', 'v1.0.3'
end
expect(response).to have_gitlab_http_status(:not_found)
context 'for the package' do
it_behaves_like 'a module version list resource', path: '/pkg'
end
context 'for the submodule' do
it_behaves_like 'a module version list resource', 'v1.0.3', path: '/mod'
end
context 'with a pseudo-version for v1' do
let(:module_name) { base }
let(:commit) { project.repository.commit_by(oid: modules[:sha][0]) }
let(:version) { "v1.0.4-0.#{commit.committed_date.strftime('%Y%m%d%H%M%S')}-#{commit.sha[0..11]}" }
context 'for the root module v2' do
it_behaves_like 'a module version list resource', 'v2.0.0', path: '/v2'
end
end
it 'returns the correct commit' do
get_resource(user)
describe 'GET /projects/:id/packages/go/*module_name/@v/:module_version.info' do
context 'with the root module v1.0.1' do
it_behaves_like 'a module version information resource', 'v1.0.1'
end
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to be_kind_of(Hash)
expect(json_response['Version']).to eq(version)
expect(json_response['Time']).to eq(commit.committed_date.strftime '%Y-%m-%dT%H:%M:%S.%L%:z')
context 'with the submodule v1.0.3' do
it_behaves_like 'a module version information resource', 'v1.0.3', path: '/mod'
end
context 'with the root module v2.0.0' do
it_behaves_like 'a module version information resource', 'v2.0.0', path: '/v2'
end
context 'with a pseudo-version for v2' do
let(:module_name) { "#{base}/v2" }
let(:commit) { project.repository.commit_by(oid: modules[:sha][1]) }
let(:version) { "v2.0.0-#{commit.committed_date.strftime('%Y%m%d%H%M%S')}-#{commit.sha[0..11]}" }
context 'with an invalid path' do
it_behaves_like 'a missing module version information resource', 'v1.0.3', path: '/pkg'
end
it 'returns the correct commit' do
get_resource(user)
context 'with an invalid version' do
it_behaves_like 'a missing module version information resource', 'v1.0.1', path: '/mod'
end
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to be_kind_of(Hash)
expect(json_response['Version']).to eq(version)
expect(json_response['Time']).to eq(commit.committed_date.strftime '%Y-%m-%dT%H:%M:%S.%L%:z')
context 'with a pseudo-version for v1' do
it_behaves_like 'a module pseudo-version information resource', 'v1.0.4-0.' do
let(:sha) { modules[:sha][0] }
end
end
context 'with a pseudo-version with an invalid timestamp' do
let(:module_name) { base }
let(:commit) { project.repository.commit_by(oid: modules[:sha][0]) }
let(:version) { "v1.0.4-0.00000000000000-#{commit.sha[0..11]}" }
it 'returns not found' do
get_resource(user)
context 'with a pseudo-version for v2' do
it_behaves_like 'a module pseudo-version information resource', 'v2.0.0-', path: '/v2' do
let(:sha) { modules[:sha][1] }
end
end
expect(response).to have_gitlab_http_status(:not_found)
context 'with a pseudo-version with an invalid timestamp' do
it_behaves_like 'a missing module pseudo-version information resource' do
let(:version) { "v1.0.4-0.00000000000000-#{modules[:sha][0][0..11]}" }
end
end
context 'with a pseudo-version with an invalid commit sha' do
let(:module_name) { base }
let(:commit) { project.repository.commit_by(oid: modules[:sha][0]) }
it_behaves_like 'a missing module pseudo-version information resource' do
let(:sha) { modules[:sha][0] }
let(:version) { "v1.0.4-0.#{commit.committed_date.strftime('%Y%m%d%H%M%S')}-000000000000" }
it 'returns not found' do
get_resource(user)
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'with a pseudo-version with a short commit sha' do
let(:module_name) { base }
let(:commit) { project.repository.commit_by(oid: modules[:sha][0]) }
let(:version) { "v1.0.4-0.#{commit.committed_date.strftime('%Y%m%d%H%M%S')}-#{commit.sha[0..10]}" }
it 'returns not found' do
get_resource(user)
expect(response).to have_gitlab_http_status(:not_found)
it_behaves_like 'a missing module pseudo-version information resource' do
let(:sha) { modules[:sha][0] }
let(:version) { "v1.0.4-0.#{commit.committed_date.strftime('%Y%m%d%H%M%S')}-#{modules[:sha][0][0..10]}" }
end
end
end
describe 'GET /projects/:id/packages/go/*module_name/@v/:module_version.mod' do
let(:resource) { "#{version}.mod" }
context 'with the root module v1.0.1' do
let(:module_name) { base }
let(:version) { "v1.0.1" }
it 'returns correct content' do
get_resource(user)
expect_module_version_mod(module_name)
end
it_behaves_like 'a module file resource', 'v1.0.1'
end
context 'with the submodule v1.0.3' do
let(:module_name) { "#{base}/mod" }
let(:version) { "v1.0.3" }
it 'returns correct content' do
get_resource(user)
expect_module_version_mod(module_name)
end
it_behaves_like 'a module file resource', 'v1.0.3', path: '/mod'
end
context 'with the root module v2.0.0' do
let(:module_name) { "#{base}/v2" }
let(:version) { "v2.0.0" }
it 'returns correct content' do
get_resource(user)
expect_module_version_mod(module_name)
end
it_behaves_like 'a module file resource', 'v2.0.0', path: '/v2'
end
context 'with an invalid path' do
let(:module_name) { "#{base}/pkg" }
let(:version) { "v1.0.3" }
it 'returns not found' do
get_resource(user)
expect(response).to have_gitlab_http_status(:not_found)
end
it_behaves_like 'a missing module file resource', 'v1.0.3', path: '/pkg'
end
context 'with an invalid version' do
let(:module_name) { "#{base}/mod" }
let(:version) { "v1.0.1" }
it 'returns not found' do
get_resource(user)
expect(response).to have_gitlab_http_status(:not_found)
end
it_behaves_like 'a missing module file resource', 'v1.0.1', path: '/mod'
end
end
describe 'GET /projects/:id/packages/go/*module_name/@v/:module_version.zip' do
let(:resource) { "#{version}.zip" }
context 'with the root module v1.0.1' do
let(:module_name) { base }
let(:version) { "v1.0.1" }
it 'returns a zip of everything' do
get_resource(user)
expect_module_version_zip(module_name, version, ['README.md', 'go.mod', 'a.go'])
end
it_behaves_like 'a module archive resource', 'v1.0.1', ['README.md', 'go.mod', 'a.go']
end
context 'with the root module v1.0.2' do
let(:module_name) { base }
let(:version) { "v1.0.2" }
it 'returns a zip of everything' do
get_resource(user)
expect_module_version_zip(module_name, version, ['README.md', 'go.mod', 'a.go', 'pkg/b.go'])
end
it_behaves_like 'a module archive resource', 'v1.0.2', ['README.md', 'go.mod', 'a.go', 'pkg/b.go']
end
context 'with the root module v1.0.3' do
let(:module_name) { base }
let(:version) { "v1.0.3" }
it 'returns a zip of everything, excluding the submodule' do
get_resource(user)
expect_module_version_zip(module_name, version, ['README.md', 'go.mod', 'a.go', 'pkg/b.go'])
end
it_behaves_like 'a module archive resource', 'v1.0.3', ['README.md', 'go.mod', 'a.go', 'pkg/b.go']
end
context 'with the submodule v1.0.3' do
let(:module_name) { "#{base}/mod" }
let(:version) { "v1.0.3" }
it 'returns a zip of the submodule' do
get_resource(user)
expect_module_version_zip(module_name, version, ['go.mod', 'a.go'])
end
it_behaves_like 'a module archive resource', 'v1.0.3', ['go.mod', 'a.go'], path: '/mod'
end
context 'with the root module v2.0.0' do
let(:module_name) { "#{base}/v2" }
let(:version) { "v2.0.0" }
it 'returns a zip of v2 of the root module' do
get_resource(user)
expect_module_version_zip(module_name, version, ['go.mod', 'a.go', 'x.go'])
end
it_behaves_like 'a module archive resource', 'v2.0.0', ['go.mod', 'a.go', 'x.go'], path: '/v2'
end
end
context 'with an invalid module directive' do
let_it_be(:project) { create :project_empty_repo, :public, creator: user }
let_it_be(:base) { "#{domain}/#{project.path_with_namespace}" }
let_it_be(:base) { "#{Settings.build_gitlab_go_url}/#{project.full_path}" }
# rubocop: disable Layout/IndentationWidth
let_it_be(:modules) do
create_file('a.go', "package a\nfunc Hi() { println(\"Hello world!\") }\n")
create_version(1, 0, 0, create_file('go.mod', "module not/a/real/module\n"))
create_file('v2/a.go', "package a\nfunc Hi() { println(\"Hello world!\") }\n")
create_version(2, 0, 0, create_file('v2/go.mod', "module #{base}\n"))
project.repository.head_commit
create(:go_module_commit, :files, project: project, files: { 'a.go' => "package\a" } )
create(:go_module_commit, :files, project: project, tag: 'v1.0.0', files: { 'go.mod' => "module not/a/real/module\n" })
create(:go_module_commit, :files, project: project, files: { 'v2/a.go' => "package a\n" } )
create(:go_module_commit, :files, project: project, tag: 'v2.0.0', files: { 'v2/go.mod' => "module #{base}\n" } )
end
describe 'GET /projects/:id/packages/go/*module_name/@v/list' do
let(:resource) { "list" }
context 'with a completely wrong directive for v1' do
let(:module_name) { base }
it 'returns nothing' do
get_resource(user)
expect_module_version_list
end
it_behaves_like 'a module version list resource'
end
context 'with a directive omitting the suffix for v2' do
let(:module_name) { "#{base}/v2" }
it 'returns nothing' do
get_resource(user)
expect_module_version_list
end
it_behaves_like 'a module version list resource', path: '/v2'
end
end
describe 'GET /projects/:id/packages/go/*module_name/@v/:module_version.info' do
context 'with a completely wrong directive for v1' do
let(:module_name) { base }
let(:resource) { "v1.0.0.info" }
it 'returns not found' do
get_resource(user)
expect(response).to have_gitlab_http_status(:not_found)
end
it_behaves_like 'a missing module version information resource', 'v1.0.0'
end
context 'with a directive omitting the suffix for v2' do
let(:module_name) { "#{base}/v2" }
let(:resource) { "v2.0.0.info" }
it 'returns not found' do
get_resource(user)
expect(response).to have_gitlab_http_status(:not_found)
end
it_behaves_like 'a missing module version information resource', 'v2.0.0', path: '/v2'
end
end
end
context 'with a case sensitive project and versions' do
let_it_be(:project) { create :project_empty_repo, :public, creator: user, path: 'MyGoLib' }
let_it_be(:base) { "#{domain}/#{project.path_with_namespace}" }
let_it_be(:base) { "#{Settings.build_gitlab_go_url}/#{project.full_path}" }
let_it_be(:base_encoded) { base.gsub(/[A-Z]/) { |s| "!#{s.downcase}"} }
let_it_be(:modules) do
create_file('README.md', 'Hi', commit_message: 'Add README.md')
create_version(1, 0, 1, create_module, prerelease: 'prerelease')
create_version(1, 0, 1, create_package('pkg'), prerelease: 'Prerelease')
project.repository.head_commit
create(:go_module_commit, :files, project: project, files: { 'README.md' => "Hi" })
create(:go_module_commit, :module, project: project, tag: 'v1.0.1-prerelease')
create(:go_module_commit, :package, project: project, tag: 'v1.0.1-Prerelease', path: 'pkg')
end
describe 'GET /projects/:id/packages/go/*module_name/@v/list' do
let(:resource) { "list" }
context 'with a case encoded path' do
it_behaves_like 'a module version list resource', 'v1.0.1-prerelease', 'v1.0.1-Prerelease' do
let(:module_name) { base_encoded }
it 'returns the tags' do
get_resource(user)
expect_module_version_list('v1.0.1-prerelease', 'v1.0.1-Prerelease')
end
end
context 'without a case encoded path' do
it_behaves_like 'a missing module version list resource' do
let(:module_name) { base.downcase }
it 'returns not found' do
get_resource(user)
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
describe 'GET /projects/:id/packages/go/*module_name/@v/:module_version.info' do
context 'with a case encoded path' do
it_behaves_like 'a module version information resource', 'v1.0.1-Prerelease' do
let(:module_name) { base_encoded }
let(:resource) { "v1.0.1-!prerelease.info" }
it 'returns the uppercase tag' do
get_resource(user)
expect_module_version_info('v1.0.1-Prerelease')
end
end
context 'without a case encoded path' do
it_behaves_like 'a module version information resource', 'v1.0.1-prerelease' do
let(:module_name) { base_encoded }
let(:resource) { "v1.0.1-prerelease.info" }
it 'returns the lowercase tag' do
get_resource(user)
expect_module_version_info('v1.0.1-prerelease')
end
end
end
......@@ -467,6 +369,11 @@ describe API::GoProxy do
expect(response).to have_gitlab_http_status(:ok)
end
it 'returns ok with a personal access token and basic authentication' do
get_resource(headers: build_basic_auth_header(user.username, pa_token.token))
expect(response).to have_gitlab_http_status(:ok)
end
it 'returns unauthorized with no authentication' do
get_resource
expect(response).to have_gitlab_http_status(:unauthorized)
......@@ -524,106 +431,11 @@ describe API::GoProxy do
end
end
before do
project.add_developer(user)
stub_licensed_features(packages: true)
modules
end
def get_resource(user = nil, **params)
get api("/projects/#{project.id}/packages/go/#{module_name}/@v/#{resource}", user, params)
end
def create_file(path, content, commit_message: 'Add file')
get_result("create file", Files::CreateService.new(
project,
project.owner,
commit_message: commit_message,
start_branch: 'master',
branch_name: 'master',
file_path: path,
file_content: content
).execute)
end
def create_package(path, commit_message: 'Add package')
get_result("create package '#{path}'", Files::MultiService.new(
project,
project.owner,
commit_message: commit_message,
start_branch: project.repository.root_ref,
branch_name: project.repository.root_ref,
actions: [
{ action: :create, file_path: path + '/b.go', content: "package b\nfunc Bye() { println(\"Goodbye world!\") }\n" }
]
).execute)
end
def create_module(path = '', commit_message: 'Add module')
name = "#{domain}/#{project.path_with_namespace}"
if path != ''
name += '/' + path
path += '/'
def get_resource(user = nil, headers: {}, **params)
get api("/projects/#{project.id}/packages/go/#{module_name}/@v/#{resource}", user, params), headers: headers
end
get_result("create module '#{name}'", Files::MultiService.new(
project,
project.owner,
commit_message: commit_message,
start_branch: project.repository.root_ref,
branch_name: project.repository.root_ref,
actions: [
{ action: :create, file_path: path + 'go.mod', content: "module #{name}\n" },
{ action: :create, file_path: path + 'a.go', content: "package a\nfunc Hi() { println(\"Hello world!\") }\n" }
]
).execute)
end
def create_version(major, minor, patch, sha, prerelease: nil, build: nil, tag_message: nil)
name = "v#{major}.#{minor}.#{patch}"
name += "-#{prerelease}" if prerelease
name += "+#{build}" if build
get_result("create version #{name[1..]}", Tags::CreateService.new(project, project.owner).execute(name, sha, tag_message))
end
def get_result(op, ret)
raise "#{op} failed: #{ret}" unless ret[:status] == :success
ret[:result]
end
def expect_module_version_list(*versions)
expect(response).to have_gitlab_http_status(:ok)
expect(response.body.split("\n").to_set).to eq(versions.to_set)
end
def expect_module_version_info(version)
time = project.repository.find_tag(version).dereferenced_target.committed_date
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to be_kind_of(Hash)
expect(json_response['Version']).to eq(version)
expect(json_response['Time']).to eq(time.strftime '%Y-%m-%dT%H:%M:%S.%L%:z')
end
def expect_module_version_mod(name)
expect(response).to have_gitlab_http_status(:ok)
expect(response.body.split("\n", 2).first).to eq("module #{name}")
end
def expect_module_version_zip(path, version, entries)
expect(response).to have_gitlab_http_status(:ok)
entries = entries.map { |e| "#{path}@#{version}/#{e}" }.to_set
actual = Set[]
Zip::InputStream.open(StringIO.new(response.body)) do |zip|
while (entry = zip.get_next_entry)
actual.add(entry.name)
end
end
expect(actual).to eq(entries)
def fmt_pseudo_version(prefix, commit)
"#{prefix}#{commit.committed_date.strftime('%Y%m%d%H%M%S')}-#{commit.sha[0..11]}"
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