diff --git a/maven/lib/dependabot/maven/shared/base_version_finder.rb b/maven/lib/dependabot/maven/shared/base_version_finder.rb new file mode 100644 index 0000000000..dcba0d5fa3 --- /dev/null +++ b/maven/lib/dependabot/maven/shared/base_version_finder.rb @@ -0,0 +1,105 @@ +# typed: strong +# frozen_string_literal: true + +require "sorbet-runtime" +require "dependabot/maven/shared/shared_version_finder" + +module Dependabot + module Maven + module Shared + # Intermediate class for ecosystems (Maven, SBT) that use a package_details-based + # release pipeline with HEAD-check verification. Gradle uses its own filter chain + # and inherits directly from SharedVersionFinder. + class BaseVersionFinder < SharedVersionFinder + extend T::Sig + extend T::Helpers + + abstract! + + sig { returns(T::Array[Dependabot::Package::PackageRelease]) } + def releases + (package_details&.releases || []).reverse + end + + sig { returns(T.nilable(T::Hash[T.untyped, T.untyped])) } + def latest_version_details + release = fetch_latest_release + release&.version ? { version: release.version, source_url: release.url } : nil + end + + sig { returns(T.nilable(T::Hash[T.untyped, T.untyped])) } + def lowest_security_fix_version_details + release = fetch_lowest_security_fix_release + release&.version ? { version: release.version, source_url: release.url } : nil + end + + protected + + sig do + params(language_version: T.nilable(T.any(String, Dependabot::Version))) + .returns(T.nilable(Dependabot::Version)) + end + def fetch_latest_version(language_version: nil) + fetch_latest_release(language_version: language_version)&.version + end + + sig do + params(language_version: T.nilable(T.any(String, Dependabot::Version))) + .returns(T.nilable(Dependabot::Version)) + end + def fetch_latest_version_with_no_unlock(language_version:) + fetch_latest_release(language_version: language_version)&.version + end + + sig do + params(language_version: T.nilable(T.any(String, Dependabot::Version))) + .returns(T.nilable(Dependabot::Version)) + end + def fetch_lowest_security_fix_version(language_version: nil) + fetch_lowest_security_fix_release(language_version: language_version)&.version + end + + sig do + params(language_version: T.nilable(T.any(String, Dependabot::Version))) + .returns(T.nilable(Dependabot::Package::PackageRelease)) + end + def fetch_latest_release(language_version: nil) # rubocop:disable Lint/UnusedMethodArgument + possible_releases = filter_prerelease_versions(releases) + possible_releases = filter_date_based_versions(possible_releases) + possible_releases = filter_version_types(possible_releases) + possible_releases = filter_ignored_versions(possible_releases) + possible_releases = filter_by_cooldown(possible_releases) + possible_releases_reverse = possible_releases.reverse + + possible_releases_reverse.find do |r| + released?(r.version) + end + end + + sig do + params(language_version: T.nilable(T.any(String, Dependabot::Version))) + .returns(T.nilable(Dependabot::Package::PackageRelease)) + end + def fetch_lowest_security_fix_release(language_version: nil) # rubocop:disable Lint/UnusedMethodArgument + possible_releases = filter_prerelease_versions(releases) + possible_releases = filter_date_based_versions(possible_releases) + possible_releases = filter_version_types(possible_releases) + possible_releases = Dependabot::UpdateCheckers::VersionFilters + .filter_vulnerable_versions( + possible_releases, + security_advisories + ) + possible_releases = filter_ignored_versions(possible_releases) + possible_releases = filter_lower_versions(possible_releases) + + possible_releases.find { |r| released?(r.version) } + end + + private + + sig { abstract.params(version: Dependabot::Version).returns(T::Boolean) } + def released?(version); end + end + end + end +end diff --git a/maven/lib/dependabot/maven/shared/shared_version_finder.rb b/maven/lib/dependabot/maven/shared/shared_version_finder.rb index d2062d27f4..7f1630b0dd 100644 --- a/maven/lib/dependabot/maven/shared/shared_version_finder.rb +++ b/maven/lib/dependabot/maven/shared/shared_version_finder.rb @@ -11,6 +11,9 @@ module Maven module Shared class SharedVersionFinder < Dependabot::Package::PackageLatestVersionFinder extend T::Sig + extend T::Helpers + + abstract! # Regex to match common Maven release qualifiers that indicate stable releases. # See https://github.com/apache/maven/blob/848fbb4bf2d427b72bdb2471c22fced7ebd9a7a1/maven-artifact/src/main/java/org/apache/maven/artifact/versioning/ComparableVersion.java#L315-L320 @@ -123,6 +126,11 @@ def version_class dependency.version_class end + sig { returns(T::Boolean) } + def cooldown_enabled? + true + end + private # Determines whether two versions have compatible suffixes. @@ -405,11 +413,6 @@ def extract_suffix_from_part(part) suffix.empty? ? nil : suffix end - - sig { override.returns(T.nilable(Dependabot::Package::PackageDetails)) } - def package_details - raise NotImplementedError, "Subclasses must implement `package_details`" - end end end end diff --git a/maven/lib/dependabot/maven/update_checker/version_finder.rb b/maven/lib/dependabot/maven/update_checker/version_finder.rb index c3d291aa87..be44857a49 100644 --- a/maven/lib/dependabot/maven/update_checker/version_finder.rb +++ b/maven/lib/dependabot/maven/update_checker/version_finder.rb @@ -6,13 +6,13 @@ require "dependabot/update_checkers/version_filters" require "dependabot/maven/package/package_details_fetcher" require "dependabot/maven/update_checker" -require "dependabot/maven/shared/shared_version_finder" +require "dependabot/maven/shared/base_version_finder" require "sorbet-runtime" module Dependabot module Maven class UpdateChecker - class VersionFinder < Dependabot::Maven::Shared::SharedVersionFinder + class VersionFinder < Dependabot::Maven::Shared::BaseVersionFinder extend T::Sig sig do @@ -52,92 +52,13 @@ def package_details @package_details ||= package_details_fetcher.fetch end - sig { returns(T::Array[Dependabot::Package::PackageRelease]) } - def releases - (package_details&.releases || []).reverse - end - - sig { returns(T.nilable(T::Hash[T.untyped, T.untyped])) } - def latest_version_details - release = fetch_latest_release - release&.version ? { version: release.version, source_url: release.url } : nil - end - - sig { returns(T.nilable(T::Hash[T.untyped, T.untyped])) } - def lowest_security_fix_version_details - release = fetch_lowest_security_fix_release - release&.version ? { version: release.version, source_url: release.url } : nil - end - - protected - - sig { returns(T::Boolean) } - def cooldown_enabled? - true - end - - sig do - params(language_version: T.nilable(T.any(String, Dependabot::Version))) - .returns(T.nilable(Dependabot::Version)) - end - def fetch_latest_version(language_version: nil) - fetch_latest_release(language_version: language_version)&.version - end - - sig do - params(language_version: T.nilable(T.any(String, Dependabot::Version))) - .returns(T.nilable(Dependabot::Version)) - end - def fetch_latest_version_with_no_unlock(language_version:) - fetch_latest_release(language_version: language_version)&.version - end - - sig do - params(language_version: T.nilable(T.any(String, Dependabot::Version))) - .returns(T.nilable(Dependabot::Version)) - end - def fetch_lowest_security_fix_version(language_version: nil) - fetch_lowest_security_fix_release(language_version: language_version)&.version - end - - sig do - params(language_version: T.nilable(T.any(String, Dependabot::Version))) - .returns(T.nilable(Dependabot::Package::PackageRelease)) - end - def fetch_latest_release(language_version: nil) # rubocop:disable Lint/UnusedMethodArgument - possible_releases = filter_prerelease_versions(releases) - possible_releases = filter_date_based_versions(possible_releases) - possible_releases = filter_version_types(possible_releases) - possible_releases = filter_ignored_versions(possible_releases) - possible_releases = filter_by_cooldown(possible_releases) - possible_releases_reverse = possible_releases.reverse - - possible_releases_reverse.find do |r| - package_details_fetcher.released?(r.version) - end - end - - sig do - params(language_version: T.nilable(T.any(String, Dependabot::Version))) - .returns(T.nilable(Dependabot::Package::PackageRelease)) - end - def fetch_lowest_security_fix_release(language_version: nil) # rubocop:disable Lint/UnusedMethodArgument - possible_releases = filter_prerelease_versions(releases) - possible_releases = filter_date_based_versions(possible_releases) - possible_releases = filter_version_types(possible_releases) - possible_releases = Dependabot::UpdateCheckers::VersionFilters - .filter_vulnerable_versions( - possible_releases, - security_advisories - ) - possible_releases = filter_ignored_versions(possible_releases) - possible_releases = filter_lower_versions(possible_releases) + private - possible_releases.find { |r| package_details_fetcher.released?(r.version) } + sig { override.params(version: Dependabot::Version).returns(T::Boolean) } + def released?(version) + package_details_fetcher.released?(version) end - private - sig { returns(Package::PackageDetailsFetcher) } def package_details_fetcher @package_details_fetcher ||= Package::PackageDetailsFetcher.new( diff --git a/maven/spec/dependabot/maven/shared/shared_version_finder_spec.rb b/maven/spec/dependabot/maven/shared/shared_version_finder_spec.rb index ca4c1d61c5..1b40ba8de0 100644 --- a/maven/spec/dependabot/maven/shared/shared_version_finder_spec.rb +++ b/maven/spec/dependabot/maven/shared/shared_version_finder_spec.rb @@ -10,8 +10,17 @@ require "dependabot/package/package_release" RSpec.describe Dependabot::Maven::Shared::SharedVersionFinder do + # SharedVersionFinder is abstract, so use a concrete subclass for testing + let(:concrete_class) do + Class.new(described_class) do + def package_details + nil + end + end + end + let(:finder) do - described_class.new( + concrete_class.new( dependency: dependency, dependency_files: dependency_files, credentials: credentials, diff --git a/sbt/lib/dependabot/sbt/package/package_details_fetcher.rb b/sbt/lib/dependabot/sbt/package/package_details_fetcher.rb new file mode 100644 index 0000000000..6136665f6d --- /dev/null +++ b/sbt/lib/dependabot/sbt/package/package_details_fetcher.rb @@ -0,0 +1,175 @@ +# typed: strict +# frozen_string_literal: true + +require "nokogiri" +require "sorbet-runtime" +require "dependabot/registry_client" +require "dependabot/package/package_release" +require "dependabot/package/package_details" +require "dependabot/sbt/file_parser/repositories_finder" +require "dependabot/sbt/version" +require "dependabot/sbt/requirement" +require "dependabot/maven/shared/shared_package_details_fetcher" +require "dependabot/maven/utils/auth_headers_finder" + +module Dependabot + module Sbt + module Package + class PackageDetailsFetcher < Dependabot::Maven::Shared::SharedPackageDetailsFetcher + extend T::Sig + + sig do + params( + dependency: Dependabot::Dependency, + dependency_files: T::Array[Dependabot::DependencyFile], + credentials: T::Array[Dependabot::Credential] + ).void + end + def initialize(dependency:, dependency_files:, credentials:) + @dependency = T.let(dependency, Dependabot::Dependency) + @dependency_files = T.let(dependency_files, T::Array[Dependabot::DependencyFile]) + @credentials = T.let(credentials, T::Array[Dependabot::Credential]) + + @repositories_cache = T.let(nil, T.nilable(T::Array[T::Hash[String, T.untyped]])) + @repository_finder = T.let(nil, T.nilable(Sbt::FileParser::RepositoriesFinder)) + @package_details = T.let(nil, T.nilable(Dependabot::Package::PackageDetails)) + end + + sig { override.returns(Dependabot::Dependency) } + attr_reader :dependency + + sig { returns(T::Array[Dependabot::DependencyFile]) } + attr_reader :dependency_files + + sig { override.returns(T::Array[Dependabot::Credential]) } + attr_reader :credentials + + sig { returns(Dependabot::Package::PackageDetails) } + def fetch + return @package_details if @package_details + + releases = versions.map do |version_details| + Dependabot::Package::PackageRelease.new( + version: version_details.fetch(:version), + released_at: version_details.fetch(:release_date, nil), + url: version_details.fetch(:source_url) + ) + end + + @package_details = Dependabot::Package::PackageDetails.new( + dependency: dependency, + releases: releases + ) + + @package_details + end + + sig { returns(T::Array[Dependabot::Package::PackageRelease]) } + def releases + fetch.releases + end + + # Assembles the list of Maven repositories to search: credential repos + SBT resolver repos. + sig { override.returns(T::Array[T::Hash[String, T.untyped]]) } + def repositories + return @repositories_cache if @repositories_cache + + @repositories_cache = credentials_repository_details + + sbt_repository_details.each do |repo| + @repositories_cache << repo unless @repositories_cache.any? do |r| + r[URL_KEY] == repo[URL_KEY] + end + end + + @repositories_cache + end + + sig { override.returns(String) } + def central_repo_url + Sbt::FileParser::RepositoriesFinder::CENTRAL_REPO_URL + end + + # Override to always use "jar" for the HEAD check. The SBT parser sets + # packaging_type to "cross-versioned" as metadata for %% dependencies, but the + # actual Maven artifact is always a .jar file. + sig { override.params(repository_url: String, version: Dependabot::Version).returns(String) } + def dependency_files_url(repository_url, version) + _, artifact_id = dependency_parts + base_url = dependency_base_url(repository_url) + + "#{base_url}/#{version}/#{artifact_id}-#{version}.jar" + end + + # Override to handle SBT plugin cross-versioning. + # SBT plugins are published with a double-suffix: artifact_scalaVersion_sbtVersion + # e.g., sbt-jmh_2.12_1.0 for SBT 1.x plugins. + sig { override.returns([String, String]) } + def dependency_parts + @dependency_parts = T.let(@dependency_parts, T.nilable([String, String])) + return @dependency_parts if @dependency_parts + + group_id, artifact_id = dependency.name.split(":") + group_path = T.must(group_id).tr(".", "/") + + artifact_id = "#{artifact_id}_#{plugin_scala_version}_#{sbt_binary_version}" if sbt_plugin? + + @dependency_parts = [group_path, T.must(artifact_id)] + end + + private + + sig { returns(Sbt::FileParser::RepositoriesFinder) } + def repository_finder + @repository_finder ||= Sbt::FileParser::RepositoriesFinder.new( + dependency_files: dependency_files, + credentials: credentials + ) + end + + # Returns the repository details discovered from SBT build files. + sig { returns(T::Array[T::Hash[String, T.untyped]]) } + def sbt_repository_details + repository_finder + .repository_urls + .map do |url| + { URL_KEY => url, AUTH_HEADERS_KEY => auth_headers(url) } + end + end + + # SBT plugins are identified by having "plugins" in their groups. + sig { returns(T::Boolean) } + def sbt_plugin? + dependency.requirements.any? { |req| req.fetch(:groups, []).include?("plugins") } + end + + # SBT 1.x plugins use Scala 2.12; SBT 2.x plugins use Scala 3. + sig { returns(String) } + def plugin_scala_version + sbt_major_version >= 2 ? "3" : "2.12" + end + + # SBT binary version for plugin cross-versioning: "1.0" for SBT 1.x, "2.0" for SBT 2.x. + sig { returns(String) } + def sbt_binary_version + "#{sbt_major_version}.0" + end + + sig { returns(Integer) } + def sbt_major_version + build_properties = dependency_files.find { |f| f.name.end_with?("build.properties") } + return 1 unless build_properties&.content + + T.must(build_properties.content).each_line do |line| + match = line.strip.match(Sbt::FileParser::SBT_VERSION_REGEX) + next unless match + + return T.must(match[:version]).strip.split(".").first.to_i + end + + 1 + end + end + end + end +end diff --git a/sbt/lib/dependabot/sbt/update_checker.rb b/sbt/lib/dependabot/sbt/update_checker.rb index 014773a409..4fa92f748f 100644 --- a/sbt/lib/dependabot/sbt/update_checker.rb +++ b/sbt/lib/dependabot/sbt/update_checker.rb @@ -1,54 +1,175 @@ -# typed: strong +# typed: strict # frozen_string_literal: true +require "sorbet-runtime" require "dependabot/update_checkers" require "dependabot/update_checkers/base" +require "dependabot/sbt/file_parser" +require "dependabot/sbt/file_parser/property_value_finder" module Dependabot module Sbt class UpdateChecker < Dependabot::UpdateCheckers::Base extend T::Sig - sig { override.returns(T.nilable(T.any(String, Gem::Version))) } + require_relative "update_checker/requirements_updater" + require_relative "update_checker/version_finder" + + sig { override.returns(T.nilable(Dependabot::Version)) } def latest_version - # TODO: Implement logic to find the latest version - # This should check the package registry/repository for updates - nil + latest_version_details&.fetch(:version) end - sig { override.returns(T.nilable(T.any(String, Gem::Version))) } + sig { override.returns(T.nilable(Dependabot::Version)) } def latest_resolvable_version - # TODO: Implement logic to find the latest resolvable version - # This might be the same as latest_version for simple ecosystems + # SBT has no transitive dependency resolution constraints in manifest files. + # Return nil if version comes from a multi-dependency property (needs full unlock). + return nil if version_comes_from_multi_dependency_property? + latest_version end - sig { override.returns(T.nilable(String)) } + sig { override.returns(T.nilable(Dependabot::Version)) } + def lowest_security_fix_version + lowest_security_fix_version_details&.fetch(:version) + end + + sig { override.returns(T.nilable(Dependabot::Version)) } + def lowest_resolvable_security_fix_version + return nil if version_comes_from_multi_dependency_property? + + lowest_security_fix_version + end + + sig { override.returns(T.nilable(Dependabot::Version)) } def latest_resolvable_version_with_no_unlock - # TODO: Implement logic for version resolution without unlocking - dependency.version + # SBT uses exact versions in build files, so no constraint resolution needed. + nil end sig { override.returns(T::Array[T::Hash[Symbol, T.untyped]]) } def updated_requirements - # TODO: Implement logic to update requirements - # Return updated requirement hashes - dependency.requirements + property_names = + declarations_using_a_property + .filter_map { |req| req.dig(:metadata, :property_name) } + + RequirementsUpdater.new( + requirements: dependency.requirements, + latest_version: preferred_resolvable_version&.to_s, + source_url: preferred_version_details&.fetch(:source_url), + properties_to_update: property_names + ).updated_requirements + end + + sig { override.returns(T::Boolean) } + def requirements_unlocked_or_can_be? + # If any requirement uses a val we couldn't resolve, we can't update + !dependency.version&.include?("${") end private sig { override.returns(T::Boolean) } def latest_version_resolvable_with_full_unlock? - # TODO: Implement resolvability check + return false unless version_comes_from_multi_dependency_property? + + # Full unlock via property updates can be added later false end sig { override.returns(T::Array[Dependabot::Dependency]) } def updated_dependencies_after_full_unlock - # TODO: Return updated dependencies if full unlock is needed [] end + + sig { override.returns(T::Boolean) } + def numeric_version_up_to_date? + return false unless version_class.correct?(dependency.version) + + super + end + + sig { override.params(requirements_to_unlock: T.nilable(Symbol)).returns(T::Boolean) } + def numeric_version_can_update?(requirements_to_unlock:) + return false unless version_class.correct?(dependency.version) + + super + end + + sig { returns(T.nilable(T::Hash[T.untyped, T.untyped])) } + def preferred_version_details + return lowest_security_fix_version_details if vulnerable? + + latest_version_details + end + + sig { returns(T.nilable(T::Hash[T.untyped, T.untyped])) } + def latest_version_details + version_finder.latest_version_details + end + + sig { returns(T.nilable(T::Hash[T.untyped, T.untyped])) } + def lowest_security_fix_version_details + version_finder.lowest_security_fix_version_details + end + + sig { returns(VersionFinder) } + def version_finder + @version_finder ||= T.let( + VersionFinder.new( + dependency: dependency, + dependency_files: dependency_files, + credentials: credentials, + ignored_versions: ignored_versions, + cooldown_options: update_cooldown, + raise_on_ignored: raise_on_ignored, + security_advisories: security_advisories + ), + T.nilable(VersionFinder) + ) + end + + sig { returns(T::Boolean) } + def version_comes_from_multi_dependency_property? + declarations_using_a_property.any? do |requirement| + property_name = requirement.dig(:metadata, :property_name) + property_source = requirement.dig(:metadata, :property_source) + + next false unless property_name + + all_property_based_dependencies.any? do |dep| + next false if dep.name == dependency.name + + dep.requirements.any? do |req| + next unless req.dig(:metadata, :property_name) == property_name + + req.dig(:metadata, :property_source) == property_source + end + end + end + end + + sig { returns(T::Array[T::Hash[Symbol, T.untyped]]) } + def declarations_using_a_property + @declarations_using_a_property ||= T.let( + dependency.requirements + .select { |req| req.dig(:metadata, :property_name) }, + T.nilable(T::Array[T::Hash[Symbol, T.untyped]]) + ) + end + + sig { returns(T::Array[Dependabot::Dependency]) } + def all_property_based_dependencies + @all_property_based_dependencies ||= T.let( + Sbt::FileParser.new( + dependency_files: dependency_files, + source: nil + ).parse.select do |dep| + dep.requirements.any? { |req| req.dig(:metadata, :property_name) } + end, + T.nilable(T::Array[Dependabot::Dependency]) + ) + end end end end diff --git a/sbt/lib/dependabot/sbt/update_checker/requirements_updater.rb b/sbt/lib/dependabot/sbt/update_checker/requirements_updater.rb new file mode 100644 index 0000000000..e0d831fc2e --- /dev/null +++ b/sbt/lib/dependabot/sbt/update_checker/requirements_updater.rb @@ -0,0 +1,98 @@ +# typed: strict +# frozen_string_literal: true + +require "sorbet-runtime" +require "dependabot/requirements_updater/base" +require "dependabot/sbt/update_checker" +require "dependabot/sbt/version" +require "dependabot/sbt/requirement" + +module Dependabot + module Sbt + class UpdateChecker < Dependabot::UpdateCheckers::Base + class RequirementsUpdater + extend T::Sig + extend T::Generic + + Version = type_member { { fixed: Dependabot::Sbt::Version } } + Requirement = type_member { { fixed: Dependabot::Sbt::Requirement } } + + include Dependabot::RequirementsUpdater::Base + + sig do + params( + requirements: T::Array[T::Hash[Symbol, T.untyped]], + latest_version: T.nilable(T.any(Version, String)), + source_url: T.nilable(String), + properties_to_update: T::Array[String] + ).void + end + def initialize( + requirements:, + latest_version:, + source_url:, + properties_to_update: + ) + @requirements = requirements + @source_url = source_url + @properties_to_update = properties_to_update + return unless latest_version + + @latest_version = T.let(version_class.new(latest_version), Version) + end + + sig { override.returns(T::Array[T::Hash[Symbol, T.untyped]]) } + def updated_requirements + return requirements unless latest_version + + requirements.map do |req| + next req if req.fetch(:requirement).nil? + next req if req.fetch(:requirement).include?(",") + + property_name = req.dig(:metadata, :property_name) + next req if property_name && !properties_to_update.include?(property_name) + + new_req = update_requirement(req[:requirement]) + req.merge(requirement: new_req, source: updated_source) + end + end + + private + + sig { returns(T::Array[T::Hash[Symbol, T.untyped]]) } + attr_reader :requirements + + sig { returns(T.nilable(Version)) } + attr_reader :latest_version + + sig { returns(T.nilable(String)) } + attr_reader :source_url + + sig { returns(T::Array[String]) } + attr_reader :properties_to_update + + sig { params(req_string: String).returns(String) } + def update_requirement(req_string) + old_version = requirement_class.new(req_string) + .requirements.first.last + req_string.gsub(old_version.to_s, T.must(latest_version).to_s) + end + + sig { override.returns(T::Class[Version]) } + def version_class + Sbt::Version + end + + sig { override.returns(T::Class[Requirement]) } + def requirement_class + Sbt::Requirement + end + + sig { returns(T::Hash[Symbol, T.untyped]) } + def updated_source + { type: "maven_repo", url: source_url } + end + end + end + end +end diff --git a/sbt/lib/dependabot/sbt/update_checker/version_finder.rb b/sbt/lib/dependabot/sbt/update_checker/version_finder.rb new file mode 100644 index 0000000000..161c5ce184 --- /dev/null +++ b/sbt/lib/dependabot/sbt/update_checker/version_finder.rb @@ -0,0 +1,76 @@ +# typed: strong +# frozen_string_literal: true + +require "sorbet-runtime" +require "dependabot/package/package_latest_version_finder" +require "dependabot/package/release_cooldown_options" +require "dependabot/update_checkers/version_filters" +require "dependabot/maven/shared/base_version_finder" +require "dependabot/sbt/update_checker" +require "dependabot/sbt/package/package_details_fetcher" + +module Dependabot + module Sbt + class UpdateChecker < Dependabot::UpdateCheckers::Base + class VersionFinder < Dependabot::Maven::Shared::BaseVersionFinder + extend T::Sig + + sig do + params( + dependency: Dependabot::Dependency, + dependency_files: T::Array[Dependabot::DependencyFile], + credentials: T::Array[Dependabot::Credential], + ignored_versions: T::Array[String], + security_advisories: T::Array[Dependabot::SecurityAdvisory], + cooldown_options: T.nilable(Dependabot::Package::ReleaseCooldownOptions), + raise_on_ignored: T::Boolean + ).void + end + def initialize( + dependency:, + dependency_files:, + credentials:, + ignored_versions:, + security_advisories:, + cooldown_options: nil, + raise_on_ignored: false + ) + @package_details_fetcher = T.let(nil, T.nilable(Package::PackageDetailsFetcher)) + @package_details = T.let(nil, T.nilable(Dependabot::Package::PackageDetails)) + + super( + dependency: dependency, + dependency_files: dependency_files, + credentials: credentials, + ignored_versions: ignored_versions, + security_advisories: security_advisories, + cooldown_options: cooldown_options, + raise_on_ignored: raise_on_ignored, + options: {} + ) + end + + sig { override.returns(T.nilable(Dependabot::Package::PackageDetails)) } + def package_details + @package_details ||= package_details_fetcher.fetch + end + + private + + sig { override.params(version: Dependabot::Version).returns(T::Boolean) } + def released?(version) + package_details_fetcher.released?(version) + end + + sig { returns(Package::PackageDetailsFetcher) } + def package_details_fetcher + @package_details_fetcher ||= Package::PackageDetailsFetcher.new( + dependency: dependency, + dependency_files: dependency_files, + credentials: credentials + ) + end + end + end + end +end diff --git a/sbt/spec/dependabot/sbt/package/package_details_fetcher_spec.rb b/sbt/spec/dependabot/sbt/package/package_details_fetcher_spec.rb new file mode 100644 index 0000000000..7c53adaf5a --- /dev/null +++ b/sbt/spec/dependabot/sbt/package/package_details_fetcher_spec.rb @@ -0,0 +1,203 @@ +# typed: false +# frozen_string_literal: true + +require "spec_helper" +require "dependabot/credential" +require "dependabot/dependency" +require "dependabot/dependency_file" +require "dependabot/sbt/package/package_details_fetcher" + +RSpec.describe Dependabot::Sbt::Package::PackageDetailsFetcher do + let(:fetcher) do + described_class.new( + dependency: dependency, + dependency_files: dependency_files, + credentials: credentials + ) + end + + let(:credentials) { [] } + let(:dependency_files) { [build_sbt] } + let(:build_sbt) do + Dependabot::DependencyFile.new( + name: "build.sbt", + content: fixture("buildfiles", "basic_build.sbt") + ) + end + + let(:dependency) do + Dependabot::Dependency.new( + name: dependency_name, + version: dependency_version, + requirements: dependency_requirements, + package_manager: "sbt" + ) + end + let(:dependency_name) { "com.google.guava:guava" } + let(:dependency_version) { "33.0.0-jre" } + let(:dependency_requirements) do + [{ + file: "build.sbt", + requirement: "33.0.0-jre", + groups: [], + source: nil, + metadata: nil + }] + end + + let(:maven_central_metadata_url) do + "https://repo.maven.apache.org/maven2/" \ + "com/google/guava/guava/maven-metadata.xml" + end + let(:maven_central_base_url) do + "https://repo.maven.apache.org/maven2/" \ + "com/google/guava/guava" + end + let(:maven_central_releases) do + fixture("maven_metadata", "guava.xml") + end + + before do + stub_request(:get, maven_central_metadata_url) + .to_return(status: 200, body: maven_central_releases) + stub_request(:get, maven_central_base_url) + .to_return(status: 404) + end + + describe "#fetch" do + subject(:package_details) { fetcher.fetch } + + it "returns a PackageDetails object" do + expect(package_details).to be_a(Dependabot::Package::PackageDetails) + end + + it "includes the correct releases" do + versions = package_details.releases.map { |r| r.version.to_s } + expect(versions).to include("33.0.0-jre", "33.4.0-jre") + end + + it "includes all versions from metadata including android variants" do + versions = package_details.releases.map { |r| r.version.to_s } + expect(versions).to include("33.0.0-android") + end + + it "returns releases sorted by version in descending order" do + versions = package_details.releases.map(&:version) + expect(versions).to eq(versions.sort.reverse) + end + end + + describe "#releases" do + subject(:releases) { fetcher.releases } + + it "returns PackageRelease objects" do + expect(releases.first).to be_a(Dependabot::Package::PackageRelease) + end + + it "includes source_url in releases" do + expect(releases.first.url).to eq("https://repo.maven.apache.org/maven2") + end + end + + describe "#repositories" do + subject(:repositories) { fetcher.repositories } + + it "includes Maven Central by default" do + urls = repositories.map { |r| r["url"] } + expect(urls).to include("https://repo.maven.apache.org/maven2") + end + + context "with custom resolvers in build file" do + let(:build_sbt) do + Dependabot::DependencyFile.new( + name: "build.sbt", + content: fixture("buildfiles", "custom_repos_build.sbt") + ) + end + + it "includes custom resolver URLs" do + urls = repositories.map { |r| r["url"] } + expect(urls).to include("https://oss.sonatype.org/content/repositories/releases") + expect(urls).to include("https://repo.artima.com/releases") + end + end + + context "with credentials" do + let(:credentials) do + [ + Dependabot::Credential.new( + { + "type" => "maven_repository", + "url" => "https://private.repo.example.com/maven2" + } + ) + ] + end + + it "includes credential-based repositories" do + urls = repositories.map { |r| r["url"] } + expect(urls).to include("https://private.repo.example.com/maven2") + end + end + end + + describe "#released?" do + subject { fetcher.released?(version) } + + let(:version) { Dependabot::Sbt::Version.new("33.4.0-jre") } + + before do + stub_request( + :head, + "https://repo.maven.apache.org/maven2/" \ + "com/google/guava/guava/33.4.0-jre/guava-33.4.0-jre.jar" + ).to_return(status: 200) + end + + it { is_expected.to be true } + + context "when the artifact is not found" do + before do + stub_request( + :head, + "https://repo.maven.apache.org/maven2/" \ + "com/google/guava/guava/33.4.0-jre/guava-33.4.0-jre.jar" + ).to_return(status: 404) + end + + it { is_expected.to be false } + end + end + + describe "cross-versioned artifact resolution" do + let(:dependency_name) { "org.typelevel:cats-core_2.13" } + let(:dependency_version) { "2.10.0" } + let(:dependency_requirements) do + [{ + file: "build.sbt", + requirement: "2.10.0", + groups: [], + source: nil, + metadata: { packaging_type: "cross-versioned" } + }] + end + + let(:maven_central_metadata_url) do + "https://repo.maven.apache.org/maven2/" \ + "org/typelevel/cats-core_2.13/maven-metadata.xml" + end + let(:maven_central_base_url) do + "https://repo.maven.apache.org/maven2/" \ + "org/typelevel/cats-core_2.13" + end + let(:maven_central_releases) do + fixture("maven_metadata", "cats_core_2.13.xml") + end + + it "correctly resolves the artifact path with Scala version suffix" do + details = fetcher.fetch + versions = details.releases.map { |r| r.version.to_s } + expect(versions).to include("2.10.0", "2.11.0", "2.12.0") + end + end +end diff --git a/sbt/spec/dependabot/sbt/update_checker/requirements_updater_spec.rb b/sbt/spec/dependabot/sbt/update_checker/requirements_updater_spec.rb new file mode 100644 index 0000000000..8f333ef656 --- /dev/null +++ b/sbt/spec/dependabot/sbt/update_checker/requirements_updater_spec.rb @@ -0,0 +1,136 @@ +# typed: false +# frozen_string_literal: true + +require "spec_helper" +require "dependabot/sbt/update_checker/requirements_updater" + +RSpec.describe Dependabot::Sbt::UpdateChecker::RequirementsUpdater do + let(:updater) do + described_class.new( + requirements: requirements, + latest_version: latest_version, + source_url: "https://repo.maven.apache.org/maven2", + properties_to_update: properties_to_update + ) + end + + let(:version_class) { Dependabot::Sbt::Version } + let(:requirements) { [sbt_req] } + let(:properties_to_update) { [] } + + let(:sbt_req) do + { + file: "build.sbt", + requirement: sbt_req_string, + groups: [], + source: nil, + metadata: nil + } + end + let(:sbt_req_string) { "2.10.0" } + let(:latest_version) { version_class.new("2.12.0") } + + describe "#updated_requirements" do + subject(:updated_requirements) { updater.updated_requirements } + + specify { expect(updated_requirements.count).to eq(1) } + + context "when there is no latest version" do + let(:latest_version) { nil } + + it "returns the existing requirements unchanged" do + expect(updated_requirements.first).to eq(sbt_req) + end + end + + context "when there is a latest version" do + let(:latest_version) { version_class.new("2.12.0") } + + context "with a simple exact version" do + let(:sbt_req_string) { "2.10.0" } + + its(:first) do + is_expected.to eq( + file: "build.sbt", + requirement: "2.12.0", + groups: [], + source: { type: "maven_repo", url: "https://repo.maven.apache.org/maven2" }, + metadata: nil + ) + end + end + + context "with a nil requirement" do + let(:sbt_req_string) { nil } + + it "returns the requirement unchanged" do + expect(updated_requirements.first).to eq(sbt_req) + end + end + + context "with a jre-suffixed version" do + let(:sbt_req_string) { "33.0.0-jre" } + let(:latest_version) { version_class.new("33.4.0-jre") } + + its(:first) do + is_expected.to include(requirement: "33.4.0-jre") + end + end + + context "with a range requirement (comma-separated)" do + let(:sbt_req_string) { "[2.10.0,2.11.0]" } + + it "does not update range requirements" do + expect(updated_requirements.first).to eq(sbt_req) + end + end + end + + context "with property-based requirements" do + let(:sbt_req) do + { + file: "build.sbt", + requirement: "2.10.0", + groups: [], + source: nil, + metadata: { property_name: "catsVersion", property_source: "build.sbt" } + } + end + + context "when the property is in properties_to_update" do + let(:properties_to_update) { ["catsVersion"] } + + its(:first) do + is_expected.to include(requirement: "2.12.0") + end + end + + context "when the property is NOT in properties_to_update" do + let(:properties_to_update) { [] } + + it "does not update the requirement" do + expect(updated_requirements.first[:requirement]).to eq("2.10.0") + end + end + end + + context "with multiple requirements" do + let(:requirements) { [sbt_req, other_req] } + let(:other_req) do + { + file: "project/plugins.sbt", + requirement: "2.10.0", + groups: ["plugins"], + source: nil, + metadata: nil + } + end + + it "updates both requirements" do + expect(updated_requirements.count).to eq(2) + expect(updated_requirements[0][:requirement]).to eq("2.12.0") + expect(updated_requirements[1][:requirement]).to eq("2.12.0") + end + end + end +end diff --git a/sbt/spec/dependabot/sbt/update_checker/version_finder_spec.rb b/sbt/spec/dependabot/sbt/update_checker/version_finder_spec.rb new file mode 100644 index 0000000000..37dc9082dc --- /dev/null +++ b/sbt/spec/dependabot/sbt/update_checker/version_finder_spec.rb @@ -0,0 +1,240 @@ +# typed: false +# frozen_string_literal: true + +require "spec_helper" +require "dependabot/credential" +require "dependabot/dependency" +require "dependabot/dependency_file" +require "dependabot/sbt/update_checker/version_finder" + +RSpec.describe Dependabot::Sbt::UpdateChecker::VersionFinder do + let(:finder) do + described_class.new( + dependency: dependency, + dependency_files: dependency_files, + credentials: credentials, + ignored_versions: ignored_versions, + raise_on_ignored: raise_on_ignored, + security_advisories: security_advisories, + cooldown_options: cooldown_options + ) + end + let(:version_class) { Dependabot::Sbt::Version } + let(:credentials) { [] } + let(:ignored_versions) { [] } + let(:raise_on_ignored) { false } + let(:security_advisories) { [] } + let(:cooldown_options) { nil } + + let(:dependency) do + Dependabot::Dependency.new( + name: dependency_name, + version: dependency_version, + requirements: dependency_requirements, + package_manager: "sbt" + ) + end + let(:dependency_files) { [build_sbt] } + let(:build_sbt) do + Dependabot::DependencyFile.new( + name: "build.sbt", + content: fixture("buildfiles", "basic_build.sbt") + ) + end + + let(:dependency_requirements) do + [{ + file: "build.sbt", + requirement: dependency_version, + groups: [], + source: nil, + metadata: nil + }] + end + let(:dependency_name) { "com.google.guava:guava" } + let(:dependency_version) { "33.0.0-jre" } + + let(:maven_central_metadata_url) do + "https://repo.maven.apache.org/maven2/" \ + "com/google/guava/guava/maven-metadata.xml" + end + let(:maven_central_base_url) do + "https://repo.maven.apache.org/maven2/" \ + "com/google/guava/guava" + end + let(:maven_central_releases) do + fixture("maven_metadata", "guava.xml") + end + let(:maven_central_version_files_url) do + "https://repo.maven.apache.org/maven2/" \ + "com/google/guava/guava/33.4.0-jre/guava-33.4.0-jre.jar" + end + + before do + stub_request(:get, maven_central_metadata_url) + .to_return(status: 200, body: maven_central_releases) + stub_request(:get, maven_central_base_url) + .to_return(status: 404) + stub_request(:head, maven_central_version_files_url) + .to_return(status: 200) + end + + describe "class hierarchy" do + it "inherits from SharedVersionFinder" do + expect(described_class < Dependabot::Maven::Shared::SharedVersionFinder).to be true + end + end + + describe "#latest_version_details" do + subject(:latest_version_details) { finder.latest_version_details } + + its([:version]) { is_expected.to eq(version_class.new("33.4.0-jre")) } + + its([:source_url]) do + is_expected.to eq("https://repo.maven.apache.org/maven2") + end + + context "when the latest version hasn't actually been released" do + before do + stub_request(:head, maven_central_version_files_url) + .to_return(status: 404) + stub_request( + :head, + "https://repo.maven.apache.org/maven2/" \ + "com/google/guava/guava/33.3.0-jre/guava-33.3.0-jre.jar" + ).to_return(status: 200) + end + + its([:version]) { is_expected.to eq(version_class.new("33.3.0-jre")) } + end + + context "with a cross-versioned Scala dependency" do + let(:dependency_name) { "org.typelevel:cats-core_2.13" } + let(:dependency_version) { "2.10.0" } + let(:dependency_requirements) do + [{ + file: "build.sbt", + requirement: "2.10.0", + groups: [], + source: nil, + metadata: { packaging_type: "cross-versioned" } + }] + end + + let(:maven_central_metadata_url) do + "https://repo.maven.apache.org/maven2/" \ + "org/typelevel/cats-core_2.13/maven-metadata.xml" + end + let(:maven_central_base_url) do + "https://repo.maven.apache.org/maven2/" \ + "org/typelevel/cats-core_2.13" + end + let(:maven_central_releases) do + fixture("maven_metadata", "cats_core_2.13.xml") + end + let(:maven_central_version_files_url) do + "https://repo.maven.apache.org/maven2/" \ + "org/typelevel/cats-core_2.13/2.12.0/cats-core_2.13-2.12.0.jar" + end + + its([:version]) { is_expected.to eq(version_class.new("2.12.0")) } + + it "excludes pre-release versions" do + # 2.13.0-RC1 should not be returned + expect(latest_version_details[:version]).not_to eq(version_class.new("2.13.0-RC1")) + end + end + + context "when the user wants a pre-release" do + let(:dependency_name) { "com.typesafe.akka:akka-actor_2.13" } + let(:dependency_version) { "2.9.0-M1" } + let(:dependency_requirements) do + [{ + file: "build.sbt", + requirement: "2.9.0-M1", + groups: [], + source: nil, + metadata: { packaging_type: "cross-versioned" } + }] + end + + let(:maven_central_metadata_url) do + "https://repo.maven.apache.org/maven2/" \ + "com/typesafe/akka/akka-actor_2.13/maven-metadata.xml" + end + let(:maven_central_base_url) do + "https://repo.maven.apache.org/maven2/" \ + "com/typesafe/akka/akka-actor_2.13" + end + let(:maven_central_releases) do + fixture("maven_metadata", "akka_actor_2.13.xml") + end + let(:maven_central_version_files_url) do + "https://repo.maven.apache.org/maven2/" \ + "com/typesafe/akka/akka-actor_2.13/2.9.3/akka-actor_2.13-2.9.3.jar" + end + + its([:version]) { is_expected.to eq(version_class.new("2.9.3")) } + end + + context "with ignored versions" do + let(:ignored_versions) { [">= 33.3.0"] } + + before do + stub_request( + :head, + "https://repo.maven.apache.org/maven2/" \ + "com/google/guava/guava/33.2.0-jre/guava-33.2.0-jre.jar" + ).to_return(status: 200) + end + + its([:version]) { is_expected.to eq(version_class.new("33.2.0-jre")) } + end + + context "with custom repositories" do + let(:build_sbt) do + Dependabot::DependencyFile.new( + name: "build.sbt", + content: fixture("buildfiles", "custom_repos_build.sbt") + ) + end + + before do + stub_request(:get, "https://oss.sonatype.org/content/repositories/releases/com/google/guava/guava/maven-metadata.xml") + .to_return(status: 404) + stub_request(:get, "https://repo.artima.com/releases/com/google/guava/guava/maven-metadata.xml") + .to_return(status: 404) + end + + its([:version]) { is_expected.to eq(version_class.new("33.4.0-jre")) } + end + end + + describe "#lowest_security_fix_version_details" do + subject { finder.lowest_security_fix_version_details } + + let(:security_advisories) do + [ + Dependabot::SecurityAdvisory.new( + dependency_name: dependency_name, + package_manager: "sbt", + vulnerable_versions: ["< 33.2.0-jre"] + ) + ] + end + + before do + stub_request( + :head, + "https://repo.maven.apache.org/maven2/" \ + "com/google/guava/guava/33.2.0-jre/guava-33.2.0-jre.jar" + ).to_return(status: 200) + end + + its([:version]) { is_expected.to eq(version_class.new("33.2.0-jre")) } + + its([:source_url]) do + is_expected.to eq("https://repo.maven.apache.org/maven2") + end + end +end diff --git a/sbt/spec/dependabot/sbt/update_checker_spec.rb b/sbt/spec/dependabot/sbt/update_checker_spec.rb index 555f6024ca..d90825a53c 100644 --- a/sbt/spec/dependabot/sbt/update_checker_spec.rb +++ b/sbt/spec/dependabot/sbt/update_checker_spec.rb @@ -2,9 +2,223 @@ # frozen_string_literal: true require "spec_helper" +require "dependabot/dependency" +require "dependabot/dependency_file" require "dependabot/sbt/update_checker" +require "dependabot/sbt/version" require_common_spec "update_checkers/shared_examples_for_update_checkers" RSpec.describe Dependabot::Sbt::UpdateChecker do + let(:version_class) { Dependabot::Sbt::Version } + let(:credentials) do + [{ + "type" => "git_source", + "host" => "github.com", + "username" => "x-access-token", + "password" => "token" + }] + end + let(:security_advisories) { [] } + let(:ignored_versions) { [] } + let(:cooldown_options) { nil } + let(:dependency_files) { [build_sbt] } + let(:build_sbt) do + Dependabot::DependencyFile.new( + name: "build.sbt", + content: fixture("buildfiles", "basic_build.sbt") + ) + end + + let(:dependency_name) { "com.google.guava:guava" } + let(:dependency_version) { "33.0.0-jre" } + let(:dependency_requirements) do + [{ + file: "build.sbt", + requirement: "33.0.0-jre", + groups: [], + source: nil, + metadata: nil + }] + end + let(:dependency) do + Dependabot::Dependency.new( + name: dependency_name, + version: dependency_version, + requirements: dependency_requirements, + package_manager: "sbt" + ) + end + + let(:checker) do + described_class.new( + dependency: dependency, + dependency_files: dependency_files, + credentials: credentials, + ignored_versions: ignored_versions, + security_advisories: security_advisories, + update_cooldown: cooldown_options + ) + end + + let(:maven_central_metadata_url) do + "https://repo.maven.apache.org/maven2/" \ + "com/google/guava/guava/maven-metadata.xml" + end + let(:maven_central_releases) do + fixture("maven_metadata", "guava.xml") + end + let(:maven_central_version_files_url) do + "https://repo.maven.apache.org/maven2/" \ + "com/google/guava/guava/33.4.0-jre/guava-33.4.0-jre.jar" + end + + before do + stub_request(:get, maven_central_metadata_url) + .to_return(status: 200, body: maven_central_releases) + stub_request(:get, "https://repo.maven.apache.org/maven2/com/google/guava/guava") + .to_return(status: 404) + stub_request(:head, maven_central_version_files_url) + .to_return(status: 200) + end + it_behaves_like "an update checker" + + describe "#latest_version" do + subject { checker.latest_version } + + it { is_expected.to eq(version_class.new("33.4.0-jre")) } + + context "when the latest version hasn't been released" do + before do + stub_request(:head, maven_central_version_files_url) + .to_return(status: 404) + stub_request( + :head, + "https://repo.maven.apache.org/maven2/" \ + "com/google/guava/guava/33.3.0-jre/guava-33.3.0-jre.jar" + ).to_return(status: 200) + end + + it { is_expected.to eq(version_class.new("33.3.0-jre")) } + end + + context "with a cross-versioned dependency" do + let(:dependency_name) { "org.typelevel:cats-core_2.13" } + let(:dependency_version) { "2.10.0" } + let(:dependency_requirements) do + [{ + file: "build.sbt", + requirement: "2.10.0", + groups: [], + source: nil, + metadata: { packaging_type: "cross-versioned" } + }] + end + + let(:maven_central_metadata_url) do + "https://repo.maven.apache.org/maven2/" \ + "org/typelevel/cats-core_2.13/maven-metadata.xml" + end + let(:maven_central_releases) do + fixture("maven_metadata", "cats_core_2.13.xml") + end + let(:maven_central_version_files_url) do + "https://repo.maven.apache.org/maven2/" \ + "org/typelevel/cats-core_2.13/2.12.0/cats-core_2.13-2.12.0.jar" + end + + it { is_expected.to eq(version_class.new("2.12.0")) } + end + end + + describe "#latest_resolvable_version" do + subject { checker.latest_resolvable_version } + + it { is_expected.to eq(version_class.new("33.4.0-jre")) } + + context "when the version comes from a multi-dependency property" do + let(:build_sbt) do + Dependabot::DependencyFile.new( + name: "build.sbt", + content: fixture("buildfiles", "val_based_build.sbt") + ) + end + let(:dependency_name) { "org.typelevel:cats-core_2.13" } + let(:dependency_version) { "2.10.0" } + let(:dependency_requirements) do + [{ + file: "build.sbt", + requirement: "2.10.0", + groups: [], + source: nil, + metadata: { property_name: "catsVersion", property_source: "build.sbt" } + }] + end + + before do + stub_request(:get, "https://repo.maven.apache.org/maven2/org/typelevel/cats-core_2.13/maven-metadata.xml") + .to_return(status: 200, body: fixture("maven_metadata", "cats_core_2.13.xml")) + stub_request(:get, "https://repo.maven.apache.org/maven2/org/typelevel/cats-core_2.13") + .to_return(status: 404) + stub_request(:head, "https://repo.maven.apache.org/maven2/org/typelevel/cats-core_2.13/2.12.0/cats-core_2.13-2.12.0.jar") + .to_return(status: 200) + end + + # catsVersion is only used by cats-core in val_based_build.sbt, so it's NOT multi-dep + it { is_expected.to eq(version_class.new("2.12.0")) } + end + end + + describe "#lowest_security_fix_version" do + subject { checker.lowest_security_fix_version } + + let(:dependency_version) { "33.0.0-jre" } + let(:security_advisories) do + [ + Dependabot::SecurityAdvisory.new( + dependency_name: dependency_name, + package_manager: "sbt", + vulnerable_versions: ["< 33.2.0-jre"] + ) + ] + end + + before do + stub_request( + :head, + "https://repo.maven.apache.org/maven2/" \ + "com/google/guava/guava/33.2.0-jre/guava-33.2.0-jre.jar" + ).to_return(status: 200) + end + + it { is_expected.to eq(version_class.new("33.2.0-jre")) } + end + + describe "#latest_resolvable_version_with_no_unlock" do + subject { checker.latest_resolvable_version_with_no_unlock } + + it { is_expected.to be_nil } + end + + describe "#updated_requirements" do + subject(:updated_requirements) { checker.updated_requirements } + + it "updates the requirement version" do + expect(updated_requirements).to eq( + [{ + file: "build.sbt", + requirement: "33.4.0-jre", + groups: [], + source: { type: "maven_repo", url: "https://repo.maven.apache.org/maven2" }, + metadata: nil + }] + ) + end + end + + describe "#requirements_unlocked_or_can_be?" do + subject { checker.requirements_unlocked_or_can_be? } + + it { is_expected.to be(true) } + end end diff --git a/sbt/spec/fixtures/maven_metadata/akka_actor_2.13.xml b/sbt/spec/fixtures/maven_metadata/akka_actor_2.13.xml new file mode 100644 index 0000000000..c424ab205e --- /dev/null +++ b/sbt/spec/fixtures/maven_metadata/akka_actor_2.13.xml @@ -0,0 +1,23 @@ + + com.typesafe.akka + akka-actor_2.13 + + 2.9.3 + 2.9.3 + + 2.6.0 + 2.6.1 + 2.6.19 + 2.6.20 + 2.7.0 + 2.8.0 + 2.8.5 + 2.9.0-M1 + 2.9.0 + 2.9.1 + 2.9.2 + 2.9.3 + + 20240501000000 + + diff --git a/sbt/spec/fixtures/maven_metadata/cats_core_2.13.xml b/sbt/spec/fixtures/maven_metadata/cats_core_2.13.xml new file mode 100644 index 0000000000..a2b4ac6cdb --- /dev/null +++ b/sbt/spec/fixtures/maven_metadata/cats_core_2.13.xml @@ -0,0 +1,26 @@ + + org.typelevel + cats-core_2.13 + + 2.12.0 + 2.12.0 + + 2.0.0 + 2.1.0 + 2.2.0 + 2.3.0 + 2.4.0 + 2.5.0 + 2.6.0 + 2.6.1 + 2.7.0 + 2.8.0 + 2.9.0 + 2.10.0 + 2.11.0 + 2.12.0 + 2.13.0-RC1 + + 20240101000000 + + diff --git a/sbt/spec/fixtures/maven_metadata/guava.xml b/sbt/spec/fixtures/maven_metadata/guava.xml new file mode 100644 index 0000000000..c3b584d739 --- /dev/null +++ b/sbt/spec/fixtures/maven_metadata/guava.xml @@ -0,0 +1,29 @@ + + com.google.guava + guava + + 33.4.0-jre + 33.4.0-jre + + 31.0-jre + 31.0-android + 31.1-jre + 31.1-android + 32.0.0-jre + 32.0.0-android + 32.1.0-jre + 32.1.0-android + 33.0.0-jre + 33.0.0-android + 33.1.0-jre + 33.1.0-android + 33.2.0-jre + 33.2.0-android + 33.3.0-jre + 33.3.0-android + 33.4.0-jre + 33.4.0-android + + 20240601000000 + + diff --git a/sbt/spec/fixtures/maven_metadata/scalatest_2.13.xml b/sbt/spec/fixtures/maven_metadata/scalatest_2.13.xml new file mode 100644 index 0000000000..4ed208a88a --- /dev/null +++ b/sbt/spec/fixtures/maven_metadata/scalatest_2.13.xml @@ -0,0 +1,19 @@ + + org.scalatest + scalatest_2.13 + + 3.2.19 + 3.2.19 + + 3.0.0 + 3.1.0 + 3.2.0 + 3.2.10 + 3.2.15 + 3.2.17 + 3.2.18 + 3.2.19 + + 20240601000000 + +