diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Analyze.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Analyze.cs new file mode 100644 index 00000000000..d16f60658cb --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Analyze.cs @@ -0,0 +1,44 @@ +using System.Text; + +using NuGetUpdater.Core.Test.Analyze; + +namespace NuGetUpdater.Cli.Test; + +using TestFile = (string Path, string Content); + +public partial class EntryPointTests +{ + public class Analyze : AnalyzeWorkerTestBase + { + private static async Task Run(Func getArgs, string dependencyName, TestFile[] initialFiles, ExpectedAnalysisResult expectedResult) + { + var actualResult = await RunAnalyzerAsync(dependencyName, initialFiles, async path => + { + var sb = new StringBuilder(); + var writer = new StringWriter(sb); + + var originalOut = Console.Out; + var originalErr = Console.Error; + Console.SetOut(writer); + Console.SetError(writer); + + try + { + var args = getArgs(path); + var result = await Program.Main(args); + if (result != 0) + { + throw new Exception($"Program exited with code {result}.\nOutput:\n\n{sb}"); + } + } + finally + { + Console.SetOut(originalOut); + Console.SetError(originalErr); + } + }); + + ValidateAnalysisResult(expectedResult, actualResult); + } + } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Discover.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Discover.cs index 2ccd2458e40..8a6b3bced45 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Discover.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Discover.cs @@ -1,7 +1,7 @@ -using System.Collections.Immutable; using System.Text; using NuGetUpdater.Core; +using NuGetUpdater.Core.Discover; using NuGetUpdater.Core.Test.Discover; using Xunit; @@ -25,6 +25,8 @@ public async Task WithSolution() path, "--workspace", path, + "--output", + Path.Combine(path, DiscoveryWorker.DiscoveryResultFileName) ], new[] { @@ -77,7 +79,7 @@ public async Task WithSolution() }, expectedResult: new() { - FilePath = "", + Path = "", Projects = [ new() { @@ -107,6 +109,8 @@ public async Task WithProject() path, "--workspace", path, + "--output", + Path.Combine(path, DiscoveryWorker.DiscoveryResultFileName) ], new[] { @@ -136,7 +140,7 @@ public async Task WithProject() }, expectedResult: new() { - FilePath = "", + Path = "", Projects = [ new() { @@ -166,6 +170,8 @@ public async Task WithDirectory() path, "--workspace", Path.Combine(path, workspacePath), + "--output", + Path.Combine(path, DiscoveryWorker.DiscoveryResultFileName) ], new[] { @@ -195,7 +201,7 @@ public async Task WithDirectory() }, expectedResult: new() { - FilePath = workspacePath, + Path = workspacePath, Projects = [ new() { @@ -221,6 +227,8 @@ public async Task WithDirectory() { var actualResult = await RunDiscoveryAsync(initialFiles, async path => { + expectedResult = expectedResult with { Path = Path.Combine(path, expectedResult.Path) }; + var sb = new StringBuilder(); var writer = new StringWriter(sb); diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.FrameworkCheck.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.FrameworkCheck.cs index 0bc2f50d8df..280affb9801 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.FrameworkCheck.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.FrameworkCheck.cs @@ -1,7 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; - using Xunit; namespace NuGetUpdater.Cli.Test; diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Update.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Update.cs index 42199f092cb..7a4905cf71a 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Update.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Update.cs @@ -1,7 +1,4 @@ -using System; -using System.IO; using System.Text; -using System.Threading.Tasks; using NuGetUpdater.Core; using NuGetUpdater.Core.Test; @@ -235,7 +232,7 @@ public async Task WithDirsProjAndDirectoryBuildPropsThatIsOutOfDirectoryButStill """), ("other-dir/Directory.Build.props", """ - + @@ -254,7 +251,7 @@ public async Task WithDirsProjAndDirectoryBuildPropsThatIsOutOfDirectoryButStill """), ("some-dir/project1/project.csproj", - """ + """ Exe @@ -282,7 +279,7 @@ public async Task WithDirsProjAndDirectoryBuildPropsThatIsOutOfDirectoryButStill """), ("other-dir/Directory.Build.props", """ - + diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Commands/AnalyzeCommand.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Commands/AnalyzeCommand.cs new file mode 100644 index 00000000000..e6129eb6067 --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Commands/AnalyzeCommand.cs @@ -0,0 +1,35 @@ +using System.CommandLine; + +using NuGetUpdater.Core; +using NuGetUpdater.Core.Analyze; + +namespace NuGetUpdater.Cli.Commands; + +internal static class AnalyzeCommand +{ + internal static readonly Option DependencyFilePathOption = new("--dependency-file-path") { IsRequired = true }; + internal static readonly Option DiscoveryFilePathOption = new("--discovery-file-path") { IsRequired = true }; + internal static readonly Option AnalysisFolderOption = new("--analysis-folder-path") { IsRequired = true }; + internal static readonly Option VerboseOption = new("--verbose", getDefaultValue: () => false); + + internal static Command GetCommand(Action setExitCode) + { + Command command = new("analyze", "Determines how to update a dependency based on the workspace discovery information.") + { + DependencyFilePathOption, + DiscoveryFilePathOption, + AnalysisFolderOption, + VerboseOption + }; + + command.TreatUnmatchedTokensAsErrors = true; + + command.SetHandler(async (discoveryPath, dependencyPath, analysisDirectory, verbose) => + { + var worker = new AnalyzeWorker(new Logger(verbose)); + await worker.RunAsync(discoveryPath.FullName, dependencyPath.FullName, analysisDirectory.FullName); + }, DiscoveryFilePathOption, DependencyFilePathOption, AnalysisFolderOption, VerboseOption); + + return command; + } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Commands/DiscoverCommand.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Commands/DiscoverCommand.cs index c3c371937c2..a88b46356de 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Commands/DiscoverCommand.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Commands/DiscoverCommand.cs @@ -7,9 +7,9 @@ namespace NuGetUpdater.Cli.Commands; internal static class DiscoverCommand { - internal static readonly Option RepoRootOption = new("--repo-root", () => new DirectoryInfo(Environment.CurrentDirectory)) { IsRequired = false }; + internal static readonly Option RepoRootOption = new("--repo-root") { IsRequired = true }; internal static readonly Option WorkspaceOption = new("--workspace") { IsRequired = true }; - internal static readonly Option OutputOption = new("--output", () => DiscoveryWorker.DiscoveryResultFileName) { IsRequired = false }; + internal static readonly Option OutputOption = new("--output") { IsRequired = true }; internal static readonly Option VerboseOption = new("--verbose", getDefaultValue: () => false); internal static Command GetCommand(Action setExitCode) @@ -27,7 +27,7 @@ internal static Command GetCommand(Action setExitCode) command.SetHandler(async (repoRoot, workspace, outputPath, verbose) => { var worker = new DiscoveryWorker(new Logger(verbose)); - await worker.RunAsync(repoRoot.FullName, workspace.FullName, outputPath); + await worker.RunAsync(repoRoot.FullName, workspace.FullName, outputPath.FullName); }, RepoRootOption, WorkspaceOption, OutputOption, VerboseOption); return command; diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Program.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Program.cs index 772fb9bf683..ff0235509b4 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Program.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Program.cs @@ -15,6 +15,7 @@ internal static async Task Main(string[] args) { FrameworkCheckCommand.GetCommand(setExitCode), DiscoverCommand.GetCommand(setExitCode), + AnalyzeCommand.GetCommand(setExitCode), UpdateCommand.GetCommand(setExitCode), }; command.TreatUnmatchedTokensAsErrors = true; diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/AnalyzeWorkerTestBase.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/AnalyzeWorkerTestBase.cs new file mode 100644 index 00000000000..6cbb6bb96b2 --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/AnalyzeWorkerTestBase.cs @@ -0,0 +1,85 @@ +using System.Collections.Immutable; +using System.Text.Json; + +using NuGetUpdater.Core.Analyze; +using NuGetUpdater.Core.Discover; +using NuGetUpdater.Core.Test.Utilities; + +using Xunit; + +namespace NuGetUpdater.Core.Test.Analyze; + +using TestFile = (string Path, string Content); + +public class AnalyzeWorkerTestBase +{ + protected static async Task TestAnalyzeAsync( + WorkspaceDiscoveryResult discovery, + DependencyInfo dependencyInfo, + ExpectedAnalysisResult expectedResult) + { + var relativeDependencyPath = $"./dependabot/dependency/{dependencyInfo.Name}.json"; + + TestFile[] files = [ + (DiscoveryWorker.DiscoveryResultFileName, JsonSerializer.Serialize(discovery, AnalyzeWorker.SerializerOptions)), + (relativeDependencyPath, JsonSerializer.Serialize(dependencyInfo, AnalyzeWorker.SerializerOptions)), + ]; + + var actualResult = await RunAnalyzerAsync(dependencyInfo.Name, files, async directoryPath => + { + var discoveryPath = Path.GetFullPath(DiscoveryWorker.DiscoveryResultFileName, directoryPath); + var dependencyPath = Path.GetFullPath(relativeDependencyPath, directoryPath); + var analysisPath = Path.GetFullPath(AnalyzeWorker.AnalysisDirectoryName, directoryPath); + + var worker = new AnalyzeWorker(new Logger(verbose: true)); + await worker.RunAsync(discoveryPath, dependencyPath, analysisPath); + }); + + ValidateAnalysisResult(expectedResult, actualResult); + } + + protected static void ValidateAnalysisResult(ExpectedAnalysisResult expectedResult, AnalysisResult actualResult) + { + Assert.NotNull(actualResult); + Assert.Equal(expectedResult.UpdatedVersion, actualResult.UpdatedVersion); + Assert.Equal(expectedResult.CanUpdate, actualResult.CanUpdate); + Assert.Equal(expectedResult.VersionComesFromMultiDependencyProperty, actualResult.VersionComesFromMultiDependencyProperty); + ValidateDependencies(expectedResult.UpdatedDependencies, actualResult.UpdatedDependencies); + Assert.Equal(expectedResult.ExpectedUpdatedDependenciesCount ?? expectedResult.UpdatedDependencies.Length, actualResult.UpdatedDependencies.Length); + + return; + + void ValidateDependencies(ImmutableArray expectedDependencies, ImmutableArray actualDependencies) + { + if (expectedDependencies.IsDefault) + { + return; + } + + foreach (var expectedDependency in expectedDependencies) + { + var actualDependency = actualDependencies.Single(d => d.Name == expectedDependency.Name); + Assert.Equal(expectedDependency.Name, actualDependency.Name); + Assert.Equal(expectedDependency.Version, actualDependency.Version); + Assert.Equal(expectedDependency.Type, actualDependency.Type); + AssertEx.Equal(expectedDependency.TargetFrameworks, actualDependency.TargetFrameworks); + Assert.Equal(expectedDependency.IsDirect, actualDependency.IsDirect); + Assert.Equal(expectedDependency.IsTransitive, actualDependency.IsTransitive); + } + } + } + + protected static async Task RunAnalyzerAsync(string dependencyName, TestFile[] files, Func action) + { + // write initial files + using var temporaryDirectory = await TemporaryDirectory.CreateWithContentsAsync(files); + + // run discovery + await action(temporaryDirectory.DirectoryPath); + + // gather results + var resultPath = Path.Join(temporaryDirectory.DirectoryPath, AnalyzeWorker.AnalysisDirectoryName, $"{dependencyName}.json"); + var resultJson = await File.ReadAllTextAsync(resultPath); + return JsonSerializer.Deserialize(resultJson, DiscoveryWorker.SerializerOptions)!; + } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/AnalyzeWorkerTests.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/AnalyzeWorkerTests.cs new file mode 100644 index 00000000000..ee1612ec462 --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/AnalyzeWorkerTests.cs @@ -0,0 +1,253 @@ +using NuGetUpdater.Core.Analyze; + +using Xunit; + +namespace NuGetUpdater.Core.Test.Analyze; + +public partial class AnalyzeWorkerTests : AnalyzeWorkerTestBase +{ + [Fact] + public async Task FindsUpdatedVersion() + { + await TestAnalyzeAsync( + discovery: new() + { + Path = "/", + Projects = [ + new() + { + FilePath = "./project.csproj", + TargetFrameworks = ["net8.0"], + Dependencies = [ + new("Microsoft.CodeAnalysis.Common", "4.0.1", DependencyType.PackageReference), + ], + }, + ], + }, + dependencyInfo: new() + { + Name = "Microsoft.CodeAnalysis.Common", + Version = "4.0.1", + IgnoredVersions = [Requirement.Parse("> 4.9.2")], + IsVulnerable = false, + Vulnerabilities = [], + }, + expectedResult: new() + { + UpdatedVersion = "4.9.2", + CanUpdate = true, + VersionComesFromMultiDependencyProperty = false, + UpdatedDependencies = [ + new("Microsoft.CodeAnalysis.Common", "4.9.2", DependencyType.Unknown, TargetFrameworks: ["net8.0"]), + ], + } + ); + } + + [Fact] + public async Task FindsUpdatedPeerDependencies() + { + await TestAnalyzeAsync( + discovery: new() + { + Path = "/", + Projects = [ + new() + { + FilePath = "./project.csproj", + TargetFrameworks = ["net8.0"], + Dependencies = [ + new("Microsoft.CodeAnalysis", "4.0.1", DependencyType.PackageReference), + new("Microsoft.CodeAnalysis.Workspaces.Common", "4.0.1", DependencyType.PackageReference), + ], + }, + ], + }, + dependencyInfo: new() + { + Name = "Microsoft.CodeAnalysis", + Version = "4.0.1", + IgnoredVersions = [Requirement.Parse("> 4.9.2")], + IsVulnerable = false, + Vulnerabilities = [], + }, + expectedResult: new() + { + UpdatedVersion = "4.9.2", + CanUpdate = true, + VersionComesFromMultiDependencyProperty = false, + UpdatedDependencies = [ + new("Microsoft.CodeAnalysis", "4.9.2", DependencyType.Unknown, TargetFrameworks: ["net8.0"]), + new("Microsoft.CodeAnalysis.Workspaces.Common", "4.9.2", DependencyType.Unknown, TargetFrameworks: ["net8.0"]), + ], + } + ); + } + + [Fact] + public async Task DeterminesMultiPropertyVersion() + { + var evaluationResult = new EvaluationResult(EvaluationResultType.Success, "$(RoslynPackageVersion)", "4.0.1", "RoslynPackageVersion", ErrorMessage: null); + await TestAnalyzeAsync( + discovery: new() + { + Path = "/", + Projects = [ + new() + { + FilePath = "./project.csproj", + TargetFrameworks = ["net8.0"], + Dependencies = [ + new("Microsoft.CodeAnalysis.Common", "4.0.1", DependencyType.PackageReference, EvaluationResult: evaluationResult, TargetFrameworks: ["net8.0"]), + ], + }, + new() + { + FilePath = "./project2.csproj", + TargetFrameworks = ["net8.0"], + Dependencies = [ + new("Microsoft.CodeAnalysis.Workspaces.Common", "4.0.1", DependencyType.PackageReference, EvaluationResult: evaluationResult, TargetFrameworks: ["net8.0"]), + ], + }, + ], + }, + dependencyInfo: new() + { + Name = "Microsoft.CodeAnalysis.Common", + Version = "4.0.1", + IgnoredVersions = [Requirement.Parse("> 4.9.2")], + IsVulnerable = false, + Vulnerabilities = [], + }, + expectedResult: new() + { + UpdatedVersion = "4.9.2", + CanUpdate = true, + VersionComesFromMultiDependencyProperty = true, + UpdatedDependencies = [ + new("Microsoft.CodeAnalysis.Common", "4.9.2", DependencyType.Unknown, TargetFrameworks: ["net8.0"]), + new("Microsoft.CodeAnalysis.Workspaces.Common", "4.9.2", DependencyType.Unknown, TargetFrameworks: ["net8.0"]), + ], + } + ); + } + + [Fact] + public async Task FailsToUpdateMultiPropertyVersion() + { + // Roslyn packages and System.Memory happen to share some versions but would fail to update in sync with each other. + var evaluationResult = new EvaluationResult(EvaluationResultType.Success, "$(RoslynPackageVersion)", "4.5.0", "RoslynPackageVersion", ErrorMessage: null); + await TestAnalyzeAsync( + discovery: new() + { + Path = "/", + Projects = [ + new() + { + FilePath = "./project.csproj", + TargetFrameworks = ["net8.0"], + Dependencies = [ + new("Microsoft.CodeAnalysis.Common", "4.5.0", DependencyType.PackageReference, EvaluationResult: evaluationResult, TargetFrameworks: ["net8.0"]), + ], + }, + new() + { + FilePath = "./project2.csproj", + TargetFrameworks = ["net8.0"], + Dependencies = [ + new("System.Memory", "4.5.0", DependencyType.PackageReference, EvaluationResult: evaluationResult, TargetFrameworks: ["net8.0"]), + ], + }, + ], + }, + dependencyInfo: new() + { + Name = "Microsoft.CodeAnalysis.Common", + Version = "4.5.0", + IgnoredVersions = [Requirement.Parse("> 4.9.2")], + IsVulnerable = false, + Vulnerabilities = [], + }, + expectedResult: new() + { + UpdatedVersion = "4.5.0", + CanUpdate = false, + VersionComesFromMultiDependencyProperty = true, + UpdatedDependencies = [], + } + ); + } + + + [Fact] + public async Task ReturnsUpToDate_ForMissingVersionProperty() + { + await TestAnalyzeAsync( + discovery: new() + { + Path = "/", + Projects = [ + new() + { + FilePath = "./project.csproj", + TargetFrameworks = ["net8.0"], + Dependencies = [ + new("Microsoft.CodeAnalysis.Common", "$(MissingPackageVersion)", DependencyType.PackageReference, EvaluationResult: new EvaluationResult(EvaluationResultType.PropertyNotFound, "$(MissingPackageVersion)", "$(MissingPackageVersion)", "$(MissingPackageVersion)", ErrorMessage: null)), + ], + }, + ], + }, + dependencyInfo: new() + { + Name = "Microsoft.CodeAnalysis", + Version = "$(MissingPackageVersion)", + IgnoredVersions = [Requirement.Parse("> 4.9.2")], + IsVulnerable = false, + Vulnerabilities = [], + }, + expectedResult: new() + { + UpdatedVersion = "$(MissingPackageVersion)", + CanUpdate = false, + VersionComesFromMultiDependencyProperty = false, + UpdatedDependencies = [], + } + ); + } + + [Fact] + public async Task ReturnsUpToDate_ForMissingDependency() + { + await TestAnalyzeAsync( + discovery: new() + { + Path = "/", + Projects = [ + new() + { + FilePath = "./project.csproj", + TargetFrameworks = ["net8.0"], + Dependencies = [ + new("Microsoft.CodeAnalysis.Common", "4.0.1", DependencyType.PackageReference), + ], + }, + ], + }, + dependencyInfo: new() + { + Name = "Microsoft.CodeAnalysis", + Version = "4.0.1", + IgnoredVersions = [Requirement.Parse("> 4.9.2")], + IsVulnerable = false, + Vulnerabilities = [], + }, + expectedResult: new() + { + UpdatedVersion = "4.0.1", + CanUpdate = false, + VersionComesFromMultiDependencyProperty = false, + UpdatedDependencies = [], + } + ); + } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/CompatibilityCheckerTests.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/CompatibilityCheckerTests.cs new file mode 100644 index 00000000000..8ad02a925b1 --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/CompatibilityCheckerTests.cs @@ -0,0 +1,145 @@ +using System.Collections.Immutable; + +using NuGet.Frameworks; +using NuGet.Packaging.Core; +using NuGet.Versioning; + +using NuGetUpdater.Core.Analyze; + +using Xunit; + +namespace NuGetUpdater.Core.Test.Analyze; + +public class CompatibilityCheckerTests +{ + [Fact] + public void PerformCheck_CompatiblePackage_IsCompatible() + { + var package = new PackageIdentity("Dependency", NuGetVersion.Parse("1.0.0")); + ImmutableArray projectFrameworks = [ + NuGetFramework.Parse("net6.0"), + NuGetFramework.Parse("netstandard2.0"), + ]; + var isDevDependency = false; + ImmutableArray packageFrameworks = [ + NuGetFramework.Parse("netstandard1.3"), + ]; + + var result = CompatibilityChecker.PerformCheck( + package, + projectFrameworks, + isDevDependency, + packageFrameworks, + new Logger(verbose: false)); + + Assert.True(result); + } + + [Fact] + public void PerformCheck_IncompatiblePackage_IsIncompatible() + { + var package = new PackageIdentity("Dependency", NuGetVersion.Parse("1.0.0")); + ImmutableArray projectFrameworks = [ + NuGetFramework.Parse("net6.0"), + NuGetFramework.Parse("netstandard2.0"), + ]; + var isDevDependency = false; + ImmutableArray packageFrameworks = [ + NuGetFramework.Parse("net462"), + ]; + + var result = CompatibilityChecker.PerformCheck( + package, + projectFrameworks, + isDevDependency, + packageFrameworks, + new Logger(verbose: false)); + + Assert.False(result); + } + + [Fact] + public void PerformCheck_DevDependencyWithPackageFrameworks_IsChecked() + { + var package = new PackageIdentity("Dependency", NuGetVersion.Parse("1.0.0")); + ImmutableArray projectFrameworks = [ + NuGetFramework.Parse("net6.0"), + NuGetFramework.Parse("netstandard2.0"), + ]; + var isDevDependency = true; + ImmutableArray packageFrameworks = [ + NuGetFramework.Parse("net462"), + ]; + + var result = CompatibilityChecker.PerformCheck( + package, + projectFrameworks, + isDevDependency, + packageFrameworks, + new Logger(verbose: false)); + + Assert.False(result); + } + + [Fact] + public void PerformCheck_DevDependencyWithoutPackageFrameworks_IsCompatibile() + { + var package = new PackageIdentity("Dependency", NuGetVersion.Parse("1.0.0")); + ImmutableArray projectFrameworks = [ + NuGetFramework.Parse("net6.0"), + NuGetFramework.Parse("netstandard2.0"), + ]; + var isDevDependency = true; + ImmutableArray packageFrameworks = []; + + var result = CompatibilityChecker.PerformCheck( + package, + projectFrameworks, + isDevDependency, + packageFrameworks, + new Logger(verbose: false)); + + Assert.True(result); + } + + [Fact] + public void PerformCheck_WithoutPackageFrameworks_IsIncompatibile() + { + var package = new PackageIdentity("Dependency", NuGetVersion.Parse("1.0.0")); + ImmutableArray projectFrameworks = [ + NuGetFramework.Parse("net6.0"), + NuGetFramework.Parse("netstandard2.0"), + ]; + var isDevDependency = false; + ImmutableArray packageFrameworks = []; + + var result = CompatibilityChecker.PerformCheck( + package, + projectFrameworks, + isDevDependency, + packageFrameworks, + new Logger(verbose: false)); + + Assert.False(result); + } + + [Fact] + public void PerformCheck_WithoutProjectFrameworks_IsIncompatible() + { + var package = new PackageIdentity("Dependency", NuGetVersion.Parse("1.0.0")); + ImmutableArray projectFrameworks = []; + var isDevDependency = true; + ImmutableArray packageFrameworks = [ + NuGetFramework.Parse("netstandard1.3"), + ]; + + var result = CompatibilityChecker.PerformCheck( + package, + projectFrameworks, + isDevDependency, + packageFrameworks, + new Logger(verbose: false)); + + Assert.False(result); + } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/ExpectedAnalysisResult.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/ExpectedAnalysisResult.cs new file mode 100644 index 00000000000..d6302fc70cf --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/ExpectedAnalysisResult.cs @@ -0,0 +1,8 @@ +using NuGetUpdater.Core.Analyze; + +namespace NuGetUpdater.Core.Test.Analyze; + +public record ExpectedAnalysisResult : AnalysisResult +{ + public int? ExpectedUpdatedDependenciesCount { get; init; } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/NuspecLocatorTests.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/NuspecLocatorTests.cs new file mode 100644 index 00000000000..307b007c7cc --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/NuspecLocatorTests.cs @@ -0,0 +1,98 @@ +using NuGet.Versioning; + +using NuGetUpdater.Core.Analyze; + +using Xunit; + +namespace NuGetUpdater.Core.Test.Analyze; + +using TestFile = (string Path, string Content); + +public class NuspecLocatorTests +{ + internal const string NuGetOrgFeedUrl = "https://api.nuget.org/v3/index.json"; + internal const string DotNetToolsFeedUrl = "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-tools/nuget/v3/index.json"; + + [Fact] + public async Task LocateNuspec_ReturnsUrl_ForNuGetOrgFeed() + { + await TestLocateAsync( + [NuGetOrgFeedUrl], + "Microsoft.CodeAnalysis.Common", + NuGetVersion.Parse("4.9.2"), + "https://api.nuget.org/v3-flatcontainer/microsoft.codeanalysis.common/4.9.2/microsoft.codeanalysis.common.nuspec"); + } + + [Fact] + public async Task LocateNuspec_ReturnsUrl_ForAzureArtifactsFeed() + { + await TestLocateAsync( + [NuGetOrgFeedUrl, DotNetToolsFeedUrl], + "Microsoft.CodeAnalysis.Common", + NuGetVersion.Parse("4.11.0-1.24219.1"), + "https://pkgs.dev.azure.com/dnceng/9ee6d478-d288-47f7-aacc-f6e6d082ae6d/_packaging/d1622942-d16f-48e5-bc83-96f4539e7601/nuget/v3/flat2/microsoft.codeanalysis.common/4.11.0-1.24219.1/microsoft.codeanalysis.common.nuspec"); + } + + [Fact] + public async Task LocateNuspec_ReturnsNull_ForInvalidFeed() + { + await TestLocateAsync( + ["https:://invalid-feed-url"], + "Microsoft.CodeAnalysis.Common", + NuGetVersion.Parse("4.9.2"), + null); + } + + [Fact] + public async Task LocateNuspec_ReturnsNull_ForInvalidPackage() + { + await TestLocateAsync( + [NuGetOrgFeedUrl], + "Microsoft.CodeAnalysis.Invalid", + NuGetVersion.Parse("4.9.2"), + null); + } + + protected static async Task TestLocateAsync( + string[] feedUrls, + string packageId, + NuGetVersion version, + string? expectedResult) + { + var currentDirectory = Environment.CurrentDirectory; + + TestFile[] files = [ + ("./nuget.config", $""" + + + + + {string.Join(Environment.NewLine, feedUrls.Select((url, index) => $""))} + + + """), + ]; + + using var temporaryDirectory = await TemporaryDirectory.CreateWithContentsAsync(files); + + try + { + Environment.CurrentDirectory = temporaryDirectory.DirectoryPath; + var nugetContext = new NuGetContext(); + var logger = new Logger(verbose: true); + + var actual = await NuspecLocator.LocateNuspecAsync( + packageId, + version, + nugetContext, + logger, + CancellationToken.None); + + Assert.Equal(expectedResult, actual); + } + finally + { + Environment.CurrentDirectory = currentDirectory; + } + } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/RequirementTests.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/RequirementTests.cs new file mode 100644 index 00000000000..f2150ae11f0 --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/RequirementTests.cs @@ -0,0 +1,69 @@ +using NuGet.Versioning; + +using NuGetUpdater.Core.Analyze; + +using Xunit; + +namespace NuGetUpdater.Core.Test.Analyze; + +public class RequirementTests +{ + // Supported OPs (=, !=, >, <, >=, <=, ~>) + [Theory] + [InlineData("1.0.0", "1.0.0", true)] + [InlineData("1.0.0-alpha", "1.0.0", false)] + [InlineData("1.0.0", "= 1.0.0", true)] + [InlineData("1.0.0-alpha", "= 1.0.0", false)] + [InlineData("1.0.0", "!=1.0.1", true)] + [InlineData("1.0.0", "!= 1.0.0", false)] + [InlineData("1.0.1", "> 1.0.0", true)] + [InlineData("1.0.0-alpha", "> 1.0.0", false)] + [InlineData("1.0.0", "< 1.0.1", true)] + [InlineData("1.0.0", "<1.0.0-alpha", false)] + [InlineData("1.0.0", ">= 1.0.0", true)] + [InlineData("1.0.1", ">= 1.0.0", true)] + [InlineData("1.0.0-alpha", ">= 1.0.0", false)] + [InlineData("1.0.0", "<= 1.0.0", true)] + [InlineData("1.0.0-alpha", "<= 1.0.0", true)] + [InlineData("1.0.1", "<= 1.0.0", false)] + [InlineData("1.0.1", "~>1.0.0", true)] + [InlineData("1.1.0", "~> 1.0.0", false)] + [InlineData("1.1", "~> 1.0", true)] + [InlineData("2.0", "~> 1.0", false)] + [InlineData("1", "~> 1", true)] + [InlineData("2", "~> 1", false)] + public void IsSatisfiedBy(string versionString, string requirementString, bool expected) + { + var version = NuGetVersion.Parse(versionString); + var requirement = Requirement.Parse(requirementString); + + var actual = requirement.IsSatisfiedBy(version); + + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData("> = 1.0.0")] // Invalid format + [InlineData("<>= 1.0.0")] // Invalid Operator + [InlineData(">")] // Missing version + public void Parse_ThrowsForInvalid(string requirementString) + { + Assert.Throws(() => Requirement.Parse(requirementString)); + } + + [Theory] + [InlineData("1.0.0-alpha", "1.1.0.0")] + [InlineData("1.0.0.0", "1.0.1.0")] + [InlineData("1.0.0", "1.1.0.0")] + [InlineData("1.0", "2.0.0.0")] + [InlineData("1", "2.0.0.0")] + public void Bump(string versionString, string expectedString) + { + var version = NuGetVersion.Parse(versionString); + var expected = Version.Parse(expectedString); + + var actual = Requirement.Bump(version); + + Assert.Equal(expected, actual); + } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/SecurityVulnerabilityExtensionsTests.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/SecurityVulnerabilityExtensionsTests.cs new file mode 100644 index 00000000000..1460e910f6e --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/SecurityVulnerabilityExtensionsTests.cs @@ -0,0 +1,78 @@ +using NuGet.Versioning; + +using NuGetUpdater.Core.Analyze; + +using Xunit; + +namespace NuGetUpdater.Core.Test.Analyze; + +public class SecurityVulnerabilityExtensionsTests +{ + [Fact] + public void VersionInSafeVersions_IsNotVulnerable() + { + var version = NuGetVersion.Parse("1.0.1"); + var vulnerability = new SecurityVulnerability + { + DependencyName = "Dependency", + PackageManager = "PackageManager", + SafeVersions = [Requirement.Parse("> 1.0.0")], + VulnerableVersions = [Requirement.Parse("<= 1.0.0")], + }; + + var result = vulnerability.IsVulnerable(version); + + Assert.False(result); + } + + [Fact] + public void VersionInVulnerableVersions_IsVulnerable() + { + var version = NuGetVersion.Parse("1.0.0"); + var vulnerability = new SecurityVulnerability + { + DependencyName = "Dependency", + PackageManager = "PackageManager", + SafeVersions = [Requirement.Parse("> 1.0.0")], + VulnerableVersions = [Requirement.Parse("<= 1.0.0")], + }; + + var result = vulnerability.IsVulnerable(version); + + Assert.True(result); + } + + [Fact] + public void VersionNotInVulnerableVersions_IsNotVulnerable() + { + var version = NuGetVersion.Parse("1.0.1"); + var vulnerability = new SecurityVulnerability + { + DependencyName = "Dependency", + PackageManager = "PackageManager", + SafeVersions = [], + VulnerableVersions = [Requirement.Parse("<= 1.0.0")], + }; + + var result = vulnerability.IsVulnerable(version); + + Assert.False(result); + } + + [Fact] + public void VersionNotInSafeVersions_IsVulnerable() + { + var version = NuGetVersion.Parse("1.0.0"); + var vulnerability = new SecurityVulnerability + { + DependencyName = "Dependency", + PackageManager = "PackageManager", + SafeVersions = [Requirement.Parse("> 1.0.0")], + VulnerableVersions = [], + }; + + var result = vulnerability.IsVulnerable(version); + + Assert.True(result); + } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/VersionFinderTests.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/VersionFinderTests.cs new file mode 100644 index 00000000000..a1617961929 --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/VersionFinderTests.cs @@ -0,0 +1,193 @@ +using NuGet.Versioning; + +using NuGetUpdater.Core.Analyze; + +using Xunit; + +namespace NuGetUpdater.Core.Test.Analyze; + +public class VersionFinderTests +{ + [Fact] + public void VersionFilter_VersionInIgnoredVersions_ReturnsFalse() + { + var dependencyInfo = new DependencyInfo + { + Name = "Dependency", + Version = "0.8.0", + IsVulnerable = false, + IgnoredVersions = [Requirement.Parse("< 1.0.0")], + Vulnerabilities = [], + }; + var filter = VersionFinder.CreateVersionFilter(dependencyInfo, VersionRange.Parse(dependencyInfo.Version)); + var version = NuGetVersion.Parse("0.9.0"); + + var result = filter(version); + + Assert.False(result); + } + + [Fact] + public void VersionFilter_VersionNotInIgnoredVersions_ReturnsTrue() + { + var dependencyInfo = new DependencyInfo + { + Name = "Dependency", + Version = "0.8.0", + IsVulnerable = false, + IgnoredVersions = [Requirement.Parse("< 1.0.0")], + Vulnerabilities = [], + }; + var filter = VersionFinder.CreateVersionFilter(dependencyInfo, VersionRange.Parse(dependencyInfo.Version)); + var version = NuGetVersion.Parse("1.0.1"); + + var result = filter(version); + + Assert.True(result); + } + + [Fact] + public void VersionFilter_VersionInVulnerabilities_ReturnsFalse() + { + var dependencyInfo = new DependencyInfo + { + Name = "Dependency", + Version = "0.8.0", + IsVulnerable = false, + IgnoredVersions = [], + Vulnerabilities = [new() + { + DependencyName = "Dependency", + PackageManager = "PackageManager", + SafeVersions = [], + VulnerableVersions = [Requirement.Parse("< 1.0.0")], + }], + }; + var filter = VersionFinder.CreateVersionFilter(dependencyInfo, VersionRange.Parse(dependencyInfo.Version)); + var version = NuGetVersion.Parse("0.9.0"); + + var result = filter(version); + + Assert.False(result); + } + + [Fact] + public void VersionFilter_VersionNotInVulnerabilities_ReturnsTrue() + { + var dependencyInfo = new DependencyInfo + { + Name = "Dependency", + Version = "0.8.0", + IsVulnerable = false, + IgnoredVersions = [], + Vulnerabilities = [new() + { + DependencyName = "Dependency", + PackageManager = "PackageManager", + SafeVersions = [], + VulnerableVersions = [Requirement.Parse("< 1.0.0")], + }], + }; + var filter = VersionFinder.CreateVersionFilter(dependencyInfo, VersionRange.Parse(dependencyInfo.Version)); + var version = NuGetVersion.Parse("1.0.1"); + + var result = filter(version); + + Assert.True(result); + } + + [Fact] + public void VersionFilter_VersionLessThanCurrentVersion_ReturnsFalse() + { + var dependencyInfo = new DependencyInfo + { + Name = "Dependency", + Version = "1.0.0", + IsVulnerable = false, + IgnoredVersions = [], + Vulnerabilities = [], + }; + var filter = VersionFinder.CreateVersionFilter(dependencyInfo, VersionRange.Parse(dependencyInfo.Version)); + var version = NuGetVersion.Parse("0.9.0"); + + var result = filter(version); + + Assert.False(result); + } + + [Fact] + public void VersionFilter_VersionHigherThanCurrentVersion_ReturnsTrue() + { + var dependencyInfo = new DependencyInfo + { + Name = "Dependency", + Version = "1.0.0", + IsVulnerable = false, + IgnoredVersions = [], + Vulnerabilities = [], + }; + var filter = VersionFinder.CreateVersionFilter(dependencyInfo, VersionRange.Parse(dependencyInfo.Version)); + var version = NuGetVersion.Parse("1.0.1"); + + var result = filter(version); + + Assert.True(result); + } + + [Fact] + public void VersionFilter_PreviewVersionDifferentThanCurrentVersion_ReturnsFalse() + { + var dependencyInfo = new DependencyInfo + { + Name = "Dependency", + Version = "1.0.0-alpha", + IsVulnerable = false, + IgnoredVersions = [], + Vulnerabilities = [], + }; + var filter = VersionFinder.CreateVersionFilter(dependencyInfo, VersionRange.Parse(dependencyInfo.Version)); + var version = NuGetVersion.Parse("1.0.1-beta"); + + var result = filter(version); + + Assert.False(result); + } + + [Fact] + public void VersionFilter_PreviewVersionSameAsCurrentVersion_ReturnsTrue() + { + var dependencyInfo = new DependencyInfo + { + Name = "Dependency", + Version = "1.0.0-alpha", + IsVulnerable = false, + IgnoredVersions = [], + Vulnerabilities = [], + }; + var filter = VersionFinder.CreateVersionFilter(dependencyInfo, VersionRange.Parse(dependencyInfo.Version)); + var version = NuGetVersion.Parse("1.0.0-beta"); + + var result = filter(version); + + Assert.True(result); + } + + [Fact] + public void VersionFilter_WildcardPreviewVersion_ReturnsTrue() + { + var dependencyInfo = new DependencyInfo + { + Name = "Dependency", + Version = "*-*", + IsVulnerable = false, + IgnoredVersions = [], + Vulnerabilities = [], + }; + var filter = VersionFinder.CreateVersionFilter(dependencyInfo, VersionRange.Parse(dependencyInfo.Version)); + var version = NuGetVersion.Parse("1.0.0-beta"); + + var result = filter(version); + + Assert.True(result); + } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTestBase.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTestBase.cs index 36e5807374e..00e16d4ffc6 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTestBase.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTestBase.cs @@ -20,6 +20,8 @@ public class DiscoveryWorkerTestBase { var actualResult = await RunDiscoveryAsync(files, async directoryPath => { + expectedResult = expectedResult with { Path = Path.Combine(directoryPath, expectedResult.Path) }; + var worker = new DiscoveryWorker(new Logger(verbose: true)); await worker.RunAsync(directoryPath, workspacePath, DiscoveryWorker.DiscoveryResultFileName); }); @@ -30,7 +32,7 @@ public class DiscoveryWorkerTestBase protected static void ValidateWorkspaceResult(ExpectedWorkspaceDiscoveryResult expectedResult, WorkspaceDiscoveryResult actualResult) { Assert.NotNull(actualResult); - Assert.Equal(expectedResult.FilePath.NormalizePathToUnix(), actualResult.FilePath.NormalizePathToUnix()); + Assert.Equal(expectedResult.Path.NormalizePathToUnix(), actualResult.Path.NormalizePathToUnix()); ValidateDirectoryPackagesProps(expectedResult.DirectoryPackagesProps, actualResult.DirectoryPackagesProps); ValidateResultWithDependencies(expectedResult.GlobalJson, actualResult.GlobalJson); ValidateResultWithDependencies(expectedResult.DotNetToolsJson, actualResult.DotNetToolsJson); diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.DotNetToolsJson.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.DotNetToolsJson.cs index ce0cffdda4c..70c6852a27d 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.DotNetToolsJson.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.DotNetToolsJson.cs @@ -35,7 +35,7 @@ public async Task DiscoversDependencies() ], expectedResult: new() { - FilePath = "", + Path = "", DotNetToolsJson = new() { FilePath = ".config/dotnet-tools.json", @@ -77,7 +77,7 @@ public async Task ReportsFailure() ], expectedResult: new() { - FilePath = "", + Path = "", DotNetToolsJson = new() { FilePath = ".config/dotnet-tools.json", diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.GlobalJson.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.GlobalJson.cs index dd0b0ee8bac..65577d5cce1 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.GlobalJson.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.GlobalJson.cs @@ -25,7 +25,7 @@ public async Task DiscoversDependencies() ], expectedResult: new() { - FilePath = "", + Path = "", GlobalJson = new() { FilePath = "global.json", @@ -57,7 +57,7 @@ public async Task ReportsFailure() ], expectedResult: new() { - FilePath = "", + Path = "", GlobalJson = new() { FilePath = "global.json", diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.PackagesConfig.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.PackagesConfig.cs index a0dcab83d5f..fc05e4e4943 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.PackagesConfig.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.PackagesConfig.cs @@ -38,7 +38,7 @@ public async Task DiscoversDependencies() ], expectedResult: new() { - FilePath = "", + Path = "", Projects = [ new() { diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.Project.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.Project.cs index 5dfa34eb784..ed30c96c525 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.Project.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.Project.cs @@ -39,7 +39,7 @@ public async Task ReturnsPackageReferencesMissingVersions() ], expectedResult: new() { - FilePath = "", + Path = "", Projects = [ new() { @@ -110,7 +110,7 @@ public async Task WithDirectoryPackagesProps() ], expectedResult: new() { - FilePath = "", + Path = "", Projects = [ new() { @@ -188,7 +188,7 @@ public async Task WithDirectoryBuildPropsAndTargets() ], expectedResult: new() { - FilePath = "", + Path = "", Projects = [ new() { @@ -293,7 +293,7 @@ public async Task WithPackagesProps() ], expectedResult: new() { - FilePath = "", + Path = "", ExpectedProjectCount = 3, Projects = [ new() @@ -357,7 +357,7 @@ public async Task ReturnsDependenciesThatCannotBeEvaluated() ], expectedResult: new() { - FilePath = "", + Path = "", Projects = [ new() { @@ -397,7 +397,7 @@ public async Task NoDependenciesReturnedIfNoTargetFrameworkCanBeResolved() ], expectedResult: new() { - FilePath = "", + Path = "", Projects = [] }); } diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.cs index 9351ca51aea..c8218c41e77 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.cs @@ -29,7 +29,7 @@ public async Task TestProjectFiles(string projectPath) }, expectedResult: new() { - FilePath = "src", + Path = "src", Projects = [ new() { @@ -85,7 +85,7 @@ public async Task TestPackageConfig() }, expectedResult: new() { - FilePath = "", + Path = "", Projects = [ new() { @@ -139,7 +139,7 @@ public async Task TestProps() }, expectedResult: new() { - FilePath = "", + Path = "", Projects = [ new() { @@ -258,7 +258,7 @@ public async Task TestRepo() }, expectedResult: new() { - FilePath = "", + Path = "", Projects = [ new() { diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/ExpectedDiscoveryResults.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/ExpectedDiscoveryResults.cs index 4fd2094d7b2..e5d65ad09dd 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/ExpectedDiscoveryResults.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/ExpectedDiscoveryResults.cs @@ -4,9 +4,9 @@ namespace NuGetUpdater.Core.Test.Discover; -public record ExpectedWorkspaceDiscoveryResult : IDiscoveryResult +public record ExpectedWorkspaceDiscoveryResult { - public required string FilePath { get; init; } + public required string Path { get; init; } public bool IsSuccess { get; init; } = true; public ImmutableArray Projects { get; init; } public int? ExpectedProjectCount { get; init; } diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/AnalysisResult.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/AnalysisResult.cs new file mode 100644 index 00000000000..5c62a0944ab --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/AnalysisResult.cs @@ -0,0 +1,11 @@ +using System.Collections.Immutable; + +namespace NuGetUpdater.Core.Analyze; + +public record AnalysisResult +{ + public required string UpdatedVersion { get; init; } + public bool CanUpdate { get; init; } + public bool VersionComesFromMultiDependencyProperty { get; init; } + public required ImmutableArray UpdatedDependencies { get; init; } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/AnalyzeWorker.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/AnalyzeWorker.cs new file mode 100644 index 00000000000..89efb2c1336 --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/AnalyzeWorker.cs @@ -0,0 +1,433 @@ +using System.Collections.Immutable; +using System.Text.Json; +using System.Text.Json.Serialization; + +using NuGet.Configuration; +using NuGet.Frameworks; +using NuGet.Versioning; + +using NuGetUpdater.Core.Discover; + +namespace NuGetUpdater.Core.Analyze; + +using MultiDependency = (string PropertyName, ImmutableArray TargetFrameworks, ImmutableHashSet DependencyNames); + +public partial class AnalyzeWorker +{ + public const string AnalysisDirectoryName = "./.dependabot/analysis"; + + private readonly Logger _logger; + + internal static readonly JsonSerializerOptions SerializerOptions = new() + { + WriteIndented = true, + Converters = { new JsonStringEnumConverter(), new RequirementConverter() }, + }; + + public AnalyzeWorker(Logger logger) + { + _logger = logger; + } + + public async Task RunAsync(string discoveryPath, string dependencyPath, string analysisDirectory) + { + var discovery = await DeserializeJsonFileAsync(discoveryPath, nameof(WorkspaceDiscoveryResult)); + var dependencyInfo = await DeserializeJsonFileAsync(dependencyPath, nameof(DependencyInfo)); + + _logger.Log($"Starting analysis of {dependencyInfo.Name}..."); + + // We need to find all projects which have the given dependency. Even in cases that they + // have it transitively may require that peer dependencies be updated in the project. + var projectsWithDependency = discovery.Projects + .Where(p => p.Dependencies.Any(d => d.Name.Equals(dependencyInfo.Name, StringComparison.OrdinalIgnoreCase))) + .ToImmutableArray(); + var projectFrameworks = projectsWithDependency + .SelectMany(p => p.TargetFrameworks) + .Distinct() + .Select(NuGetFramework.Parse) + .ToImmutableArray(); + var propertyBasedDependencies = discovery.Projects.SelectMany(p + => p.Dependencies.Where(d => !d.IsTransitive && + d.EvaluationResult?.RootPropertyName is not null) + ).ToImmutableArray(); + + bool canUpdate = false; + bool usesMultiDependencyProperty = false; + NuGetVersion? updatedVersion = null; + ImmutableArray updatedDependencies = []; + + bool isUpdateNecessary = IsUpdateNecessary(dependencyInfo, projectsWithDependency); + if (isUpdateNecessary) + { + var nugetContext = new NuGetContext(); + if (!Directory.Exists(nugetContext.TempPackageDirectory)) + { + Directory.CreateDirectory(nugetContext.TempPackageDirectory); + } + + _logger.Log($" Determining multi-dependency property."); + var multiDependencies = DetermineMultiDependencyDetails( + discovery, + dependencyInfo.Name, + propertyBasedDependencies); + + usesMultiDependencyProperty = multiDependencies.Any(md => md.DependencyNames.Count > 1); + var dependenciesToUpdate = usesMultiDependencyProperty + ? multiDependencies + .SelectMany(md => md.DependencyNames) + .ToImmutableHashSet(StringComparer.OrdinalIgnoreCase) + : [dependencyInfo.Name]; + var applicableTargetFrameworks = usesMultiDependencyProperty + ? multiDependencies + .SelectMany(md => md.TargetFrameworks) + .ToImmutableHashSet(StringComparer.OrdinalIgnoreCase) + .Select(NuGetFramework.Parse) + .ToImmutableArray() + : projectFrameworks; + + _logger.Log($" Finding updated version."); + updatedVersion = await FindUpdatedVersionAsync( + dependencyInfo, + dependenciesToUpdate, + applicableTargetFrameworks, + nugetContext, + _logger, + CancellationToken.None); + + _logger.Log($" Finding updated peer dependencies."); + updatedDependencies = updatedVersion is not null + ? await FindUpdatedDependenciesAsync( + discovery, + dependenciesToUpdate, + updatedVersion, + nugetContext, + _logger, + CancellationToken.None) + : []; + + //TODO: At this point we should add the peer dependencies to a queue where + // we will analyze them one by one to see if they themselves are part of a + // multi-dependency property. Basically looping this if-body until we have + // emptied the queue and have a complete list of updated dependencies. We + // should track the dependenciesToUpdate as they have already been analyzed. + } + + var result = new AnalysisResult + { + UpdatedVersion = updatedVersion?.ToNormalizedString() ?? dependencyInfo.Version, + CanUpdate = updatedVersion is not null, + VersionComesFromMultiDependencyProperty = usesMultiDependencyProperty, + UpdatedDependencies = updatedDependencies, + }; + + await WriteResultsAsync(analysisDirectory, dependencyInfo.Name, result, _logger); + + _logger.Log($"Analysis complete."); + } + + private static bool IsUpdateNecessary(DependencyInfo dependencyInfo, ImmutableArray projectsWithDependency) + { + if (projectsWithDependency.Length == 0) + { + return false; + } + + // We will even attempt to update transitive dependencies if the dependency is vulnerable. + if (dependencyInfo.IsVulnerable) + { + return true; + } + + // Since the dependency is not vulnerable, we only need to update if it is not transitive. + return projectsWithDependency.Any(p => + p.Dependencies.Any(d => + d.Name.Equals(dependencyInfo.Name, StringComparison.OrdinalIgnoreCase) && + !d.IsTransitive)); + } + + internal static async Task DeserializeJsonFileAsync(string path, string fileType) + { + var json = File.Exists(path) + ? await File.ReadAllTextAsync(path) + : throw new FileNotFoundException($"{fileType} file not found.", path); + + return JsonSerializer.Deserialize(json, SerializerOptions) + ?? throw new InvalidOperationException($"{fileType} file is empty."); + } + + internal static async Task FindUpdatedVersionAsync( + DependencyInfo dependencyInfo, + ImmutableHashSet packageIds, + ImmutableArray projectFrameworks, + NuGetContext nugetContext, + Logger logger, + CancellationToken cancellationToken) + { + var versionResult = await VersionFinder.GetVersionsAsync( + dependencyInfo, + nugetContext, + logger, + cancellationToken); + + return await FindUpdatedVersionAsync( + packageIds, + dependencyInfo.Version, + versionResult, + projectFrameworks, + findLowestVersion: dependencyInfo.IsVulnerable, + nugetContext, + logger, + cancellationToken); + } + + internal static async Task FindUpdatedVersionAsync( + ImmutableHashSet packageIds, + ImmutableArray projectFrameworks, + NuGetVersion version, + bool findLowestVersion, + NuGetContext nugetContext, + Logger logger, + CancellationToken cancellationToken) + { + var versionResult = await VersionFinder.GetVersionsAsync( + packageIds.First(), + version, + nugetContext, + logger, + cancellationToken); + + return await FindUpdatedVersionAsync( + packageIds, + version.ToNormalizedString(), + versionResult, + projectFrameworks, + findLowestVersion, + nugetContext, + logger, + cancellationToken); + } + + internal static async Task FindUpdatedVersionAsync( + ImmutableHashSet packageIds, + string versionString, + VersionResult versionResult, + ImmutableArray projectFrameworks, + bool findLowestVersion, + NuGetContext nugetContext, + Logger logger, + CancellationToken cancellationToken) + { + var versions = versionResult.GetVersions(); + var orderedVersions = findLowestVersion + ? versions.OrderBy(v => v) // If we are fixing a vulnerability, then we want the lowest version that is safe. + : versions.OrderByDescending(v => v); // If we are just updating versions, then we want the highest version possible. + + return await FindFirstCompatibleVersion( + packageIds, + versionString, + versionResult, + orderedVersions, + projectFrameworks, + nugetContext, + logger, + cancellationToken); + } + + internal static async Task FindFirstCompatibleVersion( + ImmutableHashSet packageIds, + string versionString, + VersionResult versionResult, + IEnumerable orderedVersions, + ImmutableArray projectFrameworks, + NuGetContext nugetContext, + Logger logger, + CancellationToken cancellationToken) + { + if (NuGetVersion.TryParse(versionString, out var currentVersion)) + { + var isCompatible = await AreAllPackagesCompatibleAsync( + packageIds, + currentVersion, + projectFrameworks, + nugetContext, + logger, + cancellationToken); + + if (!isCompatible) + { + // If the current package is incompatible, then don't check for compatibility. + return orderedVersions.First(); + } + } + + foreach (var version in orderedVersions) + { + var existsForAll = await VersionFinder.DoVersionsExistAsync(packageIds, version, nugetContext, logger, cancellationToken); + if (!existsForAll) + { + continue; + } + + var isCompatible = await AreAllPackagesCompatibleAsync( + packageIds, + version, + projectFrameworks, + nugetContext, + logger, + cancellationToken); + + if (isCompatible) + { + return version; + } + } + + // Could not find a compatible version + return null; + } + + internal static async Task AreAllPackagesCompatibleAsync( + ImmutableHashSet packageIds, + NuGetVersion currentVersion, + ImmutableArray projectFrameworks, + NuGetContext nugetContext, + Logger logger, + CancellationToken cancellationToken) + { + foreach (var packageId in packageIds) + { + var isCompatible = await CompatibilityChecker.CheckAsync( + new(packageId, currentVersion), + projectFrameworks, + nugetContext, + logger, + cancellationToken); + if (!isCompatible) + { + return false; + } + } + + return true; + } + + internal static async Task>> GetDependenciesAsync( + string workspacePath, + string projectPath, + IEnumerable frameworks, + Dependency package, + Logger logger) + { + var result = ImmutableDictionary.CreateBuilder>(); + foreach (var framework in frameworks) + { + var dependencies = await MSBuildHelper.GetAllPackageDependenciesAsync( + workspacePath, + projectPath, + framework.ToString(), + [package], + logger); + result.Add(framework, [.. dependencies]); + } + return result.ToImmutable(); + } + + internal static async Task> FindUpdatedDependenciesAsync( + WorkspaceDiscoveryResult discovery, + ImmutableHashSet packageIds, + NuGetVersion updatedVersion, + NuGetContext nugetContext, + Logger logger, + CancellationToken cancellationToken) + { + // We need to find all projects which have the given dependency. Even in cases that they + // have it transitively may require that peer dependencies be updated in the project. + var projectsWithDependency = discovery.Projects + .Where(p => p.Dependencies.Any(d => packageIds.Contains(d.Name))) + .ToImmutableArray(); + if (projectsWithDependency.Length == 0) + { + return []; + } + + var projectFrameworks = projectsWithDependency + .SelectMany(p => p.TargetFrameworks) + .Distinct() + .Select(NuGetFramework.Parse) + .ToImmutableArray(); + + // When updating peer dependencies, we only need to consider top-level dependencies. + var projectDependencyNames = projectsWithDependency + .SelectMany(p => p.Dependencies) + .Where(d => !d.IsTransitive) + .Select(d => d.Name) + .ToImmutableHashSet(StringComparer.OrdinalIgnoreCase); + + // Determine updated peer dependencies + var workspacePath = discovery.Path; + // We need any project path so the dependency finder can locate the nuget.config + var projectPath = Path.Combine(workspacePath, projectsWithDependency.First().FilePath); + + // Create distinct list of dependencies taking the highest version of each + var dependencyResult = await DependencyFinder.GetDependenciesAsync( + workspacePath, + projectPath, + projectFrameworks, + packageIds, + updatedVersion, + logger); + + // Filter dependencies by whether any project references them + return dependencyResult.GetDependencies() + .Where(d => projectDependencyNames.Contains(d.Name)) + .ToImmutableArray(); + } + + internal static ImmutableArray DetermineMultiDependencyDetails( + WorkspaceDiscoveryResult discovery, + string packageId, + ImmutableArray propertyBasedDependencies) + { + var packageDeclarationsUsingProperty = discovery.Projects + .SelectMany(p => + p.Dependencies.Where(d => !d.IsTransitive && + d.Name.Equals(packageId, StringComparison.OrdinalIgnoreCase) && + d.EvaluationResult?.RootPropertyName is not null) + ).ToImmutableArray(); + + return packageDeclarationsUsingProperty + .Select(d => d.EvaluationResult!.RootPropertyName!) + .ToImmutableHashSet(StringComparer.OrdinalIgnoreCase) + .Select(property => + { + // Find all dependencies that use the same property + var packages = propertyBasedDependencies + .Where(d => property.Equals(d.EvaluationResult?.RootPropertyName, StringComparison.OrdinalIgnoreCase)); + + // Combine all their target frameworks + var tfms = packages.SelectMany(d => d.TargetFrameworks ?? []) + .Distinct() + .ToImmutableArray(); + + var packageIds = packages.Select(d => d.Name) + .ToImmutableHashSet(StringComparer.OrdinalIgnoreCase); + + return (property, tfms, packageIds); + }).ToImmutableArray(); + } + + internal static async Task WriteResultsAsync(string analysisDirectory, string dependencyName, AnalysisResult result, Logger logger) + { + if (!Directory.Exists(analysisDirectory)) + { + Directory.CreateDirectory(analysisDirectory); + } + + var resultPath = Path.Combine(analysisDirectory, $"{dependencyName}.json"); + + logger.Log($" Writing analysis result to [{resultPath}]."); + + var resultJson = JsonSerializer.Serialize(result, SerializerOptions); + await File.WriteAllTextAsync(path: resultPath, resultJson); + } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/CompatabilityChecker.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/CompatabilityChecker.cs new file mode 100644 index 00000000000..29aa2fa88ba --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/CompatabilityChecker.cs @@ -0,0 +1,177 @@ +using System.Collections.Immutable; + +using NuGet.Common; +using NuGet.Configuration; +using NuGet.Frameworks; +using NuGet.Packaging; +using NuGet.Packaging.Core; +using NuGet.Protocol; +using NuGet.Protocol.Core.Types; + +using NuGetUpdater.Core.FrameworkChecker; + +namespace NuGetUpdater.Core.Analyze; + +using PackageInfo = (bool IsDevDependency, ImmutableArray Frameworks); +using PackageReaders = (IAsyncPackageCoreReader CoreReader, IAsyncPackageContentReader ContentReader); + +internal static class CompatibilityChecker +{ + public static async Task CheckAsync( + PackageIdentity package, + ImmutableArray projectFrameworks, + NuGetContext nugetContext, + Logger logger, + CancellationToken cancellationToken) + { + var (isDevDependency, packageFrameworks) = await GetPackageInfoAsync( + package, + nugetContext, + cancellationToken); + + return PerformCheck(package, projectFrameworks, isDevDependency, packageFrameworks, logger); + } + + internal static bool PerformCheck( + PackageIdentity package, + ImmutableArray projectFrameworks, + bool isDevDependency, + ImmutableArray packageFrameworks, + Logger logger) + { + // development dependencies are packages such as analyzers which need to be compatible with the compiler not the + // project itself, but some packages that report themselves as development dependencies still contain target + // framework dependencies and should be checked for compatibility through the regular means + if (isDevDependency && packageFrameworks.Length == 0) + { + return true; + } + + if (packageFrameworks.Length == 0 || projectFrameworks.Length == 0) + { + return false; + } + + var compatibilityService = new FrameworkCompatibilityService(); + var compatibleFrameworks = compatibilityService.GetCompatibleFrameworks(packageFrameworks); + + var incompatibleFrameworks = projectFrameworks.Where(f => !compatibleFrameworks.Contains(f)).ToArray(); + if (incompatibleFrameworks.Length > 0) + { + logger.Log($"The package {package} is not compatible. Incompatible project frameworks: {string.Join(", ", incompatibleFrameworks.Select(f => f.GetShortFolderName()))}"); + return false; + } + + return true; + } + + internal static async Task GetPackageInfoAsync( + PackageIdentity package, + NuGetContext nugetContext, + CancellationToken cancellationToken) + { + var tempPackagePath = GetTempPackagePath(package, nugetContext); + var readers = File.Exists(tempPackagePath) + ? ReadPackage(tempPackagePath) + : await DownloadPackageAsync(package, nugetContext, cancellationToken); + + var nuspecStream = await readers.CoreReader.GetNuspecAsync(cancellationToken); + var reader = new NuspecReader(nuspecStream); + + var isDevDependency = reader.GetDevelopmentDependency(); + + var tfms = reader.GetDependencyGroups() + .Select(d => d.TargetFramework) + .ToImmutableArray(); + if (tfms.Length == 0) + { + // If the nuspec doesn't have any dependency groups, + // try to get the TargetFramework from files in the lib folder. + var libItems = (await readers.ContentReader.GetLibItemsAsync(cancellationToken)).ToList(); + if (libItems.Count == 0) + { + // If there is no lib folder in this package, then assume it is a dev dependency. + isDevDependency = true; + } + + tfms = libItems.Select(item => item.TargetFramework) + .Distinct() + .ToImmutableArray(); + } + + // The interfaces we given are not disposable but the underlying type can be. + // This will ensure we dispose of any resources that need to be cleaned up. + (readers.CoreReader as IDisposable)?.Dispose(); + (readers.ContentReader as IDisposable)?.Dispose(); + + return (isDevDependency, tfms); + } + + internal static PackageReaders ReadPackage(string tempPackagePath) + { + var stream = new FileStream( + tempPackagePath, + FileMode.Open, + FileAccess.Read, + FileShare.Read, + bufferSize: 4096); + PackageArchiveReader archiveReader = new(stream); + return (archiveReader, archiveReader); + } + + internal static async Task DownloadPackageAsync( + PackageIdentity package, + NuGetContext context, + CancellationToken cancellationToken) + { + var sourceMapping = PackageSourceMapping.GetPackageSourceMapping(context.Settings); + var packageSources = sourceMapping.GetConfiguredPackageSources(package.Id).ToHashSet(); + var sources = packageSources.Count == 0 + ? context.PackageSources + : context.PackageSources + .Where(p => packageSources.Contains(p.Name)) + .ToImmutableArray(); + + foreach (var source in sources) + { + var sourceRepository = Repository.Factory.GetCoreV3(source); + var feed = await sourceRepository.GetResourceAsync(); + if (feed is null) + { + throw new NotSupportedException($"Failed to get FindPackageByIdResource for {source.SourceUri}"); + } + + var exists = await feed.DoesPackageExistAsync( + package.Id, + package.Version, + context.SourceCacheContext, + NullLogger.Instance, + cancellationToken); + + if (!exists) + { + continue; + } + + var downloader = await feed.GetPackageDownloaderAsync( + package, + context.SourceCacheContext, + context.Logger, + cancellationToken); + + var tempPackagePath = GetTempPackagePath(package, context); + var isDownloaded = await downloader.CopyNupkgFileToAsync(tempPackagePath, cancellationToken); + if (!isDownloaded) + { + throw new Exception($"Failed to download package [{package.Id}/{package.Version}] from [${source.SourceUri}]"); + } + + return (downloader.CoreReader, downloader.ContentReader); + } + + throw new Exception($"Package [{package.Id}/{package.Version}] does not exist in any of the configured sources."); + } + + internal static string GetTempPackagePath(PackageIdentity package, NuGetContext context) + => Path.Combine(context.TempPackageDirectory, package.Id + "." + package.Version + ".nupkg"); +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/DependencyFinder.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/DependencyFinder.cs new file mode 100644 index 00000000000..966bad986b2 --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/DependencyFinder.cs @@ -0,0 +1,36 @@ +using System.Collections.Immutable; + +using NuGet.Frameworks; +using NuGet.Versioning; + +namespace NuGetUpdater.Core.Analyze; + +internal static class DependencyFinder +{ + public static async Task>> GetDependenciesAsync( + string workspacePath, + string projectPath, + IEnumerable frameworks, + ImmutableHashSet packageIds, + NuGetVersion version, + Logger logger) + { + var versionString = version.ToNormalizedString(); + var packages = packageIds + .Select(id => new Dependency(id, versionString, DependencyType.Unknown)) + .ToImmutableArray(); + + var result = ImmutableDictionary.CreateBuilder>(); + foreach (var framework in frameworks) + { + var dependencies = await MSBuildHelper.GetAllPackageDependenciesAsync( + workspacePath, + projectPath, + framework.ToString(), + packages, + logger); + result.Add(framework, [.. dependencies.Select(d => d with { IsTransitive = false })]); + } + return result.ToImmutable(); + } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/DependencyInfo.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/DependencyInfo.cs new file mode 100644 index 00000000000..e04b96e17f1 --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/DependencyInfo.cs @@ -0,0 +1,12 @@ +using System.Collections.Immutable; + +namespace NuGetUpdater.Core.Analyze; + +public sealed record DependencyInfo +{ + public required string Name { get; init; } + public required string Version { get; init; } + public required bool IsVulnerable { get; init; } + public ImmutableArray IgnoredVersions { get; init; } + public ImmutableArray Vulnerabilities { get; init; } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/Extensions.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/Extensions.cs new file mode 100644 index 00000000000..8089fd5e78f --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/Extensions.cs @@ -0,0 +1,36 @@ +using System.Collections.Immutable; + +using NuGet.Frameworks; +using NuGet.Versioning; + +using NuGetUpdater.Core; + +internal static class Extensions +{ + public static ImmutableArray GetDependencies(this ImmutableDictionary> dependenciesByTfm) + { + Dictionary dependencies = []; + foreach (var (_framework, dependenciesForTfm) in dependenciesByTfm) + { + foreach (var dependency in dependenciesForTfm) + { + if (dependencies.TryGetValue(dependency.Name, out Dependency? value)) + { + if (NuGetVersion.Parse(value.Version!) < NuGetVersion.Parse(dependency.Version!)) + { + dependencies[dependency.Name] = dependency with + { + TargetFrameworks = [.. value.TargetFrameworks ?? [], .. dependency.TargetFrameworks ?? []] + }; + } + } + else + { + dependencies.Add(dependency.Name, dependency); + } + } + } + + return [.. dependencies.Values]; + } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/NuGetContext.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/NuGetContext.cs new file mode 100644 index 00000000000..203176de2e1 --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/NuGetContext.cs @@ -0,0 +1,41 @@ +using System.Collections.Immutable; + +using NuGet.CommandLine; +using NuGet.Common; +using NuGet.Configuration; +using NuGet.Protocol.Core.Types; + +namespace NuGetUpdater.Core.Analyze; + +internal record NuGetContext : IDisposable +{ + public SourceCacheContext SourceCacheContext { get; } + public string CurrentDirectory { get; } + public ISettings Settings { get; } + public IMachineWideSettings MachineWideSettings { get; } + public ImmutableArray PackageSources { get; } + public ILogger Logger { get; } + public string TempPackageDirectory { get; } + + public NuGetContext(string? currentDirectory = null, ILogger? logger = null) + { + SourceCacheContext = new SourceCacheContext(); + CurrentDirectory = currentDirectory ?? Environment.CurrentDirectory; + MachineWideSettings = new CommandLineMachineWideSettings(); + Settings = NuGet.Configuration.Settings.LoadDefaultSettings( + CurrentDirectory, + configFileName: null, + MachineWideSettings); + var sourceProvider = new PackageSourceProvider(Settings); + PackageSources = sourceProvider.LoadPackageSources() + .Where(p => p.IsEnabled) + .ToImmutableArray(); + Logger = logger ?? NullLogger.Instance; + TempPackageDirectory = Path.Combine(Path.GetTempPath(), ".dependabot", "packages"); + } + + public void Dispose() + { + SourceCacheContext?.Dispose(); + } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/NuspecLocator.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/NuspecLocator.cs new file mode 100644 index 00000000000..c35bc0987c2 --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/NuspecLocator.cs @@ -0,0 +1,195 @@ +using System.Collections.Immutable; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.RegularExpressions; + +using NuGet.Common; +using NuGet.Configuration; +using NuGet.Packaging.Core; +using NuGet.Protocol; +using NuGet.Protocol.Core.Types; +using NuGet.Versioning; + +namespace NuGetUpdater.Core.Analyze; + +internal static partial class NuspecLocator +{ + internal static readonly ImmutableArray SupportedFeedRegexes = [ + // nuget + NuGetOrgRegex(), + // azure devops + AzureArtifactsOrgProjectRegex(), + AzureArtifactsOrgRegex(), + VisualStudioRegex(), + ]; + internal static readonly Dictionary BaseUrlCache = []; + internal static readonly JsonSerializerOptions JsonSerializerOptions = new() + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }; + + public static async Task LocateNuspecAsync( + string packageId, + NuGetVersion version, + NuGetContext nugetContext, + Logger logger, + CancellationToken cancellationToken) + { + var sourceMapping = PackageSourceMapping.GetPackageSourceMapping(nugetContext.Settings); + var packageSources = sourceMapping.GetConfiguredPackageSources(packageId).ToHashSet(); + var sources = packageSources.Count == 0 + ? nugetContext.PackageSources + : nugetContext.PackageSources + .Where(p => packageSources.Contains(p.Name)) + .ToImmutableArray(); + + foreach (var source in sources) + { + var nuspecUrl = await LocateNuspecAsync(source, packageId, version, nugetContext, logger, cancellationToken); + if (nuspecUrl is not null) + { + return nuspecUrl; + } + } + + return null; + } + + public static async Task LocateNuspecAsync( + PackageSource source, + string packageId, + NuGetVersion version, + NuGetContext nugetContext, + Logger logger, + CancellationToken cancellationToken) + { + var isSupported = DoesFeedSupportNuspecDownload(source); + if (!isSupported) + { + return null; + } + + var baseUrl = await GetBaseUrlAsync(source, nugetContext, cancellationToken); + if (baseUrl is null) + { + return null; + } + + var packageExists = await DoesPackageExistInFeedAsync( + source, + packageId, + version, + nugetContext, + logger, + cancellationToken); + if (!packageExists) + { + return null; + } + + return $"{baseUrl.TrimEnd('/')}/{packageId.ToLowerInvariant()}/{version.ToNormalizedString().ToLowerInvariant()}/{packageId.ToLowerInvariant()}.nuspec"; + } + + public static async Task GetBaseUrlAsync( + PackageSource source, + NuGetContext nugetContext, + CancellationToken cancellationToken) + { + if (BaseUrlCache.TryGetValue(source.SourceUri, out var baseUrl)) + { + return baseUrl; + } + + var sourceRepository = Repository.Factory.GetCoreV3(source); + var feed = await sourceRepository.GetResourceAsync(); + if (feed is null) + { + return null; + } + + var httpSourceCacheContext = HttpSourceCacheContext.Create(nugetContext.SourceCacheContext, isFirstAttempt: true); + var request = new HttpSourceCachedRequest(source.SourceUri.AbsoluteUri, "source_uri", httpSourceCacheContext); + var result = await feed.HttpSource.GetAsync( + request, + async result => + { + try + { + return await GetV3BaseUrlAsync(result.Stream); + } + catch (JsonException) + { + // V2 endpoint perhaps + return null; + } + }, + nugetContext.Logger, + cancellationToken); + + BaseUrlCache[source.SourceUri] = result; + + return result; + } + + internal static async Task GetV3BaseUrlAsync(Stream stream) + { + return (await JsonSerializer.DeserializeAsync(stream, JsonSerializerOptions)) + ?.Resources + .FirstOrDefault(r => r.Type == "PackageBaseAddress/3.0.0") + ?.Id; + } + + internal static bool DoesFeedSupportNuspecDownload(PackageSource source) + { + var feedUrl = source.SourceUri.AbsoluteUri; + return SupportedFeedRegexes.Any(r => r.IsMatch(feedUrl)); + } + + internal static async Task DoesPackageExistInFeedAsync( + PackageSource source, + string packageId, + NuGetVersion version, + NuGetContext nugetContext, + Logger logger, + CancellationToken cancellationToken) + { + var sourceRepository = Repository.Factory.GetCoreV3(source); + var feed = await sourceRepository.GetResourceAsync(); + if (feed is null) + { + logger.Log($"Failed to get MetadataResource for [{source.Source}]"); + return false; + } + + var existsInFeed = await feed.Exists( + new PackageIdentity(packageId, version), + includeUnlisted: false, + nugetContext.SourceCacheContext, + NullLogger.Instance, + cancellationToken); + + return existsInFeed; + } + + private class RepoMetadataResult + { + public required ResourceInfo[] Resources { get; set; } + } + + private class ResourceInfo + { + [JsonPropertyName("@type")] + public required string Type { get; set; } + [JsonPropertyName("@id")] + public required string Id { get; set; } + } + + [GeneratedRegex(@"https://api\.nuget\.org/v3/index\.json")] + private static partial Regex NuGetOrgRegex(); + [GeneratedRegex(@"https://pkgs\.dev\.azure\.com/(?[^/]+)/(?[^/]+)/_packaging/(?[^/]+)/nuget/v3/index\.json")] + private static partial Regex AzureArtifactsOrgProjectRegex(); + [GeneratedRegex(@"https://pkgs\.dev\.azure\.com/(?[^/]+)/_packaging/(?[^/]+)/nuget/v3/index\.json(?)")] + private static partial Regex AzureArtifactsOrgRegex(); + [GeneratedRegex(@"https://(?[^\.\/]+)\.pkgs\.visualstudio\.com/_packaging/(?[^/]+)/nuget/v3/index\.json(?)")] + private static partial Regex VisualStudioRegex(); +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/Requirement.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/Requirement.cs new file mode 100644 index 00000000000..63695e787c3 --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/Requirement.cs @@ -0,0 +1,105 @@ +using System.Collections.Immutable; + +using NuGet.Versioning; + +namespace NuGetUpdater.Core.Analyze; + +/// +/// A Requirement is a set of one or more version restrictions. It supports a +/// few (=, !=, >, <, >=, <=, ~>) different restriction operators. +/// +/// +/// See Gem::Version for a description on how versions and requirements work +/// together in RubyGems. +/// +public class Requirement +{ + private static readonly ImmutableDictionary> Operators = new Dictionary>() + { + ["="] = (v, r) => v == r, + ["!="] = (v, r) => v != r, + [">"] = (v, r) => v > r, + ["<"] = (v, r) => v < r, + [">="] = (v, r) => v >= r, + ["<="] = (v, r) => v <= r, + ["~>"] = (v, r) => v >= r && v.Version < Bump(r), + }.ToImmutableDictionary(); + + public static Requirement Parse(string requirement) + { + var splitIndex = requirement.LastIndexOfAny(['=', '>', '<']); + + // Throw if the requirement is all operator and no version. + if (splitIndex == requirement.Length - 1) + { + throw new ArgumentException($"`{requirement}` is a invalid requirement string", nameof(requirement)); + } + + string[] parts = splitIndex == -1 + ? [requirement.Trim()] + : [requirement[..(splitIndex + 1)].Trim(), requirement[(splitIndex + 1)..].Trim()]; + + var op = parts.Length == 1 ? "=" : parts[0]; + var version = NuGetVersion.Parse(parts[^1]); + + return new Requirement(op, version); + } + + public string Operator { get; } + public NuGetVersion Version { get; } + + public Requirement(string op, NuGetVersion version) + { + if (!Operators.ContainsKey(op)) + { + throw new ArgumentException("Invalid operator", nameof(op)); + } + + Operator = op; + Version = version; + } + + public override string ToString() + { + return $"{Operator} {Version}"; + } + + public bool IsSatisfiedBy(NuGetVersion version) + { + return Operators[Operator](version, Version); + } + + private static readonly Dictionary BumpMap = []; + /// + /// Return a new version object where the next to the last revision + /// number is one greater (e.g., 5.3.1 => 5.4). + /// + /// + /// This logic intended to be similar to RubyGems Gem::Version#bump + /// + public static Version Bump(NuGetVersion version) + { + if (BumpMap.TryGetValue(version.OriginalVersion!, out var bumpedVersion)) + { + return bumpedVersion; + } + + var versionParts = version.OriginalVersion! // Get the original string this version was created from + .Split('-')[0] // Get the version part without pre-release + .Split('.') // Split into Major.Minor.Patch.Revision if present + .Select(int.Parse) + .ToArray(); + + if (versionParts.Length > 1) + { + versionParts = versionParts[..^1]; // Remove the last part + } + + versionParts[^1]++; // Increment the new last part + + bumpedVersion = NuGetVersion.Parse(string.Join('.', versionParts)).Version; + BumpMap[version.OriginalVersion!] = bumpedVersion; + + return bumpedVersion; + } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/RequirementConverter.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/RequirementConverter.cs new file mode 100644 index 00000000000..361643f67fa --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/RequirementConverter.cs @@ -0,0 +1,17 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace NuGetUpdater.Core.Analyze; + +public class RequirementConverter : JsonConverter +{ + public override Requirement? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return Requirement.Parse(reader.GetString()!); + } + + public override void Write(Utf8JsonWriter writer, Requirement value, JsonSerializerOptions options) + { + writer.WriteStringValue(value.ToString()); + } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/SecurityVulnerability.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/SecurityVulnerability.cs new file mode 100644 index 00000000000..e2edb0541c1 --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/SecurityVulnerability.cs @@ -0,0 +1,11 @@ +using System.Collections.Immutable; + +namespace NuGetUpdater.Core.Analyze; + +public sealed record SecurityVulnerability +{ + public required string DependencyName { get; init; } + public required string PackageManager { get; init; } + public required ImmutableArray VulnerableVersions { get; init; } + public required ImmutableArray SafeVersions { get; init; } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/SecurityVulnerabilityExtensions.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/SecurityVulnerabilityExtensions.cs new file mode 100644 index 00000000000..3123e5ac55d --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/SecurityVulnerabilityExtensions.cs @@ -0,0 +1,36 @@ +using NuGet.Versioning; + +namespace NuGetUpdater.Core.Analyze; + +public static class SecurityVulnerabilityExtensions +{ + // This logic taken from Dependabot security_advisory.rb + public static bool IsVulnerable(this SecurityVulnerability vulnerability, NuGetVersion version) + { + var inSafeRange = vulnerability.SafeVersions + .Any(r => r.IsSatisfiedBy(version)); + if (inSafeRange) + { + // If version is known safe for this advisory, it's not vulnerable + return false; + } + + var inVulnerableRange = vulnerability.VulnerableVersions + .Any(r => r.IsSatisfiedBy(version)); + if (inVulnerableRange) + { + // If in the vulnerable range and not known safe, it's vulnerable + return true; + } + + if (vulnerability.VulnerableVersions.Length > 0) + { + // If a vulnerable range present but not met, it's not vulnerable + return false; + } + + // Finally, if no vulnerable range provided, but a safe range provided, + // and this versions isn't included (checked earlier), it's vulnerable + return vulnerability.SafeVersions.Length > 0; + } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/VersionFinder.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/VersionFinder.cs new file mode 100644 index 00000000000..09a8c72e95f --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/VersionFinder.cs @@ -0,0 +1,193 @@ +using System.Collections.Immutable; +using System.Text.Json; + +using NuGet.Common; +using NuGet.Configuration; +using NuGet.Packaging.Core; +using NuGet.Protocol; +using NuGet.Protocol.Core.Types; +using NuGet.Versioning; + +namespace NuGetUpdater.Core.Analyze; + +internal static class VersionFinder +{ + internal static readonly System.Text.Json.JsonSerializerOptions JsonSerializerOptions = CreateJsonSerializerOptions(); + + private static System.Text.Json.JsonSerializerOptions CreateJsonSerializerOptions() + { + var options = new System.Text.Json.JsonSerializerOptions() + { + PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase + }; + return options; + } + + public static Task GetVersionsAsync( + string packageId, + NuGetVersion currentVersion, + NuGetContext nugetContext, + Logger logger, + CancellationToken cancellationToken) + { + var versionFilter = CreateVersionFilter(currentVersion); + + return GetVersionsAsync(packageId, currentVersion, versionFilter, nugetContext, logger, cancellationToken); + } + + public static Task GetVersionsAsync( + DependencyInfo dependencyInfo, + NuGetContext nugetContext, + Logger logger, + CancellationToken cancellationToken) + { + var packageId = dependencyInfo.Name; + var versionRange = VersionRange.Parse(dependencyInfo.Version); + var currentVersion = versionRange.MinVersion!; + var versionFilter = CreateVersionFilter(dependencyInfo, versionRange); + + return GetVersionsAsync(packageId, currentVersion, versionFilter, nugetContext, logger, cancellationToken); + } + + public static async Task GetVersionsAsync( + string packageId, + NuGetVersion currentVersion, + Func versionFilter, + NuGetContext nugetContext, + Logger logger, + CancellationToken cancellationToken) + { + var url = await NuspecLocator.LocateNuspecAsync(packageId, currentVersion, nugetContext, logger, cancellationToken); + + var includePrerelease = currentVersion.IsPrerelease; + VersionResult result = new(currentVersion); + + var sourceMapping = PackageSourceMapping.GetPackageSourceMapping(nugetContext.Settings); + var packageSources = sourceMapping.GetConfiguredPackageSources(packageId).ToHashSet(); + var sources = packageSources.Count == 0 + ? nugetContext.PackageSources + : nugetContext.PackageSources + .Where(p => packageSources.Contains(p.Name)) + .ToImmutableArray(); + + foreach (var source in sources) + { + var sourceRepository = Repository.Factory.GetCoreV3(source); + var feed = await sourceRepository.GetResourceAsync(); + if (feed is null) + { + logger.Log($"Failed to get MetadataResource for [{source.Source}]"); + continue; + } + + var existsInFeed = await feed.Exists( + packageId, + includePrerelease, + includeUnlisted: false, + nugetContext.SourceCacheContext, + NullLogger.Instance, + cancellationToken); + if (!existsInFeed) + { + continue; + } + + var feedVersions = (await feed.GetVersions( + packageId, + includePrerelease, + includeUnlisted: false, + nugetContext.SourceCacheContext, + NullLogger.Instance, + CancellationToken.None)).ToHashSet(); + + if (feedVersions.Contains(currentVersion)) + { + result.AddCurrentVersionSource(source); + } + + result.AddRange(source, feedVersions.Where(versionFilter)); + } + + return result; + } + + internal static Func CreateVersionFilter(DependencyInfo dependencyInfo, VersionRange versionRange) + { + // If we are floating to the absolute latest version, we should not filter pre-release versions at all. + var currentVersion = versionRange.Float?.FloatBehavior != NuGetVersionFloatBehavior.AbsoluteLatest + ? versionRange.MinVersion + : null; + + return version => (currentVersion is null || version > currentVersion) + && versionRange.Satisfies(version) + && (currentVersion is null || !currentVersion.IsPrerelease || !version.IsPrerelease || version.Version == currentVersion.Version) + && !dependencyInfo.IgnoredVersions.Any(r => r.IsSatisfiedBy(version)) + && !dependencyInfo.Vulnerabilities.Any(v => v.IsVulnerable(version)); + } + + internal static Func CreateVersionFilter(NuGetVersion currentVersion) + { + return version => version > currentVersion + && (currentVersion is null || !currentVersion.IsPrerelease || !version.IsPrerelease || version.Version == currentVersion.Version); + } + + public static async Task DoVersionsExistAsync( + IEnumerable packageIds, + NuGetVersion version, + NuGetContext nugetContext, + Logger logger, + CancellationToken cancellationToken) + { + foreach (var packageId in packageIds) + { + if (!await DoesVersionExistAsync(packageId, version, nugetContext, logger, cancellationToken)) + { + return false; + } + } + + return true; + } + + public static async Task DoesVersionExistAsync( + string packageId, + NuGetVersion version, + NuGetContext nugetContext, + Logger logger, + CancellationToken cancellationToken) + { + var includePrerelease = version.IsPrerelease; + + var sourceMapping = PackageSourceMapping.GetPackageSourceMapping(nugetContext.Settings); + var packageSources = sourceMapping.GetConfiguredPackageSources(packageId).ToHashSet(); + var sources = packageSources.Count == 0 + ? nugetContext.PackageSources + : nugetContext.PackageSources + .Where(p => packageSources.Contains(p.Name)) + .ToImmutableArray(); + + foreach (var source in sources) + { + var sourceRepository = Repository.Factory.GetCoreV3(source); + var feed = await sourceRepository.GetResourceAsync(); + if (feed is null) + { + logger.Log($"Failed to get MetadataResource for [{source.Source}]"); + continue; + } + + var existsInFeed = await feed.Exists( + new PackageIdentity(packageId, version), + includeUnlisted: false, + nugetContext.SourceCacheContext, + NullLogger.Instance, + cancellationToken); + if (existsInFeed) + { + return true; + } + } + + return false; + } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/VersionResult.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/VersionResult.cs new file mode 100644 index 00000000000..2c7d304ec28 --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/VersionResult.cs @@ -0,0 +1,54 @@ +using System.Collections.Immutable; + +using NuGet.Configuration; +using NuGet.Versioning; + +namespace NuGetUpdater.Core.Analyze; + +internal class VersionResult +{ + private readonly Dictionary> _versions = []; + private readonly List _currentVersionSources = []; + + public NuGetVersion CurrentVersion { get; } + + public VersionResult(NuGetVersion currentVersion) + { + CurrentVersion = currentVersion; + } + + public void AddCurrentVersionSource(PackageSource source) + { + _currentVersionSources.Add(source); + } + + public void AddRange(PackageSource source, IEnumerable versions) + { + foreach (var version in versions) + { + if (_versions.ContainsKey(version)) + { + _versions[version].Add(source); + } + else + { + _versions.Add(version, [source]); + } + } + } + + public ImmutableArray GetPackageSources(NuGetVersion version) + { + if (version == CurrentVersion) + { + return [.. _currentVersionSources]; + } + + return [.. _versions[version]]; + } + + public ImmutableArray GetVersions() + { + return [.. _versions.Keys]; + } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DiscoveryWorker.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DiscoveryWorker.cs index adc4e59987f..733b0a32315 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DiscoveryWorker.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DiscoveryWorker.cs @@ -72,7 +72,7 @@ public async Task RunAsync(string repoRootPath, string workspacePath, string out var result = new WorkspaceDiscoveryResult { - FilePath = repoRootPath != workspacePath ? Path.GetRelativePath(repoRootPath, workspacePath) : string.Empty, + Path = workspacePath, DotNetToolsJson = dotNetToolsJsonDiscovery, GlobalJson = globalJsonDiscovery, DirectoryPackagesProps = directoryPackagesPropsDiscovery, diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/WorkspaceDiscoveryResult.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/WorkspaceDiscoveryResult.cs index cae5259bb4c..9f38552b529 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/WorkspaceDiscoveryResult.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/WorkspaceDiscoveryResult.cs @@ -2,9 +2,9 @@ namespace NuGetUpdater.Core.Discover; -public sealed record WorkspaceDiscoveryResult : IDiscoveryResult +public sealed record WorkspaceDiscoveryResult { - public required string FilePath { get; init; } + public required string Path { get; init; } public bool IsSuccess { get; init; } = true; public ImmutableArray Projects { get; init; } public DirectoryPackagesPropsDiscoveryResult? DirectoryPackagesProps { get; init; } diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/FrameworkChecker/CompatabilityChecker.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/FrameworkChecker/CompatabilityChecker.cs index e49f4c9faf3..7457bd20346 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/FrameworkChecker/CompatabilityChecker.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/FrameworkChecker/CompatabilityChecker.cs @@ -1,5 +1,3 @@ -using System.Linq; - using NuGet.Frameworks; namespace NuGetUpdater.Core.FrameworkChecker; diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/FrameworkChecker/FrameworkCompatibilityService.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/FrameworkChecker/FrameworkCompatibilityService.cs index e3e6642e159..349df7b7e6e 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/FrameworkChecker/FrameworkCompatibilityService.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/FrameworkChecker/FrameworkCompatibilityService.cs @@ -1,9 +1,6 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System; -using System.Collections.Generic; - using NuGet.Frameworks; using NuGetGallery.Frameworks; diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/FrameworkChecker/SupportedFrameworks.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/FrameworkChecker/SupportedFrameworks.cs index 9ded2bf4f2b..429fa041c07 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/FrameworkChecker/SupportedFrameworks.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/FrameworkChecker/SupportedFrameworks.cs @@ -1,9 +1,6 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System; -using System.Collections.Generic; - using NuGet.Frameworks; using static NuGet.Frameworks.FrameworkConstants; diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/BindingRedirectManager.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/BindingRedirectManager.cs index 81fde97a042..892d506ef8f 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/BindingRedirectManager.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/BindingRedirectManager.cs @@ -1,10 +1,5 @@ extern alias CoreV2; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading.Tasks; using System.Xml.Linq; using CoreV2::NuGet.Runtime; diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/BindingRedirectResolver.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/BindingRedirectResolver.cs index 79c57646861..29bdf75dbe2 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/BindingRedirectResolver.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/BindingRedirectResolver.cs @@ -1,10 +1,6 @@ extern alias CoreV2; -using System; -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.IO; -using System.Linq; using System.Reflection; using System.Text.RegularExpressions; diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/PackagesConfigUpdater.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/PackagesConfigUpdater.cs index 237579ec9c9..8b036d82289 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/PackagesConfigUpdater.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/PackagesConfigUpdater.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; using System.Text; -using System.Threading.Tasks; using Microsoft.Language.Xml; diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/WebApplicationTargetsConditionPatcher.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/WebApplicationTargetsConditionPatcher.cs index 8468cdb8c56..5d7cce01a8c 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/WebApplicationTargetsConditionPatcher.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/WebApplicationTargetsConditionPatcher.cs @@ -1,7 +1,3 @@ -using System; -using System.IO; -using System.Linq; - using Microsoft.Language.Xml; namespace NuGetUpdater.Core.Updater diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/XmlFilePreAndPostProcessor.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/XmlFilePreAndPostProcessor.cs index 5b9af19eda4..39423fb04c7 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/XmlFilePreAndPostProcessor.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/XmlFilePreAndPostProcessor.cs @@ -1,5 +1,3 @@ -using System; - using Microsoft.Language.Xml; namespace NuGetUpdater.Core.Updater diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/HashSetExtensions.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/HashSetExtensions.cs index 41e8051a030..7dc0d277fca 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/HashSetExtensions.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/HashSetExtensions.cs @@ -1,5 +1,3 @@ -using System.Collections.Immutable; - namespace NuGetUpdater.Core.Utilities; public static class HashSetExtensions diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/JsonHelper.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/JsonHelper.cs index 4e9527a2f1a..a31d8876f36 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/JsonHelper.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/JsonHelper.cs @@ -1,7 +1,3 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; using System.Runtime.InteropServices; using System.Text; using System.Text.Encodings.Web; diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/Logger.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/Logger.cs index a658bbdfe6c..f4f8eddf3f8 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/Logger.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/Logger.cs @@ -1,6 +1,3 @@ -using System; -using System.IO; - namespace NuGetUpdater.Core; public sealed class Logger diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/MSBuildHelper.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/MSBuildHelper.cs index 2c658021ac6..30d3335b9df 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/MSBuildHelper.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/MSBuildHelper.cs @@ -538,6 +538,8 @@ private static ProjectRootElement CreateProjectRootElement(ProjectBuildFile buil true + + false """); diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/PathHelper.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/PathHelper.cs index 88380e31d02..bec844c1497 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/PathHelper.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/PathHelper.cs @@ -1,7 +1,3 @@ -using System.Collections.Generic; -using System.IO; -using System.Linq; - namespace NuGetUpdater.Core; internal static class PathHelper diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/ProcessExtensions.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/ProcessExtensions.cs index c7b5da87580..7fd5172e18a 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/ProcessExtensions.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/ProcessExtensions.cs @@ -1,8 +1,5 @@ -using System; using System.Diagnostics; using System.Text; -using System.Threading; -using System.Threading.Tasks; namespace NuGetUpdater.Core; diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/XmlExtensions.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/XmlExtensions.cs index 1a38affa7d7..a747bf54a57 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/XmlExtensions.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/XmlExtensions.cs @@ -1,7 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; - using Microsoft.Language.Xml; namespace NuGetUpdater.Core; diff --git a/nuget/lib/dependabot/nuget/analysis/analysis_json_reader.rb b/nuget/lib/dependabot/nuget/analysis/analysis_json_reader.rb new file mode 100644 index 00000000000..5ceba566fa8 --- /dev/null +++ b/nuget/lib/dependabot/nuget/analysis/analysis_json_reader.rb @@ -0,0 +1,63 @@ +# typed: strong +# frozen_string_literal: true + +require "dependabot/dependency" +require "dependabot/nuget/analysis/dependency_analysis" +require "dependabot/nuget/discovery/discovery_json_reader" +require "json" +require "sorbet-runtime" + +module Dependabot + module Nuget + class AnalysisJsonReader + extend T::Sig + + sig { returns(String) } + def self.temp_directory + File.join(DiscoveryJsonReader.temp_directory, "analysis") + end + + sig { params(dependency_name: String).returns(String) } + def self.analysis_file_path(dependency_name:) + File.join(temp_directory, "#{dependency_name}.json") + end + + sig { params(dependency_name: String).returns(T.nilable(DependencyFile)) } + def self.analysis_json(dependency_name:) + file_path = analysis_file_path(dependency_name: dependency_name) + return unless File.exist?(file_path) + + DependencyFile.new( + name: Pathname.new(file_path).cleanpath.to_path, + directory: temp_directory, + type: "file", + content: File.read(file_path) + ) + end + + sig { params(analysis_json: DependencyFile).void } + def initialize(analysis_json:) + @analysis_json = analysis_json + end + + sig { returns(DependencyAnalysis) } + def dependency_analysis + @dependency_analysis ||= T.let(begin + raise Dependabot::DependencyFileNotParseable, analysis_json.path unless analysis_json.content + + Dependabot.logger.info("#{File.basename(analysis_json.path)} analysis content: #{analysis_json.content}") + + parsed_json = T.let(JSON.parse(T.must(analysis_json.content)), T::Hash[String, T.untyped]) + DependencyAnalysis.from_json(parsed_json) + end, T.nilable(DependencyAnalysis)) + rescue JSON::ParserError + raise Dependabot::DependencyFileNotParseable, analysis_json.path + end + + private + + sig { returns(DependencyFile) } + attr_reader :analysis_json + end + end +end diff --git a/nuget/lib/dependabot/nuget/analysis/dependency_analysis.rb b/nuget/lib/dependabot/nuget/analysis/dependency_analysis.rb new file mode 100644 index 00000000000..78f682653a9 --- /dev/null +++ b/nuget/lib/dependabot/nuget/analysis/dependency_analysis.rb @@ -0,0 +1,63 @@ +# typed: strong +# frozen_string_literal: true + +require "dependabot/nuget/version" +require "sorbet-runtime" + +module Dependabot + module Nuget + class DependencyAnalysis + extend T::Sig + + sig { params(json: T::Hash[String, T.untyped]).returns(DependencyAnalysis) } + def self.from_json(json) + updated_version = T.let(json.fetch("UpdatedVersion"), String) + can_update = T.let(json.fetch("CanUpdate"), T::Boolean) + version_comes_from_multi_dependency_property = T.let(json.fetch("VersionComesFromMultiDependencyProperty"), + T::Boolean) + updated_dependencies = T.let(json.fetch("UpdatedDependencies"), + T::Array[T::Hash[String, T.untyped]]).map do |dep| + DependencyDetails.from_json(dep) + end + + DependencyAnalysis.new( + updated_version: updated_version, + can_update: can_update, + version_comes_from_multi_dependency_property: version_comes_from_multi_dependency_property, + updated_dependencies: updated_dependencies + ) + end + + sig do + params(updated_version: String, + can_update: T::Boolean, + version_comes_from_multi_dependency_property: T::Boolean, + updated_dependencies: T::Array[DependencyDetails]).void + end + def initialize(updated_version:, can_update:, version_comes_from_multi_dependency_property:, + updated_dependencies:) + @updated_version = updated_version + @can_update = can_update + @version_comes_from_multi_dependency_property = version_comes_from_multi_dependency_property + @updated_dependencies = updated_dependencies + end + + sig { returns(String) } + attr_reader :updated_version + + sig { returns(T::Boolean) } + attr_reader :can_update + + sig { returns(T::Boolean) } + attr_reader :version_comes_from_multi_dependency_property + + sig { returns(T::Array[DependencyDetails]) } + attr_reader :updated_dependencies + + sig { returns(Dependabot::Nuget::Version) } + def numeric_updated_version + @numeric_updated_version ||= T.let(Version.new(updated_version), T.nilable(Dependabot::Nuget::Version)) + end + end + end +end diff --git a/nuget/lib/dependabot/nuget/discovery/discovery_json_reader.rb b/nuget/lib/dependabot/nuget/discovery/discovery_json_reader.rb index b1926fe590c..d38f38aeac8 100644 --- a/nuget/lib/dependabot/nuget/discovery/discovery_json_reader.rb +++ b/nuget/lib/dependabot/nuget/discovery/discovery_json_reader.rb @@ -11,16 +11,16 @@ module Nuget class DiscoveryJsonReader extend T::Sig - DISCOVERY_JSON_PATH = ".dependabot/discovery.json" + DISCOVERY_JSON_FILENAME = "discovery.json" sig { returns(String) } - private_class_method def self.temp_directory - Dir.tmpdir + def self.temp_directory + File.join(Dir.tmpdir, ".dependabot") end sig { returns(String) } def self.discovery_file_path - File.join(temp_directory, DISCOVERY_JSON_PATH) + File.join(temp_directory, DISCOVERY_JSON_FILENAME) end sig { returns(T.nilable(DependencyFile)) } diff --git a/nuget/lib/dependabot/nuget/discovery/workspace_discovery.rb b/nuget/lib/dependabot/nuget/discovery/workspace_discovery.rb index 29827953c74..f07b876dc8f 100644 --- a/nuget/lib/dependabot/nuget/discovery/workspace_discovery.rb +++ b/nuget/lib/dependabot/nuget/discovery/workspace_discovery.rb @@ -13,7 +13,7 @@ class WorkspaceDiscovery sig { params(json: T::Hash[String, T.untyped]).returns(WorkspaceDiscovery) } def self.from_json(json) - file_path = T.let(json.fetch("FilePath"), String) + path = T.let(json.fetch("Path"), String) projects = T.let(json.fetch("Projects"), T::Array[T::Hash[String, T.untyped]]).filter_map do |project| ProjectDiscovery.from_json(project) end @@ -25,7 +25,7 @@ def self.from_json(json) dotnet_tools_json = DependencyFileDiscovery .from_json(T.let(json.fetch("DotNetToolsJson"), T.nilable(T::Hash[String, T.untyped]))) - WorkspaceDiscovery.new(file_path: file_path, + WorkspaceDiscovery.new(path: path, projects: projects, directory_packages_props: directory_packages_props, global_json: global_json, @@ -33,14 +33,14 @@ def self.from_json(json) end sig do - params(file_path: String, + params(path: String, projects: T::Array[ProjectDiscovery], directory_packages_props: T.nilable(DirectoryPackagesPropsDiscovery), global_json: T.nilable(DependencyFileDiscovery), dotnet_tools_json: T.nilable(DependencyFileDiscovery)).void end - def initialize(file_path:, projects:, directory_packages_props:, global_json:, dotnet_tools_json:) - @file_path = file_path + def initialize(path:, projects:, directory_packages_props:, global_json:, dotnet_tools_json:) + @path = path @projects = projects @directory_packages_props = directory_packages_props @global_json = global_json @@ -48,7 +48,7 @@ def initialize(file_path:, projects:, directory_packages_props:, global_json:, d end sig { returns(String) } - attr_reader :file_path + attr_reader :path sig { returns(T::Array[ProjectDiscovery]) } attr_reader :projects diff --git a/nuget/lib/dependabot/nuget/http_response_helpers.rb b/nuget/lib/dependabot/nuget/http_response_helpers.rb deleted file mode 100644 index 2e751f985e8..00000000000 --- a/nuget/lib/dependabot/nuget/http_response_helpers.rb +++ /dev/null @@ -1,19 +0,0 @@ -# typed: strict -# frozen_string_literal: true - -require "sorbet-runtime" - -module Dependabot - module Nuget - module HttpResponseHelpers - extend T::Sig - - sig { params(string: String).returns(String) } - def self.remove_wrapping_zero_width_chars(string) - string.force_encoding("UTF-8").encode - .gsub(/\A[\u200B-\u200D\uFEFF]/, "") - .gsub(/[\u200B-\u200D\uFEFF]\Z/, "") - end - end - end -end diff --git a/nuget/lib/dependabot/nuget/native_helpers.rb b/nuget/lib/dependabot/nuget/native_helpers.rb index 4047feb226d..a201e66f3ae 100644 --- a/nuget/lib/dependabot/nuget/native_helpers.rb +++ b/nuget/lib/dependabot/nuget/native_helpers.rb @@ -110,6 +110,62 @@ def self.run_nuget_discover_tool(repo_root:, workspace_path:, output_path:, cred end end + sig do + params(discovery_file_path: String, dependency_file_path: String, + analysis_folder_path: String).returns([String, String]) + end + def self.get_nuget_analyze_tool_command(discovery_file_path:, dependency_file_path:, + analysis_folder_path:) + exe_path = File.join(native_helpers_root, "NuGetUpdater", "NuGetUpdater.Cli") + command_parts = [ + exe_path, + "analyze", + "--discovery-file-path", + discovery_file_path, + "--dependency-file-path", + dependency_file_path, + "--analysis-folder-path", + analysis_folder_path, + "--verbose" + ].compact + + command = Shellwords.join(command_parts) + + fingerprint = [ + exe_path, + "analyze", + "--discovery-file-path", + "", + "--dependency-file-path", + "", + "--analysis-folder-path", + "", + "--verbose" + ].compact.join(" ") + + [command, fingerprint] + end + + sig do + params( + discovery_file_path: String, dependency_file_path: String, + analysis_folder_path: String, credentials: T::Array[Dependabot::Credential] + ).void + end + def self.run_nuget_analyze_tool(discovery_file_path:, dependency_file_path:, + analysis_folder_path:, credentials:) + (command, fingerprint) = get_nuget_analyze_tool_command(discovery_file_path: discovery_file_path, + dependency_file_path: dependency_file_path, + analysis_folder_path: analysis_folder_path) + + puts "running NuGet analyze:\n" + command + + NuGetConfigCredentialHelpers.patch_nuget_config_for_action(credentials) do + output = SharedHelpers.run_shell_command(command, allow_unsafe_shell_command: true, fingerprint: fingerprint) + puts output + end + end + sig do params(repo_root: String, proj_path: String, dependency: Dependency, is_transitive: T::Boolean).returns([String, String]) diff --git a/nuget/lib/dependabot/nuget/nuget_client.rb b/nuget/lib/dependabot/nuget/nuget_client.rb deleted file mode 100644 index 6fe1dbea41e..00000000000 --- a/nuget/lib/dependabot/nuget/nuget_client.rb +++ /dev/null @@ -1,223 +0,0 @@ -# typed: strict -# frozen_string_literal: true - -require "dependabot/nuget/cache_manager" -require "dependabot/nuget/http_response_helpers" -require "dependabot/nuget/update_checker/repository_finder" -require "sorbet-runtime" - -module Dependabot - module Nuget - class NugetClient - extend T::Sig - - sig do - params(dependency_name: String, repository_details: T::Hash[Symbol, String]) - .returns(T.nilable(T::Set[String])) - end - def self.get_package_versions(dependency_name, repository_details) - repository_type = repository_details.fetch(:repository_type) - if repository_type == "v3" - get_package_versions_v3(dependency_name, repository_details) - elsif repository_type == "v2" - get_package_versions_v2(dependency_name, repository_details) - elsif repository_type == "local" - get_package_versions_local(dependency_name, repository_details) - else - raise "Unknown repository type: #{repository_type}" - end - end - - sig do - params(dependency_name: String, repository_details: T::Hash[Symbol, String]) - .returns(T.nilable(T::Set[String])) - end - private_class_method def self.get_package_versions_local(dependency_name, repository_details) - url = repository_details.fetch(:base_url) - raise "Local repo #{url} doesn't exist or isn't a directory" unless File.exist?(url) && File.directory?(url) - - package_dir = File.join(url, dependency_name) - - versions = Set.new - return versions unless File.exist?(package_dir) && File.directory?(package_dir) - - Dir.each_child(package_dir) do |child| - versions.add(child) if File.directory?(File.join(package_dir, child)) - end - - versions - end - - sig do - params(dependency_name: String, repository_details: T::Hash[Symbol, String]) - .returns(T.nilable(T::Set[String])) - end - private_class_method def self.get_package_versions_v3(dependency_name, repository_details) - # Use the registration URL if possible because it is fast and correct - if repository_details[:registration_url] - get_versions_from_registration_v3(repository_details) - # use the search API if not because it is slow but correct - elsif repository_details[:search_url] - get_versions_from_search_url_v3(repository_details, dependency_name) - # Otherwise, use the versions URL (fast but wrong because it includes unlisted versions) - elsif repository_details[:versions_url] - get_versions_from_versions_url_v3(repository_details) - else - raise "No version sources were available for #{dependency_name} in #{repository_details}" - end - end - - sig do - params(dependency_name: String, repository_details: T::Hash[Symbol, String]) - .returns(T.nilable(T::Set[String])) - end - private_class_method def self.get_package_versions_v2(dependency_name, repository_details) - doc = execute_xml_nuget_request(repository_details.fetch(:versions_url), repository_details) - return unless doc - - # v2 APIs can differ, but all tested have this title value set to the name of the package - title_nodes = doc.xpath("/feed/entry/title") - matching_versions = Set.new - title_nodes.each do |title_node| - return nil unless title_node.text - - next unless title_node.text.casecmp?(dependency_name) - - version_node = title_node.parent.xpath("properties/Version") - matching_versions << version_node.text if version_node && version_node.text - end - - matching_versions - end - - sig { params(repository_details: T::Hash[Symbol, String]).returns(T.nilable(T::Set[String])) } - private_class_method def self.get_versions_from_versions_url_v3(repository_details) - body = execute_json_nuget_request(repository_details.fetch(:versions_url), repository_details) - ver_array = T.let(body&.fetch("versions"), T.nilable(T::Array[String])) - ver_array&.to_set - end - - sig { params(repository_details: T::Hash[Symbol, String]).returns(T.nilable(T::Set[String])) } - private_class_method def self.get_versions_from_registration_v3(repository_details) - url = repository_details.fetch(:registration_url) - body = execute_json_nuget_request(url, repository_details) - - return unless body - - pages = body.fetch("items") - versions = T.let(Set.new, T::Set[String]) - pages.each do |page| - items = page["items"] - if items - # inlined entries - get_versions_from_inline_page(items, versions) - else - # paged entries - page_url = page["@id"] - page_body = execute_json_nuget_request(page_url, repository_details) - next unless page_body - - items = page_body.fetch("items") - items.each do |item| - catalog_entry = item.fetch("catalogEntry") - versions << catalog_entry.fetch("version") if catalog_entry["listed"] == true - end - end - end - - versions - end - - sig { params(items: T::Array[T::Hash[String, T.untyped]], versions: T::Set[String]).void } - private_class_method def self.get_versions_from_inline_page(items, versions) - items.each do |item| - catalog_entry = item["catalogEntry"] - - # a package is considered listed if the `listed` property is either `true` or missing - listed_property = catalog_entry["listed"] - is_listed = listed_property.nil? || listed_property == true - if is_listed - vers = catalog_entry["version"] - versions << vers - end - end - end - - sig do - params(repository_details: T::Hash[Symbol, String], dependency_name: String) - .returns(T.nilable(T::Set[String])) - end - private_class_method def self.get_versions_from_search_url_v3(repository_details, dependency_name) - search_url = repository_details.fetch(:search_url) - body = execute_json_nuget_request(search_url, repository_details) - - body&.fetch("data") - &.find { |d| d.fetch("id").casecmp(dependency_name.downcase).zero? } - &.fetch("versions") - &.map { |d| d.fetch("version") } - &.to_set - end - - sig do - params(url: String, repository_details: T::Hash[Symbol, T.untyped]).returns(T.nilable(Nokogiri::XML::Document)) - end - private_class_method def self.execute_xml_nuget_request(url, repository_details) - response = execute_nuget_request_internal( - url: url, - auth_header: repository_details.fetch(:auth_header), - repository_url: repository_details.fetch(:repository_url) - ) - return unless response.status == 200 - - doc = Nokogiri::XML(response.body) - doc.remove_namespaces! - doc - end - - sig do - params(url: String, - repository_details: T::Hash[Symbol, T.untyped]) - .returns(T.nilable(T::Hash[T.untyped, T.untyped])) - end - private_class_method def self.execute_json_nuget_request(url, repository_details) - response = execute_nuget_request_internal( - url: url, - auth_header: repository_details.fetch(:auth_header), - repository_url: repository_details.fetch(:repository_url) - ) - return unless response.status == 200 - - body = HttpResponseHelpers.remove_wrapping_zero_width_chars(response.body) - JSON.parse(body) - end - - sig do - params(url: String, auth_header: T::Hash[Symbol, T.untyped], repository_url: String).returns(Excon::Response) - end - private_class_method def self.execute_nuget_request_internal(url:, auth_header:, repository_url:) - cache = CacheManager.cache("dependency_url_search_cache") - if cache[url].nil? - response = Dependabot::RegistryClient.get( - url: url, - headers: auth_header - ) - - if [401, 402, 403].include?(response.status) - raise Dependabot::PrivateSourceAuthenticationFailure, repository_url - end - - cache[url] = response if !CacheManager.caching_disabled? && response.status == 200 - else - response = cache[url] - end - - response - rescue Excon::Error::Timeout, Excon::Error::Socket - repo_url = repository_url - raise if repo_url == Dependabot::Nuget::RepositoryFinder::DEFAULT_REPOSITORY_URL - - raise PrivateSourceTimedOut, repo_url - end - end - end -end diff --git a/nuget/lib/dependabot/nuget/nuget_config_credential_helpers.rb b/nuget/lib/dependabot/nuget/nuget_config_credential_helpers.rb index c1ac11f2a02..ad8eee731b2 100644 --- a/nuget/lib/dependabot/nuget/nuget_config_credential_helpers.rb +++ b/nuget/lib/dependabot/nuget/nuget_config_credential_helpers.rb @@ -69,12 +69,13 @@ def self.patch_nuget_config_for_action(credentials, &_block) begin yield rescue StandardError => e - Dependabot.logger.error( + log_message = <<~LOG_MESSAGE Block argument of NuGetConfigCredentialHelpers::patch_nuget_config_for_action causes an exception #{e}: #{e.message} LOG_MESSAGE - ) + Dependabot.logger.error(log_message) + puts log_message ensure restore_user_nuget_config end diff --git a/nuget/lib/dependabot/nuget/update_checker.rb b/nuget/lib/dependabot/nuget/update_checker.rb index a6e4f49e55a..9158dc17b9b 100644 --- a/nuget/lib/dependabot/nuget/update_checker.rb +++ b/nuget/lib/dependabot/nuget/update_checker.rb @@ -1,7 +1,8 @@ -# typed: strict +# typed: strong # frozen_string_literal: true -require "dependabot/nuget/file_parser" +require "dependabot/nuget/analysis/analysis_json_reader" +require "dependabot/nuget/discovery/discovery_json_reader" require "dependabot/update_checkers" require "dependabot/update_checkers/base" require "sorbet-runtime" @@ -11,12 +12,7 @@ module Nuget class UpdateChecker < Dependabot::UpdateCheckers::Base extend T::Sig - require_relative "update_checker/version_finder" - require_relative "update_checker/property_updater" require_relative "update_checker/requirements_updater" - require_relative "update_checker/dependency_finder" - - PROPERTY_REGEX = /\$\((?.*?)\)/ sig { override.returns(T.nilable(String)) } def latest_version @@ -25,7 +21,7 @@ def latest_version # if no update sources have the requisite package, then we can only assume that the current version is correct @latest_version = T.let( - latest_version_details&.fetch(:version)&.to_s || dependency.version, + update_analysis.dependency_analysis.updated_version, T.nilable(String) ) end @@ -39,14 +35,14 @@ def latest_resolvable_version sig { override.returns(Dependabot::Nuget::Version) } def lowest_security_fix_version - lowest_security_fix_version_details&.fetch(:version) + update_analysis.dependency_analysis.numeric_updated_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 + update_analysis.dependency_analysis.numeric_updated_version end sig { override.returns(NilClass) } @@ -59,175 +55,118 @@ def latest_resolvable_version_with_no_unlock def updated_requirements RequirementsUpdater.new( requirements: dependency.requirements, - latest_version: preferred_resolvable_version_details&.fetch(:version, nil)&.to_s, - source_details: preferred_resolvable_version_details&.slice(:nuspec_url, :repo_url, :source_url) + latest_version: update_analysis.dependency_analysis.updated_version ).updated_requirements end sig { returns(T::Boolean) } def up_to_date? - # No need to update transitive dependencies unless they have a vulnerability. - return true if !dependency.top_level? && !vulnerable? - - # If any requirements have an uninterpolated property in them then - # that property couldn't be found, and we assume that the dependency - # is up-to-date - return true unless requirements_unlocked_or_can_be? - - super + !update_analysis.dependency_analysis.can_update end sig { returns(T::Boolean) } def requirements_unlocked_or_can_be? - # If any requirements have an uninterpolated property in them then - # that property couldn't be found, and the requirement therefore - # cannot be unlocked (since we can't update that property) - dependency.requirements.none? do |req| - req.fetch(:requirement)&.match?(PROPERTY_REGEX) - end + update_analysis.dependency_analysis.can_update end private - sig { returns(T.nilable(T::Hash[Symbol, T.untyped])) } - def preferred_resolvable_version_details - # If this dependency is vulnerable, prefer trying to update to the - # lowest_resolvable_security_fix_version. Otherwise update all the way - # to the latest_resolvable_version. - return lowest_security_fix_version_details if vulnerable? + sig { returns(AnalysisJsonReader) } + def update_analysis + @update_analysis ||= T.let(request_analysis, T.nilable(AnalysisJsonReader)) + end - latest_version_details + sig { returns(String) } + def dependency_file_path + File.join(DiscoveryJsonReader.temp_directory, "dependency", "#{dependency.name}.json") end - sig { override.returns(T::Boolean) } - def latest_version_resolvable_with_full_unlock? - # We always want a full unlock since any package update could update peer dependencies as well. - return true unless version_comes_from_multi_dependency_property? + sig { returns(AnalysisJsonReader) } + def request_analysis + discovery_file_path = DiscoveryJsonReader.discovery_file_path + analysis_folder_path = AnalysisJsonReader.temp_directory - property_updater.update_possible? - end + write_dependency_info - sig { override.returns(T::Array[Dependabot::Dependency]) } - def updated_dependencies_after_full_unlock - return property_updater.updated_dependencies if version_comes_from_multi_dependency_property? + NativeHelpers.run_nuget_analyze_tool(discovery_file_path: discovery_file_path, + dependency_file_path: dependency_file_path, + analysis_folder_path: analysis_folder_path, + credentials: credentials) - puts "Finding updated dependencies for #{dependency.name}." + analysis_json = AnalysisJsonReader.analysis_json(dependency_name: dependency.name) - updated_dependency = Dependency.new( - name: dependency.name, - version: latest_version, - requirements: updated_requirements, - previous_version: dependency.version, - previous_requirements: dependency.requirements, - package_manager: dependency.package_manager - ) - updated_dependencies = [updated_dependency] - updated_dependencies += DependencyFinder.new( - dependency: updated_dependency, - dependency_files: dependency_files, - credentials: credentials, - repo_contents_path: @repo_contents_path - ).updated_peer_dependencies - updated_dependencies + AnalysisJsonReader.new(analysis_json: T.must(analysis_json)) end - sig { returns(T.nilable(T::Hash[Symbol, T.untyped])) } - def preferred_version_details - return lowest_security_fix_version_details if vulnerable? + sig { void } + def write_dependency_info + dependency_info = { + Name: dependency.name, + Version: dependency.version.to_s, + IsVulnerable: vulnerable?, + IgnoredVersions: ignored_versions, + Vulnerabilities: security_advisories.map do |vulnerability| + { + DependencyName: vulnerability.dependency_name, + PackageManager: vulnerability.package_manager, + VulnerableVersions: vulnerability.vulnerable_versions.map(&:to_s), + SafeVersions: vulnerability.safe_versions.map(&:to_s) + } + end + }.to_json + dependency_directory = File.dirname(dependency_file_path) - latest_version_details - end + begin + Dir.mkdir(dependency_directory) + rescue StandardError + nil? + end - sig { returns(T.nilable(T::Hash[Symbol, T.untyped])) } - def latest_version_details - @latest_version_details ||= - T.let( - version_finder.latest_version_details, - T.nilable(T::Hash[Symbol, T.untyped]) - ) + File.write(dependency_file_path, dependency_info) end - sig { returns(T.nilable(T::Hash[Symbol, T.untyped])) } - def lowest_security_fix_version_details - @lowest_security_fix_version_details ||= - T.let( - version_finder.lowest_security_fix_version_details, - T.nilable(T::Hash[Symbol, T.untyped]) - ) + sig { returns(Dependabot::FileParsers::Base::DependencySet) } + def discovered_dependencies + discovery_json = DiscoveryJsonReader.discovery_json + return Dependabot::FileParsers::Base::DependencySet.new unless discovery_json + + DiscoveryJsonReader.new( + discovery_json: discovery_json + ).dependency_set end - sig { returns(Dependabot::Nuget::UpdateChecker::VersionFinder) } - def version_finder - @version_finder ||= - T.let( - VersionFinder.new( - dependency: dependency, - dependency_files: dependency_files, - credentials: credentials, - ignored_versions: ignored_versions, - raise_on_ignored: @raise_on_ignored, - security_advisories: security_advisories, - repo_contents_path: @repo_contents_path - ), - T.nilable(Dependabot::Nuget::UpdateChecker::VersionFinder) - ) + sig { override.returns(T::Boolean) } + def latest_version_resolvable_with_full_unlock? + # We always want a full unlock since any package update could update peer dependencies as well. + true end - sig { returns(Dependabot::Nuget::UpdateChecker::PropertyUpdater) } - def property_updater - @property_updater ||= - T.let( - PropertyUpdater.new( - dependency: dependency, - dependency_files: dependency_files, - target_version_details: latest_version_details, - credentials: credentials, - ignored_versions: ignored_versions, - raise_on_ignored: @raise_on_ignored, - repo_contents_path: @repo_contents_path - ), - T.nilable(Dependabot::Nuget::UpdateChecker::PropertyUpdater) + sig { override.returns(T::Array[Dependabot::Dependency]) } + def updated_dependencies_after_full_unlock + dependencies = discovered_dependencies.dependencies + update_analysis.dependency_analysis.updated_dependencies.filter_map do |dependency_details| + dep = dependencies.find { |d| d.name.casecmp(dependency_details.name)&.zero? } + next unless dep + + metadata = {} + # For peer dependencies, instruct updater to not directly update this dependency + metadata = { information_only: true } unless dependency.name.casecmp(dependency_details.name)&.zero? + + Dependency.new( + name: dep.name, + version: dependency_details.version, + requirements: dep.requirements, + previous_version: dep.version, + previous_requirements: dep.requirements, + package_manager: dep.package_manager, + metadata: metadata ) + end end sig { returns(T::Boolean) } def version_comes_from_multi_dependency_property? - declarations_using_a_property.any? do |requirement| - property_name = requirement.fetch(:metadata).fetch(:property_name) - - all_property_based_dependencies.any? do |dep| - next false if dep.name == dependency.name - - dep.requirements.any? do |req| - req.dig(:metadata, :property_name) == property_name - 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( - Nuget::FileParser.new( - dependency_files: dependency_files, - repo_contents_path: repo_contents_path, - source: nil - ).parse.select do |dep| - dep.requirements.any? { |req| req.dig(:metadata, :property_name) } - end, - T.nilable(T::Array[Dependabot::Dependency]) - ) + update_analysis.dependency_analysis.version_comes_from_multi_dependency_property end end end diff --git a/nuget/lib/dependabot/nuget/update_checker/compatibility_checker.rb b/nuget/lib/dependabot/nuget/update_checker/compatibility_checker.rb deleted file mode 100644 index 4f67de874e3..00000000000 --- a/nuget/lib/dependabot/nuget/update_checker/compatibility_checker.rb +++ /dev/null @@ -1,116 +0,0 @@ -# typed: strict -# frozen_string_literal: true - -require "sorbet-runtime" - -require "dependabot/update_checkers/base" - -module Dependabot - module Nuget - class CompatibilityChecker - extend T::Sig - - require_relative "nuspec_fetcher" - require_relative "nupkg_fetcher" - require_relative "tfm_finder" - require_relative "tfm_comparer" - - sig do - params( - dependency_urls: T::Array[T::Hash[Symbol, String]], - dependency: Dependabot::Dependency - ).void - end - def initialize(dependency_urls:, dependency:) - @dependency_urls = dependency_urls - @dependency = dependency - end - - sig { params(version: String).returns(T::Boolean) } - def compatible?(version) - nuspec_xml = NuspecFetcher.fetch_nuspec(dependency_urls, dependency.name, version) - return false unless nuspec_xml - - # development dependencies are packages such as analyzers which need to be compatible with the compiler not the - # project itself, but some packages that report themselves as development dependencies still contain target - # framework dependencies and should be checked for compatibility through the regular means - return true if pure_development_dependency?(nuspec_xml) - - package_tfms = parse_package_tfms(nuspec_xml) - package_tfms = fetch_package_tfms(version) if package_tfms.empty? - # nil is a special return value that indicates that the package is likely a development dependency - return true if package_tfms.nil? - return false if package_tfms.empty? - - return false if project_tfms.nil? || project_tfms&.empty? - - TfmComparer.are_frameworks_compatible?(T.must(project_tfms), package_tfms) - end - - private - - sig { returns(T::Array[T::Hash[Symbol, String]]) } - attr_reader :dependency_urls - - sig { returns(Dependabot::Dependency) } - attr_reader :dependency - - sig { params(nuspec_xml: Nokogiri::XML::Document).returns(T::Boolean) } - def pure_development_dependency?(nuspec_xml) - contents = nuspec_xml.at_xpath("package/metadata/developmentDependency")&.content&.strip - return false unless contents # no `developmentDependency` element - - self_reports_as_development_dependency = contents.casecmp?("true") - return false unless self_reports_as_development_dependency - - # even though a package self-reports as a development dependency, it might not be if it has dependency groups - # with a target framework - dependency_groups_with_target_framework = - nuspec_xml.at_xpath("/package/metadata/dependencies/group[@targetFramework]") - dependency_groups_with_target_framework.to_a.empty? - end - - sig { params(nuspec_xml: Nokogiri::XML::Document).returns(T::Array[String]) } - def parse_package_tfms(nuspec_xml) - nuspec_xml.xpath("//dependencies/group").filter_map { |group| group.attribute("targetFramework") } - end - - sig { returns(T.nilable(T::Array[String])) } - def project_tfms - @project_tfms ||= T.let(TfmFinder.frameworks(dependency), T.nilable(T::Array[String])) - end - - sig { params(dependency_version: String).returns(T.nilable(T::Array[String])) } - def fetch_package_tfms(dependency_version) - cache = CacheManager.cache("compatibility_checker_tfms_cache") - key = "#{dependency.name}::#{dependency_version}" - - cache[key] ||= begin - nupkg_buffer = NupkgFetcher.fetch_nupkg_buffer(dependency_urls, dependency.name, dependency_version) - return [] unless nupkg_buffer - - # Parse tfms from the folders beneath the lib folder - folder_name = "lib/" - tfms = Set.new - Zip::File.open_buffer(nupkg_buffer) do |zip| - lib_file_entries = zip.select { |entry| entry.name.start_with?(folder_name) } - # If there is no lib folder in this package, assume it is a development dependency - return nil if lib_file_entries.empty? - - lib_file_entries.each do |entry| - _, tfm = entry.name.split("/").first(2) - - # some zip compressors create empty directory entries (in this case `lib/`) which can cause the string - # split to return `nil`, so we have to explicitly guard against that - tfms << tfm if tfm - end - end - - tfms.to_a - end - - cache[key] - end - end - end -end diff --git a/nuget/lib/dependabot/nuget/update_checker/dependency_finder.rb b/nuget/lib/dependabot/nuget/update_checker/dependency_finder.rb deleted file mode 100644 index 0341e0aa80f..00000000000 --- a/nuget/lib/dependabot/nuget/update_checker/dependency_finder.rb +++ /dev/null @@ -1,292 +0,0 @@ -# typed: strict -# frozen_string_literal: true - -require "nokogiri" -require "sorbet-runtime" -require "stringio" -require "zip" - -require "dependabot/update_checkers/base" -require "dependabot/nuget/version" - -module Dependabot - module Nuget - class UpdateChecker < Dependabot::UpdateCheckers::Base - class DependencyFinder - extend T::Sig - - require_relative "requirements_updater" - require_relative "nuspec_fetcher" - - sig { returns(T::Hash[String, T.untyped]) } - def self.transitive_dependencies_cache - CacheManager.cache("dependency_finder_transitive_dependencies") - end - - sig { returns(T::Hash[String, T.untyped]) } - def self.updated_peer_dependencies_cache - CacheManager.cache("dependency_finder_updated_peer_dependencies") - end - - sig { returns(T::Hash[String, T.untyped]) } - def self.fetch_dependencies_cache - CacheManager.cache("dependency_finder_fetch_dependencies") - end - - sig do - params( - dependency: Dependabot::Dependency, - dependency_files: T::Array[Dependabot::DependencyFile], - credentials: T::Array[Dependabot::Credential], - repo_contents_path: T.nilable(String) - ).void - end - def initialize(dependency:, dependency_files:, credentials:, repo_contents_path:) - @dependency = dependency - @dependency_files = dependency_files - @credentials = credentials - @repo_contents_path = repo_contents_path - end - - sig { returns(T::Array[Dependabot::Dependency]) } - def transitive_dependencies - key = "#{dependency.name.downcase}::#{dependency.version}" - cache = DependencyFinder.transitive_dependencies_cache - - unless cache[key] - begin - # first do a quick sanity check on the version string; if it can't be parsed, an exception will be raised - _ = Version.new(dependency.version) - - cache[key] = fetch_transitive_dependencies( - @dependency.name, - T.must(@dependency.version) - ).map do |dependency_info| - package_name = dependency_info["packageName"] - target_version = dependency_info["version"] - - Dependency.new( - name: package_name, - version: target_version.to_s, - requirements: [], # Empty requirements for transitive dependencies - package_manager: @dependency.package_manager - ) - end - rescue StandardError - # if anything happened above, there are no meaningful dependencies that can be derived - cache[key] = [] - end - end - - cache[key] - end - - sig { returns(T::Array[Dependabot::Dependency]) } - def updated_peer_dependencies - key = "#{dependency.name.downcase}::#{dependency.version}" - cache = DependencyFinder.updated_peer_dependencies_cache - - cache[key] ||= fetch_transitive_dependencies( - @dependency.name, - T.must(@dependency.version) - ).filter_map do |dependency_info| - package_name = dependency_info["packageName"] - target_version = dependency_info["version"] - - # Find the Dependency object for the peer dependency. We will not return - # dependencies that are not referenced from dependency files. - peer_dependency = top_level_dependencies.find { |d| d.name == package_name } - next unless peer_dependency - next unless target_version > peer_dependency.numeric_version - - # Use version finder to determine the source details for the peer dependency. - target_version_details = version_finder(peer_dependency).versions.find do |v| - v.fetch(:version) == target_version - end - next unless target_version_details - - Dependency.new( - name: peer_dependency.name, - version: target_version_details.fetch(:version).to_s, - requirements: updated_requirements(peer_dependency, target_version_details), - previous_version: peer_dependency.version, - previous_requirements: peer_dependency.requirements, - package_manager: peer_dependency.package_manager, - metadata: { information_only: true } # Instruct updater to not directly update this dependency - ) - end - - cache[key] - end - - private - - sig { returns(Dependabot::Dependency) } - attr_reader :dependency - - sig { returns(T::Array[Dependabot::DependencyFile]) } - attr_reader :dependency_files - - sig { returns(T::Array[Dependabot::Credential]) } - attr_reader :credentials - - sig { returns(T.nilable(String)) } - attr_reader :repo_contents_path - - sig do - params( - dep: Dependabot::Dependency, - target_version_details: T::Hash[Symbol, T.untyped] - ) - .returns(T::Array[T::Hash[String, T.untyped]]) - end - def updated_requirements(dep, target_version_details) - @updated_requirements ||= T.let({}, T.nilable(T::Hash[String, T.untyped])) - @updated_requirements[dep.name] ||= - RequirementsUpdater.new( - requirements: dep.requirements, - latest_version: target_version_details.fetch(:version).to_s, - source_details: target_version_details.slice(:nuspec_url, :repo_url, :source_url) - ).updated_requirements - end - - sig { returns(T::Array[Dependabot::Dependency]) } - def top_level_dependencies - @top_level_dependencies ||= - T.let( - Nuget::FileParser.new( - dependency_files: dependency_files, - repo_contents_path: repo_contents_path, - source: nil - ).parse.select(&:top_level?), - T.nilable(T::Array[Dependabot::Dependency]) - ) - end - - sig { returns(T::Array[Dependabot::DependencyFile]) } - def nuget_configs - @nuget_configs ||= - T.let( - @dependency_files.select { |f| f.name.match?(/nuget\.config$/i) }, - T.nilable(T::Array[Dependabot::DependencyFile]) - ) - end - - sig { returns(T::Array[T::Hash[Symbol, String]]) } - def dependency_urls - @dependency_urls ||= - T.let( - RepositoryFinder.new( - dependency: @dependency, - credentials: @credentials, - config_files: nuget_configs - ) - .dependency_urls - .select { |url| url.fetch(:repository_type) == "v3" }, - T.nilable(T::Array[T::Hash[Symbol, String]]) - ) - end - - sig { params(package_id: String, package_version: String).returns(T::Array[T::Hash[String, T.untyped]]) } - def fetch_transitive_dependencies(package_id, package_version) - all_dependencies = {} - fetch_transitive_dependencies_impl(package_id, package_version, all_dependencies) - all_dependencies.map { |_, dependency_info| dependency_info } - end - - sig { params(package_id: String, package_version: String, all_dependencies: T::Hash[String, T.untyped]).void } - def fetch_transitive_dependencies_impl(package_id, package_version, all_dependencies) - dependencies = fetch_dependencies(package_id, package_version) - return unless dependencies.any? - - dependencies.each do |dependency| - next if dependency.nil? - - dependency_id = dependency["packageName"] - dependency_version_range = dependency["versionRange"] - - nuget_version_range_regex = /[\[(](\d+(\.\d+)*(-\w+(\.\d+)*)?)/ - nuget_version_range_match_data = nuget_version_range_regex.match(dependency_version_range) - - dependency_version = if nuget_version_range_match_data.nil? - dependency_version_range - else - nuget_version_range_match_data[1] - end - - dependency["version"] = Version.new(dependency_version) - - current_dependency = all_dependencies[dependency_id.downcase] - next unless current_dependency.nil? || current_dependency["version"] < dependency["version"] - - all_dependencies[dependency_id.downcase] = dependency - fetch_transitive_dependencies_impl(dependency_id, dependency_version, all_dependencies) - end - end - - sig { params(package_id: String, package_version: String).returns(T::Array[T::Hash[String, T.untyped]]) } - def fetch_dependencies(package_id, package_version) - key = "#{package_id.downcase}::#{package_version}" - cache = DependencyFinder.fetch_dependencies_cache - - cache[key] ||= begin - nuspec_xml = NuspecFetcher.fetch_nuspec(dependency_urls, package_id, package_version) - if nuspec_xml.nil? - [] - else - read_dependencies_from_nuspec(nuspec_xml) - end - end - - cache[key] - end - - sig { params(nuspec_xml: Nokogiri::XML::Document).returns(T::Array[T::Hash[String, String]]) } - def read_dependencies_from_nuspec(nuspec_xml) # rubocop:disable Metrics/PerceivedComplexity - # we want to exclude development dependencies from the lookup - allowed_attributes = %w(all compile native runtime) - - nuspec_xml_dependencies = nuspec_xml.xpath("//dependencies/child::node()/dependency").select do |dependency| - include_attr = dependency.attribute("include") - exclude_attr = dependency.attribute("exclude") - - if include_attr.nil? && exclude_attr.nil? - true - elsif include_attr - include_values = include_attr.value.split(",").map(&:strip) - include_values.any? { |element1| allowed_attributes.any? { |element2| element1.casecmp?(element2) } } - else - exclude_values = exclude_attr.value.split(",").map(&:strip) - exclude_values.none? { |element1| allowed_attributes.any? { |element2| element1.casecmp?(element2) } } - end - end - - dependency_list = [] - nuspec_xml_dependencies.each do |dependency| - next unless dependency.attribute("version") - - dependency_list << { - "packageName" => dependency.attribute("id").value, - "versionRange" => dependency.attribute("version").value - } - end - - dependency_list - end - - sig { params(dep: Dependabot::Dependency).returns(Dependabot::Nuget::UpdateChecker::VersionFinder) } - def version_finder(dep) - VersionFinder.new( - dependency: dep, - dependency_files: dependency_files, - credentials: credentials, - ignored_versions: [], - raise_on_ignored: false, - security_advisories: [], - repo_contents_path: repo_contents_path - ) - end - end - end - end -end diff --git a/nuget/lib/dependabot/nuget/update_checker/nupkg_fetcher.rb b/nuget/lib/dependabot/nuget/update_checker/nupkg_fetcher.rb deleted file mode 100644 index b9f53f9d1eb..00000000000 --- a/nuget/lib/dependabot/nuget/update_checker/nupkg_fetcher.rb +++ /dev/null @@ -1,221 +0,0 @@ -# typed: strict -# frozen_string_literal: true - -require "nokogiri" -require "stringio" -require "sorbet-runtime" -require "zip" - -require "dependabot/nuget/http_response_helpers" - -module Dependabot - module Nuget - class NupkgFetcher - extend T::Sig - - require_relative "repository_finder" - - sig do - params( - dependency_urls: T::Array[T::Hash[Symbol, String]], - package_id: String, - package_version: String - ) - .returns(T.nilable(String)) - end - def self.fetch_nupkg_buffer(dependency_urls, package_id, package_version) - # check all repositories for the first one that has the nupkg - dependency_urls.reduce(T.let(nil, T.nilable(String))) do |nupkg_buffer, repository_details| - nupkg_buffer || fetch_nupkg_buffer_from_repository(repository_details, package_id, package_version) - end - end - - sig do - params( - repository_details: T::Hash[Symbol, T.untyped], - package_id: T.nilable(String), - package_version: T.nilable(String) - ) - .returns(T.nilable(String)) - end - def self.fetch_nupkg_url_from_repository(repository_details, package_id, package_version) - return unless package_id && package_version && !package_version.empty? - - feed_url = repository_details[:repository_url] - repository_type = repository_details[:repository_type] - - package_url = if repository_type == "v2" - get_nuget_v2_package_url(repository_details, package_id, package_version) - elsif repository_type == "v3" - get_nuget_v3_package_url(repository_details, package_id, package_version) - else - raise Dependabot::DependencyFileNotResolvable, "Unexpected NuGet feed format: #{feed_url}" - end - - package_url - end - - sig do - params( - repository_details: T::Hash[Symbol, T.untyped], - package_id: String, - package_version: String - ) - .returns(T.nilable(String)) - end - def self.fetch_nupkg_buffer_from_repository(repository_details, package_id, package_version) - package_url = fetch_nupkg_url_from_repository(repository_details, package_id, package_version) - return unless package_url - - auth_header = repository_details[:auth_header] - fetch_stream(package_url, auth_header) - end - - sig do - params( - repository_details: T::Hash[Symbol, T.untyped], - package_id: String, - package_version: String - ) - .returns(T.nilable(String)) - end - def self.get_nuget_v3_package_url(repository_details, package_id, package_version) - base_url = repository_details[:base_url] - unless base_url - return get_nuget_v3_package_url_from_search(repository_details, package_id, - package_version) - end - - base_url = base_url.delete_suffix("/") - package_id_downcased = package_id.downcase - "#{base_url}/#{package_id_downcased}/#{package_version}/#{package_id_downcased}.#{package_version}.nupkg" - end - - # rubocop:disable Metrics/CyclomaticComplexity - # rubocop:disable Metrics/PerceivedComplexity - sig do - params( - repository_details: T::Hash[Symbol, T.untyped], - package_id: String, - package_version: String - ) - .returns(T.nilable(String)) - end - def self.get_nuget_v3_package_url_from_search(repository_details, package_id, package_version) - search_url = repository_details[:search_url] - return nil unless search_url - - # get search result - search_result_response = fetch_url(search_url, repository_details) - return nil unless search_result_response&.status == 200 - - search_response_body = HttpResponseHelpers.remove_wrapping_zero_width_chars(T.must(search_result_response).body) - search_results = JSON.parse(search_response_body) - - # find matching package and version - package_search_result = search_results&.[]("data")&.find { |d| package_id.casecmp?(d&.[]("id")) } - version_search_result = package_search_result&.[]("versions")&.find do |v| - package_version.casecmp?(v&.[]("version")) - end - registration_leaf_url = version_search_result&.[]("@id") - return nil unless registration_leaf_url - - registration_leaf_response = fetch_url(registration_leaf_url, repository_details) - return nil unless registration_leaf_response - return nil unless registration_leaf_response.status == 200 - - registration_leaf_response_body = - HttpResponseHelpers.remove_wrapping_zero_width_chars(registration_leaf_response.body) - registration_leaf = JSON.parse(registration_leaf_response_body) - - # finally, get the .nupkg url - registration_leaf&.[]("packageContent") - end - # rubocop:enable Metrics/PerceivedComplexity - # rubocop:enable Metrics/CyclomaticComplexity - - sig do - params( - repository_details: T::Hash[Symbol, T.untyped], - package_id: String, - package_version: String - ) - .returns(T.nilable(String)) - end - def self.get_nuget_v2_package_url(repository_details, package_id, package_version) - # get package XML - base_url = repository_details[:base_url].delete_suffix("/") - package_url = "#{base_url}/Packages(Id='#{package_id}',Version='#{package_version}')" - response = fetch_url(package_url, repository_details) - return nil unless response&.status == 200 - - # find relevant element - doc = Nokogiri::XML(T.must(response).body) - doc.remove_namespaces! - - content_element = doc.xpath("/entry/content") - nupkg_url = content_element&.attribute("src")&.value - nupkg_url - end - - sig do - params( - stream_url: String, - auth_header: T::Hash[String, String], - max_redirects: Integer - ) - .returns(T.nilable(String)) - end - def self.fetch_stream(stream_url, auth_header, max_redirects = 5) - current_url = stream_url - current_redirects = 0 - - loop do - # Directly download the stream without any additional settings _except_ for `omit_default_port: true` which - # is necessary to not break the URL signing that some NuGet feeds use. - response = Excon.get( - current_url, - headers: auth_header, - omit_default_port: true - ) - - # redirect the HTTP response as appropriate based on documentation here: - # https://developer.mozilla.org/en-US/docs/Web/HTTP/Redirections - case response.status - when 200 - return response.body - when 301, 302, 303, 307, 308 - current_redirects += 1 - return nil if current_redirects > max_redirects - - current_url = T.must(response.headers["Location"]) - else - return nil - end - end - end - - sig do - params( - url: String, - repository_details: T::Hash[Symbol, T.untyped] - ) - .returns(T.nilable(Excon::Response)) - end - def self.fetch_url(url, repository_details) - fetch_url_with_auth(url, repository_details.fetch(:auth_header)) - end - - sig { params(url: String, auth_header: T::Hash[T.any(String, Symbol), T.untyped]).returns(Excon::Response) } - def self.fetch_url_with_auth(url, auth_header) - cache = CacheManager.cache("nupkg_fetcher_cache") - cache[url] ||= Dependabot::RegistryClient.get( - url: url, - headers: auth_header - ) - - cache[url] - end - end - end -end diff --git a/nuget/lib/dependabot/nuget/update_checker/nuspec_fetcher.rb b/nuget/lib/dependabot/nuget/update_checker/nuspec_fetcher.rb deleted file mode 100644 index d07d4412331..00000000000 --- a/nuget/lib/dependabot/nuget/update_checker/nuspec_fetcher.rb +++ /dev/null @@ -1,110 +0,0 @@ -# typed: strict -# frozen_string_literal: true - -require "nokogiri" -require "stringio" -require "sorbet-runtime" -require "zip" - -module Dependabot - module Nuget - class NuspecFetcher - extend T::Sig - - require_relative "nupkg_fetcher" - require_relative "repository_finder" - - sig do - params( - dependency_urls: T::Array[T::Hash[Symbol, String]], - package_id: String, - package_version: T.nilable(String) - ) - .returns(T.nilable(Nokogiri::XML::Document)) - end - def self.fetch_nuspec(dependency_urls, package_id, package_version) - # check all repositories for the first one that has the nuspec - dependency_urls.reduce(T.let(nil, T.nilable(Nokogiri::XML::Document))) do |nuspec_xml, repository_details| - nuspec_xml || fetch_nuspec_from_repository(repository_details, package_id, package_version) - end - end - - sig do - params( - repository_details: T::Hash[Symbol, T.untyped], - package_id: T.nilable(String), - package_version: T.nilable(String) - ) - .returns(T.nilable(Nokogiri::XML::Document)) - end - def self.fetch_nuspec_from_repository(repository_details, package_id, package_version) - return unless package_id && package_version && !package_version.empty? - - feed_url = repository_details[:repository_url] - auth_header = repository_details[:auth_header] - - nuspec_xml = nil - - if feed_supports_nuspec_download?(feed_url) - # we can use the normal nuget apis to get the nuspec and list out the dependencies - base_url = repository_details[:base_url].delete_suffix("/") - package_id_downcased = package_id.downcase - nuspec_url = "#{base_url}/#{package_id_downcased}/#{package_version}/#{package_id_downcased}.nuspec" - - nuspec_response = Dependabot::RegistryClient.get( - url: nuspec_url, - headers: auth_header - ) - - return unless nuspec_response.status == 200 - - nuspec_response_body = remove_invalid_characters(nuspec_response.body) - nuspec_xml = Nokogiri::XML(nuspec_response_body) - else - # no guarantee we can directly query the .nuspec; fall back to extracting it from the .nupkg - package_data = NupkgFetcher.fetch_nupkg_buffer_from_repository(repository_details, package_id, - package_version) - return if package_data.nil? - - nuspec_string = extract_nuspec(package_data, package_id) - nuspec_xml = Nokogiri::XML(nuspec_string) - end - - nuspec_xml.remove_namespaces! - nuspec_xml - end - - sig { params(feed_url: String).returns(T::Boolean) } - def self.feed_supports_nuspec_download?(feed_url) - feed_regexs = [ - # nuget - %r{https://api\.nuget\.org/v3/index\.json}, - # azure devops - %r{https://pkgs\.dev\.azure\.com/(?[^/]+)/(?[^/]+)/_packaging/(?[^/]+)/nuget/v3/index\.json}, - %r{https://pkgs\.dev\.azure\.com/(?[^/]+)/_packaging/(?[^/]+)/nuget/v3/index\.json(?)}, - %r{https://(?[^\.\/]+)\.pkgs\.visualstudio\.com/_packaging/(?[^/]+)/nuget/v3/index\.json(?)} - ] - feed_regexs.any? { |reg| reg.match(feed_url) } - end - - sig { params(zip_stream: String, package_id: String).returns(T.nilable(String)) } - def self.extract_nuspec(zip_stream, package_id) - Zip::File.open_buffer(zip_stream) do |zip| - nuspec_entry = zip.find { |entry| entry.name == "#{package_id}.nuspec" } - return nuspec_entry.get_input_stream.read if nuspec_entry - end - nil - end - - sig { params(string: String).returns(String) } - def self.remove_invalid_characters(string) - string.dup - .force_encoding(Encoding::UTF_8) - .encode - .scrub("") - .gsub(/\A[\u200B-\u200D\uFEFF]/, "") - .gsub(/[\u200B-\u200D\uFEFF]\Z/, "") - end - end - end -end diff --git a/nuget/lib/dependabot/nuget/update_checker/property_updater.rb b/nuget/lib/dependabot/nuget/update_checker/property_updater.rb deleted file mode 100644 index 836c20fa78f..00000000000 --- a/nuget/lib/dependabot/nuget/update_checker/property_updater.rb +++ /dev/null @@ -1,195 +0,0 @@ -# typed: strict -# frozen_string_literal: true - -require "sorbet-runtime" - -require "dependabot/update_checkers/base" -require "dependabot/nuget/file_parser" - -module Dependabot - module Nuget - class UpdateChecker < Dependabot::UpdateCheckers::Base - class PropertyUpdater - extend T::Sig - - require_relative "version_finder" - require_relative "requirements_updater" - require_relative "dependency_finder" - - sig do - params( - dependency: Dependabot::Dependency, - dependency_files: T::Array[Dependabot::DependencyFile], - credentials: T::Array[Dependabot::Credential], - target_version_details: T.nilable(T::Hash[Symbol, String]), - ignored_versions: T::Array[String], - repo_contents_path: T.nilable(String), - raise_on_ignored: T::Boolean - ).void - end - def initialize(dependency:, dependency_files:, credentials:, - target_version_details:, ignored_versions:, - repo_contents_path:, raise_on_ignored: false) - @dependency = dependency - @dependency_files = dependency_files - @credentials = credentials - @ignored_versions = ignored_versions - @raise_on_ignored = raise_on_ignored - @target_version = T.let( - target_version_details&.fetch(:version), - T.nilable(T.any(String, Dependabot::Nuget::Version)) - ) - @source_details = T.let( - target_version_details&.slice(:nuspec_url, :repo_url, :source_url), - T.nilable(T::Hash[Symbol, String]) - ) - @repo_contents_path = repo_contents_path - end - - sig { returns(T::Boolean) } - def update_possible? - return false unless target_version - - @update_possible ||= T.let( - dependencies_using_property.all? do |dep| - versions = VersionFinder.new( - dependency: dep, - dependency_files: dependency_files, - credentials: credentials, - ignored_versions: ignored_versions, - raise_on_ignored: @raise_on_ignored, - security_advisories: [], - repo_contents_path: repo_contents_path - ).versions.map { |v| v.fetch(:version) } - - versions.include?(target_version) || versions.none? - end, - T.nilable(T::Boolean) - ) - end - - sig { returns(T::Array[Dependabot::Dependency]) } - def updated_dependencies - raise "Update not possible!" unless update_possible? - - @updated_dependencies ||= T.let( - begin - dependencies = T.let({}, T::Hash[String, Dependabot::Dependency]) - - dependencies_using_property.each do |dep| - # Only keep one copy of each dependency, the one with the highest target version. - visited_dependency = dependencies[dep.name.downcase] - next unless visited_dependency.nil? || T.must(visited_dependency.numeric_version) < target_version - - updated_dependency = Dependency.new( - name: dep.name, - version: target_version.to_s, - requirements: updated_requirements(dep), - previous_version: dep.version, - previous_requirements: dep.requirements, - package_manager: dep.package_manager - ) - dependencies[updated_dependency.name.downcase] = updated_dependency - # Add peer dependencies to the list of updated dependencies. - process_updated_peer_dependencies(updated_dependency, dependencies) - end - - dependencies.map { |_, dependency| dependency } - end, - T.nilable(T::Array[Dependabot::Dependency]) - ) - end - - private - - sig { returns(Dependabot::Dependency) } - attr_reader :dependency - - sig { returns(T::Array[Dependabot::DependencyFile]) } - attr_reader :dependency_files - - sig { returns(T.nilable(T.any(String, Dependabot::Nuget::Version))) } - attr_reader :target_version - - sig { returns(T.nilable(T::Hash[Symbol, String])) } - attr_reader :source_details - - sig { returns(T::Array[Dependabot::Credential]) } - attr_reader :credentials - - sig { returns(T::Array[String]) } - attr_reader :ignored_versions - - sig { returns(T.nilable(String)) } - attr_reader :repo_contents_path - - sig do - params( - dependency: Dependabot::Dependency, - dependencies: T::Hash[String, Dependabot::Dependency] - ) - .returns(T::Array[Dependabot::Dependency]) - end - def process_updated_peer_dependencies(dependency, dependencies) - DependencyFinder.new( - dependency: dependency, - dependency_files: dependency_files, - credentials: credentials, - repo_contents_path: repo_contents_path - ).updated_peer_dependencies.each do |peer_dependency| - # Only keep one copy of each dependency, the one with the highest target version. - visited_dependency = dependencies[peer_dependency.name.downcase] - unless visited_dependency.nil? || - T.must(visited_dependency.numeric_version) < peer_dependency.numeric_version - next - end - - dependencies[peer_dependency.name.downcase] = peer_dependency - end - end - - sig { returns(T::Array[Dependabot::Dependency]) } - def dependencies_using_property - @dependencies_using_property ||= - T.let( - Nuget::FileParser.new( - dependency_files: dependency_files, - repo_contents_path: repo_contents_path, - source: nil - ).parse.select do |dep| - dep.requirements.any? do |r| - r.dig(:metadata, :property_name) == property_name - end - end, - T.nilable(T::Array[Dependabot::Dependency]) - ) - end - - sig { returns(String) } - def property_name - @property_name ||= T.let( - dependency.requirements - .find { |r| r.dig(:metadata, :property_name) } - &.dig(:metadata, :property_name), - T.nilable(String) - ) - - raise "No requirement with a property name!" unless @property_name - - @property_name - end - - sig { params(dep: Dependabot::Dependency).returns(T::Array[T::Hash[Symbol, T.untyped]]) } - def updated_requirements(dep) - @updated_requirements ||= T.let({}, T.nilable(T::Hash[String, T::Array[T::Hash[Symbol, T.untyped]]])) - @updated_requirements[dep.name] ||= - RequirementsUpdater.new( - requirements: dep.requirements, - latest_version: target_version, - source_details: source_details - ).updated_requirements - end - end - end - end -end diff --git a/nuget/lib/dependabot/nuget/update_checker/repository_finder.rb b/nuget/lib/dependabot/nuget/update_checker/repository_finder.rb deleted file mode 100644 index 162e491659e..00000000000 --- a/nuget/lib/dependabot/nuget/update_checker/repository_finder.rb +++ /dev/null @@ -1,442 +0,0 @@ -# typed: strict -# frozen_string_literal: true - -require "excon" -require "nokogiri" -require "sorbet-runtime" - -require "dependabot/errors" -require "dependabot/update_checkers/base" -require "dependabot/registry_client" -require "dependabot/nuget/cache_manager" -require "dependabot/nuget/http_response_helpers" - -module Dependabot - module Nuget - class RepositoryFinder - extend T::Sig - - DEFAULT_REPOSITORY_URL = "https://api.nuget.org/v3/index.json" - DEFAULT_REPOSITORY_API_KEY = "nuget.org" - - sig do - params( - dependency: Dependabot::Dependency, - credentials: T::Array[Dependabot::Credential], - config_files: T::Array[Dependabot::DependencyFile] - ).void - end - def initialize(dependency:, credentials:, config_files: []) - @dependency = dependency - @credentials = credentials - @config_files = config_files - end - - sig { returns(T::Array[T::Hash[Symbol, String]]) } - def dependency_urls - find_dependency_urls - end - - sig { returns(T::Array[T::Hash[Symbol, String]]) } - def known_repositories - return @known_repositories if @known_repositories - - @known_repositories ||= T.let([], T.nilable(T::Array[T::Hash[Symbol, String]])) - @known_repositories += credential_repositories - @known_repositories += config_file_repositories - - @known_repositories << { url: DEFAULT_REPOSITORY_URL, token: nil } if @known_repositories.empty? - - @known_repositories = @known_repositories.map do |repo| - { url: URI::DEFAULT_PARSER.escape(repo[:url]), token: repo[:token] } - end - @known_repositories.uniq - end - - sig { params(dependency_name: String).returns(T::Hash[Symbol, T.untyped]) } - def self.get_default_repository_details(dependency_name) - { - base_url: "https://api.nuget.org/v3-flatcontainer/", - registration_url: "https://api.nuget.org/v3/registration5-gz-semver2/#{dependency_name.downcase}/index.json", - repository_url: DEFAULT_REPOSITORY_URL, - versions_url: "https://api.nuget.org/v3-flatcontainer/" \ - "#{dependency_name.downcase}/index.json", - search_url: "https://azuresearch-usnc.nuget.org/query" \ - "?q=#{dependency_name.downcase}&prerelease=true&semVerLevel=2.0.0", - auth_header: {}, - repository_type: "v3" - } - end - - private - - sig { returns(Dependabot::Dependency) } - attr_reader :dependency - - sig { returns(T::Array[Dependabot::Credential]) } - attr_reader :credentials - - sig { returns(T::Array[Dependabot::DependencyFile]) } - attr_reader :config_files - - sig { returns(T::Array[T::Hash[Symbol, T.untyped]]) } - def find_dependency_urls - @find_dependency_urls ||= - T.let( - known_repositories.flat_map do |details| - if details.fetch(:url) == DEFAULT_REPOSITORY_URL - # Save a request for the default URL, since we already know how - # it addresses packages - next default_repository_details - end - - build_url_for_details(details) - end.compact.uniq, - T.nilable(T::Array[T::Hash[Symbol, T.untyped]]) - ) - end - - sig { params(repo_details: T::Hash[Symbol, T.untyped]).returns(T.nilable(T::Hash[Symbol, T.untyped])) } - def build_url_for_details(repo_details) - url = repo_details.fetch(:url) - url_obj = URI.parse(url) - if url_obj.is_a?(URI::HTTP) - details = build_url_for_details_remote(repo_details) - elsif url_obj.is_a?(URI::File) - details = { - base_url: url, - repository_type: "local" - } - end - - details - end - - sig { params(repo_details: T::Hash[Symbol, T.untyped]).returns(T.nilable(T::Hash[Symbol, T.untyped])) } - def build_url_for_details_remote(repo_details) - response = get_repo_metadata(repo_details) - check_repo_response(response, repo_details) - return unless response.status == 200 - - body = HttpResponseHelpers.remove_wrapping_zero_width_chars(response.body) - parsed_json = JSON.parse(body) - base_url = base_url_from_v3_metadata(parsed_json) - search_url = search_url_from_v3_metadata(parsed_json) - registration_url = registration_url_from_v3_metadata(parsed_json) - - details = { - base_url: base_url, - repository_url: repo_details.fetch(:url), - auth_header: auth_header_for_token(repo_details.fetch(:token)), - repository_type: "v3" - } - if base_url - details[:versions_url] = - File.join(base_url, dependency.name.downcase, "index.json") - end - if search_url - details[:search_url] = - search_url + "?q=#{dependency.name.downcase}&prerelease=true&semVerLevel=2.0.0" - end - - if registration_url - details[:registration_url] = File.join(registration_url, dependency.name.downcase, "index.json") - end - - details - rescue JSON::ParserError - build_v2_url(T.must(response), repo_details) - rescue Excon::Error::Timeout, Excon::Error::Socket - handle_timeout(repo_metadata_url: repo_details.fetch(:url)) - end - - sig { params(repo_details: T::Hash[Symbol, T.untyped]).returns(Excon::Response) } - def get_repo_metadata(repo_details) - url = repo_details.fetch(:url) - cache = CacheManager.cache("repo_finder_metadatacache") - if cache[url] - cache[url] - else - result = Dependabot::RegistryClient.get( - url: url, - headers: auth_header_for_token(repo_details.fetch(:token)) - ) - cache[url] = result - result - end - end - - sig { params(metadata: T::Hash[String, T::Array[T::Hash[String, T.untyped]]]).returns(T.nilable(String)) } - def base_url_from_v3_metadata(metadata) - metadata - .fetch("resources", []) - .find { |r| r.fetch("@type") == "PackageBaseAddress/3.0.0" } - &.fetch("@id") - end - - sig { params(metadata: T::Hash[String, T::Array[T::Hash[String, T.untyped]]]).returns(T.nilable(String)) } - def registration_url_from_v3_metadata(metadata) - allowed_registration_types = %w( - RegistrationsBaseUrl - RegistrationsBaseUrl/3.0.0-beta - RegistrationsBaseUrl/3.0.0-rc - RegistrationsBaseUrl/3.4.0 - RegistrationsBaseUrl/3.6.0 - ) - metadata - .fetch("resources", []) - .find { |r| allowed_registration_types.find { |s| r.fetch("@type") == s } } - &.fetch("@id") - end - - sig { params(metadata: T::Hash[String, T::Array[T::Hash[String, T.untyped]]]).returns(T.nilable(String)) } - def search_url_from_v3_metadata(metadata) - # allowable values from here: https://learn.microsoft.com/en-us/nuget/api/search-query-service-resource#versioning - allowed_search_types = %w( - SearchQueryService - SearchQueryService/3.0.0-beta - SearchQueryService/3.0.0-rc - SearchQueryService/3.5.0 - ) - metadata - .fetch("resources", []) - .find { |r| allowed_search_types.find { |s| r.fetch("@type") == s } } - &.fetch("@id") - end - - sig do - params( - response: Excon::Response, - repo_details: T::Hash[Symbol, T.untyped] - ) - .returns(T::Hash[Symbol, T.untyped]) - end - def build_v2_url(response, repo_details) - doc = Nokogiri::XML(response.body) - - doc.remove_namespaces! - base_url = doc.at_xpath("service")&.attributes - &.fetch("base", nil)&.value - - base_url ||= repo_details.fetch(:url) - - { - base_url: base_url, - repository_url: base_url, - versions_url: File.join( - base_url.delete_suffix("/"), - "FindPackagesById()?id='#{dependency.name}'" - ), - auth_header: auth_header_for_token(repo_details.fetch(:token)), - repository_type: "v2" - } - end - - sig { params(response: Excon::Response, details: T::Hash[Symbol, T.untyped]).void } - def check_repo_response(response, details) - return unless [401, 402, 403].include?(response.status) - raise if details.fetch(:url) == DEFAULT_REPOSITORY_URL - - raise PrivateSourceAuthenticationFailure, details.fetch(:url) - end - - sig { params(repo_metadata_url: String).returns(T.noreturn) } - def handle_timeout(repo_metadata_url:) - raise if repo_metadata_url == DEFAULT_REPOSITORY_URL - - raise PrivateSourceTimedOut, repo_metadata_url - end - - sig { returns(T::Array[T::Hash[Symbol, String]]) } - def credential_repositories - @credential_repositories ||= - T.let( - credentials - .select { |cred| cred["type"] == "nuget_feed" && cred["url"] } - .map { |c| { url: c.fetch("url"), token: c["token"] } }, - T.nilable(T::Array[T::Hash[Symbol, String]]) - ) - end - - sig { returns(T::Array[T::Hash[Symbol, String]]) } - def config_file_repositories - config_files.flat_map { |file| repos_from_config_file(file) } - end - - # rubocop:disable Metrics/CyclomaticComplexity - # rubocop:disable Metrics/PerceivedComplexity - # rubocop:disable Metrics/MethodLength - # rubocop:disable Metrics/AbcSize - sig { params(config_file: Dependabot::DependencyFile).returns(T::Array[T::Hash[Symbol, String]]) } - def repos_from_config_file(config_file) - doc = Nokogiri::XML(config_file.content) - doc.remove_namespaces! - # analogous to having a root config with the default repository - base_sources = [{ url: DEFAULT_REPOSITORY_URL, key: "nuget.org" }] - - sources = T.let([], T::Array[T::Hash[Symbol, String]]) - - # regular package sources - doc.css("configuration > packageSources").children.each do |node| - if node.name == "clear" - sources.clear - base_sources.clear - else - key = node.attribute("key")&.value&.strip || node.at_xpath("./key")&.content&.strip - url = node.attribute("value")&.value&.strip || node.at_xpath("./value")&.content&.strip - url = expand_windows_style_environment_variables(url) if url - - # if the path isn't absolute it's relative to the nuget.config file - if url - unless url.include?("://") || Pathname.new(url).absolute? - url = Pathname(config_file.directory).join(url).to_path - end - sources << { url: url, key: key } - end - end - end - - # signed package sources - # https://learn.microsoft.com/en-us/nuget/reference/nuget-config-file#trustedsigners-section - doc.xpath("/configuration/trustedSigners/repository").each do |node| - name = node.attribute("name")&.value&.strip - service_index = node.attribute("serviceIndex")&.value&.strip - sources << { url: service_index, key: name } - end - - sources += base_sources # TODO: quirky overwrite behavior - disabled_sources = disabled_sources(doc) - sources.reject! do |s| - disabled_sources.include?(s[:key]) - end - - sources.reject! do |s| - known_urls = credential_repositories.map { |cr| cr.fetch(:url) } - known_urls.include?(s.fetch(:url)) - end - - # filter out based on packageSourceMapping - package_mapping_elements = doc.xpath("/configuration/packageSourceMapping/packageSource/package[@pattern]") - matching_package_elements = package_mapping_elements.select do |package_element| - pattern = package_element.attribute("pattern").value - - # reusing this function for a case insensitive GLOB pattern patch (e.g., "Microsoft.Azure.*") - File.fnmatch(pattern, @dependency.name, File::FNM_CASEFOLD) - end - longest_matching_package_element = matching_package_elements.max_by do |package_element| - package_element.attribute("pattern").value.length - end - matching_key = longest_matching_package_element&.parent&.attribute("key")&.value - if matching_key - # found a matching source, only keep that one - sources.select! { |s| s.fetch(:key) == matching_key } - end - - add_config_file_credentials(sources: sources, doc: doc) - sources.each { |details| details.delete(:key) } - - sources - end - # rubocop:enable Metrics/AbcSize - # rubocop:enable Metrics/MethodLength - # rubocop:enable Metrics/PerceivedComplexity - # rubocop:enable Metrics/CyclomaticComplexity - - sig { returns(T::Hash[Symbol, T.untyped]) } - def default_repository_details - RepositoryFinder.get_default_repository_details(dependency.name) - end - - # rubocop:disable Metrics/PerceivedComplexity - sig { params(doc: Nokogiri::XML::Document).returns(T::Array[String]) } - def disabled_sources(doc) - doc.css("configuration > disabledPackageSources > add").filter_map do |node| - value = node.attribute("value")&.value || - node.at_xpath("./value")&.content - - if value&.strip&.downcase == "true" - node.attribute("key")&.value&.strip || - node.at_xpath("./key")&.content&.strip - end - end - end - # rubocop:enable Metrics/PerceivedComplexity - - # rubocop:disable Metrics/PerceivedComplexity - sig do - params( - sources: T::Array[T::Hash[Symbol, T.nilable(String)]], - doc: Nokogiri::XML::Document - ) - .void - end - def add_config_file_credentials(sources:, doc:) - sources.each do |source_details| - key = source_details.fetch(:key) - next source_details[:token] = nil unless key - next source_details[:token] = nil if key.match?(/^\d/) - - tag = key.gsub(" ", "_x0020_") - creds_nodes = doc.css("configuration > packageSourceCredentials " \ - "> #{tag} > add") - - username = - creds_nodes - .find { |n| n.attribute("key")&.value == "Username" } - &.attribute("value")&.value - password = - creds_nodes - .find { |n| n.attribute("key")&.value == "ClearTextPassword" } - &.attribute("value")&.value - - # NOTE: We have to look for plain text passwords, as we have no - # way of decrypting encrypted passwords. For the same reason we - # don't fetch API keys from the nuget.config at all. - next source_details[:token] = nil unless username && password - - expanded_username = expand_windows_style_environment_variables(username) - expanded_password = expand_windows_style_environment_variables(password) - source_details[:token] = "#{expanded_username}:#{expanded_password}" - rescue Nokogiri::XML::XPath::SyntaxError - # Any non-ascii characters in the tag with cause a syntax error - next source_details[:token] = nil - end - end - # rubocop:enable Metrics/PerceivedComplexity - - sig { params(string: String).returns(String) } - def expand_windows_style_environment_variables(string) - # NuGet.Config files can have Windows-style environment variables that need to be replaced - # https://learn.microsoft.com/en-us/nuget/reference/nuget-config-file#using-environment-variables - string.gsub(/%([^%]+)%/) do - environment_variable_name = T.must(::Regexp.last_match(1)) - environment_variable_value = ENV.fetch(environment_variable_name, nil) - if environment_variable_value - environment_variable_value - else - # report that the variable couldn't be expanded, then replace it as-is - Dependabot.logger.warn <<~WARN - The variable '%#{environment_variable_name}%' could not be expanded in NuGet.Config - WARN - "%#{environment_variable_name}%" - end - end - end - - sig { params(token: T.nilable(String)).returns(T::Hash[String, String]) } - def auth_header_for_token(token) - return {} unless token - - if token.include?(":") - encoded_token = Base64.encode64(token).delete("\n") - { "Authorization" => "Basic #{encoded_token}" } - elsif Base64.decode64(token).ascii_only? && - Base64.decode64(token).include?(":") - { "Authorization" => "Basic #{token.delete("\n")}" } - else - { "Authorization" => "Bearer #{token}" } - end - end - end - end -end diff --git a/nuget/lib/dependabot/nuget/update_checker/requirements_updater.rb b/nuget/lib/dependabot/nuget/update_checker/requirements_updater.rb index fe600921c1c..d475bc87d1f 100644 --- a/nuget/lib/dependabot/nuget/update_checker/requirements_updater.rb +++ b/nuget/lib/dependabot/nuget/update_checker/requirements_updater.rb @@ -20,14 +20,12 @@ class RequirementsUpdater sig do params( requirements: T::Array[T::Hash[Symbol, T.untyped]], - latest_version: T.nilable(T.any(String, Dependabot::Nuget::Version)), - source_details: T.nilable(T::Hash[Symbol, T.untyped]) + latest_version: T.nilable(T.any(String, Dependabot::Nuget::Version)) ) .void end - def initialize(requirements:, latest_version:, source_details:) + def initialize(requirements:, latest_version:) @requirements = requirements - @source_details = source_details return unless latest_version @latest_version = T.let(version_class.new(latest_version), Dependabot::Nuget::Version) @@ -59,7 +57,7 @@ def updated_requirements next req if new_req == req.fetch(:requirement) - req.merge(requirement: new_req, source: updated_source) + req.merge(requirement: new_req) end end @@ -71,9 +69,6 @@ def updated_requirements sig { returns(T.nilable(Dependabot::Nuget::Version)) } attr_reader :latest_version - sig { returns(T.nilable(T::Hash[Symbol, T.untyped])) } - attr_reader :source_details - sig { returns(T.class_of(Dependabot::Nuget::Version)) } def version_class Dependabot::Nuget::Version @@ -93,16 +88,6 @@ def update_wildcard_requirement(req_string) version + wildcard_section end - - sig { returns(T::Hash[Symbol, T.untyped]) } - def updated_source - { - type: "nuget_repo", - url: source_details&.fetch(:repo_url), - nuspec_url: source_details&.fetch(:nuspec_url), - source_url: source_details&.fetch(:source_url) - } - end end end end diff --git a/nuget/lib/dependabot/nuget/update_checker/tfm_comparer.rb b/nuget/lib/dependabot/nuget/update_checker/tfm_comparer.rb deleted file mode 100644 index 0cfbe08faf9..00000000000 --- a/nuget/lib/dependabot/nuget/update_checker/tfm_comparer.rb +++ /dev/null @@ -1,34 +0,0 @@ -# typed: strong -# frozen_string_literal: true - -require "sorbet-runtime" - -require "dependabot/update_checkers/base" -require "dependabot/nuget/version" -require "dependabot/nuget/requirement" -require "dependabot/nuget/native_helpers" -require "dependabot/shared_helpers" - -module Dependabot - module Nuget - class TfmComparer - extend T::Sig - - sig { params(project_tfms: T::Array[String], package_tfms: T::Array[String]).returns(T::Boolean) } - def self.are_frameworks_compatible?(project_tfms, package_tfms) - return false if package_tfms.empty? - return false if project_tfms.empty? - - key = "project_ftms:#{project_tfms.sort.join(',')}:package_tfms:#{package_tfms.sort.join(',')}".downcase - - @cached_framework_check ||= T.let({}, T.nilable(T::Hash[String, T::Boolean])) - unless @cached_framework_check.key?(key) - @cached_framework_check[key] = - NativeHelpers.run_nuget_framework_check(project_tfms, - package_tfms) - end - T.must(@cached_framework_check[key]) - end - end - end -end diff --git a/nuget/lib/dependabot/nuget/update_checker/tfm_finder.rb b/nuget/lib/dependabot/nuget/update_checker/tfm_finder.rb deleted file mode 100644 index aadb0e83efd..00000000000 --- a/nuget/lib/dependabot/nuget/update_checker/tfm_finder.rb +++ /dev/null @@ -1,30 +0,0 @@ -# typed: strong -# frozen_string_literal: true - -require "dependabot/nuget/discovery/discovery_json_reader" - -module Dependabot - module Nuget - class TfmFinder - extend T::Sig - - sig { params(dependency: Dependency).returns(T::Array[String]) } - def self.frameworks(dependency) - discovery_json = DiscoveryJsonReader.discovery_json - return [] unless discovery_json - - workspace = DiscoveryJsonReader.new( - discovery_json: discovery_json - ).workspace_discovery - return [] unless workspace - - workspace.projects.select do |project| - all_dependencies = project.dependencies + project.referenced_project_paths.flat_map do |ref| - workspace.projects.find { |p| p.file_path == ref }&.dependencies || [] - end - all_dependencies.any? { |d| d.name.casecmp?(dependency.name) } - end.flat_map(&:target_frameworks).uniq - end - end - end -end diff --git a/nuget/lib/dependabot/nuget/update_checker/version_finder.rb b/nuget/lib/dependabot/nuget/update_checker/version_finder.rb deleted file mode 100644 index 8b66eff3d1b..00000000000 --- a/nuget/lib/dependabot/nuget/update_checker/version_finder.rb +++ /dev/null @@ -1,450 +0,0 @@ -# typed: strict -# frozen_string_literal: true - -require "sorbet-runtime" - -require "dependabot/nuget/version" -require "dependabot/nuget/requirement" -require "dependabot/update_checkers/base" -require "dependabot/update_checkers/version_filters" -require "dependabot/nuget/nuget_client" - -module Dependabot - module Nuget - class UpdateChecker < Dependabot::UpdateCheckers::Base - # rubocop:disable Metrics/ClassLength - class VersionFinder - extend T::Sig - - require_relative "compatibility_checker" - require_relative "repository_finder" - - NUGET_RANGE_REGEX = /[\(\[].*,.*[\)\]]/ - - 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], - repo_contents_path: T.nilable(String), - raise_on_ignored: T::Boolean - ).void - end - def initialize(dependency:, - dependency_files:, - credentials:, - ignored_versions:, - security_advisories:, - repo_contents_path:, - raise_on_ignored: false) - @dependency = dependency - @dependency_files = dependency_files - @credentials = credentials - @ignored_versions = ignored_versions - @raise_on_ignored = raise_on_ignored - @security_advisories = security_advisories - @repo_contents_path = repo_contents_path - end - - sig { returns(T.nilable(T::Hash[Symbol, T.untyped])) } - def latest_version_details - @latest_version_details ||= - T.let( - begin - possible_versions = versions - possible_versions = filter_prereleases(possible_versions) - possible_versions = filter_ignored_versions(possible_versions) - - find_highest_compatible_version(possible_versions) - end, - T.nilable(T::Hash[Symbol, T.untyped]) - ) - end - - sig { returns(T.nilable(T::Hash[Symbol, T.untyped])) } - def lowest_security_fix_version_details - @lowest_security_fix_version_details ||= - T.let( - begin - possible_versions = versions - possible_versions = filter_prereleases(possible_versions) - possible_versions = Dependabot::UpdateCheckers::VersionFilters.filter_vulnerable_versions( - possible_versions, security_advisories - ) - possible_versions = filter_ignored_versions(possible_versions) - possible_versions = filter_lower_versions(possible_versions) - - find_lowest_compatible_version(possible_versions) - end, - T.nilable(T::Hash[Symbol, T.untyped]) - ) - end - - sig { returns(T::Array[T::Hash[Symbol, T.nilable(T.any(Dependabot::Version, String))]]) } - def versions - available_v3_versions + available_v2_versions - end - - sig { returns(Dependabot::Dependency) } - attr_reader :dependency - - sig { returns(T::Array[Dependabot::DependencyFile]) } - attr_reader :dependency_files - - sig { returns(T::Array[Dependabot::Credential]) } - attr_reader :credentials - - sig { returns(T::Array[String]) } - attr_reader :ignored_versions - - sig { returns(T::Array[Dependabot::SecurityAdvisory]) } - attr_reader :security_advisories - - sig { returns(T.nilable(String)) } - attr_reader :repo_contents_path - - private - - sig do - params(possible_versions: T::Array[T::Hash[Symbol, T.untyped]]) - .returns(T.nilable(T::Hash[Symbol, T.untyped])) - end - def find_highest_compatible_version(possible_versions) - # sorted versions descending - sorted_versions = possible_versions.sort_by { |v| v.fetch(:version) }.reverse - find_compatible_version(sorted_versions) - end - - sig do - params(possible_versions: T::Array[T::Hash[Symbol, T.untyped]]) - .returns(T.nilable(T::Hash[Symbol, T.untyped])) - end - def find_lowest_compatible_version(possible_versions) - # sorted versions ascending - sorted_versions = possible_versions.sort_by { |v| v.fetch(:version) } - find_compatible_version(sorted_versions) - end - - sig do - params(sorted_versions: T::Array[T::Hash[Symbol, T.untyped]]) - .returns(T.nilable(T::Hash[Symbol, T.untyped])) - end - def find_compatible_version(sorted_versions) - # By checking the first version separately, we can avoid additional network requests - first_version = sorted_versions.first - return unless first_version - # If the current package version is incompatible, then we don't enforce compatibility. - # It could appear incompatible because they are ignoring NU1701 or the package is poorly authored. - return first_version unless version_compatible?(dependency.version) - - # once sorted by version, the best we can do is search every package, because it's entirely possible for there - # to be incompatible packages both with a higher and lower version number, so no smart searching can be done. - sorted_versions.find { |v| version_compatible?(v.fetch(:version)) } - end - - sig { params(version: T.nilable(T.any(Dependabot::Version, String))).returns(T::Boolean) } - def version_compatible?(version) - str_version_compatible?(version.to_s) - end - - sig { params(version: String).returns(T::Boolean) } - def str_version_compatible?(version) - compatibility_checker.compatible?(version) - end - - sig { returns(Dependabot::Nuget::CompatibilityChecker) } - def compatibility_checker - @compatibility_checker ||= - T.let( - CompatibilityChecker.new( - dependency_urls: dependency_urls, - dependency: dependency - ), - T.nilable(Dependabot::Nuget::CompatibilityChecker) - ) - end - - sig do - params(possible_versions: T::Array[T::Hash[Symbol, T.untyped]]) - .returns(T::Array[T::Hash[Symbol, T.untyped]]) - end - def filter_prereleases(possible_versions) - filtered = possible_versions.reject do |d| - version = d.fetch(:version) - version.prerelease? && !related_to_current_pre?(version) - end - if possible_versions.count > filtered.count - Dependabot.logger.info("Filtered out #{possible_versions.count - filtered.count} pre-release versions") - end - filtered - end - - sig do - params(possible_versions: T::Array[T::Hash[Symbol, T.untyped]]) - .returns(T::Array[T::Hash[Symbol, T.untyped]]) - end - def filter_ignored_versions(possible_versions) - filtered = possible_versions - - ignored_versions.each do |req| - ignore_req = requirement_class.new(parse_requirement_string(req)) - filtered = - filtered - .reject { |v| ignore_req.satisfied_by?(v.fetch(:version)) } - end - - if @raise_on_ignored && filter_lower_versions(filtered).empty? && - filter_lower_versions(possible_versions).any? - raise AllVersionsIgnored - end - - if possible_versions.count > filtered.count - Dependabot.logger.info("Filtered out #{possible_versions.count - filtered.count} ignored versions") - end - - filtered - end - - sig do - params(possible_versions: T::Array[T::Hash[Symbol, T.untyped]]) - .returns(T::Array[T::Hash[Symbol, T.untyped]]) - end - def filter_lower_versions(possible_versions) - return possible_versions unless dependency.numeric_version - - possible_versions.select do |v| - v.fetch(:version) > dependency.numeric_version - end - end - - sig { params(string: String).returns(T::Array[String]) } - def parse_requirement_string(string) - return [string] if string.match?(NUGET_RANGE_REGEX) - - string.split(",").map(&:strip) - end - - sig { returns(T::Array[T::Hash[Symbol, T.any(Dependabot::Version, String, NilClass)]]) } - def available_v3_versions - v3_nuget_listings.flat_map do |listing| - listing - .fetch("versions", []) - .map do |v| - listing_details = listing.fetch("listing_details") - nuspec_url = listing_details - .fetch(:versions_url, nil) - &.gsub(/index\.json$/, "#{v}/#{sanitized_name}.nuspec") - - { - version: version_class.new(v), - nuspec_url: nuspec_url, - source_url: nil, - repo_url: listing_details.fetch(:repository_url) - } - end - end - end - - sig { returns(T::Array[T::Hash[Symbol, T.any(Dependabot::Version, String, NilClass)]]) } - def available_v2_versions - v2_nuget_listings.flat_map do |listing| - body = listing.fetch("xml_body", []) - doc = Nokogiri::XML(body) - doc.remove_namespaces! - - doc.xpath("/feed/entry").filter_map do |entry| - listed = entry.at_xpath("./properties/Listed")&.content&.strip - next if listed&.casecmp("false")&.zero? - - entry_details = dependency_details_from_v2_entry(entry) - entry_details.merge( - repo_url: listing.fetch("listing_details") - .fetch(:repository_url) - ) - end - end - end - - sig do - params(entry: Nokogiri::XML::Element) - .returns(T::Hash[Symbol, T.any(Dependabot::Version, String, NilClass)]) - end - def dependency_details_from_v2_entry(entry) - version = entry.at_xpath("./properties/Version").content.strip - source_urls = [] - [ - entry.at_xpath("./properties/ProjectUrl")&.content, - entry.at_xpath("./properties/ReleaseNotes")&.content - ].compact.join(" ").scan(Source::SOURCE_REGEX) do - source_urls << Regexp.last_match.to_s - end - - source_url = source_urls.find { |url| Source.from_url(url) } - source_url = Source.from_url(source_url)&.url if source_url - - { - version: version_class.new(version), - nuspec_url: nil, - source_url: source_url - } - end - - # rubocop:disable Metrics/PerceivedComplexity - sig { params(version: Dependabot::Version).returns(T::Boolean) } - def related_to_current_pre?(version) - current_version = dependency.numeric_version - if current_version&.prerelease? && - current_version.release == version.release - return true - end - - dependency.requirements.any? do |req| - reqs = parse_requirement_string(req.fetch(:requirement) || "") - return true if reqs.any?("*-*") - next unless reqs.any? { |r| r.include?("-") } - - requirement_class - .requirements_array(req.fetch(:requirement)) - .any? do |r| - r.requirements.any? { |a| a.last.release == version.release } - end - rescue Gem::Requirement::BadRequirementError - false - end - end - # rubocop:enable Metrics/PerceivedComplexity - - sig { returns(T::Array[T::Hash[String, T.untyped]]) } - def v3_nuget_listings - @v3_nuget_listings ||= - T.let( - dependency_urls - .select { |details| details.fetch(:repository_type) == "v3" } - .filter_map do |url_details| - versions = NugetClient.get_package_versions(dependency.name, url_details) - next unless versions - - { "versions" => versions, "listing_details" => url_details } - end, - T.nilable(T::Array[T::Hash[String, T.untyped]]) - ) - end - - sig { returns(T::Array[T::Hash[String, T.untyped]]) } - def v2_nuget_listings - @v2_nuget_listings ||= - T.let( - dependency_urls - .select { |details| details.fetch(:repository_type) == "v2" } - .flat_map { |url_details| fetch_paginated_v2_nuget_listings(url_details) } - .filter_map do |url_details, response| - next unless response.status == 200 - - { - "xml_body" => response.body, - "listing_details" => url_details - } - end, - T.nilable(T::Array[T::Hash[String, T.untyped]]) - ) - end - - sig do - params( - url_details: T::Hash[Symbol, T.untyped], - results: T::Hash[T::Hash[Symbol, T.untyped], Excon::Response] - ) - .returns(T::Array[T::Array[T.untyped]]) - end - def fetch_paginated_v2_nuget_listings(url_details, results = {}) - response = Dependabot::RegistryClient.get( - url: url_details[:versions_url], - headers: url_details[:auth_header] - ) - - # NOTE: Short circuit if we get a circular next link - return results.to_a if results.key?(url_details) - - results[url_details] = response - - if (link_href = fetch_v2_next_link_href(response.body)) - url_details = url_details.dup - # Some Nuget repositories, such as JFrog's Artifactory, URL encode the "next" href - # link in the paged results. If the href is not URL decoded, the paging parameters - # are ignored and the first page is always returned. - url_details[:versions_url] = CGI.unescape(link_href) - fetch_paginated_v2_nuget_listings(url_details, results) - end - - results.to_a - end - - sig { params(xml_body: String).returns(T.nilable(String)) } - def fetch_v2_next_link_href(xml_body) - doc = Nokogiri::XML(xml_body) - doc.remove_namespaces! - link_node = doc.xpath("/feed/link").find do |node| - rel = node.attribute("rel").value.strip - rel == "next" - end - link_node.attribute("href").value.strip if link_node - rescue Nokogiri::XML::XPath::SyntaxError - nil - end - - sig { returns(T::Array[T::Hash[Symbol, T.untyped]]) } - def dependency_urls - @dependency_urls ||= - T.let( - RepositoryFinder.new( - dependency: dependency, - credentials: credentials, - config_files: nuget_configs - ).dependency_urls, - T.nilable(T::Array[T::Hash[Symbol, T.untyped]]) - ) - end - - sig { returns(T::Array[Dependabot::DependencyFile]) } - def nuget_configs - @nuget_configs ||= - T.let( - dependency_files.select { |f| f.name.match?(/nuget\.config$/i) }, - T.nilable(T::Array[Dependabot::DependencyFile]) - ) - end - - sig { returns(String) } - def sanitized_name - dependency.name.downcase - end - - sig { returns(T.class_of(Gem::Version)) } - def version_class - dependency.version_class - end - - sig { returns(T.class_of(Dependabot::Requirement)) } - def requirement_class - dependency.requirement_class - end - - sig { returns(T::Hash[Symbol, Integer]) } - def excon_options - # For large JSON files we sometimes need a little longer than for - # other languages. For example, see: - # https://dotnet.myget.org/F/aspnetcore-dev/api/v3/query? - # q=microsoft.aspnetcore.mvc&prerelease=true&semVerLevel=2.0.0 - { - connect_timeout: 30, - write_timeout: 30, - read_timeout: 30 - } - end - end - # rubocop:enable Metrics/ClassLength - end - end -end diff --git a/nuget/spec/dependabot/nuget/file_parser_spec.rb b/nuget/spec/dependabot/nuget/file_parser_spec.rb index 783f685e4b8..00c3ff1bd7c 100644 --- a/nuget/spec/dependabot/nuget/file_parser_spec.rb +++ b/nuget/spec/dependabot/nuget/file_parser_spec.rb @@ -551,7 +551,7 @@ expect(Dependabot.logger).to have_received(:info).with( <<~INFO Discovery JSON content: { - "FilePath": "", + "Path": "#{repo_contents_path}", "IsSuccess": true, "Projects": [ { diff --git a/nuget/spec/dependabot/nuget/native_helpers_spec.rb b/nuget/spec/dependabot/nuget/native_helpers_spec.rb index ab94a84df05..e83a2cbbf6e 100644 --- a/nuget/spec/dependabot/nuget/native_helpers_spec.rb +++ b/nuget/spec/dependabot/nuget/native_helpers_spec.rb @@ -102,7 +102,6 @@ lib_path, "--exclude", except_path, - "--verify-no-changes", "-v", "diag" ].join(" ") diff --git a/nuget/spec/dependabot/nuget/nuget_client_spec.rb b/nuget/spec/dependabot/nuget/nuget_client_spec.rb deleted file mode 100644 index bfe148b1ccc..00000000000 --- a/nuget/spec/dependabot/nuget/nuget_client_spec.rb +++ /dev/null @@ -1,137 +0,0 @@ -# typed: false -# frozen_string_literal: true - -require "spec_helper" -require "dependabot/nuget/nuget_client" - -RSpec.describe Dependabot::Nuget::NugetClient do - describe "#get_package_versions" do - let(:dependency_name) { "Some.Dependency" } - - subject(:package_versions) do - Dependabot::Nuget::NugetClient.get_package_versions(dependency_name, repository_details) - end - - context "package versions from local" do - let(:repository_details) do - nuget_dir = File.join(File.dirname(__FILE__), "..", "..", "fixtures", "nuget_responses", "local_repo") - base_url = File.expand_path(nuget_dir) - - { - base_url: base_url, - repository_type: "local" - } - end - - it "expects to crawl the directory" do - expect(package_versions).to eq(Set["1.0.0", "1.1.0"]) - end - end - - context "package versions _might_ have the `listed` flag" do - before do - stub_request(:get, "https://api.nuget.org/v3/registration5-gz-semver2/#{dependency_name.downcase}/index.json") - .to_return( - status: 200, - body: { - items: [ - items: [ - { - catalogEntry: { - listed: true, # nuget.org provides this flag and it should be honored - version: "0.1.0" - } - }, - { - catalogEntry: { - listed: false, # if this is ever false, the package should not be included - version: "0.1.1" - } - }, - { - catalogEntry: { - # e.g., github doesn't have the `listed` flag, but should still be returned - version: "0.1.2" - } - } - ] - ] - }.to_json - ) - end - - let(:repository_details) do - { - base_url: "https://api.nuget.org/v3-flatcontainer/", - registration_url: "https://api.nuget.org/v3/registration5-gz-semver2/#{dependency_name.downcase}/index.json", - repository_url: "https://api.nuget.org/v3/index.json", - versions_url: "https://api.nuget.org/v3-flatcontainer/" \ - "#{dependency_name.downcase}/index.json", - search_url: "https://azuresearch-usnc.nuget.org/query" \ - "?q=#{dependency_name.downcase}&prerelease=true&semVerLevel=2.0.0", - auth_header: {}, - repository_type: "v3" - } - end - - it "returns the correct version information" do - expect(package_versions).to eq(Set["0.1.0", "0.1.2"]) - end - end - - context "versions can be retrieved from v2 apis" do - before do - stub_request(:get, "https://www.nuget.org/api/v2/FindPackagesById()?id=%27Some.Dependency%27") - .to_return( - status: 200, - body: - <<~XML - - - - - Some.Dependency - - - 1.0.0.0 - - - - - Some.Dependency - - 1.1.0.0 - - - - - Some.Dependency.But.The.Wrong.One - - 1.2.0.0 - - - - - XML - ) - end - - let(:repository_details) do - { - base_url: "https://www.nuget.org/api/v2", - repository_url: "https://www.nuget.org/api/v2", - versions_url: "https://www.nuget.org/api/v2/FindPackagesById()?id='#{dependency_name}'", - auth_header: {}, - repository_type: "v2" - } - end - - it "returns the correct version information" do - expect(package_versions).to eq(Set["1.0.0.0", "1.1.0.0"]) - end - end - end -end diff --git a/nuget/spec/dependabot/nuget/update_checker/compatibility_checker_spec.rb b/nuget/spec/dependabot/nuget/update_checker/compatibility_checker_spec.rb deleted file mode 100644 index 55d5ac05e42..00000000000 --- a/nuget/spec/dependabot/nuget/update_checker/compatibility_checker_spec.rb +++ /dev/null @@ -1,259 +0,0 @@ -# typed: false -# frozen_string_literal: true - -require "spec_helper" -require "dependabot/dependency" -require "dependabot/nuget/file_parser" -require "dependabot/nuget/update_checker/compatibility_checker" -require "dependabot/nuget/update_checker/repository_finder" -require "dependabot/nuget/update_checker/tfm_finder" - -RSpec.describe Dependabot::Nuget::CompatibilityChecker do - subject(:checker) do - Dependabot::Nuget::FileParser.new(dependency_files: dependency_files, - source: source, - repo_contents_path: repo_contents_path).parse - described_class.new( - dependency_urls: dependency_urls, - dependency: dependency - ) - end - let(:repo_contents_path) { write_tmp_repo(dependency_files) } - let(:source) do - Dependabot::Source.new( - provider: "github", - repo: "gocardless/bump", - directory: "/" - ) - end - let(:dependency_urls) do - Dependabot::Nuget::RepositoryFinder.new( - dependency: dependency, - credentials: credentials, - config_files: [] - ).dependency_urls - end - - let(:credentials) do - [{ - "type" => "nuget_feed", - "url" => "https://api.nuget.org/v3/index.json", - "token" => "my:passw0rd" - }] - end - - let(:dependency) do - Dependabot::Dependency.new( - name: dependency_name, - version: dependency_version, - requirements: dependency_requirements, - package_manager: "nuget" - ) - end - - let(:dependency_name) { "Microsoft.AppCenter.Crashes" } - let(:dependency_version) { "5.0.2" } - let(:dependency_requirements) do - [{ file: "my.csproj", requirement: "5.0.2", groups: ["dependencies"], source: nil }] - end - - let(:dependency_files) { [csproj] } - let(:csproj) do - Dependabot::DependencyFile.new(name: "my.csproj", content: csproj_body) - end - let(:csproj_body) do - <<~XML - - - uap10.0.16299 - - - - - - XML - end - - context "#compatible?" do - subject(:compatible) { checker.compatible?(version) } - - before do - stub_request(:get, "https://api.nuget.org/v3/registration5-gz-semver2/microsoft.appcenter.crashes/index.json") - .to_return( - status: 200, - body: { - items: [ - items: [ - { - catalogEntry: { - listed: true, - version: "5.0.2" - } - }, - { - catalogEntry: { - listed: true, - version: "5.0.3" - } - } - ] - ] - }.to_json - ) - end - - context "when the `.nuspec` reports itself as a development dependency, but still has regular dependencies" do - let(:csproj_body) do - <<~XML - - - net6.0 - - - - - - XML - end - - before do - nuspec502 = - <<~XML - - - Microsoft.AppCenter.Crashes - 5.0.2 - true - - - - - - - XML - nuspec503 = nuspec502.gsub("5.0.2", "5.0.3") - nuspec601 = nuspec502.gsub("5.0.2", "6.0.1").gsub("net6.0", "net8.0") - stub_request(:get, "https://api.nuget.org/v3-flatcontainer/microsoft.appcenter.crashes/5.0.2/microsoft.appcenter.crashes.nuspec") - .to_return( - status: 200, - body: nuspec502 - ) - stub_request(:get, "https://api.nuget.org/v3-flatcontainer/microsoft.appcenter.crashes/5.0.3/microsoft.appcenter.crashes.nuspec") - .to_return( - status: 200, - body: nuspec503 - ) - stub_request(:get, "https://api.nuget.org/v3-flatcontainer/microsoft.appcenter.crashes/6.0.1/microsoft.appcenter.crashes.nuspec") - .to_return( - status: 200, - body: nuspec601 - ) - end - - context "with a targetFramework compatible version" do - let(:version) { "5.0.3" } - - it "returns the correct data" do - expect(compatible).to be_truthy - end - end - - context "with a targetFramework non-compatible version" do - let(:version) { "6.0.1" } - - it "returns the correct data" do - expect(compatible).to be_falsey - end - end - end - - context "when the `.nuspec` has groups without a `targetFramework` attribute" do - let(:version) { "5.0.3" } - - before do - stub_request(:get, "https://api.nuget.org/v3-flatcontainer/microsoft.appcenter.crashes/5.0.2/microsoft.appcenter.crashes.nuspec") - .to_return( - status: 200, - body: fixture("nuspecs", "Microsoft.AppCenter.Crashes_faked.nuspec") - ) - stub_request(:get, "https://api.nuget.org/v3-flatcontainer/microsoft.appcenter.crashes/5.0.3/microsoft.appcenter.crashes.nuspec") - .to_return( - status: 200, - body: fixture("nuspecs", "Microsoft.AppCenter.Crashes_faked.nuspec") - ) - end - - it "returns the correct data" do - expect(compatible).to be_truthy - end - end - - context "when the `.nupkg` zip object contains an empty `lib/` entry" do - def create_nupkg_with_lib_contents(package_name, nuspec_contents, lib_subdirectories) - content = Zip::OutputStream.write_buffer do |zio| - zio.put_next_entry("#{package_name}.nuspec") - zio.write(nuspec_contents) - - zio.put_next_entry("lib/") # some zip files have an empty directory object entry - - lib_subdirectories.each do |lib| - zio.put_next_entry("lib/#{lib}/_._") - zio.write("fake contents") - end - end - content.rewind - content.sysread - end - - let(:csproj_body) do - <<~XML - - - net481 - - - - - - XML - end - - before do - nuspec_xml = - <<~XML - - - Microsoft.AppCenter.Crashes - 5.0.2 - - - - - - - XML - stub_request(:get, "https://api.nuget.org/v3-flatcontainer/microsoft.appcenter.crashes/5.0.2/microsoft.appcenter.crashes.nuspec") - .to_return( - status: 200, - body: nuspec_xml - ) - stub_request(:get, "https://api.nuget.org/v3-flatcontainer/microsoft.appcenter.crashes/5.0.2/microsoft.appcenter.crashes.5.0.2.nupkg") - .to_return( - status: 200, - body: create_nupkg_with_lib_contents(dependency_name, nuspec_xml, ["net45"]) - ) - end - - context "checks the `.nupkg` contents" do - let(:version) { "5.0.2" } - - it "returns the correct data" do - expect(compatible).to be_truthy - end - end - end - end -end diff --git a/nuget/spec/dependabot/nuget/update_checker/dependency_finder_spec.rb b/nuget/spec/dependabot/nuget/update_checker/dependency_finder_spec.rb deleted file mode 100644 index ba0431fcd5e..00000000000 --- a/nuget/spec/dependabot/nuget/update_checker/dependency_finder_spec.rb +++ /dev/null @@ -1,98 +0,0 @@ -# typed: false -# frozen_string_literal: true - -require "spec_helper" -require "dependabot/dependency" -require "dependabot/dependency_file" -require "dependabot/nuget/update_checker/dependency_finder" - -RSpec.describe Dependabot::Nuget::UpdateChecker::DependencyFinder do - subject(:finder) do - described_class.new( - dependency: dependency, - dependency_files: dependency_files, - credentials: credentials, - repo_contents_path: "test/repo" - ) - end - let(:dependency) do - Dependabot::Dependency.new( - name: dependency_name, - version: dependency_version, - requirements: dependency_requirements, - package_manager: "nuget" - ) - end - - let(:dependency_requirements) do - [{ file: "my.csproj", requirement: "1.1.1", groups: ["dependencies"], source: nil }] - end - let(:dependency_name) { "Microsoft.Extensions.DependencyModel" } - let(:dependency_version) { "1.1.1" } - - let(:dependency_files) { [csproj] } - let(:csproj) do - Dependabot::DependencyFile.new(name: "my.csproj", content: csproj_body) - end - let(:csproj_body) { fixture("csproj", "basic.csproj") } - - let(:credentials) do - [{ - "type" => "git_source", - "host" => "github.com", - "username" => "x-access-token", - "password" => "token" - }] - end - - # Can get transitive dependencies - describe "#transitive_dependencies", :vcr do - subject(:transitive_dependencies) { finder.transitive_dependencies } - - its(:length) { is_expected.to eq(34) } - end - - context "api.nuget.org is not hit if it's not in the NuGet.Config" do - let(:dependency_version) { "42.42.42" } - let(:nuget_config_body) { fixture("configs", "example.com_nuget.config") } - let(:nuget_config) { Dependabot::DependencyFile.new(name: "NuGet.Config", content: nuget_config_body) } - let(:dependency_files) { [csproj, nuget_config] } - subject(:transitive_dependencies) { finder.transitive_dependencies } - - def create_nupkg(nuspec_name, nuspec_fixture_path) - content = Zip::OutputStream.write_buffer do |zio| - zio.put_next_entry("#{nuspec_name}.nuspec") - zio.write(fixture("nuspecs", nuspec_fixture_path)) - end - content.rewind - content.sysread - end - - before(:context) do - disallowed_urls = %w( - https://api.nuget.org/v3/index.json - https://api.nuget.org/v3-flatcontainer/microsoft.extensions.dependencymodel/42.42.42/microsoft.extensions.dependencymodel.nuspec - https://api.nuget.org/v3-flatcontainer/microsoft.netcore.platforms/43.43.43/microsoft.netcore.platforms.nuspec - ) - - disallowed_urls.each do |url| - stub_request(:get, url) - .to_raise(StandardError.new("Not allowed to query `#{url}`")) - end - - stub_request(:get, "https://nuget.example.com/v3/index.json") - .to_return(status: 200, body: fixture("nuget_responses", "example_index.json")) - stub_request(:get, "https://api.example.com/v3-flatcontainer/microsoft.extensions.dependencymodel/42.42.42/microsoft.extensions.dependencymodel.42.42.42.nupkg") - .to_return(status: 200, body: create_nupkg("Microsoft.Extensions.DependencyModel", - "Microsoft.Extensions.DependencyModel_42.42.42_faked.nuspec")) - stub_request(:get, "https://api.example.com/v3-flatcontainer/microsoft.netcore.platforms/43.43.43/microsoft.netcore.platforms.43.43.43.nupkg") - .to_return(status: 200, body: create_nupkg("Microsoft.NETCore.Platforms", - "Microsoft.NETCore.Platforms_43.43.43_faked.nuspec")) - end - - # this test doesn't really care about the dependency count, we just need to ensure that `api.nuget.org` wasn't hit - its(:length) do - is_expected.to eq(1) - end - end -end diff --git a/nuget/spec/dependabot/nuget/update_checker/nupkg_fetcher_spec.rb b/nuget/spec/dependabot/nuget/update_checker/nupkg_fetcher_spec.rb deleted file mode 100644 index 52c86eba852..00000000000 --- a/nuget/spec/dependabot/nuget/update_checker/nupkg_fetcher_spec.rb +++ /dev/null @@ -1,257 +0,0 @@ -# typed: false -# frozen_string_literal: true - -require "spec_helper" -require "dependabot/dependency" -require "dependabot/dependency_file" -require "dependabot/nuget/update_checker/nupkg_fetcher" -require "dependabot/nuget/update_checker/repository_finder" - -RSpec.describe Dependabot::Nuget::NupkgFetcher do - describe "#fetch_nupkg_url_from_repository" do - let(:dependency) { Dependabot::Dependency.new(name: package_name, requirements: [], package_manager: "nuget") } - let(:package_name) { "Newtonsoft.Json" } - let(:package_version) { "13.0.1" } - let(:credentials) { [] } - let(:config_files) { [nuget_config] } - let(:nuget_config) do - Dependabot::DependencyFile.new( - name: "NuGet.config", - content: nuget_config_content - ) - end - let(:nuget_config_content) do - <<~XML - - - - - - - - XML - end - let(:repository_finder) do - Dependabot::Nuget::RepositoryFinder.new(dependency: dependency, credentials: credentials, - config_files: config_files) - end - let(:repository_details) { repository_finder.dependency_urls.first } - subject(:nupkg_url) do - described_class.fetch_nupkg_url_from_repository(repository_details, package_name, package_version) - end - - context "with a nuget feed url" do - let(:feed_url) { "https://api.nuget.org/v3/index.json" } - - before do - stub_request(:get, feed_url) - .to_return( - status: 200, - body: fixture("nuget_responses", "index.json", "nuget.index.json") - ) - end - - it { is_expected.to eq("https://api.nuget.org/v3-flatcontainer/newtonsoft.json/13.0.1/newtonsoft.json.13.0.1.nupkg") } - end - - context "with an azure feed url" do - let(:feed_url) { "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public/nuget/v3/index.json" } - - before do - stub_request(:get, feed_url) - .to_return( - status: 200, - body: fixture("nuget_responses", "index.json", "dotnet-public.index.json") - ) - end - - it { is_expected.to eq("https://pkgs.dev.azure.com/dnceng/9ee6d478-d288-47f7-aacc-f6e6d082ae6d/_packaging/45bacae2-5efb-47c8-91e5-8ec20c22b4f8/nuget/v3/flat2/newtonsoft.json/13.0.1/newtonsoft.json.13.0.1.nupkg") } - end - - context "with a github feed url" do - let(:feed_url) { "https://nuget.pkg.github.com/some-namespace/index.json" } - - before do - stub_request(:get, feed_url) - .to_return( - status: 200, - body: fixture("nuget_responses", "index.json", "github.index.json") - ) - end - - it { is_expected.to eq("https://nuget.pkg.github.com/some-namespace/download/newtonsoft.json/13.0.1/newtonsoft.json.13.0.1.nupkg") } - end - - context "with a v2 feed url" do - let(:feed_url) { "https://www.nuget.org/api/v2" } - - before do - stub_request(:get, feed_url) - .to_return( - status: 200, - body: - +<<~XML - - - - Packages - - - - XML - ) - stub_request(:get, "https://www.nuget.org/api/v2/Packages(Id='Newtonsoft.Json',Version='13.0.1')") - .to_return( - status: 200, - body: - <<~XML - - - - - XML - ) - end - - it { is_expected.to eq("https://www.nuget.org/api/v2/Download/Newtonsoft.Json/13.0.1") } - end - - context "from a v3 feed that doesn't specify `PackageBaseAddress`" do - let(:feed_url) { "https://nuget.example.com/v3-without-package-base/index.json" } - - before do - # initial `index.json` response; only provides `SearchQueryService` and not `PackageBaseAddress` - stub_request(:get, feed_url) - .to_return( - status: 200, - body: { - version: "3.0.0", - resources: [ - { - "@id" => "https://nuget.example.com/query", - "@type" => "SearchQueryService" - } - ] - }.to_json - ) - # SearchQueryService - stub_request(:get, "https://nuget.example.com/query?q=newtonsoft.json&prerelease=true&semVerLevel=2.0.0") - .to_return( - status: 200, - body: { - totalHits: 2, - data: [ - # this is a false match - { - registration: "not-used", - version: "42.42.42", - versions: [ - { - version: "1.0.0", - "@id" => "not-used" - }, - { - version: "42.42.42", - "@id" => "not-used" - } - ], - id: "Newtonsoft.Json.False.Match" - }, - # this is the real one - { - registration: "not-used", - version: "13.0.1", - versions: [ - { - version: "12.0.1", - "@id" => "https://nuget.example.com/registration/newtonsoft.json/12.0.1.json" - }, - { - version: "13.0.1", - "@id" => "https://nuget.example.com/registration/newtonsoft.json/13.0.1.json" - } - ], - id: "Newtonsoft.Json" - } - ] - }.to_json - ) - # registration content - stub_request(:get, "https://nuget.example.com/registration/newtonsoft.json/13.0.1.json") - .to_return( - status: 200, - body: { - listed: true, - packageContent: "https://nuget.example.com/nuget-local/Download/newtonsoft.json.13.0.1.nupkg", - registration: "not-used", - "@id" => "not-used" - }.to_json - ) - end - - it { is_expected.to eq("https://nuget.example.com/nuget-local/Download/newtonsoft.json.13.0.1.nupkg") } - end - end - - describe "#fetch_nupkg_buffer" do - let(:package_id) { "Newtonsoft.Json" } - let(:package_version) { "13.0.1" } - let(:repository_details) { Dependabot::Nuget::RepositoryFinder.get_default_repository_details(package_id) } - let(:dependency_urls) { [repository_details] } - subject(:nupkg_buffer) do - described_class.fetch_nupkg_buffer(dependency_urls, package_id, package_version) - end - - before do - stub_request(:get, "https://api.nuget.org/v3-flatcontainer/newtonsoft.json/13.0.1/newtonsoft.json.13.0.1.nupkg") - .to_return( - status: 301, - headers: { - "Location" => "https://api.nuget.org/redirect-on-301" - }, - body: "redirecting on 301" - ) - stub_request(:get, "https://api.nuget.org/redirect-on-301") - .to_return( - status: 302, - headers: { - "Location" => "https://api.nuget.org/redirect-on-302" - }, - body: "redirecting on 302" - ) - stub_request(:get, "https://api.nuget.org/redirect-on-302") - .to_return( - status: 303, - headers: { - "Location" => "https://api.nuget.org/redirect-on-303" - }, - body: "redirecting on 303" - ) - stub_request(:get, "https://api.nuget.org/redirect-on-303") - .to_return( - status: 307, - headers: { - "Location" => "https://api.nuget.org/redirect-on-307" - }, - body: "redirecting on 307" - ) - stub_request(:get, "https://api.nuget.org/redirect-on-307") - .to_return( - status: 308, - headers: { - "Location" => "https://api.nuget.org/redirect-on-308" - }, - body: "redirecting on 308" - ) - stub_request(:get, "https://api.nuget.org/redirect-on-308") - .to_return( - status: 200, - body: "the final contents" - ) - end - - it "fetches the nupkg after multiple redirects" do - expect(nupkg_buffer.to_s).to eq("the final contents") - end - end -end diff --git a/nuget/spec/dependabot/nuget/update_checker/nuspec_fetcher_spec.rb b/nuget/spec/dependabot/nuget/update_checker/nuspec_fetcher_spec.rb deleted file mode 100644 index d81a191eed4..00000000000 --- a/nuget/spec/dependabot/nuget/update_checker/nuspec_fetcher_spec.rb +++ /dev/null @@ -1,55 +0,0 @@ -# typed: false -# frozen_string_literal: true - -require "spec_helper" -require "dependabot/dependency" -require "dependabot/dependency_file" -require "dependabot/nuget/update_checker/nuspec_fetcher" - -RSpec.describe Dependabot::Nuget::NuspecFetcher do - describe "#feed_supports_nuspec_download?" do - context "when checking with a azure feed url" do - let(:url) { "https://pkgs.dev.azure.com/dependabot/dependabot-test/_packaging/dependabot-feed/nuget/v3/index.json" } - subject(:result) { described_class.feed_supports_nuspec_download?(url) } - - it { is_expected.to be_truthy } - end - - context "when checking with a azure feed url (no project)" do - let(:url) { "https://pkgs.dev.azure.com/dependabot/_packaging/dependabot-feed/nuget/v3/index.json" } - subject(:result) { described_class.feed_supports_nuspec_download?(url) } - - it { is_expected.to be_truthy } - end - - context "when checking with a visual studio feed url" do - let(:url) { "https://dynamicscrm.pkgs.visualstudio.com/_packaging/CRM.Engineering/nuget/v3/index.json" } - subject(:result) { described_class.feed_supports_nuspec_download?(url) } - - it { is_expected.to be_truthy } - end - - context "when checking with the nuget.org feed url" do - let(:url) { "https://api.nuget.org/v3/index.json" } - subject(:result) { described_class.feed_supports_nuspec_download?(url) } - - it { is_expected.to be_truthy } - end - - context "when checking with github feed url" do - let(:url) { "https://nuget.pkg.github.com/some_namespace/index.json" } - subject(:result) { described_class.feed_supports_nuspec_download?(url) } - - it { is_expected.to be_falsy } - end - end - - describe "remove_invalid_characters" do - context "when a utf-16 bom is present" do - let(:response_body) { "\xFE\xFF" } - subject(:result) { described_class.remove_invalid_characters(response_body) } - - it { is_expected.to eq("") } - end - end -end diff --git a/nuget/spec/dependabot/nuget/update_checker/repository_finder_spec.rb b/nuget/spec/dependabot/nuget/update_checker/repository_finder_spec.rb deleted file mode 100644 index cfa420097d3..00000000000 --- a/nuget/spec/dependabot/nuget/update_checker/repository_finder_spec.rb +++ /dev/null @@ -1,988 +0,0 @@ -# typed: false -# frozen_string_literal: true - -require "spec_helper" -require "dependabot/dependency" -require "dependabot/dependency_file" -require "dependabot/nuget/update_checker/repository_finder" -require_relative "../nuget_search_stubs" - -RSpec.describe Dependabot::Nuget::RepositoryFinder do - RSpec.configure do |config| - config.include(NuGetSearchStubs) - end - - subject(:finder) do - described_class.new( - dependency: dependency, - credentials: credentials, - config_files: [config_file].compact - ) - end - let(:config_file) { nil } - let(:credentials) do - [{ - "type" => "git_source", - "host" => "github.com", - "username" => "x-access-token", - "password" => "token" - }] - end - let(:dependency) do - Dependabot::Dependency.new( - name: "Microsoft.Extensions.DependencyModel", - version: "1.1.1", - requirements: [{ - requirement: "1.1.1", - file: "my.csproj", - groups: ["dependencies"], - source: nil - }], - package_manager: "nuget" - ) - end - - describe "local path in NuGet.Config" do - let(:config_file) do - nuget_config_content = <<~XML - - - - - - - - - - XML - Dependabot::DependencyFile.new( - name: "NuGet.Config", - content: nuget_config_content, - directory: "some/directory" - ) - end - - subject(:known_repositories) { finder.known_repositories } - - it "finds all local paths" do - urls = known_repositories.map { |r| r[:url] } - expected = [ - "/some/directory/SomePath", - "/some/directory/RelativePath", - "/AbsolutePath", - "https://nuget.example.com/index.json" - ] - expect(urls).to match_array(expected) - end - end - - describe "environment variables in NuGet.Config" do - let(:config_file) do - nuget_config_content = <<~XML - - - - - - - - - - - - - XML - Dependabot::DependencyFile.new( - name: "NuGet.Config", - content: nuget_config_content - ) - end - - subject(:known_repositories) { finder.known_repositories } - - context "are expanded" do - before do - allow(Dependabot.logger).to receive(:warn) - ENV["FEED_URL"] = "https://nuget.example.com/index.json" - ENV["THIS_VARIBLE_EXISTS"] = "replacement-text" - ENV.delete("THIS_VARIABLE_DOES_NOT") - end - - it "contains the expected values and warns on unavailable" do - repo = known_repositories[0] - expect(repo[:url]).to eq("https://nuget.example.com/index.json") - expect(repo[:token]).to eq("user:(head)replacement-text(mid)%THIS_VARIABLE_DOES_NOT%(tail)") - expect(Dependabot.logger).to have_received(:warn).with( - <<~WARN - The variable '%THIS_VARIABLE_DOES_NOT%' could not be expanded in NuGet.Config - WARN - ) - end - - after do - ENV.delete("THIS_VARIBLE_EXISTS") - ENV.delete("THIS_VARIABLE_DOES_NOT") - end - end - end - - describe "dependency_urls" do - subject(:dependency_urls) { finder.dependency_urls } - - it "gets the right URL without making any requests" do - expect(dependency_urls).to eq( - [{ - base_url: "https://api.nuget.org/v3-flatcontainer/", - registration_url: "https://api.nuget.org/v3/registration5-gz-semver2/" \ - "microsoft.extensions.dependencymodel/index.json", - repository_url: "https://api.nuget.org/v3/index.json", - versions_url: "https://api.nuget.org/v3-flatcontainer/" \ - "microsoft.extensions.dependencymodel/index.json", - search_url: "https://azuresearch-usnc.nuget.org/query" \ - "?q=microsoft.extensions.dependencymodel" \ - "&prerelease=true&semVerLevel=2.0.0", - auth_header: {}, - repository_type: "v3" - }] - ) - end - - context "with a URL passed as a credential" do - let(:custom_repo_url) do - "https://www.myget.org/F/exceptionless/api/v3/index.json" - end - let(:credentials) do - [{ - "type" => "git_source", - "host" => "github.com", - "username" => "x-access-token", - "password" => "token" - }, { - "type" => "nuget_feed", - "url" => custom_repo_url, - "token" => "my:passw0rd" - }] - end - - before do - stub_request(:get, custom_repo_url) - .with(basic_auth: %w(my passw0rd)) - .to_return( - status: 200, - body: fixture("nuget_responses", "myget_base.json") - ) - end - - it "gets the right URL" do - expect(dependency_urls).to eq( - [{ - base_url: "https://www.myget.org/F/exceptionless/api/v3/flatcontainer/", - registration_url: "https://www.myget.org/F/exceptionless/api/v3/registration1/" \ - "microsoft.extensions.dependencymodel/index.json", - repository_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "index.json", - versions_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "flatcontainer/microsoft.extensions." \ - "dependencymodel/index.json", - search_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "query?q=microsoft.extensions.dependencymodel" \ - "&prerelease=true&semVerLevel=2.0.0", - auth_header: { "Authorization" => "Basic bXk6cGFzc3cwcmQ=" }, - repository_type: "v3" - }] - ) - end - - context "that does not return PackageBaseAddress" do - let(:custom_repo_url) { "http://localhost:8082/artifactory/api/nuget/v3/nuget-local" } - before do - stub_request(:get, custom_repo_url) - .to_return( - status: 200, - body: fixture("nuget_responses", "artifactory_base.json") - ) - end - - it "gets the right URL" do - expect(dependency_urls).to eq( - [{ - base_url: nil, - registration_url: "http://localhost:8081/artifactory/api/nuget/v3/" \ - "dependabot-nuget-local/registration/microsoft.extensions.dependencymodel/index.json", - repository_url: custom_repo_url, - search_url: "http://localhost:8081/artifactory/api/nuget/v3/" \ - "dependabot-nuget-local/query?q=microsoft.extensions.dependencymodel" \ - "&prerelease=true&semVerLevel=2.0.0", - auth_header: { "Authorization" => "Basic bXk6cGFzc3cwcmQ=" }, - repository_type: "v3" - }] - ) - end - end - - context "that has URLs that need to be escaped" do - let(:custom_repo_url) { "https://www.myget.org/F/exceptionless/api with spaces/v3/index.json" } - before do - stub_request(:get, "https://www.myget.org/F/exceptionless/api%20with%20spaces/v3/index.json") - .to_return( - status: 200, - body: fixture("nuget_responses", "myget_base.json") - ) - end - - it "gets the right URL" do - expect(dependency_urls).to eq( - [{ - base_url: "https://www.myget.org/F/exceptionless/api/v3/flatcontainer/", - registration_url: "https://www.myget.org/F/exceptionless/api/v3/registration1/" \ - "microsoft.extensions.dependencymodel/index.json", - repository_url: "https://www.myget.org/F/exceptionless/api%20with%20spaces/v3/index.json", - versions_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "flatcontainer/microsoft.extensions." \ - "dependencymodel/index.json", - search_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "query?q=microsoft.extensions.dependencymodel" \ - "&prerelease=true&semVerLevel=2.0.0", - auth_header: { "Authorization" => "Basic bXk6cGFzc3cwcmQ=" }, - repository_type: "v3" - }] - ) - end - end - - context "that 404s" do - before { stub_request(:get, custom_repo_url).to_return(status: 404) } - - # TODO: Might want to raise here instead? - it { is_expected.to eq([]) } - end - - context "that 403s" do - before { stub_request(:get, custom_repo_url).to_return(status: 403) } - - it "raises a useful error" do - error_class = Dependabot::PrivateSourceAuthenticationFailure - expect { finder.dependency_urls } - .to raise_error do |error| - expect(error).to be_a(error_class) - expect(error.source).to eq(custom_repo_url) - end - end - end - - context "without a token" do - let(:credentials) do - [{ - "type" => "git_source", - "host" => "github.com", - "username" => "x-access-token", - "password" => "token" - }, { - "type" => "nuget_feed", - "url" => custom_repo_url - }] - end - - before do - stub_request(:get, custom_repo_url) - .with(basic_auth: nil) - .to_return( - status: 200, - body: fixture("nuget_responses", "myget_base.json") - ) - end - - it "gets the right URL" do - expect(dependency_urls).to eq( - [{ - base_url: "https://www.myget.org/F/exceptionless/api/v3/flatcontainer/", - registration_url: "https://www.myget.org/F/exceptionless/api/v3/registration1/" \ - "microsoft.extensions.dependencymodel/index.json", - repository_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "index.json", - versions_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "flatcontainer/microsoft.extensions." \ - "dependencymodel/index.json", - search_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "query?q=microsoft.extensions.dependencymodel" \ - "&prerelease=true&semVerLevel=2.0.0", - auth_header: {}, - repository_type: "v3" - }] - ) - end - end - end - - context "with a URL included in the nuget.config" do - let(:config_file) do - Dependabot::DependencyFile.new( - name: "NuGet.Config", - content: fixture("configs", config_file_fixture_name) - ) - end - let(:config_file_fixture_name) { "nuget.config" } - - before do - repo_url = "https://www.myget.org/F/exceptionless/api/v3/index.json" - stub_request(:get, repo_url).to_return(status: 404) - stub_request(:get, repo_url) - .with(basic_auth: %w(my passw0rd)) - .to_return( - status: 200, - body: fixture("nuget_responses", "myget_base.json") - ) - end - - # skipped - # it "gets the right URLs" do - # expect(dependency_urls).to match_array( - # [{ - # repository_url: "https://api.nuget.org/v3/index.json", - # versions_url: "https://api.nuget.org/v3-flatcontainer/" \ - # "microsoft.extensions.dependencymodel/index.json", - # search_url: "https://azuresearch-usnc.nuget.org/query" \ - # "?q=microsoft.extensions.dependencymodel" \ - # "&prerelease=true&semVerLevel=2.0.0", - # auth_header: {}, - # repository_type: "v3" - # }, { - # repository_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - # "index.json", - # versions_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - # "flatcontainer/microsoft.extensions." \ - # "dependencymodel/index.json", - # search_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - # "query?q=microsoft.extensions.dependencymodel" \ - # "&prerelease=true&semVerLevel=2.0.0", - # auth_header: { "Authorization" => "Basic bXk6cGFzc3cwcmQ=" }, - # repository_type: "v3" - # }] - # ) - # end - - context "include the default repository" do - let(:config_file_fixture_name) { "include_default_disable_ext_sources.config" } - - it "with disable external source" do - expect(dependency_urls).to match_array( - [{ - base_url: "https://www.myget.org/F/exceptionless/api/v3/flatcontainer/", - repository_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "index.json", - registration_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "registration1/microsoft.extensions.dependencymodel/index.json", - versions_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "flatcontainer/microsoft.extensions." \ - "dependencymodel/index.json", - search_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "query?q=microsoft.extensions.dependencymodel" \ - "&prerelease=true&semVerLevel=2.0.0", - auth_header: { "Authorization" => "Basic bXk6cGFzc3cwcmQ=" }, - repository_type: "v3" - }, { - base_url: "https://api.nuget.org/v3-flatcontainer/", - registration_url: "https://api.nuget.org/v3/registration5-gz-semver2/" \ - "microsoft.extensions.dependencymodel/index.json", - repository_url: "https://api.nuget.org/v3/index.json", - versions_url: "https://api.nuget.org/v3-flatcontainer/" \ - "microsoft.extensions.dependencymodel/index.json", - search_url: "https://azuresearch-usnc.nuget.org/query" \ - "?q=microsoft.extensions.dependencymodel" \ - "&prerelease=true&semVerLevel=2.0.0", - auth_header: {}, - repository_type: "v3" - }] - ) - end - end - - context "that overrides the default package sources" do - let(:config_file_fixture_name) { "override_def_source_with_same_key.config" } - - before do - repo_url = "https://www.myget.org/F/exceptionless/api/v3/index.json" - stub_request(:get, repo_url) - .to_return( - status: 200, - body: fixture("nuget_responses", "myget_base.json") - ) - end - - it "when the default api key of default registry is provided without clear" do - expect(dependency_urls).to match_array( - [{ - base_url: "https://www.myget.org/F/exceptionless/api/v3/flatcontainer/", - registration_url: "https://www.myget.org/F/exceptionless/api/v3/registration1/" \ - "microsoft.extensions.dependencymodel/index.json", - repository_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "index.json", - versions_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "flatcontainer/microsoft.extensions." \ - "dependencymodel/index.json", - search_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "query?q=microsoft.extensions.dependencymodel" \ - "&prerelease=true&semVerLevel=2.0.0", - auth_header: {}, - repository_type: "v3" - }] - ) - end - - let(:config_file_fixture_name) { "override_def_source_with_same_key_default.config" } - it "when the default api key of default registry is provided with clear" do - expect(dependency_urls).to match_array( - [{ - base_url: "https://www.myget.org/F/exceptionless/api/v3/flatcontainer/", - registration_url: "https://www.myget.org/F/exceptionless/api/v3/registration1/" \ - "microsoft.extensions.dependencymodel/index.json", - repository_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "index.json", - versions_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "flatcontainer/microsoft.extensions." \ - "dependencymodel/index.json", - search_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "query?q=microsoft.extensions.dependencymodel" \ - "&prerelease=true&semVerLevel=2.0.0", - auth_header: {}, - repository_type: "v3" - }] - ) - end - end - - context "that doesn't include the default repository" do - let(:config_file_fixture_name) { "excludes_default.config" } - - it "still includes the default repository (as it wasn't cleared)" do - expect(dependency_urls).to match_array( - [{ - base_url: "https://api.nuget.org/v3-flatcontainer/", - registration_url: "https://api.nuget.org/v3/registration5-gz-semver2/" \ - "microsoft.extensions.dependencymodel/index.json", - repository_url: "https://api.nuget.org/v3/index.json", - versions_url: "https://api.nuget.org/v3-flatcontainer/" \ - "microsoft.extensions.dependencymodel/index.json", - search_url: "https://azuresearch-usnc.nuget.org/query" \ - "?q=microsoft.extensions.dependencymodel" \ - "&prerelease=true&semVerLevel=2.0.0", - auth_header: {}, - repository_type: "v3" - }, { - base_url: "https://www.myget.org/F/exceptionless/api/v3/flatcontainer/", - registration_url: "https://www.myget.org/F/exceptionless/api/v3/registration1/" \ - "microsoft.extensions.dependencymodel/index.json", - repository_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "index.json", - versions_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "flatcontainer/microsoft.extensions." \ - "dependencymodel/index.json", - search_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "query?q=microsoft.extensions.dependencymodel" \ - "&prerelease=true&semVerLevel=2.0.0", - auth_header: { "Authorization" => "Basic bXk6cGFzc3cwcmQ=" }, - repository_type: "v3" - }] - ) - end - - context "and clears it" do - let(:config_file_fixture_name) { "clears_default.config" } - - it "still excludes the default repository" do - expect(dependency_urls).to match_array( - [{ - base_url: "https://www.myget.org/F/exceptionless/api/v3/flatcontainer/", - registration_url: "https://www.myget.org/F/exceptionless/api/v3/registration1/" \ - "microsoft.extensions.dependencymodel/index.json", - repository_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "index.json", - versions_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "flatcontainer/microsoft.extensions." \ - "dependencymodel/index.json", - search_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "query?q=microsoft.extensions.dependencymodel" \ - "&prerelease=true&semVerLevel=2.0.0", - auth_header: { "Authorization" => "Basic bXk6cGFzc3cwcmQ=" }, - repository_type: "v3" - }] - ) - end - end - - context "that has disabled package sources" do - let(:config_file_fixture_name) { "disabled_sources.config" } - - it "only includes the enabled package sources" do - expect(dependency_urls).to match_array( - [{ - base_url: "https://www.myget.org/F/exceptionless/api/v3/flatcontainer/", - registration_url: "https://www.myget.org/F/exceptionless/api/v3/registration1/" \ - "microsoft.extensions.dependencymodel/index.json", - repository_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "index.json", - versions_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "flatcontainer/microsoft.extensions." \ - "dependencymodel/index.json", - search_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "query?q=microsoft.extensions.dependencymodel" \ - "&prerelease=true&semVerLevel=2.0.0", - auth_header: { "Authorization" => "Basic bXk6cGFzc3cwcmQ=" }, - repository_type: "v3" - }] - ) - end - end - - context "that has disabled default package sources" do - let(:config_file_fixture_name) { "disabled_default_sources.config" } - - it "only includes the enable package sources" do - expect(dependency_urls).to match_array( - [{ - base_url: "https://www.myget.org/F/exceptionless/api/v3/flatcontainer/", - registration_url: "https://www.myget.org/F/exceptionless/api/v3/registration1/" \ - "microsoft.extensions.dependencymodel/index.json", - repository_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "index.json", - versions_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "flatcontainer/microsoft.extensions." \ - "dependencymodel/index.json", - search_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "query?q=microsoft.extensions.dependencymodel" \ - "&prerelease=true&semVerLevel=2.0.0", - auth_header: { "Authorization" => "Basic bXk6cGFzc3cwcmQ=" }, - repository_type: "v3" - }] - ) - end - end - end - - context "that has a numeric key" do - let(:config_file_fixture_name) { "numeric_key.config" } - - before do - repo_url = "https://www.myget.org/F/exceptionless/api/v3/index.json" - stub_request(:get, repo_url) - .to_return( - status: 200, - body: fixture("nuget_responses", "myget_base.json") - ) - end - - it "gets the right URLs" do - expect(dependency_urls).to match_array( - [{ - base_url: "https://www.myget.org/F/exceptionless/api/v3/flatcontainer/", - registration_url: "https://www.myget.org/F/exceptionless/api/v3/registration1/" \ - "microsoft.extensions.dependencymodel/index.json", - repository_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "index.json", - versions_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "flatcontainer/microsoft.extensions." \ - "dependencymodel/index.json", - search_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "query?q=microsoft.extensions.dependencymodel" \ - "&prerelease=true&semVerLevel=2.0.0", - auth_header: {}, - repository_type: "v3" - }] - ) - end - end - - context "that only provides versioned `SearchQueryService`` entries" do - let(:config_file_fixture_name) { "versioned_search.config" } - - before do - repo_url = "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-libraries/nuget/v3/index.json" - stub_request(:get, repo_url) - .to_return( - status: 200, - body: fixture("nuget_responses", "index.json", "versioned_SearchQueryService.index.json") - ) - end - - it "gets the right URLs" do - expect(dependency_urls).to match_array( - [{ - base_url: "https://pkgs.dev.azure.com/dnceng/9ee6d478-d288-47f7-aacc-f6e6d082ae6d/_packaging/516521bf-6417-457e-9a9c-0a4bdfde03e7/nuget/v3/flat2/", - registration_url: "https://pkgs.dev.azure.com/dnceng/9ee6d478-d288-47f7-aacc-f6e6d082ae6d/_packaging/516521bf-6417-457e-9a9c-0a4bdfde03e7/nuget/v3/registrations2/microsoft.extensions.dependencymodel/index.json", - repository_url: "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-libraries/nuget/v3/index.json", - versions_url: "https://pkgs.dev.azure.com/dnceng/9ee6d478-d288-47f7-aacc-f6e6d082ae6d/_packaging/516521bf-6417-457e-9a9c-0a4bdfde03e7/nuget/v3/flat2/microsoft.extensions.dependencymodel/index.json", - search_url: "https://pkgs.dev.azure.com/dnceng/9ee6d478-d288-47f7-aacc-f6e6d082ae6d/_packaging/516521bf-6417-457e-9a9c-0a4bdfde03e7/nuget/v3/query2/?q=microsoft.extensions.dependencymodel&prerelease=true&semVerLevel=2.0.0", - auth_header: {}, - repository_type: "v3" - }] - ) - end - end - - context "includes repositories in the `trustedSigners` section" do - let(:config_file_fixture_name) { "with_trustedSigners.config" } - - before do - repo_url = "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-libraries/nuget/v3/index.json" - stub_request(:get, repo_url) - .to_return( - status: 200, - body: fixture("nuget_responses", "index.json", "versioned_SearchQueryService.index.json") - ) - end - - it "gets the right URLs" do - expect(dependency_urls).to eq( - [{ - base_url: "https://api.nuget.org/v3-flatcontainer/", - registration_url: "https://api.nuget.org/v3/registration5-gz-semver2/" \ - "microsoft.extensions.dependencymodel/index.json", - repository_url: "https://api.nuget.org/v3/index.json", - versions_url: "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.dependencymodel/index.json", - search_url: "https://azuresearch-usnc.nuget.org/query?q=microsoft.extensions.dependencymodel&prerelease=true&semVerLevel=2.0.0", - auth_header: {}, - repository_type: "v3" - }, - { - base_url: "https://pkgs.dev.azure.com/dnceng/9ee6d478-d288-47f7-aacc-f6e6d082ae6d/_packaging/516521bf-6417-457e-9a9c-0a4bdfde03e7/nuget/v3/flat2/", - registration_url: "https://pkgs.dev.azure.com/dnceng/9ee6d478-d288-47f7-aacc-f6e6d082ae6d/_packaging/516521bf-6417-457e-9a9c-0a4bdfde03e7/nuget/v3/registrations2/microsoft.extensions.dependencymodel/index.json", - repository_url: "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-libraries/nuget/v3/index.json", - versions_url: "https://pkgs.dev.azure.com/dnceng/9ee6d478-d288-47f7-aacc-f6e6d082ae6d/_packaging/516521bf-6417-457e-9a9c-0a4bdfde03e7/nuget/v3/flat2/microsoft.extensions.dependencymodel/index.json", - search_url: "https://pkgs.dev.azure.com/dnceng/9ee6d478-d288-47f7-aacc-f6e6d082ae6d/_packaging/516521bf-6417-457e-9a9c-0a4bdfde03e7/nuget/v3/query2/?q=microsoft.extensions.dependencymodel&prerelease=true&semVerLevel=2.0.0", - auth_header: {}, - repository_type: "v3" - }] - ) - end - end - - context "with GitHub packages url" do - let(:config_file_fixture_name) { "github.nuget.config" } - - before do - repo_url = "https://nuget.pkg.github.com/some-namespace/index.json" - stub_request(:get, repo_url) - .to_return( - status: 200, - body: fixture("nuget_responses", "index.json", "github.index.json") - ) - end - - it "gets the right URLs" do - expect(dependency_urls).to eq( - [{ - base_url: "https://nuget.pkg.github.com/some-namespace/download", - registration_url: "https://nuget.pkg.github.com/some-namespace/microsoft.extensions.dependencymodel/index.json", - repository_url: "https://nuget.pkg.github.com/some-namespace/index.json", - versions_url: "https://nuget.pkg.github.com/some-namespace/download/microsoft.extensions.dependencymodel/index.json", - search_url: "https://nuget.pkg.github.com/some-namespace/query?q=microsoft.extensions.dependencymodel&prerelease=true&semVerLevel=2.0.0", - auth_header: {}, - repository_type: "v3" - }] - ) - end - end - - context "that has a non-ascii key" do - let(:config_file_fixture_name) { "non_ascii_key.config" } - - before do - repo_url = "https://www.myget.org/F/exceptionless/api/v3/index.json" - stub_request(:get, repo_url) - .to_return( - status: 200, - body: fixture("nuget_responses", "myget_base.json") - ) - end - - it "gets the right URLs" do - expect(dependency_urls).to match_array( - [{ - base_url: "https://www.myget.org/F/exceptionless/api/v3/flatcontainer/", - registration_url: "https://www.myget.org/F/exceptionless/api/v3/registration1/" \ - "microsoft.extensions.dependencymodel/index.json", - repository_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "index.json", - versions_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "flatcontainer/microsoft.extensions." \ - "dependencymodel/index.json", - search_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "query?q=microsoft.extensions.dependencymodel" \ - "&prerelease=true&semVerLevel=2.0.0", - auth_header: {}, - repository_type: "v3" - }] - ) - end - end - - context "that uses the v2 API alongside the v3 API" do - let(:config_file_fixture_name) { "with_v2_endpoints.config" } - - before do - v2_repo_urls = %w( - https://www.nuget.org/api/v2/ - https://www.myget.org/F/azure-appservice/api/v2 - https://www.myget.org/F/azure-appservice-staging/api/v2 - https://www.myget.org/F/fusemandistfeed/api/v2 - https://www.myget.org/F/30de4ee06dd54956a82013fa17a3accb/ - ) - - v2_repo_urls.each do |repo_url| - stub_request(:get, repo_url) - .to_return( - status: 200, - body: fixture("nuget_responses", "v2_base.xml") - ) - end - - url = "https://dotnet.myget.org/F/aspnetcore-dev/api/v3/index.json" - stub_request(:get, url) - .to_return( - status: 200, - body: fixture("nuget_responses", "myget_base.json") - ) - end - - it "gets the right URLs" do - expect(dependency_urls).to match_array( - [{ - base_url: "https://www.myget.org/F/exceptionless/api/v3/flatcontainer/", - registration_url: "https://www.myget.org/F/exceptionless/api/v3/registration1/" \ - "microsoft.extensions.dependencymodel/index.json", - repository_url: - "https://dotnet.myget.org/F/aspnetcore-dev/api/v3/index.json", - versions_url: - "https://www.myget.org/F/exceptionless/api/v3/" \ - "flatcontainer/microsoft.extensions.dependencymodel/index.json", - search_url: - "https://www.myget.org/F/exceptionless/api/v3/" \ - "query?q=microsoft.extensions.dependencymodel&prerelease=true&semVerLevel=2.0.0", - auth_header: {}, - repository_type: "v3" - }, { - base_url: "https://www.nuget.org/api/v2", - repository_url: "https://www.nuget.org/api/v2", - versions_url: - "https://www.nuget.org/api/v2/FindPackagesById()?id=" \ - "'Microsoft.Extensions.DependencyModel'", - auth_header: {}, - repository_type: "v2" - }] - ) - end - end - - context "that has no base url in v2 API response" do - let(:config_file_fixture_name) { "with_v2_endpoints.config" } - - before do - v2_repo_urls = %w( - https://www.nuget.org/api/v2/ - https://www.myget.org/F/azure-appservice/api/v2 - https://www.myget.org/F/azure-appservice-staging/api/v2 - https://www.myget.org/F/fusemandistfeed/api/v2 - https://www.myget.org/F/30de4ee06dd54956a82013fa17a3accb/ - ) - - v2_repo_urls.each do |repo_url| - stub_request(:get, repo_url) - .to_return( - status: 200, - body: fixture("nuget_responses", "v2_no_base.xml") - ) - end - - url = "https://dotnet.myget.org/F/aspnetcore-dev/api/v3/index.json" - stub_request(:get, url) - .to_return( - status: 200, - body: fixture("nuget_responses", "myget_base.json") - ) - end - - it "gets the right URLs" do - expect(dependency_urls).to match_array( - [{ - base_url: "https://www.myget.org/F/exceptionless/api/v3/flatcontainer/", - registration_url: "https://www.myget.org/F/exceptionless/api/v3/registration1/" \ - "microsoft.extensions.dependencymodel/index.json", - repository_url: - "https://dotnet.myget.org/F/aspnetcore-dev/api/v3/index.json", - versions_url: - "https://www.myget.org/F/exceptionless/api/v3/" \ - "flatcontainer/microsoft.extensions.dependencymodel/index.json", - search_url: - "https://www.myget.org/F/exceptionless/api/v3/" \ - "query?q=microsoft.extensions.dependencymodel&prerelease=true&semVerLevel=2.0.0", - auth_header: {}, - repository_type: "v3" - }, { - base_url: "https://www.nuget.org/api/v2/", - repository_url: "https://www.nuget.org/api/v2/", - versions_url: - "https://www.nuget.org/api/v2/FindPackagesById()?id=" \ - "'Microsoft.Extensions.DependencyModel'", - auth_header: {}, - repository_type: "v2" - }, { - base_url: "https://www.myget.org/F/azure-appservice/api/v2", - repository_url: "https://www.myget.org/F/azure-appservice/api/v2", - versions_url: - "https://www.myget.org/F/azure-appservice/api/v2/" \ - "FindPackagesById()?id=" \ - "'Microsoft.Extensions.DependencyModel'", - auth_header: {}, - repository_type: "v2" - }, { - base_url: "https://www.myget.org/F/azure-appservice-staging/api/v2", - repository_url: - "https://www.myget.org/F/azure-appservice-staging/api/v2", - versions_url: - "https://www.myget.org/F/azure-appservice-staging/api/v2/" \ - "FindPackagesById()?id=" \ - "'Microsoft.Extensions.DependencyModel'", - auth_header: {}, - repository_type: "v2" - }, { - base_url: "https://www.myget.org/F/fusemandistfeed/api/v2", - repository_url: "https://www.myget.org/F/fusemandistfeed/api/v2", - versions_url: - "https://www.myget.org/F/fusemandistfeed/api/v2/" \ - "FindPackagesById()?id=" \ - "'Microsoft.Extensions.DependencyModel'", - auth_header: {}, - repository_type: "v2" - }, { - base_url: "https://www.myget.org/F/30de4ee06dd54956a82013fa17a3accb/", - repository_url: - "https://www.myget.org/F/30de4ee06dd54956a82013fa17a3accb/", - versions_url: - "https://www.myget.org/F/30de4ee06dd54956a82013fa17a3accb/" \ - "FindPackagesById()?id=" \ - "'Microsoft.Extensions.DependencyModel'", - auth_header: {}, - repository_type: "v2" - }] - ) - end - end - - context "matching `packageSourceMapping` entries are honored" do - let(:config_file) do - nuget_config_content = <<~XML - - - - - - - - - - - - - - - - - - - - XML - Dependabot::DependencyFile.new( - name: "NuGet.Config", - content: nuget_config_content - ) - end - - before do - # `source1` and `source3` should never be queried - stub_index_json("https://nuget.example.com/source2/index.json") - end - - it "matches on the best pattern" do - expect(dependency_urls).to match_array( - [{ - base_url: "https://nuget.example.com/source2/PackageBaseAddress", - registration_url: "https://nuget.example.com/source2/RegistrationsBaseUrl/microsoft.extensions.dependencymodel/index.json", - repository_url: "https://nuget.example.com/source2/index.json", - versions_url: "https://nuget.example.com/source2/PackageBaseAddress/microsoft.extensions.dependencymodel/index.json", - search_url: "https://nuget.example.com/source2/SearchQueryService?q=microsoft.extensions.dependencymodel&prerelease=true&semVerLevel=2.0.0", - auth_header: {}, - repository_type: "v3" - }] - ) - end - end - - context "non-matching `packageSourceMapping` entries are ignored" do - let(:config_file) do - nuget_config_content = <<~XML - - - - - - - - - - - - - - - - - - - - XML - Dependabot::DependencyFile.new( - name: "NuGet.Config", - content: nuget_config_content - ) - end - - before do - # all sources will need to be queried - stub_index_json("https://nuget.example.com/source1/index.json") - stub_index_json("https://nuget.example.com/source2/index.json") - stub_index_json("https://nuget.example.com/source3/index.json") - end - - it "returns all sources" do - expect(dependency_urls).to match_array( - [{ - base_url: "https://nuget.example.com/source1/PackageBaseAddress", - registration_url: "https://nuget.example.com/source1/RegistrationsBaseUrl/microsoft.extensions.dependencymodel/index.json", - repository_url: "https://nuget.example.com/source1/index.json", - versions_url: "https://nuget.example.com/source1/PackageBaseAddress/microsoft.extensions.dependencymodel/index.json", - search_url: "https://nuget.example.com/source1/SearchQueryService?q=microsoft.extensions.dependencymodel&prerelease=true&semVerLevel=2.0.0", - auth_header: {}, - repository_type: "v3" - }, { - base_url: "https://nuget.example.com/source2/PackageBaseAddress", - registration_url: "https://nuget.example.com/source2/RegistrationsBaseUrl/microsoft.extensions.dependencymodel/index.json", - repository_url: "https://nuget.example.com/source2/index.json", - versions_url: "https://nuget.example.com/source2/PackageBaseAddress/microsoft.extensions.dependencymodel/index.json", - search_url: "https://nuget.example.com/source2/SearchQueryService?q=microsoft.extensions.dependencymodel&prerelease=true&semVerLevel=2.0.0", - auth_header: {}, - repository_type: "v3" - }, { - base_url: "https://nuget.example.com/source3/PackageBaseAddress", - registration_url: "https://nuget.example.com/source3/RegistrationsBaseUrl/microsoft.extensions.dependencymodel/index.json", - repository_url: "https://nuget.example.com/source3/index.json", - versions_url: "https://nuget.example.com/source3/PackageBaseAddress/microsoft.extensions.dependencymodel/index.json", - search_url: "https://nuget.example.com/source3/SearchQueryService?q=microsoft.extensions.dependencymodel&prerelease=true&semVerLevel=2.0.0", - auth_header: {}, - repository_type: "v3" - }] - ) - end - end - end - end -end diff --git a/nuget/spec/dependabot/nuget/update_checker/requirements_updater_spec.rb b/nuget/spec/dependabot/nuget/update_checker/requirements_updater_spec.rb index 097d1b09113..1833279b291 100644 --- a/nuget/spec/dependabot/nuget/update_checker/requirements_updater_spec.rb +++ b/nuget/spec/dependabot/nuget/update_checker/requirements_updater_spec.rb @@ -8,8 +8,7 @@ let(:updater) do described_class.new( requirements: requirements, - latest_version: latest_version, - source_details: source_details + latest_version: latest_version ) end @@ -114,26 +113,12 @@ file: "my.csproj", requirement: "23.6-jre", groups: ["dependencies"], - source: { - type: "nuget_repo", - url: "https://api.nuget.org/v3/index.json", - source_url: nil, - nuspec_url: "https://api.nuget.org/v3-flatcontainer/" \ - "microsoft.extensions.dependencymodel/1.2.3/" \ - "microsoft.extensions.dependencymodel.nuspec" - } + source: nil }, { file: "another/my.csproj", requirement: "[23.6-jre]", groups: ["dependencies"], - source: { - type: "nuget_repo", - url: "https://api.nuget.org/v3/index.json", - source_url: nil, - nuspec_url: "https://api.nuget.org/v3-flatcontainer/" \ - "microsoft.extensions.dependencymodel/1.2.3/" \ - "microsoft.extensions.dependencymodel.nuspec" - } + source: nil }] ) end @@ -147,14 +132,7 @@ file: "my.csproj", requirement: "23.6-jre", groups: ["dependencies"], - source: { - type: "nuget_repo", - url: "https://api.nuget.org/v3/index.json", - source_url: nil, - nuspec_url: "https://api.nuget.org/v3-flatcontainer/" \ - "microsoft.extensions.dependencymodel/1.2.3/" \ - "microsoft.extensions.dependencymodel.nuspec" - } + source: nil }, { file: "another/my.csproj", requirement: "[23.0,)", diff --git a/nuget/spec/dependabot/nuget/update_checker/tfm_finder_spec.rb b/nuget/spec/dependabot/nuget/update_checker/tfm_finder_spec.rb deleted file mode 100644 index c3e0ba00665..00000000000 --- a/nuget/spec/dependabot/nuget/update_checker/tfm_finder_spec.rb +++ /dev/null @@ -1,56 +0,0 @@ -# typed: false -# frozen_string_literal: true - -require "spec_helper" -require "dependabot/dependency" -require "dependabot/dependency_file" -require "dependabot/nuget/file_parser" -require "dependabot/nuget/update_checker/tfm_finder" - -RSpec.describe Dependabot::Nuget::TfmFinder do - let(:project_name) { "tfm_finder" } - let(:dependency_files) { nuget_project_dependency_files(project_name, directory: "/").reverse } - let(:repo_contents_path) { nuget_build_tmp_repo(project_name) } - let(:source) do - Dependabot::Source.new( - provider: "github", - repo: "gocardless/bump", - directory: "/" - ) - end - let(:dependency) do - Dependabot::Dependency.new( - name: dependency_name, - version: dependency_version, - requirements: dependency_requirements, - package_manager: "nuget" - ) - end - - subject(:frameworks) do - Dependabot::Nuget::FileParser.new(dependency_files: dependency_files, - source: source, - repo_contents_path: repo_contents_path).parse - Dependabot::Nuget::TfmFinder.frameworks(dependency) - end - - describe "#frameworks" do - context "when checking for a transitive dependency" do - let(:dependency_requirements) { [] } - let(:dependency_name) { "Microsoft.Extensions.DependencyModel" } - let(:dependency_version) { "1.1.1" } - - its(:length) { is_expected.to eq(2) } - end - - context "when checking for a top-level dependency" do - let(:dependency_requirements) do - [{ file: "my.csproj", requirement: "2.3.0", groups: ["dependencies"], source: nil }] - end - let(:dependency_name) { "Serilog" } - let(:dependency_version) { "2.3.0" } - - its(:length) { is_expected.to eq(1) } - end - end -end diff --git a/nuget/spec/dependabot/nuget/update_checker/version_finder_spec.rb b/nuget/spec/dependabot/nuget/update_checker/version_finder_spec.rb deleted file mode 100644 index 2230a9fa3b1..00000000000 --- a/nuget/spec/dependabot/nuget/update_checker/version_finder_spec.rb +++ /dev/null @@ -1,695 +0,0 @@ -# typed: false -# frozen_string_literal: true - -require "spec_helper" -require "dependabot/dependency" -require "dependabot/dependency_file" -require "dependabot/nuget/file_parser" -require "dependabot/nuget/update_checker/version_finder" -require "dependabot/nuget/update_checker/tfm_comparer" -require_relative "../nuget_search_stubs" - -RSpec.describe Dependabot::Nuget::UpdateChecker::VersionFinder do - RSpec.configure do |config| - config.include(NuGetSearchStubs) - end - - let(:repo_contents_path) { write_tmp_repo(dependency_files) } - let(:source) do - Dependabot::Source.new( - provider: "github", - repo: "gocardless/bump", - directory: "/" - ) - end - - let(:finder) do - Dependabot::Nuget::FileParser.new(dependency_files: dependency_files, - source: source, - repo_contents_path: repo_contents_path).parse - 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, - repo_contents_path: repo_contents_path - ) - end - - let(:dependency) do - Dependabot::Dependency.new( - name: dependency_name, - version: dependency_version, - requirements: dependency_requirements, - package_manager: "nuget" - ) - end - - let(:dependency_requirements) do - [{ file: "my.csproj", requirement: "1.1.1", groups: ["dependencies"], source: nil }] - end - let(:dependency_name) { "Microsoft.Extensions.DependencyModel" } - let(:dependency_version) { "1.1.1" } - - let(:dependency_files) { [csproj] } - let(:csproj) do - Dependabot::DependencyFile.new(name: "my.csproj", content: csproj_body) - end - let(:csproj_body) { fixture("csproj", "basic.csproj") } - - let(:credentials) do - [{ - "type" => "git_source", - "host" => "github.com", - "username" => "x-access-token", - "password" => "token" - }] - end - let(:ignored_versions) { [] } - let(:raise_on_ignored) { false } - let(:security_advisories) { [] } - - let(:nuget_versions_url) do - "https://api.nuget.org/v3-flatcontainer/" \ - "microsoft.extensions.dependencymodel/index.json" - end - let(:nuget_search_url) do - "https://api.nuget.org/v3/registration5-gz-semver2/" \ - "microsoft.extensions.dependencymodel/index.json" - end - let(:version_class) { Dependabot::Nuget::Version } - let(:nuget_versions) { fixture("nuget_responses", "versions.json") } - let(:nuget_search_results) do - fixture("nuget_responses", "search_results.json") - end - let(:nuspec) do - fixture("nuspecs", "#{dependency_name}.#{dependency_version}.nuspec") - end - - let(:nuspec_url) do - "https://api.nuget.org/v3-flatcontainer/#{dependency_name.downcase}/#{dependency_version}/#{dependency_name.downcase}.nuspec" - end - - let(:version_instance) do - version_class.new(dependency_version) - end - - let(:expected_version_instance) do - version_class.new(expected_version) - end - - before do - stub_request(:get, nuget_versions_url) - .to_return(status: 200, body: nuget_versions) - stub_request(:get, nuget_search_url) - .to_return(status: 200, body: nuget_search_results) - end - - describe "#latest_version_details" do - subject(:latest_version_details) { finder.latest_version_details } - - let(:expected_version) { "2.1.0" } - let(:current_compatible) { true } - let(:expected_compatible) { true } - - before do - allow(finder).to receive(:str_version_compatible?).with(dependency_version.to_s).and_return(current_compatible) - allow(finder).to receive(:str_version_compatible?).with(expected_version.to_s).and_return(expected_compatible) - end - - its([:version]) { is_expected.to eq(expected_version_instance) } - - context "when the returned versions is prefixed with a zero-width char" do - let(:nuget_search_results) do - fixture("nuget_responses", "search_results_zero_width.json") - end - - its([:version]) { is_expected.to eq(expected_version_instance) } - end - - context "when the user wants a pre-release" do - let(:dependency_version) { "2.2.0-preview1-26216-03" } - let(:expected_version) { "2.2.0-preview2-26406-04" } - - its([:version]) do - is_expected.to eq(expected_version_instance) - end - - context "for a previous version" do - let(:dependency_version) { "2.1.0-preview1-26216-03" } - let(:expected_version) { "2.1.0" } - - its([:version]) do - is_expected.to eq(expected_version_instance) - end - end - end - - context "when the user wants a pre-release with wildcard" do - let(:dependency_version) { "*-*" } - let(:current_compatible) { false } - let(:dependency_requirements) do - [{ file: "my.csproj", requirement: "*-*", groups: ["dependencies"], source: nil }] - end - its([:version]) do - is_expected.to eq(version_class.new("2.2.0-preview2-26406-04")) - end - end - - context "when the user is using an unfound property" do - let(:dependency_version) { "$PackageVersion_LibGit2SharpNativeBinaries" } - its([:version]) { is_expected.to eq(version_class.new("2.1.0")) } - end - - context "raise_on_ignored when later versions are allowed" do - let(:raise_on_ignored) { true } - it "doesn't raise an error" do - expect { subject }.to_not raise_error - end - end - - context "when the user is on the latest version" do - let(:dependency_version) { "2.1.0" } - its([:version]) { is_expected.to eq(version_class.new("2.1.0")) } - - context "raise_on_ignored" do - let(:raise_on_ignored) { true } - it "doesn't raise an error" do - expect { subject }.to_not raise_error - end - end - end - - context "when the current version isn't known" do - let(:dependency_version) { nil } - let(:current_compatible) { false } - let(:expected_version) { nil } - let(:expected_compatible) { false } - - context "raise_on_ignored" do - let(:raise_on_ignored) { true } - it "doesn't raise an error" do - expect { subject }.to_not raise_error - end - end - end - - context "when the dependency is a git dependency" do - let(:dependency_version) { "a1b78a929dac93a52f08db4f2847d76d6cfe39bd" } - - context "raise_on_ignored" do - let(:raise_on_ignored) { true } - it "doesn't raise an error" do - expect { subject }.to_not raise_error - end - end - end - - context "when the user is ignoring all later versions" do - let(:ignored_versions) { ["> 1.1.1"] } - its([:version]) { is_expected.to eq(version_class.new("1.1.1")) } - - context "raise_on_ignored" do - let(:raise_on_ignored) { true } - it "raises an error" do - expect { subject }.to raise_error(Dependabot::AllVersionsIgnored) - end - end - end - - context "when the user is ignoring the latest version" do - let(:ignored_versions) { ["[2.a,3.0.0)"] } - let(:expected_version) { "1.1.2" } - its([:version]) { is_expected.to eq(expected_version_instance) } - end - - context "when a version range is specified using Ruby syntax" do - let(:ignored_versions) { [">= 2.a, < 3.0.0"] } - let(:expected_version) { "1.1.2" } - its([:version]) { is_expected.to eq(version_class.new("1.1.2")) } - end - - context "when the user has ignored all versions" do - let(:ignored_versions) { ["[0,)"] } - it "returns nil" do - expect(subject).to be_nil - end - - context "raise_on_ignored" do - let(:raise_on_ignored) { true } - it "raises an error" do - expect { subject }.to raise_error(Dependabot::AllVersionsIgnored) - end - end - end - - context "when an open version range is specified using Ruby syntax" do - let(:ignored_versions) { ["> 0"] } - it "returns nil" do - expect(subject).to be_nil - end - - context "raise_on_ignored" do - let(:raise_on_ignored) { true } - it "raises an error" do - expect { subject }.to raise_error(Dependabot::AllVersionsIgnored) - end - end - end - - context "with a custom repo in a nuget.config file" do - let(:config_file) do - Dependabot::DependencyFile.new( - name: "NuGet.Config", - content: fixture("configs", "nuget.config") - ) - end - let(:dependency_files) { [csproj, config_file] } - let(:custom_repo_url) do - "https://www.myget.org/F/exceptionless/api/v3/index.json" - end - let(:custom_nuget_search_url) do - "https://www.myget.org/F/exceptionless/api/v3/" \ - "registration1/microsoft.extensions.dependencymodel/index.json" - end - before do - stub_request(:get, nuget_versions_url).to_return(status: 404) - stub_request(:get, nuget_search_url).to_return(status: 404) - - stub_request(:get, custom_repo_url).to_return(status: 404) - stub_request(:get, custom_repo_url) - .with(basic_auth: %w(my passw0rd)) - .to_return( - status: 200, - body: fixture("nuget_responses", "myget_base.json") - ) - stub_request(:get, custom_nuget_search_url).to_return(status: 404) - stub_request(:get, custom_nuget_search_url) - .with(basic_auth: %w(my passw0rd)) - .to_return(status: 200, body: nuget_search_results) - end - - # skipped - # its([:version]) { is_expected.to eq(version_class.new("2.1.0")) } - - context "that uses the v2 API" do - let(:config_file) do - Dependabot::DependencyFile.new( - name: "NuGet.Config", - content: fixture("configs", "with_v2_endpoints.config") - ) - end - - let(:custom_v3_nuget_versions_url) do - "https://www.myget.org/F/exceptionless/api/v3/flatcontainer/" \ - "#{dependency_name}/index.json" - end - - let(:expected_version) { "4.8.1" } - - before do - v2_repo_urls = %w( - https://www.nuget.org/api/v2/ - https://www.myget.org/F/azure-appservice/api/v2 - https://www.myget.org/F/azure-appservice-staging/api/v2 - https://www.myget.org/F/fusemandistfeed/api/v2 - https://www.myget.org/F/30de4ee06dd54956a82013fa17a3accb/ - ) - - v2_repo_urls.each do |repo_url| - stub_request(:get, repo_url) - .to_return( - status: 200, - body: fixture("nuget_responses", "v2_base.xml") - ) - end - - url = "https://dotnet.myget.org/F/aspnetcore-dev/api/v3/index.json" - stub_request(:get, url) - .to_return( - status: 200, - body: fixture("nuget_responses", "myget_base.json") - ) - - stub_request(:get, custom_v3_nuget_versions_url) - .to_return(status: 404) - - custom_v2_nuget_versions_url = - "https://www.nuget.org/api/v2/FindPackagesById()?id=" \ - "'#{dependency_name}'" - stub_request(:get, custom_v2_nuget_versions_url) - .to_return( - status: 200, - body: fixture("nuget_responses", "v2_versions.xml") - ) - end - - its([:version]) { is_expected.to eq(expected_version_instance) } - end - end - - context "with a package that returns paginated api results when using the v2 nuget api", :vcr do - let(:dependency_files) { project_dependency_files("paginated_package_v2_api") } - let(:dependency_requirements) do - [{ file: "my.csproj", requirement: "4.7.1", groups: ["dependencies"], source: nil }] - end - let(:dependency_name) { "FakeItEasy" } - let(:dependency_version) { "4.7.1" } - let(:expected_version) { "7.3.0" } - - it "returns the expected version" do - expect(subject[:version]).to eq(expected_version_instance) - end - end - - context "with a custom repo in the credentials", :vcr do - let(:credentials) do - [{ - "type" => "git_source", - "host" => "github.com", - "username" => "x-access-token", - "password" => "token" - }, { - "type" => "nuget_feed", - "url" => custom_repo_url, - "token" => "my:passw0rd" - }] - end - - let(:nuget_versions) { fixture("nuget_responses", "versions.json") } - - let(:nuget_search_results) do - fixture("nuget_responses", "search_results.json") - end - - let(:custom_repo_url) do - "https://www.myget.org/F/exceptionless/api/v3/index.json" - end - let(:custom_nuget_search_url) do - "https://www.myget.org/F/exceptionless/api/v3/" \ - "registration1/microsoft.extensions.dependencymodel/index.json" - end - - before do - stub_request(:get, nuget_versions_url).to_return(status: 404) - stub_request(:get, nuget_search_url).to_return(status: 404) - - stub_request(:get, custom_repo_url).to_return(status: 404) - stub_request(:get, custom_repo_url) - .with(basic_auth: %w(my passw0rd)) - .to_return( - status: 200, - body: fixture("nuget_responses", "myget_base.json") - ) - - stub_request(:get, custom_nuget_search_url).to_return(status: 404) - stub_request(:get, custom_nuget_search_url) - .with(basic_auth: %w(my passw0rd)) - .to_return(status: 200, body: nuget_search_results) - end - - its([:version]) { is_expected.to eq(version_class.new("2.1.0")) } - - context "that does not return PackageBaseAddress" do - let(:custom_repo_url) { "http://www.myget.org/artifactory/api/nuget/v3/dependabot-nuget-local" } - before do - stub_request(:get, custom_repo_url) - .with(basic_auth: %w(admin password)) - .to_return( - status: 200, - body: fixture("nuget_responses", "artifactory_base.json") - ) - end - - its([:version]) { is_expected.to eq(version_class.new("2.1.0")) } - end - end - - context "with a version range specified" do - let(:dependency_files) { project_dependency_files("version_range") } - let(:dependency_version) { "1.1.0" } - let(:dependency_requirements) do - [{ file: "my.csproj", requirement: "[1.1.0, 3.0.0)", groups: ["dependencies"], source: nil }] - end - - its([:version]) { is_expected.to eq(version_class.new("2.1.0")) } - end - - context "with an open upper version range specified" do - let(:dependency_files) { project_dependency_files("open_upper_version_range") } - let(:dependency_version) { "1.1.0" } - let(:dependency_requirements) do - [{ file: "my.csproj", requirement: "[1.1.0-alpha,", groups: ["dependencies"], source: nil }] - end - - its([:version]) { is_expected.to eq(version_class.new("2.1.0")) } - end - - context "with a package that is implicitly referenced", :vcr do - let(:dependency_files) { project_dependency_files("implicit_reference") } - let(:dependency_requirements) do - [{ file: "implicitReference.csproj", requirement: "1.1.2-beta1.22511.2", groups: ["dependencies"], - source: nil }] - end - let(:dependency_name) { "NuGet.Protocol" } - let(:dependency_version) { "6.3.0" } - - # skipped - # it "returns the expected version" do - # expect(subject[:version]).to eq(version_class.new("6.5.0")) - # end - end - - context "when the package can't be meaninfully sorted by just version" do - before do - allow(finder).to receive(:str_version_compatible?).and_call_original - reported_versions = [ - "2.6.1", - "2.7.1", - "3.4.0", - "3.14.0", - "4.0.1" - ] - stub_request(:get, "https://api.nuget.org/v3/registration5-gz-semver2/nunit/index.json") - .to_return( - status: 200, - body: { - items: [ - items: reported_versions.map { |v| { catalogEntry: { listed: true, version: v } } } - ] - }.to_json - ) - stub_request(:get, "https://api.nuget.org/v3-flatcontainer/nunit/3.14.0/nunit.nuspec") - .to_return(status: 200, body: fixture("nuspecs", "nunit.3.14.0_faked.nuspec")) - stub_request(:get, "https://api.nuget.org/v3-flatcontainer/nunit/4.0.1/nunit.nuspec") - .to_return(status: 200, body: fixture("nuspecs", "nunit.4.0.1_faked.nuspec")) - end - - let(:csproj_body) do - <<~XML - - - netcoreapp3.1 - - - - - - XML - end - let(:expected_version) { version_class.new("3.14.0") } - let(:dependency_version) { "3.14.0" } - let(:dependency) do - Dependabot::Dependency.new( - name: "nunit", - version: dependency_version, - requirements: [{ file: "my.csproj", requirement: "3.14.0", groups: ["dependencies"], source: nil }], - package_manager: "nuget" - ) - end - - it "returns the expected version" do - expect(subject[:version]).to eq(version_class.new("3.14.0")) - end - end - - context "when `packageSourceMapping`s are specified" do - let(:csproj_body) do - <<~XML - - - net8.0 - - - - - - XML - end - let(:config_file) do - Dependabot::DependencyFile.new( - name: "NuGet.Config", - content: - <<~XML - - - - - - - - - - - - - - - - - - XML - ) - end - let(:dependency_files) { [csproj, config_file] } - let(:dependency) do - Dependabot::Dependency.new( - name: "Some.Package", - version: "1.0.0", - requirements: [{ file: "my.csproj", requirement: "1.0.0", groups: ["dependencies"], source: nil }], - package_manager: "nuget" - ) - end - let(:expected_version) { version_class.new("1.1.0") } - - def create_nupkg(nuspec_name, nuspec_content) - content = Zip::OutputStream.write_buffer do |zio| - zio.put_next_entry("#{nuspec_name}.nuspec") - zio.write(nuspec_content) - end - content.rewind - content.sysread - end - - before do - allow(finder).to receive(:str_version_compatible?).and_call_original - - # stub source 1 - stub_index_json("https://nuget.example.com/source1/index.json") - stub_request(:get, "https://nuget.example.com/source1/RegistrationsBaseUrl/some.package/index.json") - .to_return( - status: 200, - body: { - count: 1, - items: [ - { - count: 2, - items: [ - { - catalogEntry: { - id: "Some.Package", - version: "1.0.0" # this is what's currently installed - } - }, - { - catalogEntry: { - id: "Some.Package", - version: "1.1.0" # this is what we'd like to upgrade to - } - } - ] - } - ] - }.to_json - ) - stub_request(:get, "https://nuget.example.com/source1/PackageBaseAddress/some.package/1.0.0/some.package.1.0.0.nupkg") - .to_return( - status: 200, - body: create_nupkg( - "Some.Package", - <<~XML - - - - - - - - XML - ) - ) - stub_request(:get, "https://nuget.example.com/source1/PackageBaseAddress/some.package/1.1.0/some.package.1.1.0.nupkg") - .to_return( - status: 200, - body: create_nupkg( - "Some.Package", - <<~XML - - - - - - - - XML - ) - ) - # none of the `source2` URLs should be called - end - - it "returns the expected version honoring the package source mapping" do - expect(subject[:version]).to eq(version_class.new("1.1.0")) - end - end - end - - describe "#lowest_security_fix_version_details" do - subject(:lowest_security_fix_version_details) do - finder.lowest_security_fix_version_details - end - - let(:dependency_version) { "1.1.1" } - let(:security_advisories) do - [ - Dependabot::SecurityAdvisory.new( - dependency_name: "rails", - package_manager: "nuget", - vulnerable_versions: ["< 2.0.0"] - ) - ] - end - - let(:expected_version) { "2.0.0" } - - before do - allow(finder).to receive(:str_version_compatible?).with(dependency_version.to_s).and_return(true) - allow(finder).to receive(:str_version_compatible?).with(expected_version.to_s).and_return(true) - end - - its([:version]) { is_expected.to eq(version_class.new("2.0.0")) } - - context "when the user is ignoring the lowest version" do - let(:ignored_versions) { [">= 2.a, <= 2.0.0"] } - let(:expected_version) { "2.0.3" } - its([:version]) { is_expected.to eq(version_class.new("2.0.3")) } - end - end - - describe "#versions" do - subject(:versions) { finder.versions } - - it "includes the correct versions" do - expect(versions.count).to eq(21) - expect(versions.first).to eq( - nuspec_url: "https://api.nuget.org/v3-flatcontainer/" \ - "microsoft.extensions.dependencymodel/1.0.0-rc2-002702/" \ - "microsoft.extensions.dependencymodel.nuspec", - repo_url: "https://api.nuget.org/v3/index.json", - source_url: nil, - version: Dependabot::Nuget::Version.new("1.0.0-rc2-002702") - ) - end - end -end diff --git a/nuget/spec/dependabot/nuget/update_checker_spec.rb b/nuget/spec/dependabot/nuget/update_checker_spec.rb index f04c5da99a3..b9cfbc153dc 100644 --- a/nuget/spec/dependabot/nuget/update_checker_spec.rb +++ b/nuget/spec/dependabot/nuget/update_checker_spec.rb @@ -4,13 +4,29 @@ require "spec_helper" require "dependabot/dependency" require "dependabot/dependency_file" +require "dependabot/nuget/file_parser" require "dependabot/nuget/update_checker" +require "dependabot/nuget/requirement" require "dependabot/nuget/version" require_common_spec "update_checkers/shared_examples_for_update_checkers" + RSpec.describe Dependabot::Nuget::UpdateChecker do it_behaves_like "an update checker" + let(:repo_contents_path) { write_tmp_repo(dependency_files) } + let(:source) do + Dependabot::Source.new( + provider: "github", + repo: "gocardless/bump", + directory: "/" + ) + end + let(:checker) do + # We have to run the FileParser first to ensure the discovery.json is generated. + Dependabot::Nuget::FileParser.new(dependency_files: dependency_files, + source: source, + repo_contents_path: repo_contents_path).parse described_class.new( dependency: dependency, dependency_files: dependency_files, @@ -53,14 +69,6 @@ let(:version_class) { Dependabot::Nuget::Version } - def nuspec_url(name, version) - "https://api.nuget.org/v3-flatcontainer/#{name.downcase}/#{version}/#{name.downcase}.nuspec" - end - - def registration_index_url(name) - "https://api.nuget.org/v3/registration5-gz-semver2/#{name.downcase}/index.json" - end - describe "up_to_date?" do subject(:up_to_date?) { checker.up_to_date? } @@ -93,51 +101,6 @@ def registration_index_url(name) end end - describe "#latest_version" do - subject { checker.latest_version } - - it "delegates to the VersionFinder class" do - version_finder_class = described_class::VersionFinder - dummy_version_finder = instance_double(version_finder_class) - allow(version_finder_class) - .to receive(:new) - .and_return(dummy_version_finder) - allow(dummy_version_finder) - .to receive(:latest_version_details) - .and_return(version: Dependabot::Nuget::Version.new("1.2.3")) - - expect(checker.latest_version).to eq("1.2.3") - end - - context "the package could not be found on any source" do - before do - stub_request(:get, registration_index_url("microsoft.extensions.dependencymodel")) - .to_return(status: 404) - end - - it "reports the current version" do - expect(checker.latest_version).to eq("1.1.1") - end - end - end - - describe "#lowest_security_fix_version" do - subject { checker.lowest_security_fix_version } - - it "delegates to the VersionFinder class" do - version_finder_class = described_class::VersionFinder - dummy_version_finder = instance_double(version_finder_class) - allow(version_finder_class) - .to receive(:new) - .and_return(dummy_version_finder) - allow(dummy_version_finder) - .to receive(:lowest_security_fix_version_details) - .and_return(version: Dependabot::Nuget::Version.new("1.2.3")) - - expect(checker.lowest_security_fix_version).to eq("1.2.3") - end - end - describe "#latest_resolvable_version" do subject(:latest_resolvable_version) { checker.latest_resolvable_version } it { is_expected.to be_nil } @@ -148,8 +111,14 @@ def registration_index_url(name) it { is_expected.to be_nil } end - describe "#can_update?(requirements_to_unlock: :all)" do - subject(:can_update) { checker.can_update?(requirements_to_unlock: :all) } + describe "#requirements_unlocked_or_can_be?" do + let(:csproj_body) do + fixture("csproj", "property_version.csproj") + end + + subject(:requirements_unlocked_or_can_be) do + checker.requirements_unlocked_or_can_be? + end context "with a property dependency" do let(:dependency_requirements) do @@ -164,226 +133,33 @@ def registration_index_url(name) let(:dependency_name) { "Nuke.Common" } let(:dependency_version) { "0.1.434" } - context "that is used for multiple dependencies" do - let(:csproj_body) do - fixture("csproj", "property_version.csproj") - end - - context "where all dependencies can update to the latest version" do - before do - allow(checker).to receive(:all_property_based_dependencies).and_return( - [ - Dependabot::Dependency.new( - name: "Nuke.Common", - version: "0.1.434", - requirements: dependency_requirements, - package_manager: "nuget" - ), - Dependabot::Dependency.new( - name: "Nuke.CodeGeneration", - version: "0.1.434", - requirements: dependency_requirements, - package_manager: "nuget" - ) - ] - ) - - property_updater_class = described_class::PropertyUpdater - dummy_property_updater = instance_double(property_updater_class) - allow(checker).to receive(:latest_version).and_return("0.9.0") - allow(checker).to receive(:property_updater).and_return(dummy_property_updater) - allow(dummy_property_updater).to receive(:update_possible?).and_return(true) - end - - it { is_expected.to eq(true) } - end - - context "where not all dependencies can update to the latest version" do - before do - allow(checker).to receive(:all_property_based_dependencies).and_return( - [ - Dependabot::Dependency.new( - name: "Nuke.Common", - version: "0.1.434", - requirements: dependency_requirements, - package_manager: "nuget" - ), - Dependabot::Dependency.new( - name: "Nuke.CodeGeneration", - version: "0.1.434", - requirements: dependency_requirements, - package_manager: "nuget" - ) - ] - ) - - property_updater_class = described_class::PropertyUpdater - dummy_property_updater = instance_double(property_updater_class) - allow(checker).to receive(:latest_version).and_return("0.9.0") - allow(checker).to receive(:property_updater).and_return(dummy_property_updater) - allow(dummy_property_updater).to receive(:update_possible?).and_return(false) - end - - it { is_expected.to eq(false) } - end - end + it { is_expected.to eq(true) } end end - describe "#updated_requirements" do - subject(:updated_requirements) { checker.updated_requirements } - - let(:target_version) { "2.1.0" } - - it "delegates to the RequirementsUpdater" do - allow(checker).to receive(:latest_version_details).and_return( - { - version: target_version, - source_url: nil, - nuspec_url: nuspec_url(dependency_name, target_version), - repo_url: "https://api.nuget.org/v3/index.json" - } - ) - expect(described_class::RequirementsUpdater).to receive(:new).with( - requirements: dependency_requirements, - latest_version: target_version, - source_details: { - source_url: nil, - nuspec_url: nuspec_url(dependency_name, target_version), - repo_url: "https://api.nuget.org/v3/index.json" - } - ).and_call_original - expect(updated_requirements).to eq( - [{ - file: "my.csproj", - requirement: target_version, - groups: ["dependencies"], - source: { - type: "nuget_repo", - url: "https://api.nuget.org/v3/index.json", - source_url: nil, - nuspec_url: nuspec_url(dependency_name, target_version) - } - }] - ) - end - - context "with a security vulnerability" do - let(:target_version) { "2.0.0" } - let(:vulnerable_versions) { ["< 2.0.0"] } - let(:security_advisories) do - [ - Dependabot::SecurityAdvisory.new( - dependency_name: dependency_name, - package_manager: "nuget", - vulnerable_versions: vulnerable_versions - ) - ] - end - - it "delegates to the RequirementsUpdater" do - allow(checker).to receive(:lowest_security_fix_version_details).and_return( - { - version: target_version, - source_url: nil, - nuspec_url: nuspec_url(dependency_name, target_version), - repo_url: "https://api.nuget.org/v3/index.json" - } - ) + describe "#lowest_security_fix_version" do + subject { checker.lowest_security_fix_version } - expect(described_class::RequirementsUpdater).to receive(:new).with( - requirements: dependency_requirements, - latest_version: target_version, - source_details: { - source_url: nil, - nuspec_url: nuspec_url(dependency_name, target_version), - repo_url: "https://api.nuget.org/v3/index.json" - } - ).and_call_original - expect(updated_requirements).to eq( - [{ - file: "my.csproj", - requirement: target_version, - groups: ["dependencies"], - source: { - type: "nuget_repo", - url: "https://api.nuget.org/v3/index.json", - source_url: nil, - nuspec_url: nuspec_url(dependency_name, target_version) - } - }] + let(:target_version) { "2.0.0" } + let(:vulnerable_versions) { ["< 2.0.0"] } + let(:security_advisories) do + [ + Dependabot::SecurityAdvisory.new( + dependency_name: dependency_name, + package_manager: "nuget", + vulnerable_versions: vulnerable_versions ) - end - - context "the security vulnerability excludes all compatible packages" do - let(:target_version) { "1.1.1" } - let(:vulnerable_versions) { ["< 999.999.999"] } # it's all bad - subject(:updated_requirement_version) { updated_requirements[0].fetch(:requirement) } - - before do - # only vulnerable versions are returned - stub_request(:get, registration_index_url(dependency_name)) - .to_return( - status: 200, - body: { - items: [ - items: [ - { - catalogEntry: { - version: "1.1.1" # the currently installed version, but it's vulnerable - } - }, - { - catalogEntry: { - version: "3.0.0" # newer version, but it's still vulnerable - } - } - ] - ] - }.to_json - ) - end - - it "reports the currently installed version" do - expect(updated_requirement_version).to eq(target_version) - end - end + ] end - end - describe "#requirements_unlocked_or_can_be?" do - subject(:requirements_unlocked_or_can_be) do - checker.requirements_unlocked_or_can_be? - end + it { is_expected.to eq(target_version) } - context "with a property dependency" do - let(:dependency_requirements) do - [{ - requirement: "0.1.434", - file: "my.csproj", - groups: ["dependencies"], - source: nil, - metadata: { property_name: "NukeVersion" } - }] - end - let(:dependency_name) { "Nuke.Common" } - let(:dependency_version) { "0.1.434" } - - it { is_expected.to eq(true) } - - context "whose property couldn't be found" do - let(:dependency_requirements) do - [{ - requirement: "$(NukeVersion)", - file: "my.csproj", - groups: ["dependencies"], - source: nil, - metadata: { property_name: "NukeVersion" } - }] - end - let(:dependency_version) { "$(NukeVersion)" } + context "the security vulnerability excludes all compatible packages" do + let(:target_version) { "1.1.1" } + let(:vulnerable_versions) { ["< 999.999.999"] } # it's all bad - it { is_expected.to eq(false) } + it "reports the currently installed version" do + is_expected.to eq(target_version) end end end @@ -412,34 +188,25 @@ def registration_index_url(name) end context "where all dependencies can update to the latest version" do - before do - allow(checker).to receive(:latest_version).and_return("0.9.0") - allow(checker).to receive(:all_property_based_dependencies).and_return( - [ - Dependabot::Dependency.new( - name: "Nuke.Common", - version: "0.1.434", - requirements: dependency_requirements, - package_manager: "nuget" - ), - Dependabot::Dependency.new( - name: "Nuke.CodeGeneration", - version: "0.1.434", - requirements: dependency_requirements, - package_manager: "nuget" - ) - ] - ) - end - it "delegates to PropertyUpdater" do - property_updater_class = described_class::PropertyUpdater - dummy_property_updater = instance_double(property_updater_class) - allow(checker).to receive(:property_updater).and_return(dummy_property_updater) - allow(dummy_property_updater).to receive(:update_possible?).and_return(true) - expect(dummy_property_updater).to receive(:updated_dependencies).and_return([dependency]) - - subject + is_expected.to eq([ + Dependabot::Dependency.new( + name: "Nuke.CodeGeneration", + version: "6.3.0", + previous_version: dependency_version, + requirements: dependency_requirements, + previous_requirements: dependency_requirements, + package_manager: "nuget" + ), + Dependabot::Dependency.new( + name: "Nuke.Common", + version: "6.3.0", + previous_version: dependency_version, + requirements: dependency_requirements, + previous_requirements: dependency_requirements, + package_manager: "nuget" + ) + ]) end end end diff --git a/nuget/spec/fixtures/csproj/property_version.csproj b/nuget/spec/fixtures/csproj/property_version.csproj index 62557f88433..4d8198bc238 100644 --- a/nuget/spec/fixtures/csproj/property_version.csproj +++ b/nuget/spec/fixtures/csproj/property_version.csproj @@ -2,7 +2,7 @@ Exe - netcoreapp2.0 + net5.0 false False