File: GoPublicApiTests.cs
Web Access
Project: src\tests\Aspire.Hosting.Go.Tests\Aspire.Hosting.Go.Tests.csproj (Aspire.Hosting.Go.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
#pragma warning disable ASPIREEXTENSION001
 
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Tests.Utils;
using Aspire.Hosting.Utils;
 
namespace Aspire.Hosting.Go.Tests;
 
public class GoPublicApiTests
{
    // ---- GoAppResource constructor guards ------------------------------------
 
    [Theory]
    [InlineData(true)]
    [InlineData(false)]
    public void CtorGoAppResourceShouldThrowWhenNameIsNullOrEmpty(bool isNull)
    {
        var name = isNull ? null! : string.Empty;
        const string workingDirectory = "/src/go-app";
 
        var action = () => new GoAppResource(name, workingDirectory);
 
        var exception = isNull
            ? Assert.Throws<ArgumentNullException>(action)
            : Assert.Throws<ArgumentException>(action);
        Assert.Equal(nameof(name), exception.ParamName);
    }
 
    [Fact]
    public void CtorGoAppResourceShouldThrowWhenWorkingDirectoryIsNull()
    {
        const string name = "api";
 
        var action = () => new GoAppResource(name, workingDirectory: null!);
 
        var exception = Assert.Throws<ArgumentNullException>(action);
        Assert.Equal("workingDirectory", exception.ParamName);
    }
 
    // ---- AddGoApp guards ----------------------------------------------------
 
    [Fact]
    public void AddGoAppShouldThrowWhenBuilderIsNull()
    {
        IDistributedApplicationBuilder builder = null!;
 
        var action = () => builder.AddGoApp("api", "/src/go-app");
 
        var exception = Assert.Throws<ArgumentNullException>(action);
        Assert.Equal(nameof(builder), exception.ParamName);
    }
 
    [Theory]
    [InlineData(true)]
    [InlineData(false)]
    public void AddGoAppShouldThrowWhenNameIsNullOrEmpty(bool isNull)
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var name = isNull ? null! : string.Empty;
 
        var action = () => builder.AddGoApp(name, "/src/go-app");
 
        var exception = isNull
            ? Assert.Throws<ArgumentNullException>(action)
            : Assert.Throws<ArgumentException>(action);
        Assert.Equal(nameof(name), exception.ParamName);
    }
 
    [Theory]
    [InlineData(true)]
    [InlineData(false)]
    public void AddGoAppShouldThrowWhenAppDirectoryIsNullOrEmpty(bool isNull)
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var appDirectory = isNull ? null! : string.Empty;
 
        var action = () => builder.AddGoApp("api", appDirectory);
 
        var exception = isNull
            ? Assert.Throws<ArgumentNullException>(action)
            : Assert.Throws<ArgumentException>(action);
        Assert.Equal(nameof(appDirectory), exception.ParamName);
    }
 
    // ---- AddGoApp behaviour -------------------------------------------------
 
    [Fact]
    public void AddGoAppUsesGoAsCommand()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
 
        var app = builder.AddGoApp("api", builder.AppHostDirectory);
 
        Assert.Equal("go", app.Resource.Command);
    }
 
    [Fact]
    public async Task AddGoAppDefaultArgsAreRunDot()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var app = builder.AddGoApp("api", builder.AppHostDirectory);
 
        var args = await ArgumentEvaluator.GetArgumentListAsync(app.Resource);
 
        Assert.Equal(["run", "."], args);
    }
 
    // ---- packagePath --------------------------------------------------------
 
    [Fact]
    public async Task AddGoApp_PackagePath_DefaultsToRunDot()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var app = builder.AddGoApp("api", builder.AppHostDirectory);
 
        var args = await ArgumentEvaluator.GetArgumentListAsync(app.Resource);
 
        Assert.Equal(["run", "."], args);
    }
 
    [Fact]
    public async Task AddGoApp_PackagePath_UsedInRunMode()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var app = builder.AddGoApp("api", builder.AppHostDirectory, packagePath: "./cmd/server");
 
        var args = await ArgumentEvaluator.GetArgumentListAsync(app.Resource);
 
        Assert.Equal(["run", "./cmd/server"], args);
    }
 
    [Fact]
    public async Task AddGoApp_PackagePath_UsedInDelveMode()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var app = builder.AddGoApp("api", builder.AppHostDirectory, packagePath: "./cmd/server")
                         .WithDelveServer();
 
        var args = await ArgumentEvaluator.GetArgumentListAsync(app.Resource);
 
        Assert.Equal(["--headless=true", "--listen=127.0.0.1:2345", "--api-version=2", "debug", "./cmd/server"], args);
    }
 
    [Fact]
    public async Task AddGoApp_PackagePath_CombinedWithBuildFlags()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var app = builder.AddGoApp("api", builder.AppHostDirectory,
                         packagePath: "./cmd/server",
                         buildTags: ["netgo"],
                         raceDetector: true);
 
        var args = await ArgumentEvaluator.GetArgumentListAsync(app.Resource);
 
        Assert.Equal(["run", "-race", "-tags=netgo", "./cmd/server"], args);
    }
 
    [Fact]
    public async Task AddGoApp_BuildTagsParam_InjectsTagsFlag()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var app = builder.AddGoApp("api", builder.AppHostDirectory, buildTags: ["netgo", "osusergo"]);
 
        var args = await ArgumentEvaluator.GetArgumentListAsync(app.Resource);
 
        Assert.Equal(["run", "-tags=netgo,osusergo", "."], args);
    }
 
    [Fact]
    public async Task AddGoApp_LdFlagsParam_InjectsLdFlagsArg()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var app = builder.AddGoApp("api", builder.AppHostDirectory, ldFlags: "-X main.version=1.0.0");
 
        var args = await ArgumentEvaluator.GetArgumentListAsync(app.Resource);
 
        Assert.Equal(["run", "-ldflags=-X main.version=1.0.0", "."], args);
    }
 
    [Fact]
    public async Task AddGoApp_GcFlagsParam_InjectsGcFlagsArg()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var app = builder.AddGoApp("api", builder.AppHostDirectory, gcFlags: "all=-N -l");
 
        var args = await ArgumentEvaluator.GetArgumentListAsync(app.Resource);
 
        Assert.Equal(["run", "-gcflags=all=-N -l", "."], args);
    }
 
    [Fact]
    public async Task AddGoApp_RaceDetectorParam_InjectsRaceFlag()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var app = builder.AddGoApp("api", builder.AppHostDirectory, raceDetector: true);
 
        var args = await ArgumentEvaluator.GetArgumentListAsync(app.Resource);
 
        Assert.Equal(["run", "-race", "."], args);
    }
 
    [Fact]
    public async Task AddGoApp_AllBuildParams_ProduceCorrectOrdering()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var app = builder.AddGoApp("api", builder.AppHostDirectory,
            buildTags: ["netgo"],
            ldFlags: "-s -w",
            gcFlags: "all=-N -l",
            raceDetector: true);
 
        var args = await ArgumentEvaluator.GetArgumentListAsync(app.Resource);
 
        // Ordering: run -race -tags=... -ldflags=... -gcflags=... .
        Assert.Equal(["run", "-race", "-tags=netgo", "-ldflags=-s -w", "-gcflags=all=-N -l", "."], args);
    }
 
    // ---- WithAppArgs --------------------------------------------------------
 
    [Fact]
    public void WithAppArgsShouldThrowWhenBuilderIsNull()
    {
        IResourceBuilder<GoAppResource> builder = null!;
 
        var action = () => builder.WithAppArgs("--port", "9090");
 
        var exception = Assert.Throws<ArgumentNullException>(action);
        Assert.Equal(nameof(builder), exception.ParamName);
    }
 
    [Fact]
    public async Task WithAppArgsPassesArgsAfterDot()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var app = builder.AddGoApp("api", builder.AppHostDirectory)
                         .WithAppArgs("--port", "9090");
 
        var args = await ArgumentEvaluator.GetArgumentListAsync(app.Resource);
 
        Assert.Equal(["run", ".", "--port", "9090"], args);
    }
 
    [Fact]
    public void WithAppArgs_AcceptsReferenceExpression()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
 
        // A parameter whose value is a ReferenceExpression — no container needed.
        var param = builder.AddParameter("db-url");
        var app = builder.AddGoApp("api", builder.AppHostDirectory)
                         .WithAppArgs("--db", param.Resource);
 
        // The annotation must store the object[] including the ReferenceExpression — not
        // just strings — so that it is resolved lazily by the args callback at runtime.
        Assert.True(app.Resource.TryGetLastAnnotation<GoAppArgsAnnotation>(out var annotation));
        Assert.Equal(2, annotation!.Args.Length);
        Assert.Equal("--db", annotation.Args[0]);
        Assert.Same(param.Resource, annotation.Args[1]);
    }
 
    [Fact]
    public async Task WithAppArgsReplacesOnSecondCall()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var app = builder.AddGoApp("api", builder.AppHostDirectory)
                         .WithAppArgs("--port", "8080")
                         .WithAppArgs("--port", "9090");
 
        var args = await ArgumentEvaluator.GetArgumentListAsync(app.Resource);
 
        // Last call wins
        Assert.Equal(["run", ".", "--port", "9090"], args);
    }
 
    // ---- WithModTidy --------------------------------------------------------
 
    [Fact]
    public void WithModTidyShouldThrowWhenBuilderIsNull()
    {
        IResourceBuilder<GoAppResource> builder = null!;
 
        var action = () => builder.WithModTidy();
 
        var exception = Assert.Throws<ArgumentNullException>(action);
        Assert.Equal(nameof(builder), exception.ParamName);
    }
 
    [Fact]
    public void WithModTidyIsIdempotent()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var app = builder.AddGoApp("api", builder.AppHostDirectory)
                         .WithModTidy()
                         .WithModTidy();
 
        // Only one tidy sibling should have been created
        var tidyResources = builder.Resources.Where(r => r.Name == "api-mod-tidy").ToList();
        Assert.Single(tidyResources);
    }
 
    [Fact]
    public void WithModTidyCreatesSiblingResource()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        builder.AddGoApp("api", builder.AppHostDirectory).WithModTidy();
 
        Assert.Contains(builder.Resources, r => r.Name == "api-mod-tidy");
    }
 
    // ---- WithModVendor ------------------------------------------------------
 
    [Fact]
    public void WithModVendorShouldThrowWhenBuilderIsNull()
    {
        IResourceBuilder<GoAppResource> builder = null!;
 
        var action = () => builder.WithModVendor();
 
        var exception = Assert.Throws<ArgumentNullException>(action);
        Assert.Equal(nameof(builder), exception.ParamName);
    }
 
    [Fact]
    public void WithModVendorIsIdempotent()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        builder.AddGoApp("api", builder.AppHostDirectory)
               .WithModVendor()
               .WithModVendor();
 
        var vendorResources = builder.Resources.Where(r => r.Name == "api-mod-vendor").ToList();
        Assert.Single(vendorResources);
    }
 
    [Fact]
    public void WithModVendorCreatesSiblingResource()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        builder.AddGoApp("api", builder.AppHostDirectory).WithModVendor();
 
        Assert.Contains(builder.Resources, r => r.Name == "api-mod-vendor");
    }
 
    // ---- WithModDownload ----------------------------------------------------
 
    [Fact]
    public void WithModDownloadShouldThrowWhenBuilderIsNull()
    {
        IResourceBuilder<GoAppResource> builder = null!;
 
        var action = () => builder.WithModDownload();
 
        var exception = Assert.Throws<ArgumentNullException>(action);
        Assert.Equal(nameof(builder), exception.ParamName);
    }
 
    [Fact]
    public void WithModDownloadIsIdempotent()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        builder.AddGoApp("api", builder.AppHostDirectory)
               .WithModDownload()
               .WithModDownload();
 
        var downloadResources = builder.Resources.Where(r => r.Name == "api-mod-download").ToList();
        Assert.Single(downloadResources);
    }
 
    [Fact]
    public void WithModDownloadCreatesSiblingResource()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        builder.AddGoApp("api", builder.AppHostDirectory).WithModDownload();
 
        Assert.Contains(builder.Resources, r => r.Name == "api-mod-download");
    }
 
    // ---- WithVetTool --------------------------------------------------------
 
    [Fact]
    public void WithVetToolShouldThrowWhenBuilderIsNull()
    {
        IResourceBuilder<GoAppResource> builder = null!;
 
        var action = () => builder.WithVetTool();
 
        var exception = Assert.Throws<ArgumentNullException>(action);
        Assert.Equal(nameof(builder), exception.ParamName);
    }
 
    [Fact]
    public void WithVetToolIsIdempotent()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        builder.AddGoApp("api", builder.AppHostDirectory)
               .WithVetTool()
               .WithVetTool();
 
        var vetResources = builder.Resources.Where(r => r.Name == "api-vet-tool").ToList();
        Assert.Single(vetResources);
    }
 
    [Fact]
    public void WithVetToolCreatesSiblingResource()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        builder.AddGoApp("api", builder.AppHostDirectory).WithVetTool();
 
        Assert.Contains(builder.Resources, r => r.Name == "api-vet-tool");
    }
 
    // ---- WithDelveServer ----------------------------------------------------
 
    [Fact]
    public void WithDelveServerShouldThrowWhenBuilderIsNull()
    {
        IResourceBuilder<GoAppResource> builder = null!;
 
        var action = () => builder.WithDelveServer();
 
        var exception = Assert.Throws<ArgumentNullException>(action);
        Assert.Equal(nameof(builder), exception.ParamName);
    }
 
    [Fact]
    public async Task WithDelveServerNullOptionsUseDefaults()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var app = builder.AddGoApp("api", builder.AppHostDirectory)
            .WithDelveServer(options: null);
 
        var args = await ArgumentEvaluator.GetArgumentListAsync(app.Resource);
 
        Assert.Equal(["--headless=true", "--listen=127.0.0.1:2345", "--api-version=2", "debug", "."], args);
    }
 
    [Fact]
    public void DelveServerOptionsHaveSafeDefaults()
    {
        var options = new DelveServerOptions();
 
        Assert.Equal(
            (Port: 2345, AcceptMultiClient: false, OnlySameUser: (bool?)null, ContinueOnStart: false, Log: false, LogOutput: (string?)null),
            (options.Port, options.AcceptMultiClient, options.OnlySameUser, options.ContinueOnStart, options.Log, options.LogOutput));
    }
 
    [Fact]
    public void WithDelveServerSwitchesCommandToDlv()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var app = builder.AddGoApp("api", builder.AppHostDirectory)
                         .WithDelveServer();
 
        Assert.Equal("dlv", app.Resource.Command);
    }
 
    [Fact]
    public async Task WithDelveServerProducesCorrectArgs()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var app = builder.AddGoApp("api", builder.AppHostDirectory)
                         .WithDelveServer();
 
        var args = await ArgumentEvaluator.GetArgumentListAsync(app.Resource);
 
        Assert.Equal(["--headless=true", "--listen=127.0.0.1:2345", "--api-version=2", "debug", "."], args);
    }
 
    [Fact]
    public async Task ObsoleteWithDelveServerPortOverloadPreservesBehavior()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
