f0de11c446
GetAuthenticationStateAsync (display reads: current username, nav role checks) used to force a full revalidation + DB role re-map on every call. It now reuses the circuit's last revalidation for NonAuthoritativeRefreshInterval (10 s); GetAuthoritativeStateAsync (mutation gates, UI-13) still re-maps every time and fails closed. The revocation regression test now asserts through the authoritative read, which is what every gate uses.
92 lines
4.2 KiB
C#
92 lines
4.2 KiB
C#
using Microsoft.AspNetCore.Components.Authorization;
|
|
using Microsoft.AspNetCore.Http;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.Options;
|
|
using NSubstitute;
|
|
using ZB.MOM.WW.Auth.Abstractions.Roles;
|
|
using ZB.MOM.WW.ScadaBridge.CentralUI.Auth;
|
|
using ZB.MOM.WW.ScadaBridge.Security;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Auth;
|
|
|
|
/// <summary>
|
|
/// Review 2026-10-21 07/P-3: a non-authoritative <see cref="CookieAuthenticationStateProvider.GetAuthenticationStateAsync"/>
|
|
/// no longer re-runs the role mapping on every read — it reuses the circuit's last revalidation
|
|
/// for <see cref="CookieAuthenticationStateProvider.NonAuthoritativeRefreshInterval"/> — while
|
|
/// <see cref="CookieAuthenticationStateProvider.GetAuthoritativeStateAsync"/> (mutation gates)
|
|
/// still re-maps every time.
|
|
/// </summary>
|
|
public class CookieAuthenticationStateMemoTests
|
|
{
|
|
private sealed class MutableClock : TimeProvider
|
|
{
|
|
public DateTimeOffset Now { get; set; } = DateTimeOffset.UtcNow;
|
|
|
|
public override DateTimeOffset GetUtcNow() => Now;
|
|
}
|
|
|
|
private static (CookieAuthenticationStateProvider Auth, IGroupRoleMapper<string> Mapper, MutableClock Clock, ServiceProvider Provider) Create()
|
|
{
|
|
var clock = new MutableClock();
|
|
var mapping = new RoleMappingResult([Roles.Designer], [], false);
|
|
var mapper = Substitute.For<IGroupRoleMapper<string>>();
|
|
mapper.MapAsync(Arg.Any<IReadOnlyList<string>>(), Arg.Any<CancellationToken>())
|
|
.Returns(_ => new GroupRoleMapping<string>(mapping.Roles, Scope: mapping));
|
|
var initial = SessionClaimBuilder.Build("alice", "Alice", ["group"], mapping, clock.Now);
|
|
var services = new ServiceCollection().AddLogging();
|
|
services.AddSingleton(mapper);
|
|
services.AddSingleton<TimeProvider>(clock);
|
|
services.AddSingleton(Options.Create(new SecurityOptions()));
|
|
services.AddScoped<CookieSessionValidator>();
|
|
services.AddSingleton<IHttpContextAccessor>(new HttpContextAccessor { HttpContext = new DefaultHttpContext { User = initial } });
|
|
services.AddScoped<AuthenticationStateProvider, CookieAuthenticationStateProvider>();
|
|
var provider = services.BuildServiceProvider();
|
|
var auth = (CookieAuthenticationStateProvider)provider.CreateScope().ServiceProvider.GetRequiredService<AuthenticationStateProvider>();
|
|
return (auth, mapper, clock, provider);
|
|
}
|
|
|
|
/// <summary>Repeated display reads within the window run the role mapping once.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task NonAuthoritativeReads_WithinWindow_MapOnce()
|
|
{
|
|
var (auth, mapper, _, provider) = Create();
|
|
using var _p = provider;
|
|
|
|
for (var i = 0; i < 5; i++)
|
|
Assert.Equal("alice", (await auth.GetCurrentUsernameAsync()));
|
|
|
|
await mapper.Received(1).MapAsync(Arg.Any<IReadOnlyList<string>>(), Arg.Any<CancellationToken>());
|
|
}
|
|
|
|
/// <summary>After the window the next display read revalidates again.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task NonAuthoritativeRead_AfterWindow_Revalidates()
|
|
{
|
|
var (auth, mapper, clock, provider) = Create();
|
|
using var _p = provider;
|
|
|
|
await auth.GetAuthenticationStateAsync();
|
|
clock.Now += CookieAuthenticationStateProvider.NonAuthoritativeRefreshInterval + TimeSpan.FromSeconds(1);
|
|
await auth.GetAuthenticationStateAsync();
|
|
|
|
await mapper.Received(2).MapAsync(Arg.Any<IReadOnlyList<string>>(), Arg.Any<CancellationToken>());
|
|
}
|
|
|
|
/// <summary>Authoritative reads (mutation gates) always re-map, even inside the window.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task AuthoritativeReads_AlwaysRemap()
|
|
{
|
|
var (auth, mapper, _, provider) = Create();
|
|
using var _p = provider;
|
|
|
|
await auth.GetAuthenticationStateAsync();
|
|
await auth.GetAuthoritativeStateAsync();
|
|
await auth.GetAuthoritativeStateAsync();
|
|
|
|
await mapper.Received(3).MapAsync(Arg.Any<IReadOnlyList<string>>(), Arg.Any<CancellationToken>());
|
|
}
|
|
}
|