Files
Erupe/cmd/replay/replay_test.go
Houmgaor 7ef5efc549 feat(network): add protocol packet capture and replay system
Add a recording and replay foundation for the MHF network protocol.
A RecordingConn decorator wraps network.Conn to transparently capture
all decrypted packets to binary .mhfr files, with zero handler changes
and zero overhead when disabled.

- network/pcap: binary capture format (writer, reader, filters)
- RecordingConn: thread-safe Conn decorator with direction tracking
- CaptureOptions in config (disabled by default)
- Capture wired into all three server types (sign, entrance, channel)
- cmd/replay: CLI tool with dump, json, stats, and compare modes
- 19 new tests, all passing with -race
2026-02-23 18:50:44 +01:00

155 lines
4.3 KiB
Go

package main
import (
"bytes"
"os"
"testing"
"erupe-ce/network/pcap"
)
func createTestCapture(t *testing.T, records []pcap.PacketRecord) string {
t.Helper()
f, err := os.CreateTemp(t.TempDir(), "test-*.mhfr")
if err != nil {
t.Fatalf("CreateTemp: %v", err)
}
defer func() { _ = f.Close() }()
hdr := pcap.FileHeader{
Version: pcap.FormatVersion,
ServerType: pcap.ServerTypeChannel,
ClientMode: 40,
SessionStartNs: 1000000000,
}
meta := pcap.SessionMetadata{Host: "127.0.0.1", Port: 54001}
w, err := pcap.NewWriter(f, hdr, meta)
if err != nil {
t.Fatalf("NewWriter: %v", err)
}
for _, r := range records {
if err := w.WritePacket(r); err != nil {
t.Fatalf("WritePacket: %v", err)
}
}
if err := w.Flush(); err != nil {
t.Fatalf("Flush: %v", err)
}
return f.Name()
}
func TestRunDump(t *testing.T) {
path := createTestCapture(t, []pcap.PacketRecord{
{TimestampNs: 1000000100, Direction: pcap.DirClientToServer, Opcode: 0x0013, Payload: []byte{0x00, 0x13}},
{TimestampNs: 1000000200, Direction: pcap.DirServerToClient, Opcode: 0x0012, Payload: []byte{0x00, 0x12, 0xFF}},
})
// Just verify it doesn't error.
if err := runDump(path); err != nil {
t.Fatalf("runDump: %v", err)
}
}
func TestRunStats(t *testing.T) {
path := createTestCapture(t, []pcap.PacketRecord{
{TimestampNs: 1000000100, Direction: pcap.DirClientToServer, Opcode: 0x0013, Payload: []byte{0x00, 0x13}},
{TimestampNs: 1000000200, Direction: pcap.DirServerToClient, Opcode: 0x0012, Payload: []byte{0x00, 0x12, 0xFF}},
{TimestampNs: 1000000300, Direction: pcap.DirClientToServer, Opcode: 0x0013, Payload: []byte{0x00, 0x13, 0xAA}},
})
if err := runStats(path); err != nil {
t.Fatalf("runStats: %v", err)
}
}
func TestRunStatsEmpty(t *testing.T) {
path := createTestCapture(t, nil)
if err := runStats(path); err != nil {
t.Fatalf("runStats empty: %v", err)
}
}
func TestRunJSON(t *testing.T) {
path := createTestCapture(t, []pcap.PacketRecord{
{TimestampNs: 1000000100, Direction: pcap.DirClientToServer, Opcode: 0x0013, Payload: []byte{0x00, 0x13}},
})
// Capture stdout.
old := os.Stdout
r, w, _ := os.Pipe()
os.Stdout = w
if err := runJSON(path); err != nil {
os.Stdout = old
t.Fatalf("runJSON: %v", err)
}
_ = w.Close()
os.Stdout = old
var buf bytes.Buffer
_, _ = buf.ReadFrom(r)
if buf.Len() == 0 {
t.Error("runJSON produced no output")
}
// Should be valid JSON containing "packets".
if !bytes.Contains(buf.Bytes(), []byte(`"packets"`)) {
t.Error("runJSON output missing 'packets' key")
}
}
func TestComparePackets(t *testing.T) {
expected := []pcap.PacketRecord{
{Direction: pcap.DirClientToServer, Opcode: 0x0013, Payload: []byte{0x00, 0x13}},
{Direction: pcap.DirServerToClient, Opcode: 0x0012, Payload: []byte{0x00, 0x12, 0xAA}},
{Direction: pcap.DirServerToClient, Opcode: 0x0061, Payload: []byte{0x00, 0x61}},
}
actual := []pcap.PacketRecord{
{Direction: pcap.DirServerToClient, Opcode: 0x0012, Payload: []byte{0x00, 0x12, 0xBB, 0xCC}}, // size diff
{Direction: pcap.DirServerToClient, Opcode: 0x0099, Payload: []byte{0x00, 0x99}}, // opcode mismatch
}
diffs := ComparePackets(expected, actual)
if len(diffs) != 2 {
t.Fatalf("expected 2 diffs, got %d", len(diffs))
}
// First diff: size delta.
if diffs[0].SizeDelta != 1 {
t.Errorf("diffs[0] SizeDelta = %d, want 1", diffs[0].SizeDelta)
}
// Second diff: opcode mismatch.
if !diffs[1].OpcodeMismatch {
t.Error("diffs[1] expected OpcodeMismatch=true")
}
}
func TestComparePacketsMissingResponse(t *testing.T) {
expected := []pcap.PacketRecord{
{Direction: pcap.DirServerToClient, Opcode: 0x0012, Payload: []byte{0x00, 0x12}},
{Direction: pcap.DirServerToClient, Opcode: 0x0061, Payload: []byte{0x00, 0x61}},
}
actual := []pcap.PacketRecord{
{Direction: pcap.DirServerToClient, Opcode: 0x0012, Payload: []byte{0x00, 0x12}},
}
diffs := ComparePackets(expected, actual)
if len(diffs) != 1 {
t.Fatalf("expected 1 diff, got %d", len(diffs))
}
if diffs[0].Actual != nil {
t.Error("expected nil Actual for missing response")
}
}
func TestPacketDiffString(t *testing.T) {
d := PacketDiff{
Index: 0,
Expected: pcap.PacketRecord{Opcode: 0x0012},
Actual: nil,
}
s := d.String()
if s == "" {
t.Error("PacketDiff.String() returned empty")
}
}