File: AtsContextFilterTests.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.Reflection;
using System.Text.Json.Nodes;
using Aspire.Hosting.ApplicationModel;
using Aspire.TypeSystem;
using Xunit;
 
namespace Aspire.Hosting.RemoteHost.Tests;
 
public class AtsContextFilterTests
{
    [Fact]
    public void FilterByExportingAssemblies_StrictFilterKeepsOnlySelectedAssemblyExports()
    {
        var context = CreateContext();
 
        var filteredContext = AtsContextFilter.FilterByExportingAssemblies(
            context,
            [typeof(AtsContextFilterTests).Assembly.GetName().Name!]);
 
        Assert.Collection(
            filteredContext.Capabilities,
            capability => Assert.Equal("Aspire.Hosting.RemoteHost.Tests/addTestResource", capability.CapabilityId));
 
        Assert.Collection(
            filteredContext.HandleTypes,
            type => Assert.Equal("Aspire.Hosting.RemoteHost.Tests/TestResource", type.AtsTypeId));
 
        Assert.Collection(
            filteredContext.DtoTypes,
            type => Assert.Equal("Aspire.Hosting.RemoteHost.Tests/TestOptions", type.TypeId));
 
        Assert.Collection(
            filteredContext.EnumTypes,
            type => Assert.Equal(AtsConstants.EnumTypeId(typeof(TestMode).FullName!), type.TypeId));
 
        Assert.Collection(
            filteredContext.ExportedValues.OrderBy(value => string.Join(".", value.PathSegments), StringComparer.Ordinal),
            value => Assert.Equal("Aspire.Hosting.RemoteHost.Tests.SelectedValues.Default", string.Join(".", value.PathSegments)),
            value => Assert.Equal("Aspire.Hosting.RemoteHost.Tests.SelectedValues.Metadata", string.Join(".", value.PathSegments)));
 
        Assert.Contains(filteredContext.Diagnostics, diagnostic => diagnostic.Location == "Aspire.Hosting.RemoteHost.Tests.TestType.Method");
        Assert.DoesNotContain(filteredContext.Diagnostics, diagnostic => diagnostic.Location == "Aspire.Hosting.UnrelatedType.Method");
        Assert.DoesNotContain(filteredContext.Diagnostics, diagnostic => diagnostic.Location == "Aspire.Hosting.Redis.RedisType.Method");
 
        Assert.Contains("Aspire.Hosting.RemoteHost.Tests/addTestResource", filteredContext.Methods.Keys);
        Assert.DoesNotContain("Aspire.Hosting/createBuilder", filteredContext.Methods.Keys);
    }
 
    [Fact]
    public void FilterByExportingAssemblies_CodeGenerationFilterIncludesReferencedSupportingTypes()
    {
        var context = CreateContext();
 
        var filteredContext = AtsContextFilter.FilterByExportingAssembliesWithReferences(
            context,
            [typeof(AtsContextFilterTests).Assembly.GetName().Name!]);
 
        Assert.Contains(filteredContext.HandleTypes, type => type.AtsTypeId == "Aspire.Hosting/Aspire.Hosting.ApplicationModel.ResourceBuilder`1");
        Assert.Contains(filteredContext.DtoTypes, type => type.TypeId == "Aspire.TypeSystem/AtsContext");
        Assert.Contains(filteredContext.DtoTypes, type => type.TypeId == "Aspire.TypeSystem/ExportedValueMetadata");
        Assert.Contains(filteredContext.EnumTypes, type => type.TypeId == AtsConstants.EnumTypeId(typeof(DistributedApplicationOperation).FullName!));
        Assert.Contains(filteredContext.EnumTypes, type => type.TypeId == AtsConstants.EnumTypeId("Aspire.TypeSystem.ExportedValueMode"));
        Assert.Collection(
            filteredContext.ExportedValues.OrderBy(value => string.Join(".", value.PathSegments), StringComparer.Ordinal),
            value => Assert.Equal("Aspire.Hosting.RemoteHost.Tests.SelectedValues.Default", string.Join(".", value.PathSegments)),
            value => Assert.Equal("Aspire.Hosting.RemoteHost.Tests.SelectedValues.Metadata", string.Join(".", value.PathSegments)));
        Assert.DoesNotContain(filteredContext.Capabilities, capability => capability.CapabilityId == "Aspire.Hosting/createBuilder");
        Assert.DoesNotContain(filteredContext.HandleTypes, type => type.AtsTypeId == "Aspire.Hosting/Aspire.Hosting.DistributedApplication");
    }
 
