using System.Net;
using System.Net.Sockets;
using Grpc.Core;
using Shouldly;
using Xunit;
namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests;
///
/// Unit tests for . The probe does a TCP preflight followed by
/// a lightweight gRPC ping (the read-only GalaxyRepository.TestConnection RPC) against
/// the mxaccessgw, classifying the resulting . An auth rejection
/// ( / )
/// counts as reachable because it proves a live gRPC server answered. The
/// mapping is unit-tested directly here without a
/// live gateway; the happy OK + auth-rejected paths are verified live against the real
/// gateway (10.100.0.48:5120).
///
[Trait("Category", "Unit")]
public sealed class GalaxyDriverProbeTests
{
private readonly GalaxyDriverProbe _probe = new();
// A complete-enough GalaxyDriverOptions JSON skeleton; the {0} placeholder is the
// gateway endpoint. All other nested records carry their required positional members.
private static string ConfigJson(string endpoint, bool useTls = false) =>
$$"""
{
"Gateway": {
"Endpoint": "{{endpoint}}",
"ApiKeySecretRef": "env:GALAXY_MXGW_API_KEY",
"UseTls": {{(useTls ? "true" : "false")}}
},
"MxAccess": { "ClientName": "probe-test" },
"Repository": {},
"Reconnect": {}
}
""";
// ── 1. Invalid JSON ──────────────────────────────────────────────────────────
/// Invalid JSON returns Ok=false with a message containing "invalid".
[Fact]
public async Task InvalidJson_returns_false_with_invalid_message()
{
var result = await _probe.ProbeAsync(
"not-json",
TimeSpan.FromSeconds(2),
TestContext.Current.CancellationToken);
result.Ok.ShouldBeFalse();
result.Message.ShouldNotBeNull();
result.Message!.ShouldContain("invalid", Case.Insensitive);
result.Latency.ShouldBeNull();
}
// ── 2. Config with no endpoint ───────────────────────────────────────────────
///
/// Config JSON whose Gateway.Endpoint is empty returns Ok=false with a message
/// indicating no host/port was found.
///
[Fact]
public async Task NoEndpoint_returns_false_with_no_host_port_message()
{
var result = await _probe.ProbeAsync(
ConfigJson(endpoint: ""),
TimeSpan.FromSeconds(2),
TestContext.Current.CancellationToken);
result.Ok.ShouldBeFalse();
result.Message.ShouldNotBeNull();
result.Message!.ShouldContain("no host", Case.Insensitive);
result.Latency.ShouldBeNull();
}
// ── 3. Unreachable closed port ────────────────────────────────────────────────
///
/// Pointing at a TCP port that is not open returns Ok=false. Depending on how far the
/// probe gets, the message is the TCP "Connect failed", a gRPC "handshake failed", or a
/// "timed out" message — all of which are acceptable failure signals for an unreachable
/// gateway.
///
[Fact]
public async Task ClosedPort_returns_false()
{
// Bind a port, capture it, then release it so nothing is listening when we probe.
var listener = new TcpListener(IPAddress.Loopback, 0);
listener.Start();
var port = ((IPEndPoint)listener.LocalEndpoint).Port;
listener.Stop();
var endpoint = $"http://127.0.0.1:{port}";
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
var result = await _probe.ProbeAsync(
ConfigJson(endpoint), TimeSpan.FromSeconds(3), cts.Token);
result.Ok.ShouldBeFalse();
result.Message.ShouldNotBeNull();
result.Message!.ShouldSatisfyAllConditions(
() => (result.Message.Contains("Connect failed", StringComparison.OrdinalIgnoreCase)
|| result.Message.Contains("handshake failed", StringComparison.OrdinalIgnoreCase)
|| result.Message.Contains("timed out", StringComparison.OrdinalIgnoreCase))
.ShouldBeTrue($"unexpected message: '{result.Message}'"));
}
// ── 4. ClassifyRpc mapping (pure, no live gateway) ───────────────────────────
/// A gRPC OK status maps to a reachable, gateway-confirmed result.
[Fact]
public void ClassifyRpc_Ok_is_reachable()
{
var (ok, message) = GalaxyDriverProbe.ClassifyRpc(StatusCode.OK, "host", 5120);
ok.ShouldBeTrue();
message.ShouldBe("gateway gRPC OK");
}
///
/// A gRPC Unauthenticated status counts as reachable — an auth rejection proves a
/// live gateway gRPC server answered.
///
[Fact]
public void ClassifyRpc_Unauthenticated_is_reachable_auth_not_checked()
{
var (ok, message) = GalaxyDriverProbe.ClassifyRpc(StatusCode.Unauthenticated, "host", 5120);
ok.ShouldBeTrue();
message.ShouldContain("auth not checked", Case.Insensitive);
}
/// A gRPC PermissionDenied status also counts as reachable.
[Fact]
public void ClassifyRpc_PermissionDenied_is_reachable_auth_not_checked()
{
var (ok, message) = GalaxyDriverProbe.ClassifyRpc(StatusCode.PermissionDenied, "host", 5120);
ok.ShouldBeTrue();
message.ShouldContain("auth not checked", Case.Insensitive);
}
///
/// A gRPC Unavailable status is a transport failure — Ok=false with a message that
/// names the host, port and the failing status code.
///
[Fact]
public void ClassifyRpc_Unavailable_is_not_reachable()
{
var (ok, message) = GalaxyDriverProbe.ClassifyRpc(StatusCode.Unavailable, "host", 5120);
ok.ShouldBeFalse();
message.ShouldContain("Unavailable");
message.ShouldContain("host:5120");
}
/// A gRPC DeadlineExceeded status is likewise a handshake failure.
[Fact]
public void ClassifyRpc_DeadlineExceeded_is_not_reachable()
{
var (ok, message) = GalaxyDriverProbe.ClassifyRpc(StatusCode.DeadlineExceeded, "host", 5120);
ok.ShouldBeFalse();
message.ShouldContain("DeadlineExceeded");
message.ShouldContain("host:5120");
}
}