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); }