| File: Mcp\ListTraceStructuredLogsToolTests.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.Commands; using Aspire.Cli.Mcp.Tools; using Aspire.Cli.Tests.TestServices; using Aspire.Cli.Tests.Utils; using Aspire.Otlp.Serialization; using Microsoft.AspNetCore.InternalTesting; using Microsoft.Extensions.Logging.Testing; namespace Aspire.Cli.Tests.Mcp; public class ListTraceStructuredLogsToolTests { [Fact] public async Task ListTraceStructuredLogsTool_AuthenticatedRequestDoesNotLeakInLogsOrErrors() { var resourcesResponse = JsonSerializer.Serialize( Array.Empty<ResourceInfoJson>(), OtlpJsonSerializerContext.Default.ResourceInfoJsonArray); Uri? logsRequestUri = null; using var handler = new MockHttpMessageHandler(request => { if (request.RequestUri!.AbsolutePath.EndsWith("/resources", StringComparison.Ordinal)) { return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent( resourcesResponse, System.Text.Encoding.UTF8, "application/json") }; } logsRequestUri = request.RequestUri; throw new HttpRequestException( "Request failed at https://" + "exception-user" + ":" + "exception-password" + "@exception.example?token=exception-secret#exception-fragment"); }); var sink = new TestSink(); var logger = new TestLogger<ListTraceStructuredLogsTool>( new TestLoggerFactory(sink, enabled: true)); var tool = new ListTraceStructuredLogsTool( new StaticDashboardInfoProvider( "https://" + "request-user" + ":" + "request-password" + "@example.com:5000/login" + "?t=actual-login-token&accessKey=request-secret&view=resources#request-fragment", apiKey: "api-key"), auxiliaryBackchannelMonitor: null, new MockHttpClientFactory(handler), logger); var arguments = new Dictionary<string, JsonElement> { ["traceId"] = JsonDocument.Parse("\"trace-id\"").RootElement }; var exception = await Assert.ThrowsAsync<ModelContextProtocol.McpProtocolException>( () => tool.CallToolAsync( CallToolContextTestHelper.Create(arguments), CancellationToken.None).AsTask()).DefaultTimeout(); Assert.True(logsRequestUri is not null, exception.ToString()); Assert.Equal("request-user:request-password", logsRequestUri.UserInfo); Assert.Equal( $"?accessKey=request-secret&view=resources&traceId=trace-id&limit={TelemetryCommandHelpers.MaxTelemetryLimit}", logsRequestUri.Query); foreach (var diagnostic in new[] { exception.Message }.Concat( sink.Writes.Select(write => $"{write.Message} {write.Exception}"))) { Assert.DoesNotContain("request-user", diagnostic, StringComparison.Ordinal); Assert.DoesNotContain("request-password", diagnostic, StringComparison.Ordinal); Assert.DoesNotContain("actual-login-token", diagnostic, StringComparison.Ordinal); Assert.DoesNotContain("request-secret", diagnostic, StringComparison.Ordinal); Assert.DoesNotContain("request-fragment", diagnostic, StringComparison.Ordinal); Assert.DoesNotContain("exception-secret", diagnostic, StringComparison.Ordinal); Assert.DoesNotContain("exception-fragment", diagnostic, StringComparison.Ordinal); Assert.DoesNotContain("exception-user", diagnostic, StringComparison.Ordinal); Assert.DoesNotContain("exception-password", diagnostic, StringComparison.Ordinal); } } }