/* ======================================================================== * Copyright (c) 2005-2025 The OPC Foundation, Inc. All rights reserved. * * OPC Foundation MIT License 1.00 * * Permission is hereby granted, free of charge, to any person * obtaining a copy of this software and associated documentation * files (the "Software"), to deal in the Software without * restriction, including without limitation the rights to use, * copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following * conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * * The complete license agreement can be found here: * http://opcfoundation.org/License/MIT/1.00/ * ======================================================================*/ using NUnit.Framework; namespace Opc.Ua.Core.Tests.Stack.Client { /// /// Unit tests for covering /// the registration semantics, dispatch routing, composed /// StandardFields, child-scope inheritance, and super-type /// fallback walk. /// [TestFixture] [Category("Core")] [Category("EventRecord")] [Parallelizable] public sealed class EventRecordDecoderRegistryTests { [Test] public void TryRegisterReturnsTrueForNewEntry() { var registry = new EventRecordDecoderRegistry(); bool added = registry.TryRegister( eventTypeId: ObjectTypeIds.BaseEventType, standardFields: BaseEventTypeRecord.Decoder.StandardFields, decode: BaseEventTypeRecord.Decoder.Decode); Assert.That(added, Is.True); } [Test] public void TryRegisterReturnsFalseForDuplicate() { var registry = new EventRecordDecoderRegistry(); registry.TryRegister( ObjectTypeIds.BaseEventType, BaseEventTypeRecord.Decoder.StandardFields, BaseEventTypeRecord.Decoder.Decode); bool added = registry.TryRegister( ObjectTypeIds.BaseEventType, BaseEventTypeRecord.Decoder.StandardFields, BaseEventTypeRecord.Decoder.Decode); Assert.That(added, Is.False); } [Test] public void RegisterThrowsForDuplicate() { var registry = new EventRecordDecoderRegistry(); registry.Register( ObjectTypeIds.BaseEventType, BaseEventTypeRecord.Decoder.StandardFields, BaseEventTypeRecord.Decoder.Decode); Assert.That(() => registry.Register( ObjectTypeIds.BaseEventType, BaseEventTypeRecord.Decoder.StandardFields, BaseEventTypeRecord.Decoder.Decode), Throws.InvalidOperationException); } [Test] public void DefaultRegistryDecodesBaseEventTypeRecord() { EventRecordDecoderRegistry registry = EventRecordDecoderRegistry.Default; // Default ships with the standard UA model pre-registered. // A fields array with just the EventType pointing at // BaseEventType should resolve to a BaseEventTypeRecord. int eventTypePosition = FindEventTypeIndex(registry.StandardFields); Assert.That(eventTypePosition, Is.GreaterThanOrEqualTo(0), "Default registry must expose the EventType browse path " + "for filter construction."); var fields = new Variant[registry.StandardFields.Length]; fields[eventTypePosition] = Variant.From(ObjectTypeIds.BaseEventType); EventRecord record = registry.Decode(fields); Assert.That(record, Is.Not.Null); Assert.That(record, Is.InstanceOf()); } [Test] public void DecodeReturnsNullWhenFieldsEmpty() { EventRecordDecoderRegistry registry = EventRecordDecoderRegistry.Default; Assert.That(registry.Decode([]), Is.Null); } [Test] public void ChildScopeInheritsParentRegistrations() { EventRecordDecoderRegistry parent = EventRecordDecoderRegistry.Default; EventRecordDecoderRegistry child = parent.CreateChildScope(); // The child inherits the parent's registrations transparently // — composed StandardFields contains every parent path. Assert.That(child.StandardFields, Has.Length.EqualTo(parent.StandardFields.Length)); // New registrations on the child do not affect the parent. int parentCount = parent.StandardFields.Length; child.TryRegister( new NodeId(987654321u), [[new QualifiedName("VendorField", 0)]], _ => null); Assert.That(child.StandardFields, Has.Length.GreaterThan(parentCount)); Assert.That(parent.StandardFields, Has.Length.EqualTo(parentCount), "Child mutations must not leak into the parent scope."); } [Test] public void DecodeAsWalksSuperTypeChain() { var registry = new EventRecordDecoderRegistry(); registry.TryRegister( ObjectTypeIds.BaseEventType, BaseEventTypeRecord.Decoder.StandardFields, BaseEventTypeRecord.Decoder.Decode); // Simulate a vendor event type the registry has never seen // — the super-type resolver walks up to BaseEventType which // IS registered. var unknownVendor = new NodeId(42u, 5); registry.SuperTypeResolver = current => current == unknownVendor ? ObjectTypeIds.BaseEventType : null; EventRecord record = registry.DecodeAs( unknownVendor, [default, Variant.From(unknownVendor)]); Assert.That(record, Is.InstanceOf()); } [Test] public void DecodeAsReturnsNullWhenNoAncestorRegistered() { var registry = new EventRecordDecoderRegistry(); var unknownType = new NodeId(987u, 7); EventRecord record = registry.DecodeAs(unknownType, [Variant.From(1)]); Assert.That(record, Is.Null); } [Test] public void StandardFieldsContainsEventTypePath() { EventRecordDecoderRegistry registry = EventRecordDecoderRegistry.Default; int eventTypeIndex = FindEventTypeIndex(registry.StandardFields); Assert.That(eventTypeIndex, Is.GreaterThanOrEqualTo(0), "EventType browse path must be present in the composed " + "StandardFields so the registry can route on it."); } [Test] public void TryRegisterThrowsForNullEventTypeId() { var registry = new EventRecordDecoderRegistry(); Assert.That(() => registry.TryRegister( NodeId.Null, BaseEventTypeRecord.Decoder.StandardFields, BaseEventTypeRecord.Decoder.Decode), Throws.ArgumentException); } [Test] public void DecodeRemapsCallerFieldsToDecoderLocalLayout() { // Build a registry with two distinct decoders. The composed // StandardFields contains both layouts deduplicated, with // positions different from each decoder's local layout. The // registry must remap before invoking the decoder. EventRecordDecoderRegistry registry = EventRecordDecoderRegistry.Default; QualifiedName[][] composed = registry.StandardFields; int eventTypePosition = FindEventTypeIndex(composed); int severityPosition = FindPath(composed, BrowseNames.Severity); Assert.That(eventTypePosition, Is.GreaterThanOrEqualTo(0)); Assert.That(severityPosition, Is.GreaterThanOrEqualTo(0)); var fields = new Variant[composed.Length]; fields[eventTypePosition] = Variant.From(ObjectTypeIds.BaseEventType); fields[severityPosition] = Variant.From((ushort)999); EventRecord record = registry.Decode(fields); Assert.That(record, Is.InstanceOf()); var baseRecord = (BaseEventTypeRecord)record; Assert.That(baseRecord.Severity, Is.EqualTo((ushort)999), "Registry must remap composed-layout fields to each " + "decoder's local positional layout before decoding."); } [Test] public void DecodeReturnsNullForExplicitNullEventTypeField() { EventRecordDecoderRegistry registry = EventRecordDecoderRegistry.Default; int eventTypePosition = FindEventTypeIndex(registry.StandardFields); var fields = new Variant[registry.StandardFields.Length]; fields[eventTypePosition] = Variant.From(NodeId.Null); EventRecord record = registry.Decode(fields); Assert.That(record, Is.Null); } private static int FindPath(QualifiedName[][] standardFields, string browseName) { for (int i = 0; i < standardFields.Length; i++) { QualifiedName[] path = standardFields[i]; if (path.Length == 1 && path[0].NamespaceIndex == 0 && path[0].Name == browseName) { return i; } } return -1; } private static int FindEventTypeIndex(QualifiedName[][] standardFields) { for (int i = 0; i < standardFields.Length; i++) { QualifiedName[] path = standardFields[i]; if (path.Length == 1 && path[0].NamespaceIndex == 0 && path[0].Name == BrowseNames.EventType) { return i; } } return -1; } } }