diff options
Diffstat (limited to 'spec/services/packages/nuget')
6 files changed, 626 insertions, 0 deletions
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 |