using System.Net.Sockets;
namespace ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.IntegrationTests;
///
/// Reachability probe for an opc-plc simulator (Microsoft Industrial IoT's
/// OPC UA PLC from mcr.microsoft.com/iotedge/opc-plc) or any real OPC UA
/// server the OPCUA_SIM_ENDPOINT env var points at. Parses
/// OPCUA_SIM_ENDPOINT (default opc.tcp://localhost:50000),
/// TCP-connects to the resolved host:port at collection init, and records a
/// on failure. Tests call Assert.Skip on that, so
/// `dotnet test` stays green when Docker isn't running the simulator — mirrors the
/// / Snap7ServerFixture pattern.
///
///
///
/// Why opc-plc over loopback against our own server — (1) independent
/// cert chain + user-token handling catches interop bugs loopback can't;
/// (2) built-in alarm ConditionType + history simulation gives
/// +
/// coverage without a custom
/// driver fake; (3) pinned image tag fixes the test surface in a way our own
/// evolving server wouldn't. Follow-up: add open62541/open62541 as a
/// second image once this lands, for fully-independent-stack interop.
///
///
/// Endpoint URL contract: parser strips the opc.tcp:// scheme + resolves
/// host + port for the liveness probe only. The real test session always
/// dials the full endpoint URL via
/// so cert negotiation + security-policy selection run end-to-end.
///
///
public sealed class OpcPlcFixture : IAsyncDisposable
{
private const string DefaultEndpoint = "opc.tcp://localhost:50000";
private const string EndpointEnvVar = "OPCUA_SIM_ENDPOINT";
/// Full opc.tcp://host:port URL the driver session should connect to.
public string EndpointUrl { get; }
public string Host { get; }
public int Port { get; }
public string? SkipReason { get; }
public OpcPlcFixture()
{
EndpointUrl = Environment.GetEnvironmentVariable(EndpointEnvVar) ?? DefaultEndpoint;
(Host, Port) = ParseHostPort(EndpointUrl);
try
{
using var client = new TcpClient(AddressFamily.InterNetwork);
var task = client.ConnectAsync(
System.Net.Dns.GetHostAddresses(Host)
.FirstOrDefault(a => a.AddressFamily == AddressFamily.InterNetwork)
?? System.Net.IPAddress.Loopback,
Port);
if (!task.Wait(TimeSpan.FromSeconds(2)) || !client.Connected)
{
SkipReason = $"opc-plc simulator at {Host}:{Port} did not accept a TCP connection within 2s. " +
$"Start it (`docker compose -f Docker/docker-compose.yml up`) or override {EndpointEnvVar}.";
}
}
catch (Exception ex)
{
SkipReason = $"opc-plc simulator at {Host}:{Port} unreachable: {ex.GetType().Name}: {ex.Message}. " +
$"Start it (`docker compose -f Docker/docker-compose.yml up`) or override {EndpointEnvVar}.";
}
}
///
/// Parse "opc.tcp://host:port[/path]" → (host, port). Defaults to port 4840
/// (OPC UA standard) when the URL omits the port, but opc-plc's default is
/// 50000 so DefaultEndpoint carries it explicitly.
///
private static (string Host, int Port) ParseHostPort(string endpointUrl)
{
const string scheme = "opc.tcp://";
var body = endpointUrl.StartsWith(scheme, StringComparison.OrdinalIgnoreCase)
? endpointUrl[scheme.Length..]
: endpointUrl;
var slash = body.IndexOf('/');
if (slash >= 0) body = body[..slash];
var colon = body.IndexOf(':');
if (colon < 0) return (body, 4840);
var host = body[..colon];
return int.TryParse(body[(colon + 1)..], out var p) ? (host, p) : (host, 4840);
}
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
}
[Xunit.CollectionDefinition(Name)]
public sealed class OpcPlcCollection : Xunit.ICollectionFixture
{
public const string Name = "OpcPlc";
}