File: Commands\TelemetryTestHelper.cs
Web Access
Project: src\tests\Aspire.Cli.Tests\Aspire.Cli.Tests.csproj (Aspire.Cli.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.Text.Json;
using Aspire.Cli.Backchannel;
using Aspire.Cli.Otlp;
using Aspire.Cli.Tests.TestServices;
using Aspire.Cli.Tests.Utils;
using Aspire.Otlp.Serialization;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
 
namespace Aspire.Cli.Tests.Commands;
 
internal static class TelemetryTestHelper
{
    /// <summary>
    /// A fixed base time used by telemetry tests. All test timestamps should be expressed
    /// as offsets from this value (e.g. <c>s_testTime.AddMilliseconds(50)</c>).
    /// </summary>
    internal static readonly DateTime s_testTime = new(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
 
    /// <summary>
    /// Converts a <see cref="DateTime"/> to Unix nanoseconds (nanoseconds since the Unix epoch).
    /// </summary>
    internal static ulong DateTimeToUnixNanoseconds(DateTime dateTime)
    {
        var unixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
        var timeSinceEpoch = dateTime.ToUniversalTime() - unixEpoch;
 
        return (ulong)timeSinceEpoch.Ticks * 100;
    }
 
    /// <summary>
    /// Creates an <see cref="OtlpResourceJson"/> with the specified service name and optional instance ID.
    /// </summary>
    internal static OtlpResourceJson CreateOtlpResource(string serviceName, string? instanceId)
    {
        var attrs = new List<OtlpKeyValueJson>
        {
            new() { Key = "service.name", Value = new OtlpAnyValueJson { StringValue = serviceName } },
        };
        if (instanceId is not null)
        {
            attrs.Add(new() { Key = "service.instance.id", Value = new OtlpAnyValueJson { StringValue = instanceId } });
        }
        return new OtlpResourceJson { Attributes = [.. attrs] };
    }
 
    /// <summary>
    /// Creates a fully configured <see cref="ServiceProvider"/> for telemetry command tests,
    /// with a mock backchannel and HTTP handler that serves resource and telemetry data.
    /// </summary>
    /// <param name="workspace">The temporary workspace for the test.</param>
    /// <param name="outputHelper">The xUnit test output helper.</param>
    /// <param name="outputWriter">The test output writer to capture console output.</param>
    /// <param name="resources">The resource list returned by the /api/telemetry/resources endpoint.</param>
    /// <param name="telemetryEndpoints">
    /// A dictionary mapping URL substrings (e.g. "/api/telemetry/logs") to their JSON response content.
    /// </param>
    internal static ServiceProvider CreateTelemetryTestServices(
        TemporaryWorkspace workspace,
        ITestOutputHelper outputHelper,
        TestOutputTextWriter outputWriter,
        ResourceInfoJson[] resources,
        Dictionary<string, string> telemetryEndpoints)
    {
        var resourcesJson = JsonSerializer.Serialize(resources, OtlpCliJsonSerializerContext.Default.ResourceInfoJsonArray);
 
        var monitor = new TestAuxiliaryBackchannelMonitor();
        var connection = new TestAppHostAuxiliaryBackchannel
        {
            IsInScope = true,
            AppHostInfo = new AppHostInformation
            {
                AppHostPath = Path.Combine(workspace.WorkspaceRoot.FullName, "TestAppHost", "TestAppHost.csproj"),
                ProcessId = 1234
            },
            DashboardInfoResponse = new GetDashboardInfoResponse
            {
                ApiBaseUrl = "http://localhost:18888",
                ApiToken = "test-token",
                DashboardUrls = ["http://localhost:18888/login?t=test"],
                IsHealthy = true
            }
        };
        monitor.AddConnection("hash1", "socket.hash1", connection);
 
        var handler = new MockHttpMessageHandler(request =>
        {
            var url = request.RequestUri!.ToString();
            if (url.Contains("/api/telemetry/resources"))
            {
                return new HttpResponseMessage(HttpStatusCode.OK)
                {
                    Content = new StringContent(resourcesJson, System.Text.Encoding.UTF8, "application/json")
                };
            }
 
            foreach (var (urlPattern, json) in telemetryEndpoints)
            {
                if (url.Contains(urlPattern))
                {
                    return new HttpResponseMessage(HttpStatusCode.OK)
                    {
                        Content = new StringContent(json, System.Text.Encoding.UTF8, "application/json")
                    };
                }
            }
 
            return new HttpResponseMessage(HttpStatusCode.NotFound);
        });
 
        var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
        {
            options.AuxiliaryBackchannelMonitorFactory = _ => monitor;
            options.OutputTextWriter = outputWriter;
            options.DisableAnsi = true;
        });
 
        // Register the handler as a singleton so it is disposed with the ServiceProvider,
        // rather than at the end of this method (which would cause ObjectDisposedException).
        services.AddSingleton(handler);
        services.Replace(ServiceDescriptor.Singleton<IHttpClientFactory>(new MockHttpClientFactory(handler)));
 
        return services.BuildServiceProvider();
    }
}