304 lines
11 KiB
C#
304 lines
11 KiB
C#
namespace HSchool.Protocol.Tests;
|
|
|
|
/// <summary>
|
|
/// The wire format is a contract with the browser client. Round-trips prove the C# side is
|
|
/// self-consistent; the explicit byte-layout tests are what keeps
|
|
/// <c>src/HSchool.Client/src/net/protocol.ts</c> honest.
|
|
/// </summary>
|
|
public class ProtocolCodecTests
|
|
{
|
|
[Fact]
|
|
public void Hello_RoundTripsAndIsThreeBytes()
|
|
{
|
|
var message = new ClientHelloMessage(ProtocolConstants.Version, ProtocolConstants.LocaleEnglish);
|
|
Span<byte> buffer = stackalloc byte[ProtocolCodec.MaxFrameSize];
|
|
|
|
var length = ProtocolCodec.WriteHello(buffer, message);
|
|
|
|
Assert.Equal(3, length);
|
|
Assert.Equal((byte)MessageType.ClientHello, buffer[0]);
|
|
Assert.Equal(ProtocolConstants.Version, buffer[1]);
|
|
Assert.Equal(ProtocolConstants.LocaleEnglish, buffer[2]);
|
|
Assert.Equal(message, ProtocolCodec.ReadHello(buffer[..length]));
|
|
}
|
|
|
|
[Fact]
|
|
public void Ping_RoundTripsAndIsNineBytes()
|
|
{
|
|
var message = new ClientPingMessage(1_700_000_000_123);
|
|
Span<byte> buffer = stackalloc byte[ProtocolCodec.MaxFrameSize];
|
|
|
|
var length = ProtocolCodec.WritePing(buffer, message);
|
|
|
|
Assert.Equal(9, length);
|
|
Assert.Equal(message, ProtocolCodec.ReadPing(buffer[..length]));
|
|
}
|
|
|
|
[Fact]
|
|
public void OpenSchool_RoundTripsAndIsFiveBytes()
|
|
{
|
|
var message = new ClientOpenSchoolMessage(0x01020304);
|
|
Span<byte> buffer = stackalloc byte[ProtocolCodec.MaxFrameSize];
|
|
|
|
var length = ProtocolCodec.WriteOpenSchool(buffer, message);
|
|
|
|
Assert.Equal(5, length);
|
|
Assert.Equal(message, ProtocolCodec.ReadOpenSchool(buffer[..length]));
|
|
}
|
|
|
|
[Fact]
|
|
public void CloseSchool_IsASingleByte()
|
|
{
|
|
Span<byte> buffer = stackalloc byte[ProtocolCodec.MaxFrameSize];
|
|
|
|
var length = ProtocolCodec.WriteCloseSchool(buffer);
|
|
|
|
Assert.Equal(1, length);
|
|
Assert.Equal(MessageType.ClientCloseSchool, ProtocolCodec.PeekMessageType(buffer[..length]));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(true)]
|
|
[InlineData(false)]
|
|
public void SetRunning_RoundTripsAndIsTwoBytes(bool running)
|
|
{
|
|
var message = new ClientSetRunningMessage(running);
|
|
Span<byte> buffer = stackalloc byte[ProtocolCodec.MaxFrameSize];
|
|
|
|
var length = ProtocolCodec.WriteSetRunning(buffer, message);
|
|
|
|
Assert.Equal(2, length);
|
|
Assert.Equal(message, ProtocolCodec.ReadSetRunning(buffer[..length]));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(0)]
|
|
[InlineData(4)]
|
|
public void SetSpeed_RoundTripsAndIsTwoBytes(byte speedIndex)
|
|
{
|
|
var message = new ClientSetSpeedMessage(speedIndex);
|
|
Span<byte> buffer = stackalloc byte[ProtocolCodec.MaxFrameSize];
|
|
|
|
var length = ProtocolCodec.WriteSetSpeed(buffer, message);
|
|
|
|
Assert.Equal(2, length);
|
|
Assert.Equal(message, ProtocolCodec.ReadSetSpeed(buffer[..length]));
|
|
}
|
|
|
|
[Fact]
|
|
public void Welcome_RoundTripsAndIsFourBytes()
|
|
{
|
|
var message = new ServerWelcomeMessage(ProtocolConstants.Version, 20, 6);
|
|
Span<byte> buffer = stackalloc byte[ProtocolCodec.MaxFrameSize];
|
|
|
|
var length = ProtocolCodec.WriteWelcome(buffer, message);
|
|
|
|
Assert.Equal(4, length);
|
|
Assert.Equal(message, ProtocolCodec.ReadWelcome(buffer[..length]));
|
|
}
|
|
|
|
[Fact]
|
|
public void Pong_RoundTripsAndIsThirteenBytes()
|
|
{
|
|
var message = new ServerPongMessage(5, 99);
|
|
Span<byte> buffer = stackalloc byte[ProtocolCodec.MaxFrameSize];
|
|
|
|
var length = ProtocolCodec.WritePong(buffer, message);
|
|
|
|
Assert.Equal(13, length);
|
|
Assert.Equal(message, ProtocolCodec.ReadPong(buffer[..length]));
|
|
}
|
|
|
|
[Fact]
|
|
public void Clock_RoundTripsAndIsFifteenBytes()
|
|
{
|
|
var message = new ServerClockMessage(7, 1_333_432_800_000, Running: true, SpeedIndex: 2);
|
|
Span<byte> buffer = stackalloc byte[ProtocolCodec.MaxFrameSize];
|
|
|
|
var length = ProtocolCodec.WriteClock(buffer, message);
|
|
|
|
Assert.Equal(15, length);
|
|
Assert.Equal(message, ProtocolCodec.ReadClock(buffer[..length]));
|
|
}
|
|
|
|
[Fact]
|
|
public void SchoolGone_RoundTripsAndIsFiveBytes()
|
|
{
|
|
var message = new ServerSchoolGoneMessage(3);
|
|
Span<byte> buffer = stackalloc byte[ProtocolCodec.MaxFrameSize];
|
|
|
|
var length = ProtocolCodec.WriteSchoolGone(buffer, message);
|
|
|
|
Assert.Equal(5, length);
|
|
Assert.Equal(message, ProtocolCodec.ReadSchoolGone(buffer[..length]));
|
|
}
|
|
|
|
[Fact]
|
|
public void MapSnapshot_RoundTripsAndWritesHeaderOffsets()
|
|
{
|
|
var message = new ServerMapSnapshotMessage(7, [
|
|
new MapSnapshotNode(0, "yard", "", "Двор", 0, [], []),
|
|
new MapSnapshotNode(
|
|
3,
|
|
"office",
|
|
"floor-1",
|
|
"Кабинет директора",
|
|
0,
|
|
[new MapSnapshotItem("Стул", 2)],
|
|
["Директор"]),
|
|
]);
|
|
var buffer = new byte[ProtocolConstants.MaxMessageSize];
|
|
|
|
var length = ProtocolCodec.WriteMapSnapshot(buffer, message);
|
|
|
|
Assert.Equal((byte)MessageType.ServerMapSnapshot, buffer[0]);
|
|
Assert.Equal(7, BitConverter.ToInt32(buffer.AsSpan(1, 4)));
|
|
Assert.Equal((ushort)2, BitConverter.ToUInt16(buffer.AsSpan(5, 2)));
|
|
|
|
var read = ProtocolCodec.ReadMapSnapshot(buffer.AsSpan(0, length));
|
|
Assert.Equal(message.SchoolId, read.SchoolId);
|
|
Assert.Equal(2, read.Nodes.Count);
|
|
Assert.Equal("yard", read.Nodes[0].Id);
|
|
Assert.Equal("", read.Nodes[0].ParentId);
|
|
Assert.Equal("Двор", read.Nodes[0].Name);
|
|
Assert.Equal(0, read.Nodes[0].PupilSlots);
|
|
Assert.Equal([new MapSnapshotItem("Стул", 2)], read.Nodes[1].Items);
|
|
Assert.Equal(["Директор"], read.Nodes[1].Positions);
|
|
}
|
|
|
|
[Fact]
|
|
public void MapSnapshot_WritesPupilSlotsAndStackedItemCount()
|
|
{
|
|
var message = new ServerMapSnapshotMessage(1, [
|
|
new MapSnapshotNode(
|
|
3,
|
|
"classroom-1a",
|
|
"floor-1",
|
|
"Класс 1A",
|
|
16,
|
|
[new MapSnapshotItem("Парта", 16)],
|
|
[]),
|
|
]);
|
|
var buffer = new byte[ProtocolCodec.MapSnapshotSize(message)];
|
|
|
|
var length = ProtocolCodec.WriteMapSnapshot(buffer, message);
|
|
var read = ProtocolCodec.ReadMapSnapshot(buffer.AsSpan(0, length));
|
|
|
|
Assert.Equal(16, read.Nodes[0].PupilSlots);
|
|
Assert.Equal([new MapSnapshotItem("Парта", 16)], read.Nodes[0].Items);
|
|
}
|
|
|
|
[Fact]
|
|
public void MapSnapshotSize_IsExactlyWhatTheWriterProduces()
|
|
{
|
|
var message = new ServerMapSnapshotMessage(7, [
|
|
new MapSnapshotNode(0, "yard", "", "Двор", 0, [], []),
|
|
new MapSnapshotNode(2, "floor-1", "main", "Этаж 1", 0, [], []),
|
|
new MapSnapshotNode(
|
|
3,
|
|
"office",
|
|
"floor-1",
|
|
"Кабинет директора",
|
|
0,
|
|
[new MapSnapshotItem("Стол", 1), new MapSnapshotItem("Стул", 2)],
|
|
["Директор"]),
|
|
]);
|
|
|
|
var size = ProtocolCodec.MapSnapshotSize(message);
|
|
var buffer = new byte[size];
|
|
|
|
Assert.Equal(size, ProtocolCodec.WriteMapSnapshot(buffer, message));
|
|
}
|
|
|
|
[Fact]
|
|
public void MapSnapshot_OfALargeSchoolSurvivesTheEightKilobyteLimit()
|
|
{
|
|
// A player who keeps clicking "Add room" in the create editor gets past 8 KiB somewhere
|
|
// around sixty furnished rooms. Sizing the buffer from the message is what keeps that
|
|
// school openable instead of killing its worker thread on the first snapshot.
|
|
var nodes = new List<MapSnapshotNode> { new(0, "yard", "", "Двор", 0, [], []) };
|
|
for (var i = 1; i <= 200; i++)
|
|
{
|
|
nodes.Add(new MapSnapshotNode(
|
|
3,
|
|
$"principals-office-{i}",
|
|
"floor-1",
|
|
"Кабинет директора",
|
|
0,
|
|
[new MapSnapshotItem("Кресло директора", 1), new MapSnapshotItem("Стол", 1), new MapSnapshotItem("Стул", 2)],
|
|
["Директор"]));
|
|
}
|
|
|
|
var message = new ServerMapSnapshotMessage(1, nodes);
|
|
var buffer = new byte[ProtocolCodec.MapSnapshotSize(message)];
|
|
|
|
Assert.True(buffer.Length > ProtocolConstants.MaxMessageSize);
|
|
|
|
var length = ProtocolCodec.WriteMapSnapshot(buffer, message);
|
|
var read = ProtocolCodec.ReadMapSnapshot(buffer.AsSpan(0, length));
|
|
|
|
Assert.Equal(nodes.Count, read.Nodes.Count);
|
|
Assert.Equal("principals-office-200", read.Nodes[^1].Id);
|
|
Assert.Equal(
|
|
[new MapSnapshotItem("Кресло директора", 1), new MapSnapshotItem("Стол", 1), new MapSnapshotItem("Стул", 2)],
|
|
read.Nodes[^1].Items);
|
|
}
|
|
|
|
[Fact]
|
|
public void Numbers_AreLittleEndian()
|
|
{
|
|
Span<byte> buffer = stackalloc byte[ProtocolCodec.MaxFrameSize];
|
|
ProtocolCodec.WriteOpenSchool(buffer, new ClientOpenSchoolMessage(0x01020304));
|
|
|
|
Assert.Equal((byte)MessageType.ClientOpenSchool, buffer[0]);
|
|
Assert.Equal(new byte[] { 0x04, 0x03, 0x02, 0x01 }, buffer[1..5].ToArray());
|
|
}
|
|
|
|
[Fact]
|
|
public void MaxFrameSize_FitsEveryFixedSizeMessage()
|
|
{
|
|
// Handlers size clock/welcome/pong buffers from this constant; map snapshots use MaxMessageSize.
|
|
Span<byte> buffer = stackalloc byte[ProtocolCodec.MaxFrameSize];
|
|
var clock = ProtocolCodec.WriteClock(buffer, new ServerClockMessage(1, long.MaxValue, true, 4));
|
|
|
|
Assert.True(clock <= ProtocolCodec.MaxFrameSize);
|
|
}
|
|
|
|
[Fact]
|
|
public void PeekMessageType_ReadsTheFirstByte()
|
|
{
|
|
Span<byte> buffer = stackalloc byte[ProtocolCodec.MaxFrameSize];
|
|
ProtocolCodec.WritePing(buffer, new ClientPingMessage(1));
|
|
|
|
Assert.Equal(MessageType.ClientPing, ProtocolCodec.PeekMessageType(buffer));
|
|
Assert.Equal(MessageType.None, ProtocolCodec.PeekMessageType([]));
|
|
}
|
|
|
|
[Fact]
|
|
public void TruncatedFrame_Throws()
|
|
{
|
|
byte[] frame = [(byte)MessageType.ServerClock, 1, 2];
|
|
|
|
Assert.Throws<ProtocolException>(() => ProtocolCodec.ReadClock(frame));
|
|
}
|
|
|
|
[Fact]
|
|
public void WrongMessageId_Throws()
|
|
{
|
|
Span<byte> buffer = stackalloc byte[ProtocolCodec.MaxFrameSize];
|
|
var length = ProtocolCodec.WritePing(buffer, new ClientPingMessage(1));
|
|
var frame = buffer[..length].ToArray();
|
|
|
|
Assert.Throws<ProtocolException>(() => ProtocolCodec.ReadOpenSchool(frame));
|
|
}
|
|
|
|
[Fact]
|
|
public void UndersizedBuffer_Throws()
|
|
{
|
|
var buffer = new byte[2];
|
|
|
|
Assert.Throws<ProtocolException>(() =>
|
|
ProtocolCodec.WriteClock(buffer, new ServerClockMessage(1, 0, false, 1)));
|
|
}
|
|
}
|