/* ========================================================================
* 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.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
using Microsoft.Extensions.Logging;
using NUnit.Framework;
using Opc.Ua.Server.TestFramework;
using Opc.Ua.Test;
using Opc.Ua.Tests;
using Quickstarts.ReferenceServer;
namespace Opc.Ua.Server.Tests
{
///
/// Test Reference Server.
///
[TestFixture]
[Category("Server")]
[SetCulture("en-us")]
[SetUICulture("en-us")]
[NonParallelizable]
[MemoryDiagnoser]
[DisassemblyDiagnoser]
public class ReferenceServerTests
{
private const double kMaxAge = 10000;
private const uint kTimeoutHint = 10000;
private const uint kQueueSize = 5;
private const string kClientApplicationUri =
"urn:localhost:opcfoundation.org:ReferenceServerTests";
private static readonly int[] s_staticMatrixDimensions = [5, 5];
private ITelemetryContext m_telemetry;
private ServerFixture m_fixture;
private ReferenceServer m_server;
private RequestHeader m_requestHeader;
private SecureChannelContext m_secureChannelContext;
private OperationLimits m_operationLimits = null!;
private ArrayOf m_referenceDescriptions;
private RandomSource m_random;
private DataGenerator m_generator;
private bool m_sessionClosed;
///
/// Set up a Server fixture.
///
[OneTimeSetUp]
public async Task OneTimeSetUpAsync()
{
m_telemetry = NUnitTelemetryContext.Create();
// start Ref server
m_fixture = new ServerFixture(t => new ReferenceServer(t))
{
AllNodeManagers = true,
OperationLimits = true,
DurableSubscriptionsEnabled = false,
UseSamplingGroupsInReferenceNodeManager = false,
AutoAccept = true
};
await m_fixture.LoadConfigurationAsync().ConfigureAwait(false);
m_fixture.Config.ServerConfiguration!.UserTokenPolicies =
[
new UserTokenPolicy(UserTokenType.Anonymous),
new UserTokenPolicy(UserTokenType.UserName)
];
m_server = await m_fixture.StartAsync().ConfigureAwait(false);
}
///
/// Tear down the server fixture.
///
[OneTimeTearDown]
public async Task OneTimeTearDownAsync()
{
await m_fixture.StopAsync().ConfigureAwait(false);
Thread.Sleep(1000);
}
///
/// Create a session for a test.
///
[SetUp]
public async Task SetUpAsync()
{
(m_requestHeader, m_secureChannelContext) = await m_server.CreateAndActivateSessionAsync(
TestContext.CurrentContext.Test.Name,
clientApplicationUri: kClientApplicationUri).ConfigureAwait(false);
m_requestHeader.Timestamp = DateTimeUtc.Now;
m_requestHeader.TimeoutHint = kTimeoutHint;
m_random = new RandomSource();
m_generator = new DataGenerator(m_random, m_telemetry);
}
///
/// Tear down the test session.
///
[TearDown]
public async Task TearDownAsync()
{
if (!m_sessionClosed)
{
m_requestHeader.Timestamp = DateTimeUtc.Now;
await m_server.CloseSessionAsync(m_secureChannelContext, m_requestHeader, CancellationToken.None).ConfigureAwait(false);
m_requestHeader = null!;
}
}
///
/// Set up a Reference Server a session
///
[GlobalSetup]
public async Task GlobalSetupAsync()
{
// start Ref server
m_fixture = new ServerFixture(t => new ReferenceServer(t))
{
AllNodeManagers = true
};
m_server = await m_fixture.StartAsync(null!).ConfigureAwait(false);
(m_requestHeader, m_secureChannelContext) = await m_server.CreateAndActivateSessionAsync("Bench").ConfigureAwait(false);
}
///
/// Tear down Server and the close the session.
///
[GlobalCleanup]
public async Task GlobalCleanupAsync()
{
await m_server.CloseSessionAsync(m_secureChannelContext, m_requestHeader, true, RequestLifetime.None).ConfigureAwait(false);
await m_fixture.StopAsync().ConfigureAwait(false);
Thread.Sleep(1000);
}
///
/// Test for expected exceptions.
///
[Test]
public async Task NoInvalidTimestampExceptionAsync()
{
// test that the server accepts an invalid timestamp
m_requestHeader.Timestamp = DateTimeUtc.Now - TimeSpan.FromDays(30);
await m_server.CloseSessionAsync(m_secureChannelContext, m_requestHeader, false, RequestLifetime.None).ConfigureAwait(false);
m_sessionClosed = true;
}
///
/// Get Endpoints.
///
[Test]
public void GetEndpoints()
{
ArrayOf endpoints = m_server.GetEndpoints();
Assert.That(endpoints.IsNull, Is.False);
}
///
/// Get Operation limits.
///
[Test]
[Order(100)]
public async Task GetOperationLimitsAsync()
{
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
ILogger logger = telemetry.CreateLogger();
ArrayOf readIdCollection =
[
new ReadValueId {
AttributeId = Attributes.Value,
NodeId = VariableIds.Server_ServerCapabilities_OperationLimits_MaxNodesPerRead
},
new ReadValueId {
AttributeId = Attributes.Value,
NodeId = VariableIds
.Server_ServerCapabilities_OperationLimits_MaxNodesPerHistoryReadData
},
new ReadValueId {
AttributeId = Attributes.Value,
NodeId = VariableIds
.Server_ServerCapabilities_OperationLimits_MaxNodesPerHistoryReadEvents
},
new ReadValueId {
AttributeId = Attributes.Value,
NodeId = VariableIds.Server_ServerCapabilities_OperationLimits_MaxNodesPerWrite
},
new ReadValueId {
AttributeId = Attributes.Value,
NodeId = VariableIds
.Server_ServerCapabilities_OperationLimits_MaxNodesPerHistoryUpdateData
},
new ReadValueId {
AttributeId = Attributes.Value,
NodeId = VariableIds
.Server_ServerCapabilities_OperationLimits_MaxNodesPerHistoryUpdateEvents
},
new ReadValueId {
AttributeId = Attributes.Value,
NodeId = VariableIds.Server_ServerCapabilities_OperationLimits_MaxNodesPerBrowse
},
new ReadValueId {
AttributeId = Attributes.Value,
NodeId = VariableIds
.Server_ServerCapabilities_OperationLimits_MaxMonitoredItemsPerCall
},
new ReadValueId {
AttributeId = Attributes.Value,
NodeId = VariableIds
.Server_ServerCapabilities_OperationLimits_MaxNodesPerNodeManagement
},
new ReadValueId {
AttributeId = Attributes.Value,
NodeId = VariableIds
.Server_ServerCapabilities_OperationLimits_MaxNodesPerRegisterNodes
},
new ReadValueId {
AttributeId = Attributes.Value,
NodeId =
VariableIds.Server_ServerCapabilities_OperationLimits_MaxNodesPerTranslateBrowsePathsToNodeIds
},
new ReadValueId {
AttributeId = Attributes.Value,
NodeId = VariableIds
.Server_ServerCapabilities_OperationLimits_MaxNodesPerMethodCall
}
];
var requestHeader = (RequestHeader)m_requestHeader.Clone();
requestHeader.Timestamp = DateTimeUtc.Now;
ReadResponse readResponse = await m_server.ReadAsync(
m_secureChannelContext,
requestHeader,
kMaxAge,
TimestampsToReturn.Neither,
readIdCollection,
RequestLifetime.None).ConfigureAwait(false);
ServerFixtureUtils.ValidateResponse(readResponse.ResponseHeader, readResponse.Results, readIdCollection);
ServerFixtureUtils.ValidateDiagnosticInfos(
readResponse.DiagnosticInfos,
readResponse.Results,
readResponse.ResponseHeader.StringTable,
logger);
ArrayOf results = readResponse.Results;
Assert.That(results.IsNull, Is.False);
Assert.That(results.Count, Is.EqualTo(readIdCollection.Count));
m_operationLimits = new OperationLimits
{
MaxNodesPerRead = (uint)results[0].WrappedValue,
MaxNodesPerHistoryReadData = (uint)results[1].WrappedValue,
MaxNodesPerHistoryReadEvents = (uint)results[2].WrappedValue,
MaxNodesPerWrite = (uint)results[3].WrappedValue,
MaxNodesPerHistoryUpdateData = (uint)results[4].WrappedValue,
MaxNodesPerHistoryUpdateEvents = (uint)results[5].WrappedValue,
MaxNodesPerBrowse = (uint)results[6].WrappedValue,
MaxMonitoredItemsPerCall = (uint)results[7].WrappedValue,
MaxNodesPerNodeManagement = (uint)results[8].WrappedValue,
MaxNodesPerRegisterNodes = (uint)results[9].WrappedValue,
MaxNodesPerTranslateBrowsePathsToNodeIds = (uint)results[10].WrappedValue,
MaxNodesPerMethodCall = (uint)results[11].WrappedValue
};
}
///
/// Read node.
///
[Test]
[Benchmark]
public async Task ReadAsync()
{
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
ILogger logger = telemetry.CreateLogger();
// Read
RequestHeader requestHeader = m_requestHeader;
requestHeader.Timestamp = DateTimeUtc.Now;
var nodeId = new NodeId("Scalar_Simulation_Int32", 2);
var nodesToRead = ServerFixtureUtils.AttributesIds.Keys
.Select(attributeId => new ReadValueId { NodeId = nodeId, AttributeId = attributeId })
.ToArrayOf();
ReadResponse readResponse = await m_server.ReadAsync(
m_secureChannelContext,
requestHeader,
kMaxAge,
TimestampsToReturn.Neither,
nodesToRead, RequestLifetime.None).ConfigureAwait(false);
ServerFixtureUtils.ValidateResponse(readResponse.ResponseHeader, readResponse.Results, nodesToRead);
ServerFixtureUtils.ValidateDiagnosticInfos(
readResponse.DiagnosticInfos,
readResponse.Results,
readResponse.ResponseHeader.StringTable,
logger);
}
///
/// A response carries an empty string table when the request asked for no
/// diagnostics, the diagnostic strings when it did, and an empty one again for the
/// next request without diagnostics.
///
[Test]
public async Task ResponseStringTableHoldsOnlyTheRequestsDiagnosticsAsync()
{
ArrayOf nodesToRead =
[
new ReadValueId { NodeId = new NodeId("Scalar_Simulation_Int32", 2), AttributeId = Attributes.Value },
new ReadValueId { NodeId = new NodeId("DoesNotExist", 2), AttributeId = Attributes.Value }
];
var plain = (RequestHeader)m_requestHeader.Clone();
plain.ReturnDiagnostics = (uint)DiagnosticsMasks.None;
ReadResponse response = await m_server.ReadAsync(
m_secureChannelContext, plain, 0, TimestampsToReturn.Both, nodesToRead, RequestLifetime.None)
.ConfigureAwait(false);
Assert.That(response.Results[1].StatusCode, Is.EqualTo(StatusCodes.BadNodeIdUnknown));
Assert.That(response.ResponseHeader.StringTable.IsNull, Is.False);
Assert.That(response.ResponseHeader.StringTable.Count, Is.Zero);
var diagnostics = (RequestHeader)m_requestHeader.Clone();
diagnostics.ReturnDiagnostics = (uint)DiagnosticsMasks.All;
response = await m_server.ReadAsync(
m_secureChannelContext, diagnostics, 0, TimestampsToReturn.Both, nodesToRead, RequestLifetime.None)
.ConfigureAwait(false);
Assert.That(response.ResponseHeader.StringTable.Count, Is.GreaterThan(0));
response = await m_server.ReadAsync(
m_secureChannelContext, plain, 0, TimestampsToReturn.Both, nodesToRead, RequestLifetime.None)
.ConfigureAwait(false);
Assert.That(response.ResponseHeader.StringTable.IsNull, Is.False);
Assert.That(response.ResponseHeader.StringTable.Count, Is.Zero);
}
///
/// The response headers of Read, Write and Browse carry the timestamp, the request
/// handle, a good service result, no service diagnostics and an empty string table,
/// and every response gets a header of its own.
///
[Test]
public async Task ResponseHeadersKeepEveryFieldAsync()
{
var nodeId = new NodeId("Scalar_Simulation_Int32", 2);
var requestHeader = (RequestHeader)m_requestHeader.Clone();
requestHeader.RequestHandle = 4711;
DateTimeUtc before = DateTimeUtc.Now;
ReadResponse read1 = await m_server.ReadAsync(
m_secureChannelContext, requestHeader, 0, TimestampsToReturn.Both,
[new ReadValueId { NodeId = nodeId, AttributeId = Attributes.Value }],
RequestLifetime.None).ConfigureAwait(false);
ReadResponse read2 = await m_server.ReadAsync(
m_secureChannelContext, requestHeader, 0, TimestampsToReturn.Both,
[new ReadValueId { NodeId = nodeId, AttributeId = Attributes.Value }],
RequestLifetime.None).ConfigureAwait(false);
WriteResponse write = await m_server.WriteAsync(
m_secureChannelContext, requestHeader,
[new WriteValue { NodeId = nodeId, AttributeId = Attributes.Value, Value = read1.Results[0] }],
RequestLifetime.None).ConfigureAwait(false);
BrowseResponse browse = await m_server.BrowseAsync(
m_secureChannelContext, requestHeader, null!, 0,
[new BrowseDescription { NodeId = ObjectIds.ObjectsFolder, BrowseDirection = BrowseDirection.Forward }],
RequestLifetime.None).ConfigureAwait(false);
Assert.That(read2.ResponseHeader, Is.Not.SameAs(read1.ResponseHeader));
foreach (ResponseHeader header in new[]
{ read1.ResponseHeader, read2.ResponseHeader, write.ResponseHeader, browse.ResponseHeader })
{
Assert.That(header.RequestHandle, Is.EqualTo(4711u));
Assert.That(header.Timestamp, Is.GreaterThanOrEqualTo(before));
Assert.That(header.Timestamp, Is.LessThanOrEqualTo(DateTimeUtc.Now));
Assert.That(header.ServiceResult, Is.EqualTo(StatusCodes.Good));
Assert.That(header.ServiceDiagnostics, Is.Null);
Assert.That(header.StringTable.IsNull, Is.False);
Assert.That(header.StringTable.Count, Is.Zero);
}
}
///
/// A service that fails reports its status through the exception the transport turns
/// into a service fault, not through a half-filled response header.
///
[Test]
public void FailedReadThrowsTheServiceResult()
{
var requestHeader = (RequestHeader)m_requestHeader.Clone();
requestHeader.ReturnDiagnostics = (uint)DiagnosticsMasks.All;
ServiceResultException? e = Assert.ThrowsAsync(async () =>
await m_server.ReadAsync(
m_secureChannelContext, requestHeader, -1, TimestampsToReturn.Both,
[new ReadValueId { NodeId = new NodeId("Scalar_Simulation_Int32", 2), AttributeId = Attributes.Value }],
RequestLifetime.None).ConfigureAwait(false));
Assert.That(e!.StatusCode, Is.EqualTo(StatusCodes.BadMaxAgeInvalid));
}
///
/// Read all nodes.
///
[Test]
public async Task ReadAllNodesAsync()
{
var serverTestServices = new ServerTestServices(m_server, m_secureChannelContext);
if (m_operationLimits == null)
{
await GetOperationLimitsAsync().ConfigureAwait(false);
}
if (m_referenceDescriptions.IsNull)
{
m_referenceDescriptions = await CommonTestWorkers.BrowseFullAddressSpaceWorkerAsync(
serverTestServices,
m_requestHeader,
m_operationLimits).ConfigureAwait(false);
}
// Read all variables
RequestHeader requestHeader = m_requestHeader;
foreach (ReferenceDescription reference in m_referenceDescriptions.ToList())
{
requestHeader.Timestamp = DateTimeUtc.Now;
var nodeId = ExpandedNodeId.ToNodeId(
reference.NodeId,
m_server.CurrentInstance.NamespaceUris);
var nodesToRead = ServerFixtureUtils.AttributesIds.Keys
.Select(attributeId => new ReadValueId { NodeId = nodeId, AttributeId = attributeId })
.ToArrayOf();
TestContext.Out.WriteLine("NodeId {0} {1}", reference.NodeId, reference.BrowseName);
ReadResponse readResponse = await m_server.ReadAsync(
m_secureChannelContext,
requestHeader,
kMaxAge,
TimestampsToReturn.Both,
nodesToRead, RequestLifetime.None).ConfigureAwait(false);
ServerFixtureUtils.ValidateResponse(readResponse.ResponseHeader, readResponse.Results, nodesToRead);
ServerFixtureUtils.ValidateDiagnosticInfos(
readResponse.DiagnosticInfos,
readResponse.Results,
readResponse.ResponseHeader.StringTable,
serverTestServices.Logger);
foreach (DataValue dataValue in readResponse.Results)
{
TestContext.Out.WriteLine(" {0}", dataValue.ToString());
}
}
}
///
/// Write Node.
///
[Test]
[Benchmark]
public async Task WriteAsync()
{
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
ILogger logger = telemetry.CreateLogger();
// Write
RequestHeader requestHeader = m_requestHeader;
requestHeader.Timestamp = DateTimeUtc.Now;
var nodeId = new NodeId("Scalar_Simulation_Int32", 2);
var nodesToWrite = ServerFixtureUtils.AttributesIds.Keys
.Select(attributeId => new WriteValue
{
NodeId = nodeId,
AttributeId = Attributes.Value,
Value = new DataValue(1234)
})
.ToArrayOf();
WriteResponse writeResponse = await m_server.WriteAsync(
m_secureChannelContext,
requestHeader,
nodesToWrite, RequestLifetime.None).ConfigureAwait(false);
ServerFixtureUtils.ValidateResponse(writeResponse.ResponseHeader, writeResponse.Results, nodesToWrite);
ServerFixtureUtils.ValidateDiagnosticInfos(
writeResponse.DiagnosticInfos,
writeResponse.Results,
writeResponse.ResponseHeader.StringTable,
logger);
}
///
/// Test that ReferenceNodeManager variables update their SourceTimestamp on read.
///
[Test]
[Order(340)]
public async Task ReferenceNodeManagerVariablesUpdateTimestampOnReadAsync()
{
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
ILogger logger = telemetry.CreateLogger();
// Read a variable from the ReferenceNodeManager (namespace index 2)
var nodeId = new NodeId("Scalar_Static_Byte", 2);
ArrayOf nodesToRead =
[
new ReadValueId
{
NodeId = nodeId,
AttributeId = Attributes.Value
}
];
// First read
RequestHeader requestHeader = m_requestHeader;
requestHeader.Timestamp = DateTimeUtc.Now;
DateTimeUtc timeBeforeFirstRead = DateTimeUtc.Now;
ReadResponse firstReadResponse = await m_server.ReadAsync(
m_secureChannelContext,
requestHeader,
kMaxAge,
TimestampsToReturn.Both,
nodesToRead,
RequestLifetime.None).ConfigureAwait(false);
Assert.That(firstReadResponse, Is.Not.Null);
Assert.That(firstReadResponse.Results.IsNull, Is.False);
Assert.That(firstReadResponse.Results.Count, Is.EqualTo(1));
DataValue firstValue = firstReadResponse.Results[0];
Assert.That(firstValue.StatusCode, Is.EqualTo(StatusCodes.Good));
Assert.That(firstValue.SourceTimestamp.IsNull, Is.False);
if (logger.IsEnabled(LogLevel.Information))
{
logger.LogInformation("First read - SourceTimestamp: {SourceTimestamp}, ServerTimestamp: {ServerTimestamp}",
firstValue.SourceTimestamp, firstValue.ServerTimestamp);
}
// Verify the timestamp is recent (not startup time)
Assert.That((long)firstValue.SourceTimestamp, Is.GreaterThanOrEqualTo((long)timeBeforeFirstRead.SubtractMilliseconds(1000)),
"SourceTimestamp should be close to the read time, not the server startup time");
// Wait a bit to ensure time difference
await Task.Delay(1500).ConfigureAwait(false);
// Second read
requestHeader.Timestamp = DateTimeUtc.Now;
DateTimeUtc timeBeforeSecondRead = DateTimeUtc.Now;
ReadResponse secondReadResponse = await m_server.ReadAsync(
m_secureChannelContext,
requestHeader,
kMaxAge,
TimestampsToReturn.Both,
nodesToRead,
RequestLifetime.None).ConfigureAwait(false);
Assert.That(secondReadResponse, Is.Not.Null);
Assert.That(secondReadResponse.Results.IsNull, Is.False);
Assert.That(secondReadResponse.Results.Count, Is.EqualTo(1));
DataValue secondValue = secondReadResponse.Results[0];
Assert.That(secondValue.StatusCode, Is.EqualTo(StatusCodes.Good));
Assert.That(secondValue.SourceTimestamp.IsNull, Is.False);
if (logger.IsEnabled(LogLevel.Information))
{
logger.LogInformation("Second read - SourceTimestamp: {SourceTimestamp}, ServerTimestamp: {ServerTimestamp}",
secondValue.SourceTimestamp, secondValue.ServerTimestamp);
}
// Verify the second timestamp is more recent than the first
Assert.That((long)secondValue.SourceTimestamp, Is.GreaterThan((long)firstValue.SourceTimestamp),
"SourceTimestamp should be updated on each read");
// Verify the second timestamp is recent
Assert.That((long)secondValue.SourceTimestamp, Is.GreaterThanOrEqualTo((long)timeBeforeSecondRead.SubtractMilliseconds(1000)),
"SourceTimestamp should be close to the second read time");
}
///
/// Test that the ReferenceNodeManager SelectionList variable exposes its
/// value.
///
[Test]
public async Task ReferenceNodeManagerSelectionListVariableAsync()
{
ushort namespaceIndex = (ushort)m_server.CurrentInstance.NamespaceUris.GetIndex(
Quickstarts.ReferenceServer.Namespaces.ReferenceServer);
var valueNodeId = new NodeId("DataAccess_SelectionList_Colors", namespaceIndex);
var selectionsNodeId = new NodeId(
"DataAccess_SelectionList_Colors_Selections",
namespaceIndex);
var descriptionsNodeId = new NodeId(
"DataAccess_SelectionList_Colors_SelectionDescriptions",
namespaceIndex);
var restrictToListNodeId = new NodeId(
"DataAccess_SelectionList_Colors_RestrictToList",
namespaceIndex);
ArrayOf nodesToRead =
[
new ReadValueId { NodeId = valueNodeId, AttributeId = Attributes.Value },
new ReadValueId { NodeId = valueNodeId, AttributeId = Attributes.DataType },
new ReadValueId { NodeId = selectionsNodeId, AttributeId = Attributes.Value },
new ReadValueId { NodeId = selectionsNodeId, AttributeId = Attributes.DataType },
new ReadValueId { NodeId = descriptionsNodeId, AttributeId = Attributes.Value },
new ReadValueId { NodeId = restrictToListNodeId, AttributeId = Attributes.Value }
];
RequestHeader requestHeader = m_requestHeader;
requestHeader.Timestamp = DateTimeUtc.Now;
ReadResponse readResponse = await m_server.ReadAsync(
m_secureChannelContext,
requestHeader,
kMaxAge,
TimestampsToReturn.Both,
nodesToRead,
RequestLifetime.None).ConfigureAwait(false);
Assert.That(readResponse, Is.Not.Null);
Assert.That(readResponse.Results.IsNull, Is.False);
Assert.That(readResponse.Results.Count, Is.EqualTo(nodesToRead.Count));
DataValue value = readResponse.Results[0];
Assert.That(value.StatusCode, Is.EqualTo(StatusCodes.Good));
Assert.That(value.WrappedValue.GetString(), Is.EqualTo("Red"));
Assert.That(
readResponse.Results[1].WrappedValue.TryGetValue(out NodeId valueDataType),
Is.True);
Assert.That(valueDataType, Is.EqualTo(DataTypeIds.String));
Assert.That(
readResponse.Results[2].WrappedValue.TryGetValue(
out ArrayOf selections),
Is.True);
Assert.That(selections, Has.Count.EqualTo(3));
Assert.That(selections[0], Is.EqualTo("Red"));
Assert.That(selections[1], Is.EqualTo("Green"));
Assert.That(selections[2], Is.EqualTo("Blue"));
Assert.That(
readResponse.Results[3].WrappedValue.TryGetValue(out NodeId selectionsDataType),
Is.True);
Assert.That(selectionsDataType, Is.EqualTo(DataTypeIds.String));
Assert.That(
readResponse.Results[4].WrappedValue.TryGetValue(
out ArrayOf selectionDescriptions),
Is.True);
Assert.That(selectionDescriptions, Has.Count.EqualTo(3));
Assert.That(selectionDescriptions[0].Text, Is.EqualTo("The color red"));
Assert.That(readResponse.Results[5].WrappedValue.GetBoolean(), Is.True);
ArrayOf nodesToWrite =
[
new WriteValue
{
NodeId = valueNodeId,
AttributeId = Attributes.Value,
Value = new DataValue("Green")
},
new WriteValue
{
NodeId = valueNodeId,
AttributeId = Attributes.Value,
Value = new DataValue("Purple")
},
new WriteValue
{
NodeId = valueNodeId,
AttributeId = Attributes.Value,
Value = new DataValue("Red")
}
];
WriteResponse writeResponse = await m_server.WriteAsync(
m_secureChannelContext,
requestHeader,
nodesToWrite,
RequestLifetime.None).ConfigureAwait(false);
Assert.That(writeResponse.Results, Has.Count.EqualTo(nodesToWrite.Count));
Assert.That(writeResponse.Results[0], Is.EqualTo(StatusCodes.Good));
Assert.That(writeResponse.Results[1], Is.EqualTo(StatusCodes.BadOutOfRange));
Assert.That(writeResponse.Results[2], Is.EqualTo(StatusCodes.Good));
}
///
/// Tests that the discrete DataAccess node seed values (TrueState/FalseState,
/// EnumStrings, EnumValues, ValueAsText) baked into the NodeSet2 model are
/// materialized at runtime by the source generator.
///
[Test]
public async Task ReferenceNodeManagerDiscreteSeedValuesAsync()
{
ushort namespaceIndex = (ushort)m_server.CurrentInstance.NamespaceUris.GetIndex(
Quickstarts.ReferenceServer.Namespaces.ReferenceServer);
var twoStateTrueId = new NodeId(
"DataAccess_TwoStateDiscreteType_DataAccess_TwoStateDiscreteType_001_TrueState",
namespaceIndex);
var twoStateFalseId = new NodeId(
"DataAccess_TwoStateDiscreteType_DataAccess_TwoStateDiscreteType_001_FalseState",
namespaceIndex);
var multiStateEnumStringsId = new NodeId(
"DataAccess_MultiStateDiscreteType_DataAccess_MultiStateDiscreteType_001_EnumStrings",
namespaceIndex);
var multiStateValueEnumValuesId = new NodeId(
"DataAccess_MultiStateValueDiscreteType_DataAccess_MultiStateValueDiscreteType_001_EnumValues",
namespaceIndex);
var multiStateValueAsTextId = new NodeId(
"DataAccess_MultiStateValueDiscreteType_DataAccess_MultiStateValueDiscreteType_001_ValueAsText",
namespaceIndex);
ArrayOf nodesToRead =
[
new ReadValueId { NodeId = twoStateTrueId, AttributeId = Attributes.Value },
new ReadValueId { NodeId = twoStateFalseId, AttributeId = Attributes.Value },
new ReadValueId { NodeId = multiStateEnumStringsId, AttributeId = Attributes.Value },
new ReadValueId { NodeId = multiStateValueEnumValuesId, AttributeId = Attributes.Value },
new ReadValueId { NodeId = multiStateValueAsTextId, AttributeId = Attributes.Value }
];
RequestHeader requestHeader = m_requestHeader;
requestHeader.Timestamp = DateTimeUtc.Now;
ReadResponse readResponse = await m_server.ReadAsync(
m_secureChannelContext,
requestHeader,
kMaxAge,
TimestampsToReturn.Both,
nodesToRead,
RequestLifetime.None).ConfigureAwait(false);
Assert.That(readResponse, Is.Not.Null);
Assert.That(readResponse.Results.IsNull, Is.False);
Assert.That(readResponse.Results.Count, Is.EqualTo(nodesToRead.Count));
foreach (DataValue result in readResponse.Results)
{
Assert.That(result.StatusCode, Is.EqualTo(StatusCodes.Good));
}
Assert.That(
readResponse.Results[0].WrappedValue.TryGetValue(out LocalizedText trueState),
Is.True);
Assert.That(trueState.Text, Is.EqualTo("red"));
Assert.That(
readResponse.Results[1].WrappedValue.TryGetValue(out LocalizedText falseState),
Is.True);
Assert.That(falseState.Text, Is.EqualTo("blue"));
Assert.That(
readResponse.Results[2].WrappedValue.TryGetValue(
out ArrayOf enumStrings),
Is.True);
Assert.That(enumStrings, Has.Count.EqualTo(3));
Assert.That(enumStrings[0].Text, Is.EqualTo("open"));
Assert.That(enumStrings[1].Text, Is.EqualTo("closed"));
Assert.That(enumStrings[2].Text, Is.EqualTo("jammed"));
Assert.That(
readResponse.Results[3].WrappedValue.TryGetValue(
out ArrayOf enumValues),
Is.True);
Assert.That(enumValues, Has.Count.EqualTo(3));
Assert.That(
enumValues[0].TryGetValue(out EnumValueType? firstEnumValue),
Is.True);
Assert.That(firstEnumValue!.Value, Is.Zero);
Assert.That(firstEnumValue.DisplayName.Text, Is.EqualTo("open"));
Assert.That(
readResponse.Results[4].WrappedValue.TryGetValue(out LocalizedText valueAsText),
Is.True);
Assert.That(valueAsText.Text, Is.EqualTo("open"));
}
///
/// Tests that AccessLevelEx retains the same base access bits as AccessLevel.
///
[TestCase("Scalar_Static_NonatomicReadWrite")]
[TestCase("AccessRights_AccessAll_NonatomicReadWrite")]
public async Task ReferenceNodeManagerNonatomicAccessLevelsAreConsistentAsync(string identifier)
{
ushort namespaceIndex = (ushort)m_server.CurrentInstance.NamespaceUris.GetIndex(
Quickstarts.ReferenceServer.Namespaces.ReferenceServer);
var nodeId = new NodeId(identifier, namespaceIndex);
ArrayOf nodesToRead =
[
new ReadValueId { NodeId = nodeId, AttributeId = Attributes.AccessLevel },
new ReadValueId { NodeId = nodeId, AttributeId = Attributes.UserAccessLevel },
new ReadValueId { NodeId = nodeId, AttributeId = Attributes.AccessLevelEx }
];
var requestHeader = (RequestHeader)m_requestHeader.Clone();
requestHeader.Timestamp = DateTimeUtc.Now;
ReadResponse response = await m_server.ReadAsync(
m_secureChannelContext,
requestHeader,
kMaxAge,
TimestampsToReturn.Neither,
nodesToRead,
RequestLifetime.None).ConfigureAwait(false);
// the reference server grants StatusWrite and TimestampWrite on every
// writable variable (OPC 10000-3 8.57).
const byte expectedAccessLevel = (byte)(
AccessLevels.CurrentReadOrWrite |
AccessLevels.StatusWrite |
AccessLevels.TimestampWrite);
Assert.That(response.Results, Has.Count.EqualTo(nodesToRead.Count));
Assert.That(response.Results[0].WrappedValue.GetByte(),
Is.EqualTo(expectedAccessLevel));
Assert.That(response.Results[1].WrappedValue.GetByte(),
Is.EqualTo(expectedAccessLevel));
uint accessLevelEx = response.Results[2].WrappedValue.GetUInt32();
Assert.That(
accessLevelEx & 0xff,
Is.EqualTo((uint)expectedAccessLevel));
Assert.That(
accessLevelEx & (uint)AccessLevelExType.NonatomicRead,
Is.EqualTo((uint)AccessLevelExType.NonatomicRead));
Assert.That(
accessLevelEx & (uint)AccessLevelExType.NonatomicWrite,
Is.EqualTo((uint)AccessLevelExType.NonatomicWrite));
}
///
/// The ArrayItemType engineering units and axis definitions carry a UNECE unit, and the
/// Float AnalogItemType has a non-empty EURange, so the CTT Engineering Units and
/// PercentDeadband test cases do not skip the nodes.
///
[Test]
public async Task ReferenceNodeManagerDataAccessUnitsAndRangesAreConfiguredAsync()
{
ushort namespaceIndex = (ushort)m_server.CurrentInstance.NamespaceUris.GetIndex(
Quickstarts.ReferenceServer.Namespaces.ReferenceServer);
string[] arrayItems = ["YArray", "XYArray", "Image", "Cube", "NDimension"];
var nodesToRead = new List();
foreach (string item in arrayItems)
{
nodesToRead.Add(new ReadValueId
{
NodeId = new NodeId(
$"DataAccess_ArrayItemType_DataAccess_ArrayItemType_{item}_EngineeringUnits",
namespaceIndex),
AttributeId = Attributes.Value
});
}
nodesToRead.Add(new ReadValueId
{
NodeId = new NodeId(
"DataAccess_ArrayItemType_DataAccess_ArrayItemType_Cube_ZAxisDefinition",
namespaceIndex),
AttributeId = Attributes.Value
});
nodesToRead.Add(new ReadValueId
{
NodeId = new NodeId(
"DataAccess_AnalogType_DataAccess_AnalogType_Float_EURange",
namespaceIndex),
AttributeId = Attributes.Value
});
var requestHeader = (RequestHeader)m_requestHeader.Clone();
requestHeader.Timestamp = DateTimeUtc.Now;
ReadResponse response = await m_server.ReadAsync(
m_secureChannelContext,
requestHeader,
kMaxAge,
TimestampsToReturn.Neither,
nodesToRead.ToArrayOf(),
RequestLifetime.None).ConfigureAwait(false);
Assert.That(response.Results, Has.Count.EqualTo(nodesToRead.Count));
for (int ii = 0; ii < arrayItems.Length; ii++)
{
Assert.That(response.Results[ii].StatusCode, Is.EqualTo(StatusCodes.Good), arrayItems[ii]);
Assert.That(response.Results[ii].WrappedValue.TryGetValue(out ExtensionObject extension), Is.True);
Assert.That(extension.TryGetValue(out EUInformation? units), Is.True, arrayItems[ii]);
Assert.That(units!.NamespaceUri, Is.EqualTo("http://www.opcfoundation.org/UA/units/un/cefact"));
Assert.That(units.UnitId, Is.EqualTo(12890), arrayItems[ii]);
Assert.That(units.DisplayName.Text, Is.EqualTo("mV"));
Assert.That(units.Description.Text, Is.EqualTo("millivolt"));
}
DataValue axis = response.Results[arrayItems.Length];
Assert.That(axis.WrappedValue.TryGetValue(out ExtensionObject axisExtension), Is.True);
Assert.That(axisExtension.TryGetValue(out AxisInformation? axisInformation), Is.True);
Assert.That(axisInformation!.EngineeringUnits.UnitId, Is.EqualTo(5457219));
Assert.That(axisInformation.EngineeringUnits.DisplayName.Text, Is.EqualTo("s"));
DataValue range = response.Results[arrayItems.Length + 1];
Assert.That(range.WrappedValue.TryGetValue(out ExtensionObject rangeExtension), Is.True);
Assert.That(rangeExtension.TryGetValue(out Range? euRange), Is.True);
Assert.That(euRange!.High, Is.GreaterThan(euRange.Low));
}
///
/// Every static two-dimensional sample array is 5x5 so that the CTT multi-dimensional
/// IndexRange test cases (for example "0,2:4") can use it.
///
[Test]
public async Task ReferenceNodeManagerStaticMatricesAreFiveByFiveAsync()
{
ushort namespaceIndex = (ushort)m_server.CurrentInstance.NamespaceUris.GetIndex(
Quickstarts.ReferenceServer.Namespaces.ReferenceServer);
string[] suffixes =
[
"Boolean", "Byte", "ByteString", "DateTime", "Double", "Duration", "Float", "Guid",
"Int16", "Int32", "Int64", "Integer", "LocaleId", "LocalizedText", "NodeId", "Number",
"QualifiedName", "SByte", "String", "UInt16", "UInt32", "UInt64", "UInteger", "UtcTime",
"Variant", "XmlElement"
];
var nodesToRead = new List();
foreach (string suffix in suffixes)
{
var nodeId = new NodeId("Scalar_Static_Arrays2D_" + suffix, namespaceIndex);
nodesToRead.Add(new ReadValueId { NodeId = nodeId, AttributeId = Attributes.ArrayDimensions });
nodesToRead.Add(new ReadValueId { NodeId = nodeId, AttributeId = Attributes.Value });
}
var requestHeader = (RequestHeader)m_requestHeader.Clone();
requestHeader.Timestamp = DateTimeUtc.Now;
ReadResponse response = await m_server.ReadAsync(
m_secureChannelContext,
requestHeader,
kMaxAge,
TimestampsToReturn.Neither,
nodesToRead.ToArrayOf(),
RequestLifetime.None).ConfigureAwait(false);
Assert.That(response.Results, Has.Count.EqualTo(nodesToRead.Count));
for (int ii = 0; ii < suffixes.Length; ii++)
{
DataValue dimensions = response.Results[2 * ii];
DataValue value = response.Results[(2 * ii) + 1];
Assert.That(dimensions.StatusCode, Is.EqualTo(StatusCodes.Good), suffixes[ii]);
Assert.That(dimensions.WrappedValue.TryGetValue(out ArrayOf arrayDimensions), Is.True, suffixes[ii]);
Assert.That(arrayDimensions.ToArray(), Is.EqualTo(s_staticMatrixDimensions), suffixes[ii]);
Assert.That(value.StatusCode, Is.EqualTo(StatusCodes.Good), suffixes[ii]);
Assert.That(value.WrappedValue.TypeInfo.ValueRank, Is.EqualTo(ValueRanks.TwoDimensions), suffixes[ii]);
Assert.That(value.WrappedValue.AsBoxedObject(), Is.InstanceOf(), suffixes[ii]);
var matrix = ((IConvertableToMatrix)value.WrappedValue.AsBoxedObject())
.ToMatrix(value.WrappedValue.TypeInfo.BuiltInType);
Assert.That(matrix.Dimensions, Is.EqualTo(s_staticMatrixDimensions), suffixes[ii]);
}
}
///
/// DataAccess_DataItem_String does not provide the optional MinimumSamplingInterval attribute
/// (CTT Base Info Server Capabilities 2 002.js).
///
[Test]
public async Task ReferenceNodeManagerDataItemHasNoMinimumSamplingIntervalAsync()
{
ushort namespaceIndex = (ushort)m_server.CurrentInstance.NamespaceUris.GetIndex(
Quickstarts.ReferenceServer.Namespaces.ReferenceServer);
var nodeId = new NodeId("DataAccess_DataItem_String", namespaceIndex);
ArrayOf nodesToRead =
[
new ReadValueId { NodeId = nodeId, AttributeId = Attributes.MinimumSamplingInterval },
new ReadValueId { NodeId = nodeId, AttributeId = Attributes.Value }
];
var requestHeader = (RequestHeader)m_requestHeader.Clone();
requestHeader.Timestamp = DateTimeUtc.Now;
ReadResponse response = await m_server.ReadAsync(
m_secureChannelContext,
requestHeader,
kMaxAge,
TimestampsToReturn.Neither,
nodesToRead,
RequestLifetime.None).ConfigureAwait(false);
Assert.That(response.Results, Has.Count.EqualTo(nodesToRead.Count));
Assert.That(response.Results[0].StatusCode, Is.EqualTo(StatusCodes.BadAttributeIdInvalid));
Assert.That(response.Results[1].StatusCode, Is.EqualTo(StatusCodes.Good));
// the node can still be monitored; the sampling interval is revised as usual.
var services = new ServerTestServices(m_server, m_secureChannelContext);
CreateSubscriptionResponse subscription = await services.CreateSubscriptionAsync(
requestHeader,
100,
100,
10,
0,
true,
0).ConfigureAwait(false);
try
{
CreateMonitoredItemsResponse items = await services.CreateMonitoredItemsAsync(
requestHeader,
subscription.SubscriptionId,
TimestampsToReturn.Neither,
[
new MonitoredItemCreateRequest
{
ItemToMonitor = new ReadValueId { NodeId = nodeId, AttributeId = Attributes.Value },
MonitoringMode = MonitoringMode.Reporting,
RequestedParameters = new MonitoringParameters
{
ClientHandle = 1,
SamplingInterval = 0,
QueueSize = 1,
DiscardOldest = true
}
}
]).ConfigureAwait(false);
Assert.That(items.Results, Has.Count.EqualTo(1));
Assert.That(items.Results[0].StatusCode, Is.EqualTo(StatusCodes.Good));
Assert.That(items.Results[0].RevisedSamplingInterval, Is.GreaterThanOrEqualTo(0));
}
finally
{
await services.DeleteSubscriptionsAsync(
requestHeader,
[subscription.SubscriptionId]).ConfigureAwait(false);
}
}
///
/// Scalar_Static_NonHistorizing_Boolean advertises HistoryRead access but is not
/// historized; a HistoryRead returns Bad_HistoryOperationUnsupported (Part 4 §5.11.3,
/// CTT Historical Access Read Raw Err-025.js).
///
[Test]
public async Task HistoryReadNonHistorizingNodeIsUnsupportedAsync()
{
ushort namespaceIndex = (ushort)m_server.CurrentInstance.NamespaceUris.GetIndex(
Quickstarts.ReferenceServer.Namespaces.ReferenceServer);
var nodeId = new NodeId("Scalar_Static_NonHistorizing_Boolean", namespaceIndex);
ArrayOf nodesToRead =
[
new ReadValueId { NodeId = nodeId, AttributeId = Attributes.Historizing },
new ReadValueId { NodeId = nodeId, AttributeId = Attributes.AccessLevel },
new ReadValueId { NodeId = nodeId, AttributeId = Attributes.Value }
];
var requestHeader = (RequestHeader)m_requestHeader.Clone();
requestHeader.Timestamp = DateTimeUtc.Now;
ReadResponse readResponse = await m_server.ReadAsync(
m_secureChannelContext,
requestHeader,
kMaxAge,
TimestampsToReturn.Neither,
nodesToRead,
RequestLifetime.None).ConfigureAwait(false);
Assert.That(readResponse.Results, Has.Count.EqualTo(nodesToRead.Count));
Assert.That(readResponse.Results[0].WrappedValue.GetBoolean(), Is.False);
Assert.That(
readResponse.Results[1].WrappedValue.GetByte() & AccessLevels.HistoryRead,
Is.EqualTo(AccessLevels.HistoryRead));
Assert.That(readResponse.Results[2].StatusCode, Is.EqualTo(StatusCodes.Good));
var historyReadDetails = new ReadRawModifiedDetails
{
StartTime = DateTimeUtc.Now.SubtractMilliseconds(60 * 60 * 1000),
EndTime = DateTimeUtc.Now,
NumValuesPerNode = 10,
IsReadModified = false,
ReturnBounds = false
};
requestHeader.Timestamp = DateTimeUtc.Now;
HistoryReadResponse historyReadResponse = await m_server.HistoryReadAsync(
m_secureChannelContext,
requestHeader,
new ExtensionObject(historyReadDetails),
TimestampsToReturn.Source,
false,
[new HistoryReadValueId { NodeId = nodeId }],
RequestLifetime.None).ConfigureAwait(false);
Assert.That(historyReadResponse.Results, Has.Count.EqualTo(1));
Assert.That(
historyReadResponse.Results[0].StatusCode.Code,
Is.EqualTo(StatusCodes.BadHistoryOperationUnsupported));
}
///
/// The Server object references nodes with HasComponent and with its subtype HasAddIn and
/// has more than three references, so it can back the CTT setting
/// "/Server Test/NodeIds/References/Has References of a ReferenceType and SubType"
/// (View Basic 2 005/015/016/020, View Minimum Continuation Point 01 010/011).
///
[Test]
public async Task ServerObjectHasReferencesOfATypeAndItsSubtypeAsync()
{
var requestHeader = (RequestHeader)m_requestHeader.Clone();
requestHeader.Timestamp = DateTimeUtc.Now;
BrowseResponse response = await m_server.BrowseAsync(
m_secureChannelContext,
requestHeader,
null,
0,
[
new BrowseDescription
{
NodeId = ObjectIds.Server,
BrowseDirection = BrowseDirection.Forward,
ReferenceTypeId = ReferenceTypeIds.HasComponent,
IncludeSubtypes = true,
ResultMask = (uint)BrowseResultMask.All
}
],
RequestLifetime.None).ConfigureAwait(false);
Assert.That(response.Results, Has.Count.EqualTo(1));
ReferenceDescription[] references = response.Results[0].References.ToArray()!;
Assert.That(
references!.Count(r => r.ReferenceTypeId == ReferenceTypeIds.HasComponent),
Is.GreaterThanOrEqualTo(3));
Assert.That(
references.Count(r => r.ReferenceTypeId == ReferenceTypeIds.HasAddIn),
Is.GreaterThanOrEqualTo(1));
}
///
/// The Method and View node class sample nodes reference at least two nodes of their
/// own NodeClass (CTT View Minimum Continuation Point 01 012.js, View Basic 2 018.js).
///
[Test]
[TestCase("Methods_Void", NodeClass.Method)]
[TestCase("Views_Operations", NodeClass.View)]
public async Task NodeClassSampleNodesReferenceTheirOwnNodeClassAsync(string identifier, NodeClass nodeClass)
{
ushort namespaceIndex = (ushort)m_server.CurrentInstance.NamespaceUris.GetIndex(
Quickstarts.ReferenceServer.Namespaces.ReferenceServer);
ArrayOf nodesToBrowse =
[
new BrowseDescription
{
NodeId = new NodeId(identifier, namespaceIndex),
BrowseDirection = BrowseDirection.Both,
ReferenceTypeId = ReferenceTypeIds.References,
IncludeSubtypes = true,
NodeClassMask = 0,
ResultMask = (uint)BrowseResultMask.All
}
];
var requestHeader = (RequestHeader)m_requestHeader.Clone();
requestHeader.Timestamp = DateTimeUtc.Now;
BrowseResponse response = await m_server.BrowseAsync(
m_secureChannelContext,
requestHeader,
null,
0,
nodesToBrowse,
RequestLifetime.None).ConfigureAwait(false);
Assert.That(response.Results, Has.Count.EqualTo(1));
Assert.That(response.Results[0].StatusCode, Is.EqualTo(StatusCodes.Good));
ArrayOf references = response.Results[0].References;
Assert.That(references.ToArray()!.Count(r => r.NodeClass == nodeClass), Is.GreaterThanOrEqualTo(2));
Assert.That(references.ToArray()!.Count(r => r.NodeClass != nodeClass), Is.GreaterThanOrEqualTo(1));
}
///
/// Views can only be the source of hierarchical References (Part 3 §5.4), and the
/// CTT (Base Info Core Structure 2 001.js) also rejects Views targeted by
/// non-hierarchical References.
///
[Test]
[TestCase("Views_Operations")]
[TestCase("Views_Engineering")]
[TestCase("Views_Maintenance")]
public async Task SampleViewsHaveNoNonHierarchicalReferencesAsync(string identifier)
{
ushort namespaceIndex = (ushort)m_server.CurrentInstance.NamespaceUris.GetIndex(
Quickstarts.ReferenceServer.Namespaces.ReferenceServer);
ArrayOf nodesToBrowse =
[
new BrowseDescription
{
NodeId = new NodeId(identifier, namespaceIndex),
BrowseDirection = BrowseDirection.Both,
ReferenceTypeId = ReferenceTypeIds.NonHierarchicalReferences,
IncludeSubtypes = true,
NodeClassMask = 0,
ResultMask = (uint)BrowseResultMask.All
}
];
var requestHeader = (RequestHeader)m_requestHeader.Clone();
requestHeader.Timestamp = DateTimeUtc.Now;
BrowseResponse response = await m_server.BrowseAsync(
m_secureChannelContext,
requestHeader,
null,
0,
nodesToBrowse,
RequestLifetime.None).ConfigureAwait(false);
Assert.That(response.Results, Has.Count.EqualTo(1));
Assert.That(response.Results[0].StatusCode, Is.EqualTo(StatusCodes.Good));
Assert.That(response.Results[0].References.ToArray(), Is.Empty);
}
///
/// Test that the ReferenceNodeManager Currency variable exposes its value
/// and its CurrencyUnit property.
///
[Test]
public async Task ReferenceNodeManagerCurrencyVariableAsync()
{
ushort namespaceIndex = (ushort)m_server.CurrentInstance.NamespaceUris.GetIndex(
Quickstarts.ReferenceServer.Namespaces.ReferenceServer);
var amountNodeId = new NodeId("DataAccess_Currency_Amount", namespaceIndex);
var currencyUnitNodeId = new NodeId(
"DataAccess_Currency_Amount_CurrencyUnit",
namespaceIndex);
ArrayOf nodesToRead =
[
new ReadValueId { NodeId = amountNodeId, AttributeId = Attributes.Value },
new ReadValueId { NodeId = currencyUnitNodeId, AttributeId = Attributes.Value }
];
RequestHeader requestHeader = m_requestHeader;
requestHeader.Timestamp = DateTimeUtc.Now;
ReadResponse readResponse = await m_server.ReadAsync(
m_secureChannelContext,
requestHeader,
kMaxAge,
TimestampsToReturn.Both,
nodesToRead,
RequestLifetime.None).ConfigureAwait(false);
Assert.That(readResponse, Is.Not.Null);
Assert.That(readResponse.Results.IsNull, Is.False);
Assert.That(readResponse.Results.Count, Is.EqualTo(2));
DataValue amount = readResponse.Results[0];
Assert.That(amount.StatusCode, Is.EqualTo(StatusCodes.Good));
Assert.That(amount.WrappedValue.GetDouble(), Is.EqualTo(42.0));
DataValue currencyUnit = readResponse.Results[1];
Assert.That(currencyUnit.StatusCode, Is.EqualTo(StatusCodes.Good));
Assert.That(currencyUnit.WrappedValue.TryGetValue(out ExtensionObject currencyUnitObject), Is.True);
Assert.That(currencyUnitObject.TryGetValue(out CurrencyUnitType? unit), Is.True);
Assert.That(unit!.NumericCode, Is.EqualTo(978));
Assert.That(unit.Exponent, Is.EqualTo(2));
Assert.That(unit.AlphabeticCode, Is.EqualTo("EUR"));
using var stream = new MemoryStream();
using (var encoder = new BinaryEncoder(
stream,
m_server.CurrentInstance.MessageContext,
leaveOpen: true))
{
unit.Encode(encoder);
}
byte[] encoded = stream.ToArray();
Assert.That(encoded, Has.Length.GreaterThan(2));
Assert.That(encoded[0], Is.EqualTo(0xD2), "NumericCode low byte");
Assert.That(encoded[1], Is.EqualTo(0x03), "NumericCode high byte");
Assert.That(encoded[2], Is.EqualTo(0x02), "Exponent byte");
}
///
/// Test that ReferenceNodeManager array variables update their SourceTimestamp on read.
///
[Test]
public async Task ReferenceNodeManagerArrayVariablesUpdateTimestampOnReadAsync()
{
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
ILogger logger = telemetry.CreateLogger();
// Read an array variable from the ReferenceNodeManager (namespace index 2)
var nodeId = new NodeId("Scalar_Static_Arrays_Byte", 2);
ArrayOf nodesToRead =
[
new ReadValueId
{
NodeId = nodeId,
AttributeId = Attributes.Value
}
];
// First read
RequestHeader requestHeader = m_requestHeader;
requestHeader.Timestamp = DateTimeUtc.Now;
DateTimeUtc timeBeforeFirstRead = DateTimeUtc.Now;
ReadResponse firstReadResponse = await m_server.ReadAsync(
m_secureChannelContext,
requestHeader,
kMaxAge,
TimestampsToReturn.Both,
nodesToRead,
RequestLifetime.None).ConfigureAwait(false);
Assert.That(firstReadResponse, Is.Not.Null);
Assert.That(firstReadResponse.Results.IsNull, Is.False);
Assert.That(firstReadResponse.Results.Count, Is.EqualTo(1));
DataValue firstValue = firstReadResponse.Results[0];
Assert.That(firstValue.StatusCode, Is.EqualTo(StatusCodes.Good));
Assert.That(firstValue.SourceTimestamp.IsNull, Is.False);
if (logger.IsEnabled(LogLevel.Information))
{
logger.LogInformation("Array First read - SourceTimestamp: {SourceTimestamp}, ServerTimestamp: {ServerTimestamp}",
firstValue.SourceTimestamp, firstValue.ServerTimestamp);
}
// Verify the timestamp is recent (not startup time)
Assert.That((long)firstValue.SourceTimestamp, Is.GreaterThanOrEqualTo((long)timeBeforeFirstRead.SubtractMilliseconds(1000)),
"Array SourceTimestamp should be close to the read time, not the server startup time");
// Wait a bit to ensure time difference
await Task.Delay(1500).ConfigureAwait(false);
// Second read
requestHeader.Timestamp = DateTimeUtc.Now;
DateTimeUtc timeBeforeSecondRead = DateTimeUtc.Now;
ReadResponse secondReadResponse = await m_server.ReadAsync(
m_secureChannelContext,
requestHeader,
kMaxAge,
TimestampsToReturn.Both,
nodesToRead,
RequestLifetime.None).ConfigureAwait(false);
Assert.That(secondReadResponse, Is.Not.Null);
Assert.That(secondReadResponse.Results.IsNull, Is.False);
Assert.That(secondReadResponse.Results.Count, Is.EqualTo(1));
DataValue secondValue = secondReadResponse.Results[0];
Assert.That(secondValue.StatusCode, Is.EqualTo(StatusCodes.Good));
Assert.That(secondValue.SourceTimestamp.IsNull, Is.False);
if (logger.IsEnabled(LogLevel.Information))
{
logger.LogInformation("Array Second read - SourceTimestamp: {SourceTimestamp}, ServerTimestamp: {ServerTimestamp}",
secondValue.SourceTimestamp, secondValue.ServerTimestamp);
}
// Verify the second timestamp is more recent than the first
Assert.That((long)secondValue.SourceTimestamp, Is.GreaterThan((long)firstValue.SourceTimestamp),
"Array SourceTimestamp should be updated on each read");
// Verify the second timestamp is recent
Assert.That((long)secondValue.SourceTimestamp, Is.GreaterThanOrEqualTo((long)timeBeforeSecondRead.SubtractMilliseconds(1000)),
"Array SourceTimestamp should be close to the second read time");
}
///
/// Update static Nodes, read modify write.
///
[Test]
[Order(350)]
public async Task ReadWriteUpdateNodesAsync()
{
// Nodes
NamespaceTable namespaceUris = m_server.CurrentInstance.NamespaceUris;
NodeId[] testSet =
[
.. CommonTestWorkers.NodeIdTestSetStatic
.Select(n => ExpandedNodeId.ToNodeId(n, namespaceUris))
];
await UpdateValuesAsync(testSet).ConfigureAwait(false);
}
///
/// Browse full address space.
///
[Test]
[Order(400)]
[Benchmark]
public async Task BrowseFullAddressSpaceAsync()
{
var serverTestServices = new ServerTestServices(m_server, m_secureChannelContext);
if (m_operationLimits == null)
{
await GetOperationLimitsAsync().ConfigureAwait(false);
}
m_referenceDescriptions = await CommonTestWorkers.BrowseFullAddressSpaceWorkerAsync(
serverTestServices,
m_requestHeader,
m_operationLimits).ConfigureAwait(false);
}
///
/// Translate references.
///
[Test]
[Order(500)]
[Benchmark]
public async Task TranslateBrowsePathAsync()
{
var serverTestServices = new ServerTestServices(m_server, m_secureChannelContext);
if (m_operationLimits == null)
{
await GetOperationLimitsAsync().ConfigureAwait(false);
}
if (m_referenceDescriptions.IsNull)
{
m_referenceDescriptions = await CommonTestWorkers.BrowseFullAddressSpaceWorkerAsync(
serverTestServices,
m_requestHeader,
m_operationLimits).ConfigureAwait(false);
}
_ = await CommonTestWorkers.TranslateBrowsePathWorkerAsync(
serverTestServices,
m_referenceDescriptions,
m_requestHeader,
m_operationLimits!).ConfigureAwait(false);
}
///
/// Create a subscription with a monitored item.
/// Read a few notifications with Publish.
/// Delete the monitored item and subscription.
///
[Test]
public async Task SubscriptionAsync()
{
var serverTestServices = new ServerTestServices(m_server, m_secureChannelContext);
await CommonTestWorkers.SubscriptionTestAsync(serverTestServices, m_requestHeader).ConfigureAwait(false);
}
///
/// Create a single session + subscription and raise a single event on the server node.
/// Validates all the event fields are properly received.
///
[Test]
public async Task ServerEventSubscribeTestAsync()
{
var services = new ServerTestServices(m_server, m_secureChannelContext);
RequestHeader requestHeader = m_requestHeader;
requestHeader.Timestamp = DateTime.UtcNow;
CreateSubscriptionResponse createSubscriptionResponse = await services.CreateSubscriptionAsync(
requestHeader,
100,
100,
10,
0,
true,
0).ConfigureAwait(false);
uint subscriptionId = createSubscriptionResponse.SubscriptionId;
// Build an event filter for the base event type.
var eventFilter = new EventFilter();
eventFilter.AddSelectClause(
ObjectTypeIds.BaseEventType,
QualifiedName.From(BrowseNames.EventId));
eventFilter.AddSelectClause(
ObjectTypeIds.BaseEventType,
QualifiedName.From(BrowseNames.EventType));
eventFilter.AddSelectClause(
ObjectTypeIds.BaseEventType,
QualifiedName.From(BrowseNames.SourceNode));
eventFilter.AddSelectClause(
ObjectTypeIds.BaseEventType,
QualifiedName.From(BrowseNames.SourceName));
eventFilter.AddSelectClause(
ObjectTypeIds.BaseEventType,
QualifiedName.From(BrowseNames.Time));
eventFilter.AddSelectClause(
ObjectTypeIds.BaseEventType,
QualifiedName.From(BrowseNames.Message));
eventFilter.AddSelectClause(
ObjectTypeIds.BaseEventType,
QualifiedName.From(BrowseNames.Severity));
ArrayOf monitoredItems = [
new MonitoredItemCreateRequest
{
ItemToMonitor = new ReadValueId { NodeId = ObjectIds.Server, AttributeId = Attributes.EventNotifier },
MonitoringMode = MonitoringMode.Reporting,
RequestedParameters = new MonitoringParameters
{
ClientHandle = 1,
SamplingInterval = 0,
QueueSize = 100,
DiscardOldest = true,
Filter = new ExtensionObject(eventFilter)
}
}
];
CreateMonitoredItemsResponse createItemsResponse = await services.CreateMonitoredItemsAsync(
requestHeader,
subscriptionId,
TimestampsToReturn.Both,
monitoredItems).ConfigureAwait(false);
ServerFixtureUtils.ValidateResponse(createItemsResponse.ResponseHeader, createItemsResponse.Results, monitoredItems);
Assert.AreEqual(1, createItemsResponse.Results.Count);
Assert.IsTrue(StatusCode.IsGood(createItemsResponse.Results[0].StatusCode));
// Generate event directly on the server
IServerInternal serverInternal = m_server.CurrentInstance;
ISystemContext serverContext = serverInternal.DefaultSystemContext;
var e = new BaseEventState(null);
const string eventMessage = "Integration Test Event";
e.Initialize(
serverContext,
serverInternal.ServerObject,
EventSeverity.Medium,
new LocalizedText(eventMessage));
serverInternal.ReportEvent(serverContext, e);
// Wait for subscription to deliver the event
await Task.Delay(200).ConfigureAwait(false);
// Publish request to get the event notification
var acknowledgements = new ArrayOf();
PublishResponse publishResponse = await services.PublishAsync(
requestHeader,
acknowledgements).ConfigureAwait(false);
Assert.IsNotNull(publishResponse.NotificationMessage);
Assert.IsNotNull(publishResponse.NotificationMessage.NotificationData);
Assert.IsTrue(publishResponse.NotificationMessage.NotificationData.Count > 0);
publishResponse.NotificationMessage.NotificationData[0].TryGetValue(out EventNotificationList? eventNotification);
Assert.IsNotNull(eventNotification);
Assert.IsTrue(eventNotification.Events.Count > 0);
EventFieldList targetEvent = eventNotification.Events.ToList().FirstOrDefault(
x => x.EventFields[5].TryGetValue(out LocalizedText lt) && lt.Text == eventMessage)!;
Assert.IsNotNull(targetEvent, "Did not receive the target event.");
ArrayOf eventFields = targetEvent.EventFields;
Assert.AreEqual(7, eventFields.Count); // we requested 7 fields in select clauses
Assert.IsFalse(eventFields[0].IsNull); // EventId
Assert.IsFalse(eventFields[1].IsNull); // EventType
Assert.IsFalse(eventFields[2].IsNull); // SourceNode
Assert.IsFalse(eventFields[3].IsNull); // SourceName
Assert.IsFalse(eventFields[4].IsNull); // Time
Assert.That(eventFields[5].TryGetValue(out LocalizedText receivedMessage), Is.True); // Message
Assert.IsFalse(receivedMessage.IsNull);
Assert.AreEqual(eventMessage, receivedMessage.Text);
Assert.That(eventFields[6].TryGetValue(out ushort receiveSeverity), Is.True);
Assert.AreEqual((ushort)EventSeverity.Medium, receiveSeverity); // Severity
// Delete subscription
await services.DeleteSubscriptionsAsync(
requestHeader,
[subscriptionId]).ConfigureAwait(false);
}
///
/// Create multiple sessions, each with a subscription.
/// Close all sessions without deleting subscriptions (abandoning them).
/// Concurrently delete the abandoned subscriptions through server-owned cleanup.
/// Verifies that the concurrent dictionary backing abandoned subscriptions
/// handles parallel access correctly (fix for issue #3612).
///
[Test]
public async Task DeleteAbandonedSubscriptionsConcurrentlyAsync()
{
const int sessionCount = 5;
var subscriptionIds = new List();
var subscriptionManager = (SubscriptionManager)m_server.CurrentInstance.SubscriptionManager;
int originalSubscriptionCount = subscriptionManager.GetSubscriptions().Count;
NamespaceTable namespaceUris = m_server.CurrentInstance.NamespaceUris;
NodeId[] testSet =
[
.. CommonTestWorkers.NodeIdTestSetStatic
.Select(n => ExpandedNodeId.ToNodeId(n, namespaceUris))
];
// Create multiple sessions, each with a subscription, then close
// the session without deleting subscriptions so they become abandoned.
for (int i = 0; i < sessionCount; i++)
{
(RequestHeader header, SecureChannelContext context) =
await m_server.CreateAndActivateSessionAsync($"AbandonSession_{i}")
.ConfigureAwait(false);
var services = new ServerTestServices(m_server, context);
header.Timestamp = DateTimeUtc.Now;
ArrayOf ids = await CommonTestWorkers.CreateSubscriptionForTransferAsync(
services, header, testSet, kQueueSize, -1).ConfigureAwait(false);
subscriptionIds.AddRange(ids.ToList());
// Close session without deleting subscriptions - makes them abandoned
header.Timestamp = DateTimeUtc.Now;
await m_server.CloseSessionAsync(context, header, false, RequestLifetime.None)
.ConfigureAwait(false);
}
Assert.That(subscriptionIds, Has.Count.EqualTo(sessionCount));
Assert.That(subscriptionIds, Is.Unique);
foreach (uint id in subscriptionIds)
{
Assert.That(subscriptionManager.TryGetSubscription(id, out ISubscription? subscription), Is.True);
Assert.That(subscription!.Session, Is.Null);
}
Assert.That(subscriptionManager.CaptureAbandonedPublishTimerSnapshot()
.Select(subscription => subscription.Id), Is.SupersetOf(subscriptionIds));
// A session cannot delete another session's abandoned subscription.
// Server-owned cleanup uses the same null context as ServerInternalData.
var deleteStart = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
Task[] deleteTasks =
[
.. subscriptionIds.Select(async id =>
{
await deleteStart.Task.ConfigureAwait(false);
return await subscriptionManager.DeleteSubscriptionAsync(null!, id).ConfigureAwait(false);
})
];
deleteStart.SetResult(true);
StatusCode[] results = await Task.WhenAll(deleteTasks).ConfigureAwait(false);
Assert.That(results, Has.Length.EqualTo(sessionCount));
foreach (StatusCode result in results)
{
Assert.That(result, Is.EqualTo(StatusCodes.Good));
}
foreach (uint id in subscriptionIds)
{
Assert.That(subscriptionManager.TryGetSubscription(id, out _), Is.False);
}
Assert.That(subscriptionManager.GetSubscriptions(), Has.Count.EqualTo(originalSubscriptionCount));
Assert.That(subscriptionManager.CaptureAbandonedPublishTimerSnapshot()
.Select(subscription => subscription.Id).Intersect(subscriptionIds), Is.Empty);
}
///
/// Create a secondary Session.
/// Create a subscription with a monitored item.
/// Close session, but do not delete subscriptions.
/// Transfer subscription from closed session to the other.
///
[Theory]
public async Task TransferSubscriptionSessionClosedAsync(bool sendInitialData, bool useSecurity)
{
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
var serverTestServices = new ServerTestServices(m_server, m_secureChannelContext);
(RequestHeader transferRequestHeader, SecureChannelContext transferContext) = await m_server.CreateAndActivateSessionAsync(
"ClosedSession",
useSecurity,
clientApplicationUri: kClientApplicationUri).ConfigureAwait(false);
NamespaceTable namespaceUris = m_server.CurrentInstance.NamespaceUris;
NodeId[] testSet =
[
.. CommonTestWorkers.NodeIdTestSetStatic
.Select(n => ExpandedNodeId.ToNodeId(n, namespaceUris))
];
transferRequestHeader.Timestamp = DateTimeUtc.Now;
serverTestServices.SecureChannelContext = transferContext;
ArrayOf subscriptionIds = await CommonTestWorkers.CreateSubscriptionForTransferAsync(
serverTestServices,
transferRequestHeader,
testSet,
kQueueSize,
-1).ConfigureAwait(false);
transferRequestHeader.Timestamp = DateTimeUtc.Now;
await m_server.CloseSessionAsync(transferContext, transferRequestHeader, false, RequestLifetime.None).ConfigureAwait(false);
//restore security context, transfer abandoned subscription
serverTestServices.SecureChannelContext = m_secureChannelContext;
await CommonTestWorkers.TransferSubscriptionTestAsync(
serverTestServices,
m_requestHeader,
subscriptionIds,
sendInitialData,
!useSecurity).ConfigureAwait(false);
if (useSecurity)
{
// subscription was deleted, expect 'BadNoSubscription'
ServiceResultException sre = Assert.ThrowsAsync(async () =>
{
m_requestHeader.Timestamp = DateTimeUtc.Now;
await CommonTestWorkers.VerifySubscriptionTransferredAsync(
serverTestServices,
m_requestHeader,
subscriptionIds,
true).ConfigureAwait(false);
});
Assert.That(
sre.StatusCode,
Is.EqualTo(StatusCodes.BadNoSubscription));
}
}
///
/// Create a subscription with a monitored item.
/// Create a secondary Session.
/// Transfer subscription with a monitored item from one session to the other.
///
[Theory]
public async Task TransferSubscriptionAsync(bool sendInitialData, bool useSecurity)
{
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
var serverTestServices = new ServerTestServices(m_server, m_secureChannelContext);
NamespaceTable namespaceUris = m_server.CurrentInstance.NamespaceUris;
NodeId[] testSet =
[
.. CommonTestWorkers.NodeIdTestSetStatic
.Select(n => ExpandedNodeId.ToNodeId(n, namespaceUris))
];
ArrayOf subscriptionIds = await CommonTestWorkers.CreateSubscriptionForTransferAsync(
serverTestServices,
m_requestHeader,
testSet,
kQueueSize,
-1).ConfigureAwait(false);
(RequestHeader transferRequestHeader, SecureChannelContext transferSecurityContext) = await m_server.CreateAndActivateSessionAsync(
"TransferSession",
useSecurity,
clientApplicationUri: kClientApplicationUri).ConfigureAwait(false);
serverTestServices.SecureChannelContext = transferSecurityContext;
await CommonTestWorkers.TransferSubscriptionTestAsync(
serverTestServices,
transferRequestHeader,
subscriptionIds,
sendInitialData,
!useSecurity).ConfigureAwait(false);
if (useSecurity)
{
//restore security context
serverTestServices.SecureChannelContext = m_secureChannelContext;
await CommonTestWorkers.VerifySubscriptionTransferredAsync(
serverTestServices,
m_requestHeader,
subscriptionIds,
true).ConfigureAwait(false);
}
transferRequestHeader.Timestamp = DateTimeUtc.Now;
await m_server.CloseSessionAsync(transferSecurityContext, transferRequestHeader, true, RequestLifetime.None).ConfigureAwait(false);
}
///
/// Verifies that an anonymous Subscription cannot be transferred to a
/// Session with a different client ApplicationUri.
///
[Test]
public async Task TransferAnonymousSubscriptionWithDifferentApplicationUriDeniedAsync()
{
var serverTestServices = new ServerTestServices(m_server, m_secureChannelContext);
NamespaceTable namespaceUris = m_server.CurrentInstance.NamespaceUris;
NodeId[] testSet =
[
.. CommonTestWorkers.NodeIdTestSetStatic
.Select(n => ExpandedNodeId.ToNodeId(n, namespaceUris))
];
ArrayOf subscriptionIds = await CommonTestWorkers.CreateSubscriptionForTransferAsync(
serverTestServices,
m_requestHeader,
testSet,
kQueueSize,
-1).ConfigureAwait(false);
(RequestHeader transferRequestHeader, SecureChannelContext transferSecurityContext) =
await m_server.CreateAndActivateSessionAsync(
"TransferSessionDifferentApplicationUri",
useSecurity: true,
clientApplicationUri:
"urn:localhost:opcfoundation.org:DifferentServerFixtureClient")
.ConfigureAwait(false);
try
{
serverTestServices.SecureChannelContext = transferSecurityContext;
await CommonTestWorkers.TransferSubscriptionTestAsync(
serverTestServices,
transferRequestHeader,
subscriptionIds,
sendInitialData: false,
expectAccessDenied: true).ConfigureAwait(false);
}
finally
{
transferRequestHeader.Timestamp = DateTimeUtc.Now;
await m_server.CloseSessionAsync(
transferSecurityContext,
transferRequestHeader,
true,
RequestLifetime.None).ConfigureAwait(false);
}
}
///
/// Create a subscription with a monitored item.
/// Call ResendData.
/// Ensure only a single value per monitored item is returned after ResendData was called.
///
[Test]
[TestCase(true, kQueueSize)]
[TestCase(false, kQueueSize)]
[TestCase(true, 0U)]
[TestCase(false, 0U)]
public async Task ResendDataAsync(bool updateValues, uint queueSize)
{
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
var serverTestServices = new ServerTestServices(m_server, m_secureChannelContext);
NamespaceTable namespaceUris = m_server.CurrentInstance.NamespaceUris;
var testSetCollection = CommonTestWorkers
.NodeIdTestSetStatic.Select(n => ExpandedNodeId.ToNodeId(n, namespaceUris))
.ToList();
testSetCollection.AddRange(
CommonTestWorkers.NodeIdTestDataSetStatic
.Select(n => ExpandedNodeId.ToNodeId(n, namespaceUris)));
NodeId[] testSet = [.. testSetCollection];
//Re-use method CreateSubscriptionForTransfer to create a subscription
ArrayOf subscriptionIds = await CommonTestWorkers.CreateSubscriptionForTransferAsync(
serverTestServices,
m_requestHeader,
testSet,
queueSize,
0).ConfigureAwait(false);
(RequestHeader resendDataRequestHeader, SecureChannelContext resendDataSecurityContext) =
await m_server.CreateAndActivateSessionAsync("ResendData").ConfigureAwait(false);
serverTestServices.SecureChannelContext = m_secureChannelContext;
// After the ResendData call there will be data to publish again
ArrayOf nodesToCall = await ResendDataCallAsync(
StatusCodes.Good,
subscriptionIds).ConfigureAwait(false);
Thread.Sleep(1000);
// Make sure publish queue becomes empty by consuming it
Assert.That(subscriptionIds.Count, Is.EqualTo(1));
// Issue a Publish request
m_requestHeader.Timestamp = DateTimeUtc.Now;
ArrayOf acknowledgements = [];
PublishResponse publishResponse = await serverTestServices.PublishAsync(
m_requestHeader,
acknowledgements).ConfigureAwait(false);
Assert.That(publishResponse.ResponseHeader.ServiceResult, Is.EqualTo(StatusCodes.Good));
ServerFixtureUtils.ValidateResponse(publishResponse.ResponseHeader);
ServerFixtureUtils.ValidateDiagnosticInfos(
publishResponse.DiagnosticInfos,
acknowledgements,
publishResponse.ResponseHeader.StringTable,
serverTestServices.Logger);
Assert.That(publishResponse.SubscriptionId, Is.EqualTo(subscriptionIds[0]));
Assert.That(publishResponse.NotificationMessage.NotificationData.Count, Is.EqualTo(1));
// Validate nothing to publish a few times
const int timesToCallPublish = 3;
for (int i = 0; i < timesToCallPublish; i++)
{
m_requestHeader.Timestamp = DateTimeUtc.Now;
publishResponse = await serverTestServices.PublishAsync(
m_requestHeader,
acknowledgements).ConfigureAwait(false);
Assert.That(publishResponse.ResponseHeader.ServiceResult, Is.EqualTo(StatusCodes.Good));
ServerFixtureUtils.ValidateResponse(publishResponse.ResponseHeader);
ServerFixtureUtils.ValidateDiagnosticInfos(
publishResponse.DiagnosticInfos,
acknowledgements,
publishResponse.ResponseHeader.StringTable,
serverTestServices.Logger);
Assert.That(publishResponse.SubscriptionId, Is.EqualTo(subscriptionIds[0]));
Assert.That(publishResponse.NotificationMessage.NotificationData.Count, Is.Zero);
}
// Validate ResendData method call returns error from different session contexts
// call ResendData method from different session context
resendDataRequestHeader.Timestamp = DateTimeUtc.Now;
CallResponse callResponse = await m_server.CallAsync(
resendDataSecurityContext,
resendDataRequestHeader,
nodesToCall, RequestLifetime.None).ConfigureAwait(false);
serverTestServices.SecureChannelContext = m_secureChannelContext;
Assert.That(callResponse.Results[0].StatusCode, Is.EqualTo(StatusCodes.BadUserAccessDenied));
ServerFixtureUtils.ValidateResponse(callResponse.ResponseHeader, callResponse.Results, nodesToCall);
ServerFixtureUtils.ValidateDiagnosticInfos(
callResponse.DiagnosticInfos,
nodesToCall,
callResponse.ResponseHeader.StringTable,
serverTestServices.Logger);
// Still nothing to publish since previous ResendData call did not execute
m_requestHeader.Timestamp = DateTimeUtc.Now;
publishResponse = await serverTestServices.PublishAsync(
m_requestHeader,
acknowledgements).ConfigureAwait(false);
Assert.That(publishResponse.ResponseHeader.ServiceResult, Is.EqualTo(StatusCodes.Good));
ServerFixtureUtils.ValidateResponse(publishResponse.ResponseHeader);
ServerFixtureUtils.ValidateDiagnosticInfos(
publishResponse.DiagnosticInfos,
acknowledgements,
publishResponse.ResponseHeader.StringTable,
serverTestServices.Logger);
Assert.That(publishResponse.SubscriptionId, Is.EqualTo(subscriptionIds[0]));
Assert.That(publishResponse.NotificationMessage.NotificationData.Count, Is.Zero);
if (updateValues)
{
await UpdateValuesAsync(testSet).ConfigureAwait(false);
// fill queues, but only a single value per resend publish shall be returned
for (int i = 1; i < queueSize; i++)
{
//If sampling groups are used, samplingInterval needs to be waited before values are queued
if (m_fixture.UseSamplingGroupsInReferenceNodeManager)
{
await Task.Delay((int)(100.0 * 1.7)).ConfigureAwait(false);
}
await UpdateValuesAsync(testSet).ConfigureAwait(false);
}
// Wait a bit to ensure that the server has time to queue the values
await Task.Delay(1000).ConfigureAwait(false);
}
// call ResendData method from the same session context
await ResendDataCallAsync(StatusCodes.Good, subscriptionIds).ConfigureAwait(false);
// Data should be available for publishing now
int totalNotifications = await CollectNotificationsAsync(
serverTestServices,
m_requestHeader,
acknowledgements,
subscriptionIds[0],
testSet.Length).ConfigureAwait(false);
Assert.That(totalNotifications, Is.EqualTo(testSet.Length),
"One MonitoredItemNotification should be returned for each Node present in the TestSet");
await Task.Delay(1000).ConfigureAwait(false);
if (updateValues && queueSize > 1)
{
// remaining queue Data should be sent in this publish
int expectedCount = testSet.Length * ((int)queueSize - 1);
totalNotifications = await CollectNotificationsAsync(
serverTestServices,
m_requestHeader,
acknowledgements,
subscriptionIds[0],
expectedCount).ConfigureAwait(false);
Assert.That(
totalNotifications,
Is.EqualTo(expectedCount).Within(testSet.Length));
}
// Call ResendData method with invalid subscription Id
await ResendDataCallAsync(StatusCodes.BadSubscriptionIdInvalid, [subscriptionIds[^1] + 20]).ConfigureAwait(false);
// Nothing to publish since previous ResendData call did not execute
m_requestHeader.Timestamp = DateTimeUtc.Now;
publishResponse = await serverTestServices.PublishAsync(
m_requestHeader,
acknowledgements).ConfigureAwait(false);
Assert.That(publishResponse.ResponseHeader.ServiceResult, Is.EqualTo(StatusCodes.Good));
ServerFixtureUtils.ValidateResponse(publishResponse.ResponseHeader);
ServerFixtureUtils.ValidateDiagnosticInfos(
publishResponse.DiagnosticInfos,
acknowledgements,
publishResponse.ResponseHeader.StringTable,
serverTestServices.Logger);
Assert.That(publishResponse.SubscriptionId, Is.EqualTo(subscriptionIds[0]));
Assert.That(publishResponse.NotificationMessage.NotificationData.Count, Is.Zero);
resendDataRequestHeader.Timestamp = DateTimeUtc.Now;
await m_server.CloseSessionAsync(resendDataSecurityContext, resendDataRequestHeader, true, RequestLifetime.None).ConfigureAwait(false);
}
private static async Task CollectNotificationsAsync(
ServerTestServices serverTestServices,
RequestHeader requestHeader,
ArrayOf acknowledgements,
uint subscriptionId,
int expectedCount)
{
int totalNotifications = 0;
int retries = 50;
while (totalNotifications < expectedCount && retries-- > 0)
{
requestHeader.Timestamp = DateTime.UtcNow;
PublishResponse publishResponse = await serverTestServices.PublishAsync(
requestHeader,
acknowledgements).ConfigureAwait(false);
Assert.That(publishResponse.ResponseHeader.ServiceResult, Is.EqualTo(StatusCodes.Good));
ServerFixtureUtils.ValidateResponse(publishResponse.ResponseHeader);
Assert.That(publishResponse.SubscriptionId, Is.EqualTo(subscriptionId));
if (publishResponse.NotificationMessage.NotificationData.Count > 0)
{
// acknowledge the notification
acknowledgements =
[
new SubscriptionAcknowledgement
{
SubscriptionId = subscriptionId,
SequenceNumber = publishResponse.NotificationMessage.SequenceNumber
}
];
foreach (ExtensionObject item in publishResponse.NotificationMessage.NotificationData)
{
if (item.TryGetValue(out DataChangeNotification? dcn))
{
totalNotifications += dcn.MonitoredItems.Count;
}
}
}
if (totalNotifications >= expectedCount)
{
break;
}
if (!publishResponse.MoreNotifications)
{
// Wait for potential background processing
await Task.Delay(100).ConfigureAwait(false);
}
}
return totalNotifications;
}
private async Task> ResendDataCallAsync(
StatusCode expectedStatus,
ArrayOf subscriptionIds)
{
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
ILogger logger = telemetry.CreateLogger();
// Find the ResendData method
ArrayOf nodesToCall = subscriptionIds
.ConvertAll(subscriptionId => new CallMethodRequest
{
ObjectId = ObjectIds.Server,
MethodId = MethodIds.Server_ResendData,
InputArguments = [new Variant(subscriptionId)]
});
//call ResendData method with subscription ids
m_requestHeader.Timestamp = DateTimeUtc.Now;
CallResponse callResponse = await m_server.CallAsync(
m_secureChannelContext,
m_requestHeader,
nodesToCall, RequestLifetime.None).ConfigureAwait(false);
Assert.That(callResponse.Results[0].StatusCode, Is.EqualTo(expectedStatus));
ServerFixtureUtils.ValidateResponse(callResponse.ResponseHeader, callResponse.Results, nodesToCall);
ServerFixtureUtils.ValidateDiagnosticInfos(
callResponse.DiagnosticInfos,
nodesToCall,
callResponse.ResponseHeader.StringTable,
logger);
return nodesToCall;
}
///
/// Read Values of NodeIds, determine types, write back new random values.
///
/// The nodeIds to modify.
private async Task UpdateValuesAsync(NodeId[] testSet)
{
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
ILogger logger = telemetry.CreateLogger();
// Read values
RequestHeader requestHeader = m_requestHeader;
var nodesToRead = testSet
.Select(nodeId => new ReadValueId
{
NodeId = nodeId,
AttributeId = Attributes.Value
})
.ToArrayOf();
ReadResponse readResponse = await m_server.ReadAsync(
m_secureChannelContext,
requestHeader,
kMaxAge,
TimestampsToReturn.Neither,
nodesToRead, RequestLifetime.None).ConfigureAwait(false);
ServerFixtureUtils.ValidateResponse(readResponse.ResponseHeader, readResponse.Results, nodesToRead);
ServerFixtureUtils.ValidateDiagnosticInfos(
readResponse.DiagnosticInfos,
readResponse.Results,
readResponse.ResponseHeader.StringTable,
logger);
Assert.That(readResponse.Results.Count, Is.EqualTo(testSet.Length));
var modifiedValues = new List();
foreach (DataValue dataValue in readResponse.Results)
{
TypeInfo typeInfo = dataValue.WrappedValue.TypeInfo;
Assert.That(typeInfo.IsUnknown, Is.False);
Variant value = m_generator.GetRandomScalar(typeInfo.BuiltInType);
modifiedValues.Add(new DataValue(value));
}
int ii = 0;
var nodesToWrite = testSet
.Select(nodeId => new WriteValue
{
NodeId = nodeId,
AttributeId = Attributes.Value,
Value = modifiedValues[ii++]
})
.ToArrayOf();
// Write Nodes
requestHeader.Timestamp = DateTimeUtc.Now;
WriteResponse writeResponse = await m_server.WriteAsync(
m_secureChannelContext,
requestHeader,
nodesToWrite, RequestLifetime.None).ConfigureAwait(false);
ServerFixtureUtils.ValidateResponse(writeResponse.ResponseHeader, writeResponse.Results, nodesToWrite);
ServerFixtureUtils.ValidateDiagnosticInfos(
writeResponse.DiagnosticInfos,
writeResponse.Results,
writeResponse.ResponseHeader.StringTable,
logger);
}
///
/// Verifies that an engineering-unit change emits a semantic-change event and a data notification with
/// SemanticsChanged.
///
[Test]
public async Task SemanticChangeNotificationTestAsync()
{
var services = new ServerTestServices(m_server, m_secureChannelContext);
RequestHeader requestHeader = m_requestHeader;
requestHeader.Timestamp = DateTime.UtcNow;
// Step 1: Create a subscription
CreateSubscriptionResponse createSubscriptionResponse = await services.CreateSubscriptionAsync(
requestHeader,
100,
100,
10,
0,
true,
0).ConfigureAwait(false);
uint subscriptionId = createSubscriptionResponse.SubscriptionId;
// We monitor the server EventNotifier for SemanticChangeEventType
var serverObject = new ReadValueId { NodeId = ObjectIds.Server, AttributeId = Attributes.EventNotifier };
var eventFilter = new EventFilter();
eventFilter.AddSelectClause(
ObjectTypeIds.SemanticChangeEventType,
QualifiedName.From(BrowseNames.EventId));
eventFilter.AddSelectClause(
ObjectTypeIds.SemanticChangeEventType,
QualifiedName.From(BrowseNames.Changes));
// Monitor a variable from ReferenceNodeManager (AnalogItem with EngineeringUnits)
var nodeId = new NodeId("DataAccess_AnalogType_Double",
(ushort)m_server.CurrentInstance.NamespaceUris.GetIndex(Quickstarts.ReferenceServer.Namespaces.ReferenceServer));
ArrayOf monitoredItems =
[
new MonitoredItemCreateRequest
{
ItemToMonitor = serverObject,
MonitoringMode = MonitoringMode.Reporting,
RequestedParameters = new MonitoringParameters
{
ClientHandle = 1,
SamplingInterval = 0,
QueueSize = 1,
DiscardOldest = true,
Filter = new ExtensionObject(eventFilter)
}
},
new MonitoredItemCreateRequest
{
ItemToMonitor = new ReadValueId { NodeId = nodeId, AttributeId = Attributes.Value },
MonitoringMode = MonitoringMode.Reporting,
RequestedParameters = new MonitoringParameters
{
ClientHandle = 2,
SamplingInterval = 0,
QueueSize = 1,
DiscardOldest = true
}
}
];
CreateMonitoredItemsResponse createItemsResponse = await services.CreateMonitoredItemsAsync(
requestHeader,
subscriptionId,
TimestampsToReturn.Both,
monitoredItems).ConfigureAwait(false);
ServerFixtureUtils.ValidateResponse(createItemsResponse.ResponseHeader, createItemsResponse.Results, monitoredItems);
Assert.AreEqual(2, createItemsResponse.Results.Count);
Assert.IsTrue(StatusCode.IsGood(createItemsResponse.Results[0].StatusCode));
Assert.IsTrue(StatusCode.IsGood(createItemsResponse.Results[1].StatusCode));
// Initial publish to clear any initial data change notifications
var acknowledgements = new ArrayOf();
PublishResponse publishResponse = await services.PublishAsync(requestHeader, acknowledgements).ConfigureAwait(false);
if (publishResponse.NotificationMessage.NotificationData.Count > 0)
{
acknowledgements =
[
new SubscriptionAcknowledgement
{
SubscriptionId = subscriptionId,
SequenceNumber = publishResponse.NotificationMessage.SequenceNumber
}
];
}
// Step 2: Write a new value to a semantic property (EngineeringUnits)
var euNodeId = new NodeId("DataAccess_AnalogType_DataAccess_AnalogType_Double_EngineeringUnits",
(ushort)m_server.CurrentInstance.NamespaceUris.GetIndex(Quickstarts.ReferenceServer.Namespaces.ReferenceServer));
var engUnits = new EUInformation("V", "volt", "http://www.opcfoundation.org/UA/units/un/cefact")
{
UnitId = 4274026 // "V"
};
ArrayOf writeValues = new WriteValue[]
{
new() {
NodeId = euNodeId,
AttributeId = Attributes.Value,
Value = new DataValue(new ExtensionObject(engUnits))
}
}.ToArrayOf();
requestHeader.Timestamp = DateTime.UtcNow;
WriteResponse writeResponse = await m_server.WriteAsync(
m_secureChannelContext,
requestHeader,
writeValues, RequestLifetime.None).ConfigureAwait(false);
ServerFixtureUtils.ValidateResponse(writeResponse.ResponseHeader, writeResponse.Results, writeValues);
Assert.IsTrue(StatusCode.IsGood(writeResponse.Results[0]));
// Wait for subscription to deliver the event and data change notification
await Task.Delay(200).ConfigureAwait(false);
// Step 3: Issue publish to receive DataChangeNotification and SemanticChangeEvent
publishResponse = await services.PublishAsync(
requestHeader,
acknowledgements).ConfigureAwait(false);
Assert.IsNotNull(publishResponse.NotificationMessage);
Assert.IsNotNull(publishResponse.NotificationMessage.NotificationData);
Assert.IsTrue(publishResponse.NotificationMessage.NotificationData.Count == 2, "One Event and One DataChangeNotification expected.");
bool dataChangeReceived = false;
bool semanticsChangedBitSet = false;
bool eventReceived = false;
foreach (ExtensionObject data in publishResponse.NotificationMessage.NotificationData)
{
if (data.TryGetValue(out DataChangeNotification? dcn))
{
foreach (MonitoredItemNotification item in dcn.MonitoredItems)
{
if (item.ClientHandle == 2)
{
dataChangeReceived = true;
if (item.Value.StatusCode.SemanticsChanged)
{
semanticsChangedBitSet = true;
}
}
}
}
else if (data.TryGetValue(out EventNotificationList? enl))
{
foreach (EventFieldList e in enl.Events)
{
if (e.ClientHandle == 1 && e.EventFields.Count >= 2)
{
bool success = e.EventFields[1]
.TryGetStructure(out ArrayOf semanticChangeEvent);
if (success && !semanticChangeEvent.IsNull && semanticChangeEvent.Count > 0)
{
foreach (SemanticChangeStructureDataType change in semanticChangeEvent)
{
if (change.Affected == nodeId)
{
eventReceived = true;
break;
}
}
}
}
}
}
}
Assert.IsTrue(dataChangeReceived, "Did not receive DataChangeNotification.");
Assert.IsTrue(semanticsChangedBitSet, "SemanticsChanged bit is not set.");
Assert.IsTrue(eventReceived, "Did not receive SemanticChangeEvent.");
// Delete subscription
await services.DeleteSubscriptionsAsync(
requestHeader,
[subscriptionId]).ConfigureAwait(false);
}
///
/// Test that historical EventNotifier bits are set on the actual
/// historical notifier rather than inferred on the Server object.
///
[Test]
public async Task HistoricalEventNotifierBitsAreNodeSpecificAsync()
{
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
ILogger logger = telemetry.CreateLogger();
// Read Server object EventNotifier attribute
ArrayOf readIdCollection =
[
new ReadValueId
{
AttributeId = Attributes.EventNotifier,
NodeId = ObjectIds.Server
}
];
m_requestHeader.Timestamp = DateTimeUtc.Now;
ReadResponse readResponse = await m_server.ReadAsync(
m_secureChannelContext,
m_requestHeader,
0,
TimestampsToReturn.Both,
readIdCollection, RequestLifetime.None).ConfigureAwait(false);
ServerFixtureUtils.ValidateResponse(readResponse.ResponseHeader, readResponse.Results, readIdCollection);
Assert.That(readResponse.Results.Count, Is.EqualTo(1));
Assert.That(readResponse.Results[0].WrappedValue.IsNull, Is.False);
byte eventNotifier = (byte)readResponse.Results[0].WrappedValue;
// Read history capabilities
ArrayOf historyCapabilitiesReadIds =
[
new ReadValueId {
AttributeId = Attributes.Value,
NodeId = VariableIds.HistoryServerCapabilities_AccessHistoryEventsCapability
},
new ReadValueId {
AttributeId = Attributes.Value,
NodeId = VariableIds.HistoryServerCapabilities_AccessHistoryDataCapability
}
];
m_requestHeader.Timestamp = DateTimeUtc.Now;
readResponse = await m_server.ReadAsync(
m_secureChannelContext,
m_requestHeader,
0,
TimestampsToReturn.Both,
historyCapabilitiesReadIds, RequestLifetime.None).ConfigureAwait(false);
ServerFixtureUtils.ValidateResponse(readResponse.ResponseHeader, readResponse.Results, historyCapabilitiesReadIds);
Assert.That(readResponse.Results.Count, Is.EqualTo(2));
bool accessHistoryEventsCapability =
!readResponse.Results[0].WrappedValue.IsNull &&
(bool)readResponse.Results[0].WrappedValue;
bool accessHistoryDataCapability =
!readResponse.Results[1].WrappedValue.IsNull &&
(bool)readResponse.Results[1].WrappedValue;
if (logger.IsEnabled(LogLevel.Information))
{
logger.LogInformation("Server EventNotifier: {EventNotifier}", eventNotifier);
logger.LogInformation("AccessHistoryEventsCapability: {AccessHistoryEventsCapability}", accessHistoryEventsCapability);
logger.LogInformation("AccessHistoryDataCapability: {AccessHistoryDataCapability}", accessHistoryDataCapability);
}
Assert.That(
eventNotifier & EventNotifiers.HistoryRead,
Is.Zero,
"The Server object is not itself configured as a historical event notifier.");
if (accessHistoryEventsCapability)
{
ArrayOf notifierRead =
[
new ReadValueId
{
AttributeId = Attributes.EventNotifier,
NodeId = new NodeId("CTT", 2)
}
];
ReadResponse notifierResponse = await m_server.ReadAsync(
m_secureChannelContext,
m_requestHeader,
0,
TimestampsToReturn.Neither,
notifierRead,
RequestLifetime.None).ConfigureAwait(false);
Assert.That(
(byte)notifierResponse.Results[0].WrappedValue &
EventNotifiers.HistoryRead,
Is.Not.Zero,
"The configured CTT historical event notifier must expose HistoryRead.");
}
// Verify SubscribeToEvents bit is set (Server object should always support events)
Assert.That(eventNotifier & EventNotifiers.SubscribeToEvents,
Is.Not.Zero,
"Server EventNotifier should have SubscribeToEvents bit set");
}
///
/// Verify that ServerStatus children have matching SourceTimestamp and ServerTimestamp.
///
[Test]
public async Task ServerStatusTimestampsMatchAsync()
{
ILogger logger = m_telemetry.CreateLogger();
// Read ServerStatus children (CurrentTime, StartTime, State, etc.)
ArrayOf nodesToRead =
[
new ReadValueId { NodeId = VariableIds.Server_ServerStatus_CurrentTime, AttributeId = Attributes.Value },
new ReadValueId { NodeId = VariableIds.Server_ServerStatus_StartTime, AttributeId = Attributes.Value },
new ReadValueId { NodeId = VariableIds.Server_ServerStatus_State, AttributeId = Attributes.Value }
];
m_requestHeader.Timestamp = DateTimeUtc.Now;
ReadResponse readResponse = await m_server.ReadAsync(
m_secureChannelContext,
m_requestHeader,
0,
TimestampsToReturn.Both,
nodesToRead,
RequestLifetime.None).ConfigureAwait(false);
ServerFixtureUtils.ValidateResponse(readResponse.ResponseHeader, readResponse.Results, nodesToRead);
Assert.That(readResponse.Results.Count, Is.EqualTo(3));
// Verify that SourceTimestamp and ServerTimestamp are equal for all ServerStatus children
for (int i = 0; i < readResponse.Results.Count; i++)
{
DataValue result = readResponse.Results[i];
if (logger.IsEnabled(LogLevel.Information))
{
logger.LogInformation(
"NodeId: {NodeId}, SourceTimestamp: {SourceTimestamp}, ServerTimestamp: {ServerTimestamp}",
nodesToRead[i].NodeId,
result.SourceTimestamp,
result.ServerTimestamp);
}
Assert.That(result.ServerTimestamp, Is.EqualTo(result.SourceTimestamp),
$"SourceTimestamp and ServerTimestamp should be equal for {nodesToRead[i].NodeId}");
}
}
///
/// Test that the Int32Value node (ns=3;i=2808) allows historical data access.
/// Verifies the fix for issue #2520 where the node was marked as historizing
/// but history read operations returned BadHistoryOperationUnsupported.
///
[Test]
public async Task HistoryReadInt32ValueNodeAsync()
{
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
ILogger logger = telemetry.CreateLogger();
// Get the NodeId for Data_Dynamic_Scalar_Int32Value
var int32ValueNodeId = new NodeId(
TestData.Variables.Data_Dynamic_Scalar_Int32Value,
(ushort)m_server.CurrentInstance.NamespaceUris.GetIndex(TestData.Namespaces.TestData));
if (logger.IsEnabled(LogLevel.Information))
{
logger.LogInformation("Testing history read for Int32Value node: {NodeId}", int32ValueNodeId);
}
// Verify the node has Historizing attribute set to true
ArrayOf readIdCollection =
[
new ReadValueId
{
AttributeId = Attributes.Historizing,
NodeId = int32ValueNodeId
},
new ReadValueId
{
AttributeId = Attributes.AccessLevel,
NodeId = int32ValueNodeId
}
];
m_requestHeader.Timestamp = DateTimeUtc.Now;
ReadResponse readResponse = await m_server.ReadAsync(
m_secureChannelContext,
m_requestHeader,
kMaxAge,
TimestampsToReturn.Neither,
readIdCollection,
RequestLifetime.None).ConfigureAwait(false);
ServerFixtureUtils.ValidateResponse(readResponse.ResponseHeader, readResponse.Results, readIdCollection);
Assert.That(readResponse.Results.Count, Is.EqualTo(2));
bool historizing = (bool)readResponse.Results[0].WrappedValue;
byte accessLevel = (byte)readResponse.Results[1].WrappedValue;
if (logger.IsEnabled(LogLevel.Information))
{
logger.LogInformation("Historizing: {Historizing}, AccessLevel: {AccessLevel}", historizing, accessLevel);
}
Assert.That(historizing, Is.True, "Int32Value node should have Historizing=true");
Assert.That(accessLevel & AccessLevels.HistoryRead,
Is.Not.Zero,
"Int32Value node should have HistoryRead access level");
// Perform a history read operation
var historyReadDetails = new ReadRawModifiedDetails
{
StartTime = DateTimeUtc.Now.SubtractMilliseconds(60 * 60 * 1000),
EndTime = DateTimeUtc.Now,
NumValuesPerNode = 10,
IsReadModified = false,
ReturnBounds = false
};
ArrayOf nodesToRead =
[
new HistoryReadValueId
{
NodeId = int32ValueNodeId
}
];
m_requestHeader.Timestamp = DateTimeUtc.Now;
HistoryReadResponse historyReadResponse = await m_server.HistoryReadAsync(
m_secureChannelContext,
m_requestHeader,
new ExtensionObject(historyReadDetails),
TimestampsToReturn.Both,
false,
nodesToRead,
RequestLifetime.None).ConfigureAwait(false);
ServerFixtureUtils.ValidateResponse(historyReadResponse.ResponseHeader, historyReadResponse.Results, nodesToRead);
Assert.That(historyReadResponse.Results.Count, Is.EqualTo(1));
HistoryReadResult result = historyReadResponse.Results[0];
if (logger.IsEnabled(LogLevel.Information))
{
logger.LogInformation("History read StatusCode: {StatusCode}", result.StatusCode);
}
// The result should be Good or GoodMoreData (if there are more values)
Assert.That(StatusCode.IsGood(result.StatusCode),
Is.True,
$"History read should succeed, but got: {result.StatusCode}");
Assert.That(result.HistoryData.IsNull, Is.False, "HistoryData should not be null");
// Verify we got HistoryData back
if (result.HistoryData.TryGetValue(out HistoryData? historyData))
{
if (logger.IsEnabled(LogLevel.Information))
{
logger.LogInformation("Retrieved {Count} history values", historyData.DataValues.Count);
}
Assert.That(historyData.DataValues.IsNull, Is.False, "DataValues should not be null");
Assert.That(historyData.DataValues.Count, Is.GreaterThan(0), "Should have at least one historical value");
// Verify the data values have proper timestamps
foreach (DataValue dataValue in historyData.DataValues)
{
Assert.That(dataValue.IsNull, Is.False, "DataValue should not be null");
Assert.That(dataValue.ServerTimestamp,
Is.Not.EqualTo(DateTimeUtc.MinValue),
"DataValue should have a valid ServerTimestamp");
}
}
else
{
Assert.Fail("HistoryData body should be of type HistoryData");
}
}
///
/// Test that a HistoryRead on the AccessRights_AccessAll_RO_NotUser
/// node is denied for the current (anonymous) user. The node advertises
/// HistoryRead in its AccessLevel but its UserAccessLevel is None, so the
/// server must return BadUserAccessDenied instead of the historical data.
///
[Test]
public async Task HistoryReadUserAccessDeniedNodeAsync()
{
ushort namespaceIndex = (ushort)m_server.CurrentInstance.NamespaceUris.GetIndex(
Quickstarts.ReferenceServer.Namespaces.ReferenceServer);
var nodeId = new NodeId("AccessRights_AccessAll_RO_NotUser", namespaceIndex);
// Sanity check: the node advertises HistoryRead in AccessLevel but the
// current user is not granted HistoryRead via UserAccessLevel.
ArrayOf readIdCollection =
[
new ReadValueId
{
AttributeId = Attributes.AccessLevel,
NodeId = nodeId
},
new ReadValueId
{
AttributeId = Attributes.UserAccessLevel,
NodeId = nodeId
}
];
m_requestHeader.Timestamp = DateTimeUtc.Now;
ReadResponse readResponse = await m_server.ReadAsync(
m_secureChannelContext,
m_requestHeader,
kMaxAge,
TimestampsToReturn.Neither,
readIdCollection,
RequestLifetime.None).ConfigureAwait(false);
ServerFixtureUtils.ValidateResponse(readResponse.ResponseHeader, readResponse.Results, readIdCollection);
Assert.That(readResponse.Results.Count, Is.EqualTo(2));
byte accessLevel = (byte)readResponse.Results[0].WrappedValue;
byte userAccessLevel = (byte)readResponse.Results[1].WrappedValue;
Assert.That(accessLevel & AccessLevels.HistoryRead,
Is.Not.Zero,
"Node should advertise HistoryRead in AccessLevel");
Assert.That(userAccessLevel & AccessLevels.HistoryRead,
Is.Zero,
"Node should not grant HistoryRead to the current user via UserAccessLevel");
// Perform a history read operation; it must be denied for the current user.
var historyReadDetails = new ReadRawModifiedDetails
{
StartTime = DateTimeUtc.Now.SubtractMilliseconds(60 * 60 * 1000),
EndTime = DateTimeUtc.Now,
NumValuesPerNode = 10,
IsReadModified = false,
ReturnBounds = false
};
ArrayOf nodesToRead =
[
new HistoryReadValueId
{
NodeId = nodeId
}
];
m_requestHeader.Timestamp = DateTimeUtc.Now;
HistoryReadResponse historyReadResponse = await m_server.HistoryReadAsync(
m_secureChannelContext,
m_requestHeader,
new ExtensionObject(historyReadDetails),
TimestampsToReturn.Both,
false,
nodesToRead,
RequestLifetime.None).ConfigureAwait(false);
ServerFixtureUtils.ValidateResponse(historyReadResponse.ResponseHeader, historyReadResponse.Results, nodesToRead);
Assert.That(historyReadResponse.Results.Count, Is.EqualTo(1));
HistoryReadResult result = historyReadResponse.Results[0];
Assert.That(result.StatusCode.Code,
Is.EqualTo(StatusCodes.BadUserAccessDenied),
$"History read on a node without user HistoryRead access should be denied, but got: {result.StatusCode}");
Assert.That(result.HistoryData.IsNull,
Is.True,
"No historical data should be returned when access is denied");
}
///
/// Test provisioning mode - server should start with limited namespace.
///
[Test]
public async Task ProvisioningModeTestAsync()
{
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
// start Ref server in provisioning mode
var fixture = new ServerFixture(t => new ReferenceServer(t))
{
AllNodeManagers = false,
OperationLimits = false,
DurableSubscriptionsEnabled = false,
AutoAccept = true,
ProvisioningMode = true
};
ReferenceServer server = await fixture.StartAsync().ConfigureAwait(false);
// Verify provisioning mode is enabled
Assert.That(server.ProvisioningMode, Is.True, "Server should be in provisioning mode");
// OPC 10000-12 Annex G.2: the application setup state is reported
// as ServerState NoConfiguration and through InApplicationSetup.
Assert.That(server.CurrentInstance.CurrentState, Is.EqualTo(ServerState.NoConfiguration));
Assert.That(server.CurrentInstance.IsRunning, Is.True, "NoConfiguration still serves requests");
var configurationManager = (ConfigurationNodeManager)server.CurrentInstance.ConfigurationNodeManager;
ServerConfigurationState configuration = configurationManager
.FindPredefinedNode(ObjectIds.ServerConfiguration);
Assert.That(configuration.InApplicationSetup, Is.Not.Null);
Assert.That(configuration.InApplicationSetup.Value, Is.True);
// Get endpoints - in provisioning mode, anonymous authentication should not be allowed
ArrayOf endpoints = server.GetEndpoints();
Assert.That(endpoints.IsNull, Is.False);
Assert.That(endpoints.Count, Is.GreaterThan(0), "Server should have endpoints");
// Check that anonymous token policy is not present for at least one endpoint
bool hasEndpointWithoutAnonymous = false;
foreach (EndpointDescription endpoint in endpoints)
{
bool hasAnonymous = endpoint.UserIdentityTokens.Contains(
policy => policy.TokenType == UserTokenType.Anonymous);
if (!hasAnonymous)
{
hasEndpointWithoutAnonymous = true;
break;
}
}
Assert.That(hasEndpointWithoutAnonymous,
Is.True,
"At least one endpoint should not allow anonymous authentication in provisioning mode");
// Clean up
await fixture.StopAsync().ConfigureAwait(false);
}
///
/// Test that ArrayItemType sub-type nodes are accessible and readable.
///
[Test]
public async Task ArrayItemTypeNodesExistAndReadableAsync()
{
var nodeIds = new Dictionary
{
{ "YArray", new NodeId("DataAccess_ArrayItemType_YArray", 2) },
{ "XYArray", new NodeId("DataAccess_ArrayItemType_XYArray", 2) },
{ "Image", new NodeId("DataAccess_ArrayItemType_Image", 2) },
{ "Cube", new NodeId("DataAccess_ArrayItemType_Cube", 2) },
{ "NDimension", new NodeId("DataAccess_ArrayItemType_NDimension", 2) }
};
foreach (KeyValuePair kvp in nodeIds)
{
string name = kvp.Key;
NodeId nodeId = kvp.Value;
m_requestHeader.Timestamp = DateTimeUtc.Now;
ArrayOf nodesToRead =
[
new ReadValueId { NodeId = nodeId, AttributeId = Attributes.Value },
new ReadValueId { NodeId = nodeId, AttributeId = Attributes.DataType },
new ReadValueId { NodeId = nodeId, AttributeId = Attributes.ValueRank }
];
ReadResponse readResponse = await m_server.ReadAsync(
m_secureChannelContext,
m_requestHeader,
kMaxAge,
TimestampsToReturn.Neither,
nodesToRead,
RequestLifetime.None).ConfigureAwait(false);
ServerFixtureUtils.ValidateResponse(readResponse.ResponseHeader, readResponse.Results, nodesToRead);
Assert.That(readResponse.Results[0].StatusCode, Is.EqualTo(StatusCodes.Good),
$"Value read of {name} should succeed");
Assert.That(readResponse.Results[1].StatusCode, Is.EqualTo(StatusCodes.Good),
$"DataType read of {name} should succeed");
Assert.That(readResponse.Results[2].StatusCode, Is.EqualTo(StatusCodes.Good),
$"ValueRank read of {name} should succeed");
TestContext.Out.WriteLine("{0}: DataType={1}, ValueRank={2}",
name,
readResponse.Results[1].WrappedValue,
readResponse.Results[2].WrappedValue);
}
}
///
/// Test that TriggerNode01 and TriggerNode02 exist, are readable and writable,
/// and that writing to them fires a BaseEvent.
///
[Test]
public async Task TriggerNodesFiringEventsOnWriteAsync()
{
var triggerNode01 = new NodeId("NodeIds_Events_TriggerNode01", 2);
var triggerNode02 = new NodeId("NodeIds_Events_TriggerNode02", 2);
DateTimeUtc eventWindowStart = DateTime.UtcNow.AddSeconds(-1);
// Verify both nodes exist and are readable
foreach (NodeId nodeId in new[] { triggerNode01, triggerNode02 })
{
m_requestHeader.Timestamp = DateTimeUtc.Now;
ArrayOf nodesToRead =
[
new ReadValueId { NodeId = nodeId, AttributeId = Attributes.Value }
];
ReadResponse readResponse = await m_server.ReadAsync(
m_secureChannelContext,
m_requestHeader,
kMaxAge,
TimestampsToReturn.Neither,
nodesToRead,
RequestLifetime.None).ConfigureAwait(false);
ServerFixtureUtils.ValidateResponse(readResponse.ResponseHeader, readResponse.Results, nodesToRead);
Assert.That(readResponse.Results[0].StatusCode, Is.EqualTo(StatusCodes.Good),
$"Read of trigger node {nodeId} should succeed");
}
// Verify both nodes are writable
foreach (NodeId nodeId in new[] { triggerNode01, triggerNode02 })
{
m_requestHeader.Timestamp = DateTimeUtc.Now;
ArrayOf nodesToWrite =
[
new WriteValue
{
NodeId = nodeId,
AttributeId = Attributes.Value,
Value = new DataValue(new Variant(42))
}
];
WriteResponse writeResponse = await m_server.WriteAsync(
m_secureChannelContext,
m_requestHeader,
nodesToWrite,
RequestLifetime.None).ConfigureAwait(false);
ServerFixtureUtils.ValidateResponse(writeResponse.ResponseHeader, writeResponse.Results, nodesToWrite);
Assert.That(writeResponse.Results[0], Is.EqualTo(StatusCodes.Good),
$"Write to trigger node {nodeId} should succeed and fire an event");
}
var filter = new EventFilter();
filter.AddSelectClause(
ObjectTypeIds.BaseEventType,
BrowseNames.Message,
Attributes.Value);
bool captured = false;
for (int attempt = 0; attempt < 20 && !captured; attempt++)
{
m_requestHeader.Timestamp = DateTimeUtc.Now;
HistoryReadResponse response = await m_server.HistoryReadAsync(
m_secureChannelContext,
m_requestHeader,
new ExtensionObject(new ReadEventDetails
{
StartTime = eventWindowStart,
EndTime = DateTime.UtcNow.AddSeconds(1),
NumValuesPerNode = 100,
Filter = filter
}),
TimestampsToReturn.Neither,
false,
[new HistoryReadValueId { NodeId = new NodeId("CTT", 2) }],
RequestLifetime.None).ConfigureAwait(false);
if (response.Results.Count == 1 &&
StatusCode.IsGood(response.Results[0].StatusCode))
{
if (ExtensionObject.ToEncodeable(
response.Results[0].HistoryData) is not HistoryEvent historyEvent)
{
continue;
}
foreach (HistoryEventFieldList historicalEvent in
historyEvent.Events)
{
if (historicalEvent.EventFields.Count == 1 &&
historicalEvent.EventFields[0].TryGetValue(
out LocalizedText message) &&
message.Text?.Contains(
"Trigger event",
StringComparison.Ordinal) == true)
{
captured = true;
break;
}
}
}
if (!captured)
{
await Task.Delay(50).ConfigureAwait(false);
}
}
Assert.That(
captured,
Is.True,
"Writing a trigger node must publish and archive the event on the CTT notifier.");
}
///
/// Test that the Variant arrays (1D and 2D) are accessible with proper NodeIds.
///
[Test]
public async Task VariantArrayNodesExistAndReadableAsync()
{
var nodeIds = new Dictionary
{
{ "Scalar_Static_Arrays_Variant", new NodeId("Scalar_Static_Arrays_Variant", 2) },
{ "Scalar_Static_Arrays2D_Variant", new NodeId("Scalar_Static_Arrays2D_Variant", 2) }
};
foreach (KeyValuePair kvp in nodeIds)
{
string name = kvp.Key;
NodeId nodeId = kvp.Value;
m_requestHeader.Timestamp = DateTimeUtc.Now;
ArrayOf nodesToRead =
[
new ReadValueId { NodeId = nodeId, AttributeId = Attributes.Value }
];
ReadResponse readResponse = await m_server.ReadAsync(
m_secureChannelContext,
m_requestHeader,
kMaxAge,
TimestampsToReturn.Neither,
nodesToRead,
RequestLifetime.None).ConfigureAwait(false);
ServerFixtureUtils.ValidateResponse(readResponse.ResponseHeader, readResponse.Results, nodesToRead);
Assert.That(readResponse.Results[0].StatusCode, Is.EqualTo(StatusCodes.Good),
$"Read of {name} should succeed");
}
}
///
/// Verifies that the Variant matrix can be encoded with neighboring results in a batched Read.
///
[Test]
public async Task VariantMatrixDoesNotPoisonBatchedReadEncodingAsync()
{
ArrayOf nodesToRead =
[
new ReadValueId
{
NodeId = new NodeId("Scalar_Static_Arrays2D_Int32", 2),
AttributeId = Attributes.Value
},
new ReadValueId
{
NodeId = new NodeId("Scalar_Static_Arrays2D_Variant", 2),
AttributeId = Attributes.Value
},
new ReadValueId
{
NodeId = new NodeId("Scalar_Static_Arrays2D_UInt32", 2),
AttributeId = Attributes.Value
}
];
ReadResponse readResponse = await m_server.ReadAsync(
m_secureChannelContext,
m_requestHeader,
kMaxAge,
TimestampsToReturn.Neither,
nodesToRead,
RequestLifetime.None).ConfigureAwait(false);
ServerFixtureUtils.ValidateResponse(readResponse.ResponseHeader, readResponse.Results, nodesToRead);
Assert.That(readResponse.Results, Has.Count.EqualTo(nodesToRead.Count));
for (int i = 0; i < readResponse.Results.Count; i++)
{
Assert.That(readResponse.Results[i].StatusCode, Is.EqualTo(StatusCodes.Good));
}
using var stream = new MemoryStream();
using (var encoder = new BinaryEncoder(
stream,
m_server.CurrentInstance.MessageContext,
leaveOpen: true))
{
encoder.WriteDataValueArray(null, readResponse.Results);
}
stream.Position = 0;
using var decoder = new BinaryDecoder(stream, m_server.CurrentInstance.MessageContext);
ArrayOf decodedResults = decoder.ReadDataValueArray(null);
Assert.That(decodedResults, Has.Count.EqualTo(nodesToRead.Count));
for (int i = 0; i < decodedResults.Count; i++)
{
Assert.That(decodedResults[i].StatusCode, Is.EqualTo(StatusCodes.Good));
Assert.That(decodedResults[i].WrappedValue, Is.EqualTo(readResponse.Results[i].WrappedValue));
}
MatrixOf matrix = decodedResults[1].WrappedValue.GetVariantMatrix();
Assert.That(matrix.Dimensions, Has.Length.EqualTo(2));
Assert.That(matrix.Dimensions[0], Is.EqualTo(5));
Assert.That(matrix.Dimensions[1], Is.EqualTo(5));
Assert.That(matrix.Count, Is.EqualTo(25));
// CTT multi-dimensional IndexRange tests need scalar elements of a simple type.
foreach (Variant element in matrix.Span)
{
Assert.That(element.TypeInfo.ValueRank, Is.EqualTo(ValueRanks.Scalar));
Assert.That(element.TypeInfo.BuiltInType, Is.LessThan(BuiltInType.XmlElement));
}
}
///
/// Verifies whole-matrix writes and valid multi-dimensional NumericRange reads and writes.
///
[Test]
public async Task MatrixReadWriteAndNumericRangeAsync()
{
var nodeId = new NodeId("Scalar_Static_Arrays2D_Int32", 2);
ArrayOf wholeMatrixRead =
[
new ReadValueId
{
NodeId = nodeId,
AttributeId = Attributes.Value
}
];
ReadResponse originalRead = await m_server.ReadAsync(
m_secureChannelContext,
m_requestHeader,
kMaxAge,
TimestampsToReturn.Both,
wholeMatrixRead,
RequestLifetime.None).ConfigureAwait(false);
Assert.That(originalRead.Results[0].StatusCode, Is.EqualTo(StatusCodes.Good));
Variant originalValue = originalRead.Results[0].WrappedValue;
try
{
MatrixOf matrix = new int[5, 5]
{
{ 1, 2, 3, 4, 5 },
{ 6, 7, 8, 9, 10 },
{ 11, 12, 13, 14, 15 },
{ 16, 17, 18, 19, 20 },
{ 21, 22, 23, 24, 25 }
};
ArrayOf wholeMatrixWrite =
[
new WriteValue
{
NodeId = nodeId,
AttributeId = Attributes.Value,
Value = new DataValue(Variant.From(matrix))
}
];
WriteResponse wholeWriteResponse = await m_server.WriteAsync(
m_secureChannelContext,
m_requestHeader,
wholeMatrixWrite,
RequestLifetime.None).ConfigureAwait(false);
Assert.That(wholeWriteResponse.Results[0], Is.EqualTo(StatusCodes.Good));
ArrayOf rowRead =
[
new ReadValueId
{
NodeId = nodeId,
AttributeId = Attributes.Value,
IndexRange = "1,0:2"
}
];
ReadResponse rowReadResponse = await m_server.ReadAsync(
m_secureChannelContext,
m_requestHeader,
kMaxAge,
TimestampsToReturn.Both,
rowRead,
RequestLifetime.None).ConfigureAwait(false);
DataValue rowValue = rowReadResponse.Results[0];
Assert.That(rowValue.StatusCode, Is.EqualTo(StatusCodes.Good));
Assert.That(rowValue.SourceTimestamp.IsNull, Is.False);
MatrixOf row = rowValue.WrappedValue.GetInt32Matrix();
Assert.That(row.Dimensions, Has.Length.EqualTo(2));
Assert.That(row.Dimensions[0], Is.EqualTo(1));
Assert.That(row.Dimensions[1], Is.EqualTo(3));
Assert.That(row.Count, Is.EqualTo(3));
Assert.That(row.Span[0], Is.EqualTo(6));
Assert.That(row.Span[1], Is.EqualTo(7));
Assert.That(row.Span[2], Is.EqualTo(8));
MatrixOf replacement = new int[1, 2] { { 40, 50 } };
ArrayOf rangeWrite =
[
new WriteValue
{
NodeId = nodeId,
AttributeId = Attributes.Value,
IndexRange = "1,1:2",
Value = new DataValue(Variant.From(replacement))
}
];
WriteResponse rangeWriteResponse = await m_server.WriteAsync(
m_secureChannelContext,
m_requestHeader,
rangeWrite,
RequestLifetime.None).ConfigureAwait(false);
Assert.That(rangeWriteResponse.Results[0], Is.EqualTo(StatusCodes.Good));
ReadResponse updatedRowReadResponse = await m_server.ReadAsync(
m_secureChannelContext,
m_requestHeader,
kMaxAge,
TimestampsToReturn.Both,
rowRead,
RequestLifetime.None).ConfigureAwait(false);
DataValue updatedRowValue = updatedRowReadResponse.Results[0];
Assert.That(updatedRowValue.StatusCode, Is.EqualTo(StatusCodes.Good));
MatrixOf updatedRow = updatedRowValue.WrappedValue.GetInt32Matrix();
Assert.That(updatedRow.Dimensions, Has.Length.EqualTo(2));
Assert.That(updatedRow.Dimensions[0], Is.EqualTo(1));
Assert.That(updatedRow.Dimensions[1], Is.EqualTo(3));
Assert.That(updatedRow.Count, Is.EqualTo(3));
Assert.That(updatedRow.Span[0], Is.EqualTo(6));
Assert.That(updatedRow.Span[1], Is.EqualTo(40));
Assert.That(updatedRow.Span[2], Is.EqualTo(50));
}
finally
{
ArrayOf restoreWrite =
[
new WriteValue
{
NodeId = nodeId,
AttributeId = Attributes.Value,
Value = new DataValue(originalValue)
}
];
WriteResponse restoreResponse = await m_server.WriteAsync(
m_secureChannelContext,
m_requestHeader,
restoreWrite,
RequestLifetime.None).ConfigureAwait(false);
Assert.That(restoreResponse.Results[0], Is.EqualTo(StatusCodes.Good));
}
}
///
/// Test that Enumeration and Image type scalar nodes are accessible.
///
[Test]
public async Task ScalarStaticEnumerationAndImageNodesExistAsync()
{
var nodeIds = new Dictionary
{
{ "Enumeration", new NodeId("Scalar_Static_Enumeration", 2) },
{ "Image", new NodeId("Scalar_Static_Image", 2) },
{ "ImageBMP", new NodeId("Scalar_Static_ImageBMP", 2) },
{ "ImageGIF", new NodeId("Scalar_Static_ImageGIF", 2) },
{ "ImageJPG", new NodeId("Scalar_Static_ImageJPG", 2) },
{ "ImagePNG", new NodeId("Scalar_Static_ImagePNG", 2) }
};
foreach (KeyValuePair kvp in nodeIds)
{
string name = kvp.Key;
NodeId nodeId = kvp.Value;
m_requestHeader.Timestamp = DateTimeUtc.Now;
ArrayOf nodesToRead =
[
new ReadValueId { NodeId = nodeId, AttributeId = Attributes.Value },
new ReadValueId { NodeId = nodeId, AttributeId = Attributes.DataType }
];
ReadResponse readResponse = await m_server.ReadAsync(
m_secureChannelContext,
m_requestHeader,
kMaxAge,
TimestampsToReturn.Neither,
nodesToRead,
RequestLifetime.None).ConfigureAwait(false);
ServerFixtureUtils.ValidateResponse(readResponse.ResponseHeader, readResponse.Results, nodesToRead);
Assert.That(readResponse.Results[0].StatusCode, Is.EqualTo(StatusCodes.Good),
$"Value read of {name} should succeed");
Assert.That(readResponse.Results[1].StatusCode, Is.EqualTo(StatusCodes.Good),
$"DataType read of {name} should succeed");
}
}
///
/// Test that the HasReferenceTypeAndSubType node exists and has the expected references.
///
[Test]
public async Task ReferencesHasReferenceTypeAndSubTypePreservesBrowseReferencesAsync()
{
int namespaceIndex = m_server.CurrentInstance.NamespaceUris.GetIndex(
Quickstarts.ReferenceServer.Namespaces.ReferenceServer);
Assert.That(namespaceIndex, Is.GreaterThan(0));
var nodeId = new NodeId("References_HasReferenceTypeAndSubType", (ushort)namespaceIndex);
m_requestHeader.Timestamp = DateTimeUtc.Now;
ArrayOf nodesToRead =
[
new ReadValueId { NodeId = nodeId, AttributeId = Attributes.Value }
];
ReadResponse readResponse = await m_server.ReadAsync(
m_secureChannelContext,
m_requestHeader,
kMaxAge,
TimestampsToReturn.Neither,
nodesToRead,
RequestLifetime.None).ConfigureAwait(false);
ServerFixtureUtils.ValidateResponse(readResponse.ResponseHeader, readResponse.Results, nodesToRead);
Assert.That(readResponse.Results[0].StatusCode, Is.EqualTo(StatusCodes.Good),
"Read of References_HasReferenceTypeAndSubType should succeed");
var instructionsId = new NodeId("Scalar_Instructions", (ushort)namespaceIndex);
var booleanId = new NodeId("Scalar_Static_Boolean", (ushort)namespaceIndex);
ArrayOf nodesToBrowse =
[
Describe(nodeId, ReferenceTypeIds.HasComponent, BrowseDirection.Forward),
Describe(nodeId, ReferenceTypeIds.HasComponent, BrowseDirection.Forward, includeSubtypes: true),
Describe(instructionsId, ReferenceTypeIds.HasComponent, BrowseDirection.Inverse),
Describe(booleanId, ReferenceTypeIds.HasOrderedComponent, BrowseDirection.Inverse),
Describe(nodeId, ReferenceTypeIds.References, BrowseDirection.Both, includeSubtypes: true),
Describe(instructionsId, ReferenceTypeIds.Organizes, BrowseDirection.Inverse),
Describe(booleanId, ReferenceTypeIds.Organizes, BrowseDirection.Inverse)
];
BrowseResponse response = await m_server.BrowseAsync(
m_secureChannelContext,
m_requestHeader,
null,
0,
nodesToBrowse,
RequestLifetime.None).ConfigureAwait(false);
ServerFixtureUtils.ValidateResponse(response.ResponseHeader, response.Results, nodesToBrowse);
for (int index = 0; index < response.Results.Count; index++)
{
Assert.That(response.Results[index].StatusCode, Is.EqualTo(StatusCodes.Good));
Assert.That(response.Results[index].ContinuationPoint.IsEmpty, Is.True);
}
Assert.Multiple(() =>
{
Assert.That(response.Results[0].References.Count, Is.EqualTo(1));
Assert.That(response.Results[1].References.Count, Is.EqualTo(2));
Assert.That(response.Results[2].References.Count, Is.EqualTo(1));
Assert.That(response.Results[3].References.Count, Is.EqualTo(1));
});
ReferenceDescription component = response.Results[0].References[0];
ReferenceDescription orderedComponent = response.Results[1].References.ToArray()!
.Single(reference => reference.ReferenceTypeId == ReferenceTypeIds.HasOrderedComponent);
ReferenceDescription[] allReferences = response.Results[4].References.ToArray()!;
Assert.Multiple(() =>
{
Assert.That(component.ReferenceTypeId, Is.EqualTo(ReferenceTypeIds.HasComponent));
Assert.That(component.IsForward, Is.True);
Assert.That(component.NodeId, Is.EqualTo(new ExpandedNodeId(instructionsId)));
Assert.That(orderedComponent.IsForward, Is.True);
Assert.That(orderedComponent.NodeId, Is.EqualTo(new ExpandedNodeId(booleanId)));
Assert.That(response.Results[2].References[0].IsForward, Is.False);
Assert.That(response.Results[2].References[0].NodeId, Is.EqualTo(new ExpandedNodeId(nodeId)));
Assert.That(response.Results[3].References[0].IsForward, Is.False);
Assert.That(response.Results[3].References[0].NodeId, Is.EqualTo(new ExpandedNodeId(nodeId)));
Assert.That(allReferences, Has.Length.EqualTo(4));
Assert.That(allReferences!.Any(reference =>
reference.ReferenceTypeId == ReferenceTypeIds.HasTypeDefinition &&
reference.IsForward &&
reference.NodeId == VariableTypeIds.BaseDataVariableType), Is.True);
Assert.That(allReferences.Any(reference =>
reference.ReferenceTypeId == ReferenceTypeIds.Organizes &&
!reference.IsForward &&
reference.NodeId == new NodeId("References", (ushort)namespaceIndex)), Is.True);
Assert.That(response.Results[5].References.ToArray()!.Any(reference =>
!reference.IsForward &&
reference.NodeId == new NodeId("Scalar", (ushort)namespaceIndex)), Is.True);
Assert.That(response.Results[6].References.ToArray()!.Any(reference =>
!reference.IsForward &&
reference.NodeId == new NodeId("Scalar_Static", (ushort)namespaceIndex)), Is.True);
});
static BrowseDescription Describe(
NodeId id,
NodeId referenceTypeId,
BrowseDirection direction,
bool includeSubtypes = false)
{
return new BrowseDescription
{
NodeId = id,
BrowseDirection = direction,
ReferenceTypeId = referenceTypeId,
IncludeSubtypes = includeSubtypes,
ResultMask = (uint)BrowseResultMask.All
};
}
}
///
/// Verifies that after transferring a subscription to a session that then switches its
/// identity to one that lacks access to the monitored node, permission caches are
/// invalidated and subsequent value-change notifications for that node are suppressed.
///
///
/// Scenario:
/// 1. Session A authenticates as user1 (AuthenticatedUser role) and creates a
/// subscription monitoring AccessRights_RolePermissions_AuthenticatedUser
/// (a node whose RolePermissions allow only the AuthenticatedUser role to read it).
/// 2. Session B also authenticates as user1 so that TransferSubscriptions accepts the
/// transfer (identity check: same username).
/// 3. The subscription is transferred to session B with sendInitialValues=true; session B
/// publishes and receives the initial value — confirming the node is accessible.
/// 4. Session B re-activates with anonymous identity. This triggers
/// SessionActivatedAsync → InvalidatePermissionCacheForSession,
/// clearing the cached permission for the transferred item.
/// 5. Session A (still user1) writes a new value to the restricted node, triggering
/// ValidateItemDataChange for session B's item. With the cache cleared, the
/// permission is re-evaluated for the anonymous identity → BadUserAccessDenied →
/// the notification is suppressed.
/// 6. Session B publishes: no DataChangeNotification for the restricted node should
/// be present, proving that permissions were correctly re-evaluated.
///
///
/// The fixture has no TCP or HTTPS endpoint, or the selected endpoint has no Username token policy.
///
[Test]
public async Task PermissionsRevalidatedAfterTransferSubscriptionsAndReactivationAsync()
{
// The ReferenceServer registers its nodes under namespace index 2.
// "AccessRights_RolePermissions_AuthenticatedUser" allows Browse/Read/Write only
// for WellKnownRole_AuthenticatedUser; anonymous sessions cannot read it.
var restrictedNodeId = new NodeId("AccessRights_RolePermissions_AuthenticatedUser", 2);
EndpointDescription endpoint = m_server.GetEndpoints().Find(e =>
e.TransportProfileUri is Profiles.UaTcpTransport or Profiles.HttpsBinaryTransport)
?? throw new AssertionException("The fixture requires a TCP or HTTPS endpoint.");
UserTokenPolicy userNamePolicy = endpoint.UserIdentityTokens.Find(
policy => policy.TokenType == UserTokenType.UserName)
?? throw new AssertionException("The endpoint must advertise a Username token policy.");
var usernameToken = new UserNameIdentityToken
{
UserName = "user1",
Password = System.Text.Encoding.UTF8.GetBytes("password").ToByteString(),
PolicyId = userNamePolicy.PolicyId
};
// Session A: authenticate as user1.
(RequestHeader headerA, SecureChannelContext channelA) =
await m_server.CreateAndActivateSessionAsync(
"TransferTest_SessionA",
useSecurity: false,
identityToken: usernameToken).ConfigureAwait(false);
headerA.Timestamp = DateTimeUtc.Now;
uint subscriptionId = 0;
try
{
var servicesA = new ServerTestServices(m_server, channelA);
// Create a subscription in session A.
CreateSubscriptionResponse subResp = await servicesA.CreateSubscriptionAsync(
headerA,
requestedPublishingInterval: 100,
requestedLifetimeCount: 20,
requestedMaxKeepAliveCount: 10,
maxNotificationsPerPublish: 0,
publishingEnabled: true,
priority: 0).ConfigureAwait(false);
ServerFixtureUtils.ValidateResponse(subResp.ResponseHeader);
subscriptionId = subResp.SubscriptionId;
// Monitor the restricted node. Session A (user1 / AuthenticatedUser role)
// has Read permission, so creation should succeed.
ArrayOf monitoredItems =
[
new MonitoredItemCreateRequest
{
ItemToMonitor = new ReadValueId
{
NodeId = restrictedNodeId,
AttributeId = Attributes.Value
},
MonitoringMode = MonitoringMode.Reporting,
RequestedParameters = new MonitoringParameters
{
ClientHandle = 1,
SamplingInterval = -1,
QueueSize = 10,
DiscardOldest = true
}
}
];
CreateMonitoredItemsResponse itemsResp = await servicesA.CreateMonitoredItemsAsync(
headerA,
subscriptionId,
TimestampsToReturn.Both,
monitoredItems).ConfigureAwait(false);
ServerFixtureUtils.ValidateResponse(itemsResp.ResponseHeader, itemsResp.Results, monitoredItems);
Assert.That(StatusCode.IsGood(itemsResp.Results[0].StatusCode), Is.True,
"Monitored item creation on restricted node should succeed for AuthenticatedUser.");
// Write an initial value so the node has a known, non-default value.
// This also pre-populates the monitored item's last-value cache.
headerA.Timestamp = DateTimeUtc.Now;
WriteResponse writeResp0 = await m_server.WriteAsync(
channelA,
headerA,
[new WriteValue
{
NodeId = restrictedNodeId,
AttributeId = Attributes.Value,
Value = new DataValue(new Variant((short)10))
}],
RequestLifetime.None).ConfigureAwait(false);
ServerFixtureUtils.ValidateResponse(writeResp0.ResponseHeader);
Assert.That(StatusCode.IsGood(writeResp0.Results[0]), Is.True,
"Initial write from user1 should succeed.");
// Consume any initial notifications queued in session A.
await Task.Delay(300).ConfigureAwait(false);
headerA.Timestamp = DateTimeUtc.Now;
PublishResponse publishA = await servicesA.PublishAsync(headerA, []).ConfigureAwait(false);
ServerFixtureUtils.ValidateResponse(publishA.ResponseHeader);
// Session B: authenticate as user1 as well.
// TransferSubscriptions requires the same username for the identity check to pass.
(RequestHeader headerB, SecureChannelContext channelB) =
await m_server.CreateAndActivateSessionAsync(
"TransferTest_SessionB",
useSecurity: false,
identityToken: usernameToken).ConfigureAwait(false);
headerB.Timestamp = DateTimeUtc.Now;
try
{
var servicesB = new ServerTestServices(m_server, channelB);
// Transfer the subscription from session A to session B.
// sendInitialValues=true causes the server to re-queue the last known value
// for all monitored items immediately after transfer.
TransferSubscriptionsResponse transferResp =
await servicesB.TransferSubscriptionsAsync(
headerB,
[subscriptionId],
sendInitialValues: true).ConfigureAwait(false);
ServerFixtureUtils.ValidateResponse(transferResp.ResponseHeader);
Assert.That(StatusCode.IsGood(transferResp.Results[0].StatusCode), Is.True,
"Subscription transfer should succeed: both sessions use user1 identity.");
// Publish from session B to receive the initial-values notification.
// At this point session B is still user1 so the read is permitted.
await Task.Delay(200).ConfigureAwait(false);
headerB.Timestamp = DateTimeUtc.Now;
PublishResponse publishB1 = await servicesB.PublishAsync(
headerB,
publishA.NotificationMessage.SequenceNumber > 0
? [new SubscriptionAcknowledgement
{
SubscriptionId = subscriptionId,
SequenceNumber = publishA.NotificationMessage.SequenceNumber
}]
: []).ConfigureAwait(false);
ServerFixtureUtils.ValidateResponse(publishB1.ResponseHeader);
Assert.That(
HasDataChangeNotificationForClientHandle(publishB1.NotificationMessage, 1),
Is.True,
"Session B (user1) must receive the initial data notification for the restricted node.");
// Re-activate session B with anonymous identity.
// The identity change triggers SessionActivatedAsync →
// InvalidatePermissionCacheForSession, clearing the cached permission for
// the transferred monitored item.
headerB.Timestamp = DateTimeUtc.Now;
ActivateSessionResponse reactivateResp = await m_server.ActivateSessionAsync(
channelB,
headerB,
default,
[],
[],
default,
null,
RequestLifetime.None).ConfigureAwait(false);
ServerFixtureUtils.ValidateResponse(reactivateResp.ResponseHeader);
// Write a new value from session A (user1 still has Write permission).
// This triggers ValidateItemDataChange for session B's monitored item.
// With the permission cache cleared, anonymous cannot read the restricted
// node → notification is suppressed.
headerA.Timestamp = DateTimeUtc.Now;
WriteResponse writeResp = await m_server.WriteAsync(
channelA,
headerA,
[new WriteValue
{
NodeId = restrictedNodeId,
AttributeId = Attributes.Value,
Value = new DataValue(new Variant((short)99))
}],
RequestLifetime.None).ConfigureAwait(false);
ServerFixtureUtils.ValidateResponse(writeResp.ResponseHeader);
Assert.That(StatusCode.IsGood(writeResp.Results[0]), Is.True,
"Session A (user1) should still be able to write to the restricted node.");
// Allow time for the value change to be processed.
await Task.Delay(300).ConfigureAwait(false);
// Publish from session B. Because the anonymous identity cannot read the
// restricted node, session B must not receive the value; the denied access
// is reported as a bad status instead (Part 4 5.13.2.1).
headerB.Timestamp = DateTimeUtc.Now;
PublishResponse publishB2 = await servicesB.PublishAsync(
headerB,
[new SubscriptionAcknowledgement
{
SubscriptionId = subscriptionId,
SequenceNumber = publishB1.NotificationMessage.SequenceNumber
}]).ConfigureAwait(false);
ServerFixtureUtils.ValidateResponse(publishB2.ResponseHeader);
foreach (DataValue value in GetDataChangeValuesForClientHandle(
publishB2.NotificationMessage, 1))
{
Assert.That(
value.StatusCode == StatusCodes.BadUserAccessDenied ||
value.StatusCode == StatusCodes.BadNotReadable,
Is.True,
"After re-activation as anonymous, session B must only receive an access " +
$"error for the AuthenticatedUser-restricted node, got {value.StatusCode}.");
Assert.That(value.WrappedValue.IsNull, Is.True,
"The restricted value must not be delivered to the anonymous session.");
}
// Clean up.
headerB.Timestamp = DateTimeUtc.Now;
await servicesB.DeleteSubscriptionsAsync(headerB, [subscriptionId]).ConfigureAwait(false);
subscriptionId = 0;
}
finally
{
headerB.Timestamp = DateTimeUtc.Now;
await m_server.CloseSessionAsync(
channelB,
headerB,
CancellationToken.None).ConfigureAwait(false);
}
}
finally
{
if (subscriptionId != 0)
{
var servicesA = new ServerTestServices(m_server, channelA);
headerA.Timestamp = DateTimeUtc.Now;
await servicesA.DeleteSubscriptionsAsync(headerA, [subscriptionId])
.ConfigureAwait(false);
}
headerA.Timestamp = DateTimeUtc.Now;
await m_server.CloseSessionAsync(
channelA,
headerA,
CancellationToken.None).ConfigureAwait(false);
}
}
///
/// Returns true if any in the given
/// contains a
/// with the specified .
///
private static bool HasDataChangeNotificationForClientHandle(
NotificationMessage message,
uint clientHandle)
{
foreach (ExtensionObject extObj in message.NotificationData)
{
if (extObj.TryGetValue(out DataChangeNotification? dcn))
{
foreach (MonitoredItemNotification item in dcn.MonitoredItems)
{
if (item.ClientHandle == clientHandle)
{
return true;
}
}
}
}
return false;
}
///
/// Returns the values of all s with the specified
/// in the given .
///
private static List GetDataChangeValuesForClientHandle(
NotificationMessage message,
uint clientHandle)
{
var values = new List();
foreach (ExtensionObject extObj in message.NotificationData)
{
if (extObj.TryGetValue(out DataChangeNotification? dcn))
{
foreach (MonitoredItemNotification item in dcn.MonitoredItems)
{
if (item.ClientHandle == clientHandle)
{
values.Add(item.Value);
}
}
}
}
return values;
}
}
}