using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Core.Authorization; using ZB.MOM.WW.OtOpcUa.Server.Security; namespace ZB.MOM.WW.OtOpcUa.Server.Tests; [Trait("Category", "Unit")] public sealed class NodeScopeResolverTests { [Fact] public void Resolve_PopulatesClusterAndTag() { var resolver = new NodeScopeResolver("c-warsaw"); var scope = resolver.Resolve("TestMachine_001/Oven/SetPoint"); scope.ClusterId.ShouldBe("c-warsaw"); scope.TagId.ShouldBe("TestMachine_001/Oven/SetPoint"); scope.Kind.ShouldBe(NodeHierarchyKind.Equipment); } [Fact] public void Resolve_Leaves_UnsPath_Null_When_NoIndexSupplied() { var resolver = new NodeScopeResolver("c-1"); var scope = resolver.Resolve("tag-1"); // Cluster-only fallback path — used pre-ADR-001 and still the active path for // unindexed references (e.g. driver-discovered tags that have no Tag row yet). scope.NamespaceId.ShouldBeNull(); scope.UnsAreaId.ShouldBeNull(); scope.UnsLineId.ShouldBeNull(); scope.EquipmentId.ShouldBeNull(); } [Fact] public void Resolve_Returns_IndexedScope_When_FullReferenceFound() { var index = new Dictionary { ["plcaddr-01"] = new NodeScope { ClusterId = "c-1", NamespaceId = "ns-plc", UnsAreaId = "area-1", UnsLineId = "line-a", EquipmentId = "eq-oven-3", TagId = "plcaddr-01", Kind = NodeHierarchyKind.Equipment, }, }; var resolver = new NodeScopeResolver("c-1", index); var scope = resolver.Resolve("plcaddr-01"); scope.UnsAreaId.ShouldBe("area-1"); scope.UnsLineId.ShouldBe("line-a"); scope.EquipmentId.ShouldBe("eq-oven-3"); scope.TagId.ShouldBe("plcaddr-01"); scope.NamespaceId.ShouldBe("ns-plc"); } [Fact] public void Resolve_FallsBack_To_ClusterOnly_When_Reference_NotIndexed() { var index = new Dictionary { ["plcaddr-01"] = new NodeScope { ClusterId = "c-1", TagId = "plcaddr-01", Kind = NodeHierarchyKind.Equipment }, }; var resolver = new NodeScopeResolver("c-1", index); var scope = resolver.Resolve("not-in-index"); scope.ClusterId.ShouldBe("c-1"); scope.TagId.ShouldBe("not-in-index"); scope.EquipmentId.ShouldBeNull(); } [Fact] public void Resolve_Throws_OnEmptyFullReference() { var resolver = new NodeScopeResolver("c-1"); Should.Throw(() => resolver.Resolve("")); Should.Throw(() => resolver.Resolve(" ")); } [Fact] public void Ctor_Throws_OnEmptyClusterId() { Should.Throw(() => new NodeScopeResolver("")); } [Fact] public void Resolver_IsStateless_AcrossCalls() { var resolver = new NodeScopeResolver("c"); var s1 = resolver.Resolve("tag-a"); var s2 = resolver.Resolve("tag-b"); s1.TagId.ShouldBe("tag-a"); s2.TagId.ShouldBe("tag-b"); s1.ClusterId.ShouldBe("c"); s2.ClusterId.ShouldBe("c"); } }