Files
lmxopcua/tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests/Audit/HttpAuditActorAccessorTests.cs
T
Joseph Doherty 075c0e69da
v2-ci / build (push) Failing after 40s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (push) Has been skipped
feat(audit): OtOpcUa IAuditActorAccessor seam + HTTP impl (audit Actor from Auth principal) (Phase 3)
Introduces the IAuditActorAccessor seam and HttpAuditActorAccessor impl so the
ZB.MOM.WW.Audit.AuditEvent Actor field can be sourced from the authenticated Blazor
cookie principal (ZbClaimTypes.Username) when structured emitters are added. Adds the
AuditActor.Resolve static helper (accessor value → SystemFallback/"system") as the
canonical pattern for future emit sites. Wires DI in AddOtOpcUaAuth (TryAddScoped) with
AddHttpContextAccessor(). The structured AuditEvent path remains DORMANT — no live emit
sites exist; seam is forward-looking. SP-based audit path left untouched. 9 new unit
tests all green; Security (54) and ControlPlane (45) test suites fully pass.
2026-06-02 15:25:49 -04:00

116 lines
4.4 KiB
C#

using System.Security.Claims;
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;
/// <summary>
/// Unit tests for <see cref="HttpAuditActorAccessor"/>.
/// <para>
/// Covers the three cases:
/// <list type="bullet">
/// <item>Authenticated principal with a <see cref="ZbClaimTypes.Username"/> claim →
/// returns the username claim value.</item>
/// <item>Authenticated principal with only a <see cref="ZbClaimTypes.Name"/> / no
/// username claim → falls back to the Name claim.</item>
/// <item>No HTTP context (null) or unauthenticated principal → returns
/// <see langword="null"/>.</item>
/// </list>
/// </para>
/// </summary>
public sealed class HttpAuditActorAccessorTests
{
// ── helpers ──────────────────────────────────────────────────────────────────
private static IHttpContextAccessor ContextWith(ClaimsPrincipal principal)
{
var context = new DefaultHttpContext { User = principal };
return new HttpContextAccessorStub(context);
}
private static IHttpContextAccessor NoContext() =>
new HttpContextAccessorStub(null);
private static ClaimsPrincipal AuthenticatedWith(params Claim[] claims)
{
var identity = new ClaimsIdentity(
claims,
authenticationType: "TestScheme", // non-null authenticationType → IsAuthenticated = true
nameType: ZbClaimTypes.Name,
roleType: ZbClaimTypes.Role);
return new ClaimsPrincipal(identity);
}
private static ClaimsPrincipal Unauthenticated() =>
new(new ClaimsIdentity()); // no authenticationType → IsAuthenticated = false
// ── tests ─────────────────────────────────────────────────────────────────────
/// <summary>
/// An authenticated principal that carries <see cref="ZbClaimTypes.Username"/>
/// returns exactly that claim value — it is the canonical actor string.
/// </summary>
[Fact]
public void Returns_username_claim_for_authenticated_principal()
{
var principal = AuthenticatedWith(
new Claim(ZbClaimTypes.Username, "alice"),
new Claim(ZbClaimTypes.Name, "alice-name"),
new Claim(ZbClaimTypes.DisplayName, "Alice User"));
var sut = new HttpAuditActorAccessor(ContextWith(principal));
sut.CurrentActor.ShouldBe("alice");
}
/// <summary>
/// When the principal has no <see cref="ZbClaimTypes.Username"/> claim but does have
/// a <see cref="ZbClaimTypes.Name"/> claim, the Name claim value is returned as the
/// fallback actor.
/// </summary>
[Fact]
public void Falls_back_to_Name_claim_when_Username_claim_is_absent()
{
var principal = AuthenticatedWith(
new Claim(ZbClaimTypes.Name, "bob"));
var sut = new HttpAuditActorAccessor(ContextWith(principal));
sut.CurrentActor.ShouldBe("bob");
}
/// <summary>
/// An unauthenticated principal (Identity.IsAuthenticated == false) returns null —
/// the caller's fallback (typically <see cref="AuditActor.SystemFallback"/>) is used.
/// </summary>
[Fact]
public void Returns_null_for_unauthenticated_principal()
{
var sut = new HttpAuditActorAccessor(ContextWith(Unauthenticated()));
sut.CurrentActor.ShouldBeNull();
}
/// <summary>
/// When there is no current <c>HttpContext</c> (e.g. background task, actor mailbox
/// worker), returns null.
/// </summary>
[Fact]
public void Returns_null_when_no_HttpContext()
{
var sut = new HttpAuditActorAccessor(NoContext());
sut.CurrentActor.ShouldBeNull();
}
// ── stub ──────────────────────────────────────────────────────────────────────
private sealed class HttpContextAccessorStub(HttpContext? context) : IHttpContextAccessor
{
public HttpContext? HttpContext { get; set; } = context;
}
}