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;
///
/// Unit tests for .
///
/// Covers the three cases:
///
/// - Authenticated principal with a claim →
/// returns the username claim value.
/// - Authenticated principal with only a / no
/// username claim → falls back to the Name claim.
/// - No HTTP context (null) or unauthenticated principal → returns
/// .
///
///
///
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 ─────────────────────────────────────────────────────────────────────
///
/// An authenticated principal that carries
/// returns exactly that claim value — it is the canonical actor string.
///
[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");
}
///
/// When the principal has no claim but does have
/// a claim, the Name claim value is returned as the
/// fallback actor.
///
[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");
}
///
/// An unauthenticated principal (Identity.IsAuthenticated == false) returns null —
/// the caller's fallback (typically ) is used.
///
[Fact]
public void Returns_null_for_unauthenticated_principal()
{
var sut = new HttpAuditActorAccessor(ContextWith(Unauthenticated()));
sut.CurrentActor.ShouldBeNull();
}
///
/// When there is no current HttpContext (e.g. background task, actor mailbox
/// worker), returns null.
///
[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;
}
}