/* ========================================================================
* Copyright (c) 2005-2025 The OPC Foundation, Inc. All rights reserved.
*
* OPC Foundation MIT License 1.00
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*
* The complete license agreement can be found here:
* http://opcfoundation.org/License/MIT/1.00/
* ======================================================================*/
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Moq;
using NUnit.Framework;
using Opc.Ua.Server.TestFramework;
namespace Opc.Ua.Server.Tests
{
///
/// Deterministic, offline unit tests for the residual dispatcher surface of
/// that is reachable without a live client
/// session or transport: constructor guards, RegisterNodes pass-through,
/// Browse / BrowseNext / TranslateBrowsePaths / Read / Write / HistoryRead /
/// HistoryUpdate / Call per-item validation and routing, and the
/// monitored-item argument guards and dispatch. The per-item validation
/// StatusCodes for AddNodes / DeleteNodes / AddReferences / DeleteReferences
/// are intentionally not retested here (covered by
/// MasterNodeManagerNodeManagementTests).
///
[TestFixture]
[Category("MasterNodeManager")]
[Category("MasterNodeManagerDeterministic")]
[SetCulture("en-us")]
[SetUICulture("en-us")]
[NonParallelizable]
public class MasterNodeManagerDeterministicTests
{
private ServerFixture m_fixture = null!;
private StandardServer m_server = null!;
[OneTimeSetUp]
public async Task OneTimeSetUpAsync()
{
m_fixture = new ServerFixture(t => new StandardServer(t));
m_server = await m_fixture.StartAsync().ConfigureAwait(false);
}
[OneTimeTearDown]
public async Task OneTimeTearDownAsync()
{
await m_fixture.StopAsync().ConfigureAwait(false);
}
[TestCase(1000, true)]
[TestCase(4096, true)]
[TestCase(4097, false)]
public async Task LongTranslatePathPreservesTargetAndQuotaAsync(int count, bool accepted)
{
StatusCode expectedStatus = accepted ? StatusCodes.Good : StatusCodes.BadQueryTooComplex;
var elements = new RelativePathElement[count];
for (int ii = 0; ii < count; ii++)
{
elements[ii] = new RelativePathElement
{
ReferenceTypeId = ReferenceTypeIds.HasComponent,
IsInverse = ii % 2 != 0,
TargetName = new QualifiedName(ii % 2 == 0 ? BrowseNames.ServerStatus : BrowseNames.Server)
};
}
using var context = new OperationContext(
new RequestHeader(), null, RequestType.Browse, RequestLifetime.None, new UserIdentity());
var path = new BrowsePath
{
StartingNode = ObjectIds.Server,
RelativePath = new RelativePath { Elements = elements }
};
(ArrayOf results, _) = await Task.Run(async () =>
await m_server.CurrentInstance.NodeManager.TranslateBrowsePathsToNodeIdsAsync(
context, [path]).ConfigureAwait(false)).ConfigureAwait(false);
Assert.That(results, Has.Count.EqualTo(1));
Assert.That(results[0].StatusCode, Is.EqualTo(expectedStatus));
if (accepted)
{
Assert.That(results[0].Targets, Has.Count.EqualTo(1));
Assert.That(results[0].Targets[0].TargetId, Is.EqualTo((ExpandedNodeId)ObjectIds.Server));
Assert.That(results[0].Targets[0].RemainingPathIndex, Is.EqualTo(uint.MaxValue));
}
else
{
Assert.That(results[0].Targets, Is.Empty);
}
}
[Test]
public void Constructor_NullServer_ThrowsArgumentNullException()
{
Assert.That(
() => new MasterNodeManager(
null!,
m_fixture.Config,
null,
Array.Empty()),
Throws.TypeOf()
.With.Property("ParamName").EqualTo("server"));
}
[Test]
public void Constructor_NullConfiguration_ThrowsArgumentNullException()
{
Assert.That(
() => new MasterNodeManager(
m_server.CurrentInstance,
null!,
null,
Array.Empty()),
Throws.TypeOf()
.With.Property("ParamName").EqualTo("configuration"));
}
[Test]
public void Constructor_NoAdditionalManagers_RegistersConfigurationAndCore()
{
using MasterNodeManager sut = CreateMasterNodeManager();
Assert.That(sut.AsyncNodeManagers, Has.Count.EqualTo(2));
Assert.That(sut.NodeManagers, Has.Count.EqualTo(2));
}
[Test]
public void RegisterNodes_UnknownNodeIds_ReturnsInputNodeIds()
{
using MasterNodeManager sut = CreateMasterNodeManager();
OperationContext ctx = CreateContext();
ArrayOf input = new NodeId[]
{
new(1000u),
new("register-me", 0)
}.ToArrayOf();
sut.RegisterNodes(ctx, input, out ArrayOf registered);
Assert.That(registered.Count, Is.EqualTo(2));
Assert.That(registered[0], Is.EqualTo(new NodeId(1000u)));
Assert.That(registered[1], Is.EqualTo(new NodeId("register-me", 0)));
}
[Test]
public void BrowseAsync_NullContext_ThrowsArgumentNullException()
{
using MasterNodeManager sut = CreateMasterNodeManager();
Assert.That(
async () => await sut.BrowseAsync(
null!,
new ViewDescription(),
0u,
Array.Empty().ToArrayOf(),
cancellationToken: CancellationToken.None).ConfigureAwait(false),
Throws.TypeOf()
.With.Property("ParamName").EqualTo("context"));
}
[Test]
public void BrowseAsync_UnknownViewId_ThrowsBadViewIdUnknown()
{
using MasterNodeManager sut = CreateMasterNodeManager();
OperationContext ctx = CreateContext();
var view = new ViewDescription { ViewId = new NodeId(99999u) };
Assert.That(
async () => await sut.BrowseAsync(
ctx,
view,
0u,
Array.Empty().ToArrayOf(),
cancellationToken: CancellationToken.None).ConfigureAwait(false),
Throws.TypeOf()
.With.Property(nameof(ServiceResultException.StatusCode))
.EqualTo(StatusCodes.BadViewIdUnknown));
}
[TestCase(true, false)]
[TestCase(false, true)]
[TestCase(true, true)]
public void BrowseAsyncNullViewIdWithVersionParametersThrowsViewParameterError(
bool setTimestamp,
bool setVersion)
{
using MasterNodeManager sut = CreateMasterNodeManager();
OperationContext ctx = CreateContext();
// OPC 10000-4 Table 178: a timestamp/version for the entire AddressSpace
// is not available, which is not the same as an unknown View.
var view = new ViewDescription
{
Timestamp = setTimestamp ? DateTimeUtc.Now : DateTimeUtc.MinValue,
ViewVersion = setVersion ? 3u : 0u
};
StatusCode expected = setTimestamp && setVersion
? StatusCodes.BadViewParameterMismatch
: setTimestamp ? StatusCodes.BadViewTimestampInvalid : StatusCodes.BadViewVersionInvalid;
Assert.That(
async () => await sut.BrowseAsync(
ctx,
view,
0u,
new[] { new BrowseDescription { NodeId = ObjectIds.ObjectsFolder } }.ToArrayOf(),
cancellationToken: CancellationToken.None).ConfigureAwait(false),
Throws.TypeOf()
.With.Property(nameof(ServiceResultException.StatusCode))
.EqualTo(expected));
}
[Test]
public async Task BrowseAsync_EmptyBatch_ReturnsEmptyResultsAsync()
{
using MasterNodeManager sut = CreateMasterNodeManager();
OperationContext ctx = CreateContext();
(ArrayOf results, _) = await sut.BrowseAsync(
ctx,
new ViewDescription(),
0u,
Array.Empty().ToArrayOf(),
cancellationToken: CancellationToken.None).ConfigureAwait(false);
Assert.That(results.Count, Is.Zero);
}
[Test]
public async Task BrowseAsync_UnknownNode_ReturnsBadNodeIdUnknownAsync()
{
using MasterNodeManager sut = CreateMasterNodeManager();
OperationContext ctx = CreateContext();
var nodeToBrowse = new BrowseDescription
{
NodeId = new NodeId(99999u),
BrowseDirection = BrowseDirection.Forward
};
(ArrayOf results, _) = await sut.BrowseAsync(
ctx,
new ViewDescription(),
0u,
new BrowseDescription[] { nodeToBrowse }.ToArrayOf(),
cancellationToken: CancellationToken.None).ConfigureAwait(false);
Assert.That(results.Count, Is.EqualTo(1));
Assert.That(results[0].StatusCode, Is.EqualTo(StatusCodes.BadNodeIdUnknown));
Assert.That(results[0].ContinuationPoint.IsNull, Is.True);
}
[Test]
public async Task BrowseAsyncCompletedBrowseReturnsNullContinuationPointAsync()
{
IMasterNodeManager sut = m_server.CurrentInstance.NodeManager;
OperationContext ctx = CreateContext();
var nodeToBrowse = new BrowseDescription
{
NodeId = ObjectIds.ViewsFolder,
BrowseDirection = BrowseDirection.Forward,
ReferenceTypeId = ReferenceTypeIds.HierarchicalReferences,
IncludeSubtypes = true,
ResultMask = (uint)BrowseResultMask.All
};
(ArrayOf results, _) = await sut.BrowseAsync(
ctx,
new ViewDescription(),
0u,
new BrowseDescription[] { nodeToBrowse }.ToArrayOf(),
cancellationToken: CancellationToken.None).ConfigureAwait(false);
Assert.That(results.Count, Is.EqualTo(1));
Assert.That(results[0].StatusCode, Is.EqualTo(StatusCodes.Good));
Assert.That(results[0].ContinuationPoint.IsNull, Is.True);
}
[Test]
public async Task BrowseAsync_UnknownReferenceType_ReturnsBadReferenceTypeIdInvalidAsync()
{
IMasterNodeManager sut = m_server.CurrentInstance.NodeManager;
OperationContext ctx = CreateContext();
var nodeToBrowse = new BrowseDescription
{
NodeId = ObjectIds.ObjectsFolder,
ReferenceTypeId = new NodeId(88888u),
BrowseDirection = BrowseDirection.Forward
};
(ArrayOf results, _) = await sut.BrowseAsync(
ctx,
new ViewDescription(),
0u,
new BrowseDescription[] { nodeToBrowse }.ToArrayOf(),
cancellationToken: CancellationToken.None).ConfigureAwait(false);
Assert.That(results[0].StatusCode, Is.EqualTo(StatusCodes.BadReferenceTypeIdInvalid));
}
[Test]
public async Task BrowseAsync_KnownNonReferenceType_ReturnsBadReferenceTypeIdInvalidAsync()
{
IMasterNodeManager sut = m_server.CurrentInstance.NodeManager;
OperationContext ctx = CreateContext();
var nodeToBrowse = new BrowseDescription
{
NodeId = ObjectIds.ObjectsFolder,
ReferenceTypeId = ObjectTypeIds.BaseObjectType,
BrowseDirection = BrowseDirection.Forward
};
(ArrayOf results, _) = await sut.BrowseAsync(
ctx,
new ViewDescription(),
0u,
new BrowseDescription[] { nodeToBrowse }.ToArrayOf(),
cancellationToken: CancellationToken.None).ConfigureAwait(false);
Assert.That(results[0].StatusCode, Is.EqualTo(StatusCodes.BadReferenceTypeIdInvalid));
}
[Test]
public async Task BrowseAsync_InvalidBrowseDirection_ReturnsBadBrowseDirectionInvalidAsync()
{
IMasterNodeManager sut = m_server.CurrentInstance.NodeManager;
OperationContext ctx = CreateContext();
var nodeToBrowse = new BrowseDescription
{
NodeId = ObjectIds.ObjectsFolder,
BrowseDirection = (BrowseDirection)99
};
(ArrayOf results, _) = await sut.BrowseAsync(
ctx,
new ViewDescription(),
0u,
new BrowseDescription[] { nodeToBrowse }.ToArrayOf(),
cancellationToken: CancellationToken.None).ConfigureAwait(false);
Assert.That(results[0].StatusCode, Is.EqualTo(StatusCodes.BadBrowseDirectionInvalid));
}
[Test]
public async Task BrowseAsyncWhenPositiveContinuationQuotaIsExhaustedReturnsBadNoContinuationPointsAsync()
{
var sut = (MasterNodeManager)m_server.CurrentInstance.NodeManager;
using OperationContext ctx = CreateContextWithContinuationStore();
uint originalLimit = GetMaxBrowseContinuationPointsPerBrowse(sut);
try
{
SetMaxBrowseContinuationPointsPerBrowse(sut, 1u);
var nodeToBrowse = new BrowseDescription
{
NodeId = ObjectIds.RootFolder,
BrowseDirection = BrowseDirection.Forward
};
(ArrayOf results, _) = await sut.BrowseAsync(
ctx,
new ViewDescription(),
1u,
[nodeToBrowse, nodeToBrowse],
cancellationToken: CancellationToken.None).ConfigureAwait(false);
Assert.That(results.Count, Is.EqualTo(2));
Assert.That(results[0].StatusCode, Is.EqualTo(StatusCodes.Good));
Assert.That(results[0].References, Has.Count.EqualTo(1));
Assert.That(results[0].ContinuationPoint.IsEmpty, Is.False);
Assert.That(results[1].StatusCode, Is.EqualTo(StatusCodes.BadNoContinuationPoints));
Assert.That(results[1].ContinuationPoint.IsEmpty, Is.True);
(ArrayOf released, _) = await sut.BrowseNextAsync(
ctx, true, [results[0].ContinuationPoint]).ConfigureAwait(false);
Assert.That(released[0].StatusCode, Is.EqualTo(StatusCodes.Good));
}
finally
{
SetMaxBrowseContinuationPointsPerBrowse(sut, originalLimit);
}
}
[Test]
public void BrowseNextAsync_NullContext_ThrowsArgumentNullException()
{
using MasterNodeManager sut = CreateMasterNodeManager();
Assert.That(
async () => await sut.BrowseNextAsync(
null!,
false,
Array.Empty().ToArrayOf(),
cancellationToken: CancellationToken.None).ConfigureAwait(false),
Throws.TypeOf()
.With.Property("ParamName").EqualTo("context"));
}
[Test]
public async Task BrowseNextAsync_EmptyBatch_ReturnsEmptyResultsAsync()
{
using MasterNodeManager sut = CreateMasterNodeManager();
OperationContext ctx = CreateContext();
(ArrayOf results, _) = await sut.BrowseNextAsync(
ctx,
false,
Array.Empty().ToArrayOf(),
cancellationToken: CancellationToken.None).ConfigureAwait(false);
Assert.That(results.Count, Is.Zero);
}
[Test]
public async Task BrowseNextAsync_InvalidContinuationPoint_ReturnsBadContinuationPointInvalidAsync()
{
using MasterNodeManager sut = CreateMasterNodeManager();
OperationContext ctx = CreateContext();
ArrayOf continuationPoints = new ByteString[]
{
new byte[] { 1, 2, 3, 4 }.ToByteString()
}.ToArrayOf();
(ArrayOf results, _) = await sut.BrowseNextAsync(
ctx,
false,
continuationPoints,
cancellationToken: CancellationToken.None).ConfigureAwait(false);
Assert.That(results.Count, Is.EqualTo(1));
Assert.That(results[0].StatusCode, Is.EqualTo(StatusCodes.BadContinuationPointInvalid));
Assert.That(results[0].ContinuationPoint.IsNull, Is.True);
}
[Test]
public async Task BrowseNextAsync_ReleaseInvalidContinuationPoint_ReturnsGoodAsync()
{
using MasterNodeManager sut = CreateMasterNodeManager();
OperationContext ctx = CreateContext();
ArrayOf continuationPoints = new ByteString[]
{
new byte[] { 1, 2, 3, 4 }.ToByteString()
}.ToArrayOf();
(ArrayOf results, _) = await sut.BrowseNextAsync(
ctx,
true,
continuationPoints,
cancellationToken: CancellationToken.None).ConfigureAwait(false);
Assert.That(results.Count, Is.EqualTo(1));
Assert.That(results[0].StatusCode, Is.EqualTo(StatusCodes.Good));
Assert.That(results[0].ContinuationPoint.IsNull, Is.True);
}
[Test]
public async Task BrowseNextAsyncWhenPositiveContinuationQuotaIsExhaustedReturnsBadNoContinuationPointsAsync()
{
var sut = (MasterNodeManager)m_server.CurrentInstance.NodeManager;
using OperationContext ctx = CreateContextWithContinuationStore();
uint originalLimit = GetMaxBrowseContinuationPointsPerBrowse(sut);
try
{
SetMaxBrowseContinuationPointsPerBrowse(sut, 2u);
var nodeToBrowse = new BrowseDescription
{
NodeId = ObjectIds.RootFolder,
BrowseDirection = BrowseDirection.Forward
};
(ArrayOf firstResults, _) = await sut.BrowseAsync(
ctx,
new ViewDescription(),
1u,
[nodeToBrowse, nodeToBrowse],
cancellationToken: CancellationToken.None).ConfigureAwait(false);
Assert.That(firstResults.Count, Is.EqualTo(2));
foreach (BrowseResult result in firstResults)
{
Assert.That(result.StatusCode, Is.EqualTo(StatusCodes.Good));
Assert.That(result.References, Has.Count.EqualTo(1));
Assert.That(result.ContinuationPoint.IsEmpty, Is.False);
}
SetMaxBrowseContinuationPointsPerBrowse(sut, 1u);
(ArrayOf finalResults, _) = await sut.BrowseNextAsync(
ctx,
false,
[firstResults[0].ContinuationPoint, firstResults[1].ContinuationPoint],
cancellationToken: CancellationToken.None).ConfigureAwait(false);
Assert.That(finalResults.Count, Is.EqualTo(2));
Assert.That(finalResults[0].StatusCode, Is.EqualTo(StatusCodes.Good));
Assert.That(finalResults[0].References, Has.Count.EqualTo(1));
Assert.That(finalResults[0].ContinuationPoint.IsEmpty, Is.False);
Assert.That(finalResults[1].StatusCode, Is.EqualTo(StatusCodes.BadNoContinuationPoints));
Assert.That(finalResults[1].ContinuationPoint.IsEmpty, Is.True);
(ArrayOf released, _) = await sut.BrowseNextAsync(
ctx, true, [finalResults[0].ContinuationPoint]).ConfigureAwait(false);
Assert.That(released[0].StatusCode, Is.EqualTo(StatusCodes.Good));
}
finally
{
SetMaxBrowseContinuationPointsPerBrowse(sut, originalLimit);
}
}
[Test]
public async Task TranslateBrowsePathsToNodeIdsAsync_EmptyBatch_ReturnsEmptyResultsAsync()
{
using MasterNodeManager sut = CreateMasterNodeManager();
OperationContext ctx = CreateContext();
(ArrayOf results, _) = await sut.TranslateBrowsePathsToNodeIdsAsync(
ctx,
Array.Empty().ToArrayOf(),
cancellationToken: CancellationToken.None).ConfigureAwait(false);
Assert.That(results.Count, Is.Zero);
}
[Test]
public async Task TranslateBrowsePathsToNodeIdsAsync_UnknownStartingNode_ReturnsBadNodeIdUnknownAsync()
{
using MasterNodeManager sut = CreateMasterNodeManager();
OperationContext ctx = CreateContext();
var browsePath = new BrowsePath
{
StartingNode = new NodeId(99999u),
RelativePath = new RelativePath
{
Elements = new RelativePathElement[]
{
new() { TargetName = new QualifiedName("Any", 0) }
}.ToArrayOf()
}
};
(ArrayOf results, _) = await sut.TranslateBrowsePathsToNodeIdsAsync(
ctx,
new BrowsePath[] { browsePath }.ToArrayOf(),
cancellationToken: CancellationToken.None).ConfigureAwait(false);
Assert.That(results[0].StatusCode, Is.EqualTo(StatusCodes.BadNodeIdUnknown));
}
[Test]
public async Task TranslateBrowsePathsToNodeIdsAsync_EmptyRelativePath_ReturnsBadNothingToDoAsync()
{
IMasterNodeManager sut = m_server.CurrentInstance.NodeManager;
OperationContext ctx = CreateContext();
var browsePath = new BrowsePath
{
StartingNode = ObjectIds.ObjectsFolder,
RelativePath = new RelativePath()
};
(ArrayOf results, _) = await sut.TranslateBrowsePathsToNodeIdsAsync(
ctx,
new BrowsePath[] { browsePath }.ToArrayOf(),
cancellationToken: CancellationToken.None).ConfigureAwait(false);
Assert.That(results[0].StatusCode, Is.EqualTo(StatusCodes.BadNothingToDo));
}
[Test]
public async Task TranslateBrowsePathsToNodeIdsAsync_NullTargetName_ReturnsBadBrowseNameInvalidAsync()
{
IMasterNodeManager sut = m_server.CurrentInstance.NodeManager;
OperationContext ctx = CreateContext();
var browsePath = new BrowsePath
{
StartingNode = ObjectIds.ObjectsFolder,
RelativePath = new RelativePath
{
Elements = new RelativePathElement[]
{
new()
}.ToArrayOf()
}
};
(ArrayOf results, _) = await sut.TranslateBrowsePathsToNodeIdsAsync(
ctx,
new BrowsePath[] { browsePath }.ToArrayOf(),
cancellationToken: CancellationToken.None).ConfigureAwait(false);
Assert.That(results[0].StatusCode, Is.EqualTo(StatusCodes.BadBrowseNameInvalid));
}
[Test]
public void ReadAsync_NegativeMaxAge_ThrowsBadMaxAgeInvalid()
{
using MasterNodeManager sut = CreateMasterNodeManager();
OperationContext ctx = CreateContext();
Assert.That(
async () => await sut.ReadAsync(
ctx,
-1.0,
TimestampsToReturn.Neither,
Array.Empty().ToArrayOf(),
cancellationToken: CancellationToken.None).ConfigureAwait(false),
Throws.TypeOf()
.With.Property(nameof(ServiceResultException.StatusCode))
.EqualTo(StatusCodes.BadMaxAgeInvalid));
}
[Test]
public void ReadAsync_InvalidTimestampsToReturn_ThrowsBadTimestampsToReturnInvalid()
{
using MasterNodeManager sut = CreateMasterNodeManager();
OperationContext ctx = CreateContext();
Assert.That(
async () => await sut.ReadAsync(
ctx,
0.0,
(TimestampsToReturn)99,
Array.Empty().ToArrayOf(),
cancellationToken: CancellationToken.None).ConfigureAwait(false),
Throws.TypeOf()
.With.Property(nameof(ServiceResultException.StatusCode))
.EqualTo(StatusCodes.BadTimestampsToReturnInvalid));
}
[Test]
public async Task ReadAsync_EmptyBatch_ReturnsEmptyResultsAsync()
{
using MasterNodeManager sut = CreateMasterNodeManager();
OperationContext ctx = CreateContext();
(ArrayOf values, _) = await sut.ReadAsync(
ctx,
0.0,
TimestampsToReturn.Neither,
Array.Empty().ToArrayOf(),
cancellationToken: CancellationToken.None).ConfigureAwait(false);
Assert.That(values.Count, Is.Zero);
}
[Test]
public async Task ReadAsync_NullNodeId_ReturnsBadNodeIdInvalidAsync()
{
using MasterNodeManager sut = CreateMasterNodeManager();
OperationContext ctx = CreateContext();
(ArrayOf values, _) = await sut.ReadAsync(
ctx,
0.0,
TimestampsToReturn.Neither,
new ReadValueId[] { new() { AttributeId = Attributes.Value } }.ToArrayOf(),
cancellationToken: CancellationToken.None).ConfigureAwait(false);
Assert.That(values[0].StatusCode, Is.EqualTo(StatusCodes.BadNodeIdInvalid));
}
[Test]
public async Task ReadAsync_InvalidAttributeId_ReturnsBadAttributeIdInvalidAsync()
{
using MasterNodeManager sut = CreateMasterNodeManager();
OperationContext ctx = CreateContext();
var readValueId = new ReadValueId { NodeId = ObjectIds.Server, AttributeId = 0 };
(ArrayOf values, _) = await sut.ReadAsync(
ctx,
0.0,
TimestampsToReturn.Neither,
new ReadValueId[] { readValueId }.ToArrayOf(),
cancellationToken: CancellationToken.None).ConfigureAwait(false);
Assert.That(values[0].StatusCode, Is.EqualTo(StatusCodes.BadAttributeIdInvalid));
}
[Test]
public async Task ReadAsync_UnknownNode_ReturnsBadNodeIdUnknownAsync()
{
using MasterNodeManager sut = CreateMasterNodeManager();
OperationContext ctx = CreateContext();
var readValueId = new ReadValueId { NodeId = new NodeId(99999u), AttributeId = Attributes.Value };
(ArrayOf values, _) = await sut.ReadAsync(
ctx,
0.0,
TimestampsToReturn.Neither,
new ReadValueId[] { readValueId }.ToArrayOf(),
cancellationToken: CancellationToken.None).ConfigureAwait(false);
Assert.That(values[0].StatusCode, Is.EqualTo(StatusCodes.BadNodeIdUnknown));
}
[Test]
public void WriteAsync_NullContext_ThrowsArgumentNullException()
{
using MasterNodeManager sut = CreateMasterNodeManager();
Assert.That(
async () => await sut.WriteAsync(
null!,
Array.Empty().ToArrayOf(),
cancellationToken: CancellationToken.None).ConfigureAwait(false),
Throws.TypeOf()
.With.Property("ParamName").EqualTo("context"));
}
[Test]
public async Task WriteAsync_EmptyBatch_ReturnsEmptyResultsAsync()
{
using MasterNodeManager sut = CreateMasterNodeManager();
OperationContext ctx = CreateContext();
(ArrayOf results, _) = await sut.WriteAsync(
ctx,
Array.Empty().ToArrayOf(),
cancellationToken: CancellationToken.None).ConfigureAwait(false);
Assert.That(results.Count, Is.Zero);
}
[Test]
public async Task WriteAsync_NullNodeId_ReturnsBadNodeIdInvalidAsync()
{
using MasterNodeManager sut = CreateMasterNodeManager();
OperationContext ctx = CreateContext();
(ArrayOf results, _) = await sut.WriteAsync(
ctx,
new WriteValue[] { new() { AttributeId = Attributes.Value } }.ToArrayOf(),
cancellationToken: CancellationToken.None).ConfigureAwait(false);
Assert.That(results[0], Is.EqualTo(StatusCodes.BadNodeIdInvalid));
}
[Test]
public async Task WriteAsync_UnknownNode_ReturnsBadNodeIdUnknownAsync()
{
using MasterNodeManager sut = CreateMasterNodeManager();
OperationContext ctx = CreateContext();
var writeValue = new WriteValue
{
NodeId = new NodeId(99999u),
AttributeId = Attributes.Value,
Value = new DataValue(new Variant(123))
};
(ArrayOf results, _) = await sut.WriteAsync(
ctx,
new WriteValue[] { writeValue }.ToArrayOf(),
cancellationToken: CancellationToken.None).ConfigureAwait(false);
Assert.That(results[0], Is.EqualTo(StatusCodes.BadNodeIdUnknown));
}
[Test]
public void HistoryReadAsync_NullDetails_ThrowsBadHistoryOperationInvalid()
{
using MasterNodeManager sut = CreateMasterNodeManager();
OperationContext ctx = CreateContext();
Assert.That(
async () => await sut.HistoryReadAsync(
ctx,
default,
TimestampsToReturn.Neither,
false,
Array.Empty().ToArrayOf(),
cancellationToken: CancellationToken.None).ConfigureAwait(false),
Throws.TypeOf()
.With.Property(nameof(ServiceResultException.StatusCode))
.EqualTo(StatusCodes.BadHistoryOperationInvalid));
}
[Test]
public async Task HistoryReadAsync_EmptyBatch_ReturnsEmptyResultsAsync()
{
using MasterNodeManager sut = CreateMasterNodeManager();
OperationContext ctx = CreateContext();
(ArrayOf results, _) = await sut.HistoryReadAsync(
ctx,
new ExtensionObject(new ReadRawModifiedDetails()),
TimestampsToReturn.Neither,
false,
Array.Empty().ToArrayOf(),
cancellationToken: CancellationToken.None).ConfigureAwait(false);
Assert.That(results.Count, Is.Zero);
}
[Test]
public async Task HistoryReadAsync_NullNodeId_ReturnsBadNodeIdInvalidAsync()
{
using MasterNodeManager sut = CreateMasterNodeManager();
OperationContext ctx = CreateContext();
(ArrayOf results, _) = await sut.HistoryReadAsync(
ctx,
new ExtensionObject(new ReadRawModifiedDetails()),
TimestampsToReturn.Neither,
false,
new HistoryReadValueId[] { new() }.ToArrayOf(),
cancellationToken: CancellationToken.None).ConfigureAwait(false);
Assert.That(results[0].StatusCode, Is.EqualTo(StatusCodes.BadNodeIdInvalid));
}
[Test]
public async Task HistoryReadAsync_UnknownNode_ReturnsBadNodeIdUnknownAsync()
{
using MasterNodeManager sut = CreateMasterNodeManager();
OperationContext ctx = CreateContext();
var historyReadValueId = new HistoryReadValueId { NodeId = new NodeId(99999u) };
(ArrayOf results, _) = await sut.HistoryReadAsync(
ctx,
new ExtensionObject(new ReadRawModifiedDetails()),
TimestampsToReturn.Neither,
false,
new HistoryReadValueId[] { historyReadValueId }.ToArrayOf(),
cancellationToken: CancellationToken.None).ConfigureAwait(false);
Assert.That(results[0].StatusCode, Is.EqualTo(StatusCodes.BadNodeIdUnknown));
}
[Test]
public async Task HistoryUpdateAsync_EmptyBatch_ReturnsEmptyResultsAsync()
{
using MasterNodeManager sut = CreateMasterNodeManager();
OperationContext ctx = CreateContext();
(ArrayOf results, _) = await sut.HistoryUpdateAsync(
ctx,
Array.Empty().ToArrayOf(),
cancellationToken: CancellationToken.None).ConfigureAwait(false);
Assert.That(results.Count, Is.Zero);
}
[Test]
public async Task HistoryUpdateAsync_NullNodeId_ReturnsBadNodeIdInvalidAsync()
{
using MasterNodeManager sut = CreateMasterNodeManager();
OperationContext ctx = CreateContext();
var details = new ExtensionObject(new UpdateDataDetails
{
PerformInsertReplace = PerformUpdateType.Insert
});
(ArrayOf results, _) = await sut.HistoryUpdateAsync(
ctx,
new ExtensionObject[] { details }.ToArrayOf(),
cancellationToken: CancellationToken.None).ConfigureAwait(false);
Assert.That(results[0].StatusCode, Is.EqualTo(StatusCodes.BadNodeIdInvalid));
}
[Test]
public async Task HistoryUpdateAsync_UnknownNode_ReturnsBadNodeIdUnknownAsync()
{
using MasterNodeManager sut = CreateMasterNodeManager();
OperationContext ctx = CreateContext();
var details = new ExtensionObject(new UpdateDataDetails
{
NodeId = new NodeId(99999u),
PerformInsertReplace = PerformUpdateType.Insert
});
(ArrayOf results, _) = await sut.HistoryUpdateAsync(
ctx,
new ExtensionObject[] { details }.ToArrayOf(),
cancellationToken: CancellationToken.None).ConfigureAwait(false);
Assert.That(results[0].StatusCode, Is.EqualTo(StatusCodes.BadNodeIdUnknown));
}
[Test]
public void CallAsync_NullContext_ThrowsArgumentNullException()
{
using MasterNodeManager sut = CreateMasterNodeManager();
Assert.That(
async () => await sut.CallAsync(
null!,
Array.Empty().ToArrayOf(),
cancellationToken: CancellationToken.None).ConfigureAwait(false),
Throws.TypeOf()
.With.Property("ParamName").EqualTo("context"));
}
[Test]
public async Task CallAsync_EmptyBatch_ReturnsEmptyResultsAsync()
{
using MasterNodeManager sut = CreateMasterNodeManager();
OperationContext ctx = CreateContext();
(ArrayOf results, _) = await sut.CallAsync(
ctx,
Array.Empty().ToArrayOf(),
cancellationToken: CancellationToken.None).ConfigureAwait(false);
Assert.That(results.Count, Is.Zero);
}
[Test]
public async Task CallAsync_NullObjectId_ReturnsBadNodeIdInvalidAsync()
{
using MasterNodeManager sut = CreateMasterNodeManager();
OperationContext ctx = CreateContext();
(ArrayOf results, _) = await sut.CallAsync(
ctx,
new CallMethodRequest[] { new() }.ToArrayOf(),
cancellationToken: CancellationToken.None).ConfigureAwait(false);
Assert.That(results[0].StatusCode, Is.EqualTo(StatusCodes.BadNodeIdInvalid));
}
[Test]
public async Task CallAsync_NullMethodId_ReturnsBadMethodInvalidAsync()
{
using MasterNodeManager sut = CreateMasterNodeManager();
OperationContext ctx = CreateContext();
var request = new CallMethodRequest { ObjectId = ObjectIds.Server };
(ArrayOf results, _) = await sut.CallAsync(
ctx,
new CallMethodRequest[] { request }.ToArrayOf(),
cancellationToken: CancellationToken.None).ConfigureAwait(false);
Assert.That(results[0].StatusCode, Is.EqualTo(StatusCodes.BadMethodInvalid));
}
[Test]
public async Task CallAsync_UnknownObject_ReturnsBadNodeIdUnknownAsync()
{
using MasterNodeManager sut = CreateMasterNodeManager();
OperationContext ctx = CreateContext();
var request = new CallMethodRequest
{
ObjectId = new NodeId(99999u),
MethodId = new NodeId(99998u)
};
(ArrayOf results, _) = await sut.CallAsync(
ctx,
new CallMethodRequest[] { request }.ToArrayOf(),
cancellationToken: CancellationToken.None).ConfigureAwait(false);
Assert.That(results[0].StatusCode, Is.EqualTo(StatusCodes.BadNodeIdUnknown));
}
[Test]
public void CreateMonitoredItemsAsync_NullContext_ThrowsArgumentNullException()
{
using MasterNodeManager sut = CreateMasterNodeManager();
Assert.That(
async () => await sut.CreateMonitoredItemsAsync(
null!,
1u,
0.0,
TimestampsToReturn.Both,
Array.Empty().ToArrayOf(),
[],
[],
[],
false,
cancellationToken: CancellationToken.None).ConfigureAwait(false),
Throws.TypeOf()
.With.Property("ParamName").EqualTo("context"));
}
[Test]
public void CreateMonitoredItemsAsync_NullErrors_ThrowsArgumentNullException()
{
using MasterNodeManager sut = CreateMasterNodeManager();
OperationContext ctx = CreateContext();
Assert.That(
async () => await sut.CreateMonitoredItemsAsync(
ctx,
1u,
0.0,
TimestampsToReturn.Both,
Array.Empty().ToArrayOf(),
null!,
[],
[],
false,
cancellationToken: CancellationToken.None).ConfigureAwait(false),
Throws.TypeOf()
.With.Property("ParamName").EqualTo("errors"));
}
[Test]
public void CreateMonitoredItemsAsync_NullMonitoredItems_ThrowsArgumentNullException()
{
using MasterNodeManager sut = CreateMasterNodeManager();
OperationContext ctx = CreateContext();
Assert.That(
async () => await sut.CreateMonitoredItemsAsync(
ctx,
1u,
0.0,
TimestampsToReturn.Both,
Array.Empty().ToArrayOf(),
[],
[],
null!,
false,
cancellationToken: CancellationToken.None).ConfigureAwait(false),
Throws.TypeOf()
.With.Property("ParamName").EqualTo("monitoredItems"));
}
[Test]
public void CreateMonitoredItemsAsync_NegativePublishingInterval_ThrowsArgumentOutOfRangeException()
{
using MasterNodeManager sut = CreateMasterNodeManager();
OperationContext ctx = CreateContext();
Assert.That(
async () => await sut.CreateMonitoredItemsAsync(
ctx,
1u,
-1.0,
TimestampsToReturn.Both,
Array.Empty().ToArrayOf(),
[],
[],
[],
false,
cancellationToken: CancellationToken.None).ConfigureAwait(false),
Throws.TypeOf()
.With.Property("ParamName").EqualTo("publishingInterval"));
}
[Test]
public void CreateMonitoredItemsAsync_InvalidTimestampsToReturn_ThrowsBadTimestampsToReturnInvalid()
{
using MasterNodeManager sut = CreateMasterNodeManager();
OperationContext ctx = CreateContext();
Assert.That(
async () => await sut.CreateMonitoredItemsAsync(
ctx,
1u,
0.0,
(TimestampsToReturn)99,
Array.Empty().ToArrayOf(),
[],
[],
[],
false,
cancellationToken: CancellationToken.None).ConfigureAwait(false),
Throws.TypeOf()
.With.Property(nameof(ServiceResultException.StatusCode))
.EqualTo(StatusCodes.BadTimestampsToReturnInvalid));
}
[Test]
public void ModifyMonitoredItemsAsync_NullContext_ThrowsArgumentNullException()
{
using MasterNodeManager sut = CreateMasterNodeManager();
Assert.That(
async () => await sut.ModifyMonitoredItemsAsync(
null!,
TimestampsToReturn.Both,
[],
Array.Empty().ToArrayOf(),
[],
[],
cancellationToken: CancellationToken.None).ConfigureAwait(false),
Throws.TypeOf()
.With.Property("ParamName").EqualTo("context"));
}
[Test]
public void ModifyMonitoredItemsAsync_InvalidTimestampsToReturn_ThrowsBadTimestampsToReturnInvalid()
{
using MasterNodeManager sut = CreateMasterNodeManager();
OperationContext ctx = CreateContext();
Assert.That(
async () => await sut.ModifyMonitoredItemsAsync(
ctx,
(TimestampsToReturn)99,
[],
Array.Empty().ToArrayOf(),
[],
[],
cancellationToken: CancellationToken.None).ConfigureAwait(false),
Throws.TypeOf()
.With.Property(nameof(ServiceResultException.StatusCode))
.EqualTo(StatusCodes.BadTimestampsToReturnInvalid));
}
[Test]
public void DeleteMonitoredItemsAsync_NullContext_ThrowsArgumentNullException()
{
using MasterNodeManager sut = CreateMasterNodeManager();
Assert.That(
async () => await sut.DeleteMonitoredItemsAsync(
null!,
1u,
[],
[],
cancellationToken: CancellationToken.None).ConfigureAwait(false),
Throws.TypeOf()
.With.Property("ParamName").EqualTo("context"));
}
[Test]
public async Task DeleteMonitoredItemsAsync_UnknownItem_ReturnsBadMonitoredItemIdInvalidAsync()
{
using MasterNodeManager sut = CreateMasterNodeManager();
OperationContext ctx = CreateContext();
IMonitoredItem item = new Mock().Object;
var itemsToDelete = new List { item };
var errors = new List { ServiceResult.Good };
await sut.DeleteMonitoredItemsAsync(
ctx,
1u,
itemsToDelete,
errors,
cancellationToken: CancellationToken.None).ConfigureAwait(false);
Assert.That(errors[0].StatusCode, Is.EqualTo(StatusCodes.BadMonitoredItemIdInvalid));
}
[Test]
public async Task DeleteMonitoredItemsAsyncDetachedItemReturnsGoodAsync()
{
using MasterNodeManager sut = CreateMasterNodeManager();
using MonitoredItem item = CreateDetachedMonitoredItem();
var itemsToDelete = new List { item };
var errors = new List { ServiceResult.Good };
await sut.DeleteMonitoredItemsAsync(
CreateContext(),
1u,
itemsToDelete,
errors,
cancellationToken: CancellationToken.None).ConfigureAwait(false);
Assert.That(errors[0].StatusCode, Is.EqualTo(StatusCodes.Good));
}
[Test]
public async Task ModifyMonitoredItemsAsyncDetachedItemReturnsBadNodeIdUnknownAsync()
{
using MasterNodeManager sut = CreateMasterNodeManager();
using MonitoredItem item = CreateDetachedMonitoredItem();
var request = new MonitoredItemModifyRequest
{
RequestedParameters = new MonitoringParameters
{
ClientHandle = item.ClientHandle,
SamplingInterval = item.SamplingInterval,
QueueSize = item.QueueSize,
DiscardOldest = true
}
};
var errors = new List { ServiceResult.Good };
var filterResults = new List { null! };
await sut.ModifyMonitoredItemsAsync(
CreateContext(),
TimestampsToReturn.Both,
[item],
new[] { request }.ToArrayOf(),
errors,
filterResults,
cancellationToken: CancellationToken.None).ConfigureAwait(false);
Assert.That(errors[0].StatusCode, Is.EqualTo(StatusCodes.BadNodeIdUnknown));
Assert.That(request.Processed, Is.True);
}
[Test]
public void SetMonitoringModeAsync_NullContext_ThrowsArgumentNullException()
{
using MasterNodeManager sut = CreateMasterNodeManager();
Assert.That(
async () => await sut.SetMonitoringModeAsync(
null!,
MonitoringMode.Reporting,
[],
[],
cancellationToken: CancellationToken.None).ConfigureAwait(false),
Throws.TypeOf()
.With.Property("ParamName").EqualTo("context"));
}
[Test]
public async Task SetMonitoringModeAsync_UnknownItem_ReturnsBadMonitoredItemIdInvalidAsync()
{
using MasterNodeManager sut = CreateMasterNodeManager();
OperationContext ctx = CreateContext();
IMonitoredItem item = new Mock().Object;
var itemsToModify = new List { item };
var errors = new List { ServiceResult.Good };
await sut.SetMonitoringModeAsync(
ctx,
MonitoringMode.Reporting,
itemsToModify,
errors,
cancellationToken: CancellationToken.None).ConfigureAwait(false);
Assert.That(errors[0].StatusCode, Is.EqualTo(StatusCodes.BadMonitoredItemIdInvalid));
}
[Test]
public async Task SetMonitoringModeAsyncDetachedItemUpdatesLocallyAndQueuesBadAsync()
{
using MasterNodeManager sut = CreateMasterNodeManager();
using MonitoredItem item = CreateDetachedMonitoredItem(MonitoringMode.Disabled);
var errors = new List { ServiceResult.Good };
await sut.SetMonitoringModeAsync(
CreateContext(),
MonitoringMode.Reporting,
[item],
errors,
cancellationToken: CancellationToken.None).ConfigureAwait(false);
Assert.That(errors[0].StatusCode, Is.EqualTo(StatusCodes.Good));
Assert.That(item.MonitoringMode, Is.EqualTo(MonitoringMode.Reporting));
Assert.That(Publish(item).Peek().Value.StatusCode, Is.EqualTo(StatusCodes.BadNodeIdUnknown));
}
[Test]
public void TransferMonitoredItemsAsync_NullContext_ThrowsArgumentNullException()
{
using MasterNodeManager sut = CreateMasterNodeManager();
Assert.That(
async () => await sut.TransferMonitoredItemsAsync(
null!,
false,
[],
[],
new MonitoredItemTransferOptions(),
cancellationToken: CancellationToken.None).ConfigureAwait(false),
Throws.TypeOf()
.With.Property("ParamName").EqualTo("context"));
}
[Test]
public async Task TransferMonitoredItemsAsync_NullMonitoredItem_ReturnsBadMonitoredItemIdInvalidAsync()
{
using MasterNodeManager sut = CreateMasterNodeManager();
OperationContext ctx = CreateContext();
var monitoredItems = new List { null! };
var errors = new List { ServiceResult.Good };
await sut.TransferMonitoredItemsAsync(
ctx,
false,
monitoredItems,
errors,
new MonitoredItemTransferOptions(),
cancellationToken: CancellationToken.None).ConfigureAwait(false);
Assert.That(errors[0].StatusCode, Is.EqualTo(StatusCodes.BadMonitoredItemIdInvalid));
}
[Test]
public async Task TransferMonitoredItemsAsyncDetachedItemSucceedsAndQueuesBadAsync()
{
using MasterNodeManager sut = CreateMasterNodeManager();
using MonitoredItem item = CreateDetachedMonitoredItem();
var errors = new List { ServiceResult.Good };
await sut.TransferMonitoredItemsAsync(
CreateContext(),
true,
[item],
errors,
new MonitoredItemTransferOptions(),
cancellationToken: CancellationToken.None).ConfigureAwait(false);
Assert.That(errors[0].StatusCode, Is.EqualTo(StatusCodes.Good));
Assert.That(Publish(item).Peek().Value.StatusCode, Is.EqualTo(StatusCodes.BadNodeIdUnknown));
}
[Test]
public async Task TransferMonitoredItemsAsyncOwnerFailureLogsAndAllowsRetryAsync()
{
var sourceSession = new Mock();
sourceSession.SetupGet(session => session.Id).Returns(new NodeId(Guid.NewGuid()));
sourceSession.SetupGet(session => session.EffectiveIdentity)
.Returns(new Mock().Object);
sourceSession.SetupGet(session => session.PreferredLocales).Returns([]);
var destinationSession = new Mock();
destinationSession.SetupGet(session => session.Id).Returns(new NodeId(Guid.NewGuid()));
destinationSession.SetupGet(session => session.EffectiveIdentity)
.Returns(new Mock().Object);
destinationSession.SetupGet(session => session.PreferredLocales).Returns([]);
var owner = new Mock();
var laterOwner = new Mock();
ISession observedOwner = sourceSession.Object;
ISession observedLaterOwner = sourceSession.Object;
int ownerForwardCalls = 0;
int laterForwardCalls = 0;
bool failNextDestinationCallback = true;
owner.Setup(nodeManager => nodeManager.TransferMonitoredItemsAsync(
It.IsAny(),
It.IsAny(),
It.IsAny>(),
It.IsAny>(),
It.IsAny>(),
It.IsAny(),
It.IsAny()))
.Callback, IList, IList, MonitoredItemTransferOptions, CancellationToken>(
(context, sendInitialValues, monitoredItems, processedItems, errors, transferOptions, _) =>
{
for (int ii = 0; ii < monitoredItems.Count; ii++)
{
if (!processedItems[ii] &&
ReferenceEquals(monitoredItems[ii].NodeManager, owner.Object))
{
processedItems[ii] = true;
errors[ii] = ServiceResult.Good;
if (sendInitialValues &&
!transferOptions.DeferInitialValues)
{
monitoredItems[ii].SetupResendDataTrigger();
}
}
}
observedOwner = context.Session;
ownerForwardCalls++;
})
.Returns(default(ValueTask));
laterOwner.Setup(nodeManager => nodeManager.TransferMonitoredItemsAsync(
It.IsAny(),
It.IsAny(),
It.IsAny>(),
It.IsAny>(),
It.IsAny>(),
It.IsAny(),
It.IsAny()))
.Callback, IList, IList, MonitoredItemTransferOptions, CancellationToken>(
(context, sendInitialValues, monitoredItems, processedItems, errors, transferOptions, _) =>
{
for (int ii = 0; ii < monitoredItems.Count; ii++)
{
if (!processedItems[ii] &&
ReferenceEquals(monitoredItems[ii].NodeManager, laterOwner.Object))
{
processedItems[ii] = true;
errors[ii] = ServiceResult.Good;
if (sendInitialValues &&
!transferOptions.DeferInitialValues)
{
monitoredItems[ii].SetupResendDataTrigger();
}
}
}
observedLaterOwner = context.Session;
laterForwardCalls++;
if (ReferenceEquals(context.Session, destinationSession.Object) &&
failNextDestinationCallback)
{
failNextDestinationCallback = false;
throw new InvalidOperationException("Later owner failed.");
}
})
.Returns(default(ValueTask));
using var sut = new MasterNodeManager(
m_server.CurrentInstance,
m_fixture.Config,
null,
owner.Object,
laterOwner.Object);
using MonitoredItem item = CreateMonitoredItem(owner.Object, sourceSession.Object, 2, 3, 42);
using MonitoredItem laterItem = CreateMonitoredItem(laterOwner.Object, sourceSession.Object, 4, 5, 84);
var failedErrors = new List { null!, null! };
await sut.TransferMonitoredItemsAsync(
new OperationContext(destinationSession.Object, DiagnosticsMasks.None),
true,
[item, laterItem],
failedErrors,
new MonitoredItemTransferOptions(),
cancellationToken: CancellationToken.None).ConfigureAwait(false);
Assert.Multiple(() =>
{
Assert.That(observedOwner, Is.SameAs(destinationSession.Object));
Assert.That(observedLaterOwner, Is.SameAs(destinationSession.Object));
Assert.That(ownerForwardCalls, Is.EqualTo(1));
Assert.That(laterForwardCalls, Is.EqualTo(1));
Assert.That(failedErrors[0].StatusCode, Is.EqualTo(StatusCodes.Good));
Assert.That(failedErrors[1].StatusCode, Is.EqualTo(StatusCodes.BadUnexpectedError));
Assert.That(item.IsResendData, Is.True);
Assert.That(laterItem.IsResendData, Is.False);
});
_ = Publish(item);
var retryErrors = new List { null!, null! };
await sut.TransferMonitoredItemsAsync(
new OperationContext(destinationSession.Object, DiagnosticsMasks.None),
false,
[item, laterItem],
retryErrors,
new MonitoredItemTransferOptions(),
cancellationToken: CancellationToken.None).ConfigureAwait(false);
Assert.Multiple(() =>
{
Assert.That(observedOwner, Is.SameAs(destinationSession.Object));
Assert.That(observedLaterOwner, Is.SameAs(destinationSession.Object));
Assert.That(item.IsResendData, Is.False);
Assert.That(laterItem.IsResendData, Is.False);
Assert.That(Publish(item), Is.Empty);
Assert.That(Publish(laterItem), Is.Empty);
});
}
[Test]
public async Task TransferMonitoredItemsAsyncFlowsExplicitOptionsToEachOwnerAsync()
{
var destinationSession = new Mock();
destinationSession.SetupGet(session => session.Id).Returns(new NodeId(Guid.NewGuid()));
destinationSession.SetupGet(session => session.EffectiveIdentity)
.Returns(new Mock().Object);
destinationSession.SetupGet(session => session.PreferredLocales).Returns([]);
OperationContext destinationContext = new(destinationSession.Object, DiagnosticsMasks.None);
var sourceSession = new Mock();
sourceSession.SetupGet(session => session.EffectiveIdentity)
.Returns(new Mock().Object);
sourceSession.SetupGet(session => session.PreferredLocales).Returns([]);
var firstOwner = new Mock();
var secondOwner = new Mock();
OperationContext? firstContext = null;
OperationContext? secondContext = null;
MonitoredItemTransferOptions? firstOptions = null;
MonitoredItemTransferOptions? secondOptions = null;
firstOwner.Setup(nodeManager => nodeManager.TransferMonitoredItemsAsync(
It.IsAny(),
It.IsAny(),
It.IsAny>(),
It.IsAny>(),
It.IsAny>(),
It.IsAny(),
It.IsAny()))
.Callback, IList, IList,
MonitoredItemTransferOptions, CancellationToken>(
(context, _, monitoredItems, processedItems, errors, transferOptions, _) =>
{
firstContext = context;
firstOptions = transferOptions;
for (int ii = 0; ii < monitoredItems.Count; ii++)
{
if (!processedItems[ii] &&
ReferenceEquals(monitoredItems[ii].NodeManager, firstOwner.Object))
{
processedItems[ii] = true;
errors[ii] = ServiceResult.Good;
}
}
})
.Returns(default(ValueTask));
secondOwner.Setup(nodeManager => nodeManager.TransferMonitoredItemsAsync(
It.IsAny(),
It.IsAny(),
It.IsAny>(),
It.IsAny>(),
It.IsAny>(),
It.IsAny(),
It.IsAny()))
.Callback, IList, IList,
MonitoredItemTransferOptions, CancellationToken>(
(context, _, monitoredItems, processedItems, errors, transferOptions, _) =>
{
secondContext = context;
secondOptions = transferOptions;
for (int ii = 0; ii < monitoredItems.Count; ii++)
{
if (!processedItems[ii] &&
ReferenceEquals(monitoredItems[ii].NodeManager, secondOwner.Object))
{
processedItems[ii] = true;
errors[ii] = ServiceResult.Good;
}
}
})
.Returns(default(ValueTask));
using var sut = new MasterNodeManager(
m_server.CurrentInstance,
m_fixture.Config,
null,
firstOwner.Object,
secondOwner.Object);
using MonitoredItem firstItem = CreateMonitoredItem(firstOwner.Object, sourceSession.Object, 6, 7, 126);
using MonitoredItem secondItem = CreateMonitoredItem(secondOwner.Object, sourceSession.Object, 8, 9, 168);
var errors = new List { null!, null! };
await sut.TransferMonitoredItemsAsync(
destinationContext,
true,
[firstItem, secondItem],
errors,
new MonitoredItemTransferOptions(),
CancellationToken.None).ConfigureAwait(false);
Assert.Multiple(() =>
{
Assert.That(firstContext, Is.SameAs(destinationContext));
Assert.That(secondContext, Is.SameAs(destinationContext));
Assert.That(firstOptions, Is.Not.Null);
Assert.That(secondOptions, Is.Not.Null);
Assert.That(firstOptions!.DeferInitialValues, Is.True);
Assert.That(secondOptions!.DeferInitialValues, Is.True);
Assert.That(firstOptions, Is.SameAs(secondOptions));
Assert.That(errors[0].StatusCode, Is.EqualTo(StatusCodes.Good));
Assert.That(errors[1].StatusCode, Is.EqualTo(StatusCodes.Good));
Assert.That(Publish(firstItem), Has.Count.EqualTo(1));
Assert.That(Publish(secondItem), Has.Count.EqualTo(1));
});
}
private MasterNodeManager CreateMasterNodeManager()
{
return new MasterNodeManager(
m_server.CurrentInstance,
m_fixture.Config,
null,
Array.Empty());
}
private MonitoredItem CreateDetachedMonitoredItem(
MonitoringMode monitoringMode = MonitoringMode.Reporting)
{
var monitoredItem = new MonitoredItem(
m_server.CurrentInstance,
new Mock().Object,
new object(),
subscriptionId: 1,
id: 2,
itemToMonitor: new ReadValueId
{
NodeId = new NodeId("Detached", 2),
AttributeId = Attributes.Value
},
diagnosticsMasks: DiagnosticsMasks.None,
timestampsToReturn: TimestampsToReturn.Both,
monitoringMode,
clientHandle: 3,
originalFilter: null,
filterToUse: null,
range: null,
samplingInterval: 1000,
queueSize: 1,
discardOldest: true,
sourceSamplingInterval: 1000);
((IDetachableMonitoredItem)monitoredItem).Detach(m_server.CurrentInstance);
return monitoredItem;
}
private MonitoredItem CreateMonitoredItem(
IAsyncNodeManager nodeManager,
ISession session,
uint id,
uint clientHandle,
int value)
{
var monitoredItem = new MonitoredItem(
m_server.CurrentInstance,
nodeManager,
new object(),
subscriptionId: 1,
id: id,
itemToMonitor: new ReadValueId
{
NodeId = new NodeId($"Transactional{id}", 2),
AttributeId = Attributes.Value
},
diagnosticsMasks: DiagnosticsMasks.None,
timestampsToReturn: TimestampsToReturn.Both,
monitoringMode: MonitoringMode.Reporting,
clientHandle: clientHandle,
originalFilter: null,
filterToUse: null,
range: null,
samplingInterval: 1000,
queueSize: 1,
discardOldest: true,
sourceSamplingInterval: 1000);
var subscription = new Mock();
subscription.SetupGet(value => value.Session).Returns(session);
monitoredItem.SubscriptionCallback = subscription.Object;
monitoredItem.QueueValue(new DataValue(new Variant(value)), null);
Assert.That(Publish(monitoredItem), Has.Count.EqualTo(1));
return monitoredItem;
}
private Queue Publish(MonitoredItem item)
{
var notifications = new Queue();
var diagnostics = new Queue();
item.Publish(
new OperationContext(item),
notifications,
diagnostics,
10,
m_server.CurrentInstance.Telemetry.CreateLogger());
return notifications;
}
private static OperationContext CreateContext()
{
var session = new Mock();
session.Setup(s => s.EffectiveIdentity).Returns(new Mock().Object);
session.Setup(s => s.PreferredLocales).Returns([]);
session.Setup(s => s.ContinuationPoints).Returns(
new SessionContinuationPoints(
() => NodeId.Null, maxBrowse: 10, maxHistory: 10, store: null));
return new OperationContext(
new RequestHeader(), null!, RequestType.Read, RequestLifetime.None, session.Object);
}
private static OperationContext CreateContextWithContinuationStore()
{
var session = new Mock();
session.Setup(s => s.EffectiveIdentity).Returns(new Mock().Object);
session.Setup(s => s.PreferredLocales).Returns([]);
// The real holder, not a stand-in dictionary, so BrowseNext meets the same
// lookup and eviction rules a live session applies.
var continuationPoints = new SessionContinuationPoints(
() => NodeId.Null, maxBrowse: 10, maxHistory: 10, store: null);
session.Setup(s => s.ContinuationPoints).Returns(continuationPoints);
return new OperationContext(
new RequestHeader(), null!, RequestType.Read, RequestLifetime.None, session.Object);
}
private static void SetMaxBrowseContinuationPointsPerBrowse(MasterNodeManager sut, uint value)
{
FieldInfo? field = typeof(MasterNodeManager).GetField(
"m_maxContinuationPointsPerBrowse",
BindingFlags.Instance | BindingFlags.NonPublic);
Assert.That(field, Is.Not.Null);
field!.SetValue(sut, value);
}
private static uint GetMaxBrowseContinuationPointsPerBrowse(MasterNodeManager sut)
{
FieldInfo? field = typeof(MasterNodeManager).GetField(
"m_maxContinuationPointsPerBrowse",
BindingFlags.Instance | BindingFlags.NonPublic);
Assert.That(field, Is.Not.Null);
return (uint)field!.GetValue(sut)!;
}
}
}