using System.Net.Http.Json; using System.Text.Json; using Microsoft.AspNetCore.Mvc.Testing; using Vanguard.Server.Api; using Vanguard.Shared.Protocol; using Xunit.Sdk; namespace Vanguard.Server.Tests; public class GameServerE2ETests { [Fact] public async Task Two_Clients_Play_A_Full_Match_Over_WebSocket() { using var factory = NewFactory(); using var http = factory.CreateClient(); var deck = ServerTestData.BuildDeck(); var firstQueue = await EnqueueAsync(http, "p1", deck); var secondQueue = await EnqueueAsync(http, "p2", deck); var firstStatus = await WaitForMatchAsync(http, firstQueue.QueueId, "p1"); var secondStatus = await WaitForMatchAsync(http, secondQueue.QueueId, "p2"); Assert.Equal(firstStatus.MatchId, secondStatus.MatchId); Assert.Equal(0, firstStatus.Seat); Assert.Equal(1, secondStatus.Seat); using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(120)); await using var first = await TestGameClient.ConnectAsync( factory, firstStatus.MatchId!, firstStatus.Ticket!, "p1", 0, cts.Token); await using var second = await TestGameClient.ConnectAsync( factory, secondStatus.MatchId!, secondStatus.Ticket!, "p2", 0, cts.Token); await Task.WhenAll( first.PlayRandomlyAsync(seed: 11, cts.Token), second.PlayRandomlyAsync(seed: 22, cts.Token)); Assert.True(first.View.IsFinished, "Первый клиент не увидел завершение партии."); Assert.True(second.View.IsFinished, "Второй клиент не увидел завершение партии."); Assert.Equal(first.View.Winner, second.View.Winner); Assert.InRange(first.View.Winner, 0, 1); var loser = 1 - first.View.Winner; Assert.False(first.View.Players[first.View.Winner].IsDefeated); Assert.True(first.View.Players[loser].IsDefeated); var health = await http.GetFromJsonAsync("/health"); Assert.True(health.GetProperty("matches").GetInt32() >= 1); } [Fact] public async Task Client_Can_Reconnect_And_Resume_After_Disconnect() { using var factory = NewFactory(); using var http = factory.CreateClient(); var deck = ServerTestData.BuildDeck(); var firstQueue = await EnqueueAsync(http, "p1", deck); var secondQueue = await EnqueueAsync(http, "p2", deck); var firstStatus = await WaitForMatchAsync(http, firstQueue.QueueId, "p1"); var secondStatus = await WaitForMatchAsync(http, secondQueue.QueueId, "p2"); using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); var first = await TestGameClient.ConnectAsync( factory, firstStatus.MatchId!, firstStatus.Ticket!, "p1", 0, cts.Token); await using var second = await TestGameClient.ConnectAsync( factory, secondStatus.MatchId!, secondStatus.Ticket!, "p2", 0, cts.Token); var active = first.View.ActivePlayer == 0 ? first : second; var result = await active.SubmitAsync("mulligan-1", new FinishMulliganIntent(), cts.Token); Assert.True(result.Accepted, result.Reason); var lastSeq = result.EventSeq; await first.DisposeAsync(); var ticketResponse = await http.PostAsJsonAsync( "/api/game/tickets", new TicketRequest("p1", firstStatus.MatchId)); if (!ticketResponse.IsSuccessStatusCode) throw new XunitException(await ticketResponse.Content.ReadAsStringAsync()); var ticket = await ticketResponse.Content.ReadFromJsonAsync(); Assert.NotNull(ticket); await using var reconnected = await TestGameClient.ConnectAsync( factory, firstStatus.MatchId!, ticket!.Ticket, "p1", lastSeq, cts.Token); Assert.True(reconnected.View.EventSeq >= lastSeq); Assert.Equal(0, reconnected.Seat); await second.RequestSnapshotAsync(cts.Token); Assert.Equal(reconnected.View.EventSeq, second.View.EventSeq); Assert.Equal(reconnected.View.ActivePlayer, second.View.ActivePlayer); Assert.False(reconnected.View.IsFinished); } [Fact] public async Task Explicit_Leave_Ends_Match_As_Loss() { using var factory = NewFactory(); using var http = factory.CreateClient(); var deck = ServerTestData.BuildDeck(); var firstQueue = await EnqueueAsync(http, "p1", deck); var secondQueue = await EnqueueAsync(http, "p2", deck); var firstStatus = await WaitForMatchAsync(http, firstQueue.QueueId, "p1"); var secondStatus = await WaitForMatchAsync(http, secondQueue.QueueId, "p2"); using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30)); await using var first = await TestGameClient.ConnectAsync( factory, firstStatus.MatchId!, firstStatus.Ticket!, "p1", 0, cts.Token); await using var second = await TestGameClient.ConnectAsync( factory, secondStatus.MatchId!, secondStatus.Ticket!, "p2", 0, cts.Token); await second.SendLeaveAsync("exit", cts.Token); var sawFinish = false; for (var attempt = 0; attempt < 20 && !sawFinish; attempt++) { var events = await first.ReadUntilAsync(cts.Token); sawFinish = events.Events.Any(item => item.Event is GameFinishedView { Winner: 0 }); } Assert.True(sawFinish, "Соперник не получил GameFinished после выхода."); await first.RequestSnapshotAsync(cts.Token); Assert.True(first.View.IsFinished); Assert.Equal(0, first.View.Winner); } private static WebApplicationFactory NewFactory() => new(); private static async Task EnqueueAsync( HttpClient http, string playerId, List deck) { var response = await http.PostAsJsonAsync( "/api/game/queue", new QueueRequest(playerId, "classic-ranked", deck)); if (!response.IsSuccessStatusCode) throw new XunitException(await response.Content.ReadAsStringAsync()); return (await response.Content.ReadFromJsonAsync())!; } private static async Task WaitForMatchAsync( HttpClient http, Guid queueId, string playerId) { for (var attempt = 0; attempt < 200; attempt++) { var status = await http.GetFromJsonAsync( $"/api/game/queue/{queueId}?playerId={playerId}"); if (status is null) throw new XunitException("Очередь исчезла."); if (status.Status == "matched") return status; if (status.Status is "failed" or "expired" or "cancelled") throw new XunitException($"Очередь в состоянии {status.Status}: {status.Reason}"); await Task.Delay(50); } throw new XunitException("Матч не был найден за 10 секунд."); } }