Files
ScadaBridge/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/LoginTests.cs
T
Joseph Doherty a04eb3a752 test(playwright): token-endpoint tests tolerate DisableLogin
Under DisableLogin /auth/token deliberately answers 404 (lane K). The tests
now probe it: on a login-disabled rig TokenEndpoint_ReturnsJwt skips and the
bad-credentials test asserts 404 (the exchange is closed); with login enabled
they keep the 200 + JWT and 401 assertions.
2026-10-22 14:27:26 -04:00

154 lines
6.4 KiB
C#

using System.Text.Json;
using Microsoft.Playwright;
namespace ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests;
[Collection("Playwright")]
public class LoginTests
{
private readonly PlaywrightFixture _fixture;
/// <summary>Initializes the tests with the shared Playwright fixture.</summary>
/// <param name="fixture">The shared Playwright fixture.</param>
public LoginTests(PlaywrightFixture fixture)
{
_fixture = fixture;
}
/// <summary>Verifies that an unauthenticated user is redirected to the login page.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[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 "<Product> — 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");
}
/// <summary>Verifies that valid credentials authenticate the user successfully.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[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);
}
/// <summary>Verifies that invalid credentials show an error on the login page.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[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<int>(@"
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
}
/// <summary>
/// Verifies that the token endpoint returns a JWT for valid credentials. Skipped when the
/// rig runs with <c>DisableLogin</c>, where <c>/auth/token</c> deliberately answers 404.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[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<JsonElement>(@"
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());
}
/// <summary>
/// Verifies that the token endpoint rejects invalid credentials: HTTP 401 with login
/// enabled, or HTTP 404 under <c>DisableLogin</c> (the exchange is closed entirely, so
/// even bad credentials can never reach a credential check).
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[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<IPage> OpenLoginPageAsync()
{
var page = await _fixture.NewPageAsync();
await page.GotoAsync($"{PlaywrightFixture.BaseUrl}/login");
await page.WaitForLoadStateAsync(LoadState.DOMContentLoaded);
return page;
}
/// <summary>
/// Probes whether the rig runs with <c>DisableLogin</c>: 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.
/// </summary>
private static async Task<bool> TokenEndpointDisabledAsync(IPage page) =>
await PostTokenAsync(page, PlaywrightFixture.TestUsername, PlaywrightFixture.TestPassword) == 404;
private static Task<int> PostTokenAsync(IPage page, string username, string password) =>
page.EvaluateAsync<int>(@"
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);
}