Files
UA-.NETStandard/tests/Opc.Ua.PubSub.Server.Tests/PubSubNodeManagerDeterministicTests.cs
2026-07-16 06:25:54 +02:00

886 lines
44 KiB
C#

/* ========================================================================
* Copyright (c) 2005-2026 The OPC Foundation, Inc. All rights reserved.
*
* OPC Foundation MIT License 1.00
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*
* The complete license agreement can be found here:
* http://opcfoundation.org/License/MIT/1.00/
* ======================================================================*/
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Moq;
using NUnit.Framework;
using Opc.Ua.PubSub.Application;
using Opc.Ua.PubSub.Security;
using Opc.Ua.PubSub.Security.Sks;
using Opc.Ua.PubSub.Transports;
using Opc.Ua.Server;
using Opc.Ua.Tests;
namespace Opc.Ua.PubSub.Server.Tests
{
/// <summary>
/// Deterministic, offline coverage for the residual address-space,
/// configuration-file, security-group, key-push-target and per-instance
/// status paths of <see cref="PubSubNodeManager"/> that are not already
/// exercised by <see cref="PubSubNodeManagerTests"/>.
/// </summary>
[TestFixture]
[Category("PubSubNodeManagerDeterministic")]
public class PubSubNodeManagerDeterministicTests
{
[Test]
public async Task CreateAddressSpaceAsync_WithoutDiagnosticsNodeManager_DoesNotBindMethods()
{
using var harness = new Harness(withoutDiagnosticsNodeManager: true);
await harness.Manager.CreateAddressSpaceAsync(
new Dictionary<NodeId, IList<IReference>>()).ConfigureAwait(false);
Assert.Multiple(() =>
{
Assert.That(harness.Manager.AreMethodsBound, Is.False);
Assert.That(harness.Manager.StatusBinding, Is.Null);
Assert.That(harness.EnableMethod.OnCallMethod, Is.Null);
Assert.That(
harness.Manager.FindPredefinedNode<BaseObjectState>(
new NodeId("pubsub:configuration", harness.Manager.AddressSpaceNamespaceIndex)),
Is.Null);
});
}
[Test]
public async Task RebuildConfigurationAddressSpace_WithRichTopology_MaterializesInstanceNodes()
{
Mock<IPubSubApplication> app = CreateRichApplicationMock();
using var harness = new Harness(applicationOverride: app.Object);
await harness.Manager.CreateAddressSpaceAsync(
new Dictionary<NodeId, IList<IReference>>()).ConfigureAwait(false);
ushort ns = harness.Manager.AddressSpaceNamespaceIndex;
BaseObjectState connection = harness.Manager.FindPredefinedNode<BaseObjectState>(
new NodeId("pubsub:connection:Conn1", ns));
BaseObjectState writerGroup = harness.Manager.FindPredefinedNode<BaseObjectState>(
new NodeId("pubsub:writer-group:Conn1:WG1", ns));
BaseObjectState writer = harness.Manager.FindPredefinedNode<BaseObjectState>(
new NodeId("pubsub:writer:Conn1:WG1:W1", ns));
BaseObjectState readerGroup = harness.Manager.FindPredefinedNode<BaseObjectState>(
new NodeId("pubsub:reader-group:Conn1:RG1", ns));
BaseObjectState reader = harness.Manager.FindPredefinedNode<BaseObjectState>(
new NodeId("pubsub:reader:Conn1:RG1:R1", ns));
BaseObjectState publishedDataSet = harness.Manager.FindPredefinedNode<BaseObjectState>(
new NodeId("pubsub:published-data-set:PDS1", ns));
var addWriter = (MethodState)writerGroup.FindChild(
harness.Context, new QualifiedName("AddDataSetWriter", ns))!;
var addReader = (MethodState)readerGroup.FindChild(
harness.Context, new QualifiedName("AddDataSetReader", ns))!;
var addVariables = (MethodState)publishedDataSet.FindChild(
harness.Context, new QualifiedName("AddVariables", ns))!;
var publishedData = (BaseDataVariableState)publishedDataSet.FindChild(
harness.Context, new QualifiedName("PublishedData", ns))!;
Assert.Multiple(() =>
{
Assert.That(connection.TypeDefinitionId, Is.EqualTo(new NodeId(14209u)));
Assert.That(connection.BrowseName.Name, Is.EqualTo("Conn1"));
Assert.That(writerGroup.TypeDefinitionId, Is.EqualTo(new NodeId(17725u)));
Assert.That(writerGroup.BrowseName.Name, Is.EqualTo("WG1"));
Assert.That(writer.TypeDefinitionId, Is.EqualTo(new NodeId(15298u)));
Assert.That(writer.BrowseName.Name, Is.EqualTo("W1"));
Assert.That(readerGroup.TypeDefinitionId, Is.EqualTo(new NodeId(17999u)));
Assert.That(readerGroup.BrowseName.Name, Is.EqualTo("RG1"));
Assert.That(reader.TypeDefinitionId, Is.EqualTo(new NodeId(15306u)));
Assert.That(reader.BrowseName.Name, Is.EqualTo("R1"));
Assert.That(publishedDataSet.TypeDefinitionId, Is.EqualTo(new NodeId(14534u)));
Assert.That(publishedDataSet.BrowseName.Name, Is.EqualTo("PDS1"));
Assert.That(addWriter, Is.Not.Null);
Assert.That(addReader, Is.Not.Null);
Assert.That(addVariables, Is.Not.Null);
Assert.That(publishedData.BrowseName.Name, Is.EqualTo("PublishedData"));
});
}
[Test]
public async Task PubSubConfigurationFileRead_WithInvalidArguments_ReturnsBadInvalidArgument()
{
using var harness = new Harness();
await harness.Manager.CreateAddressSpaceAsync(
new Dictionary<NodeId, IList<IReference>>()).ConfigureAwait(false);
MethodState read = FindConfigurationFileMethod(harness, "Read");
ServiceResult missingArguments = read.OnCallMethod!(
harness.Context, read, BuildArray(Variant.From(1u)), []);
ServiceResult unknownHandle = read.OnCallMethod!(
harness.Context,
read,
BuildArray(Variant.From(4242u), Variant.From(1024)),
[]);
Assert.Multiple(() =>
{
Assert.That(missingArguments.StatusCode.Code, Is.EqualTo(StatusCodes.BadInvalidArgument));
Assert.That(unknownHandle.StatusCode.Code, Is.EqualTo(StatusCodes.BadInvalidArgument));
});
}
[Test]
public async Task PubSubConfigurationFileWrite_WithInvalidArguments_ReturnsExpectedStatus()
{
using var harness = new Harness();
await harness.Manager.CreateAddressSpaceAsync(
new Dictionary<NodeId, IList<IReference>>()).ConfigureAwait(false);
MethodState open = FindConfigurationFileMethod(harness, "Open");
MethodState write = FindConfigurationFileMethod(harness, "Write");
var openOutputs = new List<Variant>();
open.OnCallMethod!(harness.Context, open, BuildArray(Variant.From((byte)1)), openOutputs);
Assert.That(openOutputs[0].TryGetValue(out uint readHandle), Is.True);
ServiceResult missingArguments = write.OnCallMethod!(
harness.Context, write, BuildArray(Variant.From(readHandle)), []);
ServiceResult readOnlyHandle = write.OnCallMethod!(
harness.Context,
write,
BuildArray(Variant.From(readHandle), Variant.From(new ArrayOf<byte>(new byte[] { 1, 2, 3 }))),
[]);
Assert.Multiple(() =>
{
Assert.That(missingArguments.StatusCode.Code, Is.EqualTo(StatusCodes.BadInvalidArgument));
Assert.That(readOnlyHandle.StatusCode.Code, Is.EqualTo(StatusCodes.BadInvalidState));
});
}
[Test]
public async Task PubSubConfigurationFileClose_RemovesHandleAndRejectsMissingArguments()
{
using var harness = new Harness();
await harness.Manager.CreateAddressSpaceAsync(
new Dictionary<NodeId, IList<IReference>>()).ConfigureAwait(false);
MethodState open = FindConfigurationFileMethod(harness, "Open");
MethodState read = FindConfigurationFileMethod(harness, "Read");
MethodState close = FindConfigurationFileMethod(harness, "Close");
var openOutputs = new List<Variant>();
open.OnCallMethod!(harness.Context, open, BuildArray(Variant.From((byte)1)), openOutputs);
Assert.That(openOutputs[0].TryGetValue(out uint handle), Is.True);
ServiceResult closeResult = close.OnCallMethod!(
harness.Context, close, BuildArray(Variant.From(handle)), []);
ServiceResult readAfterClose = read.OnCallMethod!(
harness.Context,
read,
BuildArray(Variant.From(handle), Variant.From(1024)),
[]);
ServiceResult missingArguments = close.OnCallMethod!(
harness.Context, close, BuildArray(), []);
Assert.Multiple(() =>
{
Assert.That(closeResult.StatusCode.Code, Is.EqualTo(StatusCodes.Good));
Assert.That(readAfterClose.StatusCode.Code, Is.EqualTo(StatusCodes.BadInvalidArgument));
Assert.That(missingArguments.StatusCode.Code, Is.EqualTo(StatusCodes.BadInvalidArgument));
});
}
[Test]
public async Task PubSubConfigurationFileCloseAndUpdate_WithInvalidInputs_ReturnsExpectedStatus()
{
using var harness = new Harness();
await harness.Manager.CreateAddressSpaceAsync(
new Dictionary<NodeId, IList<IReference>>()).ConfigureAwait(false);
MethodState open = FindConfigurationFileMethod(harness, "Open");
MethodState write = FindConfigurationFileMethod(harness, "Write");
MethodState closeAndUpdate = FindConfigurationFileMethod(harness, "CloseAndUpdate");
var openOutputs = new List<Variant>();
open.OnCallMethod!(harness.Context, open, BuildArray(Variant.From((byte)2)), openOutputs);
Assert.That(openOutputs[0].TryGetValue(out uint writeHandle), Is.True);
write.OnCallMethod!(
harness.Context,
write,
BuildArray(Variant.From(writeHandle), Variant.From(new ArrayOf<byte>(new byte[] { 0, 1, 2, 3, 4 }))),
[]);
ServiceResult missingArguments = closeAndUpdate.OnCallMethod!(
harness.Context, closeAndUpdate, BuildArray(), []);
ServiceResult unknownHandle = closeAndUpdate.OnCallMethod!(
harness.Context, closeAndUpdate, BuildArray(Variant.From(9999u)), []);
ServiceResult corruptPayload = closeAndUpdate.OnCallMethod!(
harness.Context, closeAndUpdate, BuildArray(Variant.From(writeHandle)), []);
Assert.Multiple(() =>
{
Assert.That(missingArguments.StatusCode.Code, Is.EqualTo(StatusCodes.BadInvalidArgument));
Assert.That(unknownHandle.StatusCode.Code, Is.EqualTo(StatusCodes.BadInvalidArgument));
Assert.That(corruptPayload.StatusCode.Code, Is.EqualTo(StatusCodes.BadConfigurationError));
});
}
[Test]
public async Task PubSubConfigurationReserveIds_WithMissingArguments_ReturnsBadInvalidArgument()
{
using var harness = new Harness();
await harness.Manager.CreateAddressSpaceAsync(
new Dictionary<NodeId, IList<IReference>>()).ConfigureAwait(false);
MethodState reserve = FindConfigurationFileMethod(harness, "ReserveIds");
ServiceResult result = reserve.OnCallMethod!(
harness.Context,
reserve,
BuildArray(Variant.From(Profiles.PubSubUdpUadpTransport), Variant.From((ushort)1)),
[]);
Assert.That(result.StatusCode.Code, Is.EqualTo(StatusCodes.BadInvalidArgument));
}
[Test]
public async Task AddDataSetFolder_WithEmptyOrMissingName_ReturnsBadInvalidArgument()
{
using var harness = new Harness();
await harness.Manager.CreateAddressSpaceAsync(
new Dictionary<NodeId, IList<IReference>>()).ConfigureAwait(false);
MethodState addFolder = harness.AddDataSetFolderMethod;
ServiceResult emptyName = addFolder.OnCallMethod!(
harness.Context, addFolder, BuildArray(Variant.From(string.Empty)), []);
ServiceResult missingArguments = addFolder.OnCallMethod!(
harness.Context, addFolder, BuildArray(), []);
Assert.Multiple(() =>
{
Assert.That(emptyName.StatusCode.Code, Is.EqualTo(StatusCodes.BadInvalidArgument));
Assert.That(missingArguments.StatusCode.Code, Is.EqualTo(StatusCodes.BadInvalidArgument));
});
}
[Test]
public async Task RemoveDataSetFolder_WithInvalidInputs_ReturnsExpectedStatus()
{
using var harness = new Harness();
await harness.Manager.CreateAddressSpaceAsync(
new Dictionary<NodeId, IList<IReference>>()).ConfigureAwait(false);
MethodState removeFolder = harness.RemoveDataSetFolderMethod;
ServiceResult missingArguments = removeFolder.OnCallMethod!(
harness.Context, removeFolder, BuildArray(), []);
ServiceResult nonFolderNodeId = removeFolder.OnCallMethod!(
harness.Context, removeFolder, BuildArray(Variant.From(new NodeId(4321u))), []);
Assert.Multiple(() =>
{
Assert.That(missingArguments.StatusCode.Code, Is.EqualTo(StatusCodes.BadInvalidArgument));
Assert.That(nonFolderNodeId.StatusCode.Code, Is.EqualTo(StatusCodes.BadNodeIdUnknown));
});
}
[Test]
public async Task AddPushTarget_WithMissingOrEmptyArguments_ReturnsBadInvalidArgument()
{
using var harness = new Harness(opt => opt.ExposeSecurityKeyService = true, includeSks: true);
await harness.Manager.CreateAddressSpaceAsync(
new Dictionary<NodeId, IList<IReference>>()).ConfigureAwait(false);
MethodState addPushTarget = harness.AddPushTargetMethod;
ServiceResult missingArguments = addPushTarget.OnCallMethod!(
harness.Context,
addPushTarget,
BuildArray(Variant.From("app"), Variant.From("endpoint")),
[]);
ServiceResult emptyApplicationUri = addPushTarget.OnCallMethod!(
harness.Context,
addPushTarget,
BuildArray(
Variant.From(string.Empty),
Variant.From("endpoint"),
Variant.From(PubSubSecurityPolicyUri.PubSubAes128Ctr),
Variant.From(UserTokenType.Anonymous),
Variant.From((ushort)1),
Variant.From(1000.0)),
[]);
Assert.Multiple(() =>
{
Assert.That(missingArguments.StatusCode.Code, Is.EqualTo(StatusCodes.BadInvalidArgument));
Assert.That(emptyApplicationUri.StatusCode.Code, Is.EqualTo(StatusCodes.BadInvalidArgument));
});
}
[Test]
public async Task RemovePushTarget_WithInvalidInputs_ReturnsExpectedStatus()
{
using var harness = new Harness(opt => opt.ExposeSecurityKeyService = true, includeSks: true);
await harness.Manager.CreateAddressSpaceAsync(
new Dictionary<NodeId, IList<IReference>>()).ConfigureAwait(false);
MethodState removePushTarget = harness.RemovePushTargetMethod;
ServiceResult missingArguments = removePushTarget.OnCallMethod!(
harness.Context, removePushTarget, BuildArray(), []);
ServiceResult unknownTarget = removePushTarget.OnCallMethod!(
harness.Context, removePushTarget, BuildArray(Variant.From(new NodeId(7777u))), []);
Assert.Multiple(() =>
{
Assert.That(missingArguments.StatusCode.Code, Is.EqualTo(StatusCodes.BadInvalidArgument));
Assert.That(unknownTarget.StatusCode.Code, Is.EqualTo(StatusCodes.BadNodeIdUnknown));
});
}
[Test]
public async Task GetSecurityGroup_ForExistingGroup_MaterializesSecurityGroupNode()
{
using var harness = new Harness(opt => opt.ExposeSecurityKeyService = true, includeSks: true);
await harness.Manager.CreateAddressSpaceAsync(
new Dictionary<NodeId, IList<IReference>>()).ConfigureAwait(false);
await harness.SksServer.AddSecurityGroupAsync(new SksSecurityGroup(
"grp-get",
PubSubSecurityPolicyUri.PubSubAes128Ctr,
TimeSpan.FromMinutes(1),
1,
1,
Array.Empty<PubSubSecurityKey>())).ConfigureAwait(false);
MethodState getGroup = harness.GetSecurityGroupMethod;
var outputs = new List<Variant>();
ServiceResult result = getGroup.OnCallMethod!(
harness.Context, getGroup, BuildArray(Variant.From("grp-get")), outputs);
Assert.That(outputs[0].TryGetValue(out NodeId groupNodeId), Is.True);
BaseObjectState groupNode = harness.Manager.FindPredefinedNode<BaseObjectState>(groupNodeId);
Assert.Multiple(() =>
{
Assert.That(result.StatusCode.Code, Is.EqualTo(StatusCodes.Good));
Assert.That(groupNode, Is.Not.Null);
Assert.That(groupNode.TypeDefinitionId, Is.EqualTo(new NodeId(15471u)));
});
}
[Test]
public async Task RemoveSecurityGroup_ForExistingGroup_RemovesNodeAndSksEntry()
{
using var harness = new Harness(opt => opt.ExposeSecurityKeyService = true, includeSks: true);
await harness.Manager.CreateAddressSpaceAsync(
new Dictionary<NodeId, IList<IReference>>()).ConfigureAwait(false);
await harness.SksServer.AddSecurityGroupAsync(new SksSecurityGroup(
"grp-remove",
PubSubSecurityPolicyUri.PubSubAes128Ctr,
TimeSpan.FromMinutes(1),
1,
1,
Array.Empty<PubSubSecurityKey>())).ConfigureAwait(false);
await harness.Manager.RebuildSksAddressSpaceForTestsAsync().ConfigureAwait(false);
NodeId groupNodeId = harness.Manager.MethodHandlers.TryGetSecurityGroupNodeId("grp-remove") ?? NodeId.Null;
MethodState removeGroup = harness.RemoveSecurityGroupMethod;
ServiceResult result = removeGroup.OnCallMethod!(
harness.Context, removeGroup, BuildArray(Variant.From(groupNodeId)), []);
SksSecurityGroup? afterRemoval = await harness.SksServer
.GetSecurityGroupAsync("grp-remove").ConfigureAwait(false);
Assert.Multiple(() =>
{
Assert.That(result.StatusCode.Code, Is.EqualTo(StatusCodes.Good));
Assert.That(harness.Manager.FindPredefinedNode<BaseObjectState>(groupNodeId), Is.Null);
Assert.That(afterRemoval, Is.Null);
});
}
[Test]
public async Task ConnectSecurityGroups_WithUnknownGroupNode_ReturnsElementBadNodeIdUnknown()
{
using var harness = new Harness(opt => opt.ExposeSecurityKeyService = true, includeSks: true);
await harness.Manager.CreateAddressSpaceAsync(
new Dictionary<NodeId, IList<IReference>>()).ConfigureAwait(false);
NodeId targetNodeId = AddPushTarget(harness, "connect-app", "connect-endpoint");
BaseObjectState targetNode = harness.Manager.FindPredefinedNode<BaseObjectState>(targetNodeId);
var connect = (MethodState)targetNode.FindChild(
harness.Context,
new QualifiedName("ConnectSecurityGroups", harness.Manager.AddressSpaceNamespaceIndex))!;
var outputs = new List<Variant>();
ServiceResult result = connect.OnCallMethod!(
harness.Context,
connect,
BuildArray(Variant.From(new ArrayOf<NodeId>(new[] { new NodeId(654321u) }))),
outputs);
Assert.That(outputs[0].TryGetValue(out ArrayOf<StatusCode> results), Is.True);
Assert.Multiple(() =>
{
Assert.That(result.StatusCode.Code, Is.EqualTo(StatusCodes.Good));
Assert.That(results, Has.Count.EqualTo(1));
Assert.That(results[0].Code, Is.EqualTo(StatusCodes.BadNodeIdUnknown));
});
}
[Test]
public async Task DisconnectSecurityGroups_ForConnectedGroup_ReturnsGoodResults()
{
using var harness = new Harness(opt => opt.ExposeSecurityKeyService = true, includeSks: true);
await harness.Manager.CreateAddressSpaceAsync(
new Dictionary<NodeId, IList<IReference>>()).ConfigureAwait(false);
await harness.SksServer.AddSecurityGroupAsync(new SksSecurityGroup(
"grp-disconnect",
PubSubSecurityPolicyUri.PubSubAes128Ctr,
TimeSpan.FromMinutes(1),
1,
1,
Array.Empty<PubSubSecurityKey>())).ConfigureAwait(false);
await harness.Manager.RebuildSksAddressSpaceForTestsAsync().ConfigureAwait(false);
NodeId groupNodeId = harness.Manager.MethodHandlers
.TryGetSecurityGroupNodeId("grp-disconnect") ??
NodeId.Null;
NodeId targetNodeId = AddPushTarget(harness, "disconnect-app", "disconnect-endpoint");
BaseObjectState targetNode = harness.Manager.FindPredefinedNode<BaseObjectState>(targetNodeId);
ushort ns = harness.Manager.AddressSpaceNamespaceIndex;
var connect = (MethodState)targetNode.FindChild(
harness.Context, new QualifiedName("ConnectSecurityGroups", ns))!;
var disconnect = (MethodState)targetNode.FindChild(
harness.Context, new QualifiedName("DisconnectSecurityGroups", ns))!;
ServiceResult connectResult = connect.OnCallMethod!(
harness.Context,
connect,
BuildArray(Variant.From(new ArrayOf<NodeId>(new[] { groupNodeId }))),
[]);
var disconnectOutputs = new List<Variant>();
ServiceResult disconnectResult = disconnect.OnCallMethod!(
harness.Context,
disconnect,
BuildArray(Variant.From(new ArrayOf<NodeId>(new[] { groupNodeId }))),
disconnectOutputs);
Assert.That(disconnectOutputs[0].TryGetValue(out ArrayOf<StatusCode> results), Is.True);
Assert.Multiple(() =>
{
Assert.That(connectResult.StatusCode.Code, Is.EqualTo(StatusCodes.Good));
Assert.That(disconnectResult.StatusCode.Code, Is.EqualTo(StatusCodes.Good));
Assert.That(results, Has.Count.EqualTo(1));
Assert.That(results[0].Code, Is.EqualTo(StatusCodes.Good));
});
}
[Test]
public async Task TriggerKeyUpdate_WithoutConnectedGroups_ReturnsBadInvalidState()
{
var pushProvider = new PushSecurityKeyProvider("push-endpoint", NUnitTelemetryContext.Create());
using var harness = new Harness(
opt => opt.ExposeSecurityKeyService = true, includeSks: true, pushProvider: pushProvider);
await harness.Manager.CreateAddressSpaceAsync(
new Dictionary<NodeId, IList<IReference>>()).ConfigureAwait(false);
NodeId targetNodeId = AddPushTarget(harness, "trigger-app", "push-endpoint");
BaseObjectState targetNode = harness.Manager.FindPredefinedNode<BaseObjectState>(targetNodeId);
var trigger = (MethodState)targetNode.FindChild(
harness.Context,
new QualifiedName("TriggerKeyUpdate", harness.Manager.AddressSpaceNamespaceIndex))!;
ServiceResult result = trigger.OnCallMethod!(harness.Context, trigger, BuildArray(), []);
Assert.That(result.StatusCode.Code, Is.EqualTo(StatusCodes.BadInvalidState));
}
[Test]
public async Task TriggerKeyUpdate_WithoutMatchingPushProvider_ReturnsBadNotFound()
{
using var harness = new Harness(opt => opt.ExposeSecurityKeyService = true, includeSks: true);
await harness.Manager.CreateAddressSpaceAsync(
new Dictionary<NodeId, IList<IReference>>()).ConfigureAwait(false);
NodeId targetNodeId = AddPushTarget(harness, "orphan-app", "orphan-endpoint");
BaseObjectState targetNode = harness.Manager.FindPredefinedNode<BaseObjectState>(targetNodeId);
var trigger = (MethodState)targetNode.FindChild(
harness.Context,
new QualifiedName("TriggerKeyUpdate", harness.Manager.AddressSpaceNamespaceIndex))!;
ServiceResult result = trigger.OnCallMethod!(harness.Context, trigger, BuildArray(), []);
Assert.That(result.StatusCode.Code, Is.EqualTo(StatusCodes.BadNotFound));
}
[Test]
public async Task ConnectionStatusEnable_WhenTransportUnavailable_ReturnsBadInvalidState()
{
using var harness = new Harness();
await harness.Manager.CreateAddressSpaceAsync(
new Dictionary<NodeId, IList<IReference>>()).ConfigureAwait(false);
NodeId connectionId = await harness.Application.AddConnectionAsync(new PubSubConnectionDataType
{
Name = "enable-conn",
TransportProfileUri = Profiles.PubSubUdpUadpTransport,
Address = new ExtensionObject(new NetworkAddressUrlDataType
{
Url = "opc.udp://224.0.0.22:4840"
})
}).ConfigureAwait(false);
MethodState enable = harness.Manager.FindPredefinedNode<MethodState>(
new NodeId(
"pubsub:connection:enable-conn:Status:Enable",
harness.Manager.AddressSpaceNamespaceIndex));
ServiceResult result = enable.OnCallMethod!(harness.Context, enable, BuildArray(), []);
Assert.Multiple(() =>
{
Assert.That(connectionId.IsNull, Is.False);
Assert.That(result.StatusCode.Code, Is.EqualTo(StatusCodes.BadInvalidState));
});
}
[Test]
public async Task ConnectionStatusDisable_ForExistingConnection_ReturnsGood()
{
using var harness = new Harness();
await harness.Manager.CreateAddressSpaceAsync(
new Dictionary<NodeId, IList<IReference>>()).ConfigureAwait(false);
_ = await harness.Application.AddConnectionAsync(new PubSubConnectionDataType
{
Name = "disable-conn",
TransportProfileUri = Profiles.PubSubUdpUadpTransport,
Address = new ExtensionObject(new NetworkAddressUrlDataType
{
Url = "opc.udp://224.0.0.22:4840"
})
}).ConfigureAwait(false);
MethodState disable = harness.Manager.FindPredefinedNode<MethodState>(
new NodeId(
"pubsub:connection:disable-conn:Status:Disable",
harness.Manager.AddressSpaceNamespaceIndex));
ServiceResult result = disable.OnCallMethod!(harness.Context, disable, BuildArray(), []);
Assert.That(result.StatusCode.Code, Is.EqualTo(StatusCodes.Good));
}
[Test]
public async Task ConnectionStatusEnable_ForRemovedComponent_ReturnsBadInvalidState()
{
using var harness = new Harness();
await harness.Manager.CreateAddressSpaceAsync(
new Dictionary<NodeId, IList<IReference>>()).ConfigureAwait(false);
NodeId connectionId = await harness.Application.AddConnectionAsync(new PubSubConnectionDataType
{
Name = "removed-conn",
TransportProfileUri = Profiles.PubSubUdpUadpTransport,
Address = new ExtensionObject(new NetworkAddressUrlDataType
{
Url = "opc.udp://224.0.0.22:4840"
})
}).ConfigureAwait(false);
MethodState enable = harness.Manager.FindPredefinedNode<MethodState>(
new NodeId(
"pubsub:connection:removed-conn:Status:Enable",
harness.Manager.AddressSpaceNamespaceIndex));
await harness.Application.RemoveConnectionAsync(connectionId).ConfigureAwait(false);
ServiceResult result = enable.OnCallMethod!(harness.Context, enable, BuildArray(), []);
Assert.That(result.StatusCode.Code, Is.EqualTo(StatusCodes.BadInvalidState));
}
private static ArrayOf<Variant> BuildArray(params Variant[] values)
{
return new ArrayOf<Variant>(values);
}
private static NodeId AddPushTarget(Harness harness, string applicationUri, string endpointUrl)
{
var outputs = new List<Variant>();
harness.AddPushTargetMethod.OnCallMethod!(
harness.Context,
harness.AddPushTargetMethod,
BuildArray(
Variant.From(applicationUri),
Variant.From(endpointUrl),
Variant.From(PubSubSecurityPolicyUri.PubSubAes128Ctr),
Variant.From(UserTokenType.Anonymous),
Variant.From((ushort)1),
Variant.From(1000.0)),
outputs);
_ = outputs[0].TryGetValue(out NodeId targetNodeId);
return targetNodeId;
}
private static MethodState FindConfigurationFileMethod(Harness harness, string browseName)
{
ushort ns = harness.Manager.AddressSpaceNamespaceIndex;
BaseObjectState fileNode = harness.Manager.FindPredefinedNode<BaseObjectState>(
new NodeId("pubsub:configuration", ns));
return (MethodState)fileNode.FindChild(harness.Context, new QualifiedName(browseName, ns))!;
}
private static Mock<IPubSubApplication> CreateRichApplicationMock()
{
var app = new Mock<IPubSubApplication>();
app.Setup(a => a.GetConfiguration()).Returns(CreateRichConfiguration());
app.Setup(a => a.ConfigurationVersion).Returns(new ConfigurationVersionDataType());
return app;
}
private static PubSubConfigurationDataType CreateRichConfiguration()
{
var writer = new DataSetWriterDataType { Name = "W1" };
var writerGroup = new WriterGroupDataType
{
Name = "WG1",
DataSetWriters = new ArrayOf<DataSetWriterDataType>(new[] { writer })
};
var reader = new DataSetReaderDataType { Name = "R1" };
var readerGroup = new ReaderGroupDataType
{
Name = "RG1",
DataSetReaders = new ArrayOf<DataSetReaderDataType>(new[] { reader })
};
var connection = new PubSubConnectionDataType
{
Name = "Conn1",
WriterGroups = new ArrayOf<WriterGroupDataType>(new[] { writerGroup }),
ReaderGroups = new ArrayOf<ReaderGroupDataType>(new[] { readerGroup })
};
var publishedDataSet = new PublishedDataSetDataType
{
Name = "PDS1",
DataSetSource = new ExtensionObject(new PublishedDataItemsDataType
{
PublishedData = new ArrayOf<PublishedVariableDataType>(
Array.Empty<PublishedVariableDataType>())
})
};
return new PubSubConfigurationDataType
{
Enabled = true,
Connections = new ArrayOf<PubSubConnectionDataType>(new[] { connection }),
PublishedDataSets = new ArrayOf<PublishedDataSetDataType>(new[] { publishedDataSet })
};
}
private sealed class Harness : IDisposable
{
public Harness(
Action<PubSubServerOptions>? configure = null,
bool includeSks = false,
PushSecurityKeyProvider? pushProvider = null,
IPubSubApplication? applicationOverride = null,
bool withoutDiagnosticsNodeManager = false)
{
MockServer = new Mock<IServerInternal>();
NamespaceTable = new NamespaceTable();
NamespaceTable.Append(Namespaces.OpcUa);
MockServer.Setup(s => s.NamespaceUris).Returns(NamespaceTable);
MockServer.Setup(s => s.ServerUris).Returns(new StringTable());
MockServer.Setup(s => s.Factory).Returns(EncodeableFactory.Create());
MockServer.Setup(s => s.TypeTree).Returns(new TypeTable(NamespaceTable));
var mockMaster = new Mock<IMasterNodeManager>();
var mockConfig = new Mock<IConfigurationNodeManager>();
mockMaster.Setup(m => m.ConfigurationNodeManager).Returns(mockConfig.Object);
MockServer.Setup(s => s.NodeManager).Returns(mockMaster.Object);
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
MockServer.Setup(s => s.Telemetry).Returns(telemetry);
m_queueFactory = new MonitoredItemQueueFactory(telemetry);
MockServer.Setup(s => s.MonitoredItemQueueFactory).Returns(m_queueFactory);
Context = new ServerSystemContext(MockServer.Object);
MockServer.Setup(s => s.DefaultSystemContext).Returns(Context);
Configuration = new ApplicationConfiguration
{
ServerConfiguration = new ServerConfiguration
{
MaxNotificationQueueSize = 100,
MaxDurableNotificationQueueSize = 200
}
};
EnableMethod = NewMethod(17407);
DisableMethod = NewMethod(17408);
SetSecurityKeysMethod = NewMethod(17364);
AddConnectionMethod = NewMethod(17366);
RemoveConnectionMethod = NewMethod(17369);
GetSecurityKeysMethod = NewMethod(15215);
GetSecurityGroupMethod = NewMethod(15440);
AddSecurityGroupMethod = NewMethod(15444);
RemoveSecurityGroupMethod = NewMethod(15447);
AddPushTargetMethod = NewMethod(25441);
RemovePushTargetMethod = NewMethod(25444);
AddDataSetFolderMethod = NewMethod(16884);
RemoveDataSetFolderMethod = NewMethod(16923);
StatusVariable = new BaseDataVariableState(null)
{
NodeId = new NodeId(17406u),
BrowseName = new QualifiedName("State")
};
PublishSubscribeObject = new BaseObjectState(null)
{
NodeId = ObjectIds.PublishSubscribe,
BrowseName = new QualifiedName("PublishSubscribe")
};
PublishedDataSetsObject = new BaseObjectState(PublishSubscribeObject)
{
NodeId = new NodeId(14478u),
BrowseName = new QualifiedName("PublishedDataSets")
};
SecurityGroupsObject = new BaseObjectState(PublishSubscribeObject)
{
NodeId = new NodeId(15443u),
BrowseName = new QualifiedName("SecurityGroups")
};
KeyPushTargetsObject = new BaseObjectState(PublishSubscribeObject)
{
NodeId = new NodeId(25440u),
BrowseName = new QualifiedName("KeyPushTargets")
};
var diagnosticsNm = new Mock<IDiagnosticsNodeManager>();
diagnosticsNm.Setup(m => m.FindPredefinedNode<MethodState>(new NodeId(17407u))).Returns(EnableMethod);
diagnosticsNm.Setup(m => m.FindPredefinedNode<MethodState>(new NodeId(17408u))).Returns(DisableMethod);
diagnosticsNm.Setup(m => m.FindPredefinedNode<MethodState>(new NodeId(17364u))).Returns(SetSecurityKeysMethod);
diagnosticsNm.Setup(m => m.FindPredefinedNode<MethodState>(new NodeId(17366u))).Returns(AddConnectionMethod);
diagnosticsNm.Setup(m => m.FindPredefinedNode<MethodState>(new NodeId(17369u))).Returns(RemoveConnectionMethod);
diagnosticsNm.Setup(m => m.FindPredefinedNode<MethodState>(new NodeId(15215u))).Returns(GetSecurityKeysMethod);
diagnosticsNm.Setup(m => m.FindPredefinedNode<MethodState>(new NodeId(15440u))).Returns(GetSecurityGroupMethod);
diagnosticsNm.Setup(m => m.FindPredefinedNode<MethodState>(new NodeId(15444u))).Returns(AddSecurityGroupMethod);
diagnosticsNm.Setup(m => m.FindPredefinedNode<MethodState>(new NodeId(15447u))).Returns(RemoveSecurityGroupMethod);
diagnosticsNm.Setup(m => m.FindPredefinedNode<MethodState>(new NodeId(25441u))).Returns(AddPushTargetMethod);
diagnosticsNm.Setup(m => m.FindPredefinedNode<MethodState>(new NodeId(25444u))).Returns(RemovePushTargetMethod);
diagnosticsNm.Setup(m => m.FindPredefinedNode<MethodState>(new NodeId(16884u))).Returns(AddDataSetFolderMethod);
diagnosticsNm.Setup(m => m.FindPredefinedNode<MethodState>(new NodeId(16923u))).Returns(RemoveDataSetFolderMethod);
diagnosticsNm.Setup(m => m.FindPredefinedNode<BaseVariableState>(new NodeId(17406u))).Returns(StatusVariable);
diagnosticsNm.Setup(m => m.FindPredefinedNode<BaseVariableState>(It.IsAny<NodeId>()))
.Returns((NodeId id) => id == new NodeId(17406u) ? StatusVariable : null!);
diagnosticsNm.Setup(m => m.FindPredefinedNode<BaseObjectState>(ObjectIds.PublishSubscribe))
.Returns(PublishSubscribeObject);
diagnosticsNm.Setup(m => m.FindPredefinedNode<BaseObjectState>(new NodeId(14478u)))
.Returns(PublishedDataSetsObject);
diagnosticsNm.Setup(m => m.FindPredefinedNode<BaseObjectState>(new NodeId(15443u)))
.Returns(SecurityGroupsObject);
diagnosticsNm.Setup(m => m.FindPredefinedNode<BaseObjectState>(new NodeId(25440u)))
.Returns(KeyPushTargetsObject);
if (!withoutDiagnosticsNodeManager)
{
MockServer.Setup(s => s.DiagnosticsNodeManager).Returns(diagnosticsNm.Object);
}
if (applicationOverride is not null)
{
Application = applicationOverride;
m_ownsApplication = false;
}
else
{
Application = new PubSubApplicationBuilder(NUnitTelemetryContext.Create())
.WithApplicationId("test-nodemanager")
.UseConfiguration(new PubSubConfigurationDataType
{
Connections = [],
PublishedDataSets = []
})
.UseAllStandardEncoders()
.AddTransportFactory(new StubTransportFactory())
.Build();
m_ownsApplication = true;
}
SksServer = new InMemoryPubSubKeyServiceServer();
Options = new PubSubServerOptions();
configure?.Invoke(Options);
Manager = new PubSubNodeManager(
MockServer.Object,
Configuration,
Application,
includeSks ? SksServer : null,
Options,
telemetry,
pushKeyProviders: pushProvider is null ? null : [pushProvider]);
}
public Mock<IServerInternal> MockServer { get; }
public NamespaceTable NamespaceTable { get; }
public ApplicationConfiguration Configuration { get; }
public IPubSubApplication Application { get; }
public InMemoryPubSubKeyServiceServer SksServer { get; }
public PubSubServerOptions Options { get; }
public PubSubNodeManager Manager { get; }
public MethodState EnableMethod { get; }
public MethodState DisableMethod { get; }
public MethodState SetSecurityKeysMethod { get; }
public MethodState AddConnectionMethod { get; }
public MethodState RemoveConnectionMethod { get; }
public MethodState GetSecurityKeysMethod { get; }
public MethodState GetSecurityGroupMethod { get; }
public MethodState AddSecurityGroupMethod { get; }
public MethodState RemoveSecurityGroupMethod { get; }
public MethodState AddPushTargetMethod { get; }
public MethodState RemovePushTargetMethod { get; }
public MethodState AddDataSetFolderMethod { get; }
public MethodState RemoveDataSetFolderMethod { get; }
public BaseDataVariableState StatusVariable { get; }
public BaseObjectState PublishSubscribeObject { get; }
public BaseObjectState PublishedDataSetsObject { get; }
public BaseObjectState SecurityGroupsObject { get; }
public BaseObjectState KeyPushTargetsObject { get; }
public ServerSystemContext Context { get; }
public void Dispose()
{
Manager.Dispose();
if (m_ownsApplication)
{
(Application as IDisposable)?.Dispose();
(Application as IAsyncDisposable)?.DisposeAsync().AsTask().GetAwaiter().GetResult();
}
m_queueFactory.Dispose();
}
private static MethodState NewMethod(uint nodeId)
{
return new MethodState(null)
{
NodeId = new NodeId(nodeId),
BrowseName = new QualifiedName("M" + nodeId)
};
}
private readonly bool m_ownsApplication;
private readonly MonitoredItemQueueFactory m_queueFactory;
private sealed class StubTransportFactory : IPubSubTransportFactory
{
public string TransportProfileUri => Profiles.PubSubUdpUadpTransport;
public IPubSubTransport Create(
PubSubConnectionDataType connection,
ITelemetryContext telemetry,
TimeProvider timeProvider)
{
_ = connection;
_ = telemetry;
_ = timeProvider;
throw new NotSupportedException();
}
}
}
}
}