Files
h-school/tests/HSchool.Protocol.Tests/ProtocolCodecTests.cs
T

190 lines
6.3 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_RoundTripsAndIsTwoBytes()
{
var message = new ClientHelloMessage(ProtocolConstants.Version);
Span<byte> buffer = stackalloc byte[ProtocolCodec.MaxFrameSize];
var length = ProtocolCodec.WriteHello(buffer, message);
Assert.Equal(2, length);
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 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_FitsEveryMessage()
{
// The handlers size their buffers from this constant; the clock frame is the largest one.
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)));
}
}