Files
UA-.NETStandard/tests/Opc.Ua.Server.Tests/DiagnosticsNodeManagerTests.cs
84fa8e8906 CTT follow-up: monitored items, alarms, codecs, history, and model validation (#4002)
# Description

Follow-up to #3978 based on the latest OPC UA CTT aggregate, matrix,
Historical Access, Attribute, and Node Management runs.

## Aggregate run 16

- Ignore synthetic `BadBoundNotFound` raw-bound markers when feeding
aggregate calculators.
- Add direct and live historian regression coverage.

## Multi-dimensional Variant matrices

- Fix `Variant.Copy()` so rank-2 Variant matrices preserve their payload
and dimensions.
- Enforce valid Variant matrix dimensions consistently across Binary,
JSON, XML stream, and XML parser codecs.
- Encoders return `BadEncodingError`; decoders return `BadDecodingError`
for malformed matrix dimensions.
- Add cross-codec valid/null/empty/malformed tests and a batched
ReferenceServer Read regression.
- Correct the previous CTT issue attribution: the invalid
`Scalar_Static_Arrays2D_Variant` server encoding poisoned the batched
CTT Read.

## Node Management AddNodes

- Return `BadNodeIdRejected` for unsupported requested-NodeId
namespaces.
- Validate hierarchical ReferenceType source/target NodeClass
constraints.
- Document run-18 confirmation that all remaining 15 AddNodes errors are
CTT project/script configuration issues.

## Historical Access and Attribute Services run 18

- Reject whitespace in `NumericRange` syntax with
`BadIndexRangeInvalid`.
- Add HistoryRead tests for operation-level invalid syntax and
per-DataValue `BadIndexRangeNoData`.
- Document CTT defects:
- `012.js` expects `BadIndexRangeNoData` at the operation level instead
of on returned DataValues;
  - `Err-012.js` uses a non-historizing node for an access-denied test;
- generic Attribute array helpers omit NodeId[] and StatusCode[]
conversion paths.

## Base Information model validation run 19

- Run the CTT main session as `sysadmin` over Basic256Sha256
SignAndEncrypt so SecurityAdmin RolePermissions and EncryptionRequired
restrictions are both satisfied without weakening the server model.
- Preserve predefined well-known NodeIds while making generated dynamic
type and optional-child factories allocate compatible per-instance
NodeIds, remap internal references, and update references from the
owning subtree.
- Materialize dynamic RoleType properties through generated helpers and
keep role-child string NodeIds disjoint from numeric RoleManager root
IDs.
- Add net48/net10 regressions for generator output, NodeId
allocation/reference remapping, DI mandatory children, dynamic roles,
and low-privilege versus SecurityAdmin Browse visibility.
- Document CTT defects for permission-unaware mandatory-child
validation, the UA 1.04 reference model used against a UA 1.05 server,
stale ServerDiagnostics cache, and the `ConformanceUnits` scalar/array
mismatch.
## Related Issues

- Follow-up to #3960 and #3978.

## Checklist

- [x] I have signed the CLA and read the CONTRIBUTING document.
- [x] Tests prove the fixes and improve coverage.
- [x] Required CTT documentation is updated.
- [x] Focused builds/tests introduce no warnings.
- [x] Affected tests pass on .NET Framework and .NET 10.
- [ ] CI and CodeQL are green.
- [ ] All PR feedback is addressed.



## Latest CTT full run and A&C run 20

- Fix Monitor Value Change V2 `036.js`: an ambiguous
`DataValue(StatusCode)` call encoded `BadIndexRangeNoData` as a Good
value.
- Prove all 19 configured matrix monitored items notify when the
selected value actually changes; document the independent `042.js`
script defects.
- Rebase dynamic `CertificateExpired` / `TrustListOutOfDate` alarm roots
and descendants, restore standard type declarations and ModellingRules,
and expose the cross-node-manager references.
- Deliver method-triggered events to every authorized Session, fixing
the two-Session Confirm scenario.
- Preserve locale-only `LocalizedText` values consistently across
Binary, JSON, and XML and align Acknowledge/Confirm comment behavior and
tests.
- Document the remaining proven CTT defects for TransactionDiagnostics,
SemanticChange, History `013`, PolicyId scope, A&C, durable
subscriptions, and Subscription Minimum events.

## Base synchronization

Merged `master` through `007cfaf90` (`#4010`, repository layout
consolidation) and retained the matrix regression tests at their new
`tests/` locations.

## Latest validation

- Focused merged-tree tests pass on net10 and net48.
- `Opc.Ua.Server` and `Quickstarts.Servers` Release builds complete with
0 warnings and 0 errors on net10 and net48.
- The existing
`DisableEnableViaTypeAndInstanceMethodNoEventsDuringDisabledAsync`
timing test took its documented inconclusive timeout path after the
layout merge; no test failure was reported.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
Co-authored-by: romanett <romanett98@gmail.com>
2026-07-18 05:01:03 +02:00

1067 lines
54 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.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using NUnit.Framework;
using Opc.Ua.Tests;
namespace Opc.Ua.Server.Tests
{
[TestFixture]
[Category("DiagnosticsNodeManager")]
[Parallelizable]
public class DiagnosticsNodeManagerTests
{
private Mock<IServerInternal> m_serverMock;
private Mock<ICoreNodeManager> m_coreNodeManagerMock;
private Mock<ISubscriptionManager> m_subscriptionManagerMock;
private void SetupServerMock()
{
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
var namespaces = new NamespaceTable();
namespaces.Append(Ua.Namespaces.OpcUa);
var serverUris = new StringTable();
var typeTree = new TypeTable(namespaces);
var messageContext = new ServiceMessageContext(telemetry, EncodeableFactory.Create())
{
NamespaceUris = namespaces,
ServerUris = serverUris
};
m_coreNodeManagerMock = new Mock<ICoreNodeManager>();
m_coreNodeManagerMock.Setup(m => m.ImportNodesAsync(
It.IsAny<ISystemContext>(),
It.IsAny<IList<NodeState>>(),
It.IsAny<bool>(),
It.IsAny<CancellationToken>()))
.Returns(new ValueTask());
m_subscriptionManagerMock = new Mock<ISubscriptionManager>();
var masterNodeManagerMock = new Mock<IMasterNodeManager>();
masterNodeManagerMock.Setup(m => m.RemoveReferencesAsync(It.IsAny<List<LocalReference>>(), It.IsAny<CancellationToken>()))
.Returns(new ValueTask());
m_serverMock = new Mock<IServerInternal>();
m_serverMock.Setup(s => s.Telemetry).Returns(telemetry);
m_serverMock.Setup(s => s.NamespaceUris).Returns(namespaces);
m_serverMock.Setup(s => s.ServerUris).Returns(serverUris);
m_serverMock.Setup(s => s.TypeTree).Returns(typeTree);
m_serverMock.Setup(s => s.MessageContext).Returns(messageContext);
m_serverMock.Setup(s => s.CoreNodeManager).Returns(m_coreNodeManagerMock.Object);
m_serverMock.Setup(s => s.NodeManager).Returns(masterNodeManagerMock.Object);
m_serverMock.Setup(s => s.SubscriptionManager).Returns(m_subscriptionManagerMock.Object);
m_serverMock.Setup(s => s.Factory).Returns(new Mock<IEncodeableFactory>().Object);
var defSysContext = new ServerSystemContext(m_serverMock.Object);
m_serverMock.Setup(s => s.DefaultSystemContext).Returns(defSysContext);
}
[Test]
public async Task CreateAddressSpace_HooksUpGetMonitoredItemsAndResendDataAsync()
{
var config = new ApplicationConfiguration { ServerConfiguration = new ServerConfiguration() };
SetupServerMock();
using var manager = new DiagnosticsNodeManager(m_serverMock.Object, config, NullLogger.Instance);
var externalRefs = new Dictionary<NodeId, IList<IReference>>();
await manager.CreateAddressSpaceAsync(externalRefs).ConfigureAwait(false);
GetMonitoredItemsMethodState getMonitoredItems = manager.FindPredefinedNode<GetMonitoredItemsMethodState>(MethodIds.Server_GetMonitoredItems);
Assert.That(getMonitoredItems, Is.Not.Null, "GetMonitoredItems should exist.");
Assert.That(getMonitoredItems.OnCallMethod, Is.Not.Null, "GetMonitoredItems OnCallMethod should be wired.");
// The OutputArguments child must resolve at its well-known
// instance NodeId (i=11494). The source generator dispatches the
// singleton-instance child factory when the Add{Child} call's
// owner is the Server singleton, so the synthesized children
// pick up the correct well-known NodeIds without a post-hoc
// override (regression guard for the dead lookup that used to
// live in DiagnosticsNodeManager.CreateAddressSpaceAsync).
PropertyState getMonitoredItemsOutputArgs = manager.FindPredefinedNode<PropertyState>(
VariableIds.Server_GetMonitoredItems_OutputArguments);
Assert.That(getMonitoredItemsOutputArgs, Is.Not.Null,
"GetMonitoredItems OutputArguments should exist at the well-known instance NodeId.");
Assert.That(getMonitoredItemsOutputArgs.WrappedValue.IsNull, Is.False, "Output arguments value should be initialized.");
ResendDataMethodState resendData = manager.FindPredefinedNode<ResendDataMethodState>(MethodIds.Server_ResendData);
Assert.That(resendData, Is.Not.Null, "ResendData should exist.");
Assert.That(resendData.OnCallMethod, Is.Not.Null, "ResendData OnCallMethod should be wired.");
}
[Test]
public async Task CreateAddressSpace_DurableSubscriptionsEnabled_SetsOnCallAsync()
{
var config = new ApplicationConfiguration
{
ServerConfiguration = new ServerConfiguration
{
DurableSubscriptionsEnabled = true
}
};
SetupServerMock();
using var manager = new DiagnosticsNodeManager(m_serverMock.Object, config, NullLogger.Instance);
var externalRefs = new Dictionary<NodeId, IList<IReference>>();
await manager.CreateAddressSpaceAsync(externalRefs).ConfigureAwait(false);
SetSubscriptionDurableMethodState setSubscriptionDurable =
manager.FindPredefinedNode<SetSubscriptionDurableMethodState>(MethodIds.Server_SetSubscriptionDurable);
Assert.That(setSubscriptionDurable, Is.Not.Null, "SetSubscriptionDurable should exist.");
Assert.That(setSubscriptionDurable.OnCall, Is.Not.Null, "SetSubscriptionDurable OnCall method should be wired.");
}
[Test]
public async Task CreateAddressSpace_DurableSubscriptionsDisabled_DeletesNodeAsync()
{
var config = new ApplicationConfiguration
{
ServerConfiguration = new ServerConfiguration
{
DurableSubscriptionsEnabled = false
}
};
SetupServerMock();
using var manager = new DiagnosticsNodeManager(m_serverMock.Object, config, NullLogger.Instance);
var externalRefs = new Dictionary<NodeId, IList<IReference>>();
await manager.CreateAddressSpaceAsync(externalRefs).ConfigureAwait(false);
SetSubscriptionDurableMethodState setSubscriptionDurable =
manager.FindPredefinedNode<SetSubscriptionDurableMethodState>(MethodIds.Server_SetSubscriptionDurable);
Assert.That(setSubscriptionDurable, Is.Null, "SetSubscriptionDurable should not exist.");
}
[Test]
public async Task SetSubscriptionDurable_CallsSubscriptionManagerAsync()
{
var config = new ApplicationConfiguration
{
ServerConfiguration = new ServerConfiguration
{
DurableSubscriptionsEnabled = true
}
};
SetupServerMock();
uint mockRevisedLifetime = 50;
m_subscriptionManagerMock.Setup(m => m.SetSubscriptionDurable(
It.IsAny<ISystemContext>(),
1234u,
100u,
out mockRevisedLifetime))
.Returns(StatusCodes.Good);
using var manager = new DiagnosticsNodeManager(m_serverMock.Object, config, NullLogger.Instance);
var externalRefs = new Dictionary<NodeId, IList<IReference>>();
await manager.CreateAddressSpaceAsync(externalRefs).ConfigureAwait(false);
SetSubscriptionDurableMethodState setSubscriptionDurable =
manager.FindPredefinedNode<SetSubscriptionDurableMethodState>(MethodIds.Server_SetSubscriptionDurable);
uint actualRevisedLifetime = 0;
ServiceResult result = setSubscriptionDurable.OnCall(
manager.SystemContext,
setSubscriptionDurable,
ObjectIds.Server,
1234,
100,
ref actualRevisedLifetime);
Assert.That(result.StatusCode, Is.EqualTo(StatusCodes.Good));
Assert.That(actualRevisedLifetime, Is.EqualTo(50));
m_subscriptionManagerMock.Verify(m => m.SetSubscriptionDurable(It.IsAny<ISystemContext>(), 1234, 100, out It.Ref<uint>.IsAny), Times.Once);
}
[Test]
public async Task GetMonitoredItems_ValidatesSessionAndReturnsItemsAsync()
{
var config = new ApplicationConfiguration { ServerConfiguration = new ServerConfiguration() };
SetupServerMock();
var subMock = new Mock<ISubscription>();
subMock.Setup(s => s.Id).Returns(1234);
subMock.Setup(s => s.SessionId).Returns(new NodeId(1, 1));
ArrayOf<uint> serverHandles = [1, 2];
ArrayOf<uint> clientHandles = [3, 4];
subMock.Setup(s => s.GetMonitoredItems(out serverHandles, out clientHandles));
ISubscription outSub = subMock.Object;
m_subscriptionManagerMock.Setup(m => m.TryGetSubscription(It.IsAny<uint>(), out outSub)).Returns(true);
using var manager = new DiagnosticsNodeManager(m_serverMock.Object, config, NullLogger.Instance);
var externalRefs = new Dictionary<NodeId, IList<IReference>>();
await manager.CreateAddressSpaceAsync(externalRefs).ConfigureAwait(false);
GetMonitoredItemsMethodState getMonitoredItems = manager.FindPredefinedNode<GetMonitoredItemsMethodState>(MethodIds.Server_GetMonitoredItems);
var sysContextMock = new Mock<ISessionSystemContext>();
sysContextMock.Setup(c => c.SessionId).Returns(new NodeId(1, 1));
ArrayOf<Variant> inputs = [new Variant(1234u)];
var outputs = new List<Variant> { Variant.Null, Variant.Null };
ServiceResult result = getMonitoredItems.OnCallMethod(
sysContextMock.Object,
getMonitoredItems,
inputs,
outputs);
Assert.That(result.StatusCode, Is.EqualTo(StatusCodes.Good));
Assert.That(outputs[0].GetUInt32Array(), Is.EqualTo(serverHandles));
Assert.That(outputs[1].GetUInt32Array(), Is.EqualTo(clientHandles));
// Test access denied (different session ID)
sysContextMock.Setup(c => c.SessionId).Returns(new NodeId(2, 1));
result = getMonitoredItems.OnCallMethod(
sysContextMock.Object,
getMonitoredItems,
inputs,
outputs);
Assert.That(result.StatusCode, Is.EqualTo(StatusCodes.BadUserAccessDenied));
}
[Test]
public async Task GetMonitoredItems_InvalidSubscriptionId_ReturnsBadSubscriptionIdInvalidAsync()
{
var config = new ApplicationConfiguration { ServerConfiguration = new ServerConfiguration() };
SetupServerMock();
ISubscription outSub = null;
m_subscriptionManagerMock.Setup(m => m.TryGetSubscription(It.IsAny<uint>(), out outSub)).Returns(false);
using var manager = new DiagnosticsNodeManager(m_serverMock.Object, config, NullLogger.Instance);
var externalRefs = new Dictionary<NodeId, IList<IReference>>();
await manager.CreateAddressSpaceAsync(externalRefs).ConfigureAwait(false);
GetMonitoredItemsMethodState getMonitoredItems = manager.FindPredefinedNode<GetMonitoredItemsMethodState>(MethodIds.Server_GetMonitoredItems);
var sysContextMock = new Mock<ISessionSystemContext>();
ArrayOf<Variant> inputs = [new Variant(1234u)];
var outputs = new List<Variant> { Variant.Null, Variant.Null };
ServiceResult result = getMonitoredItems.OnCallMethod(
sysContextMock.Object,
getMonitoredItems,
inputs,
outputs);
Assert.That(result.StatusCode, Is.EqualTo(StatusCodes.BadSubscriptionIdInvalid));
}
[Test]
public async Task ResendData_ValidatesAccessAndCallsResendDataAsync()
{
var config = new ApplicationConfiguration { ServerConfiguration = new ServerConfiguration() };
SetupServerMock();
var subMock = new Mock<ISubscription>();
subMock.Setup(s => s.Id).Returns(1234);
subMock.Setup(s => s.SessionId).Returns(new NodeId(1, 1));
subMock.Setup(s => s.ResendData(It.IsAny<OperationContext>()));
ISubscription outSub = subMock.Object;
m_subscriptionManagerMock.Setup(m => m.TryGetSubscription(It.IsAny<uint>(), out outSub)).Returns(true);
using var manager = new DiagnosticsNodeManager(m_serverMock.Object, config, NullLogger.Instance);
var externalRefs = new Dictionary<NodeId, IList<IReference>>();
await manager.CreateAddressSpaceAsync(externalRefs).ConfigureAwait(false);
ResendDataMethodState resendData = manager.FindPredefinedNode<ResendDataMethodState>(MethodIds.Server_ResendData);
var reqHeader = new RequestHeader();
var sessionMock = new Mock<ISession>();
sessionMock.Setup(s => s.Id).Returns(new NodeId(1, 1));
var userIdentity = new UserIdentity();
sessionMock.Setup(s => s.EffectiveIdentity).Returns(userIdentity);
var opContext = new OperationContext(reqHeader, null, RequestType.Read, RequestLifetime.None, sessionMock.Object);
var sysContext = new ServerSystemContext(m_serverMock.Object, opContext);
ArrayOf<Variant> inputs = [new Variant(1234u)];
var outputs = new List<Variant>();
ServiceResult result = resendData.OnCallMethod(
sysContext,
resendData,
inputs,
outputs);
Assert.That(result.StatusCode, Is.EqualTo(StatusCodes.Good));
subMock.Verify(s => s.ResendData(opContext), Times.Once);
// Test bad access denied
sessionMock.Setup(s => s.Id).Returns(new NodeId(2, 1));
result = resendData.OnCallMethod(
sysContext,
resendData,
inputs,
outputs);
Assert.That(result.StatusCode, Is.EqualTo(StatusCodes.BadUserAccessDenied));
}
[Test]
public async Task ResendData_InvalidSubscriptionId_ReturnsBadSubscriptionIdInvalidAsync()
{
var config = new ApplicationConfiguration { ServerConfiguration = new ServerConfiguration() };
SetupServerMock();
ISubscription outSub = null;
m_subscriptionManagerMock.Setup(m => m.TryGetSubscription(It.IsAny<uint>(), out outSub)).Returns(false);
using var manager = new DiagnosticsNodeManager(m_serverMock.Object, config, NullLogger.Instance);
var externalRefs = new Dictionary<NodeId, IList<IReference>>();
await manager.CreateAddressSpaceAsync(externalRefs).ConfigureAwait(false);
ResendDataMethodState resendData = manager.FindPredefinedNode<ResendDataMethodState>(MethodIds.Server_ResendData);
var reqHeader = new RequestHeader();
var sessionMock = new Mock<ISession>();
sessionMock.Setup(s => s.Id).Returns(new NodeId(1, 1));
var userIdentity = new UserIdentity();
sessionMock.Setup(s => s.EffectiveIdentity).Returns(userIdentity);
var opContext = new OperationContext(reqHeader, null, RequestType.Read, RequestLifetime.None, sessionMock.Object);
var sysContext = new ServerSystemContext(m_serverMock.Object, opContext);
ArrayOf<Variant> inputs = [new Variant(1234u)];
var outputs = new List<Variant>();
ServiceResult result = resendData.OnCallMethod(
sysContext,
resendData,
inputs,
outputs);
Assert.That(result.StatusCode, Is.EqualTo(StatusCodes.BadSubscriptionIdInvalid));
}
[Test]
public async Task NodesAreCreatedCorrectlyAsync()
{
var config = new ApplicationConfiguration { ServerConfiguration = new ServerConfiguration() };
SetupServerMock();
using var manager = new DiagnosticsNodeManager(m_serverMock.Object, config, NullLogger.Instance);
var externalRefs = new Dictionary<NodeId, IList<IReference>>();
await manager.CreateAddressSpaceAsync(externalRefs).ConfigureAwait(false);
ServerObjectState serverObj = manager.FindPredefinedNode<ServerObjectState>(ObjectIds.Server);
Assert.That(serverObj, Is.Not.Null, "Server object should be present.");
ServerDiagnosticsState serverDiagnostics = manager.FindPredefinedNode<ServerDiagnosticsState>(ObjectIds.Server_ServerDiagnostics);
Assert.That(serverDiagnostics, Is.Not.Null, "ServerDiagnostics should be present.");
HistoryServerCapabilitiesState historyCapabilities =
manager.FindPredefinedNode<HistoryServerCapabilitiesState>(ObjectIds.HistoryServerCapabilities);
Assert.That(historyCapabilities, Is.Not.Null, "HistoryServerCapabilities should be present.");
}
[Test]
public async Task StandardAlarmDeclarationsRetainMandatoryModellingRulesAsync()
{
var config = new ApplicationConfiguration
{
ServerConfiguration = new ServerConfiguration()
};
SetupServerMock();
using var manager = new DiagnosticsNodeManager(
m_serverMock.Object,
config,
NullLogger.Instance);
var externalRefs = new Dictionary<NodeId, IList<IReference>>();
await manager.CreateAddressSpaceAsync(externalRefs).ConfigureAwait(false);
BaseInstanceState expirationDate = manager.FindPredefinedNode<BaseInstanceState>(
VariableIds.CertificateExpirationAlarmType_ExpirationDate);
BaseInstanceState trustListId = manager.FindPredefinedNode<BaseInstanceState>(
VariableIds.TrustListOutOfDateAlarmType_TrustListId);
Assert.Multiple(() =>
{
Assert.That(expirationDate, Is.Not.Null);
Assert.That(
expirationDate.ModellingRuleId,
Is.EqualTo(ObjectIds.ModellingRule_Mandatory));
Assert.That(trustListId, Is.Not.Null);
Assert.That(
trustListId.ModellingRuleId,
Is.EqualTo(ObjectIds.ModellingRule_Mandatory));
});
}
[Test]
public async Task SetDiagnosticsEnabledAsync_SameState_DoesNothingAsync()
{
var config = new ApplicationConfiguration { ServerConfiguration = new ServerConfiguration() };
SetupServerMock();
using var manager = new DiagnosticsNodeManager(m_serverMock.Object, config, NullLogger.Instance);
var externalRefs = new Dictionary<NodeId, IList<IReference>>();
await manager.CreateAddressSpaceAsync(externalRefs).ConfigureAwait(false);
// By default DiagnosticsEnabled is true
Assert.That(manager.DiagnosticsEnabled, Is.True);
await manager.SetDiagnosticsEnabledAsync(manager.SystemContext, true).ConfigureAwait(false);
// Should remain true without side effects
Assert.That(manager.DiagnosticsEnabled, Is.True);
}
[Test]
public async Task SetDiagnosticsEnabledAsync_WhenDisabled_DeletesNodesAndStopsTimerAsync()
{
var config = new ApplicationConfiguration { ServerConfiguration = new ServerConfiguration() };
SetupServerMock();
using var manager = new DiagnosticsNodeManager(m_serverMock.Object, config, NullLogger.Instance);
var externalRefs = new Dictionary<NodeId, IList<IReference>>();
await manager.CreateAddressSpaceAsync(externalRefs).ConfigureAwait(false);
// Setup a mock update callback
static ServiceResult UpdateCallback(ISystemContext ctx, NodeState node, ref Variant value) => ServiceResult.Good;
// Add a mock session to verify deletion path
var sessionDiag = new SessionDiagnosticsDataType { SessionName = "TestSession" };
var sessionSecDiag = new SessionSecurityDiagnosticsDataType();
NodeId sessionId = await manager.CreateSessionDiagnosticsAsync(
manager.SystemContext,
sessionDiag,
UpdateCallback,
sessionSecDiag,
UpdateCallback).ConfigureAwait(false);
Assert.That(sessionId.IsNull, Is.False);
// Add a mock subscription to verify deletion path
var subDiag = new SubscriptionDiagnosticsDataType { SubscriptionId = 1, SessionId = sessionId };
NodeId subId = await manager.CreateSubscriptionDiagnosticsAsync(
manager.SystemContext,
subDiag,
UpdateCallback).ConfigureAwait(false);
Assert.That(subId.IsNull, Is.False);
// Check node exists before deleting
SessionDiagnosticsObjectState sessionNodeBefore = manager.FindPredefinedNode<SessionDiagnosticsObjectState>(sessionId);
Assert.That(sessionNodeBefore, Is.Not.Null);
SubscriptionDiagnosticsState subNodeBefore = manager.FindPredefinedNode<SubscriptionDiagnosticsState>(subId);
Assert.That(subNodeBefore, Is.Not.Null);
// Act: Disable diagnostics
await manager.SetDiagnosticsEnabledAsync(manager.SystemContext, false).ConfigureAwait(false);
Assert.That(manager.DiagnosticsEnabled, Is.False);
// Verifying the nodes have been removed from the PredefinedNodes is a strong indicator DeleteNodeAsync was called
SessionDiagnosticsObjectState sessionNodeAfter = manager.FindPredefinedNode<SessionDiagnosticsObjectState>(sessionId);
Assert.That(sessionNodeAfter, Is.Null, "Session node should be deleted.");
// Also check subscription diagnostics array was updated / removed
SubscriptionDiagnosticsState subNodeAfter = manager.FindPredefinedNode<SubscriptionDiagnosticsState>(subId);
Assert.That(subNodeAfter, Is.Null, "Subscription node should be deleted.");
}
[Test]
public async Task SetDiagnosticsEnabledAsync_WhenEnabled_ClearsArraysAndStartsScanAsync()
{
var config = new ApplicationConfiguration { ServerConfiguration = new ServerConfiguration() };
SetupServerMock();
using var manager = new DiagnosticsNodeManager(m_serverMock.Object, config, NullLogger.Instance);
var externalRefs = new Dictionary<NodeId, IList<IReference>>();
await manager.CreateAddressSpaceAsync(externalRefs).ConfigureAwait(false);
static ServiceResult UpdateCallback(ISystemContext ctx, NodeState node, ref Variant value) => ServiceResult.Good;
await manager.CreateServerDiagnosticsAsync(manager.SystemContext, new ServerDiagnosticsSummaryDataType(), UpdateCallback, CancellationToken.None)
.ConfigureAwait(false);
// start disabled
await manager.SetDiagnosticsEnabledAsync(manager.SystemContext, false).ConfigureAwait(false);
Assert.That(manager.DiagnosticsEnabled, Is.False);
// Act: Enable diagnostics
await manager.SetDiagnosticsEnabledAsync(manager.SystemContext, true).ConfigureAwait(false);
Assert.That(manager.DiagnosticsEnabled, Is.True);
// Verify arrays. Following SetDiagnosticsEnabledAsync(true), DoScan(true) is called.
// Empty sessions and subscriptions will result in empty arrays and 'Good' StatusCodes.
ServerDiagnosticsState serverDiagnostics = manager.FindPredefinedNode<ServerDiagnosticsState>(ObjectIds.Server_ServerDiagnostics);
Assert.That(serverDiagnostics, Is.Not.Null);
if (serverDiagnostics.SamplingIntervalDiagnosticsArray != null)
{
// This array is not evaluated heavily on normal DoScan, so it stays BadWaitingForInitialData
Assert.That(serverDiagnostics.SamplingIntervalDiagnosticsArray.StatusCode.Code,
Is.EqualTo(StatusCodes.BadWaitingForInitialData));
}
// The rest are populated with empty arrays due to no sessions in test
if (serverDiagnostics.SubscriptionDiagnosticsArray != null)
{
Assert.That(serverDiagnostics.SubscriptionDiagnosticsArray.Value, Is.Empty);
}
if (serverDiagnostics.SessionsDiagnosticsSummary?.SessionDiagnosticsArray != null)
{
Assert.That(serverDiagnostics.SessionsDiagnosticsSummary.SessionDiagnosticsArray.Value, Is.Empty);
}
if (serverDiagnostics.SessionsDiagnosticsSummary?.SessionSecurityDiagnosticsArray != null)
{
Assert.That(serverDiagnostics.SessionsDiagnosticsSummary.SessionSecurityDiagnosticsArray.Value, Is.Empty);
}
ServerDiagnosticsSummaryState serverDiagSummary =
manager.FindPredefinedNode<ServerDiagnosticsSummaryState>(VariableIds.Server_ServerDiagnostics_ServerDiagnosticsSummary);
Assert.That(serverDiagSummary, Is.Not.Null);
Assert.That(serverDiagSummary.StatusCode.Code, Is.EqualTo(StatusCodes.Good));
}
[Test]
public async Task CreateServerDiagnosticsAsync_WiresUpHandlers_AndPerformsActionsAsync()
{
var config = new ApplicationConfiguration { ServerConfiguration = new ServerConfiguration() };
SetupServerMock();
using var manager = new DiagnosticsNodeManager(m_serverMock.Object, config, NullLogger.Instance);
var externalRefs = new Dictionary<NodeId, IList<IReference>>();
await manager.CreateAddressSpaceAsync(externalRefs).ConfigureAwait(false);
static ServiceResult UpdateCallback(ISystemContext ctx, NodeState node, ref Variant value) => ServiceResult.Good;
await manager.CreateServerDiagnosticsAsync(
manager.SystemContext,
new ServerDiagnosticsSummaryDataType(),
UpdateCallback,
CancellationToken.None).ConfigureAwait(false);
// 1. Verify ServerDiagnosticsSummaryState
ServerDiagnosticsSummaryState summaryNode = manager.FindPredefinedNode<ServerDiagnosticsSummaryState>(
VariableIds.Server_ServerDiagnostics_ServerDiagnosticsSummary);
Assert.That(summaryNode, Is.Not.Null);
Assert.That(summaryNode.OnReadUserRolePermissions, Is.Not.Null,
"OnReadUserRolePermissions should be wired on ServerDiagnosticsSummary.");
// Test OnReadUserRolePermissions (Non-admin context -> PermissionType.None)
ArrayOf<RolePermissionType> permissions = default;
ServiceResult result = summaryNode.OnReadUserRolePermissions(manager.SystemContext, summaryNode, ref permissions);
Assert.That(result.StatusCode, Is.EqualTo(StatusCodes.Good));
Assert.That(permissions.IsNull, Is.False);
Assert.That(permissions.Count, Is.GreaterThan(0));
Assert.That(permissions[0].Permissions, Is.EqualTo((uint)PermissionType.None));
// 2. Verify SessionDiagnosticsArrayState
SessionDiagnosticsArrayState sessionArrayNode = manager.FindPredefinedNode<SessionDiagnosticsArrayState>(
VariableIds.Server_ServerDiagnostics_SessionsDiagnosticsSummary_SessionDiagnosticsArray);
Assert.That(sessionArrayNode, Is.Not.Null);
Assert.That(sessionArrayNode.OnSimpleReadValue, Is.Not.Null,
"OnSimpleReadValue should be wired on SessionDiagnosticsArray.");
Assert.That(sessionArrayNode.OnReadUserRolePermissions, Is.Not.Null,
"OnReadUserRolePermissions should be wired on SessionDiagnosticsArray.");
manager.ForceDiagnosticsScan();
Variant arrayValue = default;
result = sessionArrayNode.OnSimpleReadValue(manager.SystemContext, sessionArrayNode, ref arrayValue);
Assert.That(result.StatusCode, Is.EqualTo(StatusCodes.Good));
Assert.That(arrayValue.IsNull, Is.False); // Depends on Variant.FromStructure
// 3. Verify SessionSecurityDiagnosticsArrayState
SessionSecurityDiagnosticsArrayState sessionSecurityArrayNode = manager.FindPredefinedNode<SessionSecurityDiagnosticsArrayState>(
VariableIds.Server_ServerDiagnostics_SessionsDiagnosticsSummary_SessionSecurityDiagnosticsArray);
Assert.That(sessionSecurityArrayNode, Is.Not.Null);
Assert.That(sessionSecurityArrayNode.OnSimpleReadValue, Is.Not.Null,
"OnSimpleReadValue should be wired on SessionSecurityDiagnosticsArray.");
Assert.That(sessionSecurityArrayNode.OnReadUserRolePermissions, Is.Not.Null,
"OnReadUserRolePermissions should be wired on SessionSecurityDiagnosticsArray.");
manager.ForceDiagnosticsScan();
arrayValue = default;
result = sessionSecurityArrayNode.OnSimpleReadValue(
manager.SystemContext,
sessionSecurityArrayNode,
ref arrayValue);
Assert.That(result.StatusCode, Is.EqualTo(StatusCodes.Good));
Assert.That(arrayValue.IsNull, Is.False);
// 4. Verify SubscriptionDiagnosticsArrayState
SubscriptionDiagnosticsArrayState subArrayNode = manager.FindPredefinedNode<SubscriptionDiagnosticsArrayState>(
VariableIds.Server_ServerDiagnostics_SubscriptionDiagnosticsArray);
Assert.That(subArrayNode, Is.Not.Null);
Assert.That(subArrayNode.OnSimpleReadValue, Is.Not.Null,
"OnSimpleReadValue should be wired on SubscriptionDiagnosticsArray.");
Assert.That(subArrayNode.OnReadUserRolePermissions, Is.Not.Null,
"OnReadUserRolePermissions should be wired on SubscriptionDiagnosticsArray.");
manager.ForceDiagnosticsScan();
arrayValue = default;
result = subArrayNode.OnSimpleReadValue(manager.SystemContext, subArrayNode, ref arrayValue);
Assert.That(result.StatusCode, Is.EqualTo(StatusCodes.Good));
Assert.That(arrayValue.IsNull, Is.False);
}
[Test]
public async Task ReadUserRolePermissions_AdminAndSessionHandlingAsync()
{
var config = new ApplicationConfiguration { ServerConfiguration = new ServerConfiguration() };
SetupServerMock();
using var manager = new DiagnosticsNodeManager(m_serverMock.Object, config, NullLogger.Instance);
var externalRefs = new Dictionary<NodeId, IList<IReference>>();
await manager.CreateAddressSpaceAsync(externalRefs).ConfigureAwait(false);
static ServiceResult UpdateCallback(ISystemContext ctx, NodeState node, ref Variant value) => ServiceResult.Good;
// 1. Create server and session diagnostics
await manager.CreateServerDiagnosticsAsync(
manager.SystemContext,
new ServerDiagnosticsSummaryDataType(),
UpdateCallback,
CancellationToken.None).ConfigureAwait(false);
var initialSessionId = new NodeId(1234, 1);
var sessionDiag = new SessionDiagnosticsDataType { SessionName = "TestSession", SessionId = initialSessionId };
var sessionSecDiag = new SessionSecurityDiagnosticsDataType { SessionId = initialSessionId };
NodeId sessionId = await manager.CreateSessionDiagnosticsAsync(
manager.SystemContext,
sessionDiag,
UpdateCallback,
sessionSecDiag,
UpdateCallback).ConfigureAwait(false);
// Get nodes for permissions testing
ServerDiagnosticsSummaryState summaryNode =
manager.FindPredefinedNode<ServerDiagnosticsSummaryState>(VariableIds.Server_ServerDiagnostics_ServerDiagnosticsSummary);
SessionDiagnosticsObjectState sessionObjectNode = manager.FindPredefinedNode<SessionDiagnosticsObjectState>(sessionId);
Assert.That(summaryNode, Is.Not.Null);
Assert.That(sessionObjectNode, Is.Not.Null);
Assert.That(summaryNode.OnReadUserRolePermissions, Is.Not.Null);
Assert.That(sessionObjectNode.OnReadUserRolePermissions, Is.Not.Null);
// 2. Test Context: Admin
var identity = new UserIdentity("admin", []);
Role[] roles = [Role.SecurityAdmin];
var namespaces = new NamespaceTable();
namespaces.Append(Ua.Namespaces.OpcUa);
var roleIdentity = new RoleBasedIdentity(identity, roles, namespaces);
var endpoint = new EndpointDescription { SecurityMode = MessageSecurityMode.SignAndEncrypt };
var channelContext = new SecureChannelContext(
"1",
endpoint,
RequestEncoding.Binary,
clientChannelCertificate: null,
serverChannelCertificate: null,
channelThumbprint: null);
var reqHeader = new RequestHeader();
var sessionMock = new Mock<ISession>();
sessionMock.Setup(s => s.Id).Returns(new NodeId(9999, 1)); // Different from sessionId to test admin overrides
sessionMock.Setup(s => s.EffectiveIdentity).Returns(roleIdentity);
var opContextAdmin = new OperationContext(reqHeader, channelContext, RequestType.Read, RequestLifetime.None, sessionMock.Object);
var adminContext = new ServerSystemContext(m_serverMock.Object, opContextAdmin);
ArrayOf<RolePermissionType> permissionsAdmin = default;
summaryNode.OnReadUserRolePermissions(adminContext, summaryNode, ref permissionsAdmin);
Assert.That(permissionsAdmin.IsNull, Is.False);
Assert.That(permissionsAdmin.Count, Is.GreaterThan(0));
Assert.That(permissionsAdmin[0].Permissions, Is.Not.EqualTo((uint)PermissionType.None),
"Admin should have permissions on summary");
permissionsAdmin = default;
sessionObjectNode.OnReadUserRolePermissions(adminContext, sessionObjectNode, ref permissionsAdmin);
Assert.That(permissionsAdmin.IsNull, Is.False);
Assert.That(permissionsAdmin[0].Permissions, Is.Not.EqualTo((uint)PermissionType.None),
"Admin should have permissions on any session");
// 3. Test Context: Session Owner (Non-Admin)
var normalIdentity = new UserIdentity("owner", []);
var normalRoleIdentity = new RoleBasedIdentity(normalIdentity, [Role.AuthenticatedUser], namespaces);
var sessionOwnerMock = new Mock<ISession>();
sessionOwnerMock.Setup(s => s.Id).Returns(sessionId); // Same as sessionId
sessionOwnerMock.Setup(s => s.EffectiveIdentity).Returns(normalRoleIdentity);
var opContextOwner = new OperationContext(
reqHeader, channelContext, RequestType.Read, RequestLifetime.None, sessionOwnerMock.Object);
var ownerContext = new ServerSystemContext(m_serverMock.Object, opContextOwner);
ArrayOf<RolePermissionType> permissionsOwner = default;
summaryNode.OnReadUserRolePermissions(ownerContext, summaryNode, ref permissionsOwner);
Assert.That(permissionsOwner.IsNull, Is.False);
Assert.That(permissionsOwner[0].Permissions, Is.EqualTo((uint)PermissionType.None),
"Non-admin owner should NOT have overall summary permissions");
permissionsOwner = default;
sessionObjectNode.OnReadUserRolePermissions(ownerContext, sessionObjectNode, ref permissionsOwner);
Assert.That(permissionsOwner.IsNull, Is.False);
Assert.That(permissionsOwner[0].Permissions, Is.Not.EqualTo((uint)PermissionType.None),
"Owner should have permissions on their own session");
// 4. Test Context: Other Session (Non-Admin)
var sessionOtherMock = new Mock<ISession>();
sessionOtherMock.Setup(s => s.Id).Returns(new NodeId(9999, 1)); // Different sessionId
sessionOtherMock.Setup(s => s.EffectiveIdentity).Returns(normalRoleIdentity);
var opContextOther = new OperationContext(
reqHeader, channelContext, RequestType.Read, RequestLifetime.None, sessionOtherMock.Object);
var otherContext = new ServerSystemContext(m_serverMock.Object, opContextOther);
ArrayOf<RolePermissionType> permissionsOther = default;
sessionObjectNode.OnReadUserRolePermissions(otherContext, sessionObjectNode, ref permissionsOther);
Assert.That(permissionsOther.IsNull, Is.False);
Assert.That(permissionsOther[0].Permissions, Is.EqualTo((uint)PermissionType.None),
"Other non-admin session should NOT have permissions on this session");
}
[Test]
public async Task OnReadDiagnosticsArray_ExpectedBehaviorForDifferentTypesAsync()
{
var config = new ApplicationConfiguration { ServerConfiguration = new ServerConfiguration() };
SetupServerMock();
using var manager = new DiagnosticsNodeManager(m_serverMock.Object, config, NullLogger.Instance);
var externalRefs = new Dictionary<NodeId, IList<IReference>>();
await manager.CreateAddressSpaceAsync(externalRefs).ConfigureAwait(false);
var sessionDiag = new SessionDiagnosticsDataType { SessionName = "TestSession", SessionId = new NodeId(1, 1) };
var sessionSecDiag = new SessionSecurityDiagnosticsDataType { SessionId = new NodeId(1, 1) };
var subDiag = new SubscriptionDiagnosticsDataType { SubscriptionId = 100, SessionId = new NodeId(1, 1) };
var sessionDiag2 = new SessionDiagnosticsDataType { SessionName = "TestSession2", SessionId = new NodeId(2, 1) };
var sessionSecDiag2 = new SessionSecurityDiagnosticsDataType { SessionId = new NodeId(2, 1) };
var subDiag2 = new SubscriptionDiagnosticsDataType { SubscriptionId = 200, SessionId = new NodeId(2, 1) };
ServiceResult UpdateCallback(ISystemContext ctx, NodeState node, ref Variant value)
{
var instanceState = node as BaseInstanceState;
if (node.BrowseName.Name == BrowseNames.SessionDiagnostics)
{
value = instanceState?.Parent?.BrowseName.Name == "TestSession" ? Variant.FromStructure(sessionDiag) : Variant.FromStructure(sessionDiag2);
}
else if (node.BrowseName.Name == BrowseNames.SessionSecurityDiagnostics)
{
value = instanceState?.Parent?.BrowseName.Name == "TestSession"
? Variant.FromStructure(sessionSecDiag)
: Variant.FromStructure(sessionSecDiag2);
}
else if (node.BrowseName.Name == "100")
{
value = Variant.FromStructure(subDiag); // Subscription node is named after SubscriptionId
}
else if (node.BrowseName.Name == "200")
{
value = Variant.FromStructure(subDiag2);
}
else
{
value = Variant.FromStructure(new ServerDiagnosticsSummaryDataType());
}
return ServiceResult.Good;
}
await manager.CreateServerDiagnosticsAsync(manager.SystemContext, new ServerDiagnosticsSummaryDataType(), UpdateCallback, CancellationToken.None)
.ConfigureAwait(false);
NodeId sessionId = await manager.CreateSessionDiagnosticsAsync(
manager.SystemContext,
sessionDiag,
UpdateCallback,
sessionSecDiag,
UpdateCallback).ConfigureAwait(false);
subDiag.SessionId = sessionId;
NodeId subId = await manager.CreateSubscriptionDiagnosticsAsync(
manager.SystemContext,
subDiag,
UpdateCallback).ConfigureAwait(false);
subDiag2.SessionId = await manager.CreateSessionDiagnosticsAsync(
manager.SystemContext,
sessionDiag2,
UpdateCallback,
sessionSecDiag2,
UpdateCallback).ConfigureAwait(false);
NodeId subId2 = await manager.CreateSubscriptionDiagnosticsAsync(
manager.SystemContext,
subDiag2,
UpdateCallback).ConfigureAwait(false);
manager.ForceDiagnosticsScan();
var adminIdentity = new UserIdentity("admin", []);
var namespaces = new NamespaceTable();
namespaces.Append(Ua.Namespaces.OpcUa);
var roleIdentity = new RoleBasedIdentity(adminIdentity, [Role.SecurityAdmin], namespaces);
var sessionMock = new Mock<ISession>();
sessionMock.Setup(s => s.Id).Returns(new NodeId(9999, 1));
sessionMock.Setup(s => s.EffectiveIdentity).Returns(roleIdentity);
var endpoint = new EndpointDescription { SecurityMode = MessageSecurityMode.SignAndEncrypt };
var secureChannelContext = new SecureChannelContext(
"1",
endpoint,
RequestEncoding.Binary,
clientChannelCertificate: null,
serverChannelCertificate: null,
channelThumbprint: null);
var opContext = new OperationContext(
new RequestHeader(),
secureChannelContext,
RequestType.Read,
RequestLifetime.None,
sessionMock.Object);
var adminContext = new ServerSystemContext(m_serverMock.Object, opContext);
SessionDiagnosticsArrayState sessionArrayNode = manager.FindPredefinedNode<SessionDiagnosticsArrayState>(
VariableIds.Server_ServerDiagnostics_SessionsDiagnosticsSummary_SessionDiagnosticsArray);
Variant arrayValue = default;
ServiceResult result = sessionArrayNode.OnSimpleReadValue(adminContext, sessionArrayNode, ref arrayValue);
Assert.That(result.StatusCode, Is.EqualTo(StatusCodes.Good));
Assert.That(arrayValue.TryGetValue(out ArrayOf<ExtensionObject> sessionArray), Is.True);
Assert.That(sessionArray.Count, Is.EqualTo(2));
sessionArray[0].TryGetValue(out SessionDiagnosticsDataType sessionDiagObj);
Assert.That(sessionDiagObj.SessionName, Is.EqualTo("TestSession"));
SessionSecurityDiagnosticsArrayState sessionSecurityArrayNode = manager.FindPredefinedNode<SessionSecurityDiagnosticsArrayState>(
VariableIds.Server_ServerDiagnostics_SessionsDiagnosticsSummary_SessionSecurityDiagnosticsArray);
arrayValue = default;
result = sessionSecurityArrayNode.OnSimpleReadValue(adminContext, sessionSecurityArrayNode, ref arrayValue);
Assert.That(result.StatusCode, Is.EqualTo(StatusCodes.Good));
Assert.That(arrayValue.TryGetValue(out ArrayOf<ExtensionObject> sessionSecurityArray), Is.True);
Assert.That(sessionSecurityArray.Count, Is.EqualTo(2));
sessionSecurityArray[0].TryGetValue(out SessionSecurityDiagnosticsDataType sessionSecDiagObj);
Assert.That(sessionSecDiagObj.SessionId, Is.EqualTo(sessionId));
SubscriptionDiagnosticsArrayState subArrayNode = manager.FindPredefinedNode<SubscriptionDiagnosticsArrayState>(
VariableIds.Server_ServerDiagnostics_SubscriptionDiagnosticsArray);
arrayValue = default;
result = subArrayNode.OnSimpleReadValue(adminContext, subArrayNode, ref arrayValue);
Assert.That(result.StatusCode, Is.EqualTo(StatusCodes.Good));
Assert.That(arrayValue.TryGetValue(out ArrayOf<ExtensionObject> subArray), Is.True);
Assert.That(subArray.Count, Is.EqualTo(2));
subArray[0].TryGetValue(out SubscriptionDiagnosticsDataType subDiagObj);
Assert.That(subDiagObj.SubscriptionId, Is.EqualTo(100));
// Test unauthorized non-admin user accessing their own session
var normalIdentity = new UserIdentity("user", []);
var normalRoleIdentity = new RoleBasedIdentity(normalIdentity, [Role.AuthenticatedUser], namespaces);
var normalSessionMock = new Mock<ISession>();
normalSessionMock.Setup(s => s.Id).Returns(sessionId); // set to first session
normalSessionMock.Setup(s => s.EffectiveIdentity).Returns(normalRoleIdentity);
var normalOpContext = new OperationContext(
new RequestHeader(),
secureChannelContext,
RequestType.Read,
RequestLifetime.None,
normalSessionMock.Object);
var normalContext = new ServerSystemContext(m_serverMock.Object, normalOpContext);
manager.ForceDiagnosticsScan();
arrayValue = default;
result = sessionArrayNode.OnSimpleReadValue(normalContext, sessionArrayNode, ref arrayValue);
Assert.That(result.StatusCode, Is.EqualTo(StatusCodes.Good));
Assert.That(arrayValue.TryGetValue(out ArrayOf<ExtensionObject> normalSessionArray), Is.True);
Assert.That(normalSessionArray.Count, Is.EqualTo(1));
normalSessionArray[0].TryGetValue(out SessionDiagnosticsDataType normalSessionDiagObj);
Assert.That(normalSessionDiagObj.SessionName, Is.EqualTo("TestSession"));
manager.ForceDiagnosticsScan();
arrayValue = default;
result = sessionSecurityArrayNode.OnSimpleReadValue(normalContext, sessionSecurityArrayNode, ref arrayValue);
Assert.That(result.StatusCode, Is.EqualTo(StatusCodes.Good));
Assert.That(arrayValue.TryGetValue(out ArrayOf<ExtensionObject> normalSessionSecArray), Is.True);
Assert.That(normalSessionSecArray.Count, Is.EqualTo(1));
normalSessionSecArray[0].TryGetValue(out SessionSecurityDiagnosticsDataType normalSessionSecDiagObj);
Assert.That(normalSessionSecDiagObj.SessionId, Is.EqualTo(sessionId));
manager.ForceDiagnosticsScan();
arrayValue = default;
result = subArrayNode.OnSimpleReadValue(normalContext, subArrayNode, ref arrayValue);
Assert.That(result.StatusCode, Is.EqualTo(StatusCodes.Good));
Assert.That(arrayValue.TryGetValue(out ArrayOf<ExtensionObject> normalSubArray), Is.True);
Assert.That(normalSubArray.Count, Is.EqualTo(1));
normalSubArray[0].TryGetValue(out SubscriptionDiagnosticsDataType normalSubDiagObj);
Assert.That(normalSubDiagObj.SessionId, Is.EqualTo(sessionId));
// Test if recently scanned just returns Good
result = sessionArrayNode.OnSimpleReadValue(adminContext, sessionArrayNode, ref arrayValue);
Assert.That(result.StatusCode, Is.EqualTo(StatusCodes.Good));
// Test behavior when diagnostics are disabled
await manager.SetDiagnosticsEnabledAsync(manager.SystemContext, false).ConfigureAwait(false);
result = sessionArrayNode.OnSimpleReadValue(adminContext, sessionArrayNode, ref arrayValue);
Assert.That(result.StatusCode, Is.EqualTo(StatusCodes.BadOutOfService));
}
[Test]
public async Task MonitoredItemLifecycle_ChangesDiagnosticsMonitoring_And_SamplingAsync()
{
var config = new ApplicationConfiguration { ServerConfiguration = new ServerConfiguration() };
SetupServerMock();
// Required for CreateMonitoredItemsAsync
using var queueFactory = new MonitoredItemQueueFactory(m_serverMock.Object.Telemetry);
m_serverMock.Setup(s => s.MonitoredItemQueueFactory)
.Returns(queueFactory);
using var manager = new DiagnosticsNodeManager(m_serverMock.Object, config, NullLogger.Instance);
var externalRefs = new Dictionary<NodeId, IList<IReference>>();
await manager.CreateAddressSpaceAsync(externalRefs).ConfigureAwait(false);
var endpoint = new EndpointDescription { SecurityMode = MessageSecurityMode.SignAndEncrypt };
var secureChannelContext = new SecureChannelContext(
"1",
endpoint,
RequestEncoding.Binary,
clientChannelCertificate: null,
serverChannelCertificate: null,
channelThumbprint: null);
var mockIdentity = new UserIdentity("admin", []);
var mockRoleIdentity = new RoleBasedIdentity(mockIdentity, [Role.SecurityAdmin], m_serverMock.Object.NamespaceUris);
var sessionMock = new Mock<ISession>();
sessionMock.Setup(s => s.Id).Returns(new NodeId(1, 1));
sessionMock.Setup(s => s.EffectiveIdentity).Returns(mockRoleIdentity);
var opContext = new OperationContext(new RequestHeader(), secureChannelContext, RequestType.Read, RequestLifetime.None, sessionMock.Object);
var sysContext = new ServerSystemContext(m_serverMock.Object, opContext);
static ServiceResult UpdateCallback(ISystemContext ctx, NodeState node, ref Variant value)
{
value = Variant.FromStructure(new ServerDiagnosticsSummaryDataType { CurrentSessionCount = 42 });
return ServiceResult.Good;
}
await manager.CreateServerDiagnosticsAsync(
manager.SystemContext,
new ServerDiagnosticsSummaryDataType(),
UpdateCallback,
CancellationToken.None).ConfigureAwait(false);
ServerDiagnosticsSummaryState summaryNode =
manager.FindPredefinedNode<ServerDiagnosticsSummaryState>(VariableIds.Server_ServerDiagnostics_ServerDiagnosticsSummary);
Assert.That(summaryNode, Is.Not.Null);
var itemToCreate = new MonitoredItemCreateRequest
{
ItemToMonitor = new ReadValueId { NodeId = summaryNode.NodeId, AttributeId = Attributes.Value },
MonitoringMode = MonitoringMode.Reporting,
RequestedParameters = new MonitoringParameters { ClientHandle = 1, SamplingInterval = 10, QueueSize = 10 }
};
var itemsToCreate = new List<MonitoredItemCreateRequest> { itemToCreate };
var createErrors = new List<ServiceResult> { null };
var filterErrors = new List<MonitoringFilterResult> { null };
var monitoredItems = new List<IMonitoredItem> { null };
await manager.CreateMonitoredItemsAsync(
opContext,
1,
100,
TimestampsToReturn.Both,
itemsToCreate,
createErrors,
filterErrors,
monitoredItems,
false,
new MonitoredItemIdFactory()).ConfigureAwait(false);
Assert.That(ServiceResult.IsGood(createErrors[0]), Is.True);
var monitoredItem = monitoredItems[0] as IDataChangeMonitoredItem;
Assert.That(monitoredItem, Is.Not.Null);
// Push an update
manager.ForceDiagnosticsScan();
// Wait for internal timer to call DoSample
bool valueQueued = false;
for (int i = 0; i < 10; i++)
{
var notifications = new Queue<MonitoredItemNotification>();
var diagnostics = new Queue<DiagnosticInfo>();
monitoredItem.Publish(
opContext,
notifications,
diagnostics,
1,
NullLogger.Instance);
if (notifications.Count > 0)
{
valueQueued = true;
break;
}
await Task.Delay(250).ConfigureAwait(false);
}
Assert.That(valueQueued, Is.True, "MonitoredItem did not receive updated value from sampling timer.");
// Delete nodes
var processedItems = new List<bool> { false };
var deleteErrors = new List<ServiceResult> { null };
await manager.DeleteMonitoredItemsAsync(
opContext,
[monitoredItem],
processedItems,
deleteErrors).ConfigureAwait(false);
Assert.That(ServiceResult.IsGood(deleteErrors[0]), Is.True);
}
}
}