From 0e2502c9dca721b0b2068935c85e0f0f00fbefae Mon Sep 17 00:00:00 2001 From: Houmgaor Date: Sun, 1 Feb 2026 23:36:18 +0100 Subject: [PATCH] test: add PacketID and core packet tests, expand stringstack coverage - Add network/packetid_test.go with tests for PacketID type, constants, String() method, ranges, uniqueness, and sequential verification - Add network/mhfpacket/msg_sys_core_test.go with round-trip tests for MsgSysAck (including large payloads), MsgSysNop, and MsgSysEnd - Expand common/stringstack/stringstack_test.go with Lock/Unlock tests achieving 100% coverage --- common/stringstack/stringstack_test.go | 112 +++++++++ network/mhfpacket/msg_sys_core_test.go | 310 +++++++++++++++++++++++++ network/packetid_test.go | 211 +++++++++++++++++ 3 files changed, 633 insertions(+) create mode 100644 network/mhfpacket/msg_sys_core_test.go create mode 100644 network/packetid_test.go diff --git a/common/stringstack/stringstack_test.go b/common/stringstack/stringstack_test.go index 3bfcf7656..9e39acd60 100644 --- a/common/stringstack/stringstack_test.go +++ b/common/stringstack/stringstack_test.go @@ -341,3 +341,115 @@ func BenchmarkStringStack_Set(b *testing.B) { s.Set("test string") } } + +func TestStringStack_Lock(t *testing.T) { + s := New() + + // Initially not locked + if s.Locked { + t.Error("New StringStack should not be locked") + } + + // Lock it + s.Lock() + if !s.Locked { + t.Error("Lock() should set Locked to true") + } + + // Lock again (should be idempotent) + s.Lock() + if !s.Locked { + t.Error("Lock() on already locked stack should remain locked") + } +} + +func TestStringStack_Unlock(t *testing.T) { + s := New() + + // Lock then unlock + s.Lock() + if !s.Locked { + t.Error("Lock() should set Locked to true") + } + + s.Unlock() + if s.Locked { + t.Error("Unlock() should set Locked to false") + } + + // Unlock again (should be idempotent) + s.Unlock() + if s.Locked { + t.Error("Unlock() on already unlocked stack should remain unlocked") + } +} + +func TestStringStack_LockUnlockCycle(t *testing.T) { + s := New() + + // Multiple lock/unlock cycles + for i := 0; i < 5; i++ { + s.Lock() + if !s.Locked { + t.Errorf("Cycle %d: Lock() should set Locked to true", i) + } + + s.Unlock() + if s.Locked { + t.Errorf("Cycle %d: Unlock() should set Locked to false", i) + } + } +} + +func TestStringStack_LockDoesNotAffectOperations(t *testing.T) { + s := New() + s.Push("item1") + s.Lock() + + // Lock is just a flag - operations still work + s.Push("item2") + if len(s.stack) != 2 { + t.Error("Push() should work on locked stack") + } + + val, err := s.Pop() + if err != nil { + t.Errorf("Pop() on locked stack returned error: %v", err) + } + if val != "item2" { + t.Errorf("Pop() = %q, want %q", val, "item2") + } + + s.Set("reset") + if len(s.stack) != 1 || s.stack[0] != "reset" { + t.Error("Set() should work on locked stack") + } +} + +func TestStringStack_NewUnlocked(t *testing.T) { + s := New() + if s.Locked != false { + t.Error("New() should create an unlocked StringStack") + } +} + +func TestStringStack_UnlockOnUnlocked(t *testing.T) { + s := New() + + // Unlock on already unlocked should be safe + s.Unlock() + if s.Locked { + t.Error("Unlock() on unlocked stack should keep it unlocked") + } +} + +func TestStringStack_LockOnLocked(t *testing.T) { + s := New() + s.Lock() + + // Lock on already locked should be safe + s.Lock() + if !s.Locked { + t.Error("Lock() on locked stack should keep it locked") + } +} diff --git a/network/mhfpacket/msg_sys_core_test.go b/network/mhfpacket/msg_sys_core_test.go new file mode 100644 index 000000000..29e568a64 --- /dev/null +++ b/network/mhfpacket/msg_sys_core_test.go @@ -0,0 +1,310 @@ +package mhfpacket + +import ( + "io" + "testing" + + "erupe-ce/common/byteframe" + "erupe-ce/network" + "erupe-ce/network/clientctx" +) + +func TestMsgSysAckRoundTrip(t *testing.T) { + tests := []struct { + name string + ackHandle uint32 + isBufferResponse bool + errorCode uint8 + ackData []byte + }{ + { + name: "simple non-buffer response", + ackHandle: 1, + isBufferResponse: false, + errorCode: 0, + ackData: []byte{0x00, 0x00, 0x00, 0x00}, + }, + { + name: "buffer response with small data", + ackHandle: 0x12345678, + isBufferResponse: true, + errorCode: 0, + ackData: []byte{0x01, 0x02, 0x03, 0x04, 0x05}, + }, + { + name: "error response", + ackHandle: 100, + isBufferResponse: false, + errorCode: 1, + ackData: []byte{0xDE, 0xAD, 0xBE, 0xEF}, + }, + { + name: "empty buffer response", + ackHandle: 999, + isBufferResponse: true, + errorCode: 0, + ackData: []byte{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + original := &MsgSysAck{ + AckHandle: tt.ackHandle, + IsBufferResponse: tt.isBufferResponse, + ErrorCode: tt.errorCode, + AckData: tt.ackData, + } + ctx := &clientctx.ClientContext{} + + // Build + bf := byteframe.NewByteFrame() + err := original.Build(bf, ctx) + if err != nil { + t.Fatalf("Build() error = %v", err) + } + + // Parse + bf.Seek(0, io.SeekStart) + parsed := &MsgSysAck{} + err = parsed.Parse(bf, ctx) + if err != nil { + t.Fatalf("Parse() error = %v", err) + } + + // Compare + if parsed.AckHandle != original.AckHandle { + t.Errorf("AckHandle = %d, want %d", parsed.AckHandle, original.AckHandle) + } + if parsed.IsBufferResponse != original.IsBufferResponse { + t.Errorf("IsBufferResponse = %v, want %v", parsed.IsBufferResponse, original.IsBufferResponse) + } + if parsed.ErrorCode != original.ErrorCode { + t.Errorf("ErrorCode = %d, want %d", parsed.ErrorCode, original.ErrorCode) + } + }) + } +} + +func TestMsgSysAckLargePayload(t *testing.T) { + // Test with payload larger than 0xFFFF to trigger extended size field + largeData := make([]byte, 0x10000) // 65536 bytes + for i := range largeData { + largeData[i] = byte(i % 256) + } + + original := &MsgSysAck{ + AckHandle: 1, + IsBufferResponse: true, + ErrorCode: 0, + AckData: largeData, + } + ctx := &clientctx.ClientContext{} + + // Build + bf := byteframe.NewByteFrame() + err := original.Build(bf, ctx) + if err != nil { + t.Fatalf("Build() error = %v", err) + } + + // Parse + bf.Seek(0, io.SeekStart) + parsed := &MsgSysAck{} + err = parsed.Parse(bf, ctx) + if err != nil { + t.Fatalf("Parse() error = %v", err) + } + + if len(parsed.AckData) != len(largeData) { + t.Errorf("AckData len = %d, want %d", len(parsed.AckData), len(largeData)) + } +} + +func TestMsgSysAckOpcode(t *testing.T) { + pkt := &MsgSysAck{} + if pkt.Opcode() != network.MSG_SYS_ACK { + t.Errorf("Opcode() = %s, want MSG_SYS_ACK", pkt.Opcode()) + } +} + +func TestMsgSysNopRoundTrip(t *testing.T) { + original := &MsgSysNop{} + ctx := &clientctx.ClientContext{} + + // Build + bf := byteframe.NewByteFrame() + err := original.Build(bf, ctx) + if err != nil { + t.Fatalf("Build() error = %v", err) + } + + // Should write no data + if len(bf.Data()) != 0 { + t.Errorf("MsgSysNop.Build() wrote %d bytes, want 0", len(bf.Data())) + } + + // Parse (from empty buffer) + parsed := &MsgSysNop{} + err = parsed.Parse(bf, ctx) + if err != nil { + t.Fatalf("Parse() error = %v", err) + } +} + +func TestMsgSysNopOpcode(t *testing.T) { + pkt := &MsgSysNop{} + if pkt.Opcode() != network.MSG_SYS_NOP { + t.Errorf("Opcode() = %s, want MSG_SYS_NOP", pkt.Opcode()) + } +} + +func TestMsgSysEndRoundTrip(t *testing.T) { + original := &MsgSysEnd{} + ctx := &clientctx.ClientContext{} + + // Build + bf := byteframe.NewByteFrame() + err := original.Build(bf, ctx) + if err != nil { + t.Fatalf("Build() error = %v", err) + } + + // Should write no data + if len(bf.Data()) != 0 { + t.Errorf("MsgSysEnd.Build() wrote %d bytes, want 0", len(bf.Data())) + } + + // Parse (from empty buffer) + parsed := &MsgSysEnd{} + err = parsed.Parse(bf, ctx) + if err != nil { + t.Fatalf("Parse() error = %v", err) + } +} + +func TestMsgSysEndOpcode(t *testing.T) { + pkt := &MsgSysEnd{} + if pkt.Opcode() != network.MSG_SYS_END { + t.Errorf("Opcode() = %s, want MSG_SYS_END", pkt.Opcode()) + } +} + +func TestMsgSysAckNonBufferResponse(t *testing.T) { + // Non-buffer response should always read/write 4 bytes of data + original := &MsgSysAck{ + AckHandle: 1, + IsBufferResponse: false, + ErrorCode: 0, + AckData: []byte{0xAA, 0xBB, 0xCC, 0xDD}, + } + ctx := &clientctx.ClientContext{} + + bf := byteframe.NewByteFrame() + err := original.Build(bf, ctx) + if err != nil { + t.Fatalf("Build() error = %v", err) + } + + bf.Seek(0, io.SeekStart) + parsed := &MsgSysAck{} + err = parsed.Parse(bf, ctx) + if err != nil { + t.Fatalf("Parse() error = %v", err) + } + + // Non-buffer response should have exactly 4 bytes of data + if len(parsed.AckData) != 4 { + t.Errorf("Non-buffer AckData len = %d, want 4", len(parsed.AckData)) + } +} + +func TestMsgSysAckNonBufferShortData(t *testing.T) { + // Non-buffer response with short data should pad to 4 bytes + original := &MsgSysAck{ + AckHandle: 1, + IsBufferResponse: false, + ErrorCode: 0, + AckData: []byte{0x01}, // Only 1 byte + } + ctx := &clientctx.ClientContext{} + + bf := byteframe.NewByteFrame() + err := original.Build(bf, ctx) + if err != nil { + t.Fatalf("Build() error = %v", err) + } + + bf.Seek(0, io.SeekStart) + parsed := &MsgSysAck{} + err = parsed.Parse(bf, ctx) + if err != nil { + t.Fatalf("Parse() error = %v", err) + } + + // Should still read 4 bytes + if len(parsed.AckData) != 4 { + t.Errorf("AckData len = %d, want 4", len(parsed.AckData)) + } +} + +func TestMsgSysAckBuildFormat(t *testing.T) { + pkt := &MsgSysAck{ + AckHandle: 0x12345678, + IsBufferResponse: true, + ErrorCode: 0x55, + AckData: []byte{0xAA, 0xBB}, + } + ctx := &clientctx.ClientContext{} + + bf := byteframe.NewByteFrame() + pkt.Build(bf, ctx) + + data := bf.Data() + + // Check AckHandle (big-endian) + if data[0] != 0x12 || data[1] != 0x34 || data[2] != 0x56 || data[3] != 0x78 { + t.Errorf("AckHandle bytes = %X, want 12345678", data[:4]) + } + + // Check IsBufferResponse (1 = true) + if data[4] != 1 { + t.Errorf("IsBufferResponse byte = %d, want 1", data[4]) + } + + // Check ErrorCode + if data[5] != 0x55 { + t.Errorf("ErrorCode byte = %X, want 55", data[5]) + } + + // Check payload size (2 bytes, big-endian) + if data[6] != 0x00 || data[7] != 0x02 { + t.Errorf("PayloadSize bytes = %X %X, want 00 02", data[6], data[7]) + } + + // Check actual data + if data[8] != 0xAA || data[9] != 0xBB { + t.Errorf("AckData bytes = %X %X, want AA BB", data[8], data[9]) + } +} + +func TestCorePacketsFromOpcode(t *testing.T) { + coreOpcodes := []network.PacketID{ + network.MSG_SYS_NOP, + network.MSG_SYS_END, + network.MSG_SYS_ACK, + network.MSG_SYS_PING, + } + + for _, opcode := range coreOpcodes { + t.Run(opcode.String(), func(t *testing.T) { + pkt := FromOpcode(opcode) + if pkt == nil { + t.Fatalf("FromOpcode(%s) returned nil", opcode) + } + if pkt.Opcode() != opcode { + t.Errorf("Opcode() = %s, want %s", pkt.Opcode(), opcode) + } + }) + } +} diff --git a/network/packetid_test.go b/network/packetid_test.go new file mode 100644 index 000000000..3b9f1d91d --- /dev/null +++ b/network/packetid_test.go @@ -0,0 +1,211 @@ +package network + +import ( + "testing" +) + +func TestPacketIDType(t *testing.T) { + // PacketID is based on uint16 + var p PacketID = 0xFFFF + if uint16(p) != 0xFFFF { + t.Errorf("PacketID max value = %d, want %d", uint16(p), 0xFFFF) + } +} + +func TestPacketIDConstants(t *testing.T) { + // Test critical packet IDs are correct + tests := []struct { + name string + id PacketID + expect uint16 + }{ + {"MSG_HEAD", MSG_HEAD, 0}, + {"MSG_SYS_END", MSG_SYS_END, 0x10}, + {"MSG_SYS_NOP", MSG_SYS_NOP, 0x11}, + {"MSG_SYS_ACK", MSG_SYS_ACK, 0x12}, + {"MSG_SYS_LOGIN", MSG_SYS_LOGIN, 0x14}, + {"MSG_SYS_LOGOUT", MSG_SYS_LOGOUT, 0x15}, + {"MSG_SYS_PING", MSG_SYS_PING, 0x17}, + {"MSG_SYS_TIME", MSG_SYS_TIME, 0x1A}, + {"MSG_SYS_CREATE_STAGE", MSG_SYS_CREATE_STAGE, 0x20}, + {"MSG_SYS_ENTER_STAGE", MSG_SYS_ENTER_STAGE, 0x22}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if uint16(tt.id) != tt.expect { + t.Errorf("%s = 0x%X, want 0x%X", tt.name, uint16(tt.id), tt.expect) + } + }) + } +} + +func TestPacketIDString(t *testing.T) { + // Test that String() method works for known packet IDs + tests := []struct { + id PacketID + contains string + }{ + {MSG_HEAD, "MSG_HEAD"}, + {MSG_SYS_PING, "MSG_SYS_PING"}, + {MSG_SYS_END, "MSG_SYS_END"}, + {MSG_SYS_NOP, "MSG_SYS_NOP"}, + {MSG_SYS_ACK, "MSG_SYS_ACK"}, + {MSG_SYS_LOGIN, "MSG_SYS_LOGIN"}, + {MSG_SYS_LOGOUT, "MSG_SYS_LOGOUT"}, + } + + for _, tt := range tests { + t.Run(tt.contains, func(t *testing.T) { + got := tt.id.String() + if got != tt.contains { + t.Errorf("String() = %q, want %q", got, tt.contains) + } + }) + } +} + +func TestPacketIDUnknown(t *testing.T) { + // Unknown packet ID should still have a valid string representation + unknown := PacketID(0xFFFF) + str := unknown.String() + if str == "" { + t.Error("String() for unknown PacketID should not be empty") + } +} + +func TestPacketIDZero(t *testing.T) { + // MSG_HEAD should be 0 + if MSG_HEAD != 0 { + t.Errorf("MSG_HEAD = %d, want 0", MSG_HEAD) + } +} + +func TestSystemPacketIDRange(t *testing.T) { + // System packets should be in a specific range + systemPackets := []PacketID{ + MSG_SYS_reserve01, + MSG_SYS_reserve02, + MSG_SYS_reserve03, + MSG_SYS_ADD_OBJECT, + MSG_SYS_DEL_OBJECT, + MSG_SYS_END, + MSG_SYS_NOP, + MSG_SYS_ACK, + MSG_SYS_LOGIN, + MSG_SYS_LOGOUT, + MSG_SYS_PING, + MSG_SYS_TIME, + } + + for _, pkt := range systemPackets { + // System packets should have IDs > 0 (MSG_HEAD is 0) + if pkt < MSG_SYS_reserve01 { + t.Errorf("System packet %s has ID %d, should be >= MSG_SYS_reserve01", pkt, pkt) + } + } +} + +func TestMHFPacketIDRange(t *testing.T) { + // MHF packets start at MSG_MHF_SAVEDATA (0x60) + mhfPackets := []PacketID{ + MSG_MHF_SAVEDATA, + MSG_MHF_LOADDATA, + MSG_MHF_ENUMERATE_QUEST, + MSG_MHF_ACQUIRE_TITLE, + MSG_MHF_ACQUIRE_DIST_ITEM, + MSG_MHF_ACQUIRE_MONTHLY_ITEM, + } + + for _, pkt := range mhfPackets { + // MHF packets should be >= MSG_MHF_SAVEDATA + if pkt < MSG_MHF_SAVEDATA { + t.Errorf("MHF packet %s has ID %d, should be >= MSG_MHF_SAVEDATA (%d)", pkt, pkt, MSG_MHF_SAVEDATA) + } + } +} + +func TestStagePacketIDsSequential(t *testing.T) { + // Stage-related packets should be sequential + stagePackets := []PacketID{ + MSG_SYS_CREATE_STAGE, + MSG_SYS_STAGE_DESTRUCT, + MSG_SYS_ENTER_STAGE, + MSG_SYS_BACK_STAGE, + MSG_SYS_MOVE_STAGE, + MSG_SYS_LEAVE_STAGE, + MSG_SYS_LOCK_STAGE, + MSG_SYS_UNLOCK_STAGE, + } + + for i := 1; i < len(stagePackets); i++ { + if stagePackets[i] != stagePackets[i-1]+1 { + t.Errorf("Stage packets not sequential: %s (%d) should follow %s (%d)", + stagePackets[i], stagePackets[i], stagePackets[i-1], stagePackets[i-1]) + } + } +} + +func TestPacketIDUniqueness(t *testing.T) { + // Sample of important packet IDs should be unique + packets := []PacketID{ + MSG_HEAD, + MSG_SYS_END, + MSG_SYS_NOP, + MSG_SYS_ACK, + MSG_SYS_LOGIN, + MSG_SYS_LOGOUT, + MSG_SYS_PING, + MSG_SYS_TIME, + MSG_SYS_CREATE_STAGE, + MSG_SYS_ENTER_STAGE, + MSG_MHF_SAVEDATA, + MSG_MHF_LOADDATA, + } + + seen := make(map[PacketID]bool) + for _, pkt := range packets { + if seen[pkt] { + t.Errorf("Duplicate PacketID: %s (%d)", pkt, pkt) + } + seen[pkt] = true + } +} + +func TestAcquirePacketIDs(t *testing.T) { + // Verify acquire-related packet IDs exist and are correct type + acquirePackets := []PacketID{ + MSG_MHF_ACQUIRE_DIST_ITEM, + MSG_MHF_ACQUIRE_TITLE, + MSG_MHF_ACQUIRE_ITEM, + MSG_MHF_ACQUIRE_MONTHLY_ITEM, + MSG_MHF_ACQUIRE_CAFE_ITEM, + MSG_MHF_ACQUIRE_GUILD_TRESURE, + } + + for _, pkt := range acquirePackets { + str := pkt.String() + if str == "" { + t.Errorf("PacketID %d should have a string representation", pkt) + } + } +} + +func TestGuildPacketIDs(t *testing.T) { + // Verify guild-related packet IDs + guildPackets := []PacketID{ + MSG_MHF_CREATE_GUILD, + MSG_MHF_OPERATE_GUILD, + MSG_MHF_OPERATE_GUILD_MEMBER, + MSG_MHF_INFO_GUILD, + MSG_MHF_ENUMERATE_GUILD, + MSG_MHF_UPDATE_GUILD, + } + + for _, pkt := range guildPackets { + // All guild packets should be MHF packets + if pkt < MSG_MHF_SAVEDATA { + t.Errorf("Guild packet %s should be an MHF packet (>= 0x60)", pkt) + } + } +}