File: Model\DashboardClientTests.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.Configuration;
using Aspire.Dashboard.Utils;
using Aspire.DashboardService.Proto.V1;
using Google.Protobuf.WellKnownTypes;
using Grpc.Core;
using Microsoft.AspNetCore.InternalTesting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Xunit;
 
namespace Aspire.Dashboard.Tests.Model;
 
public sealed class DashboardClientTests
{
    private readonly IConfiguration _configuration;
    private readonly IOptions<DashboardOptions> _dashboardOptions;
 
    public DashboardClientTests()
    {
        _configuration = new ConfigurationManager();
 
        var options = new DashboardOptions
        {
            ResourceServiceClient =
            {
                AuthMode = ResourceClientAuthMode.Unsecured,
                Url = "http://localhost:12345"
            }
        };
        options.ResourceServiceClient.TryParseOptions(out _);
 
        _dashboardOptions = Options.Create(options);
    }
 
    [Fact]
    public async Task SubscribeResources_OnCancel_ChannelRemoved()
    {
        await using var instance = CreateResourceServiceClient();
        instance.SetInitialDataReceived();
 
        IDashboardClient client = instance;
 
        var cts = new CancellationTokenSource();
 
        Assert.Equal(0, instance.OutgoingResourceSubscriberCount);
 
        var (_, subscription) = await client.SubscribeResourcesAsync(CancellationToken.None).DefaultTimeout();
 
        Assert.Equal(1, instance.OutgoingResourceSubscriberCount);
 
        var readTask = Task.Run(async () =>
        {
            await foreach (var item in subscription.WithCancellation(cts.Token))
            {
            }
        });
 
        cts.Cancel();
 
        await TaskHelpers.WaitIgnoreCancelAsync(readTask).DefaultTimeout();
 
        Assert.Equal(0, instance.OutgoingResourceSubscriberCount);
    }
 
    [Fact]
    public async Task SubscribeResources_OnDispose_ChannelRemoved()
    {
        await using var instance = CreateResourceServiceClient();
        instance.SetInitialDataReceived();
 
        IDashboardClient client = instance;
 
        Assert.Equal(0, instance.OutgoingResourceSubscriberCount);
 
        var (_, subscription) = await client.SubscribeResourcesAsync(CancellationToken.None).DefaultTimeout();
 
        Assert.Equal(1, instance.OutgoingResourceSubscriberCount);
 
        var readTask = Task.Run(async () =>
        {
            await foreach (var item in subscription)
            {
            }
        });
 
        await instance.DisposeAsync().DefaultTimeout();
 
        Assert.Equal(0, instance.OutgoingResourceSubscriberCount);
 
        await TaskHelpers.WaitIgnoreCancelAsync(readTask).DefaultTimeout();
    }
 
    [Fact]
    public async Task SubscribeResources_ThrowsIfDisposed()
    {
        await using IDashboardClient client = CreateResourceServiceClient();
 
        await client.DisposeAsync().DefaultTimeout();
 
        await Assert.ThrowsAsync<ObjectDisposedException>(() => client.SubscribeResourcesAsync(CancellationToken.None)).DefaultTimeout();
    }
 
    [Fact]
    public async Task SubscribeResources_IncreasesSubscriberCount()
    {
        await using var instance = CreateResourceServiceClient();
        instance.SetInitialDataReceived();
 
        IDashboardClient client = instance;
 
        Assert.Equal(0, instance.OutgoingResourceSubscriberCount);
 
        _ = await client.SubscribeResourcesAsync(CancellationToken.None).DefaultTimeout();
 
        Assert.Equal(1, instance.OutgoingResourceSubscriberCount);
 
        await instance.DisposeAsync().DefaultTimeout();
 
        Assert.Equal(0, instance.OutgoingResourceSubscriberCount);
    }
 
    [Fact]
    public async Task SubscribeResources_HasInitialData_InitialDataReturned()
    {
        await using var instance = CreateResourceServiceClient();
 
        IDashboardClient client = instance;
 
        var cts = new CancellationTokenSource();
 
        var subscribeTask = client.SubscribeResourcesAsync(CancellationToken.None);
 
        Assert.False(subscribeTask.IsCompleted);
        Assert.Equal(0, instance.OutgoingResourceSubscriberCount);
 
        instance.SetInitialDataReceived([new Resource
        {
            Name = "test",
            CreatedAt = Timestamp.FromDateTime(DateTime.UtcNow),
        }]);
 
        var (initialData, subscription) = await subscribeTask.DefaultTimeout();
 
        Assert.Single(initialData);
    }
 
    [Fact]
    public async Task SubscribeInteractions_OnCancel_ChannelRemoved()
    {
        await using var instance = CreateResourceServiceClient();
 
        IDashboardClient client = instance;
 
        var cts = new CancellationTokenSource();
 
        Assert.Equal(0, instance.OutgoingInteractionSubscriberCount);
 
        var subscription = client.SubscribeInteractionsAsync(CancellationToken.None);
 
        Assert.Equal(1, instance.OutgoingInteractionSubscriberCount);
 
        var readTask = Task.Run(async () =>
        {
            await foreach (var item in subscription.WithCancellation(cts.Token))
            {
            }
        });
 
        cts.Cancel();
 
        await TaskHelpers.WaitIgnoreCancelAsync(readTask).DefaultTimeout();
 
        Assert.Equal(0, instance.OutgoingInteractionSubscriberCount);
    }
 
