/* ======================================================================== * 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/ * ======================================================================*/ // CA2000: test code; many disposables are ownership-transferred to test fixtures or short-lived, // making CA2000 noisy without a real leak risk. Disabled file-level for the suite. #pragma warning disable CA2000 using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; using System.Threading; using System.Threading.Tasks; using Moq; using NUnit.Framework; using Opc.Ua.Client.TestFramework; using Opc.Ua.Security.Certificates; using Opc.Ua.Tests; namespace Opc.Ua.Client.Tests { [TestFixture] [Category("Client")] [Category("Session")] [SetCulture("en-us")] [SetUICulture("en-us")] public sealed class SessionTests { [TestCase(3, false, true)] [TestCase(4, true, true)] [TestCase(4, false, false)] public async Task FetchTypeTreeVisitsEveryNodeAndTerminatesAsync(int count, bool singleId, bool cycle) { using var session = SessionMock.Create(); using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(10)); ushort namespaceIndex = session.NamespaceUris.GetIndexOrAppend("urn:test:type-traversal"); var ids = Enumerable.Range(1, count) .Select(value => new NodeId((uint)value, namespaceIndex)).ToArrayOf(); var browsed = new HashSet(); session.Channel.Setup(channel => channel.SendRequestAsync( It.IsAny(), It.IsAny())) .Returns((ReadRequest request, CancellationToken ct) => { ct.ThrowIfCancellationRequested(); return new ValueTask(new ReadResponse { Results = request.NodesToRead.ConvertAll(item => item.AttributeId switch { Attributes.NodeId => new DataValue(item.NodeId), Attributes.NodeClass => new DataValue((int)NodeClass.ObjectType), Attributes.BrowseName => new DataValue(new QualifiedName("Type", namespaceIndex)), Attributes.DisplayName => new DataValue(LocalizedText.From("Type")), Attributes.IsAbstract => new DataValue(false), _ => DataValue.FromStatusCode(StatusCodes.BadAttributeIdInvalid) }) }); }); session.Channel.Setup(channel => channel.SendRequestAsync( It.IsAny(), It.IsAny())) .Returns((BrowseRequest request, CancellationToken ct) => { ct.ThrowIfCancellationRequested(); return new ValueTask(new BrowseResponse { Results = request.NodesToBrowse.ConvertAll(item => { browsed.Add(item.NodeId); int index = ids.ToList().IndexOf(item.NodeId); return new BrowseResult { References = index == count - 1 && !cycle ? [] : [ new ReferenceDescription { ReferenceTypeId = ReferenceTypeIds.HasSubtype, IsForward = true, NodeId = new ExpandedNodeId(ids[(index + 1) % count]), NodeClass = NodeClass.ObjectType, BrowseName = new QualifiedName("Type", namespaceIndex) } ] }; }) }); }); IServiceMessageContext context = session.MessageContext; int lookups = 0; session.Channel.SetupGet(channel => channel.MessageContext).Returns(() => { Assert.That(++lookups, Is.LessThan(128), "The cached graph must terminate instead of recursively revisiting the cycle."); return context; }); try { if (singleId) { await session.FetchTypeTreeAsync(new ExpandedNodeId(ids[0]), timeout.Token).ConfigureAwait(false); } else { await session.FetchTypeTreeAsync( [new ExpandedNodeId(ids[0])], timeout.Token).ConfigureAwait(false); } } finally { session.Channel.SetupGet(channel => channel.MessageContext).Returns(context); } Assert.That(browsed, Is.EquivalentTo(ids.ToList())); } [Test] public void BuildTransportChainReturnsLeafFollowedByIssuers() { using Certificate leaf = CertificateBuilder.Create("CN=TransportLeaf").CreateForRSA(); using Certificate issuer = CertificateBuilder .Create("CN=TransportIssuer") .SetCAConstraint() .CreateForRSA(); using var issuerChain = new CertificateCollection { issuer }; using var entry = new CertificateEntry( leaf, issuerChain, ObjectTypeIds.RsaSha256ApplicationCertificateType); using CertificateCollection? chain = Session.BuildTransportChain(entry); // The transport chain is [leaf, ...issuers] with the leaf at index 0. Assert.That(chain, Is.Not.Null); Assert.That(chain!, Has.Count.EqualTo(2)); Assert.That(chain[0].Thumbprint, Is.EqualTo(leaf.Thumbprint)); Assert.That(chain[1].Thumbprint, Is.EqualTo(issuer.Thumbprint)); } [Test] public void BuildTransportChainReturnsNullWhenNoIssuers() { using Certificate leaf = CertificateBuilder.Create("CN=TransportLeafOnly").CreateForRSA(); using var issuerChain = new CertificateCollection(); using var entry = new CertificateEntry( leaf, issuerChain, ObjectTypeIds.RsaSha256ApplicationCertificateType); CertificateCollection? chain = Session.BuildTransportChain(entry); // With no issuers the transport sends the leaf only, so the chain is null. Assert.That(chain, Is.Null); } [Test] public void BuildInstanceCertificateEntryReturnsNullWhenNoCertificate() { Assert.That(Session.BuildInstanceCertificateEntry(null, null), Is.Null); } [Test] public void BuildInstanceCertificateEntryStoresIssuersWithoutLeaf() { using Certificate leaf = CertificateBuilder.Create("CN=EntryLeaf").CreateForRSA(); using Certificate issuer = CertificateBuilder .Create("CN=EntryIssuer") .SetCAConstraint() .CreateForRSA(); // Transport-style chain: [leaf, ...issuers]. using var transportChain = new CertificateCollection { leaf, issuer }; using CertificateEntry? entry = Session.BuildInstanceCertificateEntry(leaf, transportChain); Assert.That(entry, Is.Not.Null); Assert.That(entry!.Certificate.Thumbprint, Is.EqualTo(leaf.Thumbprint)); // The leaf is dropped from the stored issuer chain (issuers only). Assert.That(entry.IssuerChain, Has.Count.EqualTo(1)); Assert.That(entry.IssuerChain[0].Thumbprint, Is.EqualTo(issuer.Thumbprint)); } [Test] public void BuildInstanceCertificateEntryHandlesNullChain() { using Certificate leaf = CertificateBuilder.Create("CN=EntryLeafOnly").CreateForRSA(); using CertificateEntry? entry = Session.BuildInstanceCertificateEntry(leaf, null); Assert.That(entry, Is.Not.Null); Assert.That(entry!.Certificate.Thumbprint, Is.EqualTo(leaf.Thumbprint)); Assert.That(entry.IssuerChain, Has.Count.EqualTo(0)); } [Test] public void BuildTransportChainAndInstanceEntryRoundTrip() { using Certificate leaf = CertificateBuilder.Create("CN=RoundTripLeaf").CreateForRSA(); using Certificate issuer = CertificateBuilder .Create("CN=RoundTripIssuer") .SetCAConstraint() .CreateForRSA(); using var issuerChain = new CertificateCollection { issuer }; using var original = new CertificateEntry( leaf, issuerChain, ObjectTypeIds.RsaSha256ApplicationCertificateType); using CertificateCollection? transportChain = Session.BuildTransportChain(original); using CertificateEntry? rebuilt = Session.BuildInstanceCertificateEntry(leaf, transportChain); Assert.That(rebuilt, Is.Not.Null); Assert.That(rebuilt!.Certificate.Thumbprint, Is.EqualTo(leaf.Thumbprint)); Assert.That(rebuilt.IssuerChain, Has.Count.EqualTo(1)); Assert.That(rebuilt.IssuerChain[0].Thumbprint, Is.EqualTo(issuer.Thumbprint)); } [Test] public async Task FetchOperationLimitsAsyncShouldFetchAllOperationLimitsAsync() { using var sut = SessionMock.Create(); CancellationToken ct = CancellationToken.None; ArrayOf dataValues = [ new DataValue(new Variant(2000u)), new DataValue(new Variant(3000u)), new DataValue(new Variant(4000u)), new DataValue(new Variant(1000u)), new DataValue(new Variant(5000u)), new DataValue(new Variant(6000u)), new DataValue(new Variant(7000u)), new DataValue(new Variant(8000u)), new DataValue(new Variant(9000u)), new DataValue(new Variant(10000u)), new DataValue(new Variant(11000u)), new DataValue(new Variant(12000u)), new DataValue(new Variant((ushort)13000)), new DataValue(new Variant((ushort)14000u)), new DataValue(new Variant((ushort)15000u)), new DataValue(new Variant(16000u)), new DataValue(new Variant(17000u)), new DataValue(new Variant(18000u)), new DataValue(new Variant((double)19000.0)), new DataValue(new Variant(20000u)), new DataValue(new Variant(21000u)), new DataValue(new Variant(22000u)), new DataValue(new Variant(23000u)), new DataValue(new Variant(24000u)), new DataValue(new Variant(25000u)), new DataValue(new Variant(26000u)), new DataValue(new Variant(27000u)) ]; sut.Channel .SetupSequence(c => c.SendRequestAsync( It.IsAny(), It.IsAny())) .Returns(new ValueTask(new ReadResponse { Results = [new DataValue(new Variant(1000u))], DiagnosticInfos = [] })) .Returns(new ValueTask(new ReadResponse { Results = dataValues, DiagnosticInfos = [] })); // Act await sut.FetchOperationLimitsAsync(ct).ConfigureAwait(false); // Assert Assert.That(sut.OperationLimits.MaxNodesPerRead, Is.EqualTo(1000)); Assert.That(sut.OperationLimits.MaxNodesPerHistoryReadData, Is.EqualTo(2000)); Assert.That(sut.OperationLimits.MaxNodesPerHistoryReadEvents, Is.EqualTo(3000)); Assert.That(sut.OperationLimits.MaxNodesPerWrite, Is.EqualTo(4000)); Assert.That(sut.OperationLimits.MaxNodesPerHistoryUpdateData, Is.EqualTo(5000)); Assert.That(sut.OperationLimits.MaxNodesPerHistoryUpdateEvents, Is.EqualTo(6000)); Assert.That(sut.OperationLimits.MaxNodesPerMethodCall, Is.EqualTo(7000)); Assert.That(sut.OperationLimits.MaxNodesPerBrowse, Is.EqualTo(8000)); Assert.That(sut.OperationLimits.MaxNodesPerRegisterNodes, Is.EqualTo(9000)); Assert.That(sut.OperationLimits.MaxNodesPerNodeManagement, Is.EqualTo(10000)); Assert.That(sut.OperationLimits.MaxMonitoredItemsPerCall, Is.EqualTo(11000)); Assert.That(sut.OperationLimits.MaxNodesPerTranslateBrowsePathsToNodeIds, Is.EqualTo(12000)); Assert.That(sut.ServerCapabilities.MaxBrowseContinuationPoints, Is.EqualTo(13000)); Assert.That(sut.ServerCapabilities.MaxHistoryContinuationPoints, Is.EqualTo(14000)); Assert.That(sut.ServerCapabilities.MaxQueryContinuationPoints, Is.EqualTo(15000)); Assert.That(sut.ServerCapabilities.MaxStringLength, Is.EqualTo(16000)); Assert.That(sut.ServerCapabilities.MaxArrayLength, Is.EqualTo(17000)); Assert.That(sut.ServerCapabilities.MaxByteStringLength, Is.EqualTo(18000)); Assert.That(sut.ServerCapabilities.MinSupportedSampleRate, Is.EqualTo(19000.0)); Assert.That(sut.ServerCapabilities.MaxSessions, Is.EqualTo(20000)); Assert.That(sut.ServerCapabilities.MaxSubscriptions, Is.EqualTo(21000)); Assert.That(sut.ServerCapabilities.MaxMonitoredItems, Is.EqualTo(22000)); Assert.That(sut.ServerCapabilities.MaxMonitoredItemsPerSubscription, Is.EqualTo(23000)); Assert.That(sut.ServerCapabilities.MaxMonitoredItemsQueueSize, Is.EqualTo(24000)); Assert.That(sut.ServerCapabilities.MaxSubscriptionsPerSession, Is.EqualTo(25000)); Assert.That(sut.ServerCapabilities.MaxWhereClauseParameters, Is.EqualTo(26000)); Assert.That(sut.ServerCapabilities.MaxSelectClauseParameters, Is.EqualTo(27000)); sut.Channel.Verify(); } [Test] public void FetchOperationLimitsAsyncShouldHandleEmptyResponse() { // Arrange using var sut = SessionMock.Create(); CancellationToken ct = CancellationToken.None; ArrayOf dataValues = []; ArrayOf diagnosticInfos = []; sut.Channel .Setup(c => c.SendRequestAsync( It.IsAny(), It.IsAny())) .Returns(new ValueTask(new ReadResponse { Results = dataValues, DiagnosticInfos = diagnosticInfos })) .Verifiable(Times.Once); Assert.ThrowsAsync( async () => await sut.FetchOperationLimitsAsync(ct).ConfigureAwait(false)); sut.Channel.Verify(); } [Test] public void FetchOperationLimitsAsyncShouldHandlePartialData() { // Arrange using var sut = SessionMock.Create(); CancellationToken ct = CancellationToken.None; ArrayOf dataValues = [ new DataValue(new Variant(1000u)), new DataValue(new Variant(2000u)), new DataValue(new Variant(3000u)) ]; sut.Channel .SetupSequence(c => c.SendRequestAsync( It.IsAny(), It.IsAny())) .Returns(new ValueTask(new ReadResponse { Results = [new DataValue(new Variant(1000u))], DiagnosticInfos = [] })) .Returns(new ValueTask(new ReadResponse { Results = dataValues, DiagnosticInfos = [] })); Assert.ThrowsAsync( async () => await sut.FetchOperationLimitsAsync(ct).ConfigureAwait(false)); sut.Channel.Verify(); } [Test] public void FetchOperationLimitsAsyncShouldHandleErrors() { // Arrange using var sut = SessionMock.Create(); CancellationToken ct = CancellationToken.None; ArrayOf dataValues = [DataValue.FromStatusCode(StatusCodes.BadUnexpectedError)]; ArrayOf diagnosticInfos = []; sut.Channel .Setup(c => c.SendRequestAsync( It.IsAny(), It.IsAny())) .Returns(new ValueTask(new ReadResponse { Results = dataValues, DiagnosticInfos = diagnosticInfos })) .Verifiable(Times.Exactly(2)); Assert.ThrowsAsync( async () => await sut.FetchOperationLimitsAsync(ct).ConfigureAwait(false)); sut.Channel.Verify(); } [Test] public void FetchOperationLimitsAsyncShouldThrowWhenInvalidDataTypes() { // Arrange using var sut = SessionMock.Create(); CancellationToken ct = CancellationToken.None; ArrayOf dataValues = [new DataValue("InvalidDataType")]; ArrayOf diagnosticInfos = []; sut.Channel .Setup(c => c.SendRequestAsync( It.IsAny(), It.IsAny())) .Returns(new ValueTask(new ReadResponse { Results = dataValues, DiagnosticInfos = diagnosticInfos })) .Verifiable(Times.Exactly(2)); Assert.ThrowsAsync( async () => await sut.FetchOperationLimitsAsync(ct).ConfigureAwait(false)); sut.Channel.Verify(); } [Test] public void FetchOperationLimitsAsyncShouldHandleTimeout() { using var sut = SessionMock.Create(); using var cts = new CancellationTokenSource(100); CancellationToken ct = cts.Token; // Set a short timeout sut.Channel .Setup(c => c.SendRequestAsync( It.IsAny(), It.IsAny())) .ThrowsAsync(new TaskCanceledException()) .Verifiable(Times.Once); Assert.ThrowsAsync( async () => await sut.FetchOperationLimitsAsync(ct).ConfigureAwait(false)); sut.Channel.Verify(); } [Test] public async Task FetchOperationLimitsAsyncShouldApplyClientLimitsWhenSmallerAsync() { ITelemetryContext telemetry = NUnitTelemetryContext.Create(); var channel = new Mock(); channel .SetupGet(s => s.MessageContext) .Returns(ServiceMessageContext.Create(telemetry)); channel .SetupGet(s => s.SupportedFeatures) .Returns(TransportChannelFeatures.Reconnect); // Configure client with smaller limits than server var configuration = new ApplicationConfiguration(telemetry) { ClientConfiguration = new ClientConfiguration { OperationLimits = new OperationLimits { MaxNodesPerRead = 500, MaxNodesPerWrite = 2000, MaxNodesPerBrowse = 4000 } } }; var endpoint = new EndpointDescription { SecurityMode = MessageSecurityMode.None, SecurityPolicyUri = SecurityPolicies.None, EndpointUrl = "opc.tcp://localhost:4840" }; using var sut = new SessionMock( channel, configuration, new ConfiguredEndpoint(null!, endpoint)); CancellationToken ct = CancellationToken.None; // Server returns larger values ArrayOf dataValues = [ new DataValue(new Variant(1000u)), // MaxNodesPerHistoryReadData - no client limit new DataValue(new Variant(1000u)), // MaxNodesPerHistoryReadEvents - no client limit new DataValue(new Variant(5000u)), // MaxNodesPerWrite - client has 2000 new DataValue(new Variant(1000u)), // MaxNodesPerRead - client has 500 new DataValue(new Variant(1000u)), // MaxNodesPerHistoryUpdateData new DataValue(new Variant(1000u)), // MaxNodesPerHistoryUpdateEvents new DataValue(new Variant(1000u)), // MaxNodesPerMethodCall new DataValue(new Variant(10000u)), // MaxNodesPerBrowse - client has 4000 new DataValue(new Variant(1000u)), // MaxNodesPerRegisterNodes new DataValue(new Variant(1000u)), // MaxNodesPerNodeManagement new DataValue(new Variant(1000u)), // MaxMonitoredItemsPerCall new DataValue(new Variant(1000u)), // MaxNodesPerTranslateBrowsePathsToNodeIds new DataValue(new Variant((ushort)100)), new DataValue(new Variant((ushort)100)), new DataValue(new Variant((ushort)100)), new DataValue(new Variant(1000u)), new DataValue(new Variant(100000u)), // MaxArrayLength - large so it does not cap the limits under test new DataValue(new Variant(1000u)), new DataValue(new Variant(100.0)), new DataValue(new Variant(1000u)), new DataValue(new Variant(1000u)), new DataValue(new Variant(1000u)), new DataValue(new Variant(1000u)), new DataValue(new Variant(1000u)), new DataValue(new Variant(1000u)), new DataValue(new Variant(1000u)), new DataValue(new Variant(1000u)) ]; channel .SetupSequence(c => c.SendRequestAsync( It.IsAny(), It.IsAny())) .Returns(new ValueTask(new ReadResponse { Results = [new DataValue(new Variant(1000u))], DiagnosticInfos = [] })) .Returns(new ValueTask(new ReadResponse { Results = dataValues, DiagnosticInfos = [] })); // Act await sut.FetchOperationLimitsAsync(ct).ConfigureAwait(false); // Assert - should use smaller of client and server values Assert.That(sut.OperationLimits.MaxNodesPerRead, Is.EqualTo(500)); // min(500, 1000) Assert.That(sut.OperationLimits.MaxNodesPerWrite, Is.EqualTo(2000)); // min(2000, 5000) Assert.That(sut.OperationLimits.MaxNodesPerBrowse, Is.EqualTo(4000)); // min(4000, 10000) Assert.That(sut.OperationLimits.MaxNodesPerHistoryReadData, Is.EqualTo(1000)); // 0 -> server Assert.That(sut.OperationLimits.MaxNodesPerHistoryReadEvents, Is.EqualTo(1000)); // 0 -> server channel.Verify(); } [Test] public async Task FetchOperationLimitsAsyncShouldApplyServerLimitsWhenSmallerAsync() { ITelemetryContext telemetry = NUnitTelemetryContext.Create(); var channel = new Mock(); channel .SetupGet(s => s.MessageContext) .Returns(ServiceMessageContext.Create(telemetry)); channel .SetupGet(s => s.SupportedFeatures) .Returns(TransportChannelFeatures.Reconnect); // Configure client with larger limits than server var configuration = new ApplicationConfiguration(telemetry) { ClientConfiguration = new ClientConfiguration { OperationLimits = new OperationLimits { MaxNodesPerRead = 2000, MaxNodesPerWrite = 10000, MaxNodesPerBrowse = 8000 } } }; var endpoint = new EndpointDescription { SecurityMode = MessageSecurityMode.None, SecurityPolicyUri = SecurityPolicies.None, EndpointUrl = "opc.tcp://localhost:4840" }; using var sut = new SessionMock( channel, configuration, new ConfiguredEndpoint(null!, endpoint)); CancellationToken ct = CancellationToken.None; // Server returns smaller values ArrayOf dataValues = [ new DataValue(new Variant(1000u)), // MaxNodesPerHistoryReadData new DataValue(new Variant(1000u)), // MaxNodesPerHistoryReadEvents new DataValue(new Variant(5000u)), // MaxNodesPerWrite - server has 5000, client has 10000 new DataValue(new Variant(1000u)), // MaxNodesPerRead - server has 1000, client has 2000 new DataValue(new Variant(1000u)), // MaxNodesPerHistoryUpdateData new DataValue(new Variant(1000u)), // MaxNodesPerHistoryUpdateEvents new DataValue(new Variant(1000u)), // MaxNodesPerMethodCall new DataValue(new Variant(5000u)), // MaxNodesPerBrowse - server has 5000, client has 8000 new DataValue(new Variant(1000u)), // MaxNodesPerRegisterNodes new DataValue(new Variant(1000u)), // MaxNodesPerNodeManagement new DataValue(new Variant(1000u)), // MaxMonitoredItemsPerCall new DataValue(new Variant(1000u)), // MaxNodesPerTranslateBrowsePathsToNodeIds new DataValue(new Variant((ushort)100)), new DataValue(new Variant((ushort)100)), new DataValue(new Variant((ushort)100)), new DataValue(new Variant(1000u)), new DataValue(new Variant(100000u)), // MaxArrayLength - large so it does not cap the limits under test new DataValue(new Variant(1000u)), new DataValue(new Variant(100.0)), new DataValue(new Variant(1000u)), new DataValue(new Variant(1000u)), new DataValue(new Variant(1000u)), new DataValue(new Variant(1000u)), new DataValue(new Variant(1000u)), new DataValue(new Variant(1000u)), new DataValue(new Variant(1000u)), new DataValue(new Variant(1000u)) ]; channel .SetupSequence(c => c.SendRequestAsync( It.IsAny(), It.IsAny())) .Returns(new ValueTask(new ReadResponse { Results = [new DataValue(new Variant(1000u))], DiagnosticInfos = [] })) .Returns(new ValueTask(new ReadResponse { Results = dataValues, DiagnosticInfos = [] })); // Act await sut.FetchOperationLimitsAsync(ct).ConfigureAwait(false); // Assert - should use smaller of client and server values Assert.That(sut.OperationLimits.MaxNodesPerRead, Is.EqualTo(1000)); // min(2000, 1000) Assert.That(sut.OperationLimits.MaxNodesPerWrite, Is.EqualTo(5000)); // min(10000, 5000) Assert.That(sut.OperationLimits.MaxNodesPerBrowse, Is.EqualTo(5000)); // min(8000, 5000) channel.Verify(); } [Test] public async Task FetchOperationLimitsAsyncShouldCapLimitsToServerMaxArrayLengthAsync() { using var sut = SessionMock.Create(); CancellationToken ct = CancellationToken.None; // The server advertises a small MaxArrayLength (index 16) that is lower than // most operation limits. Limits above it are reduced to it; a limit already at // or below it is left unchanged. ArrayOf dataValues = [ new DataValue(new Variant(2000u)), // MaxNodesPerHistoryReadData -> 500 new DataValue(new Variant(2000u)), // MaxNodesPerHistoryReadEvents -> 500 new DataValue(new Variant(2000u)), // MaxNodesPerWrite -> 500 new DataValue(new Variant(2000u)), // MaxNodesPerRead -> 500 new DataValue(new Variant(2000u)), // MaxNodesPerHistoryUpdateData -> 500 new DataValue(new Variant(2000u)), // MaxNodesPerHistoryUpdateEvents -> 500 new DataValue(new Variant(2000u)), // MaxNodesPerMethodCall -> 500 new DataValue(new Variant(2000u)), // MaxNodesPerBrowse -> 500 new DataValue(new Variant(2000u)), // MaxNodesPerRegisterNodes -> 500 new DataValue(new Variant(2000u)), // MaxNodesPerNodeManagement -> 500 new DataValue(new Variant(2000u)), // MaxMonitoredItemsPerCall -> 500 new DataValue(new Variant(200u)), // MaxNodesPerTranslateBrowsePathsToNodeIds -> 200 (unchanged) new DataValue(new Variant((ushort)100)), new DataValue(new Variant((ushort)100)), new DataValue(new Variant((ushort)100)), new DataValue(new Variant(1000u)), new DataValue(new Variant(500u)), // MaxArrayLength new DataValue(new Variant(1000u)), new DataValue(new Variant(100.0)), new DataValue(new Variant(1000u)), new DataValue(new Variant(1000u)), new DataValue(new Variant(1000u)), new DataValue(new Variant(1000u)), new DataValue(new Variant(1000u)), new DataValue(new Variant(1000u)), new DataValue(new Variant(1000u)), new DataValue(new Variant(1000u)) ]; sut.Channel .SetupSequence(c => c.SendRequestAsync( It.IsAny(), It.IsAny())) .Returns(new ValueTask(new ReadResponse { Results = [new DataValue(new Variant(2000u))], DiagnosticInfos = [] })) .Returns(new ValueTask(new ReadResponse { Results = dataValues, DiagnosticInfos = [] })); // Act await sut.FetchOperationLimitsAsync(ct).ConfigureAwait(false); // Assert - operation limits above MaxArrayLength are capped to it. Assert.That(sut.ServerCapabilities.MaxArrayLength, Is.EqualTo(500)); Assert.That(sut.OperationLimits.MaxNodesPerRead, Is.EqualTo(500)); Assert.That(sut.OperationLimits.MaxNodesPerWrite, Is.EqualTo(500)); Assert.That(sut.OperationLimits.MaxNodesPerBrowse, Is.EqualTo(500)); Assert.That(sut.OperationLimits.MaxNodesPerHistoryReadData, Is.EqualTo(500)); Assert.That(sut.OperationLimits.MaxNodesPerNodeManagement, Is.EqualTo(500)); Assert.That(sut.OperationLimits.MaxMonitoredItemsPerCall, Is.EqualTo(500)); Assert.That(sut.OperationLimits.MaxNodesPerTranslateBrowsePathsToNodeIds, Is.EqualTo(200)); } [Test] public async Task FetchOperationLimitsAsyncShouldCapUnlimitedLimitsToMaxArrayLengthAsync() { using var sut = SessionMock.Create(); CancellationToken ct = CancellationToken.None; // All operation limits are 0 (unlimited); the non-zero MaxArrayLength bounds them. ArrayOf dataValues = CreateOperationLimitsRead(operationLimit: 0u, maxArrayLength: 750u); sut.Channel .SetupSequence(c => c.SendRequestAsync( It.IsAny(), It.IsAny())) .Returns(new ValueTask(new ReadResponse { Results = [new DataValue(new Variant(0u))], DiagnosticInfos = [] })) .Returns(new ValueTask(new ReadResponse { Results = dataValues, DiagnosticInfos = [] })); // Act await sut.FetchOperationLimitsAsync(ct).ConfigureAwait(false); // Assert - unlimited (0) limits are bound to MaxArrayLength. Assert.That(sut.ServerCapabilities.MaxArrayLength, Is.EqualTo(750)); Assert.That(sut.OperationLimits.MaxNodesPerRead, Is.EqualTo(750)); Assert.That(sut.OperationLimits.MaxNodesPerBrowse, Is.EqualTo(750)); Assert.That(sut.OperationLimits.MaxMonitoredItemsPerCall, Is.EqualTo(750)); Assert.That(sut.OperationLimits.MaxNodesPerNodeManagement, Is.EqualTo(750)); Assert.That(sut.OperationLimits.MaxNodesPerTranslateBrowsePathsToNodeIds, Is.EqualTo(750)); } [Test] public async Task FetchOperationLimitsAsyncShouldCapToClientArrayQuotaWhenServerUnlimitedAsync() { ITelemetryContext telemetry = NUnitTelemetryContext.Create(); var channel = new Mock(); channel .SetupGet(s => s.MessageContext) .Returns(ServiceMessageContext.Create(telemetry)); channel .SetupGet(s => s.SupportedFeatures) .Returns(TransportChannelFeatures.Reconnect); // The client constrains the array length via its transport quotas while the // server reports MaxArrayLength 0 (unlimited). var configuration = new ApplicationConfiguration(telemetry) { ClientConfiguration = new ClientConfiguration(), TransportQuotas = new TransportQuotas { MaxArrayLength = 600 } }; var endpoint = new EndpointDescription { SecurityMode = MessageSecurityMode.None, SecurityPolicyUri = SecurityPolicies.None, EndpointUrl = "opc.tcp://localhost:4840" }; using var sut = new SessionMock( channel, configuration, new ConfiguredEndpoint(null!, endpoint)); CancellationToken ct = CancellationToken.None; ArrayOf dataValues = CreateOperationLimitsRead(operationLimit: 1000u, maxArrayLength: 0u); channel .SetupSequence(c => c.SendRequestAsync( It.IsAny(), It.IsAny())) .Returns(new ValueTask(new ReadResponse { Results = [new DataValue(new Variant(1000u))], DiagnosticInfos = [] })) .Returns(new ValueTask(new ReadResponse { Results = dataValues, DiagnosticInfos = [] })); // Act await sut.FetchOperationLimitsAsync(ct).ConfigureAwait(false); // Assert - the client array quota becomes the effective cap. Assert.That(sut.ServerCapabilities.MaxArrayLength, Is.EqualTo(600)); Assert.That(sut.OperationLimits.MaxNodesPerRead, Is.EqualTo(600)); Assert.That(sut.OperationLimits.MaxNodesPerBrowse, Is.EqualTo(600)); Assert.That(sut.OperationLimits.MaxMonitoredItemsPerCall, Is.EqualTo(600)); } [Test] public async Task FetchOperationLimitsAsyncShouldNotCapWhenArrayLengthUnlimitedAsync() { using var sut = SessionMock.Create(); CancellationToken ct = CancellationToken.None; // Both the server MaxArrayLength and the client array quota are 0 (unlimited): // operation limits are left untouched. ArrayOf dataValues = CreateOperationLimitsRead(operationLimit: 1000u, maxArrayLength: 0u); sut.Channel .SetupSequence(c => c.SendRequestAsync( It.IsAny(), It.IsAny())) .Returns(new ValueTask(new ReadResponse { Results = [new DataValue(new Variant(1000u))], DiagnosticInfos = [] })) .Returns(new ValueTask(new ReadResponse { Results = dataValues, DiagnosticInfos = [] })); // Act await sut.FetchOperationLimitsAsync(ct).ConfigureAwait(false); // Assert - no array length constraint, so limits are unchanged. Assert.That(sut.ServerCapabilities.MaxArrayLength, Is.Zero); Assert.That(sut.OperationLimits.MaxNodesPerRead, Is.EqualTo(1000)); Assert.That(sut.OperationLimits.MaxNodesPerBrowse, Is.EqualTo(1000)); Assert.That(sut.OperationLimits.MaxMonitoredItemsPerCall, Is.EqualTo(1000)); Assert.That(sut.OperationLimits.MaxNodesPerTranslateBrowsePathsToNodeIds, Is.EqualTo(1000)); } [Test] public async Task FetchNamespaceTablesAsyncShouldFetchAndUpdateTablesAsync() { using var sut = SessionMock.Create(); CancellationToken ct = CancellationToken.None; var namespaceArray = new DataValue(new Variant([Namespaces.OpcUa, "http://namespace2"])); var serverArray = new DataValue(new Variant(["http://server1", "http://server2"])); sut.Channel .Setup(c => c.SendRequestAsync( It.IsAny(), It.IsAny())) .Returns(new ValueTask(new ReadResponse { Results = [namespaceArray, serverArray], DiagnosticInfos = [] })) .Verifiable(Times.Once); // Act await sut.FetchNamespaceTablesAsync(ct).ConfigureAwait(false); // Assert Assert.That(sut.NamespaceUris.ToArray(), Is.EquivalentTo([Namespaces.OpcUa, "http://namespace2"])); Assert.That(sut.ServerUris.ToArray(), Is.EquivalentTo(["http://server1", "http://server2"])); sut.Channel.Verify(); } [Test] public async Task FetchNamespaceTablesAsyncShouldFetchAndUpdateTablesAndLogDifferences1Async() { // Arrange using var sut = SessionMock.Create(); CancellationToken ct = CancellationToken.None; var namespaceArray1 = new DataValue( Variant.From([Namespaces.OpcUa, "http://namespace2", "http://namespace3"])); var serverArray1 = new DataValue(Variant.From(["http://server1", "http://server2"])); var namespaceArray2 = new DataValue( Variant.From([Namespaces.OpcUa, "http://namespace3", "http://namespace2"])); var serverArray2 = new DataValue(Variant.From(["http://server1", "http://server2", "http://server3"])); sut.Channel .SetupSequence(c => c.SendRequestAsync( It.IsAny(), It.IsAny())) .Returns(new ValueTask(new ReadResponse { Results = [namespaceArray1, serverArray1], DiagnosticInfos = [] })) .Returns(new ValueTask(new ReadResponse { Results = [namespaceArray2, serverArray2], DiagnosticInfos = [] })); // Act await sut.FetchNamespaceTablesAsync(ct).ConfigureAwait(false); // Assert Assert.That(sut.NamespaceUris.ToArray(), Is.EquivalentTo([Namespaces.OpcUa, "http://namespace2", "http://namespace3"])); Assert.That(sut.ServerUris.ToArray(), Is.EquivalentTo(["http://server1", "http://server2"])); // Act await sut.FetchNamespaceTablesAsync(ct).ConfigureAwait(false); // Assert Assert.That(sut.NamespaceUris.ToArray(), Is.EquivalentTo([Namespaces.OpcUa, "http://namespace3", "http://namespace2"])); Assert.That(sut.ServerUris.ToArray(), Is.EquivalentTo(["http://server1", "http://server2", "http://server3"])); sut.Channel.Verify(); } [Test] public async Task FetchNamespaceTablesAsyncShouldFetchAndUpdateTablesAndLogDifferences2Async() { // Arrange using var sut = SessionMock.Create(); CancellationToken ct = CancellationToken.None; var namespaceArray1 = new DataValue( new Variant([Namespaces.OpcUa, "http://namespace2", "http://namespace3"])); var serverArray1 = new DataValue(new Variant(["http://server1", "http://server2"])); var namespaceArray2 = new DataValue(new Variant([Namespaces.OpcUa, "http://namespace3"])); var serverArray2 = new DataValue(new Variant(["http://server1"])); sut.Channel .SetupSequence(c => c.SendRequestAsync( It.IsAny(), It.IsAny())) .Returns(new ValueTask(new ReadResponse { Results = [namespaceArray1, serverArray1], DiagnosticInfos = [] })) .Returns(new ValueTask(new ReadResponse { Results = [namespaceArray2, serverArray2], DiagnosticInfos = [] })); // Act await sut.FetchNamespaceTablesAsync(ct).ConfigureAwait(false); // Assert Assert.That(sut.NamespaceUris.ToArray(), Is.EquivalentTo([Namespaces.OpcUa, "http://namespace2", "http://namespace3"])); Assert.That(sut.ServerUris.ToArray(), Is.EquivalentTo(["http://server1", "http://server2"])); // Act await sut.FetchNamespaceTablesAsync(ct).ConfigureAwait(false); // Assert Assert.That(sut.NamespaceUris.ToArray(), Is.EquivalentTo([Namespaces.OpcUa, "http://namespace3"])); Assert.That(sut.ServerUris.ToArray(), Is.EquivalentTo(["http://server1"])); sut.Channel.Verify(); } [Test] public async Task FetchNamespaceTablesAsyncShouldHandlePartialSuccessAsync() { // Arrange using var sut = SessionMock.Create(); CancellationToken ct = CancellationToken.None; var namespaceArray = new DataValue(new Variant([Namespaces.OpcUa, "http://namespace2"])); var serverArray = DataValue.FromStatusCode(StatusCodes.BadUnexpectedError); sut.Channel .Setup(c => c.SendRequestAsync( It.IsAny(), It.IsAny())) .Returns(new ValueTask(new ReadResponse { Results = [namespaceArray, serverArray], DiagnosticInfos = [] })) .Verifiable(Times.Once); // Act await sut.FetchNamespaceTablesAsync(ct).ConfigureAwait(false); // Assert Assert.That(sut.NamespaceUris.ToArray(), Is.EquivalentTo([Namespaces.OpcUa, "http://namespace2"])); Assert.That(sut.ServerUris.ToArray(), Is.Empty); sut.Channel.Verify(); } [Test] public void FetchNamespaceTablesAsyncShouldThrowInCaseOfBadResponse() { // Arrange using var sut = SessionMock.Create(); CancellationToken ct = CancellationToken.None; var namespaceArray = new DataValue(new Variant([Namespaces.OpcUa, "http://namespace2"])); var serverArray = new DataValue(new Variant(["http://server1", "http://server2"])); sut.Channel .Setup(c => c.SendRequestAsync( It.IsAny(), It.IsAny())) .Returns(new ValueTask(new ReadResponse { ResponseHeader = new ResponseHeader { ServiceResult = StatusCodes.BadUnexpectedError }, Results = [namespaceArray, serverArray], DiagnosticInfos = [] })) .Verifiable(Times.Once); ServiceResultException sre = Assert.ThrowsAsync( async () => await sut.FetchNamespaceTablesAsync(ct).ConfigureAwait(false)); Assert.That(sre.StatusCode, Is.EqualTo(StatusCodes.BadUnexpectedError)); sut.Channel.Verify(); } [Test] public void FetchNamespaceTablesAsyncShouldThrowWhenNamespaceArrayCouldNotBeRetrieved() { // Arrange using var sut = SessionMock.Create(); CancellationToken ct = CancellationToken.None; var namespaceArray = DataValue.FromStatusCode(StatusCodes.BadUnexpectedError); var serverArray = DataValue.FromStatusCode(StatusCodes.BadUnexpectedError); sut.Channel .Setup(c => c.SendRequestAsync( It.IsAny(), It.IsAny())) .Returns(new ValueTask(new ReadResponse { Results = [namespaceArray, serverArray], DiagnosticInfos = [] })) .Verifiable(Times.Once); ServiceResultException sre = Assert.ThrowsAsync( async () => await sut.FetchNamespaceTablesAsync(ct).ConfigureAwait(false)); Assert.That(sre.StatusCode, Is.EqualTo(StatusCodes.BadUnexpectedError)); sut.Channel.Verify(); } [Test] public void FetchNamespaceTablesAsyncShoulThrowWhenEmptyResponse() { // Arrange using var sut = SessionMock.Create(); CancellationToken ct = CancellationToken.None; ArrayOf dataValues = []; ArrayOf diagnosticInfos = []; sut.Channel .Setup(c => c.SendRequestAsync( It.IsAny(), It.IsAny())) .Returns(new ValueTask(new ReadResponse { Results = dataValues, DiagnosticInfos = diagnosticInfos })) .Verifiable(Times.Once); ServiceResultException sre = Assert.ThrowsAsync( async () => await sut.FetchNamespaceTablesAsync(ct).ConfigureAwait(false)); Assert.That(sre.StatusCode, Is.EqualTo(StatusCodes.BadUnexpectedError)); sut.Channel.Verify(); } [Test] public void FetchNamespaceTablesAsyncShouldThrowWhenInvalidDataTypesForNamespaceTable() { // Arrange using var sut = SessionMock.Create(); CancellationToken ct = CancellationToken.None; var namespaceArray = new DataValue(new Variant(12345)); var serverArray = new DataValue(new Variant(67890)); sut.Channel .Setup(c => c.SendRequestAsync( It.IsAny(), It.IsAny())) .Returns(new ValueTask(new ReadResponse { Results = [namespaceArray, serverArray], DiagnosticInfos = [] })) .Verifiable(Times.Once); ServiceResultException sre = Assert.ThrowsAsync( async () => await sut.FetchNamespaceTablesAsync(ct).ConfigureAwait(false)); Assert.That(sre.StatusCode, Is.EqualTo(StatusCodes.BadTypeMismatch)); sut.Channel.Verify(); } [Test] public void FetchNamespaceTablesAsyncShouldThrowWhenInvalidDataTypeForServerUrls() { // Arrange using var sut = SessionMock.Create(); CancellationToken ct = CancellationToken.None; var namespaceArray = new DataValue(new Variant([Namespaces.OpcUa, "http://namespace2"])); var serverArray = new DataValue(new Variant(67890)); sut.Channel .Setup(c => c.SendRequestAsync( It.IsAny(), It.IsAny())) .Returns(new ValueTask(new ReadResponse { Results = [namespaceArray, serverArray], DiagnosticInfos = [] })) .Verifiable(Times.Once); ServiceResultException sre = Assert.ThrowsAsync( async () => await sut.FetchNamespaceTablesAsync(ct).ConfigureAwait(false)); Assert.That(sre.StatusCode, Is.EqualTo(StatusCodes.BadTypeMismatch)); sut.Channel.Verify(); } [Test] public async Task CloseAsyncShouldCloseSessionAndChannelSuccessfullyAsync() { // Arrange using var sut = SessionMock.Create(); sut.SetConnectedAndResponsive(); CancellationToken ct = CancellationToken.None; sut.Channel .Setup(c => c.SendRequestAsync( It.IsAny(), It.IsAny())) .Returns(new ValueTask(new CloseSessionResponse { ResponseHeader = new ResponseHeader { ServiceResult = StatusCodes.Good } })) .Verifiable(Times.Once); sut.Channel .Setup(c => c.CloseAsync(It.IsAny())) .Returns(new ValueTask()) .Verifiable(Times.Once); // Act StatusCode result = await sut.CloseAsync(ct).ConfigureAwait(false); // Assert Assert.That(result, Is.EqualTo(StatusCodes.Good)); sut.Channel.Verify(); } [Test] public async Task CloseAsyncShouldHandleAlreadyClosedSessionAsync() { // Arrange using var sut = SessionMock.Create(); CancellationToken ct = CancellationToken.None; sut.Dispose(); // Act StatusCode result = await sut.CloseAsync(ct).ConfigureAwait(false); // Assert Assert.That(result, Is.EqualTo(StatusCodes.Good)); } [Test] public async Task CloseAsyncShouldHandleErrorsDuringCloseAsync() { // Arrange using var sut = SessionMock.Create(); sut.SetConnectedAndResponsive(); CancellationToken ct = CancellationToken.None; sut.Channel .Setup(c => c.SendRequestAsync( It.IsAny(), It.IsAny())) .ThrowsAsync(new ServiceResultException(StatusCodes.BadUnexpectedError)) .Verifiable(Times.Once); sut.Channel .Setup(c => c.CloseAsync(It.IsAny())) .Returns(new ValueTask()) .Verifiable(Times.Once); // Act StatusCode result = await sut.CloseAsync(ct).ConfigureAwait(false); // Assert Assert.That(result, Is.EqualTo(StatusCodes.BadUnexpectedError)); sut.Channel.Verify(); } /// /// A request with a null authenticationToken is a session-less /// invocation (OPC 10000-4 ยง6.3.1). A closed Session must not send /// one through the channel it keeps open. /// [Test] public async Task RequestAfterCloseFailsLocallyWithBadSessionIdInvalidAsync() { using var sut = SessionMock.Create(); sut.SetConnectedAndResponsive(); CancellationToken ct = CancellationToken.None; SetupCloseSession(sut); List sent = SetupRead(sut); StatusCode closed = await sut.CloseAsync(closeChannel: false, ct).ConfigureAwait(false); Assert.That(closed, Is.EqualTo(StatusCodes.Good)); ServiceResultException? sre = Assert.ThrowsAsync( async () => await ReadServerStateAsync(sut, ct).ConfigureAwait(false)); Assert.That(sre!.StatusCode, Is.EqualTo(StatusCodes.BadSessionIdInvalid)); Assert.That(sent, Is.Empty, "The request must not reach the channel."); } [Test] public async Task ActivateSessionIsStillSentAfterCloseAsync() { using var sut = SessionMock.Create(); sut.SetConnectedAndResponsive(); CancellationToken ct = CancellationToken.None; SetupCloseSession(sut); sut.Channel .Setup(c => c.SendRequestAsync( It.IsAny(), It.IsAny())) .ReturnsAsync(new ActivateSessionResponse()) .Verifiable(Times.Once); await sut.CloseAsync(closeChannel: false, ct).ConfigureAwait(false); ActivateSessionResponse response = await sut.ActivateSessionAsync( null, new SignatureData(), default, default, new ExtensionObject(), new SignatureData(), ct).ConfigureAwait(false); Assert.That(response, Is.Not.Null); sut.Channel.Verify(); } [Test] public async Task RequestAfterANewSessionIsCreatedCarriesItsTokenAsync() { using var sut = SessionMock.Create(); sut.SetConnectedAndResponsive(); CancellationToken ct = CancellationToken.None; SetupCloseSession(sut); List sent = SetupRead(sut); await sut.CloseAsync(closeChannel: false, ct).ConfigureAwait(false); sut.SessionCreated(NodeId.Parse("s=second"), NodeId.Parse("s=auth2")); await ReadServerStateAsync(sut, ct).ConfigureAwait(false); Assert.That(sent, Has.Count.EqualTo(1)); Assert.That(sent[0].RequestHeader.AuthenticationToken, Is.EqualTo(NodeId.Parse("s=auth2"))); } [Test] public async Task RequestBeforeASessionIsCreatedIsNotRefusedAsync() { using var sut = SessionMock.Create(); CancellationToken ct = CancellationToken.None; List sent = SetupRead(sut); await ReadServerStateAsync(sut, ct).ConfigureAwait(false); Assert.That(sent, Has.Count.EqualTo(1)); Assert.That(sent[0].RequestHeader.AuthenticationToken.IsNull, Is.True); } [Test] public async Task DisposeAsyncSendsCloseSessionAsync() { // Arrange var sut = SessionMock.Create(); sut.SetConnectedAndResponsive(); CloseSessionRequest? closeRequest = null; sut.Channel .Setup(c => c.SendRequestAsync( It.IsAny(), It.IsAny())) .Callback((IServiceRequest request, CancellationToken _) => closeRequest = request as CloseSessionRequest) .Returns(new ValueTask(new CloseSessionResponse { ResponseHeader = new ResponseHeader { ServiceResult = StatusCodes.Good } })) .Verifiable(Times.Once); // Act await sut.DisposeAsync().ConfigureAwait(false); // Assert sut.Channel.Verify(); Assert.That(closeRequest, Is.Not.Null); Assert.That(closeRequest!.DeleteSubscriptions, Is.True); Assert.That(closeRequest.RequestHeader.AuthenticationToken, Is.EqualTo(NodeId.Parse("s=auth"))); } [Test] public async Task CloseAsyncShouldCloseSessionWithoutDeletingSubscriptionsAsync() { // Arrange using var sut = SessionMock.Create(); sut.SetConnectedAndResponsive(); sut.DeleteSubscriptionsOnClose = false; CancellationToken ct = CancellationToken.None; sut.Channel .Setup(c => c.SendRequestAsync( It.IsAny(), It.IsAny())) .Returns(new ValueTask(new CloseSessionResponse { ResponseHeader = new ResponseHeader { ServiceResult = StatusCodes.Good } })) .Verifiable(Times.Once); sut.Channel .Setup(c => c.CloseAsync(It.IsAny())) .Returns(new ValueTask()) .Verifiable(Times.Once); // Act StatusCode result = await sut.CloseAsync(ct).ConfigureAwait(false); // Assert Assert.That(result, Is.EqualTo(StatusCodes.Good)); sut.Channel.Verify(); } [Test] public async Task CloseAsyncShouldHandleChannelErrorsAsync() { // Arrange using var sut = SessionMock.Create(); sut.SetConnectedAndResponsive(); CancellationToken ct = CancellationToken.None; sut.Channel .Setup(c => c.SendRequestAsync( It.IsAny(), It.IsAny())) .Returns(new ValueTask(new CloseSessionResponse { ResponseHeader = new ResponseHeader { ServiceResult = StatusCodes.Good } })) .Verifiable(Times.Once); sut.Channel .Setup(c => c.CloseAsync(It.IsAny())) .ThrowsAsync(new ServiceResultException(StatusCodes.BadUnexpectedError)) .Verifiable(Times.Once); StatusCode result = await sut.CloseAsync(ct).ConfigureAwait(false); Assert.That(result, Is.EqualTo(StatusCodes.Good)); // Eats the error from channel close sut.Channel.Verify(); } [Test] public async Task CloseAsyncShouldCloseSessionSuccessfullyAsync() { // Arrange using var sut = SessionMock.Create(); sut.SetConnectedAndResponsive(); CancellationToken ct = CancellationToken.None; sut.Channel .Setup(c => c.SendRequestAsync( It.IsAny(), It.IsAny())) .Returns(new ValueTask(new CloseSessionResponse { ResponseHeader = new ResponseHeader { ServiceResult = StatusCodes.Good } })) .Verifiable(Times.Once); sut.Channel .Setup(c => c.CloseAsync(It.IsAny())) .Returns(new ValueTask()) .Verifiable(Times.Once); // Act StatusCode result = await sut.CloseAsync(ct).ConfigureAwait(false); // Assert Assert.That(result, Is.EqualTo(StatusCodes.Good)); sut.Channel.Verify(); } [Test] public async Task CloseAsyncShouldHandleErrorsDuringCloseSessionAsync() { // Arrange using var sut = SessionMock.Create(); sut.SetConnectedAndResponsive(); CancellationToken ct = CancellationToken.None; sut.Channel .Setup(c => c.SendRequestAsync( It.IsAny(), It.IsAny())) .ThrowsAsync(new ServiceResultException(StatusCodes.BadUnexpectedError)) .Verifiable(Times.Once); sut.Channel .Setup(c => c.CloseAsync(It.IsAny())) .Returns(new ValueTask()) .Verifiable(Times.Once); // Act StatusCode result = await sut.CloseAsync(ct).ConfigureAwait(false); // Assert Assert.That(result, Is.EqualTo(StatusCodes.BadUnexpectedError)); sut.Channel.Verify(); } [TestCase(false, false)] [TestCase(false, true)] [TestCase(true, false)] public async Task ShutdownCancellationOfKeepAliveStillClosesServerSessionAsync( bool disposeAsync, bool serverRejectsClose) { StatusCode closeStatus = serverRejectsClose ? StatusCodes.BadSessionIdInvalid : StatusCodes.Good; EndpointDescription endpoint = CreateSessionEndpointDescription("opc.tcp://localhost:4840"); await using var session = SessionMock.Create(endpoint, [endpoint]); ConfigureSuccessfulOpenResponses( session.Channel, [endpoint], ByteString.From([1, 2, 3, 4]), NodeId.Parse("s=keep-alive-auth")); using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(10)); var readStarted = new TaskCompletionSource( TaskCreationOptions.RunContinuationsAsynchronously); var readCompletion = new TaskCompletionSource( TaskCreationOptions.RunContinuationsAsynchronously); int keepAliveErrors = 0; session.KeepAlive += (_, args) => { if (ServiceResult.IsBad(args.Status)) { Interlocked.Increment(ref keepAliveErrors); } }; session.Channel .Setup(channel => channel.SendRequestAsync( It.Is(request => request.NodesToRead.Count == 1 && request.NodesToRead[0].NodeId == VariableIds.Server_ServerStatus_State), It.IsAny())) .Returns((ReadRequest _, CancellationToken ct) => new ValueTask(ReadUntilCanceledAsync(ct))); session.Channel .Setup(channel => channel.SendRequestAsync( It.IsAny(), It.IsAny())) .Returns(new ValueTask(new CloseSessionResponse { ResponseHeader = new ResponseHeader { ServiceResult = closeStatus } })) .Verifiable(Times.Once); session.Channel .Setup(channel => channel.CloseAsync(It.IsAny())) .Returns(new ValueTask()) .Verifiable(Times.Once); await session.OpenAsync("test", new UserIdentity(), timeout.Token).ConfigureAwait(false); CancellationToken readCancellation = await readStarted.Task .WaitAsync(timeout.Token).ConfigureAwait(false); Assert.That(session.KeepAliveStopped, Is.False); if (disposeAsync) { await session.DisposeAsync().AsTask().WaitAsync(timeout.Token).ConfigureAwait(false); } else { StatusCode result = await session.CloseAsync(5000, true, timeout.Token).ConfigureAwait(false); Assert.That(result, Is.EqualTo(closeStatus)); } Assert.That(readCancellation.IsCancellationRequested, Is.True); Assert.That(Volatile.Read(ref keepAliveErrors), Is.Zero); Assert.That(session.KeepAliveStopped, Is.False); session.Channel.Verify(); async Task ReadUntilCanceledAsync(CancellationToken ct) { using CancellationTokenRegistration registration = ct.Register(() => readCompletion.TrySetException( new ServiceResultException(StatusCodes.BadRequestCancelledByClient))); readStarted.TrySetResult(ct); return await readCompletion.Task.ConfigureAwait(false); } } [Test] public async Task KeepAliveServiceFailureWithoutCancellationStillStopsLivenessAsync() { EndpointDescription endpoint = CreateSessionEndpointDescription("opc.tcp://localhost:4840"); await using var session = SessionMock.Create(endpoint, [endpoint]); ConfigureSuccessfulOpenResponses( session.Channel, [endpoint], ByteString.From([1, 2, 3, 4]), NodeId.Parse("s=keep-alive-auth")); using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(10)); var reportedError = new TaskCompletionSource( TaskCreationOptions.RunContinuationsAsynchronously); session.KeepAlive += (_, args) => { if (args.Status is { } status && ServiceResult.IsBad(status)) { reportedError.TrySetResult(status.StatusCode); } }; session.Channel .Setup(channel => channel.SendRequestAsync( It.Is(request => request.NodesToRead.Count == 1 && request.NodesToRead[0].NodeId == VariableIds.Server_ServerStatus_State), It.IsAny())) .ThrowsAsync(new ServiceResultException(StatusCodes.BadTimeout)); await session.OpenAsync("test", new UserIdentity(), timeout.Token).ConfigureAwait(false); StatusCode reportedStatus = await reportedError.Task.WaitAsync(timeout.Token).ConfigureAwait(false); Assert.That(reportedStatus, Is.EqualTo(StatusCodes.BadTimeout)); Assert.That(session.KeepAliveStopped, Is.True); await session.CloseAsync(5000, true, timeout.Token).ConfigureAwait(false); session.Channel.Verify(channel => channel.SendRequestAsync( It.IsAny(), It.IsAny()), Times.Never); } [Test] public async Task ReconnectAsyncShouldReconnectSuccessfullyAsync() { // Arrange using var sut = SessionMock.Create(); sut.SetConnected(); CancellationToken ct = CancellationToken.None; ByteString serverNonce = [1, 2, 3, 4]; sut.Channel .Setup(c => c.ReconnectAsync( It.IsAny(), It.IsAny())) .Returns(new ValueTask()) .Verifiable(Times.Once); sut.Channel .Setup(c => c.SupportedFeatures) .Returns(TransportChannelFeatures.Reconnect); sut.Channel .Setup(c => c.SendRequestAsync( It.IsAny(), It.IsAny())) .Returns(new ValueTask(new ActivateSessionResponse { ServerNonce = serverNonce, Results = [], DiagnosticInfos = [] })) .Verifiable(Times.Once); await sut.ReconnectAsync(ct).ConfigureAwait(false); Assert.That(sut.ServerNonce, Is.EqualTo(serverNonce)); sut.Channel.Verify(); } [Test] public void ReconnectAsyncShouldHandleReconnectFailure() { using var sut = SessionMock.Create(); sut.SetConnected(); CancellationToken ct = CancellationToken.None; sut.Channel .Setup(c => c.ReconnectAsync( It.IsAny(), It.IsAny())) .ThrowsAsync(new ServiceResultException(StatusCodes.BadUnexpectedError)) .Verifiable(Times.Once); sut.Channel .Setup(c => c.SupportedFeatures) .Returns(TransportChannelFeatures.Reconnect); ServiceResultException sre = Assert.ThrowsAsync( async () => await sut.ReconnectAsync(ct).ConfigureAwait(false)); Assert.That(sre.StatusCode, Is.EqualTo(StatusCodes.BadUnexpectedError)); sut.Channel.Verify(); } [Test] [Explicit("Requires proper channel creation mocking")] public void ReconnectAsyncShouldHandleNoSupportedFeatures() { using var sut = SessionMock.Create(); sut.SetConnected(); CancellationToken ct = CancellationToken.None; sut.Channel .Setup(c => c.SupportedFeatures) .Returns(TransportChannelFeatures.None); // TODO: Should properly mock channel creation Assert.ThrowsAsync( async () => await sut.ReconnectAsync(ct).ConfigureAwait(false)); sut.Channel.Verify(); } [Test] public void ReconnectAsyncShouldHandleCancellation() { using var sut = SessionMock.Create(); sut.SetConnected(); sut.Channel .Setup(c => c.ReconnectAsync( It.IsAny(), It.IsAny())) .ThrowsAsync(new TaskCanceledException()) .Verifiable(Times.Once); sut.Channel .Setup(c => c.SupportedFeatures) .Returns(TransportChannelFeatures.Reconnect); Assert.ThrowsAsync( async () => await sut.ReconnectAsync(default).ConfigureAwait(false)); sut.Channel.Verify(); } [Test] public async Task ReconnectAsyncShouldHandleServerResponseWithNullNonceAsync() { using var sut = SessionMock.Create(); sut.SetConnected(); CancellationToken ct = CancellationToken.None; sut.Channel .Setup(c => c.ReconnectAsync( It.IsAny(), It.IsAny())) .Returns(new ValueTask()) .Verifiable(Times.Once); sut.Channel .Setup(c => c.SupportedFeatures) .Returns(TransportChannelFeatures.Reconnect); sut.Channel .Setup(c => c.SendRequestAsync( It.IsAny(), It.IsAny())) .Returns(new ValueTask(new ActivateSessionResponse { ServerNonce = default, Results = [], DiagnosticInfos = [] })) .Verifiable(Times.Once); await sut.ReconnectAsync(ct).ConfigureAwait(false); Assert.That(sut.ServerNonce.IsEmpty, Is.True); sut.Channel.Verify(); } [Test] public void ReconnectAsyncShouldThrowWithIncompatibleIdentity() { var ep = new EndpointDescription { SecurityMode = MessageSecurityMode.None, SecurityPolicyUri = SecurityPolicies.None, EndpointUrl = "opc.tcp://localhost:4840", UserIdentityTokens = [ new UserTokenPolicy { PolicyId = "T", TokenType = UserTokenType.Certificate } ] }; using var sut = SessionMock.Create(ep); sut.SetConnected(); CancellationToken ct = CancellationToken.None; ServiceResultException sre = Assert.ThrowsAsync( async () => await sut.ReconnectAsync(ct).ConfigureAwait(false)); Assert.That(sre.StatusCode, Is.EqualTo(StatusCodes.BadIdentityTokenInvalid)); sut.Channel.Verify(); } [Test] public void ReconnectAsyncShouldThrowWithBadActivationResponse() { using var sut = SessionMock.Create(); sut.SetConnected(); CancellationToken ct = CancellationToken.None; sut.Channel .Setup(c => c.ReconnectAsync( It.IsAny(), It.IsAny())) .Returns(new ValueTask()) .Verifiable(Times.Once); sut.Channel .Setup(c => c.SupportedFeatures) .Returns(TransportChannelFeatures.Reconnect); sut.Channel .Setup(c => c.SendRequestAsync( It.IsAny(), It.IsAny())) .Returns(new ValueTask(new ActivateSessionResponse { ResponseHeader = new ResponseHeader { ServiceResult = StatusCodes.BadSessionNotActivated }, Results = [], DiagnosticInfos = [] })) .Verifiable(Times.Once); ServiceResultException sre = Assert.ThrowsAsync( async () => await sut.ReconnectAsync(ct).ConfigureAwait(false)); Assert.That(sre.StatusCode, Is.EqualTo(StatusCodes.BadSessionNotActivated)); sut.Channel.Verify(); } [Test] public void ReconnectAsyncShouldThrowWhenTimingOut() { using var sut = SessionMock.Create(); sut.SetConnected(); CancellationToken ct = CancellationToken.None; sut.Channel .Setup(c => c.ReconnectAsync( It.IsAny(), It.IsAny())) .Returns(new ValueTask()) .Verifiable(Times.Once); sut.Channel .Setup(c => c.SupportedFeatures) .Returns(TransportChannelFeatures.Reconnect); sut.Channel .Setup(c => c.SendRequestAsync( It.IsAny(), It.IsAny())) .ThrowsAsync(new OperationCanceledException()) .Verifiable(Times.Once); Assert.ThrowsAsync( async () => await sut.ReconnectAsync(ct).ConfigureAwait(false)); sut.Channel.Verify(); } [Test] public async Task OpenAsyncShouldOpenSessionSuccessfullyAsync() { // Arrange var ep = new EndpointDescription { SecurityMode = MessageSecurityMode.None, SecurityPolicyUri = SecurityPolicies.None, EndpointUrl = "opc.tcp://localhost:4840", UserIdentityTokens = [ new UserTokenPolicy() ] }; using var sut = SessionMock.Create(ep); CancellationToken ct = CancellationToken.None; ByteString serverNonce = [1, 2, 3, 4]; var authToken = NodeId.Parse("s=cookie"); sut.Channel .Setup(c => c.SendRequestAsync( It.IsAny(), It.IsAny())) .Returns(new ValueTask(new CreateSessionResponse { ServerNonce = serverNonce, SessionId = NodeId.Parse("s=connected"), AuthenticationToken = authToken, ServerEndpoints = [ep] })) .Verifiable(Times.Once); sut.Channel .Setup(c => c.SendRequestAsync( It.Is(r => r.RequestHeader.AuthenticationToken == authToken), It.IsAny())) .Returns(new ValueTask(new ActivateSessionResponse { ServerNonce = serverNonce, Results = [], DiagnosticInfos = [] })) .Verifiable(Times.Once); // Read limit and also first keep alive timers // Server_ServerStatus_State is a ServerState enum which is wire-encoded as Int32. // Using UInt32 here causes ClientBase.ValidateDataValue to log spurious // BadTypeMismatch warnings on every keep-alive while the test runs. sut.Channel .Setup(c => c.SendRequestAsync( It.Is(r => r.NodesToRead.Count == 1), It.IsAny())) .Returns(new ValueTask(new ReadResponse { Results = [ new (new Variant((int)ServerState.Running)) ], DiagnosticInfos = [] })) .Verifiable(Times.AtLeastOnce); // Operation limits sut.Channel .Setup(c => c.SendRequestAsync( It.Is(r => r.NodesToRead.Count == 27), It.IsAny())) .Returns(new ValueTask(new ReadResponse { Results = [.. Enumerable .Range(0, 27) .Select(_ => new DataValue(Variant.Null))], DiagnosticInfos = [] })) .Verifiable(Times.Once); // Namespaces sut.Channel .Setup(c => c.SendRequestAsync( It.Is(r => r.NodesToRead.Count == 2), It.IsAny())) .Returns(new ValueTask(new ReadResponse { Results = [ new(ArrayOf.Create([Namespaces.OpcUa])), new(ArrayOf.Empty()) ], DiagnosticInfos = [] })) .Verifiable(Times.Once); var identity = new UserIdentity(); await sut.OpenAsync("test", identity, ct).ConfigureAwait(false); Assert.That(sut.ServerNonce, Is.EqualTo(ByteString.From([1, 2, 3, 4]))); sut.Channel.Verify(); } [Test] public async Task OpenAsyncAcceptsServerEndpointsWithDifferentOrderingAsync() { EndpointDescription primaryEndpoint = CreateSessionEndpointDescription( "opc.tcp://localhost:4840"); primaryEndpoint.SecurityLevel = 1; primaryEndpoint.TransportProfileUri = Profiles.UaTcpTransport; EndpointDescription secondaryEndpoint = CreateSessionEndpointDescription( "opc.tcp://localhost:4841"); secondaryEndpoint.SecurityMode = MessageSecurityMode.Sign; secondaryEndpoint.SecurityPolicyUri = SecurityPolicies.Basic256Sha256; secondaryEndpoint.SecurityLevel = 2; secondaryEndpoint.TransportProfileUri = Profiles.UaTcpTransport; using var sut = SessionMock.Create( primaryEndpoint, [primaryEndpoint, secondaryEndpoint], [Profiles.UaTcpTransport]); ConfigureSuccessfulOpenResponses( sut.Channel, [secondaryEndpoint, primaryEndpoint], ByteString.From([1, 2, 3, 4]), NodeId.Parse("s=cookie")); await sut.OpenAsync("test", new UserIdentity(), CancellationToken.None) .ConfigureAwait(false); Assert.That(sut.ServerNonce, Is.EqualTo(ByteString.From([1, 2, 3, 4]))); sut.Channel.Verify(); } [Test] public async Task OpenAsyncAcceptsUserTokenPoliciesWithDifferentOrderingAsync() { EndpointDescription discoveryEndpoint = CreateSessionEndpointDescription( "opc.tcp://localhost:4840"); discoveryEndpoint.TransportProfileUri = Profiles.UaTcpTransport; discoveryEndpoint.UserIdentityTokens = [ CreateUserTokenPolicy("anonymous", UserTokenType.Anonymous), CreateUserTokenPolicy("username", UserTokenType.UserName) ]; EndpointDescription responseEndpoint = CreateSessionEndpointDescription( "opc.tcp://localhost:4840"); responseEndpoint.TransportProfileUri = Profiles.UaTcpTransport; responseEndpoint.UserIdentityTokens = [ CreateUserTokenPolicy("username", UserTokenType.UserName), CreateUserTokenPolicy("anonymous", UserTokenType.Anonymous) ]; using var sut = SessionMock.Create( discoveryEndpoint, [discoveryEndpoint], [Profiles.UaTcpTransport]); ConfigureSuccessfulOpenResponses( sut.Channel, [responseEndpoint], ByteString.From([1, 2, 3, 4]), NodeId.Parse("s=cookie")); await sut.OpenAsync("test", new UserIdentity(), CancellationToken.None) .ConfigureAwait(false); Assert.That(sut.ServerNonce, Is.EqualTo(ByteString.From([1, 2, 3, 4]))); sut.Channel.Verify(); } [TestCase(null, "", null, "")] [TestCase("", null, "", null)] [TestCase(null, null, null, null)] [TestCase("", "", "", "")] public async Task OpenAsyncAcceptsNullAndEmptyTokenFieldsAsync( string? discoveryPolicyId, string? authenticatedPolicyId, string? discoveryPolicyUri, string? authenticatedPolicyUri) { EndpointDescription discovery = CreateSessionEndpointDescription("opc.tcp://localhost:4840"); discovery.UserIdentityTokens[0].PolicyId = discoveryPolicyId; discovery.UserIdentityTokens[0].SecurityPolicyUri = discoveryPolicyUri; EndpointDescription authenticated = CreateSessionEndpointDescription("opc.tcp://localhost:4840"); authenticated.UserIdentityTokens[0].PolicyId = authenticatedPolicyId; authenticated.UserIdentityTokens[0].SecurityPolicyUri = authenticatedPolicyUri; using var session = SessionMock.Create(discovery, [discovery]); ConfigureSuccessfulOpenResponses( session.Channel, [authenticated], ByteString.From([1, 2, 3, 4]), NodeId.Parse("s=token")); using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(10)); await session.OpenAsync("test", new UserIdentity(), timeout.Token).ConfigureAwait(false); Assert.That(session.Connected, Is.True); session.Channel.Verify( channel => channel.SendRequestAsync( It.IsAny(), It.IsAny()), Times.Once); } [Test] public void OpenAsyncRejectsChangedAuthenticatedTokenType() { EndpointDescription discovery = CreateSessionEndpointDescription("opc.tcp://localhost:4840"); discovery.UserIdentityTokens = [CreateUserTokenPolicy("identity", UserTokenType.Anonymous)]; EndpointDescription authenticated = CreateSessionEndpointDescription("opc.tcp://localhost:4840"); authenticated.UserIdentityTokens = [CreateUserTokenPolicy("identity", UserTokenType.UserName)]; using var session = SessionMock.Create(discovery); ConfigureSuccessfulOpenResponses( session.Channel, [authenticated], ByteString.From([1, 2, 3, 4]), NodeId.Parse("s=token")); using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(10)); ServiceResultException exception = Assert.ThrowsAsync( async () => await session.OpenAsync("test", new UserIdentity(), timeout.Token) .ConfigureAwait(false)); Assert.That(exception.StatusCode, Is.EqualTo(StatusCodes.BadSecurityChecksFailed)); session.Channel.Verify( channel => channel.SendRequestAsync( It.IsAny(), It.IsAny()), Times.Never); } [Test] public async Task RecreateInPlaceDoesNotReuseAnotherEndpointDiscoverySnapshotAsync() { EndpointDescription initial = CreateSessionEndpointDescription("opc.tcp://initial:4840"); EndpointDescription extra = CreateSessionEndpointDescription("opc.tcp://initial:4841"); EndpointDescription replacement = CreateSessionEndpointDescription("opc.tcp://replacement:4840"); using var session = SessionMock.Create(initial, [initial, extra]); session.SetConnected(); var endpoint = new ConfiguredEndpoint(null, replacement, new EndpointConfiguration()); Mock channel = CreateReconnectChannelMock(session, replacement); ConfigureSuccessfulOpenResponses( channel, [replacement], ByteString.From([1, 2, 3, 4]), NodeId.Parse("s=replacement-token")); using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(10)); await session.RecreateInPlaceAsync(endpoint: endpoint, channel: channel.Object, ct: timeout.Token) .ConfigureAwait(false); Assert.That(session.Connected, Is.True); Assert.That(session.ConfiguredEndpoint, Is.SameAs(endpoint)); channel.Verify(value => value.SendRequestAsync( It.IsAny(), It.IsAny()), Times.Once); } [TestCase(false)] [TestCase(true)] public async Task RecreateRefreshesDiscoveryOnSuppliedChannelAsync(bool reverseConnection) { using SessionMock session = CreateSessionWithDiscoverySnapshot(); EndpointDescription refreshed = CoreUtils.Clone(session.ConfiguredEndpoint.Description)!; refreshed.SecurityLevel = 2; session.Channel.Setup(channel => channel.SendRequestAsync( It.IsAny(), It.IsAny())) .ReturnsAsync(new GetEndpointsResponse { Endpoints = [refreshed] }); ConfigureSuccessfulOpenResponses( session.Channel, [refreshed], ByteString.From([1, 2, 3, 4]), NodeId.Parse("s=recreated")); using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(10)); await session.RecreateInPlaceAsync( connection: reverseConnection ? Mock.Of() : null, channel: session.Channel.Object, ct: timeout.Token).ConfigureAwait(false); Assert.That(session.Connected, Is.True); Assert.That(session.ConfiguredEndpoint.Description.EndpointUrl, Is.EqualTo("opc.tcp://localhost:0")); session.Channel.Verify(channel => channel.SendRequestAsync( It.Is(request => request.EndpointUrl == "opc.tcp://localhost:0"), It.IsAny()), Times.Once); session.Channel.Verify(channel => channel.SendRequestAsync( It.IsAny(), It.IsAny()), Times.Once); session.Channel.Verify(channel => channel.CloseAsync(It.IsAny()), Times.Never); session.Channel.Verify(channel => channel.Dispose(), Times.Never); } [TestCaseSource(nameof(s_discoveryUnavailableStatuses))] public async Task RecreateDiscoveryUnavailableUsesStoredSnapshotAsync(StatusCode status) { using SessionMock session = CreateSessionWithDiscoverySnapshot(); session.Channel.Setup(channel => channel.SendRequestAsync( It.IsAny(), It.IsAny())) .ThrowsAsync(new ServiceResultException(status)); ConfigureSuccessfulOpenResponses( session.Channel, [session.ConfiguredEndpoint.Description], ByteString.From([1, 2, 3, 4]), NodeId.Parse("s=recreated")); using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(10)); await session.RecreateInPlaceAsync(channel: session.Channel.Object, ct: timeout.Token) .ConfigureAwait(false); Assert.That(session.Connected, Is.True); session.Channel.Verify(channel => channel.SendRequestAsync( It.IsAny(), It.IsAny()), Times.Once); session.Channel.Verify(channel => channel.SendRequestAsync( It.IsAny(), It.IsAny()), Times.Once); } [TestCase(false, false)] [TestCase(false, true)] [TestCase(true, false)] [TestCase(true, true)] public async Task RecreateDiscoveryComparisonRejectsChangedAuthenticatedEndpointsAsync( bool discoveryAvailable, bool changeToken) { using SessionMock session = CreateSessionWithDiscoverySnapshot(); EndpointDescription authenticated = CoreUtils.Clone(session.ConfiguredEndpoint.Description)!; if (changeToken) { authenticated.UserIdentityTokens[0].TokenType = UserTokenType.UserName; } else { authenticated.SecurityLevel = 2; } session.Channel.Setup(channel => channel.SendRequestAsync( It.IsAny(), It.IsAny())) .Returns(() => discoveryAvailable ? new ValueTask(new GetEndpointsResponse { Endpoints = session.ConfiguredEndpoint.DiscoveryEndpoints }) : throw new ServiceResultException(StatusCodes.BadServiceUnsupported)); ConfigureSuccessfulOpenResponses( session.Channel, [authenticated], ByteString.From([1, 2, 3, 4]), NodeId.Parse("s=recreated")); using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(10)); await Assert.ThatAsync( async () => await session.RecreateInPlaceAsync( channel: session.Channel.Object, ct: timeout.Token).ConfigureAwait(false), Throws.TypeOf().With.Property(nameof(ServiceResultException.StatusCode)) .EqualTo(StatusCodes.BadSecurityChecksFailed)).ConfigureAwait(false); session.Channel.Verify(channel => channel.SendRequestAsync( It.IsAny(), It.IsAny()), Times.Once); session.Channel.Verify(channel => channel.SendRequestAsync( It.IsAny(), It.IsAny()), Times.Never); } [TestCaseSource(nameof(s_discoverySecurityStatuses))] public async Task RecreateDiscoverySecurityFailureDoesNotFallBackAsync(StatusCode status) { using SessionMock session = CreateSessionWithDiscoverySnapshot(); session.Channel.Setup(channel => channel.SendRequestAsync( It.IsAny(), It.IsAny())) .ThrowsAsync(new ServiceResultException(status)); using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(10)); await Assert.ThatAsync( async () => await session.RecreateInPlaceAsync( channel: session.Channel.Object, ct: timeout.Token).ConfigureAwait(false), Throws.TypeOf().With.Property(nameof(ServiceResultException.StatusCode)) .EqualTo(status)).ConfigureAwait(false); session.Channel.Verify(channel => channel.SendRequestAsync( It.IsAny(), It.IsAny()), Times.Never); } [Test] public async Task RecreateDiscoveryCancellationDoesNotActivateAsync() { using SessionMock session = CreateSessionWithDiscoverySnapshot(); using var cancellation = new CancellationTokenSource(); var entered = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var blocked = new TaskCompletionSource( TaskCreationOptions.RunContinuationsAsynchronously); session.Channel.Setup(channel => channel.SendRequestAsync( It.IsAny(), It.IsAny())) .Returns(async (IServiceRequest _, CancellationToken ct) => { entered.TrySetResult(true); return await blocked.Task.WaitAsync(ct).ConfigureAwait(false); }); Task recreate = session.RecreateInPlaceAsync( channel: session.Channel.Object, ct: cancellation.Token); try { await entered.Task.WaitAsync(TimeSpan.FromSeconds(10)).ConfigureAwait(false); cancellation.Cancel(); await Assert.ThatAsync( async () => await recreate.ConfigureAwait(false), Throws.InstanceOf()).ConfigureAwait(false); session.Channel.Verify(channel => channel.SendRequestAsync( It.IsAny(), It.IsAny()), Times.Never); } finally { cancellation.Cancel(); blocked.TrySetCanceled(); } } [Test] public async Task RecreateRejectsEmptyDiscoveryInsteadOfUsingStoredSnapshotAsync() { using SessionMock session = CreateSessionWithDiscoverySnapshot(); session.Channel.Setup(channel => channel.SendRequestAsync( It.IsAny(), It.IsAny())) .ReturnsAsync(new GetEndpointsResponse { Endpoints = [] }); using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(10)); await Assert.ThatAsync( async () => await session.RecreateInPlaceAsync( channel: session.Channel.Object, ct: timeout.Token).ConfigureAwait(false), Throws.TypeOf().With.Property(nameof(ServiceResultException.StatusCode)) .EqualTo(StatusCodes.BadSecurityChecksFailed)).ConfigureAwait(false); session.Channel.Verify(channel => channel.SendRequestAsync( It.IsAny(), It.IsAny()), Times.Never); } [Test] public void OpenAsyncShouldHandleCreateSessionSuccessButActivationError() { // Arrange var ep = new EndpointDescription { SecurityMode = MessageSecurityMode.None, SecurityPolicyUri = SecurityPolicies.None, EndpointUrl = "opc.tcp://localhost:4840", UserIdentityTokens = [ new UserTokenPolicy() ] }; using var sut = SessionMock.Create(ep); CancellationToken ct = CancellationToken.None; ByteString serverNonce = [1, 2, 3, 4]; var authToken = NodeId.Parse("s=cookie"); sut.Channel .Setup(c => c.SendRequestAsync( It.IsAny(), It.IsAny())) .Returns(new ValueTask(new CreateSessionResponse { ServerNonce = serverNonce, SessionId = NodeId.Parse("s=connected"), AuthenticationToken = authToken, ServerEndpoints = [ep] })) .Verifiable(Times.Once); sut.Channel .Setup(c => c.SendRequestAsync( It.Is(r => r.RequestHeader.AuthenticationToken == authToken), It.IsAny())) .Returns(new ValueTask(new ActivateSessionResponse { ResponseHeader = new ResponseHeader { ServiceResult = StatusCodes.BadSessionNotActivated }, ServerNonce = serverNonce, Results = [], DiagnosticInfos = [] })) .Verifiable(Times.Once); sut.Channel .Setup(c => c.SendRequestAsync( It.IsAny(), It.IsAny())) .Returns(new ValueTask(new CloseSessionResponse { ResponseHeader = new ResponseHeader { ServiceResult = StatusCodes.Good } })) .Verifiable(Times.Once); sut.Channel .Setup(c => c.CloseAsync(It.IsAny())) .Returns(new ValueTask()) .Verifiable(Times.Once); var identity = new UserIdentity(); ServiceResultException sre = Assert.ThrowsAsync( async () => await sut.OpenAsync("test", identity, default).ConfigureAwait(false)); Assert.That(sre.StatusCode, Is.EqualTo(StatusCodes.BadSessionNotActivated)); sut.Channel.Verify(); } [Test] public void OpenAsyncShouldHandleCreateSessionSuccessButActivationErrorAndThenCloseAlsoFails() { // Arrange var ep = new EndpointDescription { SecurityMode = MessageSecurityMode.None, SecurityPolicyUri = SecurityPolicies.None, EndpointUrl = "opc.tcp://localhost:4840", UserIdentityTokens = [ new UserTokenPolicy() ] }; using var sut = SessionMock.Create(ep); CancellationToken ct = CancellationToken.None; ByteString serverNonce = [1, 2, 3, 4]; var authToken = NodeId.Parse("s=cookie"); sut.Channel .Setup(c => c.SendRequestAsync( It.IsAny(), It.IsAny())) .Returns(new ValueTask(new CreateSessionResponse { ServerNonce = serverNonce, SessionId = NodeId.Parse("s=connected"), AuthenticationToken = authToken, ServerEndpoints = [ep] })) .Verifiable(Times.Once); sut.Channel .Setup(c => c.SendRequestAsync( It.Is(r => r.RequestHeader.AuthenticationToken == authToken), It.IsAny())) .Returns(new ValueTask(new ActivateSessionResponse { ResponseHeader = new ResponseHeader { ServiceResult = StatusCodes.BadSessionNotActivated }, ServerNonce = serverNonce, Results = [], DiagnosticInfos = [] })) .Verifiable(Times.Once); sut.Channel .Setup(c => c.SendRequestAsync( It.IsAny(), It.IsAny())) .Returns(new ValueTask(new CloseSessionResponse { ResponseHeader = new ResponseHeader { ServiceResult = StatusCodes.BadNotConnected } })) .Verifiable(Times.Once); sut.Channel .Setup(c => c.CloseAsync(It.IsAny())) .Throws(new ServiceResultException(StatusCodes.BadNotConnected)) .Verifiable(Times.Once); var identity = new UserIdentity(); ServiceResultException sre = Assert.ThrowsAsync( async () => await sut.OpenAsync( "test", 60000, identity, default, true, closeChannel: true, default).ConfigureAwait(false)); Assert.That(sre.StatusCode, Is.EqualTo(StatusCodes.BadSessionNotActivated)); sut.Channel.Verify(); } [Test] public void OpenAsyncShouldHandleSessionOpeningFailure() { var ep = new EndpointDescription { SecurityMode = MessageSecurityMode.None, SecurityPolicyUri = SecurityPolicies.None, EndpointUrl = "opc.tcp://localhost:4840", UserIdentityTokens = [ new UserTokenPolicy() ] }; using var sut = SessionMock.Create(ep); sut.Channel .Setup(c => c.SendRequestAsync( It.IsAny(), It.IsAny())) .ThrowsAsync(new ServiceResultException(StatusCodes.BadUnexpectedError)) .Verifiable(Times.Exactly(2)); var identity = new UserIdentity(); ServiceResultException sre = Assert.ThrowsAsync( async () => await sut.OpenAsync("test", identity, default).ConfigureAwait(false)); Assert.That(sre.StatusCode, Is.EqualTo(StatusCodes.BadUnexpectedError)); sut.Channel.Verify(); } #if FALSE // TODO: Enable when moving certificate loading into OpenAsync [Test] public void OpenAsyncShouldHandleBadSecurityPolicy() { var ep = new EndpointDescription { SecurityMode = MessageSecurityMode.None, SecurityPolicyUri = "Bad", EndpointUrl = "opc.tcp://localhost:4840", UserIdentityTokens = [ new UserTokenPolicy() ] }; using var sut = SessionMock.Create(ep); var identity = new UserIdentity(); ServiceResultException sre = Assert.ThrowsAsync( async () => await sut.OpenAsync("test", identity, default).ConfigureAwait(false)); Assert.That(sre.StatusCode, Is.EqualTo(StatusCodes.BadSecurityPolicyRejected)); sut.Channel.Verify(); } #endif [Test] public void OpenAsyncShouldHandleBadIdentityTokenPolicy() { var ep = new EndpointDescription { SecurityMode = MessageSecurityMode.None, SecurityPolicyUri = SecurityPolicies.None, EndpointUrl = "opc.tcp://localhost:4840", UserIdentityTokens = [ new UserTokenPolicy { PolicyId = "PolicyId", TokenType = UserTokenType.IssuedToken } ] }; using var sut = SessionMock.Create(ep); var identity = new UserIdentity(); ServiceResultException sre = Assert.ThrowsAsync( async () => await sut.OpenAsync("test", identity, default).ConfigureAwait(false)); Assert.That(sre.StatusCode, Is.EqualTo(StatusCodes.BadIdentityTokenInvalid)); sut.Channel.Verify(); } [Test] public void OpenAsyncShouldHandleCancellation() { using var sut = SessionMock.Create(); sut.Channel .Setup(c => c.SendRequestAsync( It.IsAny(), It.IsAny())) .ThrowsAsync(new TaskCanceledException()) .Verifiable(Times.Once); var identity = new UserIdentity(); Assert.ThrowsAsync( async () => await sut.OpenAsync("test", identity, default).ConfigureAwait(false)); sut.Channel.Verify(); } [Test] public void OpenAsyncShouldHandleInvalidServerResponse() { using var sut = SessionMock.Create(); CancellationToken ct = CancellationToken.None; sut.Channel .Setup(c => c.SendRequestAsync( It.IsAny(), It.IsAny())) .Returns(new ValueTask(new CreateSessionResponse { ServerNonce = default, SessionId = NodeId.Parse("s=connected") })) .Verifiable(Times.Once); var identity = new UserIdentity(); Assert.ThrowsAsync( async () => await sut.OpenAsync("test", identity, ct).ConfigureAwait(false)); sut.Channel.Verify(); } [TestCase(false)] [TestCase(true)] public async Task RecreateInPlaceAsyncReusesTokenWhenEnabledOrRequiredAsync(bool requireTokenReuse) { EndpointDescription failoverDescription = CreateSessionEndpointDescription("opc.tcp://failover:4840"); using var sut = SessionMock.Create(); sut.SetConnected(); sut.EnableTokenReuseFailover = !requireTokenReuse; SetServerNonce(sut, [1, 2, 3, 4]); Mock failoverChannel = CreateReconnectChannelMock(sut, failoverDescription); failoverChannel .Setup(c => c.SendRequestAsync( It.Is(r => r.RequestHeader.AuthenticationToken == NodeId.Parse("s=auth")), It.IsAny())) .Returns(new ValueTask(new ActivateSessionResponse { ServerNonce = [5, 6, 7, 8], Results = [], DiagnosticInfos = [] })) .Verifiable(Times.Once); await InvokeRecreateInPlaceCoreAsync( sut, new ConfiguredEndpoint(null!, failoverDescription), failoverChannel.Object, requireTokenReuse).ConfigureAwait(false); Assert.That(sut.SessionId, Is.EqualTo(NodeId.Parse("s=connected"))); failoverChannel.Verify(); failoverChannel.Verify( c => c.SendRequestAsync( It.IsAny(), It.IsAny()), Times.Never); } [Test] public async Task RecreateInPlaceAsyncFallsBackToFullReauthenticationWhenTokenReuseFailsAsync() { EndpointDescription failoverDescription = CreateSessionEndpointDescription("opc.tcp://failover:4840"); using var sut = SessionMock.Create(); sut.SetConnected(); sut.EnableTokenReuseFailover = true; SetServerNonce(sut, [1, 2, 3, 4]); var newSessionId = NodeId.Parse("s=recreated"); var newAuthenticationToken = NodeId.Parse("s=new-auth"); Mock failoverChannel = CreateReconnectChannelMock(sut, failoverDescription); ConfigureOpenAsyncReadResponses(failoverChannel); failoverChannel .Setup(c => c.SendRequestAsync( It.Is(r => r.RequestHeader.AuthenticationToken == NodeId.Parse("s=auth")), It.IsAny())) .ThrowsAsync(new ServiceResultException(StatusCodes.BadSessionNotActivated)) .Verifiable(Times.Once); failoverChannel .Setup(c => c.SendRequestAsync( It.IsAny(), It.IsAny())) .Returns(new ValueTask(new CreateSessionResponse { ServerNonce = [9, 10, 11, 12], SessionId = newSessionId, AuthenticationToken = newAuthenticationToken, ServerEndpoints = [failoverDescription] })) .Verifiable(Times.Once); failoverChannel .Setup(c => c.SendRequestAsync( It.Is(r => r.RequestHeader.AuthenticationToken == newAuthenticationToken), It.IsAny())) .Returns(new ValueTask(new ActivateSessionResponse { ServerNonce = [13, 14, 15, 16], Results = [], DiagnosticInfos = [] })) .Verifiable(Times.Once); await InvokeRecreateInPlaceCoreAsync( sut, new ConfiguredEndpoint(null!, failoverDescription), failoverChannel.Object, requireTokenReuse: false).ConfigureAwait(false); Assert.That(sut.SessionId, Is.EqualTo(newSessionId)); failoverChannel.Verify(); } [Test] public void ReactivateMirroredSessionAsyncRequiresExistingSession() { EndpointDescription failoverDescription = CreateSessionEndpointDescription("opc.tcp://failover:4840"); using var sut = SessionMock.Create(); sut.EnableTokenReuseFailover = false; Mock failoverChannel = CreateReconnectChannelMock(sut, failoverDescription); ServiceResultException ex = Assert.ThrowsAsync(async () => await InvokeRecreateInPlaceCoreAsync( sut, new ConfiguredEndpoint(null!, failoverDescription), failoverChannel.Object, requireTokenReuse: true).ConfigureAwait(false))!; Assert.That(ex.StatusCode, Is.EqualTo(StatusCodes.BadInvalidState)); failoverChannel.Verify( c => c.SendRequestAsync( It.IsAny(), It.IsAny()), Times.Never); failoverChannel.Verify( c => c.SendRequestAsync( It.IsAny(), It.IsAny()), Times.Never); } [Test] public void SaveShouldOnlySaveSpecifiedSubscriptions() { using var sut = SessionMock.Create(); ITelemetryContext telemetry = NUnitTelemetryContext.Create(); using var subscription1 = new Subscription( telemetry, new SubscriptionOptions { DisplayName = "Subscription1" }); using var subscription2 = new Subscription( telemetry, new SubscriptionOptions { DisplayName = "Subscription2" }); using var subscription3 = new Subscription( telemetry, new SubscriptionOptions { DisplayName = "Subscription3" }); sut.AddSubscription(subscription1); sut.AddSubscription(subscription2); sut.AddSubscription(subscription3); Assert.That(sut.SubscriptionCount, Is.EqualTo(3)); // Only save a subset of subscriptions (subscription1 and subscription3) Subscription[] subscriptionsToSave = [subscription1, subscription3]; using var stream = new MemoryStream(); sut.Save(stream, subscriptionsToSave); stream.Position = 0; using var loadSession = SessionMock.Create(); var loadedSubscriptions = loadSession.Load(stream).ToList(); Assert.That(loadedSubscriptions, Has.Count.EqualTo(2), "Only the specified subscriptions should be saved"); Assert.That(loadedSubscriptions.Select(s => s.DisplayName), Is.EquivalentTo(["Subscription1", "Subscription3"])); } [Test] public void SaveAndApplySessionConfigurationShouldPersistClientNonce() { var source = SessionMock.Create(); source.SetConnected(); byte[] clientNonce = [1, 3, 5, 7, 9]; byte[] serverNonce = [2, 4, 6, 8]; SetClientNonce(source, clientNonce); SetServerNonce(source, serverNonce); SessionConfiguration configuration = source.SaveSessionConfiguration(); Assert.That(configuration.ClientNonce.ToArray(), Is.EquivalentTo(clientNonce)); Assert.That(configuration.ServerNonce.ToArray(), Is.EquivalentTo(serverNonce)); var target = SessionMock.Create(); bool success = target.ApplySessionConfiguration(configuration); Assert.That(success, Is.True); Assert.That(GetClientNonce(target), Is.EquivalentTo(clientNonce)); Assert.That(GetServerNonce(target), Is.EquivalentTo(serverNonce)); } private static EndpointDescription CreateSessionEndpointDescription(string endpointUrl) { return new EndpointDescription { SecurityMode = MessageSecurityMode.None, SecurityPolicyUri = SecurityPolicies.None, EndpointUrl = endpointUrl, UserIdentityTokens = [ new UserTokenPolicy() ] }; } private static SessionMock CreateSessionWithDiscoverySnapshot() { EndpointDescription description = CreateSessionEndpointDescription("opc.tcp://localhost:0"); description.SecurityLevel = 1; var session = SessionMock.Create(description); session.SetConnected(); ArrayOf snapshot = [CoreUtils.Clone(description)!]; typeof(ConfiguredEndpoint).GetProperty(nameof(ConfiguredEndpoint.DiscoveryEndpoints))! .SetValue(session.ConfiguredEndpoint, snapshot); return session; } private static UserTokenPolicy CreateUserTokenPolicy( string policyId, UserTokenType tokenType) { return new UserTokenPolicy { PolicyId = policyId, TokenType = tokenType, SecurityPolicyUri = SecurityPolicies.None }; } private static Mock CreateReconnectChannelMock( SessionMock session, EndpointDescription endpointDescription) { var channel = new Mock(); channel.SetupGet(c => c.MessageContext).Returns(session.MessageContext); channel.SetupGet(c => c.SupportedFeatures).Returns(TransportChannelFeatures.Reconnect); channel.SetupGet(c => c.EndpointDescription).Returns(endpointDescription); channel.SetupGet(c => c.EndpointConfiguration).Returns(new EndpointConfiguration()); channel.SetupGet(c => c.ChannelThumbprint).Returns([]); channel.SetupGet(c => c.ClientChannelCertificate).Returns([]); channel.SetupGet(c => c.ServerChannelCertificate).Returns([]); return channel; } private static void ConfigureSuccessfulOpenResponses( Mock channel, ArrayOf serverEndpoints, ByteString serverNonce, NodeId authToken) { channel .Setup(c => c.SendRequestAsync( It.IsAny(), It.IsAny())) .Returns(new ValueTask(new CreateSessionResponse { ServerNonce = serverNonce, SessionId = NodeId.Parse("s=connected"), AuthenticationToken = authToken, ServerEndpoints = serverEndpoints })); channel .Setup(c => c.SendRequestAsync( It.Is(r => r.RequestHeader.AuthenticationToken == authToken), It.IsAny())) .Returns(new ValueTask(new ActivateSessionResponse { ServerNonce = serverNonce, Results = [], DiagnosticInfos = [] })); ConfigureOpenAsyncReadResponses(channel); } private static void ConfigureOpenAsyncReadResponses(Mock channel) { channel .Setup(c => c.SendRequestAsync( It.Is(r => r.NodesToRead.Count == 1), It.IsAny())) .Returns(new ValueTask(new ReadResponse { Results = [ new(new Variant((int)ServerState.Running)) ], DiagnosticInfos = [] })); channel .Setup(c => c.SendRequestAsync( It.Is(r => r.NodesToRead.Count == 27), It.IsAny())) .Returns(new ValueTask(new ReadResponse { Results = [.. Enumerable .Range(0, 27) .Select(_ => new DataValue(Variant.Null))], DiagnosticInfos = [] })); channel .Setup(c => c.SendRequestAsync( It.Is(r => r.NodesToRead.Count == 2), It.IsAny())) .Returns(new ValueTask(new ReadResponse { Results = [ new(ArrayOf.Create([Namespaces.OpcUa])), new(ArrayOf.Empty()) ], DiagnosticInfos = [] })); } private static Task InvokeRecreateInPlaceCoreAsync( Session session, ConfiguredEndpoint endpoint, ITransportChannel channel, bool requireTokenReuse) { MethodInfo? method = typeof(Session).GetMethod( "RecreateInPlaceCoreAsync", BindingFlags.NonPublic | BindingFlags.Instance, null, [ typeof(ConfiguredEndpoint), typeof(ITransportWaitingConnection), typeof(ITransportChannel), typeof(IRetryBudget), typeof(CancellationToken), typeof(bool), typeof(bool), typeof(SessionClient), typeof(bool), typeof(bool) ], null); Assert.That(method, Is.Not.Null); var task = (Task?)method!.Invoke( session, [endpoint, null, channel, null, CancellationToken.None, false, requireTokenReuse, null, false, false]); Assert.That(task, Is.Not.Null); return task!; } private static readonly StatusCode[] s_discoveryUnavailableStatuses = [ StatusCodes.BadNotConnected, StatusCodes.BadTimeout, StatusCodes.BadServiceUnsupported ]; private static readonly StatusCode[] s_discoverySecurityStatuses = [ StatusCodes.BadSecurityChecksFailed, StatusCodes.BadCertificateUntrusted ]; private const BindingFlags PrivateInstance = BindingFlags.NonPublic | BindingFlags.Instance; private static void SetClientNonce(Session session, byte[] value) { typeof(Session) .GetField("m_clientNonce", PrivateInstance)! .SetValue(session, value.ToArray()); } private static void SetServerNonce(Session session, byte[] value) { typeof(Session) .GetField("m_serverNonce", PrivateInstance)! .SetValue(session, ByteString.From(value)); } private static byte[] GetClientNonce(Session session) { return typeof(Session) .GetField("m_clientNonce", PrivateInstance)! .GetValue(session) is byte[] bytes ? [.. bytes] : []; } private static byte[] GetServerNonce(Session session) { return ((ByteString)typeof(Session) .GetField("m_serverNonce", PrivateInstance)! .GetValue(session)!).ToArray(); } private static ArrayOf CreateOperationLimitsRead(uint operationLimit, uint maxArrayLength) { // Mirrors the node order read by FetchOperationLimitsAsync: the 12 operation // limits (all set to operationLimit) followed by the server capabilities, of // which only MaxArrayLength (index 16) varies. return [ new DataValue(new Variant(operationLimit)), // MaxNodesPerHistoryReadData new DataValue(new Variant(operationLimit)), // MaxNodesPerHistoryReadEvents new DataValue(new Variant(operationLimit)), // MaxNodesPerWrite new DataValue(new Variant(operationLimit)), // MaxNodesPerRead new DataValue(new Variant(operationLimit)), // MaxNodesPerHistoryUpdateData new DataValue(new Variant(operationLimit)), // MaxNodesPerHistoryUpdateEvents new DataValue(new Variant(operationLimit)), // MaxNodesPerMethodCall new DataValue(new Variant(operationLimit)), // MaxNodesPerBrowse new DataValue(new Variant(operationLimit)), // MaxNodesPerRegisterNodes new DataValue(new Variant(operationLimit)), // MaxNodesPerNodeManagement new DataValue(new Variant(operationLimit)), // MaxMonitoredItemsPerCall new DataValue(new Variant(operationLimit)), // MaxNodesPerTranslateBrowsePathsToNodeIds new DataValue(new Variant((ushort)100)), // MaxBrowseContinuationPoints new DataValue(new Variant((ushort)100)), // MaxHistoryContinuationPoints new DataValue(new Variant((ushort)100)), // MaxQueryContinuationPoints new DataValue(new Variant(1000u)), // MaxStringLength new DataValue(new Variant(maxArrayLength)), // MaxArrayLength new DataValue(new Variant(1000u)), // MaxByteStringLength new DataValue(new Variant(100.0)), // MinSupportedSampleRate new DataValue(new Variant(1000u)), // MaxSessions new DataValue(new Variant(1000u)), // MaxSubscriptions new DataValue(new Variant(1000u)), // MaxMonitoredItems new DataValue(new Variant(1000u)), // MaxMonitoredItemsPerSubscription new DataValue(new Variant(1000u)), // MaxMonitoredItemsQueueSize new DataValue(new Variant(1000u)), // MaxSubscriptionsPerSession new DataValue(new Variant(1000u)), // MaxWhereClauseParameters new DataValue(new Variant(1000u)) // MaxSelectClauseParameters ]; } private static void SetupCloseSession(SessionMock sut) { sut.Channel .Setup(c => c.SendRequestAsync( It.IsAny(), It.IsAny())) .Returns(new ValueTask(new CloseSessionResponse { ResponseHeader = new ResponseHeader { ServiceResult = StatusCodes.Good } })); } private static List SetupRead(SessionMock sut) { var sent = new List(); sut.Channel .Setup(c => c.SendRequestAsync( It.IsAny(), It.IsAny())) .Callback((IServiceRequest request, CancellationToken _) => sent.Add((ReadRequest)request)) .Returns(new ValueTask(new ReadResponse { ResponseHeader = new ResponseHeader { ServiceResult = StatusCodes.Good }, Results = [new DataValue(new Variant((int)ServerState.Running))] })); return sent; } private static async Task ReadServerStateAsync(SessionMock sut, CancellationToken ct) { await sut.ReadAsync( null, 0, TimestampsToReturn.Neither, [ new ReadValueId { NodeId = VariableIds.Server_ServerStatus_State, AttributeId = Attributes.Value } ], ct).ConfigureAwait(false); } } }