File: GoPublishTests.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 Hex1b.Automation;
using Xunit;
 
namespace Aspire.Cli.EndToEnd.Tests;
 
/// <summary>
/// End-to-end tests for Aspire CLI publish flows with a Go AppHost.
/// </summary>
public sealed class GoPublishTests(ITestOutputHelper output)
{
    [Fact]
    [CaptureWorkspaceOnFailure]
    public async Task PublishWithDockerComposeServiceCallbackSucceeds()
    {
        var repoRoot = CliE2ETestHelpers.GetRepoRoot();
        var strategy = CliInstallStrategy.Detect(output.WriteLine);
        using var workspace = TemporaryWorkspace.Create(output);
 
        using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, variant: CliE2ETestHelpers.DockerfileVariant.PolyglotGo, mountDockerSocket: true, workspace: workspace);
        var counter = new SequenceCounter();
        var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500));
        await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken);
 
        await auto.PrepareDockerEnvironmentAsync(counter, workspace);
        await auto.InstallAspireCliAsync(strategy, counter);
        await auto.EnableExperimentalGoSupportAsync(counter);
 
        await auto.AspireInitGoAppHostAsync(counter);
        await auto.AspireAddPackageAsync("Aspire.Hosting.Docker", counter);
        await auto.RunCommandAsync("aspire restore --non-interactive", counter, TimeSpan.FromMinutes(3));
 
        WriteDockerComposeContainerAppHost(workspace);
 
        await auto.RunCommandAsync("unset ASPIRE_PLAYGROUND", counter);
        await auto.RunCommandAsync("aspire publish -o artifacts --non-interactive", counter, TimeSpan.FromMinutes(5));
 
        await auto.RunCommandAsync("ls -la artifacts/docker-compose.yaml", counter);
        await auto.RunCommandAsync("grep -F \"api:\" artifacts/docker-compose.yaml", counter);
        await auto.RunCommandAsync("grep -F \"validation-api\" artifacts/docker-compose.yaml", counter);
    }
 
    [Fact]
    [CaptureWorkspaceOnFailure]
    public async Task PublishWithoutOutputPathUsesAppHostDirectoryDefault()
    {
        var repoRoot = CliE2ETestHelpers.GetRepoRoot();
        var strategy = CliInstallStrategy.Detect(output.WriteLine);
        using var workspace = TemporaryWorkspace.Create(output);
 
        using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, variant: CliE2ETestHelpers.DockerfileVariant.PolyglotGo, mountDockerSocket: true, workspace: workspace);
        var counter = new SequenceCounter();
        var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500));
        await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken);
 
        await auto.PrepareDockerEnvironmentAsync(counter, workspace);
        await auto.InstallAspireCliAsync(strategy, counter);
        await auto.EnableExperimentalGoSupportAsync(counter);
 
        await auto.AspireInitGoAppHostAsync(counter);
        await auto.AspireAddPackageAsync("Aspire.Hosting.Docker", counter);
        await auto.RunCommandAsync("aspire restore --non-interactive", counter, TimeSpan.FromMinutes(3));
 
        WriteDockerComposeContainerAppHost(workspace);
 
        await auto.RunCommandAsync("unset ASPIRE_PLAYGROUND", counter);
        await auto.RunCommandAsync("aspire publish --non-interactive", counter, TimeSpan.FromMinutes(5));
 
        var dockerComposePath = Path.Combine(workspace.WorkspaceRoot.FullName, "aspire-output", "docker-compose.yaml");
        Assert.True(File.Exists(dockerComposePath), $"Expected docker-compose output at {dockerComposePath}");
 
        var dockerComposeContent = await File.ReadAllTextAsync(dockerComposePath);
        Assert.Contains("api:", dockerComposeContent);
    }
 
    [Fact]
    [CaptureWorkspaceOnFailure]
    public async Task PublishGoStarterGeneratesGoAppDockerfile()
    {
        var repoRoot = CliE2ETestHelpers.GetRepoRoot();
        var strategy = CliInstallStrategy.Detect(output.WriteLine);
        using var workspace = TemporaryWorkspace.Create(output);
 
        using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, variant: CliE2ETestHelpers.DockerfileVariant.PolyglotGo, mountDockerSocket: true, workspace: workspace);
        var counter = new SequenceCounter();
        var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500));
        await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken);
 
        await auto.PrepareDockerEnvironmentAsync(counter, workspace);
        await auto.InstallAspireCliAsync(strategy, counter);
        await auto.EnableExperimentalGoSupportAsync(counter);
 
        const string projectName = "GoPublishApp";
        await auto.AspireNewAsync(projectName, counter, template: AspireTemplate.GoStarter);
        await auto.RunCommandAsync($"cd {projectName}", counter);
        await auto.AspireAddPackageAsync("Aspire.Hosting.Docker", counter);
        await auto.RunCommandAsync("aspire restore --non-interactive", counter, TimeSpan.FromMinutes(3));
 
        // The starter has no compute environment; the Go API and the Redis cache both target this one.
        GoAppHostTestHelpers.InsertBeforeBuild(
            Path.Combine(workspace.WorkspaceRoot.FullName, projectName, "apphost.go"),
            "builder.AddDockerComposeEnvironment(\"compose\")");
 
        await auto.RunCommandAsync("unset ASPIRE_PLAYGROUND", counter);
        await auto.RunCommandAsync("aspire publish -o artifacts --non-interactive", counter, TimeSpan.FromMinutes(5));
 
        var artifactsPath = Path.Combine(workspace.WorkspaceRoot.FullName, projectName, "artifacts");
        var composeContent = await File.ReadAllTextAsync(Path.Combine(artifactsPath, "docker-compose.yaml"));
 
        Assert.Contains("api:", composeContent);
        Assert.Contains("cache:", composeContent);
 
        // The Go version in the FROM line comes from api/go.mod, so only the stable parts are asserted.
        AssertDockerfileContains(
            artifactsPath,
            "api",
            "FROM golang:",
            " AS build",
            "USER app",
            "ENTRYPOINT [\"/app/api\"]");
    }
 
    private static void WriteDockerComposeContainerAppHost(TemporaryWorkspace workspace)
    {
        GoAppHostTestHelpers.InsertBeforeBuild(
            Path.Combine(workspace.WorkspaceRoot.FullName, "apphost.go"),
            """
            compose := builder.AddDockerComposeEnvironment("compose")
            api := builder.AddContainer("api", "nginx:alpine")
            api.WithComputeEnvironment(compose)
            _ = api.PublishAsDockerComposeService(func(composeService aspire.DockerComposeServiceResource, service aspire.Service) {
                service.SetContainerName(aspire.StringPtr("validation-api"))
            })
            """);
    }
 
    private static void AssertDockerfileContains(string artifactsPath, string resourceName, params string[] expectedFragments)
    {
        var dockerfilePath = Path.Combine(artifactsPath, $"{resourceName}.Dockerfile");
        Assert.True(File.Exists(dockerfilePath), $"Expected Dockerfile for resource '{resourceName}' at {dockerfilePath}");
 
        var content = File.ReadAllText(dockerfilePath);
 
        foreach (var expectedFragment in expectedFragments)
        {
            Assert.Contains(expectedFragment, content);
        }
    }
}