File: WorkloadDefinitionTests.cs
Web Access
Project: src\tests\Aspire.Hosting.Native.Core.Tests\Aspire.Hosting.Native.Core.Tests.csproj (Aspire.Hosting.Native.Core.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Collections.Immutable;
using System.Text.Json.Nodes;
using Aspire.Hosting.Native.Core.Tests.TestServices;
using Aspire.Hosting.Native.Runtime;
using static Aspire.Hosting.Native.Core.Tests.TestServices.RpcTestClient;
 
namespace Aspire.Hosting.Native.Core.Tests;
 
public class WorkloadDefinitionTests
{
    [Fact]
    public async Task GenericContainerRpcPreservesAllEndpointsEntrypointAndMounts()
    {
        var executor = new WorkloadTestExecutor();
        executor.ReleaseCleanup();
        using var context = new RuntimeRpcTestContext(executor);
        var allocation = Result(await context.Integration.InvokeAsync("startContainerWorkload", Arguments(
            ("context", context.Writer), ("launch", Launch()))));
        var plan = Assert.IsType<ContainerWorkload>(Assert.Single(executor.Plans));
 
        Assert.Equal("/bin/worker", plan.Command);
        Assert.Equal([new WorkloadMount(WorkloadMountKind.Volume, "retained-data", "/data", false)], plan.Mounts);
        Assert.Equal([new WorkloadPort("http", WorkloadProtocol.Tcp, "http", 8080, 0, ""),
            new WorkloadPort("management", WorkloadProtocol.Tcp, "https", 8443, 0, "")], plan.Endpoints);
        await Verify(allocation.ToJsonString(), "json").UseDirectory("Snapshots");
        Assert.Equal(["http://127.0.0.1:5050", "https://127.0.0.1:5051"],
            context.Read()["resources"]![0]!["urls"]!.AsArray().Select(url => url!.GetValue<string>()));
        await context.Author.InvokeAsync("retireGeneration", Arguments(
            ("context", context.Session), ("composition", context.Composition)));
        Assert.Single(executor.Removed);
    }
 
    [Theory]
    [InlineData("duplicate-name")]
    [InlineData("duplicate-port")]
    [InlineData("bad-port")]
    [InlineData("bad-protocol")]
    [InlineData("bad-scheme")]
    [InlineData("container-port-variable")]
    [InlineData("duplicate-mount")]
    [InlineData("relative-bind")]
    [InlineData("argument-null")]
    public async Task InvalidLaunchFailsBeforeExecutingOrChangingResourceState(string invalid)
    {
        var executor = new WorkloadTestExecutor();
        executor.ReleaseCleanup();
        using var context = new RuntimeRpcTestContext(executor);
        var launch = Launch();
        var endpoints = launch["endpoints"]!.AsArray();
        switch (invalid)
        {
            case "duplicate-name": endpoints[1]!["name"] = "HTTP"; break;
            case "duplicate-port": endpoints[1]!["targetPort"] = 8080; break;
            case "bad-port": endpoints[0]!["targetPort"] = 65536; break;
            case "bad-protocol": endpoints[0]!["protocol"] = "SCTP"; break;
            case "bad-scheme": endpoints[0]!["uriScheme"] = "invalid://"; break;
            case "container-port-variable": endpoints[0]!["environmentVariable"] = "PORT"; break;
            case "duplicate-mount": launch["mounts"]!.AsArray().Add(launch["mounts"]![0]!.DeepClone()); break;
            case "relative-bind":
                launch["mounts"]![0]!["type"] = "bind";
                launch["mounts"]![0]!["source"] = "relative-path";
                break;
            case "argument-null": launch["arguments"]!.AsArray().Add(null); break;
        }
        var response = await context.Integration.InvokeAsync("startContainerWorkload", Arguments(
            ("context", context.Writer), ("launch", launch)));
 
        Assert.Equal("INVALID_ARGUMENT", response["result"]!["$error"]!["code"]!.GetValue<string>());
        Assert.Empty(executor.Started);
        Assert.Equal("Unknown", context.Read()["resources"]![0]!["state"]!.GetValue<string>());
    }
 
    [Fact]
    public void ExecutablePortVariablesAreDistinctFromEnvironmentAndEachOther()
    {
        var executable = Environment.ProcessPath!;
        var directory = Path.GetDirectoryName(executable)!;
        var plan = new ExecutableWorkload(executable, directory, [], [],
            [new("http", WorkloadProtocol.Tcp, "http", 0, 0, "HTTP_PORT"), new("metrics", WorkloadProtocol.Tcp, "http", 0, 0, "METRICS_PORT")]);
        WorkloadDefinition.Validate(plan);
 
        Assert.Throws<ArgumentException>(() => WorkloadDefinition.Validate(plan with
        {
            Ports = [new("http", WorkloadProtocol.Tcp, "http", 0, 0, "PORT"), new("metrics", WorkloadProtocol.Tcp, "http", 0, 0, "PORT")]
        }));
        Assert.Throws<ArgumentException>(() => WorkloadDefinition.Validate(new ExecutableWorkload(executable, directory,
            [new("HTTP_PORT", "configured")], [], plan.Endpoints)));
    }
 
    [Fact]
    public void EndpointCapacityAndPortLimitsAreExact()
    {
        var endpoints = Enumerable.Range(0, 32)
            .Select(index => new WorkloadPort($"port-{index}", WorkloadProtocol.Tcp, "tcp", index + 1, 0, "")).ToImmutableArray();
        WorkloadDefinition.Validate(new ContainerWorkload("example:1", null, [], [], [], endpoints));
        WorkloadDefinition.Validate(new ContainerWorkload("example:1", null, [], [], [], [new("tcp", WorkloadProtocol.Tcp, "tcp", 65535, 65535, "")]));
 
        Assert.Throws<ArgumentException>(() => WorkloadDefinition.Validate(
            new ContainerWorkload("example:1", null, [], [], [], endpoints.Add(new("overflow", WorkloadProtocol.Tcp, "tcp", 33, 0, "")))));
        Assert.Throws<ArgumentException>(() => WorkloadDefinition.Validate(
            new ContainerWorkload("example:1", null, [], [], [], [new("tcp", WorkloadProtocol.Tcp, "tcp", 65535, 65536, "")])));
    }
 
    private static JsonObject Launch() => new()
    {
        ["image"] = "example/service:1", ["entrypoint"] = "/bin/worker",
        ["environment"] = new JsonArray(), ["arguments"] = new JsonArray("--serve"),
        ["mounts"] = new JsonArray(new JsonObject
        {
            ["type"] = "volume", ["source"] = "retained-data", ["target"] = "/data", ["readOnly"] = false
        }),
        ["endpoints"] = new JsonArray(
            Endpoint("http", "http", 8080), Endpoint("management", "https", 8443))
    };
 
    private static JsonObject Endpoint(string name, string scheme, int port) => new()
    {
        ["name"] = name, ["protocol"] = "TCP", ["uriScheme"] = scheme, ["targetPort"] = port,
        ["hostPort"] = 0, ["environmentVariable"] = ""
    };
}