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());
}
}