fccffd0a0f
- 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.
96 lines
4.5 KiB
C#
96 lines
4.5 KiB
C#
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests;
|
|
|
|
/// <summary>
|
|
/// Scaffold-level tests for <see cref="OpcUaClientDriver"/> that don't require a live
|
|
/// remote OPC UA server. The IReadable/IWritable/ITagDiscovery/ISubscribable wire-level
|
|
/// tests live in <c>OpcUaServer.IntegrationTests/OpcUaClientDriverWireTests</c>.
|
|
/// </summary>
|
|
[Trait("Category", "Unit")]
|
|
public sealed class OpcUaClientDriverScaffoldTests
|
|
{
|
|
/// <summary>Verifies that default options target the standard OPC UA port with anonymous auth.</summary>
|
|
[Fact]
|
|
public void Default_options_target_standard_opcua_port_and_anonymous_auth()
|
|
{
|
|
var opts = new OpcUaClientDriverOptions();
|
|
opts.EndpointUrl.ShouldBe("opc.tcp://localhost:4840", "4840 is the IANA-assigned OPC UA port");
|
|
opts.SecurityMode.ShouldBe(OpcUaSecurityMode.None);
|
|
opts.SecurityPolicy.ShouldBe(OpcUaSecurityPolicy.None);
|
|
opts.AuthType.ShouldBe(OpcUaAuthType.Anonymous);
|
|
opts.AutoAcceptCertificates.ShouldBeFalse("production default must reject untrusted server certs");
|
|
}
|
|
|
|
/// <summary>Verifies that default timeouts match driver specification section 8.</summary>
|
|
[Fact]
|
|
public void Default_timeouts_match_driver_specs_section_8()
|
|
{
|
|
var opts = new OpcUaClientDriverOptions();
|
|
opts.SessionTimeout.ShouldBe(TimeSpan.FromSeconds(120));
|
|
opts.KeepAliveInterval.ShouldBe(TimeSpan.FromSeconds(5));
|
|
}
|
|
|
|
/// <summary>Verifies that driver reports type and ID before connecting.</summary>
|
|
[Fact]
|
|
public void Driver_reports_type_and_id_before_connect()
|
|
{
|
|
using var drv = new OpcUaClientDriver(new OpcUaClientDriverOptions(), "opcua-test");
|
|
drv.DriverType.ShouldBe("OpcUaClient");
|
|
drv.DriverInstanceId.ShouldBe("opcua-test");
|
|
drv.GetHealth().State.ShouldBe(DriverState.Unknown);
|
|
}
|
|
|
|
/// <summary>Verifies that Initialize against unreachable endpoint transitions to Faulted and throws.</summary>
|
|
[Fact]
|
|
public async Task Initialize_against_unreachable_endpoint_transitions_to_Faulted_and_throws()
|
|
{
|
|
// RFC 5737 reserved-for-documentation IP; won't route anywhere. Pick opc.tcp:// so
|
|
// endpoint selection hits the transport-layer connection rather than a DNS lookup.
|
|
var opts = new OpcUaClientDriverOptions
|
|
{
|
|
// Port 1 on loopback is effectively guaranteed to be closed — the OS responds
|
|
// with TCP RST immediately instead of hanging on connect, which keeps the
|
|
// unreachable-host tests snappy. Don't use an RFC 5737 reserved IP; those get
|
|
// routed to a black-hole + time out only after the SDK's internal retry/backoff
|
|
// fully elapses (~60s even with Options.Timeout=500ms).
|
|
EndpointUrl = "opc.tcp://127.0.0.1:1",
|
|
Timeout = TimeSpan.FromMilliseconds(500),
|
|
AutoAcceptCertificates = true, // dev-mode to bypass cert validation in the test
|
|
};
|
|
using var drv = new OpcUaClientDriver(opts, "opcua-unreach");
|
|
|
|
await Should.ThrowAsync<Exception>(async () =>
|
|
await drv.InitializeAsync("{}", TestContext.Current.CancellationToken));
|
|
|
|
var health = drv.GetHealth();
|
|
health.State.ShouldBe(DriverState.Faulted);
|
|
health.LastError.ShouldNotBeNull();
|
|
}
|
|
|
|
/// <summary>Verifies that Reinitialize against unreachable endpoint re-throws the error.</summary>
|
|
[Fact]
|
|
public async Task Reinitialize_against_unreachable_endpoint_re_throws()
|
|
{
|
|
var opts = new OpcUaClientDriverOptions
|
|
{
|
|
// Port 1 on loopback is effectively guaranteed to be closed — the OS responds
|
|
// with TCP RST immediately instead of hanging on connect, which keeps the
|
|
// unreachable-host tests snappy. Don't use an RFC 5737 reserved IP; those get
|
|
// routed to a black-hole + time out only after the SDK's internal retry/backoff
|
|
// fully elapses (~60s even with Options.Timeout=500ms).
|
|
EndpointUrl = "opc.tcp://127.0.0.1:1",
|
|
Timeout = TimeSpan.FromMilliseconds(500),
|
|
AutoAcceptCertificates = true,
|
|
};
|
|
using var drv = new OpcUaClientDriver(opts, "opcua-reinit");
|
|
|
|
await Should.ThrowAsync<Exception>(async () =>
|
|
await drv.InitializeAsync("{}", TestContext.Current.CancellationToken));
|
|
await Should.ThrowAsync<Exception>(async () =>
|
|
await drv.ReinitializeAsync("{}", TestContext.Current.CancellationToken));
|
|
}
|
|
}
|