using System.Security.Claims; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using ZB.MOM.WW.Auth.Abstractions.Roles; namespace ZB.MOM.WW.ScadaBridge.Security.Tests; public class PassiveSessionRegressionTests { private static readonly DateTimeOffset Start = new(2026, 9, 9, 12, 0, 0, TimeSpan.Zero); [Fact] public async Task PassivePing_RefreshesRolesWithoutAdvancingActivity_ThenExpires() { var clock = new Clock { Now = Start.AddMinutes(16) }; IGroupRoleMapper mapper = new Mapper(); var services = new ServiceCollection().AddSingleton(clock).AddLogging(); services.AddSingleton(mapper); services.AddSingleton(new Auth()); services.AddSingleton(Options.Create(new SecurityOptions())); services.AddScoped(); using var provider = services.BuildServiceProvider(); var principal = SessionClaimBuilder.Build("alice", "Alice", ["group"], new RoleMappingResult([Roles.Deployer], ["1"], false), Start); var ctx = Context(provider, principal, "/auth/ping"); await ServiceCollectionExtensions.OnValidatePrincipalAsync(ctx); Assert.Equal(Start.ToString("o"), ctx.Principal!.FindFirst(JwtTokenService.LastActivityClaimType)!.Value); Assert.Contains(Roles.Viewer, ctx.Principal.FindAll(JwtTokenService.RoleClaimType).Select(c => c.Value)); clock.Now = Start.AddMinutes(31); var expired = Context(provider, ctx.Principal, "/auth/ping"); await ServiceCollectionExtensions.OnValidatePrincipalAsync(expired); Assert.Null(expired.Principal); } private static CookieValidatePrincipalContext Context(IServiceProvider sp, ClaimsPrincipal principal, string path) { var http = new DefaultHttpContext { RequestServices = sp }; http.Request.Path = path; return new CookieValidatePrincipalContext(http, new AuthenticationScheme("Cookies", null, typeof(CookieAuthenticationHandler)), new CookieAuthenticationOptions(), new AuthenticationTicket(principal, new AuthenticationProperties(), "Cookies")); } private sealed class Mapper : IGroupRoleMapper { public Task> MapAsync(IReadOnlyList groups, CancellationToken ct) => Task.FromResult(new GroupRoleMapping([Roles.Viewer], new RoleMappingResult([Roles.Viewer], ["2"], false))); } private sealed class Auth : IAuthenticationService { public Task AuthenticateAsync(HttpContext c, string? s) => Task.FromResult(AuthenticateResult.NoResult()); public Task ChallengeAsync(HttpContext c, string? s, AuthenticationProperties? p) => Task.CompletedTask; public Task ForbidAsync(HttpContext c, string? s, AuthenticationProperties? p) => Task.CompletedTask; public Task SignInAsync(HttpContext c, string? s, ClaimsPrincipal u, AuthenticationProperties? p) => Task.CompletedTask; public Task SignOutAsync(HttpContext c, string? s, AuthenticationProperties? p) => Task.CompletedTask; } private sealed class Clock : TimeProvider { public DateTimeOffset Now { get; set; } public override DateTimeOffset GetUtcNow() => Now; } }