195 lines
7.6 KiB
C#
195 lines
7.6 KiB
C#
using System.Net;
|
|
using System.Net.Sockets;
|
|
using AxiOmron.PcbCheck.Options;
|
|
using AxiOmron.PcbCheck.Services.Implementations;
|
|
using Xunit;
|
|
|
|
namespace AxiOmron.PcbCheck.Tests;
|
|
|
|
/// <summary>
|
|
/// 验证 TCP 双扫码枪服务的连接探活行为。
|
|
/// </summary>
|
|
public sealed class TcpDualScannerServiceTests
|
|
{
|
|
/// <summary>
|
|
/// 连接测试成功时,日志应包含本机出口和远端地址,便于和部署机脚本结果对照。
|
|
/// </summary>
|
|
/// <returns>异步测试任务。</returns>
|
|
[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<TcpClient> firstAccept = firstListener.AcceptTcpClientAsync();
|
|
Task<TcpClient> secondAccept = secondListener.AcceptTcpClientAsync();
|
|
var logger = new FakeAppLogger<TcpDualScannerService>();
|
|
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"));
|
|
}
|
|
|
|
/// <summary>
|
|
/// 连接测试失败时,日志应包含实际读取到的目标地址和端口,便于排查部署配置差异。
|
|
/// </summary>
|
|
/// <returns>异步测试任务。</returns>
|
|
[Fact]
|
|
public async Task TestConnectionAsync_ShouldLogEndpointAddress_WhenConnectFails()
|
|
{
|
|
var logger = new FakeAppLogger<TcpDualScannerService>();
|
|
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));
|
|
}
|
|
|
|
/// <summary>
|
|
/// 连接超时时,探活应返回离线结果,而不是向启动流程传播取消异常。
|
|
/// </summary>
|
|
/// <returns>异步测试任务。</returns>
|
|
[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<TcpDualScannerService>());
|
|
|
|
bool connected = await service.TestConnectionAsync(CancellationToken.None);
|
|
|
|
Assert.False(connected);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 调用方主动取消时,探活应继续向上层传播取消,便于应用关闭流程及时收尾。
|
|
/// </summary>
|
|
/// <returns>异步测试任务。</returns>
|
|
[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<TcpDualScannerService>());
|
|
|
|
using var cancellationTokenSource = new CancellationTokenSource();
|
|
cancellationTokenSource.Cancel();
|
|
|
|
await Assert.ThrowsAnyAsync<OperationCanceledException>(
|
|
() => service.TestConnectionAsync(cancellationTokenSource.Token));
|
|
}
|
|
|
|
private sealed class FakeAppLogger<TCategoryName> : AxiOmron.PcbCheck.Services.Interfaces.IAppLogger<TCategoryName>
|
|
{
|
|
public List<LogEntry> InformationEntries { get; } = new();
|
|
|
|
public List<LogEntry> 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);
|
|
}
|