using Opc.Ua; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Configuration.Entities; using ZB.MOM.WW.OtOpcUa.Configuration.Enums; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; using ZB.MOM.WW.OtOpcUa.Core.Authorization; using ZB.MOM.WW.OtOpcUa.Server.OpcUa; using ZB.MOM.WW.OtOpcUa.Server.Security; namespace ZB.MOM.WW.OtOpcUa.Server.Tests; /// /// Unit tests for — /// Phase 6.2 Stream C per-item subscription gating. Pre-populates the errors array /// with for denied items; base stack /// honours the pre-set error and skips the item. /// [Trait("Category", "Unit")] public sealed class MonitoredItemGatingTests { [Fact] public void Gate_null_leaves_errors_untouched() { var items = new List { NewRequest("c1/area/line/eq/tag1") }; var errors = new List { (ServiceResult)null! }; DriverNodeManager.GateMonitoredItemCreateRequests(items, errors, new UserIdentity(), gate: null, scopeResolver: null); errors[0].ShouldBeNull(); } [Fact] public void Denied_item_gets_BadUserAccessDenied() { var items = new List { NewRequest("c1/area/line/eq/tag1") }; var errors = new List { (ServiceResult)null! }; var gate = MakeGate(strict: true, rows: []); // no grants → deny DriverNodeManager.GateMonitoredItemCreateRequests(items, errors, NewIdentity("alice"), gate, new NodeScopeResolver("c1")); ServiceResult.IsBad(errors[0]).ShouldBeTrue(); errors[0].StatusCode.ShouldBe((StatusCode)StatusCodes.BadUserAccessDenied); } [Fact] public void Allowed_item_is_not_touched() { var items = new List { NewRequest("c1/area/line/eq/tag1") }; var errors = new List { (ServiceResult)null! }; var gate = MakeGate(strict: true, rows: [Row("grp-ops", NodePermissions.Subscribe)]); DriverNodeManager.GateMonitoredItemCreateRequests(items, errors, NewIdentity("alice", "grp-ops"), gate, new NodeScopeResolver("c1")); errors[0].ShouldBeNull(); } [Fact] public void Mixed_batch_denies_per_item() { var items = new List { NewRequest("c1/area/line/eq/tagA"), NewRequest("c1/area/line/eq/tagB"), }; var errors = new List { (ServiceResult)null!, (ServiceResult)null! }; // Grant Browse not CreateMonitoredItems → still denied for this op var gate = MakeGate(strict: true, rows: [Row("grp-ops", NodePermissions.Browse)]); DriverNodeManager.GateMonitoredItemCreateRequests(items, errors, NewIdentity("alice", "grp-ops"), gate, new NodeScopeResolver("c1")); ServiceResult.IsBad(errors[0]).ShouldBeTrue(); ServiceResult.IsBad(errors[1]).ShouldBeTrue(); } [Fact] public void Pre_populated_error_is_preserved() { // Base stack may have already flagged an item (e.g. BadNodeIdUnknown). The gate // must not overwrite that with a generic BadUserAccessDenied — the first diagnosis // wins. var items = new List { NewRequest("c1/area/line/eq/tag1") }; var errors = new List { new(StatusCodes.BadNodeIdUnknown) }; var gate = MakeGate(strict: true, rows: []); DriverNodeManager.GateMonitoredItemCreateRequests(items, errors, NewIdentity("alice"), gate, new NodeScopeResolver("c1")); errors[0].StatusCode.ShouldBe((StatusCode)StatusCodes.BadNodeIdUnknown); } [Fact] public void Non_string_identifier_bypasses_the_gate() { // Numeric-id references (standard-type nodes) aren't keyed into the authz trie. var items = new List { new() { ItemToMonitor = new ReadValueId { NodeId = new NodeId(62u) } }, }; var errors = new List { (ServiceResult)null! }; var gate = MakeGate(strict: true, rows: []); DriverNodeManager.GateMonitoredItemCreateRequests(items, errors, NewIdentity("alice"), gate, new NodeScopeResolver("c1")); errors[0].ShouldBeNull("numeric-id references bypass the gate"); } // ---- helpers ----------------------------------------------------------- private static MonitoredItemCreateRequest NewRequest(string fullRef) => new() { ItemToMonitor = new ReadValueId { NodeId = new NodeId(fullRef, 2) }, }; private static NodeAcl Row(string group, NodePermissions flags) => new() { NodeAclRowId = Guid.NewGuid(), NodeAclId = Guid.NewGuid().ToString(), GenerationId = 1, ClusterId = "c1", LdapGroup = group, ScopeKind = NodeAclScopeKind.Cluster, ScopeId = null, PermissionFlags = flags, }; private static AuthorizationGate MakeGate(bool strict, NodeAcl[] rows) { var cache = new PermissionTrieCache(); cache.Install(PermissionTrieBuilder.Build("c1", 1, rows)); var evaluator = new TriePermissionEvaluator(cache); return new AuthorizationGate(evaluator, strictMode: strict); } private static IUserIdentity NewIdentity(string name, params string[] groups) => new FakeIdentity(name, groups); private sealed class FakeIdentity : UserIdentity, ILdapGroupsBearer { public FakeIdentity(string name, IReadOnlyList groups) { DisplayName = name; LdapGroups = groups; } public new string DisplayName { get; } public IReadOnlyList LdapGroups { get; } } }