gRPC M0: probe (R0.4, live-verified) + system-param (R0.3) + shared handshake
Roadmap docs/plans/hcal-roadmap.md, milestone M0 (gRPC parity for the DONE surface). Now unblocked for live verification by a reachable 2023 R2 server. - R0.4 Probe over gRPC: new HistorianGrpcProbe calls History/Retrieval/Status GetInterfaceVersion (unauthenticated). ProbeAsync routes over gRPC when Transport==RemoteGrpc. LIVE-VERIFIED against a real 2023 R2 server — needs no credentials (runs before the auth loop), so it works despite the auth blocker. - R0.3 System parameter over gRPC: new HistorianGrpcStatusClient calls StatusService.GetSystemParameter over the authenticated session; routed in the dialect. Built + unit-tested (request/response field mapping pinned). Live-verification pending an auth fix (see below). - Extracted the proven auth handshake from HistorianGrpcReadOrchestrator into shared Grpc/HistorianGrpcHandshake (reused by read + status + future browse/metadata). Repointed the IL structural guardrail test to it. - Diagnostics: round-failure now decodes the native server error + hex/ASCII preview (HistorianNativeHandshake.DescribeError). This surfaced the live auth blocker as SEC_E_LOGON_DENIED (0x8009030C) at NTLM round 1 — framing is correct, the credential did not validate. Probable cause: stale file password or NAM-domain NTLM restriction (Kerberos/RDP works, NTLM denied; no SPN path over the tunnel). 216 unit tests pass; live gRPC probe passes. Sanitization scan clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B6mcaT2PjRFKcogzp9UkfC
This commit is contained in:
@@ -11,7 +11,7 @@ namespace AVEVA.Historian.Client.Tests;
|
||||
/// credentials (live-confirmed against a real 2023 R2 server, 2026-06-21). An earlier revision
|
||||
/// routed the loop to ExchangeKey; this test fails if that regression returns.
|
||||
///
|
||||
/// It works by disassembling the IL of <c>HistorianGrpcReadOrchestrator</c> (and its
|
||||
/// It works by disassembling the IL of <c>HistorianGrpcHandshake</c> (and its
|
||||
/// compiler-generated nested closure types — the token-loop call lives inside a lambda) and
|
||||
/// collecting every method invoked.
|
||||
/// </summary>
|
||||
@@ -20,8 +20,10 @@ public sealed class HistorianGrpcHandshakeRoutingTests
|
||||
[Fact]
|
||||
public void Handshake_UsesValidateClientCredential_NotExchangeKey()
|
||||
{
|
||||
// The auth token loop lives in the shared handshake helper (reused by the read, status,
|
||||
// and future browse/metadata gRPC paths).
|
||||
HashSet<string> calledMethods = CollectCalledMethodNames(
|
||||
"AVEVA.Historian.Client.Grpc.HistorianGrpcReadOrchestrator");
|
||||
"AVEVA.Historian.Client.Grpc.HistorianGrpcHandshake");
|
||||
|
||||
Assert.Contains("ValidateClientCredential", calledMethods);
|
||||
Assert.DoesNotContain("ExchangeKey", calledMethods);
|
||||
|
||||
@@ -12,6 +12,21 @@ namespace AVEVA.Historian.Client.Tests;
|
||||
/// </summary>
|
||||
public sealed class HistorianGrpcIntegrationTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task ProbeAsync_OverGrpc_ReturnsTrue()
|
||||
{
|
||||
string? host = Environment.GetEnvironmentVariable("HISTORIAN_GRPC_HOST");
|
||||
if (string.IsNullOrWhiteSpace(host))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// ProbeAsync calls the unauthenticated GetInterfaceVersion RPCs, so it succeeds even when
|
||||
// credentials are unavailable — no HISTORIAN_USER/PASSWORD required.
|
||||
HistorianClient client = new(BuildOptions(host));
|
||||
Assert.True(await client.ProbeAsync(CancellationToken.None));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReadRawAsync_OverGrpc_ReturnsAtLeastOneRow()
|
||||
{
|
||||
@@ -37,6 +52,20 @@ public sealed class HistorianGrpcIntegrationTests
|
||||
Assert.All(samples, s => Assert.Equal(testTag, s.TagName));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetSystemParameterAsync_OverGrpc_ReturnsValue()
|
||||
{
|
||||
string? host = Environment.GetEnvironmentVariable("HISTORIAN_GRPC_HOST");
|
||||
if (string.IsNullOrWhiteSpace(host) || string.IsNullOrEmpty(Environment.GetEnvironmentVariable("HISTORIAN_USER")))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
HistorianClient client = new(BuildOptions(host));
|
||||
string? version = await client.GetSystemParameterAsync("HistorianVersion", CancellationToken.None);
|
||||
Assert.False(string.IsNullOrWhiteSpace(version));
|
||||
}
|
||||
|
||||
private static HistorianClientOptions BuildOptions(string host)
|
||||
{
|
||||
string? user = Environment.GetEnvironmentVariable("HISTORIAN_USER");
|
||||
|
||||
@@ -100,6 +100,41 @@ public sealed class HistorianGrpcTransportTests
|
||||
Assert.Equal((uint)HistorianDataQueryProtocol.QueryRequestTypeData, decoded.UiQueryRequestType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InterfaceVersionResponses_ExposeErrorAndVersion_AsProbeExpects()
|
||||
{
|
||||
// R0.4 ProbeAsync reads uiError/uiVersion off each service's GetInterfaceVersion response.
|
||||
// Pin that field mapping (success = uiError 0 + uiVersion > 0) via a protobuf round-trip.
|
||||
var history = GrpcHistory.GetInterfaceVersionResponse.Parser.ParseFrom(
|
||||
new GrpcHistory.GetInterfaceVersionResponse { UiError = 0, UiVersion = 12 }.ToByteArray());
|
||||
var retrieval = GetRetrievalInterfaceVersionResponse.Parser.ParseFrom(
|
||||
new GetRetrievalInterfaceVersionResponse { UiError = 0, UiVersion = 4 }.ToByteArray());
|
||||
|
||||
Assert.Equal(0u, history.UiError);
|
||||
Assert.Equal(12u, history.UiVersion);
|
||||
Assert.Equal(0u, retrieval.UiError);
|
||||
Assert.Equal(4u, retrieval.UiVersion);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetSystemParameterMessages_CarryHandleNameAndValue_AsStatusClientExpects()
|
||||
{
|
||||
// R0.3 sends {uiHandle, strParameterName} and reads strParameterValue when status succeeds.
|
||||
var request = ArchestrA.Grpc.Contract.Status.GetSystemParameterRequest.Parser.ParseFrom(
|
||||
new ArchestrA.Grpc.Contract.Status.GetSystemParameterRequest { UiHandle = 9, StrParameterName = "HistorianVersion" }.ToByteArray());
|
||||
Assert.Equal(9u, request.UiHandle);
|
||||
Assert.Equal("HistorianVersion", request.StrParameterName);
|
||||
|
||||
var response = ArchestrA.Grpc.Contract.Status.GetSystemParameterResponse.Parser.ParseFrom(
|
||||
new ArchestrA.Grpc.Contract.Status.GetSystemParameterResponse
|
||||
{
|
||||
Status = new ArchestrA.Grpc.Contract.RequestStatus.Status { BSuccess = true },
|
||||
StrParameterValue = "20.0.000"
|
||||
}.ToByteArray());
|
||||
Assert.True(response.Status.BSuccess);
|
||||
Assert.Equal("20.0.000", response.StrParameterValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OpenConnectionRequest_CarriesNativeOpen2BufferUnchanged()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user