File: Integration\TelemetryApiTests.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 Aspire.Dashboard.Api;
using Aspire.Dashboard.Configuration;
using Aspire.Dashboard.Otlp.Model.Serialization;
using Aspire.Hosting;
using Microsoft.AspNetCore.InternalTesting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Xunit;
 
namespace Aspire.Dashboard.Tests.Integration;
 
public class TelemetryApiTests
{
    private readonly ITestOutputHelper _testOutputHelper;
 
    public TelemetryApiTests(ITestOutputHelper testOutputHelper)
    {
        _testOutputHelper = testOutputHelper;
    }
 
    #region Configuration Tests
 
    [Fact]
    public async Task Configuration_ApiAuthModeDefaults_WhenNotConfigured()
    {
        // Arrange & Act
        await using var app = IntegrationTestHelpers.CreateDashboardWebApplication(_testOutputHelper, config =>
        {
            config[DashboardConfigNames.DashboardFrontendAuthModeName.ConfigKey] = FrontendAuthMode.Unsecured.ToString();
            // Don't set any Api config
        });
        await app.StartAsync().DefaultTimeout();
 
        // Assert - verify ApiOptions defaults
        var options = app.Services.GetRequiredService<IOptionsMonitor<DashboardOptions>>().CurrentValue;
        Assert.NotNull(options.Api);
        Assert.Equal(ApiAuthMode.Unsecured, options.Api.AuthMode);
    }
 
    [Fact]
    public async Task Configuration_ApiKeyFromMcp_CopiedToApi()
    {
        // Arrange - only set MCP key (legacy config)
        var apiKey = "LegacyMcpKey123!";
        await using var app = IntegrationTestHelpers.CreateDashboardWebApplication(_testOutputHelper, config =>
        {
            config[DashboardConfigNames.DashboardFrontendAuthModeName.ConfigKey] = FrontendAuthMode.Unsecured.ToString();
            config[DashboardConfigNames.DashboardMcpAuthModeName.ConfigKey] = McpAuthMode.ApiKey.ToString();
            config[DashboardConfigNames.DashboardMcpPrimaryApiKeyName.ConfigKey] = apiKey;
        });
        await app.StartAsync().DefaultTimeout();
 
        // Assert - verify Api gets MCP key
        var options = app.Services.GetRequiredService<IOptionsMonitor<DashboardOptions>>().CurrentValue;
        Assert.NotNull(options.Api.GetPrimaryApiKeyBytesOrNull());
        Assert.Equal(apiKey.Length, options.Api.GetPrimaryApiKeyBytesOrNull()!.Length);
    }
 
    [Fact]
    public async Task Configuration_ApiKeyExplicit_OverridesMcp()
    {
        // Arrange - set both MCP and API keys (API should take precedence)
        var mcpKey = "McpKey123!";
        var apiKey = "ApiKey456!";
        await using var app = IntegrationTestHelpers.CreateDashboardWebApplication(_testOutputHelper, config =>
        {
            config[DashboardConfigNames.DashboardFrontendAuthModeName.ConfigKey] = FrontendAuthMode.Unsecured.ToString();
            config[DashboardConfigNames.DashboardMcpPrimaryApiKeyName.ConfigKey] = mcpKey;
            config[DashboardConfigNames.DashboardApiAuthModeName.ConfigKey] = ApiAuthMode.ApiKey.ToString();
            config[DashboardConfigNames.DashboardApiPrimaryApiKeyName.ConfigKey] = apiKey;
        });
        await app.StartAsync().DefaultTimeout();
 
        // Assert - Api should use its own key, not MCP's
        var options = app.Services.GetRequiredService<IOptionsMonitor<DashboardOptions>>().CurrentValue;
        Assert.NotNull(options.Api.GetPrimaryApiKeyBytesOrNull());
        Assert.Equal(apiKey.Length, options.Api.GetPrimaryApiKeyBytesOrNull()!.Length);
    }
 
    #endregion
 
