fix(central-ui): pending-updates indicator resolves roles/scope once per auth state, not per poll
SiteScopeService's instance methods resolve the AUTHORITATIVE state — a fail-closed, DB-backed group-to-role re-mapping. The indicator called them on every 30s tick, and it lives in MainLayout, so that was one or two forced session revalidations per open circuit per page, forever: exactly the per-circuit cost IPendingUpdatesCache exists to avoid. Extracts the pure rule as SiteScopeService.Resolve(ClaimsPrincipal) (no I/O; ResolveAsync now delegates to it, no behaviour change for existing callers). The indicator takes the cascaded AuthenticationState — falling back to a NON-forced GetAuthenticationStateAsync, as Templates/FlowDebugger do — and computes roles plus scope once per auth state, re-running on AuthenticationStateChanged so a mid-session revocation hides the block instead of leaving it stale for the life of the circuit. Also routes the first load through PollGate, adds aria-hidden to the icons, and comments the off-renderer field writes and the InvalidOperationException catch. Claude-Session: https://claude.ai/code/session_01Y5ef34iUUMjmwnMpL9xgmB
This commit is contained in:
@@ -161,4 +161,67 @@ public class SiteScopeServiceTests
|
||||
Assert.False(await svc.IsSystemWideAsync());
|
||||
Assert.False(await svc.IsSiteAllowedAsync(2));
|
||||
}
|
||||
|
||||
// ── Resolve(ClaimsPrincipal): the same rule as a pure function ───────────
|
||||
// Display-gating callers (the rail pending-updates indicator) hold a principal
|
||||
// already and must NOT pay for the instance methods' authoritative, fail-closed
|
||||
// state refresh — which for a polling component would be a forced session
|
||||
// revalidation per tick per circuit. These pin that the pure overload answers
|
||||
// identically to the async path.
|
||||
|
||||
private static ClaimsPrincipal Principal(params Claim[] claims)
|
||||
=> new(new ClaimsIdentity(claims, authenticationType: "TestCookie"));
|
||||
|
||||
[Fact]
|
||||
public void Resolve_Administrator_IsSystemWideWithAnEmptySet()
|
||||
{
|
||||
// Even carrying SiteId claims: an Administrator's ids are not collected at
|
||||
// all, so "system-wide => empty set" holds for every caller.
|
||||
var (isSystemWide, sites) = SiteScopeService.Resolve(
|
||||
Principal(Role("Administrator"), SiteClaim(1), SiteClaim(2)));
|
||||
|
||||
Assert.True(isSystemWide);
|
||||
Assert.Empty(sites);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Resolve_SiteClaims_YieldTheScopedSet()
|
||||
{
|
||||
var (isSystemWide, sites) = SiteScopeService.Resolve(
|
||||
Principal(Role("Deployer"), SiteClaim(1), SiteClaim(3)));
|
||||
|
||||
Assert.False(isSystemWide);
|
||||
Assert.Equal([1, 3], sites.OrderBy(id => id));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Resolve_NoSiteClaims_IsSystemWide()
|
||||
{
|
||||
var (isSystemWide, sites) = SiteScopeService.Resolve(Principal(Role("Deployer")));
|
||||
|
||||
Assert.True(isSystemWide);
|
||||
Assert.Empty(sites);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Resolve_UnauthenticatedPrincipal_IsScopedToNothing()
|
||||
{
|
||||
// Fail closed: not system-wide AND no permitted sites.
|
||||
var (isSystemWide, sites) = SiteScopeService.Resolve(new ClaimsPrincipal(new ClaimsIdentity()));
|
||||
|
||||
Assert.False(isSystemWide);
|
||||
Assert.Empty(sites);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Resolve_AgreesWithTheAsyncPath()
|
||||
{
|
||||
var claims = new[] { Role("Deployer"), SiteClaim(2), SiteClaim(5) };
|
||||
var svc = ForUser(claims);
|
||||
|
||||
var (isSystemWide, sites) = SiteScopeService.Resolve(Principal(claims));
|
||||
|
||||
Assert.Equal(await svc.IsSystemWideAsync(), isSystemWide);
|
||||
Assert.Equal((await svc.PermittedSiteIdsAsync()).OrderBy(id => id), sites.OrderBy(id => id));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user