summaryrefslogtreecommitdiff
path: root/spec/services/packages
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-07-20 12:26:25 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2020-07-20 12:26:25 +0000
commita09983ae35713f5a2bbb100981116d31ce99826e (patch)
tree2ee2af7bd104d57086db360a7e6d8c9d5d43667a /spec/services/packages
parent18c5ab32b738c0b6ecb4d0df3994000482f34bd8 (diff)
downloadgitlab-ce-a09983ae35713f5a2bbb100981116d31ce99826e.tar.gz
Add latest changes from gitlab-org/gitlab@13-2-stable-ee
Diffstat (limited to 'spec/services/packages')
-rw-r--r--spec/services/packages/composer/composer_json_service_spec.rb39
-rw-r--r--spec/services/packages/composer/create_package_service_spec.rb97
-rw-r--r--spec/services/packages/composer/version_parser_service_spec.rb31
-rw-r--r--spec/services/packages/conan/create_package_file_service_spec.rb130
-rw-r--r--spec/services/packages/conan/create_package_service_spec.rb48
-rw-r--r--spec/services/packages/conan/search_service_spec.rb74
-rw-r--r--spec/services/packages/create_dependency_service_spec.rb113
-rw-r--r--spec/services/packages/create_package_file_service_spec.rb38
-rw-r--r--spec/services/packages/maven/create_package_service_spec.rb77
-rw-r--r--spec/services/packages/maven/find_or_create_package_service_spec.rb38
-rw-r--r--spec/services/packages/npm/create_package_service_spec.rb96
-rw-r--r--spec/services/packages/npm/create_tag_service_spec.rb53
-rw-r--r--spec/services/packages/nuget/create_dependency_service_spec.rb76
-rw-r--r--spec/services/packages/nuget/create_package_service_spec.rb34
-rw-r--r--spec/services/packages/nuget/metadata_extraction_service_spec.rb106
-rw-r--r--spec/services/packages/nuget/search_service_spec.rb116
-rw-r--r--spec/services/packages/nuget/sync_metadatum_service_spec.rb57
-rw-r--r--spec/services/packages/nuget/update_package_from_metadata_service_spec.rb237
-rw-r--r--spec/services/packages/pypi/create_package_service_spec.rb83
-rw-r--r--spec/services/packages/remove_tag_service_spec.rb20
-rw-r--r--spec/services/packages/update_tags_service_spec.rb59
21 files changed, 1622 insertions, 0 deletions
diff --git a/spec/services/packages/composer/composer_json_service_spec.rb b/spec/services/packages/composer/composer_json_service_spec.rb
new file mode 100644
index 00000000000..3996fcea679
--- /dev/null
+++ b/spec/services/packages/composer/composer_json_service_spec.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe Packages::Composer::ComposerJsonService do
+ describe '#execute' do
+ let(:branch) { project.repository.find_branch('master') }
+ let(:target) { branch.target }
+
+ subject { described_class.new(project, target).execute }
+
+ context 'with an existing file' do
+ let(:project) { create(:project, :custom_repo, files: { 'composer.json' => json } ) }
+
+ context 'with a valid file' do
+ let(:json) { '{ "name": "package-name"}' }
+
+ it 'returns the parsed json' do
+ expect(subject).to eq({ 'name' => 'package-name' })
+ end
+ end
+
+ context 'with an invalid file' do
+ let(:json) { '{ name": "package-name"}' }
+
+ it 'raises an error' do
+ expect { subject }.to raise_error(/Invalid/)
+ end
+ end
+ end
+
+ context 'without the composer.json file' do
+ let(:project) { create(:project, :repository) }
+
+ it 'raises an error' do
+ expect { subject }.to raise_error(/not found/)
+ end
+ end
+ end
+end
diff --git a/spec/services/packages/composer/create_package_service_spec.rb b/spec/services/packages/composer/create_package_service_spec.rb
new file mode 100644
index 00000000000..3f9da31cf6e
--- /dev/null
+++ b/spec/services/packages/composer/create_package_service_spec.rb
@@ -0,0 +1,97 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe Packages::Composer::CreatePackageService do
+ include PackagesManagerApiSpecHelpers
+
+ let_it_be(:package_name) { 'composer-package-name' }
+ let_it_be(:json) { { name: package_name }.to_json }
+ let_it_be(:project) { create(:project, :custom_repo, files: { 'composer.json' => json } ) }
+ let_it_be(:user) { create(:user) }
+ let(:params) do
+ {
+ branch: branch,
+ tag: tag
+ }
+ end
+
+ describe '#execute' do
+ let(:tag) { nil }
+ let(:branch) { nil }
+
+ subject { described_class.new(project, user, params).execute }
+
+ let(:created_package) { Packages::Package.composer.last }
+
+ context 'without an existing package' do
+ context 'with a branch' do
+ let(:branch) { project.repository.find_branch('master') }
+
+ it 'creates the package' do
+ expect { subject }
+ .to change { Packages::Package.composer.count }.by(1)
+ .and change { Packages::Composer::Metadatum.count }.by(1)
+
+ expect(created_package.name).to eq package_name
+ expect(created_package.version).to eq 'dev-master'
+ expect(created_package.composer_metadatum.target_sha).to eq branch.target
+ expect(created_package.composer_metadatum.composer_json.to_json).to eq json
+ end
+ end
+
+ context 'with a tag' do
+ let(:tag) { project.repository.find_tag('v1.2.3') }
+
+ before do
+ project.repository.add_tag(user, 'v1.2.3', 'master')
+ end
+
+ it 'creates the package' do
+ expect { subject }
+ .to change { Packages::Package.composer.count }.by(1)
+ .and change { Packages::Composer::Metadatum.count }.by(1)
+
+ expect(created_package.name).to eq package_name
+ expect(created_package.version).to eq '1.2.3'
+ end
+ end
+ end
+
+ context 'with an existing package' do
+ let(:branch) { project.repository.find_branch('master') }
+
+ context 'belonging to the same project' do
+ before do
+ described_class.new(project, user, params).execute
+ end
+
+ it 'does not create a new package' do
+ expect { subject }
+ .to change { Packages::Package.composer.count }.by(0)
+ .and change { Packages::Composer::Metadatum.count }.by(0)
+ end
+ end
+
+ context 'belonging to another project' do
+ let(:other_project) { create(:project) }
+ let!(:other_package) { create(:composer_package, name: package_name, version: 'dev-master', project: other_project) }
+
+ it 'fails with an error' do
+ expect { subject }
+ .to raise_error(/is already taken/)
+ end
+ end
+
+ context 'same name but of different type' do
+ let(:other_project) { create(:project) }
+ let!(:other_package) { create(:package, name: package_name, version: 'dev-master', project: other_project) }
+
+ it 'creates the package' do
+ expect { subject }
+ .to change { Packages::Package.composer.count }.by(1)
+ .and change { Packages::Composer::Metadatum.count }.by(1)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/packages/composer/version_parser_service_spec.rb b/spec/services/packages/composer/version_parser_service_spec.rb
new file mode 100644
index 00000000000..904c75ab0a1
--- /dev/null
+++ b/spec/services/packages/composer/version_parser_service_spec.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe Packages::Composer::VersionParserService do
+ let_it_be(:params) { {} }
+
+ describe '#execute' do
+ using RSpec::Parameterized::TableSyntax
+
+ subject { described_class.new(tag_name: tagname, branch_name: branchname).execute }
+
+ where(:tagname, :branchname, :expected_version) do
+ nil | 'master' | 'dev-master'
+ nil | 'my-feature' | 'dev-my-feature'
+ nil | 'v1' | '1.x-dev'
+ nil | 'v1.x' | '1.x-dev'
+ nil | 'v1.7.x' | '1.7.x-dev'
+ nil | 'v1.7' | '1.7.x-dev'
+ nil | '1.7.x' | '1.7.x-dev'
+ 'v1.0.0' | nil | '1.0.0'
+ 'v1.0' | nil | '1.0'
+ '1.0' | nil | '1.0'
+ '1.0.2' | nil | '1.0.2'
+ '1.0.2-beta2' | nil | '1.0.2-beta2'
+ end
+
+ with_them do
+ it { is_expected.to eq expected_version }
+ end
+ end
+end
diff --git a/spec/services/packages/conan/create_package_file_service_spec.rb b/spec/services/packages/conan/create_package_file_service_spec.rb
new file mode 100644
index 00000000000..0e9cbba5fc1
--- /dev/null
+++ b/spec/services/packages/conan/create_package_file_service_spec.rb
@@ -0,0 +1,130 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe Packages::Conan::CreatePackageFileService do
+ include WorkhorseHelpers
+
+ let_it_be(:package) { create(:conan_package) }
+
+ describe '#execute' do
+ let(:file_name) { 'foo.tgz' }
+
+ subject { described_class.new(package, file, params) }
+
+ shared_examples 'a valid package_file' do
+ let(:params) do
+ {
+ file_name: file_name,
+ 'file.md5': '12345',
+ 'file.sha1': '54321',
+ 'file.size': '128',
+ 'file.type': 'txt',
+ recipe_revision: '0',
+ package_revision: '0',
+ conan_package_reference: '123456789',
+ conan_file_type: :package_file
+ }.with_indifferent_access
+ end
+
+ it 'creates a new package file' do
+ package_file = subject.execute
+
+ expect(package_file).to be_valid
+ expect(package_file.file_name).to eq(file_name)
+ expect(package_file.file_md5).to eq('12345')
+ expect(package_file.size).to eq(128)
+ expect(package_file.conan_file_metadatum).to be_valid
+ expect(package_file.conan_file_metadatum.recipe_revision).to eq('0')
+ expect(package_file.conan_file_metadatum.package_revision).to eq('0')
+ expect(package_file.conan_file_metadatum.conan_package_reference).to eq('123456789')
+ expect(package_file.conan_file_metadatum.conan_file_type).to eq('package_file')
+ expect(package_file.file.read).to eq('content')
+ end
+ end
+
+ shared_examples 'a valid recipe_file' do
+ let(:params) do
+ {
+ file_name: file_name,
+ 'file.md5': '12345',
+ 'file.sha1': '54321',
+ 'file.size': '128',
+ 'file.type': 'txt',
+ recipe_revision: '0',
+ conan_file_type: :recipe_file
+ }.with_indifferent_access
+ end
+
+ it 'creates a new recipe file' do
+ package_file = subject.execute
+
+ expect(package_file).to be_valid
+ expect(package_file.file_name).to eq(file_name)
+ expect(package_file.file_md5).to eq('12345')
+ expect(package_file.size).to eq(128)
+ expect(package_file.conan_file_metadatum).to be_valid
+ expect(package_file.conan_file_metadatum.recipe_revision).to eq('0')
+ expect(package_file.conan_file_metadatum.package_revision).to be_nil
+ expect(package_file.conan_file_metadatum.conan_package_reference).to be_nil
+ expect(package_file.conan_file_metadatum.conan_file_type).to eq('recipe_file')
+ expect(package_file.file.read).to eq('content')
+ end
+ end
+
+ context 'with temp file' do
+ let!(:file) do
+ upload_path = ::Packages::PackageFileUploader.workhorse_local_upload_path
+ file_path = upload_path + '/' + file_name
+
+ FileUtils.mkdir_p(upload_path)
+ File.write(file_path, 'content')
+
+ UploadedFile.new(file_path, filename: File.basename(file_path))
+ end
+
+ before do
+ allow_any_instance_of(Packages::PackageFileUploader).to receive(:size).and_return(128)
+ end
+
+ it_behaves_like 'a valid package_file'
+ it_behaves_like 'a valid recipe_file'
+ end
+
+ context 'with remote file' do
+ let!(:fog_connection) do
+ stub_package_file_object_storage(direct_upload: true)
+ end
+
+ before do
+ allow_any_instance_of(Packages::PackageFileUploader).to receive(:size).and_return(128)
+ end
+
+ let(:tmp_object) do
+ fog_connection.directories.new(key: 'packages').files.create(
+ key: "tmp/uploads/#{file_name}",
+ body: 'content'
+ )
+ end
+
+ let(:file) { fog_to_uploaded_file(tmp_object) }
+
+ it_behaves_like 'a valid package_file'
+ it_behaves_like 'a valid recipe_file'
+ end
+
+ context 'file is missing' do
+ let(:file) { nil }
+ let(:params) do
+ {
+ file_name: file_name,
+ recipe_revision: '0',
+ conan_file_type: :recipe_file
+ }
+ end
+
+ it 'raises an error' do
+ expect { subject.execute }.to raise_error(ActiveRecord::RecordInvalid)
+ end
+ end
+ end
+end
diff --git a/spec/services/packages/conan/create_package_service_spec.rb b/spec/services/packages/conan/create_package_service_spec.rb
new file mode 100644
index 00000000000..f8068f6e57b
--- /dev/null
+++ b/spec/services/packages/conan/create_package_service_spec.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe Packages::Conan::CreatePackageService do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:user) { create(:user) }
+
+ subject { described_class.new(project, user, params) }
+
+ describe '#execute' do
+ context 'valid params' do
+ let(:params) do
+ {
+ package_name: 'my-pkg',
+ package_version: '1.0.0',
+ package_username: ::Packages::Conan::Metadatum.package_username_from(full_path: project.full_path),
+ package_channel: 'stable'
+ }
+ end
+
+ it 'creates a new package' do
+ package = subject.execute
+
+ expect(package).to be_valid
+ expect(package.name).to eq(params[:package_name])
+ expect(package.version).to eq(params[:package_version])
+ expect(package.package_type).to eq('conan')
+ expect(package.conan_metadatum.package_username).to eq(params[:package_username])
+ expect(package.conan_metadatum.package_channel).to eq(params[:package_channel])
+ end
+ end
+
+ context 'invalid params' do
+ let(:params) do
+ {
+ package_name: 'my-pkg',
+ package_version: '1.0.0',
+ package_username: 'foo/bar',
+ package_channel: 'stable'
+ }
+ end
+
+ it 'fails' do
+ expect { subject.execute }.to raise_exception(ActiveRecord::RecordInvalid)
+ end
+ end
+ end
+end
diff --git a/spec/services/packages/conan/search_service_spec.rb b/spec/services/packages/conan/search_service_spec.rb
new file mode 100644
index 00000000000..39d284ee088
--- /dev/null
+++ b/spec/services/packages/conan/search_service_spec.rb
@@ -0,0 +1,74 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Packages::Conan::SearchService do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project, :public) }
+ let!(:conan_package) { create(:conan_package, project: project) }
+ let!(:conan_package2) { create(:conan_package, project: project) }
+
+ subject { described_class.new(user, query: query) }
+
+ before do
+ project.add_developer(user)
+ end
+
+ describe '#execute' do
+ context 'with wildcard' do
+ let(:partial_name) { conan_package.name.first[0, 3] }
+ let(:query) { "#{partial_name}*" }
+
+ it 'makes a wildcard query' do
+ result = subject.execute
+
+ expect(result.status).to eq :success
+ expect(result.payload).to eq(results: [conan_package.conan_recipe, conan_package2.conan_recipe])
+ end
+ end
+
+ context 'with only wildcard' do
+ let(:query) { '*' }
+
+ it 'returns empty' do
+ result = subject.execute
+
+ expect(result.status).to eq :success
+ expect(result.payload).to eq(results: [])
+ end
+ end
+
+ context 'with no wildcard' do
+ let(:query) { conan_package.name }
+
+ it 'makes a search using the beginning of the recipe' do
+ result = subject.execute
+
+ expect(result.status).to eq :success
+ expect(result.payload).to eq(results: [conan_package.conan_recipe])
+ end
+ end
+
+ context 'with full recipe match' do
+ let(:query) { conan_package.conan_recipe }
+
+ it 'makes an exact search' do
+ result = subject.execute
+
+ expect(result.status).to eq :success
+ expect(result.payload).to eq(results: [conan_package.conan_recipe])
+ end
+ end
+
+ context 'with malicious query' do
+ let(:query) { 'DROP TABLE foo;' }
+
+ it 'returns empty' do
+ result = subject.execute
+
+ expect(result.status).to eq :success
+ expect(result.payload).to eq(results: [])
+ end
+ end
+ end
+end
diff --git a/spec/services/packages/create_dependency_service_spec.rb b/spec/services/packages/create_dependency_service_spec.rb
new file mode 100644
index 00000000000..00e5e5c6d96
--- /dev/null
+++ b/spec/services/packages/create_dependency_service_spec.rb
@@ -0,0 +1,113 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe Packages::CreateDependencyService do
+ describe '#execute' do
+ let_it_be(:namespace) {create(:namespace)}
+ let_it_be(:version) { '1.0.1' }
+ let_it_be(:package_name) { "@#{namespace.path}/my-app".freeze }
+
+ context 'when packages are published' do
+ let(:json_file) { 'packages/npm/payload.json' }
+ let(:params) do
+ Gitlab::Json.parse(fixture_file(json_file)
+ .gsub('@root/npm-test', package_name)
+ .gsub('1.0.1', version))
+ .with_indifferent_access
+ end
+ let(:package_version) { params[:versions].each_key.first }
+ let(:dependencies) { params[:versions][package_version] }
+ let(:package) { create(:npm_package) }
+ let(:dependency_names) { package.dependency_links.flat_map(&:dependency).map(&:name).sort }
+ let(:dependency_link_types) { package.dependency_links.map(&:dependency_type).sort }
+
+ subject { described_class.new(package, dependencies).execute }
+
+ it 'creates dependencies and links' do
+ expect(Packages::Dependency)
+ .to receive(:ids_for_package_names_and_version_patterns)
+ .once
+ .and_call_original
+
+ expect { subject }
+ .to change { Packages::Dependency.count }.by(1)
+ .and change { Packages::DependencyLink.count }.by(1)
+ expect(dependency_names).to match_array(%w(express))
+ expect(dependency_link_types).to match_array(%w(dependencies))
+ end
+
+ context 'with repeated packages' do
+ let(:json_file) { 'packages/npm/payload_with_duplicated_packages.json' }
+
+ it 'creates dependencies and links' do
+ expect(Packages::Dependency)
+ .to receive(:ids_for_package_names_and_version_patterns)
+ .exactly(4).times
+ .and_call_original
+
+ expect { subject }
+ .to change { Packages::Dependency.count }.by(4)
+ .and change { Packages::DependencyLink.count }.by(6)
+ expect(dependency_names).to match_array(%w(d3 d3 d3 dagre-d3 dagre-d3 express))
+ expect(dependency_link_types).to match_array(%w(bundleDependencies dependencies dependencies devDependencies devDependencies peerDependencies))
+ end
+ end
+
+ context 'with dependencies bulk insert conflicts' do
+ let_it_be(:rows) { [{ name: 'express', version_pattern: '^4.16.4' }] }
+
+ it 'creates dependences and links' do
+ original_bulk_insert = ::Gitlab::Database.method(:bulk_insert)
+ expect(::Gitlab::Database)
+ .to receive(:bulk_insert) do |table, rows, return_ids: false, disable_quote: [], on_conflict: nil|
+ call_count = table == Packages::Dependency.table_name ? 2 : 1
+ call_count.times { original_bulk_insert.call(table, rows, return_ids: return_ids, disable_quote: disable_quote, on_conflict: on_conflict) }
+ end.twice
+ expect(Packages::Dependency)
+ .to receive(:ids_for_package_names_and_version_patterns)
+ .twice
+ .and_call_original
+
+ expect { subject }
+ .to change { Packages::Dependency.count }.by(1)
+ .and change { Packages::DependencyLink.count }.by(1)
+ expect(dependency_names).to match_array(%w(express))
+ expect(dependency_link_types).to match_array(%w(dependencies))
+ end
+ end
+
+ context 'with existing dependencies' do
+ let(:other_package) { create(:npm_package) }
+
+ before do
+ described_class.new(other_package, dependencies).execute
+ end
+
+ it 'reuses them' do
+ expect { subject }
+ .to not_change { Packages::Dependency.count }
+ .and change { Packages::DependencyLink.count }.by(1)
+ end
+ end
+
+ context 'with a dependency not described with a hash' do
+ let(:invalid_dependencies) { dependencies.tap { |d| d['bundleDependencies'] = false } }
+
+ subject { described_class.new(package, invalid_dependencies).execute }
+
+ it 'creates dependencies and links' do
+ expect(Packages::Dependency)
+ .to receive(:ids_for_package_names_and_version_patterns)
+ .once
+ .and_call_original
+
+ expect { subject }
+ .to change { Packages::Dependency.count }.by(1)
+ .and change { Packages::DependencyLink.count }.by(1)
+ expect(dependency_names).to match_array(%w(express))
+ expect(dependency_link_types).to match_array(%w(dependencies))
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/packages/create_package_file_service_spec.rb b/spec/services/packages/create_package_file_service_spec.rb
new file mode 100644
index 00000000000..93dde54916a
--- /dev/null
+++ b/spec/services/packages/create_package_file_service_spec.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe Packages::CreatePackageFileService do
+ let(:package) { create(:maven_package) }
+
+ describe '#execute' do
+ context 'with valid params' do
+ let(:params) do
+ {
+ file: Tempfile.new,
+ file_name: 'foo.jar'
+ }
+ end
+
+ it 'creates a new package file' do
+ package_file = described_class.new(package, params).execute
+
+ expect(package_file).to be_valid
+ expect(package_file.file_name).to eq('foo.jar')
+ end
+ end
+
+ context 'file is missing' do
+ let(:params) do
+ {
+ file_name: 'foo.jar'
+ }
+ end
+
+ it 'raises an error' do
+ service = described_class.new(package, params)
+
+ expect { service.execute }.to raise_error(ActiveRecord::RecordInvalid)
+ end
+ end
+ end
+end
diff --git a/spec/services/packages/maven/create_package_service_spec.rb b/spec/services/packages/maven/create_package_service_spec.rb
new file mode 100644
index 00000000000..bfdf62008ba
--- /dev/null
+++ b/spec/services/packages/maven/create_package_service_spec.rb
@@ -0,0 +1,77 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe Packages::Maven::CreatePackageService do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+ let(:app_name) { 'my-app' }
+ let(:version) { '1.0-SNAPSHOT' }
+ let(:path) { "my/company/app/#{app_name}" }
+ let(:path_with_version) { "#{path}/#{version}" }
+
+ describe '#execute' do
+ subject(:package) { described_class.new(project, user, params).execute }
+
+ context 'with version' do
+ let(:params) do
+ {
+ path: path_with_version,
+ name: path,
+ version: version
+ }
+ end
+
+ it 'creates a new package with metadatum' do
+ expect(package).to be_valid
+ expect(package.name).to eq(path)
+ expect(package.version).to eq(version)
+ expect(package.package_type).to eq('maven')
+ expect(package.maven_metadatum).to be_valid
+ expect(package.maven_metadatum.path).to eq(path_with_version)
+ expect(package.maven_metadatum.app_group).to eq('my.company.app')
+ expect(package.maven_metadatum.app_name).to eq(app_name)
+ expect(package.maven_metadatum.app_version).to eq(version)
+ end
+
+ it_behaves_like 'assigns build to package'
+ end
+
+ context 'without version' do
+ let(:params) do
+ {
+ path: path,
+ name: path,
+ version: nil
+ }
+ end
+
+ it 'creates a new package with metadatum' do
+ package = described_class.new(project, user, params).execute
+
+ expect(package).to be_valid
+ expect(package.name).to eq(path)
+ expect(package.version).to be nil
+ expect(package.maven_metadatum).to be_valid
+ expect(package.maven_metadatum.path).to eq(path)
+ expect(package.maven_metadatum.app_group).to eq('my.company.app')
+ expect(package.maven_metadatum.app_name).to eq(app_name)
+ expect(package.maven_metadatum.app_version).to be nil
+ end
+ end
+
+ context 'path is missing' do
+ let(:params) do
+ {
+ name: path,
+ version: version
+ }
+ end
+
+ it 'raises an error' do
+ service = described_class.new(project, user, params)
+
+ expect { service.execute }.to raise_error(ActiveRecord::RecordInvalid)
+ end
+ end
+ end
+end
diff --git a/spec/services/packages/maven/find_or_create_package_service_spec.rb b/spec/services/packages/maven/find_or_create_package_service_spec.rb
new file mode 100644
index 00000000000..c9441324216
--- /dev/null
+++ b/spec/services/packages/maven/find_or_create_package_service_spec.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe Packages::Maven::FindOrCreatePackageService do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:app_name) { 'my-app' }
+ let_it_be(:version) { '1.0-SNAPSHOT' }
+ let_it_be(:path) { "my/company/app/#{app_name}" }
+ let_it_be(:path_with_version) { "#{path}/#{version}" }
+ let_it_be(:params) do
+ {
+ path: path_with_version,
+ name: path,
+ version: version
+ }
+ end
+
+ describe '#execute' do
+ subject { described_class.new(project, user, params).execute }
+
+ context 'without any existing package' do
+ it 'creates a package' do
+ expect { subject }.to change { Packages::Package.count }.by(1)
+ end
+ end
+
+ context 'with an existing package' do
+ let_it_be(:existing_package) { create(:maven_package, name: path, version: version, project: project) }
+
+ it { is_expected.to eq existing_package }
+ it "doesn't create a new package" do
+ expect { subject }
+ .to not_change { Packages::Package.count }
+ end
+ end
+ end
+end
diff --git a/spec/services/packages/npm/create_package_service_spec.rb b/spec/services/packages/npm/create_package_service_spec.rb
new file mode 100644
index 00000000000..25bbbf82bec
--- /dev/null
+++ b/spec/services/packages/npm/create_package_service_spec.rb
@@ -0,0 +1,96 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe Packages::Npm::CreatePackageService do
+ let(:namespace) {create(:namespace)}
+ let(:project) { create(:project, namespace: namespace) }
+ let(:user) { create(:user) }
+ let(:version) { '1.0.1' }
+
+ let(:params) do
+ Gitlab::Json.parse(fixture_file('packages/npm/payload.json')
+ .gsub('@root/npm-test', package_name)
+ .gsub('1.0.1', version)).with_indifferent_access
+ .merge!(override)
+ end
+ let(:override) { {} }
+ let(:package_name) { "@#{namespace.path}/my-app".freeze }
+
+ subject { described_class.new(project, user, params).execute }
+
+ shared_examples 'valid package' do
+ it 'creates a package' do
+ expect { subject }
+ .to change { Packages::Package.count }.by(1)
+ .and change { Packages::Package.npm.count }.by(1)
+ .and change { Packages::Tag.count }.by(1)
+ end
+
+ it { is_expected.to be_valid }
+
+ it 'creates a package with name and version' do
+ package = subject
+
+ expect(package.name).to eq(package_name)
+ expect(package.version).to eq(version)
+ end
+
+ it { expect(subject.name).to eq(package_name) }
+ it { expect(subject.version).to eq(version) }
+ end
+
+ describe '#execute' do
+ context 'scoped package' do
+ it_behaves_like 'valid package'
+
+ it_behaves_like 'assigns build to package'
+ end
+
+ context 'invalid package name' do
+ let(:package_name) { "@#{namespace.path}/my-group/my-app".freeze }
+
+ it { expect { subject }.to raise_error(ActiveRecord::RecordInvalid) }
+ end
+
+ context 'package already exists' do
+ let(:package_name) { "@#{namespace.path}/my_package" }
+ let!(:existing_package) { create(:npm_package, project: project, name: package_name, version: '1.0.1') }
+
+ it { expect(subject[:http_status]).to eq 403 }
+ it { expect(subject[:message]).to be 'Package already exists.' }
+ end
+
+ context 'with incorrect namespace' do
+ let(:package_name) { '@my_other_namespace/my-app' }
+
+ it 'raises a RecordInvalid error' do
+ expect { subject }.to raise_error(ActiveRecord::RecordInvalid)
+ end
+ end
+
+ context 'with empty versions' do
+ let(:override) { { versions: {} } }
+
+ it { expect(subject[:http_status]).to eq 400 }
+ it { expect(subject[:message]).to eq 'Version is empty.' }
+ end
+
+ context 'with invalid versions' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:version) do
+ [
+ '1',
+ '1.2',
+ '1./2.3',
+ '../../../../../1.2.3',
+ '%2e%2e%2f1.2.3'
+ ]
+ end
+
+ with_them do
+ it { expect { subject }.to raise_error(ActiveRecord::RecordInvalid, 'Validation failed: Version is invalid') }
+ end
+ end
+ end
+end
diff --git a/spec/services/packages/npm/create_tag_service_spec.rb b/spec/services/packages/npm/create_tag_service_spec.rb
new file mode 100644
index 00000000000..e7a784068fa
--- /dev/null
+++ b/spec/services/packages/npm/create_tag_service_spec.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe Packages::Npm::CreateTagService do
+ let(:package) { create(:npm_package) }
+ let(:tag_name) { 'test-tag' }
+
+ describe '#execute' do
+ subject { described_class.new(package, tag_name).execute }
+
+ shared_examples 'it creates the tag' do
+ it { expect { subject }.to change { Packages::Tag.count }.by(1) }
+ it { expect(subject.name).to eq(tag_name) }
+ it 'adds tag to the package' do
+ tag = subject
+ expect(package.reload.tags).to match_array([tag])
+ end
+ end
+
+ context 'with no existing tag name' do
+ it_behaves_like 'it creates the tag'
+ end
+
+ context 'with exisiting tag name' do
+ let!(:package_tag2) { create(:packages_tag, package: package2, name: tag_name) }
+
+ context 'on package with different name' do
+ let!(:package2) { create(:npm_package, project: package.project) }
+
+ it_behaves_like 'it creates the tag'
+ end
+
+ context 'on different package type' do
+ let!(:package2) { create(:conan_package, project: package.project, name: 'conan_package_name', version: package.version) }
+
+ it_behaves_like 'it creates the tag'
+ end
+
+ context 'on same package with different version' do
+ let!(:package2) { create(:npm_package, project: package.project, name: package.name, version: '5.0.0-testing') }
+
+ it { expect { subject }.to not_change { Packages::Tag.count } }
+ it { expect(subject.name).to eq(tag_name) }
+
+ it 'adds tag to the package' do
+ tag = subject
+ expect(package.reload.tags).to match_array([tag])
+ expect(package2.reload.tags).to be_empty
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/packages/nuget/create_dependency_service_spec.rb b/spec/services/packages/nuget/create_dependency_service_spec.rb
new file mode 100644
index 00000000000..268c8837e25
--- /dev/null
+++ b/spec/services/packages/nuget/create_dependency_service_spec.rb
@@ -0,0 +1,76 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe Packages::Nuget::CreateDependencyService do
+ let_it_be(:package, reload: true) { create(:nuget_package) }
+
+ describe '#execute' do
+ RSpec.shared_examples 'creating dependencies, links and nuget metadata for' do |expected_dependency_names, dependency_count, dependency_link_count|
+ let(:dependencies_with_metadata) { dependencies.select { |dep| dep[:target_framework].present? } }
+
+ it 'creates dependencies, links and nuget metadata' do
+ expect { subject }
+ .to change { Packages::Dependency.count }.by(dependency_count)
+ .and change { Packages::DependencyLink.count }.by(dependency_link_count)
+ .and change { Packages::Nuget::DependencyLinkMetadatum.count }.by(dependencies_with_metadata.size)
+ expect(expected_dependency_names).to contain_exactly(*dependency_names)
+ expect(package.dependency_links.map(&:dependency_type).uniq).to contain_exactly('dependencies')
+
+ dependencies_with_metadata.each do |dependency|
+ name = dependency[:name]
+ version_pattern = service.send(:version_or_empty_string, dependency[:version])
+ metadatum = package.dependency_links.joins(:dependency)
+ .find_by(packages_dependencies: { name: name, version_pattern: version_pattern })
+ .nuget_metadatum
+ expect(metadatum.target_framework).to eq dependency[:target_framework]
+ end
+ end
+ end
+
+ let_it_be(:dependencies) do
+ [
+ { name: 'Moqi', version: '2.5.6' },
+ { name: 'Castle.Core' },
+ { name: 'Test.Dependency', version: '2.3.7', target_framework: '.NETStandard2.0' },
+ { name: 'Newtonsoft.Json', version: '12.0.3', target_framework: '.NETStandard2.0' }
+ ]
+ end
+
+ let(:dependency_names) { package.dependency_links.flat_map(&:dependency).map(&:name) }
+ let(:service) { described_class.new(package, dependencies) }
+
+ subject { service.execute }
+
+ it_behaves_like 'creating dependencies, links and nuget metadata for', %w(Castle.Core Moqi Newtonsoft.Json Test.Dependency), 4, 4
+
+ context 'with existing dependencies' do
+ let_it_be(:exisiting_dependency) { create(:packages_dependency, name: 'Moqi', version_pattern: '2.5.6') }
+
+ it_behaves_like 'creating dependencies, links and nuget metadata for', %w(Castle.Core Moqi Newtonsoft.Json Test.Dependency), 3, 4
+ end
+
+ context 'with dependencies with no target framework' do
+ let_it_be(:dependencies) do
+ [
+ { name: 'Moqi', version: '2.5.6' },
+ { name: 'Castle.Core' },
+ { name: 'Test.Dependency', version: '2.3.7' },
+ { name: 'Newtonsoft.Json', version: '12.0.3' }
+ ]
+ end
+
+ it_behaves_like 'creating dependencies, links and nuget metadata for', %w(Castle.Core Moqi Newtonsoft.Json Test.Dependency), 4, 4
+ end
+
+ context 'with empty dependencies' do
+ let_it_be(:dependencies) { [] }
+
+ it 'is a no op' do
+ expect(service).not_to receive(:create_dependency_links)
+ expect(service).not_to receive(:create_dependency_link_metadata)
+
+ subject
+ end
+ end
+ end
+end
diff --git a/spec/services/packages/nuget/create_package_service_spec.rb b/spec/services/packages/nuget/create_package_service_spec.rb
new file mode 100644
index 00000000000..1579b42d9ad
--- /dev/null
+++ b/spec/services/packages/nuget/create_package_service_spec.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe Packages::Nuget::CreatePackageService do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:params) { {} }
+
+ describe '#execute' do
+ subject { described_class.new(project, user, params).execute }
+
+ it 'creates the package' do
+ expect { subject }.to change { Packages::Package.count }.by(1)
+ package = Packages::Package.last
+
+ expect(package).to be_valid
+ expect(package.name).to eq(Packages::Nuget::CreatePackageService::TEMPORARY_PACKAGE_NAME)
+ expect(package.version).to start_with(Packages::Nuget::CreatePackageService::PACKAGE_VERSION)
+ expect(package.package_type).to eq('nuget')
+ end
+
+ it 'can create two packages in a row' do
+ expect { subject }.to change { Packages::Package.count }.by(1)
+ expect { described_class.new(project, user, params).execute }.to change { Packages::Package.count }.by(1)
+
+ package = Packages::Package.last
+
+ expect(package).to be_valid
+ expect(package.name).to eq(Packages::Nuget::CreatePackageService::TEMPORARY_PACKAGE_NAME)
+ expect(package.version).to start_with(Packages::Nuget::CreatePackageService::PACKAGE_VERSION)
+ expect(package.package_type).to eq('nuget')
+ end
+ end
+end
diff --git a/spec/services/packages/nuget/metadata_extraction_service_spec.rb b/spec/services/packages/nuget/metadata_extraction_service_spec.rb
new file mode 100644
index 00000000000..39fc0f9e6a1
--- /dev/null
+++ b/spec/services/packages/nuget/metadata_extraction_service_spec.rb
@@ -0,0 +1,106 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Packages::Nuget::MetadataExtractionService do
+ let(:package_file) { create(:nuget_package).package_files.first }
+ let(:service) { described_class.new(package_file.id) }
+
+ describe '#execute' do
+ subject { service.execute }
+
+ context 'with valid package file id' do
+ expected_metadata = {
+ package_name: 'DummyProject.DummyPackage',
+ package_version: '1.0.0',
+ package_dependencies: [
+ {
+ name: 'Newtonsoft.Json',
+ target_framework: '.NETCoreApp3.0',
+ version: '12.0.3'
+ }
+ ],
+ package_tags: []
+ }
+
+ it { is_expected.to eq(expected_metadata) }
+ end
+
+ context 'with nuspec file' do
+ before do
+ allow(service).to receive(:nuspec_file).and_return(fixture_file(nuspec_filepath))
+ end
+
+ context 'with dependencies' do
+ let(:nuspec_filepath) { 'packages/nuget/with_dependencies.nuspec' }
+
+ it { is_expected.to have_key(:package_dependencies) }
+
+ it 'extracts dependencies' do
+ dependencies = subject[:package_dependencies]
+
+ expect(dependencies).to include(name: 'Moqi', version: '2.5.6')
+ expect(dependencies).to include(name: 'Castle.Core')
+ expect(dependencies).to include(name: 'Test.Dependency', version: '2.3.7', target_framework: '.NETStandard2.0')
+ expect(dependencies).to include(name: 'Newtonsoft.Json', version: '12.0.3', target_framework: '.NETStandard2.0')
+ end
+ end
+
+ context 'with a nuspec file with metadata' do
+ let(:nuspec_filepath) { 'packages/nuget/with_metadata.nuspec' }
+
+ it { expect(subject[:package_tags].sort).to eq(%w(foo bar test tag1 tag2 tag3 tag4 tag5).sort) }
+ end
+ end
+
+ context 'with a nuspec file with metadata' do
+ let_it_be(:nuspec_filepath) { 'packages/nuget/with_metadata.nuspec' }
+
+ before do
+ allow(service).to receive(:nuspec_file).and_return(fixture_file(nuspec_filepath))
+ end
+
+ it { expect(subject[:license_url]).to eq('https://opensource.org/licenses/MIT') }
+ it { expect(subject[:project_url]).to eq('https://gitlab.com/gitlab-org/gitlab') }
+ it { expect(subject[:icon_url]).to eq('https://opensource.org/files/osi_keyhole_300X300_90ppi_0.png') }
+ end
+
+ context 'with invalid package file id' do
+ let(:package_file) { OpenStruct.new(id: 555) }
+
+ it { expect { subject }.to raise_error(::Packages::Nuget::MetadataExtractionService::ExtractionError, 'invalid package file') }
+ end
+
+ context 'linked to a non nuget package' do
+ before do
+ package_file.package.maven!
+ end
+
+ it { expect { subject }.to raise_error(::Packages::Nuget::MetadataExtractionService::ExtractionError, 'invalid package file') }
+ end
+
+ context 'with a 0 byte package file id' do
+ before do
+ allow_any_instance_of(Packages::PackageFileUploader).to receive(:size).and_return(0)
+ end
+
+ it { expect { subject }.to raise_error(::Packages::Nuget::MetadataExtractionService::ExtractionError, 'invalid package file') }
+ end
+
+ context 'without the nuspec file' do
+ before do
+ allow_any_instance_of(Zip::File).to receive(:glob).and_return([])
+ end
+
+ it { expect { subject }.to raise_error(::Packages::Nuget::MetadataExtractionService::ExtractionError, 'nuspec file not found') }
+ end
+
+ context 'with a too big nuspec file' do
+ before do
+ allow_any_instance_of(Zip::File).to receive(:glob).and_return([OpenStruct.new(size: 6.megabytes)])
+ end
+
+ it { expect { subject }.to raise_error(::Packages::Nuget::MetadataExtractionService::ExtractionError, 'nuspec file too big') }
+ end
+ end
+end
diff --git a/spec/services/packages/nuget/search_service_spec.rb b/spec/services/packages/nuget/search_service_spec.rb
new file mode 100644
index 00000000000..d163e7087e4
--- /dev/null
+++ b/spec/services/packages/nuget/search_service_spec.rb
@@ -0,0 +1,116 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe Packages::Nuget::SearchService do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:package_a) { create(:nuget_package, project: project, name: 'DummyPackageA') }
+ let_it_be(:packages_b) { create_list(:nuget_package, 5, project: project, name: 'DummyPackageB') }
+ let_it_be(:packages_c) { create_list(:nuget_package, 5, project: project, name: 'DummyPackageC') }
+ let_it_be(:package_d) { create(:nuget_package, project: project, name: 'FooBarD') }
+ let_it_be(:other_package_a) { create(:nuget_package, name: 'DummyPackageA') }
+ let_it_be(:other_package_a) { create(:nuget_package, name: 'DummyPackageB') }
+ let(:search_term) { 'ummy' }
+ let(:per_page) { 5 }
+ let(:padding) { 0 }
+ let(:include_prerelease_versions) { true }
+ let(:options) { { include_prerelease_versions: include_prerelease_versions, per_page: per_page, padding: padding } }
+
+ describe '#execute' do
+ subject { described_class.new(project, search_term, options).execute }
+
+ it { expect_search_results 3, package_a, packages_b, packages_c }
+
+ context 'with a smaller per page count' do
+ let(:per_page) { 2 }
+
+ it { expect_search_results 3, package_a, packages_b }
+ end
+
+ context 'with 0 per page count' do
+ let(:per_page) { 0 }
+
+ it { expect_search_results 3, [] }
+ end
+
+ context 'with a negative per page count' do
+ let(:per_page) { -1 }
+
+ it { expect { subject }.to raise_error(ArgumentError, 'negative per_page') }
+ end
+
+ context 'with a padding' do
+ let(:padding) { 2 }
+
+ it { expect_search_results 3, packages_c }
+ end
+
+ context 'with a too big padding' do
+ let(:padding) { 5 }
+
+ it { expect_search_results 3, [] }
+ end
+
+ context 'with a negative padding' do
+ let(:padding) { -1 }
+
+ it { expect { subject }.to raise_error(ArgumentError, 'negative padding') }
+ end
+
+ context 'with search term' do
+ let(:search_term) { 'umm' }
+
+ it { expect_search_results 3, package_a, packages_b, packages_c }
+ end
+
+ context 'with nil search term' do
+ let(:search_term) { nil }
+
+ it { expect_search_results 4, package_a, packages_b, packages_c, package_d }
+ end
+
+ context 'with empty search term' do
+ let(:search_term) { '' }
+
+ it { expect_search_results 4, package_a, packages_b, packages_c, package_d }
+ end
+
+ context 'with prefix search term' do
+ let(:search_term) { 'dummy' }
+
+ it { expect_search_results 3, package_a, packages_b, packages_c }
+ end
+
+ context 'with suffix search term' do
+ let(:search_term) { 'packagec' }
+
+ it { expect_search_results 1, packages_c }
+ end
+
+ context 'with pre release packages' do
+ let_it_be(:package_e) { create(:nuget_package, project: project, name: 'DummyPackageE', version: '3.2.1-alpha') }
+
+ context 'including them' do
+ it { expect_search_results 4, package_a, packages_b, packages_c, package_e }
+ end
+
+ context 'excluding them' do
+ let(:include_prerelease_versions) { false }
+
+ it { expect_search_results 3, package_a, packages_b, packages_c }
+
+ context 'when mixed with release versions' do
+ let_it_be(:package_e_release) { create(:nuget_package, project: project, name: 'DummyPackageE', version: '3.2.1') }
+
+ it { expect_search_results 4, package_a, packages_b, packages_c, package_e_release }
+ end
+ end
+ end
+
+ def expect_search_results(total_count, *results)
+ search = subject
+
+ expect(search.total_count).to eq total_count
+ expect(search.results).to match_array(Array.wrap(results).flatten)
+ end
+ end
+end
diff --git a/spec/services/packages/nuget/sync_metadatum_service_spec.rb b/spec/services/packages/nuget/sync_metadatum_service_spec.rb
new file mode 100644
index 00000000000..32093c48b76
--- /dev/null
+++ b/spec/services/packages/nuget/sync_metadatum_service_spec.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Packages::Nuget::SyncMetadatumService do
+ let_it_be(:package, reload: true) { create(:nuget_package) }
+ let_it_be(:metadata) do
+ {
+ project_url: 'https://test.org/test',
+ license_url: 'https://test.org/MIT',
+ icon_url: 'https://test.org/icon.png'
+ }
+ end
+
+ let(:service) { described_class.new(package, metadata) }
+ let(:nuget_metadatum) { package.nuget_metadatum }
+
+ describe '#execute' do
+ subject { service.execute }
+
+ RSpec.shared_examples 'saving metadatum attributes' do
+ it 'saves nuget metadatum' do
+ subject
+
+ metadata.each do |attribute, expected_value|
+ expect(nuget_metadatum.send(attribute)).to eq(expected_value)
+ end
+ end
+ end
+
+ it 'creates a nuget metadatum' do
+ expect { subject }
+ .to change { package.nuget_metadatum.present? }.from(false).to(true)
+ end
+
+ it_behaves_like 'saving metadatum attributes'
+
+ context 'with exisiting nuget metadatum' do
+ let_it_be(:package) { create(:nuget_package, :with_metadatum) }
+
+ it 'does not create a nuget metadatum' do
+ expect { subject }.to change { ::Packages::Nuget::Metadatum.count }.by(0)
+ end
+
+ it_behaves_like 'saving metadatum attributes'
+
+ context 'with empty metadata' do
+ let_it_be(:metadata) { {} }
+
+ it 'destroys the nuget metadatum' do
+ expect { subject }
+ .to change { package.reload.nuget_metadatum.present? }.from(true).to(false)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/packages/nuget/update_package_from_metadata_service_spec.rb b/spec/services/packages/nuget/update_package_from_metadata_service_spec.rb
new file mode 100644
index 00000000000..b7c780c1ee2
--- /dev/null
+++ b/spec/services/packages/nuget/update_package_from_metadata_service_spec.rb
@@ -0,0 +1,237 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Packages::Nuget::UpdatePackageFromMetadataService, :clean_gitlab_redis_shared_state do
+ include ExclusiveLeaseHelpers
+
+ let(:package) { create(:nuget_package) }
+ let(:package_file) { package.package_files.first }
+ let(:service) { described_class.new(package_file) }
+ let(:package_name) { 'DummyProject.DummyPackage' }
+ let(:package_version) { '1.0.0' }
+ let(:package_file_name) { 'dummyproject.dummypackage.1.0.0.nupkg' }
+
+ RSpec.shared_examples 'raising an' do |error_class|
+ it "raises an #{error_class}" do
+ expect { subject }.to raise_error(error_class)
+ end
+ end
+
+ describe '#execute' do
+ subject { service.execute }
+
+ before do
+ stub_package_file_object_storage(enabled: true, direct_upload: true)
+ end
+
+ RSpec.shared_examples 'taking the lease' do
+ before do
+ allow(service).to receive(:lease_release?).and_return(false)
+ end
+
+ it 'takes the lease' do
+ expect(service).to receive(:try_obtain_lease).and_call_original
+
+ subject
+
+ expect(service.exclusive_lease.exists?).to be_truthy
+ end
+ end
+
+ RSpec.shared_examples 'not updating the package if the lease is taken' do
+ context 'without obtaining the exclusive lease' do
+ let(:lease_key) { "packages:nuget:update_package_from_metadata_service:package:#{package_id}" }
+ let(:metadata) { { package_name: package_name, package_version: package_version } }
+ let(:package_from_package_file) { package_file.package }
+
+ before do
+ stub_exclusive_lease_taken(lease_key, timeout: 1.hour)
+ # to allow the above stub, we need to stub the metadata function as the
+ # original implementation will try to get an exclusive lease on the
+ # file in object storage
+ allow(service).to receive(:metadata).and_return(metadata)
+ end
+
+ it 'does not update the package' do
+ expect(service).to receive(:try_obtain_lease).and_call_original
+
+ expect { subject }
+ .to change { ::Packages::Package.count }.by(0)
+ .and change { Packages::DependencyLink.count }.by(0)
+ expect(package_file.reload.file_name).not_to eq(package_file_name)
+ expect(package_file.package.reload.name).not_to eq(package_name)
+ expect(package_file.package.version).not_to eq(package_version)
+ end
+ end
+ end
+
+ context 'with no existing package' do
+ let(:package_id) { package.id }
+
+ it 'updates package and package file' do
+ expect { subject }
+ .to change { ::Packages::Package.count }.by(1)
+ .and change { Packages::Dependency.count }.by(1)
+ .and change { Packages::DependencyLink.count }.by(1)
+ .and change { ::Packages::Nuget::Metadatum.count }.by(0)
+
+ expect(package.reload.name).to eq(package_name)
+ expect(package.version).to eq(package_version)
+ expect(package_file.reload.file_name).to eq(package_file_name)
+ # hard reset needed to properly reload package_file.file
+ expect(Packages::PackageFile.find(package_file.id).file.size).not_to eq 0
+ end
+
+ it_behaves_like 'taking the lease'
+
+ it_behaves_like 'not updating the package if the lease is taken'
+ end
+
+ context 'with existing package' do
+ let!(:existing_package) { create(:nuget_package, project: package.project, name: package_name, version: package_version) }
+ let(:package_id) { existing_package.id }
+
+ it 'link existing package and updates package file' do
+ expect(service).to receive(:try_obtain_lease).and_call_original
+
+ expect { subject }
+ .to change { ::Packages::Package.count }.by(-1)
+ .and change { Packages::Dependency.count }.by(0)
+ .and change { Packages::DependencyLink.count }.by(0)
+ .and change { Packages::Nuget::DependencyLinkMetadatum.count }.by(0)
+ .and change { ::Packages::Nuget::Metadatum.count }.by(0)
+ expect(package_file.reload.file_name).to eq(package_file_name)
+ expect(package_file.package).to eq(existing_package)
+ end
+
+ it_behaves_like 'taking the lease'
+
+ it_behaves_like 'not updating the package if the lease is taken'
+ end
+
+ context 'with a nuspec file with metadata' do
+ let(:nuspec_filepath) { 'packages/nuget/with_metadata.nuspec' }
+ let(:expected_tags) { %w(foo bar test tag1 tag2 tag3 tag4 tag5) }
+
+ before do
+ allow_any_instance_of(Packages::Nuget::MetadataExtractionService)
+ .to receive(:nuspec_file)
+ .and_return(fixture_file(nuspec_filepath))
+ end
+
+ it 'creates tags' do
+ expect(service).to receive(:try_obtain_lease).and_call_original
+ expect { subject }.to change { ::Packages::Tag.count }.by(8)
+ expect(package.reload.tags.map(&:name)).to contain_exactly(*expected_tags)
+ end
+
+ context 'with existing package and tags' do
+ let!(:existing_package) { create(:nuget_package, project: package.project, name: 'DummyProject.WithMetadata', version: '1.2.3') }
+ let!(:tag1) { create(:packages_tag, package: existing_package, name: 'tag1') }
+ let!(:tag2) { create(:packages_tag, package: existing_package, name: 'tag2') }
+ let!(:tag3) { create(:packages_tag, package: existing_package, name: 'tag_not_in_metadata') }
+
+ it 'creates tags and deletes those not in metadata' do
+ expect(service).to receive(:try_obtain_lease).and_call_original
+ expect { subject }.to change { ::Packages::Tag.count }.by(5)
+ expect(existing_package.tags.map(&:name)).to contain_exactly(*expected_tags)
+ end
+ end
+
+ it 'creates nuget metadatum' do
+ expect { subject }
+ .to change { ::Packages::Package.count }.by(1)
+ .and change { ::Packages::Nuget::Metadatum.count }.by(1)
+
+ metadatum = package_file.reload.package.nuget_metadatum
+ expect(metadatum.license_url).to eq('https://opensource.org/licenses/MIT')
+ expect(metadatum.project_url).to eq('https://gitlab.com/gitlab-org/gitlab')
+ expect(metadatum.icon_url).to eq('https://opensource.org/files/osi_keyhole_300X300_90ppi_0.png')
+ end
+
+ context 'with too long url' do
+ let_it_be(:too_long_url) { "http://localhost/#{'bananas' * 50}" }
+
+ let(:metadata) { { package_name: package_name, package_version: package_version, license_url: too_long_url } }
+
+ before do
+ allow(service).to receive(:metadata).and_return(metadata)
+ end
+
+ it_behaves_like 'raising an', ::Packages::Nuget::UpdatePackageFromMetadataService::InvalidMetadataError
+ end
+ end
+
+ context 'with nuspec file with dependencies' do
+ let(:nuspec_filepath) { 'packages/nuget/with_dependencies.nuspec' }
+ let(:package_name) { 'Test.Package' }
+ let(:package_version) { '3.5.2' }
+ let(:package_file_name) { 'test.package.3.5.2.nupkg' }
+
+ before do
+ allow_any_instance_of(Packages::Nuget::MetadataExtractionService)
+ .to receive(:nuspec_file)
+ .and_return(fixture_file(nuspec_filepath))
+ end
+
+ it 'updates package and package file' do
+ expect { subject }
+ .to change { ::Packages::Package.count }.by(1)
+ .and change { Packages::Dependency.count }.by(4)
+ .and change { Packages::DependencyLink.count }.by(4)
+ .and change { Packages::Nuget::DependencyLinkMetadatum.count }.by(2)
+
+ expect(package.reload.name).to eq(package_name)
+ expect(package.version).to eq(package_version)
+ expect(package_file.reload.file_name).to eq(package_file_name)
+ # hard reset needed to properly reload package_file.file
+ expect(Packages::PackageFile.find(package_file.id).file.size).not_to eq 0
+ end
+ end
+
+ context 'with package file not containing a nuspec file' do
+ before do
+ allow_any_instance_of(Zip::File).to receive(:glob).and_return([])
+ end
+
+ it_behaves_like 'raising an', ::Packages::Nuget::MetadataExtractionService::ExtractionError
+ end
+
+ context 'with package file with a blank package name' do
+ before do
+ allow(service).to receive(:package_name).and_return('')
+ end
+
+ it_behaves_like 'raising an', ::Packages::Nuget::UpdatePackageFromMetadataService::InvalidMetadataError
+ end
+
+ context 'with package file with a blank package version' do
+ before do
+ allow(service).to receive(:package_version).and_return('')
+ end
+
+ it_behaves_like 'raising an', ::Packages::Nuget::UpdatePackageFromMetadataService::InvalidMetadataError
+ end
+
+ context 'with an invalid package version' do
+ invalid_versions = [
+ '555',
+ '1.2',
+ '1./2.3',
+ '../../../../../1.2.3',
+ '%2e%2e%2f1.2.3'
+ ]
+
+ invalid_versions.each do |invalid_version|
+ it "raises an error for version #{invalid_version}" do
+ allow(service).to receive(:package_version).and_return(invalid_version)
+
+ expect { subject }.to raise_error(ActiveRecord::RecordInvalid, 'Validation failed: Version is invalid')
+ expect(package_file.file_name).not_to include(invalid_version)
+ expect(package_file.file.file.path).not_to include(invalid_version)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/packages/pypi/create_package_service_spec.rb b/spec/services/packages/pypi/create_package_service_spec.rb
new file mode 100644
index 00000000000..250b43d1f75
--- /dev/null
+++ b/spec/services/packages/pypi/create_package_service_spec.rb
@@ -0,0 +1,83 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe Packages::Pypi::CreatePackageService do
+ include PackagesManagerApiSpecHelpers
+
+ let_it_be(:project) { create(:project) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:params) do
+ {
+ name: 'foo',
+ version: '1.0',
+ content: temp_file('foo.tgz'),
+ requires_python: '>=2.7',
+ sha256_digest: '123',
+ md5_digest: '567'
+ }
+ end
+
+ describe '#execute' do
+ subject { described_class.new(project, user, params).execute }
+
+ let(:created_package) { Packages::Package.pypi.last }
+
+ context 'without an existing package' do
+ it 'creates the package' do
+ expect { subject }.to change { Packages::Package.pypi.count }.by(1)
+
+ expect(created_package.name).to eq 'foo'
+ expect(created_package.version).to eq '1.0'
+
+ expect(created_package.pypi_metadatum.required_python).to eq '>=2.7'
+ expect(created_package.package_files.size).to eq 1
+ expect(created_package.package_files.first.file_name).to eq 'foo.tgz'
+ expect(created_package.package_files.first.file_sha256).to eq '123'
+ expect(created_package.package_files.first.file_md5).to eq '567'
+ end
+ end
+
+ context 'with an existing package' do
+ before do
+ described_class.new(project, user, params).execute
+ end
+
+ context 'with an existing file' do
+ before do
+ params[:content] = temp_file('foo.tgz')
+ params[:sha256_digest] = 'abc'
+ params[:md5_digest] = 'def'
+ end
+
+ it 'replaces the file' do
+ expect { subject }
+ .to change { Packages::Package.pypi.count }.by(0)
+ .and change { Packages::PackageFile.count }.by(1)
+
+ expect(created_package.package_files.size).to eq 2
+ expect(created_package.package_files.first.file_name).to eq 'foo.tgz'
+ expect(created_package.package_files.first.file_sha256).to eq '123'
+ expect(created_package.package_files.first.file_md5).to eq '567'
+ expect(created_package.package_files.last.file_name).to eq 'foo.tgz'
+ expect(created_package.package_files.last.file_sha256).to eq 'abc'
+ expect(created_package.package_files.last.file_md5).to eq 'def'
+ end
+ end
+
+ context 'without an existing file' do
+ before do
+ params[:content] = temp_file('another.tgz')
+ end
+
+ it 'adds the file' do
+ expect { subject }
+ .to change { Packages::Package.pypi.count }.by(0)
+ .and change { Packages::PackageFile.count }.by(1)
+
+ expect(created_package.package_files.size).to eq 2
+ expect(created_package.package_files.map(&:file_name).sort).to eq ['another.tgz', 'foo.tgz']
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/packages/remove_tag_service_spec.rb b/spec/services/packages/remove_tag_service_spec.rb
new file mode 100644
index 00000000000..084635824e5
--- /dev/null
+++ b/spec/services/packages/remove_tag_service_spec.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe Packages::RemoveTagService do
+ let!(:package_tag) { create(:packages_tag) }
+
+ describe '#execute' do
+ subject { described_class.new(package_tag).execute }
+
+ context 'with existing tag' do
+ it { expect { subject }.to change { Packages::Tag.count }.by(-1) }
+ end
+
+ context 'with nil' do
+ subject { described_class.new(nil) }
+
+ it { expect { subject }.to raise_error(ArgumentError) }
+ end
+ end
+end
diff --git a/spec/services/packages/update_tags_service_spec.rb b/spec/services/packages/update_tags_service_spec.rb
new file mode 100644
index 00000000000..4a122d1c718
--- /dev/null
+++ b/spec/services/packages/update_tags_service_spec.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe Packages::UpdateTagsService do
+ let_it_be(:package, reload: true) { create(:nuget_package) }
+
+ let(:tags) { %w(test-tag tag1 tag2 tag3) }
+ let(:service) { described_class.new(package, tags) }
+
+ describe '#execute' do
+ subject { service.execute }
+
+ RSpec.shared_examples 'updating tags' do |tags_count|
+ it 'updates a tag' do
+ expect { subject }.to change { Packages::Tag.count }.by(tags_count)
+ expect(package.reload.tags.map(&:name)).to contain_exactly(*tags)
+ end
+ end
+
+ it_behaves_like 'updating tags', 4
+
+ context 'with an existing tag' do
+ before do
+ create(:packages_tag, package: package2, name: 'test-tag')
+ end
+
+ context 'on the same package' do
+ let_it_be(:package2) { package }
+
+ it_behaves_like 'updating tags', 3
+
+ context 'with different name' do
+ before do
+ create(:packages_tag, package: package2, name: 'to_be_destroyed')
+ end
+
+ it_behaves_like 'updating tags', 2
+ end
+ end
+
+ context 'on a different package' do
+ let_it_be(:package2) { create(:nuget_package) }
+
+ it_behaves_like 'updating tags', 4
+ end
+ end
+
+ context 'with empty tags' do
+ let(:tags) { [] }
+
+ it 'is a no op' do
+ expect(package).not_to receive(:tags)
+ expect(::Gitlab::Database).not_to receive(:bulk_insert)
+
+ subject
+ end
+ end
+ end
+end