    [Fact]
    public async Task GetSpans_NoAuth_ReturnsUnauthorized()
    {
        // Arrange - with browser token auth mode and API key auth required
        var apiKey = "TestKey123!";
        await using var app = IntegrationTestHelpers.CreateDashboardWebApplication(_testOutputHelper, config =>
        {
            config[DashboardConfigNames.DashboardFrontendAuthModeName.ConfigKey] = FrontendAuthMode.BrowserToken.ToString();
            config[DashboardConfigNames.DashboardApiAuthModeName.ConfigKey] = ApiAuthMode.ApiKey.ToString();
            config[DashboardConfigNames.DashboardApiPrimaryApiKeyName.ConfigKey] = apiKey;
        });
        await app.StartAsync().DefaultTimeout();
 
        // Use a handler that doesn't follow redirects to capture the redirect response
        using var handler = new SocketsHttpHandler { AllowAutoRedirect = false };
        using var httpClient = new HttpClient(handler) { BaseAddress = new Uri($"http://{app.FrontendSingleEndPointAccessor().EndPoint}") };
 
        // Act - no auth (no API key header, no browser token)
        var response = await httpClient.GetAsync("/api/telemetry/spans").DefaultTimeout();
 
        // Assert - should redirect to login (since frontend auth is browser token)
        Assert.True(response.StatusCode is HttpStatusCode.Redirect or HttpStatusCode.Unauthorized or HttpStatusCode.Found,
            $"Expected redirect or unauthorized, got {response.StatusCode}");
    }
 
    [Fact]
    public async Task GetSpans_UnsecuredMode_Returns200()
    {
        // Arrange - unsecured frontend mode
        await using var app = IntegrationTestHelpers.CreateDashboardWebApplication(_testOutputHelper, config =>
        {
            config[DashboardConfigNames.DashboardFrontendAuthModeName.ConfigKey] = FrontendAuthMode.Unsecured.ToString();
        });
        await app.StartAsync().DefaultTimeout();
 
        using var httpClient = IntegrationTestHelpers.CreateHttpClient($"http://{app.FrontendSingleEndPointAccessor().EndPoint}");
 
        // Act
        var response = await httpClient.GetAsync("/api/telemetry/spans").DefaultTimeout();
 
        // Assert
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
        var content = await response.Content.ReadFromJsonAsync<TelemetryApiResponse<OtlpTelemetryDataJson>>(OtlpJsonSerializerContext.Default.TelemetryApiResponseOtlpTelemetryDataJson);
        Assert.NotNull(content);
        Assert.NotNull(content.Data);
    }
 
    [Fact]
    public async Task GetSpans_WithApiKey_Returns200()
    {
        // Arrange - with Dashboard API key auth
        var apiKey = "TestKey123!";
        await using var app = IntegrationTestHelpers.CreateDashboardWebApplication(_testOutputHelper, config =>
        {
            config[DashboardConfigNames.DashboardFrontendAuthModeName.ConfigKey] = FrontendAuthMode.BrowserToken.ToString();
            config[DashboardConfigNames.DashboardApiAuthModeName.ConfigKey] = ApiAuthMode.ApiKey.ToString();
            config[DashboardConfigNames.DashboardApiPrimaryApiKeyName.ConfigKey] = apiKey;
        });
        await app.StartAsync().DefaultTimeout();
 
        using var httpClient = IntegrationTestHelpers.CreateHttpClient($"http://{app.FrontendSingleEndPointAccessor().EndPoint}");
        httpClient.DefaultRequestHeaders.TryAddWithoutValidation(ApiAuthenticationHandler.ApiKeyHeaderName, apiKey);
 
        // Act
        var response = await httpClient.GetAsync("/api/telemetry/spans").DefaultTimeout();
 
        // Assert
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
        var content = await response.Content.ReadFromJsonAsync<TelemetryApiResponse<OtlpTelemetryDataJson>>(OtlpJsonSerializerContext.Default.TelemetryApiResponseOtlpTelemetryDataJson);
        Assert.NotNull(content);
    }
 
