Files
Erupe/cmd/replay/replay_test.go
Houmgaor f712e3c04d feat(pcap): complete replay system with filtering, metadata, and live replay
Wire ExcludeOpcodes config into RecordingConn so configured opcodes
(e.g. ping, nop, position) are filtered at record time. Add padded
metadata with in-place PatchMetadata to populate CharID/UserID after
login. Implement --mode replay using protbot's encrypted connection
with timing-aware packet sending, auto-ping response, concurrent
S→C collection, and byte-level payload diff reporting.
2026-02-23 19:34:30 +01:00

313 lines
8.6 KiB
Go

package main
import (
"bytes"
"encoding/binary"
"net"
"os"
"strings"
"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 TestComparePacketsPayloadDiff(t *testing.T) {
expected := []pcap.PacketRecord{
{Direction: pcap.DirServerToClient, Opcode: 0x0012, Payload: []byte{0x00, 0x12, 0xAA, 0xBB}},
}
actual := []pcap.PacketRecord{
{Direction: pcap.DirServerToClient, Opcode: 0x0012, Payload: []byte{0x00, 0x12, 0xCC, 0xBB}},
}
diffs := ComparePackets(expected, actual)
if len(diffs) != 1 {
t.Fatalf("expected 1 diff, got %d", len(diffs))
}
if len(diffs[0].PayloadDiffs) != 1 {
t.Fatalf("expected 1 payload diff, got %d", len(diffs[0].PayloadDiffs))
}
bd := diffs[0].PayloadDiffs[0]
if bd.Offset != 2 || bd.Expected != 0xAA || bd.Actual != 0xCC {
t.Errorf("ByteDiff = {Offset:%d, Expected:0x%02X, Actual:0x%02X}, want {2, 0xAA, 0xCC}",
bd.Offset, bd.Expected, bd.Actual)
}
}
func TestComparePacketsIdentical(t *testing.T) {
records := []pcap.PacketRecord{
{Direction: pcap.DirServerToClient, Opcode: 0x0012, Payload: []byte{0x00, 0x12, 0xAA}},
}
diffs := ComparePackets(records, records)
if len(diffs) != 0 {
t.Errorf("expected 0 diffs for identical packets, got %d", len(diffs))
}
}
func TestPacketDiffString(t *testing.T) {
tests := []struct {
name string
diff PacketDiff
contains string
}{
{
name: "missing response",
diff: PacketDiff{
Index: 0,
Expected: pcap.PacketRecord{Opcode: 0x0012},
Actual: nil,
},
contains: "no response",
},
{
name: "opcode mismatch",
diff: PacketDiff{
Index: 1,
Expected: pcap.PacketRecord{Opcode: 0x0012},
Actual: &pcap.PacketRecord{Opcode: 0x0099},
OpcodeMismatch: true,
},
contains: "opcode mismatch",
},
{
name: "size delta",
diff: PacketDiff{
Index: 2,
Expected: pcap.PacketRecord{Opcode: 0x0012},
Actual: &pcap.PacketRecord{Opcode: 0x0012},
SizeDelta: 5,
},
contains: "size delta",
},
{
name: "payload diffs",
diff: PacketDiff{
Index: 3,
Expected: pcap.PacketRecord{Opcode: 0x0012},
Actual: &pcap.PacketRecord{Opcode: 0x0012},
PayloadDiffs: []ByteDiff{
{Offset: 2, Expected: 0xAA, Actual: 0xBB},
},
},
contains: "byte diff",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
s := tc.diff.String()
if !strings.Contains(s, tc.contains) {
t.Errorf("String() = %q, want it to contain %q", s, tc.contains)
}
})
}
}
func TestRunReplayWithMockServer(t *testing.T) {
// Start a mock TCP server that echoes a response for each received packet.
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("Listen: %v", err)
}
defer func() { _ = ln.Close() }()
serverDone := make(chan struct{})
go func() {
defer close(serverDone)
c, err := ln.Accept()
if err != nil {
return
}
defer func() { _ = c.Close() }()
// This mock doesn't do Blowfish encryption — it just reads raw and echoes.
// Since the replay uses protbot's CryptConn (Blowfish), we need a real crypto echo.
// For a simpler test, just verify the function handles connection errors gracefully.
// Read a bit and close.
buf := make([]byte, 1024)
_, _ = c.Read(buf)
}()
// Create a minimal capture with one C→S packet.
path := createTestCapture(t, []pcap.PacketRecord{
{TimestampNs: 1000000100, Direction: pcap.DirClientToServer, Opcode: 0x0013,
Payload: []byte{0x00, 0x13, 0xDE, 0xAD}},
})
// Run replay — the connection will fail (no Blowfish on mock), but it should not panic.
err = runReplay(path, ln.Addr().String(), 0)
// We expect an error or graceful handling since the mock doesn't speak Blowfish.
// The important thing is no panic.
_ = err
}
func TestComparePayloads(t *testing.T) {
a := []byte{0x00, 0x12, 0xAA, 0xBB, 0xCC}
b := []byte{0x00, 0x12, 0xAA, 0xDD, 0xCC}
diffs := comparePayloads(a, b)
if len(diffs) != 1 {
t.Fatalf("expected 1 diff, got %d", len(diffs))
}
if diffs[0].Offset != 3 {
t.Errorf("Offset = %d, want 3", diffs[0].Offset)
}
}
func TestComparePayloadsMaxDiffs(t *testing.T) {
// All bytes different — should cap at maxPayloadDiffs.
a := make([]byte, 100)
b := make([]byte, 100)
for i := range b {
b[i] = 0xFF
}
diffs := comparePayloads(a, b)
if len(diffs) != maxPayloadDiffs {
t.Errorf("expected %d diffs (capped), got %d", maxPayloadDiffs, len(diffs))
}
}
func TestBuildPingResponse(t *testing.T) {
pong := buildPingResponse()
if len(pong) < 2 {
t.Fatal("ping response too short")
}
opcode := binary.BigEndian.Uint16(pong[:2])
if opcode != opcodeSysPing {
t.Errorf("opcode = 0x%04X, want 0x%04X", opcode, opcodeSysPing)
}
}