File: DevTunnelMonitorTests.cs
Web Access
Project: src\tests\Aspire.Hosting.DevTunnels.Tests\Aspire.Hosting.DevTunnels.Tests.csproj (Aspire.Hosting.DevTunnels.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Eventing;
using Microsoft.AspNetCore.InternalTesting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Logging;
 
namespace Aspire.Hosting.DevTunnels.Tests;
 
public class DevTunnelMonitorTests
{
    [Fact]
    public async Task HostOutputAllocatesPortsWithoutQueryingServiceStatus()
    {
        using var test = new TestDevTunnelMonitor();
        await test.StartAsync();
        var health = test.Monitor.CheckHealthAsync(CancellationToken.None);
        await test.ReadyAsync();
        Assert.Equal(HealthStatus.Healthy, (await health.DefaultTimeout()).Status);
        Assert.Equal(KnownResourceStates.Running, test.Snapshot(test.Port).State?.Text);
        Assert.Equal(HealthStatus.Healthy, test.Snapshot(test.Port).HealthStatus);
        Assert.Equal("original-3000.usw2.devtunnels.ms", test.Port.TunnelEndpointAnnotation.AllocatedEndpoint?.Address);
        Assert.All(test.Client.Calls, c => Assert.Equal(nameof(IDevTunnelClient.GetAccessAsync), c.Method));
    }
 
    [Fact]
    public async Task UnrecognizedOutputWarnsOnceAndReconcilesReadiness()
    {
        using var test = new TestDevTunnelMonitor();
        test.Monitor.StartupLogTimeout = TimeSpan.Zero;
        await test.StartAsync();
        await test.LogAsync("Connection to host tunnel relay restored.");
        await test.LogAsync("A new output format");
        await test.LogAsync("Another unrecognized line");
        Assert.Equal(HealthStatus.Healthy, (await test.Monitor.CheckHealthAsync(CancellationToken.None).DefaultTimeout()).Status);
        Assert.Equal(HealthStatus.Healthy, test.Snapshot(test.Port).HealthStatus);
        Assert.Single(await test.LogsAsync(), l => l.Content.Contains("Some devtunnel output was not recognized", StringComparison.Ordinal));
        Assert.Single(test.Client.Calls, c => c.Method == nameof(IDevTunnelClient.GetTunnelAsync));
    }
 
    [Fact]
    public async Task AnotherHostCannotEstablishUnknownLocalReadiness()
    {
        using var test = new TestDevTunnelMonitor();
        test.Monitor.StartupLogTimeout = TimeSpan.Zero;
        test.Monitor.ReconciliationRetryInterval = TimeSpan.FromMinutes(1);
        var queried = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
        test.Client.GetTunnelCallback = (_, _) =>
        {
            queried.TrySetResult();
            return Task.FromResult(test.Client.TunnelStatus);
        };
        await test.StartAsync();
        await test.LogAsync("Unrecognized connection output.");
        var health = test.Monitor.CheckHealthAsync(CancellationToken.None);
        await queried.Task.DefaultTimeout();
        await test.App.ResourceNotifications.WaitForResourceAsync(test.Tunnel.Name,
            _ => test.Tunnel.LastKnownStatus?.HostConnections == 1).DefaultTimeout();
        await test.LogAsync("");
 
        Assert.False(health.IsCompleted);
        Assert.Equal(HealthStatus.Unhealthy, Assert.Single(test.Snapshot(test.Tunnel).HealthReports, r => r.Name == "tunnel-connection").Status);
        Assert.Equal(KnownResourceStates.NotStarted, test.Snapshot(test.Port).State?.Text);
        Assert.Null(test.Port.TunnelEndpointAnnotation.AllocatedEndpoint);
        Assert.All(test.Snapshot(test.Port).Urls, u => Assert.True(u.IsInactive));
 
        // Even after service metadata is available, an unrecognized local host is not enough.
        // This run must supply a connection observation before those URLs can become active.
        await test.LogAsync("Connection to host tunnel relay restored.");
        await health.DefaultTimeout();
        Assert.Equal(HealthStatus.Healthy, test.Snapshot(test.Port).HealthStatus);
    }
 
    [Theory]
    [InlineData("Executing command 'stop'.")]
    [InlineData("Executing command 'restart'.")]
    [InlineData("Successfully executed command 'start'.")]
    [InlineData("Error executing command 'restart'.\nSystem.Exception: command failure\n   at SomeMethod()")]
    [InlineData("Failure executing command 'start'. Error message: failed")]
    [InlineData("Command 'restart' was canceled.")]
    [InlineData("[sys] Starting process...\nUnprefixed system detail")]
    [InlineData("[Aspire dev tunnels] Failed to refresh tunnel access metadata.\nRetryableProvisioningException: Tunnel port not found\n   at SomeMethod()")]
    public async Task AspireDiagnosticEntriesDoNotTriggerOutputWarnings(string content)
    {
        using var test = new TestDevTunnelMonitor();
        await test.StartAsync();
        await test.ReadyAsync();
        await test.LogAsync(content);
        test.App.Services.GetRequiredService<ResourceLoggerService>().GetLogger(test.Tunnel)
            .LogInformation(DevTunnelMonitor.DiagnosticPrefix + "Log capture boundary.");
        Assert.Equal(0, (await test.LogsAsync()).Count(l => l.Content.Contains("Some devtunnel output was not recognized", StringComparison.Ordinal)));
 
        // Filtering a known source must not use up the once-per-start warning allowance.
        await test.LogAsync("An actual unknown CLI message");
        Assert.Single(await test.LogsAsync(), l => l.Content.Contains("Some devtunnel output was not recognized", StringComparison.Ordinal));
    }
 
    [Fact]
    public async Task ReconciliationRetriesUntilUnrecognizedOutputCanBeReplacedByServiceStatus()
    {
        var time = new TestDevTunnelTimeProvider();
        using var test = new TestDevTunnelMonitor(time);
        test.Monitor.StartupLogTimeout = TimeSpan.Zero;
        test.Monitor.ReconciliationRetryInterval = TimeSpan.FromSeconds(1);
        var firstReturned = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
        var retried = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
        var response = new TaskCompletionSource<DevTunnelStatus>(TaskCreationOptions.RunContinuationsAsynchronously);
        var callbackStarted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
        var callbackRelease = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
        test.App.Services.GetRequiredService<IDistributedApplicationEventing>().Subscribe<ResourceEndpointsAllocatedEvent>(test.Port, async (_, ct) =>
        {
            callbackStarted.TrySetResult();
            await callbackRelease.Task.WaitAsync(ct);
        });
        var count = 0;
        test.Client.GetTunnelCallback = (_, ct) =>
        {
            if (Interlocked.Increment(ref count) == 1)
            {
                firstReturned.TrySetResult();
                return Task.FromResult(test.Client.TunnelStatus with { HostConnections = 0 });
            }
            retried.TrySetResult();
            return response.Task.WaitAsync(ct);
        };
        await test.StartAsync();
        await test.LogAsync("Connection to host tunnel relay restored.");
        var health = test.Monitor.CheckHealthAsync(CancellationToken.None);
        await firstReturned.Task.DefaultTimeout();
        Assert.Equal(TimeSpan.FromSeconds(1), await time.ScheduledDelays.Reader.ReadAsync().AsTask().DefaultTimeout());
        time.Advance(TimeSpan.FromSeconds(1));
        await retried.Task.DefaultTimeout();
        Assert.False(health.IsCompleted);
        response.SetResult(test.Client.TunnelStatus);
        await callbackStarted.Task.DefaultTimeout();
        time.Advance(TimeSpan.FromMinutes(1));
        await test.LogAsync("");
        Assert.False(health.IsCompleted);
        Assert.Equal(2, count);
        callbackRelease.SetResult();
        await health.DefaultTimeout();
        Assert.Equal(HealthStatus.Healthy, test.Snapshot(test.Port).HealthStatus);
        Assert.Equal(2, count);
    }
 
    [Fact]
    public async Task AcceptsReadinessMessagesCombinedOnOneLine()
    {
        using var test = new TestDevTunnelMonitor();
        await test.StartAsync();
        await test.LogAsync("Hosting port: 3000; Connect via browser: https://original-3000.usw2.devtunnels.ms; Ready to accept connections for tunnel: mytunnel.usw2");
        await test.App.ResourceNotifications.WaitForResourceAsync(test.Port.Name, KnownResourceStates.Running).DefaultTimeout();
        Assert.Equal(HealthStatus.Healthy, test.Snapshot(test.Port).HealthStatus);
    }
 
    [Fact]
    public async Task ReconciliationUpdatesAndRemovesPreviouslyObservedPorts()
    {
        using var test = new TestDevTunnelMonitor();
        await test.StartAsync();
        await test.ReadyAsync();
        test.Client.TunnelStatus = test.Client.TunnelStatus with
        {
            Ports = [new(3000, "http") { PortUri = new("https://replacement-3000.usw2.devtunnels.ms/") }]
        };
        await test.Monitor.CheckHealthAsync(CancellationToken.None).DefaultTimeout();
        Assert.Equal("replacement-3000.usw2.devtunnels.ms", test.Port.TunnelEndpointAnnotation.AllocatedEndpoint?.Address);
        test.Client.TunnelStatus = test.Client.TunnelStatus with { Ports = [] };
        await test.Monitor.CheckHealthAsync(CancellationToken.None).DefaultTimeout();
        Assert.Equal(HealthStatus.Unhealthy, test.Snapshot(test.Port).HealthStatus);
        Assert.Null(test.Port.LastKnownStatus);
    }
 
    [Fact]
    public async Task MissingTunnelInvalidatesLogDerivedReadiness()
    {
        using var test = new TestDevTunnelMonitor();
        await test.StartAsync();
        await test.ReadyAsync();
        await test.App.ResourceNotifications.PublishUpdateAsync(test.Port, s => s with
        {
            Urls = [new("tunnel", test.Port.LastKnownStatus!.PortUri!.AbsoluteUri, false)]
        });
        test.Client.GetTunnelCallback = (_, _) => throw new DevTunnelNotFoundException("mytunnel.usw2", "Tunnel not found.");
 
        await test.Monitor.CheckHealthAsync(CancellationToken.None).DefaultTimeout();
 
        Assert.Equal(HealthStatus.Unhealthy, test.Snapshot(test.Port).HealthStatus);
        Assert.Equal(HealthStatus.Unhealthy, Assert.Single(test.Snapshot(test.Tunnel).HealthReports, r => r.Name == "tunnel-connection").Status);
        Assert.True(Assert.Single(test.Snapshot(test.Port).Urls).IsInactive);
        Assert.Null(test.Port.LastKnownStatus);
        Assert.Null(test.Tunnel.LastKnownStatus);
    }
 
    [Theory]
    [InlineData(false)]
    [InlineData(true)]
    public async Task EstablishedConnectionLossRequiresFreshLocalEvidence(bool deletion)
    {
        using var test = new TestDevTunnelMonitor();
        await test.StartAsync();
        await test.ReadyAsync();
        await test.Monitor.CheckHealthAsync(CancellationToken.None).DefaultTimeout();
        await test.App.ResourceNotifications.PublishUpdateAsync(test.Port, s => s with
        {
            Urls = [new("tunnel", "https://original-3000.usw2.devtunnels.ms/", false)]
        });
        var connectedStatus = test.Client.TunnelStatus;
        if (deletion)
        {
            test.Client.GetTunnelCallback = (_, _) => throw new DevTunnelNotFoundException("mytunnel.usw2", "Tunnel not found.");
        }
        else
        {
            test.Client.TunnelStatus = connectedStatus with { HostConnections = 0 };
        }
        await test.Monitor.CheckHealthAsync(CancellationToken.None).DefaultTimeout();
        Assert.Equal(HealthStatus.Unhealthy, test.Snapshot(test.Port).HealthStatus);
        Assert.True(Assert.Single(test.Snapshot(test.Port).Urls).IsInactive);
 
        // A replacement/foreign host is positive service metadata, not evidence of a local reconnect.
        test.Client.GetTunnelCallback = null;
        test.Client.TunnelStatus = connectedStatus;
        await test.Monitor.CheckHealthAsync(CancellationToken.None).DefaultTimeout();
        Assert.Equal(HealthStatus.Unhealthy, test.Snapshot(test.Port).HealthStatus);
        Assert.Equal(HealthStatus.Unhealthy, Assert.Single(test.Snapshot(test.Tunnel).HealthReports, r => r.Name == "tunnel-connection").Status);
        Assert.True(Assert.Single(test.Snapshot(test.Port).Urls).IsInactive);
 
        await test.LogAsync("Connection to host tunnel relay restored.");
        Assert.Equal(HealthStatus.Healthy, test.Snapshot(test.Port).HealthStatus);
        Assert.False(Assert.Single(test.Snapshot(test.Port).Urls).IsInactive);
    }
 
    [Fact]
    public async Task InitialZeroHostObservationDoesNotDiscardStartupEvidence()
    {
        using var test = new TestDevTunnelMonitor();
        await test.StartAsync();
        await test.ReadyAsync();
        var connectedStatus = test.Client.TunnelStatus;
        test.Client.TunnelStatus = connectedStatus with { HostConnections = 0 };
        await test.Monitor.CheckHealthAsync(CancellationToken.None).DefaultTimeout();
        Assert.Equal(HealthStatus.Unhealthy, test.Snapshot(test.Port).HealthStatus);
        test.Client.TunnelStatus = connectedStatus;
        await test.Monitor.CheckHealthAsync(CancellationToken.None).DefaultTimeout();
        Assert.Equal(HealthStatus.Healthy, test.Snapshot(test.Port).HealthStatus);
    }
 
    [Fact]
    public async Task CustomizedUrlsSurviveReadinessReconciliationAndReconnect()
    {
        using var test = new TestDevTunnelMonitor();
        test.App.Services.GetRequiredService<IDistributedApplicationEventing>().Subscribe<ResourceEndpointsAllocatedEvent>(test.Port,
            async (_, _) => await test.App.ResourceNotifications.PublishUpdateAsync(test.Port, s => s with
            {
                Urls = [
                    new("tunnel", "https://original-3000.usw2.devtunnels.ms/swagger?x=1#operations", false),
                    new("tunnel", "https://original-3000.usw2.devtunnels.ms/health?next=%2F", false),
                    new("tunnel", "https://docs.example/guide?x=1", false),
                    new(null, "https://original-3000-inspect.usw2.devtunnels.ms/requests?id=1", true) { DisplayProperties = new("Inspect") }
                ]
            }));
        await test.StartAsync();
        await test.ReadyAsync();
        var originalUrls = test.Snapshot(test.Port).Urls.Select(u => u.Url).ToArray();
        Assert.Equal([
            "https://original-3000.usw2.devtunnels.ms/swagger?x=1#operations",
            "https://original-3000.usw2.devtunnels.ms/health?next=%2F",
            "https://docs.example/guide?x=1",
            "https://original-3000-inspect.usw2.devtunnels.ms/requests?id=1"
        ], originalUrls);
        await test.Monitor.CheckHealthAsync(CancellationToken.None).DefaultTimeout();
        await test.LogAsync("Connection to host tunnel relay closed.");
        await test.LogAsync("Connection to host tunnel relay restored.");
        Assert.Equal(originalUrls, test.Snapshot(test.Port).Urls.Select(u => u.Url));
 
        test.Client.TunnelStatus = test.Client.TunnelStatus with
        {
            Ports = [new(3000, "http") { PortUri = new("https://replacement-3000.usw2.devtunnels.ms") }]
        };
        await test.Monitor.CheckHealthAsync(CancellationToken.None).DefaultTimeout();
        Assert.Equal([
            "https://replacement-3000.usw2.devtunnels.ms/swagger?x=1#operations",
            "https://replacement-3000.usw2.devtunnels.ms/health?next=%2F",
            "https://docs.example/guide?x=1",
            "https://replacement-3000-inspect.usw2.devtunnels.ms/requests?id=1"
        ], test.Snapshot(test.Port).Urls.Select(u => u.Url));
        Assert.All(test.Snapshot(test.Port).Urls, u => Assert.False(u.IsInactive));
    }
 
    [Fact]
    public async Task ServiceHostCountCannotOverrideLocalDisconnect()
    {
        using var test = new TestDevTunnelMonitor();
        await test.StartAsync();
        await test.ReadyAsync();
        await test.LogAsync("Connection to host tunnel relay closed. Another host for the tunnel has connected.");
        await test.Monitor.CheckHealthAsync(CancellationToken.None).DefaultTimeout();
        Assert.Equal(HealthStatus.Unhealthy, test.Snapshot(test.Port).HealthStatus);
        Assert.Equal(HealthStatus.Unhealthy, Assert.Single(test.Snapshot(test.Tunnel).HealthReports, r => r.Name == "tunnel-connection").Status);
        await test.LogAsync("Connection to host tunnel relay restored.");
        Assert.Equal(HealthStatus.Healthy, test.Snapshot(test.Port).HealthStatus);
    }
 
    [Theory]
    [InlineData(false)]
    [InlineData(true)]
    public async Task IncompleteMessageCannotHideHostTakeover(bool wrappedDisconnect)
    {
        using var test = new TestDevTunnelMonitor();
        await test.StartAsync();
        await test.ReadyAsync();
        await test.App.ResourceNotifications.PublishUpdateAsync(test.Port, s => s with
        {
            Urls = [new("tunnel", "https://original-3000.usw2.devtunnels.ms/", false)]
        });
        await test.LogAsync("Inspect network activity:");
        if (wrappedDisconnect)
        {
            await test.LogAsync("Connection");
            await test.LogAsync("to host tunnel relay");
            await test.LogAsync("closed. Another host for the tunnel has connected.");
        }
        else
        {
            await test.LogAsync("Connection to host tunnel relay closed. Another host for the tunnel has connected.");
        }
        Assert.Equal(HealthStatus.Unhealthy, test.Snapshot(test.Port).HealthStatus);
        Assert.True(Assert.Single(test.Snapshot(test.Port).Urls).IsInactive);
 
        await test.Monitor.CheckHealthAsync(CancellationToken.None).DefaultTimeout();
        Assert.Equal(HealthStatus.Unhealthy, test.Snapshot(test.Port).HealthStatus);
        Assert.True(Assert.Single(test.Snapshot(test.Port).Urls).IsInactive);
        await test.LogAsync("Connection to host tunnel relay restored.");
        Assert.Equal(HealthStatus.Healthy, test.Snapshot(test.Port).HealthStatus);
        Assert.False(Assert.Single(test.Snapshot(test.Port).Urls).IsInactive);
    }
 
    [Fact]
    public async Task ColoredCombinedMessagesCannotHideHostTakeover()
    {
        using var test = new TestDevTunnelMonitor();
        await test.StartAsync();
        await test.ReadyAsync();
        await test.App.ResourceNotifications.PublishUpdateAsync(test.Port, s => s with
        {
            Urls = [new("tunnel", "https://original-3000.usw2.devtunnels.ms/", false)]
        });
 
        await test.LogAsync("Connection to host tunnel relay restored.; \u001b[31mConnection to host tunnel relay closed.\u001b[0m");
        Assert.Equal(HealthStatus.Unhealthy, test.Snapshot(test.Port).HealthStatus);
        Assert.True(Assert.Single(test.Snapshot(test.Port).Urls).IsInactive);
        Assert.Equal(HealthStatus.Unhealthy, Assert.Single(test.Snapshot(test.Tunnel).HealthReports, r => r.Name == "tunnel-connection").Status);
 
        await test.Monitor.CheckHealthAsync(CancellationToken.None).DefaultTimeout();
        Assert.Equal(HealthStatus.Unhealthy, test.Snapshot(test.Port).HealthStatus);
        Assert.True(Assert.Single(test.Snapshot(test.Port).Urls).IsInactive);
        await test.LogAsync("\u001b[32mConnection to host tunnel relay restored.\u001b[0m");
        Assert.Equal(HealthStatus.Healthy, test.Snapshot(test.Port).HealthStatus);
        Assert.False(Assert.Single(test.Snapshot(test.Port).Urls).IsInactive);
    }
 
    [Fact]
    public async Task OlderReconciliationCannotUndoNewerLogObservations()
    {
        using var test = new TestDevTunnelMonitor();
        await test.StartAsync();
        await test.ReadyAsync();
        var called = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
        var response = new TaskCompletionSource<DevTunnelStatus>(TaskCreationOptions.RunContinuationsAsynchronously);
        test.Client.GetTunnelCallback = (_, ct) =>
        {
            called.TrySetResult();
            return response.Task.WaitAsync(ct);
        };
        var health = test.Monitor.CheckHealthAsync(CancellationToken.None);
        await called.Task.DefaultTimeout();
        await test.LogAsync("Connection to host tunnel relay closed.");
        response.SetResult(test.Client.TunnelStatus);
        await health.DefaultTimeout();
        Assert.Equal(HealthStatus.Unhealthy, test.Snapshot(test.Port).HealthStatus);
    }
 
    [Fact]
    public async Task LogsCanCompleteReadinessWhileInitialReconciliationIsInFlight()
    {
        using var test = new TestDevTunnelMonitor();
        test.Monitor.StartupLogTimeout = TimeSpan.Zero;
        var called = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
        var response = new TaskCompletionSource<DevTunnelStatus>(TaskCreationOptions.RunContinuationsAsynchronously);
        test.Client.GetTunnelCallback = (_, ct) =>
        {
            called.TrySetResult();
            return response.Task.WaitAsync(ct);
        };
        await test.StartAsync();
        var health = test.Monitor.CheckHealthAsync(CancellationToken.None);
        await called.Task.DefaultTimeout();
        await test.ReadyAsync();
        await health.DefaultTimeout();
        Assert.False(response.Task.IsCompleted);
        var subsequentHealth = test.Monitor.CheckHealthAsync(CancellationToken.None);
        await test.LogAsync("");
        response.SetResult(test.Client.TunnelStatus with { HostConnections = 0, Ports = [] });
        await subsequentHealth.DefaultTimeout();
        Assert.Equal(HealthStatus.Healthy, test.Snapshot(test.Port).HealthStatus);
    }
 
    [Fact]
    public async Task OverlappingHealthEvaluationsShareServiceQuery()
    {
        using var test = new TestDevTunnelMonitor();
        var called = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
        var response = new TaskCompletionSource<DevTunnelStatus>(TaskCreationOptions.RunContinuationsAsynchronously);
        test.Client.GetTunnelCallback = (_, ct) =>
        {
            called.TrySetResult();
            return response.Task.WaitAsync(ct);
        };
        await test.StartAsync();
        await test.ReadyAsync();
        var first = test.Monitor.CheckHealthAsync(CancellationToken.None);
        var second = test.Monitor.CheckHealthAsync(CancellationToken.None);
        await called.Task.DefaultTimeout();
        await test.LogAsync("");
        Assert.Single(test.Client.Calls, c => c.Method == nameof(IDevTunnelClient.GetTunnelAsync));
        response.SetResult(test.Client.TunnelStatus);
        await Task.WhenAll(first, second).DefaultTimeout();
    }
 
    [Fact]
    public async Task RestartRejectsReadinessFromPreviousRun()
    {
        using var test = new TestDevTunnelMonitor();
        await test.StartAsync();
        await test.ReadyAsync();
        await test.Monitor.StopAsync(CancellationToken.None).DefaultTimeout();
        Assert.Equal(KnownResourceStates.Finished, test.Snapshot(test.Port).State?.Text);
        await test.StartAsync();
        var health = test.Monitor.CheckHealthAsync(CancellationToken.None);
        await test.LogAsync("2000-01-01T00:00:00Z Ready to accept connections for tunnel: mytunnel.usw2");
        Assert.False(health.IsCompleted);
        Assert.Null(test.Port.LastKnownStatus);
        await test.ReadyAsync();
        await health.DefaultTimeout();
        Assert.Equal(HealthStatus.Healthy, test.Snapshot(test.Port).HealthStatus);
    }
 
    [Fact]
    public async Task StopCancelsPendingReadinessAndServiceQuery()
    {
        using var test = new TestDevTunnelMonitor();
        test.Monitor.StartupLogTimeout = TimeSpan.Zero;
        var called = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
        test.Client.GetTunnelCallback = async (_, ct) =>
        {
            called.TrySetResult();
            await Task.Delay(Timeout.Infinite, ct);
            return test.Client.TunnelStatus;
        };
        await test.StartAsync();
        var health = test.Monitor.CheckHealthAsync(CancellationToken.None);
        await called.Task.DefaultTimeout();
        await test.Monitor.StopAsync(CancellationToken.None).DefaultTimeout();
        await Assert.ThrowsAnyAsync<OperationCanceledException>(() => health).DefaultTimeout();
        Assert.Equal(KnownResourceStates.Finished, test.Snapshot(test.Port).State?.Text);
        Assert.Null(test.Tunnel.LastKnownStatus);
    }
 
    [Fact]
    public async Task FailedProcessStopsPortsWithoutStoppedEventOrFinalLog()
    {
        using var test = new TestDevTunnelMonitor();
        await test.StartAsync();
        var health = test.Monitor.CheckHealthAsync(CancellationToken.None);
        await test.App.ResourceNotifications.PublishUpdateAsync(test.Tunnel, s => s with { State = KnownResourceStates.FailedToStart });
        await test.App.ResourceNotifications.WaitForResourceAsync(test.Port.Name, KnownResourceStates.Finished).DefaultTimeout();
        await Assert.ThrowsAnyAsync<OperationCanceledException>(() => health).DefaultTimeout();
        Assert.Null(test.Port.LastKnownStatus);
    }
 
    [Fact]
    public async Task CancellingAHealthCallerDoesNotCancelTheSharedObserver()
    {
        using var test = new TestDevTunnelMonitor();
        using var cts = new CancellationTokenSource();
        await test.StartAsync();
        var health = test.Monitor.CheckHealthAsync(cts.Token);
        await test.LogAsync("");
        await cts.CancelAsync();
        await Assert.ThrowsAnyAsync<OperationCanceledException>(() => health);
        await test.ReadyAsync();
        Assert.Equal(HealthStatus.Healthy, test.Snapshot(test.Port).HealthStatus);
    }
 
    [Fact]
    public async Task AccessQueriesDoNotBlockReadiness()
    {
        using var test = new TestDevTunnelMonitor();
        var queried = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
        var response = new TaskCompletionSource<DevTunnelAccessStatus>(TaskCreationOptions.RunContinuationsAsynchronously);
        test.Client.GetAccessCallback = (_, ct) =>
        {
            queried.TrySetResult();
            return response.Task.WaitAsync(ct);
        };
        await test.StartAsync();
        var health = test.Monitor.CheckHealthAsync(CancellationToken.None);
        await test.ReadyAsync();
        await queried.Task.DefaultTimeout();
        await health.DefaultTimeout();
        Assert.Equal(HealthStatus.Healthy, test.Snapshot(test.Port).HealthStatus);
        response.SetResult(test.Client.AccessStatus);
        await test.ReconcileAsync();
        Assert.Same(test.Client.AccessStatus, test.Port.LastKnownAccessStatus);
    }
 
    [Fact]
    public async Task StalledAccessRefreshDoesNotBlockSubsequentStatusQueries()
    {
        using var test = new TestDevTunnelMonitor();
        var queried = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
        var response = new TaskCompletionSource<DevTunnelAccessStatus>(TaskCreationOptions.RunContinuationsAsynchronously);
        var accessQueries = 0;
        test.Client.GetAccessCallback = (_, ct) =>
        {
            if (Interlocked.Increment(ref accessQueries) == 2)
            {
                queried.TrySetResult();
            }
            return response.Task.WaitAsync(ct);
        };
        await test.StartAsync();
        await test.ReadyAsync();
        await queried.Task.DefaultTimeout();
        await test.Monitor.CheckHealthAsync(CancellationToken.None).DefaultTimeout();
        Assert.False(response.Task.IsCompleted);
 
        test.Client.TunnelStatus = test.Client.TunnelStatus with { Ports = [] };
        await test.Monitor.CheckHealthAsync(CancellationToken.None).DefaultTimeout();
        await test.Monitor.CheckHealthAsync(CancellationToken.None).DefaultTimeout();
        Assert.Equal(3, test.Client.Calls.Count(c => c.Method == nameof(IDevTunnelClient.GetTunnelAsync)));
        Assert.Equal(2, accessQueries);
        Assert.Equal(HealthStatus.Unhealthy, test.Snapshot(test.Port).HealthStatus);
        Assert.Null(test.Port.LastKnownStatus);
        Assert.False(response.Task.IsCompleted);
 
        // Shared access work still belongs to the run and is canceled/tracked during stop.
        await test.Monitor.StopAsync(CancellationToken.None).DefaultTimeout();
        Assert.Equal(KnownResourceStates.Finished, test.Snapshot(test.Port).State?.Text);
    }
 
    [Theory]
    [InlineData(true)]
    [InlineData(false)]
    public async Task AccessQueryFailuresClearOnlyFailedResults(bool missingPort)
    {
        using var test = new TestDevTunnelMonitor(includeSecondPort: true);
        var otherPort = Assert.Single(test.Tunnel.Ports, p => p.TargetEndpoint.EndpointName == "other");
        test.Client.TunnelStatus = test.Client.TunnelStatus with
        {
            Ports = [
                .. test.Client.TunnelStatus.Ports,
                new(3001, "http") { PortUri = new("https://original-3001.usw2.devtunnels.ms/") }
            ]
        };
        await test.StartAsync();
        await test.ReadyAsync();
        await test.ReconcileAsync();
        Assert.Equal("Denied", Assert.Single(test.Snapshot(test.Port).Properties, p => p.Name == "Anonymous access").Value);
        Assert.Equal("Denied", Assert.Single(test.Snapshot(otherPort).Properties, p => p.Name == "Anonymous access").Value);
 
        var allowed = new DevTunnelAccessStatus
        {
            AccessControlEntries = [new("Anonymous", false, true, [], ["connect"])]
        };
        test.Client.GetAccessCallback = (number, _) =>
        {
            if (number == (missingPort ? 3001 : (int?)null))
            {
                throw new DistributedApplicationException("Access query failed.");
            }
            return Task.FromResult(allowed);
        };
        await test.ReconcileAsync();
 
        Assert.Same(allowed, test.Port.LastKnownAccessStatus);
        Assert.Equal("Allowed", Assert.Single(test.Snapshot(test.Port).Properties, p => p.Name == "Anonymous access").Value);
        if (missingPort)
        {
            Assert.Same(allowed, test.Tunnel.LastKnownAccessStatus);
            Assert.Null(otherPort.LastKnownAccessStatus);
            Assert.Equal(0, test.Snapshot(otherPort).Properties.Count(p => p.Name == "Anonymous access"));
        }
        else
        {
            Assert.Null(test.Tunnel.LastKnownAccessStatus);
            Assert.Same(allowed, otherPort.LastKnownAccessStatus);
            Assert.Equal("Allowed", Assert.Single(test.Snapshot(otherPort).Properties, p => p.Name == "Anonymous access").Value);
        }
        Assert.Equal(0, (await test.LogsAsync()).Count(l => l.Content.Contains("Some devtunnel output was not recognized", StringComparison.Ordinal)));
 
        test.Client.GetAccessCallback = (_, _) => Task.FromResult(allowed);
        await test.ReconcileAsync();
        Assert.Same(allowed, test.Tunnel.LastKnownAccessStatus);
        Assert.Equal("Allowed", Assert.Single(test.Snapshot(otherPort).Properties, p => p.Name == "Anonymous access").Value);
    }
 
    [Fact]
    public async Task AccessPolicyLogsOnlyEffectiveChanges()
    {
        using var test = new TestDevTunnelMonitor();
        await test.StartAsync();
        await test.ReadyAsync();
        await test.ReconcileAsync();
        await test.ReconcileAsync();
        Assert.Single(await test.LogsAsync(test.Port), l => l.Content.Contains("Anonymous access is not allowed", StringComparison.Ordinal));
 
        test.Client.AccessStatus = new()
        {
            AccessControlEntries = [new("Anonymous", false, true, [], ["connect"])]
        };
        await test.ReconcileAsync();
        test.Client.AccessStatus = new()
        {
            AccessControlEntries = [new("Anonymous", false, false, [], ["connect"])]
        };
        await test.ReconcileAsync();
        Assert.Single(await test.LogsAsync(test.Port), l => l.Content.Contains("!! Anonymous access is allowed", StringComparison.Ordinal));
        Assert.Equal("Allowed", Assert.Single(test.Snapshot(test.Port).Properties, p => p.Name == "Anonymous access").Value);
 
        test.Client.GetAccessCallback = (_, _) => throw new DistributedApplicationException("Access query failed.");
        await test.ReconcileAsync();
        Assert.Equal(0, test.Snapshot(test.Port).Properties.Count(p => p.Name == "Anonymous access"));
        test.Client.GetAccessCallback = null;
        await test.ReconcileAsync();
        Assert.Equal(2, (await test.LogsAsync(test.Port)).Count(l => l.Content.Contains("!! Anonymous access is allowed", StringComparison.Ordinal)));
    }
 
    [Fact]
    public async Task ExpirationAndInverseSemanticsFlowToAccessProperties()
    {
        var time = new TestDevTunnelTimeProvider();
        using var test = new TestDevTunnelMonitor(time);
        var expiration = time.GetUtcNow().AddMinutes(1);
        test.Client.AccessStatus = new()
        {
            AccessControlEntries = [
                new("Anonymous", false, true, [], ["connect"]),
                new("Anonymous", true, false, [], ["connect"]) { Expiration = expiration },
                new("Anonymous", true, false, [], ["connect"]) { IsInverse = true }
            ]
        };
        await test.StartAsync();
        await test.ReadyAsync();
        await test.ReconcileAsync();
        Assert.Equal("Denied", Assert.Single(test.Snapshot(test.Port).Properties, p => p.Name == "Anonymous access").Value);
        time.Advance(TimeSpan.FromMinutes(1));
        await test.ReconcileAsync();
        Assert.Equal("Allowed", Assert.Single(test.Snapshot(test.Port).Properties, p => p.Name == "Anonymous access").Value);
    }
 
    [Fact]
    public async Task AuthenticationNotificationDoesNotBlockReconciliation()
    {
        using var test = new TestDevTunnelMonitor();
        test.Interaction.IsAvailable = true;
        test.Client.LoginStatus = new("Logged out", LoginProvider.Microsoft, "");
        test.Client.GetAccessCallback = (_, _) => throw new InvalidOperationException("Authentication expired.");
        await test.StartAsync();
        var health = test.Monitor.CheckHealthAsync(CancellationToken.None);
        await test.ReadyAsync();
        var notification = await test.Interaction.Interactions.Reader.ReadAsync().AsTask().DefaultTimeout();
        try
        {
            await health.DefaultTimeout();
            await test.Monitor.CheckHealthAsync(CancellationToken.None).DefaultTimeout();
            Assert.False(notification.CompletionTcs.Task.IsCompleted);
            Assert.Equal(HealthStatus.Healthy, test.Snapshot(test.Port).HealthStatus);
        }
        finally
        {
            notification.CompletionTcs.TrySetResult(InteractionResult.Ok(true));
        }
    }
 
    [Fact]
    public async Task EndpointCallbackCanStopAndRestartParentWithoutBlockingMonitor()
    {
        using var test = new TestDevTunnelMonitor();
        var stopped = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
        var callbacks = 0;
        test.App.Services.GetRequiredService<IDistributedApplicationEventing>().Subscribe<ResourceEndpointsAllocatedEvent>(test.Port, async (_, ct) =>
        {
            Interlocked.Increment(ref callbacks);
            await test.Monitor.StopAsync(ct);
            stopped.TrySetResult();
        });
        await test.StartAsync();
        var health = test.Monitor.CheckHealthAsync(CancellationToken.None);
        await test.LogAsync("Hosting port 3000 at https://original-3000.usw2.devtunnels.ms\nReady to accept connections for tunnel: mytunnel.usw2");
        await stopped.Task.DefaultTimeout();
        await Assert.ThrowsAnyAsync<OperationCanceledException>(() => health);
        Assert.Equal(KnownResourceStates.Finished, test.Snapshot(test.Port).State?.Text);
        await test.StartAsync();
        await test.ReadyAsync();
        Assert.Equal(1, callbacks);
    }
 
    [Fact]
    public async Task EndpointCallbackFailureCannotBecomeHealthyThroughReconciliation()
    {
        using var test = new TestDevTunnelMonitor();
        test.App.Services.GetRequiredService<IDistributedApplicationEventing>().Subscribe<ResourceEndpointsAllocatedEvent>(test.Port, (_, _) =>
            throw new InvalidOperationException("Endpoint initialization failed."));
        await test.StartAsync();
        var health = test.Monitor.CheckHealthAsync(CancellationToken.None);
        await test.LogAsync("Hosting port 3000 at https://original-3000.usw2.devtunnels.ms\nReady to accept connections for tunnel: mytunnel.usw2");
        var error = await Assert.ThrowsAsync<InvalidOperationException>(() => health).DefaultTimeout();
        Assert.Equal("Endpoint initialization failed.", error.Message);
        Assert.Equal(HealthStatus.Unhealthy, Assert.Single(test.Snapshot(test.Port).HealthReports).Status);
        await Assert.ThrowsAsync<InvalidOperationException>(() => test.Monitor.CheckHealthAsync(CancellationToken.None)).DefaultTimeout();
    }
 
    [Fact]
    public async Task ReconciliationDoesNotDependOnConsoleUrlNamingConvention()
    {
        using var test = new TestDevTunnelMonitor();
        test.Monitor.StartupLogTimeout = TimeSpan.Zero;
        test.Client.TunnelStatus = test.Client.TunnelStatus with
        {
            Ports = [new(3000, "http") { PortUri = new("https://service-assigned-name.usw2.devtunnels.ms") }]
        };
        await test.StartAsync();
        await test.LogAsync("Connection to host tunnel relay restored.");
        await test.Monitor.CheckHealthAsync(CancellationToken.None).DefaultTimeout();
        Assert.Equal(HealthStatus.Healthy, test.Snapshot(test.Port).HealthStatus);
        Assert.Equal("service-assigned-name.usw2.devtunnels.ms", test.Port.TunnelEndpointAnnotation.AllocatedEndpoint?.Address);
    }
 
    [Fact]
    public async Task DisposedMonitorRejectsNewWork()
    {
        using var test = new TestDevTunnelMonitor();
        await test.StartAsync();
        await test.Monitor.DisposeAsync();
        await Assert.ThrowsAsync<ObjectDisposedException>(() => test.Monitor.CheckHealthAsync(CancellationToken.None));
    }
 
    [Fact]
    public async Task DisposalCancelsIndependentAccessQueries()
    {
        using var test = new TestDevTunnelMonitor(includeSecondPort: true);
        var called = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
        var response = new TaskCompletionSource<DevTunnelAccessStatus>(TaskCreationOptions.RunContinuationsAsynchronously);
        var queries = 0;
        test.Client.GetAccessCallback = (_, ct) =>
        {
            if (Interlocked.Increment(ref queries) == 3)
            {
                called.TrySetResult();
            }
            return response.Task.WaitAsync(ct);
        };
        await test.StartAsync();
        await test.ReadyAsync();
        await called.Task.DefaultTimeout();
        await test.Monitor.DisposeAsync().AsTask().DefaultTimeout();
        Assert.Equal(3, queries);
        Assert.False(response.Task.IsCompleted);
        await Assert.ThrowsAsync<ObjectDisposedException>(() => test.Monitor.CheckHealthAsync(CancellationToken.None));
    }
}