    [Fact]
    public void FilterByExportingAssemblies_ScannedAssemblies_OnlyReturnsSpecifiedAssemblyExports()
    {
        // End-to-end: scan real assemblies through the capability scanner, then filter
        // to a single assembly and verify only that assembly's capabilities appear.
        var hostingAssembly = typeof(DistributedApplication).Assembly;
        var testAssembly = typeof(AtsContextFilterTests).Assembly;
        var testAssemblyName = testAssembly.GetName().Name!;
 
        var scanResult = AtsCapabilityScanner.ScanAssemblies([hostingAssembly, testAssembly]);
        var unfilteredContext = scanResult.ToAtsContext();
 
        // Precondition: the unfiltered context has capabilities from both assemblies
        Assert.Contains(unfilteredContext.Capabilities, c => c.CapabilityId.StartsWith("Aspire.Hosting/", StringComparison.Ordinal));
        Assert.Contains(unfilteredContext.Capabilities, c => c.CapabilityId.StartsWith(testAssemblyName + "/", StringComparison.Ordinal));
 
        var filteredContext = AtsContextFilter.FilterByExportingAssembliesWithReferences(
            unfilteredContext,
            [testAssemblyName]);
 
        // Only the test assembly's capabilities should remain
        Assert.All(filteredContext.Capabilities, c =>
            Assert.StartsWith(testAssemblyName + "/", c.CapabilityId));
 
        // No Aspire.Hosting capabilities should be present
        Assert.DoesNotContain(filteredContext.Capabilities,
            c => c.CapabilityId.StartsWith("Aspire.Hosting/", StringComparison.Ordinal));
 
        // The test assembly should still have at least one capability
        Assert.NotEmpty(filteredContext.Capabilities);
 
        // Referenced types from Aspire.Hosting used by the test assembly's capabilities
        // should be included (WithReferences), but no standalone Aspire.Hosting capabilities
        Assert.True(filteredContext.HandleTypes.Count > 0);
    }
 
    [Fact]
    public void FilterByExportingAssemblies_DiagnosticsUseMostSpecificKnownAssemblyPrefix()
    {
        var context = CreateContext();
 
        var filteredContext = AtsContextFilter.FilterByExportingAssemblies(
            context,
            ["Aspire.Hosting"]);
 
        Assert.Contains(filteredContext.Diagnostics, diagnostic => diagnostic.Location == "Aspire.Hosting.UnrelatedType.Method");
        Assert.DoesNotContain(filteredContext.Diagnostics, diagnostic => diagnostic.Location == "Aspire.Hosting.Redis.RedisType.Method");
    }
 
