Files
lmxopcua/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests/OpcUaClientDriverFactoryTests.cs
T
Joseph Doherty fccffd0a0f feat(opcuaclient): #676 follow-ups — endpoint rewrite, stall wire test, fast definitive-loss recovery
- Endpoint rewrite: the driver now connects to the configured host AND port (scheme
  match, advertised path kept) instead of the server-advertised URL — fixes NAT/port-map
  upstreams and lets an in-test proxy carry the session (SDK SelectEndpoint rule).
- TcpStallProxy + OpcUaClientDriverStallWireTests: the in-place blip (#670) is now
  wire-tested on loopback — one Bad, frozen read stamp, same session, identical value
  republished Good, a post-recovery change arrives, no source-time regression after
  the SDK's late backlog flush.
- Defect found by that test, fixed: a change published right after a stall rode a
  Publish response the SDK timed out and never republished, leaving the node Good on
  the pre-change value indefinitely. New trailing reconcile read (Timeout +
  KeepAliveInterval after recovery) publishes any Good value newer than the last
  accepted one, through the replay guard's mark.
- IDriverConnectionLossSource (Core.Abstractions): opt-in definitive-loss signal.
  DriverInstanceActor attaches in PreStart / detaches in PostStop and turns a raise
  into a disconnect only while Connected. OpcUaClient raises once per session on
  keep-alive BadSessionIdInvalid/BadSessionClosed/BadSessionNotActivated/
  BadServerHalted (measured: the SDK never re-opens a halted channel) or a dropped
  subscription; ambiguous codes stay with the #527 watchdog.
- Replay guard: one Information summary per recovery when the window closes
  ("dropped N late notifications across M refs"), with a timer so quiet
  subscriptions log it too.
- ReconnectPeriod deleted (options, AdminUI form, docs, tests); stored configs that
  carry reconnectPeriod still parse in the factory, probe and browser.
- Docs: OpcUaClient.md, DriverLifecycle.md, driver-specs.md, CLAUDE.md.
2026-10-11 11:35:52 -04:00

98 lines
4.7 KiB
C#

using Shouldly;
using Microsoft.Extensions.Logging.Abstractions;
using Xunit;
namespace ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests;
/// <summary>
/// Tests for <see cref="OpcUaClientDriverFactoryExtensions"/> — the factory that lets the
/// Server-side <c>DriverFactoryBootstrap</c> materialise a real <see cref="OpcUaClientDriver"/>
/// from a <c>DriverInstance</c> row instead of falling back to a stub.
/// </summary>
public class OpcUaClientDriverFactoryTests
{
private const string SampleConfig =
"""{"EndpointUrl":"opc.tcp://host:4840","SecurityMode":"None","AutoAcceptCertificates":true}""";
/// <summary>Verifies the factory builds a driver carrying the right type + instance identity.</summary>
[Fact]
public void CreateInstance_builds_an_OpcUaClient_driver_with_the_right_identity()
{
var driver = OpcUaClientDriverFactoryExtensions.CreateInstance("drv-1", SampleConfig, NullLoggerFactory.Instance);
driver.DriverType.ShouldBe("OpcUaClient");
driver.DriverInstanceId.ShouldBe("drv-1");
}
/// <summary>Verifies the public driver-type-name constant matches the driver's DriverType.</summary>
[Fact]
public void DriverTypeName_is_OpcUaClient()
=> OpcUaClientDriverFactoryExtensions.DriverTypeName.ShouldBe("OpcUaClient");
/// <summary>Verifies a JSON literal that deserialises to null is rejected with a clear error.</summary>
[Fact]
public void CreateInstance_throws_on_null_json_deserialisation()
=> Should.Throw<System.InvalidOperationException>(
() => OpcUaClientDriverFactoryExtensions.CreateInstance("drv-1", "null", NullLoggerFactory.Instance));
private const string RawTagsConfig =
"""
{
"EndpointUrl": "opc.tcp://host:4840",
"RawTags": [
{ "rawPath": "Plant/Line1/Temp", "tagConfig": "{\"nodeId\":\"ns=2;s=Temperature\"}", "writeIdempotent": false },
{ "rawPath": "Plant/Line1/Speed", "tagConfig": "{\"nodeId\":\"ns=2;s=Speed\"}", "writeIdempotent": true }
]
}
""";
/// <summary>
/// Verifies the v3 authored <c>RawTags</c> (RawPath + TagConfig blob + WriteIdempotent) bind
/// through the DriverConfig deserialiser directly onto <see cref="OpcUaClientDriverOptions"/> —
/// the OpcUaClient driver has no separate DTO, so the factory's direct-into-options path must
/// populate the RawPath resolution list.
/// </summary>
[Fact]
public void CreateInstance_binds_rawtags_from_driverconfig()
{
var driver = OpcUaClientDriverFactoryExtensions.CreateInstance("drv-raw", RawTagsConfig, NullLoggerFactory.Instance);
// The factory succeeds and identity is intact — the deserialiser tolerated + bound the RawTags array.
driver.DriverType.ShouldBe("OpcUaClient");
driver.DriverInstanceId.ShouldBe("drv-raw");
// Assert the actual binding: deserialise the same DriverConfig into public options with the
// factory's serialiser settings (case-insensitive + string enums) and confirm RawTags populated.
var opts = System.Text.Json.JsonSerializer.Deserialize<OpcUaClientDriverOptions>(
RawTagsConfig,
new System.Text.Json.JsonSerializerOptions
{
PropertyNameCaseInsensitive = true,
Converters = { new System.Text.Json.Serialization.JsonStringEnumConverter() },
});
opts.ShouldNotBeNull();
opts!.RawTags.Count.ShouldBe(2);
opts.RawTags[0].RawPath.ShouldBe("Plant/Line1/Temp");
opts.RawTags[0].TagConfig.ShouldContain("ns=2;s=Temperature");
opts.RawTags[0].WriteIdempotent.ShouldBeFalse();
opts.RawTags[1].WriteIdempotent.ShouldBeTrue();
}
/// <summary>
/// #676 deleted the dead <c>ReconnectPeriod</c> option (unused since #670 removed the SDK reconnect handler).
/// DriverConfig rows stored before that still carry it and must keep parsing — and the keys around it must
/// still bind, so the unknown member is skipped rather than aborting the read.
/// </summary>
[Fact]
public void A_stored_config_still_carrying_reconnectPeriod_parses()
{
const string legacy =
"""{"endpointUrl":"opc.tcp://host:4840","reconnectPeriod":"00:00:05","keepAliveInterval":"00:00:07"}""";
var opts = OpcUaClientDriverFactoryExtensions.ParseOptions("drv-legacy", legacy);
opts.EndpointUrl.ShouldBe("opc.tcp://host:4840");
opts.KeepAliveInterval.ShouldBe(TimeSpan.FromSeconds(7));
var driver = OpcUaClientDriverFactoryExtensions.CreateInstance("drv-legacy", legacy, NullLoggerFactory.Instance);
driver.DriverInstanceId.ShouldBe("drv-legacy");
}
}