File: EndpointReferenceTests.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.Net.Sockets;
using System.Runtime.CompilerServices;
using Aspire.Hosting.Utils;
 
namespace Aspire.Hosting.Tests;
 
[Trait("Partition", "5")]
public class EndpointReferenceTests
{
    [Theory]
    [InlineData(false)]
    [InlineData(true)]
    public void BuilderGetEndpointPreservesCanonicalName(bool explicitNetwork)
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var web = builder.AddContainer("web", "image").WithHttpEndpoint(targetPort: 8080);
 
        var endpoint = explicitNetwork
            ? web.GetEndpoint("HTTP", KnownNetworkIdentifiers.LocalhostNetwork)
            : web.GetEndpoint("HTTP");
 
        Assert.Equal("http", endpoint.EndpointName);
        Assert.Same(web.Resource.Annotations.OfType<EndpointAnnotation>().Single(), endpoint.EndpointAnnotation);
        Assert.Equal(explicitNetwork ? KnownNetworkIdentifiers.LocalhostNetwork : null, endpoint.ContextNetworkID);
    }
 
    [Theory]
    [InlineData(false)]
    [InlineData(true)]
    public void BuilderGetEndpointSupportsLaterDeclaration(bool explicitNetwork)
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var web = builder.AddContainer("web", "image");
        var endpoint = explicitNetwork
            ? web.GetEndpoint("http", KnownNetworkIdentifiers.LocalhostNetwork)
            : web.GetEndpoint("http");
        Assert.False(endpoint.Exists);
 
        web.WithHttpEndpoint(targetPort: 8080);
 
        Assert.True(endpoint.Exists);
        Assert.Same(web.Resource.Annotations.OfType<EndpointAnnotation>().Single(), endpoint.EndpointAnnotation);
    }
 
    [Fact]
    public async Task GetValueAsync_WaitsForEndpointAllocation()
    {
        var resource = new TestResource("test");
        var annotation = new EndpointAnnotation(ProtocolType.Tcp, uriScheme: "http", name: "http");
        resource.Annotations.Add(annotation);
 
        var endpointRef = new EndpointReference(resource, annotation);
 
        var getValueTask = endpointRef.GetValueAsync(CancellationToken.None);
        Assert.False(getValueTask.IsCompleted);
 
        annotation.AllocatedEndpoint = new AllocatedEndpoint(annotation, "localhost", 8080);
 
        var url = await getValueTask;
        Assert.Equal("http://localhost:8080", url);
    }
 
    [Fact]
    public async Task GetUrlPropertyValueAsync_WaitsForEndpointAllocation()
    {
        var resource = new TestResource("test");
        var annotation = new EndpointAnnotation(ProtocolType.Tcp, uriScheme: "http", name: "http");
        resource.Annotations.Add(annotation);
 
        var endpointRef = new EndpointReference(resource, annotation);
        var endpointExpr = endpointRef.Property(EndpointProperty.Url);
 
        var getValueTask = endpointExpr.GetValueAsync(CancellationToken.None);
        Assert.False(getValueTask.IsCompleted);
 
        annotation.AllocatedEndpoint = new AllocatedEndpoint(annotation, "localhost", 8080);
 
        var url = await getValueTask;
        Assert.Equal("http://localhost:8080", url);
    }
 
    [Fact]
    public async Task GetValueAsync_ReturnsImmediatelyWhenAlreadyAllocated()
    {
        var resource = new TestResource("test");
        var annotation = new EndpointAnnotation(ProtocolType.Tcp, uriScheme: "http", name: "http");
        resource.Annotations.Add(annotation);
        annotation.AllocatedEndpoint = new AllocatedEndpoint(annotation, "localhost", 8080);
 
        var endpointRef = new EndpointReference(resource, annotation);
        var endpointExpr = endpointRef.Property(EndpointProperty.Url);
 
        var url = await endpointExpr.GetValueAsync(CancellationToken.None);
        Assert.Equal("http://localhost:8080", url);
    }
 
    [Fact]
    public async Task GetValueAsync_Host_WaitsForAllocation()
    {
        var resource = new TestResource("test");
        var annotation = new EndpointAnnotation(ProtocolType.Tcp, uriScheme: "http", name: "http");
        resource.Annotations.Add(annotation);
 
        var endpointRef = new EndpointReference(resource, annotation);
        var hostExpr = endpointRef.Property(EndpointProperty.Host);
 
        var getValueTask = hostExpr.GetValueAsync(CancellationToken.None);
        Assert.False(getValueTask.IsCompleted);
 
        annotation.AllocatedEndpoint = new AllocatedEndpoint(annotation, "192.168.1.100", 8080);
 
        var host = await getValueTask;
        Assert.Equal("192.168.1.100", host);
    }
 
    [Fact]
    public async Task GetValueAsync_Port_WaitsForAllocation()
    {
        var resource = new TestResource("test");
        var annotation = new EndpointAnnotation(ProtocolType.Tcp, uriScheme: "http", name: "http");
        resource.Annotations.Add(annotation);
 
        var endpointRef = new EndpointReference(resource, annotation);
        var portExpr = endpointRef.Property(EndpointProperty.Port);
 
        var getValueTask = portExpr.GetValueAsync(CancellationToken.None);
        Assert.False(getValueTask.IsCompleted);
 
        annotation.AllocatedEndpoint = new AllocatedEndpoint(annotation, "localhost", 9090);
 
        var port = await getValueTask;
        Assert.Equal("9090", port);
    }
 
    [Fact]
    public async Task GetValueAsync_Scheme_ReturnsImmediately()
    {
        var resource = new TestResource("test");
        var annotation = new EndpointAnnotation(ProtocolType.Tcp, uriScheme: "https", name: "https");
        resource.Annotations.Add(annotation);
 
        var endpointRef = new EndpointReference(resource, annotation);
        var schemeExpr = endpointRef.Property(EndpointProperty.Scheme);
 
        var scheme = await schemeExpr.GetValueAsync(CancellationToken.None);
        Assert.Equal("https", scheme);
    }
 
    [Fact]
    public async Task GetValueAsync_IPV4Host_ReturnsImmediately()
    {
        var resource = new TestResource("test");
        var annotation = new EndpointAnnotation(ProtocolType.Tcp, uriScheme: "http", name: "http");
        resource.Annotations.Add(annotation);
 
        var endpointRef = new EndpointReference(resource, annotation);
        var ipv4Expr = endpointRef.Property(EndpointProperty.IPV4Host);
 
        var ipv4 = await ipv4Expr.GetValueAsync(CancellationToken.None);
        Assert.Equal("127.0.0.1", ipv4);
    }
 
    [Fact]
    public async Task GetValueAsync_TargetPort_WithStaticPort_ReturnsImmediately()
    {
        var resource = new TestResource("test");
        var annotation = new EndpointAnnotation(ProtocolType.Tcp, uriScheme: "http", name: "http", targetPort: 5000);
        resource.Annotations.Add(annotation);
 
        var endpointRef = new EndpointReference(resource, annotation);
        var targetPortExpr = endpointRef.Property(EndpointProperty.TargetPort);
 
        var targetPort = await targetPortExpr.GetValueAsync(CancellationToken.None);
        Assert.Equal("5000", targetPort);
    }
 
    [Fact]
    public async Task GetValueAsync_TargetPort_WithExpression_WaitsForAllocation()
    {
        var resource = new TestResource("test");
        var annotation = new EndpointAnnotation(ProtocolType.Tcp, uriScheme: "http", name: "http");
        resource.Annotations.Add(annotation);
 
        var endpointRef = new EndpointReference(resource, annotation);
        var targetPortExpr = endpointRef.Property(EndpointProperty.TargetPort);
 
        var getValueTask = targetPortExpr.GetValueAsync(CancellationToken.None);
        Assert.False(getValueTask.IsCompleted);
 
        annotation.AllocatedEndpoint = new AllocatedEndpoint(annotation, "localhost", 8080, targetPortExpression: "5000");
 
        var targetPort = await getValueTask;
        Assert.Equal("5000", targetPort);
    }
 
    [Fact]
    public async Task GetValueAsync_SupportsCancellation()
    {
        var resource = new TestResource("test");
        var annotation = new EndpointAnnotation(ProtocolType.Tcp, uriScheme: "http", name: "http");
        resource.Annotations.Add(annotation);
 
        var endpointRef = new EndpointReference(resource, annotation);
        var endpointExpr = endpointRef.Property(EndpointProperty.Url);
 
        using var cts = new CancellationTokenSource();
        var getValueTask = endpointExpr.GetValueAsync(cts.Token);
 
        cts.Cancel();
 
        await Assert.ThrowsAsync<TaskCanceledException>(async () => await getValueTask);
    }
 
    [Fact]
    public async Task GetValueAsync_MultipleWaiters_AllCompleteWhenAllocated()
    {
        var resource = new TestResource("test");
        var annotation = new EndpointAnnotation(ProtocolType.Tcp, uriScheme: "http", name: "http");
        resource.Annotations.Add(annotation);
 
        var endpointRef = new EndpointReference(resource, annotation);
        var expr1 = endpointRef.Property(EndpointProperty.Url);
        var expr2 = endpointRef.Property(EndpointProperty.Host);
        var expr3 = endpointRef.Property(EndpointProperty.Port);
 
        var task1 = expr1.GetValueAsync(CancellationToken.None);
        var task2 = expr2.GetValueAsync(CancellationToken.None);
        var task3 = expr3.GetValueAsync(CancellationToken.None);
 
        Assert.False(task1.IsCompleted);
        Assert.False(task2.IsCompleted);
        Assert.False(task3.IsCompleted);
 
        annotation.AllocatedEndpoint = new AllocatedEndpoint(annotation, "localhost", 8080);
 
        var url = await task1;
        var host = await task2;
        var port = await task3;
 
        Assert.Equal("http://localhost:8080", url);
        Assert.Equal("localhost", host);
        Assert.Equal("8080", port);
    }
 
    [Fact]
    public void Port_ThrowsWhenEndpointNotAllocated()
    {
        var resource = new TestResource("test");
        var annotation = new EndpointAnnotation(ProtocolType.Tcp, uriScheme: "http", name: "http");
        resource.Annotations.Add(annotation);
 
        var endpointRef = new EndpointReference(resource, annotation);
 
        var ex = Assert.Throws<InvalidOperationException>(() => endpointRef.Port);
        Assert.Equal("The endpoint `http` is not allocated for the resource `test`.", ex.Message);
    }
 
    [Fact]
    public void Host_ThrowsWhenEndpointNotAllocated()
    {
        var resource = new TestResource("test");
        var annotation = new EndpointAnnotation(ProtocolType.Tcp, uriScheme: "http", name: "http");
        resource.Annotations.Add(annotation);
 
        var endpointRef = new EndpointReference(resource, annotation);
 
        var ex = Assert.Throws<InvalidOperationException>(() => endpointRef.Host);
        Assert.Equal("The endpoint `http` is not allocated for the resource `test`.", ex.Message);
    }
 
    [Fact]
    public void Url_ThrowsWhenEndpointNotAllocated()
    {
        var resource = new TestResource("test");
        var annotation = new EndpointAnnotation(ProtocolType.Tcp, uriScheme: "http", name: "http");
        resource.Annotations.Add(annotation);
 
        var endpointRef = new EndpointReference(resource, annotation);
 
        var ex = Assert.Throws<InvalidOperationException>(() => endpointRef.Url);
        Assert.Equal("The endpoint `http` is not allocated for the resource `test`.", ex.Message);
    }
 
    [Fact]
    public void Scheme_DoesNotThrowWhenEndpointNotAllocated()
    {
        var resource = new TestResource("test");
        var annotation = new EndpointAnnotation(ProtocolType.Tcp, uriScheme: "https", name: "https");
        resource.Annotations.Add(annotation);
 
        var endpointRef = new EndpointReference(resource, annotation);
 
        var scheme = endpointRef.Scheme;
        Assert.Equal("https", scheme);
    }
 
    [Fact]
    public void TargetPort_DoesNotThrowWhenStaticPortDefined()
    {
        var resource = new TestResource("test");
        var annotation = new EndpointAnnotation(ProtocolType.Tcp, uriScheme: "http", name: "http", targetPort: 5000);
        resource.Annotations.Add(annotation);
 
        var endpointRef = new EndpointReference(resource, annotation);
 
        var targetPort = endpointRef.TargetPort;
        Assert.Equal(5000, targetPort);
    }
 
    [Fact]
    public void TargetPort_ReturnsNullWhenNotDefined()
    {
        var resource = new TestResource("test");
        var annotation = new EndpointAnnotation(ProtocolType.Tcp, uriScheme: "http", name: "http");
        resource.Annotations.Add(annotation);
 
        var endpointRef = new EndpointReference(resource, annotation);
 
        var targetPort = endpointRef.TargetPort;
        Assert.Null(targetPort);
    }
 
    [Fact]
    public void AllocatedEndpoint_ThrowsWhenNetworkIdDoesNotMatch()
    {
        var annotation = new EndpointAnnotation(ProtocolType.Tcp, KnownNetworkIdentifiers.LocalhostNetwork, uriScheme: "http", name: "http");
 
        // Create an AllocatedEndpoint with a different network ID.
        var mismatchedEndpoint = new AllocatedEndpoint(
            annotation, "localhost", 8080,
            EndpointBindingMode.SingleAddress,
            targetPortExpression: null,
            networkId: KnownNetworkIdentifiers.DefaultAspireContainerNetwork);
 
        var ex = Assert.Throws<InvalidOperationException>(() => annotation.AllocatedEndpoint = mismatchedEndpoint);
    }
 
    [Theory]
    [InlineData(EndpointProperty.Url, ResourceKind.Host, ResourceKind.Host, "blah://localhost:1234")]
    [InlineData(EndpointProperty.Url, ResourceKind.Host, ResourceKind.Container, "blah://localhost:1234")]
    [InlineData(EndpointProperty.Url, ResourceKind.Container, ResourceKind.Host, "blah://host.docker.internal:1234")]
    [InlineData(EndpointProperty.Url, ResourceKind.Container, ResourceKind.Container, "blah://destination.dev.internal:4567")]
    [InlineData(EndpointProperty.Host, ResourceKind.Host, ResourceKind.Host, "localhost")]
    [InlineData(EndpointProperty.Host, ResourceKind.Host, ResourceKind.Container, "localhost")]
    [InlineData(EndpointProperty.Host, ResourceKind.Container, ResourceKind.Host, "host.docker.internal")]
    [InlineData(EndpointProperty.Host, ResourceKind.Container, ResourceKind.Container, "destination.dev.internal")]
    [InlineData(EndpointProperty.IPV4Host, ResourceKind.Host, ResourceKind.Host, "127.0.0.1")]
    [InlineData(EndpointProperty.IPV4Host, ResourceKind.Host, ResourceKind.Container, "127.0.0.1")]
    [InlineData(EndpointProperty.IPV4Host, ResourceKind.Container, ResourceKind.Host, "host.docker.internal")]
    [InlineData(EndpointProperty.IPV4Host, ResourceKind.Container, ResourceKind.Container, "destination.dev.internal")]
    [InlineData(EndpointProperty.Port, ResourceKind.Host, ResourceKind.Host, "1234")]
    [InlineData(EndpointProperty.Port, ResourceKind.Host, ResourceKind.Container, "1234")]
    [InlineData(EndpointProperty.Port, ResourceKind.Container, ResourceKind.Host, "1234")]
    [InlineData(EndpointProperty.Port, ResourceKind.Container, ResourceKind.Container, "4567")]
    [InlineData(EndpointProperty.Scheme, ResourceKind.Host, ResourceKind.Host, "blah")]
    [InlineData(EndpointProperty.Scheme, ResourceKind.Host, ResourceKind.Container, "blah")]
    [InlineData(EndpointProperty.Scheme, ResourceKind.Container, ResourceKind.Host, "blah")]
    [InlineData(EndpointProperty.Scheme, ResourceKind.Container, ResourceKind.Container, "blah")]
    [InlineData(EndpointProperty.HostAndPort, ResourceKind.Host, ResourceKind.Host, "localhost:1234")]
    [InlineData(EndpointProperty.HostAndPort, ResourceKind.Host, ResourceKind.Container, "localhost:1234")]
    [InlineData(EndpointProperty.HostAndPort, ResourceKind.Container, ResourceKind.Host, "host.docker.internal:1234")]
    [InlineData(EndpointProperty.HostAndPort, ResourceKind.Container, ResourceKind.Container, "destination.dev.internal:4567")]
    public async Task PropertyResolutionTest(EndpointProperty property, ResourceKind sourceKind, ResourceKind destinationKind, object expectedResult)
    {
        int port = 1234;
        int targetPort = 4567;
 
        var source = CreateResource("caller", sourceKind);
        var destination = CreateResource("destination", destinationKind);
 
        var network = source.GetDefaultResourceNetwork();
 
        // This logic is tightly coupled to how `DcpExecutor` allocates endpoints
        var annotation = new EndpointAnnotation(ProtocolType.Tcp, uriScheme: "blah", name: "http");
        annotation.AllocatedEndpoint = new(annotation, "localhost", port);
        destination.Annotations.Add(annotation);
 
        (string containerHost, int containerPort) = destination.IsContainer()
            ? ("destination.dev.internal", targetPort)
            : ("host.docker.internal", port);
 
        var containerEndpoint = new AllocatedEndpoint(annotation, containerHost, containerPort, EndpointBindingMode.SingleAddress, targetPortExpression: targetPort.ToString(), KnownNetworkIdentifiers.DefaultAspireContainerNetwork);
        annotation.AllAllocatedEndpoints.AddOrUpdateAllocatedEndpoint(KnownNetworkIdentifiers.DefaultAspireContainerNetwork, containerEndpoint);
 
        var expression = destination.GetEndpoint(annotation.Name).Property(property);
 
        var resultFromCaller = await expression.GetValueAsync(new ValueProviderContext
        {
            Caller = source
        });
        Assert.Equal(expectedResult, resultFromCaller);
 
        var resultFromNetwork = await expression.GetValueAsync(new ValueProviderContext
        {
            Network = network
        });
        Assert.Equal(expectedResult, resultFromNetwork);
 
        static IResourceWithEndpoints CreateResource(string name, ResourceKind kind)
        {
            if (kind == ResourceKind.Container)
            {
                var resource = new TestResource(name);
                resource.Annotations.Add(new ContainerImageAnnotation { Image = "test-image" });
                return resource;
            }
            else
            {
                return new TestResource(name);
            }
        }
    }
 
    [Fact]
    public async Task WaitingForAllocatedEndpointWorks()
    {
        var resource = new TestResource("test");
        var annotation = new EndpointAnnotation(ProtocolType.Tcp, uriScheme: "http", name: "http");
        resource.Annotations.Add(annotation);
        var endpointRef = new EndpointReference(resource, annotation);
 
        var waitStarted = new SemaphoreSlim(0, 1);
 
#pragma warning disable CA2012 // Use ValueTasks correctly
        var consumer = new WithWaitStartedNotification<string?>(waitStarted, endpointRef.GetValueAsync(CancellationToken.None).GetAwaiter());
#pragma warning restore CA2012 // Use ValueTasks correctly
 
        await Task.WhenAll
        (
            Task.Run(async() =>
            {
                await waitStarted.WaitAsync();
                var allocatedEndpoint = new AllocatedEndpoint(annotation, "localhost", 5000);
                annotation.AllAllocatedEndpoints.AddOrUpdateAllocatedEndpoint(KnownNetworkIdentifiers.LocalhostNetwork, allocatedEndpoint);
            }),
            Task.Run(async () =>
            {
                var url = await consumer;
                Assert.Equal("http://localhost:5000", url);
            })
        ).WaitAsync(TimeSpan.FromSeconds(10));
    }
 
    [Fact]
    public void EndpointAnnotation_ThrowsWhenEndpointNameNotDefined_ListsAvailableEndpoints()
    {
        var resource = new TestResource("api-boston");
        var httpEndpoint = new EndpointAnnotation(ProtocolType.Tcp, uriScheme: "https", name: "http");
        resource.Annotations.Add(httpEndpoint);
 
        var endpointRef = new EndpointReference(resource, "https");
 
        var ex = Assert.Throws<InvalidOperationException>(() => endpointRef.EndpointAnnotation);
        Assert.Contains("`https`", ex.Message);
        Assert.Contains("`api-boston`", ex.Message);
        Assert.Contains("Available endpoints", ex.Message);
        Assert.Contains("`http`", ex.Message);
    }
 
    [Fact]
    public void EndpointAnnotation_ThrowsWhenEndpointNameNotDefined_ListsMultipleAvailableEndpoints()
    {
        var resource = new TestResource("api");
        resource.Annotations.Add(new EndpointAnnotation(ProtocolType.Tcp, uriScheme: "http", name: "http"));
        resource.Annotations.Add(new EndpointAnnotation(ProtocolType.Tcp, uriScheme: "tcp", name: "grpc"));
 
        var endpointRef = new EndpointReference(resource, "https");
 
        var ex = Assert.Throws<InvalidOperationException>(() => endpointRef.EndpointAnnotation);
        Assert.Contains("`https`", ex.Message);
        Assert.Contains("`http`", ex.Message);
        Assert.Contains("`grpc`", ex.Message);
    }
 
    [Fact]
    public void EndpointAnnotation_ThrowsWhenResourceHasNoEndpoints_MessageIsClear()
    {
        var resource = new TestResource("api");
 
        var endpointRef = new EndpointReference(resource, "https");
 
        var ex = Assert.Throws<InvalidOperationException>(() => endpointRef.EndpointAnnotation);
        Assert.Contains("`https`", ex.Message);
        Assert.Contains("`api`", ex.Message);
        Assert.Contains("no endpoints defined", ex.Message);
    }
 
    [Fact]
    public void EndpointAnnotation_ThrowsWithCustomErrorMessageWhenSet()
    {
        var resource = new TestResource("api");
        resource.Annotations.Add(new EndpointAnnotation(ProtocolType.Tcp, uriScheme: "http", name: "http"));
 
        var endpointRef = new EndpointReference(resource, "https") { ErrorMessage = "custom message" };
 
        var ex = Assert.Throws<InvalidOperationException>(() => endpointRef.EndpointAnnotation);
        Assert.Equal("custom message", ex.Message);
    }
 
    [Fact]
    public void BrowserOriginsSelectsEndpointProperty()
    {
        var resource = new TestResource("frontend");
        var endpoint = new EndpointReference(resource, "http");
        var origins = endpoint.BrowserOrigins;
 
        Assert.Equal(EndpointProperty.BrowserOrigins, origins.Property);
        Assert.Equal(EndpointProperty.PrimaryBrowserOrigin, endpoint.PrimaryBrowserOrigin.Property);
        Assert.Equal("{frontend.bindings.http.primaryBrowserOrigin}", endpoint.PrimaryBrowserOrigin.ValueExpression);
        Assert.Same(endpoint, origins.Endpoint);
        Assert.Same(endpoint, Assert.Single(((IValueWithReferences)origins).References));
        Assert.Empty(resource.Annotations);
    }
 
    [Fact]
    public async Task BrowserOriginsKeepAllocatedEndpointAfterCustomOrigins()
    {
        var resource = new TestResource("frontend");
        resource.Annotations.Add(new EndpointAnnotation(ProtocolType.Tcp, uriScheme: "https"));
        var endpoint = resource.GetEndpoint("https");
        endpoint.EndpointAnnotation.AllocatedEndpoint = new(endpoint.EndpointAnnotation, "localhost", 8443);
        endpoint.EndpointAnnotation.BrowserUrlsSnapshot.SetValue(
        [
            new() { Url = "https://localhost:8443/original" },
            new() { Url = "https://public.example/path" },
            new() { Url = "https://alias.example/home" },
            new() { Url = "https://public.example/duplicate" }
        ]);
 
        Assert.Equal("https://public.example,https://alias.example,https://localhost:8443", await endpoint.BrowserOrigins.GetValueAsync());
        Assert.Equal("https://public.example", await endpoint.PrimaryBrowserOrigin.GetValueAsync());
    }
 
    [Theory]
    [InlineData("https", "https://localhost:8443")]
    [InlineData("tcp", "")]
    public async Task BrowserOriginsWithNoDisplayUrlsIncludeOnlyHttpAllocatedEndpoints(string scheme, string expected)
    {
        var resource = new TestResource("frontend");
        resource.Annotations.Add(new EndpointAnnotation(ProtocolType.Tcp, uriScheme: scheme));
        var endpoint = resource.GetEndpoint(scheme);
        endpoint.EndpointAnnotation.AllocatedEndpoint = new(endpoint.EndpointAnnotation, "localhost", 8443);
        endpoint.EndpointAnnotation.BrowserUrlsSnapshot.SetValue([]);
 
        Assert.Equal(expected, await endpoint.BrowserOrigins.GetValueAsync());
        Assert.Equal(expected, await endpoint.PrimaryBrowserOrigin.GetValueAsync());
    }
 
    [Fact]
    public async Task BrowserOriginsDeduplicateAllocatedEndpointAndFollowAllocationChanges()
    {
        var resource = new TestResource("frontend");
        resource.Annotations.Add(new EndpointAnnotation(ProtocolType.Tcp, uriScheme: "https"));
        var endpoint = resource.GetEndpoint("https");
        endpoint.EndpointAnnotation.AllocatedEndpoint = new(endpoint.EndpointAnnotation, "frontend.example", 443);
        endpoint.EndpointAnnotation.BrowserUrlsSnapshot.SetValue([new() { Url = "https://frontend.example/path" }]);
 
        Assert.Equal("https://frontend.example", await endpoint.BrowserOrigins.GetValueAsync());
 
        endpoint.EndpointAnnotation.AllocatedEndpoint = new(endpoint.EndpointAnnotation, "replacement.example", 443);
 
        Assert.Equal("https://frontend.example,https://replacement.example", await endpoint.BrowserOrigins.GetValueAsync());
        Assert.Equal("https://frontend.example", await endpoint.PrimaryBrowserOrigin.GetValueAsync());
    }
 
    [Fact]
    public async Task BrowserOriginsAreEndpointScoped()
    {
        var resource = new TestResource("frontend");
        resource.Annotations.Add(new EndpointAnnotation(ProtocolType.Tcp, uriScheme: "http"));
        resource.Annotations.Add(new EndpointAnnotation(ProtocolType.Tcp, uriScheme: "https"));
        resource.GetEndpoint("http").EndpointAnnotation.BrowserUrlsSnapshot.SetValue([new() { Url = "http://frontend.example" }]);
        resource.GetEndpoint("https").EndpointAnnotation.BrowserUrlsSnapshot.SetValue([new() { Url = "https://separate.example" }]);
 
        Assert.Equal("http://frontend.example", await resource.GetEndpoint("HTTP").BrowserOrigins.GetValueAsync());
        Assert.Equal("https://separate.example", await resource.GetEndpoint("https").BrowserOrigins.GetValueAsync());
    }
 
    [Fact]
    public async Task BrowserOriginsIgnoreServiceDiscoveryNetworkContext()
    {
        var resource = new TestResource("frontend");
        resource.Annotations.Add(new EndpointAnnotation(ProtocolType.Tcp, uriScheme: "http"));
        var host = resource.GetEndpoint("http", KnownNetworkIdentifiers.LocalhostNetwork);
        var container = resource.GetEndpoint("http", KnownNetworkIdentifiers.DefaultAspireContainerNetwork);
        host.EndpointAnnotation.BrowserUrlsSnapshot.SetValue([new() { Url = "https://public.example/app" }]);
 
        Assert.Equal("https://public.example", await host.BrowserOrigins.GetValueAsync());
        Assert.Equal("https://public.example", await container.BrowserOrigins.GetValueAsync());
        Assert.Equal("https://public.example", await host.PrimaryBrowserOrigin.GetValueAsync());
        Assert.Equal("https://public.example", await container.PrimaryBrowserOrigin.GetValueAsync(
            new ValueProviderContext { Network = KnownNetworkIdentifiers.DefaultAspireContainerNetwork }));
    }
 
    [Theory]
    [InlineData(EndpointProperty.BrowserOrigins, "http://preferred.example:8080,https://frontend.example,https://127.0.0.1:8443,https://frontend.example:9443")]
    [InlineData(EndpointProperty.PrimaryBrowserOrigin, "http://preferred.example:8080")]
    public async Task BrowserOriginPropertiesResolveFinalizedUrls(EndpointProperty property, string expected)
    {
        var resource = new TestResource("frontend");
        resource.Annotations.Add(new EndpointAnnotation(ProtocolType.Tcp, uriScheme: "http"));
        var endpoint = resource.GetEndpoint("http");
        endpoint.EndpointAnnotation.BrowserUrlsSnapshot.SetValue(
        [
            new() { Url = "http://preferred.example:8080/app" },
            new() { Url = "https://frontend.example/app" },
            new() { Url = "https://frontend.example:443/swagger" },
            new() { Url = "https://127.0.0.1:8443/" },
            new() { Url = "https://frontend.example:9443/" }
        ]);
 
        Assert.Equal(expected, await endpoint.Property(property).GetValueAsync());
    }
 
    [Theory]
    [InlineData("https://b\u00fccher.example:443/app?x=1#top", "https://xn--bcher-kva.example")]
    [InlineData("https://[::1]:443/app", "https://[::1]")]
    [InlineData("http://127.0.0.1:8080/app", "http://127.0.0.1:8080")]
    [InlineData("https://user:password@frontend.example:9443/app", "https://frontend.example:9443")]
    public async Task BrowserOriginsNormalizeAuthority(string url, string expected)
    {
        var resource = new TestResource("frontend");
        resource.Annotations.Add(new EndpointAnnotation(ProtocolType.Tcp, uriScheme: "http"));
        var endpoint = resource.GetEndpoint("http");
        endpoint.EndpointAnnotation.BrowserUrlsSnapshot.SetValue([new() { Url = url }]);
 
        Assert.Equal(expected, await endpoint.BrowserOrigins.GetValueAsync());
        Assert.Equal(expected, await endpoint.PrimaryBrowserOrigin.GetValueAsync());
    }
 
    [Theory]
    [InlineData(EndpointProperty.BrowserOrigins)]
    [InlineData(EndpointProperty.PrimaryBrowserOrigin)]
    public async Task BrowserOriginPropertiesSupportCancellation(EndpointProperty property)
    {
        var resource = new TestResource("frontend");
        resource.Annotations.Add(new EndpointAnnotation(ProtocolType.Tcp, uriScheme: "http"));
        using var cts = new CancellationTokenSource();
        var value = resource.GetEndpoint("http").Property(property).GetValueAsync(cts.Token);
        Assert.False(value.IsCompleted);
        cts.Cancel();
 
        await Assert.ThrowsAsync<TaskCanceledException>(async () => await value);
    }
 
    [Theory]
    [InlineData(EndpointProperty.BrowserOrigins)]
    [InlineData(EndpointProperty.PrimaryBrowserOrigin)]
    public async Task BrowserOriginPropertiesAwaitFinalizedUrlsRegardlessOfExecutionMode(EndpointProperty property)
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
        var frontend = builder.AddContainer("frontend", "image").WithHttpEndpoint(targetPort: 8080);
        var endpoint = frontend.GetEndpoint("http");
        var expression = endpoint.Property(property);
 
        using var cts = new CancellationTokenSource();
        var value = expression.GetValueAsync(new ValueProviderContext { ExecutionContext = builder.ExecutionContext }, cts.Token);
 
        Assert.False(value.IsCompleted);
        endpoint.EndpointAnnotation.BrowserUrlsSnapshot.SetValue([new() { Url = "https://frontend.example/app" }]);
        Assert.Equal("https://frontend.example",
            await value.AsTask().WaitAsync(TimeSpan.FromSeconds(10)));
    }
 
    [Fact]
    public async Task BrowserOriginDependencyDiscoveryDoesNotCreateParametersOrMutateReferences()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        using var publishingBuilder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
        var frontend = builder.AddContainer("frontend", "image").WithHttpEndpoint(targetPort: 8080);
        var endpoint = frontend.GetEndpoint("http");
        var origin = endpoint.BrowserOrigins;
        var backend = builder.AddContainer("backend", "image").WithEnvironment("ORIGIN", origin);
        endpoint.EndpointAnnotation.BrowserUrlsSnapshot.SetValue([new() { Url = "https://local.example" }]);
 
        foreach (var context in new[] { publishingBuilder.ExecutionContext, builder.ExecutionContext, publishingBuilder.ExecutionContext })
        {
            var dependencies = await backend.Resource.GetResourceDependenciesAsync(context);
            Assert.Empty(dependencies.OfType<ParameterResource>());
            Assert.Same(endpoint, Assert.Single(((IValueWithReferences)origin).References));
            Assert.Equal("https://local.example", await origin.GetValueAsync());
        }
    }
 
    public enum ResourceKind
    {
        Host,
        Container
    }
 
    private sealed class TestResource(string name) : Resource(name), IResourceWithEndpoints
    {
    }
 
    private struct WithWaitStartedNotification<T>
    {
        private readonly WaitStartedNotificationAwaiter<T> _awaiter;
 
        public WithWaitStartedNotification(SemaphoreSlim waitStarted, ValueTaskAwaiter<T> inner)
        {
            _awaiter = new WaitStartedNotificationAwaiter<T>(waitStarted, inner);
        }
        public WaitStartedNotificationAwaiter<T> GetAwaiter() => _awaiter;
    }
 
    private struct WaitStartedNotificationAwaiter<T>: INotifyCompletion
    {
        private readonly ValueTaskAwaiter<T> _inner;
        private readonly SemaphoreSlim _waitStarted;
 
        public WaitStartedNotificationAwaiter(SemaphoreSlim waitStarted, ValueTaskAwaiter<T> inner)
        {
            _waitStarted = waitStarted;
            _inner = inner;
        }
 
        public bool IsCompleted => false; // Force continuation
 
        public void OnCompleted(Action continuation)
        {
            _waitStarted.Release();
            _inner.OnCompleted(continuation);
        }
 
        public T GetResult() => _inner.GetResult();
    }
}