diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/Package/Download/PackageDownloadRunner.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/Package/Download/PackageDownloadRunner.cs index f6a93645940..940821c4ad3 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/Package/Download/PackageDownloadRunner.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/Package/Download/PackageDownloadRunner.cs @@ -1,6 +1,8 @@ // 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. +#nullable enable + using System; using System.Collections.Generic; using System.Globalization; @@ -48,18 +50,32 @@ public static async Task RunAsync(PackageDownloadArgs args, CancellationTok public static async Task RunAsync(PackageDownloadArgs args, ILoggerWithColor logger, IReadOnlyList packageSources, ISettings settings, CancellationToken token) { - // Check for insecure sources - if (DetectAndReportInsecureSources(args.AllowInsecureConnections, packageSources, logger)) + bool hasSourcesArg = args.Sources?.Count > 0; + PackageSourceMapping? packageSourceMapping = null; + if (!hasSourcesArg) + { + packageSourceMapping = PackageSourceMapping.GetPackageSourceMapping(settings); + } + + bool ignorePackageSourceMapping = + hasSourcesArg + || packageSourceMapping is null + || !packageSourceMapping.IsEnabled; + + // When package source mapping is disabled, validate all configured sources upfront. + // When mapping is enabled, source validation is deferred to the per-package resolution step, + // since each package may map to a different subset of sources. + if (ignorePackageSourceMapping && DetectAndReportInsecureSources(args.AllowInsecureConnections, packageSources, logger)) { return ExitCodeError; } string outputDirectory = args.OutputDirectory ?? Directory.GetCurrentDirectory(); var cache = new SourceCacheContext(); - IReadOnlyList sourceRepositories = GetSourceRepositories(packageSources); + IReadOnlyList allRepositories = GetSourceRepositories(packageSources); bool downloadedAllSuccessfully = true; - foreach (var package in args.Packages) + foreach (var package in args.Packages ?? []) { logger.LogMinimal(string.Format( CultureInfo.CurrentCulture, @@ -67,9 +83,29 @@ public static async Task RunAsync(PackageDownloadArgs args, ILoggerWithColo package.Id, string.IsNullOrEmpty(package.NuGetVersion?.ToNormalizedString()) ? Strings.PackageDownloadCommand_LatestVersion : package.NuGetVersion.ToNormalizedString())); + // Resolve which repositories to use for this package + IReadOnlyList sourceRepositories; + if (ignorePackageSourceMapping) + { + sourceRepositories = allRepositories; + } + else + { + if (!TryGetRepositoriesForPackage( + package.Id, + args, + packageSourceMapping!, + allRepositories, + logger, + out sourceRepositories)) + { + return ExitCodeError; + } + } + try { - (NuGetVersion version, SourceRepository downloadRepository) = + (NuGetVersion? version, SourceRepository? downloadRepository) = await ResolvePackageDownloadVersion( package, sourceRepositories, @@ -88,7 +124,7 @@ await ResolvePackageDownloadVersion( bool success = await DownloadPackageAsync( package.Id, version, - downloadRepository, + downloadRepository!, cache, settings, outputDirectory, @@ -127,16 +163,16 @@ await ResolvePackageDownloadVersion( return downloadedAllSuccessfully ? ExitCodeSuccess : ExitCodeError; } - internal static async Task<(NuGetVersion, SourceRepository)> ResolvePackageDownloadVersion( + internal static async Task<(NuGetVersion?, SourceRepository?)> ResolvePackageDownloadVersion( PackageWithNuGetVersion packageWithNuGetVersion, - IEnumerable sourceRepositories, + IReadOnlyList sourceRepositories, SourceCacheContext cache, ILoggerWithColor logger, bool includePrerelease, CancellationToken token) { - NuGetVersion versionToDownload = null; - SourceRepository downloadSourceRepository = null; + NuGetVersion? versionToDownload = null; + SourceRepository? downloadSourceRepository = null; bool versionSpecified = packageWithNuGetVersion.NuGetVersion != null; foreach (var repo in sourceRepositories) @@ -188,6 +224,69 @@ await ResolvePackageDownloadVersion( return (versionToDownload, downloadSourceRepository); } + /// + /// Builds the set of SourceRepository objects to use for a given package, + /// applying package source mapping + /// validating HTTP usage only on the *effective* sources. + /// + private static bool TryGetRepositoriesForPackage( + string packageId, + PackageDownloadArgs args, + PackageSourceMapping packageSourceMapping, + IReadOnlyList allRepos, + ILoggerWithColor logger, + out IReadOnlyList repositories) + { + var mappedNames = packageSourceMapping.GetConfiguredPackageSources(packageId); + + // Only validate insecure sources when mapping produced something + if (mappedNames.Count > 0) + { + var mappedRepos = new List(mappedNames.Count); + foreach (var mappedName in mappedNames) + { + SourceRepository? repo = null; + for (int i = 0; i < allRepos.Count; i++) + { + if (string.Equals(allRepos[i].PackageSource.Name, mappedName, StringComparison.OrdinalIgnoreCase)) + { + repo = allRepos[i]; + break; + } + } + + if (repo != null) + { + mappedRepos.Add(repo); + } + else + { + logger.LogVerbose( + string.Format( + CultureInfo.CurrentCulture, + Strings.PackageDownloadCommand_PackageSourceMapping_NoSuchSource, + mappedName, + packageId)); + } + } + + if (DetectAndReportInsecureSources(args.AllowInsecureConnections, mappedRepos.Select(repo => repo.PackageSource), logger)) + { + repositories = []; + return false; + } + + repositories = mappedRepos; + return true; + } + else + { + // No mapping for this package: fall back to all sources + repositories = allRepos; + return true; + } + } + private static async Task DownloadPackageAsync( string id, NuGetVersion version, @@ -239,7 +338,7 @@ private static async Task DownloadPackageAsync( return success; } - private static IReadOnlyList GetPackageSources(IList sources, IPackageSourceProvider sourceProvider) + private static IReadOnlyList GetPackageSources(IList? sources, IPackageSourceProvider sourceProvider) { IEnumerable configuredSources = sourceProvider.LoadPackageSources() .Where(s => s.IsEnabled); diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.Designer.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.Designer.cs index 33be7a2c176..a34243fa494 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.Designer.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.Designer.cs @@ -1661,6 +1661,15 @@ internal static string PackageDownloadCommand_PackageIdDescription { } } + /// + /// Looks up a localized string similar to The mapped source '{0}' for package '{1}' was not found among the configured sources.. + /// + internal static string PackageDownloadCommand_PackageSourceMapping_NoSuchSource { + get { + return ResourceManager.GetString("PackageDownloadCommand_PackageSourceMapping_NoSuchSource", resourceCulture); + } + } + /// /// Looks up a localized string similar to Specifies one or more NuGet package sources to use.. /// diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.resx b/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.resx index 76c0f91d27b..3a9b3a4c269 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.resx +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.resx @@ -1157,4 +1157,9 @@ Do not translate "PackageVersion" Unable to find a valid package version + + The mapped source '{0}' for package '{1}' was not found among the configured sources. + 0 - package source name +1 - package name + \ No newline at end of file diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.cs.xlf b/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.cs.xlf index 7a54623b1e4..9c8611810b4 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.cs.xlf +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.cs.xlf @@ -929,6 +929,12 @@ Další informace najdete tady: https://docs.nuget.org/docs/reference/command-li Package identifier (e.g. 'Newtonsoft.Json'). + + The mapped source '{0}' for package '{1}' was not found among the configured sources. + The mapped source '{0}' for package '{1}' was not found among the configured sources. + 0 - package source name +1 - package name + Specifies one or more NuGet package sources to use. Specifies one or more NuGet package sources to use. diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.de.xlf b/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.de.xlf index 4df698a6e77..1561190af34 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.de.xlf +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.de.xlf @@ -929,6 +929,12 @@ Weitere Informationen finden Sie unter: https://docs.nuget.org/docs/reference/co Package identifier (e.g. 'Newtonsoft.Json'). + + The mapped source '{0}' for package '{1}' was not found among the configured sources. + The mapped source '{0}' for package '{1}' was not found among the configured sources. + 0 - package source name +1 - package name + Specifies one or more NuGet package sources to use. Specifies one or more NuGet package sources to use. diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.es.xlf b/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.es.xlf index 3f8b89bf311..10590d156e3 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.es.xlf +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.es.xlf @@ -929,6 +929,12 @@ Para obtener más información, visite https://docs.nuget.org/docs/reference/com Package identifier (e.g. 'Newtonsoft.Json'). + + The mapped source '{0}' for package '{1}' was not found among the configured sources. + The mapped source '{0}' for package '{1}' was not found among the configured sources. + 0 - package source name +1 - package name + Specifies one or more NuGet package sources to use. Specifies one or more NuGet package sources to use. diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.fr.xlf b/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.fr.xlf index 8708500291d..e9ac4d4a071 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.fr.xlf +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.fr.xlf @@ -929,6 +929,12 @@ Pour plus d'informations, visitez https://docs.nuget.org/docs/reference/command- Package identifier (e.g. 'Newtonsoft.Json'). + + The mapped source '{0}' for package '{1}' was not found among the configured sources. + The mapped source '{0}' for package '{1}' was not found among the configured sources. + 0 - package source name +1 - package name + Specifies one or more NuGet package sources to use. Specifies one or more NuGet package sources to use. diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.it.xlf b/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.it.xlf index 2a30afecf31..5a4c20f00a3 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.it.xlf +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.it.xlf @@ -929,6 +929,12 @@ Per altre informazioni, vedere https://docs.nuget.org/docs/reference/command-lin Package identifier (e.g. 'Newtonsoft.Json'). + + The mapped source '{0}' for package '{1}' was not found among the configured sources. + The mapped source '{0}' for package '{1}' was not found among the configured sources. + 0 - package source name +1 - package name + Specifies one or more NuGet package sources to use. Specifies one or more NuGet package sources to use. diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.ja.xlf b/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.ja.xlf index d8aa3b3fbef..2b2dfa9f833 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.ja.xlf +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.ja.xlf @@ -929,6 +929,12 @@ For more information, visit https://docs.nuget.org/docs/reference/command-line-r Package identifier (e.g. 'Newtonsoft.Json'). + + The mapped source '{0}' for package '{1}' was not found among the configured sources. + The mapped source '{0}' for package '{1}' was not found among the configured sources. + 0 - package source name +1 - package name + Specifies one or more NuGet package sources to use. Specifies one or more NuGet package sources to use. diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.ko.xlf b/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.ko.xlf index 6ba03ee2e91..809170e5b82 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.ko.xlf +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.ko.xlf @@ -929,6 +929,12 @@ For more information, visit https://docs.nuget.org/docs/reference/command-line-r Package identifier (e.g. 'Newtonsoft.Json'). + + The mapped source '{0}' for package '{1}' was not found among the configured sources. + The mapped source '{0}' for package '{1}' was not found among the configured sources. + 0 - package source name +1 - package name + Specifies one or more NuGet package sources to use. Specifies one or more NuGet package sources to use. diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.pl.xlf b/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.pl.xlf index 7f31ead7118..1ba1ff39581 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.pl.xlf +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.pl.xlf @@ -929,6 +929,12 @@ Aby uzyskać więcej informacji, odwiedź stronę https://docs.nuget.org/docs/re Package identifier (e.g. 'Newtonsoft.Json'). + + The mapped source '{0}' for package '{1}' was not found among the configured sources. + The mapped source '{0}' for package '{1}' was not found among the configured sources. + 0 - package source name +1 - package name + Specifies one or more NuGet package sources to use. Specifies one or more NuGet package sources to use. diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.pt-BR.xlf b/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.pt-BR.xlf index 2140037fd02..3ac34709fcf 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.pt-BR.xlf +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.pt-BR.xlf @@ -929,6 +929,12 @@ Para obter mais informações, acesse https://docs.nuget.org/docs/reference/comm Package identifier (e.g. 'Newtonsoft.Json'). + + The mapped source '{0}' for package '{1}' was not found among the configured sources. + The mapped source '{0}' for package '{1}' was not found among the configured sources. + 0 - package source name +1 - package name + Specifies one or more NuGet package sources to use. Specifies one or more NuGet package sources to use. diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.ru.xlf b/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.ru.xlf index 65f1b305575..62e0a674d4d 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.ru.xlf +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.ru.xlf @@ -929,6 +929,12 @@ For more information, visit https://docs.nuget.org/docs/reference/command-line-r Package identifier (e.g. 'Newtonsoft.Json'). + + The mapped source '{0}' for package '{1}' was not found among the configured sources. + The mapped source '{0}' for package '{1}' was not found among the configured sources. + 0 - package source name +1 - package name + Specifies one or more NuGet package sources to use. Specifies one or more NuGet package sources to use. diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.tr.xlf b/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.tr.xlf index 01e3e4eb894..7e53ce27174 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.tr.xlf +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.tr.xlf @@ -930,6 +930,12 @@ Daha fazla bilgi için bkz. https://docs.nuget.org/docs/reference/command-line-r Package identifier (e.g. 'Newtonsoft.Json'). + + The mapped source '{0}' for package '{1}' was not found among the configured sources. + The mapped source '{0}' for package '{1}' was not found among the configured sources. + 0 - package source name +1 - package name + Specifies one or more NuGet package sources to use. Specifies one or more NuGet package sources to use. diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.zh-Hans.xlf b/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.zh-Hans.xlf index b62044171b7..6a6b143d1e5 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.zh-Hans.xlf +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.zh-Hans.xlf @@ -929,6 +929,12 @@ For more information, visit https://docs.nuget.org/docs/reference/command-line-r Package identifier (e.g. 'Newtonsoft.Json'). + + The mapped source '{0}' for package '{1}' was not found among the configured sources. + The mapped source '{0}' for package '{1}' was not found among the configured sources. + 0 - package source name +1 - package name + Specifies one or more NuGet package sources to use. Specifies one or more NuGet package sources to use. diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.zh-Hant.xlf b/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.zh-Hant.xlf index 5dd4fe8bc5c..41c7e5b851b 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.zh-Hant.xlf +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.zh-Hant.xlf @@ -929,6 +929,12 @@ For more information, visit https://docs.nuget.org/docs/reference/command-line-r Package identifier (e.g. 'Newtonsoft.Json'). + + The mapped source '{0}' for package '{1}' was not found among the configured sources. + The mapped source '{0}' for package '{1}' was not found among the configured sources. + 0 - package source name +1 - package name + Specifies one or more NuGet package sources to use. Specifies one or more NuGet package sources to use. diff --git a/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/Package/Download/PackageDownloadRunnerTests.cs b/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/Package/Download/PackageDownloadRunnerTests.cs index c8ceeb8d334..aa3186220db 100644 --- a/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/Package/Download/PackageDownloadRunnerTests.cs +++ b/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/Package/Download/PackageDownloadRunnerTests.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Linq; using System.Threading; @@ -16,6 +17,7 @@ using NuGet.Configuration; using NuGet.Test.Utility; using NuGet.Versioning; +using Test.Utility; using Xunit; namespace NuGet.CommandLine.Xplat.Tests; @@ -426,4 +428,234 @@ [new PackageSource(context.PackageSource)], File.Exists(Path.Combine(context.WorkingDirectory, $"{id.ToLowerInvariant()}.{v}.nupkg")) .Should().BeFalse("Package does not exist in sources"); } + + public static IEnumerable Cases() + { + // Parameters: + // A-packages, B-packages, sourceMappings, sourcesArgs, downloadId, downloadVersion, + // allowInsecureConnections, expectSuccess, expectedInstalled + + // --source specified, mapping ignored, package only in A -> success + yield return new object[] + { + new List<(string,string)> { ("Contoso.Lib", "1.0.0") }, // A + new List<(string,string)>(), // B + new List<(string,string)> { ("B", "Contoso.*") }, // mapping ignored + new List { "A" }, // --source A + "Contoso.Lib", "1.0.0", // downloadId, downloadVersion + true, // allow insecure + true, // expect success + ("Contoso.Lib", "1.0.0") // expectedInstalled + }; + + // no --source, mapping -> B, package only in B -> success + yield return new object[] + { + new List<(string,string)>(), // A + new List<(string,string)> { ("Contoso.Mapped", "2.0.0") }, // B + new List<(string,string)> { ("B", "Contoso.*") }, // mapping -> B + null, // no --source + "Contoso.Mapped", "2.0.0", // downloadId, downloadVersion + true, // allow insecure + true, // expect success + ("Contoso.Mapped", "2.0.0") // expectedInstalled + }; + + // no --source, mapping -> A, package only in B -> fail + yield return new object[] + { + new List<(string,string)>(), // A + new List<(string,string)> { ("Contoso.Mapped", "2.0.0") }, + new List<(string,string)> { ("A", "Contoso.*") }, // mapped to A + null, + "Contoso.Mapped", "2.0.0", + true, + false, + null! + }; + + // --source specified, no source mapping with an insecure source + yield return new object[] + { + new List<(string,string)> { ("Contoso.Lib", "1.0.0") }, // A + new List<(string,string)>(), + new List<(string,string)> { ("A", "Contoso.*") }, + new List { "A" }, // --source + "Contoso.Lib", "1.0.0", + false, // allow insecure connections false / not set to true + false, + null! + }; + + // no --source, mapping -> B, allow insecure not enabled -> fail + yield return new object[] + { + new List<(string,string)>(), // A + new List<(string,string)> { ("Contoso.Mapped", "1.0.0") }, + new List<(string,string)> { ("B", "Contoso.*") }, + null, + "Contoso.Mapped", "1.0.0", + false, // allow insecure connections false / not set to true + false, + null! + }; + } + + [Theory] + [MemberData(nameof(Cases))] + public async Task RunAsync_WithSourceMapping( + IReadOnlyList<(string id, string version)> sourceAPackages, + IReadOnlyList<(string id, string version)> sourceBPackages, + IReadOnlyList<(string source, string pattern)> sourceMappings, + IReadOnlyList sourcesArgs, + string downloadId, + string downloadVersion, + bool allowInsecureConnections, + bool expectSuccess, + (string id, string version)? expectedInstalled) + { + // Arrange + using var context = new SimpleTestPathContext(); + string srcADirectory = Path.Combine(context.PackageSource, "SourceA"); + string srcBDirectory = Path.Combine(context.PackageSource, "SourceB"); + + using var serverA = new FileSystemBackedV3MockServer(srcADirectory); + using var serverB = new FileSystemBackedV3MockServer(srcBDirectory); + + foreach (var (id, ver) in sourceAPackages) + { + await SimpleTestPackageUtility.CreateFullPackageAsync(srcADirectory, id, ver); + } + + foreach (var (id, ver) in sourceBPackages) + { + await SimpleTestPackageUtility.CreateFullPackageAsync(srcBDirectory, id, ver); + } + + serverA.Start(); + serverB.Start(); + + // sources + context.Settings.AddSource("A", serverA.ServiceIndexUri); + context.Settings.AddSource("B", serverB.ServiceIndexUri); + + // mapping + foreach (var (src, pattern) in sourceMappings) + { + context.Settings.AddPackageSourceMapping(src, pattern); + } + + var settings = Settings.LoadSettingsGivenConfigPaths([context.Settings.ConfigPath]); + + var packageSources = new List + { + new(serverA.ServiceIndexUri, "A"), + new(serverB.ServiceIndexUri, "B") + }; + + // args + var args = new PackageDownloadArgs + { + Packages = + [ + new PackageWithNuGetVersion + { + Id = downloadId, + NuGetVersion = downloadVersion is null ? null : NuGetVersion.Parse(downloadVersion) + } + ], + OutputDirectory = context.WorkingDirectory, + AllowInsecureConnections = allowInsecureConnections, + Sources = sourcesArgs == null ? [] : sourcesArgs.ToList() + }; + + string capturedLogs = string.Empty; + var logger = new Mock(MockBehavior.Loose); + logger + .Setup(l => l.LogError(It.IsAny())) + .Callback(msg => capturedLogs += msg + Environment.NewLine); + + // Act + var exit = await PackageDownloadRunner.RunAsync( + args, + logger.Object, + packageSources, + settings, + CancellationToken.None); + + serverA.Stop(); + serverB.Stop(); + + // Assert + if (expectSuccess) + { + exit.Should().Be(PackageDownloadRunner.ExitCodeSuccess, because: capturedLogs); + expectedInstalled.Should().NotBeNull(); + + var (expId, expVer) = expectedInstalled!.Value; + var installDir = Path.Combine(context.WorkingDirectory, expId.ToLowerInvariant(), expVer); + Directory.Exists(installDir).Should().BeTrue(); + File.Exists(Path.Combine(installDir, $"{expId.ToLowerInvariant()}.{expVer}.nupkg")).Should().BeTrue(); + } + else + { + exit.Should().Be(PackageDownloadRunner.ExitCodeError); + } + } + + [Fact] + public async Task RunAsync_WhenMappedSourceMissing_LogsVerbose() + { + // Arrange + using var context = new SimpleTestPathContext(); + string srcADirectory = Path.Combine(context.PackageSource, "SourceA"); + using var serverA = new FileSystemBackedV3MockServer(srcADirectory); + + await SimpleTestPackageUtility.CreateFullPackageAsync(srcADirectory, "Contoso.Utils", "1.0.0"); + context.Settings.AddSource("A", serverA.ServiceIndexUri); + + // Map the package to a NON-EXISTING source name "MissingSource" + context.Settings.AddPackageSourceMapping("MissingSource", "Contoso.*"); + var settings = Settings.LoadSettingsGivenConfigPaths([context.Settings.ConfigPath]); + + var args = new PackageDownloadArgs + { + Packages = + [ + new PackageWithNuGetVersion + { + Id = "Contoso.Utils", + NuGetVersion = NuGetVersion.Parse("1.0.0") + } + ], + OutputDirectory = context.WorkingDirectory, + AllowInsecureConnections = true, + Sources = [] + }; + string capturedVerbose = string.Empty; + var logger = new Mock(MockBehavior.Loose); + logger.Setup(l => l.LogVerbose(It.IsAny())) + .Callback(msg => capturedVerbose += msg + Environment.NewLine); + + // Act + serverA.Start(); + + var exit = await PackageDownloadRunner.RunAsync( + args, + logger.Object, + [new(serverA.ServiceIndexUri, "A")], + settings, + CancellationToken.None); + + serverA.Stop(); + + // Assert + var expected = string.Format( + CultureInfo.CurrentCulture, + Strings.PackageDownloadCommand_PackageSourceMapping_NoSuchSource, + "MissingSource", + "Contoso.Utils"); + + capturedVerbose.Should().Contain(expected, because: "a package mapped to a non-existing source name must log a verbose diagnostic"); + } } diff --git a/test/NuGet.Core.Tests/NuGet.CommandLine.Xplat.Tests/Commands/Package/Download/PackageDownloadRunnerTests.cs b/test/NuGet.Core.Tests/NuGet.CommandLine.Xplat.Tests/Commands/Package/Download/PackageDownloadRunnerTests.cs index c4426e176ed..4348aa6bcf0 100644 --- a/test/NuGet.Core.Tests/NuGet.CommandLine.Xplat.Tests/Commands/Package/Download/PackageDownloadRunnerTests.cs +++ b/test/NuGet.Core.Tests/NuGet.CommandLine.Xplat.Tests/Commands/Package/Download/PackageDownloadRunnerTests.cs @@ -200,5 +200,4 @@ public async Task ResolvePackageDownloadVersion_UnlistedPackage_BehavesAsExpecte foundRepo.Should().BeNull(); } } - } diff --git a/test/TestUtilities/Test.Utility/FileSystemBackedV3MockServer.cs b/test/TestUtilities/Test.Utility/FileSystemBackedV3MockServer.cs index 125ab70933e..ae0a128b4fc 100644 --- a/test/TestUtilities/Test.Utility/FileSystemBackedV3MockServer.cs +++ b/test/TestUtilities/Test.Utility/FileSystemBackedV3MockServer.cs @@ -99,9 +99,21 @@ private Action ServerHandlerV3(HttpListenerRequest request } else if (path.StartsWith("/flat/") && path.EndsWith(".nupkg")) { - var file = new FileInfo(Path.Combine(_packageDirectory, parts.Last())); + var requestedFileName = parts.Last(); + var directory = new DirectoryInfo(_packageDirectory); + FileInfo file = null; - if (file.Exists) + // Scan for file and ignore case to make sure this works in linux too + foreach (var candidate in directory.EnumerateFiles("*.nupkg", SearchOption.TopDirectoryOnly)) + { + if (string.Equals(candidate.Name, requestedFileName, StringComparison.OrdinalIgnoreCase)) + { + file = candidate; + break; + } + } + + if (file != null && file.Exists) { return new Action(response => {