/* ======================================================================== * 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 System; using System.Collections.Generic; using System.Linq; using Moq; using NUnit.Framework; // CA2000: test code; disposables are ownership-transferred to the EventManager // under test or are torn down with the fixture. #pragma warning disable CA2000 namespace Opc.Ua.Server.Tests.NodeManager { /// /// Deterministic, offline unit tests for . /// [TestFixture] [Category("NodeManager")] [Category("EventManager")] [Parallelizable(ParallelScope.All)] public class EventManagerTests { [Test] public void FailedEventItemConstructionDoesNotPoisonLaterEnumeration() { Mock server = DeterministicServerMock.Create(out MonitoredItemQueueFactory queues); using (queues) using (var manager = new EventManager(server.Object, 100, 100)) using (OperationContext context = NewContext()) { var nodeManager = new Mock(); var ids = new MonitoredItemIdFactory(); MonitoredItemCreateRequest request = NewCreateRequest(1000, 5); ServiceResultException failure = Assert.Throws(() => manager.CreateMonitoredItem( context, nodeManager.Object, null!, 1, ids, TimestampsToReturn.Both, 1000, request, new EventFilter(), createDurable: true)); Assert.That(failure.StatusCode, Is.EqualTo(StatusCodes.BadInternalError)); Assert.That(manager.GetMonitoredItems(), Is.Empty); Assert.That(manager.GetMonitoredItems().Where(item => item.MonitoringAllEvents), Is.Empty); IEventMonitoredItem recovered = manager.CreateMonitoredItem( context, nodeManager.Object, null!, 1, ids, TimestampsToReturn.Both, 1000, request, new EventFilter(), createDurable: false); Assert.That(manager.GetMonitoredItems(), Has.Count.EqualTo(1)); Assert.That(manager.GetMonitoredItems()[0], Is.SameAs(recovered)); } } private static OperationContext NewContext() { return new OperationContext( new RequestHeader(), null, RequestType.CreateMonitoredItems, RequestLifetime.None); } private static MonitoredItemCreateRequest NewCreateRequest( double samplingInterval, uint queueSize) { return new MonitoredItemCreateRequest { ItemToMonitor = new ReadValueId(), MonitoringMode = MonitoringMode.Reporting, RequestedParameters = new MonitoringParameters { ClientHandle = 42, SamplingInterval = samplingInterval, QueueSize = queueSize, DiscardOldest = true } }; } private static EventManager CreateManager( uint maxQueueSize, uint maxDurableQueueSize, out Mock mockServer, out Mock mockNodeManager) { mockServer = DeterministicServerMock.Create(out _); mockNodeManager = new Mock(); return new EventManager(mockServer.Object, maxQueueSize, maxDurableQueueSize); } [Test] public void ConstructorWithNullServerThrows() { Assert.That( () => new EventManager(null!, 10, 10), Throws.TypeOf()); } [Test] public void CreateMonitoredItemAddsItemToManager() { EventManager manager = CreateManager(100, 100, out _, out Mock nm); var idFactory = new MonitoredItemIdFactory(); IEventMonitoredItem item = manager.CreateMonitoredItem( NewContext(), nm.Object, null!, 1, idFactory, TimestampsToReturn.Both, 1000.0, NewCreateRequest(1000.0, 5), new EventFilter(), false); Assert.That(item, Is.Not.Null); Assert.That(manager.GetMonitoredItems(), Has.Count.EqualTo(1)); Assert.That(manager.GetMonitoredItems()[0].Id, Is.EqualTo(item.Id)); } [Test] public void CreateMonitoredItemClampsQueueSizeToMax() { EventManager manager = CreateManager(3, 3, out _, out Mock nm); var idFactory = new MonitoredItemIdFactory(); IEventMonitoredItem item = manager.CreateMonitoredItem( NewContext(), nm.Object, null!, 1, idFactory, TimestampsToReturn.Both, 1000.0, NewCreateRequest(1000.0, 1000), new EventFilter(), false); Assert.That(item, Is.InstanceOf()); Assert.That(((MonitoredItem)item).QueueSize, Is.EqualTo(3)); } /// /// Part 4 ยง7.21: for event monitored items a requested queueSize of 0 /// means the server default and 1 means the minimum queue size the /// server requires for Event Notifications. Neither may be taken /// literally, otherwise a burst of events (for example the /// OpenSecureChannel, CreateSession and ActivateSession audit events of /// one connect) collapses to the last event before the next publish. /// [TestCase(0u)] [TestCase(1u)] public void CreateMonitoredItemRevisesSpecialEventQueueSizes(uint requestedQueueSize) { EventManager manager = CreateManager(10000, 10000, out _, out Mock nm); var idFactory = new MonitoredItemIdFactory(); IEventMonitoredItem item = manager.CreateMonitoredItem( NewContext(), nm.Object, null!, 1, idFactory, TimestampsToReturn.Both, 1000.0, NewCreateRequest(1000.0, requestedQueueSize), new EventFilter(), false); Assert.That( ((MonitoredItem)item).QueueSize, Is.EqualTo(EventManager.DefaultEventQueueSize)); } /// /// The server default is still limited by the configured maximum, and a /// configured maximum of 1 is honored rather than read as another /// queueSize 1 request. /// [TestCase(0u, 3u)] [TestCase(1u, 3u)] [TestCase(0u, 1u)] [TestCase(1u, 1u)] public void CreateMonitoredItemClampsDefaultEventQueueSizeToMax( uint requestedQueueSize, uint maxQueueSize) { EventManager manager = CreateManager( maxQueueSize, 10000, out _, out Mock nm); var idFactory = new MonitoredItemIdFactory(); IEventMonitoredItem item = manager.CreateMonitoredItem( NewContext(), nm.Object, null!, 1, idFactory, TimestampsToReturn.Both, 1000.0, NewCreateRequest(1000.0, requestedQueueSize), new EventFilter(), false); Assert.That(((MonitoredItem)item).QueueSize, Is.EqualTo(maxQueueSize)); } [Test] public void ModifyMonitoredItemRevisesEventQueueSizeOne() { EventManager manager = CreateManager(10000, 10000, out _, out Mock nm); var idFactory = new MonitoredItemIdFactory(); IEventMonitoredItem item = manager.CreateMonitoredItem( NewContext(), nm.Object, null!, 1, idFactory, TimestampsToReturn.Both, 1000.0, NewCreateRequest(1000.0, 5), new EventFilter(), false); manager.ModifyMonitoredItem( NewContext(), item, TimestampsToReturn.Both, new MonitoredItemModifyRequest { RequestedParameters = new MonitoringParameters { ClientHandle = 42, SamplingInterval = 1000.0, QueueSize = 1, DiscardOldest = true } }, new EventFilter()); Assert.That( ((MonitoredItem)item).QueueSize, Is.EqualTo(EventManager.DefaultEventQueueSize)); } /// /// Part 4 7.21: a NaN sampling interval on create is revised to the publishing interval /// instead of being echoed back. /// [Test] public void CreateMonitoredItemRevisesNaNSamplingIntervalToPublishingInterval() { EventManager manager = CreateManager(100, 100, out _, out Mock nm); IEventMonitoredItem item = manager.CreateMonitoredItem( NewContext(), nm.Object, null!, 1, new MonitoredItemIdFactory(), TimestampsToReturn.Both, 750.0, NewCreateRequest(double.NaN, 5), new EventFilter(), false); Assert.That(((MonitoredItem)item).SamplingInterval, Is.EqualTo(750.0)); } /// /// Part 4 7.21: any negative sampling interval on modify selects the publishing interval /// of the subscription; the negative value is never returned as the revised interval. /// [TestCase(-1.0)] [TestCase(-5.0)] [TestCase(double.NaN)] public void ModifyMonitoredItemRevisesNegativeSamplingIntervalToPublishingInterval( double requestedSamplingInterval) { EventManager manager = CreateManager(100, 100, out _, out Mock nm); IEventMonitoredItem item = manager.CreateMonitoredItem( NewContext(), nm.Object, null!, 1, new MonitoredItemIdFactory(), TimestampsToReturn.Both, 1000.0, NewCreateRequest(500.0, 5), new EventFilter(), false); var subscription = new Mock(); subscription.SetupGet(s => s.PublishingInterval).Returns(250.0); item.SubscriptionCallback = subscription.Object; manager.ModifyMonitoredItem( NewContext(), item, TimestampsToReturn.Both, new MonitoredItemModifyRequest { RequestedParameters = new MonitoringParameters { ClientHandle = 42, SamplingInterval = requestedSamplingInterval, QueueSize = 5, DiscardOldest = true } }, new EventFilter()); Assert.That(((MonitoredItem)item).SamplingInterval, Is.EqualTo(250.0)); } /// /// Regression for the CTT "Auditing Connections" failures: the CTT audit /// subscription requests QueueSize 1 on Server.EventNotifier. Every event /// raised between two publishes must still be delivered. /// [Test] public void EventBurstIsNotCollapsedWhenQueueSizeOneRequested() { EventManager manager = CreateManager(10000, 10000, out _, out Mock nm); var idFactory = new MonitoredItemIdFactory(); IEventMonitoredItem item = manager.CreateMonitoredItem( NewContext(), nm.Object, null!, 1, idFactory, TimestampsToReturn.Both, 1000.0, NewCreateRequest(1000.0, 1), new EventFilter(), false); for (int ii = 0; ii < 3; ii++) { ((MonitoredItem)item).QueueEvent(new EventFieldList { ClientHandle = 42, EventFields = [new Variant("event" + ii)] }); } var notifications = new Queue(); ((MonitoredItem)item).Publish(NewContext(), notifications, 100); // an overflowing queue would drop events and append an // EventQueueOverflowEvent, so compare the exact payloads. Assert.That( notifications.Select(n => n.EventFields[0].GetString()), Is.EqualTo(s_eventBurst)); } /// /// A durable event queue persisted with the literal size 1 must be resized /// to the revised size when the monitored item is restored. /// [Test] public void RestoreMonitoredItemResizesRestoredEventQueue() { Mock server = DeterministicServerMock.Create( out MonitoredItemQueueFactory queueFactory); using var manager = new EventManager(server.Object, 10000, 10000); IEventMonitoredItemQueue persisted = queueFactory.CreateEventQueue(false, 7); persisted.SetQueueSize(1, true); persisted.Enqueue(new EventFieldList { ClientHandle = 42, EventFields = [new Variant("event0")] }); var stored = new StoredMonitoredItem { Id = 7, SubscriptionId = 1, TypeMask = MonitoredItemTypeMask.Events, MonitoringMode = MonitoringMode.Reporting, NodeId = ObjectIds.Server, AttributeId = Attributes.EventNotifier, ClientHandle = 42, QueueSize = 1, DiscardOldest = true, SamplingInterval = 0, TimestampsToReturn = TimestampsToReturn.Both, DiagnosticsMasks = DiagnosticsMasks.None, OriginalFilter = new EventFilter(), FilterToUse = new EventFilter(), IndexRange = string.Empty, ParsedIndexRange = NumericRange.Null, RestoredEventQueue = persisted }; var item = (MonitoredItem)manager.RestoreMonitoredItem( new Mock().Object, null!, stored); Assert.That(item.QueueSize, Is.EqualTo(EventManager.DefaultEventQueueSize)); for (int ii = 1; ii < 3; ii++) { item.QueueEvent(new EventFieldList { ClientHandle = 42, EventFields = [new Variant("event" + ii)] }); } var notifications = new Queue(); item.Publish(NewContext(), notifications, 100); Assert.That( notifications.Select(n => n.EventFields[0].GetString()), Is.EqualTo(s_eventBurst)); } [Test] public void CreateMonitoredItemUsesPublishingIntervalWhenSamplingNegative() { EventManager manager = CreateManager(100, 100, out _, out Mock nm); var idFactory = new MonitoredItemIdFactory(); IEventMonitoredItem item = manager.CreateMonitoredItem( NewContext(), nm.Object, null!, 1, idFactory, TimestampsToReturn.Both, 2500.0, NewCreateRequest(-1.0, 5), new EventFilter(), false); Assert.That(item.SamplingInterval, Is.EqualTo(2500.0)); } [Test] public void CreateMonitoredItemAssignsDistinctIds() { EventManager manager = CreateManager(100, 100, out _, out Mock nm); var idFactory = new MonitoredItemIdFactory(); IEventMonitoredItem first = manager.CreateMonitoredItem( NewContext(), nm.Object, null!, 1, idFactory, TimestampsToReturn.Both, 1000.0, NewCreateRequest(1000.0, 5), new EventFilter(), false); IEventMonitoredItem second = manager.CreateMonitoredItem( NewContext(), nm.Object, null!, 1, idFactory, TimestampsToReturn.Both, 1000.0, NewCreateRequest(1000.0, 5), new EventFilter(), false); Assert.That(first.Id, Is.Not.EqualTo(second.Id)); Assert.That(manager.GetMonitoredItems(), Has.Count.EqualTo(2)); } [Test] public void DeleteMonitoredItemRemovesItem() { EventManager manager = CreateManager(100, 100, out _, out Mock nm); var idFactory = new MonitoredItemIdFactory(); IEventMonitoredItem item = manager.CreateMonitoredItem( NewContext(), nm.Object, null!, 1, idFactory, TimestampsToReturn.Both, 1000.0, NewCreateRequest(1000.0, 5), new EventFilter(), false); manager.DeleteMonitoredItem(item.Id); Assert.That(manager.GetMonitoredItems(), Is.Empty); } [Test] public void ModifyMonitoredItemIgnoresForeignItem() { EventManager manager = CreateManager(100, 100, out _, out _); var foreign = new Mock(); foreign.SetupGet(m => m.Id).Returns(9999); foreign.SetupGet(m => m.IsDurable).Returns(false); Assert.DoesNotThrow(() => manager.ModifyMonitoredItem( NewContext(), foreign.Object, TimestampsToReturn.Both, new MonitoredItemModifyRequest { RequestedParameters = new MonitoringParameters { QueueSize = 5 } }, new EventFilter())); foreign.Verify( m => m.ModifyAttributes( It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } [Test] public void ModifyMonitoredItemUpdatesOwnedItem() { EventManager manager = CreateManager(100, 100, out _, out Mock nm); var idFactory = new MonitoredItemIdFactory(); IEventMonitoredItem item = manager.CreateMonitoredItem( NewContext(), nm.Object, null!, 1, idFactory, TimestampsToReturn.Both, 1000.0, NewCreateRequest(1000.0, 5), new EventFilter(), false); Assert.DoesNotThrow(() => manager.ModifyMonitoredItem( NewContext(), item, TimestampsToReturn.Both, new MonitoredItemModifyRequest { RequestedParameters = new MonitoringParameters { ClientHandle = 7, SamplingInterval = 2000.0, QueueSize = 8, DiscardOldest = false } }, new EventFilter())); Assert.That(((MonitoredItem)item).QueueSize, Is.EqualTo(8)); } [Test] public void GetMonitoredItemsReturnsSnapshot() { EventManager manager = CreateManager(100, 100, out _, out Mock nm); var idFactory = new MonitoredItemIdFactory(); manager.CreateMonitoredItem( NewContext(), nm.Object, null!, 1, idFactory, TimestampsToReturn.Both, 1000.0, NewCreateRequest(1000.0, 5), new EventFilter(), false); IList first = manager.GetMonitoredItems(); first.Clear(); Assert.That(manager.GetMonitoredItems(), Has.Count.EqualTo(1)); } [Test] public void DisposeClearsMonitoredItems() { EventManager manager = CreateManager(100, 100, out _, out Mock nm); var idFactory = new MonitoredItemIdFactory(); manager.CreateMonitoredItem( NewContext(), nm.Object, null!, 1, idFactory, TimestampsToReturn.Both, 1000.0, NewCreateRequest(1000.0, 5), new EventFilter(), false); manager.Dispose(); Assert.That(manager.GetMonitoredItems(), Is.Empty); } [Test] public void ReportEventWithNullEventThrows() { #pragma warning disable CS0618 // testing the obsolete overload deliberately Assert.That( () => EventManager.ReportEvent(null!, []), Throws.TypeOf()); #pragma warning restore CS0618 } [Test] public void ReportEventQueuesEventToEachItem() { var filterTarget = new Mock(); var item1 = new Mock(); var item2 = new Mock(); #pragma warning disable CS0618 // testing the obsolete overload deliberately EventManager.ReportEvent( filterTarget.Object, [item1.Object, item2.Object]); #pragma warning restore CS0618 item1.Verify(m => m.QueueEvent(filterTarget.Object), Times.Once); item2.Verify(m => m.QueueEvent(filterTarget.Object), Times.Once); } /// /// Defines the ordered event payloads expected after publishing a three-event burst. /// private static readonly string[] s_eventBurst = ["event0", "event1", "event2"]; } }