VanguardReMaster/server/tests/Vanguard.Server.Tests/GameServerE2ETests.cs

184 lines
7.0 KiB
C#

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<JsonElement>("/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 result = await first.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<TicketResponse>();
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<ServerEvents>(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<Program> NewFactory() => new();
private static async Task<QueueAccepted> EnqueueAsync(
HttpClient http,
string playerId,
List<string> 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<QueueAccepted>())!;
}
private static async Task<QueueStatusResponse> WaitForMatchAsync(
HttpClient http,
Guid queueId,
string playerId)
{
for (var attempt = 0; attempt < 200; attempt++)
{
var status = await http.GetFromJsonAsync<QueueStatusResponse>(
$"/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 секунд.");
}
}