File: AtsAnnotationTests.cs
Web Access
Project: src\tests\Aspire.Hosting.Tests\Aspire.Hosting.Tests.csproj (Aspire.Hosting.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.Text.Json;
using Aspire.Hosting.Utils;
 
namespace Aspire.Hosting.Tests;
 
public class AtsAnnotationTests
{
    private static readonly AnnotationDefinition<DenoStateDto> s_stateAnnotation = new("spike.deno/state");
 
    [Fact]
    public void WithSerializedAnnotation_RoundTripsValue()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var container = builder.AddContainer("container", "image")
            .WithSerializedAnnotation("my.id", "{\"value\":1}");
 
        Assert.Equal("{\"value\":1}", container.Resource.GetSerializedAnnotation("my.id"));
        Assert.Equal("{\"value\":1}", container.GetSerializedAnnotation("my.id"));
    }
 
    [Fact]
    public void WithSerializedAnnotation_ReplacesExistingValueWithSameId()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var container = builder.AddContainer("container", "image")
            .WithSerializedAnnotation("my.id", "first")
            .WithSerializedAnnotation("my.id", "second");
 
        Assert.Single(container.Resource.Annotations.OfType<AtsAnnotation>());
        Assert.Equal("second", container.Resource.GetSerializedAnnotation("my.id"));
    }
 
    [Fact]
    public void GetSerializedAnnotation_ThrowsWhenAbsent()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var container = builder.AddContainer("container", "image");
 
        Assert.Throws<InvalidOperationException>(() => container.Resource.GetSerializedAnnotation("missing"));
    }
 
    [Fact]
    public void HasSerializedAnnotation_ReflectsPresence()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var container = builder.AddContainer("container", "image");
 
        Assert.False(container.HasSerializedAnnotation("my.id"));
        Assert.False(container.Resource.HasSerializedAnnotation("my.id"));
 
        container.WithSerializedAnnotation("my.id", "value");
 
        Assert.True(container.HasSerializedAnnotation("my.id"));
        Assert.True(container.Resource.HasSerializedAnnotation("my.id"));
    }
 
    [Fact]
    public void WithAnnotation_TypedRoundTrip()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var state = new DenoStateDto { ScriptPath = "main.ts", Permissions = ["--allow-net"], Mode = DenoMode.Watch };
 
        var container = builder.AddContainer("container", "image")
            .WithAnnotation(s_stateAnnotation, state);
 
        var roundTripped = container.Resource.GetAnnotation(s_stateAnnotation);
 
        Assert.Equal("main.ts", roundTripped.ScriptPath);
        Assert.Equal(["--allow-net"], roundTripped.Permissions);
        Assert.Equal(DenoMode.Watch, roundTripped.Mode);
    }
 
    [Fact]
    public void WithAnnotation_SerializesUsingCrossLanguageJsonShape()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var state = new DenoStateDto { ScriptPath = "main.ts", Permissions = ["--allow-net"], Mode = DenoMode.Watch };
 
        var container = builder.AddContainer("container", "image")
            .WithAnnotation(s_stateAnnotation, state);
 
        // The stored JSON is what a TypeScript integration sees via JSON.parse, so it must use
        // camelCase property names and serialize enums as strings (matching JSON.stringify of the
        // generated TS interface). This guards the contract that a value written from C# is readable
        // from TypeScript and vice-versa.
        var json = container.Resource.GetSerializedAnnotation(s_stateAnnotation.Id);
        using var document = JsonDocument.Parse(json);
        var root = document.RootElement;
 
        Assert.Equal("main.ts", root.GetProperty("scriptPath").GetString());
        Assert.Equal("--allow-net", root.GetProperty("permissions")[0].GetString());
        Assert.Equal("Watch", root.GetProperty("mode").GetString());
    }
 
    [Fact]
    public async Task WithAnnotation_SerializesTimeSpanAsMilliseconds()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var definition = new AnnotationDefinition<Dictionary<string, TimeSpan?>>("test/durations");
        var durations = new Dictionary<string, TimeSpan?>
        {
            ["zero"] = TimeSpan.Zero,
            ["fractional"] = TimeSpan.FromMilliseconds(1250.5),
            ["negative"] = TimeSpan.FromMilliseconds(-1),
            ["optional"] = null
        };
        var container = builder.AddContainer("container", "image").WithAnnotation(definition, durations);
 
        await Verify(container.GetSerializedAnnotation(definition.Id), "json");
        Assert.Equal(durations, container.GetAnnotation(definition));
    }
 
    [Fact]
    public void GetAnnotation_DeserializesMillisecondsFromTypeScript()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var definition = new AnnotationDefinition<Dictionary<string, TimeSpan?>>("test/durations");
        var container = builder.AddContainer("container", "image")
            .WithSerializedAnnotation(definition.Id, """{"delay":1250.5,"optional":null}""");
 
        var durations = container.GetAnnotation(definition);
        Assert.Equal(TimeSpan.FromMilliseconds(1250.5), durations["delay"]);
        Assert.Null(durations["optional"]);
        Assert.True(container.Resource.TryGetAnnotation(definition, out var readByTryGet));
        Assert.Equal(durations, readByTryGet);
    }
 
    [Fact]
    public void TryGetAnnotation_ReturnsFalseWhenAbsentAndTrueWhenPresent()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var container = builder.AddContainer("container", "image");
 
        Assert.False(container.Resource.TryGetAnnotation(s_stateAnnotation, out var missing));
        Assert.Null(missing);
 
        container.WithAnnotation(s_stateAnnotation, new DenoStateDto { ScriptPath = "main.ts" });
 
        Assert.True(container.Resource.TryGetAnnotation(s_stateAnnotation, out var present));
        Assert.Equal("main.ts", present.ScriptPath);
    }
 
    private sealed class DenoStateDto
    {
        public string ScriptPath { get; set; } = "";
        public string[] Permissions { get; set; } = [];
        public DenoMode Mode { get; set; }
    }
 
    private enum DenoMode
    {
        Run,
        Watch
    }
}