192 lines
7.4 KiB
C#
192 lines
7.4 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(
|
|
PlayAsync(first, seed: 11, cts.Token),
|
|
PlayAsync(second, 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);
|
|
}
|
|
|
|
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 секунд.");
|
|
}
|
|
|
|
private static async Task PlayAsync(
|
|
TestGameClient client,
|
|
int seed,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
var random = new Random(seed);
|
|
var deadline = DateTime.UtcNow.AddSeconds(90);
|
|
|
|
while (!client.View.IsFinished)
|
|
{
|
|
if (DateTime.UtcNow > deadline)
|
|
throw new XunitException(
|
|
$"Тест не доиграл партию за 90 секунд (seat {client.Seat}).");
|
|
|
|
if (client.View.ActivePlayer == client.Seat)
|
|
{
|
|
var choices = client.View.LegalIntents
|
|
.Where(intent => intent is not ConcedeIntent)
|
|
.ToList();
|
|
|
|
if (choices.Count == 0)
|
|
throw new XunitException(
|
|
$"Нет легальных интентов для seat {client.Seat}, " +
|
|
$"фаза {client.View.Phase}, шаг {client.View.Step}.");
|
|
|
|
var intent = choices[random.Next(choices.Count)];
|
|
var intentId = Guid.NewGuid().ToString("N");
|
|
|
|
var result = await client.SubmitAsync(intentId, intent, cancellationToken);
|
|
if (result.Duplicate)
|
|
throw new XunitException($"Неожиданный duplicate для {intentId}.");
|
|
|
|
if (!result.Accepted)
|
|
throw new XunitException(
|
|
$"Интент {intent.GetType().Name} отклонён: {result.Code} {result.Reason}");
|
|
|
|
// держимся ниже лимита 16 интентов в секунду
|
|
await Task.Delay(75, cancellationToken);
|
|
}
|
|
|
|
await client.RequestSnapshotAsync(cancellationToken);
|
|
|
|
if (!client.View.IsFinished && client.View.ActivePlayer != client.Seat)
|
|
await Task.Delay(20, cancellationToken);
|
|
}
|
|
}
|
|
}
|