From e40fd4e14d3ca27b085a06f4676d5baebe3a4c0a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 26 Sep 2025 06:32:57 +0000 Subject: [PATCH 1/5] Initial plan From e9cc2b57299112ab0f0a61dab79ecbaa905d4520 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 26 Sep 2025 06:42:23 +0000 Subject: [PATCH 2/5] Add SuppressUnsecuredTelemetryMessage option to OtlpOptions Co-authored-by: JamesNK <303201+JamesNK@users.noreply.github.com> --- .../Components/Layout/MainLayout.razor.cs | 2 +- .../Configuration/DashboardOptions.cs | 6 +++ src/Shared/DashboardConfigNames.cs | 1 + .../Layout/MainLayoutTests.cs | 52 ++++++++++++++++++- .../DashboardOptionsTests.cs | 21 ++++++++ 5 files changed, 79 insertions(+), 3 deletions(-) diff --git a/src/Aspire.Dashboard/Components/Layout/MainLayout.razor.cs b/src/Aspire.Dashboard/Components/Layout/MainLayout.razor.cs index 4b18e713c67..6a6cc151010 100644 --- a/src/Aspire.Dashboard/Components/Layout/MainLayout.razor.cs +++ b/src/Aspire.Dashboard/Components/Layout/MainLayout.razor.cs @@ -105,7 +105,7 @@ protected override async Task OnInitializedAsync() TimeProvider.SetBrowserTimeZone(result.TimeZone); TelemetryContextProvider.SetBrowserUserAgent(result.UserAgent); - if (Options.CurrentValue.Otlp.AuthMode == OtlpAuthMode.Unsecured) + if (Options.CurrentValue.Otlp.AuthMode == OtlpAuthMode.Unsecured && !Options.CurrentValue.Otlp.SuppressUnsecuredTelemetryMessage) { var dismissedResult = await LocalStorage.GetUnprotectedAsync(BrowserStorageKeys.UnsecuredTelemetryMessageDismissedKey); var skipMessage = dismissedResult.Success && dismissedResult.Value; diff --git a/src/Aspire.Dashboard/Configuration/DashboardOptions.cs b/src/Aspire.Dashboard/Configuration/DashboardOptions.cs index 37e51f58535..363e667a372 100644 --- a/src/Aspire.Dashboard/Configuration/DashboardOptions.cs +++ b/src/Aspire.Dashboard/Configuration/DashboardOptions.cs @@ -85,6 +85,12 @@ public sealed class OtlpOptions public List AllowedCertificates { get; set; } = new(); + /// + /// Gets or sets a value indicating whether to suppress the unsecured telemetry message in the dashboard UI. + /// When true, the warning message about unsecured OTLP endpoints will not be displayed. + /// + public bool SuppressUnsecuredTelemetryMessage { get; set; } + public BindingAddress? GetGrpcEndpointAddress() { return _parsedGrpcEndpointAddress; diff --git a/src/Shared/DashboardConfigNames.cs b/src/Shared/DashboardConfigNames.cs index 3eb3a1076a8..86bc1eb6e56 100644 --- a/src/Shared/DashboardConfigNames.cs +++ b/src/Shared/DashboardConfigNames.cs @@ -18,6 +18,7 @@ internal static class DashboardConfigNames public static readonly ConfigName DashboardOtlpAuthModeName = new("Dashboard:Otlp:AuthMode", "DASHBOARD__OTLP__AUTHMODE"); public static readonly ConfigName DashboardOtlpPrimaryApiKeyName = new("Dashboard:Otlp:PrimaryApiKey", "DASHBOARD__OTLP__PRIMARYAPIKEY"); public static readonly ConfigName DashboardOtlpSecondaryApiKeyName = new("Dashboard:Otlp:SecondaryApiKey", "DASHBOARD__OTLP__SECONDARYAPIKEY"); + public static readonly ConfigName DashboardOtlpSuppressUnsecuredTelemetryMessageName = new("Dashboard:Otlp:SuppressUnsecuredTelemetryMessage", "DASHBOARD__OTLP__SUPPRESSUNSECUREDTELEMETRYMESSAGE"); public static readonly ConfigName DashboardOtlpCorsAllowedOriginsKeyName = new("Dashboard:Otlp:Cors:AllowedOrigins", "DASHBOARD__OTLP__CORS__ALLOWEDORIGINS"); public static readonly ConfigName DashboardOtlpCorsAllowedHeadersKeyName = new("Dashboard:Otlp:Cors:AllowedHeaders", "DASHBOARD__OTLP__CORS__ALLOWEDHEADERS"); public static readonly ConfigName DashboardOtlpAllowedCertificatesName = new("Dashboard:Otlp:AllowedCertificates", "DASHBOARD__OTLP__ALLOWEDCERTIFICATES"); diff --git a/tests/Aspire.Dashboard.Components.Tests/Layout/MainLayoutTests.cs b/tests/Aspire.Dashboard.Components.Tests/Layout/MainLayoutTests.cs index afe1bf75b4d..bcb93335df6 100644 --- a/tests/Aspire.Dashboard.Components.Tests/Layout/MainLayoutTests.cs +++ b/tests/Aspire.Dashboard.Components.Tests/Layout/MainLayoutTests.cs @@ -125,7 +125,51 @@ public async Task OnInitialize_UnsecuredOtlp_Dismissed_NoMessageBar() Assert.Empty(messageService.AllMessages); } - private void SetupMainLayoutServices(TestLocalStorage? localStorage = null, MessageService? messageService = null) + [Fact] + public async Task OnInitialize_UnsecuredOtlp_SuppressConfigured_NoMessageBar() + { + // Arrange + var testLocalStorage = new TestLocalStorage(); + var messageService = new MessageService(); + + SetupMainLayoutServices(localStorage: testLocalStorage, messageService: messageService, suppressUnsecuredMessage: true); + + var messageShownTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + messageService.OnMessageItemsUpdatedAsync += () => + { + messageShownTcs.TrySetResult(); + return Task.CompletedTask; + }; + + testLocalStorage.OnGetUnprotectedAsync = key => + { + if (key == BrowserStorageKeys.UnsecuredTelemetryMessageDismissedKey) + { + return (false, false); // Message not dismissed, but should be suppressed by config + } + else + { + throw new InvalidOperationException("Unexpected key."); + } + }; + + // Act + var cut = RenderComponent(builder => + { + builder.Add(p => p.ViewportInformation, new ViewportInformation(IsDesktop: true, IsUltraLowHeight: false, IsUltraLowWidth: false)); + }); + + // Assert + var timeoutTask = Task.Delay(100); + var completedTask = await Task.WhenAny(messageShownTcs.Task, timeoutTask).WaitAsync(TimeSpan.FromSeconds(5)); + + // It's hard to test something not happening. + // In this case of checking for a message, apply a small display and then double check that no message was displayed. + Assert.True(completedTask != messageShownTcs.Task, "No message bar should be displayed when suppressed by configuration."); + Assert.Empty(messageService.AllMessages); + } + + private void SetupMainLayoutServices(TestLocalStorage? localStorage = null, MessageService? messageService = null, bool suppressUnsecuredMessage = false) { Services.AddLocalization(); Services.AddOptions(); @@ -144,7 +188,11 @@ private void SetupMainLayoutServices(TestLocalStorage? localStorage = null, Mess Services.AddSingleton(); Services.AddSingleton(); Services.AddSingleton(); - Services.Configure(o => o.Otlp.AuthMode = OtlpAuthMode.Unsecured); + Services.Configure(o => + { + o.Otlp.AuthMode = OtlpAuthMode.Unsecured; + o.Otlp.SuppressUnsecuredTelemetryMessage = suppressUnsecuredMessage; + }); var version = typeof(FluentMain).Assembly.GetName().Version!; diff --git a/tests/Aspire.Dashboard.Tests/DashboardOptionsTests.cs b/tests/Aspire.Dashboard.Tests/DashboardOptionsTests.cs index 64666c3d07c..abd84ec8a77 100644 --- a/tests/Aspire.Dashboard.Tests/DashboardOptionsTests.cs +++ b/tests/Aspire.Dashboard.Tests/DashboardOptionsTests.cs @@ -245,6 +245,27 @@ public void OtlpOptions_HTTP_InvalidUrl() Assert.Equal("Failed to parse OTLP HTTP endpoint URL 'invalid'.", result.FailureMessage); } + [Fact] + public void OtlpOptions_SuppressUnsecuredTelemetryMessage_DefaultValue() + { + var options = GetValidOptions(); + + Assert.False(options.Otlp.SuppressUnsecuredTelemetryMessage); + } + + [Fact] + public void OtlpOptions_SuppressUnsecuredTelemetryMessage_CanBeSet() + { + var options = GetValidOptions(); + options.Otlp.SuppressUnsecuredTelemetryMessage = true; + + Assert.True(options.Otlp.SuppressUnsecuredTelemetryMessage); + + // Ensure setting this property doesn't affect validation + var result = new ValidateDashboardOptions().Validate(null, options); + Assert.True(result.Succeeded); + } + #endregion #region OpenIDConnect options From 51cad31ffa16f9e3aeb810f6405e5fdbbb32a2b4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 26 Sep 2025 06:59:55 +0000 Subject: [PATCH 3/5] Address feedback: Parameterize MainLayout test and remove DashboardOptions tests Co-authored-by: JamesNK <303201+JamesNK@users.noreply.github.com> --- .../Layout/MainLayoutTests.cs | 26 +++++++++++++------ .../DashboardOptionsTests.cs | 21 --------------- 2 files changed, 18 insertions(+), 29 deletions(-) diff --git a/tests/Aspire.Dashboard.Components.Tests/Layout/MainLayoutTests.cs b/tests/Aspire.Dashboard.Components.Tests/Layout/MainLayoutTests.cs index bcb93335df6..ccd1634277d 100644 --- a/tests/Aspire.Dashboard.Components.Tests/Layout/MainLayoutTests.cs +++ b/tests/Aspire.Dashboard.Components.Tests/Layout/MainLayoutTests.cs @@ -125,14 +125,16 @@ public async Task OnInitialize_UnsecuredOtlp_Dismissed_NoMessageBar() Assert.Empty(messageService.AllMessages); } - [Fact] - public async Task OnInitialize_UnsecuredOtlp_SuppressConfigured_NoMessageBar() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task OnInitialize_UnsecuredOtlp_SuppressConfigured_NoMessageBar(bool suppressUnsecuredMessage) { // Arrange var testLocalStorage = new TestLocalStorage(); var messageService = new MessageService(); - SetupMainLayoutServices(localStorage: testLocalStorage, messageService: messageService, suppressUnsecuredMessage: true); + SetupMainLayoutServices(localStorage: testLocalStorage, messageService: messageService, suppressUnsecuredMessage: suppressUnsecuredMessage); var messageShownTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); messageService.OnMessageItemsUpdatedAsync += () => @@ -145,7 +147,7 @@ public async Task OnInitialize_UnsecuredOtlp_SuppressConfigured_NoMessageBar() { if (key == BrowserStorageKeys.UnsecuredTelemetryMessageDismissedKey) { - return (false, false); // Message not dismissed, but should be suppressed by config + return (false, false); // Message not dismissed, but should be suppressed by config if suppressUnsecuredMessage is true } else { @@ -163,10 +165,18 @@ public async Task OnInitialize_UnsecuredOtlp_SuppressConfigured_NoMessageBar() var timeoutTask = Task.Delay(100); var completedTask = await Task.WhenAny(messageShownTcs.Task, timeoutTask).WaitAsync(TimeSpan.FromSeconds(5)); - // It's hard to test something not happening. - // In this case of checking for a message, apply a small display and then double check that no message was displayed. - Assert.True(completedTask != messageShownTcs.Task, "No message bar should be displayed when suppressed by configuration."); - Assert.Empty(messageService.AllMessages); + if (suppressUnsecuredMessage) + { + // When suppressed, no message should be displayed + Assert.True(completedTask != messageShownTcs.Task, "No message bar should be displayed when suppressed by configuration."); + Assert.Empty(messageService.AllMessages); + } + else + { + // When not suppressed, message should be displayed since it wasn't dismissed + Assert.True(completedTask == messageShownTcs.Task, "Message bar should be displayed when not suppressed and not dismissed."); + Assert.NotEmpty(messageService.AllMessages); + } } private void SetupMainLayoutServices(TestLocalStorage? localStorage = null, MessageService? messageService = null, bool suppressUnsecuredMessage = false) diff --git a/tests/Aspire.Dashboard.Tests/DashboardOptionsTests.cs b/tests/Aspire.Dashboard.Tests/DashboardOptionsTests.cs index abd84ec8a77..64666c3d07c 100644 --- a/tests/Aspire.Dashboard.Tests/DashboardOptionsTests.cs +++ b/tests/Aspire.Dashboard.Tests/DashboardOptionsTests.cs @@ -245,27 +245,6 @@ public void OtlpOptions_HTTP_InvalidUrl() Assert.Equal("Failed to parse OTLP HTTP endpoint URL 'invalid'.", result.FailureMessage); } - [Fact] - public void OtlpOptions_SuppressUnsecuredTelemetryMessage_DefaultValue() - { - var options = GetValidOptions(); - - Assert.False(options.Otlp.SuppressUnsecuredTelemetryMessage); - } - - [Fact] - public void OtlpOptions_SuppressUnsecuredTelemetryMessage_CanBeSet() - { - var options = GetValidOptions(); - options.Otlp.SuppressUnsecuredTelemetryMessage = true; - - Assert.True(options.Otlp.SuppressUnsecuredTelemetryMessage); - - // Ensure setting this property doesn't affect validation - var result = new ValidateDashboardOptions().Validate(null, options); - Assert.True(result.Succeeded); - } - #endregion #region OpenIDConnect options From b3bfdb1cb01676df46d7017fa3ff629ca08a0597 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Fri, 26 Sep 2025 15:13:36 +0800 Subject: [PATCH 4/5] Apply suggestion from @JamesNK --- .../Layout/MainLayoutTests.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/Aspire.Dashboard.Components.Tests/Layout/MainLayoutTests.cs b/tests/Aspire.Dashboard.Components.Tests/Layout/MainLayoutTests.cs index ccd1634277d..c8873c8dbde 100644 --- a/tests/Aspire.Dashboard.Components.Tests/Layout/MainLayoutTests.cs +++ b/tests/Aspire.Dashboard.Components.Tests/Layout/MainLayoutTests.cs @@ -162,11 +162,11 @@ public async Task OnInitialize_UnsecuredOtlp_SuppressConfigured_NoMessageBar(boo }); // Assert - var timeoutTask = Task.Delay(100); - var completedTask = await Task.WhenAny(messageShownTcs.Task, timeoutTask).WaitAsync(TimeSpan.FromSeconds(5)); - if (suppressUnsecuredMessage) { + var timeoutTask = Task.Delay(100); + var completedTask = await Task.WhenAny(messageShownTcs.Task, timeoutTask).DefaultTimeout(); + // When suppressed, no message should be displayed Assert.True(completedTask != messageShownTcs.Task, "No message bar should be displayed when suppressed by configuration."); Assert.Empty(messageService.AllMessages); @@ -174,7 +174,7 @@ public async Task OnInitialize_UnsecuredOtlp_SuppressConfigured_NoMessageBar(boo else { // When not suppressed, message should be displayed since it wasn't dismissed - Assert.True(completedTask == messageShownTcs.Task, "Message bar should be displayed when not suppressed and not dismissed."); + await messageShownTcs.Task.DefaultTimeout(); Assert.NotEmpty(messageService.AllMessages); } } From cc13d09f25dd75a8a76cbc7a3eeb8594c73346a7 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Fri, 26 Sep 2025 15:21:44 +0800 Subject: [PATCH 5/5] Update --- .../Layout/MainLayoutTests.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/Aspire.Dashboard.Components.Tests/Layout/MainLayoutTests.cs b/tests/Aspire.Dashboard.Components.Tests/Layout/MainLayoutTests.cs index c8873c8dbde..6c3b2f662cc 100644 --- a/tests/Aspire.Dashboard.Components.Tests/Layout/MainLayoutTests.cs +++ b/tests/Aspire.Dashboard.Components.Tests/Layout/MainLayoutTests.cs @@ -12,6 +12,7 @@ using Aspire.Dashboard.Tests; using Aspire.Dashboard.Utils; using Bunit; +using Microsoft.AspNetCore.InternalTesting; using Microsoft.Extensions.DependencyInjection; using Microsoft.FluentUI.AspNetCore.Components; using Microsoft.FluentUI.AspNetCore.Components.Components.Tooltip; @@ -72,13 +73,13 @@ public async Task OnInitialize_UnsecuredOtlp_NotDismissed_DisplayMessageBar() }); // Assert - await messageShownTcs.Task.WaitAsync(TimeSpan.FromSeconds(5)); + await messageShownTcs.Task.DefaultTimeout(); Assert.NotNull(message); message.Close(); - Assert.True(await dismissedSettingSetTcs.Task.WaitAsync(TimeSpan.FromSeconds(5))); + Assert.True(await dismissedSettingSetTcs.Task.DefaultTimeout()); } [Fact] @@ -117,7 +118,7 @@ public async Task OnInitialize_UnsecuredOtlp_Dismissed_NoMessageBar() // Assert var timeoutTask = Task.Delay(100); - var completedTask = await Task.WhenAny(messageShownTcs.Task, timeoutTask).WaitAsync(TimeSpan.FromSeconds(5)); + var completedTask = await Task.WhenAny(messageShownTcs.Task, timeoutTask).DefaultTimeout(); // It's hard to test something not happening. // In this case of checking for a message, apply a small display and then double check that no message was displayed.