File: AtsMarshallerTests.cs
Web Access
Project: src\tests\Aspire.Hosting.RemoteHost.Tests\Aspire.Hosting.RemoteHost.Tests.csproj (Aspire.Hosting.RemoteHost.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.Nodes;
using Aspire.Hosting.Ats;
using Aspire.Hosting.RemoteHost.Ats;
using Xunit;
 
namespace Aspire.Hosting.RemoteHost.Tests;
 
public class AtsMarshallerTests
{
    private static AtsContext CreateTestContext()
    {
        return new AtsContext
        {
            Capabilities = [],
            HandleTypes = [],
            DtoTypes = [new AtsDtoTypeInfo { TypeId = "test/TestDto", Name = "TestDto", ClrType = typeof(TestDto), Properties = [] }],
            EnumTypes = []
        };
    }
 
    private static AtsMarshaller CreateTestMarshaller(HandleRegistry? handles = null, CancellationTokenRegistry? ctRegistry = null)
    {
        handles ??= new HandleRegistry();
        ctRegistry ??= new CancellationTokenRegistry();
        var context = CreateTestContext();
        return new AtsMarshaller(handles, context, ctRegistry, new Lazy<AtsCallbackProxyFactory>(() => throw new NotImplementedException()));
    }
 
    private static AtsMarshaller CreateMarshaller(HandleRegistry? registry = null)
    {
        return CreateTestMarshaller(registry);
    }
 
    [Theory]
    [InlineData(typeof(string))]
    [InlineData(typeof(bool))]
    [InlineData(typeof(int))]
    [InlineData(typeof(long))]
    [InlineData(typeof(double))]
    [InlineData(typeof(float))]
    [InlineData(typeof(decimal))]
    [InlineData(typeof(DateTime))]
    [InlineData(typeof(Guid))]
    public void IsSimpleType_ReturnsTrueForPrimitives(Type type)
    {
        Assert.True(AtsMarshaller.IsSimpleType(type));
    }
 
    [Fact]
    public void IsSimpleType_ReturnsTrueForNullablePrimitives()
    {
        Assert.True(AtsMarshaller.IsSimpleType(typeof(int?)));
        Assert.True(AtsMarshaller.IsSimpleType(typeof(bool?)));
        Assert.True(AtsMarshaller.IsSimpleType(typeof(DateTime?)));
    }
 
    [Fact]
    public void IsSimpleType_ReturnsTrueForEnums()
    {
        Assert.True(AtsMarshaller.IsSimpleType(typeof(TestEnum)));
    }
 
    [Fact]
    public void IsSimpleType_ReturnsFalseForComplexTypes()
    {
        Assert.False(AtsMarshaller.IsSimpleType(typeof(object)));
        Assert.False(AtsMarshaller.IsSimpleType(typeof(List<int>)));
        Assert.False(AtsMarshaller.IsSimpleType(typeof(TestClass)));
    }
 
    [Fact]
    public void MarshalToJson_ReturnsNullForNull()
    {
        var marshaller = CreateMarshaller();
 
        var result = marshaller.MarshalToJson(null);
 
        Assert.Null(result);
    }
 
    [Fact]
    public void MarshalToJson_MarshalsStringDirectly()
    {
        var marshaller = CreateMarshaller();
 
        var result = marshaller.MarshalToJson("hello");
 
        Assert.NotNull(result);
        Assert.Equal("hello", result.GetValue<string>());
    }
 
    [Fact]
    public void MarshalToJson_MarshalsIntDirectly()
    {
        var marshaller = CreateMarshaller();
 
        var result = marshaller.MarshalToJson(42);
 
        Assert.NotNull(result);
        Assert.Equal(42, result.GetValue<int>());
    }
 
    [Fact]
    public void MarshalToJson_MarshalsBoolDirectly()
    {
        var marshaller = CreateMarshaller();
 
        var result = marshaller.MarshalToJson(true);
 
        Assert.NotNull(result);
        Assert.True(result.GetValue<bool>());
    }
 
    [Fact]
    public void MarshalToJson_MarshalsEnumAsString()
    {
        var marshaller = CreateMarshaller();
 
        var result = marshaller.MarshalToJson(TestEnum.ValueB);
 
        Assert.NotNull(result);
        Assert.Equal("ValueB", result.GetValue<string>());
    }
 
    [Fact]
    public void MarshalToJson_MarshalsTimeSpanAsMilliseconds()
    {
        var marshaller = CreateMarshaller();
 
        var result = marshaller.MarshalToJson(TimeSpan.FromSeconds(1.5));
 
        Assert.NotNull(result);
        Assert.Equal(1500.0, result.GetValue<double>());
    }
 
    [Fact]
    public void MarshalToJson_MarshalsArrayRecursively()
    {
        var marshaller = CreateMarshaller();
        var array = new[] { 1, 2, 3 };
 
        var result = marshaller.MarshalToJson(array);
 
        Assert.NotNull(result);
        Assert.IsType<JsonArray>(result);
        var jsonArray = (JsonArray)result;
        Assert.Equal(3, jsonArray.Count);
        Assert.Equal(1, jsonArray[0]!.GetValue<int>());
        Assert.Equal(2, jsonArray[1]!.GetValue<int>());
        Assert.Equal(3, jsonArray[2]!.GetValue<int>());
    }
 
    [Fact]
    public void ConvertPrimitive_ConvertsStringCorrectly()
    {
        var value = JsonValue.Create("test");
 
        var result = AtsMarshaller.ConvertPrimitive(value!, typeof(string));
 
        Assert.Equal("test", result);
    }
 
    [Fact]
    public void ConvertPrimitive_ConvertsIntCorrectly()
    {
        var value = JsonValue.Create(42);
 
        var result = AtsMarshaller.ConvertPrimitive(value!, typeof(int));
 
        Assert.Equal(42, result);
    }
 
    [Fact]
    public void ConvertPrimitive_ConvertsBoolCorrectly()
    {
        var value = JsonValue.Create(true);
 
        var result = AtsMarshaller.ConvertPrimitive(value!, typeof(bool));
 
        Assert.Equal(true, result);
    }
 
    [Fact]
    public void ConvertPrimitive_ConvertsTimeSpanFromMilliseconds()
    {
        var value = JsonValue.Create(1500.0);
 
        var result = AtsMarshaller.ConvertPrimitive(value!, typeof(TimeSpan));
 
        Assert.Equal(TimeSpan.FromMilliseconds(1500), result);
    }
 
    [Fact]
    public void ConvertPrimitive_ConvertsNullableInt()
    {
        var value = JsonValue.Create(42);
 
        var result = AtsMarshaller.ConvertPrimitive(value!, typeof(int?));
 
        Assert.Equal(42, result);
    }
 
    [Fact]
    public void UnmarshalFromJson_ReturnsNullForNullNode()
    {
        var (marshaller, context) = CreateMarshallerWithContext();
 
        var result = marshaller.UnmarshalFromJson(null, typeof(string), context);
 
        Assert.Null(result);
    }
 
    [Fact]
    public void UnmarshalFromJson_UnmarshalsHandleReference()
    {
        var registry = new HandleRegistry();
        var obj = new TestClass { Value = 42 };
        var handleId = registry.Register(obj, "aspire/Test");
        var json = new JsonObject { ["$handle"] = handleId };
        var (marshaller, context) = CreateMarshallerWithContext(registry);
 
        var result = marshaller.UnmarshalFromJson(json, typeof(TestClass), context);
 
        Assert.Same(obj, result);
    }
 
    [Fact]
    public void UnmarshalFromJson_ThrowsForUnknownHandle()
    {
        var (marshaller, context) = CreateMarshallerWithContext();
        var json = new JsonObject { ["$handle"] = "aspire/Unknown:999" };
 
        Assert.Throws<CapabilityException>(() =>
            marshaller.UnmarshalFromJson(json, typeof(object), context));
    }
 
    [Fact]
    public void UnmarshalFromJson_UnmarshalsArray()
    {
        var (marshaller, context) = CreateMarshallerWithContext();
        var json = new JsonArray { 1, 2, 3 };
 
        var result = marshaller.UnmarshalFromJson(json, typeof(int[]), context);
 
        Assert.NotNull(result);
        var array = Assert.IsType<int[]>(result);
        Assert.Equal(new[] { 1, 2, 3 }, array);
    }
 
    [Fact]
    public void UnmarshalFromJson_UnmarshalsList()
    {
        var (marshaller, context) = CreateMarshallerWithContext();
        var json = new JsonArray { "a", "b", "c" };
 
        var result = marshaller.UnmarshalFromJson(json, typeof(List<string>), context);
 
        Assert.NotNull(result);
        var list = Assert.IsType<List<string>>(result);
        Assert.Equal(["a", "b", "c"], list);
    }
 
    [Fact]
    public void UnmarshalFromJson_UnmarshalsDictionary()
    {
        var (marshaller, context) = CreateMarshallerWithContext();
        var json = new JsonObject { ["key1"] = 1, ["key2"] = 2 };
 
        var result = marshaller.UnmarshalFromJson(json, typeof(Dictionary<string, int>), context);
 
        Assert.NotNull(result);
        var dict = Assert.IsType<Dictionary<string, int>>(result);
        Assert.Equal(1, dict["key1"]);
        Assert.Equal(2, dict["key2"]);
    }
 
    // Additional primitive type tests
    [Theory]
    [InlineData(typeof(byte))]
    [InlineData(typeof(short))]
    [InlineData(typeof(uint))]
    [InlineData(typeof(ulong))]
    [InlineData(typeof(ushort))]
    [InlineData(typeof(sbyte))]
    [InlineData(typeof(char))]
    [InlineData(typeof(DateTimeOffset))]
    [InlineData(typeof(DateOnly))]
    [InlineData(typeof(TimeOnly))]
    [InlineData(typeof(TimeSpan))]
    [InlineData(typeof(Uri))]
    public void IsSimpleType_ReturnsTrueForAdditionalPrimitives(Type type)
    {
        Assert.True(AtsMarshaller.IsSimpleType(type));
    }
 
    // DateOnly marshalling tests
    [Fact]
    public void MarshalToJson_MarshalsDateOnlyAsIsoString()
    {
        var marshaller = CreateMarshaller();
        var dateOnly = new DateOnly(2024, 6, 15);
 
        var result = marshaller.MarshalToJson(dateOnly);
 
        Assert.NotNull(result);
        Assert.Equal("2024-06-15", result.GetValue<string>());
    }
 
    [Fact]
    public void ConvertPrimitive_ConvertsDateOnlyFromString()
    {
        var value = JsonValue.Create("2024-06-15");
 
        var result = AtsMarshaller.ConvertPrimitive(value!, typeof(DateOnly));
 
        Assert.Equal(new DateOnly(2024, 6, 15), result);
    }
 
    // TimeOnly marshalling tests
    [Fact]
    public void MarshalToJson_MarshalsTimeOnlyAsIsoString()
    {
        var marshaller = CreateMarshaller();
        var timeOnly = new TimeOnly(14, 30, 45);
 
        var result = marshaller.MarshalToJson(timeOnly);
 
        Assert.NotNull(result);
        Assert.Contains("14:30:45", result.GetValue<string>());
    }
 
    [Fact]
    public void ConvertPrimitive_ConvertsTimeOnlyFromString()
    {
        var value = JsonValue.Create("14:30:45");
 
        var result = AtsMarshaller.ConvertPrimitive(value!, typeof(TimeOnly));
 
        Assert.Equal(new TimeOnly(14, 30, 45), result);
    }
 
    [Fact]
    public void MarshalToJson_MarshalsLong()
    {
        var marshaller = CreateMarshaller();
 
        var result = marshaller.MarshalToJson(9223372036854775807L);
 
        Assert.NotNull(result);
        Assert.Equal(9223372036854775807L, result.GetValue<long>());
    }
 
    [Fact]
    public void MarshalToJson_MarshalsDouble()
    {
        var marshaller = CreateMarshaller();
 
        var result = marshaller.MarshalToJson(3.14159);
 
        Assert.NotNull(result);
        Assert.Equal(3.14159, result.GetValue<double>());
    }
 
    [Fact]
    public void MarshalToJson_MarshalsGuid()
    {
        var marshaller = CreateMarshaller();
        var guid = Guid.NewGuid();
 
        var result = marshaller.MarshalToJson(guid);
 
        Assert.NotNull(result);
    }
 
    [Fact]
    public void MarshalToJson_MarshalsListAsHandle()
    {
        var registry = new HandleRegistry();
        var marshaller = CreateMarshaller(registry);
        var list = new List<int> { 1, 2, 3 };
 
        var result = marshaller.MarshalToJson(list);
 
        Assert.NotNull(result);
        Assert.IsType<JsonObject>(result);
        var jsonObj = (JsonObject)result;
        Assert.NotNull(jsonObj["$handle"]);
        Assert.NotNull(jsonObj["$type"]);
        // Type ID is derived from assembly and type name
        var typeId = jsonObj["$type"]!.GetValue<string>();
        Assert.Contains("List", typeId);
    }
 
    [Fact]
    public void MarshalToJson_MarshalsDictionaryAsHandle()
    {
        var registry = new HandleRegistry();
        var marshaller = CreateMarshaller(registry);
        var dict = new Dictionary<string, int> { ["a"] = 1 };
 
        var result = marshaller.MarshalToJson(dict);
 
        Assert.NotNull(result);
        Assert.IsType<JsonObject>(result);
        var jsonObj = (JsonObject)result;
        Assert.NotNull(jsonObj["$handle"]);
        Assert.NotNull(jsonObj["$type"]);
        // Type ID uses special format for dictionary handles: Dict<K,V>
        var typeId = jsonObj["$type"]!.GetValue<string>();
        Assert.Contains("Dict<", typeId);
    }
 
    [Fact]
    public void MarshalToJson_MarshalsComplexObjectAsHandle()
    {
        var registry = new HandleRegistry();
        var marshaller = CreateMarshaller(registry);
        var obj = new TestClass { Value = 42 };
 
        var result = marshaller.MarshalToJson(obj);
 
        Assert.NotNull(result);
        Assert.IsType<JsonObject>(result);
        var jsonObj = (JsonObject)result;
        Assert.NotNull(jsonObj["$handle"]);
        Assert.NotNull(jsonObj["$type"]);
    }
 
    [Fact]
    public void ConvertPrimitive_ConvertsLong()
    {
        var value = JsonValue.Create(9223372036854775807L);
 
        var result = AtsMarshaller.ConvertPrimitive(value!, typeof(long));
 
        Assert.Equal(9223372036854775807L, result);
    }
 
    [Fact]
    public void ConvertPrimitive_ConvertsDouble()
    {
        var value = JsonValue.Create(3.14159);
 
        var result = AtsMarshaller.ConvertPrimitive(value!, typeof(double));
 
        Assert.Equal(3.14159, result);
    }
 
    [Fact]
    public void ConvertPrimitive_ConvertsFloat()
    {
        var value = JsonValue.Create(3.14);
 
        var result = AtsMarshaller.ConvertPrimitive(value!, typeof(float));
 
        Assert.Equal(3.14f, (float)result!, 2);
    }
 
    [Fact]
    public void ConvertPrimitive_ConvertsDecimal()
    {
        var value = JsonValue.Create(123.456m);
 
        var result = AtsMarshaller.ConvertPrimitive(value!, typeof(decimal));
 
        Assert.Equal(123.456m, result);
    }
 
    [Fact]
    public void ConvertPrimitive_ConvertsTimeSpanFromString()
    {
        var value = JsonValue.Create("01:30:00");
 
        var result = AtsMarshaller.ConvertPrimitive(value!, typeof(TimeSpan));
 
        Assert.Equal(TimeSpan.FromHours(1.5), result);
    }
 
    [Fact]
    public void UnmarshalFromJson_UnmarshalsIList()
    {
        var (marshaller, context) = CreateMarshallerWithContext();
        var json = new JsonArray { 1, 2, 3 };
 
        var result = marshaller.UnmarshalFromJson(json, typeof(IList<int>), context);
 
        Assert.NotNull(result);
        var list = Assert.IsAssignableFrom<IList<int>>(result);
        Assert.Equal(3, list.Count);
    }
 
    [Fact]
    public void UnmarshalFromJson_UnmarshalsIEnumerable()
    {
        var (marshaller, context) = CreateMarshallerWithContext();
        var json = new JsonArray { "x", "y" };
 
        var result = marshaller.UnmarshalFromJson(json, typeof(IEnumerable<string>), context);
 
        Assert.NotNull(result);
        var enumerable = Assert.IsAssignableFrom<IEnumerable<string>>(result);
        Assert.Equal(2, enumerable.Count());
    }
 
    [Fact]
    public void UnmarshalFromJson_UnmarshalsIDictionary()
    {
        var (marshaller, context) = CreateMarshallerWithContext();
        var json = new JsonObject { ["a"] = 1, ["b"] = 2 };
 
        var result = marshaller.UnmarshalFromJson(json, typeof(IDictionary<string, int>), context);
 
        Assert.NotNull(result);
        var dict = Assert.IsAssignableFrom<IDictionary<string, int>>(result);
        Assert.Equal(2, dict.Count);
    }
 
    [Fact]
    public void UnmarshalFromJson_ThrowsForNonDtoObject()
    {
        var (marshaller, context) = CreateMarshallerWithContext();
        var json = new JsonObject { ["value"] = 42 };
 
        // TestClass doesn't have [AspireDto] attribute
        var ex = Assert.Throws<CapabilityException>(() =>
            marshaller.UnmarshalFromJson(json, typeof(TestClass), context));
 
        Assert.Contains("AspireDto", ex.Message);
    }
 
    [Fact]
    public void UnmarshalFromJson_UnmarshalsDto()
    {
        var (marshaller, context) = CreateMarshallerWithContext();
        var json = new JsonObject { ["name"] = "test", ["count"] = 5 };
 
        var result = marshaller.UnmarshalFromJson(json, typeof(TestDto), context);
 
        Assert.NotNull(result);
        var dto = Assert.IsType<TestDto>(result);
        Assert.Equal("test", dto.Name);
        Assert.Equal(5, dto.Count);
    }
 
    [Fact]
    public void MarshalToJson_MarshalsDto()
    {
        var marshaller = CreateMarshaller();
        var dto = new TestDto { Name = "test", Count = 10 };
 
        var result = marshaller.MarshalToJson(dto);
 
        Assert.NotNull(result);
        Assert.IsType<JsonObject>(result);
        var jsonObj = (JsonObject)result;
        Assert.Equal("test", jsonObj["name"]?.GetValue<string>());
        Assert.Equal(10, jsonObj["count"]?.GetValue<int>());
    }
 
    [Fact]
    public void UnmarshalFromJson_UnmarshalsEnumFromString()
    {
        var (marshaller, context) = CreateMarshallerWithContext();
        var json = JsonValue.Create("ValueB");
 
        var result = marshaller.UnmarshalFromJson(json, typeof(TestEnum), context);
 
        Assert.Equal(TestEnum.ValueB, result);
    }
 
    [Fact]
    public void UnmarshalFromJson_UnmarshalsEnumFromStringCaseInsensitive()
    {
        var (marshaller, context) = CreateMarshallerWithContext();
        var json = JsonValue.Create("valueb"); // lowercase
 
        var result = marshaller.UnmarshalFromJson(json, typeof(TestEnum), context);
 
        Assert.Equal(TestEnum.ValueB, result);
    }
 
    [Fact]
    public void UnmarshalFromJson_UnmarshalsEnumFromNumericValue()
    {
        var (marshaller, context) = CreateMarshallerWithContext();
        var json = JsonValue.Create(1); // ValueB is index 1
 
        var result = marshaller.UnmarshalFromJson(json, typeof(TestEnum), context);
 
        Assert.Equal(TestEnum.ValueB, result);
    }
 
    [Fact]
    public void UnmarshalFromJson_UnmarshalsNullableEnumFromString()
    {
        var (marshaller, context) = CreateMarshallerWithContext();
        var json = JsonValue.Create("ValueA");
 
        var result = marshaller.UnmarshalFromJson(json, typeof(TestEnum?), context);
 
        Assert.Equal(TestEnum.ValueA, result);
    }
 
    [Fact]
    public void UnmarshalFromJson_UnmarshalsNullableEnumFromNull()
    {
        var (marshaller, context) = CreateMarshallerWithContext();
 
        var result = marshaller.UnmarshalFromJson(null, typeof(TestEnum?), context);
 
        Assert.Null(result);
    }
 
    private static (AtsMarshaller Marshaller, AtsMarshaller.UnmarshalContext Context) CreateMarshallerWithContext(HandleRegistry? registry = null)
    {
        var handles = registry ?? new HandleRegistry();
        var context = new AtsMarshaller.UnmarshalContext
        {
            CapabilityId = "test/capability",
            ParameterName = "testParam"
        };
        var marshaller = CreateTestMarshaller(handles);
        return (marshaller, context);
    }
 
    private enum TestEnum
    {
        ValueA,
        ValueB
    }
 
    private sealed class TestClass
    {
        public int Value { get; set; }
    }
 
    [AspireDto]
    private sealed class TestDto
    {
        public string? Name { get; set; }
        public int Count { get; set; }
    }
}