namespace HSchool.Protocol.Tests; /// /// 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 /// src/HSchool.Client/src/net/protocol.ts honest. /// public class ProtocolCodecTests { [Fact] public void Hello_RoundTripsAndIsTwoBytes() { var message = new ClientHelloMessage(ProtocolConstants.Version); Span 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 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 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 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 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 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 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 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 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 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 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 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 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(() => ProtocolCodec.ReadClock(frame)); } [Fact] public void WrongMessageId_Throws() { Span buffer = stackalloc byte[ProtocolCodec.MaxFrameSize]; var length = ProtocolCodec.WritePing(buffer, new ClientPingMessage(1)); var frame = buffer[..length].ToArray(); Assert.Throws(() => ProtocolCodec.ReadOpenSchool(frame)); } [Fact] public void UndersizedBuffer_Throws() { var buffer = new byte[2]; Assert.Throws(() => ProtocolCodec.WriteClock(buffer, new ServerClockMessage(1, 0, false, 1))); } }