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
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.
116 lines
4.4 KiB
C#
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;
|
|
}
|
|
}
|