    [Fact]
    public async Task GetSpans_WithWrongApiKey_ReturnsUnauthorized()
    {
        // Arrange
        var apiKey = "TestKey123!";
        await using var app = IntegrationTestHelpers.CreateDashboardWebApplication(_testOutputHelper, config =>
        {
            config[DashboardConfigNames.DashboardFrontendAuthModeName.ConfigKey] = FrontendAuthMode.BrowserToken.ToString();
            config[DashboardConfigNames.DashboardApiAuthModeName.ConfigKey] = ApiAuthMode.ApiKey.ToString();
            config[DashboardConfigNames.DashboardApiPrimaryApiKeyName.ConfigKey] = apiKey;
        });
        await app.StartAsync().DefaultTimeout();
 
        // Use a handler that doesn't follow redirects
        using var handler = new SocketsHttpHandler { AllowAutoRedirect = false };
        using var httpClient = new HttpClient(handler) { BaseAddress = new Uri($"http://{app.FrontendSingleEndPointAccessor().EndPoint}") };
        httpClient.DefaultRequestHeaders.TryAddWithoutValidation(ApiAuthenticationHandler.ApiKeyHeaderName, "WrongKey!");
 
        // Act
        var response = await httpClient.GetAsync("/api/telemetry/spans").DefaultTimeout();
 
        // Assert - wrong API key should return 401 Unauthorized
        Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
    }
 
    [Fact]
    public async Task GetSpans_ApiDisabled_Returns404()
    {
        // Arrange - disable the Telemetry API explicitly
        await using var app = IntegrationTestHelpers.CreateDashboardWebApplication(_testOutputHelper, config =>
        {
            config[DashboardConfigNames.DashboardApiEnabledName.ConfigKey] = "false";
        });
        await app.StartAsync().DefaultTimeout();
 
        using var httpClient = IntegrationTestHelpers.CreateHttpClient($"http://{app.FrontendSingleEndPointAccessor().EndPoint}");
 
        // Act
        var response = await httpClient.GetAsync("/api/telemetry/spans").DefaultTimeout();
 
        // Assert
        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
    }
 
    [Fact]
    public async Task GetLogs_UnsecuredMode_Returns200()
    {
        // Arrange
        await using var app = IntegrationTestHelpers.CreateDashboardWebApplication(_testOutputHelper, config =>
        {
            config[DashboardConfigNames.DashboardFrontendAuthModeName.ConfigKey] = FrontendAuthMode.Unsecured.ToString();
        });
        await app.StartAsync().DefaultTimeout();
 
        using var httpClient = IntegrationTestHelpers.CreateHttpClient($"http://{app.FrontendSingleEndPointAccessor().EndPoint}");
 
        // Act
        var response = await httpClient.GetAsync("/api/telemetry/logs").DefaultTimeout();
 
        // Assert
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
        var content = await response.Content.ReadFromJsonAsync<TelemetryApiResponse<OtlpTelemetryDataJson>>(OtlpJsonSerializerContext.Default.TelemetryApiResponseOtlpTelemetryDataJson);
        Assert.NotNull(content);
        Assert.NotNull(content.Data);
    }
 
    [Fact]
    public async Task GetLogs_WithTraceIdFilter_Returns200()
    {
        // Arrange
        await using var app = IntegrationTestHelpers.CreateDashboardWebApplication(_testOutputHelper, config =>
        {
            config[DashboardConfigNames.DashboardFrontendAuthModeName.ConfigKey] = FrontendAuthMode.Unsecured.ToString();
        });
        await app.StartAsync().DefaultTimeout();
 
        using var httpClient = IntegrationTestHelpers.CreateHttpClient($"http://{app.FrontendSingleEndPointAccessor().EndPoint}");
 
        // Act - use ?traceId query param instead of /spans/{traceId}/logs
        var response = await httpClient.GetAsync("/api/telemetry/logs?traceId=some-trace-id").DefaultTimeout();
 
        // Assert - returns 200 with empty data when no logs match the trace ID
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
        var content = await response.Content.ReadFromJsonAsync<TelemetryApiResponse<OtlpTelemetryDataJson>>(OtlpJsonSerializerContext.Default.TelemetryApiResponseOtlpTelemetryDataJson);
        Assert.NotNull(content);
        Assert.Equal(0, content.TotalCount);
    }
 
