/* ======================================================================== * 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; } } }