/* ======================================================================== * 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.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Moq; using NUnit.Framework; using Opc.Ua.Server.TestFramework; namespace Opc.Ua.Server.Tests { /// /// Verifies that Read and Write are dispatched only to the node managers that serve /// the namespaces of the requested nodes, with unchanged results. /// [TestFixture] [Category("MasterNodeManager")] [Parallelizable] public sealed class NodeManagerDispatchRoutingTests { private const string kNamespaceA = "http://test.org/UA/RoutingA/"; private const string kNamespaceB = "http://test.org/UA/RoutingB/"; [Test] public async Task ReadCallsOnlyTheNodeManagersOfTheRequestedNamespaceAsync() { await RunAsync(async (sut, a, b, nsA, nsB) => { (ArrayOf values, _) = await sut.ReadAsync( Context(), 0, TimestampsToReturn.Neither, [Value(new NodeId(1, nsA)), Value(new NodeId(2, nsA))]).ConfigureAwait(false); Assert.That(values[0].WrappedValue, Is.EqualTo(new Variant("A"))); Assert.That(values[1].WrappedValue, Is.EqualTo(new Variant("A"))); Assert.That(a.ReadCalls, Is.EqualTo(1)); Assert.That(b.ReadCalls, Is.Zero, "a node manager without a requested namespace is skipped"); }).ConfigureAwait(false); } [Test] public async Task ReadCallsEveryOwningNodeManagerOnceAndKeepsResultOrderAsync() { await RunAsync(async (sut, a, b, nsA, nsB) => { (ArrayOf values, _) = await sut.ReadAsync( Context(), 0, TimestampsToReturn.Neither, [ Value(new NodeId(1, nsB)), Value(new NodeId(1, nsA)), Value(new NodeId(2, nsB)) ]).ConfigureAwait(false); Assert.That(values[0].WrappedValue, Is.EqualTo(new Variant("B"))); Assert.That(values[1].WrappedValue, Is.EqualTo(new Variant("A"))); Assert.That(values[2].WrappedValue, Is.EqualTo(new Variant("B"))); Assert.That(a.ReadCalls, Is.EqualTo(1)); Assert.That(b.ReadCalls, Is.EqualTo(1)); }).ConfigureAwait(false); } [Test] public async Task ReadOfAnUnroutedNamespaceStillCallsEveryNodeManagerAsync() { await RunAsync(async (sut, a, b, nsA, nsB) => { (ArrayOf values, _) = await sut.ReadAsync( Context(), 0, TimestampsToReturn.Neither, [Value(new NodeId(1, nsA)), Value(new NodeId(1, 250))]).ConfigureAwait(false); Assert.That(values[0].WrappedValue, Is.EqualTo(new Variant("A"))); Assert.That(values[1].StatusCode, Is.EqualTo(StatusCodes.BadNodeIdUnknown)); Assert.That(a.ReadCalls, Is.EqualTo(1)); Assert.That(b.ReadCalls, Is.EqualTo(1), "an unrouted node keeps the call to every node manager"); }).ConfigureAwait(false); } [Test] public async Task ReadOfOnlyInvalidItemsCallsNoNodeManagerAsync() { await RunAsync(async (sut, a, b, nsA, nsB) => { (ArrayOf values, _) = await sut.ReadAsync( Context(), 0, TimestampsToReturn.Neither, [new ReadValueId { NodeId = new NodeId(1, nsA), AttributeId = 999 }]).ConfigureAwait(false); Assert.That(values[0].StatusCode, Is.EqualTo(StatusCodes.BadAttributeIdInvalid)); Assert.That(a.ReadCalls, Is.Zero); Assert.That(b.ReadCalls, Is.Zero); }).ConfigureAwait(false); } [Test] public async Task SharedNamespaceKeepsDispatchOrderBetweenItsNodeManagersAsync() { // the first node manager of a namespace in dispatch order processes the node; the // second one is still called and sees the node as processed, as before. var second = new FakeNodeManager("A2", kNamespaceA); await RunAsync(async (sut, a, b, nsA, nsB) => { Assert.That(sut.NamespaceManagers[nsA], Has.Count.EqualTo(2)); (ArrayOf values, _) = await sut.ReadAsync( Context(), 0, TimestampsToReturn.Neither, [Value(new NodeId(1, nsA))]).ConfigureAwait(false); Assert.That(values[0].WrappedValue, Is.EqualTo(new Variant("A"))); Assert.That(a.ReadCalls, Is.EqualTo(1)); Assert.That(second.ReadCalls, Is.EqualTo(1)); Assert.That(b.ReadCalls, Is.Zero); }, second).ConfigureAwait(false); } [Test] public async Task WriteCallsOnlyTheNodeManagersOfTheRequestedNamespaceAsync() { await RunAsync(async (sut, a, b, nsA, nsB) => { (ArrayOf results, _) = await sut.WriteAsync( Context(), [ new WriteValue { NodeId = new NodeId(1, nsB), AttributeId = Attributes.Value, Value = new DataValue(new Variant(1)) } ]).ConfigureAwait(false); Assert.That(results[0], Is.EqualTo(StatusCodes.Good)); Assert.That(b.WriteCalls, Is.EqualTo(1)); Assert.That(a.WriteCalls, Is.Zero); }).ConfigureAwait(false); } private delegate Task Scenario( MasterNodeManager sut, FakeNodeManager a, FakeNodeManager b, ushort nsA, ushort nsB); private static async Task RunAsync(Scenario scenario, FakeNodeManager? sameNamespaceAsA = null) { var fixture = new ServerFixture(t => new StandardServer(t)); try { StandardServer server = await fixture.StartAsync().ConfigureAwait(false); var a = new FakeNodeManager("A", kNamespaceA); var b = new FakeNodeManager("B", kNamespaceB); IAsyncNodeManager[] managers = sameNamespaceAsA == null ? [a.Object, b.Object] : [a.Object, sameNamespaceAsA.Object, b.Object]; using var sut = new MasterNodeManager( server.CurrentInstance, fixture.Config, null, managers); NamespaceTable namespaces = server.CurrentInstance.NamespaceUris; a.NamespaceIndex = (ushort)namespaces.GetIndex(kNamespaceA); b.NamespaceIndex = (ushort)namespaces.GetIndex(kNamespaceB); if (sameNamespaceAsA != null) { sameNamespaceAsA.NamespaceIndex = a.NamespaceIndex; } await scenario(sut, a, b, a.NamespaceIndex, b.NamespaceIndex).ConfigureAwait(false); } finally { await fixture.StopAsync().ConfigureAwait(false); } } private static OperationContext Context() { return new OperationContext(new RequestHeader(), null, RequestType.Read, RequestLifetime.None); } private static ReadValueId Value(NodeId nodeId) { return new ReadValueId { NodeId = nodeId, AttributeId = Attributes.Value }; } /// /// A node manager that owns every node of its namespace and counts its calls. /// private sealed class FakeNodeManager { public FakeNodeManager(string tag, string namespaceUri) { var mock = new Mock(); mock.SetupGet(m => m.NamespaceUris).Returns([namespaceUri]); mock.Setup(m => m.ReadAsync( It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny>(), It.IsAny>(), It.IsAny())) .Returns((OperationContext context, double maxAge, ArrayOf nodesToRead, IList values, IList errors, CancellationToken ct) => { Interlocked.Increment(ref m_readCalls); for (int ii = 0; ii < nodesToRead.Count; ii++) { if (!nodesToRead[ii].Processed && nodesToRead[ii].NodeId.NamespaceIndex == NamespaceIndex) { nodesToRead[ii].Processed = true; values[ii] = new DataValue(new Variant(tag)); errors[ii] = ServiceResult.Good; } } return default; }); mock.Setup(m => m.WriteAsync( It.IsAny(), It.IsAny>(), It.IsAny>(), It.IsAny())) .Returns((OperationContext context, ArrayOf nodesToWrite, IList errors, CancellationToken ct) => { Interlocked.Increment(ref m_writeCalls); for (int ii = 0; ii < nodesToWrite.Count; ii++) { if (!nodesToWrite[ii].Processed && nodesToWrite[ii].NodeId.NamespaceIndex == NamespaceIndex) { nodesToWrite[ii].Processed = true; errors[ii] = ServiceResult.Good; } } return default; }); Object = mock.Object; } public IAsyncNodeManager Object { get; } public int ReadCalls => Volatile.Read(ref m_readCalls); public int WriteCalls => Volatile.Read(ref m_writeCalls); public ushort NamespaceIndex { get; set; } private int m_readCalls; private int m_writeCalls; } } }