using Bunit; using Bunit.TestDoubles; using Microsoft.AspNetCore.Components; using Microsoft.Extensions.DependencyInjection; using ScadaLink.CentralUI.Components.Shared; namespace ScadaLink.CentralUI.Tests.Auth; /// /// Regression tests for CentralUI-020 and CentralUI-025. SessionExpiry /// used to poll the Blazor AuthenticationStateProvider, which (via /// CookieAuthenticationStateProvider) 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 /// GET /auth/ping endpoint, which reflects the live cookie session: a /// 401 response triggers a redirect to /login. These tests exercise that /// redirect path directly (CentralUI-025: the path was previously untested). /// public class SessionExpiryComponentTests : BunitContext { private const string ModulePath = "./_content/ScadaLink.CentralUI/js/session-expiry.js"; [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("ping", "/auth/ping").SetResult(401); var nav = Services.GetRequiredService(); var cut = Render(); await cut.InvokeAsync(() => cut.Instance.CheckSessionAsync()); Assert.EndsWith("/login", nav.Uri); } [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("ping", "/auth/ping").SetResult(200); var nav = Services.GetRequiredService(); var before = nav.Uri; var cut = Render(); await cut.InvokeAsync(() => cut.Instance.CheckSessionAsync()); Assert.Equal(before, nav.Uri); Assert.DoesNotContain("/login", nav.Uri); } [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("ping", "/auth/ping").SetResult(0); var nav = Services.GetRequiredService(); var before = nav.Uri; var cut = Render(); await cut.InvokeAsync(() => cut.Instance.CheckSessionAsync()); Assert.Equal(before, nav.Uri); } [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(); nav.NavigateTo("login"); var cut = Render(); await cut.InvokeAsync(() => cut.Instance.CheckSessionAsync()); // No JS module import was attempted and the URL is unchanged. Assert.EndsWith("/login", nav.Uri); } }