F2: the logoff inventory is skipped when the host already let go of the items (it may reset the session before reporting the logoff): an empty list would have emptied the backend's inventory and overwritten the saved copy. F3/F4: each socket start owns its run; Stop no longer blocks the game thread and gives the run up to ten seconds to send what was queued (a large logoff inventory) before closing, flushing that run's own send loop; a relog never hears the previous run's frames. F7: on connect, share_subscribe goes out before the first telemetry frame, as the original sent them. F8: a rare announcement still waiting at logoff is dropped rather than sent as the next character. F9: each dungeon map is sent once per login, as the original (reloaded per login) did. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
144 lines
5.2 KiB
C#
144 lines
5.2 KiB
C#
using OpenAC.MosswartMassacre.Backend;
|
|
using OpenAC.MosswartMassacre.Tests.Fakes;
|
|
|
|
namespace OpenAC.MosswartMassacre.Tests.Backend;
|
|
|
|
public sealed class BackendClientTests
|
|
{
|
|
private static readonly DateTime Connected = new(2026, 9, 25, 8, 30, 15, DateTimeKind.Utc);
|
|
|
|
[Fact]
|
|
public async Task Connect_sends_the_secret_header_then_registers_first()
|
|
{
|
|
using var server = new TestSocketServer();
|
|
using var client = new BackendClient(new RecordingLogger());
|
|
|
|
client.Start(server.Uri, "s3cret", "Mossy", () => Connected);
|
|
|
|
Assert.Equal("{\"type\":\"register\",\"player_name\":\"Mossy\"}", server.NextFrame());
|
|
Assert.True(server.Secrets.TryPeek(out string? secret));
|
|
Assert.Equal("s3cret", secret);
|
|
Assert.True(await WaitAsync(() => client.TryTakeConnection(out _)));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Frames_sent_after_connect_arrive_in_order()
|
|
{
|
|
using var server = new TestSocketServer();
|
|
using var client = new BackendClient(new RecordingLogger());
|
|
client.Start(server.Uri, "", "Mossy", () => Connected);
|
|
server.NextFrame();
|
|
Assert.True(await WaitAsync(() => client.IsConnected));
|
|
|
|
client.Send("{\"n\":1}");
|
|
client.Send("{\"n\":2}");
|
|
|
|
Assert.Equal("{\"n\":1}", server.NextFrame());
|
|
Assert.Equal("{\"n\":2}", server.NextFrame());
|
|
}
|
|
|
|
[Fact]
|
|
public void A_frame_sent_while_disconnected_is_dropped()
|
|
{
|
|
using var client = new BackendClient(new RecordingLogger());
|
|
Assert.False(client.Send("{}"));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task A_frame_larger_than_one_receive_buffer_is_reassembled()
|
|
{
|
|
using var server = new TestSocketServer();
|
|
using var client = new BackendClient(new RecordingLogger());
|
|
client.Start(server.Uri, "", "Mossy", () => Connected);
|
|
server.NextFrame();
|
|
Assert.True(await WaitAsync(() => client.IsConnected));
|
|
|
|
string big = "{\"type\":\"share_vital_update\",\"pad\":\"" + new string('x', 20000) + "\"}";
|
|
await server.PushAsync(" " + big + "\n");
|
|
|
|
string? received = null;
|
|
Assert.True(await WaitAsync(() => client.TryReceive(out received)));
|
|
Assert.Equal(big, received);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task A_dropped_socket_reconnects_and_registers_again()
|
|
{
|
|
using var server = new TestSocketServer();
|
|
using var client = new BackendClient(new RecordingLogger());
|
|
client.Start(server.Uri, "", "Mossy", () => Connected);
|
|
server.NextFrame();
|
|
Assert.True(await WaitAsync(() => client.IsConnected));
|
|
|
|
await server.DropAsync();
|
|
|
|
Assert.Equal("{\"type\":\"register\",\"player_name\":\"Mossy\"}", server.NextFrame(8000));
|
|
Assert.Equal(2, server.Connections);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Stop_closes_and_stops_reconnecting()
|
|
{
|
|
using var server = new TestSocketServer();
|
|
using var client = new BackendClient(new RecordingLogger());
|
|
client.Start(server.Uri, "", "Mossy", () => Connected);
|
|
server.NextFrame();
|
|
Assert.True(await WaitAsync(() => client.IsConnected));
|
|
|
|
client.Stop();
|
|
|
|
Assert.False(client.IsRunning);
|
|
Assert.False(client.IsConnected);
|
|
await Task.Delay(2500);
|
|
Assert.Equal(1, server.Connections);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Stop_sends_what_was_queued_before_it_closes_without_blocking()
|
|
{
|
|
using var server = new TestSocketServer();
|
|
using var client = new BackendClient(new RecordingLogger());
|
|
client.Start(server.Uri, "", "Mossy", () => Connected);
|
|
server.NextFrame();
|
|
Assert.True(await WaitAsync(() => client.IsConnected));
|
|
|
|
string big = "{\"type\":\"full_inventory\",\"pad\":\"" + new string('x', 400_000) + "\"}";
|
|
client.Send(big);
|
|
client.Send("{\"type\":\"share_unsubscribe\"}");
|
|
var watch = System.Diagnostics.Stopwatch.StartNew();
|
|
client.Stop();
|
|
Assert.True(watch.ElapsedMilliseconds < 200, $"Stop blocked for {watch.ElapsedMilliseconds} ms");
|
|
|
|
Assert.Equal(big, server.NextFrame());
|
|
Assert.Equal("{\"type\":\"share_unsubscribe\"}", server.NextFrame());
|
|
await client.LastStop.WaitAsync(TimeSpan.FromSeconds(15));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task A_new_start_right_after_stop_hears_only_its_own_connection()
|
|
{
|
|
using var server = new TestSocketServer();
|
|
using var client = new BackendClient(new RecordingLogger());
|
|
client.Start(server.Uri, "", "Mossy", () => Connected);
|
|
server.NextFrame();
|
|
Assert.True(await WaitAsync(() => client.IsConnected));
|
|
client.Stop();
|
|
|
|
client.Start(server.Uri, "", "Horan", () => Connected);
|
|
|
|
Assert.Equal("{\"type\":\"register\",\"player_name\":\"Horan\"}", server.NextFrame(8000));
|
|
Assert.True(await WaitAsync(() => client.TryTakeConnection(out _)));
|
|
Assert.False(client.TryTakeConnection(out _));
|
|
}
|
|
|
|
internal static async Task<bool> WaitAsync(Func<bool> condition, int timeoutMs = 5000)
|
|
{
|
|
for (int waited = 0; waited < timeoutMs; waited += 20)
|
|
{
|
|
if (condition())
|
|
return true;
|
|
await Task.Delay(20);
|
|
}
|
|
return condition();
|
|
}
|
|
}
|