/* ======================================================================== * Copyright (c) 2005-2026 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.Threading; using System.Threading.Tasks; using Moq; using NUnit.Framework; using Opc.Ua.Client.TestFramework; using Opc.Ua.Server; namespace Opc.Ua.Subscriptions.Tests { /// /// Verifies live Publish and Republish behavior respects monitoring mode, session ownership, and diagnostics. /// [TestFixture] [Category("Subscription")] [Category("Integration")] [NonParallelizable] public sealed class SubscriptionSemanticsServiceTests : TestFixture { /// /// Verifies completed aggregates are published only by monitored items in Reporting mode. /// [TestCase(MonitoringMode.Disabled)] [TestCase(MonitoringMode.Sampling)] [TestCase(MonitoringMode.Reporting)] public async Task AggregateCompletionRespectsMonitoringModeOverPublishServiceAsync(MonitoringMode mode) { using var deadline = new CancellationTokenSource(TimeSpan.FromSeconds(15)); AggregateManager aggregates = ReferenceServer.CurrentInstance.AggregateManager; var calculator = new Mock(); calculator.Setup(value => value.HasEndTimePassed(It.IsAny())).Returns(true); calculator.Setup(value => value.QueueRawValue(It.IsAny())).Returns(true); var aggregateValue = new DataValue(42.0); calculator.Setup(value => value.TryGetProcessedValue(true, out aggregateValue)).Returns(true); var aggregateId = new NodeId(Guid.NewGuid(), 1); await aggregates.RegisterFactoryAsync( aggregateId, "Subscription readiness regression", (id, start, end, interval, stepped, configuration, telemetry) => calculator.Object, deadline.Token).ConfigureAwait(false); CreateSubscriptionResponse subscription = await Session.CreateSubscriptionAsync( null, 50, 1000, 1, 0, true, 0, deadline.Token).ConfigureAwait(false); try { CreateMonitoredItemsResponse created = await Session.CreateMonitoredItemsAsync( null, subscription.SubscriptionId, TimestampsToReturn.Both, [ new MonitoredItemCreateRequest { ItemToMonitor = new ReadValueId { NodeId = new NodeId(Variables.Server_ServerStatus_SecondsTillShutdown), AttributeId = Attributes.Value }, MonitoringMode = mode, RequestedParameters = new MonitoringParameters { ClientHandle = 9, SamplingInterval = 50, QueueSize = 4, DiscardOldest = true, Filter = new ExtensionObject(new AggregateFilter { AggregateType = aggregateId, StartTime = DateTime.UtcNow.AddSeconds(-1), ProcessingInterval = 1000, AggregateConfiguration = new AggregateConfiguration { UseServerCapabilitiesDefaults = true } }) } } ], deadline.Token).ConfigureAwait(false); Assert.That(created.Results, Has.Count.EqualTo(1)); Assert.That(created.Results[0].StatusCode, Is.EqualTo(StatusCodes.Good)); bool received = false; for (int attempt = 0; attempt < 3; attempt++) { PublishResponse response = await Session.PublishAsync( new RequestHeader { TimeoutHint = 2000 }, [], deadline.Token).ConfigureAwait(false); Assert.That(response.ResponseHeader.ServiceResult, Is.EqualTo(StatusCodes.Good)); Assert.That(response.SubscriptionId, Is.EqualTo(subscription.SubscriptionId)); if (mode != MonitoringMode.Reporting) { Assert.That(response.NotificationMessage.NotificationData, Is.Empty); continue; } foreach (ExtensionObject notification in response.NotificationMessage.NotificationData) { Assert.That(notification.TryGetValue(out DataChangeNotification? data), Is.True); Assert.That(data!.MonitoredItems, Has.Count.EqualTo(1)); Assert.That(data.MonitoredItems[0].ClientHandle, Is.EqualTo(9u)); AssertValue(data.MonitoredItems[0].Value, 42.0); received = true; } if (received) { break; } } Assert.That(received, Is.EqualTo(mode == MonitoringMode.Reporting)); } finally { await Session.DeleteSubscriptionsAsync( null, [subscription.SubscriptionId], CancellationToken.None).ConfigureAwait(false); aggregates.RegisterFactory(aggregateId); } } /// /// Verifies Publish reports no subscription when the caller owns none, regardless of other sessions. /// [TestCase(false, false)] [TestCase(true, false)] [TestCase(false, true)] [TestCase(true, true)] public async Task PublishWithoutOwnSubscriptionsIgnoresOtherSessionsAsync( bool otherHasSubscription, bool deletedOwnSubscription) { using var other = await OpenAuxSessionAsync().ConfigureAwait(false); uint otherId = 0; if (otherHasSubscription) { CreateSubscriptionResponse created = await other.CreateSubscriptionAsync( null, 1000, 1000, 10, 0, false, 0, CancellationToken.None).ConfigureAwait(false); otherId = created.SubscriptionId; } try { if (deletedOwnSubscription) { CreateSubscriptionResponse own = await Session.CreateSubscriptionAsync( null, 1000, 1000, 10, 0, false, 0, CancellationToken.None).ConfigureAwait(false); await Session.DeleteSubscriptionsAsync(null, [own.SubscriptionId], CancellationToken.None) .ConfigureAwait(false); } ServiceResultException error = Assert.ThrowsAsync(async () => await Session.PublishAsync( new RequestHeader { TimeoutHint = 1000 }, [], CancellationToken.None).ConfigureAwait(false)); Assert.That(error.StatusCode, Is.EqualTo(StatusCodes.BadNoSubscription)); } finally { if (otherId != 0) { await other.DeleteSubscriptionsAsync(null, [otherId], CancellationToken.None).ConfigureAwait(false); } await other.CloseAsync(5000, true).ConfigureAwait(false); } } /// /// Verifies Republish diagnostics count authorized requests once without counting another session's denied /// request. /// [Test] public async Task RepublishCountersCountEachAuthorizedRequestOnceAsync() { using var deadline = new CancellationTokenSource(TimeSpan.FromSeconds(15)); CreateSubscriptionResponse created = await Session.CreateSubscriptionAsync( null, 50, 1000, 1, 0, true, 0, deadline.Token).ConfigureAwait(false); try { CreateMonitoredItemsResponse items = await Session.CreateMonitoredItemsAsync( null, created.SubscriptionId, TimestampsToReturn.Both, [new MonitoredItemCreateRequest { ItemToMonitor = new ReadValueId { NodeId = VariableIds.Server_ServerStatus_SecondsTillShutdown, AttributeId = Attributes.Value }, MonitoringMode = MonitoringMode.Reporting, RequestedParameters = new MonitoringParameters { ClientHandle = 317, QueueSize = 1 } }], deadline.Token).ConfigureAwait(false); Assert.That(items.Results[0].StatusCode, Is.EqualTo(StatusCodes.Good)); PublishResponse published; do { published = await Session.PublishAsync( new RequestHeader { TimeoutHint = 2000 }, [], deadline.Token).ConfigureAwait(false); } while (published.NotificationMessage.NotificationData.IsEmpty); Assert.That(ReferenceServer.CurrentInstance.SubscriptionManager.TryGetSubscription( created.SubscriptionId, out ISubscription? subscription), Is.True); RepublishResponse republished = await Session.RepublishAsync( null, created.SubscriptionId, published.NotificationMessage.SequenceNumber, deadline.Token) .ConfigureAwait(false); Assert.That(republished.NotificationMessage.SequenceNumber, Is.EqualTo(published.NotificationMessage.SequenceNumber)); Assert.That(republished.NotificationMessage.NotificationData.Count, Is.EqualTo(published.NotificationMessage.NotificationData.Count)); Assert.That(subscription!.ReadDiagnostics(d => d.RepublishRequestCount), Is.EqualTo(1)); Assert.That(subscription.ReadDiagnostics(d => d.RepublishMessageRequestCount), Is.EqualTo(1)); Assert.That(subscription.ReadDiagnostics(d => d.RepublishMessageCount), Is.EqualTo(1)); ServiceResultException missing = Assert.ThrowsAsync(async () => await Session.RepublishAsync(null, created.SubscriptionId, uint.MaxValue, deadline.Token) .ConfigureAwait(false)); Assert.That(missing.StatusCode, Is.EqualTo(StatusCodes.BadMessageNotAvailable)); Assert.That(subscription.ReadDiagnostics(d => d.RepublishRequestCount), Is.EqualTo(2)); Assert.That(subscription.ReadDiagnostics(d => d.RepublishMessageRequestCount), Is.EqualTo(2)); Assert.That(subscription.ReadDiagnostics(d => d.RepublishMessageCount), Is.EqualTo(1)); using var other = await OpenAuxSessionAsync().ConfigureAwait(false); try { ServiceResultException denied = Assert.ThrowsAsync(async () => await other.RepublishAsync( null, created.SubscriptionId, published.NotificationMessage.SequenceNumber, deadline.Token) .ConfigureAwait(false)); Assert.That(denied.StatusCode, Is.EqualTo(StatusCodes.BadSubscriptionIdInvalid)); Assert.That(subscription.ReadDiagnostics(d => d.RepublishRequestCount), Is.EqualTo(2)); Assert.That(subscription.ReadDiagnostics(d => d.RepublishMessageRequestCount), Is.EqualTo(2)); } finally { await other.CloseAsync(5000, true).ConfigureAwait(false); } } finally { await Session.DeleteSubscriptionsAsync(null, [created.SubscriptionId], CancellationToken.None) .ConfigureAwait(false); } } /// /// Requires a Good aggregate value containing the exact expected Double. /// private static void AssertValue(in DataValue value, double expected) { Assert.That(value.StatusCode, Is.EqualTo(StatusCodes.Good)); Assert.That(value.WrappedValue.TryGetValue(out double actual), Is.True); Assert.That(actual, Is.EqualTo(expected)); } } }