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
};
}
}