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; /// /// Review 2026-10-21 07/P-3: a non-authoritative /// no longer re-runs the role mapping on every read — it reuses the circuit's last revalidation /// for — while /// (mutation gates) /// still re-maps every time. /// 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 Mapper, MutableClock Clock, ServiceProvider Provider) Create() { var clock = new MutableClock(); var mapping = new RoleMappingResult([Roles.Designer], [], false); var mapper = Substitute.For>(); mapper.MapAsync(Arg.Any>(), Arg.Any()) .Returns(_ => new GroupRoleMapping(mapping.Roles, Scope: mapping)); var initial = SessionClaimBuilder.Build("alice", "Alice", ["group"], mapping, clock.Now); var services = new ServiceCollection().AddLogging(); services.AddSingleton(mapper); services.AddSingleton(clock); services.AddSingleton(Options.Create(new SecurityOptions())); services.AddScoped(); services.AddSingleton(new HttpContextAccessor { HttpContext = new DefaultHttpContext { User = initial } }); services.AddScoped(); var provider = services.BuildServiceProvider(); var auth = (CookieAuthenticationStateProvider)provider.CreateScope().ServiceProvider.GetRequiredService(); return (auth, mapper, clock, provider); } /// Repeated display reads within the window run the role mapping once. /// A task that represents the asynchronous operation. [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>(), Arg.Any()); } /// After the window the next display read revalidates again. /// A task that represents the asynchronous operation. [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>(), Arg.Any()); } /// Authoritative reads (mutation gates) always re-map, even inside the window. /// A task that represents the asynchronous operation. [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>(), Arg.Any()); } }