fix(data-connection): resolve DataConnectionLayer-006..012 — quality-counter reconciliation, per-tag batch reads, configurable failover threshold, dedup retry, stale-callback guard, secure cert default
This commit is contained in:
@@ -37,6 +37,44 @@ public class RealOpcUaClientThreadSafetyTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// DataConnectionLayer-012: secure-by-default certificate handling.
|
||||
/// </summary>
|
||||
public class OpcUaCertificateDefaultTests
|
||||
{
|
||||
[Fact]
|
||||
public void DCL012_OpcUaConnectionOptions_AutoAcceptUntrustedCerts_DefaultsToFalse()
|
||||
{
|
||||
// Regression test for DataConnectionLayer-012. AutoAcceptUntrustedCerts defaulted
|
||||
// to true, accepting every server certificate unconditionally and defeating the
|
||||
// Sign / SignAndEncrypt security modes against an active man-in-the-middle. A
|
||||
// secure-by-default posture rejects untrusted certs unless explicitly opted in.
|
||||
var options = new OpcUaConnectionOptions();
|
||||
Assert.False(options.AutoAcceptUntrustedCerts);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// DataConnectionLayer-009: failover-stability tunables must be configurable.
|
||||
/// </summary>
|
||||
public class DataConnectionOptionsStabilityTests
|
||||
{
|
||||
[Fact]
|
||||
public void DCL009_StableConnectionThreshold_IsConfigurable_WithSixtySecondDefault()
|
||||
{
|
||||
// Regression test for DataConnectionLayer-009. The unstable-disconnect failover
|
||||
// path used a hard-coded 60s StableConnectionThreshold constant inside
|
||||
// DataConnectionActor. It must live on DataConnectionOptions like the other
|
||||
// tunables (ReconnectInterval, TagResolutionRetryInterval, WriteTimeout) so it
|
||||
// is configurable via appsettings.json.
|
||||
var options = new DataConnectionOptions();
|
||||
Assert.Equal(TimeSpan.FromSeconds(60), options.StableConnectionThreshold);
|
||||
|
||||
options.StableConnectionThreshold = TimeSpan.FromSeconds(30);
|
||||
Assert.Equal(TimeSpan.FromSeconds(30), options.StableConnectionThreshold);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// WP-7: Tests for OPC UA adapter.
|
||||
/// </summary>
|
||||
@@ -162,6 +200,36 @@ public class OpcUaDataConnectionTests
|
||||
Assert.All(results.Values, r => Assert.True(r.Success));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DCL007_ReadBatch_ReturnsPerTagResults_WhenOneTagFails()
|
||||
{
|
||||
// Regression test for DataConnectionLayer-007. ReadBatchAsync looped calling
|
||||
// ReadAsync per tag; ReadAsync re-throws any non-cancellation exception, so a
|
||||
// single failing tag aborted the whole batch and the caller got NO results for
|
||||
// the tags that did read successfully — even though ReadResult already carries
|
||||
// a per-tag Success/ErrorMessage shape. After the fix the batch catches per-tag
|
||||
// exceptions and returns a complete map.
|
||||
_mockClient.IsConnected.Returns(true);
|
||||
_mockClient.ReadValueAsync("good1", Arg.Any<CancellationToken>())
|
||||
.Returns((1.0, DateTime.UtcNow, 0u));
|
||||
_mockClient.ReadValueAsync("bad", Arg.Any<CancellationToken>())
|
||||
.Returns<(object?, DateTime, uint)>(_ => throw new InvalidOperationException("node not found"));
|
||||
_mockClient.ReadValueAsync("good2", Arg.Any<CancellationToken>())
|
||||
.Returns((2.0, DateTime.UtcNow, 0u));
|
||||
|
||||
await _adapter.ConnectAsync(new Dictionary<string, string>());
|
||||
|
||||
var results = await _adapter.ReadBatchAsync(["good1", "bad", "good2"]);
|
||||
|
||||
// Every requested tag is present in the result map.
|
||||
Assert.Equal(3, results.Count);
|
||||
Assert.True(results["good1"].Success);
|
||||
Assert.True(results["good2"].Success);
|
||||
// The failing tag is reported as a failed ReadResult, not by aborting the batch.
|
||||
Assert.False(results["bad"].Success);
|
||||
Assert.NotNull(results["bad"].ErrorMessage);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task NotConnected_ThrowsOnOperations()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user