File: Terminal\DefaultTerminalConnectionResolverTests.cs
Web Access
Project: src\tests\Aspire.Dashboard.Tests\Aspire.Dashboard.Tests.csproj (Aspire.Dashboard.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.Dashboard.Model;
using Aspire.Dashboard.Terminal;
using Aspire.Dashboard.Tests.Integration.Playwright.Infrastructure;
using Aspire.DashboardService.Proto.V1;
using Aspire.Tests.Shared.DashboardModel;
using Xunit;
using Value = Google.Protobuf.WellKnownTypes.Value;
 
namespace Aspire.Dashboard.Tests.Terminal;
 
public class DefaultTerminalConnectionResolverTests
{
    [Fact]
    public async Task ConnectAsync_WhenClientNotEnabled_ReturnsNull()
    {
        var client = new DisabledDashboardClient();
        var resolver = new DefaultTerminalConnectionResolver(client);
 
        var stream = await resolver.ConnectAsync("anything", 0, CancellationToken.None);
 
        Assert.Null(stream);
    }
 
    [Fact]
    public async Task ConnectAsync_WhenResourceNotFound_ReturnsNull()
    {
        var client = new MockDashboardClient(resources:
        [
            CreateTerminalResource("other-abc", displayName: "other", replicaIndex: 0, replicaCount: 1, udsPath: "/tmp/other-r0.sock"),
        ]);
        var resolver = new DefaultTerminalConnectionResolver(client);
 
        var stream = await resolver.ConnectAsync("missing", 0, CancellationToken.None);
 
        Assert.Null(stream);
    }
 
    [Fact]
    public async Task ConnectAsync_WhenReplicaIndexDoesNotMatch_ReturnsNull()
    {
        var client = new MockDashboardClient(resources:
        [
            CreateTerminalResource("svc-abc", displayName: "svc", replicaIndex: 0, replicaCount: 2, udsPath: "/tmp/svc-r0.sock"),
            CreateTerminalResource("svc-def", displayName: "svc", replicaIndex: 1, replicaCount: 2, udsPath: "/tmp/svc-r1.sock"),
        ]);
        var resolver = new DefaultTerminalConnectionResolver(client);
 
        var stream = await resolver.ConnectAsync("svc", 5, CancellationToken.None);
 
        Assert.Null(stream);
    }
 
    [Fact]
    public async Task ConnectAsync_WhenSnapshotMissingTerminalEnabledMarker_ReturnsNull()
    {
        var resource = ModelTestHelpers.CreateResource(
            resourceName: "svc-abc",
            displayName: "svc",
            properties: new Dictionary<string, ResourcePropertyViewModel>
            {
                [KnownProperties.Terminal.ReplicaIndex] = StringProperty(KnownProperties.Terminal.ReplicaIndex, "0"),
                [KnownProperties.Terminal.ReplicaCount] = StringProperty(KnownProperties.Terminal.ReplicaCount, "1"),
                [KnownProperties.Terminal.ConsumerUdsPath] = StringProperty(KnownProperties.Terminal.ConsumerUdsPath, "/tmp/svc.sock"),
            });
        var client = new MockDashboardClient(resources: [resource]);
        var resolver = new DefaultTerminalConnectionResolver(client);
 
        var stream = await resolver.ConnectAsync("svc", 0, CancellationToken.None);
 
        Assert.Null(stream);
    }
 
    [Fact]
    public async Task ConnectAsync_WhenSnapshotMissingConsumerUdsPath_ReturnsNull()
    {
        var resource = ModelTestHelpers.CreateResource(
            resourceName: "svc-abc",
            displayName: "svc",
            properties: new Dictionary<string, ResourcePropertyViewModel>
            {
                [KnownProperties.Terminal.Enabled] = StringProperty(KnownProperties.Terminal.Enabled, "true"),
                [KnownProperties.Terminal.ReplicaIndex] = StringProperty(KnownProperties.Terminal.ReplicaIndex, "0"),
                [KnownProperties.Terminal.ReplicaCount] = StringProperty(KnownProperties.Terminal.ReplicaCount, "1"),
            });
        var client = new MockDashboardClient(resources: [resource]);
        var resolver = new DefaultTerminalConnectionResolver(client);
 
        var stream = await resolver.ConnectAsync("svc", 0, CancellationToken.None);
 
        Assert.Null(stream);
    }
 
    [Fact]
    public async Task ConnectAsync_WhenUdsPathDoesNotExist_FailsToConnect()
    {
        // Resolver locates the snapshot and the path; the actual UDS connect throws
        // because the path is not a live socket. We just assert that the resolver
        // attempted the connection — transport errors bubble up to the WS proxy.
        var resource = CreateTerminalResource(
            resourceName: "svc-abc",
            displayName: "svc",
            replicaIndex: 0,
            replicaCount: 1,
            udsPath: Path.Combine(Path.GetTempPath(), "nonexistent-aspire-term-" + Guid.NewGuid().ToString("N") + ".sock"));
        var client = new MockDashboardClient(resources: [resource]);
        var resolver = new DefaultTerminalConnectionResolver(client);
 
        await Assert.ThrowsAnyAsync<Exception>(() => resolver.ConnectAsync("svc", 0, CancellationToken.None));
    }
 
    private static ResourceViewModel CreateTerminalResource(string resourceName, string displayName, int replicaIndex, int replicaCount, string udsPath)
    {
        return ModelTestHelpers.CreateResource(
            resourceName: resourceName,
            displayName: displayName,
            properties: new Dictionary<string, ResourcePropertyViewModel>
            {
                [KnownProperties.Terminal.Enabled] = StringProperty(KnownProperties.Terminal.Enabled, "true"),
                [KnownProperties.Terminal.ReplicaIndex] = StringProperty(KnownProperties.Terminal.ReplicaIndex, replicaIndex.ToString()),
                [KnownProperties.Terminal.ReplicaCount] = StringProperty(KnownProperties.Terminal.ReplicaCount, replicaCount.ToString()),
                [KnownProperties.Terminal.ConsumerUdsPath] = StringProperty(KnownProperties.Terminal.ConsumerUdsPath, udsPath),
            });
    }
 
    private static ResourcePropertyViewModel StringProperty(string name, string value)
    {
        return new ResourcePropertyViewModel(
            name,
            new Value { StringValue = value },
            isValueSensitive: false,
            knownProperty: null,
            sortOrder: 0,
            displayName: null,
            isHighlighted: false);
    }
 
    private sealed class DisabledDashboardClient : IDashboardClient
    {
        public bool IsEnabled => false;
        public Task WhenConnected => Task.CompletedTask;
        public string ApplicationName => "Disabled";
        public string? MinRequiredVersion => null;
        public DashboardConnectionState ConnectionState => DashboardConnectionState.Connected;
#pragma warning disable CS0067 // Event is never used - required by interface
        public event Action<DashboardConnectionState>? ConnectionStateChanged;
#pragma warning restore CS0067
        public Task ReconnectAsync() => Task.CompletedTask;
        public ValueTask DisposeAsync() => ValueTask.CompletedTask;
        public Task<ResourceCommandResponseViewModel> ExecuteResourceCommandAsync(string resourceName, string resourceType, CommandViewModel command, ExecuteResourceCommandOptions options, CancellationToken cancellationToken) => throw new NotImplementedException();
        public Task<string> UploadFileAsync(Stream fileStream, string fileName, long expectedSize, int interactionId, string inputName, CancellationToken cancellationToken) => throw new NotImplementedException();
        public IAsyncEnumerable<IReadOnlyList<ResourceLogLine>> SubscribeConsoleLogs(string resourceName, CancellationToken cancellationToken) => throw new NotImplementedException();
        public IAsyncEnumerable<IReadOnlyList<ResourceLogLine>> GetConsoleLogs(string resourceName, CancellationToken cancellationToken) => throw new NotImplementedException();
        public Task<ResourceViewModelSubscription> SubscribeResourcesAsync(CancellationToken cancellationToken) => throw new NotImplementedException();
        public IAsyncEnumerable<WatchInteractionsResponseUpdate> SubscribeInteractionsAsync(CancellationToken cancellationToken) => throw new NotImplementedException();
        public Task SendInteractionRequestAsync(WatchInteractionsRequestUpdate request, CancellationToken cancellationToken) => throw new NotImplementedException();
        public ResourceViewModel? GetResource(string resourceName) => null;
        public IReadOnlyList<ResourceViewModel> GetResources() => [];
    }
}