#pragma warning disable CS0618 // The obsolete overload must remain callable for source and binary compatibility.
        var app = builder.AddGoApp("api", builder.AppHostDirectory)
            .WithDelveServer(port: 3456);
#pragma warning restore CS0618
 
        var args = await ArgumentEvaluator.GetArgumentListAsync(app.Resource);
 
        Assert.Equal(["--headless=true", "--listen=127.0.0.1:3456", "--api-version=2", "debug", "."], args);
    }
 
    [Fact]
    public async Task WithDelveServerOptionsProduceCorrectArgs()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var app = builder.AddGoApp("api", builder.AppHostDirectory)
            .WithDelveServer(new DelveServerOptions
            {
                Port = 3456,
                AcceptMultiClient = true,
                OnlySameUser = false,
                ContinueOnStart = true,
                Log = true,
                LogOutput = "rpc,dap,debugger"
            });
 
        var args = await ArgumentEvaluator.GetArgumentListAsync(app.Resource);
 
        Assert.Equal(
            [
                "--headless=true",
                "--listen=127.0.0.1:3456",
                "--api-version=2",
                "--accept-multiclient",
                "--only-same-user=false",
                "--log",
                "--log-output=rpc,dap,debugger",
                "debug",
                "--continue",
                "."
            ],
            args);
    }
 
    [Fact]
    public async Task WithDelveServerIncludesBuildFlagsWhenPresent()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var app = builder.AddGoApp("api", builder.AppHostDirectory,
                            buildTags: ["netgo"],
                            ldFlags: "-s -w")
                         .WithDelveServer();
 
        var args = await ArgumentEvaluator.GetArgumentListAsync(app.Resource);
 
        // All user-influenced values are shell-quoted so the Delve parser keeps them as single tokens.
        Assert.Equal(["--headless=true", "--listen=127.0.0.1:2345", "--api-version=2", "debug", "--build-flags=-tags='netgo' -ldflags='-s -w'", "."], args);
    }
 
    [Fact]
    public async Task WithDelveServerPassesAppArgsAfterDoubleDash()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var app = builder.AddGoApp("api", builder.AppHostDirectory)
                         .WithAppArgs("--config", "dev.yaml")
                         .WithDelveServer();
 
        var args = await ArgumentEvaluator.GetArgumentListAsync(app.Resource);
 
        Assert.Equal(["--headless=true", "--listen=127.0.0.1:2345", "--api-version=2", "debug", ".", "--", "--config", "dev.yaml"], args);
    }
}