File: AzureKubernetesEnvironmentExtensionsTests.cs
Web Access
Project: src\tests\Aspire.Hosting.Azure.Kubernetes.Tests\Aspire.Hosting.Azure.Kubernetes.Tests.csproj (Aspire.Hosting.Azure.Kubernetes.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 ASPIREAZURE003 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
#pragma warning disable ASPIRECOMPUTE003 // Type is for evaluation purposes only
#pragma warning disable ASPIREPIPELINES001 // PipelineStepAnnotation is evaluation-only
 
using System.Runtime.CompilerServices;
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Azure.Kubernetes;
using Aspire.Hosting.Kubernetes;
using Aspire.Hosting.Pipelines;
using Aspire.Hosting.Utils;
using Microsoft.Extensions.DependencyInjection;
 
namespace Aspire.Hosting.Azure.Tests;
 
public class AzureKubernetesEnvironmentExtensionsTests
{
    [Fact]
    public async Task AddAzureKubernetesEnvironment_BasicConfiguration()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
 
        var aks = builder.AddAzureKubernetesEnvironment("aks");
 
        Assert.Equal("aks", aks.Resource.Name);
        Assert.Equal("{aks.outputs.id}", aks.Resource.Id.ValueExpression);
        Assert.Equal("{aks.outputs.clusterFqdn}", aks.Resource.ClusterFqdn.ValueExpression);
        Assert.Equal("{aks.outputs.oidcIssuerUrl}", aks.Resource.OidcIssuerUrl.ValueExpression);
 
        var manifest = await AzureManifestUtils.GetManifestWithBicep(aks.Resource);
        await Verify(manifest.BicepText, extension: "bicep");
    }
 
    [Fact]
    public void AddNodePool_ReturnsNodePoolResource()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
 
        var aks = builder.AddAzureKubernetesEnvironment("aks");
        var gpuPool = aks.AddNodePool("gpu", "Standard_NC6s_v3", 0, 5);
 
        // Default system pool + added user pool
        Assert.Equal(2, aks.Resource.NodePools.Count);
 
        Assert.Equal("gpu", gpuPool.Resource.Name);
        Assert.Equal("gpu", gpuPool.Resource.Config.Name);
        Assert.Equal("Standard_NC6s_v3", gpuPool.Resource.Config.VmSize);
        Assert.Equal(0, gpuPool.Resource.Config.MinCount);
        Assert.Equal(5, gpuPool.Resource.Config.MaxCount);
        Assert.Equal(AksNodePoolMode.User, gpuPool.Resource.Config.Mode);
        Assert.Same(aks.Resource, gpuPool.Resource.AksParent);
    }
 
    [Fact]
    public void AddAzureKubernetesEnvironment_DefaultNodePool()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
 
        var aks = builder.AddAzureKubernetesEnvironment("aks");
 
        Assert.Single(aks.Resource.NodePools);
        var defaultPool = aks.Resource.NodePools[0];
        Assert.Equal("system", defaultPool.Name);
        Assert.Equal("Standard_D2s_v5", defaultPool.VmSize);
        Assert.Equal(1, defaultPool.MinCount);
        Assert.Equal(3, defaultPool.MaxCount);
        Assert.Equal(AksNodePoolMode.System, defaultPool.Mode);
    }
 
    [Fact]
    public void AddAzureKubernetesEnvironment_DefaultConfiguration()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
 
        var aks = builder.AddAzureKubernetesEnvironment("aks");
 
        Assert.True(aks.Resource.OidcIssuerEnabled);
        Assert.True(aks.Resource.WorkloadIdentityEnabled);
        Assert.Null(aks.Resource.KubernetesVersion);
        Assert.False(aks.Resource.IsPrivateCluster);
        Assert.False(aks.Resource.ContainerInsightsEnabled);
        Assert.Null(aks.Resource.LogAnalyticsWorkspace);
    }
 
    [Fact]
    public void AddAzureKubernetesEnvironment_HasInternalKubernetesEnvironment()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
 
        var aks = builder.AddAzureKubernetesEnvironment("aks");
 
        Assert.NotNull(aks.Resource.KubernetesEnvironment);
        Assert.Equal("aks", aks.Resource.KubernetesEnvironment.Name);
        Assert.True(aks.Resource.TryGetLastAnnotation<KubernetesEnvironmentAnnotation>(out var annotation));
    }
 
    [Fact]
    public void AddAzureKubernetesEnvironment_AddsOnlyAksComputeEnvironmentToModel()
    {
        using var builder = TestDistributedApplicationBuilder.Create(
            DistributedApplicationOperation.Publish);
 
        var aks = builder.AddAzureKubernetesEnvironment("aks");
 
        using var app = builder.Build();
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
 
        Assert.DoesNotContain(model.Resources, r => r is KubernetesEnvironmentResource);
 
        var computeEnvironment = Assert.Single(model.Resources.OfType<IComputeEnvironmentResource>());
        Assert.Same(aks.Resource, computeEnvironment);
    }
 
    [Fact]
    public async Task AddAzureKubernetesEnvironment_AllowsKubernetesServiceCustomizationWithoutVisibleKubernetesEnvironment()
    {
        using var builder = TestDistributedApplicationBuilder.Create(
            DistributedApplicationOperation.Publish);
 
        builder.AddAzureKubernetesEnvironment("aks");
        builder.AddContainer("api", "myimage")
            .PublishAsKubernetesService(_ => { });
 
        await using var app = builder.Build();
 
        await ExecuteBeforeStartHooksAsync(app, default);
    }
 
    [Fact]
    public void AddAzureKubernetesEnvironment_ThrowsOnNullBuilder()
    {
        IDistributedApplicationBuilder builder = null!;
 
        Assert.Throws<ArgumentNullException>(() =>
            builder.AddAzureKubernetesEnvironment("aks"));
    }
 
    [Fact]
    public void AddAzureKubernetesEnvironment_ThrowsOnEmptyName()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
 
        Assert.Throws<ArgumentException>(() =>
            builder.AddAzureKubernetesEnvironment(""));
    }
 
    [Fact]
    public void WithWorkloadIdentity_EnablesOidcAndWorkloadIdentity()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
 
        var aks = builder.AddAzureKubernetesEnvironment("aks")
            .WithWorkloadIdentity();
 
        Assert.True(aks.Resource.OidcIssuerEnabled);
        Assert.True(aks.Resource.WorkloadIdentityEnabled);
    }
 
    [Fact]
    public void WithAzureUserAssignedIdentity_WorksWithAks()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
 
        var aks = builder.AddAzureKubernetesEnvironment("aks");
        var identity = builder.AddAzureUserAssignedIdentity("myIdentity");
 
        var project = builder.AddContainer("myapi", "myimage")
            .WithAzureUserAssignedIdentity(identity);
 
        Assert.True(project.Resource.TryGetLastAnnotation<AppIdentityAnnotation>(out var appIdentity));
        Assert.Same(identity.Resource, appIdentity.IdentityResource);
    }
 
    [Fact]
    public void AzureKubernetesEnvironment_ImplementsIAzureComputeEnvironmentResource()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var aks = builder.AddAzureKubernetesEnvironment("aks");
        Assert.IsAssignableFrom<IAzureComputeEnvironmentResource>(aks.Resource);
    }
 
    [Fact]
    public void AzureKubernetesEnvironment_ImplementsIAzureNspAssociationTarget()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var aks = builder.AddAzureKubernetesEnvironment("aks");
        Assert.IsAssignableFrom<IAzureNspAssociationTarget>(aks.Resource);
    }
 
    [Fact]
    public void AsExisting_WorksOnAksResource()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
 
        var nameParam = builder.AddParameter("aks-name");
        var rgParam = builder.AddParameter("aks-rg");
 
        var aks = builder.AddAzureKubernetesEnvironment("aks")
            .AsExisting(nameParam, rgParam);
 
        Assert.NotNull(aks);
    }
 
    [Fact]
    public void WithSubnet_OnNodePool_StoresPerPoolSubnet()
    {
        using var builder = TestDistributedApplicationBuilder.Create(
            DistributedApplicationOperation.Publish);
 
        var vnet = builder.AddAzureVirtualNetwork("vnet", "10.0.0.0/16");
        var defaultSubnet = vnet.AddSubnet("default-subnet", "10.0.0.0/22");
        var gpuSubnet = vnet.AddSubnet("gpu-subnet", "10.0.4.0/24");
 
        var aks = builder.AddAzureKubernetesEnvironment("aks")
            .WithSubnet(defaultSubnet);
 
        var gpuPool = aks.AddNodePool("gpu", "Standard_NC6s_v3", 0, 5)
            .WithSubnet(gpuSubnet);
 
        // Environment-level subnet should be set via annotation
        Assert.True(aks.Resource.TryGetLastAnnotation<AksSubnetAnnotation>(out _));
 
        // Per-pool subnet should be stored in NodePoolSubnets dictionary
        Assert.Single(aks.Resource.NodePoolSubnets);
        Assert.True(aks.Resource.NodePoolSubnets.ContainsKey("gpu"));
 
        // Node pool should also have its own subnet annotation
        Assert.True(gpuPool.Resource.TryGetLastAnnotation<AksSubnetAnnotation>(out _));
    }
 
    [Fact]
    public void WithSubnet_OnNodePool_WithoutEnvironmentSubnet()
    {
        using var builder = TestDistributedApplicationBuilder.Create(
            DistributedApplicationOperation.Publish);
 
        var vnet = builder.AddAzureVirtualNetwork("vnet", "10.0.0.0/16");
        var gpuSubnet = vnet.AddSubnet("gpu-subnet", "10.0.4.0/24");
 
        var aks = builder.AddAzureKubernetesEnvironment("aks");
 
        // Only set subnet on the pool, not the environment
        var gpuPool = aks.AddNodePool("gpu", "Standard_NC6s_v3", 0, 5)
            .WithSubnet(gpuSubnet);
 
        // No environment-level subnet
        Assert.False(aks.Resource.TryGetLastAnnotation<AksSubnetAnnotation>(out _));
 
        // Per-pool subnet should still work
        Assert.Single(aks.Resource.NodePoolSubnets);
        Assert.True(aks.Resource.NodePoolSubnets.ContainsKey("gpu"));
    }
 
    [Fact]
    public void WithNodePool_AddsAnnotation()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
 
        var aks = builder.AddAzureKubernetesEnvironment("aks");
        var gpuPool = aks.AddNodePool("gpu", "Standard_NC6s_v3", 0, 5);
 
        var container = builder.AddContainer("myapi", "myimage")
            .WithNodePool(gpuPool);
 
        Assert.True(container.Resource.TryGetLastAnnotation<KubernetesNodePoolAnnotation>(out var affinity));
        Assert.Same(gpuPool.Resource, affinity.NodePool);
        Assert.Equal("gpu", affinity.NodePool.Name);
    }
 
    [Fact]
    public void AddNodePool_MultiplePoolsSupported()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
 
        var aks = builder.AddAzureKubernetesEnvironment("aks");
        var pool1 = aks.AddNodePool("cpu", "Standard_D2s_v5", 1, 10);
        var pool2 = aks.AddNodePool("gpu", "Standard_NC6s_v3", 0, 5);
 
        // Default system pool + 2 user pools
        Assert.Equal(3, aks.Resource.NodePools.Count);
        Assert.Equal("cpu", pool1.Resource.Name);
        Assert.Equal("gpu", pool2.Resource.Name);
    }
 
    [Fact]
    public void AddAzureKubernetesEnvironment_AutoCreatesDefaultRegistry()
    {
        using var builder = TestDistributedApplicationBuilder.Create(
            DistributedApplicationOperation.Publish);
 
        var aks = builder.AddAzureKubernetesEnvironment("aks");
 
        Assert.NotNull(aks.Resource.DefaultContainerRegistry);
        Assert.Equal("aks-acr", aks.Resource.DefaultContainerRegistry.Name);
    }
 
    [Fact]
    public void WithContainerRegistry_ReplacesDefault()
    {
        using var builder = TestDistributedApplicationBuilder.Create(
            DistributedApplicationOperation.Publish);
 
        var aks = builder.AddAzureKubernetesEnvironment("aks");
        var explicitAcr = builder.AddAzureContainerRegistry("my-acr");
 
        aks.WithContainerRegistry(explicitAcr);
 
        // Default registry should be removed
        Assert.Null(aks.Resource.DefaultContainerRegistry);
 
        // Explicit registry should be set via annotation
        Assert.True(aks.Resource.TryGetLastAnnotation<ContainerRegistryReferenceAnnotation>(out var annotation));
        Assert.Same(explicitAcr.Resource, annotation.Registry);
    }
 
    [Fact]
    public async Task ContainerRegistry_FlowsToInnerKubernetesEnvironment()
    {
        using var builder = TestDistributedApplicationBuilder.Create(
            DistributedApplicationOperation.Publish);
 
        var aks = builder.AddAzureKubernetesEnvironment("aks");
        var container = builder.AddContainer("myapi", "myimage");
 
        await using var app = builder.Build();
        await ExecuteBeforeStartHooksAsync(app, default);
 
        Assert.True(aks.Resource.TryGetLastAnnotation<ContainerRegistryReferenceAnnotation>(out var aksAnnotation));
        Assert.Same(aks.Resource.DefaultContainerRegistry, aksAnnotation.Registry);
 
        Assert.True(aks.Resource.KubernetesEnvironment
            .TryGetLastAnnotation<ContainerRegistryReferenceAnnotation>(out var annotation));
        Assert.Same(aks.Resource.DefaultContainerRegistry, annotation.Registry);
    }
 
    [Fact]
    public void WithSystemNodePool_CustomVmSize()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
 
        var aks = builder.AddAzureKubernetesEnvironment("aks")
            .WithSystemNodePool("Standard_B2s");
 
        Assert.Single(aks.Resource.NodePools, p => p.Mode is AksNodePoolMode.System);
        var systemPool = aks.Resource.NodePools.First(p => p.Mode is AksNodePoolMode.System);
        Assert.Equal("system", systemPool.Name);
        Assert.Equal("Standard_B2s", systemPool.VmSize);
        Assert.Equal(1, systemPool.MinCount);
        Assert.Equal(3, systemPool.MaxCount);
    }
 
    [Fact]
    public void WithSystemNodePool_CustomVmSizeAndScaling()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
 
        var aks = builder.AddAzureKubernetesEnvironment("aks")
            .WithSystemNodePool("Standard_B4ms", minCount: 2, maxCount: 5);
 
        var systemPool = aks.Resource.NodePools.First(p => p.Mode is AksNodePoolMode.System);
        Assert.Equal("Standard_B4ms", systemPool.VmSize);
        Assert.Equal(2, systemPool.MinCount);
        Assert.Equal(5, systemPool.MaxCount);
    }
 
    [Fact]
    public void WithSystemNodePool_ReplacesDefaultSystemPool()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
 
        var aks = builder.AddAzureKubernetesEnvironment("aks");
 
        // Default system pool should exist
        Assert.Equal("Standard_D2s_v5", aks.Resource.NodePools[0].VmSize);
 
        // Replace it
        aks.WithSystemNodePool("Standard_B2s");
 
        // Should still be exactly one system pool
        Assert.Single(aks.Resource.NodePools, p => p.Mode is AksNodePoolMode.System);
        Assert.Equal("Standard_B2s", aks.Resource.NodePools.First(p => p.Mode is AksNodePoolMode.System).VmSize);
    }
 
    [Fact]
    public void WithSystemNodePool_CalledMultipleTimesUsesLastValue()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
 
        var aks = builder.AddAzureKubernetesEnvironment("aks")
            .WithSystemNodePool("Standard_B2s")
            .WithSystemNodePool("Standard_D4s_v5", minCount: 3, maxCount: 10);
 
        Assert.Single(aks.Resource.NodePools, p => p.Mode is AksNodePoolMode.System);
        var systemPool = aks.Resource.NodePools.First(p => p.Mode is AksNodePoolMode.System);
        Assert.Equal("Standard_D4s_v5", systemPool.VmSize);
        Assert.Equal(3, systemPool.MinCount);
        Assert.Equal(10, systemPool.MaxCount);
    }
 
    [Fact]
    public void WithSystemNodePool_ChainsWithAddNodePool()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
 
        var aks = builder.AddAzureKubernetesEnvironment("aks")
            .WithSystemNodePool("Standard_B2s");
 
        var gpuPool = aks.AddNodePool("gpu", "Standard_NC6s_v3", 0, 5);
 
        // 1 system pool + 1 user pool
        Assert.Equal(2, aks.Resource.NodePools.Count);
        Assert.Equal("Standard_B2s", aks.Resource.NodePools.First(p => p.Mode is AksNodePoolMode.System).VmSize);
        Assert.Equal("gpu", gpuPool.Resource.Name);
    }
 
    [Fact]
    public void WithSystemNodePool_RejectsZeroMinCount()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
 
        var aks = builder.AddAzureKubernetesEnvironment("aks");
        Assert.Throws<ArgumentOutOfRangeException>(() => aks.WithSystemNodePool("Standard_B2s", minCount: 0));
    }
 
    [Fact]
    public async Task WithSystemNodePool_BicepReflectsCustomVmSize()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
 
        var aks = builder.AddAzureKubernetesEnvironment("aks")
            .WithSystemNodePool("Standard_B2s");
 
        var manifest = await AzureManifestUtils.GetManifestWithBicep(aks.Resource);
        await Verify(manifest.BicepText, extension: "bicep");
    }
 
    [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "ExecuteBeforeStartHooksAsync")]
    private static extern Task ExecuteBeforeStartHooksAsync(DistributedApplication app, CancellationToken cancellationToken);
 
    [Fact]
    public async Task AddLoadBalancer_BicepEnablesIngressProfileAndUsesPreviewApi()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
 
        var vnet = builder.AddAzureVirtualNetwork("vnet", "10.0.0.0/16");
        var aksSubnet = vnet.AddSubnet("aksnodes", "10.0.0.0/22");
        var albSubnet = vnet.AddSubnet("alb", "10.0.4.0/24");
 
        var aks = builder.AddAzureKubernetesEnvironment("aks").WithSubnet(aksSubnet);
        aks.AddLoadBalancer("lb", albSubnet);
 
        var manifest = await AzureManifestUtils.GetManifestWithBicep(aks.Resource);
        await Verify(manifest.BicepText, extension: "bicep");
    }
 
    [Fact]
    public void AddLoadBalancer_AppliesSubnetDelegation()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
 
        var vnet = builder.AddAzureVirtualNetwork("vnet", "10.0.0.0/16");
        var albSubnet = vnet.AddSubnet("alb", "10.0.4.0/24");
 
        var aks = builder.AddAzureKubernetesEnvironment("aks");
        aks.AddLoadBalancer("lb", albSubnet);
 
        Assert.True(aks.Resource.GatewayApiEnabled);
        Assert.True(aks.Resource.ApplicationLoadBalancerEnabled);
        Assert.True(aks.Resource.RequiresPreviewIngressApi);
 
        // AGC requires the subnet be delegated to Microsoft.ServiceNetworking/trafficControllers.
        Assert.True(albSubnet.Resource.TryGetLastAnnotation<AzureSubnetServiceDelegationAnnotation>(out var delegation));
        Assert.Equal("Microsoft.ServiceNetworking/trafficControllers", delegation!.ServiceName);
    }
 
    [Fact]
    public void AddLoadBalancer_RegistersPerLBPipelineStep()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
 
        var vnet = builder.AddAzureVirtualNetwork("vnet", "10.0.0.0/16");
        var albSubnet = vnet.AddSubnet("alb", "10.0.4.0/24");
 
        var aks = builder.AddAzureKubernetesEnvironment("aks");
        var lb = aks.AddLoadBalancer("lb", albSubnet);
 
        Assert.Same(aks.Resource, lb.Resource.Parent);
        Assert.Equal("alb-lb", lb.Resource.AlbName);
        Assert.True(lb.Resource.TryGetAnnotationsOfType<PipelineStepAnnotation>(out var stepAnnotations));
        Assert.Single(stepAnnotations);
    }
 
    [Fact]
    public void AddLoadBalancer_MultipleLBs_AllStepsRegistered()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
 
        var vnet = builder.AddAzureVirtualNetwork("vnet", "10.0.0.0/16");
        var alb1 = vnet.AddSubnet("alb1", "10.0.4.0/24");
        var alb2 = vnet.AddSubnet("alb2", "10.0.5.0/24");
 
        var aks = builder.AddAzureKubernetesEnvironment("aks");
        var lb1 = aks.AddLoadBalancer("lb1", alb1);
        var lb2 = aks.AddLoadBalancer("lb2", alb2);
 
        Assert.NotSame(lb1.Resource, lb2.Resource);
        Assert.Equal("alb-lb1", lb1.Resource.AlbName);
        Assert.Equal("alb-lb2", lb2.Resource.AlbName);
 
        // Each LB owns its own pipeline step annotation.
        Assert.True(lb1.Resource.TryGetAnnotationsOfType<PipelineStepAnnotation>(out var lb1Steps));
        Assert.True(lb2.Resource.TryGetAnnotationsOfType<PipelineStepAnnotation>(out var lb2Steps));
        Assert.Single(lb1Steps);
        Assert.Single(lb2Steps);
 
        // Both LBs share the subnet/delegation requirements but the annotation
        // is applied idempotently per subnet (one delegation per subnet).
        Assert.True(alb1.Resource.TryGetLastAnnotation<AzureSubnetServiceDelegationAnnotation>(out _));
        Assert.True(alb2.Resource.TryGetLastAnnotation<AzureSubnetServiceDelegationAnnotation>(out _));
    }
 
    [Fact]
    public void AddLoadBalancer_OnUserDelegatedSubnet_ReplacesDelegationAndRecordsDisplaced()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
 
        var vnet = builder.AddAzureVirtualNetwork("vnet", "10.0.0.0/16");
 
        // The user delegates their ALB subnet to a different service before handing it to AGC.
        var albSubnet = vnet.AddSubnet("alb", "10.0.4.0/24")
            .WithServiceDelegation("Microsoft.Netapp/volumes");
 
        var aks = builder.AddAzureKubernetesEnvironment("aks");
        var lb = aks.AddLoadBalancer("lb", albSubnet);
 
        // AGC requires trafficControllers, so the user's delegation is replaced rather than appended
        // and the subnet retains exactly one effective delegation.
        var delegation = Assert.Single(albSubnet.Resource.Annotations.OfType<AzureSubnetServiceDelegationAnnotation>());
        Assert.Equal("Microsoft.ServiceNetworking/trafficControllers", delegation.ServiceName);
 
        // The displaced delegation is surfaced so the LB pipeline step can warn the user at deploy time.
        Assert.Equal("Microsoft.Netapp/volumes", lb.Resource.DisplacedDelegationServiceName);
    }
 
    [Fact]
    public void AddLoadBalancer_SharedSubnet_KeepsSingleDelegationWithNoDisplacement()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
 
        var vnet = builder.AddAzureVirtualNetwork("vnet", "10.0.0.0/16");
        var albSubnet = vnet.AddSubnet("alb", "10.0.4.0/24");
 
        var aks = builder.AddAzureKubernetesEnvironment("aks");
        var lb1 = aks.AddLoadBalancer("lb1", albSubnet);
        var lb2 = aks.AddLoadBalancer("lb2", albSubnet);
 
        // Sharing a subnet across load balancers is idempotent: exactly one delegation remains.
        var delegation = Assert.Single(albSubnet.Resource.Annotations.OfType<AzureSubnetServiceDelegationAnnotation>());
        Assert.Equal("Microsoft.ServiceNetworking/trafficControllers", delegation.ServiceName);
 
        // Neither LB displaced a user delegation; the only prior delegation was AGC's own.
        Assert.Null(lb1.Resource.DisplacedDelegationServiceName);
        Assert.Null(lb2.Resource.DisplacedDelegationServiceName);
    }
 
    [Fact]
    public void AddLoadBalancer_EquivalentDelegationWithDifferentCasing_DoesNotRecordDisplacement()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
 
        var vnet = builder.AddAzureVirtualNetwork("vnet", "10.0.0.0/16");
        var albSubnet = vnet.AddSubnet("alb", "10.0.4.0/24")
            .WithServiceDelegation("microsoft.servicenetworking/trafficcontrollers");
 
        var aks = builder.AddAzureKubernetesEnvironment("aks");
        var lb = aks.AddLoadBalancer("lb", albSubnet);
 
        var delegation = Assert.Single(albSubnet.Resource.Annotations.OfType<AzureSubnetServiceDelegationAnnotation>());
        Assert.Equal("Microsoft.ServiceNetworking/trafficControllers", delegation.ServiceName);
        Assert.Null(lb.Resource.DisplacedDelegationServiceName);
    }
 
    [Fact]
    public void AddLoadBalancer_OnSubnetWithMultipleDirectDelegations_CollapsesToSingle()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
 
        var vnet = builder.AddAzureVirtualNetwork("vnet", "10.0.0.0/16");
 
        // AzureSubnetServiceDelegationAnnotation is public, so a subnet can carry several directly-appended
        // delegations. AddLoadBalancer must read the last one (last-write-wins) without throwing on the
        // duplicates and then collapse them to AGC's single required delegation.
        var albSubnet = vnet.AddSubnet("alb", "10.0.4.0/24")
            .WithAnnotation(new AzureSubnetServiceDelegationAnnotation("First", "Microsoft.Netapp/volumes"))
            .WithAnnotation(new AzureSubnetServiceDelegationAnnotation("Second", "Microsoft.Sql/managedInstances"));
 
        var aks = builder.AddAzureKubernetesEnvironment("aks");
        var lb = aks.AddLoadBalancer("lb", albSubnet);
 
        var delegation = Assert.Single(albSubnet.Resource.Annotations.OfType<AzureSubnetServiceDelegationAnnotation>());
        Assert.Equal("Microsoft.ServiceNetworking/trafficControllers", delegation.ServiceName);
 
        // The last of the user's delegations is surfaced as displaced so the LB pipeline step can warn.
        Assert.Equal("Microsoft.Sql/managedInstances", lb.Resource.DisplacedDelegationServiceName);
    }
 
}