File: CentralPackageManagementTests.cs
Web Access
Project: src\tests\Aspire.Cli.EndToEnd.Tests\Aspire.Cli.EndToEnd.Tests.csproj (Aspire.Cli.EndToEnd.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using Aspire.Cli.EndToEnd.Tests.Helpers;
using Aspire.Cli.Tests.Utils;
using Hex1b.Automation;
using Xunit;
 
namespace Aspire.Cli.EndToEnd.Tests;
 
/// <summary>
/// End-to-end tests for Central Package Management (CPM) compatibility.
/// Validates that aspire update correctly handles CPM projects.
/// Each test class runs as a separate CI job for parallelization.
/// </summary>
public sealed class CentralPackageManagementTests(ITestOutputHelper output)
{
    [Fact]
    public async Task AspireUpdateRemovesAppHostPackageVersionFromDirectoryPackagesProps()
    {
        var workspace = TemporaryWorkspace.Create(output);
 
        var prNumber = CliE2ETestHelpers.GetRequiredPrNumber();
        var commitSha = CliE2ETestHelpers.GetRequiredCommitSha();
        var isCI = CliE2ETestHelpers.IsRunningInCI;
        using var terminal = CliE2ETestHelpers.CreateTestTerminal();
 
        var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken);
 
        // aspire update prompts
        var waitingForPerformUpdates = new CellPatternSearcher()
            .Find("Perform updates?");
 
        var waitingForNuGetConfigDirectory = new CellPatternSearcher()
            .Find("Which directory for NuGet.config file?");
 
        var waitingForApplyNuGetConfig = new CellPatternSearcher()
            .Find("Apply these changes to NuGet.config?");
 
        var waitingForUpdateSuccessful = new CellPatternSearcher()
            .Find("Update successful!");
 
        var counter = new SequenceCounter();
        var sequenceBuilder = new Hex1bTerminalInputSequenceBuilder();
 
        sequenceBuilder.PrepareEnvironment(workspace, counter);
 
        if (isCI)
        {
            sequenceBuilder.InstallAspireCliFromPullRequest(prNumber, counter);
            sequenceBuilder.SourceAspireCliEnvironment(counter);
            sequenceBuilder.VerifyAspireCliVersion(commitSha, counter);
        }
 
        // Disable update notifications to prevent the CLI self-update prompt
        // from appearing after "Update successful!" and blocking the test.
        sequenceBuilder
            .Type("aspire config set features.updateNotificationsEnabled false -g")
            .Enter()
            .WaitForSuccessPrompt(counter);
 
        // Set up an old-format AppHost project with CPM that has a PackageVersion
        // for Aspire.Hosting.AppHost. This simulates a pre-migration project where
        // the user adopted CPM before the SDK started adding the implicit reference.
        var projectDir = Path.Combine(workspace.WorkspaceRoot.FullName, "CpmTest");
        var appHostDir = Path.Combine(projectDir, "CpmTest.AppHost");
        var appHostCsprojPath = Path.Combine(appHostDir, "CpmTest.AppHost.csproj");
        var directoryPackagesPropsPath = Path.Combine(projectDir, "Directory.Packages.props");
 
        sequenceBuilder
            .ExecuteCallback(() =>
            {
                Directory.CreateDirectory(appHostDir);
 
                File.WriteAllText(appHostCsprojPath, """
                    <Project Sdk="Microsoft.NET.Sdk">
                        <Sdk Name="Aspire.AppHost.Sdk" Version="9.1.0" />
                        <PropertyGroup>
                            <OutputType>Exe</OutputType>
                            <TargetFramework>net9.0</TargetFramework>
                            <IsAspireHost>true</IsAspireHost>
                        </PropertyGroup>
                        <ItemGroup>
                            <PackageReference Include="Aspire.Hosting.AppHost" />
                        </ItemGroup>
                    </Project>
                    """);
 
                File.WriteAllText(Path.Combine(appHostDir, "Program.cs"), """
                    var builder = DistributedApplication.CreateBuilder(args);
                    builder.Build().Run();
                    """);
 
                File.WriteAllText(directoryPackagesPropsPath, """
                    <Project>
                        <PropertyGroup>
                            <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
                        </PropertyGroup>
                        <ItemGroup>
                            <PackageVersion Include="Aspire.Hosting.AppHost" Version="9.1.0" />
                        </ItemGroup>
                    </Project>
                    """);
            })
            // Use --channel stable to skip the channel selection prompt that appears
            // in CI when PR hive directories are present.
            .Type($"aspire update --project \"{appHostCsprojPath}\" --channel stable")
            .Enter()
            .WaitUntil(s => waitingForPerformUpdates.Search(s).Count > 0, TimeSpan.FromSeconds(60))
            .Enter() // confirm "Perform updates?" (default: Yes)
            // The updater may prompt for a NuGet.config location and ask to apply changes
            // when the project doesn't have an existing NuGet.config. Accept defaults for both.
            .WaitUntil(s => waitingForNuGetConfigDirectory.Search(s).Count > 0, TimeSpan.FromSeconds(30))
            .Enter() // accept default directory
            .WaitUntil(s => waitingForApplyNuGetConfig.Search(s).Count > 0, TimeSpan.FromSeconds(30))
            .Enter() // confirm "Apply these changes to NuGet.config?" (default: Yes)
            .WaitUntil(s => waitingForUpdateSuccessful.Search(s).Count > 0, TimeSpan.FromSeconds(60))
            .WaitForSuccessPrompt(counter)
            // Verify the PackageVersion for Aspire.Hosting.AppHost was removed
            .VerifyFileDoesNotContain(directoryPackagesPropsPath, "Aspire.Hosting.AppHost")
            // Verify dotnet restore succeeds (would fail with NU1009 without the fix)
            .Type($"dotnet restore \"{appHostCsprojPath}\"")
            .Enter()
            .WaitForSuccessPrompt(counter, TimeSpan.FromSeconds(120))
            // Clean up: re-enable update notifications
            .Type("aspire config delete features.updateNotificationsEnabled -g")
            .Enter()
            .WaitForSuccessPrompt(counter)
            .Type("exit")
            .Enter();
 
        var sequence = sequenceBuilder.Build();
 
        await sequence.ApplyAsync(terminal, TestContext.Current.CancellationToken);
 
        await pendingRun;
    }
}