Commit 8314833c authored by Stan Hu's avatar Stan Hu

Merge branch '37364-fix-npm-validator-with-name+version-duplicates' into 'master'

Update the npm package validator

See merge request gitlab-org/gitlab!67427
parents ac03d316 508e2b0b
...@@ -324,7 +324,7 @@ class Packages::Package < ApplicationRecord ...@@ -324,7 +324,7 @@ class Packages::Package < ApplicationRecord
return unless project return unless project
return unless follows_npm_naming_convention? return unless follows_npm_naming_convention?
if project.package_already_taken?(name, package_type: :npm) if project.package_already_taken?(name, version, package_type: :npm)
errors.add(:base, _('Package already exists')) errors.add(:base, _('Package already exists'))
end end
end end
......
...@@ -2566,14 +2566,14 @@ class Project < ApplicationRecord ...@@ -2566,14 +2566,14 @@ class Project < ApplicationRecord
[project&.id, root_group&.id] [project&.id, root_group&.id]
end end
def package_already_taken?(package_name, package_type:) def package_already_taken?(package_name, package_version, package_type:)
namespace.root_ancestor.all_projects Packages::Package.with_name(package_name)
.joins(:packages) .with_version(package_version)
.where.not(id: id)
.merge(
Packages::Package.default_scoped
.with_name(package_name)
.with_package_type(package_type) .with_package_type(package_type)
.for_projects(
root_ancestor.all_projects
.id_not_in(id)
.select(:id)
).exists? ).exists?
end end
......
...@@ -353,6 +353,13 @@ In this configuration: ...@@ -353,6 +353,13 @@ In this configuration:
You cannot publish a package if a package of the same name and version already exists. You cannot publish a package if a package of the same name and version already exists.
You must delete the existing package first. You must delete the existing package first.
This rule has a different impact depending on the package name:
- For packages following the [naming convention](#package-naming-convention), you can't publish a
package with a duplicate name and version to the root namespace.
- For packages not following the [naming convention](#package-naming-convention), you can't publish
a package with a duplicate name and version to the project you target with the upload.
This aligns with npmjs.org's behavior. However, npmjs.org does not ever let you publish This aligns with npmjs.org's behavior. However, npmjs.org does not ever let you publish
the same version more than once, even if it has been deleted. the same version more than once, even if it has been deleted.
......
...@@ -3,6 +3,7 @@ require 'spec_helper' ...@@ -3,6 +3,7 @@ require 'spec_helper'
RSpec.describe Packages::Package, type: :model do RSpec.describe Packages::Package, type: :model do
include SortingHelper include SortingHelper
using RSpec::Parameterized::TableSyntax
it_behaves_like 'having unique enum values' it_behaves_like 'having unique enum values'
...@@ -435,33 +436,154 @@ RSpec.describe Packages::Package, type: :model do ...@@ -435,33 +436,154 @@ RSpec.describe Packages::Package, type: :model do
let_it_be(:second_project) { create(:project, namespace: group)} let_it_be(:second_project) { create(:project, namespace: group)}
let(:package) { build(:npm_package, project: project, name: name) } let(:package) { build(:npm_package, project: project, name: name) }
let(:second_package) { build(:npm_package, project: second_project, name: name, version: '5.0.0') }
context 'following the naming convention' do shared_examples 'validating the first package' do
let(:name) { "@#{group.path}/test" } it 'validates the first package' do
it 'will allow the first package' do
expect(package).to be_valid expect(package).to be_valid
end end
end
it 'will not allow npm package with duplicate name' do shared_examples 'validating the second package' do
it 'validates the second package' do
package.save!
expect(second_package).to be_valid
end
end
shared_examples 'not validating the second package' do |field_with_error:|
it 'does not validate the second package' do
package.save! package.save!
expect(second_package).not_to be_valid expect(second_package).not_to be_valid
case field_with_error
when :base
expect(second_package.errors.messages[:base]).to eq ['Package already exists']
when :name
expect(second_package.errors.messages[:name]).to eq ['has already been taken']
else
raise ArgumentError, "field #{field_with_error} not expected"
end
end
end
context 'following the naming convention' do
let(:name) { "@#{group.path}/test" }
context 'with the second package in the project of the first package' do
let(:second_package) { build(:npm_package, project: project, name: second_package_name, version: second_package_version) }
context 'with no duplicated name' do
let(:second_package_name) { "@#{group.path}/test2" }
let(:second_package_version) { '5.0.0' }
it_behaves_like 'validating the first package'
it_behaves_like 'validating the second package'
end
context 'with duplicated name' do
let(:second_package_name) { package.name }
let(:second_package_version) { '5.0.0' }
it_behaves_like 'validating the first package'
it_behaves_like 'validating the second package'
end
context 'with duplicate name and duplicated version' do
let(:second_package_name) { package.name }
let(:second_package_version) { package.version }
it_behaves_like 'validating the first package'
it_behaves_like 'not validating the second package', field_with_error: :name
end
end
context 'with the second package in a different project than the first package' do
let(:second_package) { build(:npm_package, project: second_project, name: second_package_name, version: second_package_version) }
context 'with no duplicated name' do
let(:second_package_name) { "@#{group.path}/test2" }
let(:second_package_version) { '5.0.0' }
it_behaves_like 'validating the first package'
it_behaves_like 'validating the second package'
end
context 'with duplicated name' do
let(:second_package_name) { package.name }
let(:second_package_version) { '5.0.0' }
it_behaves_like 'validating the first package'
it_behaves_like 'validating the second package'
end
context 'with duplicate name and duplicated version' do
let(:second_package_name) { package.name }
let(:second_package_version) { package.version }
it_behaves_like 'validating the first package'
it_behaves_like 'not validating the second package', field_with_error: :base
end
end end
end end
context 'not following the naming convention' do context 'not following the naming convention' do
let(:name) { '@foobar/test' } let(:name) { '@foobar/test' }
it 'will allow the first package' do context 'with the second package in the project of the first package' do
expect(package).to be_valid let(:second_package) { build(:npm_package, project: project, name: second_package_name, version: second_package_version) }
context 'with no duplicated name' do
let(:second_package_name) { "@foobar/test2" }
let(:second_package_version) { '5.0.0' }
it_behaves_like 'validating the first package'
it_behaves_like 'validating the second package'
end end
it 'will allow npm package with duplicate name' do context 'with duplicated name' do
package.save! let(:second_package_name) { package.name }
let(:second_package_version) { '5.0.0' }
expect(second_package).to be_valid it_behaves_like 'validating the first package'
it_behaves_like 'validating the second package'
end
context 'with duplicate name and duplicated version' do
let(:second_package_name) { package.name }
let(:second_package_version) { package.version }
it_behaves_like 'validating the first package'
it_behaves_like 'not validating the second package', field_with_error: :name
end
end
context 'with the second package in a different project than the first package' do
let(:second_package) { build(:npm_package, project: second_project, name: second_package_name, version: second_package_version) }
context 'with no duplicated name' do
let(:second_package_name) { "@foobar/test2" }
let(:second_package_version) { '5.0.0' }
it_behaves_like 'validating the first package'
it_behaves_like 'validating the second package'
end
context 'with duplicated name' do
let(:second_package_name) { package.name }
let(:second_package_version) { '5.0.0' }
it_behaves_like 'validating the first package'
it_behaves_like 'validating the second package'
end
context 'with duplicate name and duplicated version' do
let(:second_package_name) { package.name }
let(:second_package_version) { package.version }
it_behaves_like 'validating the first package'
it_behaves_like 'validating the second package'
end
end end
end end
end end
......
...@@ -826,8 +826,6 @@ RSpec.describe Project, factory_default: :keep do ...@@ -826,8 +826,6 @@ RSpec.describe Project, factory_default: :keep do
end end
describe '#merge_method' do describe '#merge_method' do
using RSpec::Parameterized::TableSyntax
where(:ff, :rebase, :method) do where(:ff, :rebase, :method) do
true | true | :ff true | true | :ff
true | false | :ff true | false | :ff
...@@ -1951,8 +1949,6 @@ RSpec.describe Project, factory_default: :keep do ...@@ -1951,8 +1949,6 @@ RSpec.describe Project, factory_default: :keep do
end end
context 'when set to INTERNAL in application settings' do context 'when set to INTERNAL in application settings' do
using RSpec::Parameterized::TableSyntax
before do before do
stub_application_setting(default_project_visibility: Gitlab::VisibilityLevel::INTERNAL) stub_application_setting(default_project_visibility: Gitlab::VisibilityLevel::INTERNAL)
end end
...@@ -2013,8 +2009,6 @@ RSpec.describe Project, factory_default: :keep do ...@@ -2013,8 +2009,6 @@ RSpec.describe Project, factory_default: :keep do
end end
describe '#default_branch_protected?' do describe '#default_branch_protected?' do
using RSpec::Parameterized::TableSyntax
let_it_be(:namespace) { create(:namespace) } let_it_be(:namespace) { create(:namespace) }
let_it_be(:project) { create(:project, namespace: namespace) } let_it_be(:project) { create(:project, namespace: namespace) }
...@@ -6839,33 +6833,44 @@ RSpec.describe Project, factory_default: :keep do ...@@ -6839,33 +6833,44 @@ RSpec.describe Project, factory_default: :keep do
end end
describe '#package_already_taken?' do describe '#package_already_taken?' do
let_it_be(:namespace) { create(:namespace) } let_it_be(:namespace) { create(:namespace, path: 'test') }
let_it_be(:project) { create(:project, :public, namespace: namespace) } let_it_be(:project) { create(:project, :public, namespace: namespace) }
let_it_be(:package) { create(:npm_package, project: project, name: "@#{namespace.path}/foo") } let_it_be(:package) { create(:npm_package, project: project, name: "@#{namespace.path}/foo", version: '1.2.3') }
context 'no package exists with the same name' do subject { project.package_already_taken?(package_name, package_version, package_type: :npm) }
it 'returns false' do
result = project.package_already_taken?("@#{namespace.path}/bar", package_type: :npm) context 'within the package project' do
expect(result).to be false where(:package_name, :package_version, :expected_result) do
'@test/bar' | '1.2.3' | false
'@test/bar' | '5.5.5' | false
'@test/foo' | '1.2.3' | false
'@test/foo' | '5.5.5' | false
end end
it 'returns false if it is the project that the package belongs to' do with_them do
result = project.package_already_taken?("@#{namespace.path}/foo", package_type: :npm) it { is_expected.to eq expected_result}
expect(result).to be false
end end
end end
context 'a package already exists with the same name' do context 'within a different project' do
let_it_be(:alt_project) { create(:project, :public, namespace: namespace) } let_it_be(:alt_project) { create(:project, :public, namespace: namespace) }
it 'returns true' do subject { alt_project.package_already_taken?(package_name, package_version, package_type: :npm) }
result = alt_project.package_already_taken?(package.name, package_type: :npm)
expect(result).to be true where(:package_name, :package_version, :expected_result) do
'@test/bar' | '1.2.3' | false
'@test/bar' | '5.5.5' | false
'@test/foo' | '1.2.3' | true
'@test/foo' | '5.5.5' | false
end
with_them do
it { is_expected.to eq expected_result}
end end
context 'for a different package type' do context 'for a different package type' do
it 'returns false' do it 'returns false' do
result = alt_project.package_already_taken?(package.name, package_type: :nuget) result = alt_project.package_already_taken?(package.name, package.version, package_type: :nuget)
expect(result).to be false expect(result).to be false
end end
end end
......
...@@ -161,8 +161,10 @@ RSpec.describe API::NpmProjectPackages do ...@@ -161,8 +161,10 @@ RSpec.describe API::NpmProjectPackages do
end end
end end
context 'valid package record' do context 'valid package params' do
let(:params) { upload_params(package_name: package_name) } let_it_be(:version) { '1.2.3' }
let(:params) { upload_params(package_name: package_name, package_version: version) }
let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace, user: user } } let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace, user: user } }
shared_examples 'handling upload with different authentications' do shared_examples 'handling upload with different authentications' do
...@@ -211,6 +213,15 @@ RSpec.describe API::NpmProjectPackages do ...@@ -211,6 +213,15 @@ RSpec.describe API::NpmProjectPackages do
end end
end end
shared_examples 'uploading the package' do
it 'uploads the package' do
expect { upload_package_with_token(package_name, params) }
.to change { project.packages.count }.by(1)
expect(response).to have_gitlab_http_status(:ok)
end
end
context 'with a scoped name' do context 'with a scoped name' do
let(:package_name) { "@#{group.path}/my_package_name" } let(:package_name) { "@#{group.path}/my_package_name" }
...@@ -233,24 +244,25 @@ RSpec.describe API::NpmProjectPackages do ...@@ -233,24 +244,25 @@ RSpec.describe API::NpmProjectPackages do
let_it_be(:second_project) { create(:project, namespace: namespace) } let_it_be(:second_project) { create(:project, namespace: namespace) }
context 'following the naming convention' do context 'following the naming convention' do
let_it_be(:second_package) { create(:npm_package, project: second_project, name: "@#{group.path}/test") } let_it_be(:second_package) { create(:npm_package, project: second_project, name: "@#{group.path}/test", version: version) }
let(:package_name) { "@#{group.path}/test" } let(:package_name) { "@#{group.path}/test" }
it_behaves_like 'handling invalid record with 400 error' it_behaves_like 'handling invalid record with 400 error'
context 'with a new version' do
let_it_be(:version) { '4.5.6' }
it_behaves_like 'uploading the package'
end
end end
context 'not following the naming convention' do context 'not following the naming convention' do
let_it_be(:second_package) { create(:npm_package, project: second_project, name: "@any_scope/test") } let_it_be(:second_package) { create(:npm_package, project: second_project, name: "@any_scope/test", version: version) }
let(:package_name) { "@any_scope/test" } let(:package_name) { "@any_scope/test" }
it "uploads the package" do it_behaves_like 'uploading the package'
expect { upload_package_with_token(package_name, params) }
.to change { project.packages.count }.by(1)
expect(response).to have_gitlab_http_status(:ok)
end
end end
end end
end end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment