164 lines
6.9 KiB
C#
164 lines
6.9 KiB
C#
using System.Net;
|
|
using System.Net.Sockets;
|
|
using Grpc.Core;
|
|
using Shouldly;
|
|
using Xunit;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests;
|
|
|
|
/// <summary>
|
|
/// Unit tests for <see cref="GalaxyDriverProbe"/>. The probe does a TCP preflight followed by
|
|
/// a lightweight gRPC ping (the read-only <c>GalaxyRepository.TestConnection</c> RPC) against
|
|
/// the mxaccessgw, classifying the resulting <see cref="StatusCode"/>. An auth rejection
|
|
/// (<see cref="StatusCode.Unauthenticated"/> / <see cref="StatusCode.PermissionDenied"/>)
|
|
/// counts as reachable because it proves a live gRPC server answered. The
|
|
/// <see cref="GalaxyDriverProbe.ClassifyRpc"/> mapping is unit-tested directly here without a
|
|
/// live gateway; the happy <c>OK</c> + auth-rejected paths are verified live against the real
|
|
/// gateway (10.100.0.48:5120).
|
|
/// </summary>
|
|
[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 ──────────────────────────────────────────────────────────
|
|
|
|
/// <summary>Invalid JSON returns Ok=false with a message containing "invalid".</summary>
|
|
[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 ───────────────────────────────────────────────
|
|
|
|
/// <summary>
|
|
/// Config JSON whose Gateway.Endpoint is empty returns Ok=false with a message
|
|
/// indicating no host/port was found.
|
|
/// </summary>
|
|
[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 ────────────────────────────────────────────────
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
[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) ───────────────────────────
|
|
|
|
/// <summary>A gRPC <c>OK</c> status maps to a reachable, gateway-confirmed result.</summary>
|
|
[Fact]
|
|
public void ClassifyRpc_Ok_is_reachable()
|
|
{
|
|
var (ok, message) = GalaxyDriverProbe.ClassifyRpc(StatusCode.OK, "host", 5120);
|
|
ok.ShouldBeTrue();
|
|
message.ShouldBe("gateway gRPC OK");
|
|
}
|
|
|
|
/// <summary>
|
|
/// A gRPC <c>Unauthenticated</c> status counts as reachable — an auth rejection proves a
|
|
/// live gateway gRPC server answered.
|
|
/// </summary>
|
|
[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);
|
|
}
|
|
|
|
/// <summary>A gRPC <c>PermissionDenied</c> status also counts as reachable.</summary>
|
|
[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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// A gRPC <c>Unavailable</c> status is a transport failure — Ok=false with a message that
|
|
/// names the host, port and the failing status code.
|
|
/// </summary>
|
|
[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");
|
|
}
|
|
|
|
/// <summary>A gRPC <c>DeadlineExceeded</c> status is likewise a handshake failure.</summary>
|
|
[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");
|
|
}
|
|
}
|