fix(central-ui): resolve CentralUI-020..025 — auth-ping idle logout, DebugView race, push-handler disposal guard, JS-interop catch narrowing, claim-constant helper, SessionExpiry tests
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
using System.Security.Claims;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using ScadaLink.CentralUI.Auth;
|
||||
|
||||
namespace ScadaLink.CentralUI.Tests.Auth;
|
||||
|
||||
/// <summary>
|
||||
/// Regression tests for CentralUI-020. The Blazor circuit's
|
||||
/// <c>CookieAuthenticationStateProvider</c> serves a frozen constructor-time
|
||||
/// principal, so <c>SessionExpiry</c> could never observe a server-side cookie
|
||||
/// expiry by polling the auth state. The fix adds <c>GET /auth/ping</c>, an
|
||||
/// endpoint evaluated per HTTP request (where the cookie middleware re-validates
|
||||
/// the cookie): it returns 200 while the session is live and 401 once the
|
||||
/// cookie has lapsed, giving <c>SessionExpiry</c> a real signal to redirect on.
|
||||
/// </summary>
|
||||
public class AuthPingEndpointTests
|
||||
{
|
||||
private static IReadOnlyList<RouteEndpoint> BuildEndpoints()
|
||||
{
|
||||
var builder = WebApplication.CreateBuilder();
|
||||
builder.Services.AddRouting();
|
||||
builder.Services.AddAntiforgery();
|
||||
var app = builder.Build();
|
||||
app.MapAuthEndpoints();
|
||||
|
||||
return ((IEndpointRouteBuilder)app).DataSources
|
||||
.SelectMany(ds => ds.Endpoints)
|
||||
.OfType<RouteEndpoint>()
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static RouteEndpoint? Find(IReadOnlyList<RouteEndpoint> endpoints, string pattern, string method)
|
||||
=> endpoints.FirstOrDefault(e =>
|
||||
e.RoutePattern.RawText == pattern &&
|
||||
(e.Metadata.GetMetadata<HttpMethodMetadata>()?.HttpMethods.Contains(method) ?? false));
|
||||
|
||||
[Fact]
|
||||
public void AuthPing_GetRoute_IsMapped()
|
||||
{
|
||||
var ping = Find(BuildEndpoints(), "/auth/ping", "GET");
|
||||
Assert.NotNull(ping);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AuthPing_AnonymousUser_Returns401()
|
||||
{
|
||||
var context = new DefaultHttpContext
|
||||
{
|
||||
User = new ClaimsPrincipal(new ClaimsIdentity()) // not authenticated
|
||||
};
|
||||
|
||||
await AuthEndpoints.HandlePing(context);
|
||||
|
||||
Assert.Equal(StatusCodes.Status401Unauthorized, context.Response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AuthPing_AuthenticatedUser_Returns200()
|
||||
{
|
||||
var identity = new ClaimsIdentity(
|
||||
new[] { new Claim(ClaimTypes.Name, "alice") }, authenticationType: "TestCookie");
|
||||
var context = new DefaultHttpContext
|
||||
{
|
||||
User = new ClaimsPrincipal(identity)
|
||||
};
|
||||
|
||||
await AuthEndpoints.HandlePing(context);
|
||||
|
||||
Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AuthPing_DoesNotTriggerCookieRedirect()
|
||||
{
|
||||
// The endpoint must NOT use RequireAuthorization(): that would make the
|
||||
// cookie middleware answer an expired request with a 302 to /login,
|
||||
// which a fetch() follows transparently and reads as a 200 login page —
|
||||
// SessionExpiry would never see the expiry. The endpoint allows
|
||||
// anonymous access and decides 200/401 itself.
|
||||
var ping = Find(BuildEndpoints(), "/auth/ping", "GET");
|
||||
Assert.NotNull(ping);
|
||||
|
||||
var authorize = ping!.Metadata
|
||||
.GetOrderedMetadata<Microsoft.AspNetCore.Authorization.IAuthorizeData>();
|
||||
Assert.Empty(authorize);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user