From 127d3af1670af18408718e2a82f6d6c4c7809eda Mon Sep 17 00:00:00 2001 From: Houmgaor Date: Mon, 27 Oct 2025 12:18:41 +0100 Subject: [PATCH] tests(network): adds tests for network features (except mhfpacket). --- network/binpacket/msg_bin_chat_test.go | 380 ++++++++++++++ network/binpacket/msg_bin_mail_notify_test.go | 219 ++++++++ network/binpacket/msg_bin_targeted_test.go | 404 +++++++++++++++ network/clientctx/clientcontext_test.go | 31 ++ network/crypt_conn_test.go | 482 ++++++++++++++++++ network/crypt_packet_test.go | 385 ++++++++++++++ 6 files changed, 1901 insertions(+) create mode 100644 network/binpacket/msg_bin_chat_test.go create mode 100644 network/binpacket/msg_bin_mail_notify_test.go create mode 100644 network/binpacket/msg_bin_targeted_test.go create mode 100644 network/clientctx/clientcontext_test.go create mode 100644 network/crypt_conn_test.go create mode 100644 network/crypt_packet_test.go diff --git a/network/binpacket/msg_bin_chat_test.go b/network/binpacket/msg_bin_chat_test.go new file mode 100644 index 000000000..9e4baf4fb --- /dev/null +++ b/network/binpacket/msg_bin_chat_test.go @@ -0,0 +1,380 @@ +package binpacket + +import ( + "bytes" + "erupe-ce/common/byteframe" + "erupe-ce/network" + "testing" +) + +func TestMsgBinChat_Opcode(t *testing.T) { + msg := &MsgBinChat{} + if msg.Opcode() != network.MSG_SYS_CAST_BINARY { + t.Errorf("Opcode() = %v, want %v", msg.Opcode(), network.MSG_SYS_CAST_BINARY) + } +} + +func TestMsgBinChat_Build(t *testing.T) { + tests := []struct { + name string + msg *MsgBinChat + wantErr bool + validate func(*testing.T, []byte) + }{ + { + name: "basic message", + msg: &MsgBinChat{ + Unk0: 0x01, + Type: ChatTypeWorld, + Flags: 0x0000, + Message: "Hello", + SenderName: "Player1", + }, + wantErr: false, + validate: func(t *testing.T, data []byte) { + if len(data) == 0 { + t.Error("Build() returned empty data") + } + // Verify the structure starts with Unk0, Type, Flags + if data[0] != 0x01 { + t.Errorf("Unk0 = 0x%X, want 0x01", data[0]) + } + if data[1] != byte(ChatTypeWorld) { + t.Errorf("Type = 0x%X, want 0x%X", data[1], byte(ChatTypeWorld)) + } + }, + }, + { + name: "all chat types", + msg: &MsgBinChat{ + Unk0: 0x00, + Type: ChatTypeStage, + Flags: 0x1234, + Message: "Test", + SenderName: "Sender", + }, + wantErr: false, + }, + { + name: "empty message", + msg: &MsgBinChat{ + Unk0: 0x00, + Type: ChatTypeGuild, + Flags: 0x0000, + Message: "", + SenderName: "Player", + }, + wantErr: false, + }, + { + name: "empty sender", + msg: &MsgBinChat{ + Unk0: 0x00, + Type: ChatTypeParty, + Flags: 0x0000, + Message: "Hello", + SenderName: "", + }, + wantErr: false, + }, + { + name: "long message", + msg: &MsgBinChat{ + Unk0: 0x00, + Type: ChatTypeWhisper, + Flags: 0x0000, + Message: "This is a very long message that contains a lot of text to test the handling of longer strings in the binary packet format.", + SenderName: "LongNamePlayer", + }, + wantErr: false, + }, + { + name: "special characters", + msg: &MsgBinChat{ + Unk0: 0x00, + Type: ChatTypeAlliance, + Flags: 0x0000, + Message: "Hello!@#$%^&*()", + SenderName: "Player_123", + }, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + bf := byteframe.NewByteFrame() + err := tt.msg.Build(bf) + + if (err != nil) != tt.wantErr { + t.Errorf("Build() error = %v, wantErr %v", err, tt.wantErr) + return + } + + if !tt.wantErr { + data := bf.Data() + if tt.validate != nil { + tt.validate(t, data) + } + } + }) + } +} + +func TestMsgBinChat_Parse(t *testing.T) { + tests := []struct { + name string + data []byte + want *MsgBinChat + wantErr bool + }{ + { + name: "basic message", + data: []byte{ + 0x01, // Unk0 + 0x00, // Type (ChatTypeWorld) + 0x00, 0x00, // Flags + 0x00, 0x08, // lenSenderName (8) + 0x00, 0x06, // lenMessage (6) + // Message: "Hello" + null terminator (SJIS compatible ASCII) + 0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x00, + // SenderName: "Player1" + null terminator + 0x50, 0x6C, 0x61, 0x79, 0x65, 0x72, 0x31, 0x00, + }, + want: &MsgBinChat{ + Unk0: 0x01, + Type: ChatTypeWorld, + Flags: 0x0000, + Message: "Hello", + SenderName: "Player1", + }, + wantErr: false, + }, + { + name: "different chat type", + data: []byte{ + 0x00, // Unk0 + 0x02, // Type (ChatTypeGuild) + 0x12, 0x34, // Flags + 0x00, 0x05, // lenSenderName + 0x00, 0x03, // lenMessage + // Message: "Hi" + null + 0x48, 0x69, 0x00, + // SenderName: "Bob" + null + padding + 0x42, 0x6F, 0x62, 0x00, 0x00, + }, + want: &MsgBinChat{ + Unk0: 0x00, + Type: ChatTypeGuild, + Flags: 0x1234, + Message: "Hi", + SenderName: "Bob", + }, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + bf := byteframe.NewByteFrameFromBytes(tt.data) + msg := &MsgBinChat{} + + err := msg.Parse(bf) + if (err != nil) != tt.wantErr { + t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr) + return + } + + if !tt.wantErr { + if msg.Unk0 != tt.want.Unk0 { + t.Errorf("Unk0 = 0x%X, want 0x%X", msg.Unk0, tt.want.Unk0) + } + if msg.Type != tt.want.Type { + t.Errorf("Type = %v, want %v", msg.Type, tt.want.Type) + } + if msg.Flags != tt.want.Flags { + t.Errorf("Flags = 0x%X, want 0x%X", msg.Flags, tt.want.Flags) + } + if msg.Message != tt.want.Message { + t.Errorf("Message = %q, want %q", msg.Message, tt.want.Message) + } + if msg.SenderName != tt.want.SenderName { + t.Errorf("SenderName = %q, want %q", msg.SenderName, tt.want.SenderName) + } + } + }) + } +} + +func TestMsgBinChat_RoundTrip(t *testing.T) { + tests := []struct { + name string + msg *MsgBinChat + }{ + { + name: "world chat", + msg: &MsgBinChat{ + Unk0: 0x01, + Type: ChatTypeWorld, + Flags: 0x0000, + Message: "Hello World", + SenderName: "TestPlayer", + }, + }, + { + name: "stage chat", + msg: &MsgBinChat{ + Unk0: 0x00, + Type: ChatTypeStage, + Flags: 0x1234, + Message: "Stage message", + SenderName: "Player2", + }, + }, + { + name: "guild chat", + msg: &MsgBinChat{ + Unk0: 0x02, + Type: ChatTypeGuild, + Flags: 0xFFFF, + Message: "Guild announcement", + SenderName: "GuildMaster", + }, + }, + { + name: "alliance chat", + msg: &MsgBinChat{ + Unk0: 0x00, + Type: ChatTypeAlliance, + Flags: 0x0001, + Message: "Alliance msg", + SenderName: "AllyLeader", + }, + }, + { + name: "party chat", + msg: &MsgBinChat{ + Unk0: 0x01, + Type: ChatTypeParty, + Flags: 0x0000, + Message: "Party up!", + SenderName: "PartyLeader", + }, + }, + { + name: "whisper", + msg: &MsgBinChat{ + Unk0: 0x00, + Type: ChatTypeWhisper, + Flags: 0x0002, + Message: "Secret message", + SenderName: "Whisperer", + }, + }, + { + name: "empty strings", + msg: &MsgBinChat{ + Unk0: 0x00, + Type: ChatTypeWorld, + Flags: 0x0000, + Message: "", + SenderName: "", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Build + bf := byteframe.NewByteFrame() + err := tt.msg.Build(bf) + if err != nil { + t.Fatalf("Build() error = %v", err) + } + + // Parse + parsedMsg := &MsgBinChat{} + parsedBf := byteframe.NewByteFrameFromBytes(bf.Data()) + err = parsedMsg.Parse(parsedBf) + if err != nil { + t.Fatalf("Parse() error = %v", err) + } + + // Compare + if parsedMsg.Unk0 != tt.msg.Unk0 { + t.Errorf("Unk0 = 0x%X, want 0x%X", parsedMsg.Unk0, tt.msg.Unk0) + } + if parsedMsg.Type != tt.msg.Type { + t.Errorf("Type = %v, want %v", parsedMsg.Type, tt.msg.Type) + } + if parsedMsg.Flags != tt.msg.Flags { + t.Errorf("Flags = 0x%X, want 0x%X", parsedMsg.Flags, tt.msg.Flags) + } + if parsedMsg.Message != tt.msg.Message { + t.Errorf("Message = %q, want %q", parsedMsg.Message, tt.msg.Message) + } + if parsedMsg.SenderName != tt.msg.SenderName { + t.Errorf("SenderName = %q, want %q", parsedMsg.SenderName, tt.msg.SenderName) + } + }) + } +} + +func TestChatType_Values(t *testing.T) { + tests := []struct { + chatType ChatType + expected uint8 + }{ + {ChatTypeWorld, 0}, + {ChatTypeStage, 1}, + {ChatTypeGuild, 2}, + {ChatTypeAlliance, 3}, + {ChatTypeParty, 4}, + {ChatTypeWhisper, 5}, + } + + for _, tt := range tests { + if uint8(tt.chatType) != tt.expected { + t.Errorf("ChatType value = %d, want %d", uint8(tt.chatType), tt.expected) + } + } +} + +func TestMsgBinChat_BuildParseConsistency(t *testing.T) { + // Test that Build and Parse are consistent with each other + // by building, parsing, building again, and comparing + original := &MsgBinChat{ + Unk0: 0x01, + Type: ChatTypeWorld, + Flags: 0x1234, + Message: "Test message", + SenderName: "TestSender", + } + + // First build + bf1 := byteframe.NewByteFrame() + err := original.Build(bf1) + if err != nil { + t.Fatalf("First Build() error = %v", err) + } + + // Parse + parsed := &MsgBinChat{} + parsedBf := byteframe.NewByteFrameFromBytes(bf1.Data()) + err = parsed.Parse(parsedBf) + if err != nil { + t.Fatalf("Parse() error = %v", err) + } + + // Second build + bf2 := byteframe.NewByteFrame() + err = parsed.Build(bf2) + if err != nil { + t.Fatalf("Second Build() error = %v", err) + } + + // Compare the two builds + if !bytes.Equal(bf1.Data(), bf2.Data()) { + t.Errorf("Build-Parse-Build inconsistency:\nFirst: %v\nSecond: %v", bf1.Data(), bf2.Data()) + } +} diff --git a/network/binpacket/msg_bin_mail_notify_test.go b/network/binpacket/msg_bin_mail_notify_test.go new file mode 100644 index 000000000..91c8708dd --- /dev/null +++ b/network/binpacket/msg_bin_mail_notify_test.go @@ -0,0 +1,219 @@ +package binpacket + +import ( + "erupe-ce/common/byteframe" + "erupe-ce/network" + "testing" +) + +func TestMsgBinMailNotify_Opcode(t *testing.T) { + msg := MsgBinMailNotify{} + if msg.Opcode() != network.MSG_SYS_CASTED_BINARY { + t.Errorf("Opcode() = %v, want %v", msg.Opcode(), network.MSG_SYS_CASTED_BINARY) + } +} + +func TestMsgBinMailNotify_Build(t *testing.T) { + tests := []struct { + name string + senderName string + wantErr bool + validate func(*testing.T, []byte) + }{ + { + name: "basic sender name", + senderName: "Player1", + wantErr: false, + validate: func(t *testing.T, data []byte) { + if len(data) == 0 { + t.Error("Build() returned empty data") + } + // First byte should be 0x01 (Unk) + if data[0] != 0x01 { + t.Errorf("First byte = 0x%X, want 0x01", data[0]) + } + // Total length should be 1 (Unk) + 21 (padded string) + expectedLen := 1 + 21 + if len(data) != expectedLen { + t.Errorf("data length = %d, want %d", len(data), expectedLen) + } + }, + }, + { + name: "empty sender name", + senderName: "", + wantErr: false, + validate: func(t *testing.T, data []byte) { + if len(data) != 22 { // 1 + 21 + t.Errorf("data length = %d, want 22", len(data)) + } + }, + }, + { + name: "long sender name", + senderName: "VeryLongPlayerNameThatExceeds21Characters", + wantErr: false, + validate: func(t *testing.T, data []byte) { + if len(data) != 22 { // 1 + 21 (truncated/padded) + t.Errorf("data length = %d, want 22", len(data)) + } + }, + }, + { + name: "exactly 21 characters", + senderName: "ExactlyTwentyOneChar1", + wantErr: false, + validate: func(t *testing.T, data []byte) { + if len(data) != 22 { + t.Errorf("data length = %d, want 22", len(data)) + } + }, + }, + { + name: "special characters", + senderName: "Player_123", + wantErr: false, + validate: func(t *testing.T, data []byte) { + if len(data) != 22 { + t.Errorf("data length = %d, want 22", len(data)) + } + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + msg := MsgBinMailNotify{ + SenderName: tt.senderName, + } + + bf := byteframe.NewByteFrame() + err := msg.Build(bf) + + if (err != nil) != tt.wantErr { + t.Errorf("Build() error = %v, wantErr %v", err, tt.wantErr) + return + } + + if !tt.wantErr && tt.validate != nil { + tt.validate(t, bf.Data()) + } + }) + } +} + +func TestMsgBinMailNotify_Parse_Panics(t *testing.T) { + // Document that Parse() is not implemented and panics + msg := MsgBinMailNotify{} + bf := byteframe.NewByteFrame() + + defer func() { + if r := recover(); r == nil { + t.Error("Parse() did not panic, but should panic with 'implement me'") + } + }() + + // This should panic + _ = msg.Parse(bf) +} + +func TestMsgBinMailNotify_BuildMultiple(t *testing.T) { + // Test building multiple messages to ensure no state pollution + names := []string{"Player1", "Player2", "Player3"} + + for _, name := range names { + msg := MsgBinMailNotify{SenderName: name} + bf := byteframe.NewByteFrame() + err := msg.Build(bf) + if err != nil { + t.Errorf("Build(%s) error = %v", name, err) + } + + data := bf.Data() + if len(data) != 22 { + t.Errorf("Build(%s) length = %d, want 22", name, len(data)) + } + } +} + +func TestMsgBinMailNotify_PaddingBehavior(t *testing.T) { + // Test that the padded string is always 21 bytes + tests := []struct { + name string + senderName string + }{ + {"short", "A"}, + {"medium", "PlayerName"}, + {"long", "VeryVeryLongPlayerName"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + msg := MsgBinMailNotify{SenderName: tt.senderName} + bf := byteframe.NewByteFrame() + err := msg.Build(bf) + if err != nil { + t.Fatalf("Build() error = %v", err) + } + + data := bf.Data() + // Skip first byte (Unk), check remaining 21 bytes + if len(data) < 22 { + t.Fatalf("data too short: %d bytes", len(data)) + } + + paddedString := data[1:22] + if len(paddedString) != 21 { + t.Errorf("padded string length = %d, want 21", len(paddedString)) + } + }) + } +} + +func TestMsgBinMailNotify_BuildStructure(t *testing.T) { + // Test the structure of the built data + msg := MsgBinMailNotify{SenderName: "Test"} + bf := byteframe.NewByteFrame() + err := msg.Build(bf) + if err != nil { + t.Fatalf("Build() error = %v", err) + } + + data := bf.Data() + + // Check structure: 1 byte Unk + 21 bytes padded string = 22 bytes total + if len(data) != 22 { + t.Errorf("data length = %d, want 22", len(data)) + } + + // First byte should be 0x01 + if data[0] != 0x01 { + t.Errorf("Unk byte = 0x%X, want 0x01", data[0]) + } + + // The rest (21 bytes) should contain the sender name (SJIS encoded) and padding + // We can't verify exact content without knowing SJIS encoding details, + // but we can verify length + paddedPortion := data[1:] + if len(paddedPortion) != 21 { + t.Errorf("padded portion length = %d, want 21", len(paddedPortion)) + } +} + +func TestMsgBinMailNotify_ValueSemantics(t *testing.T) { + // Test that MsgBinMailNotify uses value semantics (not pointer receiver for Opcode) + msg := MsgBinMailNotify{SenderName: "Test"} + + // Should work with value + opcode := msg.Opcode() + if opcode != network.MSG_SYS_CASTED_BINARY { + t.Errorf("Opcode() = %v, want %v", opcode, network.MSG_SYS_CASTED_BINARY) + } + + // Should also work with pointer (Go allows this) + msgPtr := &MsgBinMailNotify{SenderName: "Test"} + opcode2 := msgPtr.Opcode() + if opcode2 != network.MSG_SYS_CASTED_BINARY { + t.Errorf("Opcode() on pointer = %v, want %v", opcode2, network.MSG_SYS_CASTED_BINARY) + } +} diff --git a/network/binpacket/msg_bin_targeted_test.go b/network/binpacket/msg_bin_targeted_test.go new file mode 100644 index 000000000..ca2943a08 --- /dev/null +++ b/network/binpacket/msg_bin_targeted_test.go @@ -0,0 +1,404 @@ +package binpacket + +import ( + "bytes" + "erupe-ce/common/byteframe" + "erupe-ce/network" + "testing" +) + +func TestMsgBinTargeted_Opcode(t *testing.T) { + msg := &MsgBinTargeted{} + if msg.Opcode() != network.MSG_SYS_CAST_BINARY { + t.Errorf("Opcode() = %v, want %v", msg.Opcode(), network.MSG_SYS_CAST_BINARY) + } +} + +func TestMsgBinTargeted_Build(t *testing.T) { + tests := []struct { + name string + msg *MsgBinTargeted + wantErr bool + validate func(*testing.T, []byte) + }{ + { + name: "single target with payload", + msg: &MsgBinTargeted{ + TargetCount: 1, + TargetCharIDs: []uint32{12345}, + RawDataPayload: []byte{0x01, 0x02, 0x03, 0x04}, + }, + wantErr: false, + validate: func(t *testing.T, data []byte) { + if len(data) < 2+4+4 { // 2 bytes count + 4 bytes ID + 4 bytes payload + t.Errorf("data length = %d, want at least %d", len(data), 2+4+4) + } + }, + }, + { + name: "multiple targets", + msg: &MsgBinTargeted{ + TargetCount: 3, + TargetCharIDs: []uint32{100, 200, 300}, + RawDataPayload: []byte{0xAA, 0xBB}, + }, + wantErr: false, + validate: func(t *testing.T, data []byte) { + expectedLen := 2 + (3 * 4) + 2 // count + 3 IDs + payload + if len(data) != expectedLen { + t.Errorf("data length = %d, want %d", len(data), expectedLen) + } + }, + }, + { + name: "zero targets", + msg: &MsgBinTargeted{ + TargetCount: 0, + TargetCharIDs: []uint32{}, + RawDataPayload: []byte{0xFF}, + }, + wantErr: false, + validate: func(t *testing.T, data []byte) { + if len(data) < 2+1 { // count + payload + t.Errorf("data length = %d, want at least %d", len(data), 2+1) + } + }, + }, + { + name: "empty payload", + msg: &MsgBinTargeted{ + TargetCount: 1, + TargetCharIDs: []uint32{999}, + RawDataPayload: []byte{}, + }, + wantErr: false, + validate: func(t *testing.T, data []byte) { + expectedLen := 2 + 4 // count + 1 ID + if len(data) != expectedLen { + t.Errorf("data length = %d, want %d", len(data), expectedLen) + } + }, + }, + { + name: "large payload", + msg: &MsgBinTargeted{ + TargetCount: 2, + TargetCharIDs: []uint32{1000, 2000}, + RawDataPayload: bytes.Repeat([]byte{0xCC}, 256), + }, + wantErr: false, + }, + { + name: "max uint32 target IDs", + msg: &MsgBinTargeted{ + TargetCount: 2, + TargetCharIDs: []uint32{0xFFFFFFFF, 0x12345678}, + RawDataPayload: []byte{0x01}, + }, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + bf := byteframe.NewByteFrame() + err := tt.msg.Build(bf) + + if (err != nil) != tt.wantErr { + t.Errorf("Build() error = %v, wantErr %v", err, tt.wantErr) + return + } + + if !tt.wantErr { + data := bf.Data() + if tt.validate != nil { + tt.validate(t, data) + } + } + }) + } +} + +func TestMsgBinTargeted_Parse(t *testing.T) { + tests := []struct { + name string + data []byte + want *MsgBinTargeted + wantErr bool + }{ + { + name: "single target", + data: []byte{ + 0x00, 0x01, // TargetCount = 1 + 0x00, 0x00, 0x30, 0x39, // TargetCharID = 12345 + 0xAA, 0xBB, 0xCC, // RawDataPayload + }, + want: &MsgBinTargeted{ + TargetCount: 1, + TargetCharIDs: []uint32{12345}, + RawDataPayload: []byte{0xAA, 0xBB, 0xCC}, + }, + wantErr: false, + }, + { + name: "multiple targets", + data: []byte{ + 0x00, 0x03, // TargetCount = 3 + 0x00, 0x00, 0x00, 0x64, // Target 1 = 100 + 0x00, 0x00, 0x00, 0xC8, // Target 2 = 200 + 0x00, 0x00, 0x01, 0x2C, // Target 3 = 300 + 0x01, 0x02, // RawDataPayload + }, + want: &MsgBinTargeted{ + TargetCount: 3, + TargetCharIDs: []uint32{100, 200, 300}, + RawDataPayload: []byte{0x01, 0x02}, + }, + wantErr: false, + }, + { + name: "zero targets", + data: []byte{ + 0x00, 0x00, // TargetCount = 0 + 0xFF, 0xFF, // RawDataPayload + }, + want: &MsgBinTargeted{ + TargetCount: 0, + TargetCharIDs: []uint32{}, + RawDataPayload: []byte{0xFF, 0xFF}, + }, + wantErr: false, + }, + { + name: "no payload", + data: []byte{ + 0x00, 0x01, // TargetCount = 1 + 0x00, 0x00, 0x03, 0xE7, // Target = 999 + }, + want: &MsgBinTargeted{ + TargetCount: 1, + TargetCharIDs: []uint32{999}, + RawDataPayload: []byte{}, + }, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + bf := byteframe.NewByteFrameFromBytes(tt.data) + msg := &MsgBinTargeted{} + + err := msg.Parse(bf) + if (err != nil) != tt.wantErr { + t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr) + return + } + + if !tt.wantErr { + if msg.TargetCount != tt.want.TargetCount { + t.Errorf("TargetCount = %d, want %d", msg.TargetCount, tt.want.TargetCount) + } + + if len(msg.TargetCharIDs) != len(tt.want.TargetCharIDs) { + t.Errorf("len(TargetCharIDs) = %d, want %d", len(msg.TargetCharIDs), len(tt.want.TargetCharIDs)) + } else { + for i, id := range msg.TargetCharIDs { + if id != tt.want.TargetCharIDs[i] { + t.Errorf("TargetCharIDs[%d] = %d, want %d", i, id, tt.want.TargetCharIDs[i]) + } + } + } + + if !bytes.Equal(msg.RawDataPayload, tt.want.RawDataPayload) { + t.Errorf("RawDataPayload = %v, want %v", msg.RawDataPayload, tt.want.RawDataPayload) + } + } + }) + } +} + +func TestMsgBinTargeted_RoundTrip(t *testing.T) { + tests := []struct { + name string + msg *MsgBinTargeted + }{ + { + name: "single target", + msg: &MsgBinTargeted{ + TargetCount: 1, + TargetCharIDs: []uint32{12345}, + RawDataPayload: []byte{0x01, 0x02, 0x03}, + }, + }, + { + name: "multiple targets", + msg: &MsgBinTargeted{ + TargetCount: 5, + TargetCharIDs: []uint32{100, 200, 300, 400, 500}, + RawDataPayload: []byte{0xAA, 0xBB, 0xCC, 0xDD}, + }, + }, + { + name: "zero targets", + msg: &MsgBinTargeted{ + TargetCount: 0, + TargetCharIDs: []uint32{}, + RawDataPayload: []byte{0xFF}, + }, + }, + { + name: "empty payload", + msg: &MsgBinTargeted{ + TargetCount: 2, + TargetCharIDs: []uint32{1000, 2000}, + RawDataPayload: []byte{}, + }, + }, + { + name: "large IDs and payload", + msg: &MsgBinTargeted{ + TargetCount: 3, + TargetCharIDs: []uint32{0xFFFFFFFF, 0x12345678, 0xABCDEF00}, + RawDataPayload: bytes.Repeat([]byte{0xDD}, 128), + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Build + bf := byteframe.NewByteFrame() + err := tt.msg.Build(bf) + if err != nil { + t.Fatalf("Build() error = %v", err) + } + + // Parse + parsedMsg := &MsgBinTargeted{} + parsedBf := byteframe.NewByteFrameFromBytes(bf.Data()) + err = parsedMsg.Parse(parsedBf) + if err != nil { + t.Fatalf("Parse() error = %v", err) + } + + // Compare + if parsedMsg.TargetCount != tt.msg.TargetCount { + t.Errorf("TargetCount = %d, want %d", parsedMsg.TargetCount, tt.msg.TargetCount) + } + + if len(parsedMsg.TargetCharIDs) != len(tt.msg.TargetCharIDs) { + t.Errorf("len(TargetCharIDs) = %d, want %d", len(parsedMsg.TargetCharIDs), len(tt.msg.TargetCharIDs)) + } else { + for i, id := range parsedMsg.TargetCharIDs { + if id != tt.msg.TargetCharIDs[i] { + t.Errorf("TargetCharIDs[%d] = %d, want %d", i, id, tt.msg.TargetCharIDs[i]) + } + } + } + + if !bytes.Equal(parsedMsg.RawDataPayload, tt.msg.RawDataPayload) { + t.Errorf("RawDataPayload length mismatch: got %d, want %d", len(parsedMsg.RawDataPayload), len(tt.msg.RawDataPayload)) + } + }) + } +} + +func TestMsgBinTargeted_TargetCountMismatch(t *testing.T) { + // Test that TargetCount and actual array length don't have to match + // The Build function uses the TargetCount field + msg := &MsgBinTargeted{ + TargetCount: 2, // Says 2 + TargetCharIDs: []uint32{100, 200, 300}, // But has 3 + RawDataPayload: []byte{0x01}, + } + + bf := byteframe.NewByteFrame() + err := msg.Build(bf) + if err != nil { + t.Fatalf("Build() error = %v", err) + } + + // Parse should read exactly 2 IDs as specified by TargetCount + parsedMsg := &MsgBinTargeted{} + parsedBf := byteframe.NewByteFrameFromBytes(bf.Data()) + err = parsedMsg.Parse(parsedBf) + if err != nil { + t.Fatalf("Parse() error = %v", err) + } + + if parsedMsg.TargetCount != 2 { + t.Errorf("TargetCount = %d, want 2", parsedMsg.TargetCount) + } + + if len(parsedMsg.TargetCharIDs) != 2 { + t.Errorf("len(TargetCharIDs) = %d, want 2", len(parsedMsg.TargetCharIDs)) + } +} + +func TestMsgBinTargeted_BuildParseConsistency(t *testing.T) { + original := &MsgBinTargeted{ + TargetCount: 3, + TargetCharIDs: []uint32{111, 222, 333}, + RawDataPayload: []byte{0x11, 0x22, 0x33, 0x44}, + } + + // First build + bf1 := byteframe.NewByteFrame() + err := original.Build(bf1) + if err != nil { + t.Fatalf("First Build() error = %v", err) + } + + // Parse + parsed := &MsgBinTargeted{} + parsedBf := byteframe.NewByteFrameFromBytes(bf1.Data()) + err = parsed.Parse(parsedBf) + if err != nil { + t.Fatalf("Parse() error = %v", err) + } + + // Second build + bf2 := byteframe.NewByteFrame() + err = parsed.Build(bf2) + if err != nil { + t.Fatalf("Second Build() error = %v", err) + } + + // Compare the two builds + if !bytes.Equal(bf1.Data(), bf2.Data()) { + t.Errorf("Build-Parse-Build inconsistency:\nFirst: %v\nSecond: %v", bf1.Data(), bf2.Data()) + } +} + +func TestMsgBinTargeted_PayloadForwarding(t *testing.T) { + // Test that RawDataPayload is correctly preserved + // This is important as it forwards another binpacket + originalPayload := []byte{ + 0x10, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70, 0x80, + 0x90, 0xA0, 0xB0, 0xC0, 0xD0, 0xE0, 0xF0, 0xFF, + } + + msg := &MsgBinTargeted{ + TargetCount: 1, + TargetCharIDs: []uint32{999}, + RawDataPayload: originalPayload, + } + + bf := byteframe.NewByteFrame() + err := msg.Build(bf) + if err != nil { + t.Fatalf("Build() error = %v", err) + } + + parsed := &MsgBinTargeted{} + parsedBf := byteframe.NewByteFrameFromBytes(bf.Data()) + err = parsed.Parse(parsedBf) + if err != nil { + t.Fatalf("Parse() error = %v", err) + } + + if !bytes.Equal(parsed.RawDataPayload, originalPayload) { + t.Errorf("Payload not preserved:\ngot: %v\nwant: %v", parsed.RawDataPayload, originalPayload) + } +} diff --git a/network/clientctx/clientcontext_test.go b/network/clientctx/clientcontext_test.go new file mode 100644 index 000000000..2eb333ab5 --- /dev/null +++ b/network/clientctx/clientcontext_test.go @@ -0,0 +1,31 @@ +package clientctx + +import ( + "testing" +) + +// TestClientContext_Exists verifies that the ClientContext type exists +// and can be instantiated, even though it's currently unused. +func TestClientContext_Exists(t *testing.T) { + // This test documents that ClientContext is currently an empty struct + // and is marked as unused in the codebase. + var ctx ClientContext + + // Verify it's a zero-size struct + _ = ctx + + // Just verify we can create it + ctx2 := ClientContext{} + _ = ctx2 +} + +// TestClientContext_IsEmpty verifies that ClientContext has no fields +func TestClientContext_IsEmpty(t *testing.T) { + // The struct should be empty as marked by the comment "// Unused" + // This test documents the current state of the struct + ctx := ClientContext{} + _ = ctx + + // If fields are added in the future, this test will need to be updated + // Currently it's just a placeholder/documentation test +} diff --git a/network/crypt_conn_test.go b/network/crypt_conn_test.go new file mode 100644 index 000000000..b1893714e --- /dev/null +++ b/network/crypt_conn_test.go @@ -0,0 +1,482 @@ +package network + +import ( + "bytes" + _config "erupe-ce/config" + "erupe-ce/network/crypto" + "errors" + "io" + "net" + "testing" + "time" +) + +// mockConn implements net.Conn for testing +type mockConn struct { + readData *bytes.Buffer + writeData *bytes.Buffer + closed bool + readErr error + writeErr error +} + +func newMockConn(readData []byte) *mockConn { + return &mockConn{ + readData: bytes.NewBuffer(readData), + writeData: bytes.NewBuffer(nil), + } +} + +func (m *mockConn) Read(b []byte) (n int, err error) { + if m.readErr != nil { + return 0, m.readErr + } + return m.readData.Read(b) +} + +func (m *mockConn) Write(b []byte) (n int, err error) { + if m.writeErr != nil { + return 0, m.writeErr + } + return m.writeData.Write(b) +} + +func (m *mockConn) Close() error { + m.closed = true + return nil +} + +func (m *mockConn) LocalAddr() net.Addr { return nil } +func (m *mockConn) RemoteAddr() net.Addr { return nil } +func (m *mockConn) SetDeadline(t time.Time) error { return nil } +func (m *mockConn) SetReadDeadline(t time.Time) error { return nil } +func (m *mockConn) SetWriteDeadline(t time.Time) error { return nil } + +func TestNewCryptConn(t *testing.T) { + mockConn := newMockConn(nil) + cc := NewCryptConn(mockConn) + + if cc == nil { + t.Fatal("NewCryptConn() returned nil") + } + + if cc.conn != mockConn { + t.Error("conn not set correctly") + } + + if cc.readKeyRot != 995117 { + t.Errorf("readKeyRot = %d, want 995117", cc.readKeyRot) + } + + if cc.sendKeyRot != 995117 { + t.Errorf("sendKeyRot = %d, want 995117", cc.sendKeyRot) + } + + if cc.sentPackets != 0 { + t.Errorf("sentPackets = %d, want 0", cc.sentPackets) + } + + if cc.prevRecvPacketCombinedCheck != 0 { + t.Errorf("prevRecvPacketCombinedCheck = %d, want 0", cc.prevRecvPacketCombinedCheck) + } + + if cc.prevSendPacketCombinedCheck != 0 { + t.Errorf("prevSendPacketCombinedCheck = %d, want 0", cc.prevSendPacketCombinedCheck) + } +} + +func TestCryptConn_SendPacket(t *testing.T) { + // Save original config and restore after test + originalMode := _config.ErupeConfig.RealClientMode + defer func() { + _config.ErupeConfig.RealClientMode = originalMode + }() + + tests := []struct { + name string + data []byte + }{ + { + name: "small packet", + data: []byte{0x01, 0x02, 0x03, 0x04}, + }, + { + name: "empty packet", + data: []byte{}, + }, + { + name: "larger packet", + data: bytes.Repeat([]byte{0xAA}, 256), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockConn := newMockConn(nil) + cc := NewCryptConn(mockConn) + + err := cc.SendPacket(tt.data) + if err != nil { + t.Fatalf("SendPacket() error = %v, want nil", err) + } + + written := mockConn.writeData.Bytes() + if len(written) < CryptPacketHeaderLength { + t.Fatalf("written data length = %d, want at least %d", len(written), CryptPacketHeaderLength) + } + + // Verify header was written + headerData := written[:CryptPacketHeaderLength] + header, err := NewCryptPacketHeader(headerData) + if err != nil { + t.Fatalf("Failed to parse header: %v", err) + } + + // Verify packet counter incremented + if cc.sentPackets != 1 { + t.Errorf("sentPackets = %d, want 1", cc.sentPackets) + } + + // Verify header fields + if header.KeyRotDelta != 3 { + t.Errorf("header.KeyRotDelta = %d, want 3", header.KeyRotDelta) + } + + if header.PacketNum != 0 { + t.Errorf("header.PacketNum = %d, want 0", header.PacketNum) + } + + // Verify encrypted data was written + encryptedData := written[CryptPacketHeaderLength:] + if len(encryptedData) != int(header.DataSize) { + t.Errorf("encrypted data length = %d, want %d", len(encryptedData), header.DataSize) + } + }) + } +} + +func TestCryptConn_SendPacket_MultiplePackets(t *testing.T) { + mockConn := newMockConn(nil) + cc := NewCryptConn(mockConn) + + // Send first packet + err := cc.SendPacket([]byte{0x01, 0x02}) + if err != nil { + t.Fatalf("SendPacket(1) error = %v", err) + } + + if cc.sentPackets != 1 { + t.Errorf("After 1 packet: sentPackets = %d, want 1", cc.sentPackets) + } + + // Send second packet + err = cc.SendPacket([]byte{0x03, 0x04}) + if err != nil { + t.Fatalf("SendPacket(2) error = %v", err) + } + + if cc.sentPackets != 2 { + t.Errorf("After 2 packets: sentPackets = %d, want 2", cc.sentPackets) + } + + // Send third packet + err = cc.SendPacket([]byte{0x05, 0x06}) + if err != nil { + t.Fatalf("SendPacket(3) error = %v", err) + } + + if cc.sentPackets != 3 { + t.Errorf("After 3 packets: sentPackets = %d, want 3", cc.sentPackets) + } +} + +func TestCryptConn_SendPacket_KeyRotation(t *testing.T) { + mockConn := newMockConn(nil) + cc := NewCryptConn(mockConn) + + initialKey := cc.sendKeyRot + + err := cc.SendPacket([]byte{0x01, 0x02, 0x03}) + if err != nil { + t.Fatalf("SendPacket() error = %v", err) + } + + // Key should have been rotated (keyRotDelta=3, so new key = 3 * (oldKey + 1)) + expectedKey := 3 * (initialKey + 1) + if cc.sendKeyRot != expectedKey { + t.Errorf("sendKeyRot = %d, want %d", cc.sendKeyRot, expectedKey) + } +} + +func TestCryptConn_SendPacket_WriteError(t *testing.T) { + mockConn := newMockConn(nil) + mockConn.writeErr = errors.New("write error") + cc := NewCryptConn(mockConn) + + err := cc.SendPacket([]byte{0x01, 0x02, 0x03}) + // Note: Current implementation doesn't return write error + // This test documents the behavior + if err != nil { + t.Logf("SendPacket() returned error: %v", err) + } +} + +func TestCryptConn_ReadPacket_Success(t *testing.T) { + // Save original config and restore after test + originalMode := _config.ErupeConfig.RealClientMode + _config.ErupeConfig.RealClientMode = _config.Z1 // Use older mode for simpler test + defer func() { + _config.ErupeConfig.RealClientMode = originalMode + }() + + testData := []byte{0x74, 0x65, 0x73, 0x74} // "test" + key := uint32(0) + + // Encrypt the data + encryptedData, combinedCheck, check0, check1, check2 := crypto.Crypto(testData, key, true, nil) + + // Build header + header := &CryptPacketHeader{ + Pf0: 0x03, + KeyRotDelta: 0, + PacketNum: 0, + DataSize: uint16(len(encryptedData)), + PrevPacketCombinedCheck: 0, + Check0: check0, + Check1: check1, + Check2: check2, + } + + headerBytes, _ := header.Encode() + + // Combine header and encrypted data + packet := append(headerBytes, encryptedData...) + + mockConn := newMockConn(packet) + cc := NewCryptConn(mockConn) + + // Set the key to match what we used for encryption + cc.readKeyRot = key + + result, err := cc.ReadPacket() + if err != nil { + t.Fatalf("ReadPacket() error = %v, want nil", err) + } + + if !bytes.Equal(result, testData) { + t.Errorf("ReadPacket() = %v, want %v", result, testData) + } + + if cc.prevRecvPacketCombinedCheck != combinedCheck { + t.Errorf("prevRecvPacketCombinedCheck = %d, want %d", cc.prevRecvPacketCombinedCheck, combinedCheck) + } +} + +func TestCryptConn_ReadPacket_KeyRotation(t *testing.T) { + // Save original config and restore after test + originalMode := _config.ErupeConfig.RealClientMode + _config.ErupeConfig.RealClientMode = _config.Z1 + defer func() { + _config.ErupeConfig.RealClientMode = originalMode + }() + + testData := []byte{0x01, 0x02, 0x03, 0x04} + key := uint32(995117) + keyRotDelta := byte(3) + + // Calculate expected rotated key + rotatedKey := uint32(keyRotDelta) * (key + 1) + + // Encrypt with the rotated key + encryptedData, _, check0, check1, check2 := crypto.Crypto(testData, rotatedKey, true, nil) + + // Build header with key rotation + header := &CryptPacketHeader{ + Pf0: 0x03, + KeyRotDelta: keyRotDelta, + PacketNum: 0, + DataSize: uint16(len(encryptedData)), + PrevPacketCombinedCheck: 0, + Check0: check0, + Check1: check1, + Check2: check2, + } + + headerBytes, _ := header.Encode() + packet := append(headerBytes, encryptedData...) + + mockConn := newMockConn(packet) + cc := NewCryptConn(mockConn) + cc.readKeyRot = key + + result, err := cc.ReadPacket() + if err != nil { + t.Fatalf("ReadPacket() error = %v, want nil", err) + } + + if !bytes.Equal(result, testData) { + t.Errorf("ReadPacket() = %v, want %v", result, testData) + } + + // Verify key was rotated + if cc.readKeyRot != rotatedKey { + t.Errorf("readKeyRot = %d, want %d", cc.readKeyRot, rotatedKey) + } +} + +func TestCryptConn_ReadPacket_NoKeyRotation(t *testing.T) { + // Save original config and restore after test + originalMode := _config.ErupeConfig.RealClientMode + _config.ErupeConfig.RealClientMode = _config.Z1 + defer func() { + _config.ErupeConfig.RealClientMode = originalMode + }() + + testData := []byte{0x01, 0x02} + key := uint32(12345) + + // Encrypt without key rotation + encryptedData, _, check0, check1, check2 := crypto.Crypto(testData, key, true, nil) + + header := &CryptPacketHeader{ + Pf0: 0x03, + KeyRotDelta: 0, // No rotation + PacketNum: 0, + DataSize: uint16(len(encryptedData)), + PrevPacketCombinedCheck: 0, + Check0: check0, + Check1: check1, + Check2: check2, + } + + headerBytes, _ := header.Encode() + packet := append(headerBytes, encryptedData...) + + mockConn := newMockConn(packet) + cc := NewCryptConn(mockConn) + cc.readKeyRot = key + + originalKeyRot := cc.readKeyRot + + result, err := cc.ReadPacket() + if err != nil { + t.Fatalf("ReadPacket() error = %v, want nil", err) + } + + if !bytes.Equal(result, testData) { + t.Errorf("ReadPacket() = %v, want %v", result, testData) + } + + // Verify key was NOT rotated + if cc.readKeyRot != originalKeyRot { + t.Errorf("readKeyRot = %d, want %d (should not have changed)", cc.readKeyRot, originalKeyRot) + } +} + +func TestCryptConn_ReadPacket_HeaderReadError(t *testing.T) { + mockConn := newMockConn([]byte{0x01, 0x02}) // Only 2 bytes, header needs 14 + cc := NewCryptConn(mockConn) + + _, err := cc.ReadPacket() + if err == nil { + t.Fatal("ReadPacket() error = nil, want error") + } + + if err != io.EOF && err != io.ErrUnexpectedEOF { + t.Errorf("ReadPacket() error = %v, want io.EOF or io.ErrUnexpectedEOF", err) + } +} + +func TestCryptConn_ReadPacket_InvalidHeader(t *testing.T) { + // Create invalid header data (wrong endianness or malformed) + invalidHeader := []byte{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF} + mockConn := newMockConn(invalidHeader) + cc := NewCryptConn(mockConn) + + _, err := cc.ReadPacket() + if err == nil { + t.Fatal("ReadPacket() error = nil, want error") + } +} + +func TestCryptConn_ReadPacket_BodyReadError(t *testing.T) { + // Save original config and restore after test + originalMode := _config.ErupeConfig.RealClientMode + _config.ErupeConfig.RealClientMode = _config.Z1 + defer func() { + _config.ErupeConfig.RealClientMode = originalMode + }() + + // Create valid header but incomplete body + header := &CryptPacketHeader{ + Pf0: 0x03, + KeyRotDelta: 0, + PacketNum: 0, + DataSize: 100, // Claim 100 bytes + PrevPacketCombinedCheck: 0, + Check0: 0x1234, + Check1: 0x5678, + Check2: 0x9ABC, + } + + headerBytes, _ := header.Encode() + incompleteBody := []byte{0x01, 0x02, 0x03} // Only 3 bytes, not 100 + + packet := append(headerBytes, incompleteBody...) + + mockConn := newMockConn(packet) + cc := NewCryptConn(mockConn) + + _, err := cc.ReadPacket() + if err == nil { + t.Fatal("ReadPacket() error = nil, want error") + } +} + +func TestCryptConn_ReadPacket_ChecksumMismatch(t *testing.T) { + // Save original config and restore after test + originalMode := _config.ErupeConfig.RealClientMode + _config.ErupeConfig.RealClientMode = _config.Z1 + defer func() { + _config.ErupeConfig.RealClientMode = originalMode + }() + + testData := []byte{0x01, 0x02, 0x03, 0x04} + key := uint32(0) + + encryptedData, _, _, _, _ := crypto.Crypto(testData, key, true, nil) + + // Build header with WRONG checksums + header := &CryptPacketHeader{ + Pf0: 0x03, + KeyRotDelta: 0, + PacketNum: 0, + DataSize: uint16(len(encryptedData)), + PrevPacketCombinedCheck: 0, + Check0: 0xFFFF, // Wrong checksum + Check1: 0xFFFF, // Wrong checksum + Check2: 0xFFFF, // Wrong checksum + } + + headerBytes, _ := header.Encode() + packet := append(headerBytes, encryptedData...) + + mockConn := newMockConn(packet) + cc := NewCryptConn(mockConn) + cc.readKeyRot = key + + _, err := cc.ReadPacket() + if err == nil { + t.Fatal("ReadPacket() error = nil, want error for checksum mismatch") + } + + expectedErr := "decrypted data checksum doesn't match header" + if err.Error() != expectedErr { + t.Errorf("ReadPacket() error = %q, want %q", err.Error(), expectedErr) + } +} + +func TestCryptConn_Interface(t *testing.T) { + // Test that CryptConn implements Conn interface + var _ Conn = (*CryptConn)(nil) +} diff --git a/network/crypt_packet_test.go b/network/crypt_packet_test.go new file mode 100644 index 000000000..9a92f9bca --- /dev/null +++ b/network/crypt_packet_test.go @@ -0,0 +1,385 @@ +package network + +import ( + "bytes" + "testing" +) + +func TestNewCryptPacketHeader_ValidData(t *testing.T) { + tests := []struct { + name string + data []byte + expected *CryptPacketHeader + }{ + { + name: "basic header", + data: []byte{ + 0x03, // Pf0 + 0x03, // KeyRotDelta + 0x00, 0x01, // PacketNum (1) + 0x00, 0x0A, // DataSize (10) + 0x00, 0x00, // PrevPacketCombinedCheck (0) + 0x12, 0x34, // Check0 (0x1234) + 0x56, 0x78, // Check1 (0x5678) + 0x9A, 0xBC, // Check2 (0x9ABC) + }, + expected: &CryptPacketHeader{ + Pf0: 0x03, + KeyRotDelta: 0x03, + PacketNum: 1, + DataSize: 10, + PrevPacketCombinedCheck: 0, + Check0: 0x1234, + Check1: 0x5678, + Check2: 0x9ABC, + }, + }, + { + name: "all zero values", + data: []byte{ + 0x00, 0x00, + 0x00, 0x00, + 0x00, 0x00, + 0x00, 0x00, + 0x00, 0x00, + 0x00, 0x00, + 0x00, 0x00, + }, + expected: &CryptPacketHeader{ + Pf0: 0x00, + KeyRotDelta: 0x00, + PacketNum: 0, + DataSize: 0, + PrevPacketCombinedCheck: 0, + Check0: 0, + Check1: 0, + Check2: 0, + }, + }, + { + name: "max values", + data: []byte{ + 0xFF, 0xFF, + 0xFF, 0xFF, + 0xFF, 0xFF, + 0xFF, 0xFF, + 0xFF, 0xFF, + 0xFF, 0xFF, + 0xFF, 0xFF, + }, + expected: &CryptPacketHeader{ + Pf0: 0xFF, + KeyRotDelta: 0xFF, + PacketNum: 0xFFFF, + DataSize: 0xFFFF, + PrevPacketCombinedCheck: 0xFFFF, + Check0: 0xFFFF, + Check1: 0xFFFF, + Check2: 0xFFFF, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := NewCryptPacketHeader(tt.data) + if err != nil { + t.Fatalf("NewCryptPacketHeader() error = %v, want nil", err) + } + + if result.Pf0 != tt.expected.Pf0 { + t.Errorf("Pf0 = 0x%X, want 0x%X", result.Pf0, tt.expected.Pf0) + } + if result.KeyRotDelta != tt.expected.KeyRotDelta { + t.Errorf("KeyRotDelta = 0x%X, want 0x%X", result.KeyRotDelta, tt.expected.KeyRotDelta) + } + if result.PacketNum != tt.expected.PacketNum { + t.Errorf("PacketNum = 0x%X, want 0x%X", result.PacketNum, tt.expected.PacketNum) + } + if result.DataSize != tt.expected.DataSize { + t.Errorf("DataSize = 0x%X, want 0x%X", result.DataSize, tt.expected.DataSize) + } + if result.PrevPacketCombinedCheck != tt.expected.PrevPacketCombinedCheck { + t.Errorf("PrevPacketCombinedCheck = 0x%X, want 0x%X", result.PrevPacketCombinedCheck, tt.expected.PrevPacketCombinedCheck) + } + if result.Check0 != tt.expected.Check0 { + t.Errorf("Check0 = 0x%X, want 0x%X", result.Check0, tt.expected.Check0) + } + if result.Check1 != tt.expected.Check1 { + t.Errorf("Check1 = 0x%X, want 0x%X", result.Check1, tt.expected.Check1) + } + if result.Check2 != tt.expected.Check2 { + t.Errorf("Check2 = 0x%X, want 0x%X", result.Check2, tt.expected.Check2) + } + }) + } +} + +func TestNewCryptPacketHeader_InvalidData(t *testing.T) { + tests := []struct { + name string + data []byte + }{ + { + name: "empty data", + data: []byte{}, + }, + { + name: "too short - 1 byte", + data: []byte{0x03}, + }, + { + name: "too short - 13 bytes", + data: []byte{0x03, 0x03, 0x00, 0x01, 0x00, 0x0A, 0x00, 0x00, 0x12, 0x34, 0x56, 0x78, 0x9A}, + }, + { + name: "too short - 7 bytes", + data: []byte{0x03, 0x03, 0x00, 0x01, 0x00, 0x0A, 0x00}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := NewCryptPacketHeader(tt.data) + if err == nil { + t.Fatal("NewCryptPacketHeader() error = nil, want error") + } + }) + } +} + +func TestNewCryptPacketHeader_ExtraDataIgnored(t *testing.T) { + // Test that extra data beyond 14 bytes is ignored + data := []byte{ + 0x03, 0x03, + 0x00, 0x01, + 0x00, 0x0A, + 0x00, 0x00, + 0x12, 0x34, + 0x56, 0x78, + 0x9A, 0xBC, + 0xFF, 0xFF, 0xFF, // Extra bytes + } + + result, err := NewCryptPacketHeader(data) + if err != nil { + t.Fatalf("NewCryptPacketHeader() error = %v, want nil", err) + } + + expected := &CryptPacketHeader{ + Pf0: 0x03, + KeyRotDelta: 0x03, + PacketNum: 1, + DataSize: 10, + PrevPacketCombinedCheck: 0, + Check0: 0x1234, + Check1: 0x5678, + Check2: 0x9ABC, + } + + if result.Pf0 != expected.Pf0 || result.KeyRotDelta != expected.KeyRotDelta || + result.PacketNum != expected.PacketNum || result.DataSize != expected.DataSize { + t.Errorf("Extra data affected parsing") + } +} + +func TestCryptPacketHeader_Encode(t *testing.T) { + tests := []struct { + name string + header *CryptPacketHeader + expected []byte + }{ + { + name: "basic header", + header: &CryptPacketHeader{ + Pf0: 0x03, + KeyRotDelta: 0x03, + PacketNum: 1, + DataSize: 10, + PrevPacketCombinedCheck: 0, + Check0: 0x1234, + Check1: 0x5678, + Check2: 0x9ABC, + }, + expected: []byte{ + 0x03, 0x03, + 0x00, 0x01, + 0x00, 0x0A, + 0x00, 0x00, + 0x12, 0x34, + 0x56, 0x78, + 0x9A, 0xBC, + }, + }, + { + name: "all zeros", + header: &CryptPacketHeader{ + Pf0: 0x00, + KeyRotDelta: 0x00, + PacketNum: 0, + DataSize: 0, + PrevPacketCombinedCheck: 0, + Check0: 0, + Check1: 0, + Check2: 0, + }, + expected: []byte{ + 0x00, 0x00, + 0x00, 0x00, + 0x00, 0x00, + 0x00, 0x00, + 0x00, 0x00, + 0x00, 0x00, + 0x00, 0x00, + }, + }, + { + name: "max values", + header: &CryptPacketHeader{ + Pf0: 0xFF, + KeyRotDelta: 0xFF, + PacketNum: 0xFFFF, + DataSize: 0xFFFF, + PrevPacketCombinedCheck: 0xFFFF, + Check0: 0xFFFF, + Check1: 0xFFFF, + Check2: 0xFFFF, + }, + expected: []byte{ + 0xFF, 0xFF, + 0xFF, 0xFF, + 0xFF, 0xFF, + 0xFF, 0xFF, + 0xFF, 0xFF, + 0xFF, 0xFF, + 0xFF, 0xFF, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := tt.header.Encode() + if err != nil { + t.Fatalf("Encode() error = %v, want nil", err) + } + + if !bytes.Equal(result, tt.expected) { + t.Errorf("Encode() = %v, want %v", result, tt.expected) + } + + // Check that the length is always 14 + if len(result) != CryptPacketHeaderLength { + t.Errorf("Encode() length = %d, want %d", len(result), CryptPacketHeaderLength) + } + }) + } +} + +func TestCryptPacketHeader_RoundTrip(t *testing.T) { + tests := []struct { + name string + header *CryptPacketHeader + }{ + { + name: "basic header", + header: &CryptPacketHeader{ + Pf0: 0x03, + KeyRotDelta: 0x03, + PacketNum: 100, + DataSize: 1024, + PrevPacketCombinedCheck: 0x1234, + Check0: 0xABCD, + Check1: 0xEF01, + Check2: 0x2345, + }, + }, + { + name: "zero values", + header: &CryptPacketHeader{ + Pf0: 0x00, + KeyRotDelta: 0x00, + PacketNum: 0, + DataSize: 0, + PrevPacketCombinedCheck: 0, + Check0: 0, + Check1: 0, + Check2: 0, + }, + }, + { + name: "max values", + header: &CryptPacketHeader{ + Pf0: 0xFF, + KeyRotDelta: 0xFF, + PacketNum: 0xFFFF, + DataSize: 0xFFFF, + PrevPacketCombinedCheck: 0xFFFF, + Check0: 0xFFFF, + Check1: 0xFFFF, + Check2: 0xFFFF, + }, + }, + { + name: "realistic values", + header: &CryptPacketHeader{ + Pf0: 0x07, + KeyRotDelta: 0x03, + PacketNum: 523, + DataSize: 2048, + PrevPacketCombinedCheck: 0x2A56, + Check0: 0x06EA, + Check1: 0x0215, + Check2: 0x8FB3, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Encode + encoded, err := tt.header.Encode() + if err != nil { + t.Fatalf("Encode() error = %v, want nil", err) + } + + // Decode + decoded, err := NewCryptPacketHeader(encoded) + if err != nil { + t.Fatalf("NewCryptPacketHeader() error = %v, want nil", err) + } + + // Compare + if decoded.Pf0 != tt.header.Pf0 { + t.Errorf("Pf0 = 0x%X, want 0x%X", decoded.Pf0, tt.header.Pf0) + } + if decoded.KeyRotDelta != tt.header.KeyRotDelta { + t.Errorf("KeyRotDelta = 0x%X, want 0x%X", decoded.KeyRotDelta, tt.header.KeyRotDelta) + } + if decoded.PacketNum != tt.header.PacketNum { + t.Errorf("PacketNum = 0x%X, want 0x%X", decoded.PacketNum, tt.header.PacketNum) + } + if decoded.DataSize != tt.header.DataSize { + t.Errorf("DataSize = 0x%X, want 0x%X", decoded.DataSize, tt.header.DataSize) + } + if decoded.PrevPacketCombinedCheck != tt.header.PrevPacketCombinedCheck { + t.Errorf("PrevPacketCombinedCheck = 0x%X, want 0x%X", decoded.PrevPacketCombinedCheck, tt.header.PrevPacketCombinedCheck) + } + if decoded.Check0 != tt.header.Check0 { + t.Errorf("Check0 = 0x%X, want 0x%X", decoded.Check0, tt.header.Check0) + } + if decoded.Check1 != tt.header.Check1 { + t.Errorf("Check1 = 0x%X, want 0x%X", decoded.Check1, tt.header.Check1) + } + if decoded.Check2 != tt.header.Check2 { + t.Errorf("Check2 = 0x%X, want 0x%X", decoded.Check2, tt.header.Check2) + } + }) + } +} + +func TestCryptPacketHeaderLength_Constant(t *testing.T) { + if CryptPacketHeaderLength != 14 { + t.Errorf("CryptPacketHeaderLength = %d, want 14", CryptPacketHeaderLength) + } +}