    [Fact]
    public async Task GetSpans_WithQueryParameters_Returns200()
    {
        // Arrange
        await using var app = IntegrationTestHelpers.CreateDashboardWebApplication(_testOutputHelper, config =>
        {
            config[DashboardConfigNames.DashboardFrontendAuthModeName.ConfigKey] = FrontendAuthMode.Unsecured.ToString();
        });
        await app.StartAsync().DefaultTimeout();
 
        using var httpClient = IntegrationTestHelpers.CreateHttpClient($"http://{app.FrontendSingleEndPointAccessor().EndPoint}");
 
        // Act - test query parameters without resource filter (no resources exist in test)
        var response = await httpClient.GetAsync("/api/telemetry/spans?hasError=true&limit=50").DefaultTimeout();
 
        // Assert
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
        var content = await response.Content.ReadFromJsonAsync<TelemetryApiResponse<OtlpTelemetryDataJson>>(OtlpJsonSerializerContext.Default.TelemetryApiResponseOtlpTelemetryDataJson);
        Assert.NotNull(content);
    }
 
    [Fact]
    public async Task GetSpans_WithUnknownResource_Returns404()
    {
        // Arrange
        await using var app = IntegrationTestHelpers.CreateDashboardWebApplication(_testOutputHelper, config =>
        {
            config[DashboardConfigNames.DashboardFrontendAuthModeName.ConfigKey] = FrontendAuthMode.Unsecured.ToString();
        });
        await app.StartAsync().DefaultTimeout();
 
        using var httpClient = IntegrationTestHelpers.CreateHttpClient($"http://{app.FrontendSingleEndPointAccessor().EndPoint}");
 
        // Act - request with unknown resource filter
        var response = await httpClient.GetAsync("/api/telemetry/spans?resource=unknown-resource").DefaultTimeout();
 
        // Assert - should return 404 for unknown resource
        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
    }
 
    [Fact]
    public async Task GetLogs_WithQueryParameters_Returns200()
    {
        // Arrange
        await using var app = IntegrationTestHelpers.CreateDashboardWebApplication(_testOutputHelper, config =>
        {
            config[DashboardConfigNames.DashboardFrontendAuthModeName.ConfigKey] = FrontendAuthMode.Unsecured.ToString();
        });
        await app.StartAsync().DefaultTimeout();
 
        using var httpClient = IntegrationTestHelpers.CreateHttpClient($"http://{app.FrontendSingleEndPointAccessor().EndPoint}");
 
        // Act - test query parameters without resource filter (no resources exist in test)
        var response = await httpClient.GetAsync("/api/telemetry/logs?severity=Error&limit=50").DefaultTimeout();
 
        // Assert
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
        var content = await response.Content.ReadFromJsonAsync<TelemetryApiResponse<OtlpTelemetryDataJson>>(OtlpJsonSerializerContext.Default.TelemetryApiResponseOtlpTelemetryDataJson);
        Assert.NotNull(content);
    }
 
    [Fact]
    public async Task GetLogs_WithUnknownResource_Returns404()
    {
        // Arrange
        await using var app = IntegrationTestHelpers.CreateDashboardWebApplication(_testOutputHelper, config =>
        {
            config[DashboardConfigNames.DashboardFrontendAuthModeName.ConfigKey] = FrontendAuthMode.Unsecured.ToString();
        });
        await app.StartAsync().DefaultTimeout();
 
        using var httpClient = IntegrationTestHelpers.CreateHttpClient($"http://{app.FrontendSingleEndPointAccessor().EndPoint}");
 
        // Act - request with unknown resource filter
        var response = await httpClient.GetAsync("/api/telemetry/logs?resource=unknown-resource").DefaultTimeout();
 
        // Assert - should return 404 for unknown resource
        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
    }
 
    [Fact]
    public async Task GetSpans_ApiAuthModeUnsecured_AllowsAccessWithoutAuth()
    {
        // Arrange - Api auth mode is unsecured (default)
        await using var app = IntegrationTestHelpers.CreateDashboardWebApplication(_testOutputHelper, config =>
        {
            config[DashboardConfigNames.DashboardFrontendAuthModeName.ConfigKey] = FrontendAuthMode.BrowserToken.ToString();
            config[DashboardConfigNames.DashboardApiAuthModeName.ConfigKey] = ApiAuthMode.Unsecured.ToString();
        });
        await app.StartAsync().DefaultTimeout();
 
        using var httpClient = IntegrationTestHelpers.CreateHttpClient($"http://{app.FrontendSingleEndPointAccessor().EndPoint}");
 
        // Act - no auth headers at all
        var response = await httpClient.GetAsync("/api/telemetry/spans").DefaultTimeout();
 
        // Assert - should succeed because Api auth is unsecured
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
    }
 
