using System.Net.Sockets;
using Microsoft.Extensions.Logging.Abstractions;
using ZB.MOM.WW.OtOpcUa.Runtime.Historian;
namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests.Live;
///
/// Env-gated fixture for the live validation suite that exercises the gateway-backed
/// read / write / alarm paths against a real, running ZB.MOM.WW.HistorianGateway
/// sidecar (typically wonder-sql-vd03 on the corporate VPN). Mirrors the
/// HistorianGateway repo's GatewayIntegrationFixture env-gating convention and this
/// repo's OpcPlcFixture reachability-probe pattern: the fixture is cheap at
/// construction (reads env vars + one short TCP probe) and records a
/// so tests call Assert.Skip(SkipReason) and report as Skipped (not Failed)
/// when the suite is not configured or the gateway is unreachable.
///
///
///
/// Env vars consumed (skip-gate + config source):
///
/// - HISTGW_GATEWAY_ENDPOINT — absolute gateway URI, e.g. https://wonder-sql-vd03:5222. Required; absent ⇒ all tests skip.
/// - HISTGW_GATEWAY_APIKEY — the histgw_<id>_<secret> key (must carry historian:read + historian:write scopes). Required; absent ⇒ all tests skip.
/// - HISTGW_TEST_TAG — an existing Galaxy / historian tag for the read round-trip.
/// - HISTGW_WRITE_SANDBOX_TAG — a Float sandbox tag the write round-trip may EnsureTags + write (e.g. HistGW.LiveTest.Sandbox).
/// - HISTGW_ALARM_SOURCE — a source name for the alarm SendEvent → ReadEvents round-trip.
/// - HISTGW_GATEWAY_ALLOW_UNTRUSTED — true to accept a self-signed dev cert (optional).
///
///
///
/// VPN-gated. wonder-sql-vd03 is reachable only on the VPN. When the endpoint
/// is configured but the host does not accept a TCP connection within
/// , is set to a message that prompts
/// the operator to connect the VPN — the suite skips rather than hangs.
///
///
/// Never connects from the fixture. The gRPC channel is built lazily by the package
/// client, so constructing an adapter performs no network I/O. The fixture's only network
/// touch is the bounded TCP reachability probe.
///
///
public sealed class GatewayLiveFixture
{
private const string EnvEndpoint = "HISTGW_GATEWAY_ENDPOINT";
private const string EnvApiKey = "HISTGW_GATEWAY_APIKEY";
private const string EnvTestTag = "HISTGW_TEST_TAG";
private const string EnvWriteSandboxTag = "HISTGW_WRITE_SANDBOX_TAG";
private const string EnvAlarmSource = "HISTGW_ALARM_SOURCE";
private const string EnvAllowUntrusted = "HISTGW_GATEWAY_ALLOW_UNTRUSTED";
/// Bounded deadline for the TCP reachability probe (keeps an unreachable VPN from hanging the run).
private static readonly TimeSpan ProbeTimeout = TimeSpan.FromSeconds(3);
/// Short per-call deadline so a misconfigured / unreachable gateway fails fast instead of hanging.
private static readonly TimeSpan CallTimeout = TimeSpan.FromSeconds(20);
private readonly string? _endpoint;
private readonly string? _apiKey;
private readonly bool _allowUntrusted;
///
/// Reads the env config and runs one bounded TCP reachability probe. On any gap
/// (missing endpoint/key, malformed URI, or unreachable host) is set
/// and the suite skips cleanly.
///
public GatewayLiveFixture()
{
_endpoint = Trimmed(EnvEndpoint);
_apiKey = Trimmed(EnvApiKey);
TestTag = Trimmed(EnvTestTag);
WriteSandboxTag = Trimmed(EnvWriteSandboxTag);
AlarmSource = Trimmed(EnvAlarmSource);
_allowUntrusted = string.Equals(
Trimmed(EnvAllowUntrusted), "true", StringComparison.OrdinalIgnoreCase);
if (_endpoint is null || _apiKey is null)
{
SkipReason =
$"Skipped: set {EnvEndpoint} (e.g. https://wonder-sql-vd03:5222) and {EnvApiKey} " +
$"(a histgw__ key with historian:read + historian:write scopes) to run the live validation suite.";
return;
}
if (!Uri.TryCreate(_endpoint, UriKind.Absolute, out var uri))
{
SkipReason = $"Skipped: {EnvEndpoint}='{_endpoint}' is not an absolute URI (expected e.g. https://wonder-sql-vd03:5222).";
return;
}
// Bounded TCP probe — a configured-but-unreachable gateway (VPN down) skips, never hangs.
SkipReason = ProbeReachable(uri.Host, uri.Port)
? null
: $"Skipped: gateway {uri.Host}:{uri.Port} (from {EnvEndpoint}) did not accept a TCP connection within " +
$"{ProbeTimeout.TotalSeconds:0}s. It is reachable only on the corporate VPN — connect the VPN (host wonder-sql-vd03) and re-run.";
}
/// Non-null when the suite must skip (unconfigured, malformed endpoint, or unreachable host).
public string? SkipReason { get; }
/// Convenience flag: true when env config is absent / malformed / unreachable.
public bool NotConfigured => SkipReason is not null;
/// The existing Galaxy / historian tag for the read round-trip (HISTGW_TEST_TAG); null when unset.
public string? TestTag { get; }
/// The Float sandbox tag for the write round-trip (HISTGW_WRITE_SANDBOX_TAG); null when unset.
public string? WriteSandboxTag { get; }
/// The source name for the alarm round-trip (HISTGW_ALARM_SOURCE); null when unset.
public string? AlarmSource { get; }
///
/// Builds the bound from env. Only valid when
/// is false (the endpoint + key are non-null by then).
///
public ServerHistorianOptions BuildOptions()
{
var useTls = Uri.TryCreate(_endpoint, UriKind.Absolute, out var uri)
&& string.Equals(uri.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase);
return new ServerHistorianOptions
{
Enabled = true,
Endpoint = _endpoint!,
ApiKey = _apiKey!,
UseTls = useTls,
AllowUntrustedServerCertificate = _allowUntrusted,
CallTimeout = CallTimeout,
};
}
///
/// Creates a fresh real adapter over the package gateway client. Each caller owns and disposes
/// its own adapter (the data source / writers each take exclusive ownership of their client).
///
public HistorianGatewayClientAdapter CreateClient() =>
HistorianGatewayClientAdapter.Create(BuildOptions(), NullLoggerFactory.Instance);
/// Creates a fresh real over its own adapter.
public GatewayHistorianDataSource CreateDataSource() =>
new(CreateClient(), NullLogger.Instance);
private static string? Trimmed(string envVar)
{
var value = Environment.GetEnvironmentVariable(envVar);
return string.IsNullOrWhiteSpace(value) ? null : value.Trim();
}
///
/// Bounded, never-throwing TCP connectivity probe to :.
/// Returns true only on a connection accepted within .
///
private static bool ProbeReachable(string host, int port)
{
try
{
using var client = new TcpClient();
var connect = client.ConnectAsync(host, port);
return connect.Wait(ProbeTimeout) && client.Connected;
}
catch
{
// Timeout, connection refused, DNS failure, … → unreachable (skip, never fail/hang).
return false;
}
}
}