    [Fact]
    public async Task SubscribeInteractions_OnDispose_ChannelRemoved()
    {
        await using var instance = CreateResourceServiceClient();
 
        IDashboardClient client = instance;
 
        Assert.Equal(0, instance.OutgoingInteractionSubscriberCount);
 
        var subscription = client.SubscribeInteractionsAsync(CancellationToken.None);
 
        Assert.Equal(1, instance.OutgoingInteractionSubscriberCount);
 
        var readTask = Task.Run(async () =>
        {
            await foreach (var item in subscription)
            {
            }
        });
 
        await instance.DisposeAsync().DefaultTimeout();
 
        Assert.Equal(0, instance.OutgoingInteractionSubscriberCount);
 
        await TaskHelpers.WaitIgnoreCancelAsync(readTask).DefaultTimeout();
    }
 
    [Fact]
    public async Task SubscribeInteractions_ThrowsIfDisposed()
    {
        await using IDashboardClient client = CreateResourceServiceClient();
 
        await client.DisposeAsync().DefaultTimeout();
 
        Assert.Throws<ObjectDisposedException>(() => client.SubscribeInteractionsAsync(CancellationToken.None));
    }
 
    [Fact]
    public async Task SubscribeInteractions_IncreasesSubscriberCount()
    {
        await using var instance = CreateResourceServiceClient();
 
        IDashboardClient client = instance;
 
        Assert.Equal(0, instance.OutgoingInteractionSubscriberCount);
 
        _ = client.SubscribeInteractionsAsync(CancellationToken.None);
 
        Assert.Equal(1, instance.OutgoingInteractionSubscriberCount);
 
        await instance.DisposeAsync().DefaultTimeout();
 
        Assert.Equal(0, instance.OutgoingInteractionSubscriberCount);
    }
 
    [Fact]
    public async Task WhenConnected_InteractionMethodUnimplemented_InteractionWatchCompleted()
    {
        await using var instance = CreateResourceServiceClient();
        instance.SetDashboardServiceClient(new MockDashboardServiceClient());
 
        await instance.WhenConnected.DefaultTimeout();
 
        await instance.InteractionWatchCompleteTask.DefaultTimeout();
    }
 
    private sealed class MockDashboardServiceClient : Aspire.DashboardService.Proto.V1.DashboardService.DashboardServiceClient
    {
        public override AsyncDuplexStreamingCall<WatchInteractionsRequestUpdate, WatchInteractionsResponseUpdate> WatchInteractions(CallOptions options)
        {
            return new AsyncDuplexStreamingCall<WatchInteractionsRequestUpdate, WatchInteractionsResponseUpdate>(
                new ClientStreamWriter<WatchInteractionsRequestUpdate>(),
                new AsyncStreamReader<WatchInteractionsResponseUpdate>(),
                Task.FromResult(new Metadata()),
                () => new Status(StatusCode.Unimplemented, "Unimplemented!"),
                () => new Metadata(),
                () => { });
        }
 
        public override AsyncUnaryCall<ApplicationInformationResponse> GetApplicationInformationAsync(ApplicationInformationRequest request, CallOptions options)
        {
            return new AsyncUnaryCall<ApplicationInformationResponse>(
                Task.FromResult(new ApplicationInformationResponse
                {
                    ApplicationName = "TestApplication"
                }),
                Task.FromResult(new Metadata()),
                () => Status.DefaultSuccess,
                () => new Metadata(),
                () => { });
        }
 
        public override AsyncServerStreamingCall<WatchResourcesUpdate> WatchResources(WatchResourcesRequest request, CallOptions options)
        {
            return new AsyncServerStreamingCall<WatchResourcesUpdate>(
                new AsyncStreamReader<WatchResourcesUpdate>(),
                Task.FromResult(new Metadata()),
                () => Status.DefaultSuccess,
                () => new Metadata(),
                () => { });
        }
    }
 
    private sealed class AsyncStreamReader<T> : IAsyncStreamReader<T>
    {
        public T Current { get; } = default!;
 
        public Task<bool> MoveNext(CancellationToken cancellationToken)
        {
            return Task.FromResult(false);
        }
    }
 
    private sealed class ClientStreamWriter<T> : IClientStreamWriter<T>
    {
        public WriteOptions? WriteOptions { get; set; }
 
        public Task CompleteAsync()
        {
            throw new NotImplementedException();
        }
 
        public Task WriteAsync(T message)
        {
            throw new NotImplementedException();
        }
    }
 
    private DashboardClient CreateResourceServiceClient()
    {
        return new DashboardClient(NullLoggerFactory.Instance, _configuration, _dashboardOptions, new MockKnownPropertyLookup());
    }
}