    private static AtsContext CreateContext()
    {
        const string selectedCapabilityId = "Aspire.Hosting.RemoteHost.Tests/addTestResource";
        const string unrelatedCapabilityId = "Aspire.Hosting/createBuilder";
 
        var selectedHandleType = new AtsTypeInfo
        {
            AtsTypeId = "Aspire.Hosting.RemoteHost.Tests/TestResource",
            ClrType = typeof(TestResource),
            IsInterface = false,
            HasExposeMethods = true,
            HasExposeProperties = false,
            BaseTypeHierarchy = [],
            ImplementedInterfaces = []
        };
 
        var referencedCoreHandleType = new AtsTypeInfo
        {
            AtsTypeId = "Aspire.Hosting/Aspire.Hosting.ApplicationModel.ResourceBuilder`1",
            ClrType = typeof(IResourceBuilder<IResource>),
            IsInterface = true,
            HasExposeMethods = false,
            HasExposeProperties = false,
            BaseTypeHierarchy = [],
            ImplementedInterfaces = []
        };
 
        var unrelatedCoreHandleType = new AtsTypeInfo
        {
            AtsTypeId = "Aspire.Hosting/Aspire.Hosting.DistributedApplication",
            ClrType = typeof(DistributedApplication),
            IsInterface = false,
            HasExposeMethods = true,
            HasExposeProperties = false,
            BaseTypeHierarchy = [],
            ImplementedInterfaces = []
        };
 
        var siblingHandleType = new AtsTypeInfo
        {
            AtsTypeId = "Aspire.Hosting.Redis/Aspire.Hosting.Redis.RedisResource",
            ClrType = null,
            IsInterface = false,
            HasExposeMethods = true,
            HasExposeProperties = false,
            BaseTypeHierarchy = [],
            ImplementedInterfaces = []
        };
 
        var selectedDtoType = new AtsDtoTypeInfo
        {
            TypeId = "Aspire.Hosting.RemoteHost.Tests/TestOptions",
            Name = nameof(TestOptions),
            ClrType = typeof(TestOptions),
            Properties =
            [
                new AtsDtoPropertyInfo
                {
                    Name = nameof(TestOptions.Mode),
                    Type = new AtsTypeRef
                    {
                        TypeId = AtsConstants.EnumTypeId(typeof(TestMode).FullName!),
                        ClrType = typeof(TestMode),
                        Category = AtsTypeCategory.Enum
                    },
                    IsOptional = false
                }
            ]
        };
 
        var referencedCoreDtoType = new AtsDtoTypeInfo
        {
            TypeId = "Aspire.TypeSystem/AtsContext",
            Name = nameof(AtsContext),
            ClrType = typeof(AtsContext),
            Properties = []
        };
 
        var selectedEnumType = new AtsEnumTypeInfo
        {
            TypeId = AtsConstants.EnumTypeId(typeof(TestMode).FullName!),
            Name = nameof(TestMode),
            ClrType = typeof(TestMode),
            Values = Enum.GetNames<TestMode>()
        };
 
        var exportedValueOnlyEnumType = new AtsEnumTypeInfo
        {
            TypeId = AtsConstants.EnumTypeId("Aspire.TypeSystem.ExportedValueMode"),
            Name = "ExportedValueMode",
            ClrType = typeof(DistributedApplicationOperation),
            Values = Enum.GetNames<DistributedApplicationOperation>()
        };
 
        var referencedCoreEnumType = new AtsEnumTypeInfo
        {
            TypeId = AtsConstants.EnumTypeId(typeof(DistributedApplicationOperation).FullName!),
            Name = nameof(DistributedApplicationOperation),
            ClrType = typeof(DistributedApplicationOperation),
            Values = Enum.GetNames<DistributedApplicationOperation>()
        };
 
        var exportedValueOnlyDtoType = new AtsDtoTypeInfo
        {
            TypeId = "Aspire.TypeSystem/ExportedValueMetadata",
            Name = "ExportedValueMetadata",
            ClrType = typeof(AtsContext),
            Properties =
            [
                new AtsDtoPropertyInfo
                {
                    Name = "Mode",
                    Type = new AtsTypeRef
                    {
                        TypeId = exportedValueOnlyEnumType.TypeId,
                        ClrType = exportedValueOnlyEnumType.ClrType,
                        Category = AtsTypeCategory.Enum
                    },
                    IsOptional = false
                }
            ]
        };
 
        var selectedCapability = new AtsCapabilityInfo
        {
            CapabilityId = selectedCapabilityId,
            MethodName = "addTestResource",
            Parameters =
            [
                new AtsParameterInfo
                {
                    Name = "builder",
                    Type = new AtsTypeRef
                    {
                        TypeId = selectedHandleType.AtsTypeId,
                        ClrType = selectedHandleType.ClrType,
                        Category = AtsTypeCategory.Handle
                    }
                },
                new AtsParameterInfo
                {
                    Name = "options",
                    Type = new AtsTypeRef
                    {
                        TypeId = referencedCoreDtoType.TypeId,
                        ClrType = referencedCoreDtoType.ClrType,
                        Category = AtsTypeCategory.Dto
                    }
                },
                new AtsParameterInfo
                {
                    Name = "operation",
                    Type = new AtsTypeRef
                    {
                        TypeId = referencedCoreEnumType.TypeId,
                        ClrType = referencedCoreEnumType.ClrType,
                        Category = AtsTypeCategory.Enum
                    }
                }
            ],
            ReturnType = new AtsTypeRef
            {
                TypeId = referencedCoreHandleType.AtsTypeId,
                ClrType = referencedCoreHandleType.ClrType,
                Category = AtsTypeCategory.Handle,
                IsInterface = true
            },
            TargetTypeId = selectedHandleType.AtsTypeId,
            TargetType = new AtsTypeRef
            {
                TypeId = selectedHandleType.AtsTypeId,
                ClrType = selectedHandleType.ClrType,
                Category = AtsTypeCategory.Handle
            },
            TargetParameterName = "builder",
            ReturnsBuilder = true,
            CapabilityKind = AtsCapabilityKind.Method,
            ExpandedTargetTypes = []
        };
 
        var unrelatedCapability = new AtsCapabilityInfo
        {
            CapabilityId = unrelatedCapabilityId,
            MethodName = "addRedis",
            Parameters = [],
            ReturnType = new AtsTypeRef
            {
                TypeId = unrelatedCoreHandleType.AtsTypeId,
                ClrType = unrelatedCoreHandleType.ClrType,
                Category = AtsTypeCategory.Handle
            },
            ReturnsBuilder = true,
            CapabilityKind = AtsCapabilityKind.Method,
            ExpandedTargetTypes = []
        };
 
        var selectedAssemblyName = typeof(AtsContextFilterTests).Assembly.GetName().Name!;
        var unrelatedAssemblyName = typeof(DistributedApplication).Assembly.GetName().Name!;
 
        var selectedPrimitiveExportedValue = new AtsExportedValueInfo
        {
            OwningAssemblyName = selectedAssemblyName,
            PathSegments = ["Aspire.Hosting.RemoteHost.Tests", "SelectedValues", "Default"],
            Type = new AtsTypeRef
            {
                TypeId = "System/String",
                ClrType = typeof(string),
                Category = AtsTypeCategory.Primitive
            },
            Value = JsonValue.Create("selected")
        };
 
        var selectedDtoExportedValue = new AtsExportedValueInfo
        {
            OwningAssemblyName = selectedAssemblyName,
            PathSegments = ["Aspire.Hosting.RemoteHost.Tests", "SelectedValues", "Metadata"],
            Type = new AtsTypeRef
            {
                TypeId = exportedValueOnlyDtoType.TypeId,
                ClrType = exportedValueOnlyDtoType.ClrType,
                Category = AtsTypeCategory.Dto
            },
            Value = JsonNode.Parse("""{"mode":"Run"}""")
        };
 
        var unrelatedPrimitiveExportedValue = new AtsExportedValueInfo
        {
            OwningAssemblyName = unrelatedAssemblyName,
            PathSegments = ["Aspire.Hosting", "CoreValues", "Default"],
            Type = new AtsTypeRef
            {
                TypeId = "System/String",
                ClrType = typeof(string),
                Category = AtsTypeCategory.Primitive
            },
            Value = JsonValue.Create("unrelated")
        };
 
        var context = new AtsContext
        {
            Capabilities = [selectedCapability, unrelatedCapability],
            HandleTypes = [selectedHandleType, referencedCoreHandleType, unrelatedCoreHandleType, siblingHandleType],
            DtoTypes = [selectedDtoType, referencedCoreDtoType, exportedValueOnlyDtoType],
            EnumTypes = [selectedEnumType, referencedCoreEnumType, exportedValueOnlyEnumType],
            ExportedValues = [selectedPrimitiveExportedValue, selectedDtoExportedValue, unrelatedPrimitiveExportedValue],
            Diagnostics =
            [
                AtsDiagnostic.Warning("Selected warning", "Aspire.Hosting.RemoteHost.Tests.TestType.Method"),
                AtsDiagnostic.Warning("Unrelated warning", "Aspire.Hosting.UnrelatedType.Method"),
                AtsDiagnostic.Warning("Sibling warning", "Aspire.Hosting.Redis.RedisType.Method")
            ]
        };
 
        var testMethod = typeof(AtsContextFilterTests).GetMethod(nameof(TestCapability), BindingFlags.Static | BindingFlags.NonPublic)!;
        context.Methods[selectedCapabilityId] = testMethod;
        context.Methods[unrelatedCapabilityId] = typeof(DistributedApplication)
            .GetMethods(BindingFlags.Static | BindingFlags.Public)
            .Single(method => method.Name == nameof(DistributedApplication.CreateBuilder) && method.GetParameters().Length == 0);
 
        return context;
    }
 
    private static void TestCapability()
    {
    }
 
    private sealed class TestResource
    {
    }
 
    private sealed class TestOptions
    {
        public TestMode Mode { get; init; }
    }
 
    private enum TestMode
    {
        Basic,
        Advanced
    }
}