using System.Net; using System.Net.Sockets; using AxiOmron.PcbCheck.Options; using AxiOmron.PcbCheck.Services.Implementations; using Xunit; namespace AxiOmron.PcbCheck.Tests; /// /// 验证 TCP 双扫码枪服务的连接探活行为。 /// public sealed class TcpDualScannerServiceTests { /// /// 连接测试成功时,日志应包含本机出口和远端地址,便于和部署机脚本结果对照。 /// /// 异步测试任务。 [Fact] public async Task TestConnectionAsync_ShouldLogLocalAndRemoteEndpoint_WhenConnectSucceeds() { using var firstListener = new TcpListener(IPAddress.Loopback, 0); using var secondListener = new TcpListener(IPAddress.Loopback, 0); firstListener.Start(); secondListener.Start(); Task firstAccept = firstListener.AcceptTcpClientAsync(); Task secondAccept = secondListener.AcceptTcpClientAsync(); var logger = new FakeAppLogger(); var service = new TcpDualScannerService( new AppConfig { Scanner = new ScannerOptions { Scanner1 = new TcpScannerEndpointOptions { Name = "扫码枪1", Host = IPAddress.Loopback.ToString(), Port = ((IPEndPoint)firstListener.LocalEndpoint).Port }, Scanner2 = new TcpScannerEndpointOptions { Name = "扫码枪2", Host = IPAddress.Loopback.ToString(), Port = ((IPEndPoint)secondListener.LocalEndpoint).Port }, ConnectTimeoutMs = 5000 } }, logger); bool connected = await service.TestConnectionAsync(CancellationToken.None); using TcpClient firstAcceptedClient = await firstAccept; using TcpClient secondAcceptedClient = await secondAccept; Assert.True(connected); Assert.Contains(logger.InformationEntries, entry => entry.Message.Contains("LocalEndPoint={LocalEndPoint}", StringComparison.Ordinal) && entry.Message.Contains("RemoteEndPoint={RemoteEndPoint}", StringComparison.Ordinal) && entry.Args.Contains("扫码枪1")); Assert.Contains(logger.InformationEntries, entry => entry.Message.Contains("LocalEndPoint={LocalEndPoint}", StringComparison.Ordinal) && entry.Message.Contains("RemoteEndPoint={RemoteEndPoint}", StringComparison.Ordinal) && entry.Args.Contains("扫码枪2")); } /// /// 连接测试失败时,日志应包含实际读取到的目标地址和端口,便于排查部署配置差异。 /// /// 异步测试任务。 [Fact] public async Task TestConnectionAsync_ShouldLogEndpointAddress_WhenConnectFails() { var logger = new FakeAppLogger(); var service = new TcpDualScannerService( new AppConfig { Scanner = new ScannerOptions { Scanner1 = new TcpScannerEndpointOptions { Name = "扫码枪1", Host = "192.0.2.10", Port = 2001 }, Scanner2 = new TcpScannerEndpointOptions { Name = "扫码枪2", Host = "192.0.2.11", Port = 2002 }, ConnectTimeoutMs = 1 } }, logger); await service.TestConnectionAsync(CancellationToken.None); Assert.Contains(logger.WarningEntries, entry => entry.Message.Contains("{Host}:{Port}", StringComparison.Ordinal) && entry.Args.Contains("192.0.2.10") && entry.Args.Contains(2001)); Assert.Contains(logger.WarningEntries, entry => entry.Message.Contains("{Host}:{Port}", StringComparison.Ordinal) && entry.Args.Contains("192.0.2.11") && entry.Args.Contains(2002)); } /// /// 连接超时时,探活应返回离线结果,而不是向启动流程传播取消异常。 /// /// 异步测试任务。 [Fact] public async Task TestConnectionAsync_ShouldReturnFalse_WhenConnectTimesOut() { var service = new TcpDualScannerService( new AppConfig { Scanner = new ScannerOptions { Scanner1 = new TcpScannerEndpointOptions { Name = "扫码枪1", Host = "192.0.2.1", Port = 2001 }, Scanner2 = new TcpScannerEndpointOptions { Name = "扫码枪2", Host = "192.0.2.2", Port = 2001 }, ConnectTimeoutMs = 1 } }, new FakeAppLogger()); bool connected = await service.TestConnectionAsync(CancellationToken.None); Assert.False(connected); } /// /// 调用方主动取消时,探活应继续向上层传播取消,便于应用关闭流程及时收尾。 /// /// 异步测试任务。 [Fact] public async Task TestConnectionAsync_ShouldThrow_WhenCallerCancels() { var service = new TcpDualScannerService( new AppConfig { Scanner = new ScannerOptions { Scanner1 = new TcpScannerEndpointOptions { Name = "扫码枪1", Host = "192.0.2.1", Port = 2001 }, Scanner2 = new TcpScannerEndpointOptions { Name = "扫码枪2", Host = "192.0.2.2", Port = 2001 }, ConnectTimeoutMs = 3000 } }, new FakeAppLogger()); using var cancellationTokenSource = new CancellationTokenSource(); cancellationTokenSource.Cancel(); await Assert.ThrowsAnyAsync( () => service.TestConnectionAsync(cancellationTokenSource.Token)); } private sealed class FakeAppLogger : AxiOmron.PcbCheck.Services.Interfaces.IAppLogger { public List InformationEntries { get; } = new(); public List WarningEntries { get; } = new(); public void LogError(string message, bool showInUi = false, params object?[] args) { } public void LogError(Exception exception, string message, bool showInUi = false, params object?[] args) { } public void LogInformation(string message, bool showInUi = false, params object?[] args) { InformationEntries.Add(new LogEntry(message, args)); } public void LogWarning(string message, bool showInUi = false, params object?[] args) { } public void LogWarning(Exception exception, string message, bool showInUi = false, params object?[] args) { WarningEntries.Add(new LogEntry(message, args)); } } private sealed record LogEntry(string Message, object?[] Args); }