diff --git a/src/Aspire.Cli/Commands/AddCommand.cs b/src/Aspire.Cli/Commands/AddCommand.cs index fc6f1db8282..e735f096efa 100644 --- a/src/Aspire.Cli/Commands/AddCommand.cs +++ b/src/Aspire.Cli/Commands/AddCommand.cs @@ -91,8 +91,6 @@ protected override async Task ExecuteAsync(ParseResult parseResult, Cancell } } - var source = parseResult.GetValue(s_sourceOption); - // For non-.NET projects, read the channel from the local Aspire configuration if available. // Unlike .NET projects which have a nuget.config, polyglot apphosts persist the channel // in aspire.config.json (or the legacy settings.json during migration). @@ -162,8 +160,6 @@ await Parallel.ForEachAsync(channels, cancellationToken, async (channel, ct) => throw new EmptyChoicesException(AddCommandStrings.NoIntegrationPackagesFound); } - var version = parseResult.GetValue(s_versionOption); - var packagesWithShortName = packagesWithChannels.Select(GenerateFriendlyName).OrderBy(p => p.FriendlyName, new CommunityToolkitFirstComparer()); if (!packagesWithShortName.Any()) @@ -172,7 +168,8 @@ await Parallel.ForEachAsync(channels, cancellationToken, async (channel, ct) => return ExitCodeConstants.FailedToAddPackage; } - var filteredPackagesWithShortName = packagesWithShortName.Where(p => p.FriendlyName == integrationName || p.Package.Id == integrationName); + var filteredPackagesWithShortName = packagesWithShortName + .Where(p => p.FriendlyName == integrationName || p.Package.Id == integrationName); if (!filteredPackagesWithShortName.Any() && integrationName is not null) { @@ -194,19 +191,20 @@ await Parallel.ForEachAsync(channels, cancellationToken, async (channel, ct) => .ToList(); } + var version = parseResult.GetValue(s_versionOption); + // If we didn't match any, show a complete list. If we matched one, and its // an exact match, then we still prompt, but it will only prompt for // the version. If there is more than one match then we prompt. var selectedNuGetPackage = filteredPackagesWithShortName.Count() switch { - 0 => await GetPackageByInteractiveFlowWithNoMatchesMessage(packagesWithShortName, integrationName, cancellationToken), - 1 => filteredPackagesWithShortName.First().Package.Version == version - ? filteredPackagesWithShortName.First() - : await GetPackageByInteractiveFlow(filteredPackagesWithShortName, null, cancellationToken), - > 1 => await GetPackageByInteractiveFlow(filteredPackagesWithShortName, version, cancellationToken), - _ => throw new InvalidOperationException(AddCommandStrings.UnexpectedNumberOfPackagesFound) + 0 => await GetPackageByInteractiveFlowWithNoMatchesMessage(effectiveAppHostProjectFile.Directory!, packagesWithShortName, integrationName, cancellationToken), + 1 when filteredPackagesWithShortName.First().Package.Version == version + => filteredPackagesWithShortName.First(), + _ => await GetPackageByInteractiveFlow(effectiveAppHostProjectFile.Directory!, filteredPackagesWithShortName, version, cancellationToken) }; + var source = parseResult.GetValue(s_sourceOption); // Add the package using the appropriate project handler context = new AddPackageContext { @@ -280,7 +278,24 @@ await Parallel.ForEachAsync(channels, cancellationToken, async (channel, ct) => } } - private async Task<(string FriendlyName, NuGetPackage Package, PackageChannel Channel)> GetPackageByInteractiveFlow(IEnumerable<(string FriendlyName, NuGetPackage Package, PackageChannel Channel)> possiblePackages, string? preferredVersion, CancellationToken cancellationToken) + private static async Task> GetAllPackageVersions(DirectoryInfo workingDirectory, IEnumerable<(string FriendlyName, NuGetPackage Package, PackageChannel Channel)> possiblePackages, CancellationToken cancellationToken) + { + var distinctPackageIds = possiblePackages.DistinctBy(package => package.Package.Id); + var channels = possiblePackages.Select(package => package.Channel); + + var versions = new List<(string FriendlyName, NuGetPackage Package, PackageChannel Channel)>(); + foreach (var channel in channels) + { + foreach (var package in distinctPackageIds) + { + var packages = await channel.GetPackageVersionsAsync(package.Package.Id, workingDirectory, cancellationToken); + versions.AddRange(packages.Select(p => (FriendlyName: package.FriendlyName, Package: p, Channel: channel))); + } + } + return versions; + } + + private async Task<(string FriendlyName, NuGetPackage Package, PackageChannel Channel)> GetPackageByInteractiveFlow(DirectoryInfo workingDirectory, IEnumerable<(string FriendlyName, NuGetPackage Package, PackageChannel Channel)> possiblePackages, string? preferredVersion, CancellationToken cancellationToken) { var distinctPackages = possiblePackages.DistinctBy(p => p.Package.Id); @@ -298,10 +313,21 @@ await Parallel.ForEachAsync(channels, cancellationToken, async (channel, ct) => // If any of the package versions are an exact match for the preferred version // then we can skip the version prompt and just use that version. - if (packageVersions.Any(p => p.Package.Version == preferredVersion)) + if (!string.IsNullOrEmpty(preferredVersion)) { - var preferredVersionPackage = packageVersions.First(p => p.Package.Version == preferredVersion); - return preferredVersionPackage; + if (packageVersions.Any(p => p.Package.Version == preferredVersion)) + { + var preferredVersionPackage = packageVersions.First(p => p.Package.Version == preferredVersion); + return preferredVersionPackage; + } + else // search all versions of the selected package for a match + { + var allVersions = await GetAllPackageVersions(workingDirectory, possiblePackages, cancellationToken); + if (allVersions.Any(packageVersion => packageVersion.Package.Version == preferredVersion)) + { + return allVersions.First(package => package.Package.Version == preferredVersion); + } + } } // In non-interactive mode, prefer the implicit/default channel first to keep @@ -321,14 +347,14 @@ await Parallel.ForEachAsync(channels, cancellationToken, async (channel, ct) => return version; } - private async Task<(string FriendlyName, NuGetPackage Package, PackageChannel Channel)> GetPackageByInteractiveFlowWithNoMatchesMessage(IEnumerable<(string FriendlyName, NuGetPackage Package, PackageChannel Channel)> possiblePackages, string? searchTerm, CancellationToken cancellationToken) + private async Task<(string FriendlyName, NuGetPackage Package, PackageChannel Channel)> GetPackageByInteractiveFlowWithNoMatchesMessage(DirectoryInfo workingDirectory, IEnumerable<(string FriendlyName, NuGetPackage Package, PackageChannel Channel)> possiblePackages, string? searchTerm, CancellationToken cancellationToken) { if (searchTerm is not null) { InteractionService.DisplaySubtleMessage(string.Format(CultureInfo.CurrentCulture, AddCommandStrings.NoPackagesMatchedSearchTerm, searchTerm)); } - return await GetPackageByInteractiveFlow(possiblePackages, null, cancellationToken); + return await GetPackageByInteractiveFlow(workingDirectory, possiblePackages, null, cancellationToken); } internal static (string FriendlyName, NuGetPackage Package, PackageChannel Channel) GenerateFriendlyName((NuGetPackage Package, PackageChannel Channel) packageWithChannel) diff --git a/src/Aspire.Cli/DotNet/DotNetCliRunner.cs b/src/Aspire.Cli/DotNet/DotNetCliRunner.cs index 2f88d686563..9e90a67c75e 100644 --- a/src/Aspire.Cli/DotNet/DotNetCliRunner.cs +++ b/src/Aspire.Cli/DotNet/DotNetCliRunner.cs @@ -35,7 +35,7 @@ internal interface IDotNetCliRunner Task BuildAsync(FileInfo projectFilePath, bool noRestore, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken); Task AddPackageAsync(FileInfo projectFilePath, string packageName, string packageVersion, string? nugetSource, bool noRestore, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken); Task AddProjectToSolutionAsync(FileInfo solutionFile, FileInfo projectFile, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken); - Task<(int ExitCode, NuGetPackage[]? Packages)> SearchPackagesAsync(DirectoryInfo workingDirectory, string query, bool prerelease, int take, int skip, FileInfo? nugetConfigFile, bool useCache, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken); + Task<(int ExitCode, NuGetPackage[]? Packages)> SearchPackagesAsync(DirectoryInfo workingDirectory, string query, bool exactMatch, bool prerelease, int take, int skip, FileInfo? nugetConfigFile, bool useCache, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken); Task<(int ExitCode, string[] ConfigPaths)> GetNuGetConfigPathsAsync(DirectoryInfo workingDirectory, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken); Task<(int ExitCode, IReadOnlyList Projects)> GetSolutionProjectsAsync(FileInfo solutionFile, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken); Task AddProjectReferenceAsync(FileInfo projectFile, FileInfo referencedProject, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken); @@ -316,7 +316,7 @@ private async Task StartBackchannelAsync(IDotNetCliExecution? execution, string using var activity = telemetry.StartDiagnosticActivity(); var isSingleFileAppHost = projectFile.Name.Equals("apphost.cs", StringComparison.OrdinalIgnoreCase); - + // If we are a single file app host then we use the build command instead of msbuild command. var cliArgsList = new List { isSingleFileAppHost ? "build" : "msbuild" }; @@ -826,7 +826,7 @@ public async Task ComputeNuGetConfigHierarchySha256Async(DirectoryInfo w return result; } - public async Task<(int ExitCode, NuGetPackage[]? Packages)> SearchPackagesAsync(DirectoryInfo workingDirectory, string query, bool prerelease, int take, int skip, FileInfo? nugetConfigFile, bool useCache, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken) + public async Task<(int ExitCode, NuGetPackage[]? Packages)> SearchPackagesAsync(DirectoryInfo workingDirectory, string query, bool exactMatch, bool prerelease, int take, int skip, FileInfo? nugetConfigFile, bool useCache, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken) { using var activity = telemetry.StartDiagnosticActivity(); @@ -851,7 +851,7 @@ public async Task ComputeNuGetConfigHierarchySha256Async(DirectoryInfo w // Build a cache key using the main discriminators, including CLI version. var cliVersion = VersionHelper.GetDefaultTemplateVersion(); - rawKey = $"query={query}|prerelease={prerelease}|take={take}|skip={skip}|nugetConfigHash={nugetConfigHash}|cliVersion={cliVersion}"; + rawKey = $"query={query}|exactMatch={exactMatch}|prerelease={prerelease}|take={take}|skip={skip}|nugetConfigHash={nugetConfigHash}|cliVersion={cliVersion}"; var cached = await _diskCache.GetAsync(rawKey, cancellationToken).ConfigureAwait(false); if (cached is not null) { @@ -878,14 +878,24 @@ public async Task ComputeNuGetConfigHierarchySha256Async(DirectoryInfo w "package", "search", query, - "--take", - take.ToString(CultureInfo.InvariantCulture), - "--skip", - skip.ToString(CultureInfo.InvariantCulture), "--format", "json" ]; + if (exactMatch) // search for all versions that match the query exactly + { + cliArgs.Add("--exact-match"); + } + else // 'exaxt-match' flag causes the take and skip arguments to be ignored + { + cliArgs.AddRange([ + "--take", + take.ToString(CultureInfo.InvariantCulture), + "--skip", + skip.ToString(CultureInfo.InvariantCulture), + ]); + } + if (nugetConfigFile is not null) { cliArgs.Add("--configfile"); @@ -1073,7 +1083,7 @@ public async Task ComputeNuGetConfigHierarchySha256Async(DirectoryInfo w // Parse output - skip header lines (Project(s) and ----------) var projects = new List(); var startParsing = false; - + foreach (var line in stdoutLines) { if (string.IsNullOrWhiteSpace(line)) diff --git a/src/Aspire.Cli/NuGet/BundleNuGetPackageCache.cs b/src/Aspire.Cli/NuGet/BundleNuGetPackageCache.cs index 7b21897deed..52f6e779f4a 100644 --- a/src/Aspire.Cli/NuGet/BundleNuGetPackageCache.cs +++ b/src/Aspire.Cli/NuGet/BundleNuGetPackageCache.cs @@ -104,6 +104,24 @@ public async Task> GetPackagesAsync( return FilterPackages(packages, filter); } + public async Task> GetPackageVersionsAsync( + DirectoryInfo workingDirectory, + string exactPackageId, + bool prerelease, + FileInfo? nugetConfigFile, + bool useCache, + CancellationToken cancellationToken) + { + return await GetPackagesAsync( + workingDirectory, + exactPackageId, + filter: id => string.Equals(id, exactPackageId, StringComparison.OrdinalIgnoreCase), + prerelease, + nugetConfigFile, + useCache, + cancellationToken); + } + private async Task> SearchPackagesInternalAsync( DirectoryInfo workingDirectory, string query, diff --git a/src/Aspire.Cli/NuGet/NuGetPackageCache.cs b/src/Aspire.Cli/NuGet/NuGetPackageCache.cs index 3ca8347e88d..87d68fe22ec 100644 --- a/src/Aspire.Cli/NuGet/NuGetPackageCache.cs +++ b/src/Aspire.Cli/NuGet/NuGetPackageCache.cs @@ -17,12 +17,13 @@ internal interface INuGetPackageCache Task> GetIntegrationPackagesAsync(DirectoryInfo workingDirectory, bool prerelease, FileInfo? nugetConfigFile, CancellationToken cancellationToken); Task> GetCliPackagesAsync(DirectoryInfo workingDirectory, bool prerelease, FileInfo? nugetConfigFile, CancellationToken cancellationToken); Task> GetPackagesAsync(DirectoryInfo workingDirectory, string packageId, Func? filter, bool prerelease, FileInfo? nugetConfigFile, bool useCache, CancellationToken cancellationToken); + Task> GetPackageVersionsAsync(DirectoryInfo workingDirectory, string exactPackageId, bool prerelease, FileInfo? nugetConfigFile, bool useCache, CancellationToken cancellationToken); } internal sealed class NuGetPackageCache(IDotNetCliRunner cliRunner, IMemoryCache memoryCache, AspireCliTelemetry telemetry, IFeatures features) : INuGetPackageCache { private const int SearchPageSize = 1000; - + // List of deprecated packages that should be filtered by default private static readonly HashSet s_deprecatedPackages = new(StringComparer.OrdinalIgnoreCase) { @@ -87,6 +88,7 @@ public async Task> GetPackagesAsync(DirectoryInfo work var result = await cliRunner.SearchPackagesAsync( workingDirectory, query, + exactMatch: false, prerelease, SearchPageSize, skip, @@ -121,7 +123,7 @@ public async Task> GetPackagesAsync(DirectoryInfo work // If no specific filter is specified we use the fallback filter which is useful in most circumstances // other that aspire update which really needs to see all the packages to work effectively. - var effectiveFilter = (NuGetPackage p) => + var effectiveFilter = (NuGetPackage p) => { if (filter is not null) { @@ -129,7 +131,7 @@ public async Task> GetPackagesAsync(DirectoryInfo work } var isOfficialPackage = IsOfficialOrCommunityToolkitPackage(p.Id); - + // Apply deprecated package filter unless the user wants to show deprecated packages if (isOfficialPackage && !features.IsFeatureEnabled(KnownFeatures.ShowDeprecatedPackages, defaultValue: false)) { @@ -138,7 +140,7 @@ public async Task> GetPackagesAsync(DirectoryInfo work return isOfficialPackage; }; - + return collectedPackages.Where(effectiveFilter); static bool IsOfficialOrCommunityToolkitPackage(string packageName) @@ -157,6 +159,50 @@ static bool IsOfficialOrCommunityToolkitPackage(string packageName) return isHostingOrCommunityToolkitNamespaced && !isExcluded; } } + + public async Task> GetPackageVersionsAsync(DirectoryInfo workingDirectory, string exactPackageId, bool prerelease, FileInfo? nugetConfigFile, bool useCache, CancellationToken cancellationToken) + { + using var activity = telemetry.StartDiagnosticActivity(); + + var collectedPackages = new List(); + + var result = await cliRunner.SearchPackagesAsync( + workingDirectory, + exactPackageId, + exactMatch: true, + prerelease, + take: 0, + skip: 0, // skip and take parameters are ignored when exactMatch is true + nugetConfigFile, + useCache, // Pass through the useCache parameter + new DotNetCliRunnerInvocationOptions { SuppressLogging = true }, + cancellationToken + ); + + if (result.ExitCode != 0) + { + throw new NuGetPackageCacheException(string.Format(CultureInfo.CurrentCulture, ErrorStrings.FailedToSearchForPackages, result.ExitCode)); + } + + if (result.Packages?.Length > 0) + { + collectedPackages.AddRange(result.Packages); + } + + // If no specific filter is specified we use the fallback filter which is useful in most circumstances + // other that aspire update which really needs to see all the packages to work effectively. + var effectiveFilter = (NuGetPackage p) => + { + // Apply deprecated package filter unless the user wants to show deprecated packages + if (!features.IsFeatureEnabled(KnownFeatures.ShowDeprecatedPackages, defaultValue: false)) + { + return !s_deprecatedPackages.Contains(p.Id); + } + return true; + }; + + return collectedPackages.Where(effectiveFilter); + } } internal sealed class NuGetPackageCacheException(string message) : Exception(message) diff --git a/src/Aspire.Cli/Packaging/PackageChannel.cs b/src/Aspire.Cli/Packaging/PackageChannel.cs index 5bcd3bb2071..2ccd6e94db2 100644 --- a/src/Aspire.Cli/Packaging/PackageChannel.cs +++ b/src/Aspire.Cli/Packaging/PackageChannel.cs @@ -17,16 +17,16 @@ internal class PackageChannel(string name, PackageChannelQuality quality, Packag public bool ConfigureGlobalPackagesFolder { get; } = configureGlobalPackagesFolder; public string? CliDownloadBaseUrl { get; } = cliDownloadBaseUrl; public string? PinnedVersion { get; } = pinnedVersion; - + public string SourceDetails { get; } = ComputeSourceDetails(mappings); - + private static string ComputeSourceDetails(PackageMapping[]? mappings) { if (mappings is null) { return PackagingStrings.BasedOnNuGetConfig; } - + var aspireMapping = mappings.FirstOrDefault(m => m.PackageFilter.StartsWith("Aspire", StringComparison.OrdinalIgnoreCase)); var allPackagesMapping = mappings.FirstOrDefault(m => m.PackageFilter == PackageMapping.AllPackages); @@ -67,7 +67,7 @@ public async Task> GetTemplatePackagesAsync(DirectoryI .SelectMany(p => p) .DistinctBy(p => $"{p.Id}-{p.Version}"); - // When doing a `dotnet package search` the the results may include stable packages even when searching for + // When doing a `dotnet package search` the results may include stable packages even when searching for // prerelease packages. This filters out this noise. var filteredPackages = packages.Where(p => new { SemVer = SemVersion.Parse(p.Version), Quality = Quality } switch { @@ -102,7 +102,7 @@ public async Task> GetIntegrationPackagesAsync(Directo .SelectMany(p => p) .DistinctBy(p => $"{p.Id}-{p.Version}"); - // When doing a `dotnet package search` the the results may include stable packages even when searching for + // When doing a `dotnet package search` the results may include stable packages even when searching for // prerelease packages. This filters out this noise. var filteredPackages = packages.Where(p => new { SemVer = SemVersion.Parse(p.Version), Quality = Quality } switch { @@ -180,7 +180,70 @@ public async Task> GetPackagesAsync(string packageId, return packages; } - // When doing a `dotnet package search` the the results may include stable packages even when searching for + // When doing a `dotnet package search` the results may include stable packages even when searching for + // prerelease packages. This filters out this noise. + var filteredPackages = packages.Where(p => new { SemVer = SemVersion.Parse(p.Version), Quality = Quality } switch + { + { Quality: PackageChannelQuality.Both } => true, + { Quality: PackageChannelQuality.Stable, SemVer: { IsPrerelease: false } } => true, + { Quality: PackageChannelQuality.Prerelease, SemVer: { IsPrerelease: true } } => true, + _ => false + }); + + return filteredPackages; + } + + public async Task> GetPackageVersionsAsync(string packageId, DirectoryInfo workingDirectory, CancellationToken cancellationToken) + { + var tasks = new List>>(); + + using var tempNuGetConfig = Type is PackageChannelType.Explicit ? await TemporaryNuGetConfig.CreateAsync(Mappings!) : null; + + if (Quality is PackageChannelQuality.Stable || Quality is PackageChannelQuality.Both) + { + tasks.Add(nuGetPackageCache.GetPackageVersionsAsync( + workingDirectory: workingDirectory, + exactPackageId: packageId, + prerelease: false, + nugetConfigFile: tempNuGetConfig?.ConfigFile, + useCache: true, // Enable caching for package channel resolution + cancellationToken: cancellationToken)); + } + + if (Quality is PackageChannelQuality.Prerelease || Quality is PackageChannelQuality.Both) + { + tasks.Add(nuGetPackageCache.GetPackageVersionsAsync( + workingDirectory: workingDirectory, + exactPackageId: packageId, + prerelease: true, + nugetConfigFile: tempNuGetConfig?.ConfigFile, + useCache: true, // Enable caching for package channel resolution + cancellationToken: cancellationToken)); + } + + var packageResults = await Task.WhenAll(tasks); + + var packages = packageResults + .SelectMany(p => p) + .DistinctBy(p => $"{p.Id}-{p.Version}"); + + // In the event that we have no stable packages we fallback to + // returning prerelease packages. Example a package that is currently + // in preview (Aspire.Hosting.Docker circa 9.4). + if (Quality is PackageChannelQuality.Stable && !packages.Any()) + { + packages = await nuGetPackageCache.GetPackageVersionsAsync( + workingDirectory: workingDirectory, + exactPackageId: packageId, + prerelease: true, + nugetConfigFile: tempNuGetConfig?.ConfigFile, + useCache: true, // Enable caching for package channel resolution + cancellationToken: cancellationToken); + + return packages; + } + + // When doing a `dotnet package search` the results may include stable packages even when searching for // prerelease packages. This filters out this noise. var filteredPackages = packages.Where(p => new { SemVer = SemVersion.Parse(p.Version), Quality = Quality } switch { @@ -207,4 +270,4 @@ public static PackageChannel CreateImplicitChannel(INuGetPackageCache nuGetPacka // for broader templating options. return new PackageChannel("default", PackageChannelQuality.Both, null, nuGetPackageCache); } -} \ No newline at end of file +} diff --git a/tests/Aspire.Cli.EndToEnd.Tests/CentralPackageManagementTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/CentralPackageManagementTests.cs index 099501c4689..26a2573d9bf 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/CentralPackageManagementTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/CentralPackageManagementTests.cs @@ -170,33 +170,8 @@ public async Task AspireAddPackageVersionToDirectoryPackagesProps() """); - var waitingForVersionSelection = new CellPatternSearcher() - .Find("Select a version of"); - var versionSelectionShown = false; - - await auto.TypeAsync("aspire add Aspire.Hosting.Redis"); + await auto.TypeAsync("aspire add Aspire.Hosting.Redis --version 13.1.2"); await auto.EnterAsync(); - await auto.WaitUntilAsync(s => - { - if (waitingForVersionSelection.Search(s).Count > 0) - { - versionSelectionShown = true; - return true; - } - - var successPromptSearcher = new CellPatternSearcher() - .FindPattern(counter.Value.ToString()) - .RightText(" OK] $ "); - - return successPromptSearcher.Search(s).Count > 0; - }, timeout: TimeSpan.FromSeconds(180), description: "version selection prompt or success prompt"); - - if (versionSelectionShown) - { - // PR hives can surface multiple channels in CI. Accept the default implicit-channel version - // so this test validates CPM behavior without pinning a specific package version. - await auto.EnterAsync(); - } await auto.WaitForSuccessPromptAsync(counter); diff --git a/tests/Aspire.Cli.Tests/Commands/AddCommandTests.cs b/tests/Aspire.Cli.Tests/Commands/AddCommandTests.cs index 6b7921dac29..f6f71261505 100644 --- a/tests/Aspire.Cli.Tests/Commands/AddCommandTests.cs +++ b/tests/Aspire.Cli.Tests/Commands/AddCommandTests.cs @@ -47,7 +47,7 @@ public async Task AddCommandInteractiveFlowSmokeTest() options.DotNetCliRunnerFactory = (sp) => { var runner = new TestDotNetCliRunner(); - runner.SearchPackagesAsyncCallback = (dir, query, prerelease, take, skip, nugetSource, useCache, options, cancellationToken) => + runner.SearchPackagesAsyncCallback = (dir, query, exactMatch, prerelease, take, skip, nugetSource, useCache, options, cancellationToken) => { var dockerPackage = new NuGetPackage() { @@ -122,7 +122,7 @@ public async Task AddCommandDoesNotPromptForIntegrationArgumentIfSpecifiedOnComm options.DotNetCliRunnerFactory = (sp) => { var runner = new TestDotNetCliRunner(); - runner.SearchPackagesAsyncCallback = (dir, query, prerelease, take, skip, nugetSource, useCache, options, cancellationToken) => + runner.SearchPackagesAsyncCallback = (dir, query, exactMatch, prerelease, take, skip, nugetSource, useCache, options, cancellationToken) => { var dockerPackage = new NuGetPackage() { @@ -205,7 +205,7 @@ public async Task AddCommandDoesNotPromptForVersionIfSpecifiedOnCommandLine() options.DotNetCliRunnerFactory = (sp) => { var runner = new TestDotNetCliRunner(); - runner.SearchPackagesAsyncCallback = (dir, query, prerelease, take, skip, nugetSource, useCache, options, cancellationToken) => + runner.SearchPackagesAsyncCallback = (dir, query, exactMatch, prerelease, take, skip, nugetSource, useCache, options, cancellationToken) => { var dockerPackage = new NuGetPackage() { @@ -254,6 +254,211 @@ public async Task AddCommandDoesNotPromptForVersionIfSpecifiedOnCommandLine() Assert.False(promptedForVersion); } + [Fact] + public async Task AddCommandDoesNotPromptForVersionIfOlderVersionSpecifiedOnCommandLine() + { + var promptedForIntegrationPackages = false; + var promptedForVersion = false; + + using var workspace = TemporaryWorkspace.Create(outputHelper); + var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options => + { + options.AddCommandPrompterFactory = (sp) => + { + var interactionService = sp.GetRequiredService(); + var prompter = new TestAddCommandPrompter(interactionService); + + prompter.PromptForIntegrationCallback = (packages) => + { + promptedForIntegrationPackages = true; + throw new InvalidOperationException("Should not have been prompted for integration packages."); + }; + + prompter.PromptForIntegrationVersionCallback = (packages) => + { + promptedForVersion = true; + throw new InvalidOperationException("Should not have been prompted for integration version."); + }; + + return prompter; + }; + + options.ProjectLocatorFactory = _ => new TestProjectLocator(); + + options.DotNetCliRunnerFactory = (sp) => + { + var runner = new TestDotNetCliRunner(); + runner.SearchPackagesAsyncCallback = (dir, query, exactMatch, prerelease, take, skip, nugetSource, useCache, options, cancellationToken) => + { + var dockerLatestPackage = new NuGetPackage() + { + Id = "Aspire.Hosting.Docker", + Source = "nuget", + Version = "13.2.0" + }; + var dockerOlderPackage = new NuGetPackage() + { + Id = "Aspire.Hosting.Docker", + Source = "nuget", + Version = "9.2.0" + }; + + var redisPackage = new NuGetPackage() + { + Id = "Aspire.Hosting.Redis", + Source = "nuget", + Version = "13.2.0" + }; + + var azureRedisPackage = new NuGetPackage() + { + Id = "Aspire.Hosting.Azure.Redis", + Source = "nuget", + Version = "13.2.0" + }; + + if (!exactMatch) // package search returns only latest version + { + return ( + 0, // Exit code. + new NuGetPackage[] { dockerLatestPackage, redisPackage, azureRedisPackage } + ); + } + else // exact match gets all previous versions of a specific package + { + return ( + 0, // Exit code. + new NuGetPackage[] { dockerLatestPackage, dockerOlderPackage } + ); + } + }; + + runner.AddPackageAsyncCallback = (projectFilePath, packageName, packageVersion, nugetSource, noRestore, options, cancellationToken) => + { + // Simulate adding the package. + return 0; // Success. + }; + + return runner; + }; + }); + var provider = services.BuildServiceProvider(); + + var command = provider.GetRequiredService(); + var result = command.Parse("add docker --version 9.2.0"); + + var exitCode = await result.InvokeAsync().DefaultTimeout(); + Assert.Equal(0, exitCode); + Assert.False(promptedForIntegrationPackages); + Assert.False(promptedForVersion); + } + + [Fact] + public async Task AddCommandPromptsForLatestVersionIfVersionSpecifiedOnCommandLineDoesNotExist() + { + IEnumerable<(string FriendlyName, NuGetPackage Package, PackageChannel Channel)>? promptedPackageVersions = null; + var promptedForIntegrationPackages = false; + var promptedForVersion = false; + + using var workspace = TemporaryWorkspace.Create(outputHelper); + var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options => + { + options.CliHostEnvironmentFactory = _ => TestHelpers.CreateInteractiveHostEnvironment(); + + options.AddCommandPrompterFactory = (sp) => + { + var interactionService = sp.GetRequiredService(); + var prompter = new TestAddCommandPrompter(interactionService); + + prompter.PromptForIntegrationCallback = (packages) => + { + promptedForIntegrationPackages = true; + throw new InvalidOperationException("Should not have been prompted for integration packages."); + }; + + prompter.PromptForIntegrationVersionCallback = (packages) => + { + promptedForVersion = true; + promptedPackageVersions = packages; + return packages.First(); + }; + + return prompter; + }; + + options.ProjectLocatorFactory = _ => new TestProjectLocator(); + + options.DotNetCliRunnerFactory = (sp) => + { + var runner = new TestDotNetCliRunner(); + runner.SearchPackagesAsyncCallback = (dir, query, exactMatch, prerelease, take, skip, nugetSource, useCache, options, cancellationToken) => + { + var dockerLatestPackage = new NuGetPackage() + { + Id = "Aspire.Hosting.Docker", + Source = "nuget", + Version = "13.2.0" + }; + var dockerOlderPackage = new NuGetPackage() + { + Id = "Aspire.Hosting.Docker", + Source = "nuget", + Version = "9.2.0" + }; + + var redisPackage = new NuGetPackage() + { + Id = "Aspire.Hosting.Redis", + Source = "nuget", + Version = "13.2.0" + }; + + var azureRedisPackage = new NuGetPackage() + { + Id = "Aspire.Hosting.Azure.Redis", + Source = "nuget", + Version = "13.2.0" + }; + + if (!exactMatch) // package search returns only latest version + { + return ( + 0, // Exit code. + new NuGetPackage[] { dockerLatestPackage, redisPackage, azureRedisPackage } + ); + } + else // exact match gets all previous versions of a specific package + { + return ( + 0, // Exit code. + new NuGetPackage[] { dockerLatestPackage, dockerOlderPackage } + ); + } + }; + + runner.AddPackageAsyncCallback = (projectFilePath, packageName, packageVersion, nugetSource, noRestore, options, cancellationToken) => + { + // Simulate adding the package. + return 0; // Success. + }; + + return runner; + }; + }); + var provider = services.BuildServiceProvider(); + + var command = provider.GetRequiredService(); + var result = command.Parse("add docker --version 11.2.0"); + + var exitCode = await result.InvokeAsync().DefaultTimeout(); + Assert.Equal(0, exitCode); + Assert.False(promptedForIntegrationPackages); + Assert.True(promptedForVersion); + var promptedPackage = Assert.Single(promptedPackageVersions!); + Assert.Equal("Aspire.Hosting.Docker", promptedPackage.Package.Id); + Assert.Equal("13.2.0", promptedPackage.Package.Version); + } + [Fact] public async Task AddCommandPromptsForDisambiguation() { @@ -285,7 +490,7 @@ public async Task AddCommandPromptsForDisambiguation() options.DotNetCliRunnerFactory = (sp) => { var runner = new TestDotNetCliRunner(); - runner.SearchPackagesAsyncCallback = (dir, query, prerelease, take, skip, nugetSource, useCache, options, cancellationToken) => + runner.SearchPackagesAsyncCallback = (dir, query, exactMatch, prerelease, take, skip, nugetSource, useCache, options, cancellationToken) => { var dockerPackage = new NuGetPackage() { @@ -366,7 +571,7 @@ public async Task AddCommandPreservesSourceArgumentInBothCommands() options.DotNetCliRunnerFactory = (sp) => { var runner = new TestDotNetCliRunner(); - runner.SearchPackagesAsyncCallback = (dir, query, prerelease, take, skip, nugetSource, useCache, options, cancellationToken) => + runner.SearchPackagesAsyncCallback = (dir, query, exactMatch, prerelease, take, skip, nugetSource, useCache, options, cancellationToken) => { var redisPackage = new NuGetPackage() { @@ -425,7 +630,7 @@ public async Task AddCommand_EmptyPackageList_DisplaysErrorMessage() options.DotNetCliRunnerFactory = (sp) => { var runner = new TestDotNetCliRunner(); - runner.SearchPackagesAsyncCallback = (dir, query, prerelease, take, skip, nugetSource, useCache, options, cancellationToken) => + runner.SearchPackagesAsyncCallback = (dir, query, exactMatch, prerelease, take, skip, nugetSource, useCache, options, cancellationToken) => { return (0, Array.Empty()); }; @@ -482,7 +687,7 @@ public async Task AddCommand_NoMatchingPackages_DisplaysNoMatchesMessage() options.DotNetCliRunnerFactory = (sp) => { var runner = new TestDotNetCliRunner(); - runner.SearchPackagesAsyncCallback = (dir, query, prerelease, take, skip, nugetSource, useCache, options, cancellationToken) => + runner.SearchPackagesAsyncCallback = (dir, query, exactMatch, prerelease, take, skip, nugetSource, useCache, options, cancellationToken) => { var dockerPackage = new NuGetPackage() { @@ -666,7 +871,7 @@ public async Task AddCommandPrompter_ShowsHighestVersionPerChannelWhenMultipleCh // Create two different channels var fakeCache = new FakeNuGetPackageCache(); var implicitChannel = PackageChannel.CreateImplicitChannel(fakeCache); - + var mappings = new[] { new PackageMapping("Aspire*", "https://preview-feed") }; var explicitChannel = PackageChannel.CreateExplicitChannel("preview", PackageChannelQuality.Prerelease, mappings, fakeCache); @@ -693,9 +898,9 @@ public async Task AddCommand_WithoutHives_UsesImplicitChannelWithoutPrompting() { // Arrange using var workspace = TemporaryWorkspace.Create(outputHelper); - + var selectedPackageId = string.Empty; - + var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options => { options.ProjectLocatorFactory = _ => new TestProjectLocator(); @@ -711,7 +916,7 @@ public async Task AddCommand_WithoutHives_UsesImplicitChannelWithoutPrompting() options.DotNetCliRunnerFactory = (sp) => { var runner = new TestDotNetCliRunner(); - runner.SearchPackagesAsyncCallback = (dir, query, prerelease, take, skip, nugetSource, useCache, options, cancellationToken) => + runner.SearchPackagesAsyncCallback = (dir, query, exactMatch, prerelease, take, skip, nugetSource, useCache, options, cancellationToken) => { var redisPackage = new NuGetPackage() { @@ -732,7 +937,7 @@ public async Task AddCommand_WithoutHives_UsesImplicitChannelWithoutPrompting() return runner; }; }); - + var provider = services.BuildServiceProvider(); // Act - without hives, should automatically select from implicit channel without prompting @@ -770,7 +975,7 @@ public async Task AddCommand_WithHives_PrefersImplicitChannelVersionInNonInterac options.DotNetCliRunnerFactory = (sp) => { var runner = new TestDotNetCliRunner(); - runner.SearchPackagesAsyncCallback = (dir, query, prerelease, take, skip, nugetSource, useCache, invocationOptions, cancellationToken) => + runner.SearchPackagesAsyncCallback = (dir, query, exactMatch, prerelease, take, skip, nugetSource, useCache, invocationOptions, cancellationToken) => { var implicitPackage = new NuGetPackage { @@ -868,7 +1073,7 @@ public async Task AddCommand_WithStartsWith_FindsMatchUsingFuzzySearch() options.DotNetCliRunnerFactory = (sp) => { var runner = new TestDotNetCliRunner(); - runner.SearchPackagesAsyncCallback = (dir, query, prerelease, take, skip, nugetSource, useCache, options, cancellationToken) => + runner.SearchPackagesAsyncCallback = (dir, query, exactMatch, prerelease, take, skip, nugetSource, useCache, options, cancellationToken) => { var postgresPackage = new NuGetPackage() { @@ -947,7 +1152,7 @@ public async Task AddCommand_WithPartialMatch_FiltersUsingFuzzySearch() options.DotNetCliRunnerFactory = (sp) => { var runner = new TestDotNetCliRunner(); - runner.SearchPackagesAsyncCallback = (dir, query, prerelease, take, skip, nugetSource, useCache, options, cancellationToken) => + runner.SearchPackagesAsyncCallback = (dir, query, exactMatch, prerelease, take, skip, nugetSource, useCache, options, cancellationToken) => { var postgresPackage = new NuGetPackage() { @@ -1023,7 +1228,7 @@ public async Task AddCommand_WithTypo_FindsMatchUsingFuzzySearch() options.DotNetCliRunnerFactory = (sp) => { var runner = new TestDotNetCliRunner(); - runner.SearchPackagesAsyncCallback = (dir, query, prerelease, take, skip, nugetSource, useCache, options, cancellationToken) => + runner.SearchPackagesAsyncCallback = (dir, query, exactMatch, prerelease, take, skip, nugetSource, useCache, options, cancellationToken) => { var appContainersPackage = new NuGetPackage() { diff --git a/tests/Aspire.Cli.Tests/Commands/InitCommandTests.cs b/tests/Aspire.Cli.Tests/Commands/InitCommandTests.cs index a671db89691..b1bee98370f 100644 --- a/tests/Aspire.Cli.Tests/Commands/InitCommandTests.cs +++ b/tests/Aspire.Cli.Tests/Commands/InitCommandTests.cs @@ -7,6 +7,7 @@ using Aspire.Cli.Packaging; using Aspire.Cli.Tests.TestServices; using Aspire.Cli.Tests.Utils; +using Aspire.Shared; using Microsoft.Extensions.DependencyInjection; using Microsoft.AspNetCore.InternalTesting; @@ -352,7 +353,7 @@ public async Task InitCommand_WithSingleFileAppHost_DoesNotPromptForProjectNameO }; // Mock package search for template version selection - runner.SearchPackagesAsyncCallback = (dir, query, prerelease, take, skip, nugetConfigFile, useCache, invocationOptions, cancellationToken) => + runner.SearchPackagesAsyncCallback = (dir, query, exactMatch, prerelease, take, skip, nugetConfigFile, useCache, invocationOptions, cancellationToken) => { var package = new Aspire.Shared.NuGetPackageCli { @@ -460,13 +461,18 @@ private sealed class FakeNuGetPackageCache : INuGetPackageCache { return Task.FromResult>(Array.Empty()); } + + public Task> GetPackageVersionsAsync(DirectoryInfo workingDirectory, string exactPackageId, bool prerelease, FileInfo? nugetConfigFile, bool useCache, CancellationToken cancellationToken) + { + return Task.FromResult>(Array.Empty()); + } } [Fact] public async Task InitCommandWithChannelOptionUsesSpecifiedChannel() { using var workspace = TemporaryWorkspace.Create(outputHelper); - + string? channelNameUsed = null; bool promptedForVersion = false; @@ -476,13 +482,13 @@ public async Task InitCommandWithChannelOptionUsesSpecifiedChannel() { var interactionService = sp.GetRequiredService(); var prompter = new TestNewCommandPrompter(interactionService); - + prompter.PromptForTemplatesVersionCallback = (packages) => { promptedForVersion = true; throw new InvalidOperationException("Should not prompt for version when --channel is specified"); }; - + return prompter; }; @@ -490,7 +496,7 @@ public async Task InitCommandWithChannelOptionUsesSpecifiedChannel() { return new TestPackagingServiceWithChannelTracking((channelName) => channelNameUsed = channelName); }; - + options.DotNetCliRunnerFactory = (sp) => { var runner = new TestDotNetCliRunner(); @@ -513,7 +519,7 @@ public async Task InitCommandWithChannelOptionUsesSpecifiedChannel() var result = command.Parse("init --channel stable"); var exitCode = await result.InvokeAsync().DefaultTimeout(); - + // Assert Assert.Equal(0, exitCode); Assert.Equal("stable", channelNameUsed); @@ -538,7 +544,7 @@ public async Task InitCommandWithInvalidChannelShowsError() var result = command.Parse("init --channel invalid-channel"); var exitCode = await result.InvokeAsync().DefaultTimeout(); - + // Assert - should fail with non-zero exit code for invalid channel Assert.NotEqual(0, exitCode); } @@ -549,10 +555,10 @@ public Task> GetChannelsAsync(CancellationToken canc { var stableCache = new FakeNuGetPackageCacheWithTracking("stable", onChannelUsed); var dailyCache = new FakeNuGetPackageCacheWithTracking("daily", onChannelUsed); - + var stableChannel = PackageChannel.CreateExplicitChannel("stable", PackageChannelQuality.Both, [], stableCache); var dailyChannel = PackageChannel.CreateExplicitChannel("daily", PackageChannelQuality.Both, [], dailyCache); - + return Task.FromResult>(new[] { stableChannel, dailyChannel }); } } @@ -585,5 +591,10 @@ private sealed class FakeNuGetPackageCacheWithTracking(string channelName, Actio { return Task.FromResult>(Array.Empty()); } + + public Task> GetPackageVersionsAsync(DirectoryInfo workingDirectory, string exactPackageId, bool prerelease, FileInfo? nugetConfigFile, bool useCache, CancellationToken cancellationToken) + { + return Task.FromResult>(Array.Empty()); + } } } diff --git a/tests/Aspire.Cli.Tests/Commands/NewCommandTests.cs b/tests/Aspire.Cli.Tests/Commands/NewCommandTests.cs index d671a48055d..ff837b19f84 100644 --- a/tests/Aspire.Cli.Tests/Commands/NewCommandTests.cs +++ b/tests/Aspire.Cli.Tests/Commands/NewCommandTests.cs @@ -82,7 +82,7 @@ public async Task NewCommandInteractiveFlowSmokeTest() options.DotNetCliRunnerFactory = (sp) => { var runner = new TestDotNetCliRunner(); - runner.SearchPackagesAsyncCallback = (dir, query, prerelease, take, skip, nugetSource, useCache, options, cancellationToken) => + runner.SearchPackagesAsyncCallback = (dir, query, exactMatch, prerelease, take, skip, nugetSource, useCache, options, cancellationToken) => { var package = new NuGetPackage() { @@ -139,7 +139,7 @@ public async Task NewCommandDerivesOutputPathFromProjectNameForStarterTemplate() options.DotNetCliRunnerFactory = (sp) => { var runner = new TestDotNetCliRunner(); - runner.SearchPackagesAsyncCallback = (dir, query, prerelease, take, skip, nugetSource, useCache, options, cancellationToken) => + runner.SearchPackagesAsyncCallback = (dir, query, exactMatch, prerelease, take, skip, nugetSource, useCache, options, cancellationToken) => { var package = new NuGetPackage() { @@ -192,7 +192,7 @@ public async Task NewCommandDoesNotPromptForProjectNameIfSpecifiedOnCommandLine( options.DotNetCliRunnerFactory = (sp) => { var runner = new TestDotNetCliRunner(); - runner.SearchPackagesAsyncCallback = (dir, query, prerelease, take, skip, nugetSource, useCache, options, cancellationToken) => + runner.SearchPackagesAsyncCallback = (dir, query, exactMatch, prerelease, take, skip, nugetSource, useCache, options, cancellationToken) => { var package = new NuGetPackage() { @@ -247,7 +247,7 @@ public async Task NewCommandDoesNotPromptForOutputPathIfSpecifiedOnCommandLine() options.DotNetCliRunnerFactory = (sp) => { var runner = new TestDotNetCliRunner(); - runner.SearchPackagesAsyncCallback = (dir, query, prerelease, take, skip, nugetSource, useCache, options, cancellationToken) => + runner.SearchPackagesAsyncCallback = (dir, query, exactMatch, prerelease, take, skip, nugetSource, useCache, options, cancellationToken) => { var package = new NuGetPackage() { @@ -279,7 +279,7 @@ public async Task NewCommandDoesNotPromptForOutputPathIfSpecifiedOnCommandLine() public async Task NewCommandWithChannelOptionUsesSpecifiedChannel() { using var workspace = TemporaryWorkspace.Create(outputHelper); - + string? channelNameUsed = null; bool promptedForVersion = false; @@ -289,13 +289,13 @@ public async Task NewCommandWithChannelOptionUsesSpecifiedChannel() { var interactionService = sp.GetRequiredService(); var prompter = new TestNewCommandPrompter(interactionService); - + prompter.PromptForTemplatesVersionCallback = (packages) => { promptedForVersion = true; throw new InvalidOperationException("Should not prompt for version when --channel is specified"); }; - + return prompter; }; @@ -311,7 +311,7 @@ public async Task NewCommandWithChannelOptionUsesSpecifiedChannel() var package = new NuGetPackage { Id = "Aspire.ProjectTemplates", Source = "nuget", Version = "9.2.0" }; return Task.FromResult>([package]); }; - + var dailyCache = new NewCommandTestFakeNuGetPackageCache(); dailyCache.GetTemplatePackagesAsyncCallback = (dir, prerelease, nugetConfig, ct) => { @@ -319,13 +319,13 @@ public async Task NewCommandWithChannelOptionUsesSpecifiedChannel() var package = new NuGetPackage { Id = "Aspire.ProjectTemplates", Source = "nuget", Version = "10.0.0-dev" }; return Task.FromResult>([package]); }; - + var stableChannel = PackageChannel.CreateExplicitChannel("stable", PackageChannelQuality.Both, [], stableCache); var dailyChannel = PackageChannel.CreateExplicitChannel("daily", PackageChannelQuality.Both, [], dailyCache); - + return Task.FromResult>([stableChannel, dailyChannel]); }; - + return packagingService; }; @@ -349,7 +349,7 @@ public async Task NewCommandWithChannelOptionUsesSpecifiedChannel() var result = command.Parse("new aspire-starter --channel stable --use-redis-cache --test-framework None"); var exitCode = await result.InvokeAsync().DefaultTimeout(); - + // Assert Assert.Equal(0, exitCode); Assert.Equal("stable", channelNameUsed); // Verify the stable channel was used @@ -360,7 +360,7 @@ public async Task NewCommandWithChannelOptionUsesSpecifiedChannel() public async Task NewCommandWithChannelOptionAutoSelectsHighestVersion() { using var workspace = TemporaryWorkspace.Create(outputHelper); - + string? selectedVersion = null; bool promptedForVersion = false; @@ -370,13 +370,13 @@ public async Task NewCommandWithChannelOptionAutoSelectsHighestVersion() { var interactionService = sp.GetRequiredService(); var prompter = new TestNewCommandPrompter(interactionService); - + prompter.PromptForTemplatesVersionCallback = (packages) => { promptedForVersion = true; throw new InvalidOperationException("Should not prompt for version when --channel is specified"); }; - + return prompter; }; @@ -397,14 +397,14 @@ public async Task NewCommandWithChannelOptionAutoSelectsHighestVersion() }; return Task.FromResult>(packages); }; - + var stableChannel = PackageChannel.CreateExplicitChannel("stable", PackageChannelQuality.Both, [], fakeCache); return Task.FromResult>([stableChannel]); }; - + return packagingService; }; - + options.DotNetCliRunnerFactory = (sp) => { var runner = new TestDotNetCliRunner(); @@ -426,7 +426,7 @@ public async Task NewCommandWithChannelOptionAutoSelectsHighestVersion() var result = command.Parse("new aspire-starter --channel stable --use-redis-cache --test-framework None"); var exitCode = await result.InvokeAsync().DefaultTimeout(); - + // Assert Assert.Equal(0, exitCode); Assert.Equal("9.2.0", selectedVersion); // Should auto-select highest version (9.2.0) @@ -460,7 +460,7 @@ public async Task NewCommandDoesNotPromptForTemplateIfSpecifiedOnCommandLine() options.DotNetCliRunnerFactory = (sp) => { var runner = new TestDotNetCliRunner(); - runner.SearchPackagesAsyncCallback = (dir, query, prerelease, take, skip, nugetSource, useCache, options, cancellationToken) => + runner.SearchPackagesAsyncCallback = (dir, query, exactMatch, prerelease, take, skip, nugetSource, useCache, options, cancellationToken) => { var package = new NuGetPackage() { @@ -514,7 +514,7 @@ public async Task NewCommandDoesNotPromptForTemplateVersionIfSpecifiedOnCommandL options.DotNetCliRunnerFactory = (sp) => { var runner = new TestDotNetCliRunner(); - runner.SearchPackagesAsyncCallback = (dir, query, prerelease, take, skip, nugetConfigFile, useCache, options, cancellationToken) => + runner.SearchPackagesAsyncCallback = (dir, query, exactMatch, prerelease, take, skip, nugetConfigFile, useCache, options, cancellationToken) => { var package = new NuGetPackage() { @@ -556,7 +556,7 @@ public async Task NewCommand_EmptyPackageList_DisplaysErrorMessage() options.DotNetCliRunnerFactory = (sp) => { var runner = new TestDotNetCliRunner(); - runner.SearchPackagesAsyncCallback = (dir, query, prerelease, take, skip, nugetSource, useCache, options, cancellationToken) => { + runner.SearchPackagesAsyncCallback = (dir, query, exactMatch, prerelease, take, skip, nugetSource, useCache, options, cancellationToken) => { return (0, Array.Empty()); }; return runner; @@ -591,7 +591,7 @@ public async Task NewCommand_WhenCertificateServiceThrows_ReturnsNonZeroExitCode options.DotNetCliRunnerFactory = (sp) => { var runner = new TestDotNetCliRunner(); - runner.SearchPackagesAsyncCallback = (dir, query, prerelease, take, skip, nugetSource, useCache, options, cancellationToken) => + runner.SearchPackagesAsyncCallback = (dir, query, exactMatch, prerelease, take, skip, nugetSource, useCache, options, cancellationToken) => { var package = new NuGetPackage() { @@ -644,7 +644,7 @@ public async Task NewCommandWithExitCode73ShowsUserFriendlyError() options.DotNetCliRunnerFactory = (sp) => { var runner = new TestDotNetCliRunner(); - runner.SearchPackagesAsyncCallback = (dir, query, prerelease, take, skip, nugetSource, useCache, options, cancellationToken) => + runner.SearchPackagesAsyncCallback = (dir, query, exactMatch, prerelease, take, skip, nugetSource, useCache, options, cancellationToken) => { var package = new NuGetPackage() { @@ -733,7 +733,7 @@ public async Task NewCommandPromptsForTemplateVersionBeforeTemplateOptions() options.DotNetCliRunnerFactory = (sp) => { var runner = new TestDotNetCliRunner(); - runner.SearchPackagesAsyncCallback = (dir, query, prerelease, take, skip, nugetConfigFile, useCache, options, cancellationToken) => + runner.SearchPackagesAsyncCallback = (dir, query, exactMatch, prerelease, take, skip, nugetConfigFile, useCache, options, cancellationToken) => { var package = new NuGetPackage() { @@ -780,7 +780,7 @@ public async Task NewCommandEscapesMarkupInProjectNameAndOutputPath() // This test validates that project names containing Spectre markup characters // (like '[' and ']') are properly escaped when displayed as default values in prompts. // This prevents crashes when the markup parser encounters malformed markup. - + var projectNameWithMarkup = "[27;5;13~"; // Example of input that could crash the markup parser var capturedProjectNameDefault = string.Empty; var capturedOutputPathDefault = string.Empty; @@ -815,7 +815,7 @@ public async Task NewCommandEscapesMarkupInProjectNameAndOutputPath() options.DotNetCliRunnerFactory = (sp) => { var runner = new TestDotNetCliRunner(); - runner.SearchPackagesAsyncCallback = (dir, query, prerelease, take, skip, nugetSource, useCache, options, cancellationToken) => + runner.SearchPackagesAsyncCallback = (dir, query, exactMatch, prerelease, take, skip, nugetSource, useCache, options, cancellationToken) => { var package = new NuGetPackage() { @@ -860,7 +860,7 @@ public async Task NewCommandWithoutTemplateCanCreateTypeScriptEmptyTemplate() options.DotNetCliRunnerFactory = (sp) => { var runner = new TestDotNetCliRunner(); - runner.SearchPackagesAsyncCallback = (dir, query, prerelease, take, skip, nugetSource, useCache, dotnetOptions, cancellationToken) => + runner.SearchPackagesAsyncCallback = (dir, query, exactMatch, prerelease, take, skip, nugetSource, useCache, dotnetOptions, cancellationToken) => { var package = new NuGetPackage() { @@ -956,7 +956,7 @@ public async Task NewCommandWithoutTemplatePromptsWithDistinctLanguageSpecificEm options.DotNetCliRunnerFactory = (sp) => { var runner = new TestDotNetCliRunner(); - runner.SearchPackagesAsyncCallback = (dir, query, prerelease, take, skip, nugetSource, useCache, dotnetOptions, cancellationToken) => + runner.SearchPackagesAsyncCallback = (dir, query, exactMatch, prerelease, take, skip, nugetSource, useCache, dotnetOptions, cancellationToken) => { var package = new NuGetPackage() { @@ -993,7 +993,7 @@ public async Task NewCommandWithExplicitCSharpEmptyTemplateCreatesCSharpAppHost( options.DotNetCliRunnerFactory = (sp) => { var runner = new TestDotNetCliRunner(); - runner.SearchPackagesAsyncCallback = (dir, query, prerelease, take, skip, nugetSource, useCache, options, cancellationToken) => + runner.SearchPackagesAsyncCallback = (dir, query, exactMatch, prerelease, take, skip, nugetSource, useCache, options, cancellationToken) => { var package = new NuGetPackage() { @@ -1054,7 +1054,7 @@ public async Task NewCommandWithEmptyTemplateAndCSharpPromptsForLocalhostTldAndU options.DotNetCliRunnerFactory = (sp) => { var runner = new TestDotNetCliRunner(); - runner.SearchPackagesAsyncCallback = (dir, query, prerelease, take, skip, nugetSource, useCache, options, cancellationToken) => + runner.SearchPackagesAsyncCallback = (dir, query, exactMatch, prerelease, take, skip, nugetSource, useCache, options, cancellationToken) => { var package = new NuGetPackage() { @@ -1096,7 +1096,7 @@ public async Task NewCommandWithTypeScriptEmptyTemplateUsesScaffolding() options.DotNetCliRunnerFactory = (sp) => { var runner = new TestDotNetCliRunner(); - runner.SearchPackagesAsyncCallback = (dir, query, prerelease, take, skip, nugetSource, useCache, options, cancellationToken) => + runner.SearchPackagesAsyncCallback = (dir, query, exactMatch, prerelease, take, skip, nugetSource, useCache, options, cancellationToken) => { var package = new NuGetPackage() { @@ -1152,7 +1152,7 @@ public async Task NewCommandWithEmptyTemplateNormalizesDefaultOutputPath() options.DotNetCliRunnerFactory = (sp) => { var runner = new TestDotNetCliRunner(); - runner.SearchPackagesAsyncCallback = (dir, query, prerelease, take, skip, nugetSource, useCache, options, cancellationToken) => + runner.SearchPackagesAsyncCallback = (dir, query, exactMatch, prerelease, take, skip, nugetSource, useCache, options, cancellationToken) => { var package = new NuGetPackage() { @@ -1231,7 +1231,7 @@ public async Task NewCommandWithEmptyTemplateAndTypeScriptPromptsForLocalhostTld options.DotNetCliRunnerFactory = (sp) => { var runner = new TestDotNetCliRunner(); - runner.SearchPackagesAsyncCallback = (dir, query, prerelease, take, skip, nugetSource, useCache, options, cancellationToken) => + runner.SearchPackagesAsyncCallback = (dir, query, exactMatch, prerelease, take, skip, nugetSource, useCache, options, cancellationToken) => { var package = new NuGetPackage() { @@ -1300,7 +1300,7 @@ public async Task NewCommandWithTypeScriptStarterGeneratesSdkArtifacts() { options.DotNetCliRunnerFactory = _ => new TestDotNetCliRunner { - SearchPackagesAsyncCallback = (dir, query, prerelease, take, skip, nugetSource, useCache, runnerOptions, cancellationToken) => + SearchPackagesAsyncCallback = (dir, query, exactMatch, prerelease, take, skip, nugetSource, useCache, runnerOptions, cancellationToken) => { var package = new NuGetPackage { @@ -1373,7 +1373,7 @@ public async Task NewCommandWithTypeScriptStarterReturnsFailedToBuildArtifactsWh { options.DotNetCliRunnerFactory = _ => new TestDotNetCliRunner { - SearchPackagesAsyncCallback = (dir, query, prerelease, take, skip, nugetSource, useCache, runnerOptions, cancellationToken) => + SearchPackagesAsyncCallback = (dir, query, exactMatch, prerelease, take, skip, nugetSource, useCache, runnerOptions, cancellationToken) => { var package = new NuGetPackage { @@ -1441,7 +1441,7 @@ public async Task NewCommandNonInteractiveDoesNotPrompt() options.DotNetCliRunnerFactory = (sp) => { var runner = new TestDotNetCliRunner(); - runner.SearchPackagesAsyncCallback = (dir, query, prerelease, take, skip, nugetSource, useCache, options, cancellationToken) => + runner.SearchPackagesAsyncCallback = (dir, query, exactMatch, prerelease, take, skip, nugetSource, useCache, options, cancellationToken) => { var package = new NuGetPackage() { @@ -1601,7 +1601,7 @@ public Task> GetChannelsAsync(CancellationToken canc { return GetChannelsAsyncCallback(cancellationToken); } - + // Default: Return a fake channel var testChannel = PackageChannel.CreateImplicitChannel(new NewCommandTestFakeNuGetPackageCache()); return Task.FromResult>(new[] { testChannel }); @@ -1642,6 +1642,11 @@ public Task> GetPackagesAsync(DirectoryInfo workingDir { return Task.FromResult>(Array.Empty()); } + + public Task> GetPackageVersionsAsync(DirectoryInfo workingDirectory, string exactPackageId, bool prerelease, FileInfo? nugetConfigFile, bool useCache, CancellationToken cancellationToken) + { + return Task.FromResult>(Array.Empty()); + } } internal sealed class TestScaffoldingService : IScaffoldingService diff --git a/tests/Aspire.Cli.Tests/DotNet/DotNetCliRunnerTests.cs b/tests/Aspire.Cli.Tests/DotNet/DotNetCliRunnerTests.cs index a7974739d47..3c5e0d6f947 100644 --- a/tests/Aspire.Cli.Tests/DotNet/DotNetCliRunnerTests.cs +++ b/tests/Aspire.Cli.Tests/DotNet/DotNetCliRunnerTests.cs @@ -1197,7 +1197,7 @@ public async Task GetProjectItemsAndPropertiesAsync_UsesBuild_ForSingleFileAppHo // Verify that "build" command is used for single-file app host Assert.Contains("build", args); Assert.DoesNotContain("msbuild", args); - + // Provide valid JSON output invocationOptions.StandardOutputCallback?.Invoke("{\"Properties\":{\"MSBuildVersion\":\"17.0.0\",\"AspireHostingSDKVersion\":\"9.0.0\"},\"Items\":{\"PackageReference\":[]}}"); }, @@ -1233,7 +1233,7 @@ public async Task GetProjectItemsAndPropertiesAsync_UsesMsBuild_ForCsProjFile() // Verify that "msbuild" command is used for .csproj files Assert.Contains("msbuild", args); Assert.DoesNotContain("build", args); - + // Provide valid JSON output invocationOptions.StandardOutputCallback?.Invoke("{\"Properties\":{\"MSBuildVersion\":\"17.0.0\",\"AspireHostingSDKVersion\":\"9.0.0\"},\"Items\":{\"PackageReference\":[]}}"); }, @@ -1280,6 +1280,7 @@ public async Task SearchPackagesAsyncRetriesOnFailureAndSucceedsOnSecondAttempt( var result = await runner.SearchPackagesAsync( workspace.WorkspaceRoot, "Aspire.Hosting", + exactMatch: false, prerelease: false, take: 100, skip: 0, @@ -1321,6 +1322,7 @@ public async Task SearchPackagesAsyncRetriesMaxTimesAndReturnsFailure() var result = await runner.SearchPackagesAsync( workspace.WorkspaceRoot, "Aspire.Hosting", + exactMatch: false, prerelease: false, take: 100, skip: 0, @@ -1362,6 +1364,7 @@ public async Task SearchPackagesAsyncSucceedsOnFirstAttemptWithoutRetry() var result = await runner.SearchPackagesAsync( workspace.WorkspaceRoot, "Aspire.Hosting", + exactMatch: false, prerelease: false, take: 100, skip: 0, diff --git a/tests/Aspire.Cli.Tests/Mcp/MockPackagingService.cs b/tests/Aspire.Cli.Tests/Mcp/MockPackagingService.cs index 27505f5e9a3..a7be1f355c5 100644 --- a/tests/Aspire.Cli.Tests/Mcp/MockPackagingService.cs +++ b/tests/Aspire.Cli.Tests/Mcp/MockPackagingService.cs @@ -45,6 +45,8 @@ public Task> GetCliPackagesAsync(DirectoryInfo work public Task> GetPackagesAsync(DirectoryInfo workingDirectory, string packageId, Func? filter, bool prerelease, FileInfo? nugetConfigFile, bool useCache, CancellationToken cancellationToken) => Task.FromResult>([]); + public Task> GetPackageVersionsAsync(DirectoryInfo workingDirectory, string exactPackageId, bool prerelease, FileInfo? nugetConfigFile, bool useCache, CancellationToken cancellationToken) + => Task.FromResult>([]); } internal static class TestExecutionContextFactory diff --git a/tests/Aspire.Cli.Tests/NuGet/NuGetPackageCacheTests.cs b/tests/Aspire.Cli.Tests/NuGet/NuGetPackageCacheTests.cs index 27ef3717480..7f3d833d81e 100644 --- a/tests/Aspire.Cli.Tests/NuGet/NuGetPackageCacheTests.cs +++ b/tests/Aspire.Cli.Tests/NuGet/NuGetPackageCacheTests.cs @@ -21,7 +21,7 @@ public async Task NonAspireCliPackagesWillNotBeConsidered() configure.DotNetCliRunnerFactory = (sp) => { var runner = new TestDotNetCliRunner(); - runner.SearchPackagesAsyncCallback = (_, _, _, _, _, _, _, _, _) => + runner.SearchPackagesAsyncCallback = (_, _, _, _, _, _, _, _, _, _) => { // Simulate a search that returns packages that do not match Aspire.Cli return (0, [ @@ -54,7 +54,7 @@ public async Task DeprecatedPackagesAreFilteredByDefault() configure.DotNetCliRunnerFactory = (sp) => { var runner = new TestDotNetCliRunner(); - runner.SearchPackagesAsyncCallback = (_, _, _, _, _, _, _, _, _) => + runner.SearchPackagesAsyncCallback = (_, _, _, _, _, _, _, _, _, _) => { // Simulate a search that returns both regular and deprecated packages return (0, [ @@ -88,11 +88,11 @@ public async Task DeprecatedPackagesAreIncludedWhenShowDeprecatedPackagesEnabled { // Enable showing deprecated packages configure.EnabledFeatures = [Aspire.Cli.KnownFeatures.ShowDeprecatedPackages]; - + configure.DotNetCliRunnerFactory = (sp) => { var runner = new TestDotNetCliRunner(); - runner.SearchPackagesAsyncCallback = (_, _, _, _, _, _, _, _, _) => + runner.SearchPackagesAsyncCallback = (_, _, _, _, _, _, _, _, _, _) => { // Simulate a search that returns both regular and deprecated packages return (0, [ @@ -127,7 +127,7 @@ public async Task CustomFilterBypassesDeprecatedPackageFiltering() configure.DotNetCliRunnerFactory = (sp) => { var runner = new TestDotNetCliRunner(); - runner.SearchPackagesAsyncCallback = (_, _, _, _, _, _, _, _, _) => + runner.SearchPackagesAsyncCallback = (_, _, _, _, _, _, _, _, _, _) => { // Simulate a search that returns both regular and deprecated packages return (0, [ @@ -144,14 +144,14 @@ public async Task CustomFilterBypassesDeprecatedPackageFiltering() var provider = services.BuildServiceProvider(); var nuGetPackageCache = provider.GetRequiredService(); - + // Use a custom filter that includes all packages containing "Dapr" var packages = await nuGetPackageCache.GetPackagesAsync( - workspace.WorkspaceRoot, - "Aspire.Hosting", - filter: id => id.Contains("Dapr", StringComparison.OrdinalIgnoreCase), - prerelease: false, - nugetConfigFile: null, + workspace.WorkspaceRoot, + "Aspire.Hosting", + filter: id => id.Contains("Dapr", StringComparison.OrdinalIgnoreCase), + prerelease: false, + nugetConfigFile: null, useCache: true, CancellationToken.None).DefaultTimeout(); @@ -171,7 +171,7 @@ public async Task DeprecatedPackageFilteringIsCaseInsensitive() configure.DotNetCliRunnerFactory = (sp) => { var runner = new TestDotNetCliRunner(); - runner.SearchPackagesAsyncCallback = (_, _, _, _, _, _, _, _, _) => + runner.SearchPackagesAsyncCallback = (_, _, _, _, _, _, _, _, _, _) => { // Test different casing of deprecated package name return (0, [ diff --git a/tests/Aspire.Cli.Tests/Packaging/NuGetConfigMergerSnapshotTests.cs b/tests/Aspire.Cli.Tests/Packaging/NuGetConfigMergerSnapshotTests.cs index 77fd8b738aa..be727f93c5b 100644 --- a/tests/Aspire.Cli.Tests/Packaging/NuGetConfigMergerSnapshotTests.cs +++ b/tests/Aspire.Cli.Tests/Packaging/NuGetConfigMergerSnapshotTests.cs @@ -24,10 +24,16 @@ public NuGetConfigMergerSnapshotTests(ITestOutputHelper output) private sealed class FakeNuGetPackageCache : INuGetPackageCache { - public Task> GetTemplatePackagesAsync(DirectoryInfo workingDirectory, bool prerelease, FileInfo? nugetConfigFile, CancellationToken cancellationToken) => Task.FromResult>([]); - public Task> GetIntegrationPackagesAsync(DirectoryInfo workingDirectory, bool prerelease, FileInfo? nugetConfigFile, CancellationToken cancellationToken) => Task.FromResult>([]); - public Task> GetCliPackagesAsync(DirectoryInfo workingDirectory, bool prerelease, FileInfo? nugetConfigFile, CancellationToken cancellationToken) => Task.FromResult>([]); - public Task> GetPackagesAsync(DirectoryInfo workingDirectory, string packageId, Func? filter, bool prerelease, FileInfo? nugetConfigFile, bool useCache, CancellationToken cancellationToken) => Task.FromResult>([]); + public Task> GetTemplatePackagesAsync(DirectoryInfo workingDirectory, bool prerelease, FileInfo? nugetConfigFile, CancellationToken cancellationToken) + => Task.FromResult>([]); + public Task> GetIntegrationPackagesAsync(DirectoryInfo workingDirectory, bool prerelease, FileInfo? nugetConfigFile, CancellationToken cancellationToken) + => Task.FromResult>([]); + public Task> GetCliPackagesAsync(DirectoryInfo workingDirectory, bool prerelease, FileInfo? nugetConfigFile, CancellationToken cancellationToken) + => Task.FromResult>([]); + public Task> GetPackagesAsync(DirectoryInfo workingDirectory, string packageId, Func? filter, bool prerelease, FileInfo? nugetConfigFile, bool useCache, CancellationToken cancellationToken) + => Task.FromResult>([]); + public Task> GetPackageVersionsAsync(DirectoryInfo workingDirectory, string exactPackageId, bool prerelease, FileInfo? nugetConfigFile, bool useCache, CancellationToken cancellationToken) + => Task.FromResult>([]); } private sealed class FakeFeatures : IFeatures diff --git a/tests/Aspire.Cli.Tests/Packaging/NuGetConfigMergerTests.cs b/tests/Aspire.Cli.Tests/Packaging/NuGetConfigMergerTests.cs index 3f648c69a8c..ca9b6ee512d 100644 --- a/tests/Aspire.Cli.Tests/Packaging/NuGetConfigMergerTests.cs +++ b/tests/Aspire.Cli.Tests/Packaging/NuGetConfigMergerTests.cs @@ -7,6 +7,7 @@ using Aspire.Cli.Packaging; using Aspire.Cli.NuGet; using Aspire.Cli.Tests.Utils; +using Aspire.Shared; namespace Aspire.Cli.Tests.Packaging; @@ -44,6 +45,11 @@ private sealed class FakeNuGetPackageCache : INuGetPackageCache { _ = workingDirectory; _ = packageId; _ = filter; _ = prerelease; _ = nugetConfigFile; _ = useCache; _ = cancellationToken; return Task.FromResult>([]); } + + public Task> GetPackageVersionsAsync(DirectoryInfo workingDirectory, string exactPackageId, bool prerelease, FileInfo? nugetConfigFile, bool useCache, CancellationToken cancellationToken) + { + _ = workingDirectory; _ = exactPackageId; _ = prerelease; _ = nugetConfigFile; _ = useCache; _ = cancellationToken; return Task.FromResult>([]); + } } private static PackageChannel CreateChannel(PackageMapping[] mappings) => PackageChannel.CreateExplicitChannel("test", PackageChannelQuality.Both, mappings, new FakeNuGetPackageCache()); @@ -60,14 +66,14 @@ public async Task CreateOrUpdateAsync_CreatesConfigFromMappings_WhenNoExistingCo new PackageMapping(PackageMapping.AllPackages, "https://feed2.example") }; - var channel = CreateChannel(mappings); - await NuGetConfigMerger.CreateOrUpdateAsync(root, channel).DefaultTimeout(); + var channel = CreateChannel(mappings); + await NuGetConfigMerger.CreateOrUpdateAsync(root, channel).DefaultTimeout(); var targetConfigPath = Path.Combine(root.FullName, "nuget.config"); Assert.True(File.Exists(targetConfigPath)); - using var tempConfig = await TemporaryNuGetConfig.CreateAsync(mappings); - var expected = await File.ReadAllTextAsync(tempConfig.ConfigFile.FullName); + using var tempConfig = await TemporaryNuGetConfig.CreateAsync(mappings); + var expected = await File.ReadAllTextAsync(tempConfig.ConfigFile.FullName); var actual = await File.ReadAllTextAsync(targetConfigPath); Assert.Equal(NormalizeLineEndings(expected), NormalizeLineEndings(actual)); } @@ -84,8 +90,8 @@ public async Task CreateOrUpdateAsync_GeneratesConfigFromMappings_WhenChannelPro new PackageMapping(PackageMapping.AllPackages, "https://feed2.example") }; - var channel = CreateChannel(mappings); - await NuGetConfigMerger.CreateOrUpdateAsync(root, channel).DefaultTimeout(); + var channel = CreateChannel(mappings); + await NuGetConfigMerger.CreateOrUpdateAsync(root, channel).DefaultTimeout(); var targetConfigPath = Path.Combine(root.FullName, "nuget.config"); Assert.True(File.Exists(targetConfigPath)); @@ -128,8 +134,8 @@ await WriteConfigAsync(root, new PackageMapping("Microsoft.*", "https://feed2.example") // feed2 missing }; - var channel = CreateChannel(mappings); - await NuGetConfigMerger.CreateOrUpdateAsync(root, channel).DefaultTimeout(); + var channel = CreateChannel(mappings); + await NuGetConfigMerger.CreateOrUpdateAsync(root, channel).DefaultTimeout(); var xml = XDocument.Load(Path.Combine(root.FullName, "nuget.config")); var packageSources = xml.Root!.Element("packageSources")!; @@ -167,8 +173,8 @@ await WriteConfigAsync(root, new PackageMapping("Lib.*", "https://new.example") }; - var channel = CreateChannel(mappings); - await NuGetConfigMerger.CreateOrUpdateAsync(root, channel).DefaultTimeout(); + var channel = CreateChannel(mappings); + await NuGetConfigMerger.CreateOrUpdateAsync(root, channel).DefaultTimeout(); var xml = XDocument.Load(Path.Combine(root.FullName, "nuget.config")); var packageSources = xml.Root!.Element("packageSources")!; @@ -207,8 +213,8 @@ await WriteConfigAsync(root, new PackageMapping("Microsoft.*", "https://feed2.example") }; - var channel = CreateChannel(mappings); - await NuGetConfigMerger.CreateOrUpdateAsync(root, channel).DefaultTimeout(); + var channel = CreateChannel(mappings); + await NuGetConfigMerger.CreateOrUpdateAsync(root, channel).DefaultTimeout(); var xml = XDocument.Load(Path.Combine(root.FullName, "nuget.config")); var psm = xml.Root!.Element("packageSourceMapping"); @@ -221,9 +227,9 @@ public void HasMissingSources_ReturnsTrue_WhenConfigAbsent() { using var workspace = TemporaryWorkspace.Create(_outputHelper); var root = workspace.WorkspaceRoot; - var mappings = new[] { new PackageMapping("Aspire.*", "https://feed.example") }; - var channel = CreateChannel(mappings); - Assert.True(NuGetConfigMerger.HasMissingSources(root, channel)); + var mappings = new[] { new PackageMapping("Aspire.*", "https://feed.example") }; + var channel = CreateChannel(mappings); + Assert.True(NuGetConfigMerger.HasMissingSources(root, channel)); } [Fact] @@ -253,8 +259,8 @@ await WriteConfigAsync(root, new PackageMapping("Aspire.*", "https://feed2.example") // should be feed2, but config has feed1 }; - var channel = CreateChannel(mappings); - Assert.True(NuGetConfigMerger.HasMissingSources(root, channel)); + var channel = CreateChannel(mappings); + Assert.True(NuGetConfigMerger.HasMissingSources(root, channel)); } [Fact] @@ -288,8 +294,8 @@ await WriteConfigAsync(root, new PackageMapping("Microsoft.*", "https://feed2.example") }; - var channel = CreateChannel(mappings); - Assert.False(NuGetConfigMerger.HasMissingSources(root, channel)); + var channel = CreateChannel(mappings); + Assert.False(NuGetConfigMerger.HasMissingSources(root, channel)); } [Fact] @@ -322,7 +328,7 @@ await WriteConfigAsync(root, var xml = XDocument.Load(Path.Combine(root.FullName, "nuget.config")); var packageSources = xml.Root!.Element("packageSources")!; - + // Existing sources should still be present with their original keys Assert.Contains(packageSources.Elements("add"), e => (string?)e.Attribute("key") == "nuget" && (string?)e.Attribute("value") == "https://api.nuget.org/v3/index.json"); Assert.Contains(packageSources.Elements("add"), e => (string?)e.Attribute("key") == "dotnet9" && (string?)e.Attribute("value") == "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet9/nuget/v3/index.json"); @@ -372,7 +378,7 @@ await WriteConfigAsync(root, var xml = XDocument.Load(Path.Combine(root.FullName, "nuget.config")); var packageSources = xml.Root!.Element("packageSources")!; - + // All original sources should still be present Assert.Contains(packageSources.Elements("add"), e => (string?)e.Attribute("key") == "nuget.org"); Assert.Contains(packageSources.Elements("add"), e => (string?)e.Attribute("key") == "custom"); @@ -388,7 +394,7 @@ await WriteConfigAsync(root, // Since the original config had NO packageSourceMapping, all existing sources should get "*" patterns // so they can continue to serve packages var psm = xml.Root!.Element("packageSourceMapping")!; - + // The aspire source should have its specific pattern var aspireMapping = psm.Elements("packageSource").FirstOrDefault(ps => (string?)ps.Attribute("key") == "https://example.com/aspire-daily"); Assert.NotNull(aspireMapping); @@ -441,7 +447,7 @@ await WriteConfigAsync(root, var xml = XDocument.Load(Path.Combine(root.FullName, "nuget.config")); var packageSources = xml.Root!.Element("packageSources")!; - + // Original source should still be present Assert.Contains(packageSources.Elements("add"), e => (string?)e.Attribute("key") == "nuget.org"); @@ -454,7 +460,7 @@ await WriteConfigAsync(root, // Package source mapping should have both the original wildcard and the new specific mappings var psm = xml.Root!.Element("packageSourceMapping")!; - + // Original nuget.org should still have the wildcard pattern var nugetMapping = psm.Elements("packageSource").FirstOrDefault(ps => (string?)ps.Attribute("key") == "nuget.org"); Assert.NotNull(nugetMapping); @@ -507,31 +513,31 @@ await WriteConfigAsync(root, var xml = XDocument.Load(Path.Combine(root.FullName, "nuget.config")); var packageSources = xml.Root!.Element("packageSources")!; - + // The PR hive source should be removed because it's safe to remove and no longer needed - Assert.DoesNotContain(packageSources.Elements("add"), + Assert.DoesNotContain(packageSources.Elements("add"), e => (string?)e.Attribute("value") == "C:\\Users\\user\\.aspire\\hives\\invalid-pr"); - + // The user-defined source should be preserved even though its patterns were remapped - Assert.Contains(packageSources.Elements("add"), + Assert.Contains(packageSources.Elements("add"), e => (string?)e.Attribute("value") == "https://valid.example"); - + // NuGet.org should be added for all the patterns - Assert.Contains(packageSources.Elements("add"), + Assert.Contains(packageSources.Elements("add"), e => (string?)e.Attribute("value") == "https://api.nuget.org/v3/index.json"); var psm = xml.Root!.Element("packageSourceMapping")!; - + // The PR hive source should not have any mapping entries (removed entirely) - Assert.DoesNotContain(psm.Elements("packageSource"), + Assert.DoesNotContain(psm.Elements("packageSource"), ps => (string?)ps.Attribute("key") == "C:\\Users\\user\\.aspire\\hives\\invalid-pr"); - + // The user-defined source should get a wildcard pattern to remain functional var validExampleMapping = psm.Elements("packageSource") .FirstOrDefault(ps => (string?)ps.Attribute("key") == "https://valid.example"); Assert.NotNull(validExampleMapping); Assert.Contains(validExampleMapping.Elements("package"), p => (string?)p.Attribute("pattern") == "*"); - + // NuGet.org should have all the patterns var nugetMapping = psm.Elements("packageSource") .FirstOrDefault(ps => (string?)ps.Attribute("key") == "https://api.nuget.org/v3/index.json"); @@ -539,7 +545,7 @@ await WriteConfigAsync(root, Assert.Contains(nugetMapping.Elements("package"), p => (string?)p.Attribute("pattern") == "Aspire*"); Assert.Contains(nugetMapping.Elements("package"), p => (string?)p.Attribute("pattern") == "Microsoft.Extensions.ServiceDiscovery*"); Assert.Contains(nugetMapping.Elements("package"), p => (string?)p.Attribute("pattern") == "*"); - + // There should be two packageSource elements (nuget.org and valid.example) Assert.Equal(2, psm.Elements("packageSource").Count()); } @@ -556,7 +562,7 @@ public async Task CreateOrUpdateAsync_CallbackInvokedForNewConfig() }; var channel = CreateChannel(mappings); - + bool callbackInvoked = false; FileInfo? callbackTargetFile = null; XmlDocument? callbackOriginalContent = null; @@ -595,7 +601,7 @@ public async Task CreateOrUpdateAsync_CallbackCanPreventNewConfigCreation() }; var channel = CreateChannel(mappings); - + bool callbackInvoked = false; await NuGetConfigMerger.CreateOrUpdateAsync(root, channel, (targetFile, originalContent, proposedContent, cancellationToken) => @@ -627,7 +633,7 @@ public async Task CreateOrUpdateAsync_CallbackInvokedForExistingConfig() """; - + await WriteConfigAsync(root, existingConfig).DefaultTimeout(); var mappings = new[] @@ -636,7 +642,7 @@ public async Task CreateOrUpdateAsync_CallbackInvokedForExistingConfig() }; var channel = CreateChannel(mappings); - + bool callbackInvoked = false; FileInfo? callbackTargetFile = null; XmlDocument? callbackOriginalContent = null; @@ -678,7 +684,7 @@ public async Task CreateOrUpdateAsync_CallbackCanPreventExistingConfigUpdate() """; - + await WriteConfigAsync(root, existingConfig).DefaultTimeout(); var originalContent = await File.ReadAllTextAsync(Path.Combine(root.FullName, "nuget.config")).DefaultTimeout(); @@ -688,7 +694,7 @@ public async Task CreateOrUpdateAsync_CallbackCanPreventExistingConfigUpdate() }; var channel = CreateChannel(mappings); - + bool callbackInvoked = false; await NuGetConfigMerger.CreateOrUpdateAsync(root, channel, (targetFile, originalContent, proposedContent, cancellationToken) => @@ -718,7 +724,7 @@ public async Task CreateOrUpdateAsync_WorksWithoutCallback() }; var channel = CreateChannel(mappings); - + // Call without callback - should work as before await NuGetConfigMerger.CreateOrUpdateAsync(root, channel).DefaultTimeout(); diff --git a/tests/Aspire.Cli.Tests/Packaging/PackageChannelTests.cs b/tests/Aspire.Cli.Tests/Packaging/PackageChannelTests.cs index 5277c4323e7..db99eb629b2 100644 --- a/tests/Aspire.Cli.Tests/Packaging/PackageChannelTests.cs +++ b/tests/Aspire.Cli.Tests/Packaging/PackageChannelTests.cs @@ -4,6 +4,7 @@ using Aspire.Cli.NuGet; using Aspire.Cli.Packaging; using Aspire.Cli.Resources; +using Aspire.Shared; namespace Aspire.Cli.Tests.Packaging; @@ -11,10 +12,16 @@ public class PackageChannelTests { private sealed class FakeNuGetPackageCache : INuGetPackageCache { - public Task> GetTemplatePackagesAsync(DirectoryInfo workingDirectory, bool prerelease, FileInfo? nugetConfigFile, CancellationToken cancellationToken) => Task.FromResult>([]); - public Task> GetIntegrationPackagesAsync(DirectoryInfo workingDirectory, bool prerelease, FileInfo? nugetConfigFile, CancellationToken cancellationToken) => Task.FromResult>([]); - public Task> GetCliPackagesAsync(DirectoryInfo workingDirectory, bool prerelease, FileInfo? nugetConfigFile, CancellationToken cancellationToken) => Task.FromResult>([]); - public Task> GetPackagesAsync(DirectoryInfo workingDirectory, string packageId, Func? filter, bool prerelease, FileInfo? nugetConfigFile, bool useCache, CancellationToken cancellationToken) => Task.FromResult>([]); + public Task> GetTemplatePackagesAsync(DirectoryInfo workingDirectory, bool prerelease, FileInfo? nugetConfigFile, CancellationToken cancellationToken) + => Task.FromResult>([]); + public Task> GetIntegrationPackagesAsync(DirectoryInfo workingDirectory, bool prerelease, FileInfo? nugetConfigFile, CancellationToken cancellationToken) + => Task.FromResult>([]); + public Task> GetCliPackagesAsync(DirectoryInfo workingDirectory, bool prerelease, FileInfo? nugetConfigFile, CancellationToken cancellationToken) + => Task.FromResult>([]); + public Task> GetPackagesAsync(DirectoryInfo workingDirectory, string packageId, Func? filter, bool prerelease, FileInfo? nugetConfigFile, bool useCache, CancellationToken cancellationToken) + => Task.FromResult>([]); + public Task> GetPackageVersionsAsync(DirectoryInfo workingDirectory, string exactPackageId, bool prerelease, FileInfo? nugetConfigFile, bool useCache, CancellationToken cancellationToken) + => Task.FromResult>([]); } [Fact] diff --git a/tests/Aspire.Cli.Tests/Packaging/PackagingServiceTests.cs b/tests/Aspire.Cli.Tests/Packaging/PackagingServiceTests.cs index 51ce56b6907..4b13cea14e1 100644 --- a/tests/Aspire.Cli.Tests/Packaging/PackagingServiceTests.cs +++ b/tests/Aspire.Cli.Tests/Packaging/PackagingServiceTests.cs @@ -6,6 +6,7 @@ using Aspire.Cli.NuGet; using Aspire.Cli.Packaging; using Aspire.Cli.Tests.Utils; +using Aspire.Shared; using Microsoft.Extensions.Configuration; using System.Xml.Linq; @@ -16,10 +17,16 @@ public class PackagingServiceTests(ITestOutputHelper outputHelper) private sealed class FakeNuGetPackageCache : INuGetPackageCache { - public Task> GetTemplatePackagesAsync(DirectoryInfo workingDirectory, bool prerelease, FileInfo? nugetConfigFile, CancellationToken cancellationToken) => Task.FromResult>([]); - public Task> GetIntegrationPackagesAsync(DirectoryInfo workingDirectory, bool prerelease, FileInfo? nugetConfigFile, CancellationToken cancellationToken) => Task.FromResult>([]); - public Task> GetCliPackagesAsync(DirectoryInfo workingDirectory, bool prerelease, FileInfo? nugetConfigFile, CancellationToken cancellationToken) => Task.FromResult>([]); - public Task> GetPackagesAsync(DirectoryInfo workingDirectory, string packageId, Func? filter, bool prerelease, FileInfo? nugetConfigFile, bool useCache, CancellationToken cancellationToken) => Task.FromResult>([]); + public Task> GetTemplatePackagesAsync(DirectoryInfo workingDirectory, bool prerelease, FileInfo? nugetConfigFile, CancellationToken cancellationToken) + => Task.FromResult>([]); + public Task> GetIntegrationPackagesAsync(DirectoryInfo workingDirectory, bool prerelease, FileInfo? nugetConfigFile, CancellationToken cancellationToken) + => Task.FromResult>([]); + public Task> GetCliPackagesAsync(DirectoryInfo workingDirectory, bool prerelease, FileInfo? nugetConfigFile, CancellationToken cancellationToken) + => Task.FromResult>([]); + public Task> GetPackagesAsync(DirectoryInfo workingDirectory, string packageId, Func? filter, bool prerelease, FileInfo? nugetConfigFile, bool useCache, CancellationToken cancellationToken) + => Task.FromResult>([]); + public Task> GetPackageVersionsAsync(DirectoryInfo workingDirectory, string exactPackageId, bool prerelease, FileInfo? nugetConfigFile, bool useCache, CancellationToken cancellationToken) + => Task.FromResult>([]); } private sealed class TestFeatures : IFeatures @@ -46,7 +53,7 @@ public async Task GetChannelsAsync_WhenStagingChannelDisabled_DoesNotIncludeStag var hivesDir = new DirectoryInfo(Path.Combine(tempDir.FullName, ".aspire", "hives")); var cacheDir = new DirectoryInfo(Path.Combine(tempDir.FullName, ".aspire", "cache")); var executionContext = new CliExecutionContext(tempDir, hivesDir, cacheDir, new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-runtimes")), new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-logs")), "test.log"); - + var features = new TestFeatures(); var configuration = new ConfigurationBuilder().Build(); var packagingService = new PackagingService(executionContext, new FakeNuGetPackageCache(), features, configuration); @@ -64,10 +71,10 @@ public async Task GetChannelsAsync_WhenStagingChannelDisabled_DoesNotIncludeStag // Verify that non-staging channels have ConfigureGlobalPackagesFolder = false var defaultChannel = channels.First(c => c.Name == "default"); Assert.False(defaultChannel.ConfigureGlobalPackagesFolder); - + var stableChannel = channels.First(c => c.Name == "stable"); Assert.False(stableChannel.ConfigureGlobalPackagesFolder); - + var dailyChannel = channels.First(c => c.Name == "daily"); Assert.False(dailyChannel.ConfigureGlobalPackagesFolder); } @@ -81,10 +88,10 @@ public async Task GetChannelsAsync_WhenStagingChannelEnabled_IncludesStagingChan var hivesDir = new DirectoryInfo(Path.Combine(tempDir.FullName, ".aspire", "hives")); var cacheDir = new DirectoryInfo(Path.Combine(tempDir.FullName, ".aspire", "cache")); var executionContext = new CliExecutionContext(tempDir, hivesDir, cacheDir, new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-runtimes")), new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-logs")), "test.log"); - + var features = new TestFeatures(); features.SetFeature(KnownFeatures.StagingChannelEnabled, true); - + var testFeedUrl = "https://example.com/nuget/v3/index.json"; var configuration = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary @@ -101,16 +108,16 @@ public async Task GetChannelsAsync_WhenStagingChannelEnabled_IncludesStagingChan // Assert var channelNames = channels.Select(c => c.Name).ToList(); Assert.Contains("staging", channelNames); - + var stagingChannel = channels.First(c => c.Name == "staging"); Assert.Equal(PackageChannelQuality.Stable, stagingChannel.Quality); Assert.True(stagingChannel.ConfigureGlobalPackagesFolder); Assert.NotNull(stagingChannel.Mappings); - + var aspireMapping = stagingChannel.Mappings!.FirstOrDefault(m => m.PackageFilter == "Aspire*"); Assert.NotNull(aspireMapping); Assert.Equal(testFeedUrl, aspireMapping.Source); - + var nugetMapping = stagingChannel.Mappings!.FirstOrDefault(m => m.PackageFilter == "*"); Assert.NotNull(nugetMapping); Assert.Equal("https://api.nuget.org/v3/index.json", nugetMapping.Source); @@ -125,10 +132,10 @@ public async Task GetChannelsAsync_WhenStagingChannelEnabledWithOverrideFeed_Use var hivesDir = new DirectoryInfo(Path.Combine(tempDir.FullName, ".aspire", "hives")); var cacheDir = new DirectoryInfo(Path.Combine(tempDir.FullName, ".aspire", "cache")); var executionContext = new CliExecutionContext(tempDir, hivesDir, cacheDir, new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-runtimes")), new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-logs")), "test.log"); - + var features = new TestFeatures(); features.SetFeature(KnownFeatures.StagingChannelEnabled, true); - + var customFeedUrl = "https://custom-feed.example.com/v3/index.json"; var configuration = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary @@ -158,10 +165,10 @@ public async Task GetChannelsAsync_WhenStagingChannelEnabledWithAzureDevOpsFeedO var hivesDir = new DirectoryInfo(Path.Combine(tempDir.FullName, ".aspire", "hives")); var cacheDir = new DirectoryInfo(Path.Combine(tempDir.FullName, ".aspire", "cache")); var executionContext = new CliExecutionContext(tempDir, hivesDir, cacheDir, new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-runtimes")), new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-logs")), "test.log"); - + var features = new TestFeatures(); features.SetFeature(KnownFeatures.StagingChannelEnabled, true); - + var azureDevOpsFeedUrl = "https://pkgs.dev.azure.com/dnceng/public/_packaging/darc-pub-dotnet-aspire-abcd1234/nuget/v3/index.json"; var configuration = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary @@ -191,10 +198,10 @@ public async Task GetChannelsAsync_WhenStagingChannelEnabledWithInvalidOverrideF var hivesDir = new DirectoryInfo(Path.Combine(tempDir.FullName, ".aspire", "hives")); var cacheDir = new DirectoryInfo(Path.Combine(tempDir.FullName, ".aspire", "cache")); var executionContext = new CliExecutionContext(tempDir, hivesDir, cacheDir, new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-runtimes")), new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-logs")), "test.log"); - + var features = new TestFeatures(); features.SetFeature(KnownFeatures.StagingChannelEnabled, true); - + var invalidFeedUrl = "not-a-valid-url"; var configuration = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary @@ -223,10 +230,10 @@ public async Task GetChannelsAsync_WhenStagingChannelEnabledWithQualityOverride_ var hivesDir = new DirectoryInfo(Path.Combine(tempDir.FullName, ".aspire", "hives")); var cacheDir = new DirectoryInfo(Path.Combine(tempDir.FullName, ".aspire", "cache")); var executionContext = new CliExecutionContext(tempDir, hivesDir, cacheDir, new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-runtimes")), new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-logs")), "test.log"); - + var features = new TestFeatures(); features.SetFeature(KnownFeatures.StagingChannelEnabled, true); - + var configuration = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary { @@ -254,10 +261,10 @@ public async Task GetChannelsAsync_WhenStagingChannelEnabledWithQualityBoth_Uses var hivesDir = new DirectoryInfo(Path.Combine(tempDir.FullName, ".aspire", "hives")); var cacheDir = new DirectoryInfo(Path.Combine(tempDir.FullName, ".aspire", "cache")); var executionContext = new CliExecutionContext(tempDir, hivesDir, cacheDir, new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-runtimes")), new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-logs")), "test.log"); - + var features = new TestFeatures(); features.SetFeature(KnownFeatures.StagingChannelEnabled, true); - + var configuration = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary { @@ -285,10 +292,10 @@ public async Task GetChannelsAsync_WhenStagingChannelEnabledWithInvalidQuality_D var hivesDir = new DirectoryInfo(Path.Combine(tempDir.FullName, ".aspire", "hives")); var cacheDir = new DirectoryInfo(Path.Combine(tempDir.FullName, ".aspire", "cache")); var executionContext = new CliExecutionContext(tempDir, hivesDir, cacheDir, new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-runtimes")), new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-logs")), "test.log"); - + var features = new TestFeatures(); features.SetFeature(KnownFeatures.StagingChannelEnabled, true); - + var configuration = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary { @@ -316,10 +323,10 @@ public async Task GetChannelsAsync_WhenStagingChannelEnabledWithoutQualityOverri var hivesDir = new DirectoryInfo(Path.Combine(tempDir.FullName, ".aspire", "hives")); var cacheDir = new DirectoryInfo(Path.Combine(tempDir.FullName, ".aspire", "cache")); var executionContext = new CliExecutionContext(tempDir, hivesDir, cacheDir, new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-runtimes")), new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-logs")), "test.log"); - + var features = new TestFeatures(); features.SetFeature(KnownFeatures.StagingChannelEnabled, true); - + var configuration = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary { @@ -343,10 +350,10 @@ public async Task NuGetConfigMerger_WhenChannelRequiresGlobalPackagesFolder_Adds // Arrange using var workspace = TemporaryWorkspace.Create(outputHelper); var tempDir = workspace.WorkspaceRoot; - + var features = new TestFeatures(); features.SetFeature(KnownFeatures.StagingChannelEnabled, true); - + var configuration = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary { @@ -355,9 +362,9 @@ public async Task NuGetConfigMerger_WhenChannelRequiresGlobalPackagesFolder_Adds .Build(); var packagingService = new PackagingService( - new CliExecutionContext(tempDir, tempDir, tempDir, new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-runtimes")), new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-logs")), "test.log"), - new FakeNuGetPackageCache(), - features, + new CliExecutionContext(tempDir, tempDir, tempDir, new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-runtimes")), new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-logs")), "test.log"), + new FakeNuGetPackageCache(), + features, configuration); var channels = await packagingService.GetChannelsAsync().DefaultTimeout(); @@ -369,7 +376,7 @@ public async Task NuGetConfigMerger_WhenChannelRequiresGlobalPackagesFolder_Adds // Assert var nugetConfigPath = Path.Combine(tempDir.FullName, "nuget.config"); Assert.True(File.Exists(nugetConfigPath)); - + var configContent = await File.ReadAllTextAsync(nugetConfigPath); Assert.Contains("globalPackagesFolder", configContent); Assert.Contains(".nugetpackages", configContent); @@ -378,7 +385,7 @@ public async Task NuGetConfigMerger_WhenChannelRequiresGlobalPackagesFolder_Adds var doc = XDocument.Load(nugetConfigPath); var configSection = doc.Root?.Element("config"); Assert.NotNull(configSection); - + var globalPackagesFolderAdd = configSection.Elements("add") .FirstOrDefault(add => string.Equals((string?)add.Attribute("key"), "globalPackagesFolder", StringComparison.OrdinalIgnoreCase)); Assert.NotNull(globalPackagesFolderAdd); @@ -393,17 +400,17 @@ public async Task GetChannelsAsync_WhenStagingChannelEnabled_StagingAppearsAfter var tempDir = workspace.WorkspaceRoot; var hivesDir = new DirectoryInfo(Path.Combine(tempDir.FullName, ".aspire", "hives")); var cacheDir = new DirectoryInfo(Path.Combine(tempDir.FullName, ".aspire", "cache")); - + // Create some PR hives to ensure staging appears before them hivesDir.Create(); Directory.CreateDirectory(Path.Combine(hivesDir.FullName, "pr-10167")); Directory.CreateDirectory(Path.Combine(hivesDir.FullName, "pr-11832")); - + var executionContext = new CliExecutionContext(tempDir, hivesDir, cacheDir, new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-runtimes")), new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-logs")), "test.log"); - + var features = new TestFeatures(); features.SetFeature(KnownFeatures.StagingChannelEnabled, true); - + var configuration = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary { @@ -418,7 +425,7 @@ public async Task GetChannelsAsync_WhenStagingChannelEnabled_StagingAppearsAfter // Assert var channelNames = channels.Select(c => c.Name).ToList(); - + // Verify all expected channels are present Assert.Contains("default", channelNames); Assert.Contains("stable", channelNames); @@ -426,7 +433,7 @@ public async Task GetChannelsAsync_WhenStagingChannelEnabled_StagingAppearsAfter Assert.Contains("daily", channelNames); Assert.Contains("pr-10167", channelNames); Assert.Contains("pr-11832", channelNames); - + // Verify the order: default, stable, staging, daily, pr-* var defaultIndex = channelNames.IndexOf("default"); var stableIndex = channelNames.IndexOf("stable"); @@ -434,7 +441,7 @@ public async Task GetChannelsAsync_WhenStagingChannelEnabled_StagingAppearsAfter var dailyIndex = channelNames.IndexOf("daily"); var pr10167Index = channelNames.IndexOf("pr-10167"); var pr11832Index = channelNames.IndexOf("pr-11832"); - + Assert.True(defaultIndex < stableIndex, $"default should come before stable (default: {defaultIndex}, stable: {stableIndex})"); Assert.True(stableIndex < stagingIndex, $"stable should come before staging (stable: {stableIndex}, staging: {stagingIndex})"); Assert.True(stagingIndex < dailyIndex, $"staging should come before daily (staging: {stagingIndex}, daily: {dailyIndex})"); @@ -450,13 +457,13 @@ public async Task GetChannelsAsync_WhenStagingChannelDisabled_OrderIsDefaultStab var tempDir = workspace.WorkspaceRoot; var hivesDir = new DirectoryInfo(Path.Combine(tempDir.FullName, ".aspire", "hives")); var cacheDir = new DirectoryInfo(Path.Combine(tempDir.FullName, ".aspire", "cache")); - + // Create some PR hives hivesDir.Create(); Directory.CreateDirectory(Path.Combine(hivesDir.FullName, "pr-12345")); - + var executionContext = new CliExecutionContext(tempDir, hivesDir, cacheDir, new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-runtimes")), new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-logs")), "test.log"); - + var features = new TestFeatures(); // Staging disabled by default var configuration = new ConfigurationBuilder().Build(); @@ -468,16 +475,16 @@ public async Task GetChannelsAsync_WhenStagingChannelDisabled_OrderIsDefaultStab // Assert var channelNames = channels.Select(c => c.Name).ToList(); - + // Verify staging is not present Assert.DoesNotContain("staging", channelNames); - + // Verify the order: default, stable, daily, pr-* var defaultIndex = channelNames.IndexOf("default"); var stableIndex = channelNames.IndexOf("stable"); var dailyIndex = channelNames.IndexOf("daily"); var pr12345Index = channelNames.IndexOf("pr-12345"); - + Assert.True(defaultIndex < stableIndex, "default should come before stable"); Assert.True(stableIndex < dailyIndex, "stable should come before daily"); Assert.True(dailyIndex < pr12345Index, "daily should come before pr-12345"); @@ -492,10 +499,10 @@ public async Task GetChannelsAsync_WhenStagingQualityPrerelease_AndNoFeedOverrid var hivesDir = new DirectoryInfo(Path.Combine(tempDir.FullName, ".aspire", "hives")); var cacheDir = new DirectoryInfo(Path.Combine(tempDir.FullName, ".aspire", "cache")); var executionContext = new CliExecutionContext(tempDir, hivesDir, cacheDir, new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-runtimes")), new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-logs")), "test.log"); - + var features = new TestFeatures(); features.SetFeature(KnownFeatures.StagingChannelEnabled, true); - + // Set quality to Prerelease but do NOT set overrideStagingFeed var configuration = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary @@ -513,7 +520,7 @@ public async Task GetChannelsAsync_WhenStagingQualityPrerelease_AndNoFeedOverrid var stagingChannel = channels.First(c => c.Name == "staging"); Assert.Equal(PackageChannelQuality.Prerelease, stagingChannel.Quality); Assert.False(stagingChannel.ConfigureGlobalPackagesFolder); - + var aspireMapping = stagingChannel.Mappings!.FirstOrDefault(m => m.PackageFilter == "Aspire*"); Assert.NotNull(aspireMapping); Assert.Equal("https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet9/nuget/v3/index.json", aspireMapping.Source); @@ -528,10 +535,10 @@ public async Task GetChannelsAsync_WhenStagingQualityBoth_AndNoFeedOverride_Uses var hivesDir = new DirectoryInfo(Path.Combine(tempDir.FullName, ".aspire", "hives")); var cacheDir = new DirectoryInfo(Path.Combine(tempDir.FullName, ".aspire", "cache")); var executionContext = new CliExecutionContext(tempDir, hivesDir, cacheDir, new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-runtimes")), new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-logs")), "test.log"); - + var features = new TestFeatures(); features.SetFeature(KnownFeatures.StagingChannelEnabled, true); - + // Set quality to Both but do NOT set overrideStagingFeed var configuration = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary @@ -549,7 +556,7 @@ public async Task GetChannelsAsync_WhenStagingQualityBoth_AndNoFeedOverride_Uses var stagingChannel = channels.First(c => c.Name == "staging"); Assert.Equal(PackageChannelQuality.Both, stagingChannel.Quality); Assert.False(stagingChannel.ConfigureGlobalPackagesFolder); - + var aspireMapping = stagingChannel.Mappings!.FirstOrDefault(m => m.PackageFilter == "Aspire*"); Assert.NotNull(aspireMapping); Assert.Equal("https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet9/nuget/v3/index.json", aspireMapping.Source); @@ -564,10 +571,10 @@ public async Task GetChannelsAsync_WhenStagingQualityPrerelease_WithFeedOverride var hivesDir = new DirectoryInfo(Path.Combine(tempDir.FullName, ".aspire", "hives")); var cacheDir = new DirectoryInfo(Path.Combine(tempDir.FullName, ".aspire", "cache")); var executionContext = new CliExecutionContext(tempDir, hivesDir, cacheDir, new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-runtimes")), new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-logs")), "test.log"); - + var features = new TestFeatures(); features.SetFeature(KnownFeatures.StagingChannelEnabled, true); - + // Set both quality override AND feed override — feed override should win var customFeed = "https://custom-feed.example.com/v3/index.json"; var configuration = new ConfigurationBuilder() @@ -588,7 +595,7 @@ public async Task GetChannelsAsync_WhenStagingQualityPrerelease_WithFeedOverride Assert.Equal(PackageChannelQuality.Prerelease, stagingChannel.Quality); // When an explicit feed override is provided, globalPackagesFolder stays enabled Assert.True(stagingChannel.ConfigureGlobalPackagesFolder); - + var aspireMapping = stagingChannel.Mappings!.FirstOrDefault(m => m.PackageFilter == "Aspire*"); Assert.NotNull(aspireMapping); Assert.Equal(customFeed, aspireMapping.Source); @@ -600,10 +607,10 @@ public async Task NuGetConfigMerger_WhenStagingUsesSharedFeed_DoesNotAddGlobalPa // Arrange using var workspace = TemporaryWorkspace.Create(outputHelper); var tempDir = workspace.WorkspaceRoot; - + var features = new TestFeatures(); features.SetFeature(KnownFeatures.StagingChannelEnabled, true); - + // Quality=Prerelease with no feed override → shared feed mode var configuration = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary @@ -613,9 +620,9 @@ public async Task NuGetConfigMerger_WhenStagingUsesSharedFeed_DoesNotAddGlobalPa .Build(); var packagingService = new PackagingService( - new CliExecutionContext(tempDir, tempDir, tempDir, new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-runtimes")), new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-logs")), "test.log"), - new FakeNuGetPackageCache(), - features, + new CliExecutionContext(tempDir, tempDir, tempDir, new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-runtimes")), new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-logs")), "test.log"), + new FakeNuGetPackageCache(), + features, configuration); var channels = await packagingService.GetChannelsAsync().DefaultTimeout(); @@ -627,11 +634,11 @@ public async Task NuGetConfigMerger_WhenStagingUsesSharedFeed_DoesNotAddGlobalPa // Assert var nugetConfigPath = Path.Combine(tempDir.FullName, "nuget.config"); Assert.True(File.Exists(nugetConfigPath)); - + var configContent = await File.ReadAllTextAsync(nugetConfigPath); Assert.DoesNotContain("globalPackagesFolder", configContent); Assert.DoesNotContain(".nugetpackages", configContent); - + // Verify it still has the shared feed URL Assert.Contains("dotnet9", configContent); } @@ -645,10 +652,10 @@ public async Task GetChannelsAsync_WhenStagingPinToCliVersionSet_ChannelHasPinne var hivesDir = new DirectoryInfo(Path.Combine(tempDir.FullName, ".aspire", "hives")); var cacheDir = new DirectoryInfo(Path.Combine(tempDir.FullName, ".aspire", "cache")); var executionContext = new CliExecutionContext(tempDir, hivesDir, cacheDir, new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-runtimes")), new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-logs")), "test.log"); - + var features = new TestFeatures(); features.SetFeature(KnownFeatures.StagingChannelEnabled, true); - + var configuration = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary { @@ -678,10 +685,10 @@ public async Task GetChannelsAsync_WhenStagingPinToCliVersionNotSet_ChannelHasNo var hivesDir = new DirectoryInfo(Path.Combine(tempDir.FullName, ".aspire", "hives")); var cacheDir = new DirectoryInfo(Path.Combine(tempDir.FullName, ".aspire", "cache")); var executionContext = new CliExecutionContext(tempDir, hivesDir, cacheDir, new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-runtimes")), new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-logs")), "test.log"); - + var features = new TestFeatures(); features.SetFeature(KnownFeatures.StagingChannelEnabled, true); - + var configuration = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary { @@ -709,10 +716,10 @@ public async Task GetChannelsAsync_WhenStagingPinToCliVersionSetButNotSharedFeed var hivesDir = new DirectoryInfo(Path.Combine(tempDir.FullName, ".aspire", "hives")); var cacheDir = new DirectoryInfo(Path.Combine(tempDir.FullName, ".aspire", "cache")); var executionContext = new CliExecutionContext(tempDir, hivesDir, cacheDir, new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-runtimes")), new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-logs")), "test.log"); - + var features = new TestFeatures(); features.SetFeature(KnownFeatures.StagingChannelEnabled, true); - + var configuration = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary { @@ -902,6 +909,13 @@ private sealed class FakeNuGetPackageCacheWithPackages(List !Semver.SemVersion.Parse(p.Version).IsPrerelease); return Task.FromResult>(filtered.ToList()); } + public Task> GetPackageVersionsAsync(DirectoryInfo workingDirectory, string exactPackageId, bool prerelease, FileInfo? nugetConfigFile, bool useCache, CancellationToken cancellationToken) + { + var filtered = prerelease + ? packages.Where(p => Semver.SemVersion.Parse(p.Version).IsPrerelease) + : packages.Where(p => !Semver.SemVersion.Parse(p.Version).IsPrerelease); + return Task.FromResult>(filtered.ToList()); + } public Task> GetIntegrationPackagesAsync(DirectoryInfo workingDirectory, bool prerelease, FileInfo? nugetConfigFile, CancellationToken cancellationToken) => GetTemplatePackagesAsync(workingDirectory, prerelease, nugetConfigFile, cancellationToken); diff --git a/tests/Aspire.Cli.Tests/Projects/AppHostServerProjectTests.cs b/tests/Aspire.Cli.Tests/Projects/AppHostServerProjectTests.cs index 2deebbb0aca..294080529c6 100644 --- a/tests/Aspire.Cli.Tests/Projects/AppHostServerProjectTests.cs +++ b/tests/Aspire.Cli.Tests/Projects/AppHostServerProjectTests.cs @@ -248,7 +248,7 @@ await File.WriteAllTextAsync(settingsJson, """ prNewHive.FullName); var runner = new TestDotNetCliRunner(); - + // Use a real logger to capture debug output for diagnostics using var loggerFactory = LoggerFactory.Create(builder => { @@ -354,18 +354,21 @@ public Task> GetCliPackagesAsync(DirectoryInfo work public Task> GetPackagesAsync(DirectoryInfo workingDirectory, string packageId, Func? filter, bool prerelease, FileInfo? nugetConfigFile, bool useCache, CancellationToken cancellationToken) => Task.FromResult>([]); + + public Task> GetPackageVersionsAsync(DirectoryInfo workingDirectory, string exactPackageId, bool prerelease, FileInfo? nugetConfigFile, bool useCache, CancellationToken cancellationToken) + => Task.FromResult>([]); } private static void DumpDirectoryTree(string path, ITestOutputHelper output, string indent = "") { var dirInfo = new DirectoryInfo(path); output.WriteLine($"{indent}{dirInfo.Name}/"); - + foreach (var file in dirInfo.GetFiles()) { output.WriteLine($"{indent} {file.Name}"); } - + foreach (var dir in dirInfo.GetDirectories()) { DumpDirectoryTree(dir.FullName, output, indent + " "); diff --git a/tests/Aspire.Cli.Tests/Projects/ProjectUpdaterTests.cs b/tests/Aspire.Cli.Tests/Projects/ProjectUpdaterTests.cs index 4aef9e1b59e..b942606268c 100644 --- a/tests/Aspire.Cli.Tests/Projects/ProjectUpdaterTests.cs +++ b/tests/Aspire.Cli.Tests/Projects/ProjectUpdaterTests.cs @@ -49,7 +49,7 @@ await File.WriteAllTextAsync( { return new TestDotNetCliRunner() { - SearchPackagesAsyncCallback = (_, query, _, _, _, _, _, _, _) => + SearchPackagesAsyncCallback = (_, query, _, _, _, _, _, _, _, _) => { var packages = new List(); @@ -160,7 +160,7 @@ await File.WriteAllTextAsync( { return new TestDotNetCliRunner() { - SearchPackagesAsyncCallback = (_, query, _, _, _, _, _, _, _) => + SearchPackagesAsyncCallback = (_, query, _, _, _, _, _, _, _, _) => { var packages = new List(); @@ -291,7 +291,7 @@ await File.WriteAllTextAsync( { return new TestDotNetCliRunner() { - SearchPackagesAsyncCallback = (_, query, prerelease, _, _, _, _, _, _) => + SearchPackagesAsyncCallback = (_, query, _, prerelease, _, _, _, _, _, _) => { var packages = new List(); @@ -444,7 +444,7 @@ await File.WriteAllTextAsync( { return new TestDotNetCliRunner() { - SearchPackagesAsyncCallback = (_, query, _, _, _, _, _, _, _) => + SearchPackagesAsyncCallback = (_, query, _, _, _, _, _, _, _, _) => { var packages = new List(); @@ -604,7 +604,7 @@ await File.WriteAllTextAsync( { return new TestDotNetCliRunner() { - SearchPackagesAsyncCallback = (_, query, _, _, _, _, _, _, _) => + SearchPackagesAsyncCallback = (_, query, _, _, _, _, _, _, _, _) => { var packages = new List(); @@ -723,7 +723,7 @@ await File.WriteAllTextAsync( { return new TestDotNetCliRunner() { - SearchPackagesAsyncCallback = (_, query, _, _, _, _, _, _, _) => + SearchPackagesAsyncCallback = (_, query, _, _, _, _, _, _, _, _) => { var packages = new List(); @@ -825,7 +825,7 @@ await File.WriteAllTextAsync( { return new TestDotNetCliRunner() { - SearchPackagesAsyncCallback = (_, query, _, _, _, _, _, _, _) => + SearchPackagesAsyncCallback = (_, query, _, _, _, _, _, _, _, _) => { var packages = new List(); @@ -959,7 +959,7 @@ await File.WriteAllTextAsync( { return new TestDotNetCliRunner() { - SearchPackagesAsyncCallback = (_, query, _, _, _, _, _, _, _) => + SearchPackagesAsyncCallback = (_, query, _, _, _, _, _, _, _, _) => { var packages = new List(); @@ -1075,7 +1075,7 @@ await File.WriteAllTextAsync( { return new TestDotNetCliRunner() { - SearchPackagesAsyncCallback = (_, query, _, _, _, _, _, _, _) => + SearchPackagesAsyncCallback = (_, query, _, _, _, _, _, _, _, _) => { var packages = new List(); @@ -1196,7 +1196,7 @@ await File.WriteAllTextAsync( { return new TestDotNetCliRunner() { - SearchPackagesAsyncCallback = (_, query, _, _, _, _, _, _, _) => + SearchPackagesAsyncCallback = (_, query, _, _, _, _, _, _, _, _) => { var packages = new List(); @@ -1309,7 +1309,7 @@ await File.WriteAllTextAsync( { return new TestDotNetCliRunner() { - SearchPackagesAsyncCallback = (_, query, _, _, _, _, _, _, _) => + SearchPackagesAsyncCallback = (_, query, _, _, _, _, _, _, _, _) => { var packages = new List(); @@ -1405,7 +1405,7 @@ await File.WriteAllTextAsync( { return new TestDotNetCliRunner() { - SearchPackagesAsyncCallback = (_, query, _, _, _, _, _, _, _) => + SearchPackagesAsyncCallback = (_, query, _, _, _, _, _, _, _, _) => { var packages = new List(); @@ -1514,7 +1514,7 @@ await File.WriteAllTextAsync( { return new TestDotNetCliRunner() { - SearchPackagesAsyncCallback = (_, query, _, _, _, _, _, _, _) => + SearchPackagesAsyncCallback = (_, query, _, _, _, _, _, _, _, _) => { var packages = new List(); @@ -1599,7 +1599,7 @@ await File.WriteAllTextAsync( { return new TestDotNetCliRunner() { - SearchPackagesAsyncCallback = (_, query, _, _, _, _, _, _, _) => + SearchPackagesAsyncCallback = (_, query, _, _, _, _, _, _, _, _) => { var packages = new List(); @@ -1675,7 +1675,7 @@ await File.WriteAllTextAsync( { return new TestDotNetCliRunner() { - SearchPackagesAsyncCallback = (_, query, _, _, _, _, _, _, _) => + SearchPackagesAsyncCallback = (_, query, _, _, _, _, _, _, _, _) => { var packages = new List(); @@ -1759,7 +1759,7 @@ await File.WriteAllTextAsync( { return new TestDotNetCliRunner() { - SearchPackagesAsyncCallback = (_, query, _, _, _, _, _, _, _) => + SearchPackagesAsyncCallback = (_, query, _, _, _, _, _, _, _, _) => { var packages = new List(); @@ -1842,7 +1842,7 @@ await File.WriteAllTextAsync( { return new TestDotNetCliRunner() { - SearchPackagesAsyncCallback = (_, query, _, _, _, _, _, _, _) => + SearchPackagesAsyncCallback = (_, query, _, _, _, _, _, _, _, _) => { var packages = new List(); @@ -1922,7 +1922,7 @@ await File.WriteAllTextAsync( { return new TestDotNetCliRunner() { - SearchPackagesAsyncCallback = (_, query, _, _, _, _, _, _, _) => + SearchPackagesAsyncCallback = (_, query, _, _, _, _, _, _, _, _) => { var packages = new List(); @@ -2012,7 +2012,7 @@ await File.WriteAllTextAsync( { return new TestDotNetCliRunner() { - SearchPackagesAsyncCallback = (_, query, _, _, _, _, _, _, _) => + SearchPackagesAsyncCallback = (_, query, _, _, _, _, _, _, _, _) => { var packages = new List(); @@ -2091,7 +2091,7 @@ await File.WriteAllTextAsync( { return new TestDotNetCliRunner() { - SearchPackagesAsyncCallback = (_, query, _, _, _, _, _, _, _) => + SearchPackagesAsyncCallback = (_, query, _, _, _, _, _, _, _, _) => { var packages = new List(); @@ -2169,7 +2169,7 @@ await File.WriteAllTextAsync( { return new TestDotNetCliRunner() { - SearchPackagesAsyncCallback = (_, query, _, _, _, _, _, _, _) => + SearchPackagesAsyncCallback = (_, query, _, _, _, _, _, _, _, _) => { var packages = new List(); @@ -2297,7 +2297,7 @@ await File.WriteAllTextAsync( { return new TestDotNetCliRunner() { - SearchPackagesAsyncCallback = (_, query, _, _, _, _, _, _, _) => + SearchPackagesAsyncCallback = (_, query, _, _, _, _, _, _, _, _) => { var packages = new List(); diff --git a/tests/Aspire.Cli.Tests/Templating/DotNetTemplateFactoryTests.cs b/tests/Aspire.Cli.Tests/Templating/DotNetTemplateFactoryTests.cs index 3bf653368cc..6a6062efe3f 100644 --- a/tests/Aspire.Cli.Tests/Templating/DotNetTemplateFactoryTests.cs +++ b/tests/Aspire.Cli.Tests/Templating/DotNetTemplateFactoryTests.cs @@ -49,6 +49,10 @@ private sealed class FakeNuGetPackageCache : INuGetPackageCache { _ = workingDirectory; _ = packageId; _ = filter; _ = prerelease; _ = nugetConfigFile; _ = useCache; _ = cancellationToken; return Task.FromResult>([]); } + public Task> GetPackageVersionsAsync(DirectoryInfo workingDirectory, string exactPackageId, bool prerelease, FileInfo? nugetConfigFile, bool useCache, CancellationToken cancellationToken) + { + _ = workingDirectory; _ = exactPackageId; _ = prerelease; _ = nugetConfigFile; _ = useCache; _ = cancellationToken; return Task.FromResult>([]); + } } private static PackageChannel CreateExplicitChannel(PackageMapping[] mappings) => @@ -522,7 +526,7 @@ public Task AddProjectToSolutionAsync(FileInfo solutionFile, FileInfo proje public Task AddProjectReferenceAsync(FileInfo projectFile, FileInfo referencedProjectFile, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken) => throw new NotImplementedException(); - public Task<(int ExitCode, NuGetPackageCli[]? Packages)> SearchPackagesAsync(DirectoryInfo workingDirectory, string query, bool prerelease, int take, int skip, FileInfo? nugetConfigFile, bool useCache, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken) + public Task<(int ExitCode, NuGetPackageCli[]? Packages)> SearchPackagesAsync(DirectoryInfo workingDirectory, string query, bool exactMatch, bool prerelease, int take, int skip, FileInfo? nugetConfigFile, bool useCache, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken) => throw new NotImplementedException(); public Task<(int ExitCode, bool IsAspireHost, string? AspireHostingVersion)> GetAppHostInformationAsync(FileInfo projectFile, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken) diff --git a/tests/Aspire.Cli.Tests/TestServices/FakeNuGetPackageCache.cs b/tests/Aspire.Cli.Tests/TestServices/FakeNuGetPackageCache.cs index b628d68ae9e..bd50a77319a 100644 --- a/tests/Aspire.Cli.Tests/TestServices/FakeNuGetPackageCache.cs +++ b/tests/Aspire.Cli.Tests/TestServices/FakeNuGetPackageCache.cs @@ -19,4 +19,7 @@ public Task> GetCliPackagesAsync(DirectoryInfo working public Task> GetPackagesAsync(DirectoryInfo workingDirectory, string packageId, Func? filter, bool prerelease, FileInfo? nugetConfigFile, bool useCache, CancellationToken cancellationToken) => Task.FromResult>([]); + + public Task> GetPackageVersionsAsync(DirectoryInfo workingDirectory, string exactPackageId, bool prerelease, FileInfo? nugetConfigFile, bool useCache, CancellationToken cancellationToken) + => Task.FromResult>([]); } diff --git a/tests/Aspire.Cli.Tests/TestServices/TestDotNetCliRunner.cs b/tests/Aspire.Cli.Tests/TestServices/TestDotNetCliRunner.cs index 32101c9ae3a..1a02b9480ef 100644 --- a/tests/Aspire.Cli.Tests/TestServices/TestDotNetCliRunner.cs +++ b/tests/Aspire.Cli.Tests/TestServices/TestDotNetCliRunner.cs @@ -21,7 +21,7 @@ internal sealed class TestDotNetCliRunner : IDotNetCliRunner public Func? InstallTemplateAsyncCallback { get; set; } public Func? NewProjectAsyncCallback { get; set; } public Func?, TaskCompletionSource?, DotNetCliRunnerInvocationOptions, CancellationToken, Task>? RunAsyncCallback { get; set; } - public Func? SearchPackagesAsyncCallback { get; set; } + public Func? SearchPackagesAsyncCallback { get; set; } public Func Projects)>? GetSolutionProjectsAsyncCallback { get; set; } public Func? AddProjectReferenceAsyncCallback { get; set; } @@ -68,7 +68,7 @@ public Task RestoreAsync(FileInfo projectFilePath, DotNetCliRunnerInvocatio ? Task.FromResult(GetNuGetConfigPathsAsyncCallback(workingDirectory, options, cancellationToken)) : Task.FromResult((0, GetGlobalNuGetPaths())); // If not overridden, return success with no config paths which will blow up. } - + private static string[] GetGlobalNuGetPaths() { return Environment.OSVersion.Platform switch @@ -106,10 +106,10 @@ public Task RunAsync(FileInfo projectFile, bool watch, bool noBuild, bool n : throw new NotImplementedException(); } - public Task<(int ExitCode, NuGetPackage[]? Packages)> SearchPackagesAsync(DirectoryInfo workingDirectory, string query, bool prerelease, int take, int skip, FileInfo? nugetConfigFile, bool useCache, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken) + public Task<(int ExitCode, NuGetPackage[]? Packages)> SearchPackagesAsync(DirectoryInfo workingDirectory, string query, bool exactMatch, bool prerelease, int take, int skip, FileInfo? nugetConfigFile, bool useCache, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken) { return SearchPackagesAsyncCallback != null - ? Task.FromResult(SearchPackagesAsyncCallback(workingDirectory, query, prerelease, take, skip, nugetConfigFile, useCache, options, cancellationToken)) + ? Task.FromResult(SearchPackagesAsyncCallback(workingDirectory, query, exactMatch, prerelease, take, skip, nugetConfigFile, useCache, options, cancellationToken)) : throw new NotImplementedException(); } diff --git a/tests/Aspire.Cli.Tests/Utils/CliUpdateNotificationServiceTests.cs b/tests/Aspire.Cli.Tests/Utils/CliUpdateNotificationServiceTests.cs index 081dc306dbc..e3933d6c8bc 100644 --- a/tests/Aspire.Cli.Tests/Utils/CliUpdateNotificationServiceTests.cs +++ b/tests/Aspire.Cli.Tests/Utils/CliUpdateNotificationServiceTests.cs @@ -312,4 +312,9 @@ public Task> GetPackagesAsync(DirectoryInfo workingDir { return Task.FromResult(Enumerable.Empty()); } + + public Task> GetPackageVersionsAsync(DirectoryInfo workingDirectory, string exactPackageId, bool prerelease, FileInfo? nugetConfigFile, bool useCache, CancellationToken cancellationToken) + { + return Task.FromResult(Enumerable.Empty()); + } }