From 8097992e2a028c06272ef3f58b5fd33ea7d80fe0 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Mon, 23 Mar 2026 14:07:42 +0800 Subject: [PATCH] Strip /login path from dashboard base URL in describe command The describe command was passing BaseUrlWithLoginToken (e.g., http://localhost:18888/login?t=token) directly to the resource snapshot mapper, producing broken dashboard URLs like http://localhost:18888/login?t=token/?resource=redis. Reuse TelemetryCommandHelpers.ExtractDashboardBaseUrl to strip the /login?t=... path before combining with resource URLs. --- src/Aspire.Cli/Commands/DescribeCommand.cs | 2 +- .../Commands/TelemetryCommandHelpers.cs | 2 +- .../Commands/DescribeCommandTests.cs | 65 ++++++++++++++++++- 3 files changed, 65 insertions(+), 4 deletions(-) diff --git a/src/Aspire.Cli/Commands/DescribeCommand.cs b/src/Aspire.Cli/Commands/DescribeCommand.cs index 45bed5dfcfe..50733fda462 100644 --- a/src/Aspire.Cli/Commands/DescribeCommand.cs +++ b/src/Aspire.Cli/Commands/DescribeCommand.cs @@ -142,7 +142,7 @@ protected override async Task ExecuteAsync(ParseResult parseResult, Cancell await Task.WhenAll(dashboardUrlsTask, snapshotsTask).ConfigureAwait(false); - var dashboardBaseUrl = (await dashboardUrlsTask.ConfigureAwait(false))?.BaseUrlWithLoginToken; + var dashboardBaseUrl = TelemetryCommandHelpers.ExtractDashboardBaseUrl((await dashboardUrlsTask.ConfigureAwait(false))?.BaseUrlWithLoginToken); var snapshots = await snapshotsTask.ConfigureAwait(false); // Pre-resolve colors for all resource names so that assignment is diff --git a/src/Aspire.Cli/Commands/TelemetryCommandHelpers.cs b/src/Aspire.Cli/Commands/TelemetryCommandHelpers.cs index 99daad6f807..b327cd2476a 100644 --- a/src/Aspire.Cli/Commands/TelemetryCommandHelpers.cs +++ b/src/Aspire.Cli/Commands/TelemetryCommandHelpers.cs @@ -136,7 +136,7 @@ public static bool HasJsonContentType(HttpResponseMessage response) /// /// Extracts the base URL from a dashboard URL (removes /login?t=... path). /// - private static string? ExtractDashboardBaseUrl(string? dashboardUrlWithToken) + internal static string? ExtractDashboardBaseUrl(string? dashboardUrlWithToken) { if (string.IsNullOrEmpty(dashboardUrlWithToken)) { diff --git a/tests/Aspire.Cli.Tests/Commands/DescribeCommandTests.cs b/tests/Aspire.Cli.Tests/Commands/DescribeCommandTests.cs index 2a78b9d402d..58c0a7eb120 100644 --- a/tests/Aspire.Cli.Tests/Commands/DescribeCommandTests.cs +++ b/tests/Aspire.Cli.Tests/Commands/DescribeCommandTests.cs @@ -331,11 +331,71 @@ public async Task DescribeCommand_Follow_TableFormat_DeduplicatesIdenticalSnapsh Assert.Equal("[redis] Stopping", resourceLines[1]); } + [Fact] + public async Task DescribeCommand_JsonFormat_StripsLoginPathFromDashboardUrl() + { + using var workspace = TemporaryWorkspace.Create(outputHelper); + var outputWriter = new TestOutputTextWriter(outputHelper); + var provider = CreateDescribeTestServices(workspace, outputWriter, [ + new ResourceSnapshot { Name = "redis", DisplayName = "redis", ResourceType = "Container", State = "Running" }, + ], dashboardUrlsState: new DashboardUrlsState + { + BaseUrlWithLoginToken = "http://localhost:18888/login?t=abcd1234" + }); + + var command = provider.GetRequiredService(); + var result = command.Parse("describe --format json"); + + var exitCode = await result.InvokeAsync().DefaultTimeout(); + + Assert.Equal(ExitCodeConstants.Success, exitCode); + + var jsonOutput = string.Join("", outputWriter.Logs); + var deserialized = JsonSerializer.Deserialize(jsonOutput, ResourcesCommandJsonContext.RelaxedEscaping.ResourcesOutput); + + Assert.NotNull(deserialized); + Assert.Single(deserialized.Resources); + + Assert.Equal("http://localhost:18888/?resource=redis", deserialized.Resources[0].DashboardUrl); + } + + [Fact] + public async Task DescribeCommand_Follow_JsonFormat_StripsLoginPathFromDashboardUrl() + { + using var workspace = TemporaryWorkspace.Create(outputHelper); + var outputWriter = new TestOutputTextWriter(outputHelper); + var provider = CreateDescribeTestServices(workspace, outputWriter, [ + new ResourceSnapshot { Name = "redis", DisplayName = "redis", ResourceType = "Container", State = "Running" }, + ], dashboardUrlsState: new DashboardUrlsState + { + BaseUrlWithLoginToken = "http://localhost:18888/login?t=abcd1234" + }); + + var command = provider.GetRequiredService(); + var result = command.Parse("describe --follow --format json"); + + var exitCode = await result.InvokeAsync().DefaultTimeout(); + + Assert.Equal(ExitCodeConstants.Success, exitCode); + + var jsonLines = outputWriter.Logs + .Where(l => l.TrimStart().StartsWith("{", StringComparison.Ordinal)) + .ToList(); + + Assert.NotEmpty(jsonLines); + + var resource = JsonSerializer.Deserialize(jsonLines[0], ResourcesCommandJsonContext.Ndjson.ResourceJson); + Assert.NotNull(resource); + + Assert.Equal("http://localhost:18888/?resource=redis", resource.DashboardUrl); + } + private ServiceProvider CreateDescribeTestServices( TemporaryWorkspace workspace, TestOutputTextWriter outputWriter, List resourceSnapshots, - bool disableAnsi = false) + bool disableAnsi = false, + DashboardUrlsState? dashboardUrlsState = null) { var monitor = new TestAuxiliaryBackchannelMonitor(); var connection = new TestAppHostAuxiliaryBackchannel @@ -346,7 +406,8 @@ private ServiceProvider CreateDescribeTestServices( AppHostPath = Path.Combine(workspace.WorkspaceRoot.FullName, "TestAppHost", "TestAppHost.csproj"), ProcessId = 1234 }, - ResourceSnapshots = resourceSnapshots + ResourceSnapshots = resourceSnapshots, + DashboardUrlsState = dashboardUrlsState }; monitor.AddConnection("hash1", "socket.hash1", connection);