using AVEVA.Historian.Client.Grpc; using AVEVA.Historian.Client.Models; using Xunit.Abstractions; namespace AVEVA.Historian.Client.Tests; /// /// Live end-to-end round-trip for (the v8 EVENT sibling of /// ): open ONE reusable event session, SendEvent on it /// TWICE (no re-handshake/re-register between sends), ping once, dispose. Env-gated exactly like /// (silent early-return skip without HISTORIAN_GRPC_HOST + /// HISTORIAN_USER + HISTORIAN_PASSWORD + HISTORIAN_EVENT_SANDBOX_TAG). Every send targets the sandbox /// identity ONLY (carried as the event SourceName/Type), never a production tag. /// public sealed class HistorianEventSessionRoundTripTests { private readonly ITestOutputHelper _output; public HistorianEventSessionRoundTripTests(ITestOutputHelper output) => _output = output; [Fact] public async Task EventSession_SendTwicePing_AllOnOneSession() { string? host = Environment.GetEnvironmentVariable("HISTORIAN_GRPC_HOST"); string? sandboxTag = Environment.GetEnvironmentVariable("HISTORIAN_EVENT_SANDBOX_TAG"); if (string.IsNullOrWhiteSpace(host) || string.IsNullOrWhiteSpace(sandboxTag) || string.IsNullOrEmpty(Environment.GetEnvironmentVariable("HISTORIAN_USER")) || string.IsNullOrEmpty(Environment.GetEnvironmentVariable("HISTORIAN_PASSWORD"))) { return; // skip — env not configured } HistorianClientOptions options = BuildOptions(host); await using var client = new HistorianClient(options); await using HistorianEventSession session = await client.OpenEventSessionAsync(CancellationToken.None); // 1) send TWICE on the SAME (open + CM_EVENT-registered) session — the 2nd skips ECDH+register. for (int i = 0; i < 2; i++) { bool sent = await session.SendEventAsync(BuildSandboxEvent(sandboxTag, attempt: i), CancellationToken.None); _output.WriteLine($"{i + 1}) reused-send[{i}] -> ok={sent}"); Assert.True(sent, $"reused v8 Event session send[{i}] should be accepted (AddStreamValues BSuccess)."); } // 2) ping on the SAME session — must not throw. await session.PingAsync(CancellationToken.None); _output.WriteLine("3) ping -> ok"); // 3) prove the keepalive op actually RETURNS DATA on the v8 Event handle (not just no-throw): // issue the same underlying GetSystemParameter the ping uses, directly against the event // session's connection + ClientHandle, and assert it yields a non-empty value. string? keepalive = HistorianGrpcStatusClient.GetSystemParameterOnSession( session.Connection, session.Session.ClientHandle, options, "HistorianVersion", CancellationToken.None); Assert.False(string.IsNullOrEmpty(keepalive)); _output.WriteLine($"4) keepalive GetSystemParameter on event handle -> '{keepalive}'"); _output.WriteLine("event-session round-trip OK (two sends + ping + verified keepalive on one session)"); } // Build a send event that targets the sandbox identity ONLY (mirrors EventSessionReuseSpikeTests. // BuildSandboxEvent): the CM_EVENT send buffer carries no per-tag routing field, so the sandbox tag // NAME is stamped into SourceName + Type + a marker Property so the appended event is unambiguously // attributable to the sandbox — never a production tag. A fresh Id/timestamps per attempt. private static HistorianEvent BuildSandboxEvent(string sandboxTag, int attempt) { DateTime now = DateTime.UtcNow; return new HistorianEvent( Id: Guid.NewGuid(), EventTimeUtc: now.AddSeconds(-attempt), ReceivedTimeUtc: now, Type: sandboxTag, SourceName: sandboxTag, Namespace: "HistGW.EventSessionRoundTrip", RevisionVersion: 0, Properties: new Dictionary { ["RoundTripAttempt"] = attempt.ToString(System.Globalization.CultureInfo.InvariantCulture), ["RoundTripMarker"] = "B1-event-session-roundtrip", }); } // verbatim copy of BuildOptions from HistorianSessionRoundTripTests / EventSessionReuseSpikeTests 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); TimeSpan timeout = int.TryParse(Environment.GetEnvironmentVariable("HISTORIAN_GRPC_TIMEOUT"), out int secs) && secs > 0 ? TimeSpan.FromSeconds(secs) : new HistorianClientOptions { Host = host }.RequestTimeout; 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, RequestTimeout = timeout, Compression = true }; } }