feat(opcuaclient): subscription tuning, OperationLimits chunking, diagnostics counters, revocation options, DiscoveryUrl (#273 #275 #276 #277 #278)
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
+111
@@ -0,0 +1,111 @@
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests;
|
||||
|
||||
/// <summary>#276 — counters are thread-safe and surface through <c>DriverHealth.Diagnostics</c>.</summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class OpcUaClientDiagnosticsTests
|
||||
{
|
||||
[Fact]
|
||||
public void Fresh_snapshot_is_all_zero_with_the_full_key_set()
|
||||
{
|
||||
var snap = new OpcUaClientDiagnostics().Snapshot();
|
||||
|
||||
snap.Keys.ShouldBe(
|
||||
[
|
||||
"PublishResponseCount", "NotificationCount", "DataChangeCount", "BadStatusValueCount",
|
||||
"PublishErrorCount", "KeepAliveMissCount", "ReconnectAttemptCount", "ReconnectSuccessCount",
|
||||
"ReconnectExhaustedCount", "LastReconnectUtcTicks", "ReadCallCount", "ReadFailureCount",
|
||||
"WriteCallCount", "WriteFailureCount", "ChunkedRequestCount",
|
||||
], ignoreOrder: true);
|
||||
snap.Values.ShouldAllBe(v => v == 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Each_recorder_moves_exactly_its_own_counter()
|
||||
{
|
||||
var d = new OpcUaClientDiagnostics();
|
||||
d.RecordPublishResponse();
|
||||
d.RecordNotifications(3);
|
||||
d.RecordDataChange(bad: false);
|
||||
d.RecordDataChange(bad: true);
|
||||
d.RecordPublishError();
|
||||
d.RecordKeepAliveMiss();
|
||||
d.RecordReconnectAttempt();
|
||||
d.RecordReconnectSucceeded();
|
||||
d.RecordReconnectExhausted();
|
||||
d.RecordRead(failed: true);
|
||||
d.RecordWrite(failed: false);
|
||||
d.RecordChunkedRequest();
|
||||
|
||||
var s = d.Snapshot();
|
||||
s["PublishResponseCount"].ShouldBe(1);
|
||||
s["NotificationCount"].ShouldBe(3);
|
||||
s["DataChangeCount"].ShouldBe(2);
|
||||
s["BadStatusValueCount"].ShouldBe(1);
|
||||
s["PublishErrorCount"].ShouldBe(1);
|
||||
s["KeepAliveMissCount"].ShouldBe(1);
|
||||
s["ReconnectAttemptCount"].ShouldBe(1);
|
||||
s["ReconnectSuccessCount"].ShouldBe(1);
|
||||
s["ReconnectExhaustedCount"].ShouldBe(1);
|
||||
s["LastReconnectUtcTicks"].ShouldBeGreaterThan(0);
|
||||
s["ReadCallCount"].ShouldBe(1);
|
||||
s["ReadFailureCount"].ShouldBe(1);
|
||||
s["WriteCallCount"].ShouldBe(1);
|
||||
s["WriteFailureCount"].ShouldBe(0);
|
||||
s["ChunkedRequestCount"].ShouldBe(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Concurrent_increments_do_not_lose_updates()
|
||||
{
|
||||
var d = new OpcUaClientDiagnostics();
|
||||
const int threads = 8, each = 5_000;
|
||||
|
||||
Parallel.For(0, threads, _ =>
|
||||
{
|
||||
for (var i = 0; i < each; i++) { d.RecordDataChange(bad: i % 2 == 0); d.RecordNotifications(2); }
|
||||
});
|
||||
|
||||
var s = d.Snapshot();
|
||||
s["DataChangeCount"].ShouldBe(threads * each);
|
||||
s["BadStatusValueCount"].ShouldBe(threads * each / 2);
|
||||
s["NotificationCount"].ShouldBe(threads * each * 2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Snapshot_is_a_point_in_time_copy()
|
||||
{
|
||||
var d = new OpcUaClientDiagnostics();
|
||||
var before = d.Snapshot();
|
||||
d.RecordKeepAliveMiss();
|
||||
|
||||
before["KeepAliveMissCount"].ShouldBe(0);
|
||||
d.Snapshot()["KeepAliveMissCount"].ShouldBe(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetHealth_carries_the_diagnostics_and_keeps_state_fields()
|
||||
{
|
||||
using var drv = new OpcUaClientDriver(new OpcUaClientDriverOptions(), "opcua-diag");
|
||||
drv.DiagnosticsForTest.RecordKeepAliveMiss();
|
||||
drv.DiagnosticsForTest.RecordKeepAliveMiss();
|
||||
|
||||
var h = drv.GetHealth();
|
||||
|
||||
h.State.ShouldBe(DriverState.Unknown);
|
||||
h.Diagnostics.ShouldNotBeNull();
|
||||
h.Diagnostics!["KeepAliveMissCount"].ShouldBe(2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DriverHealth_without_diagnostics_stays_back_compatible()
|
||||
{
|
||||
var h = new DriverHealth(DriverState.Healthy, null, null);
|
||||
|
||||
h.Diagnostics.ShouldBeNull();
|
||||
h.ShouldBe(new DriverHealth(DriverState.Healthy, null, null));
|
||||
}
|
||||
}
|
||||
+163
@@ -0,0 +1,163 @@
|
||||
using Moq;
|
||||
using Opc.Ua;
|
||||
using Opc.Ua.Client;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests;
|
||||
|
||||
/// <summary>#275 — the driver splits Read / Write calls to the server's declared OperationLimits.</summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class OpcUaClientOperationLimitsTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(0, 0u, new int[0])] // nothing to send => no call
|
||||
[InlineData(5, 0u, new[] { 5 })] // 0 = unlimited
|
||||
[InlineData(5, 5u, new[] { 5 })] // exactly fits
|
||||
[InlineData(5, 100u, new[] { 5 })] // limit larger than the batch
|
||||
[InlineData(10, 4u, new[] { 4, 4, 2 })] // remainder chunk
|
||||
[InlineData(9, 3u, new[] { 3, 3, 3 })]
|
||||
[InlineData(3, 1u, new[] { 1, 1, 1 })]
|
||||
public void Chunk_splits_to_the_limit(int total, uint limit, int[] expectedSizes)
|
||||
{
|
||||
var chunks = OpcUaClientOperationLimits.Chunk(total, limit).ToList();
|
||||
|
||||
chunks.Select(c => c.Count).ToArray().ShouldBe(expectedSizes);
|
||||
// Contiguous cover of [0, total): every item exactly once, in order.
|
||||
var next = 0;
|
||||
foreach (var (start, count) in chunks) { start.ShouldBe(next); next += count; }
|
||||
next.ShouldBe(total);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FromValues_reads_good_uints_and_treats_bad_or_missing_as_unlimited()
|
||||
{
|
||||
var values = new List<DataValue>
|
||||
{
|
||||
new(new Variant(100u)),
|
||||
new(new Variant(50u)),
|
||||
new(StatusCodes.BadNodeIdUnknown), // server doesn't publish MaxNodesPerBrowse
|
||||
new(new Variant(7u)),
|
||||
};
|
||||
|
||||
var limits = OpcUaClientOperationLimits.FromValues(values);
|
||||
|
||||
limits.MaxNodesPerRead.ShouldBe(100u);
|
||||
limits.MaxNodesPerWrite.ShouldBe(50u);
|
||||
limits.MaxNodesPerBrowse.ShouldBe(0u);
|
||||
limits.MaxMonitoredItemsPerCall.ShouldBe(7u);
|
||||
limits.MaxNodesPerHistoryReadData.ShouldBe(0u); // short list => 0
|
||||
limits.MaxNodesPerHistoryReadEvents.ShouldBe(0u);
|
||||
}
|
||||
|
||||
private static (OpcUaClientDriver Drv, Mock<ISession> Session, List<int> ReadSizes, List<int> WriteSizes) Rig(
|
||||
uint readLimit, uint writeLimit, int tagCount)
|
||||
{
|
||||
var tags = Enumerable.Range(0, tagCount)
|
||||
.Select(i => new RawTagEntry($"raw/t{i}", $$"""{"nodeId":"ns=2;s=T{{i}}"}""", WriteIdempotent: false))
|
||||
.ToList();
|
||||
var options = new OpcUaClientDriverOptions { RawTags = tags };
|
||||
var drv = new OpcUaClientDriver(options, "opcua-limits");
|
||||
drv.SetNamespaceMapForTest(ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.NamespaceMap.FromTable(new NamespaceTable(), tags));
|
||||
drv.SetOperationLimitsForTest(OpcUaClientOperationLimits.None with
|
||||
{
|
||||
MaxNodesPerRead = readLimit, MaxNodesPerWrite = writeLimit,
|
||||
});
|
||||
|
||||
var readSizes = new List<int>();
|
||||
var writeSizes = new List<int>();
|
||||
var mock = new Mock<ISession>(MockBehavior.Loose);
|
||||
mock.SetupGet(s => s.MessageContext)
|
||||
.Returns(new ServiceMessageContext(SdkTelemetry.Context));
|
||||
mock.SetupGet(s => s.NamespaceUris).Returns(new NamespaceTable());
|
||||
mock.Setup(s => s.ReadAsync(It.IsAny<RequestHeader>(), It.IsAny<double>(), It.IsAny<TimestampsToReturn>(),
|
||||
It.IsAny<ReadValueIdCollection>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync((RequestHeader _, double _, TimestampsToReturn _, ReadValueIdCollection nodes, CancellationToken _) =>
|
||||
{
|
||||
readSizes.Add(nodes.Count);
|
||||
// Echo each node's identifier number back as the value so ordering across chunks is checkable.
|
||||
var results = new DataValueCollection();
|
||||
foreach (var n in nodes) results.Add(new DataValue(new Variant((string)n.NodeId.Identifier)));
|
||||
return new ReadResponse { ResponseHeader = new ResponseHeader(), Results = results };
|
||||
});
|
||||
mock.Setup(s => s.WriteAsync(It.IsAny<RequestHeader>(), It.IsAny<WriteValueCollection>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync((RequestHeader _, WriteValueCollection nodes, CancellationToken _) =>
|
||||
{
|
||||
writeSizes.Add(nodes.Count);
|
||||
var codes = new StatusCodeCollection();
|
||||
foreach (var _ in nodes) codes.Add(StatusCodes.Good);
|
||||
return new WriteResponse { ResponseHeader = new ResponseHeader(), Results = codes };
|
||||
});
|
||||
drv.SetSessionForTest(mock.Object);
|
||||
return (drv, mock, readSizes, writeSizes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReadAsync_splits_to_MaxNodesPerRead_and_keeps_result_order()
|
||||
{
|
||||
var (drv, _, readSizes, _) = Rig(readLimit: 4, writeLimit: 0, tagCount: 10);
|
||||
using var _ = drv;
|
||||
|
||||
var refs = Enumerable.Range(0, 10).Select(i => $"raw/t{i}").ToList();
|
||||
var res = await drv.ReadAsync(refs, TestContext.Current.CancellationToken);
|
||||
|
||||
readSizes.ShouldBe([4, 4, 2]);
|
||||
res.Count.ShouldBe(10);
|
||||
for (var i = 0; i < 10; i++) res[i].Value.ShouldBe($"T{i}"); // positional mapping survives chunking
|
||||
drv.GetHealth().State.ShouldBe(DriverState.Healthy);
|
||||
drv.GetHealth().Diagnostics!["ChunkedRequestCount"].ShouldBe(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReadAsync_with_no_declared_limit_sends_one_request()
|
||||
{
|
||||
var (drv, _, readSizes, _) = Rig(readLimit: 0, writeLimit: 0, tagCount: 10);
|
||||
using var _ = drv;
|
||||
|
||||
await drv.ReadAsync(Enumerable.Range(0, 10).Select(i => $"raw/t{i}").ToList(), TestContext.Current.CancellationToken);
|
||||
|
||||
readSizes.ShouldBe([10]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReadAsync_a_failing_chunk_only_fails_its_own_tags()
|
||||
{
|
||||
var (drv, session, _, _) = Rig(readLimit: 2, writeLimit: 0, tagCount: 4);
|
||||
using var _ = drv;
|
||||
var call = 0;
|
||||
session.Setup(s => s.ReadAsync(It.IsAny<RequestHeader>(), It.IsAny<double>(), It.IsAny<TimestampsToReturn>(),
|
||||
It.IsAny<ReadValueIdCollection>(), It.IsAny<CancellationToken>()))
|
||||
.Returns((RequestHeader _, double _, TimestampsToReturn _, ReadValueIdCollection nodes, CancellationToken _) =>
|
||||
{
|
||||
if (Interlocked.Increment(ref call) == 2) throw new ServiceResultException(StatusCodes.BadConnectionClosed);
|
||||
var results = new DataValueCollection();
|
||||
foreach (var n in nodes) results.Add(new DataValue(new Variant((string)n.NodeId.Identifier)));
|
||||
return Task.FromResult(new ReadResponse { ResponseHeader = new ResponseHeader(), Results = results });
|
||||
});
|
||||
|
||||
var res = await drv.ReadAsync(["raw/t0", "raw/t1", "raw/t2", "raw/t3"], TestContext.Current.CancellationToken);
|
||||
|
||||
res[0].Value.ShouldBe("T0");
|
||||
res[1].Value.ShouldBe("T1");
|
||||
res[2].StatusCode.ShouldBe(0x80050000u); // BadCommunicationError, chunk 2 only
|
||||
res[3].StatusCode.ShouldBe(0x80050000u);
|
||||
drv.GetHealth().State.ShouldBe(DriverState.Degraded);
|
||||
drv.GetHealth().Diagnostics!["ReadFailureCount"].ShouldBe(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WriteAsync_splits_to_MaxNodesPerWrite()
|
||||
{
|
||||
var (drv, _, _, writeSizes) = Rig(readLimit: 0, writeLimit: 3, tagCount: 7);
|
||||
using var _ = drv;
|
||||
|
||||
var res = await drv.WriteAsync(
|
||||
Enumerable.Range(0, 7).Select(i => new Core.Abstractions.WriteRequest($"raw/t{i}", i)).ToList(),
|
||||
TestContext.Current.CancellationToken);
|
||||
|
||||
writeSizes.ShouldBe([3, 3, 1]);
|
||||
res.Count.ShouldBe(7);
|
||||
res.ShouldAllBe(r => r.StatusCode == 0u);
|
||||
}
|
||||
}
|
||||
+157
@@ -0,0 +1,157 @@
|
||||
using Opc.Ua;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests;
|
||||
|
||||
/// <summary>#273 subscription tuning, #277 revocation options, #278 discovery URL.</summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class OpcUaClientTuningAndDiscoveryTests
|
||||
{
|
||||
// ---- #273 ----
|
||||
|
||||
[Fact]
|
||||
public void Defaults_equal_the_values_that_used_to_be_hardcoded()
|
||||
{
|
||||
var o = new OpcUaClientDriverOptions();
|
||||
|
||||
o.SubscriptionKeepAliveCount.ShouldBe(10u);
|
||||
o.SubscriptionLifetimeCount.ShouldBe(1000u);
|
||||
o.SubscriptionMaxNotificationsPerPublish.ShouldBe(0u);
|
||||
o.SubscriptionPriority.ShouldBe((byte)0);
|
||||
o.SubscriptionPublishingInterval.ShouldBeNull();
|
||||
o.HonorServerOperationLimits.ShouldBeTrue();
|
||||
o.DiscoveryUrl.ShouldBeNull();
|
||||
o.CertificateValidation.RejectUnknownRevocationStatus.ShouldBeFalse();
|
||||
o.CertificateValidation.IssuerStorePath.ShouldBeNull();
|
||||
Should.NotThrow(() => OpcUaClientDriver.ValidateTuning(o));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Factory_json_binds_the_new_keys_and_old_json_keeps_defaults()
|
||||
{
|
||||
var o = OpcUaClientDriverFactoryExtensions.ParseOptions("d", """
|
||||
{"subscriptionPublishingInterval":"00:00:02","subscriptionKeepAliveCount":20,
|
||||
"subscriptionLifetimeCount":600,"subscriptionMaxNotificationsPerPublish":500,
|
||||
"subscriptionPriority":9,"honorServerOperationLimits":false,
|
||||
"discoveryUrl":"opc.tcp://lds:4840",
|
||||
"certificateValidation":{"rejectUnknownRevocationStatus":true,"issuerStorePath":"/pki/issuers"}}
|
||||
""");
|
||||
|
||||
o.SubscriptionPublishingInterval.ShouldBe(TimeSpan.FromSeconds(2));
|
||||
o.SubscriptionKeepAliveCount.ShouldBe(20u);
|
||||
o.SubscriptionLifetimeCount.ShouldBe(600u);
|
||||
o.SubscriptionMaxNotificationsPerPublish.ShouldBe(500u);
|
||||
o.SubscriptionPriority.ShouldBe((byte)9);
|
||||
o.HonorServerOperationLimits.ShouldBeFalse();
|
||||
o.DiscoveryUrl.ShouldBe("opc.tcp://lds:4840");
|
||||
o.CertificateValidation.RejectUnknownRevocationStatus.ShouldBeTrue();
|
||||
o.CertificateValidation.IssuerStorePath.ShouldBe("/pki/issuers");
|
||||
|
||||
var legacy = OpcUaClientDriverFactoryExtensions.ParseOptions("d", """{"endpointUrl":"opc.tcp://x:1"}""");
|
||||
legacy.SubscriptionKeepAliveCount.ShouldBe(10u);
|
||||
legacy.SubscriptionLifetimeCount.ShouldBe(1000u);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0u, 1000u)] // keep-alive must be >= 1
|
||||
[InlineData(10u, 29u)] // lifetime < 3x keep-alive
|
||||
[InlineData(500u, 1000u)]
|
||||
public void ValidateTuning_rejects_out_of_range_counts(uint keepAlive, uint lifetime)
|
||||
{
|
||||
var o = new OpcUaClientDriverOptions { SubscriptionKeepAliveCount = keepAlive, SubscriptionLifetimeCount = lifetime };
|
||||
|
||||
Should.Throw<InvalidOperationException>(() => OpcUaClientDriver.ValidateTuning(o));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidateTuning_accepts_the_3x_boundary_and_rejects_bad_interval_or_discovery_url()
|
||||
{
|
||||
Should.NotThrow(() => OpcUaClientDriver.ValidateTuning(
|
||||
new OpcUaClientDriverOptions { SubscriptionKeepAliveCount = 10, SubscriptionLifetimeCount = 30 }));
|
||||
Should.Throw<InvalidOperationException>(() => OpcUaClientDriver.ValidateTuning(
|
||||
new OpcUaClientDriverOptions { SubscriptionPublishingInterval = TimeSpan.Zero }));
|
||||
Should.Throw<InvalidOperationException>(() => OpcUaClientDriver.ValidateTuning(
|
||||
new OpcUaClientDriverOptions { DiscoveryUrl = "not a uri" }));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(null, 1000, 1000)] // no override: the per-call value
|
||||
[InlineData(null, 10, 50)] // floor still applies to the per-call value
|
||||
[InlineData(2000, 250, 2000)] // override wins over the per-call value
|
||||
[InlineData(5, 250, 50)] // ... and is floored too
|
||||
public void ResolvePublishingIntervalMs_prefers_the_override_and_floors_at_50(int? overrideMs, int requestedMs, int expected)
|
||||
{
|
||||
var o = new OpcUaClientDriverOptions
|
||||
{
|
||||
SubscriptionPublishingInterval = overrideMs is { } ms ? TimeSpan.FromMilliseconds(ms) : null,
|
||||
};
|
||||
|
||||
OpcUaClientDriver.ResolvePublishingIntervalMs(o, TimeSpan.FromMilliseconds(requestedMs)).ShouldBe(expected);
|
||||
}
|
||||
|
||||
// ---- #277 ----
|
||||
|
||||
[Fact]
|
||||
public void Revocation_failures_are_exactly_Revoked_and_IssuerRevoked()
|
||||
{
|
||||
OpcUaClientDriver.IsRevocationFailure(StatusCodes.BadCertificateRevoked).ShouldBeTrue();
|
||||
OpcUaClientDriver.IsRevocationFailure(StatusCodes.BadCertificateIssuerRevoked).ShouldBeTrue();
|
||||
// "unknown" is governed by RejectUnknownRevocationStatus, not auto-accept's hard exception;
|
||||
// untrusted is what auto-accept exists to wave through.
|
||||
OpcUaClientDriver.IsRevocationFailure(StatusCodes.BadCertificateRevocationUnknown).ShouldBeFalse();
|
||||
OpcUaClientDriver.IsRevocationFailure(StatusCodes.BadCertificateUntrusted).ShouldBeFalse();
|
||||
OpcUaClientDriver.IsRevocationFailure(StatusCodes.Good).ShouldBeFalse();
|
||||
}
|
||||
|
||||
// ---- #278 ----
|
||||
|
||||
private static EndpointDescription Ep(string url, string policyUri, MessageSecurityMode mode) =>
|
||||
new() { EndpointUrl = url, SecurityPolicyUri = policyUri, SecurityMode = mode };
|
||||
|
||||
[Fact]
|
||||
public void ResolveEndpointCandidates_without_discovery_is_unchanged()
|
||||
{
|
||||
var o = new OpcUaClientDriverOptions { EndpointUrls = ["opc.tcp://a:1", "opc.tcp://b:2"] };
|
||||
|
||||
OpcUaClientDriver.ResolveEndpointCandidates(o, []).ShouldBe(["opc.tcp://a:1", "opc.tcp://b:2"]);
|
||||
OpcUaClientDriver.ResolveEndpointCandidates(o).ShouldBe(["opc.tcp://a:1", "opc.tcp://b:2"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResolveEndpointCandidates_prepends_discovered_urls_and_dedups_case_insensitively()
|
||||
{
|
||||
var o = new OpcUaClientDriverOptions { EndpointUrls = ["opc.tcp://Static:1", "opc.tcp://found:2"] };
|
||||
|
||||
var merged = OpcUaClientDriver.ResolveEndpointCandidates(o, ["opc.tcp://found:2", "opc.tcp://new:3"]);
|
||||
|
||||
merged.ShouldBe(["opc.tcp://found:2", "opc.tcp://new:3", "opc.tcp://Static:1"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResolveEndpointCandidates_uses_the_single_EndpointUrl_after_discovered_when_no_list()
|
||||
{
|
||||
var o = new OpcUaClientDriverOptions { EndpointUrl = "opc.tcp://only:9" };
|
||||
|
||||
OpcUaClientDriver.ResolveEndpointCandidates(o, ["opc.tcp://d:1"]).ShouldBe(["opc.tcp://d:1", "opc.tcp://only:9"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FilterEndpointUrls_keeps_only_the_wanted_policy_and_mode()
|
||||
{
|
||||
const string b256 = "http://opcfoundation.org/UA/SecurityPolicy#Basic256Sha256";
|
||||
const string none = "http://opcfoundation.org/UA/SecurityPolicy#None";
|
||||
var endpoints = new[]
|
||||
{
|
||||
Ep("opc.tcp://h:1/none", none, MessageSecurityMode.None),
|
||||
Ep("opc.tcp://h:1/sign", b256, MessageSecurityMode.Sign),
|
||||
Ep("opc.tcp://h:1/enc", b256, MessageSecurityMode.SignAndEncrypt),
|
||||
Ep("", b256, MessageSecurityMode.SignAndEncrypt),
|
||||
};
|
||||
|
||||
OpcUaClientDriver.FilterEndpointUrls(endpoints, b256, MessageSecurityMode.SignAndEncrypt)
|
||||
.ShouldBe(["opc.tcp://h:1/enc"]);
|
||||
OpcUaClientDriver.FilterEndpointUrls(endpoints, none, MessageSecurityMode.None)
|
||||
.ShouldBe(["opc.tcp://h:1/none"]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user