using Opc.Ua;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.OpcUaServer.Security;
namespace ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests;
///
/// Task 11 — the inbound operator-write authz gate + fire-and-forget dispatch. The OnWriteValue handler
/// on a writable equipment-tag node extracts the caller's , gates
/// on the role (deny otherwise), and on pass kicks off the
/// fire-and-forget route through the and returns
/// Good (optimistic write). The pure decision is extracted into
/// so the gate + dispatch are unit-testable
/// without booting an SDK server: the handler just supplies the extracted identity and a thunk that
/// starts the router (or null when no router is wired).
///
public sealed class EquipmentWriteGateTests
{
/// (a) A null identity (anonymous / no role-carrying identity on the context) is denied with
/// BadUserAccessDenied and the route thunk is NEVER invoked — the gate fails closed.
[Fact]
public void Null_identity_is_denied_and_does_not_route()
{
var routed = false;
var result = OtOpcUaNodeManager.EvaluateEquipmentWrite(
identity: null,
route: () => routed = true);
result.StatusCode.Code.ShouldBe(StatusCodes.BadUserAccessDenied);
routed.ShouldBeFalse();
}
/// (b) An identity WITHOUT the WriteOperate role is denied with
/// BadUserAccessDenied and the route thunk is NEVER invoked.
[Fact]
public void Identity_without_WriteOperate_is_denied_and_does_not_route()
{
var routed = false;
var identity = IdentityWith("ReadOnly", OpcUaDataPlaneRoles.AlarmAck); // no WriteOperate
var result = OtOpcUaNodeManager.EvaluateEquipmentWrite(
identity,
route: () => routed = true);
result.StatusCode.Code.ShouldBe(StatusCodes.BadUserAccessDenied);
routed.ShouldBeFalse();
}
/// (c) An identity WITH the WriteOperate role and a non-null route invokes the route
/// thunk (fire-and-forget) and returns ServiceResult.Good so the SDK applies the value
/// optimistically. The role match is case-insensitive (the role set + gate both use
/// OrdinalIgnoreCase).
[Fact]
public void Identity_with_WriteOperate_routes_and_returns_good()
{
var routed = false;
var identity = IdentityWith("readonly", "writeoperate"); // lower-cased: case-insensitive match
var result = OtOpcUaNodeManager.EvaluateEquipmentWrite(
identity,
route: () => routed = true);
routed.ShouldBeTrue();
result.ShouldBe(ServiceResult.Good);
}
/// (d) An identity WITH the WriteOperate role but a null route (no router wired — e.g.
/// admin-only nodes) maps to BadNotWritable ("writes unavailable") — the gate passes but there is
/// nowhere to route the write.
[Fact]
public void Identity_with_WriteOperate_and_null_route_maps_to_bad_not_writable()
{
var identity = IdentityWith(OpcUaDataPlaneRoles.WriteOperate);
var result = OtOpcUaNodeManager.EvaluateEquipmentWrite(
identity,
route: null);
result.StatusCode.Code.ShouldBe(StatusCodes.BadNotWritable);
result.LocalizedText.Text.ShouldContain("writes unavailable");
}
private static RoleCarryingUserIdentity IdentityWith(params string[] roles) =>
new(new UserNameIdentityToken { UserName = "op" }, roles);
}