File: Telemetry\DashboardTelemetryServiceTests.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 System.Net;
using System.Net.Http.Json;
using System.Security.Cryptography.X509Certificates;
using System.Text.Json;
using Aspire.Dashboard.Configuration;
using Aspire.Dashboard.Telemetry;
using Aspire.Tests.Shared.Telemetry;
using Microsoft.AspNetCore.InternalTesting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Logging.Testing;
using Microsoft.Extensions.Options;
using Xunit;
 
namespace Aspire.Dashboard.Tests.Telemetry;
 
public class DashboardTelemetryServiceTests(ITestOutputHelper output)
{
    [Fact]
    public async Task CreateTelemetryService_WithDebugSession_Optout_ShowsTelemetryUnsupported()
    {
        var options = new TestDashboardOptions(new DashboardOptions
        {
            DebugSession = new DebugSessionOptions
            {
                Port = 5000,
                Token = "test",
                TelemetryOptOut = true
            }
        });
 
        Assert.True(options.Value.DebugSession.TryParseOptions(out _));
 
        var telemetrySender = new DashboardTelemetrySender(options, NullLogger<DashboardTelemetrySender>.Instance);
        var telemetryService = await CreateTelemetryServiceAsync(telemetrySender);
 
        await telemetryService.InitializeAsync();
        Assert.False(telemetryService.IsTelemetryEnabled);
 
        var callbackExecuted = false;
        telemetrySender.QueueRequest(OperationContext.Empty, (_, _) =>
        {
            callbackExecuted = true;
            return Task.FromResult(0);
        });
 
        await telemetrySender.DisposeAsync();
        Assert.False(callbackExecuted, "Telemetry request should not be sent when telemetry is disabled.");
    }
 
    [Theory]
    [InlineData(true, 4)]
    [InlineData(false, 0)]
    public async Task CreateTelemetryService_WithoutValidDebugSession_ShowsTelemetryUnsupported(bool enabled, int expectCount)
    {
        var sender = new TestDashboardTelemetrySender { IsTelemetryEnabled = enabled };
        var telemetryService = await CreateTelemetryServiceAsync(sender);
 
        var context = telemetryService.PostUserTask("testTask", TelemetryResult.Success);
        telemetryService.PostOperation("testOperation", TelemetryResult.Success, correlatedWith: context.Properties);
 
        await sender.DisposeAsync();
 
        var count = 0;
        await foreach (var item in sender.ContextChannel.Reader.ReadAllAsync())
        {
            count++;
        }
 
        Assert.Equal(expectCount, count);
    }
 
    [Fact]
    public async Task WriteTelemetry_IntegrationTest_TelemetrySent()
    {
        var testSink = new TestSink();
        var loggerFactory = LoggerFactory.Create(b =>
        {
            b.AddProvider(new TestLoggerProvider(testSink));
            b.AddXunit(output);
        });
 
        var options = new TestDashboardOptions(new DashboardOptions
        {
            DebugSession = new DebugSessionOptions
            {
                Port = 5000,
                ServerCertificate = Convert.ToBase64String(TelemetryTestHelpers.GenerateDummyCertificate().Export(X509ContentType.Cert)),
                Token = "test"
            }
        });
 
        Assert.True(options.Value.DebugSession.TryParseOptions(out _));
 
        var userTaskCorrelationId = Guid.NewGuid();
        var tcs = new TaskCompletionSource<PostOperationRequest>(TaskCreationOptions.RunContinuationsAsynchronously);
 
        var telemetrySender = new DashboardTelemetrySender(options, loggerFactory.CreateLogger<DashboardTelemetrySender>())
        {
            CreateHandler = _ => new TestHttpMessageHandler(
                (request, _) =>
                {
                    if (request.RequestUri!.AbsolutePath == TelemetryEndpoints.TelemetryEnabled)
                    {
                        return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
                        {
                            Content = new StringContent(JsonSerializer.Serialize(new TelemetryEnabledResponse(IsEnabled: true)) ?? string.Empty)
                        });
                    }
                    else if (request.RequestUri!.AbsolutePath == TelemetryEndpoints.TelemetryStart)
                    {
                        return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK));
                    }
                    else if (request.RequestUri!.AbsolutePath == TelemetryEndpoints.TelemetryPostProperty)
                    {
                        return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK));
                    }
                    else if (request.RequestUri!.AbsolutePath == TelemetryEndpoints.TelemetryPostUserTask)
                    {
                        return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
                        {
                            Content = new StringContent(JsonSerializer.Serialize(new TelemetryEventCorrelation { Id = userTaskCorrelationId }))
                        });
                    }
                    else if (request.RequestUri!.AbsolutePath == TelemetryEndpoints.TelemetryPostOperation)
                    {
                        var requestContent = (JsonContent)request.Content!;
                        tcs.SetResult((PostOperationRequest)requestContent.Value!);
 
                        return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
                        {
                            Content = new StringContent(JsonSerializer.Serialize(new TelemetryEventCorrelation { Id = Guid.NewGuid() }))
                        });
                    }
                    else
                    {
                        throw new InvalidCastException($"Unexpected path: {request.RequestUri}");
                    }
                })
        };
 
        var telemetryService = await CreateTelemetryServiceAsync(telemetrySender, loggerFactory: loggerFactory);
        await telemetryService.InitializeAsync();
 
        var context = telemetryService.PostUserTask("testTask", TelemetryResult.Success);
        telemetryService.PostOperation("testOperation", TelemetryResult.Success, correlatedWith: context.Properties);
 
        var operationRequest = await tcs.Task.DefaultTimeout();
        var correlatedProperty = Assert.Single(operationRequest.CorrelatedWith!);
 
        Assert.Equal(userTaskCorrelationId, correlatedProperty.Id);
 
        await telemetrySender.DisposeAsync();
 
        Assert.False(testSink.Writes.Any(w => w.LogLevel >= LogLevel.Warning), "Test ran without any warnings or errors logged.");
    }
 
    private static async Task<DashboardTelemetryService> CreateTelemetryServiceAsync(IDashboardTelemetrySender? dashboardTelemetrySender = null, ILoggerFactory? loggerFactory = null)
    {
        loggerFactory ??= NullLoggerFactory.Instance;
 
        var telemetryService = new DashboardTelemetryService(loggerFactory.CreateLogger<DashboardTelemetryService>(), dashboardTelemetrySender ?? new TestDashboardTelemetrySender());
        await telemetryService.InitializeAsync();
 
        return telemetryService;
    }
 
    public class TestDashboardOptions(DashboardOptions value) : IOptions<DashboardOptions>
    {
        public DashboardOptions Value { get; } = value;
    }
}