File: Configuration\DotNetBasedAppHostServerChannelResolutionTests.cs
Web Access
Project: src\tests\Aspire.Cli.Tests\Aspire.Cli.Tests.csproj (Aspire.Cli.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.Configuration;
using Aspire.Cli.Packaging;
using Aspire.Cli.Projects;
using Aspire.Cli.Tests.Mcp;
using Aspire.Cli.Tests.TestServices;
using Aspire.Cli.Tests.Utils;
using Microsoft.Extensions.Logging.Abstractions;
 
namespace Aspire.Cli.Tests.Configuration;
 
/// <summary>
/// Behavioral guards on <see cref="DotNetBasedAppHostServerProject"/>'s channel resolution:
/// it consults only per-project state (<c>aspire.config.json</c>, then legacy
/// <c>AspireJsonConfiguration</c>) and falls through to the channel system itself when no
/// per-project channel is set. A global-channel read fallback (previously
/// <c>IConfigurationService.GetConfigurationAsync("channel", ...)</c>) was removed in
/// PR1; these tests pin the post-fix contract so a regression can't quietly re-introduce
/// cross-route channel contamination.
/// </summary>
public class DotNetBasedAppHostServerChannelResolutionTests(ITestOutputHelper outputHelper)
{
    [Fact]
    public async Task DotNetBasedAppHostServerProject_CreateProjectFiles_ReturnsNullChannel_WhenNoPerProjectChannelAndOnlyImplicit()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var appPath = workspace.WorkspaceRoot.FullName;
 
        var project = CreateProject(appPath, MockPackagingServiceFactory.Create());
 
        var (_, channelName) = await project.CreateProjectFilesAsync(
            [IntegrationReference.FromPackage("Aspire.Hosting", "13.1.0")]);
 
        // No per-project channel + only an Implicit channel registered → no name to pin.
        // Before the fallback was removed, the resolver would have consulted the global
        // IConfigurationService here and could have returned a stale "channel" value.
        Assert.Null(channelName);
    }
 
    [Fact]
    public async Task DotNetBasedAppHostServerProject_CreateProjectFiles_HonorsAspireConfigJson()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var appPath = workspace.WorkspaceRoot.FullName;
 
        var config = AspireConfigFile.LoadOrCreate(appPath);
        config.Channel = "staging";
        config.Save(appPath);
 
        var project = CreateProject(appPath, CreatePackagingServiceWithExplicitChannels("staging", "daily"));
 
        var (_, channelName) = await project.CreateProjectFilesAsync(
            [IntegrationReference.FromPackage("Aspire.Hosting", "13.1.0")]);
 
        Assert.Equal("staging", channelName);
    }
 
    [Fact]
    public async Task DotNetBasedAppHostServerProject_CreateProjectFiles_FallsBackToLegacyAspireSettings_WhenAspireConfigJsonMissing()
    {
        // Migration safety: projects scaffolded before per-project aspire.config.json landed
        // wrote their channel into .aspire/settings.json (AspireJsonConfiguration). The reader
        // must still honor the legacy file so existing on-disk projects keep working.
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var appPath = workspace.WorkspaceRoot.FullName;
 
        var legacy = new AspireJsonConfiguration { Channel = "daily", SdkVersion = "13.3.0" };
        legacy.Save(appPath);
 
        Assert.False(File.Exists(Path.Combine(appPath, AspireConfigFile.FileName)));
 
        var project = CreateProject(appPath, CreatePackagingServiceWithExplicitChannels("staging", "daily"));
 
        var (_, channelName) = await project.CreateProjectFilesAsync(
            [IntegrationReference.FromPackage("Aspire.Hosting", "13.1.0")]);
 
        Assert.Equal("daily", channelName);
    }
 
    [Fact]
    public async Task DotNetBasedAppHostServerProject_CreateProjectFiles_PrefersAspireConfigJsonOverLegacyAspireSettings()
    {
        // When both files exist, the new format wins. Pins the `??` operand order in the
        // reader so an accidental swap is caught.
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var appPath = workspace.WorkspaceRoot.FullName;
 
        var legacy = new AspireJsonConfiguration { Channel = "daily", SdkVersion = "13.3.0" };
        legacy.Save(appPath);
 
        var config = AspireConfigFile.LoadOrCreate(appPath);
        config.Channel = "staging";
        config.Save(appPath);
 
        var project = CreateProject(appPath, CreatePackagingServiceWithExplicitChannels("staging", "daily"));
 
        var (_, channelName) = await project.CreateProjectFilesAsync(
            [IntegrationReference.FromPackage("Aspire.Hosting", "13.1.0")]);
 
        Assert.Equal("staging", channelName);
    }
 
    private static DotNetBasedAppHostServerProject CreateProject(string appPath, TestPackagingService packagingService)
    {
        // Pin ProjectModelPath inside the workspace so test artifacts don't bleed into the
        // user's ~/.aspire/hosts directory.
        var projectModelPath = Path.Combine(appPath, ".aspire_server");
 
        return new DotNetBasedAppHostServerProject(
            appPath,
            socketPath: "test.sock",
            repoRoot: appPath,
            new TestDotNetCliRunner(),
            packagingService,
            new TestProcessExecutionFactory(),
            new TestEnvironment(),
            NullLogger<DotNetBasedAppHostServerProject>.Instance,
            projectModelPath);
    }
 
    private static TestPackagingService CreatePackagingServiceWithExplicitChannels(params string[] channelNames)
    {
        return new TestPackagingService
        {
            GetChannelsAsyncCallback = _ =>
            {
                var cache = new FakeNuGetPackageCache();
                var channels = channelNames
                    .Select(name => PackageChannel.CreateExplicitChannel(
                        name,
                        PackageChannelQuality.Both,
                        mappings: [],
                        cache,
                        new TestFeatures(),
                        NullLogger.Instance))
                    .ToArray();
                return Task.FromResult<IEnumerable<PackageChannel>>(channels);
            }
        };
    }
}