using System.Diagnostics; using System.Net; using System.Net.Sockets; using Microsoft.Extensions.Logging.Abstractions; using ZB.MOM.WW.ScadaBridge.Commons.Types.DataConnections; using ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Adapters; namespace ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Tests.Adapters; /// /// Review 2026-10-21 02/P2: OPC UA endpoint discovery ran SYNCHRONOUSLY on a discovery client /// built with the SDK's default configuration, so neither the connection's /// OperationTimeoutMs nor the verify probe's budget bounded it — a black-holed endpoint held /// the caller for the SDK's default operation timeout instead. /// public class RealOpcUaDiscoveryBoundTests { /// /// A listener that accepts the TCP connection and then never speaks — the protocol-level /// black hole. The verify probe must give up within (about) its own budget. /// [Fact] public async Task Verify_AgainstABlackHoledEndpoint_ReturnsWithinItsBudget() { var listener = new TcpListener(IPAddress.Loopback, 0); listener.Start(); var held = new List(); using var stop = new CancellationTokenSource(); _ = Task.Run(async () => { try { while (!stop.IsCancellationRequested) held.Add(await listener.AcceptTcpClientAsync(stop.Token)); } catch (OperationCanceledException) { } catch (ObjectDisposedException) { } }); try { var port = ((IPEndPoint)listener.LocalEndpoint).Port; var config = new OpcUaEndpointConfig { EndpointUrl = $"opc.tcp://127.0.0.1:{port}", OperationTimeoutMs = 60_000, }; var sw = Stopwatch.StartNew(); var verify = RealOpcUaClient.VerifyEndpointAsync( config, new OpcUaGlobalOptions(), NullLogger.Instance, TimeSpan.FromSeconds(2), CancellationToken.None); var result = await verify.WaitAsync(TimeSpan.FromSeconds(20)); sw.Stop(); Assert.False(result.Success); Assert.True(sw.Elapsed < TimeSpan.FromSeconds(10), $"verify took {sw.Elapsed}"); } finally { stop.Cancel(); listener.Stop(); foreach (var c in held) c.Dispose(); } } /// /// Cancellation of discovery is never mistaken for "discovery unavailable": the unsecured /// fallback endpoint is only for a genuine discovery failure, not for an abandoned attempt. /// [Fact] public async Task CancelledDiscovery_IsNotTreatedAsADiscoveryFailure() { using var cts = new CancellationTokenSource(); cts.Cancel(); await Assert.ThrowsAnyAsync(() => RealOpcUaClient.DiscoverEndpointAsync( "opc.tcp://test:4840", Opc.Ua.MessageSecurityMode.None, async ct => { await Task.Delay(Timeout.Infinite, ct); return new Opc.Ua.EndpointDescriptionCollection(); }, cts.Token)); } }