mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-23 18:39:26 +08:00
d547747e22
# Description The CTT/compliance `ReferenceNodeManager` built its ~6,000 node address space entirely in imperative C#. This PR migrates it to a source-generated `[NodeManager]` partial driven by an exported NodeSet2 model (`ReferenceServer.NodeSet2.xml`), mirroring the `MinimalBoilerServer` sample and completing the goal of issue #4204. The address space is now materialized from the model by the source generator. The large orchestration body was kept but converted from create-based to **find-instead-of-create**: each `Create*` primitive now looks up the model-materialized node via `FindPredefinedNode<T>` and re-applies runtime values, DataAccess metadata, and write handlers, rather than constructing nodes from scratch. This keeps the deterministic value seeding and behavior wiring intact while the structure comes from the model. ## Notable details for reviewers - **Idempotent reference wiring.** The mesh references in the `References` folder are already present in the model, so the historic manual `AddReference` calls would throw on duplicates. Added a small `AddReferenceIfMissing` helper so those calls are safe. - **Non-string NodeIds.** The `NodeIds` folder assigns numeric / GUID / opaque NodeIds. The model exports those nodes with their final ids, so find-by-string-path no longer works; added `CreateVariableById(NodeId)` to locate them by their real id. - **Generator bug fix (worth a careful look).** `ModelDesignExtensions.GetValueRankAsCode` reconstructed `ValueRank` as `dimensions.Length + 1`, so a node with `ArrayDimensions="3,3"` was emitted as `ValueRank = 3`. This affected **any** NodeSet2 with a fixed multi-dimensional array (rank-3 matrices, `BadTypeMismatch` on whole-matrix writes, wrong decoded dimensions). Corrected to `dimensions.Length` and updated the unit tests that had codified the off-by-one. ## Validation - `ReferenceServerTest`: 44/44 on net10.0 and net48 - `Opc.Ua.SourceGeneration.Core.Tests`: pass on net8.0/net9.0/net10.0/net48 - Integration on net10.0: ComplexTypes, Client, History, Sessions, Subscriptions all pass - Clean build, no new analyzer warnings ## Related Issues - Fixes #4204 ## Checklist _Put an `x` in the boxes that apply. You can complete these step by step after opening the PR._ - [ ] I have signed the [CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf) and read the [CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md) doc. - [x] I have added tests that prove my fix is effective or that my feature works and increased code coverage. - [ ] I have added all necessary documentation. - [x] I have verified that my changes do not introduce (new) build or analyzer warnings. - [x] I ran **all** tests locally using the **UA.slnx** solution against at least .net **framework** and .net **10**, and all passed. - [ ] I fixed **all** failing and flaky tests in the CI pipelines and **all** CodeQL warnings. - [ ] I have addressed **all** PR feedback received. --------- Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2921 lines
132 KiB
C#
2921 lines
132 KiB
C#
/* ========================================================================
|
|
* 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
|
|
{
|
|
/// <summary>
|
|
/// Test Reference Server.
|
|
/// </summary>
|
|
[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 ITelemetryContext m_telemetry;
|
|
private ServerFixture<ReferenceServer> m_fixture;
|
|
private ReferenceServer m_server;
|
|
private RequestHeader m_requestHeader;
|
|
private SecureChannelContext m_secureChannelContext;
|
|
private OperationLimits m_operationLimits;
|
|
private ArrayOf<ReferenceDescription> m_referenceDescriptions;
|
|
private RandomSource m_random;
|
|
private DataGenerator m_generator;
|
|
private bool m_sessionClosed;
|
|
|
|
/// <summary>
|
|
/// Set up a Server fixture.
|
|
/// </summary>
|
|
[OneTimeSetUp]
|
|
public async Task OneTimeSetUpAsync()
|
|
{
|
|
m_telemetry = NUnitTelemetryContext.Create();
|
|
// start Ref server
|
|
m_fixture = new ServerFixture<ReferenceServer>(t => new ReferenceServer(t))
|
|
{
|
|
AllNodeManagers = true,
|
|
OperationLimits = true,
|
|
DurableSubscriptionsEnabled = false,
|
|
UseSamplingGroupsInReferenceNodeManager = false,
|
|
AutoAccept = true
|
|
};
|
|
m_server = await m_fixture.StartAsync().ConfigureAwait(false);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tear down the server fixture.
|
|
/// </summary>
|
|
[OneTimeTearDown]
|
|
public async Task OneTimeTearDownAsync()
|
|
{
|
|
await m_fixture.StopAsync().ConfigureAwait(false);
|
|
Thread.Sleep(1000);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Create a session for a test.
|
|
/// </summary>
|
|
[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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tear down the test session.
|
|
/// </summary>
|
|
[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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Set up a Reference Server a session
|
|
/// </summary>
|
|
[GlobalSetup]
|
|
public async Task GlobalSetupAsync()
|
|
{
|
|
// start Ref server
|
|
m_fixture = new ServerFixture<ReferenceServer>(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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tear down Server and the close the session.
|
|
/// </summary>
|
|
[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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Test for expected exceptions.
|
|
/// </summary>
|
|
[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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get Endpoints.
|
|
/// </summary>
|
|
[Test]
|
|
public void GetEndpoints()
|
|
{
|
|
ArrayOf<EndpointDescription> endpoints = m_server.GetEndpoints();
|
|
Assert.That(endpoints.IsNull, Is.False);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get Operation limits.
|
|
/// </summary>
|
|
[Test]
|
|
[Order(100)]
|
|
public async Task GetOperationLimitsAsync()
|
|
{
|
|
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
|
|
ILogger logger = telemetry.CreateLogger<ReferenceServerTests>();
|
|
|
|
ArrayOf<ReadValueId> 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<DataValue> 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
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Read node.
|
|
/// </summary>
|
|
[Test]
|
|
[Benchmark]
|
|
public async Task ReadAsync()
|
|
{
|
|
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
|
|
ILogger logger = telemetry.CreateLogger<ReferenceServerTests>();
|
|
|
|
// 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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Read all nodes.
|
|
/// </summary>
|
|
[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());
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Write Node.
|
|
/// </summary>
|
|
[Test]
|
|
[Benchmark]
|
|
public async Task WriteAsync()
|
|
{
|
|
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
|
|
ILogger logger = telemetry.CreateLogger<ReferenceServerTests>();
|
|
|
|
// 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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Test that ReferenceNodeManager variables update their SourceTimestamp on read.
|
|
/// </summary>
|
|
[Test]
|
|
[Order(340)]
|
|
public async Task ReferenceNodeManagerVariablesUpdateTimestampOnReadAsync()
|
|
{
|
|
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
|
|
ILogger logger = telemetry.CreateLogger<ReferenceServerTests>();
|
|
|
|
// Read a variable from the ReferenceNodeManager (namespace index 2)
|
|
var nodeId = new NodeId("Scalar_Static_Byte", 2);
|
|
ArrayOf<ReadValueId> 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");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Test that the ReferenceNodeManager SelectionList variable exposes its
|
|
/// value.
|
|
/// </summary>
|
|
[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<ReadValueId> 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<string> 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<LocalizedText> 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<WriteValue> 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));
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
[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<ReadValueId> 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<LocalizedText> 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<ExtensionObject> 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"));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that AccessLevelEx retains the same base access bits as AccessLevel.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task ReferenceNodeManagerNonatomicAccessLevelsAreConsistentAsync()
|
|
{
|
|
ushort namespaceIndex = (ushort)m_server.CurrentInstance.NamespaceUris.GetIndex(
|
|
Quickstarts.ReferenceServer.Namespaces.ReferenceServer);
|
|
var nodeId = new NodeId("Scalar_Static_NonatomicReadWrite", namespaceIndex);
|
|
ArrayOf<ReadValueId> 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);
|
|
|
|
Assert.That(response.Results, Has.Count.EqualTo(nodesToRead.Count));
|
|
Assert.That(response.Results[0].WrappedValue.GetByte(),
|
|
Is.EqualTo(AccessLevels.CurrentReadOrWrite));
|
|
Assert.That(response.Results[1].WrappedValue.GetByte(),
|
|
Is.EqualTo(AccessLevels.CurrentReadOrWrite));
|
|
|
|
uint accessLevelEx = response.Results[2].WrappedValue.GetUInt32();
|
|
Assert.That(
|
|
accessLevelEx & 0xff,
|
|
Is.EqualTo((uint)AccessLevels.CurrentReadOrWrite));
|
|
Assert.That(
|
|
accessLevelEx & (uint)AccessLevelExType.NonatomicRead,
|
|
Is.EqualTo((uint)AccessLevelExType.NonatomicRead));
|
|
Assert.That(
|
|
accessLevelEx & (uint)AccessLevelExType.NonatomicWrite,
|
|
Is.EqualTo((uint)AccessLevelExType.NonatomicWrite));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Test that the ReferenceNodeManager Currency variable exposes its value
|
|
/// and its CurrencyUnit property.
|
|
/// </summary>
|
|
[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<ReadValueId> 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");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Test that ReferenceNodeManager array variables update their SourceTimestamp on read.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task ReferenceNodeManagerArrayVariablesUpdateTimestampOnReadAsync()
|
|
{
|
|
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
|
|
ILogger logger = telemetry.CreateLogger<ReferenceServerTests>();
|
|
|
|
// Read an array variable from the ReferenceNodeManager (namespace index 2)
|
|
var nodeId = new NodeId("Scalar_Static_Arrays_Byte", 2);
|
|
ArrayOf<ReadValueId> 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");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Update static Nodes, read modify write.
|
|
/// </summary>
|
|
[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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Browse full address space.
|
|
/// </summary>
|
|
[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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Translate references.
|
|
/// </summary>
|
|
[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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Create a subscription with a monitored item.
|
|
/// Read a few notifications with Publish.
|
|
/// Delete the monitored item and subscription.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task SubscriptionAsync()
|
|
{
|
|
var serverTestServices = new ServerTestServices(m_server, m_secureChannelContext);
|
|
await CommonTestWorkers.SubscriptionTestAsync(serverTestServices, m_requestHeader).ConfigureAwait(false);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Create a single session + subscription and raise a single event on the server node.
|
|
/// Validates all the event fields are properly received.
|
|
/// </summary>
|
|
[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<MonitoredItemCreateRequest> 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<SubscriptionAcknowledgement>();
|
|
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<Variant> 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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Create multiple sessions, each with a subscription.
|
|
/// Close all sessions without deleting subscriptions (abandoning them).
|
|
/// Concurrently delete the abandoned subscriptions from the main session.
|
|
/// Verifies that the concurrent dictionary backing abandoned subscriptions
|
|
/// handles parallel access correctly (fix for issue #3612).
|
|
/// </summary>
|
|
[Test]
|
|
public async Task DeleteAbandonedSubscriptionsConcurrentlyAsync()
|
|
{
|
|
const int sessionCount = 5;
|
|
var subscriptionIds = new List<uint>();
|
|
|
|
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<uint> 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);
|
|
}
|
|
|
|
// Concurrently delete all abandoned subscriptions from the main session.
|
|
var mainServices = new ServerTestServices(m_server, m_secureChannelContext);
|
|
var deleteTasks = new List<Task<DeleteSubscriptionsResponse>>();
|
|
foreach (uint id in subscriptionIds)
|
|
{
|
|
ArrayOf<uint> singleId = [id];
|
|
m_requestHeader.Timestamp = DateTimeUtc.Now;
|
|
deleteTasks.Add(
|
|
mainServices.DeleteSubscriptionsAsync(m_requestHeader, singleId)
|
|
.AsTask());
|
|
}
|
|
|
|
DeleteSubscriptionsResponse[] responses = await Task.WhenAll(deleteTasks)
|
|
.ConfigureAwait(false);
|
|
|
|
// All deletions should succeed.
|
|
foreach (DeleteSubscriptionsResponse response in responses)
|
|
{
|
|
Assert.AreEqual(StatusCodes.Good, response.ResponseHeader.ServiceResult);
|
|
Assert.AreEqual(1, response.Results.Count);
|
|
Assert.AreEqual(StatusCodes.Good, (uint)response.Results[0]);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
[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<uint> 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<ServiceResultException>(async () =>
|
|
{
|
|
m_requestHeader.Timestamp = DateTimeUtc.Now;
|
|
await CommonTestWorkers.VerifySubscriptionTransferredAsync(
|
|
serverTestServices,
|
|
m_requestHeader,
|
|
subscriptionIds,
|
|
true).ConfigureAwait(false);
|
|
});
|
|
Assert.That(
|
|
sre.StatusCode,
|
|
Is.EqualTo(StatusCodes.BadNoSubscription));
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Create a subscription with a monitored item.
|
|
/// Create a secondary Session.
|
|
/// Transfer subscription with a monitored item from one session to the other.
|
|
/// </summary>
|
|
[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<uint> 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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that an anonymous Subscription cannot be transferred to a
|
|
/// Session with a different client ApplicationUri.
|
|
/// </summary>
|
|
[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<uint> 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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Create a subscription with a monitored item.
|
|
/// Call ResendData.
|
|
/// Ensure only a single value per monitored item is returned after ResendData was called.
|
|
/// </summary>
|
|
[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<uint> 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<CallMethodRequest> 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<SubscriptionAcknowledgement> 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<int> CollectNotificationsAsync(
|
|
ServerTestServices serverTestServices,
|
|
RequestHeader requestHeader,
|
|
ArrayOf<SubscriptionAcknowledgement> 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<ArrayOf<CallMethodRequest>> ResendDataCallAsync(
|
|
StatusCode expectedStatus,
|
|
ArrayOf<uint> subscriptionIds)
|
|
{
|
|
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
|
|
ILogger logger = telemetry.CreateLogger<ReferenceServerTests>();
|
|
|
|
// Find the ResendData method
|
|
ArrayOf<CallMethodRequest> 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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Read Values of NodeIds, determine types, write back new random values.
|
|
/// </summary>
|
|
/// <param name="testSet">The nodeIds to modify.</param>
|
|
private async Task UpdateValuesAsync(NodeId[] testSet)
|
|
{
|
|
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
|
|
ILogger logger = telemetry.CreateLogger<ReferenceServerTests>();
|
|
|
|
// 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<DataValue>();
|
|
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);
|
|
}
|
|
|
|
[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<MonitoredItemCreateRequest> 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<SubscriptionAcknowledgement>();
|
|
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<WriteValue> 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<SemanticChangeStructureDataType> 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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Test that Server object EventNotifier has HistoryRead bit set when history capabilities are enabled.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task ServerEventNotifierHistoryReadBitAsync()
|
|
{
|
|
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
|
|
ILogger logger = telemetry.CreateLogger<ReferenceServerTests>();
|
|
|
|
// Read Server object EventNotifier attribute
|
|
ArrayOf<ReadValueId> 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<ReadValueId> 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);
|
|
}
|
|
|
|
// If either history capability is enabled, the HistoryRead bit should be set
|
|
if (accessHistoryEventsCapability || accessHistoryDataCapability)
|
|
{
|
|
Assert.That(eventNotifier & EventNotifiers.HistoryRead,
|
|
Is.Not.Zero,
|
|
"Server EventNotifier should have HistoryRead bit set when history capabilities are enabled");
|
|
}
|
|
|
|
// 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");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verify that ServerStatus children have matching SourceTimestamp and ServerTimestamp.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task ServerStatusTimestampsMatchAsync()
|
|
{
|
|
ILogger<ReferenceServerTests> logger = m_telemetry.CreateLogger<ReferenceServerTests>();
|
|
|
|
// Read ServerStatus children (CurrentTime, StartTime, State, etc.)
|
|
ArrayOf<ReadValueId> 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}");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task HistoryReadInt32ValueNodeAsync()
|
|
{
|
|
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
|
|
ILogger logger = telemetry.CreateLogger<ReferenceServerTests>();
|
|
|
|
// 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<ReadValueId> 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<HistoryReadValueId> 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");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Test that a HistoryRead on the <c>AccessRights_AccessAll_RO_NotUser</c>
|
|
/// 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.
|
|
/// </summary>
|
|
[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<ReadValueId> 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<HistoryReadValueId> 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");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Test provisioning mode - server should start with limited namespace.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task ProvisioningModeTestAsync()
|
|
{
|
|
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
|
|
|
|
// start Ref server in provisioning mode
|
|
var fixture = new ServerFixture<ReferenceServer>(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");
|
|
|
|
// Get endpoints - in provisioning mode, anonymous authentication should not be allowed
|
|
ArrayOf<EndpointDescription> 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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Test that ArrayItemType sub-type nodes are accessible and readable.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task ArrayItemTypeNodesExistAndReadableAsync()
|
|
{
|
|
var nodeIds = new Dictionary<string, NodeId>
|
|
{
|
|
{ "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<string, NodeId> kvp in nodeIds)
|
|
{
|
|
string name = kvp.Key;
|
|
NodeId nodeId = kvp.Value;
|
|
m_requestHeader.Timestamp = DateTimeUtc.Now;
|
|
ArrayOf<ReadValueId> 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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Test that TriggerNode01 and TriggerNode02 exist, are readable and writable,
|
|
/// and that writing to them fires a BaseEvent.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task TriggerNodesFiringEventsOnWriteAsync()
|
|
{
|
|
var triggerNode01 = new NodeId("NodeIds_Events_TriggerNode01", 2);
|
|
var triggerNode02 = new NodeId("NodeIds_Events_TriggerNode02", 2);
|
|
|
|
// Verify both nodes exist and are readable
|
|
foreach (NodeId nodeId in new[] { triggerNode01, triggerNode02 })
|
|
{
|
|
m_requestHeader.Timestamp = DateTimeUtc.Now;
|
|
ArrayOf<ReadValueId> 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<WriteValue> 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");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Test that the Variant arrays (1D and 2D) are accessible with proper NodeIds.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task VariantArrayNodesExistAndReadableAsync()
|
|
{
|
|
var nodeIds = new Dictionary<string, NodeId>
|
|
{
|
|
{ "Scalar_Static_Arrays_Variant", new NodeId("Scalar_Static_Arrays_Variant", 2) },
|
|
{ "Scalar_Static_Arrays2D_Variant", new NodeId("Scalar_Static_Arrays2D_Variant", 2) }
|
|
};
|
|
|
|
foreach (KeyValuePair<string, NodeId> kvp in nodeIds)
|
|
{
|
|
string name = kvp.Key;
|
|
NodeId nodeId = kvp.Value;
|
|
m_requestHeader.Timestamp = DateTimeUtc.Now;
|
|
ArrayOf<ReadValueId> 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");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that the Variant matrix can be encoded with neighboring results in a batched Read.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task VariantMatrixDoesNotPoisonBatchedReadEncodingAsync()
|
|
{
|
|
ArrayOf<ReadValueId> 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<DataValue> 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<Variant> matrix = decodedResults[1].WrappedValue.GetVariantMatrix();
|
|
Assert.That(matrix.Dimensions, Has.Length.EqualTo(2));
|
|
Assert.That(matrix.Dimensions[0], Is.EqualTo(3));
|
|
Assert.That(matrix.Dimensions[1], Is.EqualTo(3));
|
|
Assert.That(matrix.Count, Is.EqualTo(9));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies whole-matrix writes and valid multi-dimensional NumericRange reads and writes.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task MatrixReadWriteAndNumericRangeAsync()
|
|
{
|
|
var nodeId = new NodeId("Scalar_Static_Arrays2D_Int32", 2);
|
|
ArrayOf<ReadValueId> 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<int> matrix = new int[3, 3]
|
|
{
|
|
{ 1, 2, 3 },
|
|
{ 4, 5, 6 },
|
|
{ 7, 8, 9 }
|
|
};
|
|
ArrayOf<WriteValue> 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<ReadValueId> 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<int> 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(4));
|
|
Assert.That(row.Span[1], Is.EqualTo(5));
|
|
Assert.That(row.Span[2], Is.EqualTo(6));
|
|
|
|
MatrixOf<int> replacement = new int[1, 2] { { 40, 50 } };
|
|
ArrayOf<WriteValue> 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<int> 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(4));
|
|
Assert.That(updatedRow.Span[1], Is.EqualTo(40));
|
|
Assert.That(updatedRow.Span[2], Is.EqualTo(50));
|
|
}
|
|
finally
|
|
{
|
|
ArrayOf<WriteValue> 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));
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Test that Enumeration and Image type scalar nodes are accessible.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task ScalarStaticEnumerationAndImageNodesExistAsync()
|
|
{
|
|
var nodeIds = new Dictionary<string, NodeId>
|
|
{
|
|
{ "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<string, NodeId> kvp in nodeIds)
|
|
{
|
|
string name = kvp.Key;
|
|
NodeId nodeId = kvp.Value;
|
|
m_requestHeader.Timestamp = DateTimeUtc.Now;
|
|
ArrayOf<ReadValueId> 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");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Test that the HasReferenceTypeAndSubType node exists and has the expected references.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task ReferencesHasReferenceTypeAndSubTypeNodeExistsAsync()
|
|
{
|
|
var nodeId = new NodeId("References_HasReferenceTypeAndSubType", 2);
|
|
m_requestHeader.Timestamp = DateTimeUtc.Now;
|
|
ArrayOf<ReadValueId> 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");
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Scenario:
|
|
/// 1. Session A authenticates as user1 (AuthenticatedUser role) and creates a
|
|
/// subscription monitoring <c>AccessRights_RolePermissions_AuthenticatedUser</c>
|
|
/// (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
|
|
/// <c>SessionActivatedAsync</c> → <c>InvalidatePermissionCacheForSession</c>,
|
|
/// clearing the cached permission for the transferred item.
|
|
/// 5. Session A (still user1) writes a new value to the restricted node, triggering
|
|
/// <c>ValidateItemDataChange</c> 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.
|
|
/// </remarks>
|
|
[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);
|
|
|
|
// Username token for user1 (AuthenticatedUser role). PolicyId "1" is the first
|
|
// user-token policy registered by the ReferenceServer (username with Basic256Sha256).
|
|
var usernameToken = new UserNameIdentityToken
|
|
{
|
|
UserName = "user1",
|
|
Password = System.Text.Encoding.UTF8.GetBytes("password").ToByteString(),
|
|
PolicyId = "1"
|
|
};
|
|
|
|
// 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<MonitoredItemCreateRequest> 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, no DataChangeNotification for client handle 1 should
|
|
// appear in the response.
|
|
headerB.Timestamp = DateTimeUtc.Now;
|
|
PublishResponse publishB2 = await servicesB.PublishAsync(
|
|
headerB,
|
|
[new SubscriptionAcknowledgement
|
|
{
|
|
SubscriptionId = subscriptionId,
|
|
SequenceNumber = publishB1.NotificationMessage.SequenceNumber
|
|
}]).ConfigureAwait(false);
|
|
ServerFixtureUtils.ValidateResponse(publishB2.ResponseHeader);
|
|
|
|
Assert.That(
|
|
HasDataChangeNotificationForClientHandle(publishB2.NotificationMessage, 1),
|
|
Is.False,
|
|
"After re-activation as anonymous, session B must NOT receive notifications " +
|
|
"for the AuthenticatedUser-restricted node.");
|
|
|
|
// 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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns <c>true</c> if any <see cref="DataChangeNotification"/> in the given
|
|
/// <paramref name="message"/> contains a <see cref="MonitoredItemNotification"/>
|
|
/// with the specified <paramref name="clientHandle"/>.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
}
|
|
}
|