using AVEVA.Historian.Client.Grpc; using GrpcHistory = ArchestrA.Grpc.Contract.History; using GrpcRetrieval = ArchestrA.Grpc.Contract.Retrieval; using GrpcStatus = ArchestrA.Grpc.Contract.Status; using GrpcTransaction = ArchestrA.Grpc.Contract.Transaction; using Xunit.Abstractions; namespace AVEVA.Historian.Client.Tests; /// /// Live evidence test (C3a): reads the four unauthenticated GetInterfaceVersion RPCs from a /// real 2023 R2 Historian over gRPC and asserts the accepted version set. These RPCs run before any /// credential exchange, so no HISTORIAN_USER / HISTORIAN_PASSWORD is required — only a reachable host. /// /// Skips silently when HISTORIAN_GRPC_HOST is absent (offline / CI). The captured integers /// are recorded in docs/reverse-engineering/grpc-interface-versions.md and close the C3a /// "2023 R2 gRPC server-version integers not yet captured" gap. /// public sealed class GrpcInterfaceVersionEvidenceTests { private readonly ITestOutputHelper _output; public GrpcInterfaceVersionEvidenceTests(ITestOutputHelper output) => _output = output; [Fact] public void GrpcInterfaceVersions_LiveServer_MatchAcceptedSet() { string host = Environment.GetEnvironmentVariable("HISTORIAN_GRPC_HOST") ?? ""; if (string.IsNullOrWhiteSpace(host)) { _output.WriteLine("SKIP: HISTORIAN_GRPC_HOST is not set — no live historian available."); return; } HistorianClientOptions options = BuildOptions(host); using HistorianGrpcConnection connection = HistorianGrpcChannelFactory.Create(options); DateTime deadline = DateTime.UtcNow.Add(TimeSpan.FromSeconds(10)); GrpcHistory.GetInterfaceVersionResponse history = new GrpcHistory.HistoryService.HistoryServiceClient(connection.Channel) .GetInterfaceVersion(new GrpcHistory.GetInterfaceVersionRequest(), connection.Metadata, deadline, default); GrpcRetrieval.GetRetrievalInterfaceVersionResponse retrieval = new GrpcRetrieval.RetrievalService.RetrievalServiceClient(connection.Channel) .GetRetrievalInterfaceVersion(new GrpcRetrieval.GetRetrievalInterfaceVersionRequest(), connection.Metadata, deadline, default); GrpcStatus.GetStatusInterfaceVersionResponse status = new GrpcStatus.StatusService.StatusServiceClient(connection.Channel) .GetStatusInterfaceVersion(new GrpcStatus.GetStatusInterfaceVersionRequest(), connection.Metadata, deadline, default); GrpcTransaction.GetTransactionInterfaceVersionResponse transaction = new GrpcTransaction.TransactionService.TransactionServiceClient(connection.Channel) .GetTransactionInterfaceVersion(new GrpcTransaction.GetTransactionInterfaceVersionRequest(), connection.Metadata, deadline, default); _output.WriteLine($"History UiVersion={history.UiVersion} UiError={history.UiError}"); _output.WriteLine($"Retrieval UiVersion={retrieval.UiVersion} UiError={retrieval.UiError}"); _output.WriteLine($"Status UiVersion={status.UiVersion} UiError={status.UiError}"); // Note: Transaction response fields are named Error/Version (not UiError/UiVersion) per the proto. _output.WriteLine($"Transaction Version={transaction.Version} Error={transaction.Error}"); // History: accepted set is {11 (2020 WCF), 12 (2023 R2 gRPC)}. Assert.Equal(0u, history.UiError); Assert.Contains(history.UiVersion, new uint[] { 11u, 12u }); // Retrieval: 4 on both 2020 WCF and 2023 R2 gRPC. Assert.Equal(0u, retrieval.UiError); Assert.Equal(4u, retrieval.UiVersion); // Transaction: 2 (confirmed by live gRPC capture — see grpc-interface-versions.md). // NOTE: the Transaction response proto uses Error/Version (not UiError/UiVersion). Assert.Equal(0u, transaction.Error); Assert.Equal(2u, transaction.Version); // Status: reachability-only — assert UiError==0 only; UiVersion is not value-gated // (observed 4 on 2023 R2 gRPC, 0 on 2020 WCF). Assert.Equal(0u, status.UiError); } private static HistorianClientOptions BuildOptions(string host) { string? user = Environment.GetEnvironmentVariable("HISTORIAN_USER"); string? password = Environment.GetEnvironmentVariable("HISTORIAN_PASSWORD"); bool explicitCreds = !string.IsNullOrEmpty(user); int port = int.TryParse(Environment.GetEnvironmentVariable("HISTORIAN_GRPC_PORT"), out int parsed) ? parsed : HistorianClientOptions.DefaultGrpcPort; bool tls = string.Equals(Environment.GetEnvironmentVariable("HISTORIAN_GRPC_TLS"), "true", StringComparison.OrdinalIgnoreCase); return new HistorianClientOptions { Host = host, Port = port, Transport = HistorianTransport.RemoteGrpc, GrpcUseTls = tls, AllowUntrustedServerCertificate = tls, ServerDnsIdentity = Environment.GetEnvironmentVariable("HISTORIAN_GRPC_DNSID"), IntegratedSecurity = !explicitCreds, UserName = user ?? string.Empty, Password = password ?? string.Empty, }; } }