Files
SVSimServer/SVSim.UnitTests/BattleNode/Wire/SocketIoFrameTests.cs
gamer147 6ff4f70f1a fix(battle-node): SocketIoFrame disposal safety + escaping + empty-args encoding
- Wrap all JsonDocument.Parse calls in using blocks and Clone() each
  retained JsonElement to eliminate UAF hazard after GC.
- Use JsonSerializer.Serialize with UnsafeRelaxedJsonEscaping so event
  names with " or \ produce \" / \ rather than " / plain \;
  avoids malformed JSON on Encode().
- Guard the [ ] block in Encode() behind EventName-or-args check so
  Connect/Disconnect packets round-trip as bare "0"/"1" not "0[]".
- Add three regression tests: Connect no-bracket, Event round-trip,
  special-char event name escaping.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 21:46:02 -04:00

119 lines
4.2 KiB
C#

using System.Text.Json;
using NUnit.Framework;
using SVSim.BattleNode.Wire;
namespace SVSim.UnitTests.BattleNode.Wire;
[TestFixture]
public class SocketIoFrameTests
{
[Test]
public void Parse_ConnectPacket_HasNoPayload()
{
var frame = SocketIoFrame.Parse("0");
Assert.That(frame.Type, Is.EqualTo(SocketIoPacketType.Connect));
Assert.That(frame.AckId, Is.Null);
Assert.That(frame.AttachmentCount, Is.EqualTo(0));
}
[Test]
public void Parse_EventWithAck_ExtractsAckIdAndArgs()
{
var frame = SocketIoFrame.Parse("27[\"msg\",42]");
Assert.That(frame.Type, Is.EqualTo(SocketIoPacketType.Event));
Assert.That(frame.AckId, Is.EqualTo(7));
Assert.That(frame.EventName, Is.EqualTo("msg"));
Assert.That(frame.RawArgs[0].GetInt32(), Is.EqualTo(42));
}
[Test]
public void Parse_BinaryEvent_RecordsAttachmentCount_WithAttachments_Assembles()
{
var attachment = new byte[] { 0x01, 0x02, 0x03 };
var header = SocketIoFrame.Parse("51-[\"msg\",{\"_placeholder\":true,\"num\":0}]");
var assembled = header.WithAttachments(new[] { attachment });
Assert.That(assembled.Type, Is.EqualTo(SocketIoPacketType.BinaryEvent));
Assert.That(assembled.AckId, Is.Null);
Assert.That(assembled.AttachmentCount, Is.EqualTo(1));
Assert.That(assembled.BinaryAttachments[0], Is.EqualTo(attachment));
Assert.That(assembled.EventName, Is.EqualTo("msg"));
}
[Test]
public void Parse_BinaryEventWithAckId_ExtractsBoth()
{
var frame = SocketIoFrame.Parse("51-3[\"msg\",{\"_placeholder\":true,\"num\":0}]");
Assert.That(frame.Type, Is.EqualTo(SocketIoPacketType.BinaryEvent));
Assert.That(frame.AckId, Is.EqualTo(3));
Assert.That(frame.AttachmentCount, Is.EqualTo(1));
}
[Test]
public void Parse_AckResponse_ExtractsIdAndIntArg()
{
var frame = SocketIoFrame.Parse("37[123]");
Assert.That(frame.Type, Is.EqualTo(SocketIoPacketType.Ack));
Assert.That(frame.AckId, Is.EqualTo(7));
Assert.That(frame.RawArgs[0].GetInt32(), Is.EqualTo(123));
}
[Test]
public void Encode_BinaryEventWithAttachment_EmitsCountDashAndPlaceholder()
{
var attachment = new byte[] { 0xff };
var frame = SocketIoFrame.BinaryEventWithAttachments("synchronize", new[] { attachment });
var (text, bins) = frame.Encode();
Assert.That(text, Is.EqualTo("51-[\"synchronize\",{\"_placeholder\":true,\"num\":0}]"));
Assert.That(bins.Single(), Is.EqualTo(attachment));
}
[Test]
public void Encode_AckResponse_IsTypeIdAndArrayOfArgs()
{
var frame = SocketIoFrame.AckResponse(ackId: 7, arg: 123);
var (text, bins) = frame.Encode();
Assert.That(text, Is.EqualTo("37[123]"));
Assert.That(bins, Is.Empty);
}
[Test]
public void WithAttachments_CountMismatch_Throws()
{
var header = SocketIoFrame.Parse("51-[\"msg\",{\"_placeholder\":true,\"num\":0}]");
Assert.Throws<ArgumentException>(() => header.WithAttachments(Array.Empty<byte[]>()));
}
[Test]
public void Encode_ConnectPacket_HasNoBracketedArgs()
{
// Regression for an earlier bug where Encode always emitted "[]".
var frame = SocketIoFrame.Parse("0");
var (text, bins) = frame.Encode();
Assert.That(text, Is.EqualTo("0"));
Assert.That(bins, Is.Empty);
}
[Test]
public void ParseThenEncode_EventWithAck_RoundTripsByteForByte()
{
const string wire = "27[\"msg\",42]";
var frame = SocketIoFrame.Parse(wire);
var (text, _) = frame.Encode();
Assert.That(text, Is.EqualTo(wire));
}
[Test]
public void Encode_EventNameWithSpecialChars_IsJsonEscaped()
{
var frame = SocketIoFrame.BinaryEventWithAttachments(
eventName: "weird \"name\" with \\ backslash",
attachments: new[] { new byte[] { 0x00 } });
var (text, _) = frame.Encode();
// The event name must be JSON-escaped: each " becomes \", and the literal \ becomes \\.
Assert.That(text, Does.Contain("\"weird \\\"name\\\" with \\\\ backslash\""));
}
}