mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-21 17:16:16 +08:00
9b034f8291
# 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>
1430 lines
73 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|