using System.Text.Json; using Microsoft.Playwright; namespace ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests; [Collection("Playwright")] public class LoginTests { private readonly PlaywrightFixture _fixture; /// Initializes the tests with the shared Playwright fixture. /// The shared Playwright fixture. public LoginTests(PlaywrightFixture fixture) { _fixture = fixture; } /// Verifies that an unauthenticated user is redirected to the login page. /// A task that represents the asynchronous operation. [Fact] public async Task UnauthenticatedUser_RedirectsToLogin() { var page = await _fixture.NewPageAsync(); await page.GotoAsync(PlaywrightFixture.BaseUrl); Assert.Contains("/login", page.Url); // The kit's LoginCard renders the heading " — sign in" with the // product token in its own .login-product span; the submit button reads // "Sign in". await Expect(page.Locator(".login-product")).ToHaveTextAsync("ScadaBridge"); await Expect(page.Locator("#username")).ToBeVisibleAsync(); await Expect(page.Locator("#password")).ToBeVisibleAsync(); await Expect(page.Locator("button[type='submit']")).ToHaveTextAsync("Sign in"); } /// Verifies that valid credentials authenticate the user successfully. /// A task that represents the asynchronous operation. [Fact] public async Task ValidCredentials_AuthenticatesSuccessfully() { var page = await _fixture.NewAuthenticatedPageAsync(); // Should be on the dashboard, not the login page Assert.DoesNotContain("/login", page.Url); } /// Verifies that invalid credentials show an error on the login page. /// A task that represents the asynchronous operation. [Fact] public async Task InvalidCredentials_ShowsError() { var page = await _fixture.NewPageAsync(); await page.GotoAsync($"{PlaywrightFixture.BaseUrl}/login"); await page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); // POST invalid credentials via fetch var status = await page.EvaluateAsync(@" async () => { const resp = await fetch('/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: 'username=baduser&password=badpass&__RequestVerificationToken=' + encodeURIComponent(document.querySelector('input[name=__RequestVerificationToken]').value), redirect: 'follow' }); return resp.status; } "); // The login endpoint redirects to /login?error=... on failure. // Reload the page to see the error state. await page.ReloadAsync(); Assert.Equal(200, status); // redirect followed to login page } /// /// Verifies that the token endpoint returns a JWT for valid credentials. Skipped when the /// rig runs with DisableLogin, where /auth/token deliberately answers 404. /// /// A task that represents the asynchronous operation. [SkippableFact] public async Task TokenEndpoint_ReturnsJwt() { var page = await OpenLoginPageAsync(); Skip.If(await TokenEndpointDisabledAsync(page), "Login is disabled on this rig: POST /auth/token answers 404 by design, so no JWT is issued."); var result = await page.EvaluateAsync(@" async () => { const resp = await fetch('/auth/token', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: 'username=multi-role&password=password' }); const json = await resp.json(); return { status: resp.status, hasToken: !!json.access_token, username: json.username || '' }; } "); Assert.Equal(200, result.GetProperty("status").GetInt32()); Assert.True(result.GetProperty("hasToken").GetBoolean()); Assert.Equal("multi-role", result.GetProperty("username").GetString()); } /// /// Verifies that the token endpoint rejects invalid credentials: HTTP 401 with login /// enabled, or HTTP 404 under DisableLogin (the exchange is closed entirely, so /// even bad credentials can never reach a credential check). /// /// A task that represents the asynchronous operation. [Fact] public async Task TokenEndpoint_InvalidCredentials_Returns401() { var page = await OpenLoginPageAsync(); var disabled = await TokenEndpointDisabledAsync(page); var status = await PostTokenAsync(page, "baduser", "badpass"); Assert.Equal(disabled ? 404 : 401, status); } private async Task OpenLoginPageAsync() { var page = await _fixture.NewPageAsync(); await page.GotoAsync($"{PlaywrightFixture.BaseUrl}/login"); await page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); return page; } /// /// Probes whether the rig runs with DisableLogin: the token exchange then answers /// 404 for any request (AuthEndpoints.HandleTokenAsync), while with login enabled a /// valid-credential POST is a 200 and never a 404. /// private static async Task TokenEndpointDisabledAsync(IPage page) => await PostTokenAsync(page, PlaywrightFixture.TestUsername, PlaywrightFixture.TestPassword) == 404; private static Task PostTokenAsync(IPage page, string username, string password) => page.EvaluateAsync(@" async ([u, p]) => { const resp = await fetch('/auth/token', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ username: u, password: p }).toString() }); return resp.status; } ", new object[] { username, password }); private static ILocatorAssertions Expect(ILocator locator) => Assertions.Expect(locator); }