using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Configuration.Entities; using ZB.MOM.WW.OtOpcUa.Configuration.Enums; using ZB.MOM.WW.OtOpcUa.Core.Authorization; namespace ZB.MOM.WW.OtOpcUa.Core.Tests.Authorization; [Trait("Category", "Unit")] public sealed class PermissionTrieTests { private static NodeAcl Row(string group, NodeAclScopeKind scope, string? scopeId, NodePermissions flags, string clusterId = "c1") => new() { NodeAclRowId = Guid.NewGuid(), NodeAclId = $"acl-{Guid.NewGuid():N}", GenerationId = 1, ClusterId = clusterId, LdapGroup = group, ScopeKind = scope, ScopeId = scopeId, PermissionFlags = flags, }; private static NodeScope EquipmentTag(string cluster, string ns, string area, string line, string equip, string tag) => new() { ClusterId = cluster, NamespaceId = ns, UnsAreaId = area, UnsLineId = line, EquipmentId = equip, TagId = tag, Kind = NodeHierarchyKind.Equipment, }; private static NodeScope GalaxyTag(string cluster, string ns, string[] folders, string tag) => new() { ClusterId = cluster, NamespaceId = ns, FolderSegments = folders, TagId = tag, Kind = NodeHierarchyKind.SystemPlatform, }; [Fact] public void ClusterLevelGrant_Cascades_ToEveryTag() { var rows = new[] { Row("cn=ops", NodeAclScopeKind.Cluster, scopeId: null, NodePermissions.Read) }; var trie = PermissionTrieBuilder.Build("c1", 1, rows); var matches = trie.CollectMatches( EquipmentTag("c1", "ns", "area1", "line1", "eq1", "tag1"), ["cn=ops"]); matches.Count.ShouldBe(1); matches[0].PermissionFlags.ShouldBe(NodePermissions.Read); matches[0].Scope.ShouldBe(NodeAclScopeKind.Cluster); } [Fact] public void EquipmentScope_DoesNotLeak_ToSibling() { var paths = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["eq-A"] = new(new[] { "ns", "area1", "line1", "eq-A" }), }; var rows = new[] { Row("cn=ops", NodeAclScopeKind.Equipment, "eq-A", NodePermissions.Read) }; var trie = PermissionTrieBuilder.Build("c1", 1, rows, paths); var matchA = trie.CollectMatches(EquipmentTag("c1", "ns", "area1", "line1", "eq-A", "tag1"), ["cn=ops"]); var matchB = trie.CollectMatches(EquipmentTag("c1", "ns", "area1", "line1", "eq-B", "tag1"), ["cn=ops"]); matchA.Count.ShouldBe(1); matchB.ShouldBeEmpty("grant at eq-A must not apply to sibling eq-B"); } [Fact] public void MultiGroup_Union_OrsPermissionFlags() { var rows = new[] { Row("cn=readers", NodeAclScopeKind.Cluster, null, NodePermissions.Read), Row("cn=writers", NodeAclScopeKind.Cluster, null, NodePermissions.WriteOperate), }; var trie = PermissionTrieBuilder.Build("c1", 1, rows); var matches = trie.CollectMatches( EquipmentTag("c1", "ns", "area1", "line1", "eq1", "tag1"), ["cn=readers", "cn=writers"]); matches.Count.ShouldBe(2); var combined = matches.Aggregate(NodePermissions.None, (acc, m) => acc | m.PermissionFlags); combined.ShouldBe(NodePermissions.Read | NodePermissions.WriteOperate); } [Fact] public void NoMatchingGroup_ReturnsEmpty() { var rows = new[] { Row("cn=different", NodeAclScopeKind.Cluster, null, NodePermissions.Read) }; var trie = PermissionTrieBuilder.Build("c1", 1, rows); var matches = trie.CollectMatches( EquipmentTag("c1", "ns", "area1", "line1", "eq1", "tag1"), ["cn=ops"]); matches.ShouldBeEmpty(); } [Fact] public void Galaxy_FolderSegment_Grant_DoesNotLeak_To_Sibling_Folder() { var paths = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["folder-A"] = new(new[] { "ns-gal", "folder-A" }), }; var rows = new[] { Row("cn=ops", NodeAclScopeKind.Equipment, "folder-A", NodePermissions.Read) }; var trie = PermissionTrieBuilder.Build("c1", 1, rows, paths); var matchA = trie.CollectMatches(GalaxyTag("c1", "ns-gal", ["folder-A"], "tag1"), ["cn=ops"]); var matchB = trie.CollectMatches(GalaxyTag("c1", "ns-gal", ["folder-B"], "tag1"), ["cn=ops"]); matchA.Count.ShouldBe(1); matchB.ShouldBeEmpty(); } [Fact] public void CrossCluster_Grant_DoesNotLeak() { var rows = new[] { Row("cn=ops", NodeAclScopeKind.Cluster, null, NodePermissions.Read, clusterId: "c-other") }; var trie = PermissionTrieBuilder.Build("c1", 1, rows); var matches = trie.CollectMatches( EquipmentTag("c1", "ns", "area1", "line1", "eq1", "tag1"), ["cn=ops"]); matches.ShouldBeEmpty("rows for cluster c-other must not land in c1's trie"); } [Fact] public void Build_IsIdempotent() { var rows = new[] { Row("cn=a", NodeAclScopeKind.Cluster, null, NodePermissions.Read), Row("cn=b", NodeAclScopeKind.Cluster, null, NodePermissions.WriteOperate), }; var trie1 = PermissionTrieBuilder.Build("c1", 1, rows); var trie2 = PermissionTrieBuilder.Build("c1", 1, rows); trie1.Root.Grants.Count.ShouldBe(trie2.Root.Grants.Count); trie1.ClusterId.ShouldBe(trie2.ClusterId); trie1.GenerationId.ShouldBe(trie2.GenerationId); } }