diff --git a/server/channelserver/handlers_achievement_test.go b/server/channelserver/handlers_achievement_test.go index f2becb8aa..151b2bb75 100644 --- a/server/channelserver/handlers_achievement_test.go +++ b/server/channelserver/handlers_achievement_test.go @@ -2,6 +2,8 @@ package channelserver import ( "testing" + + "erupe-ce/network/mhfpacket" ) func TestGetAchData_Level0(t *testing.T) { @@ -189,3 +191,52 @@ func TestAchievementCurveMap_Coverage(t *testing.T) { } } } + +func TestHandleMsgMhfSetCaAchievementHist(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfSetCaAchievementHist{ + AckHandle: 12345, + } + + handleMsgMhfSetCaAchievementHist(session, pkt) + + // Verify response packet was queued + select { + case p := <-session.sendPackets: + if len(p.data) == 0 { + t.Error("Response packet should have data") + } + default: + t.Error("No response packet queued") + } +} + +// Test empty achievement handlers don't panic +func TestEmptyAchievementHandlers(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + tests := []struct { + name string + handler func(s *Session, p mhfpacket.MHFPacket) + }{ + {"handleMsgMhfResetAchievement", handleMsgMhfResetAchievement}, + {"handleMsgMhfPaymentAchievement", handleMsgMhfPaymentAchievement}, + {"handleMsgMhfDisplayedAchievement", handleMsgMhfDisplayedAchievement}, + {"handleMsgMhfGetCaAchievementHist", handleMsgMhfGetCaAchievementHist}, + {"handleMsgMhfSetCaAchievement", handleMsgMhfSetCaAchievement}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + defer func() { + if r := recover(); r != nil { + t.Errorf("%s panicked: %v", tt.name, r) + } + }() + tt.handler(session, nil) + }) + } +} diff --git a/server/channelserver/handlers_core_test.go b/server/channelserver/handlers_core_test.go index 127e022bc..84a470059 100644 --- a/server/channelserver/handlers_core_test.go +++ b/server/channelserver/handlers_core_test.go @@ -365,3 +365,320 @@ func TestHandleMsgSysTerminalLog_WithEntries(t *testing.T) { t.Error("No response packet queued") } } + +// Test ping handler +func TestHandleMsgSysPing(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgSysPing{ + AckHandle: 12345, + } + + handleMsgSysPing(session, pkt) + + select { + case p := <-session.sendPackets: + if len(p.data) == 0 { + t.Error("Response packet should have data") + } + default: + t.Error("No response packet queued") + } +} + +// Test time handler +func TestHandleMsgSysTime(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgSysTime{ + GetRemoteTime: true, + } + + handleMsgSysTime(session, pkt) + + select { + case p := <-session.sendPackets: + if len(p.data) == 0 { + t.Error("Response packet should have data") + } + default: + t.Error("No response packet queued") + } +} + +// Test issue logkey handler +func TestHandleMsgSysIssueLogkey(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgSysIssueLogkey{ + AckHandle: 12345, + } + + handleMsgSysIssueLogkey(session, pkt) + + // Verify logkey was set + if len(session.logKey) != 16 { + t.Errorf("logKey length = %d, want 16", len(session.logKey)) + } + + select { + case p := <-session.sendPackets: + if len(p.data) == 0 { + t.Error("Response packet should have data") + } + default: + t.Error("No response packet queued") + } +} + +// Test record log handler +func TestHandleMsgSysRecordLog(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + // Setup stage + stage := NewStage("test_stage") + session.stage = stage + stage.reservedClientSlots[session.charID] = true + + pkt := &mhfpacket.MsgSysRecordLog{ + AckHandle: 12345, + } + + handleMsgSysRecordLog(session, pkt) + + // Verify charID removed from reserved slots + if _, exists := stage.reservedClientSlots[session.charID]; exists { + t.Error("charID should be removed from reserved slots") + } + + select { + case p := <-session.sendPackets: + if len(p.data) == 0 { + t.Error("Response packet should have data") + } + default: + t.Error("No response packet queued") + } +} + +// Test unlock global sema handler +func TestHandleMsgSysUnlockGlobalSema(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgSysUnlockGlobalSema{ + AckHandle: 12345, + } + + handleMsgSysUnlockGlobalSema(session, pkt) + + select { + case p := <-session.sendPackets: + if len(p.data) == 0 { + t.Error("Response packet should have data") + } + default: + t.Error("No response packet queued") + } +} + +// Test more empty handlers +func TestHandleMsgSysSetStatus(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + defer func() { + if r := recover(); r != nil { + t.Errorf("handleMsgSysSetStatus panicked: %v", r) + } + }() + + handleMsgSysSetStatus(session, nil) +} + +func TestHandleMsgSysEcho(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + defer func() { + if r := recover(); r != nil { + t.Errorf("handleMsgSysEcho panicked: %v", r) + } + }() + + handleMsgSysEcho(session, nil) +} + +func TestHandleMsgSysUpdateRight(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + defer func() { + if r := recover(); r != nil { + t.Errorf("handleMsgSysUpdateRight panicked: %v", r) + } + }() + + handleMsgSysUpdateRight(session, nil) +} + +func TestHandleMsgSysAuthQuery(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + defer func() { + if r := recover(); r != nil { + t.Errorf("handleMsgSysAuthQuery panicked: %v", r) + } + }() + + handleMsgSysAuthQuery(session, nil) +} + +func TestHandleMsgSysAuthTerminal(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + defer func() { + if r := recover(); r != nil { + t.Errorf("handleMsgSysAuthTerminal panicked: %v", r) + } + }() + + handleMsgSysAuthTerminal(session, nil) +} + +// Test lock global sema handler +func TestHandleMsgSysLockGlobalSema_NoMatch(t *testing.T) { + server := createMockServer() + server.GlobalID = "test-server" + server.Channels = []*Server{} + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgSysLockGlobalSema{ + AckHandle: 12345, + UserIDString: "user123", + ServerChannelIDString: "channel1", + } + + handleMsgSysLockGlobalSema(session, pkt) + + select { + case p := <-session.sendPackets: + if len(p.data) == 0 { + t.Error("Response packet should have data") + } + default: + t.Error("No response packet queued") + } +} + +func TestHandleMsgSysLockGlobalSema_WithChannel(t *testing.T) { + server := createMockServer() + server.GlobalID = "test-server" + + // Create a mock channel with stages + channel := &Server{ + GlobalID: "other-server", + stages: make(map[string]*Stage), + } + channel.stages["stage_user123"] = NewStage("stage_user123") + server.Channels = []*Server{channel} + + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgSysLockGlobalSema{ + AckHandle: 12345, + UserIDString: "user123", + ServerChannelIDString: "channel1", + } + + handleMsgSysLockGlobalSema(session, pkt) + + select { + case p := <-session.sendPackets: + if len(p.data) == 0 { + t.Error("Response packet should have data") + } + default: + t.Error("No response packet queued") + } +} + +func TestHandleMsgSysLockGlobalSema_SameServer(t *testing.T) { + server := createMockServer() + server.GlobalID = "test-server" + + // Create a mock channel with same GlobalID + channel := &Server{ + GlobalID: "test-server", + stages: make(map[string]*Stage), + } + channel.stages["stage_user456"] = NewStage("stage_user456") + server.Channels = []*Server{channel} + + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgSysLockGlobalSema{ + AckHandle: 12345, + UserIDString: "user456", + ServerChannelIDString: "channel2", + } + + handleMsgSysLockGlobalSema(session, pkt) + + select { + case p := <-session.sendPackets: + if len(p.data) == 0 { + t.Error("Response packet should have data") + } + default: + t.Error("No response packet queued") + } +} + +func TestHandleMsgMhfAnnounce(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfAnnounce{ + AckHandle: 12345, + IPAddress: 0x7F000001, // 127.0.0.1 + Port: 54001, + StageID: []byte("test_stage"), + Type: 1, + } + + handleMsgMhfAnnounce(session, pkt) + + select { + case p := <-session.sendPackets: + if len(p.data) == 0 { + t.Error("Response packet should have data") + } + default: + t.Error("No response packet queued") + } +} + +func TestHandleMsgSysRightsReload(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgSysRightsReload{ + AckHandle: 12345, + } + + // This will panic due to nil db, which is expected in test + defer func() { + if r := recover(); r != nil { + t.Log("Expected panic due to nil database in test") + } + }() + + handleMsgSysRightsReload(session, pkt) +} diff --git a/server/channelserver/handlers_discord_test.go b/server/channelserver/handlers_discord_test.go new file mode 100644 index 000000000..9e5e4c050 --- /dev/null +++ b/server/channelserver/handlers_discord_test.go @@ -0,0 +1,163 @@ +package channelserver + +import ( + "testing" +) + +func TestPlayerStruct(t *testing.T) { + player := Player{ + CharName: "TestPlayer", + QuestID: 5, + } + + if player.CharName != "TestPlayer" { + t.Errorf("CharName = %s, want TestPlayer", player.CharName) + } + if player.QuestID != 5 { + t.Errorf("QuestID = %d, want 5", player.QuestID) + } +} + +func TestGetPlayerSlice_EmptyServer(t *testing.T) { + server := createMockServer() + server.Channels = []*Server{} + + players := getPlayerSlice(server) + + if len(players) != 0 { + t.Errorf("Expected 0 players, got %d", len(players)) + } +} + +func TestGetPlayerSlice_WithChannel(t *testing.T) { + server := createMockServer() + + // Create a channel with stages + channel := &Server{ + stages: make(map[string]*Stage), + } + + // Create a stage with clients + stage := NewStage("test_stage") + session := createMockSession(1, server) + session.Name = "Player1" + stage.clients[session] = session.charID + + channel.stages["test_stage"] = stage + server.Channels = []*Server{channel} + + players := getPlayerSlice(server) + + if len(players) != 1 { + t.Errorf("Expected 1 player, got %d", len(players)) + } + if len(players) > 0 && players[0].CharName != "Player1" { + t.Errorf("Expected CharName Player1, got %s", players[0].CharName) + } +} + +func TestGetPlayerSlice_MultiplePlayersMultipleStages(t *testing.T) { + server := createMockServer() + + channel := &Server{ + stages: make(map[string]*Stage), + } + + // Stage 1 with one player + stage1 := NewStage("stage1") + session1 := createMockSession(1, server) + session1.Name = "Player1" + stage1.clients[session1] = session1.charID + channel.stages["stage1"] = stage1 + + // Stage 2 with two players + stage2 := NewStage("stage2") + session2 := createMockSession(2, server) + session2.Name = "Player2" + session3 := createMockSession(3, server) + session3.Name = "Player3" + stage2.clients[session2] = session2.charID + stage2.clients[session3] = session3.charID + channel.stages["stage2"] = stage2 + + server.Channels = []*Server{channel} + + players := getPlayerSlice(server) + + if len(players) != 3 { + t.Errorf("Expected 3 players, got %d", len(players)) + } +} + +func TestGetPlayerSlice_EmptyStage(t *testing.T) { + server := createMockServer() + + channel := &Server{ + stages: make(map[string]*Stage), + } + + // Empty stage (no clients) + emptyStage := NewStage("empty_stage") + channel.stages["empty_stage"] = emptyStage + + server.Channels = []*Server{channel} + + players := getPlayerSlice(server) + + if len(players) != 0 { + t.Errorf("Expected 0 players from empty stage, got %d", len(players)) + } +} + +func TestGetCharacterList_EmptyServer(t *testing.T) { + server := createMockServer() + server.Channels = []*Server{} + + result := getCharacterList(server) + + expected := "===== Online: 0 =====\n" + if result != expected { + t.Errorf("Expected %q, got %q", expected, result) + } +} + +func TestGetCharacterList_WithPlayers(t *testing.T) { + server := createMockServer() + + channel := &Server{ + stages: make(map[string]*Stage), + } + + stage := NewStage("lobby") + session := createMockSession(1, server) + session.Name = "Hunter1" + stage.clients[session] = session.charID + channel.stages["lobby"] = stage + + server.Channels = []*Server{channel} + + result := getCharacterList(server) + + // Should contain the online count + if len(result) == 0 { + t.Error("Expected non-empty result") + } + + // Should contain "Online: 1" + if !contains(result, "Online: 1") { + t.Errorf("Expected result to contain 'Online: 1', got %q", result) + } +} + +func contains(s, substr string) bool { + return len(s) >= len(substr) && (s == substr || len(s) > 0 && containsAt(s, substr)) +} + +func containsAt(s, substr string) bool { + for i := 0; i <= len(s)-len(substr); i++ { + if s[i:i+len(substr)] == substr { + return true + } + } + return false +} diff --git a/server/channelserver/handlers_diva_test.go b/server/channelserver/handlers_diva_test.go index 9930bc6ff..414078e80 100644 --- a/server/channelserver/handlers_diva_test.go +++ b/server/channelserver/handlers_diva_test.go @@ -295,3 +295,49 @@ func TestGenerateDivaTimestamps_Debug(t *testing.T) { }) } } + +func TestGenerateDivaTimestamps_Debug_StartGreaterThan3(t *testing.T) { + // Test debug mode with start > 3 (falls through to non-debug path) + server := createMockServer() + session := createMockSession(1, server) + + // With debug=true but start > 3, should fall through to non-debug path + // This will try to access DB which will panic, so we catch it + defer func() { + if r := recover(); r != nil { + t.Log("Expected panic due to nil database in test") + } + }() + + timestamps := generateDivaTimestamps(session, 100, true) + if len(timestamps) != 6 { + t.Errorf("Expected 6 timestamps, got %d", len(timestamps)) + } +} + +func TestGenerateDivaTimestamps_NonDebug_WithValidStart(t *testing.T) { + // Test non-debug mode with valid start timestamp (not expired) + server := createMockServer() + session := createMockSession(1, server) + + // Use a start time in the future (won't trigger cleanup) + futureStart := uint32(TimeAdjusted().Unix() + 1000000) // Far in the future + + timestamps := generateDivaTimestamps(session, futureStart, false) + if len(timestamps) != 6 { + t.Errorf("Expected 6 timestamps, got %d", len(timestamps)) + } + + // Verify first timestamp matches start + if timestamps[0] != futureStart { + t.Errorf("First timestamp should match start, got %d want %d", timestamps[0], futureStart) + } + + // Verify timestamp intervals + if timestamps[1] != timestamps[0]+601200 { + t.Error("Second timestamp should be start + 601200") + } + if timestamps[2] != timestamps[1]+3900 { + t.Error("Third timestamp should be second + 3900") + } +} diff --git a/server/channelserver/handlers_festa_test.go b/server/channelserver/handlers_festa_test.go index 2f21dbd81..99fa0decb 100644 --- a/server/channelserver/handlers_festa_test.go +++ b/server/channelserver/handlers_festa_test.go @@ -110,3 +110,47 @@ func TestHandleMsgMhfEnumerateRanking_State3(t *testing.T) { t.Error("No response packet queued") } } + +func TestHandleMsgMhfVoteFesta(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfVoteFesta{ + AckHandle: 12345, + } + + handleMsgMhfVoteFesta(session, pkt) + + select { + case p := <-session.sendPackets: + if len(p.data) == 0 { + t.Error("Response packet should have data") + } + default: + t.Error("No response packet queued") + } +} + +func TestEmptyFestaHandlers(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + tests := []struct { + name string + handler func(s *Session, p mhfpacket.MHFPacket) + }{ + {"handleMsgMhfEntryTournament", handleMsgMhfEntryTournament}, + {"handleMsgMhfAcquireTournament", handleMsgMhfAcquireTournament}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + defer func() { + if r := recover(); r != nil { + t.Errorf("%s panicked: %v", tt.name, r) + } + }() + tt.handler(session, nil) + }) + } +} diff --git a/server/channelserver/handlers_house_test.go b/server/channelserver/handlers_house_test.go new file mode 100644 index 000000000..2e719808c --- /dev/null +++ b/server/channelserver/handlers_house_test.go @@ -0,0 +1,203 @@ +package channelserver + +import ( + "testing" + + "erupe-ce/common/byteframe" + "erupe-ce/network/mhfpacket" +) + +func TestBoxToBytes_EmptyItemBox(t *testing.T) { + stacks := []mhfpacket.WarehouseStack{} + result := boxToBytes(stacks, "item") + + bf := byteframe.NewByteFrameFromBytes(result) + numStacks := bf.ReadUint16() + if numStacks != 0 { + t.Errorf("Expected 0 stacks, got %d", numStacks) + } + + // Should have trailing uint16(0) + if len(result) != 4 { + t.Errorf("Expected 4 bytes for empty box, got %d", len(result)) + } +} + +func TestBoxToBytes_SingleItemStack(t *testing.T) { + stacks := []mhfpacket.WarehouseStack{ + { + ID: 1, + Index: 0, + ItemID: 100, + Quantity: 50, + }, + } + result := boxToBytes(stacks, "item") + + bf := byteframe.NewByteFrameFromBytes(result) + numStacks := bf.ReadUint16() + if numStacks != 1 { + t.Errorf("Expected 1 stack, got %d", numStacks) + } + + // Read first stack + id := bf.ReadUint32() + index := bf.ReadUint16() + itemID := bf.ReadUint16() + quantity := bf.ReadUint16() + _ = bf.ReadUint16() // padding + + if id != 1 { + t.Errorf("Expected ID 1, got %d", id) + } + if index != 1 { // Index is written as i+1 + t.Errorf("Expected index 1, got %d", index) + } + if itemID != 100 { + t.Errorf("Expected itemID 100, got %d", itemID) + } + if quantity != 50 { + t.Errorf("Expected quantity 50, got %d", quantity) + } +} + +func TestBoxToBytes_MultipleItemStacks(t *testing.T) { + stacks := []mhfpacket.WarehouseStack{ + {ID: 1, Index: 0, ItemID: 100, Quantity: 10}, + {ID: 2, Index: 1, ItemID: 200, Quantity: 20}, + {ID: 3, Index: 2, ItemID: 300, Quantity: 30}, + } + result := boxToBytes(stacks, "item") + + bf := byteframe.NewByteFrameFromBytes(result) + numStacks := bf.ReadUint16() + if numStacks != 3 { + t.Errorf("Expected 3 stacks, got %d", numStacks) + } +} + +func TestBoxToBytes_EmptyEquipBox(t *testing.T) { + stacks := []mhfpacket.WarehouseStack{} + result := boxToBytes(stacks, "equip") + + bf := byteframe.NewByteFrameFromBytes(result) + numStacks := bf.ReadUint16() + if numStacks != 0 { + t.Errorf("Expected 0 stacks, got %d", numStacks) + } +} + +func TestBoxToBytes_SingleEquipStack(t *testing.T) { + equipData := make([]byte, 56) + for i := range equipData { + equipData[i] = byte(i) + } + + stacks := []mhfpacket.WarehouseStack{ + { + ID: 1, + Index: 0, + EquipType: 5, + ItemID: 1000, + Data: equipData, + }, + } + result := boxToBytes(stacks, "equip") + + bf := byteframe.NewByteFrameFromBytes(result) + numStacks := bf.ReadUint16() + if numStacks != 1 { + t.Errorf("Expected 1 stack, got %d", numStacks) + } + + // Read first equip stack + id := bf.ReadUint32() + index := bf.ReadUint16() + equipType := bf.ReadUint16() + itemID := bf.ReadUint16() + data := bf.ReadBytes(56) + + if id != 1 { + t.Errorf("Expected ID 1, got %d", id) + } + if index != 1 { // Index is written as i+1 + t.Errorf("Expected index 1, got %d", index) + } + if equipType != 5 { + t.Errorf("Expected equipType 5, got %d", equipType) + } + if itemID != 1000 { + t.Errorf("Expected itemID 1000, got %d", itemID) + } + if len(data) != 56 { + t.Errorf("Expected 56 bytes data, got %d", len(data)) + } +} + +func TestBoxToBytes_MultipleEquipStacks(t *testing.T) { + equipData := make([]byte, 56) + + stacks := []mhfpacket.WarehouseStack{ + {ID: 1, Index: 0, EquipType: 1, ItemID: 100, Data: equipData}, + {ID: 2, Index: 1, EquipType: 2, ItemID: 200, Data: equipData}, + } + result := boxToBytes(stacks, "equip") + + bf := byteframe.NewByteFrameFromBytes(result) + numStacks := bf.ReadUint16() + if numStacks != 2 { + t.Errorf("Expected 2 stacks, got %d", numStacks) + } +} + +// Test HouseData struct +func TestHouseDataStruct(t *testing.T) { + house := HouseData{ + CharID: 12345, + HRP: 999, + GR: 500, + Name: "TestPlayer", + HouseState: 2, + HousePassword: "pass123", + } + + if house.CharID != 12345 { + t.Errorf("CharID = %d, want 12345", house.CharID) + } + if house.HRP != 999 { + t.Errorf("HRP = %d, want 999", house.HRP) + } + if house.GR != 500 { + t.Errorf("GR = %d, want 500", house.GR) + } + if house.Name != "TestPlayer" { + t.Errorf("Name = %s, want TestPlayer", house.Name) + } + if house.HouseState != 2 { + t.Errorf("HouseState = %d, want 2", house.HouseState) + } + if house.HousePassword != "pass123" { + t.Errorf("HousePassword = %s, want pass123", house.HousePassword) + } +} + +// Test Title struct +func TestTitleStruct(t *testing.T) { + title := Title{ + ID: 42, + } + + if title.ID != 42 { + t.Errorf("ID = %d, want 42", title.ID) + } +} + +// Test decoMyset constants +func TestDecoMysetConstants(t *testing.T) { + if maxDecoMysets != 40 { + t.Errorf("maxDecoMysets = %d, want 40", maxDecoMysets) + } + if decoMysetSize != 78 { + t.Errorf("decoMysetSize = %d, want 78", decoMysetSize) + } +} diff --git a/server/channelserver/handlers_mail_test.go b/server/channelserver/handlers_mail_test.go new file mode 100644 index 000000000..8fe726c0d --- /dev/null +++ b/server/channelserver/handlers_mail_test.go @@ -0,0 +1,83 @@ +package channelserver + +import ( + "testing" + "time" +) + +func TestMailStruct(t *testing.T) { + mail := Mail{ + ID: 123, + SenderID: 1000, + RecipientID: 2000, + Subject: "Test Subject", + Body: "Test Body Content", + Read: false, + Deleted: false, + Locked: true, + AttachedItemReceived: false, + AttachedItemID: 500, + AttachedItemAmount: 10, + CreatedAt: time.Now(), + IsGuildInvite: false, + IsSystemMessage: true, + SenderName: "TestSender", + } + + if mail.ID != 123 { + t.Errorf("ID = %d, want 123", mail.ID) + } + if mail.SenderID != 1000 { + t.Errorf("SenderID = %d, want 1000", mail.SenderID) + } + if mail.RecipientID != 2000 { + t.Errorf("RecipientID = %d, want 2000", mail.RecipientID) + } + if mail.Subject != "Test Subject" { + t.Errorf("Subject = %s, want 'Test Subject'", mail.Subject) + } + if mail.Body != "Test Body Content" { + t.Errorf("Body = %s, want 'Test Body Content'", mail.Body) + } + if mail.Read { + t.Error("Read should be false") + } + if mail.Deleted { + t.Error("Deleted should be false") + } + if !mail.Locked { + t.Error("Locked should be true") + } + if mail.AttachedItemReceived { + t.Error("AttachedItemReceived should be false") + } + if mail.AttachedItemID != 500 { + t.Errorf("AttachedItemID = %d, want 500", mail.AttachedItemID) + } + if mail.AttachedItemAmount != 10 { + t.Errorf("AttachedItemAmount = %d, want 10", mail.AttachedItemAmount) + } + if mail.IsGuildInvite { + t.Error("IsGuildInvite should be false") + } + if !mail.IsSystemMessage { + t.Error("IsSystemMessage should be true") + } + if mail.SenderName != "TestSender" { + t.Errorf("SenderName = %s, want 'TestSender'", mail.SenderName) + } +} + +func TestMailStruct_DefaultValues(t *testing.T) { + mail := Mail{} + + if mail.ID != 0 { + t.Errorf("Default ID should be 0, got %d", mail.ID) + } + if mail.Subject != "" { + t.Errorf("Default Subject should be empty, got %s", mail.Subject) + } + if mail.Read { + t.Error("Default Read should be false") + } +} diff --git a/server/channelserver/handlers_mercenary_test.go b/server/channelserver/handlers_mercenary_test.go new file mode 100644 index 000000000..340e4024c --- /dev/null +++ b/server/channelserver/handlers_mercenary_test.go @@ -0,0 +1,27 @@ +package channelserver + +import ( + "testing" + + "erupe-ce/network/mhfpacket" +) + +func TestHandleMsgMhfLoadLegendDispatch(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfLoadLegendDispatch{ + AckHandle: 12345, + } + + handleMsgMhfLoadLegendDispatch(session, pkt) + + select { + case p := <-session.sendPackets: + if len(p.data) == 0 { + t.Error("Response packet should have data") + } + default: + t.Error("No response packet queued") + } +} diff --git a/server/channelserver/handlers_rengoku_test.go b/server/channelserver/handlers_rengoku_test.go index e95d83776..605068e26 100644 --- a/server/channelserver/handlers_rengoku_test.go +++ b/server/channelserver/handlers_rengoku_test.go @@ -26,3 +26,28 @@ func TestHandleMsgMhfGetRengokuRankingRank(t *testing.T) { t.Error("No response packet queued") } } + +func TestRengokuScoreStruct(t *testing.T) { + score := RengokuScore{ + Name: "TestPlayer", + Score: 12345, + } + + if score.Name != "TestPlayer" { + t.Errorf("Name = %s, want TestPlayer", score.Name) + } + if score.Score != 12345 { + t.Errorf("Score = %d, want 12345", score.Score) + } +} + +func TestRengokuScoreStruct_DefaultValues(t *testing.T) { + score := RengokuScore{} + + if score.Name != "" { + t.Errorf("Default Name should be empty, got %s", score.Name) + } + if score.Score != 0 { + t.Errorf("Default Score should be 0, got %d", score.Score) + } +} diff --git a/server/channelserver/handlers_reserve_test.go b/server/channelserver/handlers_reserve_test.go index af21dce94..72fb6fe24 100644 --- a/server/channelserver/handlers_reserve_test.go +++ b/server/channelserver/handlers_reserve_test.go @@ -101,6 +101,21 @@ func TestEmptyReserveHandlers(t *testing.T) { {"handleMsgSysReserve18F", handleMsgSysReserve18F}, {"handleMsgSysReserve19E", handleMsgSysReserve19E}, {"handleMsgSysReserve19F", handleMsgSysReserve19F}, + {"handleMsgSysReserve1A4", handleMsgSysReserve1A4}, + {"handleMsgSysReserve1A6", handleMsgSysReserve1A6}, + {"handleMsgSysReserve1A7", handleMsgSysReserve1A7}, + {"handleMsgSysReserve1A8", handleMsgSysReserve1A8}, + {"handleMsgSysReserve1A9", handleMsgSysReserve1A9}, + {"handleMsgSysReserve1AA", handleMsgSysReserve1AA}, + {"handleMsgSysReserve1AB", handleMsgSysReserve1AB}, + {"handleMsgSysReserve1AC", handleMsgSysReserve1AC}, + {"handleMsgSysReserve1AD", handleMsgSysReserve1AD}, + {"handleMsgSysReserve1AE", handleMsgSysReserve1AE}, + {"handleMsgSysReserve1AF", handleMsgSysReserve1AF}, + {"handleMsgSysReserve19B", handleMsgSysReserve19B}, + {"handleMsgSysReserve192", handleMsgSysReserve192}, + {"handleMsgSysReserve193", handleMsgSysReserve193}, + {"handleMsgSysReserve194", handleMsgSysReserve194}, } for _, tt := range tests { diff --git a/server/channelserver/handlers_shop_gacha_test.go b/server/channelserver/handlers_shop_gacha_test.go new file mode 100644 index 000000000..a6301804b --- /dev/null +++ b/server/channelserver/handlers_shop_gacha_test.go @@ -0,0 +1,232 @@ +package channelserver + +import ( + "testing" + + "erupe-ce/common/byteframe" +) + +func TestWriteShopItems_Empty(t *testing.T) { + bf := byteframe.NewByteFrame() + items := []ShopItem{} + + writeShopItems(bf, items) + + result := byteframe.NewByteFrameFromBytes(bf.Data()) + count1 := result.ReadUint16() + count2 := result.ReadUint16() + + if count1 != 0 { + t.Errorf("Expected first count 0, got %d", count1) + } + if count2 != 0 { + t.Errorf("Expected second count 0, got %d", count2) + } +} + +func TestWriteShopItems_SingleItem(t *testing.T) { + bf := byteframe.NewByteFrame() + items := []ShopItem{ + { + ID: 1, + ItemID: 100, + Cost: 500, + Quantity: 10, + MinHR: 1, + MinSR: 0, + MinGR: 0, + StoreLevel: 1, + MaxQuantity: 99, + UsedQuantity: 5, + RoadFloors: 0, + RoadFatalis: 0, + }, + } + + writeShopItems(bf, items) + + result := byteframe.NewByteFrameFromBytes(bf.Data()) + count1 := result.ReadUint16() + count2 := result.ReadUint16() + + if count1 != 1 { + t.Errorf("Expected first count 1, got %d", count1) + } + if count2 != 1 { + t.Errorf("Expected second count 1, got %d", count2) + } + + // Read the item data + id := result.ReadUint32() + _ = result.ReadUint16() // padding + itemID := result.ReadUint16() + cost := result.ReadUint32() + quantity := result.ReadUint16() + minHR := result.ReadUint16() + minSR := result.ReadUint16() + minGR := result.ReadUint16() + storeLevel := result.ReadUint16() + maxQuantity := result.ReadUint16() + usedQuantity := result.ReadUint16() + roadFloors := result.ReadUint16() + roadFatalis := result.ReadUint16() + + if id != 1 { + t.Errorf("Expected ID 1, got %d", id) + } + if itemID != 100 { + t.Errorf("Expected itemID 100, got %d", itemID) + } + if cost != 500 { + t.Errorf("Expected cost 500, got %d", cost) + } + if quantity != 10 { + t.Errorf("Expected quantity 10, got %d", quantity) + } + if minHR != 1 { + t.Errorf("Expected minHR 1, got %d", minHR) + } + if minSR != 0 { + t.Errorf("Expected minSR 0, got %d", minSR) + } + if minGR != 0 { + t.Errorf("Expected minGR 0, got %d", minGR) + } + if storeLevel != 1 { + t.Errorf("Expected storeLevel 1, got %d", storeLevel) + } + if maxQuantity != 99 { + t.Errorf("Expected maxQuantity 99, got %d", maxQuantity) + } + if usedQuantity != 5 { + t.Errorf("Expected usedQuantity 5, got %d", usedQuantity) + } + if roadFloors != 0 { + t.Errorf("Expected roadFloors 0, got %d", roadFloors) + } + if roadFatalis != 0 { + t.Errorf("Expected roadFatalis 0, got %d", roadFatalis) + } +} + +func TestWriteShopItems_MultipleItems(t *testing.T) { + bf := byteframe.NewByteFrame() + items := []ShopItem{ + {ID: 1, ItemID: 100, Cost: 500, Quantity: 10}, + {ID: 2, ItemID: 200, Cost: 1000, Quantity: 5}, + {ID: 3, ItemID: 300, Cost: 2000, Quantity: 1}, + } + + writeShopItems(bf, items) + + result := byteframe.NewByteFrameFromBytes(bf.Data()) + count1 := result.ReadUint16() + count2 := result.ReadUint16() + + if count1 != 3 { + t.Errorf("Expected first count 3, got %d", count1) + } + if count2 != 3 { + t.Errorf("Expected second count 3, got %d", count2) + } +} + +// Test struct definitions +func TestShopItemStruct(t *testing.T) { + item := ShopItem{ + ID: 42, + ItemID: 1234, + Cost: 9999, + Quantity: 50, + MinHR: 10, + MinSR: 5, + MinGR: 100, + StoreLevel: 3, + MaxQuantity: 99, + UsedQuantity: 10, + RoadFloors: 50, + RoadFatalis: 25, + } + + if item.ID != 42 { + t.Errorf("ID = %d, want 42", item.ID) + } + if item.ItemID != 1234 { + t.Errorf("ItemID = %d, want 1234", item.ItemID) + } + if item.Cost != 9999 { + t.Errorf("Cost = %d, want 9999", item.Cost) + } +} + +func TestGachaStruct(t *testing.T) { + gacha := Gacha{ + ID: 1, + MinGR: 100, + MinHR: 999, + Name: "Test Gacha", + URLBanner: "http://example.com/banner.png", + URLFeature: "http://example.com/feature.png", + URLThumbnail: "http://example.com/thumb.png", + Wide: true, + Recommended: true, + GachaType: 2, + Hidden: false, + } + + if gacha.ID != 1 { + t.Errorf("ID = %d, want 1", gacha.ID) + } + if gacha.Name != "Test Gacha" { + t.Errorf("Name = %s, want Test Gacha", gacha.Name) + } + if !gacha.Wide { + t.Error("Wide should be true") + } + if !gacha.Recommended { + t.Error("Recommended should be true") + } +} + +func TestGachaEntryStruct(t *testing.T) { + entry := GachaEntry{ + EntryType: 1, + ID: 100, + ItemType: 0, + ItemNumber: 1234, + ItemQuantity: 10, + Weight: 0.5, + Rarity: 3, + Rolls: 1, + FrontierPoints: 500, + DailyLimit: 5, + } + + if entry.EntryType != 1 { + t.Errorf("EntryType = %d, want 1", entry.EntryType) + } + if entry.ID != 100 { + t.Errorf("ID = %d, want 100", entry.ID) + } + if entry.Weight != 0.5 { + t.Errorf("Weight = %f, want 0.5", entry.Weight) + } +} + +func TestGachaItemStruct(t *testing.T) { + item := GachaItem{ + ItemType: 0, + ItemID: 5678, + Quantity: 20, + } + + if item.ItemType != 0 { + t.Errorf("ItemType = %d, want 0", item.ItemType) + } + if item.ItemID != 5678 { + t.Errorf("ItemID = %d, want 5678", item.ItemID) + } + if item.Quantity != 20 { + t.Errorf("Quantity = %d, want 20", item.Quantity) + } +} diff --git a/server/channelserver/handlers_simple_test.go b/server/channelserver/handlers_simple_test.go index 8a2423e74..5f922046d 100644 --- a/server/channelserver/handlers_simple_test.go +++ b/server/channelserver/handlers_simple_test.go @@ -200,3 +200,115 @@ func TestSessionQueueSendNonBlocking_FullQueue(t *testing.T) { t.Error("QueueSendNonBlocking blocked on full queue") } } + +// Additional handler tests for coverage + +func TestHandleMsgMhfGetGuildWeeklyBonusMaster(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfGetGuildWeeklyBonusMaster{ + AckHandle: 12345, + } + + handleMsgMhfGetGuildWeeklyBonusMaster(session, pkt) + + select { + case p := <-session.sendPackets: + if len(p.data) == 0 { + t.Error("Response packet should have data") + } + default: + t.Error("No response packet queued") + } +} + +func TestHandleMsgMhfGetGuildWeeklyBonusActiveCount(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfGetGuildWeeklyBonusActiveCount{ + AckHandle: 12345, + } + + handleMsgMhfGetGuildWeeklyBonusActiveCount(session, pkt) + + select { + case p := <-session.sendPackets: + if len(p.data) == 0 { + t.Error("Response packet should have data") + } + default: + t.Error("No response packet queued") + } +} + +func TestHandleMsgMhfAddGuildWeeklyBonusExceptionalUser(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfAddGuildWeeklyBonusExceptionalUser{ + AckHandle: 12345, + } + + handleMsgMhfAddGuildWeeklyBonusExceptionalUser(session, pkt) + + select { + case p := <-session.sendPackets: + if len(p.data) == 0 { + t.Error("Response packet should have data") + } + default: + t.Error("No response packet queued") + } +} + +func TestEmptyHandlers_NoDb(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + // Test handlers that are empty and should not panic + tests := []struct { + name string + handler func(s *Session, p mhfpacket.MHFPacket) + }{ + {"handleMsgHead", handleMsgHead}, + {"handleMsgSysExtendThreshold", handleMsgSysExtendThreshold}, + {"handleMsgSysEnd", handleMsgSysEnd}, + {"handleMsgSysNop", handleMsgSysNop}, + {"handleMsgSysAck", handleMsgSysAck}, + {"handleMsgSysUpdateRight", handleMsgSysUpdateRight}, + {"handleMsgSysAuthQuery", handleMsgSysAuthQuery}, + {"handleMsgSysAuthTerminal", handleMsgSysAuthTerminal}, + {"handleMsgCaExchangeItem", handleMsgCaExchangeItem}, + {"handleMsgMhfServerCommand", handleMsgMhfServerCommand}, + {"handleMsgMhfSetLoginwindow", handleMsgMhfSetLoginwindow}, + {"handleMsgSysTransBinary", handleMsgSysTransBinary}, + {"handleMsgSysCollectBinary", handleMsgSysCollectBinary}, + {"handleMsgSysGetState", handleMsgSysGetState}, + {"handleMsgSysSerialize", handleMsgSysSerialize}, + {"handleMsgSysEnumlobby", handleMsgSysEnumlobby}, + {"handleMsgSysEnumuser", handleMsgSysEnumuser}, + {"handleMsgSysInfokyserver", handleMsgSysInfokyserver}, + {"handleMsgMhfGetCaUniqueID", handleMsgMhfGetCaUniqueID}, + {"handleMsgMhfEnumerateItem", handleMsgMhfEnumerateItem}, + {"handleMsgMhfAcquireItem", handleMsgMhfAcquireItem}, + {"handleMsgMhfGetExtraInfo", handleMsgMhfGetExtraInfo}, + {"handleMsgMhfGetCogInfo", handleMsgMhfGetCogInfo}, + {"handleMsgMhfStampcardPrize", handleMsgMhfStampcardPrize}, + {"handleMsgMhfUnreserveSrg", handleMsgMhfUnreserveSrg}, + {"handleMsgMhfKickExportForce", handleMsgMhfKickExportForce}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + defer func() { + if r := recover(); r != nil { + t.Errorf("%s panicked: %v", tt.name, r) + } + }() + tt.handler(session, nil) + }) + } +} + diff --git a/server/channelserver/handlers_stage_test.go b/server/channelserver/handlers_stage_test.go index d34a83bbc..8321c8e36 100644 --- a/server/channelserver/handlers_stage_test.go +++ b/server/channelserver/handlers_stage_test.go @@ -4,6 +4,8 @@ import ( "sync" "testing" "time" + + "erupe-ce/network/mhfpacket" ) // TestWaitStageBinaryInfiniteLoopRisk documents the infinite loop risk in handleMsgSysWaitStageBinary. @@ -268,3 +270,582 @@ func TestWaitStageBinaryTimeoutDuration(t *testing.T) { t.Logf("After fix, WaitStageBinary will timeout after %v (%d iterations * %v sleep)", expectedTimeout, maxIterations, sleepDuration) } + +// TestHandleMsgSysCreateStage_NewStage tests creating a new stage +func TestHandleMsgSysCreateStage_NewStage(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgSysCreateStage{ + AckHandle: 12345, + StageID: "test_create_stage", + PlayerCount: 4, + } + + handleMsgSysCreateStage(session, pkt) + + // Verify stage was created + server.Lock() + stage, exists := server.stages["test_create_stage"] + server.Unlock() + + if !exists { + t.Error("Stage should be created") + } + if stage.maxPlayers != 4 { + t.Errorf("stage.maxPlayers = %d, want 4", stage.maxPlayers) + } + if stage.host != session { + t.Error("Session should be host of the stage") + } + + // Verify response packet was queued + select { + case p := <-session.sendPackets: + if len(p.data) == 0 { + t.Error("Response packet should have data") + } + default: + t.Error("No response packet queued") + } +} + +// TestHandleMsgSysCreateStage_ExistingStage tests creating a stage that already exists +func TestHandleMsgSysCreateStage_ExistingStage(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + // Create existing stage + existingStage := NewStage("existing_stage") + server.stages["existing_stage"] = existingStage + + pkt := &mhfpacket.MsgSysCreateStage{ + AckHandle: 12345, + StageID: "existing_stage", + PlayerCount: 4, + } + + handleMsgSysCreateStage(session, pkt) + + // Verify response packet was queued (should be failure) + select { + case p := <-session.sendPackets: + if len(p.data) == 0 { + t.Error("Response packet should have data") + } + default: + t.Error("No response packet queued") + } +} + +// TestDoStageTransfer_NewStage tests entering a stage that doesn't exist +func TestDoStageTransfer_NewStage(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + doStageTransfer(session, 12345, "new_transfer_stage") + + // Verify stage was created + server.Lock() + stage, exists := server.stages["new_transfer_stage"] + server.Unlock() + + if !exists { + t.Error("Stage should be created") + } + + // Verify session is in the stage + stage.RLock() + _, inStage := stage.clients[session] + stage.RUnlock() + + if !inStage { + t.Error("Session should be in the stage") + } + + // Verify session's stage reference is set + if session.stage != stage { + t.Error("Session's stage reference should be set") + } + + // Verify response packets were queued + packetCount := 0 + for { + select { + case <-session.sendPackets: + packetCount++ + default: + goto done + } + } +done: + if packetCount < 2 { + t.Errorf("Expected at least 2 packets (cleanup + ack), got %d", packetCount) + } +} + +// TestDoStageTransfer_ExistingStage tests entering an existing stage +func TestDoStageTransfer_ExistingStage(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + // Create existing stage + existingStage := NewStage("existing_transfer_stage") + server.stages["existing_transfer_stage"] = existingStage + + doStageTransfer(session, 12345, "existing_transfer_stage") + + // Verify session is in the stage + existingStage.RLock() + _, inStage := existingStage.clients[session] + existingStage.RUnlock() + + if !inStage { + t.Error("Session should be in the stage") + } + + // Verify response packets were queued + packetCount := 0 + for { + select { + case <-session.sendPackets: + packetCount++ + default: + goto done + } + } +done: + if packetCount < 2 { + t.Errorf("Expected at least 2 packets, got %d", packetCount) + } +} + +// TestHandleMsgSysStageDestruct tests the empty handler +func TestHandleMsgSysStageDestruct(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + // Should not panic + defer func() { + if r := recover(); r != nil { + t.Errorf("handleMsgSysStageDestruct panicked: %v", r) + } + }() + + handleMsgSysStageDestruct(session, nil) +} + +// TestHandleMsgSysLockStage tests the lock stage handler +func TestHandleMsgSysLockStage(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgSysLockStage{ + AckHandle: 12345, + } + + handleMsgSysLockStage(session, pkt) + + // Verify response packet was queued + select { + case p := <-session.sendPackets: + if len(p.data) == 0 { + t.Error("Response packet should have data") + } + default: + t.Error("No response packet queued") + } +} + +// TestHandleMsgSysLeaveStage tests the empty handler +func TestHandleMsgSysLeaveStage(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + // Should not panic + defer func() { + if r := recover(); r != nil { + t.Errorf("handleMsgSysLeaveStage panicked: %v", r) + } + }() + + handleMsgSysLeaveStage(session, nil) +} + +// TestDestructEmptyStages tests the stage cleanup function +func TestDestructEmptyStages(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + // Create different types of stages + questStage := NewStage("00XQsStage1") // Quest stage + myStage := NewStage("00XMsStage1") // My series stage + guildStage := NewStage("00XGsStage1") // Guild stage + townStage := NewStage("00XTwStage1") // Town stage (should not be deleted) + + server.stages["00XQsStage1"] = questStage + server.stages["00XMsStage1"] = myStage + server.stages["00XGsStage1"] = guildStage + server.stages["00XTwStage1"] = townStage + + destructEmptyStages(session) + + // Quest/My/Guild stages should be deleted (empty) + if _, exists := server.stages["00XQsStage1"]; exists { + t.Error("Empty quest stage should be deleted") + } + if _, exists := server.stages["00XMsStage1"]; exists { + t.Error("Empty my series stage should be deleted") + } + if _, exists := server.stages["00XGsStage1"]; exists { + t.Error("Empty guild stage should be deleted") + } + + // Town stage should remain + if _, exists := server.stages["00XTwStage1"]; !exists { + t.Error("Town stage should not be deleted") + } +} + +// TestDestructEmptyStages_NonEmpty tests that non-empty stages are preserved +func TestDestructEmptyStages_NonEmpty(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + // Create quest stage with clients + questStage := NewStage("00XQsStage2") + questStage.clients[session] = session.charID + server.stages["00XQsStage2"] = questStage + + destructEmptyStages(session) + + // Stage with clients should not be deleted + if _, exists := server.stages["00XQsStage2"]; !exists { + t.Error("Non-empty quest stage should not be deleted") + } +} + +// TestDestructEmptyStages_WithReservations tests that stages with reservations are preserved +func TestDestructEmptyStages_WithReservations(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + // Create quest stage with reservations + questStage := NewStage("00XQsStage3") + questStage.reservedClientSlots[session.charID] = true + server.stages["00XQsStage3"] = questStage + + destructEmptyStages(session) + + // Stage with reservations should not be deleted + if _, exists := server.stages["00XQsStage3"]; !exists { + t.Error("Quest stage with reservations should not be deleted") + } +} + +// TestRemoveSessionFromStage tests removing a session from its stage +func TestRemoveSessionFromStage(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + // Create a stage and add session to it + stage := NewStage("00XTwRemove1") + stage.clients[session] = session.charID + session.stage = stage + server.stages["00XTwRemove1"] = stage + + // Verify session is in stage + if _, exists := stage.clients[session]; !exists { + t.Error("Session should be in stage before removal") + } + + removeSessionFromStage(session) + + // Verify session is removed from stage + if _, exists := stage.clients[session]; exists { + t.Error("Session should be removed from stage") + } +} + +// TestRemoveSessionFromStage_WithObjects tests removing objects when leaving stage +func TestRemoveSessionFromStage_WithObjects(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + // Create a stage and add session with objects + stage := NewStage("00XTwRemove2") + stage.clients[session] = session.charID + stage.objects[session.charID] = &Object{ + id: 1, + ownerCharID: session.charID, + x: 100.0, + y: 200.0, + z: 300.0, + } + session.stage = stage + server.stages["00XTwRemove2"] = stage + + removeSessionFromStage(session) + + // Verify objects owned by session are removed + if _, exists := stage.objects[session.charID]; exists { + t.Error("Objects owned by session should be removed") + } +} + +// TestHandleMsgSysSetStageBinary tests setting stage binary data +func TestHandleMsgSysSetStageBinary(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + // Create a stage + stage := NewStage("test_binary_stage") + server.stages["test_binary_stage"] = stage + + pkt := &mhfpacket.MsgSysSetStageBinary{ + StageID: "test_binary_stage", + BinaryType0: 1, + BinaryType1: 2, + RawDataPayload: []byte{0xDE, 0xAD, 0xBE, 0xEF}, + } + + handleMsgSysSetStageBinary(session, pkt) + + // Verify binary was stored + stage.Lock() + data, exists := stage.rawBinaryData[stageBinaryKey{1, 2}] + stage.Unlock() + + if !exists { + t.Error("Binary data should be stored") + } + if len(data) != 4 { + t.Errorf("Binary data length = %d, want 4", len(data)) + } +} + +// TestHandleMsgSysGetStageBinary tests getting stage binary data +func TestHandleMsgSysGetStageBinary_WithData(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + // Create a stage with binary data + stage := NewStage("test_get_binary") + stage.rawBinaryData[stageBinaryKey{1, 2}] = []byte{0x01, 0x02, 0x03, 0x04} + server.stages["test_get_binary"] = stage + + pkt := &mhfpacket.MsgSysGetStageBinary{ + AckHandle: 12345, + StageID: "test_get_binary", + BinaryType0: 1, + BinaryType1: 2, + } + + handleMsgSysGetStageBinary(session, pkt) + + // Verify response packet was queued + select { + case p := <-session.sendPackets: + if len(p.data) == 0 { + t.Error("Response packet should have data") + } + default: + t.Error("No response packet queued") + } +} + +// TestHandleMsgSysGetStageBinary_NoData tests getting non-existent binary data +func TestHandleMsgSysGetStageBinary_NoData(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + // Create a stage without the requested binary data + stage := NewStage("test_no_binary") + server.stages["test_no_binary"] = stage + + pkt := &mhfpacket.MsgSysGetStageBinary{ + AckHandle: 12345, + StageID: "test_no_binary", + BinaryType0: 1, + BinaryType1: 2, + } + + handleMsgSysGetStageBinary(session, pkt) + + // Should still return a response (empty) + select { + case p := <-session.sendPackets: + if p.data == nil { + t.Error("Response packet should not be nil") + } + default: + t.Error("No response packet queued") + } +} + +// TestHandleMsgSysGetStageBinary_Type4 tests special type 4 binary +func TestHandleMsgSysGetStageBinary_Type4(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + // Create a stage + stage := NewStage("test_type4_binary") + server.stages["test_type4_binary"] = stage + + pkt := &mhfpacket.MsgSysGetStageBinary{ + AckHandle: 12345, + StageID: "test_type4_binary", + BinaryType0: 0, + BinaryType1: 4, + } + + handleMsgSysGetStageBinary(session, pkt) + + // Should return empty response for type 4 + select { + case p := <-session.sendPackets: + if p.data == nil { + t.Error("Response packet should not be nil") + } + default: + t.Error("No response packet queued") + } +} + +// TestHandleMsgSysSetStagePass tests setting stage password +func TestHandleMsgSysSetStagePass_WithReservation(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + // Create a stage and add session reservation + stage := NewStage("test_pass_stage") + stage.reservedClientSlots[session.charID] = true + server.stages["test_pass_stage"] = stage + session.reservationStage = stage + + pkt := &mhfpacket.MsgSysSetStagePass{ + Password: "secret123", + } + + handleMsgSysSetStagePass(session, pkt) + + // Verify password was set + stage.Lock() + password := stage.password + stage.Unlock() + + if password != "secret123" { + t.Errorf("Stage password = %s, want secret123", password) + } +} + +// TestHandleMsgSysSetStagePass_NoReservation tests setting pass without reservation +func TestHandleMsgSysSetStagePass_NoReservation(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgSysSetStagePass{ + Password: "secret456", + } + + handleMsgSysSetStagePass(session, pkt) + + // Verify password was stored in session for later use + session.Lock() + password := session.stagePass + session.Unlock() + + if password != "secret456" { + t.Errorf("Session stagePass = %s, want secret456", password) + } +} + +// TestHandleMsgSysEnumerateStage tests enumerating stages +func TestHandleMsgSysEnumerateStage(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + // Create some stages + stage1 := NewStage("00XQsStage1") + stage1.reservedClientSlots[100] = true + stage1.maxPlayers = 4 + + stage2 := NewStage("00XQsStage2") + stage2.clients[session] = session.charID + stage2.maxPlayers = 2 + + server.stages["00XQsStage1"] = stage1 + server.stages["00XQsStage2"] = stage2 + + pkt := &mhfpacket.MsgSysEnumerateStage{ + AckHandle: 12345, + StagePrefix: "Qs", + } + + handleMsgSysEnumerateStage(session, pkt) + + // Verify response packet was queued + select { + case p := <-session.sendPackets: + if len(p.data) == 0 { + t.Error("Response packet should have data") + } + default: + t.Error("No response packet queued") + } +} + +// TestHandleMsgSysEnumerateStage_Empty tests enumerating with no matching stages +func TestHandleMsgSysEnumerateStage_Empty(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgSysEnumerateStage{ + AckHandle: 12345, + StagePrefix: "NonExistent", + } + + handleMsgSysEnumerateStage(session, pkt) + + // Should still return a response + select { + case p := <-session.sendPackets: + if p.data == nil { + t.Error("Response packet should not be nil") + } + default: + t.Error("No response packet queued") + } +} + +// TestHandleMsgSysUnreserveStage tests unreserving a stage +func TestHandleMsgSysUnreserveStage(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + // Create a stage and add session reservation + stage := NewStage("test_unreserve") + stage.reservedClientSlots[session.charID] = true + server.stages["test_unreserve"] = stage + session.reservationStage = stage + + handleMsgSysUnreserveStage(session, nil) + + // Verify reservation was removed + stage.Lock() + _, exists := stage.reservedClientSlots[session.charID] + stage.Unlock() + + if exists { + t.Error("Reservation should be removed") + } + + // Verify session's reservation stage is cleared + session.Lock() + reservationStage := session.reservationStage + session.Unlock() + + if reservationStage != nil { + t.Error("Session's reservation stage should be nil") + } +} diff --git a/server/channelserver/sys_channel_server_test.go b/server/channelserver/sys_channel_server_test.go index 70d92bfcc..776b2075a 100644 --- a/server/channelserver/sys_channel_server_test.go +++ b/server/channelserver/sys_channel_server_test.go @@ -7,6 +7,7 @@ import ( "time" "erupe-ce/config" + "erupe-ce/network/mhfpacket" "go.uber.org/zap" ) @@ -482,3 +483,265 @@ func TestConfigStruct(t *testing.T) { t.Error("Config Enable should be true") } } + +func TestServerBroadcastMHF_NoSessions(t *testing.T) { + logger, _ := zap.NewDevelopment() + cfg := &Config{ + ID: 1, + Logger: logger, + ErupeConfig: &config.Config{DevMode: true}, + } + + s := NewServer(cfg) + + // Should not panic with no sessions + pkt := &mhfpacket.MsgSysAck{ + AckHandle: 1, + IsBufferResponse: false, + ErrorCode: 0, + AckData: []byte{0x00}, + } + + s.BroadcastMHF(pkt, nil) +} + +func TestServerBroadcastMHF_WithSession(t *testing.T) { + logger, _ := zap.NewDevelopment() + cfg := &Config{ + ID: 1, + Logger: logger, + ErupeConfig: &config.Config{DevMode: true}, + } + + s := NewServer(cfg) + session := createMockSession(1, s) + s.sessions[nil] = session + + pkt := &mhfpacket.MsgSysAck{ + AckHandle: 1, + IsBufferResponse: false, + ErrorCode: 0, + AckData: []byte{0x00}, + } + + s.BroadcastMHF(pkt, nil) + + // Check if packet was queued + select { + case p := <-session.sendPackets: + if p.data == nil { + t.Error("Packet data should not be nil") + } + default: + t.Error("No packet queued to session") + } +} + +func TestServerBroadcastMHF_SkipsOrigin(t *testing.T) { + logger, _ := zap.NewDevelopment() + cfg := &Config{ + ID: 1, + Logger: logger, + ErupeConfig: &config.Config{DevMode: true}, + } + + s := NewServer(cfg) + session1 := createMockSession(1, s) + session2 := createMockSession(2, s) + s.sessions[nil] = session1 + s.sessions[session2.rawConn] = session2 + + pkt := &mhfpacket.MsgSysAck{ + AckHandle: 1, + IsBufferResponse: false, + ErrorCode: 0, + AckData: []byte{0x00}, + } + + // Broadcast from session1 - should skip session1 + s.BroadcastMHF(pkt, session1) + + // session1 should not receive the packet + select { + case <-session1.sendPackets: + t.Error("Origin session should not receive broadcast") + default: + // Good - no packet for origin + } +} + +func TestServerFindSessionByCharID_Found(t *testing.T) { + logger, _ := zap.NewDevelopment() + cfg := &Config{ + ID: 1, + Logger: logger, + ErupeConfig: &config.Config{DevMode: true}, + } + + s := NewServer(cfg) + s.Channels = []*Server{s} + + session := createMockSession(12345, s) + s.sessions[nil] = session + + // Search for existing character + found := s.FindSessionByCharID(12345) + if found == nil { + t.Error("Expected to find session") + } + if found != session { + t.Error("Found wrong session") + } +} + +func TestServerFindObjectByChar_Found(t *testing.T) { + logger, _ := zap.NewDevelopment() + cfg := &Config{ + ID: 1, + Logger: logger, + ErupeConfig: &config.Config{DevMode: true}, + } + + s := NewServer(cfg) + + // Add a stage with an object + stage := NewStage("test_obj_stage") + obj := &Object{ + id: 1, + ownerCharID: 12345, + x: 100.0, + y: 200.0, + z: 300.0, + } + stage.objects[obj.ownerCharID] = obj + s.stages["test_obj_stage"] = stage + + // Search for existing object + found := s.FindObjectByChar(12345) + if found == nil { + t.Error("Expected to find object") + } + if found != obj { + t.Error("Found wrong object") + } +} + +func TestServerConcurrentBinaryPartsAccess(t *testing.T) { + logger, _ := zap.NewDevelopment() + cfg := &Config{ + ID: 1, + Logger: logger, + ErupeConfig: &config.Config{DevMode: true}, + } + + s := NewServer(cfg) + + var wg sync.WaitGroup + + // Concurrent writers + for i := 0; i < 10; i++ { + wg.Add(1) + go func(id int) { + defer wg.Done() + for j := 0; j < 10; j++ { + partID := userBinaryPartID{charID: uint32(id), index: uint8(j % 4)} + s.userBinaryPartsLock.Lock() + s.userBinaryParts[partID] = []byte{byte(id), byte(j)} + s.userBinaryPartsLock.Unlock() + } + }(i) + } + + // Concurrent readers + for i := 0; i < 5; i++ { + wg.Add(1) + go func() { + defer wg.Done() + for j := 0; j < 20; j++ { + s.userBinaryPartsLock.RLock() + _ = len(s.userBinaryParts) + s.userBinaryPartsLock.RUnlock() + } + }() + } + + wg.Wait() +} + +func TestServerNextSemaphoreID(t *testing.T) { + logger, _ := zap.NewDevelopment() + cfg := &Config{ + ID: 1, + Logger: logger, + ErupeConfig: &config.Config{DevMode: true}, + } + + s := NewServer(cfg) + + // Get multiple IDs and verify they're unique + ids := make(map[uint32]bool) + for i := 0; i < 10; i++ { + id := s.NextSemaphoreID() + if ids[id] { + t.Errorf("Duplicate semaphore ID: %d", id) + } + ids[id] = true + + // IDs should be >= 7 (reserved indexes skipped) + if id < 7 { + t.Errorf("Semaphore ID %d should be >= 7", id) + } + } +} + +func TestServerNextSemaphoreID_WrapAround(t *testing.T) { + logger, _ := zap.NewDevelopment() + cfg := &Config{ + ID: 1, + Logger: logger, + ErupeConfig: &config.Config{DevMode: true}, + } + + s := NewServer(cfg) + + // Set semaphoreIndex to near max uint32 to test wrap-around + s.semaphoreIndex = ^uint32(0) - 1 // Max uint32 - 1 + + id1 := s.NextSemaphoreID() + id2 := s.NextSemaphoreID() + + // After wrap-around, should skip to 7 + if id1 < 7 && id1 != 0 { + t.Errorf("After wrap-around, first ID should be >= 7, got %d", id1) + } + + if id1 == id2 { + t.Error("IDs should be different") + } +} + +func TestServerNextSemaphoreID_SkipsExisting(t *testing.T) { + logger, _ := zap.NewDevelopment() + cfg := &Config{ + ID: 1, + Logger: logger, + ErupeConfig: &config.Config{DevMode: true}, + } + + s := NewServer(cfg) + + // Pre-populate with some semaphores + for i := uint32(7); i <= 15; i++ { + s.semaphore["test_"+string(rune('a'+i))] = &Semaphore{id: i} + } + + // Get a new ID - should skip the existing ones + id := s.NextSemaphoreID() + + // Verify ID is not one of the existing ones + for _, sema := range s.semaphore { + if sema.id == id { + t.Errorf("NextSemaphoreID returned existing ID: %d", id) + } + } +}