using System.Security.Claims; using Microsoft.AspNetCore.Components.Authorization; using Microsoft.AspNetCore.Http; using Shouldly; using Xunit; using ZB.MOM.WW.Auth.AspNetCore; using ZB.MOM.WW.OtOpcUa.Security.Audit; namespace ZB.MOM.WW.OtOpcUa.Security.Tests.Audit; /// /// R03:2.5 — inside a Blazor circuit there is no HttpContext after the first render, so an /// HTTP-only accessor stamps every audit row from an interactive page "system". The circuit's /// is consulted first; the HTTP context is the fallback. /// public sealed class AuthenticationStateAuditActorAccessorTests { [Fact] public void Circuit_principal_wins_when_there_is_no_http_context() { var accessor = new AuthenticationStateAuditActorAccessor( new HttpContextAccessorStub(null), new FixedStateProvider(Authenticated(new Claim(ZbClaimTypes.Username, "alice")))); accessor.CurrentActor.ShouldBe("alice"); } [Fact] public void Http_principal_is_the_fallback_outside_a_circuit() { var context = new DefaultHttpContext { User = Authenticated(new Claim(ZbClaimTypes.Username, "bob")) }; var accessor = new AuthenticationStateAuditActorAccessor(new HttpContextAccessorStub(context), authenticationState: null); accessor.CurrentActor.ShouldBe("bob"); } [Fact] public void Unauthenticated_circuit_falls_through_to_http_then_null() { var anonymousCircuit = new FixedStateProvider(new ClaimsPrincipal(new ClaimsIdentity())); new AuthenticationStateAuditActorAccessor(new HttpContextAccessorStub(null), anonymousCircuit) .CurrentActor.ShouldBeNull(); var context = new DefaultHttpContext { User = Authenticated(new Claim(ZbClaimTypes.Name, "carol")) }; new AuthenticationStateAuditActorAccessor(new HttpContextAccessorStub(context), anonymousCircuit) .CurrentActor.ShouldBe("carol"); } /// An incomplete state task is "no circuit identity", never a block. [Fact] public void Pending_authentication_state_is_not_awaited() { var accessor = new AuthenticationStateAuditActorAccessor( new HttpContextAccessorStub(null), new PendingStateProvider()); accessor.CurrentActor.ShouldBeNull(); } /// /// Fix round 1 / I5 — the REAL server-side provider, resolved in a non-circuit scope (a minimal-API /// request, a background service), throws from /// GetAuthenticationStateAsync because nothing called SetAuthenticationState. That used /// to escape CurrentActor; it must fall back to the HTTP principal instead. /// [Fact] public void Real_server_provider_outside_a_circuit_falls_back_to_the_http_principal() { var context = new DefaultHttpContext { User = Authenticated(new Claim(ZbClaimTypes.Username, "dave")) }; var accessor = new AuthenticationStateAuditActorAccessor( new HttpContextAccessorStub(context), new Microsoft.AspNetCore.Components.Server.ServerAuthenticationStateProvider()); accessor.CurrentActor.ShouldBe("dave"); // No HTTP principal either ⇒ null, still no throw. new AuthenticationStateAuditActorAccessor( new HttpContextAccessorStub(null), new Microsoft.AspNetCore.Components.Server.ServerAuthenticationStateProvider()) .CurrentActor.ShouldBeNull(); } /// Positive control on the real provider: once a circuit has set its state, that principal wins. [Fact] public void Real_server_provider_inside_a_circuit_serves_the_circuit_principal() { var provider = new Microsoft.AspNetCore.Components.Server.ServerAuthenticationStateProvider(); provider.SetAuthenticationState(Task.FromResult( new AuthenticationState(Authenticated(new Claim(ZbClaimTypes.Username, "erin"))))); var context = new DefaultHttpContext { User = Authenticated(new Claim(ZbClaimTypes.Username, "http-user")) }; new AuthenticationStateAuditActorAccessor(new HttpContextAccessorStub(context), provider) .CurrentActor.ShouldBe("erin"); } private static ClaimsPrincipal Authenticated(params Claim[] claims) => new(new ClaimsIdentity(claims, "TestScheme", ZbClaimTypes.Name, ZbClaimTypes.Role)); private sealed class HttpContextAccessorStub(HttpContext? context) : IHttpContextAccessor { public HttpContext? HttpContext { get; set; } = context; } private sealed class FixedStateProvider(ClaimsPrincipal user) : AuthenticationStateProvider { public override Task GetAuthenticationStateAsync() => Task.FromResult(new AuthenticationState(user)); } private sealed class PendingStateProvider : AuthenticationStateProvider { public override Task GetAuthenticationStateAsync() => new TaskCompletionSource().Task; } }