| File: Integration\Playwright\BrowserTokenAuthenticationTests.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 Aspire.TestUtilities; using Aspire.Dashboard.Configuration; using Aspire.Dashboard.Resources; using Aspire.Dashboard.Tests.Integration.Playwright.Infrastructure; using Aspire.Hosting; using Microsoft.AspNetCore.InternalTesting; using Microsoft.Playwright; using Xunit; namespace Aspire.Dashboard.Tests.Integration.Playwright; [RequiresFeature(TestFeature.Playwright)] public class BrowserTokenAuthenticationTests : PlaywrightTestsBase<BrowserTokenAuthenticationTests.BrowserTokenDashboardServerFixture> { public class BrowserTokenDashboardServerFixture : DashboardServerFixture { public BrowserTokenDashboardServerFixture() { Configuration[DashboardConfigNames.DashboardFrontendAuthModeName.ConfigKey] = nameof(FrontendAuthMode.BrowserToken); Configuration[DashboardConfigNames.DashboardFrontendBrowserTokenName.ConfigKey] = "VALID_TOKEN"; } } public sealed class BrowserTokenDashboardServerWithHttpAndHttpsFixture : DashboardServerFixture { public BrowserTokenDashboardServerWithHttpAndHttpsFixture() { // Bind to 127.0.0.1 rather than the "localhost" hostname: Kestrel rejects dynamic-port // binding (":0") on "localhost" with "Dynamic port binding is not supported when binding // to localhost. You must either bind to 127.0.0.1:0 or [::1]:0, or both." The WebKit test // navigates to the resolved address with IgnoreHTTPSErrors, so the loopback IP is fine. Configuration[DashboardConfigNames.DashboardFrontendUrlName.ConfigKey] = "https://127.0.0.1:0;http://127.0.0.1:0"; Configuration[DashboardConfigNames.DashboardFrontendAuthModeName.ConfigKey] = nameof(FrontendAuthMode.BrowserToken); Configuration[DashboardConfigNames.DashboardFrontendBrowserTokenName.ConfigKey] = "VALID_TOKEN"; } } public BrowserTokenAuthenticationTests(BrowserTokenDashboardServerFixture dashboardServerFixture) : base(dashboardServerFixture) { } [Fact] [OuterloopTest("Resource-intensive Playwright browser test")] public async Task BrowserToken_LoginPage_Success_RedirectToResources() { // Arrange await RunTestAsync(async page => { // Act var response = await page.GotoAsync("/").DefaultTimeout(); var uri = new Uri(response!.Url); Assert.Equal("/login?returnUrl=%2F", uri.PathAndQuery); var tokenTextBox = page.GetByRole(AriaRole.Textbox); await tokenTextBox.FillAsync("VALID_TOKEN").DefaultTimeout(); var submitButton = SubmitButton(page); await submitButton.ClickAsync().DefaultTimeout(); // Wait for navigation to complete after successful login. // The page redirects from /login to / (resources page). await page.WaitForURLAsync(url => new Uri(url).AbsolutePath == "/").DefaultTimeout(); // Assert await Assertions .Expect(page.GetByText(MockDashboardClient.TestResource1.DisplayName)) .ToBeVisibleAsync() .DefaultTimeout(); }); } [Fact] [OuterloopTest("Resource-intensive Playwright browser test")] public async Task BrowserToken_LoginPage_SuccessWithWhitespaceToken_RedirectToResources() { // Arrange await RunTestAsync(async page => { // Act var response = await page.GotoAsync("/").DefaultTimeout(); var uri = new Uri(response!.Url); Assert.Equal("/login?returnUrl=%2F", uri.PathAndQuery); var tokenTextBox = page.GetByRole(AriaRole.Textbox); await tokenTextBox.FillAsync(" VALID_TOKEN ").DefaultTimeout(); var submitButton = SubmitButton(page); await submitButton.ClickAsync().DefaultTimeout(); await page.WaitForURLAsync(url => new Uri(url).AbsolutePath == "/").DefaultTimeout(); // Assert await Assertions .Expect(page.GetByText(MockDashboardClient.TestResource1.DisplayName)) .ToBeVisibleAsync() .DefaultTimeout(); }); } [Fact] [OuterloopTest("Resource-intensive Playwright browser test")] public async Task BrowserToken_LoginPage_Failure_DisplayFailureMessage() { // Arrange await RunTestAsync(async page => { // Act var response = await page.GotoAsync("/").DefaultTimeout(); var uri = new Uri(response!.Url); Assert.Equal("/login?returnUrl=%2F", uri.PathAndQuery); var tokenTextBox = page.GetByRole(AriaRole.Textbox); await tokenTextBox.FillAsync("INVALID_TOKEN").DefaultTimeout(); var submitButton = SubmitButton(page); await submitButton.ClickAsync().DefaultTimeout(); // Assert await Assertions .Expect(page.GetByText(Login.InvalidTokenErrorMessage)) .ToBeVisibleAsync() .DefaultTimeout(); }); } [Fact] [OuterloopTest("Resource-intensive Playwright browser test")] public async Task BrowserToken_QueryStringToken_Success_RestrictToResources() { // Arrange await RunTestAsync(async page => { // Act await page.GotoAsync("/login?t=VALID_TOKEN").DefaultTimeout(); // Assert await Assertions .Expect(page.GetByText(MockDashboardClient.TestResource1.DisplayName)) .ToBeVisibleAsync() .DefaultTimeout(); }); } [Fact] [OuterloopTest("Resource-intensive Playwright browser test")] public async Task BrowserToken_QueryStringToken_Failure_DisplayLoginPage() { // Arrange await RunTestAsync(async page => { // Act await page.GotoAsync("/login?t=INVALID_TOKEN").DefaultTimeout(); var submitButton = SubmitButton(page); var name = await submitButton.GetAttributeAsync("name").DefaultTimeout(); // Assert Assert.Equal("submit-token", name); }); } private static ILocator SubmitButton(IPage page) => page.Locator("fluent-button[name='submit-token']"); } [RequiresFeature(TestFeature.Playwright)] public sealed class BrowserTokenAuthenticationApplicationNameTests(PlaywrightFixture playwrightFixture) : IClassFixture<PlaywrightFixture> { private const string BrowserToken = "VALID_TOKEN"; [Theory] [InlineData("Same application", "Same application", true)] [InlineData("First application", "Second application", false)] [OuterloopTest("Resource-intensive Playwright browser test")] public async Task BrowserToken_ApplicationName_ScopesAuthentication(string firstApplicationName, string secondApplicationName, bool canAccessBoth) { await using var firstDashboard = CreateDashboard(firstApplicationName); await using var secondDashboard = CreateDashboard(secondApplicationName); await firstDashboard.StartAsync(); await secondDashboard.StartAsync(); var firstBaseUrl = firstDashboard.FrontendSingleEndPointAccessor().GetResolvedAddress(); var secondBaseUrl = secondDashboard.FrontendSingleEndPointAccessor().GetResolvedAddress(); // Browser cookies are scoped to the loopback host rather than the port, so both dashboards // must be accessed from the same browser context to reproduce localhost cookie sharing. await using var context = await playwrightFixture.Browser.NewContextAsync(); var page = await context.NewPageAsync(); var firstResponse = await page.GotoAsync($"{firstBaseUrl}/login?t={BrowserToken}").DefaultTimeout(TestConstants.LongTimeoutTimeSpan); Assert.Equal("/", new Uri(firstResponse!.Url).AbsolutePath); var secondResponse = await page.GotoAsync(secondBaseUrl).DefaultTimeout(TestConstants.LongTimeoutTimeSpan); Assert.Equal(canAccessBoth ? "/" : "/login", new Uri(secondResponse!.Url).AbsolutePath); } private static DashboardWebApplication CreateDashboard(string applicationName) { var configuration = new Dictionary<string, string?> { [DashboardConfigNames.DashboardFrontendUrlName.ConfigKey] = "http://127.0.0.1:0", [DashboardConfigNames.DashboardOtlpHttpUrlName.ConfigKey] = "http://127.0.0.1:0", [DashboardConfigNames.DashboardOtlpAuthModeName.ConfigKey] = nameof(OtlpAuthMode.Unsecured), [DashboardConfigNames.DashboardFrontendAuthModeName.ConfigKey] = nameof(FrontendAuthMode.BrowserToken), [DashboardConfigNames.DashboardFrontendBrowserTokenName.ConfigKey] = BrowserToken, [DashboardConfigNames.DashboardApplicationName.ConfigKey] = applicationName }; return DashboardServerFixture.CreateDashboardApp(configuration); } } [RequiresFeature(TestFeature.Playwright)] public class BrowserTokenAuthenticationHttpAndHttpsTests : PlaywrightTestsBase<BrowserTokenAuthenticationTests.BrowserTokenDashboardServerWithHttpAndHttpsFixture> { public BrowserTokenAuthenticationHttpAndHttpsTests(BrowserTokenAuthenticationTests.BrowserTokenDashboardServerWithHttpAndHttpsFixture dashboardServerFixture) : base(dashboardServerFixture) { } [Fact] [OuterloopTest("Resource-intensive Playwright browser test")] public async Task BrowserToken_QueryStringToken_HttpsThenHttp_WebKit_Success() { using var playwright = await Microsoft.Playwright.Playwright.CreateAsync(); await using var browser = await LaunchWebKitAsync(playwright); await RunHttpsThenHttpAsync(browser); } private static async Task<IBrowser> LaunchWebKitAsync(IPlaywright playwright) { try { return await playwright.Webkit.LaunchAsync(new BrowserTypeLaunchOptions { Headless = true }); } catch (PlaywrightException ex) when (IsWebKitBrowserUnavailable(ex)) { Assert.Skip("Playwright WebKit is not available in this environment."); throw; } } private static bool IsWebKitBrowserUnavailable(PlaywrightException ex) { return ex.Message.Contains("Executable doesn't exist", StringComparison.Ordinal) || ex.Message.Contains("Host system is missing dependencies", StringComparison.Ordinal); } private async Task RunHttpsThenHttpAsync(IBrowser browser) { var endpoints = DashboardServerFixture.DashboardApp.FrontendEndPointsAccessor .Select(accessor => accessor()) .ToList(); var httpsEndpoint = endpoints.Single(e => e.IsHttps); var httpEndpoint = endpoints.Single(e => !e.IsHttps); var httpsBaseUrl = httpsEndpoint.GetResolvedAddress(replaceIPAnyWithLocalhost: true); var httpBaseUrl = httpEndpoint.GetResolvedAddress(replaceIPAnyWithLocalhost: true); var context = await browser.NewContextAsync(new BrowserNewContextOptions { IgnoreHTTPSErrors = true }); PlaywrightFixture.ConfigureTimeouts(context); try { var page = await context.NewPageAsync(); try { await page.GotoAsync($"{httpsBaseUrl}/login?t=VALID_TOKEN").DefaultTimeout(); await Assertions .Expect(page.GetByText(MockDashboardClient.TestResource1.DisplayName)) .ToBeVisibleAsync() .DefaultTimeout(); await page.GotoAsync($"{httpBaseUrl}/login?t=VALID_TOKEN").DefaultTimeout(); await Assertions .Expect(page.GetByText(MockDashboardClient.TestResource1.DisplayName)) .ToBeVisibleAsync() .DefaultTimeout(); await page.GotoAsync($"{httpBaseUrl}/structuredlogs").DefaultTimeout(); Assert.Equal("/structuredlogs", new Uri(page.Url).AbsolutePath); await Assertions .Expect(page.GetByRole(AriaRole.Button, new() { Name = "submit-token" })) .Not .ToBeVisibleAsync() .DefaultTimeout(); } finally { await page.CloseAsync(); } } finally { await context.DisposeAsync(); } } }