    [Fact]
    public async Task GetSpans_ApiAuthModeApiKey_RequiresApiKey()
    {
        // Arrange - Api auth mode requires API key
        var apiKey = "TestKey123!";
        await using var app = IntegrationTestHelpers.CreateDashboardWebApplication(_testOutputHelper, config =>
        {
            config[DashboardConfigNames.DashboardFrontendAuthModeName.ConfigKey] = FrontendAuthMode.BrowserToken.ToString();
            config[DashboardConfigNames.DashboardApiAuthModeName.ConfigKey] = ApiAuthMode.ApiKey.ToString();
            config[DashboardConfigNames.DashboardApiPrimaryApiKeyName.ConfigKey] = apiKey;
        });
        await app.StartAsync().DefaultTimeout();
 
        // Use a handler that doesn't follow redirects
        using var handler = new SocketsHttpHandler { AllowAutoRedirect = false };
        using var httpClient = new HttpClient(handler) { BaseAddress = new Uri($"http://{app.FrontendSingleEndPointAccessor().EndPoint}") };
 
        // Act - no API key header
        var response = await httpClient.GetAsync("/api/telemetry/spans").DefaultTimeout();
 
        // Assert - should redirect to login or return unauthorized
        Assert.True(response.StatusCode is HttpStatusCode.Redirect or HttpStatusCode.Unauthorized or HttpStatusCode.Found,
            $"Expected redirect or unauthorized, got {response.StatusCode}");
    }
 
    [Fact]
    public async Task GetSpans_WithSecondaryApiKey_Returns200()
    {
        // Arrange - with secondary API key (for key rotation)
        var primaryKey = "PrimaryKey123!";
        var secondaryKey = "SecondaryKey456!";
        await using var app = IntegrationTestHelpers.CreateDashboardWebApplication(_testOutputHelper, config =>
        {
            config[DashboardConfigNames.DashboardFrontendAuthModeName.ConfigKey] = FrontendAuthMode.BrowserToken.ToString();
            config[DashboardConfigNames.DashboardApiAuthModeName.ConfigKey] = ApiAuthMode.ApiKey.ToString();
            config[DashboardConfigNames.DashboardApiPrimaryApiKeyName.ConfigKey] = primaryKey;
            config[DashboardConfigNames.DashboardApiSecondaryApiKeyName.ConfigKey] = secondaryKey;
        });
        await app.StartAsync().DefaultTimeout();
 
        using var httpClient = IntegrationTestHelpers.CreateHttpClient($"http://{app.FrontendSingleEndPointAccessor().EndPoint}");
        httpClient.DefaultRequestHeaders.TryAddWithoutValidation(ApiAuthenticationHandler.ApiKeyHeaderName, secondaryKey);
 
        // Act
        var response = await httpClient.GetAsync("/api/telemetry/spans").DefaultTimeout();
 
        // Assert - secondary key should work
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
    }
 
    [Fact]
    public async Task GetSpans_McpKeyFallback_Returns200()
    {
        // Arrange - using legacy MCP key config (backward compatibility)
        var apiKey = "LegacyMcpKey123!";
        await using var app = IntegrationTestHelpers.CreateDashboardWebApplication(_testOutputHelper, config =>
        {
            config[DashboardConfigNames.DashboardFrontendAuthModeName.ConfigKey] = FrontendAuthMode.BrowserToken.ToString();
            // Use legacy MCP config instead of new Api config
            config[DashboardConfigNames.DashboardMcpAuthModeName.ConfigKey] = McpAuthMode.ApiKey.ToString();
            config[DashboardConfigNames.DashboardMcpPrimaryApiKeyName.ConfigKey] = apiKey;
        });
        await app.StartAsync().DefaultTimeout();
 
        using var httpClient = IntegrationTestHelpers.CreateHttpClient($"http://{app.FrontendSingleEndPointAccessor().EndPoint}");
        httpClient.DefaultRequestHeaders.TryAddWithoutValidation(ApiAuthenticationHandler.ApiKeyHeaderName, apiKey);
 
        // Act
        var response = await httpClient.GetAsync("/api/telemetry/spans").DefaultTimeout();
 
        // Assert - MCP key should work via fallback
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
    }
 
