Commit 71a89b9e authored by Steve Abrams's avatar Steve Abrams Committed by David Fernandez

Gem metadata extraction

Takes a gem file and extracts the metadata
parent 55959477
...@@ -82,6 +82,10 @@ export const PACKAGE_TYPES = [ ...@@ -82,6 +82,10 @@ export const PACKAGE_TYPES = [
title: s__('PackageRegistry|PyPI'), title: s__('PackageRegistry|PyPI'),
type: PackageType.PYPI, type: PackageType.PYPI,
}, },
{
title: s__('PackageRegistry|RubyGems'),
type: PackageType.RUBYGEMS,
},
]; ];
export const LIST_TITLE_TEXT = s__('PackageRegistry|Package Registry'); export const LIST_TITLE_TEXT = s__('PackageRegistry|Package Registry');
......
...@@ -7,6 +7,7 @@ export const PackageType = { ...@@ -7,6 +7,7 @@ export const PackageType = {
NUGET: 'nuget', NUGET: 'nuget',
PYPI: 'pypi', PYPI: 'pypi',
COMPOSER: 'composer', COMPOSER: 'composer',
RUBYGEMS: 'rubygems',
GENERIC: 'generic', GENERIC: 'generic',
}; };
......
...@@ -19,6 +19,8 @@ export const getPackageTypeLabel = (packageType) => { ...@@ -19,6 +19,8 @@ export const getPackageTypeLabel = (packageType) => {
return s__('PackageType|NuGet'); return s__('PackageType|NuGet');
case PackageType.PYPI: case PackageType.PYPI:
return s__('PackageType|PyPI'); return s__('PackageType|PyPI');
case PackageType.RUBYGEMS:
return s__('PackageType|RubyGems');
case PackageType.COMPOSER: case PackageType.COMPOSER:
return s__('PackageType|Composer'); return s__('PackageType|Composer');
case PackageType.GENERIC: case PackageType.GENERIC:
......
# frozen_string_literal: true
module Packages
module Rubygems
class CreateDependenciesService
include BulkInsertSafe
def initialize(package, gemspec)
@package = package
@gemspec = gemspec
end
def execute
set_dependencies
end
private
attr_reader :package, :gemspec
def set_dependencies
Packages::Dependency.transaction do
dependency_type_rows = gemspec.dependencies.map do |dependency|
dependency = Packages::Dependency.safe_find_or_create_by!(
name: dependency.name,
version_pattern: dependency.requirement.to_s
)
{
dependency_id: dependency.id,
package_id: package.id,
dependency_type: :dependencies
}
end
package.dependency_links.upsert_all(
dependency_type_rows,
unique_by: %i[package_id dependency_id dependency_type]
)
end
end
end
end
end
# frozen_string_literal: true
module Packages
module Rubygems
class CreateGemspecService
def initialize(package, gemspec)
@package = package
@gemspec = gemspec
end
def execute
write_gemspec_to_file
end
private
attr_reader :package, :gemspec
def write_gemspec_to_file
file = Tempfile.new
begin
content = gemspec.to_ruby
file.write(content)
file.flush
package.package_files.create!(
file: file,
size: file.size,
file_name: "#{gemspec.name}.gemspec",
file_sha1: Digest::SHA1.hexdigest(content),
file_md5: Digest::MD5.hexdigest(content),
file_sha256: Digest::SHA256.hexdigest(content)
)
ensure
file.close
file.unlink
end
end
end
end
end
# frozen_string_literal: true
module Packages
module Rubygems
class MetadataExtractionService
def initialize(package, gemspec)
@package = package
@gemspec = gemspec
end
def execute
write_metadata
end
private
attr_reader :package, :gemspec
# rubocop:disable Metrics/AbcSize
# rubocop:disable Metrics/PerceivedComplexity
# rubocop:disable Metrics/CyclomaticComplexity
def write_metadata
metadatum.update!(
authors: gemspec&.authors,
files: gemspec&.files&.to_json,
summary: gemspec&.summary,
description: gemspec&.description,
email: gemspec&.email,
homepage: gemspec&.homepage,
licenses: gemspec&.licenses&.to_json,
metadata: gemspec&.metadata&.to_json,
author: gemspec&.author,
bindir: gemspec&.bindir,
executables: gemspec&.executables&.to_json,
extensions: gemspec&.extensions&.to_json,
extra_rdoc_files: gemspec&.extra_rdoc_files&.to_json,
platform: gemspec&.platform,
post_install_message: gemspec&.post_install_message,
rdoc_options: gemspec&.rdoc_options&.to_json,
require_paths: gemspec&.require_paths&.to_json,
required_ruby_version: gemspec&.required_ruby_version&.to_s,
required_rubygems_version: gemspec&.required_rubygems_version&.to_s,
requirements: gemspec&.requirements&.to_json,
rubygems_version: gemspec&.rubygems_version
)
end
# rubocop:enable Metrics/AbcSize
# rubocop:enable Metrics/PerceivedComplexity
# rubocop:enable Metrics/CyclomaticComplexity
def metadatum
Packages::Rubygems::Metadatum.safe_find_or_create_by!(package: package)
end
end
end
end
# frozen_string_literal: true
require 'rubygems/package'
module Packages
module Rubygems
class ProcessGemService
include Gitlab::Utils::StrongMemoize
include ExclusiveLeaseGuard
ExtractionError = Class.new(StandardError)
DEFAULT_LEASE_TIMEOUT = 1.hour.to_i.freeze
def initialize(package_file)
@package_file = package_file
end
def execute
return success if process_gem
error('Gem was not processed')
end
private
attr_reader :package_file
def process_gem
return false unless package_file
try_obtain_lease do
package.transaction do
rename_package_and_set_version
rename_package_file
::Packages::Rubygems::MetadataExtractionService.new(package, gemspec).execute
::Packages::Rubygems::CreateGemspecService.new(package, gemspec).execute
::Packages::Rubygems::CreateDependenciesService.new(package, gemspec).execute
cleanup_temp_package
end
end
true
end
def rename_package_and_set_version
package.update!(
name: gemspec.name,
version: gemspec.version,
status: :default
)
end
def rename_package_file
# Updating file_name updates the path where the file is stored.
# We must pass the file again so that CarrierWave can handle the update
package_file.update!(
file_name: "#{gemspec.name}-#{gemspec.version}.gem",
file: package_file.file,
package_id: package.id
)
end
def cleanup_temp_package
temp_package.destroy if package.id != temp_package.id
end
def gemspec
strong_memoize(:gemspec) do
gem.spec
end
end
def success
ServiceResponse.success(payload: { package: package })
end
def error(message)
ServiceResponse.error(message: message)
end
def temp_package
strong_memoize(:temp_package) do
package_file.package
end
end
def package
strong_memoize(:package) do
# if package with name/version already exists, use that package
package = temp_package.project
.packages
.rubygems
.with_name(gemspec.name)
.with_version(gemspec.version.to_s)
.last
package || temp_package
end
end
def gem
# use_file will set an exclusive lease on the file for as long as
# the resulting gem object is being used. This means we are not
# able to rename the package_file while also using the gem object.
# We need to use a separate AR object to create the gem file to allow
# `package_file` to be free for update so we re-find the file here.
Packages::PackageFile.find(package_file.id).file.use_file do |file_path|
Gem::Package.new(File.open(file_path))
end
rescue
raise ExtractionError.new('Unable to read gem file')
end
# used by ExclusiveLeaseGuard
def lease_key
"packages:rubygems:process_gem_service:package:#{package.id}"
end
# used by ExclusiveLeaseGuard
def lease_timeout
DEFAULT_LEASE_TIMEOUT
end
end
end
end
...@@ -1107,6 +1107,14 @@ ...@@ -1107,6 +1107,14 @@
:weight: 1 :weight: 1
:idempotent: :idempotent:
:tags: [] :tags: []
- :name: package_repositories:packages_rubygems_extraction
:feature_category: :package_registry
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent: true
:tags: []
- :name: pipeline_background:archive_trace - :name: pipeline_background:archive_trace
:feature_category: :continuous_integration :feature_category: :continuous_integration
:has_external_dependencies: :has_external_dependencies:
......
# frozen_string_literal: true
module Packages
module Rubygems
class ExtractionWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
queue_namespace :package_repositories
feature_category :package_registry
deduplicate :until_executing
idempotent!
def perform(package_file_id)
package_file = ::Packages::PackageFile.find_by_id(package_file_id)
return unless package_file
::Packages::Rubygems::ProcessGemService.new(package_file).execute
rescue ::Packages::Rubygems::ProcessGemService::ExtractionError => e
Gitlab::ErrorTracking.log_exception(e, project_id: package_file.project_id)
package_file.package.destroy!
end
end
end
end
---
title: Update RubyGems metadata constraints and add gem metadata extraction
merge_request: 53673
author:
type: changed
# frozen_string_literal: true
class UpdateRubygemsMetadataMetadata < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
remove_text_limit :packages_rubygems_metadata, :metadata
add_text_limit :packages_rubygems_metadata, :metadata, 30000
end
def down
remove_text_limit :packages_rubygems_metadata, :metadata
add_text_limit :packages_rubygems_metadata, :metadata, 255, validate: false
end
end
18d64af208338baec9d56a6ac9d7fc35aaeb79d3f8036d3cf5bcc72879827299
\ No newline at end of file
...@@ -15658,7 +15658,7 @@ CREATE TABLE packages_rubygems_metadata ( ...@@ -15658,7 +15658,7 @@ CREATE TABLE packages_rubygems_metadata (
CONSTRAINT check_b7b296b420 CHECK ((char_length(author) <= 255)), CONSTRAINT check_b7b296b420 CHECK ((char_length(author) <= 255)),
CONSTRAINT check_bf16b21a47 CHECK ((char_length(rdoc_options) <= 255)), CONSTRAINT check_bf16b21a47 CHECK ((char_length(rdoc_options) <= 255)),
CONSTRAINT check_ca641a3354 CHECK ((char_length(required_ruby_version) <= 255)), CONSTRAINT check_ca641a3354 CHECK ((char_length(required_ruby_version) <= 255)),
CONSTRAINT check_ea02f4800f CHECK ((char_length(metadata) <= 255)), CONSTRAINT check_ea02f4800f CHECK ((char_length(metadata) <= 30000)),
CONSTRAINT check_f76bad1a9a CHECK ((char_length(require_paths) <= 255)) CONSTRAINT check_f76bad1a9a CHECK ((char_length(require_paths) <= 255))
); );
...@@ -99,6 +99,8 @@ module API ...@@ -99,6 +99,8 @@ module API
track_package_event('push_package', :rubygems) track_package_event('push_package', :rubygems)
package_file = nil
ActiveRecord::Base.transaction do ActiveRecord::Base.transaction do
package = ::Packages::CreateTemporaryPackageService.new( package = ::Packages::CreateTemporaryPackageService.new(
user_project, current_user, declared_params.merge(build: current_authenticated_job) user_project, current_user, declared_params.merge(build: current_authenticated_job)
...@@ -109,12 +111,18 @@ module API ...@@ -109,12 +111,18 @@ module API
file_name: PACKAGE_FILENAME file_name: PACKAGE_FILENAME
} }
::Packages::CreatePackageFileService.new( package_file = ::Packages::CreatePackageFileService.new(
package, file_params.merge(build: current_authenticated_job) package, file_params.merge(build: current_authenticated_job)
).execute ).execute
end end
created! if package_file
::Packages::Rubygems::ExtractionWorker.perform_async(package_file.id) # rubocop:disable CodeReuse/Worker
created!
else
bad_request!('Package creation failed')
end
rescue ObjectStorage::RemoteStoreError => e rescue ObjectStorage::RemoteStoreError => e
Gitlab::ErrorTracking.track_exception(e, extra: { file_name: params[:file_name], project_id: user_project.id }) Gitlab::ErrorTracking.track_exception(e, extra: { file_name: params[:file_name], project_id: user_project.id })
......
...@@ -22051,6 +22051,9 @@ msgstr "" ...@@ -22051,6 +22051,9 @@ msgstr ""
msgid "PackageRegistry|Remove package" msgid "PackageRegistry|Remove package"
msgstr "" msgstr ""
msgid "PackageRegistry|RubyGems"
msgstr ""
msgid "PackageRegistry|Settings for Maven packages" msgid "PackageRegistry|Settings for Maven packages"
msgstr "" msgstr ""
...@@ -22147,6 +22150,9 @@ msgstr "" ...@@ -22147,6 +22150,9 @@ msgstr ""
msgid "PackageType|PyPI" msgid "PackageType|PyPI"
msgstr "" msgstr ""
msgid "PackageType|RubyGems"
msgstr ""
msgid "PackageType|npm" msgid "PackageType|npm"
msgstr "" msgstr ""
......
...@@ -36,8 +36,8 @@ FactoryBot.define do ...@@ -36,8 +36,8 @@ FactoryBot.define do
package_type { :rubygems } package_type { :rubygems }
after :create do |package| after :create do |package|
create :package_file, :gem, package: package create :package_file, package.processing? ? :unprocessed_gem : :gem, package: package
create :package_file, :gemspec, package: package create :package_file, :gemspec, package: package unless package.processing?
end end
trait(:with_metadatum) do trait(:with_metadatum) do
......
...@@ -247,6 +247,14 @@ FactoryBot.define do ...@@ -247,6 +247,14 @@ FactoryBot.define do
size { 4.kilobytes } size { 4.kilobytes }
end end
trait(:unprocessed_gem) do
package
file_fixture { 'spec/fixtures/packages/rubygems/package.gem' }
file_name { 'package.gem' }
file_sha1 { '5fe852b2a6abd96c22c11fa1ff2fb19d9ce58b57' }
size { 4.kilobytes }
end
trait(:gemspec) do trait(:gemspec) do
package package
file_fixture { 'spec/fixtures/packages/rubygems/package.gemspec' } file_fixture { 'spec/fixtures/packages/rubygems/package.gemspec' }
......
...@@ -23,7 +23,7 @@ ...@@ -23,7 +23,7 @@
}, },
"packageType": { "packageType": {
"type": ["string"], "type": ["string"],
"enum": ["MAVEN", "NPM", "CONAN", "NUGET", "PYPI", "COMPOSER", "GENERIC", "GOLANG", "DEBIAN"] "enum": ["MAVEN", "NPM", "CONAN", "NUGET", "PYPI", "COMPOSER", "GENERIC", "GOLANG", "RUBYGEMS", "DEBIAN"]
}, },
"tags": { "tags": {
"type": "object", "type": "object",
......
# frozen_string_literal: true # frozen_string_literal: true
Gem::Specification.new do |s| Gem::Specification.new do |s|
s.name = %q{package} s.name = 'package'
s.authors = ["Tanuki Steve"] s.authors = ['Tanuki Steve', 'Hal 9000']
s.version = "0.0.1" s.author = 'Tanuki Steve'
s.date = %q{2011-09-29} s.version = '0.0.1'
s.summary = %q{package is the best} s.date = '2011-09-29'
s.files = [ s.summary = 'package is the best'
"lib/package.rb" s.files = ['lib/test_gem.rb']
] s.require_paths = ['lib']
s.description = 'A test package for GitLab.'
s.email = 'tanuki@not_real.com'
s.homepage = 'https://gitlab.com/ruby-co/my-package'
s.license = 'MIT'
s.metadata = {
'bug_tracker_uri' => 'https://gitlab.com/ruby-co/my-package/issues',
'changelog_uri' => 'https://gitlab.com/ruby-co/my-package/CHANGELOG.md',
'documentation_uri' => 'https://gitlab.com/ruby-co/my-package/docs',
'mailing_list_uri' => 'https://gitlab.com/ruby-co/my-package/mailme',
'source_code_uri' => 'https://gitlab.com/ruby-co/my-package'
}
s.bindir = 'bin'
s.executables = ['rake']
s.extensions = ['ext/foo.rb']
s.extra_rdoc_files = ['README.md', 'doc/userguide.md']
s.platform = Gem::Platform::RUBY
s.post_install_message = 'Installed, thank you!'
s.rdoc_options = ['--main', 'README.md']
s.required_ruby_version = '>= 2.7.0' s.required_ruby_version = '>= 2.7.0'
s.rubygems_version = '>= 1.8.11' s.required_rubygems_version = '>= 1.8.11'
s.require_paths = ["lib"] s.requirements = 'A high powered server or calculator'
s.rubygems_version = '1.8.09'
s.add_dependency 'dependency_1', '~> 1.2.3'
s.add_dependency 'dependency_2', '3.0.0'
s.add_dependency 'dependency_3', '>= 1.0.0'
s.add_dependency 'dependency_4'
end end
...@@ -27,6 +27,7 @@ import { ...@@ -27,6 +27,7 @@ import {
mockPipelineInfo, mockPipelineInfo,
mavenPackage as packageWithoutBuildInfo, mavenPackage as packageWithoutBuildInfo,
pypiPackage, pypiPackage,
rubygemsPackage,
} from '../../mock_data'; } from '../../mock_data';
import { import {
generateMavenCommand, generateMavenCommand,
...@@ -104,6 +105,7 @@ describe('Getters PackageDetails Store', () => { ...@@ -104,6 +105,7 @@ describe('Getters PackageDetails Store', () => {
${npmPackage} | ${'npm'} ${npmPackage} | ${'npm'}
${nugetPackage} | ${'NuGet'} ${nugetPackage} | ${'NuGet'}
${pypiPackage} | ${'PyPI'} ${pypiPackage} | ${'PyPI'}
${rubygemsPackage} | ${'RubyGems'}
`(`package type`, ({ packageEntity, expectedResult }) => { `(`package type`, ({ packageEntity, expectedResult }) => {
beforeEach(() => setupState({ packageEntity })); beforeEach(() => setupState({ packageEntity }));
......
...@@ -134,6 +134,23 @@ export const nugetPackage = { ...@@ -134,6 +134,23 @@ export const nugetPackage = {
}, },
}; };
export const rubygemsPackage = {
created_at: '2015-12-10',
id: 4,
name: 'RubyGem1',
package_files: [],
package_type: 'rubygems',
project_id: 1,
tags: [],
updated_at: '2015-12-10',
version: '1.0.0',
rubygems_metadatum: {
author: 'Fake Name',
summary: 'My gem',
email: 'tanuki@fake.com',
},
};
export const pypiPackage = { export const pypiPackage = {
created_at: '2015-12-10', created_at: '2015-12-10',
id: 5, id: 5,
......
...@@ -38,6 +38,7 @@ describe('Packages shared utils', () => { ...@@ -38,6 +38,7 @@ describe('Packages shared utils', () => {
${'npm'} | ${'npm'} ${'npm'} | ${'npm'}
${'nuget'} | ${'NuGet'} ${'nuget'} | ${'NuGet'}
${'pypi'} | ${'PyPI'} ${'pypi'} | ${'PyPI'}
${'rubygems'} | ${'RubyGems'}
${'composer'} | ${'Composer'} ${'composer'} | ${'Composer'}
${'foo'} | ${null} ${'foo'} | ${null}
`(`package type`, ({ packageType, expectedResult }) => { `(`package type`, ({ packageType, expectedResult }) => {
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Packages::Rubygems::CreateDependenciesService do
include RubygemsHelpers
let_it_be(:package) { create(:rubygems_package) }
let_it_be(:package_file) { create(:package_file, :gem) }
let_it_be(:gem) { gem_from_file(package_file.file) }
let_it_be(:gemspec) { gem.spec }
let(:service) { described_class.new(package, gemspec) }
describe '#execute' do
subject { service.execute }
it 'creates dependencies', :aggregate_failures do
expect { subject }.to change { Packages::Dependency.count }.by(4)
gemspec.dependencies.each do |dependency|
persisted_dependency = Packages::Dependency.find_by(name: dependency.name)
expect(persisted_dependency.version_pattern).to eq dependency.requirement.to_s
end
end
it 'links dependencies to the package' do
expect { subject }.to change { package.dependency_links.count }.by(4)
expect(package.dependency_links.first).to be_dependencies
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Packages::Rubygems::CreateGemspecService do
include RubygemsHelpers
let_it_be(:package) { create(:rubygems_package) }
let_it_be(:package_file) { create(:package_file, :gem) }
let_it_be(:gem) { gem_from_file(package_file.file) }
let_it_be(:gemspec) { gem.spec }
let(:service) { described_class.new(package, gemspec) }
describe '#execute' do
subject { service.execute }
it 'creates a new package file', :aggregate_failures do
expect { subject }.to change { package.package_files.count }.by(1)
gemspec_file = package.package_files.find_by(file_name: "#{gemspec.name}.gemspec")
expect(gemspec_file.file).not_to be_nil
expect(gemspec_file.size).not_to be_nil
expect(gemspec_file.file_md5).not_to be_nil
expect(gemspec_file.file_sha1).not_to be_nil
expect(gemspec_file.file_sha256).not_to be_nil
end
end
end
# frozen_string_literal: true
require 'spec_helper'
require 'rubygems/package'
RSpec.describe Packages::Rubygems::MetadataExtractionService do
include RubygemsHelpers
let_it_be(:package) { create(:rubygems_package) }
let_it_be(:package_file) { create(:package_file, :gem) }
let_it_be(:gem) { gem_from_file(package_file.file) }
let_it_be(:gemspec) { gem.spec }
let(:service) { described_class.new(package, gemspec) }
describe '#execute' do
subject { service.execute }
it 'creates the metadata' do
expect { subject }.to change { Packages::Rubygems::Metadatum.count }.by(1)
end
it 'stores the metadata', :aggregate_failures do
subject
metadata = package.rubygems_metadatum
expect(metadata.authors).to eq(gemspec.authors.to_json)
expect(metadata.files).to eq(gemspec.files.to_json)
expect(metadata.summary).to eq(gemspec.summary)
expect(metadata.description).to eq(gemspec.description)
expect(metadata.email).to eq(gemspec.email)
expect(metadata.homepage).to eq(gemspec.homepage)
expect(metadata.licenses).to eq(gemspec.licenses.to_json)
expect(metadata.metadata).to eq(gemspec.metadata.to_json)
expect(metadata.author).to eq(gemspec.author)
expect(metadata.bindir).to eq(gemspec.bindir)
expect(metadata.executables).to eq(gemspec.executables.to_json)
expect(metadata.extensions).to eq(gemspec.extensions.to_json)
expect(metadata.extra_rdoc_files).to eq(gemspec.extra_rdoc_files.to_json)
expect(metadata.platform).to eq(gemspec.platform)
expect(metadata.post_install_message).to eq(gemspec.post_install_message)
expect(metadata.rdoc_options).to eq(gemspec.rdoc_options.to_json)
expect(metadata.require_paths).to eq(gemspec.require_paths.to_json)
expect(metadata.required_ruby_version).to eq(gemspec.required_ruby_version.to_s)
expect(metadata.required_rubygems_version).to eq(gemspec.required_rubygems_version.to_s)
expect(metadata.requirements).to eq(gemspec.requirements.to_json)
expect(metadata.rubygems_version).to eq(gemspec.rubygems_version)
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Packages::Rubygems::ProcessGemService do
include ExclusiveLeaseHelpers
include RubygemsHelpers
let_it_be_with_reload(:package) { create(:rubygems_package, :processing, name: 'temp_name', version: '0.0.0') }
let(:package_file) { create(:package_file, :unprocessed_gem, package: package) }
let(:gem) { gem_from_file(package_file.file) }
let(:gemspec) { gem.spec }
let(:service) { described_class.new(package_file) }
describe '#execute' do
subject { service.execute }
context 'no gem file', :aggregate_failures do
let(:package_file) { nil }
it 'returns an error' do
expect(subject.error?).to be(true)
expect(subject.message).to eq('Gem was not processed')
end
end
context 'success' do
let(:sub_service) { double }
before do
expect(Packages::Rubygems::MetadataExtractionService).to receive(:new).with(package, gemspec).and_return(sub_service)
expect(Packages::Rubygems::CreateGemspecService).to receive(:new).with(package, gemspec).and_return(sub_service)
expect(Packages::Rubygems::CreateDependenciesService).to receive(:new).with(package, gemspec).and_return(sub_service)
expect(sub_service).to receive(:execute).exactly(3).times.and_return(true)
end
it 'returns successfully', :aggregate_failures do
result = subject
expect(result.success?).to be true
expect(result.payload[:package]).to eq(package)
end
it 'updates the package name and version', :aggregate_failures do
expect(package.name).to eq('temp_name')
expect(package.version).to eq('0.0.0')
expect(package).to be_processing
subject
expect(package.reload.name).to eq('package')
expect(package.version).to eq('0.0.1')
expect(package).to be_default
end
it 'updates the package file name', :aggregate_failures do
expect(package_file.file_name).to eq('package.gem')
subject
expect(package_file.reload.file_name).to eq('package-0.0.1.gem')
end
end
context 'when the package already exists' do
let_it_be(:existing_package) { create(:rubygems_package, name: 'package', version: '0.0.1', project: package.project) }
let(:sub_service) { double }
before do
expect(Packages::Rubygems::MetadataExtractionService).to receive(:new).with(existing_package, gemspec).and_return(sub_service)
expect(Packages::Rubygems::CreateGemspecService).to receive(:new).with(existing_package, gemspec).and_return(sub_service)
expect(Packages::Rubygems::CreateDependenciesService).to receive(:new).with(existing_package, gemspec).and_return(sub_service)
expect(sub_service).to receive(:execute).exactly(3).times.and_return(true)
end
it 'assigns the package_file to the existing package and deletes the temporary package', :aggregate_failures do
expect(package).to receive(:destroy)
expect { subject }.to change { existing_package.package_files.count }.by(1)
expect(package_file.reload.package).to eq(existing_package)
end
end
context 'sub-service failure' do
before do
expect(Packages::Rubygems::MetadataExtractionService).to receive(:new).with(package, gemspec).and_raise(::Packages::Rubygems::ProcessGemService::ExtractionError.new('failure'))
end
it 'returns an error' do
expect { subject }.to raise_error(::Packages::Rubygems::ProcessGemService::ExtractionError, 'failure')
end
end
context 'bad gem file' do
before do
expect(Gem::Package).to receive(:new).and_raise(ArgumentError)
end
it 'returns an error' do
expect { subject }.to raise_error(::Packages::Rubygems::ProcessGemService::ExtractionError, 'Unable to read gem file')
end
end
context 'without obtaining an exclusive lease' do
let(:lease_key) { "packages:rubygems:process_gem_service:package:#{package.id}" }
before do
stub_exclusive_lease_taken(lease_key, timeout: 1.hour)
end
it 'does not perform the services', :aggregate_failures do
# The #use_file call triggers a separate lease on the package file being opened
# for use with the gem. We don't want to test that here, so we allow the call to proceed
expect(Gitlab::ExclusiveLease).to receive(:new).with("object_storage_migrate:Packages::PackageFile:#{package_file.id}", anything).and_call_original
expect(Packages::Rubygems::MetadataExtractionService).not_to receive(:new)
expect(Packages::Rubygems::CreateGemspecService).not_to receive(:new)
expect(Packages::Rubygems::CreateDependenciesService).not_to receive(:new)
subject
expect(package.reload.name).to eq('temp_name')
expect(package.version).to eq('0.0.0')
expect(package).to be_processing
expect(package_file.reload.file_name).to eq('package.gem')
end
end
end
end
# frozen_string_literal: true
module RubygemsHelpers
def gem_from_file(file)
full_path = File.expand_path(
Rails.root.join('spec', 'fixtures', 'packages', 'rubygems', file.filename)
)
Gem::Package.new(File.open(full_path))
end
end
...@@ -43,6 +43,8 @@ end ...@@ -43,6 +43,8 @@ end
RSpec.shared_examples 'process rubygems upload' do |user_type, status, add_member = true| RSpec.shared_examples 'process rubygems upload' do |user_type, status, add_member = true|
RSpec.shared_examples 'creates rubygems package files' do RSpec.shared_examples 'creates rubygems package files' do
it 'creates package files', :aggregate_failures do it 'creates package files', :aggregate_failures do
expect(::Packages::Rubygems::ExtractionWorker).to receive(:perform_async).once
expect { subject } expect { subject }
.to change { project.packages.count }.by(1) .to change { project.packages.count }.by(1)
.and change { Packages::PackageFile.count }.by(1) .and change { Packages::PackageFile.count }.by(1)
...@@ -51,6 +53,17 @@ RSpec.shared_examples 'process rubygems upload' do |user_type, status, add_membe ...@@ -51,6 +53,17 @@ RSpec.shared_examples 'process rubygems upload' do |user_type, status, add_membe
package_file = project.packages.last.package_files.reload.last package_file = project.packages.last.package_files.reload.last
expect(package_file.file_name).to eq('package.gem') expect(package_file.file_name).to eq('package.gem')
end end
it 'returns bad request if package creation fails' do
file_service = double('file_service', execute: nil)
expect(::Packages::CreatePackageFileService).to receive(:new).and_return(file_service)
expect(::Packages::Rubygems::ExtractionWorker).not_to receive(:perform_async)
subject
expect(response).to have_gitlab_http_status(:bad_request)
end
end end
context "for user type #{user_type}" do context "for user type #{user_type}" do
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Packages::Rubygems::ExtractionWorker, type: :worker do
describe '#perform' do
let_it_be(:package) { create(:rubygems_package) }
let(:package_file) { package.package_files.first }
let(:package_file_id) { package_file.id }
let(:package_name) { 'TempProject.TempPackage' }
let(:package_version) { '1.0.0' }
let(:job_args) { package_file_id }
subject { described_class.new.perform(*job_args) }
include_examples 'an idempotent worker' do
it 'processes the gem', :aggregate_failures do
expect { subject }
.to change { Packages::Package.count }.by(0)
.and change { Packages::PackageFile.count }.by(2)
expect(Packages::Package.last.id).to be(package.id)
expect(package.name).not_to be(package_name)
end
end
it 'handles a processing failure', :aggregate_failures do
expect(::Packages::Rubygems::ProcessGemService).to receive(:new)
.and_raise(::Packages::Rubygems::ProcessGemService::ExtractionError)
expect(Gitlab::ErrorTracking).to receive(:log_exception).with(
instance_of(::Packages::Rubygems::ProcessGemService::ExtractionError),
project_id: package.project_id
)
expect { subject }
.to change { Packages::Package.count }.by(-1)
.and change { Packages::PackageFile.count }.by(-2)
end
context 'returns when there is no package file' do
let(:package_file_id) { 999999 }
it 'returns without action' do
expect(::Packages::Rubygems::ProcessGemService).not_to receive(:new)
expect { subject }
.to change { Packages::Package.count }.by(0)
.and change { Packages::PackageFile.count }.by(0)
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