test(opcuaclient): HistoryRead Raw/AtTime/Processed wire tests against in-process server (#622)

Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
Joseph Doherty
2026-10-08 16:52:59 -04:00
parent a8b07ae12d
commit dcc8adcf48
3 changed files with 252 additions and 33 deletions
@@ -13,19 +13,17 @@ namespace ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests;
/// fixture" scaffolds in <c>Driver.OpcUaClient.Tests</c> (Gitea #622). No opc-plc and no Docker: every test
/// here runs a real secure-channel + session + subscription exchange over loopback. The driver authenticates
/// with the rig's API-key credential so writes carry the <c>WriteOperate</c> role.
/// <para>
/// HistoryRead is NOT covered here (deferred): the server-side leg needs an <c>IHistorianDataSource</c>
/// stub plus a historized-variable rig seam, and the driver's passthrough shapes are unit-tested already.
/// </para>
/// HistoryRead lives in <see cref="OpcUaClientDriverHistoryWireTests"/>; reconnect/resume in
/// <see cref="OpcUaClientDriverReconnectWireTests"/>.
/// </summary>
[Trait("Category", "Integration")]
public sealed class OpcUaClientDriverWireTests
{
private const string SpeedRef = "Sim/dev1/Speed";
private const string RoRef = "Sim/dev1/Counter";
internal const string SpeedRef = "Sim/dev1/Speed";
internal const string RoRef = "Sim/dev1/Counter";
private const string MissingRef = "Sim/dev1/Missing";
private const string SpeedId = ServerEventRig.RawDeviceFolder + "/Speed";
private const string CounterId = ServerEventRig.RawDeviceFolder + "/Counter";
internal const string SpeedId = ServerEventRig.RawDeviceFolder + "/Speed";
internal const string CounterId = ServerEventRig.RawDeviceFolder + "/Counter";
private sealed class RecordingWriteGateway(bool succeed) : IOpcUaNodeWriteGateway
{
@@ -38,10 +36,12 @@ public sealed class OpcUaClientDriverWireTests
}
}
private static RawTagEntry Tag(string rawPath, string nodeId) =>
internal static RawTagEntry Tag(string rawPath, string nodeId) =>
new(rawPath, JsonSerializer.Serialize(new { nodeId }), WriteIdempotent: true);
private static OpcUaClientDriver NewDriver(ServerEventRig rig, string id, string? browseRoot = null) =>
internal static OpcUaClientDriver NewDriver(
ServerEventRig rig, string id, string? browseRoot = null, IReadOnlyList<RawTagEntry>? extraTags = null,
TimeSpan? reconnectPeriod = null) =>
new(new OpcUaClientDriverOptions
{
EndpointUrl = rig.EndpointUrl,
@@ -50,6 +50,7 @@ public sealed class OpcUaClientDriverWireTests
Tag(SpeedRef, ServerEventRig.RawNsu(SpeedId)),
Tag(RoRef, ServerEventRig.RawNsu(CounterId)),
Tag(MissingRef, ServerEventRig.RawNsu($"{ServerEventRig.RawDeviceFolder}/NoSuchNode")),
.. extraTags ?? [],
],
SecurityPolicy = OpcUaSecurityPolicy.None,
SecurityMode = OpcUaSecurityMode.None,
@@ -62,9 +63,10 @@ public sealed class OpcUaClientDriverWireTests
SessionTimeout = TimeSpan.FromSeconds(30),
KeepAliveInterval = TimeSpan.FromSeconds(1),
BrowseRoot = browseRoot,
ReconnectPeriod = reconnectPeriod ?? TimeSpan.FromSeconds(5),
}, id);
private static async Task<ServerEventRig> StartRigAsync(string name, CancellationToken ct)
internal static async Task<ServerEventRig> StartRigAsync(string name, CancellationToken ct)
{
var rig = await ServerEventRig.StartAsync(name, encrypted: false, ct);
rig.EnsureRawVariable("Speed", "Float", writable: true);
@@ -72,7 +74,7 @@ public sealed class OpcUaClientDriverWireTests
return rig;
}
private static void Publish(ServerEventRig rig, string id, object value) =>
internal static void Publish(ServerEventRig rig, string id, object value) =>
rig.Sink.WriteValue(id, value, OpcUaStatus.Good, DateTime.UtcNow, AddressSpaceRealm.Raw);
/// <summary>Reads round-trip through the real wire; a node the server lacks does not read Good.</summary>