116 lines
4.8 KiB
C#
116 lines
4.8 KiB
C#
using Bunit;
|
|
using Bunit.TestDoubles;
|
|
using Microsoft.AspNetCore.Components;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using ZB.MOM.WW.ScadaBridge.CentralUI.Components.Shared;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Auth;
|
|
|
|
/// <summary>
|
|
/// Regression tests for the session-expiry redirect. <c>SessionExpiry</c>
|
|
/// used to poll the Blazor <c>AuthenticationStateProvider</c>, which (via
|
|
/// <c>CookieAuthenticationStateProvider</c>) serves a frozen constructor-time
|
|
/// principal — so the polled state could never become "expired" and the
|
|
/// idle-logout redirect never fired. The component now polls the server
|
|
/// <c>GET /auth/ping</c> endpoint, which reflects the live cookie session: a
|
|
/// 401 response triggers a redirect to <c>/login</c>. These tests exercise that
|
|
/// redirect path directly (the path was previously untested).
|
|
/// </summary>
|
|
public class SessionExpiryComponentTests : BunitContext
|
|
{
|
|
private const string ModulePath = "./_content/ZB.MOM.WW.ScadaBridge.CentralUI/js/session-expiry.js";
|
|
|
|
/// <summary>Verifies that an expired session redirects to the login page.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task CheckSession_ExpiredSession_RedirectsToLogin()
|
|
{
|
|
// The server reports the cookie has lapsed: ping returns HTTP 401.
|
|
var module = JSInterop.SetupModule(ModulePath);
|
|
module.Setup<int>("ping", "/auth/ping").SetResult(401);
|
|
|
|
var nav = Services.GetRequiredService<NavigationManager>();
|
|
var cut = Render<SessionExpiry>();
|
|
|
|
await cut.InvokeAsync(() => cut.Instance.CheckSessionAsync());
|
|
|
|
Assert.EndsWith("/login", nav.Uri);
|
|
}
|
|
|
|
/// <summary>Verifies a 401 landing after disposal neither redirects nor throws (review 2026-10-21 07/C-3).</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task CheckSession_ExpiredAfterDispose_DoesNotRedirectOrThrow()
|
|
{
|
|
var module = JSInterop.SetupModule(ModulePath);
|
|
module.Setup<int>("ping", "/auth/ping").SetResult(401);
|
|
|
|
var nav = Services.GetRequiredService<NavigationManager>();
|
|
var before = nav.Uri;
|
|
var cut = Render<SessionExpiry>();
|
|
var instance = cut.Instance;
|
|
await cut.InvokeAsync(() => ((IDisposable)instance).Dispose());
|
|
|
|
await instance.CheckSessionAsync();
|
|
|
|
Assert.Equal(before, nav.Uri);
|
|
}
|
|
|
|
/// <summary>Verifies that a live session does not redirect.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task CheckSession_LiveSession_DoesNotRedirect()
|
|
{
|
|
// The server reports the session is still valid: ping returns HTTP 200.
|
|
var module = JSInterop.SetupModule(ModulePath);
|
|
module.Setup<int>("ping", "/auth/ping").SetResult(200);
|
|
|
|
var nav = Services.GetRequiredService<NavigationManager>();
|
|
var before = nav.Uri;
|
|
var cut = Render<SessionExpiry>();
|
|
|
|
await cut.InvokeAsync(() => cut.Instance.CheckSessionAsync());
|
|
|
|
Assert.Equal(before, nav.Uri);
|
|
Assert.DoesNotContain("/login", nav.Uri);
|
|
}
|
|
|
|
/// <summary>Verifies that a transient network failure does not redirect.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task CheckSession_TransientNetworkFailure_DoesNotRedirect()
|
|
{
|
|
// A network blip surfaces as status 0 — inconclusive. The component must
|
|
// NOT log an authenticated user out on a transient failure.
|
|
var module = JSInterop.SetupModule(ModulePath);
|
|
module.Setup<int>("ping", "/auth/ping").SetResult(0);
|
|
|
|
var nav = Services.GetRequiredService<NavigationManager>();
|
|
var before = nav.Uri;
|
|
var cut = Render<SessionExpiry>();
|
|
|
|
await cut.InvokeAsync(() => cut.Instance.CheckSessionAsync());
|
|
|
|
Assert.Equal(before, nav.Uri);
|
|
}
|
|
|
|
/// <summary>Verifies that checking the session on the login page neither pings the server nor redirects.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task CheckSession_OnLoginPage_DoesNotPingOrRedirect()
|
|
{
|
|
// On /login the component must neither poll nor redirect (a /login →
|
|
// /login redirect would loop). JSInterop is left in Strict mode with no
|
|
// module setup, so any ping call would throw and fail the test.
|
|
var nav = (BunitNavigationManager)Services
|
|
.GetRequiredService<NavigationManager>();
|
|
nav.NavigateTo("login");
|
|
|
|
var cut = Render<SessionExpiry>();
|
|
await cut.InvokeAsync(() => cut.Instance.CheckSessionAsync());
|
|
|
|
// No JS module import was attempted and the URL is unchanged.
|
|
Assert.EndsWith("/login", nav.Uri);
|
|
}
|
|
}
|