using AVEVA.Historian.Client.Grpc; using AVEVA.Historian.Client.Models; using AVEVA.Historian.Client.Wcf; using Google.Protobuf; using ArchestrA.Grpc.Contract.Retrieval; using GrpcHistory = ArchestrA.Grpc.Contract.History; namespace AVEVA.Historian.Client.Tests; /// /// Unit coverage for the 2023 R2 RemoteGrpc transport — the parts that do not require a live /// server: channel address/port resolution, metadata, transport routing, and the invariant that /// gRPC request messages carry the same native byte buffers the WCF path uses. /// public sealed class HistorianGrpcTransportTests { private static HistorianClientOptions Options( string host = "histserver", int port = HistorianClientOptions.DefaultPort, bool tls = false, string? dnsIdentity = null, bool compression = false) => new() { Host = host, Port = port, Transport = HistorianTransport.RemoteGrpc, GrpcUseTls = tls, ServerDnsIdentity = dnsIdentity, Compression = compression, IntegratedSecurity = true }; [Fact] public void ResolvePort_DefaultWcfPort_SubstitutesGrpcDefault() { Assert.Equal(HistorianClientOptions.DefaultGrpcPort, HistorianGrpcChannelFactory.ResolvePort(Options(port: HistorianClientOptions.DefaultPort))); } [Fact] public void ResolvePort_ExplicitPort_IsHonoured() { Assert.Equal(443, HistorianGrpcChannelFactory.ResolvePort(Options(port: 443))); } [Fact] public void ResolveAddress_Plaintext_UsesHttpAndHost() { Assert.Equal("http://histserver:32565", HistorianGrpcChannelFactory.ResolveAddress(Options())); } [Fact] public void ResolveAddress_Tls_UsesHttpsAndHostWhenNoDnsIdentity() { Assert.Equal("https://histserver:32565", HistorianGrpcChannelFactory.ResolveAddress(Options(tls: true))); } [Fact] public void ResolveAddress_Tls_PrefersDnsIdentityForCertMatch() { string address = HistorianGrpcChannelFactory.ResolveAddress(Options(host: "10.0.0.5", tls: true, dnsIdentity: "localhost")); Assert.Equal("https://localhost:32565", address); } [Fact] public void Create_CompressionDisabled_EmitsNoEncodingHeader() { using HistorianGrpcConnection connection = HistorianGrpcChannelFactory.Create(Options(compression: false)); Assert.DoesNotContain(connection.Metadata, e => e.Key == "grpc-internal-encoding-request"); } [Fact] public void Create_CompressionEnabled_AdvertisesGzipRequestEncoding() { using HistorianGrpcConnection connection = HistorianGrpcChannelFactory.Create(Options(compression: true)); global::Grpc.Core.Metadata.Entry entry = Assert.Single(connection.Metadata, e => e.Key == "grpc-internal-encoding-request"); Assert.Equal("gzip", entry.Value); } [Fact] public void StartQueryRequest_CarriesNativeDataQueryBufferUnchanged() { // The gRPC envelope must wrap the exact bytes the WCF StartQuery2 path sends, so the // already-reverse-engineered DataQueryRequest serializer is reused verbatim. HistorianDataQueryRequest request = HistorianWcfReadOrchestrator.BuildDataQueryRequest( "Tag.Counter", new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc), new DateTime(2026, 1, 2, 0, 0, 0, DateTimeKind.Utc), 100); byte[] nativeBuffer = HistorianDataQueryProtocol.SerializeFullHistoryRequest(request); var message = new StartQueryRequest { UiHandle = 7, UiQueryRequestType = HistorianDataQueryProtocol.QueryRequestTypeData, BtRequestBuffer = ByteString.CopyFrom(nativeBuffer) }; // Round-trip through protobuf and confirm the native buffer survives byte-for-byte. byte[] wire = message.ToByteArray(); var decoded = StartQueryRequest.Parser.ParseFrom(wire); Assert.Equal(nativeBuffer, decoded.BtRequestBuffer.ToByteArray()); Assert.Equal(7u, decoded.UiHandle); 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 BuildTagNamesBuffer_EncodesCountThenLengthPrefixedUtf16Names() { // R0.2 request framing: uint count + per-name(uint charCount + UTF-16LE). Golden bytes. byte[] buffer = AVEVA.Historian.Client.Grpc.HistorianGrpcTagClient.BuildTagNamesBuffer(["AB", "C"]); byte[] expected = [ 0x02, 0x00, 0x00, 0x00, // count = 2 0x02, 0x00, 0x00, 0x00, // "AB" char count = 2 0x41, 0x00, 0x42, 0x00, // 'A','B' UTF-16LE 0x01, 0x00, 0x00, 0x00, // "C" char count = 1 0x43, 0x00 // 'C' UTF-16LE ]; Assert.Equal(expected, buffer); } [Fact] public void OpenConnectionRequest_CarriesNativeOpen2BufferUnchanged() { byte[] open2 = HistorianNativeHandshake.BuildOpenConnection3Request( "histserver", Guid.NewGuid(), HistorianWcfAuthChainHelper.NativeIntegratedReadOnlyConnectionMode); var message = new GrpcHistory.OpenConnectionRequest { BtConnectionRequest = ByteString.CopyFrom(open2) }; var decoded = GrpcHistory.OpenConnectionRequest.Parser.ParseFrom(message.ToByteArray()); Assert.Equal(open2, decoded.BtConnectionRequest.ToByteArray()); } }