using Opc.Ua; using Opc.Ua.Client; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Commons.OpcUa; namespace ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests; /// Preserves baseline alarm discovery through hierarchical folder browses in both application realms. public sealed class Sdk2AlarmBrowseCompatibilityTests { [Theory] [InlineData("OffNormalAlarm", AddressSpaceRealm.Raw)] [InlineData("DiscreteAlarm", AddressSpaceRealm.Raw)] [InlineData("AlarmCondition", AddressSpaceRealm.Raw)] [InlineData("OffNormalAlarm", AddressSpaceRealm.Uns)] [InlineData("DiscreteAlarm", AddressSpaceRealm.Uns)] [InlineData("AlarmCondition", AddressSpaceRealm.Uns)] public async Task Alarm_is_a_hierarchical_component_with_the_same_inverse_parent_and_object_type( string type, AddressSpaceRealm realm) { var ct = TestContext.Current.CancellationToken; await using var rig = await ServerEventRig.StartAsync("AlarmBrowseCompatibility", false, ct); rig.Enforce(false, [ServerEventRig.Key()]); rig.Sink.EnsureFolder("browse-parent", null, "Parent", realm); rig.Sink.MaterialiseAlarmCondition("browse-alarm", "browse-parent", "Alarm", type, 500, realm, isNative: realm == AddressSpaceRealm.Raw); using var session = await rig.ConnectAsync(true, ct); var uri = realm == AddressSpaceRealm.Raw ? V3NodeIds.RawNamespaceUri : V3NodeIds.UnsNamespaceUri; var ns = session.NamespaceUris.GetIndex(uri); ns.ShouldBeGreaterThan(0); var parent = new NodeId("browse-parent", (ushort)ns); var alarm = new NodeId("browse-alarm", (ushort)ns); var expectedType = type switch { "OffNormalAlarm" => ObjectTypeIds.OffNormalAlarmType, "DiscreteAlarm" => ObjectTypeIds.DiscreteAlarmType, _ => ObjectTypeIds.AlarmConditionType, }; var children = await BrowseAsync(session, parent, ReferenceTypeIds.HierarchicalReferences, BrowseDirection.Forward, true, ct); var discovered = children.SingleOrDefault(reference => ExpandedNodeId.ToNodeId(reference.NodeId, session.NamespaceUris) == alarm); discovered.ShouldNotBeNull("the baseline folder browse exposes every materialised alarm object"); discovered.ReferenceTypeId.ShouldBe(ReferenceTypeIds.HasComponent); discovered.IsForward.ShouldBeTrue(); discovered.NodeClass.ShouldBe(NodeClass.Object); ExpandedNodeId.ToNodeId(discovered.TypeDefinition, session.NamespaceUris).ShouldBe(expectedType); var components = await BrowseAsync(session, parent, ReferenceTypeIds.HasComponent, BrowseDirection.Forward, false, ct); components.ShouldContain(reference => ExpandedNodeId.ToNodeId(reference.NodeId, session.NamespaceUris) == alarm); var inverse = await BrowseAsync(session, alarm, ReferenceTypeIds.HasComponent, BrowseDirection.Inverse, false, ct); inverse.ShouldContain(reference => !reference.IsForward && ExpandedNodeId.ToNodeId(reference.NodeId, session.NamespaceUris) == parent); var types = await BrowseAsync(session, alarm, ReferenceTypeIds.HasTypeDefinition, BrowseDirection.Forward, false, ct); types.ShouldContain(reference => ExpandedNodeId.ToNodeId(reference.NodeId, session.NamespaceUris) == expectedType); await session.CloseAsync(ct); } private static async Task> BrowseAsync(ISession session, NodeId node, NodeId reference, BrowseDirection direction, bool subtypes, CancellationToken ct) { var result = (await session.BrowseAsync(null, null, 0, [new BrowseDescription { NodeId = node, ReferenceTypeId = reference, BrowseDirection = direction, IncludeSubtypes = subtypes, ResultMask = (uint)BrowseResultMask.All, }], ct)).Results[0]; result.StatusCode.ShouldBe(StatusCodes.Good); result.ContinuationPoint.IsEmpty.ShouldBeTrue(); return result.References.ToList(); } }