6b892b69ba
Turns the previously-discarded GetInterfaceVersion result into a connect-time version pin. The native buffers carried in the WCF/MDAS body (and in the 2023 R2 gRPC bytes fields) are framed per native interface version; parsing them against an unexpected version risks silent misinterpretation, so we throw rather than best-effort parse. - HistorianServerVersionGate + HistorianServiceInterface: evidence-based supported versions discovered from a live Historian 2020 server (product 20.0.000) via the wcf-probe command — History=11, Retrieval=4, Transaction=2. Status' GetInterfaceVersion returns 0, so Status is reachability-only. - HistorianClientOptions.VerifyServerInterfaceVersion (default true) — bypass knob for bringing up a server whose reported integers aren't yet captured (e.g. a 2023 R2 gRPC endpoint carrying the same proven 2020 buffers). - Wired into both transports' connect paths: WCF history (auth-chain helper) + retrieval (read orchestrator), and gRPC history + retrieval. - Mismatch throws ProtocolEvidenceMissingException naming reported/expected version and the bypass knob. 10 new unit tests (198 total green). Verified the gate does not regress the proven WCF read path: a live read against the local 2020 server reaches past the gate (Retr=4 matches) — the only live failures are a pre-existing environmental read timeout (OperationCanceledException), identical with and without this change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
99 lines
4.4 KiB
C#
99 lines
4.4 KiB
C#
namespace AVEVA.Historian.Client.Tests;
|
|
|
|
/// <summary>
|
|
/// Unit coverage for the R0.6 connect-time interface-version gate. The supported versions
|
|
/// (History=11, Retrieval=4, Transaction=2) are evidence-based, captured from a live AVEVA
|
|
/// Historian 2020 server via the reverse-engineering wcf-probe command.
|
|
/// </summary>
|
|
public sealed class HistorianServerVersionGateTests
|
|
{
|
|
private static HistorianClientOptions Options(bool verify = true) => new()
|
|
{
|
|
Host = "histserver",
|
|
IntegratedSecurity = true,
|
|
VerifyServerInterfaceVersion = verify
|
|
};
|
|
|
|
[Fact]
|
|
public void VerifyServerInterfaceVersion_DefaultsToTrue()
|
|
{
|
|
Assert.True(new HistorianClientOptions { Host = "h" }.VerifyServerInterfaceVersion);
|
|
}
|
|
|
|
[Fact]
|
|
public void Validate_MatchingVersion_DoesNotThrow()
|
|
{
|
|
// Each value-gated service accepts exactly the version this SDK targets.
|
|
HistorianServerVersionGate.Validate(HistorianServiceInterface.History, HistorianServerVersionGate.HistoryInterfaceVersion, Options());
|
|
HistorianServerVersionGate.Validate(HistorianServiceInterface.Retrieval, HistorianServerVersionGate.RetrievalInterfaceVersion, Options());
|
|
HistorianServerVersionGate.Validate(HistorianServiceInterface.Transaction, HistorianServerVersionGate.TransactionInterfaceVersion, Options());
|
|
}
|
|
|
|
[Fact]
|
|
public void Validate_MismatchedVersion_ThrowsProtocolEvidenceMissing()
|
|
{
|
|
// (service, wrong version) cases — one below and one above each expected value.
|
|
(HistorianServiceInterface Service, uint Version)[] cases =
|
|
[
|
|
(HistorianServiceInterface.History, 10u),
|
|
(HistorianServiceInterface.History, 12u),
|
|
(HistorianServiceInterface.Retrieval, 3u),
|
|
(HistorianServiceInterface.Retrieval, 5u),
|
|
(HistorianServiceInterface.Transaction, 1u),
|
|
];
|
|
|
|
foreach ((HistorianServiceInterface service, uint version) in cases)
|
|
{
|
|
ProtocolEvidenceMissingException ex = Assert.Throws<ProtocolEvidenceMissingException>(
|
|
() => HistorianServerVersionGate.Validate(service, version, Options()));
|
|
|
|
// The message must name the reported version, the expected version, and the bypass knob.
|
|
Assert.Contains(version.ToString(System.Globalization.CultureInfo.InvariantCulture), ex.Operation);
|
|
Assert.Contains(HistorianServerVersionGate.ExpectedVersion(service).ToString(System.Globalization.CultureInfo.InvariantCulture), ex.Operation);
|
|
Assert.Contains(nameof(HistorianClientOptions.VerifyServerInterfaceVersion), ex.Operation);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Validate_VerificationDisabled_NeverThrows()
|
|
{
|
|
// A wildly wrong version is tolerated when the operator opts out (e.g. bringing up a
|
|
// 2023 R2 gRPC server whose reported integers have not yet been captured).
|
|
HistorianServerVersionGate.Validate(HistorianServiceInterface.History, 999u, Options(verify: false));
|
|
HistorianServerVersionGate.Validate(HistorianServiceInterface.Retrieval, 0u, Options(verify: false));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(0u)]
|
|
[InlineData(7u)]
|
|
[InlineData(999u)]
|
|
public void Validate_StatusService_IsReachabilityOnly_NeverThrows(uint anyVersion)
|
|
{
|
|
// Status' GetInterfaceVersion returns 0 on the live server; it is not value-gated.
|
|
HistorianServerVersionGate.Validate(HistorianServiceInterface.Status, anyVersion, Options());
|
|
Assert.False(HistorianServerVersionGate.IsValueGated(HistorianServiceInterface.Status));
|
|
}
|
|
|
|
[Fact]
|
|
public void IsValueGated_HistoryRetrievalTransaction_AreGated()
|
|
{
|
|
Assert.True(HistorianServerVersionGate.IsValueGated(HistorianServiceInterface.History));
|
|
Assert.True(HistorianServerVersionGate.IsValueGated(HistorianServiceInterface.Retrieval));
|
|
Assert.True(HistorianServerVersionGate.IsValueGated(HistorianServiceInterface.Transaction));
|
|
}
|
|
|
|
[Fact]
|
|
public void ExpectedVersion_Status_Throws()
|
|
{
|
|
Assert.Throws<ArgumentOutOfRangeException>(
|
|
() => HistorianServerVersionGate.ExpectedVersion(HistorianServiceInterface.Status));
|
|
}
|
|
|
|
[Fact]
|
|
public void Validate_NullOptions_Throws()
|
|
{
|
|
Assert.Throws<ArgumentNullException>(
|
|
() => HistorianServerVersionGate.Validate(HistorianServiceInterface.History, 11u, null!));
|
|
}
|
|
}
|