using System.Net; using System.Net.Sockets; using System.Text; using MrGameEng.Net; using Xunit; namespace MrGameEng.Net.Tests; public class HeartbeatTests { [Fact] public async Task HealthyClient_SurvivesPastIdleTimeout() { var port = FreePort(); using var server = new WebSocketServer( port, heartbeatInterval: TimeSpan.FromMilliseconds(100), idleTimeout: TimeSpan.FromMilliseconds(400) ); server.Start(); using var client = await WebSocketClient.ConnectAsync( new Uri($"ws://localhost:{port}/"), new CancellationTokenSource(TimeSpan.FromSeconds(10)).Token ); var connection = await WaitFor( () => server.TryAcceptConnection(out var c) ? c : null, "server accept" ); // Дольше idleTimeout: живой клиент авто-отвечает pong на server-ping и остаётся открыт. await Task.Delay(900, TestContext.Current.CancellationToken); Assert.True(connection.IsOpen); Assert.True(client.IsOpen); } [Fact] public async Task SilentPeer_IsDroppedAfterIdleTimeout() { var port = FreePort(); using var server = new WebSocketServer( port, heartbeatInterval: TimeSpan.FromMilliseconds(100), idleTimeout: TimeSpan.FromMilliseconds(400) ); server.Start(); // Сырой peer: проходит рукопожатие, но дальше молчит и не отвечает на ping. using var tcp = new TcpClient(); await tcp.ConnectAsync(IPAddress.Loopback, port, TestContext.Current.CancellationToken); var request = "GET / HTTP/1.1\r\n" + "Host: localhost\r\n" + "Upgrade: websocket\r\n" + "Connection: Upgrade\r\n" + "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n" + "Sec-WebSocket-Version: 13\r\n" + "\r\n"; var bytes = Encoding.ASCII.GetBytes(request); await tcp.GetStream().WriteAsync(bytes, TestContext.Current.CancellationToken); var connection = await WaitFor( () => server.TryAcceptConnection(out var c) ? c : null, "server accept" ); Assert.True(connection.IsOpen); // Peer не отвечает pong'ом → активность не обновляется → сервер закрывает по простою. await WaitFor(() => connection.IsOpen ? null : "closed", "idle drop"); Assert.False(connection.IsOpen); } private static async Task WaitFor(Func poll, string what) where T : class { for (var i = 0; i < 200; i++) { if (poll() is { } result) { return result; } await Task.Delay(25); } throw new TimeoutException($"Timed out waiting for {what}."); } private static int FreePort() { var listener = new TcpListener(IPAddress.Loopback, 0); listener.Start(); var port = ((IPEndPoint)listener.LocalEndpoint).Port; listener.Stop(); return port; } }