Auto: opcuaclient-6 — Discovery URL FindServers
Adds optional `DiscoveryUrl` knob to OpcUaClientDriverOptions. When set, the driver runs `DiscoveryClient.CreateAsync` + `FindServersAsync` + `GetEndpointsAsync` against that URL during InitializeAsync and prepends the discovered endpoint URLs (filtered to matching SecurityPolicy + SecurityMode) to the failover candidate list. De-duplicates URLs that appear in both discovered and static lists (case-insensitive). Discovery failures are non-fatal — falls back to statically configured candidates. The doc comment notes that FindServers requires SecurityMode=None on the discovery channel per OPC UA spec, even when the data channel uses Sign or SignAndEncrypt. Closes #278 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -55,6 +55,85 @@ public sealed class OpcUaClientFailoverTests
|
||||
"pre-connect the dashboard should show the first candidate URL so operators can link back");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DiscoveryUrl_defaults_null_so_existing_configs_are_unaffected()
|
||||
{
|
||||
var opts = new OpcUaClientDriverOptions();
|
||||
opts.DiscoveryUrl.ShouldBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResolveEndpointCandidates_prepends_discovered_urls_before_static_candidates()
|
||||
{
|
||||
var opts = new OpcUaClientDriverOptions
|
||||
{
|
||||
EndpointUrls = ["opc.tcp://static1:4840", "opc.tcp://static2:4841"],
|
||||
};
|
||||
var discovered = new[] { "opc.tcp://discovered1:4840", "opc.tcp://discovered2:4841" };
|
||||
var list = OpcUaClientDriver.ResolveEndpointCandidates(opts, discovered);
|
||||
list.Count.ShouldBe(4);
|
||||
list[0].ShouldBe("opc.tcp://discovered1:4840");
|
||||
list[1].ShouldBe("opc.tcp://discovered2:4841");
|
||||
list[2].ShouldBe("opc.tcp://static1:4840");
|
||||
list[3].ShouldBe("opc.tcp://static2:4841");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResolveEndpointCandidates_dedupes_url_appearing_in_both_discovered_and_static()
|
||||
{
|
||||
var opts = new OpcUaClientDriverOptions
|
||||
{
|
||||
EndpointUrls = ["opc.tcp://shared:4840", "opc.tcp://static:4841"],
|
||||
};
|
||||
var discovered = new[] { "opc.tcp://shared:4840", "opc.tcp://only-discovered:4842" };
|
||||
var list = OpcUaClientDriver.ResolveEndpointCandidates(opts, discovered);
|
||||
list.Count.ShouldBe(3);
|
||||
list[0].ShouldBe("opc.tcp://shared:4840");
|
||||
list[1].ShouldBe("opc.tcp://only-discovered:4842");
|
||||
list[2].ShouldBe("opc.tcp://static:4841");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResolveEndpointCandidates_dedup_is_case_insensitive()
|
||||
{
|
||||
// Discovery URLs sometimes return uppercase hostnames; static config typically has
|
||||
// lowercase. The de-dup should treat them as the same URL so the failover sweep
|
||||
// doesn't attempt the same host twice in a row.
|
||||
var opts = new OpcUaClientDriverOptions
|
||||
{
|
||||
EndpointUrls = ["opc.tcp://host:4840"],
|
||||
};
|
||||
var discovered = new[] { "OPC.TCP://HOST:4840" };
|
||||
var list = OpcUaClientDriver.ResolveEndpointCandidates(opts, discovered);
|
||||
list.Count.ShouldBe(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResolveEndpointCandidates_with_only_default_endpoint_is_replaced_by_discovery()
|
||||
{
|
||||
// No EndpointUrls list, default EndpointUrl — the static "candidate" is the default
|
||||
// localhost shortcut. When discovery returns URLs they should still be prepended
|
||||
// (the localhost default isn't worth filtering out specially since it's harmless to
|
||||
// try last and it's still a valid configured fallback).
|
||||
var opts = new OpcUaClientDriverOptions(); // EndpointUrl=opc.tcp://localhost:4840 default
|
||||
var discovered = new[] { "opc.tcp://discovered:4840" };
|
||||
var list = OpcUaClientDriver.ResolveEndpointCandidates(opts, discovered);
|
||||
list[0].ShouldBe("opc.tcp://discovered:4840");
|
||||
list.ShouldContain("opc.tcp://localhost:4840");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResolveEndpointCandidates_no_discovered_falls_back_to_static_behaviour()
|
||||
{
|
||||
var opts = new OpcUaClientDriverOptions
|
||||
{
|
||||
EndpointUrls = ["opc.tcp://only:4840"],
|
||||
};
|
||||
var list = OpcUaClientDriver.ResolveEndpointCandidates(opts, []);
|
||||
list.Count.ShouldBe(1);
|
||||
list[0].ShouldBe("opc.tcp://only:4840");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Initialize_against_all_unreachable_endpoints_throws_AggregateException_listing_each()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user