using AVEVA.Historian.Client.Models; namespace AVEVA.Historian.Client.Tests; /// /// Live integration tests for the 2023 R2 RemoteGrpc transport. Gated on a dedicated /// HISTORIAN_GRPC_HOST env var (plus HISTORIAN_TEST_TAG) so they skip cleanly until /// a 2023 R2 Historian is available. Optional: /// HISTORIAN_GRPC_PORT (default 32565), HISTORIAN_GRPC_TLS (true/false), /// HISTORIAN_USER / HISTORIAN_PASSWORD (explicit creds; otherwise IntegratedSecurity), /// HISTORIAN_GRPC_DNSID (server certificate name when connecting by IP over TLS). /// 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() { string? host = Environment.GetEnvironmentVariable("HISTORIAN_GRPC_HOST"); string? testTag = Environment.GetEnvironmentVariable("HISTORIAN_TEST_TAG"); if (string.IsNullOrWhiteSpace(host) || string.IsNullOrWhiteSpace(testTag)) { return; } HistorianClient client = new(BuildOptions(host)); DateTime endUtc = DateTime.UtcNow; DateTime startUtc = endUtc - TimeSpan.FromDays(7); List samples = []; await foreach (HistorianSample sample in client.ReadRawAsync(testTag, startUtc, endUtc, maxValues: 8, CancellationToken.None)) { samples.Add(sample); } Assert.NotEmpty(samples); 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)); } [Fact] public async Task GetTagMetadataAsync_OverGrpc_ReturnsRequestedTag() { string? host = Environment.GetEnvironmentVariable("HISTORIAN_GRPC_HOST"); string? tag = Environment.GetEnvironmentVariable("HISTORIAN_TEST_TAG"); if (string.IsNullOrWhiteSpace(host) || string.IsNullOrWhiteSpace(tag) || string.IsNullOrEmpty(Environment.GetEnvironmentVariable("HISTORIAN_USER"))) { return; } HistorianClient client = new(BuildOptions(host)); HistorianTagMetadata? metadata = await client.GetTagMetadataAsync(tag, CancellationToken.None); Assert.NotNull(metadata); Assert.Equal(tag, metadata!.Name); // A real metadata record decodes to a known data type (descriptor passed MapDataType). Assert.True(Enum.IsDefined(metadata.DataType)); } [Fact] public async Task StartTagQuery_OverGrpc_AcceptsODataFilter() { string? host = Environment.GetEnvironmentVariable("HISTORIAN_GRPC_HOST"); if (string.IsNullOrWhiteSpace(host) || string.IsNullOrEmpty(Environment.GetEnvironmentVariable("HISTORIAN_USER"))) { return; } // R0.1 finding (2026-06-21): on the 2023 R2 gRPC front door the metadata-server pipe IS // reachable (unlike 2020 WCF) and StartActiveTagnamesQuery parses the filter as OData — // startswith/contains/eq succeed; SQL-LIKE "%"/glob "*" fail with "ODataFilter: bad token". // StartTagQuery returns the 8-byte (queryHandle, tagCount) response. The follow-on QueryTag // paging request format is not yet captured (see roadmap R0.1), so browse is not yet wired. HistorianClientOptions options = BuildOptions(host); var result = await Task.Run(() => AVEVA.Historian.Client.Grpc.HistorianGrpcTagClient.ProbeStartTagQuery(options, "startswith(TagName,'Sys')", CancellationToken.None)); Assert.True(result.Success, $"StartTagQuery(OData) should succeed; errLen={result.ErrorLength} " + $"err=\"{System.Text.Encoding.ASCII.GetString(result.Error).Replace('\0', '.')}\""); Assert.Equal(8, result.ResponseLength); // (queryHandle, tagCount) } 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 }; } }