File: AzureWebPubSubEmulatorTests.cs
Web Access
Project: src\tests\Aspire.Hosting.Azure.Tests\Aspire.Hosting.Azure.Tests.csproj (Aspire.Hosting.Azure.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;
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Tests.Utils;
using Aspire.Hosting.Utils;
using Azure.Provisioning.WebPubSub;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Options;
 
namespace Aspire.Hosting.Azure.Tests;
 
public class AzureWebPubSubEmulatorTests(ITestOutputHelper testOutputHelper)
{
    [Theory]
    [InlineData(10001)]
    [InlineData(18080)]
    public async Task RunAsEmulatorProvidesLocalConnectionStrings(int port)
    {
        using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper);
        var webPubSub = builder.AddAzureWebPubSub("webpubsub").RunAsEmulator(emulator =>
            emulator.WithEndpoint("emulator", endpoint => endpoint.AllocatedEndpoint = new AllocatedEndpoint(endpoint, "localhost", port)));
        var hub = webPubSub.AddHub("chat-resource", "chat");
        var consumer = builder.AddExecutable("consumer", "unused", ".")
            .WithReference(webPubSub)
            .WithReference(hub);
 
        var env = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(consumer.Resource, serviceProvider: TestServiceProvider.Instance);
        var expectedConnectionString = $"Endpoint=http://localhost:{port};AccessKey=ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGH;Version=1.0";
        Assert.Equal(expectedConnectionString, env["ConnectionStrings__webpubsub"]);
        Assert.Equal($"{expectedConnectionString};Hub=chat", env["ConnectionStrings__chat-resource"]);
        Assert.Equal($"http://localhost:{port}", await webPubSub.Resource.UriExpression.GetValueAsync(CancellationToken.None));
 
        var properties = ((IResourceWithConnectionString)webPubSub.Resource).GetConnectionProperties().ToDictionary(p => p.Key, p => p.Value);
        Assert.Equal($"http://localhost:{port}", await properties["Uri"].GetValueAsync(CancellationToken.None));
 
        var image = Assert.Single(webPubSub.Resource.Annotations.OfType<ContainerImageAnnotation>());
        Assert.Equal("mcr.microsoft.com/webpubsub/emulator:1.0.0-beta.2", $"{image.Registry}/{image.Image}:{image.Tag}");
        Assert.Equal(8080, webPubSub.GetEndpoint("emulator").TargetPort);
    }
 
    [Fact]
    public async Task RunAsEmulatorConfiguresEventHandlersRegardlessOfCallOrder()
    {
        var emulatorFirst = await CreateEventHandlerConfigurationAsync(emulatorFirst: true);
        var emulatorLast = await CreateEventHandlerConfigurationAsync(emulatorFirst: false);
        Assert.Equal(emulatorFirst, emulatorLast);
        await VerifyJson(emulatorFirst);
    }
 
    private async Task<string> CreateEventHandlerConfigurationAsync(bool emulatorFirst)
    {
        using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper);
        var webPubSub = builder.AddAzureWebPubSub("webpubsub");
        var api = builder.AddExecutable("api", "unused", ".").WithHttpEndpoint();
        var endpoint = api.GetEndpoint("http").EndpointAnnotation;
        endpoint.AllocatedEndpoint = new AllocatedEndpoint(endpoint, "localhost", 5000);
        endpoint.AllAllocatedEndpoints.AddOrUpdateAllocatedEndpoint(
            KnownNetworkIdentifiers.DefaultAspireContainerNetwork,
            new AllocatedEndpoint(endpoint, "host.docker.internal", 5000, EndpointBindingMode.SingleAddress,
                targetPortExpression: null, networkId: KnownNetworkIdentifiers.DefaultAspireContainerNetwork));
 
        if (emulatorFirst)
        {
            webPubSub.RunAsEmulator();
        }
 
        webPubSub.AddHub("chat-resource", "chat")
            .AddEventHandler($"{api.GetEndpoint("http")}/events/{{hub}}/{{event}}", systemEvents: ["connect", "connected", "disconnected"])
            .AddEventHandler($"http://example.com/events", userEventPattern: "message");
        webPubSub.AddHub("notifications").AddEventHandler($"http://example.com/notifications");
        webPubSub.AddHub("special-hub", "chat__room").AddEventHandler($"http://example.com/special");
 
        if (!emulatorFirst)
        {
            webPubSub.RunAsEmulator();
        }
 
        var dependencies = await webPubSub.Resource.GetResourceDependenciesAsync(
            builder.ExecutionContext, ResourceDependencyDiscoveryMode.DirectOnly);
        Assert.Contains(api.Resource, dependencies);
 
        var env = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(webPubSub.Resource, serviceProvider: TestServiceProvider.Instance);
        Assert.Equal("ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGH", env["WebPubSub__AccessKey"]);
        return await GetEmulatorConfigurationAsync(webPubSub.Resource);
    }
 
    [Fact]
    public async Task RunAsEmulatorRejectsUnsupportedEventHandlerAuthentication()
    {
        using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper);
        var webPubSub = builder.AddAzureWebPubSub("webpubsub").RunAsEmulator();
        webPubSub.AddHub("chat").AddEventHandler($"http://example.com/events", authSettings: new UpstreamAuthSettings());
 
        var exception = await Assert.ThrowsAsync<NotSupportedException>(async () =>
            await GetEmulatorConfigurationAsync(webPubSub.Resource));
 
        Assert.Contains("does not support event handler authentication for hub 'chat'", exception.Message);
    }
 
    [Fact]
    public async Task RunAsEmulatorPreservesAzureManifestWhenPublishing()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, testOutputHelper);
        var webPubSub = builder.AddAzureWebPubSub("webpubsub");
        var hub = webPubSub.AddHub("chat").AddEventHandler($"https://example.com/events", systemEvents: ["connected"]);
        var original = await AzureManifestUtils.GetManifestWithBicep(webPubSub.Resource);
 
        webPubSub.RunAsEmulator();
 
        var manifest = await AzureManifestUtils.GetManifestWithBicep(webPubSub.Resource);
        Assert.Equal(original.ManifestNode.ToString(), manifest.ManifestNode.ToString());
        Assert.Equal(original.BicepText, manifest.BicepText);
        Assert.Equal("Endpoint={webpubsub.outputs.endpoint};Hub=chat", hub.Resource.ConnectionStringExpression.ValueExpression);
        Assert.Empty(webPubSub.Resource.Annotations.OfType<HealthCheckAnnotation>());
    }
 
    [Theory]
    [InlineData(HttpStatusCode.OK, HealthStatus.Healthy)]
    [InlineData(HttpStatusCode.ServiceUnavailable, HealthStatus.Unhealthy)]
    public async Task RunAsEmulatorChecksHealthWithGet(HttpStatusCode statusCode, HealthStatus expectedStatus)
    {
        using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper);
        var webPubSub = builder.AddAzureWebPubSub("webpubsub").RunAsEmulator(emulator =>
            emulator.WithEndpoint("emulator", endpoint => endpoint.AllocatedEndpoint = new AllocatedEndpoint(endpoint, "localhost", 18080)));
        var healthCheckKey = Assert.Single(webPubSub.Resource.Annotations.OfType<HealthCheckAnnotation>()).Key;
        builder.Services.AddHttpClient(healthCheckKey).ConfigurePrimaryHttpMessageHandler(() => new EmulatorHealthHandler(statusCode));
        using var app = builder.Build();
 
        var health = await app.Services.GetRequiredService<HealthCheckService>().CheckHealthAsync();
 
        Assert.Equal(expectedStatus, health.Entries[healthCheckKey].Status);
    }
 
    [Fact]
    public async Task RunAsEmulatorHealthWaitsForEndpointAllocation()
    {
        using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper);
        var webPubSub = builder.AddAzureWebPubSub("webpubsub").RunAsEmulator();
        var healthCheckKey = Assert.Single(webPubSub.Resource.Annotations.OfType<HealthCheckAnnotation>()).Key;
        builder.Services.AddHttpClient(healthCheckKey).ConfigurePrimaryHttpMessageHandler(() => new EmulatorHealthHandler(HttpStatusCode.OK));
        using var app = builder.Build();
        var registration = Assert.Single(app.Services.GetRequiredService<IOptions<HealthCheckServiceOptions>>().Value.Registrations,
            registration => registration.Name == healthCheckKey);
        using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
        var healthTask = registration.Factory(app.Services).CheckHealthAsync(new HealthCheckContext { Registration = registration }, cts.Token);
        Assert.False(healthTask.IsCompleted);
 
        var endpoint = webPubSub.GetEndpoint("emulator").EndpointAnnotation;
        endpoint.AllocatedEndpoint = new AllocatedEndpoint(endpoint, "localhost", 18080);
 
        var health = await healthTask;
        Assert.Equal(HealthStatus.Healthy, health.Status);
    }
 
    private sealed class EmulatorHealthHandler(HttpStatusCode statusCode) : HttpMessageHandler
    {
        protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            var matches = request.Method == HttpMethod.Get && request.RequestUri == new Uri("http://localhost:18080/api/health");
            return Task.FromResult(new HttpResponseMessage(matches ? statusCode : HttpStatusCode.MethodNotAllowed));
        }
    }
 
    private static async Task<string> GetEmulatorConfigurationAsync(AzureWebPubSubResource resource)
    {
        var annotation = Assert.Single(resource.Annotations.OfType<ContainerFileSystemCallbackAnnotation>());
        Assert.Equal("/app", annotation.DestinationPath);
        var files = await annotation.Callback(new ContainerFileSystemCallbackContext
        {
            Model = resource,
            Services = TestServiceProvider.Instance
        }, CancellationToken.None);
        var file = Assert.IsType<ContainerFile>(Assert.Single(files));
        Assert.Equal("appsettings.json", file.Name);
        return Assert.IsType<string>(file.Contents);
    }
}