    [Fact]
    public async Task GetSpans_StreamingMode_ReturnsNdjsonContentType()
    {
        // Arrange
        await using var app = IntegrationTestHelpers.CreateDashboardWebApplication(_testOutputHelper, config =>
        {
            config[DashboardConfigNames.DashboardFrontendAuthModeName.ConfigKey] = FrontendAuthMode.Unsecured.ToString();
        });
        await app.StartAsync().DefaultTimeout();
 
        using var httpClient = IntegrationTestHelpers.CreateHttpClient($"http://{app.FrontendSingleEndPointAccessor().EndPoint}");
 
        // Act - request streaming mode
        using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(2));
        try
        {
            var response = await httpClient.GetAsync("/api/telemetry/spans?follow=true", HttpCompletionOption.ResponseHeadersRead, cts.Token).DefaultTimeout();
 
            // Assert - should have NDJSON content type and streaming headers
            Assert.Equal(HttpStatusCode.OK, response.StatusCode);
            Assert.Equal("application/x-ndjson", response.Content.Headers.ContentType?.MediaType);
            Assert.Equal("no-cache", response.Headers.CacheControl?.ToString());
            Assert.True(response.Headers.TryGetValues("X-Accel-Buffering", out var bufferingValues));
            Assert.Equal("no", bufferingValues.Single());
        }
        catch (OperationCanceledException)
        {
            // Expected - streaming mode keeps connection open
        }
    }
 
    [Fact]
    public async Task GetLogs_StreamingMode_ReturnsNdjsonContentType()
    {
        // Arrange
        await using var app = IntegrationTestHelpers.CreateDashboardWebApplication(_testOutputHelper, config =>
        {
            config[DashboardConfigNames.DashboardFrontendAuthModeName.ConfigKey] = FrontendAuthMode.Unsecured.ToString();
        });
        await app.StartAsync().DefaultTimeout();
 
        using var httpClient = IntegrationTestHelpers.CreateHttpClient($"http://{app.FrontendSingleEndPointAccessor().EndPoint}");
 
        // Act - request streaming mode
        using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(2));
        try
        {
            var response = await httpClient.GetAsync("/api/telemetry/logs?follow=true", HttpCompletionOption.ResponseHeadersRead, cts.Token).DefaultTimeout();
 
            // Assert - should have NDJSON content type and streaming headers
            Assert.Equal(HttpStatusCode.OK, response.StatusCode);
            Assert.Equal("application/x-ndjson", response.Content.Headers.ContentType?.MediaType);
            Assert.Equal("no-cache", response.Headers.CacheControl?.ToString());
            Assert.True(response.Headers.TryGetValues("X-Accel-Buffering", out var bufferingValues));
            Assert.Equal("no", bufferingValues.Single());
        }
        catch (OperationCanceledException)
        {
            // Expected - streaming mode keeps connection open
        }
    }
 
    #region Trace Endpoint Tests
 
    [Fact]
    public async Task GetTraces_UnsecuredMode_Returns200()
    {
        // Arrange
        await using var app = IntegrationTestHelpers.CreateDashboardWebApplication(_testOutputHelper, config =>
        {
            config[DashboardConfigNames.DashboardFrontendAuthModeName.ConfigKey] = FrontendAuthMode.Unsecured.ToString();
        });
        await app.StartAsync().DefaultTimeout();
 
        using var httpClient = IntegrationTestHelpers.CreateHttpClient($"http://{app.FrontendSingleEndPointAccessor().EndPoint}");
 
        // Act
        var response = await httpClient.GetAsync("/api/telemetry/traces").DefaultTimeout();
 
        // Assert
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
        var content = await response.Content.ReadFromJsonAsync<TelemetryApiResponse<OtlpTelemetryDataJson>>(OtlpJsonSerializerContext.Default.TelemetryApiResponseOtlpTelemetryDataJson);
        Assert.NotNull(content);
        Assert.NotNull(content.Data);
    }
 
    [Fact]
    public async Task GetTraces_WithHasErrorFilter_Returns200()
    {
        // Arrange
        await using var app = IntegrationTestHelpers.CreateDashboardWebApplication(_testOutputHelper, config =>
        {
            config[DashboardConfigNames.DashboardFrontendAuthModeName.ConfigKey] = FrontendAuthMode.Unsecured.ToString();
        });
        await app.StartAsync().DefaultTimeout();
 
        using var httpClient = IntegrationTestHelpers.CreateHttpClient($"http://{app.FrontendSingleEndPointAccessor().EndPoint}");
 
        // Act
        var response = await httpClient.GetAsync("/api/telemetry/traces?hasError=true&limit=10").DefaultTimeout();
 
        // Assert - returns 200 with empty data when no traces match
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
        var content = await response.Content.ReadFromJsonAsync<TelemetryApiResponse<OtlpTelemetryDataJson>>(OtlpJsonSerializerContext.Default.TelemetryApiResponseOtlpTelemetryDataJson);
        Assert.NotNull(content);
        Assert.Equal(0, content.TotalCount);
    }
 
    [Fact]
    public async Task GetTraces_WithUnknownResource_Returns404()
    {
        // Arrange
        await using var app = IntegrationTestHelpers.CreateDashboardWebApplication(_testOutputHelper, config =>
        {
            config[DashboardConfigNames.DashboardFrontendAuthModeName.ConfigKey] = FrontendAuthMode.Unsecured.ToString();
        });
        await app.StartAsync().DefaultTimeout();
 
        using var httpClient = IntegrationTestHelpers.CreateHttpClient($"http://{app.FrontendSingleEndPointAccessor().EndPoint}");
 
        // Act
        var response = await httpClient.GetAsync("/api/telemetry/traces?resource=unknownresource").DefaultTimeout();
 
        // Assert
        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
    }
 
    [Fact]
    public async Task GetTraceById_NotFound_Returns404()
    {
        // Arrange
        await using var app = IntegrationTestHelpers.CreateDashboardWebApplication(_testOutputHelper, config =>
        {
            config[DashboardConfigNames.DashboardFrontendAuthModeName.ConfigKey] = FrontendAuthMode.Unsecured.ToString();
        });
        await app.StartAsync().DefaultTimeout();
 
        using var httpClient = IntegrationTestHelpers.CreateHttpClient($"http://{app.FrontendSingleEndPointAccessor().EndPoint}");
 
        // Act
        var response = await httpClient.GetAsync("/api/telemetry/traces/nonexistent-trace-id").DefaultTimeout();
 
        // Assert
        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
    }
 
    [Fact]
    public async Task GetTraces_WithApiKey_Returns200()
    {
        // Arrange
        var apiKey = "TestApiKey123!";
        await using var app = IntegrationTestHelpers.CreateDashboardWebApplication(_testOutputHelper, config =>
        {
            config[DashboardConfigNames.DashboardFrontendAuthModeName.ConfigKey] = FrontendAuthMode.BrowserToken.ToString();
            config[DashboardConfigNames.DashboardApiAuthModeName.ConfigKey] = ApiAuthMode.ApiKey.ToString();
            config[DashboardConfigNames.DashboardApiPrimaryApiKeyName.ConfigKey] = apiKey;
        });
        await app.StartAsync().DefaultTimeout();
 
        using var httpClient = IntegrationTestHelpers.CreateHttpClient($"http://{app.FrontendSingleEndPointAccessor().EndPoint}");
        httpClient.DefaultRequestHeaders.TryAddWithoutValidation(ApiAuthenticationHandler.ApiKeyHeaderName, apiKey);
 
        // Act
        var response = await httpClient.GetAsync("/api/telemetry/traces").DefaultTimeout();
 
        // Assert
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
    }
 
    #endregion
}