diff options
Diffstat (limited to 'spec/models/packages')
-rw-r--r-- | spec/models/packages/composer/metadatum_spec.rb | 14 | ||||
-rw-r--r-- | spec/models/packages/conan/file_metadatum_spec.rb | 106 | ||||
-rw-r--r-- | spec/models/packages/conan/metadatum_spec.rb | 90 | ||||
-rw-r--r-- | spec/models/packages/dependency_link_spec.rb | 56 | ||||
-rw-r--r-- | spec/models/packages/dependency_spec.rb | 113 | ||||
-rw-r--r-- | spec/models/packages/go/module_spec.rb | 59 | ||||
-rw-r--r-- | spec/models/packages/go/module_version_spec.rb | 114 | ||||
-rw-r--r-- | spec/models/packages/maven/metadatum_spec.rb | 40 | ||||
-rw-r--r-- | spec/models/packages/nuget/dependency_link_metadatum_spec.rb | 32 | ||||
-rw-r--r-- | spec/models/packages/nuget/metadatum_spec.rb | 44 | ||||
-rw-r--r-- | spec/models/packages/package_file_spec.rb | 69 | ||||
-rw-r--r-- | spec/models/packages/package_spec.rb | 485 | ||||
-rw-r--r-- | spec/models/packages/pypi/metadatum_spec.rb | 22 | ||||
-rw-r--r-- | spec/models/packages/sem_ver_spec.rb | 42 | ||||
-rw-r--r-- | spec/models/packages/tag_spec.rb | 62 |
15 files changed, 1348 insertions, 0 deletions
diff --git a/spec/models/packages/composer/metadatum_spec.rb b/spec/models/packages/composer/metadatum_spec.rb new file mode 100644 index 00000000000..ae53532696b --- /dev/null +++ b/spec/models/packages/composer/metadatum_spec.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true +require 'spec_helper' + +RSpec.describe Packages::Composer::Metadatum, type: :model do + describe 'relationships' do + it { is_expected.to belong_to(:package) } + end + + describe 'validations' do + it { is_expected.to validate_presence_of(:package) } + it { is_expected.to validate_presence_of(:target_sha) } + it { is_expected.to validate_presence_of(:composer_json) } + end +end diff --git a/spec/models/packages/conan/file_metadatum_spec.rb b/spec/models/packages/conan/file_metadatum_spec.rb new file mode 100644 index 00000000000..a66a2813196 --- /dev/null +++ b/spec/models/packages/conan/file_metadatum_spec.rb @@ -0,0 +1,106 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Packages::Conan::FileMetadatum, type: :model do + describe 'relationships' do + it { is_expected.to belong_to(:package_file) } + end + + describe 'validations' do + let(:package_file) { create(:conan_package_file, :conan_recipe_file) } + + it { is_expected.to validate_presence_of(:package_file) } + it { is_expected.to validate_presence_of(:recipe_revision) } + + describe '#recipe_revision' do + it { is_expected.to allow_value("0").for(:recipe_revision) } + it { is_expected.not_to allow_value(nil).for(:recipe_revision) } + end + + describe '#package_revision_for_package_file' do + context 'recipe file' do + let(:conan_file_metadatum) { build(:conan_file_metadatum, :recipe_file, package_file: package_file) } + + it 'is valid with empty value' do + conan_file_metadatum.package_revision = nil + + expect(conan_file_metadatum).to be_valid + end + + it 'is invalid with value' do + conan_file_metadatum.package_revision = '0' + + expect(conan_file_metadatum).to be_invalid + end + end + + context 'package file' do + let(:conan_file_metadatum) { build(:conan_file_metadatum, :package_file, package_file: package_file) } + + it 'is valid with default value' do + conan_file_metadatum.package_revision = '0' + + expect(conan_file_metadatum).to be_valid + end + + it 'is invalid with non-default value' do + conan_file_metadatum.package_revision = 'foo' + + expect(conan_file_metadatum).to be_invalid + end + end + end + + describe '#conan_package_reference_for_package_file' do + context 'recipe file' do + let(:conan_file_metadatum) { build(:conan_file_metadatum, :recipe_file, package_file: package_file) } + + it 'is valid with empty value' do + conan_file_metadatum.conan_package_reference = nil + + expect(conan_file_metadatum).to be_valid + end + + it 'is invalid with value' do + conan_file_metadatum.conan_package_reference = '123456789' + + expect(conan_file_metadatum).to be_invalid + end + end + + context 'package file' do + let(:conan_file_metadatum) { build(:conan_file_metadatum, :package_file, package_file: package_file) } + + it 'is valid with acceptable value' do + conan_file_metadatum.conan_package_reference = '123456asdf' + + expect(conan_file_metadatum).to be_valid + end + + it 'is invalid with invalid value' do + conan_file_metadatum.conan_package_reference = 'foo@bar' + + expect(conan_file_metadatum).to be_invalid + end + + it 'is invalid when nil' do + conan_file_metadatum.conan_package_reference = nil + + expect(conan_file_metadatum).to be_invalid + end + end + end + + describe '#conan_package_type' do + it 'validates package of type conan' do + package = build('package') + package_file = build('package_file', package: package) + conan_file_metadatum = build('conan_file_metadatum', package_file: package_file) + + expect(conan_file_metadatum).not_to be_valid + expect(conan_file_metadatum.errors.to_a).to contain_exactly('Package type must be Conan') + end + end + end +end diff --git a/spec/models/packages/conan/metadatum_spec.rb b/spec/models/packages/conan/metadatum_spec.rb new file mode 100644 index 00000000000..112f395818b --- /dev/null +++ b/spec/models/packages/conan/metadatum_spec.rb @@ -0,0 +1,90 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Packages::Conan::Metadatum, type: :model do + describe 'relationships' do + it { is_expected.to belong_to(:package) } + end + + describe 'validations' do + let(:fifty_one_characters) { 'f_a' * 17} + + it { is_expected.to validate_presence_of(:package) } + it { is_expected.to validate_presence_of(:package_username) } + it { is_expected.to validate_presence_of(:package_channel) } + + describe '#package_username' do + it { is_expected.to allow_value("my-package+username").for(:package_username) } + it { is_expected.to allow_value("my_package.username").for(:package_username) } + it { is_expected.to allow_value("_my-package.username123").for(:package_username) } + it { is_expected.to allow_value("my").for(:package_username) } + it { is_expected.not_to allow_value('+my_package').for(:package_username) } + it { is_expected.not_to allow_value('.my_package').for(:package_username) } + it { is_expected.not_to allow_value('-my_package').for(:package_username) } + it { is_expected.not_to allow_value('m').for(:package_username) } + it { is_expected.not_to allow_value(fifty_one_characters).for(:package_username) } + it { is_expected.not_to allow_value("my/package").for(:package_username) } + it { is_expected.not_to allow_value("my(package)").for(:package_username) } + it { is_expected.not_to allow_value("my@package").for(:package_username) } + end + + describe '#package_channel' do + it { is_expected.to allow_value("beta").for(:package_channel) } + it { is_expected.to allow_value("stable+1.0").for(:package_channel) } + it { is_expected.to allow_value("my").for(:package_channel) } + it { is_expected.to allow_value("my_channel.beta").for(:package_channel) } + it { is_expected.to allow_value("_my-channel.beta123").for(:package_channel) } + it { is_expected.not_to allow_value('+my_channel').for(:package_channel) } + it { is_expected.not_to allow_value('.my_channel').for(:package_channel) } + it { is_expected.not_to allow_value('-my_channel').for(:package_channel) } + it { is_expected.not_to allow_value('m').for(:package_channel) } + it { is_expected.not_to allow_value(fifty_one_characters).for(:package_channel) } + it { is_expected.not_to allow_value("my/channel").for(:package_channel) } + it { is_expected.not_to allow_value("my(channel)").for(:package_channel) } + it { is_expected.not_to allow_value("my@channel").for(:package_channel) } + end + + describe '#conan_package_type' do + it 'will not allow a package with a different package_type' do + package = build('package') + conan_metadatum = build('conan_metadatum', package: package) + + expect(conan_metadatum).not_to be_valid + expect(conan_metadatum.errors.to_a).to include('Package type must be Conan') + end + end + end + + describe '#recipe' do + let(:package) { create(:conan_package) } + + it 'returns the recipe' do + expect(package.conan_recipe).to eq("#{package.name}/#{package.version}@#{package.conan_metadatum.package_username}/#{package.conan_metadatum.package_channel}") + end + end + + describe '#recipe_url' do + let(:package) { create(:conan_package) } + + it 'returns the recipe url' do + expect(package.conan_recipe_path).to eq("#{package.name}/#{package.version}/#{package.conan_metadatum.package_username}/#{package.conan_metadatum.package_channel}") + end + end + + describe '.package_username_from' do + let(:full_path) { 'foo/bar/baz-buz' } + + it 'returns the username formatted package path' do + expect(described_class.package_username_from(full_path: full_path)).to eq('foo+bar+baz-buz') + end + end + + describe '.full_path_from' do + let(:username) { 'foo+bar+baz-buz' } + + it 'returns the username formatted package path' do + expect(described_class.full_path_from(package_username: username)).to eq('foo/bar/baz-buz') + end + end +end diff --git a/spec/models/packages/dependency_link_spec.rb b/spec/models/packages/dependency_link_spec.rb new file mode 100644 index 00000000000..d8fde8f5eb3 --- /dev/null +++ b/spec/models/packages/dependency_link_spec.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true +require 'spec_helper' + +RSpec.describe Packages::DependencyLink, type: :model do + describe 'relationships' do + it { is_expected.to belong_to(:package).inverse_of(:dependency_links) } + it { is_expected.to belong_to(:dependency).inverse_of(:dependency_links) } + it { is_expected.to have_one(:nuget_metadatum).inverse_of(:dependency_link) } + end + + describe 'validations' do + subject { create(:packages_dependency_link) } + + it { is_expected.to validate_presence_of(:package) } + it { is_expected.to validate_presence_of(:dependency) } + + context 'package_id and package_dependency_id uniqueness for dependency_type' do + it 'is not valid' do + exisiting_link = subject + link = build( + :packages_dependency_link, + package: exisiting_link.package, + dependency: exisiting_link.dependency, + dependency_type: exisiting_link.dependency_type + ) + + expect(link).not_to be_valid + expect(link.errors.to_a).to include("Dependency type has already been taken") + end + end + end + + context 'with multiple links' do + let_it_be(:link1) { create(:packages_dependency_link) } + let_it_be(:link2) { create(:packages_dependency_link, dependency: link1.dependency, dependency_type: :devDependencies) } + let_it_be(:link3) { create(:packages_dependency_link, dependency: link1.dependency, dependency_type: :bundleDependencies) } + + subject { described_class } + + describe '.with_dependency_type' do + it 'returns links of the given type' do + expect(subject.with_dependency_type(:bundleDependencies)).to eq([link3]) + end + end + + describe '.for_package' do + let_it_be(:link1) { create(:packages_dependency_link) } + let_it_be(:link2) { create(:packages_dependency_link, dependency: link1.dependency, dependency_type: :devDependencies) } + let_it_be(:link3) { create(:packages_dependency_link, dependency: link1.dependency, dependency_type: :bundleDependencies) } + + it 'returns the link for the given package' do + expect(subject.for_package(link1.package)).to eq([link1]) + end + end + end +end diff --git a/spec/models/packages/dependency_spec.rb b/spec/models/packages/dependency_spec.rb new file mode 100644 index 00000000000..fa6b0fd1848 --- /dev/null +++ b/spec/models/packages/dependency_spec.rb @@ -0,0 +1,113 @@ +# frozen_string_literal: true +require 'spec_helper' + +RSpec.describe Packages::Dependency, type: :model do + describe 'relationships' do + it { is_expected.to have_many(:dependency_links) } + end + + describe 'validations' do + subject { create(:packages_dependency) } + + it { is_expected.to validate_presence_of(:name) } + it { is_expected.to validate_presence_of(:version_pattern) } + it { is_expected.to validate_uniqueness_of(:name).scoped_to(:version_pattern) } + end + + describe '.ids_for_package_names_and_version_patterns' do + let_it_be(:package_dependency1) { create(:packages_dependency, name: 'foo', version_pattern: '~1.0.0') } + let_it_be(:package_dependency2) { create(:packages_dependency, name: 'bar', version_pattern: '~2.5.0') } + let_it_be(:expected_ids) { [package_dependency1.id, package_dependency2.id] } + let(:names_and_version_patterns) { build_names_and_version_patterns(package_dependency1, package_dependency2) } + let(:chunk_size) { 50 } + let(:rows_limit) { 50 } + + subject { Packages::Dependency.ids_for_package_names_and_version_patterns(names_and_version_patterns, chunk_size, rows_limit) } + + it { is_expected.to match_array(expected_ids) } + + context 'with unknown names' do + let(:names_and_version_patterns) { { unknown: '~1.0.0' } } + + it { is_expected.to be_empty } + end + + context 'with unknown version patterns' do + let(:names_and_version_patterns) { { 'foo' => '~1.0.0beta' } } + + it { is_expected.to be_empty } + end + + context 'with a name bigger than column size' do + let_it_be(:big_name) { 'a' * (Packages::Dependency::MAX_STRING_LENGTH + 1) } + let(:names_and_version_patterns) { build_names_and_version_patterns(package_dependency1, package_dependency2).merge(big_name => '~1.0.0') } + + it { is_expected.to match_array(expected_ids) } + end + + context 'with a version pattern bigger than column size' do + let_it_be(:big_version_pattern) { 'a' * (Packages::Dependency::MAX_STRING_LENGTH + 1) } + let(:names_and_version_patterns) { build_names_and_version_patterns(package_dependency1, package_dependency2).merge('test' => big_version_pattern) } + + it { is_expected.to match_array(expected_ids) } + end + + context 'with too big parameter' do + let(:size) { (Packages::Dependency::MAX_CHUNKED_QUERIES_COUNT * chunk_size) + 1 } + let(:names_and_version_patterns) { Hash[(1..size).map { |v| [v, v] }] } + + it { expect { subject }.to raise_error(ArgumentError, 'Too many names_and_version_patterns') } + end + + context 'with parameters size' do + let_it_be(:package_dependency3) { create(:packages_dependency, name: 'foo3', version_pattern: '~1.5.3') } + let_it_be(:package_dependency4) { create(:packages_dependency, name: 'foo4', version_pattern: '~1.5.4') } + let_it_be(:package_dependency5) { create(:packages_dependency, name: 'foo5', version_pattern: '~1.5.5') } + let_it_be(:package_dependency6) { create(:packages_dependency, name: 'foo6', version_pattern: '~1.5.6') } + let_it_be(:package_dependency7) { create(:packages_dependency, name: 'foo7', version_pattern: '~1.5.7') } + let(:expected_ids) { [package_dependency1.id, package_dependency2.id, package_dependency3.id, package_dependency4.id, package_dependency5.id, package_dependency6.id, package_dependency7.id] } + let(:names_and_version_patterns) { build_names_and_version_patterns(package_dependency1, package_dependency2, package_dependency3, package_dependency4, package_dependency5, package_dependency6, package_dependency7) } + + context 'above the chunk size' do + let(:chunk_size) { 2 } + + it { is_expected.to match_array(expected_ids) } + end + + context 'selecting too many rows' do + let(:rows_limit) { 2 } + + it { expect { subject }.to raise_error(ArgumentError, 'Too many Dependencies selected') } + end + end + end + + describe '.for_package_names_and_version_patterns' do + let_it_be(:package_dependency1) { create(:packages_dependency, name: 'foo', version_pattern: '~1.0.0') } + let_it_be(:package_dependency2) { create(:packages_dependency, name: 'bar', version_pattern: '~2.5.0') } + let_it_be(:expected_array) { [package_dependency1, package_dependency2] } + let(:names_and_version_patterns) { build_names_and_version_patterns(package_dependency1, package_dependency2) } + + subject { Packages::Dependency.for_package_names_and_version_patterns(names_and_version_patterns) } + + it { is_expected.to match_array(expected_array) } + + context 'with unknown names' do + let(:names_and_version_patterns) { { unknown: '~1.0.0' } } + + it { is_expected.to be_empty } + end + + context 'with unknown version patterns' do + let(:names_and_version_patterns) { { 'foo' => '~1.0.0beta' } } + + it { is_expected.to be_empty } + end + end + + def build_names_and_version_patterns(*package_dependencies) + result = Hash.new { |h, dependency| h[dependency.name] = dependency.version_pattern } + package_dependencies.each { |dependency| result[dependency] } + result + end +end diff --git a/spec/models/packages/go/module_spec.rb b/spec/models/packages/go/module_spec.rb new file mode 100644 index 00000000000..03af4cf4b70 --- /dev/null +++ b/spec/models/packages/go/module_spec.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Packages::Go::Module, type: :model do + before do + stub_feature_flags(go_proxy_disable_gomod_validation: false) + end + + describe '#path_valid?' do + context 'with root path' do + let_it_be(:package) { create(:go_module) } + + context 'with major version 0' do + it('returns true') { expect(package.path_valid?(0)).to eq(true) } + end + + context 'with major version 1' do + it('returns true') { expect(package.path_valid?(1)).to eq(true) } + end + + context 'with major version 2' do + it('returns false') { expect(package.path_valid?(2)).to eq(false) } + end + end + + context 'with path ./v2' do + let_it_be(:package) { create(:go_module, path: '/v2') } + + context 'with major version 0' do + it('returns false') { expect(package.path_valid?(0)).to eq(false) } + end + + context 'with major version 1' do + it('returns false') { expect(package.path_valid?(1)).to eq(false) } + end + + context 'with major version 2' do + it('returns true') { expect(package.path_valid?(2)).to eq(true) } + end + end + end + + describe '#gomod_valid?' do + let_it_be(:package) { create(:go_module) } + + context 'with good gomod' do + it('returns true') { expect(package.gomod_valid?("module #{package.name}")).to eq(true) } + end + + context 'with bad gomod' do + it('returns false') { expect(package.gomod_valid?("module #{package.name}/v2")).to eq(false) } + end + + context 'with empty gomod' do + it('returns false') { expect(package.gomod_valid?("")).to eq(false) } + end + end +end diff --git a/spec/models/packages/go/module_version_spec.rb b/spec/models/packages/go/module_version_spec.rb new file mode 100644 index 00000000000..c4c6a07d9e9 --- /dev/null +++ b/spec/models/packages/go/module_version_spec.rb @@ -0,0 +1,114 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Packages::Go::ModuleVersion, type: :model do + let_it_be(:user) { create :user } + let_it_be(:project) { create :project_empty_repo, creator: user, path: 'my-go-lib' } + let_it_be(:mod) { create :go_module, project: project } + + before :all do + create :go_module_commit, :files, project: project, tag: 'v1.0.0', files: { 'README.md' => 'Hi' } + create :go_module_commit, :module, project: project, tag: 'v1.0.1' + create :go_module_commit, :package, project: project, tag: 'v1.0.2', path: 'pkg' + create :go_module_commit, :module, project: project, tag: 'v1.0.3', name: 'mod' + create :go_module_commit, :files, project: project, files: { 'y.go' => "package a\n" } + create :go_module_commit, :module, project: project, name: 'v2' + create :go_module_commit, :files, project: project, tag: 'v2.0.0', files: { 'v2/x.go' => "package a\n" } + end + + shared_examples '#files' do |desc, *entries| + it "returns #{desc}" do + actual = version.files.map { |x| x }.to_set + expect(actual).to eq(entries.to_set) + end + end + + shared_examples '#archive' do |desc, *entries| + it "returns an archive of #{desc}" do + expected = entries.map { |e| "#{version.full_name}/#{e}" }.to_set + + actual = Set[] + Zip::InputStream.open(StringIO.new(version.archive.string)) do |zip| + while (entry = zip.get_next_entry) + actual.add(entry.name) + end + end + + expect(actual).to eq(expected) + end + end + + describe '#name' do + context 'with ref and name specified' do + let_it_be(:version) { create :go_module_version, mod: mod, name: 'foobar', commit: project.repository.head_commit, ref: project.repository.find_tag('v1.0.0') } + it('returns that name') { expect(version.name).to eq('foobar') } + end + + context 'with ref specified and name unspecified' do + let_it_be(:version) { create :go_module_version, mod: mod, commit: project.repository.head_commit, ref: project.repository.find_tag('v1.0.0') } + it('returns the name of the ref') { expect(version.name).to eq('v1.0.0') } + end + + context 'with ref and name unspecified' do + let_it_be(:version) { create :go_module_version, mod: mod, commit: project.repository.head_commit } + it('returns nil') { expect(version.name).to eq(nil) } + end + end + + describe '#gomod' do + context 'with go.mod missing' do + let_it_be(:version) { create :go_module_version, :tagged, mod: mod, name: 'v1.0.0' } + it('returns nil') { expect(version.gomod).to eq(nil) } + end + + context 'with go.mod present' do + let_it_be(:version) { create :go_module_version, :tagged, mod: mod, name: 'v1.0.1' } + it('returns the contents of go.mod') { expect(version.gomod).to eq("module #{mod.name}\n") } + end + end + + describe '#files' do + context 'with a root module' do + context 'with an empty module path' do + let_it_be(:version) { create :go_module_version, :tagged, mod: mod, name: 'v1.0.2' } + it_behaves_like '#files', 'all the files', 'README.md', 'go.mod', 'a.go', 'pkg/b.go' + end + end + + context 'with a root module and a submodule' do + context 'with an empty module path' do + let_it_be(:version) { create :go_module_version, :tagged, mod: mod, name: 'v1.0.3' } + it_behaves_like '#files', 'files excluding the submodule', 'README.md', 'go.mod', 'a.go', 'pkg/b.go' + end + + context 'with the submodule\'s path' do + let_it_be(:mod) { create :go_module, project: project, path: 'mod' } + let_it_be(:version) { create :go_module_version, :tagged, mod: mod, name: 'v1.0.3' } + it_behaves_like '#files', 'the submodule\'s files', 'mod/go.mod', 'mod/a.go' + end + end + end + + describe '#archive' do + context 'with a root module' do + context 'with an empty module path' do + let_it_be(:version) { create :go_module_version, :tagged, mod: mod, name: 'v1.0.2' } + it_behaves_like '#archive', 'all the files', 'README.md', 'go.mod', 'a.go', 'pkg/b.go' + end + end + + context 'with a root module and a submodule' do + context 'with an empty module path' do + let_it_be(:version) { create :go_module_version, :tagged, mod: mod, name: 'v1.0.3' } + it_behaves_like '#archive', 'files excluding the submodule', 'README.md', 'go.mod', 'a.go', 'pkg/b.go' + end + + context 'with the submodule\'s path' do + let_it_be(:mod) { create :go_module, project: project, path: 'mod' } + let_it_be(:version) { create :go_module_version, :tagged, mod: mod, name: 'v1.0.3' } + it_behaves_like '#archive', 'the submodule\'s files', 'go.mod', 'a.go' + end + end + end +end diff --git a/spec/models/packages/maven/metadatum_spec.rb b/spec/models/packages/maven/metadatum_spec.rb new file mode 100644 index 00000000000..16f6929d710 --- /dev/null +++ b/spec/models/packages/maven/metadatum_spec.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true +require 'spec_helper' + +RSpec.describe Packages::Maven::Metadatum, type: :model do + describe 'relationships' do + it { is_expected.to belong_to(:package) } + end + + describe 'validations' do + it { is_expected.to validate_presence_of(:package) } + + describe '#app_name' do + it { is_expected.to allow_value("my-app").for(:app_name) } + it { is_expected.not_to allow_value("my/app").for(:app_name) } + it { is_expected.not_to allow_value("my(app)").for(:app_name) } + end + + describe '#app_group' do + it { is_expected.to allow_value("my.domain.com").for(:app_group) } + it { is_expected.not_to allow_value("my/domain/com").for(:app_group) } + it { is_expected.not_to allow_value("my(domain)").for(:app_group) } + end + + describe '#path' do + it { is_expected.to allow_value("my/domain/com/my-app").for(:path) } + it { is_expected.to allow_value("my/domain/com/my-app/1.0-SNAPSHOT").for(:path) } + it { is_expected.not_to allow_value("my(domain)com.my-app").for(:path) } + end + + describe '#maven_package_type' do + it 'will not allow a package with a different package_type' do + package = build('conan_package') + maven_metadatum = build('maven_metadatum', package: package) + + expect(maven_metadatum).not_to be_valid + expect(maven_metadatum.errors.to_a).to include('Package type must be Maven') + end + end + end +end diff --git a/spec/models/packages/nuget/dependency_link_metadatum_spec.rb b/spec/models/packages/nuget/dependency_link_metadatum_spec.rb new file mode 100644 index 00000000000..0c03c65028e --- /dev/null +++ b/spec/models/packages/nuget/dependency_link_metadatum_spec.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Packages::Nuget::DependencyLinkMetadatum, type: :model do + describe 'relationships' do + it { is_expected.to belong_to(:dependency_link) } + end + + describe 'validations' do + it { is_expected.to validate_presence_of(:dependency_link) } + it { is_expected.to validate_presence_of(:target_framework) } + + describe '#ensure_nuget_package_type' do + it 'validates package of type nuget' do + package = build('conan_package') + dependency_link = build('packages_dependency_link', package: package) + nuget_metadatum = build('nuget_dependency_link_metadatum', dependency_link: dependency_link) + + expect(nuget_metadatum).not_to be_valid + expect(nuget_metadatum.errors.to_a).to contain_exactly('Package type must be NuGet') + end + + it 'validates package of type nuget with nil dependency_link' do + nuget_metadatum = build('nuget_dependency_link_metadatum', dependency_link: nil) + + expect(nuget_metadatum).not_to be_valid + expect(nuget_metadatum.errors.to_a).to contain_exactly("Dependency link can't be blank", 'Package type must be NuGet') + end + end + end +end diff --git a/spec/models/packages/nuget/metadatum_spec.rb b/spec/models/packages/nuget/metadatum_spec.rb new file mode 100644 index 00000000000..c1bc5429500 --- /dev/null +++ b/spec/models/packages/nuget/metadatum_spec.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Packages::Nuget::Metadatum, type: :model do + describe 'relationships' do + it { is_expected.to belong_to(:package).inverse_of(:nuget_metadatum) } + end + + describe 'validations' do + it { is_expected.to validate_presence_of(:package) } + + %i[license_url project_url icon_url].each do |url| + describe "##{url}" do + it { is_expected.to allow_value('http://sandbox.com').for(url) } + it { is_expected.to allow_value('https://sandbox.com').for(url) } + it { is_expected.not_to allow_value('123').for(url) } + it { is_expected.not_to allow_value('sandbox.com').for(url) } + end + + describe '#ensure_at_least_one_field_supplied' do + subject { build(:nuget_metadatum) } + + it 'rejects unfilled metadatum' do + subject.attributes = { license_url: nil, project_url: nil, icon_url: nil } + + expect(subject).not_to be_valid + expect(subject.errors).to contain_exactly('Nuget metadatum must have at least license_url, project_url or icon_url set') + end + end + + describe '#ensure_nuget_package_type' do + subject { build(:nuget_metadatum) } + + it 'rejects if not linked to a nuget package' do + subject.package = build(:npm_package) + + expect(subject).not_to be_valid + expect(subject.errors).to contain_exactly('Package type must be NuGet') + end + end + end + end +end diff --git a/spec/models/packages/package_file_spec.rb b/spec/models/packages/package_file_spec.rb new file mode 100644 index 00000000000..7758ed4a500 --- /dev/null +++ b/spec/models/packages/package_file_spec.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true +require 'spec_helper' + +RSpec.describe Packages::PackageFile, type: :model do + describe 'relationships' do + it { is_expected.to belong_to(:package) } + it { is_expected.to have_one(:conan_file_metadatum) } + end + + describe 'validations' do + it { is_expected.to validate_presence_of(:package) } + end + + context 'with package filenames' do + let_it_be(:package_file1) { create(:package_file, :xml, file_name: 'FooBar') } + let_it_be(:package_file2) { create(:package_file, :xml, file_name: 'ThisIsATest') } + + describe '.with_file_name' do + let(:filename) { 'FooBar' } + + subject { described_class.with_file_name(filename) } + + it { is_expected.to match_array([package_file1]) } + end + + describe '.with_file_name_like' do + let(:filename) { 'foobar' } + + subject { described_class.with_file_name_like(filename) } + + it { is_expected.to match_array([package_file1]) } + end + end + + it_behaves_like 'UpdateProjectStatistics' do + subject { build(:package_file, :jar, size: 42) } + + before do + allow_any_instance_of(Packages::PackageFileUploader).to receive(:size).and_return(42) + end + end + + describe '.with_conan_package_reference' do + let_it_be(:non_matching_package_file) { create(:package_file, :nuget) } + let_it_be(:metadatum) { create(:conan_file_metadatum, :package_file) } + let_it_be(:reference) { metadatum.conan_package_reference} + + it 'returns matching packages' do + expect(described_class.with_conan_package_reference(reference)) + .to eq([metadatum.package_file]) + end + end + + describe '#update_file_metadata callback' do + let_it_be(:package_file) { build(:package_file, :nuget, file_store: nil, size: nil) } + + subject { package_file.save! } + + it 'updates metadata columns' do + expect(package_file) + .to receive(:update_file_metadata) + .and_call_original + + expect { subject } + .to change { package_file.file_store }.from(nil).to(::Packages::PackageFileUploader::Store::LOCAL) + .and change { package_file.size }.from(nil).to(3513) + end + end +end diff --git a/spec/models/packages/package_spec.rb b/spec/models/packages/package_spec.rb new file mode 100644 index 00000000000..4170bf595f0 --- /dev/null +++ b/spec/models/packages/package_spec.rb @@ -0,0 +1,485 @@ +# frozen_string_literal: true +require 'spec_helper' + +RSpec.describe Packages::Package, type: :model do + include SortingHelper + + describe 'relationships' do + it { is_expected.to belong_to(:project) } + it { is_expected.to have_many(:package_files).dependent(:destroy) } + it { is_expected.to have_many(:dependency_links).inverse_of(:package) } + it { is_expected.to have_many(:tags).inverse_of(:package) } + it { is_expected.to have_one(:conan_metadatum).inverse_of(:package) } + it { is_expected.to have_one(:maven_metadatum).inverse_of(:package) } + it { is_expected.to have_one(:nuget_metadatum).inverse_of(:package) } + end + + describe '.with_composer_target' do + let!(:package1) { create(:composer_package, :with_metadatum, sha: '123') } + let!(:package2) { create(:composer_package, :with_metadatum, sha: '123') } + let!(:package3) { create(:composer_package, :with_metadatum, sha: '234') } + + subject { described_class.with_composer_target('123').to_a } + + it 'selects packages with the specified sha' do + expect(subject).to include(package1) + expect(subject).to include(package2) + expect(subject).not_to include(package3) + end + end + + describe '.sort_by_attribute' do + let_it_be(:group) { create(:group, :public) } + let_it_be(:project) { create(:project, :public, namespace: group, name: 'project A') } + let!(:package1) { create(:npm_package, project: project, version: '3.1.0', name: "@#{project.root_namespace.path}/foo1") } + let!(:package2) { create(:nuget_package, project: project, version: '2.0.4') } + let(:package3) { create(:maven_package, project: project, version: '1.1.1', name: 'zzz') } + + before do + travel_to(1.day.ago) do + package3 + end + end + + RSpec.shared_examples 'package sorting by attribute' do |order_by| + subject { described_class.where(id: packages.map(&:id)).sort_by_attribute("#{order_by}_#{sort}").to_a } + + context "sorting by #{order_by}" do + context 'ascending order' do + let(:sort) { 'asc' } + + it { is_expected.to eq packages } + end + + context 'descending order' do + let(:sort) { 'desc' } + + it { is_expected.to eq packages.reverse } + end + end + end + + it_behaves_like 'package sorting by attribute', 'name' do + let(:packages) { [package1, package2, package3] } + end + + it_behaves_like 'package sorting by attribute', 'created_at' do + let(:packages) { [package3, package1, package2] } + end + + it_behaves_like 'package sorting by attribute', 'version' do + let(:packages) { [package3, package2, package1] } + end + + it_behaves_like 'package sorting by attribute', 'type' do + let(:packages) { [package3, package1, package2] } + end + + it_behaves_like 'package sorting by attribute', 'project_path' do + let(:another_project) { create(:project, :public, namespace: group, name: 'project B') } + let!(:package4) { create(:npm_package, project: another_project, version: '3.1.0', name: "@#{project.root_namespace.path}/bar") } + + let(:packages) { [package1, package2, package3, package4] } + end + end + + describe 'validations' do + subject { create(:package) } + + it { is_expected.to validate_presence_of(:project) } + it { is_expected.to validate_uniqueness_of(:name).scoped_to(:project_id, :version, :package_type) } + + describe '#name' do + it { is_expected.to allow_value("my/domain/com/my-app").for(:name) } + it { is_expected.to allow_value("my.app-11.07.2018").for(:name) } + it { is_expected.not_to allow_value("my(dom$$$ain)com.my-app").for(:name) } + + context 'conan package' do + subject { create(:conan_package) } + + let(:fifty_one_characters) {'f_b' * 17} + + it { is_expected.to allow_value('foo+bar').for(:name) } + it { is_expected.to allow_value('foo_bar').for(:name) } + it { is_expected.to allow_value('foo.bar').for(:name) } + it { is_expected.not_to allow_value(fifty_one_characters).for(:name) } + it { is_expected.not_to allow_value('+foobar').for(:name) } + it { is_expected.not_to allow_value('.foobar').for(:name) } + it { is_expected.not_to allow_value('%foo%bar').for(:name) } + end + end + + describe '#version' do + RSpec.shared_examples 'validating version to be SemVer compliant for' do |factory_name| + context "for #{factory_name}" do + subject { create(factory_name) } + + it { is_expected.to allow_value('1.2.3').for(:version) } + it { is_expected.to allow_value('1.2.3-beta').for(:version) } + it { is_expected.to allow_value('1.2.3-alpha.3').for(:version) } + it { is_expected.not_to allow_value('1').for(:version) } + it { is_expected.not_to allow_value('1.2').for(:version) } + it { is_expected.not_to allow_value('1./2.3').for(:version) } + it { is_expected.not_to allow_value('../../../../../1.2.3').for(:version) } + it { is_expected.not_to allow_value('%2e%2e%2f1.2.3').for(:version) } + end + end + + context 'conan package' do + subject { create(:conan_package) } + + let(:fifty_one_characters) {'1.2' * 17} + + it { is_expected.to allow_value('1.2').for(:version) } + it { is_expected.to allow_value('1.2.3-beta').for(:version) } + it { is_expected.to allow_value('1.2.3-pre1+build2').for(:version) } + it { is_expected.not_to allow_value('1').for(:version) } + it { is_expected.not_to allow_value(fifty_one_characters).for(:version) } + it { is_expected.not_to allow_value('1./2.3').for(:version) } + it { is_expected.not_to allow_value('.1.2.3').for(:version) } + it { is_expected.not_to allow_value('+1.2.3').for(:version) } + it { is_expected.not_to allow_value('%2e%2e%2f1.2.3').for(:version) } + end + + context 'maven package' do + subject { create(:maven_package) } + + it { is_expected.to allow_value('0').for(:version) } + it { is_expected.to allow_value('1').for(:version) } + it { is_expected.to allow_value('10').for(:version) } + it { is_expected.to allow_value('1.0').for(:version) } + it { is_expected.to allow_value('1.3.350.v20200505-1744').for(:version) } + it { is_expected.to allow_value('1.1-beta-2').for(:version) } + it { is_expected.to allow_value('1.2-SNAPSHOT').for(:version) } + it { is_expected.to allow_value('12.1.2-2-1').for(:version) } + it { is_expected.to allow_value('1.2.3..beta').for(:version) } + it { is_expected.to allow_value('1.2.3-beta').for(:version) } + it { is_expected.to allow_value('10.2.3-beta').for(:version) } + it { is_expected.to allow_value('2.0.0.v200706041905-7C78EK9E_EkMNfNOd2d8qq').for(:version) } + it { is_expected.to allow_value('1.2-alpha-1-20050205.060708-1').for(:version) } + it { is_expected.to allow_value('703220b4e2cea9592caeb9f3013f6b1e5335c293').for(:version) } + it { is_expected.to allow_value('RELEASE').for(:version) } + it { is_expected.not_to allow_value('..1.2.3').for(:version) } + it { is_expected.not_to allow_value(' 1.2.3').for(:version) } + it { is_expected.not_to allow_value("1.2.3 \r\t").for(:version) } + it { is_expected.not_to allow_value("\r\t 1.2.3").for(:version) } + it { is_expected.not_to allow_value('1.2.3-4/../../').for(:version) } + it { is_expected.not_to allow_value('1.2.3-4%2e%2e%').for(:version) } + it { is_expected.not_to allow_value('../../../../../1.2.3').for(:version) } + it { is_expected.not_to allow_value('%2e%2e%2f1.2.3').for(:version) } + end + + it_behaves_like 'validating version to be SemVer compliant for', :npm_package + it_behaves_like 'validating version to be SemVer compliant for', :nuget_package + end + + describe '#package_already_taken' do + context 'npm package' do + let!(:package) { create(:npm_package) } + + it 'will not allow a package of the same name' do + new_package = build(:npm_package, name: package.name) + + expect(new_package).not_to be_valid + end + end + + context 'maven package' do + let!(:package) { create(:maven_package) } + + it 'will allow a package of the same name' do + new_package = build(:maven_package, name: package.name) + + expect(new_package).to be_valid + end + end + end + + context "recipe uniqueness for conan packages" do + let!(:package) { create('conan_package') } + + it "will allow a conan package with same project, name, version and package_type" do + new_package = build('conan_package', project: package.project, name: package.name, version: package.version) + new_package.conan_metadatum.package_channel = 'beta' + expect(new_package).to be_valid + end + + it "will not allow a conan package with same recipe (name, version, metadatum.package_channel, metadatum.package_username, and package_type)" do + new_package = build('conan_package', project: package.project, name: package.name, version: package.version) + expect(new_package).not_to be_valid + expect(new_package.errors.to_a).to include("Package recipe already exists") + end + end + + Packages::Package.package_types.keys.without('conan').each do |pt| + context "project id, name, version and package type uniqueness for package type #{pt}" do + let(:package) { create("#{pt}_package") } + + it "will not allow a #{pt} package with same project, name, version and package_type" do + new_package = build("#{pt}_package", project: package.project, name: package.name, version: package.version) + expect(new_package).not_to be_valid + expect(new_package.errors.to_a).to include("Name has already been taken") + end + end + end + end + + describe '#destroy' do + let(:package) { create(:npm_package) } + let(:package_file) { package.package_files.first } + let(:project_statistics) { ProjectStatistics.for_project_ids(package.project.id).first } + + it 'affects project statistics' do + expect { package.destroy! } + .to change { project_statistics.reload.packages_size } + .from(package_file.size).to(0) + end + end + + describe '.by_name_and_file_name' do + let!(:package) { create(:npm_package) } + let!(:package_file) { package.package_files.first } + + subject { described_class } + + it 'finds a package with correct arguiments' do + expect(subject.by_name_and_file_name(package.name, package_file.file_name)).to eq(package) + end + + it 'will raise error if not found' do + expect { subject.by_name_and_file_name('foo', 'foo-5.5.5.tgz') }.to raise_error(ActiveRecord::RecordNotFound) + end + end + + context 'version scopes' do + let!(:package1) { create(:npm_package, version: '1.0.0') } + let!(:package2) { create(:npm_package, version: '1.0.1') } + let!(:package3) { create(:npm_package, version: '1.0.1') } + + describe '.last_of_each_version' do + subject { described_class.last_of_each_version } + + it 'includes only latest package per version' do + is_expected.to include(package1, package3) + is_expected.not_to include(package2) + end + end + + describe '.has_version' do + subject { described_class.has_version } + + before do + create(:maven_metadatum).package.update!(version: nil) + end + + it 'includes only packages with version attribute' do + is_expected.to match_array([package1, package2, package3]) + end + end + + describe '.with_version' do + subject { described_class.with_version('1.0.1') } + + it 'includes only packages with specified version' do + is_expected.to match_array([package2, package3]) + end + end + + describe '.without_version_like' do + let(:version_pattern) { '%.0.0%' } + + subject { described_class.without_version_like(version_pattern) } + + it 'includes packages without the version pattern' do + is_expected.to match_array([package2, package3]) + end + end + end + + context 'conan scopes' do + let!(:package) { create(:conan_package) } + + describe '.with_conan_channel' do + subject { described_class.with_conan_channel('stable') } + + it 'includes only packages with specified version' do + is_expected.to include(package) + end + end + + describe '.with_conan_username' do + subject do + described_class.with_conan_username( + Packages::Conan::Metadatum.package_username_from(full_path: package.project.full_path) + ) + end + + it 'includes only packages with specified version' do + is_expected.to match_array([package]) + end + end + end + + describe '.without_nuget_temporary_name' do + let!(:package1) { create(:nuget_package) } + let!(:package2) { create(:nuget_package, name: Packages::Nuget::CreatePackageService::TEMPORARY_PACKAGE_NAME) } + + subject { described_class.without_nuget_temporary_name } + + it 'does not include nuget temporary packages' do + expect(subject).to eq([package1]) + end + end + + describe '.processed' do + let!(:package1) { create(:nuget_package) } + let!(:package2) { create(:npm_package) } + let!(:package3) { create(:nuget_package) } + + subject { described_class.processed } + + it { is_expected.to match_array([package1, package2, package3]) } + + context 'with temporary packages' do + let!(:package1) { create(:nuget_package, name: Packages::Nuget::CreatePackageService::TEMPORARY_PACKAGE_NAME) } + + it { is_expected.to match_array([package2, package3]) } + end + end + + describe '.limit_recent' do + let!(:package1) { create(:nuget_package) } + let!(:package2) { create(:nuget_package) } + let!(:package3) { create(:nuget_package) } + + subject { described_class.limit_recent(2) } + + it { is_expected.to match_array([package3, package2]) } + end + + context 'with several packages' do + let_it_be(:package1) { create(:nuget_package, name: 'FooBar') } + let_it_be(:package2) { create(:nuget_package, name: 'foobar') } + let_it_be(:package3) { create(:npm_package) } + let_it_be(:package4) { create(:npm_package) } + + describe '.pluck_names' do + subject { described_class.pluck_names } + + it { is_expected.to match_array([package1, package2, package3, package4].map(&:name)) } + end + + describe '.pluck_versions' do + subject { described_class.pluck_versions } + + it { is_expected.to match_array([package1, package2, package3, package4].map(&:version)) } + end + + describe '.with_name_like' do + subject { described_class.with_name_like(name_term) } + + context 'with downcase name' do + let(:name_term) { 'foobar' } + + it { is_expected.to match_array([package1, package2]) } + end + + context 'with prefix wildcard' do + let(:name_term) { '%ar' } + + it { is_expected.to match_array([package1, package2]) } + end + + context 'with suffix wildcard' do + let(:name_term) { 'foo%' } + + it { is_expected.to match_array([package1, package2]) } + end + + context 'with surrounding wildcards' do + let(:name_term) { '%ooba%' } + + it { is_expected.to match_array([package1, package2]) } + end + end + + describe '.search_by_name' do + let(:query) { 'oba' } + + subject { described_class.search_by_name(query) } + + it { is_expected.to match_array([package1, package2]) } + end + end + + describe '.select_distinct_name' do + let_it_be(:nuget_package) { create(:nuget_package) } + let_it_be(:nuget_packages) { create_list(:nuget_package, 3, name: nuget_package.name, project: nuget_package.project) } + let_it_be(:maven_package) { create(:maven_package) } + let_it_be(:maven_packages) { create_list(:maven_package, 3, name: maven_package.name, project: maven_package.project) } + + subject { described_class.select_distinct_name } + + it 'returns only distinct names' do + packages = subject + + expect(packages.size).to eq(2) + expect(packages.pluck(:name)).to match_array([nuget_package.name, maven_package.name]) + end + end + + describe '#versions' do + let_it_be(:project) { create(:project) } + let_it_be(:package) { create(:maven_package, project: project) } + let_it_be(:package2) { create(:maven_package, project: project) } + let_it_be(:package3) { create(:maven_package, project: project, name: 'foo') } + + it 'returns other package versions of the same package name belonging to the project' do + expect(package.versions).to contain_exactly(package2) + end + + it 'does not return different packages' do + expect(package.versions).not_to include(package3) + end + end + + describe '#pipeline' do + let_it_be(:package) { create(:maven_package) } + + context 'package without pipeline' do + it 'returns nil if there is no pipeline' do + expect(package.pipeline).to be_nil + end + end + + context 'package with pipeline' do + let_it_be(:pipeline) { create(:ci_pipeline) } + + before do + package.create_build_info!(pipeline: pipeline) + end + + it 'returns the pipeline' do + expect(package.pipeline).to eq(pipeline) + end + end + end + + describe '#tag_names' do + let_it_be(:package) { create(:nuget_package) } + + subject { package.tag_names } + + it { is_expected.to eq([]) } + + context 'with tags' do + let(:tags) { %w(tag1 tag2 tag3) } + + before do + tags.each { |t| create(:packages_tag, name: t, package: package) } + end + + it { is_expected.to contain_exactly(*tags) } + end + end +end diff --git a/spec/models/packages/pypi/metadatum_spec.rb b/spec/models/packages/pypi/metadatum_spec.rb new file mode 100644 index 00000000000..2c9893ef8f3 --- /dev/null +++ b/spec/models/packages/pypi/metadatum_spec.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true +require 'spec_helper' + +RSpec.describe Packages::Pypi::Metadatum, type: :model do + describe 'relationships' do + it { is_expected.to belong_to(:package) } + end + + describe 'validations' do + it { is_expected.to validate_presence_of(:package) } + + describe '#pypi_package_type' do + it 'will not allow a package with a different package_type' do + package = build('package') + pypi_metadatum = build('pypi_metadatum', package: package) + + expect(pypi_metadatum).not_to be_valid + expect(pypi_metadatum.errors.to_a).to include('Package type must be PyPi') + end + end + end +end diff --git a/spec/models/packages/sem_ver_spec.rb b/spec/models/packages/sem_ver_spec.rb new file mode 100644 index 00000000000..419653dca19 --- /dev/null +++ b/spec/models/packages/sem_ver_spec.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Packages::SemVer, type: :model do + shared_examples '#parse with a valid semver' do |str, major, minor, patch, prerelease, build| + context "with #{str}" do + it "returns #{described_class.new(major, minor, patch, prerelease, build, prefixed: true)} with prefix" do + expected = described_class.new(major, minor, patch, prerelease, build, prefixed: true) + expect(described_class.parse('v' + str, prefixed: true)).to eq(expected) + end + + it "returns #{described_class.new(major, minor, patch, prerelease, build)} without prefix" do + expected = described_class.new(major, minor, patch, prerelease, build) + expect(described_class.parse(str)).to eq(expected) + end + end + end + + shared_examples '#parse with an invalid semver' do |str| + context "with #{str}" do + it 'returns nil with prefix' do + expect(described_class.parse('v' + str, prefixed: true)).to be_nil + end + + it 'returns nil without prefix' do + expect(described_class.parse(str)).to be_nil + end + end + end + + describe '#parse' do + it_behaves_like '#parse with a valid semver', '1.0.0', 1, 0, 0, nil, nil + it_behaves_like '#parse with a valid semver', '1.0.0-pre', 1, 0, 0, 'pre', nil + it_behaves_like '#parse with a valid semver', '1.0.0+build', 1, 0, 0, nil, 'build' + it_behaves_like '#parse with a valid semver', '1.0.0-pre+build', 1, 0, 0, 'pre', 'build' + it_behaves_like '#parse with an invalid semver', '01.0.0' + it_behaves_like '#parse with an invalid semver', '0.01.0' + it_behaves_like '#parse with an invalid semver', '0.0.01' + it_behaves_like '#parse with an invalid semver', '1.0.0asdf' + end +end diff --git a/spec/models/packages/tag_spec.rb b/spec/models/packages/tag_spec.rb new file mode 100644 index 00000000000..18ec99c3d51 --- /dev/null +++ b/spec/models/packages/tag_spec.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true +require 'spec_helper' + +RSpec.describe Packages::Tag, type: :model do + let!(:project) { create(:project) } + let!(:package) { create(:npm_package, version: '1.0.2', project: project, updated_at: 3.days.ago) } + + describe 'relationships' do + it { is_expected.to belong_to(:package).inverse_of(:tags) } + end + + describe 'validations' do + subject { create(:packages_tag) } + + it { is_expected.to validate_presence_of(:package) } + it { is_expected.to validate_presence_of(:name) } + end + + describe '.for_packages' do + let(:package2) { create(:package, project: project, updated_at: 2.days.ago) } + let(:package3) { create(:package, project: project, updated_at: 1.day.ago) } + let!(:tag1) { create(:packages_tag, package: package) } + let!(:tag2) { create(:packages_tag, package: package2) } + let!(:tag3) { create(:packages_tag, package: package3) } + + subject { described_class.for_packages(project.packages) } + + it { is_expected.to match_array([tag1, tag2, tag3]) } + + context 'with too many tags' do + before do + stub_const('Packages::Tag::FOR_PACKAGES_TAGS_LIMIT', 2) + end + + it { is_expected.to match_array([tag2, tag3]) } + end + end + + describe '.with_name' do + let_it_be(:package) { create(:package) } + let_it_be(:tag1) { create(:packages_tag, package: package, name: 'tag1') } + let_it_be(:tag2) { create(:packages_tag, package: package, name: 'tag2') } + let_it_be(:tag3) { create(:packages_tag, package: package, name: 'tag3') } + let(:name) { 'tag1' } + + subject { described_class.with_name(name) } + + it { is_expected.to contain_exactly(tag1) } + + context 'with nil name' do + let(:name) { nil } + + it { is_expected.to eq([]) } + end + + context 'with multiple names' do + let(:name) { %w(tag1 tag3) } + + it { is_expected.to contain_exactly(tag1, tag3) } + end + end +end |