Files
UA-.NETStandard/tests/Opc.Ua.Server.Tests/DiagnosticsNodeManagerTests.cs
romanett 9b034f8291 Enable nullable reference types centrally; migrate all unset and annotations-only projects (#4700)
# Description

Nullable reference types are now enabled once, in `common.props`, and
every project in the repository builds warning-clean with them.

**Configuration**
- `<Nullable>enable</Nullable>` lives in `common.props`. All per-project
`<Nullable>` settings are removed: 178 project files (enable,
annotations, and the `disable` in the three Aot test projects).
- 419 per-file `#nullable enable`/`restore` directives and the 4
`#nullable disable` directives are removed.
- Kept on purpose:
- The 5 `<auto-generated>` schema files (`UANodeSet.cs`, `UA Type
Dictionary.cs`, `OPCBinarySchema.cs`, `SecuredApplication.cs`,
`PInvokeHelper.cs`) keep `#nullable enable`. The compiler ignores the
project setting in generated code, so the directive is required there.
- `tests/Opc.Ua.Interop.LegacyPeer` keeps `disable`. It builds against
the 1.5.378 packages and does not import the repository props.
- `docs/DeveloperGuide.md` and `.github/copilot-instructions.md` now
describe the central setting.

**Migration** (the 43 unset and 13 annotations-only projects, roughly
10k diagnostics)
- **Production code: annotated at the declarations.** Covers
`tools/Opc.Ua.SourceGeneration.Core`, `src/Opc.Ua.Lds.Server` and the
LDS sample.
- Members, parameters and returns are nullable where null actually
flows.
- Null-check helpers carry `[NotNullWhen]` (`IsNull`, `IsValidXmlName`,
`IsMethodTypeDesign`).
- `CoreUtils.Format`, `Utils.Format`, `ITemplateWriter.WriteLine` take
`params object?[]`. `Template.AddReplacement` takes `string?` (rendering
already skips null replacements).
- `IRegisteredServerStore.RegisterAsync` returns
`Task<RegistrationEntry?>`: it returns null when a registration is
removed.
- Dereferences backed by an invariant (for example, the validated model)
use `!`, so runtime behaviour is unchanged.
- **Behavioural changes in production code** (small, deliberate):
- `NodeSetToModelDesign` throws `InvalidOperationException` when the
file is not a NodeSet. Before, it threw a NullReferenceException on the
next line.
- `ModelDesignValidator` starts its lookup tables empty instead of null.
A redundant `if (m_nodes == null) return;` guard is gone.
- **Test code: minimal annotations.**
  - `?` on values that can be null, `!` where the test asserts non-null.
  - `= null!` for fixture members assigned in SetUp.
- `TryGetStructure<T>(out T? x)` where the inferred `T?` broke the
`IEncodeable` constraint.
- A few redundant `== null` checks on structs (`NodeId`, `ArrayOf<T>`)
folded into the adjacent `IsNull`/`Count` checks.

**Notes for reviewers**
- The diff is large (~1,500 files) but almost entirely mechanical: `?`,
`!` and directive removal. The non-annotation changes are listed above.
- Known follow-up, not done here:
`IModelDesign.Namespaces/TargetNamespace/NamespaceUris` stay
non-nullable even though the validator returns null before it loads a
dictionary. Making them nullable would ripple through every generator
call site.

## Related Issues

- Fixes #4439

## Checklist

- [ ] 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.
- [ ] I have added tests that prove my fix is effective or that my
feature works and increased code coverage.
- [x] I have added all necessary documentation.
- [x] I have verified that my changes do not introduce (new) build or
analyzer warnings.
- [ ] 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.

The "no new warnings" box covers net10.0 and net48: full non-incremental
builds of `UA.slnx`, `tools/SourceGeneration.slnx`,
`tools/MigrationAnalyzer.slnx` and the Aot test projects finished with 0
warnings and 0 errors under the repository's warnings-as-errors
settings. net9.0 and net8.0 also had no nullable diagnostics, but they
were not rebuilt after the final fixes. Tests have not been run locally
yet.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 08:48:13 +02:00

1430 lines
73 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.Linq;
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 static readonly string[] s_declaredAndContributedProfiles =
["urn:profile:declared", "urn:profile:contributed"];
private static readonly string[] s_declaredProfiles = ["urn:profile:declared"];
private Mock<IServerInternal> m_serverMock = null!;
private Mock<ICoreNodeManager> m_coreNodeManagerMock = null!;
private Mock<ISubscriptionManager> m_subscriptionManagerMock = null!;
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 PublishConformanceUnits_KeepsDeclaredProfilesAndWithdrawsContributedOnesAsync()
{
var config = new ApplicationConfiguration { ServerConfiguration = new ServerConfiguration() };
SetupServerMock();
using var manager = new DiagnosticsNodeManager(m_serverMock.Object, config, NullLogger.Instance);
await manager.CreateAddressSpaceAsync(new Dictionary<NodeId, IList<IReference>>()).ConfigureAwait(false);
BaseVariableState profiles = manager.FindPredefinedNode<BaseVariableState>(
VariableIds.Server_ServerCapabilities_ServerProfileArray);
BaseVariableState units = manager.FindPredefinedNode<BaseVariableState>(
VariableIds.Server_ServerCapabilities_ConformanceUnits);
ArrayOf<string> declared = ["urn:profile:declared"];
profiles.Value = Variant.From(declared);
await manager.PublishConformanceUnitsAsync(
[new QualifiedName("Contributed Unit")],
["urn:profile:contributed", "urn:profile:declared"]).ConfigureAwait(false);
string[] first = profiles.Value.TryGetValue(out ArrayOf<string> afterFirst) ? afterFirst.ToArray()! : [];
await manager.PublishConformanceUnitsAsync([], []).ConfigureAwait(false);
string[] second = profiles.Value.TryGetValue(out ArrayOf<string> afterSecond) ? afterSecond.ToArray()! : [];
Assert.That(first, Is.EqualTo(s_declaredAndContributedProfiles));
Assert.That(second, Is.EqualTo(s_declaredProfiles));
Assert.That(
units.Value.TryGetValue(out ArrayOf<QualifiedName> unitsAfter) && unitsAfter.Count == 0,
Is.True);
}
[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)
{
// Sampling interval diagnostics are not collected; the array is empty but readable.
Assert.That(serverDiagnostics.SamplingIntervalDiagnosticsArray.StatusCode.Code,
Is.EqualTo(StatusCodes.Good));
Assert.That(serverDiagnostics.SamplingIntervalDiagnosticsArray.Value, Is.Empty);
}
Assert.That(serverDiagnostics.SubscriptionDiagnosticsArray?.StatusCode.Code, Is.EqualTo(StatusCodes.Good));
// 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 SetDiagnosticsEnabledAsync_Toggle_HidesAndRestoresDiagnosticsAsync()
{
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 SummaryCallback(ISystemContext ctx, NodeState node, ref Variant value)
{
value = Variant.FromStructure(new ServerDiagnosticsSummaryDataType { CumulatedSessionCount = 7 });
return ServiceResult.Good;
}
static ServiceResult UpdateCallback(ISystemContext ctx, NodeState node, ref Variant value) => ServiceResult.Good;
await manager.CreateServerDiagnosticsAsync(
manager.SystemContext,
new ServerDiagnosticsSummaryDataType(),
SummaryCallback,
CancellationToken.None).ConfigureAwait(false);
NodeId liveSessionId = await manager.CreateSessionDiagnosticsAsync(
manager.SystemContext,
new SessionDiagnosticsDataType { SessionName = "LiveSession" },
UpdateCallback,
new SessionSecurityDiagnosticsDataType(),
UpdateCallback).ConfigureAwait(false);
NodeId subscriptionId = await manager.CreateSubscriptionDiagnosticsAsync(
manager.SystemContext,
new SubscriptionDiagnosticsDataType { SubscriptionId = 42, SessionId = liveSessionId },
UpdateCallback).ConfigureAwait(false);
BaseVariableState cumulatedSessionCount = manager.FindPredefinedNode<BaseVariableState>(
VariableIds.Server_ServerDiagnostics_ServerDiagnosticsSummary_CumulatedSessionCount);
Assert.That(ReadValue(manager, cumulatedSessionCount).StatusCode, Is.EqualTo(StatusCodes.Good));
// Act: disable
await manager.SetDiagnosticsEnabledAsync(manager.SystemContext, false).ConfigureAwait(false);
DataValue disabledValue = ReadValue(manager, cumulatedSessionCount);
Assert.That(disabledValue.StatusCode, Is.EqualTo(StatusCodes.BadNotReadable));
Assert.That(disabledValue.WrappedValue.IsNull, Is.True);
ServerDiagnosticsState serverDiagnostics = manager.FindPredefinedNode<ServerDiagnosticsState>(ObjectIds.Server_ServerDiagnostics);
Assert.That(
ReadValue(manager, serverDiagnostics.SubscriptionDiagnosticsArray!).StatusCode,
Is.EqualTo(StatusCodes.BadNotReadable));
Assert.That(manager.FindPredefinedNode<NodeState>(liveSessionId), Is.Null);
Assert.That(manager.FindPredefinedNode<NodeState>(subscriptionId), Is.Null);
// a session created while disabled gets an id but no node.
NodeId laterSessionId = await manager.CreateSessionDiagnosticsAsync(
manager.SystemContext,
new SessionDiagnosticsDataType { SessionName = "LaterSession" },
UpdateCallback,
new SessionSecurityDiagnosticsDataType(),
UpdateCallback).ConfigureAwait(false);
Assert.That(laterSessionId.IsNull, Is.False);
Assert.That(manager.FindPredefinedNode<NodeState>(laterSessionId), Is.Null);
// Act: enable
await manager.SetDiagnosticsEnabledAsync(manager.SystemContext, true).ConfigureAwait(false);
DataValue enabledValue = ReadValue(manager, cumulatedSessionCount);
Assert.That(enabledValue.StatusCode, Is.EqualTo(StatusCodes.Good));
Assert.That(enabledValue.WrappedValue.GetUInt32(), Is.EqualTo(7));
SessionDiagnosticsObjectState liveSession = manager.FindPredefinedNode<SessionDiagnosticsObjectState>(liveSessionId);
SessionDiagnosticsObjectState laterSession = manager.FindPredefinedNode<SessionDiagnosticsObjectState>(laterSessionId);
Assert.That(liveSession, Is.Not.Null, "The live session node is restored with its NodeId.");
Assert.That(laterSession, Is.Not.Null, "The session created while disabled gets its node.");
Assert.That(liveSession.SessionDiagnostics, Is.Not.Null);
Assert.That(liveSession.SessionDiagnostics.NodeId.IsNull, Is.False);
Assert.That(manager.FindPredefinedNode<SubscriptionDiagnosticsState>(subscriptionId), Is.Not.Null);
SessionsDiagnosticsSummaryState summary = manager.FindPredefinedNode<SessionsDiagnosticsSummaryState>(
ObjectIds.Server_ServerDiagnostics_SessionsDiagnosticsSummary);
var references = new List<IReference>();
summary.GetReferences(manager.SystemContext, references, ReferenceTypeIds.HasComponent, false);
Assert.That(
references.Select(r => (NodeId)r.TargetId),
Is.SupersetOf([liveSessionId, laterSessionId]));
Assert.That(
references.Count(r => (NodeId)r.TargetId == liveSessionId),
Is.EqualTo(1),
"The summary must not reference a session twice.");
// deleting the restored nodes still works.
await manager.DeleteSubscriptionDiagnosticsAsync(manager.SystemContext, subscriptionId).ConfigureAwait(false);
await manager.DeleteSessionDiagnosticsAsync(manager.SystemContext, liveSessionId).ConfigureAwait(false);
Assert.That(manager.FindPredefinedNode<NodeState>(subscriptionId), Is.Null);
Assert.That(manager.FindPredefinedNode<NodeState>(liveSessionId), Is.Null);
}
[Test]
public async Task SetDiagnosticsEnabledAsync_EnableDuringDisable_KeepsRestoredNodesAsync()
{
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);
// make the removal of the dynamic nodes asynchronous, as it is on a running server.
var slowNodeManager = new Mock<IMasterNodeManager>();
slowNodeManager.Setup(m => m.RemoveReferencesAsync(It.IsAny<List<LocalReference>>(), It.IsAny<CancellationToken>()))
.Returns(() => new ValueTask(Task.Delay(50)));
m_serverMock.Setup(s => s.NodeManager).Returns(slowNodeManager.Object);
static ServiceResult UpdateCallback(ISystemContext ctx, NodeState node, ref Variant value) => ServiceResult.Good;
var sessionIds = new List<NodeId>();
for (int ii = 0; ii < 3; ii++)
{
sessionIds.Add(await manager.CreateSessionDiagnosticsAsync(
manager.SystemContext,
new SessionDiagnosticsDataType { SessionName = "RacingSession" + ii },
UpdateCallback,
new SessionSecurityDiagnosticsDataType(),
UpdateCallback).ConfigureAwait(false));
}
// the enable starts while the disable still has to delete the session node; it must
// run after the disable has finished and leave the restored node in place.
ValueTask disable = manager.SetDiagnosticsEnabledAsync(manager.SystemContext, false);
ValueTask enable = manager.SetDiagnosticsEnabledAsync(manager.SystemContext, true);
await disable.ConfigureAwait(false);
await enable.ConfigureAwait(false);
Assert.That(manager.DiagnosticsEnabled, Is.True);
foreach (NodeId sessionId in sessionIds)
{
Assert.That(
manager.FindPredefinedNode<SessionDiagnosticsObjectState>(sessionId),
Is.Not.Null,
"A disable that finishes after the enable started must not delete the restored nodes.");
}
}
private static DataValue ReadValue(DiagnosticsNodeManager manager, NodeState node)
{
var value = new DataValue();
ServiceResult result = node.ReadAttribute(
manager.SystemContext,
Attributes.Value,
default,
default,
ref value);
return ServiceResult.IsBad(result) ? DataValue.FromStatusCode(result.StatusCode) : value;
}
[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");
// 5. Any session may browse the server wide subscription array, whose entries are
// filtered per owning session; only an administrator may read its value.
NodeState subscriptionArray = manager.FindPredefinedNode<NodeState>(
VariableIds.Server_ServerDiagnostics_SubscriptionDiagnosticsArray);
ArrayOf<RolePermissionType> permissionsArray = default;
subscriptionArray.OnReadUserRolePermissions!(otherContext, subscriptionArray, ref permissionsArray);
Assert.That(permissionsArray[0].Permissions, Is.EqualTo((uint)PermissionType.Browse),
"Non-admin sessions may browse, but not read, the subscription diagnostics array");
permissionsArray = default;
subscriptionArray.OnReadUserRolePermissions(adminContext, subscriptionArray, ref permissionsArray);
Assert.That(((PermissionType)permissionsArray[0].Permissions).HasFlag(PermissionType.Read), Is.True,
"Admin should read the subscription diagnostics array");
foreach (NodeId arrayId in new[]
{
VariableIds.Server_ServerDiagnostics_SessionsDiagnosticsSummary_SessionDiagnosticsArray,
VariableIds.Server_ServerDiagnostics_SessionsDiagnosticsSummary_SessionSecurityDiagnosticsArray
})
{
NodeState arrayNode = manager.FindPredefinedNode<NodeState>(arrayId);
permissionsArray = default;
arrayNode.OnReadUserRolePermissions!(otherContext, arrayNode, ref permissionsArray);
Assert.That(permissionsArray[0].Permissions, Is.EqualTo((uint)PermissionType.None), arrayId.ToString());
}
}
[Test]
public async Task DiagnosticsDescendantsAndTransferredSubscriptionsUseOwningSessionAsync()
{
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;
NodeId sessionId = await manager.CreateSessionDiagnosticsAsync(
manager.SystemContext,
new SessionDiagnosticsDataType { SessionName = "Owner" },
UpdateCallback,
new SessionSecurityDiagnosticsDataType(),
UpdateCallback).ConfigureAwait(false);
var subscriptionDiagnostics = new SubscriptionDiagnosticsDataType
{
SessionId = sessionId,
SubscriptionId = 1
};
NodeId subscriptionId = await manager.CreateSubscriptionDiagnosticsAsync(
manager.SystemContext,
subscriptionDiagnostics,
UpdateCallback).ConfigureAwait(false);
SessionDiagnosticsObjectState sessionNode =
manager.FindPredefinedNode<SessionDiagnosticsObjectState>(sessionId);
SubscriptionDiagnosticsState subscriptionNode =
manager.FindPredefinedNode<SubscriptionDiagnosticsState>(subscriptionId);
Assert.That(sessionNode, Is.Not.Null);
Assert.That(subscriptionNode, Is.Not.Null);
var descendants = new List<BaseInstanceState>();
sessionNode.GetChildren(manager.SystemContext, descendants);
Assert.That(descendants, Is.Not.Empty);
foreach (BaseInstanceState descendant in descendants)
{
Assert.That(descendant.OnReadUserRolePermissions, Is.Not.Null);
}
Assert.That(subscriptionNode.OnReadUserRolePermissions, Is.Not.Null);
ServerSystemContext oldOwnerContext = CreateDiagnosticsReadContext(m_serverMock.Object, sessionId);
ArrayOf<RolePermissionType> permissions = default;
subscriptionNode.OnReadUserRolePermissions!(
oldOwnerContext,
subscriptionNode,
ref permissions);
Assert.That(permissions[0].Permissions, Is.Not.EqualTo((uint)PermissionType.None));
NodeId newSessionId = new(2, 1);
manager.RelinkSubscriptionDiagnostics(subscriptionId, sessionId, newSessionId);
permissions = default;
subscriptionNode.OnReadUserRolePermissions!(
oldOwnerContext,
subscriptionNode,
ref permissions);
Assert.That(permissions[0].Permissions, Is.EqualTo((uint)PermissionType.None));
ServerSystemContext newOwnerContext = CreateDiagnosticsReadContext(m_serverMock.Object, newSessionId);
permissions = default;
subscriptionNode.OnReadUserRolePermissions!(
newOwnerContext,
subscriptionNode,
ref permissions);
Assert.That(permissions[0].Permissions, Is.Not.EqualTo((uint)PermissionType.None));
}
[TestCase(false)]
[TestCase(true)]
public async Task SessionlessCallerDoesNotOwnOrphanedSubscriptionDiagnosticsAsync(bool restored)
{
SetupServerMock();
var configuration = new ApplicationConfiguration { ServerConfiguration = new ServerConfiguration() };
using var manager = new DiagnosticsNodeManager(m_serverMock.Object, configuration, NullLogger.Instance);
await manager.CreateAddressSpaceAsync(new Dictionary<NodeId, IList<IReference>>()).ConfigureAwait(false);
NodeId owner = restored ? NodeId.Null : new NodeId("former-owner", 1);
NodeId id = await manager.CreateSubscriptionDiagnosticsAsync(
manager.SystemContext,
new SubscriptionDiagnosticsDataType { SubscriptionId = 123, SessionId = owner },
static (_, _, ref _) => ServiceResult.Good).ConfigureAwait(false);
if (!restored)
{
manager.RelinkSubscriptionDiagnostics(id, owner, NodeId.Null);
}
SubscriptionDiagnosticsState node = manager.FindPredefinedNode<SubscriptionDiagnosticsState>(id);
using var operation = new OperationContext(
new RequestHeader(), null, RequestType.Read, RequestLifetime.None, new UserIdentity());
var context = new ServerSystemContext(m_serverMock.Object, operation);
ArrayOf<RolePermissionType> permissions = default;
node.OnReadUserRolePermissions!(context, node, ref permissions);
Assert.That(permissions, Is.Not.Empty);
foreach (RolePermissionType permission in permissions)
{
Assert.That(permission.Permissions, Is.EqualTo((uint)PermissionType.None));
}
var children = new List<BaseInstanceState>();
node.GetChildren(manager.SystemContext, children);
Assert.That(children, Is.Not.Empty);
foreach (BaseInstanceState child in children)
{
child.OnReadUserRolePermissions!(context, child, ref permissions);
foreach (RolePermissionType permission in permissions)
{
Assert.That(permission.Permissions, Is.EqualTo((uint)PermissionType.None));
}
}
}
[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.BadNotReadable));
}
[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);
}
private static ServerSystemContext CreateDiagnosticsReadContext(
IServerInternal server,
NodeId sessionId)
{
var namespaces = new NamespaceTable();
namespaces.Append(Ua.Namespaces.OpcUa);
var identity = new RoleBasedIdentity(
new UserIdentity("owner", []),
[Role.AuthenticatedUser],
namespaces);
var session = new Mock<ISession>();
session.Setup(value => value.Id).Returns(sessionId);
session.Setup(value => value.EffectiveIdentity).Returns(identity);
var endpoint = new EndpointDescription
{
SecurityMode = MessageSecurityMode.SignAndEncrypt
};
var channelContext = new SecureChannelContext(
"1",
endpoint,
RequestEncoding.Binary,
clientChannelCertificate: null,
serverChannelCertificate: null,
channelThumbprint: null);
var operationContext = new OperationContext(
new RequestHeader(),
channelContext,
RequestType.Read,
RequestLifetime.None,
session.Object);
return new ServerSystemContext(server, operationContext);
}
}
}