From b55394d7f2dcca8faec77421dcc05c9b31d94604 Mon Sep 17 00:00:00 2001 From: Kamil Bukum Date: Thu, 31 Oct 2024 14:12:12 -0700 Subject: [PATCH] Refactor npm_and_yarn to use separate classes for npm, yarn, and pnpm, aligning with centralized package manager abstraction used across ecosystems. --- .../dependabot/npm_and_yarn/file_fetcher.rb | 29 ++- .../dependabot/npm_and_yarn/file_parser.rb | 76 +++++- .../lib/dependabot/npm_and_yarn/helpers.rb | 21 +- .../npm_and_yarn/package_manager.rb | 228 +++++++++++++++++- .../npm_and_yarn/npm_package_manager_spec.rb | 50 ++++ .../package_manager_detector_spec.rb | 117 +++++++++ .../package_manager_helper_spec.rb | 121 ++++++++++ .../npm_and_yarn/pnpm_package_manager_spec.rb | 50 ++++ .../npm_and_yarn/yarn_package_manager_spec.rb | 50 ++++ 9 files changed, 716 insertions(+), 26 deletions(-) create mode 100644 npm_and_yarn/spec/dependabot/npm_and_yarn/npm_package_manager_spec.rb create mode 100644 npm_and_yarn/spec/dependabot/npm_and_yarn/package_manager_detector_spec.rb create mode 100644 npm_and_yarn/spec/dependabot/npm_and_yarn/package_manager_helper_spec.rb create mode 100644 npm_and_yarn/spec/dependabot/npm_and_yarn/pnpm_package_manager_spec.rb create mode 100644 npm_and_yarn/spec/dependabot/npm_and_yarn/yarn_package_manager_spec.rb diff --git a/npm_and_yarn/lib/dependabot/npm_and_yarn/file_fetcher.rb b/npm_and_yarn/lib/dependabot/npm_and_yarn/file_fetcher.rb index eb9161a676a..03e53ecee7e 100644 --- a/npm_and_yarn/lib/dependabot/npm_and_yarn/file_fetcher.rb +++ b/npm_and_yarn/lib/dependabot/npm_and_yarn/file_fetcher.rb @@ -182,25 +182,36 @@ def inferred_npmrc # rubocop:disable Metrics/PerceivedComplexity sig { returns(T.nilable(T.any(Integer, String))) } def npm_version - @npm_version ||= T.let(package_manager.setup("npm"), T.nilable(T.any(Integer, String))) + @npm_version ||= T.let(package_manager_helper.setup("npm"), T.nilable(T.any(Integer, String))) end sig { returns(T.nilable(T.any(Integer, String))) } def yarn_version - @yarn_version ||= T.let(package_manager.setup("yarn"), T.nilable(T.any(Integer, String))) + @yarn_version ||= T.let(package_manager_helper.setup("yarn"), T.nilable(T.any(Integer, String))) end sig { returns(T.nilable(T.any(Integer, String))) } def pnpm_version - @pnpm_version ||= T.let(package_manager.setup("pnpm"), T.nilable(T.any(Integer, String))) + @pnpm_version ||= T.let(package_manager_helper.setup("pnpm"), T.nilable(T.any(Integer, String))) end - sig { returns(PackageManager) } - def package_manager - @package_manager ||= T.let(PackageManager.new( - parsed_package_json, - lockfiles: { npm: package_lock || shrinkwrap, yarn: yarn_lock, pnpm: pnpm_lock } - ), T.nilable(PackageManager)) + sig { returns(PackageManagerHelper) } + def package_manager_helper + @package_manager_helper ||= T.let( + PackageManagerHelper.new( + parsed_package_json, + lockfiles: lockfiles + ), T.nilable(PackageManagerHelper) + ) + end + + sig { returns(T::Hash[Symbol, T.nilable(Dependabot::DependencyFile)]) } + def lockfiles + { + npm: package_lock || shrinkwrap, + yarn: yarn_lock, + pnpm: pnpm_lock + } end sig { returns(DependencyFile) } diff --git a/npm_and_yarn/lib/dependabot/npm_and_yarn/file_parser.rb b/npm_and_yarn/lib/dependabot/npm_and_yarn/file_parser.rb index e78b6c23a81..2873648c01d 100644 --- a/npm_and_yarn/lib/dependabot/npm_and_yarn/file_parser.rb +++ b/npm_and_yarn/lib/dependabot/npm_and_yarn/file_parser.rb @@ -19,7 +19,7 @@ module Dependabot module NpmAndYarn - class FileParser < Dependabot::FileParsers::Base + class FileParser < Dependabot::FileParsers::Base # rubocop:disable Metrics/ClassLength extend T::Sig require "dependabot/file_parsers/base/dependency_set" @@ -78,8 +78,82 @@ def parse end end + sig { returns(Ecosystem) } + def ecosystem + @ecosystem ||= T.let( + Ecosystem.new( + name: ECOSYSTEM, + package_manager: package_manager_helper.package_manager + ), + T.nilable(Ecosystem) + ) + end + private + sig { returns(PackageManagerHelper) } + def package_manager_helper + @package_manager_helper ||= T.let( + PackageManagerHelper.new( + parsed_package_json, + lockfiles: lockfiles + ), T.nilable(PackageManagerHelper) + ) + end + + sig { returns(T::Hash[Symbol, T.nilable(Dependabot::DependencyFile)]) } + def lockfiles + { + npm: package_lock || shrinkwrap, + yarn: yarn_lock, + pnpm: pnpm_lock + } + end + + sig { returns(T.untyped) } + def parsed_package_json + JSON.parse(T.must(package_json.content)) + rescue JSON::ParserError + raise Dependabot::DependencyFileNotParseable, package_json.path + end + + sig { returns(Dependabot::DependencyFile) } + def package_json + # Declare the instance variable with T.let and the correct type + @package_json ||= T.let( + T.must(dependency_files.find { |f| f.name == "package.json" }), + T.nilable(Dependabot::DependencyFile) + ) + end + + sig { returns(T.nilable(Dependabot::DependencyFile)) } + def shrinkwrap + @shrinkwrap ||= T.let(dependency_files.find do |f| + f.name == "npm-shrinkwrap.json" + end, T.nilable(Dependabot::DependencyFile)) + end + + sig { returns(T.nilable(Dependabot::DependencyFile)) } + def package_lock + @package_lock ||= T.let(dependency_files.find do |f| + f.name == "package-lock.json" + end, T.nilable(Dependabot::DependencyFile)) + end + + sig { returns(T.nilable(Dependabot::DependencyFile)) } + def yarn_lock + @yarn_lock ||= T.let(dependency_files.find do |f| + f.name == "yarn.lock" + end, T.nilable(Dependabot::DependencyFile)) + end + + sig { returns(T.nilable(Dependabot::DependencyFile)) } + def pnpm_lock + @pnpm_lock ||= T.let(dependency_files.find do |f| + f.name == "pnpm-lock.yaml" + end, T.nilable(Dependabot::DependencyFile)) + end + sig { returns(Dependabot::FileParsers::Base::DependencySet) } def manifest_dependencies dependency_set = DependencySet.new diff --git a/npm_and_yarn/lib/dependabot/npm_and_yarn/helpers.rb b/npm_and_yarn/lib/dependabot/npm_and_yarn/helpers.rb index baed146df29..5e05766226a 100644 --- a/npm_and_yarn/lib/dependabot/npm_and_yarn/helpers.rb +++ b/npm_and_yarn/lib/dependabot/npm_and_yarn/helpers.rb @@ -37,7 +37,7 @@ module Helpers # Determines the npm version depends to the feature flag # If the feature flag is enabled, we are going to use the minimum version npm 8 # Otherwise, we are going to use old versionining npm 6 - sig { params(lockfile: DependencyFile).returns(Integer) } + sig { params(lockfile: T.nilable(DependencyFile)).returns(Integer) } def self.npm_version_numeric(lockfile) fallback_version_npm8 = Dependabot::Experiments.enabled?(:npm_fallback_version_above_v6) @@ -46,9 +46,12 @@ def self.npm_version_numeric(lockfile) npm_version_numeric_npm6_or_higher(lockfile) end - sig { params(lockfile: DependencyFile).returns(Integer) } + sig { params(lockfile: T.nilable(DependencyFile)).returns(Integer) } def self.npm_version_numeric_npm6_or_higher(lockfile) - lockfile_content = T.must(lockfile.content) + lockfile_content = lockfile&.content + + return NPM_V8 if lockfile_content.nil? || lockfile_content.strip.empty? + return NPM_V8 if JSON.parse(lockfile_content)["lockfileVersion"].to_i >= 2 NPM_V6 @@ -60,9 +63,9 @@ def self.npm_version_numeric_npm6_or_higher(lockfile) # - NPM 7 uses lockfileVersion 2 # - NPM 8 uses lockfileVersion 2 # - NPM 9 uses lockfileVersion 3 - sig { params(lockfile: DependencyFile).returns(Integer) } + sig { params(lockfile: T.nilable(DependencyFile)).returns(Integer) } def self.npm_version_numeric_npm8_or_higher(lockfile) - lockfile_content = lockfile.content + lockfile_content = lockfile&.content # Return default NPM version if there's no lockfile or it's empty return NPM_DEFAULT_VERSION if lockfile_content.nil? || lockfile_content.strip.empty? @@ -85,8 +88,12 @@ def self.npm_version_numeric_npm8_or_higher(lockfile) NPM_DEFAULT_VERSION # Fallback to default npm version if parsing fails end - sig { params(yarn_lock: DependencyFile).returns(Integer) } + sig { params(yarn_lock: T.nilable(DependencyFile)).returns(Integer) } def self.yarn_version_numeric(yarn_lock) + lockfile_content = yarn_lock&.content + + return YARN_DEFAULT_VERSION if lockfile_content.nil? || lockfile_content.strip.empty? + if yarn_berry?(yarn_lock) YARN_DEFAULT_VERSION else @@ -117,7 +124,7 @@ def self.fetch_yarnrc_yml_value(key, default_value) sig { params(package_lock: T.nilable(DependencyFile)).returns(T::Boolean) } def self.npm8?(package_lock) - return true unless package_lock + return true unless package_lock&.content npm_version_numeric(package_lock) == NPM_V8 end diff --git a/npm_and_yarn/lib/dependabot/npm_and_yarn/package_manager.rb b/npm_and_yarn/lib/dependabot/npm_and_yarn/package_manager.rb index c60d98d6b5a..bc6cf6b13fc 100644 --- a/npm_and_yarn/lib/dependabot/npm_and_yarn/package_manager.rb +++ b/npm_and_yarn/lib/dependabot/npm_and_yarn/package_manager.rb @@ -2,18 +2,213 @@ # frozen_string_literal: true require "dependabot/shared_helpers" +require "dependabot/ecosystem" require "dependabot/npm_and_yarn/version_selector" module Dependabot module NpmAndYarn - class PackageManager + ECOSYSTEM = "npm_and_yarn" + + class NpmPackageManager < Ecosystem::VersionManager + extend T::Sig + NAME = "npm" + LOCKFILE_NAME = "package-lock.json" + + NPM_V6 = "6" + NPM_V7 = "7" + NPM_V8 = "8" + NPM_V9 = "9" + + # Keep versions in ascending order + SUPPORTED_VERSIONS = T.let([ + Version.new(NPM_V6), + Version.new(NPM_V7), + Version.new(NPM_V8), + Version.new(NPM_V9) + ].freeze, T::Array[Dependabot::Version]) + + DEPRECATED_VERSIONS = T.let([].freeze, T::Array[Dependabot::Version]) + + sig { params(raw_version: String).void } + def initialize(raw_version) + super( + NAME, + Version.new(raw_version), + DEPRECATED_VERSIONS, + SUPPORTED_VERSIONS + ) + end + + sig { override.returns(T::Boolean) } + def deprecated? + false + end + + sig { override.returns(T::Boolean) } + def unsupported? + false + end + end + + class YarnPackageManager < Ecosystem::VersionManager + extend T::Sig + NAME = "yarn" + LOCKFILE_NAME = "yarn.lock" + + YARN_V1 = "1" + YARN_V2 = "2" + YARN_V3 = "3" + + SUPPORTED_VERSIONS = T.let([ + Version.new(YARN_V1), + Version.new(YARN_V2), + Version.new(YARN_V3) + ].freeze, T::Array[Dependabot::Version]) + + DEPRECATED_VERSIONS = T.let([].freeze, T::Array[Dependabot::Version]) + + sig { params(raw_version: String).void } + def initialize(raw_version) + super( + NAME, + Version.new(raw_version), + DEPRECATED_VERSIONS, + SUPPORTED_VERSIONS + ) + end + + sig { override.returns(T::Boolean) } + def deprecated? + false + end + + sig { override.returns(T::Boolean) } + def unsupported? + false + end + end + + class PNPMPackageManager < Ecosystem::VersionManager + extend T::Sig + NAME = "pnpm" + LOCKFILE_NAME = "pnpm-lock.yaml" + + PNPM_V7 = "7" + PNPM_V8 = "8" + PNPM_V9 = "9" + + SUPPORTED_VERSIONS = T.let([ + Version.new(PNPM_V7), + Version.new(PNPM_V8), + Version.new(PNPM_V9) + ].freeze, T::Array[Dependabot::Version]) + + DEPRECATED_VERSIONS = T.let([].freeze, T::Array[Dependabot::Version]) + + sig { params(raw_version: String).void } + def initialize(raw_version) + super( + NAME, + Version.new(raw_version), + DEPRECATED_VERSIONS, + SUPPORTED_VERSIONS + ) + end + + sig { override.returns(T::Boolean) } + def deprecated? + false + end + + sig { override.returns(T::Boolean) } + def unsupported? + false + end + end + + PACKAGE_MANAGER_CLASSES = { + NpmPackageManager::NAME => NpmPackageManager, + YarnPackageManager::NAME => YarnPackageManager, + PNPMPackageManager::NAME => PNPMPackageManager + }.freeze + + class PackageManagerDetector + extend T::Sig + extend T::Helpers + + sig do + params( + lockfiles: T::Hash[Symbol, T.nilable(Dependabot::DependencyFile)], + package_json: T::Hash[String, T.untyped] + ).void + end + def initialize(lockfiles, package_json) + @lockfiles = lockfiles + @package_json = package_json + @manifest_package_manager = package_json["packageManager"] + @engines = package_json.fetch("engines", nil) + end + + sig { returns(T.nilable(String)) } + def detect_package_manager + name_from_lockfiles || name_from_package_manager_attr || name_from_engines + end + + private + + sig { returns(T.nilable(String)) } + def name_from_lockfiles + # We prioritize the package manager that has a lockfile + PACKAGE_MANAGER_CLASSES.each_key do |manager_name| # iterates keys in order as defined in the hash + return manager_name.to_s if @lockfiles[manager_name.to_sym] + end + nil + end + + sig { returns(T.nilable(String)) } + def name_from_package_manager_attr + return unless @manifest_package_manager + + # We prioritize the package manager that has a lockfile + PACKAGE_MANAGER_CLASSES.each_key do |manager_name| # iterates keys in order as defined in the hash + return manager_name.to_s if @manifest_package_manager.start_with?("#{manager_name}@") + end + end + + sig { returns(T.nilable(String)) } + def name_from_engines + return unless @engines.is_a?(Hash) + + PACKAGE_MANAGER_CLASSES.each_key do |manager_name| # iterates keys in order as defined in the hash + return manager_name.to_s if @engines[manager_name.to_s] + end + end + end + + class PackageManagerHelper extend T::Sig extend T::Helpers + + DEFAULT_PACKAGE_MANAGER = NpmPackageManager::NAME + + sig do + params( + package_json: T::Hash[String, T.untyped], + lockfiles: T::Hash[Symbol, T.nilable(Dependabot::DependencyFile)] + ).void + end def initialize(package_json, lockfiles:) @package_json = package_json @lockfiles = lockfiles - @package_manager = package_json.fetch("packageManager", nil) + @manifest_package_manager = package_json["packageManager"] @engines = package_json.fetch("engines", nil) + @package_manager_detector = PackageManagerDetector.new(@lockfiles, @package_json) + end + + sig { returns(Ecosystem::VersionManager) } + def package_manager + name = @package_manager_detector.detect_package_manager || DEFAULT_PACKAGE_MANAGER + package_manager_by_name(name) end # rubocop:disable Metrics/CyclomaticComplexity @@ -23,24 +218,28 @@ def setup(name) # i.e. if { engines : "pnpm" : "6" } and { packageManager: "pnpm@6.0.2" }, # we go for the specificity mentioned in packageManager (6.0.2) - unless @package_manager&.start_with?("#{name}@") || (@package_manager&.==name.to_s) || @package_manager.nil? + unless @manifest_package_manager&.start_with?("#{name}@") || + (@manifest_package_manager&.==name.to_s) || + @manifest_package_manager.nil? return end - if @engines && @package_manager.nil? + if @engines && @manifest_package_manager.nil? # if "packageManager" doesn't exists in manifest file, # we check if we can extract "engines" information version = check_engine_version(name) - elsif @package_manager&.==name.to_s + elsif @manifest_package_manager&.==name.to_s # if "packageManager" is found but no version is specified (i.e. pnpm@1.2.3), # we check if we can get "engines" info to override default version version = check_engine_version(name) if @engines - elsif @package_manager&.start_with?("#{name}@") + elsif @manifest_package_manager&.start_with?("#{name}@") # if "packageManager" info has version specification i.e. yarn@3.3.1 # we go with the version in "packageManager" - Dependabot.logger.info("Found \"packageManager\" : \"#{@package_manager}\". Skipped checking \"engines\".") + Dependabot.logger.info( + "Found \"packageManager\" : \"#{@manifest_package_manager}\". Skipped checking \"engines\"." + ) end version ||= requested_version(name) @@ -66,6 +265,17 @@ def setup(name) private + sig { params(name: String).returns(Ecosystem::VersionManager) } + def package_manager_by_name(name) + package_manager_class = PACKAGE_MANAGER_CLASSES[name] + + package_manager_class ||= PACKAGE_MANAGER_CLASSES[DEFAULT_PACKAGE_MANAGER] + + version = Helpers.send(:"#{name}_version_numeric", @lockfiles[name.to_sym]) + + package_manager_class.new(version.to_s) + end + def raise_if_unsupported!(name, version) return unless name == "pnpm" return unless Version.new(version) < Version.new("7") @@ -83,9 +293,9 @@ def install(name, version) end def requested_version(name) - return unless @package_manager + return unless @manifest_package_manager - match = @package_manager.match(/^#{name}@(?\d+.\d+.\d+)/) + match = @manifest_package_manager.match(/^#{name}@(?\d+.\d+.\d+)/) return unless match Dependabot.logger.info("Requested version #{match['version']}") diff --git a/npm_and_yarn/spec/dependabot/npm_and_yarn/npm_package_manager_spec.rb b/npm_and_yarn/spec/dependabot/npm_and_yarn/npm_package_manager_spec.rb new file mode 100644 index 00000000000..77ac6c251be --- /dev/null +++ b/npm_and_yarn/spec/dependabot/npm_and_yarn/npm_package_manager_spec.rb @@ -0,0 +1,50 @@ +# typed: false +# frozen_string_literal: true + +require "dependabot/npm_and_yarn/package_manager" +require "dependabot/ecosystem" +require "spec_helper" + +RSpec.describe Dependabot::NpmAndYarn::NpmPackageManager do + let(:package_manager) { described_class.new(version) } + + describe "#initialize" do + context "when version is a String" do + let(:version) { "8" } + + it "sets the version correctly" do + expect(package_manager.version).to eq(Dependabot::Version.new(version)) + end + + it "sets the name correctly" do + expect(package_manager.name).to eq(Dependabot::NpmAndYarn::NpmPackageManager::NAME) + end + + it "sets the deprecated_versions correctly" do + expect(package_manager.deprecated_versions).to eq( + Dependabot::NpmAndYarn::NpmPackageManager::DEPRECATED_VERSIONS + ) + end + + it "sets the supported_versions correctly" do + expect(package_manager.supported_versions).to eq(Dependabot::NpmAndYarn::NpmPackageManager::SUPPORTED_VERSIONS) + end + end + end + + describe "#deprecated?" do + let(:version) { "6" } + + it "returns false" do + expect(package_manager.deprecated?).to be false + end + end + + describe "#unsupported?" do + let(:version) { "5" } + + it "returns false for supported versions" do + expect(package_manager.unsupported?).to be false + end + end +end diff --git a/npm_and_yarn/spec/dependabot/npm_and_yarn/package_manager_detector_spec.rb b/npm_and_yarn/spec/dependabot/npm_and_yarn/package_manager_detector_spec.rb new file mode 100644 index 00000000000..13c2da09717 --- /dev/null +++ b/npm_and_yarn/spec/dependabot/npm_and_yarn/package_manager_detector_spec.rb @@ -0,0 +1,117 @@ +# typed: false +# frozen_string_literal: true + +require "dependabot/npm_and_yarn/package_manager" +require "spec_helper" + +RSpec.describe Dependabot::NpmAndYarn::PackageManagerDetector do + let(:npm_lockfile) do + instance_double( + Dependabot::DependencyFile, + name: "package-lock.json", + content: <<~LOCKFILE + { + "name": "example-npm-project", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "dependencies": { + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-abc123" + } + } + } + LOCKFILE + ) + end + + let(:yarn_lockfile) do + instance_double( + Dependabot::DependencyFile, + name: "yarn.lock", + content: <<~LOCKFILE + # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. + # yarn lockfile v1 + + lodash@^4.17.20: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#abc123" + integrity sha512-abc123 + LOCKFILE + ) + end + + let(:pnpm_lockfile) do + instance_double( + Dependabot::DependencyFile, + name: "pnpm-lock.yaml", + content: <<~LOCKFILE + lockfileVersion: 5.4 + + dependencies: + lodash: + specifier: ^4.17.20 + version: 4.17.21 + resolution: + integrity: sha512-abc123 + tarball: https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz + LOCKFILE + ) + end + + let(:lockfiles) { { npm: npm_lockfile, yarn: yarn_lockfile, pnpm: pnpm_lockfile } } + let(:package_json) { { "packageManager" => "npm@7" } } + let(:detector) { described_class.new(lockfiles, package_json) } + + describe "#detect_package_manager" do + context "when npm lockfile exists" do + it "returns npm as the package manager" do + expect(detector.detect_package_manager).to eq("npm") + end + end + + context "when yarn lockfile exists and npm lockfile is absent" do + let(:lockfiles) { { yarn: yarn_lockfile } } + + it "returns yarn as the package manager" do + expect(detector.detect_package_manager).to eq("yarn") + end + end + + context "when pnpm lockfile exists and other lockfiles are absent" do + let(:lockfiles) { { pnpm: pnpm_lockfile } } + + it "returns pnpm as the package manager" do + expect(detector.detect_package_manager).to eq("pnpm") + end + end + + context "when no lockfile but packageManager attribute exists in package.json" do + let(:lockfiles) { {} } + + it "returns npm from packageManager attribute" do + expect(detector.detect_package_manager).to eq("npm") + end + end + + context "when no lockfile and packageManager attribute, but engines field exists" do + let(:lockfiles) { {} } + let(:package_json) { { "engines" => { "yarn" => "1" } } } + + it "returns yarn from engines field" do + expect(detector.detect_package_manager).to eq("yarn") + end + end + + context "when neither lockfile, packageManager, nor engines field exists" do + let(:lockfiles) { {} } + let(:package_json) { {} } + + it "returns nil when no package manager can be detected" do + expect(detector.detect_package_manager).to be_nil + end + end + end +end diff --git a/npm_and_yarn/spec/dependabot/npm_and_yarn/package_manager_helper_spec.rb b/npm_and_yarn/spec/dependabot/npm_and_yarn/package_manager_helper_spec.rb new file mode 100644 index 00000000000..a28675147b4 --- /dev/null +++ b/npm_and_yarn/spec/dependabot/npm_and_yarn/package_manager_helper_spec.rb @@ -0,0 +1,121 @@ +# typed: false +# frozen_string_literal: true + +require "dependabot/npm_and_yarn/package_manager" +require "dependabot/npm_and_yarn/helpers" +require "spec_helper" + +RSpec.describe Dependabot::NpmAndYarn::PackageManagerHelper do + let(:npm_lockfile) do + instance_double( + Dependabot::DependencyFile, + name: "package-lock.json", + content: <<~LOCKFILE + { + "name": "example-npm-project", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "dependencies": { + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-abc123" + } + } + } + LOCKFILE + ) + end + + let(:yarn_lockfile) do + instance_double( + Dependabot::DependencyFile, + name: "yarn.lock", + content: <<~LOCKFILE + # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. + # yarn lockfile v1 + + lodash@^4.17.20: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#abc123" + integrity sha512-abc123 + LOCKFILE + ) + end + + let(:pnpm_lockfile) do + instance_double( + Dependabot::DependencyFile, + name: "pnpm-lock.yaml", + content: <<~LOCKFILE + lockfileVersion: 5.4 + + dependencies: + lodash: + specifier: ^4.17.20 + version: 4.17.21 + resolution: + integrity: sha512-abc123 + tarball: https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz + LOCKFILE + ) + end + + let(:lockfiles) { { npm: npm_lockfile, yarn: yarn_lockfile, pnpm: pnpm_lockfile } } + let(:package_json) { { "packageManager" => "npm@7" } } + let(:helper) { described_class.new(package_json, lockfiles: lockfiles) } + + describe "#package_manager" do + context "when npm lockfile exists" do + it "returns an NpmPackageManager instance" do + allow(Dependabot::NpmAndYarn::Helpers).to receive(:npm_version_numeric).and_return("7") + expect(helper.package_manager).to be_a(Dependabot::NpmAndYarn::NpmPackageManager) + end + end + + context "when only yarn lockfile exists" do + let(:lockfiles) { { yarn: yarn_lockfile } } + + it "returns a YarnPackageManager instance" do + allow(Dependabot::NpmAndYarn::Helpers).to receive(:yarn_version_numeric).and_return("1") + expect(helper.package_manager).to be_a(Dependabot::NpmAndYarn::YarnPackageManager) + end + end + + context "when only pnpm lockfile exists" do + let(:lockfiles) { { pnpm: pnpm_lockfile } } + + it "returns a PNPMPackageManager instance" do + allow(Dependabot::NpmAndYarn::Helpers).to receive(:pnpm_version_numeric).and_return("7") + expect(helper.package_manager).to be_a(Dependabot::NpmAndYarn::PNPMPackageManager) + end + end + + context "when no lockfile but packageManager attribute exists" do + let(:lockfiles) { {} } + + it "returns an NpmPackageManager instance based on the packageManager attribute" do + expect(helper.package_manager).to be_a(Dependabot::NpmAndYarn::NpmPackageManager) + end + end + + context "when no lockfile and packageManager attribute, but engines field exists" do + let(:lockfiles) { {} } + let(:package_json) { { "engines" => { "yarn" => "1" } } } + + it "returns a YarnPackageManager instance from engines field" do + expect(helper.package_manager).to be_a(Dependabot::NpmAndYarn::YarnPackageManager) + end + end + + context "when neither lockfile, packageManager, nor engines field exists" do + let(:lockfiles) { {} } + let(:package_json) { {} } + + it "returns default package manager" do + expect(helper.package_manager).to be_a(Dependabot::NpmAndYarn::NpmPackageManager) + end + end + end +end diff --git a/npm_and_yarn/spec/dependabot/npm_and_yarn/pnpm_package_manager_spec.rb b/npm_and_yarn/spec/dependabot/npm_and_yarn/pnpm_package_manager_spec.rb new file mode 100644 index 00000000000..e613c8c5669 --- /dev/null +++ b/npm_and_yarn/spec/dependabot/npm_and_yarn/pnpm_package_manager_spec.rb @@ -0,0 +1,50 @@ +# typed: false +# frozen_string_literal: true + +require "dependabot/npm_and_yarn/package_manager" +require "dependabot/ecosystem" +require "spec_helper" + +RSpec.describe Dependabot::NpmAndYarn::PNPMPackageManager do + let(:package_manager) { described_class.new(version) } + + describe "#initialize" do + context "when version is a String" do + let(:version) { "9" } + + it "sets the version correctly" do + expect(package_manager.version).to eq(Dependabot::Version.new(version)) + end + + it "sets the name correctly" do + expect(package_manager.name).to eq(Dependabot::NpmAndYarn::PNPMPackageManager::NAME) + end + + it "sets the deprecated_versions correctly" do + expect(package_manager.deprecated_versions).to eq( + Dependabot::NpmAndYarn::PNPMPackageManager::DEPRECATED_VERSIONS + ) + end + + it "sets the supported_versions correctly" do + expect(package_manager.supported_versions).to eq(Dependabot::NpmAndYarn::PNPMPackageManager::SUPPORTED_VERSIONS) + end + end + end + + describe "#deprecated?" do + let(:version) { "7" } + + it "returns false" do + expect(package_manager.deprecated?).to be false + end + end + + describe "#unsupported?" do + let(:version) { "6" } + + it "returns true for unsupported versions" do + expect(package_manager.unsupported?).to be false + end + end +end diff --git a/npm_and_yarn/spec/dependabot/npm_and_yarn/yarn_package_manager_spec.rb b/npm_and_yarn/spec/dependabot/npm_and_yarn/yarn_package_manager_spec.rb new file mode 100644 index 00000000000..d052bf9f2ee --- /dev/null +++ b/npm_and_yarn/spec/dependabot/npm_and_yarn/yarn_package_manager_spec.rb @@ -0,0 +1,50 @@ +# typed: false +# frozen_string_literal: true + +require "dependabot/npm_and_yarn/package_manager" +require "dependabot/ecosystem" +require "spec_helper" + +RSpec.describe Dependabot::NpmAndYarn::YarnPackageManager do + let(:package_manager) { described_class.new(version) } + + describe "#initialize" do + context "when version is a String" do + let(:version) { "2" } + + it "sets the version correctly" do + expect(package_manager.version).to eq(Dependabot::Version.new(version)) + end + + it "sets the name correctly" do + expect(package_manager.name).to eq(Dependabot::NpmAndYarn::YarnPackageManager::NAME) + end + + it "sets the deprecated_versions correctly" do + expect(package_manager.deprecated_versions).to eq( + Dependabot::NpmAndYarn::YarnPackageManager::DEPRECATED_VERSIONS + ) + end + + it "sets the supported_versions correctly" do + expect(package_manager.supported_versions).to eq(Dependabot::NpmAndYarn::YarnPackageManager::SUPPORTED_VERSIONS) + end + end + end + + describe "#deprecated?" do + let(:version) { "1" } + + it "returns false" do + expect(package_manager.deprecated?).to be false + end + end + + describe "#unsupported?" do + let(:version) { "4" } + + it "returns false for supported versions" do + expect(package_manager.unsupported?).to be false + end + end +end