Commit ff87c7cd authored by David Fernandez's avatar David Fernandez

Fix the maven metadata file sync

when a maven (with a version) is deleted using the UI.

This sync is baked by a background worker. Using different services, it
will:
* locate the package file for the `maven-metadata.xml`
* read the content and update it with the version that exist in the
database
* create (append) new package files for this updated xml content
parent c19699f1
...@@ -224,6 +224,12 @@ class Packages::Package < ApplicationRecord ...@@ -224,6 +224,12 @@ class Packages::Package < ApplicationRecord
end end
end end
def sync_maven_metadata(user)
return unless maven? && version? && user
::Packages::Maven::Metadata::SyncWorker.perform_async(user.id, project.id, name)
end
private private
def composer_tag_version? def composer_tag_version?
......
...@@ -2,7 +2,6 @@ ...@@ -2,7 +2,6 @@
module Packages module Packages
module Maven module Maven
class FindOrCreatePackageService < BaseService class FindOrCreatePackageService < BaseService
MAVEN_METADATA_FILE = 'maven-metadata.xml'
SNAPSHOT_TERM = '-SNAPSHOT' SNAPSHOT_TERM = '-SNAPSHOT'
def execute def execute
...@@ -33,7 +32,7 @@ module Packages ...@@ -33,7 +32,7 @@ module Packages
# - my-company/my-app/maven-metadata.xml # - my-company/my-app/maven-metadata.xml
# #
# The first upload has to create the proper package (the one with the version set). # The first upload has to create the proper package (the one with the version set).
if params[:file_name] == MAVEN_METADATA_FILE && !params[:path]&.ends_with?(SNAPSHOT_TERM) if params[:file_name] == Packages::Maven::Metadata.filename && !params[:path]&.ends_with?(SNAPSHOT_TERM)
package_name, version = params[:path], nil package_name, version = params[:path], nil
else else
package_name, _, version = params[:path].rpartition('/') package_name, _, version = params[:path].rpartition('/')
......
# frozen_string_literal: true
module Packages
module Maven
module Metadata
FILENAME = 'maven-metadata.xml'
def self.filename
FILENAME
end
end
end
end
# frozen_string_literal: true
module Packages
module Maven
module Metadata
class AppendPackageFileService
XML_CONTENT_TYPE = 'application/xml'
DEFAULT_CONTENT_TYPE = 'application/octet-stream'
MD5_FILE_NAME = "#{Metadata.filename}.md5"
SHA1_FILE_NAME = "#{Metadata.filename}.sha1"
SHA256_FILE_NAME = "#{Metadata.filename}.sha256"
SHA512_FILE_NAME = "#{Metadata.filename}.sha512"
def initialize(package:, metadata_content:)
@package = package
@metadata_content = metadata_content
end
def execute
return ServiceResponse.error(message: 'package is not set') unless @package
return ServiceResponse.error(message: 'metadata content is not set') unless @metadata_content
file_md5 = digest_from(@metadata_content, :md5)
file_sha1 = digest_from(@metadata_content, :sha1)
file_sha256 = digest_from(@metadata_content, :sha256)
file_sha512 = digest_from(@metadata_content, :sha512)
@package.transaction do
append_metadata_file(
content: @metadata_content,
file_name: Metadata.filename,
content_type: XML_CONTENT_TYPE,
sha1: file_sha1,
md5: file_md5,
sha256: file_sha256
)
append_metadata_file(content: file_md5, file_name: MD5_FILE_NAME)
append_metadata_file(content: file_sha1, file_name: SHA1_FILE_NAME)
append_metadata_file(content: file_sha256, file_name: SHA256_FILE_NAME)
append_metadata_file(content: file_sha512, file_name: SHA512_FILE_NAME)
end
ServiceResponse.success(message: 'New metadata package file created')
end
private
def append_metadata_file(content:, file_name:, content_type: DEFAULT_CONTENT_TYPE, sha1: nil, md5: nil, sha256: nil)
file_md5 = md5 || digest_from(content, :md5)
file_sha1 = sha1 || digest_from(content, :sha1)
file_sha256 = sha256 || digest_from(content, :sha256)
file = CarrierWaveStringFile.new_file(
file_content: content,
filename: file_name,
content_type: content_type
)
::Packages::CreatePackageFileService.new(
@package,
file: file,
size: file.size,
file_name: file_name,
file_sha1: file_sha1,
file_md5: file_md5,
file_sha256: file_sha256
).execute
end
def digest_from(content, type)
digest_class = case type
when :md5
Digest::MD5
when :sha1
Digest::SHA1
when :sha256
Digest::SHA256
when :sha512
Digest::SHA512
end
digest_class.hexdigest(content)
end
end
end
end
end
# frozen_string_literal: true
module Packages
module Maven
module Metadata
class CreateVersionsXmlService
include Gitlab::Utils::StrongMemoize
XPATH_VERSIONING = '//metadata/versioning'
XPATH_VERSIONS = '//versions'
XPATH_VERSION = '//version'
XPATH_LATEST = '//latest'
XPATH_RELEASE = '//release'
XPATH_LAST_UPDATED = '//lastUpdated'
INDENT_SPACE = 2
EMPTY_VERSIONS_PAYLOAD = {
changes_exist: true,
empty_versions: true
}.freeze
def initialize(metadata_content:, package:)
@metadata_content = metadata_content
@package = package
end
def execute
return ServiceResponse.error(message: 'package not set') unless @package
return ServiceResponse.error(message: 'metadata_content not set') unless @metadata_content
return ServiceResponse.error(message: 'metadata_content is invalid') unless valid_metadata_content?
return ServiceResponse.success(payload: EMPTY_VERSIONS_PAYLOAD) if versions_from_database.empty?
changes_exist = false
changes_exist = true if update_versions_list
changes_exist = true if update_latest
changes_exist = true if update_release
update_last_updated_timestamp if changes_exist
payload = { changes_exist: changes_exist, empty_versions: false }
payload[:metadata_content] = xml_doc.to_xml(indent: INDENT_SPACE) if changes_exist
ServiceResponse.success(payload: payload)
end
private
def valid_metadata_content?
versioning_xml_node.present? &&
versions_xml_node.present? &&
last_updated_xml_node.present?
end
def update_versions_list
return false if versions_from_xml == versions_from_database
version_xml_nodes.remove
versions_from_database.each do |version|
versions_xml_node.add_child(version_node_for(version))
end
true
end
def update_latest
return false if latest_coherent?
latest_xml_node.content = latest_from_database
true
end
def latest_coherent?
latest_from_xml.nil? || latest_from_xml == latest_from_database
end
def update_release
return false if release_coherent?
if release_from_database
release_xml_node.content = release_from_database
else
release_xml_node.remove
end
true
end
def release_coherent?
release_from_xml == release_from_database
end
def update_last_updated_timestamp
last_updated_xml_node.content = Time.zone.now.strftime('%Y%m%d%H%M%S')
end
def versioning_xml_node
strong_memoize(:versioning_xml_node) do
xml_doc.xpath(XPATH_VERSIONING).first
end
end
def versions_xml_node
strong_memoize(:versions_xml_node) do
versioning_xml_node&.xpath(XPATH_VERSIONS)
&.first
end
end
def version_xml_nodes
versions_xml_node&.xpath(XPATH_VERSION)
end
def latest_xml_node
strong_memoize(:latest_xml_node) do
versioning_xml_node&.xpath(XPATH_LATEST)
&.first
end
end
def release_xml_node
strong_memoize(:release_xml_node) do
versioning_xml_node&.xpath(XPATH_RELEASE)
&.first
end
end
def last_updated_xml_node
strong_memoize(:last_updated_xml_mode) do
versioning_xml_node.xpath(XPATH_LAST_UPDATED)
.first
end
end
def version_node_for(version)
Nokogiri::XML::Node.new('version', xml_doc).tap { |node| node.content = version }
end
def versions_from_xml
strong_memoize(:versions_from_xml) do
versions_xml_node.xpath(XPATH_VERSION)
.map(&:text)
end
end
def latest_from_xml
latest_xml_node&.text
end
def release_from_xml
release_xml_node&.text
end
def versions_from_database
strong_memoize(:versions_from_database) do
@package.project.packages
.maven
.displayable
.with_name(@package.name)
.has_version
.order_created
.pluck_versions
end
end
def latest_from_database
versions_from_database.last
end
def release_from_database
strong_memoize(:release_from_database) do
non_snapshot_versions_from_database = versions_from_database.reject { |v| v.ends_with?('SNAPSHOT') }
non_snapshot_versions_from_database.last
end
end
def xml_doc
strong_memoize(:xml_doc) do
Nokogiri::XML(@metadata_content) do |config|
config.default_xml.noblanks
end
end
end
end
end
end
end
# frozen_string_literal: true
module Packages
module Maven
module Metadata
class SyncService < BaseContainerService
include Gitlab::Utils::StrongMemoize
alias_method :project, :container
MAX_FILE_SIZE = 10.megabytes.freeze
def execute
return error('Blank package name') unless package_name
return error('Not allowed') unless Ability.allowed?(current_user, :destroy_package, project)
return error('Non existing versionless package') unless versionless_package_for_versions
return error('Non existing metadata file for versions') unless metadata_package_file_for_versions
update_versions_xml
end
private
def update_versions_xml
return error('Metadata file for versions is too big') if metadata_package_file_for_versions.size > MAX_FILE_SIZE
metadata_package_file_for_versions.file.use_open_file do |file|
result = CreateVersionsXmlService.new(metadata_content: file, package: versionless_package_for_versions)
.execute
next result unless result.success?
next success('No changes for versions xml') unless result.payload[:changes_exist]
if result.payload[:empty_versions]
versionless_package_for_versions.destroy!
success('Versionless package for versions destroyed')
else
AppendPackageFileService.new(metadata_content: result.payload[:metadata_content], package: versionless_package_for_versions)
.execute
end
end
end
def metadata_package_file_for_versions
strong_memoize(:metadata_file_for_versions) do
versionless_package_for_versions.package_files
.with_file_name(Metadata.filename)
.recent
.first
end
end
def versionless_package_for_versions
strong_memoize(:versionless_package_for_versions) do
project.packages
.maven
.displayable
.with_name(package_name)
.with_version(nil)
.first
end
end
def package_name
params[:package_name]
end
def error(message)
ServiceResponse.error(message: message)
end
def success(message)
ServiceResponse.success(message: message)
end
end
end
end
end
...@@ -1083,6 +1083,14 @@ ...@@ -1083,6 +1083,14 @@
:weight: 1 :weight: 1
:idempotent: :idempotent:
:tags: [] :tags: []
- :name: package_repositories:packages_maven_metadata_sync
:feature_category: :package_registry
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent: true
:tags: []
- :name: package_repositories:packages_nuget_extraction - :name: package_repositories:packages_nuget_extraction
:feature_category: :package_registry :feature_category: :package_registry
:has_external_dependencies: :has_external_dependencies:
......
# frozen_string_literal: true
module Packages
module Maven
module Metadata
class SyncWorker
include ApplicationWorker
include Gitlab::Utils::StrongMemoize
queue_namespace :package_repositories
feature_category :package_registry
deduplicate :until_executing
idempotent!
loggable_arguments 2
SyncError = Class.new(StandardError)
def perform(user_id, project_id, package_name)
@user_id = user_id
@project_id = project_id
@package_name = package_name
return unless valid?
result = ::Packages::Maven::Metadata::SyncService.new(container: project, current_user: user, params: { package_name: @package_name })
.execute
if result.success?
log_extra_metadata_on_done(:message, result.message)
else
raise SyncError.new(result.message)
end
raise SyncError.new(result.message) unless result.success?
end
private
def valid?
@package_name.present? && user.present? && project.present?
end
def user
strong_memoize(:user) do
User.find_by_id(@user_id)
end
end
def project
strong_memoize(:project) do
Project.find_by_id(@project_id)
end
end
end
end
end
end
---
title: Sync the maven metadata file upon package deletion through the UI
merge_request: 55207
author:
type: fixed
...@@ -70,7 +70,11 @@ module API ...@@ -70,7 +70,11 @@ module API
package = ::Packages::PackageFinder package = ::Packages::PackageFinder
.new(user_project, params[:package_id]).execute .new(user_project, params[:package_id]).execute
destroy_conditionally!(package) destroy_conditionally!(package) do |package|
if package.destroy
package.sync_maven_metadata(current_user)
end
end
end end
end end
end end
......
...@@ -813,4 +813,45 @@ RSpec.describe Packages::Package, type: :model do ...@@ -813,4 +813,45 @@ RSpec.describe Packages::Package, type: :model do
expect(package.package_settings).to eq(group.package_settings) expect(package.package_settings).to eq(group.package_settings)
end end
end end
describe '#sync_maven_metadata' do
let_it_be(:user) { create(:user) }
let_it_be(:package) { create(:maven_package) }
subject { package.sync_maven_metadata(user) }
shared_examples 'not enqueuing a sync worker job' do
it 'does not enqueue a sync worker job' do
expect(::Packages::Maven::Metadata::SyncWorker)
.not_to receive(:perform_async)
subject
end
end
it 'enqueues a sync worker job' do
expect(::Packages::Maven::Metadata::SyncWorker)
.to receive(:perform_async).with(user.id, package.project.id, package.name)
subject
end
context 'with no user' do
let(:user) { nil }
it_behaves_like 'not enqueuing a sync worker job'
end
context 'with a versionless maven package' do
let_it_be(:package) { create(:maven_package, version: nil) }
it_behaves_like 'not enqueuing a sync worker job'
end
context 'with a non maven package' do
let_it_be(:package) { create(:npm_package) }
it_behaves_like 'not enqueuing a sync worker job'
end
end
end end
...@@ -257,6 +257,10 @@ RSpec.describe API::ProjectPackages do ...@@ -257,6 +257,10 @@ RSpec.describe API::ProjectPackages do
context 'project is private' do context 'project is private' do
let(:project) { create(:project, :private) } let(:project) { create(:project, :private) }
before do
expect(::Packages::Maven::Metadata::SyncWorker).not_to receive(:perform_async)
end
it 'returns 404 for non authenticated user' do it 'returns 404 for non authenticated user' do
delete api(package_url) delete api(package_url)
...@@ -301,6 +305,19 @@ RSpec.describe API::ProjectPackages do ...@@ -301,6 +305,19 @@ RSpec.describe API::ProjectPackages do
expect(response).to have_gitlab_http_status(:no_content) expect(response).to have_gitlab_http_status(:no_content)
end end
end end
context 'with a maven package' do
let_it_be(:package1) { create(:maven_package, project: project) }
it 'enqueues a sync worker job' do
project.add_maintainer(user)
expect(::Packages::Maven::Metadata::SyncWorker)
.to receive(:perform_async).with(user.id, project.id, package1.name)
delete api(package_url, user)
end
end
end end
end end
end end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe ::Packages::Maven::Metadata::AppendPackageFileService do
let_it_be(:package) { create(:maven_package, version: nil) }
let(:service) { described_class.new(package: package, metadata_content: content) }
let(:content) { 'test' }
describe '#execute' do
subject { service.execute }
context 'with some content' do
it 'creates all the related package files', :aggregate_failures do
expect { subject }.to change { package.package_files.count }.by(5)
expect(subject).to be_success
expect_file(metadata_file_name, with_content: content, with_content_type: 'application/xml')
expect_file("#{metadata_file_name}.md5")
expect_file("#{metadata_file_name}.sha1")
expect_file("#{metadata_file_name}.sha256")
expect_file("#{metadata_file_name}.sha512")
end
end
context 'with nil content' do
let(:content) { nil }
it_behaves_like 'returning an error service response', message: 'metadata content is not set'
end
context 'with nil package' do
let(:package) { nil }
it_behaves_like 'returning an error service response', message: 'package is not set'
end
def expect_file(file_name, with_content: nil, with_content_type: '')
package_file = package.package_files.recent.with_file_name(file_name).first
expect(package_file.file).to be_present
expect(package_file.file_name).to eq(file_name)
expect(package_file.size).to be > 0
expect(package_file.file_md5).to be_present
expect(package_file.file_sha1).to be_present
expect(package_file.file_sha256).to be_present
expect(package_file.file.content_type).to eq(with_content_type)
if with_content
expect(package_file.file.read).to eq(with_content)
end
end
def metadata_file_name
::Packages::Maven::Metadata.filename
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe ::Packages::Maven::Metadata::CreateVersionsXmlService do
let_it_be(:package) { create(:maven_package, version: nil) }
let(:versions_in_database) { %w[1.3 2.0-SNAPSHOT 1.6 1.4 1.5-SNAPSHOT] }
let(:versions_in_xml) { %w[1.3 2.0-SNAPSHOT 1.6 1.4 1.5-SNAPSHOT] }
let(:version_latest) { nil }
let(:version_release) { '1.4' }
let(:service) { described_class.new(metadata_content: metadata_xml, package: package) }
describe '#execute' do
subject { service.execute }
before do
next unless package
versions_in_database.each do |version|
create(:maven_package, name: package.name, version: version, project: package.project)
end
end
shared_examples 'returning an xml with versions in the database' do
it 'returns an metadata versions xml with versions in the database', :aggregate_failures do
result = subject
expect(result).to be_success
expect(versions_from(result.payload[:metadata_content])).to match_array(versions_in_database)
end
end
shared_examples 'returning an xml with' do |release:, latest:|
it 'returns an xml with the updated release and latest versions', :aggregate_failures do
result = subject
expect(result).to be_success
expect(result.payload[:changes_exist]).to be_truthy
xml = result.payload[:metadata_content]
expect(release_from(xml)).to eq(release)
expect(latest_from(xml)).to eq(latest)
end
end
context 'with same versions in both sides' do
it 'returns no changes', :aggregate_failures do
result = subject
expect(result).to be_success
expect(result.payload).to eq(changes_exist: false, empty_versions: false)
end
end
context 'with more versions' do
let(:additional_versions) { %w[5.5 5.6 5.7-SNAPSHOT] }
context 'in the xml side' do
let(:versions_in_xml) { versions_in_database + additional_versions }
it_behaves_like 'returning an xml with versions in the database'
end
context 'in the database side' do
let(:versions_in_database) { versions_in_xml + additional_versions }
it_behaves_like 'returning an xml with versions in the database'
end
end
context 'with completely different versions' do
let(:versions_in_database) { %w[1.0 1.1 1.2] }
let(:versions_in_xml) { %w[2.0 2.1 2.2] }
it_behaves_like 'returning an xml with versions in the database'
end
context 'with no versions in the database' do
let(:versions_in_database) { [] }
it 'returns a success', :aggregate_failures do
result = subject
expect(result).to be_success
expect(result.payload).to eq(changes_exist: true, empty_versions: true)
end
context 'with an xml without a release version' do
let(:version_release) { nil }
it 'returns a success', :aggregate_failures do
result = subject
expect(result).to be_success
expect(result.payload).to eq(changes_exist: true, empty_versions: true)
end
end
end
context 'with differences in both sides' do
let(:shared_versions) { %w[1.3 2.0-SNAPSHOT 1.6 1.4 1.5-SNAPSHOT] }
let(:additional_versions_in_xml) { %w[5.5 5.6 5.7-SNAPSHOT] }
let(:versions_in_xml) { shared_versions + additional_versions_in_xml }
let(:additional_versions_in_database) { %w[6.5 6.6 6.7-SNAPSHOT] }
let(:versions_in_database) { shared_versions + additional_versions_in_database }
it_behaves_like 'returning an xml with versions in the database'
end
context 'with a new release and latest from the database' do
let(:versions_in_database) { versions_in_xml + %w[4.1 4.2-SNAPSHOT] }
it_behaves_like 'returning an xml with', release: '4.1', latest: nil
context 'with a latest in the xml' do
let(:version_latest) { '1.6' }
it_behaves_like 'returning an xml with', release: '4.1', latest: '4.2-SNAPSHOT'
end
end
context 'with release and latest not existing in the database' do
let(:version_release) { '7.0' }
let(:version_latest) { '8.0-SNAPSHOT' }
it_behaves_like 'returning an xml with', release: '1.4', latest: '1.5-SNAPSHOT'
end
context 'with added versions in the database side no more recent than release' do
let(:versions_in_database) { versions_in_xml + %w[4.1 4.2-SNAPSHOT] }
before do
::Packages::Package.find_by(name: package.name, version: '4.1').update!(created_at: 2.weeks.ago)
::Packages::Package.find_by(name: package.name, version: '4.2-SNAPSHOT').update!(created_at: 2.weeks.ago)
end
it_behaves_like 'returning an xml with', release: '1.4', latest: nil
context 'with a latest in the xml' do
let(:version_latest) { '1.6' }
it_behaves_like 'returning an xml with', release: '1.4', latest: '1.5-SNAPSHOT'
end
end
context 'only snapshot versions are in the database' do
let(:versions_in_database) { %w[4.2-SNAPSHOT] }
it_behaves_like 'returning an xml with', release: nil, latest: nil
it 'returns an xml without any release element' do
result = subject
xml_doc = Nokogiri::XML(result.payload[:metadata_content])
expect(xml_doc.xpath('//metadata/versioning/release')).to be_empty
end
end
context 'last updated timestamp' do
let(:versions_in_database) { versions_in_xml + %w[4.1 4.2-SNAPSHOT] }
it 'updates the last updated timestamp' do
original = last_updated_from(metadata_xml)
result = subject
expect(result).to be_success
expect(original).not_to eq(last_updated_from(result.payload[:metadata_content]))
end
end
context 'with an incomplete metadata content' do
let(:metadata_xml) { '<metadata></metadata>' }
it_behaves_like 'returning an error service response', message: 'metadata_content is invalid'
end
context 'with an invalid metadata content' do
let(:metadata_xml) { '<meta></metadata>' }
it_behaves_like 'returning an error service response', message: 'metadata_content is invalid'
end
context 'with metadata content pointing to a file' do
let(:service) { described_class.new(metadata_content: file, package: package) }
let(:file) do
Tempfile.new('metadata').tap do |file|
if file_contents
file.write(file_contents)
file.flush
file.rewind
end
end
end
after do
file.close
file.unlink
end
context 'with valid content' do
let(:file_contents) { metadata_xml }
it 'returns no changes' do
result = subject
expect(result).to be_success
expect(result.payload).to eq(changes_exist: false, empty_versions: false)
end
end
context 'with invalid content' do
let(:file_contents) { '<meta></metadata>' }
it_behaves_like 'returning an error service response', message: 'metadata_content is invalid'
end
context 'with no content' do
let(:file_contents) { nil }
it_behaves_like 'returning an error service response', message: 'metadata_content is invalid'
end
end
context 'with no package' do
let(:metadata_xml) { '' }
let(:package) { nil }
it_behaves_like 'returning an error service response', message: 'package not set'
end
context 'with no metadata content' do
let(:metadata_xml) { nil }
it_behaves_like 'returning an error service response', message: 'metadata_content not set'
end
end
def metadata_xml
Nokogiri::XML::Builder.new do |xml|
xml.metadata do
xml.groupId(package.maven_metadatum.app_group)
xml.artifactId(package.maven_metadatum.app_name)
xml.versioning do
xml.release(version_release) if version_release
xml.latest(version_latest) if version_latest
xml.lastUpdated('20210113130531')
xml.versions do
versions_in_xml.each do |version|
xml.version(version)
end
end
end
end
end.to_xml
end
def versions_from(xml_content)
doc = Nokogiri::XML(xml_content)
doc.xpath('//metadata/versioning/versions/version').map(&:content)
end
def release_from(xml_content)
doc = Nokogiri::XML(xml_content)
doc.xpath('//metadata/versioning/release').first&.content
end
def latest_from(xml_content)
doc = Nokogiri::XML(xml_content)
doc.xpath('//metadata/versioning/latest').first&.content
end
def last_updated_from(xml_content)
doc = Nokogiri::XML(xml_content)
doc.xpath('//metadata/versioning/lastUpdated').first.content
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe ::Packages::Maven::Metadata::SyncService do
using RSpec::Parameterized::TableSyntax
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user) }
let_it_be_with_reload(:versionless_package_for_versions) { create(:maven_package, name: 'test', version: nil, project: project) }
let_it_be_with_reload(:metadata_file_for_versions) { create(:package_file, :xml, package: versionless_package_for_versions) }
let(:service) { described_class.new(container: project, current_user: user, params: { package_name: versionless_package_for_versions.name }) }
describe '#execute' do
let(:create_versions_xml_service_double) { double(::Packages::Maven::Metadata::CreateVersionsXmlService, execute: create_versions_xml_service_response) }
let(:append_package_file_service_double) { double(::Packages::Maven::Metadata::AppendPackageFileService, execute: append_package_file_service_response) }
let(:create_versions_xml_service_response) { ServiceResponse.success(payload: { changes_exist: true, empty_versions: false, metadata_content: 'test' }) }
let(:append_package_file_service_response) { ServiceResponse.success(message: 'New metadata package files created') }
subject { service.execute }
before do
allow(::Packages::Maven::Metadata::CreateVersionsXmlService)
.to receive(:new).with(metadata_content: an_instance_of(ObjectStorage::Concern::OpenFile), package: versionless_package_for_versions).and_return(create_versions_xml_service_double)
allow(::Packages::Maven::Metadata::AppendPackageFileService)
.to receive(:new).with(metadata_content: an_instance_of(String), package: versionless_package_for_versions).and_return(append_package_file_service_double)
end
context 'permissions' do
where(:role, :expected_result) do
:anonymous | :rejected
:developer | :rejected
:maintainer | :accepted
end
with_them do
if params[:role] == :anonymous
let_it_be(:user) { nil }
end
before do
project.send("add_#{role}", user) unless role == :anonymous
end
if params[:expected_result] == :rejected
it_behaves_like 'returning an error service response', message: 'Not allowed'
else
it_behaves_like 'returning a success service response', message: 'New metadata package files created'
end
end
end
context 'with a maintainer' do
before do
project.add_maintainer(user)
end
context 'with no changes' do
let(:create_versions_xml_service_response) { ServiceResponse.success(payload: { changes_exist: false }) }
before do
expect(::Packages::Maven::Metadata::AppendPackageFileService).not_to receive(:new)
end
it_behaves_like 'returning a success service response', message: 'No changes for versions xml'
end
context 'with changes' do
let(:create_versions_xml_service_response) { ServiceResponse.success(payload: { changes_exist: true, empty_versions: false, metadata_content: 'new metadata' }) }
it_behaves_like 'returning a success service response', message: 'New metadata package files created'
context 'with empty versions' do
let(:create_versions_xml_service_response) { ServiceResponse.success(payload: { changes_exist: true, empty_versions: true }) }
before do
expect(service.send(:versionless_package_for_versions)).to receive(:destroy!)
expect(::Packages::Maven::Metadata::AppendPackageFileService).not_to receive(:new)
end
it_behaves_like 'returning a success service response', message: 'Versionless package for versions destroyed'
end
end
context 'with a too big maven metadata file for versions' do
before do
metadata_file_for_versions.update!(size: 100.megabytes)
end
it_behaves_like 'returning an error service response', message: 'Metadata file for versions is too big'
end
context 'an error from the create versions xml service' do
let(:create_versions_xml_service_response) { ServiceResponse.error(message: 'metadata_content is invalid') }
before do
expect(::Packages::Maven::Metadata::AppendPackageFileService).not_to receive(:new)
end
it_behaves_like 'returning an error service response', message: 'metadata_content is invalid'
end
context 'an error from the append package file service' do
let(:append_package_file_service_response) { ServiceResponse.error(message: 'metadata content is not set') }
it_behaves_like 'returning an error service response', message: 'metadata content is not set'
end
context 'without a package name' do
let(:service) { described_class.new(container: project, current_user: user, params: { package_name: nil }) }
before do
expect(::Packages::Maven::Metadata::AppendPackageFileService).not_to receive(:new)
expect(::Packages::Maven::Metadata::CreateVersionsXmlService).not_to receive(:new)
end
it_behaves_like 'returning an error service response', message: 'Blank package name'
end
context 'without a versionless package for version' do
before do
versionless_package_for_versions.update!(version: '2.2.2')
expect(::Packages::Maven::Metadata::AppendPackageFileService).not_to receive(:new)
expect(::Packages::Maven::Metadata::CreateVersionsXmlService).not_to receive(:new)
end
it_behaves_like 'returning an error service response', message: 'Non existing versionless package'
end
context 'without a metadata package file for versions' do
before do
versionless_package_for_versions.package_files.update_all(file_name: 'test.txt')
expect(::Packages::Maven::Metadata::AppendPackageFileService).not_to receive(:new)
expect(::Packages::Maven::Metadata::CreateVersionsXmlService).not_to receive(:new)
end
it_behaves_like 'returning an error service response', message: 'Non existing metadata file for versions'
end
context 'without a project' do
let(:service) { described_class.new(container: nil, current_user: user, params: { package_name: versionless_package_for_versions.name }) }
before do
expect(::Packages::Maven::Metadata::AppendPackageFileService).not_to receive(:new)
expect(::Packages::Maven::Metadata::CreateVersionsXmlService).not_to receive(:new)
end
it_behaves_like 'returning an error service response', message: 'Not allowed'
end
end
end
end
# frozen_string_literal: true
RSpec.shared_examples 'returning an error service response' do |message: nil|
it 'returns an error service response' do
result = subject
expect(result).to be_error
if message
expect(result.message).to eq(message)
end
end
end
RSpec.shared_examples 'returning a success service response' do |message: nil|
it 'returns a success service response' do
result = subject
expect(result).to be_success
if message
expect(result.message).to eq(message)
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Packages::Maven::Metadata::SyncWorker, type: :worker do
let_it_be(:versionless_package_for_versions) { create(:maven_package, name: 'MyDummyMavenPkg', version: nil) }
let_it_be(:metadata_package_file) { create(:package_file, :xml, package: versionless_package_for_versions) }
let(:versions) { %w[1.2 1.1 2.1 3.0-SNAPSHOT] }
let(:worker) { described_class.new }
describe '#perform' do
let(:user) { create(:user) }
let(:project) { versionless_package_for_versions.project }
let(:package_name) { versionless_package_for_versions.name }
let(:role) { :maintainer }
let(:most_recent_metadata_file_for_versions) { versionless_package_for_versions.package_files.recent.with_file_name(Packages::Maven::Metadata.filename).first }
before do
project.send("add_#{role}", user)
end
subject { worker.perform(user.id, project.id, package_name) }
context 'with a valid package name' do
before do
file = CarrierWaveStringFile.new_file(file_content: versions_xml_content, filename: 'maven-metadata.xml', content_type: 'application/xml')
metadata_package_file.update!(file: file)
versions.each do |version|
create(:maven_package, name: versionless_package_for_versions.name, version: version, project: versionless_package_for_versions.project)
end
end
context 'idempotent worker' do
include_examples 'an idempotent worker' do
let(:job_args) { [user.id, project.id, package_name] }
it 'creates the updated metadata files', :aggregate_failures do
expect { subject }.to change { ::Packages::PackageFile.count }.by(5)
most_recent_versions = versions_from(most_recent_metadata_file_for_versions.file.read)
expect(most_recent_versions.latest).to eq('3.0-SNAPSHOT')
expect(most_recent_versions.release).to eq('2.1')
expect(most_recent_versions.versions).to match_array(versions)
end
end
end
it 'logs the message from the service' do
expect(worker).to receive(:log_extra_metadata_on_done).with(:message, 'New metadata package file created')
subject
end
context 'not in the passed project' do
let(:project) { create(:project) }
it 'does not create the updated metadata files' do
expect { subject }
.to change { ::Packages::PackageFile.count }.by(0)
.and raise_error(described_class::SyncError, 'Non existing versionless package')
end
end
context 'with a user with not enough permissions' do
let(:role) { :guest }
it 'does not create the updated metadata files' do
expect { subject }
.to change { ::Packages::PackageFile.count }.by(0)
.and raise_error(described_class::SyncError, 'Not allowed')
end
end
end
context 'with no package name' do
subject { worker.perform(user.id, project.id, nil) }
it 'does not run' do
expect(::Packages::Maven::Metadata::SyncService).not_to receive(:new)
expect { subject }.not_to change { ::Packages::PackageFile.count }
end
end
context 'with no user id' do
subject { worker.perform(nil, project.id, package_name) }
it 'does not run' do
expect(::Packages::Maven::Metadata::SyncService).not_to receive(:new)
expect { subject }.not_to change { ::Packages::PackageFile.count }
end
end
context 'with no project id' do
subject { worker.perform(user.id, nil, package_name) }
it 'does not run' do
expect(::Packages::Maven::Metadata::SyncService).not_to receive(:new)
expect { subject }.not_to change { ::Packages::PackageFile.count }
end
end
end
def versions_from(xml_content)
xml_doc = Nokogiri::XML(xml_content)
OpenStruct.new(
release: xml_doc.xpath('//metadata/versioning/release').first.content,
latest: xml_doc.xpath('//metadata/versioning/latest').first.content,
versions: xml_doc.xpath('//metadata/versioning/versions/version').map(&:content)
)
end
def versions_xml_content
Nokogiri::XML::Builder.new do |xml|
xml.metadata do
xml.groupId(versionless_package_for_versions.maven_metadatum.app_group)
xml.artifactId(versionless_package_for_versions.maven_metadatum.app_name)
xml.versioning do
xml.release('1.3')
xml.latest('1.3')
xml.lastUpdated('20210113130531')
xml.versions do
xml.version('1.1')
xml.version('1.2')
xml.version('1.3')
end
end
end
end.to_xml
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