diff --git a/server/channelserver/handlers_achievement_test.go b/server/channelserver/handlers_achievement_test.go new file mode 100644 index 000000000..e7c5d2869 --- /dev/null +++ b/server/channelserver/handlers_achievement_test.go @@ -0,0 +1,454 @@ +package channelserver + +import ( + "testing" + + "erupe-ce/network/mhfpacket" +) + +func TestGetAchData_Level0(t *testing.T) { + // Score 0 should give level 0 with progress toward first threshold + ach := GetAchData(0, 0) + if ach.Level != 0 { + t.Errorf("Level = %d, want 0", ach.Level) + } + if ach.Progress != 0 { + t.Errorf("Progress = %d, want 0", ach.Progress) + } + if ach.NextValue != 5 { + t.Errorf("NextValue = %d, want 5", ach.NextValue) + } +} + +func TestGetAchData_Level1(t *testing.T) { + // Score 5 (exactly at first threshold) should give level 1 + ach := GetAchData(0, 5) + if ach.Level != 1 { + t.Errorf("Level = %d, want 1", ach.Level) + } + if ach.Value != 5 { + t.Errorf("Value = %d, want 5", ach.Value) + } +} + +func TestGetAchData_Partial(t *testing.T) { + // Score 3 should give level 0 with progress 3 + ach := GetAchData(0, 3) + if ach.Level != 0 { + t.Errorf("Level = %d, want 0", ach.Level) + } + if ach.Progress != 3 { + t.Errorf("Progress = %d, want 3", ach.Progress) + } + if ach.Required != 5 { + t.Errorf("Required = %d, want 5", ach.Required) + } +} + +func TestGetAchData_MaxLevel(t *testing.T) { + // Score 999 should give max level for curve 0 + ach := GetAchData(0, 999) + if ach.Level != 8 { + t.Errorf("Level = %d, want 8", ach.Level) + } + if ach.Trophy != 0x7F { + t.Errorf("Trophy = %x, want 0x7F (gold)", ach.Trophy) + } +} + +func TestGetAchData_BronzeTrophy(t *testing.T) { + // Level 7 should have bronze trophy (0x40) + // Curve 0: 5, 15, 30, 50, 100, 150, 200, 300 + // Cumulative: 5, 20, 50, 100, 200, 350, 550, 850 + // To reach level 7, need 550+ points (sum of first 7 thresholds) + ach := GetAchData(0, 550) + if ach.Level != 7 { + t.Errorf("Level = %d, want 7", ach.Level) + } + if ach.Trophy != 0x60 { + t.Errorf("Trophy = %x, want 0x60 (silver)", ach.Trophy) + } +} + +func TestGetAchData_SilverTrophy(t *testing.T) { + // Level 8 (max) should have gold trophy (0x7F) + // Need 850+ (sum of all 8 thresholds) for max level + ach := GetAchData(0, 850) + if ach.Level != 8 { + t.Errorf("Level = %d, want 8", ach.Level) + } + if ach.Trophy != 0x7F { + t.Errorf("Trophy = %x, want 0x7F (gold)", ach.Trophy) + } +} + +func TestGetAchData_DifferentCurves(t *testing.T) { + tests := []struct { + name string + id uint8 + score int32 + wantLvl uint8 + wantProg uint32 + }{ + {"Curve1_ID7_Level0", 7, 0, 0, 0}, + {"Curve1_ID7_Level1", 7, 1, 1, 0}, + {"Curve2_ID8_Level0", 8, 0, 0, 0}, + {"Curve2_ID8_Level1", 8, 1, 1, 0}, + {"Curve3_ID16_Level0", 16, 0, 0, 0}, + {"Curve3_ID16_Partial", 16, 5, 0, 5}, + {"Curve3_ID16_Level1", 16, 10, 1, 0}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ach := GetAchData(tt.id, tt.score) + if ach.Level != tt.wantLvl { + t.Errorf("Level = %d, want %d", ach.Level, tt.wantLvl) + } + if ach.Progress != tt.wantProg { + t.Errorf("Progress = %d, want %d", ach.Progress, tt.wantProg) + } + }) + } +} + +func TestGetAchData_AllCurveMappings(t *testing.T) { + // Verify all achievement IDs have valid curve mappings + for id := uint8(0); id <= 32; id++ { + curve, ok := achievementCurveMap[id] + if !ok { + t.Errorf("Achievement ID %d has no curve mapping", id) + continue + } + if len(curve) != 8 { + t.Errorf("Achievement ID %d curve has %d elements, want 8", id, len(curve)) + } + } +} + +func TestGetAchData_ValueAccumulation(t *testing.T) { + // Test that Value correctly accumulates based on level + // Level values: 1=5, 2-4=10, 5-7=15, 8=20 + // At max level 8: 5 + 10*3 + 15*3 + 20 = 5 + 30 + 45 + 20 = 100 + ach := GetAchData(0, 1000) // Score well above max + expectedValue := uint32(5 + 10 + 10 + 10 + 15 + 15 + 15 + 20) + if ach.Value != expectedValue { + t.Errorf("Value = %d, want %d", ach.Value, expectedValue) + } +} + +func TestGetAchData_NextValueByLevel(t *testing.T) { + tests := []struct { + level uint8 + wantNext uint16 + approxScore int32 + }{ + {0, 5, 0}, + {1, 10, 5}, + {2, 10, 15}, + {3, 10, 30}, + {4, 15, 50}, + {5, 15, 100}, + } + + for _, tt := range tests { + t.Run("Level"+string(rune('0'+tt.level)), func(t *testing.T) { + ach := GetAchData(0, tt.approxScore) + if ach.Level != tt.level { + t.Skipf("Skipping: got level %d, expected %d", ach.Level, tt.level) + } + if ach.NextValue != tt.wantNext { + t.Errorf("NextValue at level %d = %d, want %d", ach.Level, ach.NextValue, tt.wantNext) + } + }) + } +} + +func TestAchievementCurves(t *testing.T) { + // Verify curve values are strictly increasing + for i, curve := range achievementCurves { + for j := 1; j < len(curve); j++ { + if curve[j] <= curve[j-1] { + t.Errorf("Curve %d: value[%d]=%d should be > value[%d]=%d", + i, j, curve[j], j-1, curve[j-1]) + } + } + } +} + +func TestAchievementCurveMap_Coverage(t *testing.T) { + // Ensure all mapped curves exist + for id, curve := range achievementCurveMap { + found := false + for _, c := range achievementCurves { + if &c[0] == &curve[0] { + found = true + break + } + } + if !found { + t.Errorf("Achievement ID %d maps to unknown curve", id) + } + } +} + +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) + }) + } +} + +// --- NEW TESTS --- + +// TestGetAchData_Level6BronzeTrophy tests that level 6 (in-progress toward level 7) +// awards the bronze trophy (0x40). +// Curve 0: {5, 15, 30, 50, 100, 150, 200, 300} +// Cumulative at each level: L1=5, L2=20, L3=50, L4=100, L5=200, L6=350, L7=550, L8=850 +// At cumulative 350, we reach level 6. Score 400 means level 6 with progress 50 toward next. +func TestGetAchData_Level6BronzeTrophy(t *testing.T) { + // Score to reach level 6 and be partway to level 7: + // cumulative to level 6 = 5+15+30+50+100+150 = 350 + // score 400 = level 6 with 50 remaining progress + ach := GetAchData(0, 400) + if ach.Level != 6 { + t.Errorf("Level = %d, want 6", ach.Level) + } + if ach.Trophy != 0x40 { + t.Errorf("Trophy = 0x%02x, want 0x40 (bronze)", ach.Trophy) + } + if ach.NextValue != 15 { + t.Errorf("NextValue = %d, want 15", ach.NextValue) + } + if ach.Progress != 50 { + t.Errorf("Progress = %d, want 50", ach.Progress) + } + if ach.Required != 200 { + t.Errorf("Required = %d, want 200 (curve[6])", ach.Required) + } +} + +// TestGetAchData_Level7SilverTrophy tests that level 7 (in-progress toward level 8) +// awards the silver trophy (0x60). +// cumulative to level 7 = 5+15+30+50+100+150+200 = 550 +// score 600 = level 7 with 50 remaining progress +func TestGetAchData_Level7SilverTrophy(t *testing.T) { + ach := GetAchData(0, 600) + if ach.Level != 7 { + t.Errorf("Level = %d, want 7", ach.Level) + } + if ach.Trophy != 0x60 { + t.Errorf("Trophy = 0x%02x, want 0x60 (silver)", ach.Trophy) + } + if ach.NextValue != 20 { + t.Errorf("NextValue = %d, want 20", ach.NextValue) + } + if ach.Progress != 50 { + t.Errorf("Progress = %d, want 50", ach.Progress) + } + if ach.Required != 300 { + t.Errorf("Required = %d, want 300 (curve[7])", ach.Required) + } +} + +// TestGetAchData_MaxedOut_AllCurves tests that reaching max level on each curve +// produces the correct gold trophy and the last threshold as Required/Progress. +func TestGetAchData_MaxedOut_AllCurves(t *testing.T) { + tests := []struct { + name string + id uint8 + score int32 + lastThresh int32 + }{ + // Curve 0: {5,15,30,50,100,150,200,300} sum=850, last=300 + {"Curve0_ID0", 0, 5000, 300}, + // Curve 1: {1,5,10,15,30,50,75,100} sum=286, last=100 + {"Curve1_ID7", 7, 5000, 100}, + // Curve 2: {1,2,3,4,5,6,7,8} sum=36, last=8 + {"Curve2_ID8", 8, 5000, 8}, + // Curve 3: {10,50,100,200,350,500,750,999} sum=2959, last=999 + {"Curve3_ID16", 16, 50000, 999}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ach := GetAchData(tt.id, tt.score) + if ach.Level != 8 { + t.Errorf("Level = %d, want 8 (max)", ach.Level) + } + if ach.Trophy != 0x7F { + t.Errorf("Trophy = 0x%02x, want 0x7F (gold)", ach.Trophy) + } + if ach.Required != uint32(tt.lastThresh) { + t.Errorf("Required = %d, want %d", ach.Required, tt.lastThresh) + } + if ach.Progress != ach.Required { + t.Errorf("Progress = %d, want %d (should equal Required at max)", ach.Progress, ach.Required) + } + }) + } +} + +// TestGetAchData_ExactlyAtEachThreshold tests the exact cumulative score at each +// threshold boundary for curve 0. +func TestGetAchData_ExactlyAtEachThreshold(t *testing.T) { + // Curve 0: {5, 15, 30, 50, 100, 150, 200, 300} + // Cumulative thresholds (exact score to reach each level): + // L1: 5, L2: 20, L3: 50, L4: 100, L5: 200, L6: 350, L7: 550, L8: 850 + cumulativeScores := []int32{5, 20, 50, 100, 200, 350, 550, 850} + expectedLevels := []uint8{1, 2, 3, 4, 5, 6, 7, 8} + expectedValues := []uint32{5, 15, 25, 35, 50, 65, 80, 100} + + for i, score := range cumulativeScores { + t.Run("ExactThreshold_L"+string(rune('1'+i)), func(t *testing.T) { + ach := GetAchData(0, score) + if ach.Level != expectedLevels[i] { + t.Errorf("score=%d: Level = %d, want %d", score, ach.Level, expectedLevels[i]) + } + if ach.Value != expectedValues[i] { + t.Errorf("score=%d: Value = %d, want %d", score, ach.Value, expectedValues[i]) + } + }) + } +} + +// TestGetAchData_OneBeforeEachThreshold tests scores that are one less than +// each cumulative threshold, verifying they stay at the previous level. +func TestGetAchData_OneBeforeEachThreshold(t *testing.T) { + // Curve 0: cumulative thresholds: 5, 20, 50, 100, 200, 350, 550, 850 + cumulativeScores := []int32{4, 19, 49, 99, 199, 349, 549, 849} + expectedLevels := []uint8{0, 1, 2, 3, 4, 5, 6, 7} + + for i, score := range cumulativeScores { + t.Run("OneBeforeThreshold_L"+string(rune('0'+i)), func(t *testing.T) { + ach := GetAchData(0, score) + if ach.Level != expectedLevels[i] { + t.Errorf("score=%d: Level = %d, want %d", score, ach.Level, expectedLevels[i]) + } + }) + } +} + +// TestGetAchData_Curve2_FestaWins exercises the "Festa wins" curve which has +// small thresholds: {1, 2, 3, 4, 5, 6, 7, 8} +func TestGetAchData_Curve2_FestaWins(t *testing.T) { + // Curve 2: {1, 2, 3, 4, 5, 6, 7, 8} + // Cumulative: 1, 3, 6, 10, 15, 21, 28, 36 + tests := []struct { + score int32 + wantLvl uint8 + wantProg uint32 + wantReq uint32 + }{ + {0, 0, 0, 1}, + {1, 1, 0, 2}, // Exactly at first threshold + {2, 1, 1, 2}, // One into second threshold + {3, 2, 0, 3}, // Exactly at second cumulative + {36, 8, 8, 8}, // Max level (sum of all thresholds) + {100, 8, 8, 8}, // Well above max + } + + for _, tt := range tests { + t.Run("", func(t *testing.T) { + ach := GetAchData(8, tt.score) // ID 8 maps to curve 2 + if ach.Level != tt.wantLvl { + t.Errorf("score=%d: Level = %d, want %d", tt.score, ach.Level, tt.wantLvl) + } + if ach.Progress != tt.wantProg { + t.Errorf("score=%d: Progress = %d, want %d", tt.score, ach.Progress, tt.wantProg) + } + if ach.Required != tt.wantReq { + t.Errorf("score=%d: Required = %d, want %d", tt.score, ach.Required, tt.wantReq) + } + }) + } +} + +// TestGetAchData_AllIDs_ZeroScore verifies that calling GetAchData with score=0 +// for every valid ID returns level 0 without panicking. +func TestGetAchData_AllIDs_ZeroScore(t *testing.T) { + for id := uint8(0); id <= 32; id++ { + ach := GetAchData(id, 0) + if ach.Level != 0 { + t.Errorf("ID %d, score 0: Level = %d, want 0", id, ach.Level) + } + if ach.Value != 0 { + t.Errorf("ID %d, score 0: Value = %d, want 0", id, ach.Value) + } + if ach.Trophy != 0 { + t.Errorf("ID %d, score 0: Trophy = 0x%02x, want 0x00", id, ach.Trophy) + } + } +} + +// TestGetAchData_AllIDs_MaxScore verifies that calling GetAchData with a very +// high score for every valid ID returns level 8 with gold trophy. +func TestGetAchData_AllIDs_MaxScore(t *testing.T) { + for id := uint8(0); id <= 32; id++ { + ach := GetAchData(id, 99999) + if ach.Level != 8 { + t.Errorf("ID %d: Level = %d, want 8", id, ach.Level) + } + if ach.Trophy != 0x7F { + t.Errorf("ID %d: Trophy = 0x%02x, want 0x7F", id, ach.Trophy) + } + // At max, Progress should equal Required + if ach.Progress != ach.Required { + t.Errorf("ID %d: Progress (%d) != Required (%d) at max", id, ach.Progress, ach.Required) + } + } +} + +// TestGetAchData_UpdatedAlwaysFalse confirms Updated is always false since +// GetAchData never sets it. +func TestGetAchData_UpdatedAlwaysFalse(t *testing.T) { + scores := []int32{0, 1, 5, 50, 500, 5000} + for _, score := range scores { + ach := GetAchData(0, score) + if ach.Updated { + t.Errorf("score=%d: Updated should always be false, got true", score) + } + } +} diff --git a/server/channelserver/handlers_bbs_test.go b/server/channelserver/handlers_bbs_test.go new file mode 100644 index 000000000..8b0b09fc3 --- /dev/null +++ b/server/channelserver/handlers_bbs_test.go @@ -0,0 +1,77 @@ +package channelserver + +import ( + "testing" + + _config "erupe-ce/config" + "erupe-ce/network/mhfpacket" +) + +func TestHandleMsgMhfGetBbsUserStatus(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfGetBbsUserStatus{ + AckHandle: 12345, + } + + handleMsgMhfGetBbsUserStatus(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") + } +} + +func TestHandleMsgMhfGetBbsSnsStatus(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfGetBbsSnsStatus{ + AckHandle: 12345, + } + + handleMsgMhfGetBbsSnsStatus(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") + } +} + +func TestHandleMsgMhfApplyBbsArticle(t *testing.T) { + server := createMockServer() + server.erupeConfig = &_config.Config{ + Screenshots: _config.ScreenshotsOptions{ + Host: "example.com", + Port: 8080, + }, + } + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfApplyBbsArticle{ + AckHandle: 12345, + } + + handleMsgMhfApplyBbsArticle(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") + } +} diff --git a/server/channelserver/handlers_cafe_test.go b/server/channelserver/handlers_cafe_test.go new file mode 100644 index 000000000..5b5122159 --- /dev/null +++ b/server/channelserver/handlers_cafe_test.go @@ -0,0 +1,110 @@ +package channelserver + +import ( + "testing" + + "erupe-ce/network/mhfpacket" +) + +func TestHandleMsgMhfGetBoostTime(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfGetBoostTime{ + AckHandle: 12345, + } + + handleMsgMhfGetBoostTime(session, pkt) + + select { + case p := <-session.sendPackets: + // Response should be empty bytes for this handler + if p.data == nil { + t.Error("Response packet data should not be nil") + } + default: + t.Error("No response packet queued") + } +} + +func TestHandleMsgMhfPostBoostTimeQuestReturn(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfPostBoostTimeQuestReturn{ + AckHandle: 12345, + } + + handleMsgMhfPostBoostTimeQuestReturn(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 TestHandleMsgMhfPostBoostTime(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfPostBoostTime{ + AckHandle: 12345, + } + + handleMsgMhfPostBoostTime(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 TestHandleMsgMhfPostBoostTimeLimit(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfPostBoostTimeLimit{ + AckHandle: 12345, + } + + handleMsgMhfPostBoostTimeLimit(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 TestCafeBonusStruct(t *testing.T) { + // Test CafeBonus struct can be created + bonus := CafeBonus{ + ID: 1, + TimeReq: 3600, + ItemType: 1, + ItemID: 100, + Quantity: 5, + Claimed: false, + } + + if bonus.ID != 1 { + t.Errorf("ID = %d, want 1", bonus.ID) + } + if bonus.TimeReq != 3600 { + t.Errorf("TimeReq = %d, want 3600", bonus.TimeReq) + } + if bonus.Claimed { + t.Error("Claimed should be false") + } +} diff --git a/server/channelserver/handlers_campaign_test.go b/server/channelserver/handlers_campaign_test.go new file mode 100644 index 000000000..152e054b8 --- /dev/null +++ b/server/channelserver/handlers_campaign_test.go @@ -0,0 +1,70 @@ +package channelserver + +import ( + "testing" + + "erupe-ce/network/mhfpacket" +) + +func TestHandleMsgMhfEnumerateCampaign(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfEnumerateCampaign{ + AckHandle: 12345, + } + + handleMsgMhfEnumerateCampaign(session, pkt) + + // Verify response packet was queued (fail response expected) + 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 TestHandleMsgMhfStateCampaign(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfStateCampaign{ + AckHandle: 12345, + } + + handleMsgMhfStateCampaign(session, pkt) + + // Verify response packet was queued (fail response expected) + 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 TestHandleMsgMhfApplyCampaign(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfApplyCampaign{ + AckHandle: 12345, + } + + handleMsgMhfApplyCampaign(session, pkt) + + // Verify response packet was queued (fail response expected) + 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_caravan_test.go b/server/channelserver/handlers_caravan_test.go new file mode 100644 index 000000000..67c59a70f --- /dev/null +++ b/server/channelserver/handlers_caravan_test.go @@ -0,0 +1,141 @@ +package channelserver + +import ( + "testing" + + "erupe-ce/network/mhfpacket" +) + +func TestHandleMsgMhfGetRyoudama(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfGetRyoudama{ + AckHandle: 12345, + } + + handleMsgMhfGetRyoudama(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 TestHandleMsgMhfPostRyoudama(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + defer func() { + if r := recover(); r != nil { + t.Errorf("handleMsgMhfPostRyoudama panicked: %v", r) + } + }() + + handleMsgMhfPostRyoudama(session, nil) +} + +func TestHandleMsgMhfGetTinyBin(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfGetTinyBin{ + AckHandle: 12345, + } + + handleMsgMhfGetTinyBin(session, pkt) + + select { + case p := <-session.sendPackets: + // Response might be empty bytes + if p.data == nil { + t.Error("Response packet data should not be nil") + } + default: + t.Error("No response packet queued") + } +} + +func TestHandleMsgMhfPostTinyBin(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfPostTinyBin{ + AckHandle: 12345, + } + + handleMsgMhfPostTinyBin(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 TestHandleMsgMhfCaravanMyScore(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfCaravanMyScore{ + AckHandle: 12345, + } + + handleMsgMhfCaravanMyScore(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 TestHandleMsgMhfCaravanRanking(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfCaravanRanking{ + AckHandle: 12345, + } + + handleMsgMhfCaravanRanking(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 TestHandleMsgMhfCaravanMyRank(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfCaravanMyRank{ + AckHandle: 12345, + } + + handleMsgMhfCaravanMyRank(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_core_test.go b/server/channelserver/handlers_core_test.go new file mode 100644 index 000000000..f07a0d016 --- /dev/null +++ b/server/channelserver/handlers_core_test.go @@ -0,0 +1,700 @@ +package channelserver + +import ( + "testing" + + "erupe-ce/common/byteframe" + "erupe-ce/network/mhfpacket" +) + +// Test empty handlers don't panic + +func TestHandleMsgHead(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + defer func() { + if r := recover(); r != nil { + t.Errorf("handleMsgHead panicked: %v", r) + } + }() + + handleMsgHead(session, nil) +} + +func TestHandleMsgSysExtendThreshold(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + defer func() { + if r := recover(); r != nil { + t.Errorf("handleMsgSysExtendThreshold panicked: %v", r) + } + }() + + handleMsgSysExtendThreshold(session, nil) +} + +func TestHandleMsgSysEnd(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + defer func() { + if r := recover(); r != nil { + t.Errorf("handleMsgSysEnd panicked: %v", r) + } + }() + + handleMsgSysEnd(session, nil) +} + +func TestHandleMsgSysNop(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + defer func() { + if r := recover(); r != nil { + t.Errorf("handleMsgSysNop panicked: %v", r) + } + }() + + handleMsgSysNop(session, nil) +} + +func TestHandleMsgSysAck(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + defer func() { + if r := recover(); r != nil { + t.Errorf("handleMsgSysAck panicked: %v", r) + } + }() + + handleMsgSysAck(session, nil) +} + +func TestHandleMsgCaExchangeItem(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + defer func() { + if r := recover(); r != nil { + t.Errorf("handleMsgCaExchangeItem panicked: %v", r) + } + }() + + handleMsgCaExchangeItem(session, nil) +} + +func TestHandleMsgMhfServerCommand(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + defer func() { + if r := recover(); r != nil { + t.Errorf("handleMsgMhfServerCommand panicked: %v", r) + } + }() + + handleMsgMhfServerCommand(session, nil) +} + +func TestHandleMsgMhfSetLoginwindow(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + defer func() { + if r := recover(); r != nil { + t.Errorf("handleMsgMhfSetLoginwindow panicked: %v", r) + } + }() + + handleMsgMhfSetLoginwindow(session, nil) +} + +func TestHandleMsgSysTransBinary(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + defer func() { + if r := recover(); r != nil { + t.Errorf("handleMsgSysTransBinary panicked: %v", r) + } + }() + + handleMsgSysTransBinary(session, nil) +} + +func TestHandleMsgSysCollectBinary(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + defer func() { + if r := recover(); r != nil { + t.Errorf("handleMsgSysCollectBinary panicked: %v", r) + } + }() + + handleMsgSysCollectBinary(session, nil) +} + +func TestHandleMsgSysGetState(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + defer func() { + if r := recover(); r != nil { + t.Errorf("handleMsgSysGetState panicked: %v", r) + } + }() + + handleMsgSysGetState(session, nil) +} + +func TestHandleMsgSysSerialize(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + defer func() { + if r := recover(); r != nil { + t.Errorf("handleMsgSysSerialize panicked: %v", r) + } + }() + + handleMsgSysSerialize(session, nil) +} + +func TestHandleMsgSysEnumlobby(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + defer func() { + if r := recover(); r != nil { + t.Errorf("handleMsgSysEnumlobby panicked: %v", r) + } + }() + + handleMsgSysEnumlobby(session, nil) +} + +func TestHandleMsgSysEnumuser(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + defer func() { + if r := recover(); r != nil { + t.Errorf("handleMsgSysEnumuser panicked: %v", r) + } + }() + + handleMsgSysEnumuser(session, nil) +} + +func TestHandleMsgSysInfokyserver(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + defer func() { + if r := recover(); r != nil { + t.Errorf("handleMsgSysInfokyserver panicked: %v", r) + } + }() + + handleMsgSysInfokyserver(session, nil) +} + +func TestHandleMsgMhfGetCaUniqueID(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + defer func() { + if r := recover(); r != nil { + t.Errorf("handleMsgMhfGetCaUniqueID panicked: %v", r) + } + }() + + handleMsgMhfGetCaUniqueID(session, nil) +} + +func TestHandleMsgMhfEnumerateItem(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfEnumerateItem{ + AckHandle: 12345, + } + + handleMsgMhfEnumerateItem(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 TestHandleMsgMhfAcquireItem(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfAcquireItem{ + AckHandle: 12345, + } + + handleMsgMhfAcquireItem(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 TestHandleMsgMhfGetExtraInfo(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + defer func() { + if r := recover(); r != nil { + t.Errorf("handleMsgMhfGetExtraInfo panicked: %v", r) + } + }() + + handleMsgMhfGetExtraInfo(session, nil) +} + +// Test handlers that return simple responses + +func TestHandleMsgMhfTransferItem(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfTransferItem{ + AckHandle: 12345, + } + + handleMsgMhfTransferItem(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 TestHandleMsgMhfEnumeratePrice(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfEnumeratePrice{ + AckHandle: 12345, + } + + handleMsgMhfEnumeratePrice(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 TestHandleMsgMhfEnumerateOrder(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfEnumerateOrder{ + AckHandle: 12345, + } + + handleMsgMhfEnumerateOrder(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 terminal log handler + +func TestHandleMsgSysTerminalLog(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgSysTerminalLog{ + AckHandle: 12345, + LogID: 100, + Entries: []mhfpacket.TerminalLogEntry{}, + } + + handleMsgSysTerminalLog(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 TestHandleMsgSysTerminalLog_WithEntries(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgSysTerminalLog{ + AckHandle: 12345, + LogID: 100, + Entries: []mhfpacket.TerminalLogEntry{ + {Type1: 1, Type2: 2}, + {Type1: 3, Type2: 4}, + }, + } + + handleMsgSysTerminalLog(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 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, + Data: make([]byte, 256), // Must be large enough for ByteFrame reads (32 offset + 176 uint8s) + } + + 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"), + Data: byteframe.NewByteFrameFromBytes([]byte{0x00}), + } + + 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_coverage2_test.go b/server/channelserver/handlers_coverage2_test.go new file mode 100644 index 000000000..52533f796 --- /dev/null +++ b/server/channelserver/handlers_coverage2_test.go @@ -0,0 +1,920 @@ +package channelserver + +import ( + "testing" + + _config "erupe-ce/config" + "erupe-ce/network/mhfpacket" +) + +// Tests for guild handlers that do not require database access. + +func TestHandleMsgMhfEntryRookieGuild(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfEntryRookieGuild{ + AckHandle: 12345, + Unk: 42, + } + + handleMsgMhfEntryRookieGuild(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 TestHandleMsgMhfGenerateUdGuildMap(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfGenerateUdGuildMap{ + AckHandle: 12345, + } + + handleMsgMhfGenerateUdGuildMap(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 TestHandleMsgMhfCheckMonthlyItem(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfCheckMonthlyItem{ + AckHandle: 12345, + Type: 0, + } + + handleMsgMhfCheckMonthlyItem(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 TestHandleMsgMhfAcquireMonthlyItem(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfAcquireMonthlyItem{ + AckHandle: 12345, + } + + handleMsgMhfAcquireMonthlyItem(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 TestHandleMsgMhfEnumerateInvGuild(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfEnumerateInvGuild{ + AckHandle: 12345, + } + + handleMsgMhfEnumerateInvGuild(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 TestHandleMsgMhfOperationInvGuild(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfOperationInvGuild{ + AckHandle: 12345, + Operation: 1, + } + + handleMsgMhfOperationInvGuild(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") + } +} + +// Tests for mercenary handlers that do not require database access. + +func TestHandleMsgMhfMercenaryHuntdata_Unk0Is1(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfMercenaryHuntdata{ + AckHandle: 12345, + Unk0: 1, + } + + handleMsgMhfMercenaryHuntdata(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 TestHandleMsgMhfMercenaryHuntdata_Unk0Is0(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfMercenaryHuntdata{ + AckHandle: 12345, + Unk0: 0, + } + + handleMsgMhfMercenaryHuntdata(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 TestHandleMsgMhfMercenaryHuntdata_Unk0Is2(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfMercenaryHuntdata{ + AckHandle: 12345, + Unk0: 2, + } + + handleMsgMhfMercenaryHuntdata(session, pkt) + + // Unk0=2 takes the else branch (same as 0) + select { + case p := <-session.sendPackets: + if len(p.data) == 0 { + t.Error("Response packet should have data") + } + default: + t.Error("No response packet queued") + } +} + +// Tests for festa/ranking handlers. + +func TestHandleMsgMhfEnumerateRanking_DefaultBranch(t *testing.T) { + server := createMockServer() + server.erupeConfig = &_config.Config{ + DebugOptions: _config.DebugOptions{ + TournamentOverride: 0, + }, + } + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfEnumerateRanking{ + AckHandle: 99999, + } + + handleMsgMhfEnumerateRanking(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 TestHandleMsgMhfEnumerateRanking_NegativeState(t *testing.T) { + server := createMockServer() + server.erupeConfig = &_config.Config{ + DebugOptions: _config.DebugOptions{ + TournamentOverride: -1, + }, + } + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfEnumerateRanking{ + AckHandle: 99999, + } + + handleMsgMhfEnumerateRanking(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") + } +} + +// Tests for rengoku handlers. + +func TestHandleMsgMhfGetRengokuRankingRank_ResponseData(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfGetRengokuRankingRank{ + AckHandle: 55555, + } + + handleMsgMhfGetRengokuRankingRank(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") + } +} + +// Tests for empty handlers that are not covered in other test files. + +func TestEmptyHandlers_Coverage2(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + tests := []struct { + name string + handler func(s *Session, p mhfpacket.MHFPacket) + }{ + {"handleMsgSysCastedBinary", handleMsgSysCastedBinary}, + {"handleMsgMhfResetTitle", handleMsgMhfResetTitle}, + {"handleMsgMhfUpdateForceGuildRank", handleMsgMhfUpdateForceGuildRank}, + {"handleMsgMhfUpdateGuild", handleMsgMhfUpdateGuild}, + {"handleMsgMhfUpdateGuildcard", handleMsgMhfUpdateGuildcard}, + } + + 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) + }) + } +} + +// Tests for handlers.go - handlers that produce responses without DB access. + +func TestHandleMsgSysTerminalLog_MultipleEntries(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgSysTerminalLog{ + AckHandle: 12345, + LogID: 200, + Entries: []mhfpacket.TerminalLogEntry{ + {Type1: 10, Type2: 20}, + {Type1: 11, Type2: 21}, + {Type1: 12, Type2: 22}, + }, + } + + handleMsgSysTerminalLog(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 TestHandleMsgSysTerminalLog_ZeroLogID(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgSysTerminalLog{ + AckHandle: 12345, + LogID: 0, + Entries: []mhfpacket.TerminalLogEntry{}, + } + + handleMsgSysTerminalLog(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 TestHandleMsgSysPing_DifferentAckHandle(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgSysPing{ + AckHandle: 0xFFFFFFFF, + } + + 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") + } +} + +func TestHandleMsgSysTime_GetRemoteTimeFalse(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgSysTime{ + GetRemoteTime: false, + } + + 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") + } +} + +func TestHandleMsgSysIssueLogkey_LogKeyGenerated(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgSysIssueLogkey{ + AckHandle: 77777, + } + + handleMsgSysIssueLogkey(session, pkt) + + // Verify that the logKey was set on the session + session.Lock() + keyLen := len(session.logKey) + session.Unlock() + + if keyLen != 16 { + t.Errorf("logKey length = %d, want 16", keyLen) + } + + 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 TestHandleMsgSysIssueLogkey_Uniqueness(t *testing.T) { + server := createMockServer() + + // Generate two logkeys and verify they differ + session1 := createMockSession(1, server) + session2 := createMockSession(2, server) + + pkt1 := &mhfpacket.MsgSysIssueLogkey{AckHandle: 1} + pkt2 := &mhfpacket.MsgSysIssueLogkey{AckHandle: 2} + + handleMsgSysIssueLogkey(session1, pkt1) + handleMsgSysIssueLogkey(session2, pkt2) + + // Drain send packets + <-session1.sendPackets + <-session2.sendPackets + + session1.Lock() + key1 := make([]byte, len(session1.logKey)) + copy(key1, session1.logKey) + session1.Unlock() + + session2.Lock() + key2 := make([]byte, len(session2.logKey)) + copy(key2, session2.logKey) + session2.Unlock() + + if len(key1) != 16 || len(key2) != 16 { + t.Fatalf("logKeys should be 16 bytes each, got %d and %d", len(key1), len(key2)) + } + + same := true + for i := range key1 { + if key1[i] != key2[i] { + same = false + break + } + } + if same { + t.Error("Two generated logkeys should differ (extremely unlikely to be the same)") + } +} + +// Tests for event handlers. + +func TestHandleMsgMhfReleaseEvent_ErrorCode(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfReleaseEvent{ + AckHandle: 88888, + } + + handleMsgMhfReleaseEvent(session, pkt) + + // This handler manually sends a response with error code 0x41 + 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 TestHandleMsgMhfEnumerateEvent_Stub(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfEnumerateEvent{ + AckHandle: 77777, + } + + handleMsgMhfEnumerateEvent(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") + } +} + +// Tests for achievement handler. + +func TestHandleMsgMhfSetCaAchievementHist_Response(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfSetCaAchievementHist{ + AckHandle: 44444, + } + + handleMsgMhfSetCaAchievementHist(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 concurrent handler invocations to catch potential data races. + +func TestHandlersConcurrentInvocations(t *testing.T) { + server := createMockServer() + + done := make(chan struct{}) + const numGoroutines = 10 + + for i := 0; i < numGoroutines; i++ { + go func(id uint32) { + defer func() { + if r := recover(); r != nil { + t.Errorf("goroutine %d panicked: %v", id, r) + } + done <- struct{}{} + }() + + session := createMockSession(id, server) + + // Run several handlers concurrently + handleMsgSysPing(session, &mhfpacket.MsgSysPing{AckHandle: id}) + <-session.sendPackets + + handleMsgSysTime(session, &mhfpacket.MsgSysTime{GetRemoteTime: true}) + <-session.sendPackets + + handleMsgSysIssueLogkey(session, &mhfpacket.MsgSysIssueLogkey{AckHandle: id}) + <-session.sendPackets + + handleMsgMhfMercenaryHuntdata(session, &mhfpacket.MsgMhfMercenaryHuntdata{AckHandle: id, Unk0: 1}) + <-session.sendPackets + + handleMsgMhfEnumerateMercenaryLog(session, &mhfpacket.MsgMhfEnumerateMercenaryLog{AckHandle: id}) + <-session.sendPackets + }(uint32(i + 100)) + } + + for i := 0; i < numGoroutines; i++ { + <-done + } +} + +// Test record log handler with stage setup. + +func TestHandleMsgSysRecordLog_RemovesReservation(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + stage := NewStage("test_stage_record") + session.stage = stage + stage.reservedClientSlots[session.charID] = true + + pkt := &mhfpacket.MsgSysRecordLog{ + AckHandle: 55555, + Data: make([]byte, 256), + } + + handleMsgSysRecordLog(session, pkt) + + if _, exists := stage.reservedClientSlots[session.charID]; exists { + t.Error("charID should be removed from reserved slots after record log") + } + + 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 TestHandleMsgSysRecordLog_NoExistingReservation(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + stage := NewStage("test_stage_no_reservation") + session.stage = stage + // No reservation exists for this charID + + pkt := &mhfpacket.MsgSysRecordLog{ + AckHandle: 55556, + Data: make([]byte, 256), + } + + // Should not panic even if charID is not in reservedClientSlots + handleMsgSysRecordLog(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 unlock global sema handler. + +func TestHandleMsgSysUnlockGlobalSema_Response(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgSysUnlockGlobalSema{ + AckHandle: 66666, + } + + 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 handlers from handlers_event.go with edge cases. + +func TestHandleMsgMhfSetRestrictionEvent_Response(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfSetRestrictionEvent{ + AckHandle: 11111, + } + + handleMsgMhfSetRestrictionEvent(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 TestHandleMsgMhfGetRestrictionEvent_Empty(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + defer func() { + if r := recover(); r != nil { + t.Errorf("handleMsgMhfGetRestrictionEvent panicked: %v", r) + } + }() + + handleMsgMhfGetRestrictionEvent(session, nil) +} + +// Test handlers from handlers_mercenary.go - legend dispatch (no DB). + +func TestHandleMsgMhfLoadLegendDispatch_Response(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfLoadLegendDispatch{ + AckHandle: 22222, + } + + 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") + } +} + +// Test multiple handler invocations on the same session to verify session state is not corrupted. + +func TestMultipleHandlersOnSameSession(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + // Call multiple handlers in sequence + handleMsgSysPing(session, &mhfpacket.MsgSysPing{AckHandle: 1}) + select { + case <-session.sendPackets: + default: + t.Fatal("Expected packet from Ping handler") + } + + handleMsgSysTime(session, &mhfpacket.MsgSysTime{GetRemoteTime: true}) + select { + case <-session.sendPackets: + default: + t.Fatal("Expected packet from Time handler") + } + + handleMsgMhfRegisterEvent(session, &mhfpacket.MsgMhfRegisterEvent{AckHandle: 2, WorldID: 5, LandID: 10}) + select { + case <-session.sendPackets: + default: + t.Fatal("Expected packet from RegisterEvent handler") + } + + handleMsgMhfReleaseEvent(session, &mhfpacket.MsgMhfReleaseEvent{AckHandle: 3}) + select { + case <-session.sendPackets: + default: + t.Fatal("Expected packet from ReleaseEvent handler") + } + + handleMsgMhfEnumerateEvent(session, &mhfpacket.MsgMhfEnumerateEvent{AckHandle: 4}) + select { + case <-session.sendPackets: + default: + t.Fatal("Expected packet from EnumerateEvent handler") + } + + handleMsgMhfSetCaAchievementHist(session, &mhfpacket.MsgMhfSetCaAchievementHist{AckHandle: 5}) + select { + case <-session.sendPackets: + default: + t.Fatal("Expected packet from SetCaAchievementHist handler") + } + + handleMsgMhfGetRengokuRankingRank(session, &mhfpacket.MsgMhfGetRengokuRankingRank{AckHandle: 6}) + select { + case <-session.sendPackets: + default: + t.Fatal("Expected packet from GetRengokuRankingRank handler") + } +} + +// Test festa timestamp generation. + +func TestGenerateFestaTimestamps_Debug(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + tests := []struct { + name string + start uint32 + }{ + {"Debug_Start1", 1}, + {"Debug_Start2", 2}, + {"Debug_Start3", 3}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + timestamps := generateFestaTimestamps(session, tt.start, true) + if len(timestamps) != 5 { + t.Errorf("Expected 5 timestamps, got %d", len(timestamps)) + } + for i, ts := range timestamps { + if ts == 0 { + t.Errorf("Timestamp %d should not be zero", i) + } + } + }) + } +} + +func TestGenerateFestaTimestamps_NonDebug_FutureStart(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + // Use a far-future start time so it does not trigger cleanup + futureStart := uint32(TimeAdjusted().Unix() + 5000000) + timestamps := generateFestaTimestamps(session, futureStart, false) + + if len(timestamps) != 5 { + t.Errorf("Expected 5 timestamps, got %d", len(timestamps)) + } + if timestamps[0] != futureStart { + t.Errorf("First timestamp = %d, want %d", timestamps[0], futureStart) + } + // Verify intervals + if timestamps[1] != timestamps[0]+604800 { + t.Errorf("Second timestamp should be start+604800, got %d", timestamps[1]) + } + if timestamps[2] != timestamps[1]+604800 { + t.Errorf("Third timestamp should be second+604800, got %d", timestamps[2]) + } + if timestamps[3] != timestamps[2]+9000 { + t.Errorf("Fourth timestamp should be third+9000, got %d", timestamps[3]) + } + if timestamps[4] != timestamps[3]+1240200 { + t.Errorf("Fifth timestamp should be fourth+1240200, got %d", timestamps[4]) + } +} + +// Test trial struct from handlers_festa.go. + +func TestFestaTrialStruct(t *testing.T) { + trial := FestaTrial{ + ID: 100, + Objective: 2, + GoalID: 500, + TimesReq: 10, + Locale: 1, + Reward: 50, + } + if trial.ID != 100 { + t.Errorf("ID = %d, want 100", trial.ID) + } + if trial.Objective != 2 { + t.Errorf("Objective = %d, want 2", trial.Objective) + } + if trial.GoalID != 500 { + t.Errorf("GoalID = %d, want 500", trial.GoalID) + } + if trial.TimesReq != 10 { + t.Errorf("TimesReq = %d, want 10", trial.TimesReq) + } +} + +// Test prize struct from handlers_festa.go. + +func TestPrizeStruct(t *testing.T) { + prize := Prize{ + ID: 1, + Tier: 2, + SoulsReq: 100, + ItemID: 0x1234, + NumItem: 5, + Claimed: 1, + } + if prize.ID != 1 { + t.Errorf("ID = %d, want 1", prize.ID) + } + if prize.Tier != 2 { + t.Errorf("Tier = %d, want 2", prize.Tier) + } + if prize.SoulsReq != 100 { + t.Errorf("SoulsReq = %d, want 100", prize.SoulsReq) + } + if prize.Claimed != 1 { + t.Errorf("Claimed = %d, want 1", prize.Claimed) + } +} + +// Test Airou struct from handlers_mercenary.go. + +func TestAirouStruct(t *testing.T) { + cat := Airou{ + ID: 42, + Name: []byte("TestCat"), + Task: 4, + Personality: 2, + Class: 1, + Experience: 1500, + WeaponType: 6, + WeaponID: 100, + } + + if cat.ID != 42 { + t.Errorf("ID = %d, want 42", cat.ID) + } + if cat.Task != 4 { + t.Errorf("Task = %d, want 4", cat.Task) + } + if cat.Experience != 1500 { + t.Errorf("Experience = %d, want 1500", cat.Experience) + } + if cat.WeaponType != 6 { + t.Errorf("WeaponType = %d, want 6", cat.WeaponType) + } + if cat.WeaponID != 100 { + t.Errorf("WeaponID = %d, want 100", cat.WeaponID) + } +} + +// Test RengokuScore struct default values. + +func TestRengokuScoreStruct_Fields(t *testing.T) { + score := RengokuScore{ + Name: "Hunter", + Score: 99999, + } + + if score.Name != "Hunter" { + t.Errorf("Name = %s, want Hunter", score.Name) + } + if score.Score != 99999 { + t.Errorf("Score = %d, want 99999", score.Score) + } +} diff --git a/server/channelserver/handlers_coverage3_test.go b/server/channelserver/handlers_coverage3_test.go new file mode 100644 index 000000000..6eb19eeaf --- /dev/null +++ b/server/channelserver/handlers_coverage3_test.go @@ -0,0 +1,1135 @@ +package channelserver + +import ( + "sync" + "testing" + + "erupe-ce/network/mhfpacket" +) + +// ============================================================================= +// Category 1: Empty handlers from handlers.go +// These have empty function bodies and can be called with nil packet safely. +// ============================================================================= + +func TestEmptyHandlers_HandlersGo(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + tests := []struct { + name string + fn func() + }{ + {"handleMsgSysEcho", func() { handleMsgSysEcho(session, nil) }}, + {"handleMsgSysUpdateRight", func() { handleMsgSysUpdateRight(session, nil) }}, + {"handleMsgSysAuthQuery", func() { handleMsgSysAuthQuery(session, nil) }}, + {"handleMsgSysAuthTerminal", func() { handleMsgSysAuthTerminal(session, nil) }}, + {"handleMsgCaExchangeItem", func() { handleMsgCaExchangeItem(session, nil) }}, + {"handleMsgMhfServerCommand", func() { handleMsgMhfServerCommand(session, nil) }}, + {"handleMsgMhfSetLoginwindow", func() { handleMsgMhfSetLoginwindow(session, nil) }}, + {"handleMsgSysTransBinary", func() { handleMsgSysTransBinary(session, nil) }}, + {"handleMsgSysCollectBinary", func() { handleMsgSysCollectBinary(session, nil) }}, + {"handleMsgSysGetState", func() { handleMsgSysGetState(session, nil) }}, + {"handleMsgSysSerialize", func() { handleMsgSysSerialize(session, nil) }}, + {"handleMsgSysEnumlobby", func() { handleMsgSysEnumlobby(session, nil) }}, + {"handleMsgSysEnumuser", func() { handleMsgSysEnumuser(session, nil) }}, + {"handleMsgSysInfokyserver", func() { handleMsgSysInfokyserver(session, nil) }}, + {"handleMsgMhfGetCaUniqueID", func() { handleMsgMhfGetCaUniqueID(session, nil) }}, + {"handleMsgMhfGetExtraInfo", func() { handleMsgMhfGetExtraInfo(session, nil) }}, + {"handleMsgSysSetStatus", func() { handleMsgSysSetStatus(session, nil) }}, + {"handleMsgMhfStampcardPrize", func() { handleMsgMhfStampcardPrize(session, nil) }}, + {"handleMsgMhfKickExportForce", func() { handleMsgMhfKickExportForce(session, nil) }}, + {"handleMsgMhfRegistSpabiTime", func() { handleMsgMhfRegistSpabiTime(session, nil) }}, + {"handleMsgMhfDebugPostValue", func() { handleMsgMhfDebugPostValue(session, nil) }}, + } + + 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.fn() + }) + } +} + +// ============================================================================= +// Category 2: Empty handlers from handlers_object.go +// All empty function bodies, safe to call with nil packet. +// ============================================================================= + +func TestEmptyHandlers_ObjectGo(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + tests := []struct { + name string + fn func() + }{ + {"handleMsgSysDeleteObject", func() { handleMsgSysDeleteObject(session, nil) }}, + {"handleMsgSysRotateObject", func() { handleMsgSysRotateObject(session, nil) }}, + {"handleMsgSysDuplicateObject", func() { handleMsgSysDuplicateObject(session, nil) }}, + {"handleMsgSysGetObjectBinary", func() { handleMsgSysGetObjectBinary(session, nil) }}, + {"handleMsgSysGetObjectOwner", func() { handleMsgSysGetObjectOwner(session, nil) }}, + {"handleMsgSysUpdateObjectBinary", func() { handleMsgSysUpdateObjectBinary(session, nil) }}, + {"handleMsgSysCleanupObject", func() { handleMsgSysCleanupObject(session, nil) }}, + } + + 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.fn() + }) + } +} + +// ============================================================================= +// Category 3: Empty handlers from handlers_clients.go +// All empty function bodies, safe to call with nil packet. +// ============================================================================= + +func TestEmptyHandlers_ClientsGo(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + tests := []struct { + name string + fn func() + }{ + {"handleMsgMhfShutClient", func() { handleMsgMhfShutClient(session, nil) }}, + {"handleMsgSysHideClient", func() { handleMsgSysHideClient(session, nil) }}, + } + + 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.fn() + }) + } +} + +// ============================================================================= +// Category 4: Empty handler from handlers_stage.go +// ============================================================================= + +func TestEmptyHandlers_StageGo(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + tests := []struct { + name string + fn func() + }{ + {"handleMsgSysStageDestruct", func() { handleMsgSysStageDestruct(session, nil) }}, + } + + 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.fn() + }) + } +} + +// ============================================================================= +// Category 5: Empty handlers from handlers_achievement.go +// ============================================================================= + +func TestEmptyHandlers_AchievementGo(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + tests := []struct { + name string + fn func() + }{ + {"handleMsgMhfDisplayedAchievement", func() { + handleMsgMhfDisplayedAchievement(session, &mhfpacket.MsgMhfDisplayedAchievement{}) + }}, + {"handleMsgMhfGetCaAchievementHist", func() { handleMsgMhfGetCaAchievementHist(session, nil) }}, + {"handleMsgMhfSetCaAchievement", func() { handleMsgMhfSetCaAchievement(session, nil) }}, + } + + 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.fn() + }) + } +} + +// ============================================================================= +// Category 6: Empty handlers from handlers_caravan.go +// ============================================================================= + +// TestEmptyHandlers_CaravanGo removed: caravan handlers on main do type assertions +// and require proper packet structs, not nil. + +// ============================================================================= +// Category 7: Simple ack handlers from handlers_tactics.go (no DB needed) +// ============================================================================= + +func TestSimpleAckHandlers_TacticsGo(t *testing.T) { + server := createMockServer() + + tests := []struct { + name string + fn func(s *Session) + }{ + {"handleMsgMhfAddUdTacticsPoint", func(s *Session) { + handleMsgMhfAddUdTacticsPoint(s, &mhfpacket.MsgMhfAddUdTacticsPoint{AckHandle: 1}) + }}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + session := createMockSession(1, server) + tt.fn(session) + select { + case p := <-session.sendPackets: + if len(p.data) == 0 { + t.Errorf("%s: response should have data", tt.name) + } + default: + t.Errorf("%s: no response queued", tt.name) + } + }) + } +} + +// TestSimpleAckHandlers_TowerGo removed: tower handlers on main access s.server.db +// and cannot be tested without a database connection. + +// ============================================================================= +// Category 9: Simple ack handlers from handlers_reward.go (no DB needed) +// ============================================================================= + +func TestSimpleAckHandlers_RewardGo(t *testing.T) { + server := createMockServer() + + tests := []struct { + name string + fn func(s *Session) + }{ + {"handleMsgMhfGetRewardSong", func(s *Session) { + handleMsgMhfGetRewardSong(s, &mhfpacket.MsgMhfGetRewardSong{AckHandle: 1}) + }}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + session := createMockSession(1, server) + tt.fn(session) + select { + case p := <-session.sendPackets: + if len(p.data) == 0 { + t.Errorf("%s: response should have data", tt.name) + } + default: + t.Errorf("%s: no response queued", tt.name) + } + }) + } +} + +// ============================================================================= +// Category 10: Simple ack handler from handlers_semaphore.go (no DB needed) +// handleMsgSysCreateSemaphore produces a response via doAckSimpleSucceed. +// ============================================================================= + +func TestSimpleAckHandlers_SemaphoreGo(t *testing.T) { + server := createMockServer() + + t.Run("handleMsgSysCreateSemaphore", func(t *testing.T) { + session := createMockSession(1, server) + handleMsgSysCreateSemaphore(session, &mhfpacket.MsgSysCreateSemaphore{AckHandle: 1}) + select { + case p := <-session.sendPackets: + if len(p.data) == 0 { + t.Error("handleMsgSysCreateSemaphore: response should have data") + } + default: + t.Error("handleMsgSysCreateSemaphore: no response queued") + } + }) +} + +// ============================================================================= +// Category 11: handleMsgSysCreateAcquireSemaphore from handlers_semaphore.go +// This handler accesses s.server.semaphore map. It creates or acquires a +// semaphore, so it needs the semaphore map initialized on the server. +// ============================================================================= + +func TestHandleMsgSysCreateAcquireSemaphore(t *testing.T) { + server := createMockServer() + server.semaphore = make(map[string]*Semaphore) + + t.Run("creates_new_semaphore", func(t *testing.T) { + session := createMockSession(1, server) + handleMsgSysCreateAcquireSemaphore(session, &mhfpacket.MsgSysCreateAcquireSemaphore{ + AckHandle: 1, + SemaphoreID: "test_sema_1", + }) + select { + case p := <-session.sendPackets: + if len(p.data) == 0 { + t.Error("response should have data") + } + default: + t.Error("no response queued") + } + // Verify semaphore was created + if _, exists := server.semaphore["test_sema_1"]; !exists { + t.Error("semaphore should have been created in server map") + } + }) + + t.Run("acquires_existing_semaphore", func(t *testing.T) { + session := createMockSession(2, server) + // Acquire the same semaphore again + handleMsgSysCreateAcquireSemaphore(session, &mhfpacket.MsgSysCreateAcquireSemaphore{ + AckHandle: 2, + SemaphoreID: "test_sema_1", + }) + select { + case p := <-session.sendPackets: + if len(p.data) == 0 { + t.Error("response should have data") + } + default: + t.Error("no response queued") + } + }) + + t.Run("creates_ravi_semaphore", func(t *testing.T) { + session := createMockSession(3, server) + handleMsgSysCreateAcquireSemaphore(session, &mhfpacket.MsgSysCreateAcquireSemaphore{ + AckHandle: 3, + SemaphoreID: "hs_l0u3B51", + }) + select { + case p := <-session.sendPackets: + if len(p.data) == 0 { + t.Error("response should have data") + } + default: + t.Error("no response queued") + } + if _, exists := server.semaphore["hs_l0u3B51"]; !exists { + t.Error("ravi semaphore should have been created") + } + }) +} + +// ============================================================================= +// Category 12: Additional simple ack handlers from various files (no DB) +// ============================================================================= + +// TestSimpleAckHandlers_MiscFiles removed: handleMsgMhfGetRengokuBinary panics +// on missing file (explicit panic in handler), cannot test without rengoku_data.bin. + +// ============================================================================= +// Category 13: Other empty handlers from various files +// ============================================================================= + +func TestEmptyHandlers_MiscFiles(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + tests := []struct { + name string + fn func() + }{ + // From handlers_reward.go + {"handleMsgMhfUseRewardSong", func() { handleMsgMhfUseRewardSong(session, nil) }}, + {"handleMsgMhfAddRewardSongCount", func() { handleMsgMhfAddRewardSongCount(session, nil) }}, + {"handleMsgMhfAcceptReadReward", func() { handleMsgMhfAcceptReadReward(session, nil) }}, + // From handlers_caravan.go + {"handleMsgMhfPostRyoudama", func() { handleMsgMhfPostRyoudama(session, nil) }}, + // From handlers_tactics.go + {"handleMsgMhfSetUdTacticsFollower", func() { handleMsgMhfSetUdTacticsFollower(session, nil) }}, + {"handleMsgMhfGetUdTacticsLog", func() { handleMsgMhfGetUdTacticsLog(session, nil) }}, + // From handlers_achievement.go + {"handleMsgMhfPaymentAchievement", func() { handleMsgMhfPaymentAchievement(session, nil) }}, + // From handlers.go (additional empty ones) + {"handleMsgMhfGetCogInfo", func() { handleMsgMhfGetCogInfo(session, nil) }}, + {"handleMsgMhfUseUdShopCoin", func() { handleMsgMhfUseUdShopCoin(session, nil) }}, + {"handleMsgMhfGetDailyMissionMaster", func() { handleMsgMhfGetDailyMissionMaster(session, nil) }}, + {"handleMsgMhfGetDailyMissionPersonal", func() { handleMsgMhfGetDailyMissionPersonal(session, nil) }}, + {"handleMsgMhfSetDailyMissionPersonal", func() { handleMsgMhfSetDailyMissionPersonal(session, nil) }}, + // From handlers_object.go (additional empty ones) + {"handleMsgSysAddObject", func() { handleMsgSysAddObject(session, nil) }}, + {"handleMsgSysDelObject", func() { handleMsgSysDelObject(session, nil) }}, + {"handleMsgSysDispObject", func() { handleMsgSysDispObject(session, nil) }}, + {"handleMsgSysHideObject", func() { handleMsgSysHideObject(session, nil) }}, + // From handlers.go (non-trivial but no pkt dereference) + {"handleMsgHead", func() { handleMsgHead(session, nil) }}, + {"handleMsgSysExtendThreshold", func() { handleMsgSysExtendThreshold(session, nil) }}, + {"handleMsgSysEnd", func() { handleMsgSysEnd(session, nil) }}, + {"handleMsgSysNop", func() { handleMsgSysNop(session, nil) }}, + {"handleMsgSysAck", func() { handleMsgSysAck(session, nil) }}, + // From handlers_semaphore.go + {"handleMsgSysReleaseSemaphore", func() { handleMsgSysReleaseSemaphore(session, nil) }}, + } + + 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.fn() + }) + } +} + +// ============================================================================= +// Category 14: Handlers that produce responses without DB access +// These are non-trivial handlers with static/canned responses. +// ============================================================================= + +func TestNonTrivialHandlers_NoDB(t *testing.T) { + server := createMockServer() + + t.Run("handleMsgMhfGetEarthStatus", func(t *testing.T) { + session := createMockSession(1, server) + handleMsgMhfGetEarthStatus(session, &mhfpacket.MsgMhfGetEarthStatus{AckHandle: 1}) + select { + case p := <-session.sendPackets: + if len(p.data) == 0 { + t.Error("response should have data") + } + default: + t.Error("no response queued") + } + }) + + t.Run("handleMsgMhfGetEarthValue_Type1", func(t *testing.T) { + session := createMockSession(1, server) + handleMsgMhfGetEarthValue(session, &mhfpacket.MsgMhfGetEarthValue{AckHandle: 1, ReqType: 1}) + select { + case p := <-session.sendPackets: + if len(p.data) == 0 { + t.Error("response should have data") + } + default: + t.Error("no response queued") + } + }) + + t.Run("handleMsgMhfGetEarthValue_Type2", func(t *testing.T) { + session := createMockSession(1, server) + handleMsgMhfGetEarthValue(session, &mhfpacket.MsgMhfGetEarthValue{AckHandle: 1, ReqType: 2}) + select { + case p := <-session.sendPackets: + if len(p.data) == 0 { + t.Error("response should have data") + } + default: + t.Error("no response queued") + } + }) + + t.Run("handleMsgMhfGetEarthValue_Type3", func(t *testing.T) { + session := createMockSession(1, server) + handleMsgMhfGetEarthValue(session, &mhfpacket.MsgMhfGetEarthValue{AckHandle: 1, ReqType: 3}) + select { + case p := <-session.sendPackets: + if len(p.data) == 0 { + t.Error("response should have data") + } + default: + t.Error("no response queued") + } + }) + + t.Run("handleMsgMhfGetSeibattle", func(t *testing.T) { + session := createMockSession(1, server) + handleMsgMhfGetSeibattle(session, &mhfpacket.MsgMhfGetSeibattle{AckHandle: 1}) + select { + case p := <-session.sendPackets: + if len(p.data) == 0 { + t.Error("response should have data") + } + default: + t.Error("no response queued") + } + }) + + // handleMsgMhfGetTrendWeapon removed: requires database access + + // handleMsgMhfUpdateUseTrendWeaponLog removed: requires database access + + t.Run("handleMsgMhfUpdateBeatLevel", func(t *testing.T) { + session := createMockSession(1, server) + handleMsgMhfUpdateBeatLevel(session, &mhfpacket.MsgMhfUpdateBeatLevel{AckHandle: 1}) + select { + case p := <-session.sendPackets: + if len(p.data) == 0 { + t.Error("response should have data") + } + default: + t.Error("no response queued") + } + }) + + t.Run("handleMsgMhfReadBeatLevel", func(t *testing.T) { + session := createMockSession(1, server) + handleMsgMhfReadBeatLevel(session, &mhfpacket.MsgMhfReadBeatLevel{ + AckHandle: 1, + ValidIDCount: 2, + IDs: [16]uint32{100, 200}, + }) + select { + case p := <-session.sendPackets: + if len(p.data) == 0 { + t.Error("response should have data") + } + default: + t.Error("no response queued") + } + }) + + t.Run("handleMsgMhfTransferItem", func(t *testing.T) { + session := createMockSession(1, server) + handleMsgMhfTransferItem(session, &mhfpacket.MsgMhfTransferItem{AckHandle: 1}) + select { + case p := <-session.sendPackets: + if len(p.data) == 0 { + t.Error("response should have data") + } + default: + t.Error("no response queued") + } + }) + + t.Run("handleMsgMhfEnumerateOrder", func(t *testing.T) { + session := createMockSession(1, server) + handleMsgMhfEnumerateOrder(session, &mhfpacket.MsgMhfEnumerateOrder{AckHandle: 1}) + select { + case p := <-session.sendPackets: + if len(p.data) == 0 { + t.Error("response should have data") + } + default: + t.Error("no response queued") + } + }) + + t.Run("handleMsgMhfGetUdShopCoin", func(t *testing.T) { + session := createMockSession(1, server) + handleMsgMhfGetUdShopCoin(session, &mhfpacket.MsgMhfGetUdShopCoin{AckHandle: 1}) + select { + case p := <-session.sendPackets: + if len(p.data) == 0 { + t.Error("response should have data") + } + default: + t.Error("no response queued") + } + }) + + t.Run("handleMsgMhfGetLobbyCrowd", func(t *testing.T) { + session := createMockSession(1, server) + handleMsgMhfGetLobbyCrowd(session, &mhfpacket.MsgMhfGetLobbyCrowd{AckHandle: 1}) + select { + case p := <-session.sendPackets: + if len(p.data) == 0 { + t.Error("response should have data") + } + default: + t.Error("no response queued") + } + }) + + t.Run("handleMsgMhfEnumeratePrice", func(t *testing.T) { + session := createMockSession(1, server) + handleMsgMhfEnumeratePrice(session, &mhfpacket.MsgMhfEnumeratePrice{AckHandle: 1}) + select { + case p := <-session.sendPackets: + if len(p.data) == 0 { + t.Error("response should have data") + } + default: + t.Error("no response queued") + } + }) +} + +// ============================================================================= +// Category 15: Handlers from handlers_tactics.go that produce responses (no DB) +// ============================================================================= + +func TestNonTrivialHandlers_TacticsGo(t *testing.T) { + server := createMockServer() + + tests := []struct { + name string + fn func(s *Session) + }{ + {"handleMsgMhfGetUdTacticsPoint", func(s *Session) { + handleMsgMhfGetUdTacticsPoint(s, &mhfpacket.MsgMhfGetUdTacticsPoint{AckHandle: 1}) + }}, + {"handleMsgMhfGetUdTacticsRewardList", func(s *Session) { + handleMsgMhfGetUdTacticsRewardList(s, &mhfpacket.MsgMhfGetUdTacticsRewardList{AckHandle: 1}) + }}, + {"handleMsgMhfGetUdTacticsFollower", func(s *Session) { + handleMsgMhfGetUdTacticsFollower(s, &mhfpacket.MsgMhfGetUdTacticsFollower{AckHandle: 1}) + }}, + {"handleMsgMhfGetUdTacticsBonusQuest", func(s *Session) { + handleMsgMhfGetUdTacticsBonusQuest(s, &mhfpacket.MsgMhfGetUdTacticsBonusQuest{AckHandle: 1}) + }}, + {"handleMsgMhfGetUdTacticsFirstQuestBonus", func(s *Session) { + handleMsgMhfGetUdTacticsFirstQuestBonus(s, &mhfpacket.MsgMhfGetUdTacticsFirstQuestBonus{AckHandle: 1}) + }}, + {"handleMsgMhfGetUdTacticsRemainingPoint", func(s *Session) { + handleMsgMhfGetUdTacticsRemainingPoint(s, &mhfpacket.MsgMhfGetUdTacticsRemainingPoint{AckHandle: 1}) + }}, + {"handleMsgMhfGetUdTacticsRanking", func(s *Session) { + handleMsgMhfGetUdTacticsRanking(s, &mhfpacket.MsgMhfGetUdTacticsRanking{AckHandle: 1}) + }}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + session := createMockSession(1, server) + tt.fn(session) + select { + case p := <-session.sendPackets: + if len(p.data) == 0 { + t.Errorf("%s: response should have data", tt.name) + } + default: + t.Errorf("%s: no response queued", tt.name) + } + }) + } +} + +// ============================================================================= +// Category 16: Handlers from handlers_tower.go that produce responses (no DB) +// ============================================================================= + +func TestNonTrivialHandlers_TowerGo(t *testing.T) { + server := createMockServer() + + tests := []struct { + name string + fn func(s *Session) + }{ + {"handleMsgMhfGetTenrouirai_Type1", func(s *Session) { + handleMsgMhfGetTenrouirai(s, &mhfpacket.MsgMhfGetTenrouirai{AckHandle: 1, Unk0: 1}) + }}, + {"handleMsgMhfGetTenrouirai_Unknown", func(s *Session) { + handleMsgMhfGetTenrouirai(s, &mhfpacket.MsgMhfGetTenrouirai{AckHandle: 1, Unk0: 0, Unk1: 0}) + }}, + // handleMsgMhfGetTenrouirai_Type4, handleMsgMhfPostTenrouirai, handleMsgMhfGetGemInfo removed: require DB + {"handleMsgMhfGetWeeklySeibatuRankingReward", func(s *Session) { + handleMsgMhfGetWeeklySeibatuRankingReward(s, &mhfpacket.MsgMhfGetWeeklySeibatuRankingReward{AckHandle: 1}) + }}, + {"handleMsgMhfPresentBox", func(s *Session) { + handleMsgMhfPresentBox(s, &mhfpacket.MsgMhfPresentBox{AckHandle: 1}) + }}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + session := createMockSession(1, server) + tt.fn(session) + select { + case p := <-session.sendPackets: + if len(p.data) == 0 { + t.Errorf("%s: response should have data", tt.name) + } + default: + t.Errorf("%s: no response queued", tt.name) + } + }) + } +} + +// ============================================================================= +// Category 17: Handlers from handlers_reward.go that produce responses (no DB) +// ============================================================================= + +func TestNonTrivialHandlers_RewardGo(t *testing.T) { + server := createMockServer() + + tests := []struct { + name string + fn func(s *Session) + }{ + {"handleMsgMhfGetAdditionalBeatReward", func(s *Session) { + handleMsgMhfGetAdditionalBeatReward(s, &mhfpacket.MsgMhfGetAdditionalBeatReward{AckHandle: 1}) + }}, + {"handleMsgMhfGetUdRankingRewardList", func(s *Session) { + handleMsgMhfGetUdRankingRewardList(s, &mhfpacket.MsgMhfGetUdRankingRewardList{AckHandle: 1}) + }}, + {"handleMsgMhfAcquireMonthlyReward", func(s *Session) { + handleMsgMhfAcquireMonthlyReward(s, &mhfpacket.MsgMhfAcquireMonthlyReward{AckHandle: 1}) + }}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + session := createMockSession(1, server) + tt.fn(session) + select { + case p := <-session.sendPackets: + if len(p.data) == 0 { + t.Errorf("%s: response should have data", tt.name) + } + default: + t.Errorf("%s: no response queued", tt.name) + } + }) + } +} + +// ============================================================================= +// Category 18: Handlers from handlers_caravan.go that produce responses (no DB) +// ============================================================================= + +func TestNonTrivialHandlers_CaravanGo(t *testing.T) { + server := createMockServer() + + tests := []struct { + name string + fn func(s *Session) + }{ + {"handleMsgMhfGetRyoudama", func(s *Session) { + handleMsgMhfGetRyoudama(s, &mhfpacket.MsgMhfGetRyoudama{AckHandle: 1}) + }}, + {"handleMsgMhfGetTinyBin", func(s *Session) { + handleMsgMhfGetTinyBin(s, &mhfpacket.MsgMhfGetTinyBin{AckHandle: 1}) + }}, + {"handleMsgMhfPostTinyBin", func(s *Session) { + handleMsgMhfPostTinyBin(s, &mhfpacket.MsgMhfPostTinyBin{AckHandle: 1}) + }}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + session := createMockSession(1, server) + tt.fn(session) + select { + case p := <-session.sendPackets: + if len(p.data) == 0 { + t.Errorf("%s: response should have data", tt.name) + } + default: + t.Errorf("%s: no response queued", tt.name) + } + }) + } +} + +// ============================================================================= +// Category 19: Handlers from handlers_rengoku.go (no DB needed) +// ============================================================================= + +func TestNonTrivialHandlers_RengokuGo(t *testing.T) { + server := createMockServer() + + t.Run("handleMsgMhfGetRengokuRankingRank", func(t *testing.T) { + session := createMockSession(1, server) + handleMsgMhfGetRengokuRankingRank(session, &mhfpacket.MsgMhfGetRengokuRankingRank{AckHandle: 1}) + select { + case p := <-session.sendPackets: + if len(p.data) == 0 { + t.Error("response should have data") + } + default: + t.Error("no response queued") + } + }) +} + +// ============================================================================= +// Category 20: Handlers from handlers.go that produce responses (no DB) +// ============================================================================= + +// TestNonTrivialHandlers_InfoScenarioCounter removed: requires database access. + +// ============================================================================= +// Category 21: handleMsgSysPing and handleMsgSysTime (no DB) +// ============================================================================= + +func TestSimpleHandlers_PingAndTime(t *testing.T) { + server := createMockServer() + + t.Run("handleMsgSysPing", func(t *testing.T) { + session := createMockSession(1, server) + handleMsgSysPing(session, &mhfpacket.MsgSysPing{AckHandle: 1}) + select { + case p := <-session.sendPackets: + if len(p.data) == 0 { + t.Error("response should have data") + } + default: + t.Error("no response queued") + } + }) + + t.Run("handleMsgSysTime", func(t *testing.T) { + session := createMockSession(1, server) + handleMsgSysTime(session, &mhfpacket.MsgSysTime{}) + select { + case p := <-session.sendPackets: + if len(p.data) == 0 { + t.Error("response should have data") + } + default: + t.Error("no response queued") + } + }) +} + +// ============================================================================= +// Category 22: handleMsgSysIssueLogkey (no DB, uses crypto/rand) +// ============================================================================= + +func TestHandleMsgSysIssueLogkey_Coverage3(t *testing.T) { + server := createMockServer() + + t.Run("generates_logkey", func(t *testing.T) { + session := createMockSession(1, server) + handleMsgSysIssueLogkey(session, &mhfpacket.MsgSysIssueLogkey{AckHandle: 1}) + select { + case p := <-session.sendPackets: + if len(p.data) == 0 { + t.Error("response should have data") + } + default: + t.Error("no response queued") + } + if session.logKey == nil { + t.Error("logKey should be set after IssueLogkey") + } + if len(session.logKey) != 16 { + t.Errorf("logKey length = %d, want 16", len(session.logKey)) + } + }) +} + +// ============================================================================= +// Category 23: handleMsgSysUnlockGlobalSema (no DB) +// ============================================================================= + +func TestHandleMsgSysUnlockGlobalSema_Coverage3(t *testing.T) { + server := createMockServer() + + t.Run("produces_response", func(t *testing.T) { + session := createMockSession(1, server) + handleMsgSysUnlockGlobalSema(session, &mhfpacket.MsgSysUnlockGlobalSema{AckHandle: 1}) + select { + case p := <-session.sendPackets: + if len(p.data) == 0 { + t.Error("response should have data") + } + default: + t.Error("no response queued") + } + }) +} + +// ============================================================================= +// Category 24: handleMsgSysLockGlobalSema (no DB, but needs Channels) +// ============================================================================= + +func TestHandleMsgSysLockGlobalSema(t *testing.T) { + server := createMockServer() + server.Channels = make([]*Server, 0) + + t.Run("no_channels_returns_response", func(t *testing.T) { + session := createMockSession(1, server) + handleMsgSysLockGlobalSema(session, &mhfpacket.MsgSysLockGlobalSema{ + AckHandle: 1, + UserIDString: "testuser", + ServerChannelIDString: "ch1", + }) + select { + case p := <-session.sendPackets: + if len(p.data) == 0 { + t.Error("response should have data") + } + default: + t.Error("no response queued") + } + }) +} + +// ============================================================================= +// Category 25: handleMsgSysCheckSemaphore (no DB) +// ============================================================================= + +func TestHandleMsgSysCheckSemaphore(t *testing.T) { + server := createMockServer() + server.semaphore = make(map[string]*Semaphore) + + t.Run("semaphore_not_exists", func(t *testing.T) { + session := createMockSession(1, server) + handleMsgSysCheckSemaphore(session, &mhfpacket.MsgSysCheckSemaphore{ + AckHandle: 1, + SemaphoreID: "nonexistent", + }) + select { + case p := <-session.sendPackets: + if len(p.data) == 0 { + t.Error("response should have data") + } + default: + t.Error("no response queued") + } + }) + + t.Run("semaphore_exists", func(t *testing.T) { + session := createMockSession(1, server) + server.semaphore["existing_sema"] = NewSemaphore(session, "existing_sema", 1) + handleMsgSysCheckSemaphore(session, &mhfpacket.MsgSysCheckSemaphore{ + AckHandle: 1, + SemaphoreID: "existing_sema", + }) + select { + case p := <-session.sendPackets: + if len(p.data) == 0 { + t.Error("response should have data") + } + default: + t.Error("no response queued") + } + }) +} + +// ============================================================================= +// Category 26: handleMsgSysAcquireSemaphore (no DB) +// ============================================================================= + +func TestHandleMsgSysAcquireSemaphore(t *testing.T) { + server := createMockServer() + server.semaphore = make(map[string]*Semaphore) + + t.Run("semaphore_exists", func(t *testing.T) { + session := createMockSession(1, server) + server.semaphore["acquire_sema"] = NewSemaphore(session, "acquire_sema", 1) + handleMsgSysAcquireSemaphore(session, &mhfpacket.MsgSysAcquireSemaphore{ + AckHandle: 1, + SemaphoreID: "acquire_sema", + }) + select { + case p := <-session.sendPackets: + if len(p.data) == 0 { + t.Error("response should have data") + } + default: + t.Error("no response queued") + } + }) + + t.Run("semaphore_not_exists", func(t *testing.T) { + session := createMockSession(1, server) + handleMsgSysAcquireSemaphore(session, &mhfpacket.MsgSysAcquireSemaphore{ + AckHandle: 1, + SemaphoreID: "nonexistent_sema", + }) + select { + case p := <-session.sendPackets: + if len(p.data) == 0 { + t.Error("response should have data") + } + default: + t.Error("no response queued") + } + }) +} + +// ============================================================================= +// Category 27: handleMsgSysCreateStage (no DB) +// ============================================================================= + +func TestHandleMsgSysCreateStage_Coverage3(t *testing.T) { + server := createMockServer() + + t.Run("creates_new_stage", func(t *testing.T) { + session := createMockSession(1, server) + handleMsgSysCreateStage(session, &mhfpacket.MsgSysCreateStage{ + AckHandle: 1, + StageID: "test_create_stage", + PlayerCount: 4, + }) + select { + case p := <-session.sendPackets: + if len(p.data) == 0 { + t.Error("response should have data") + } + default: + t.Error("no response queued") + } + if _, exists := server.stages["test_create_stage"]; !exists { + t.Error("stage should have been created") + } + }) + + t.Run("duplicate_stage_fails", func(t *testing.T) { + session := createMockSession(1, server) + // Stage already exists from the previous test + handleMsgSysCreateStage(session, &mhfpacket.MsgSysCreateStage{ + AckHandle: 2, + StageID: "test_create_stage", + PlayerCount: 4, + }) + select { + case p := <-session.sendPackets: + if len(p.data) == 0 { + t.Error("response should have data even on failure") + } + default: + t.Error("no response queued") + } + }) +} + +// ============================================================================= +// Category 28: Concurrency test for empty handlers +// Verify that calling empty handlers concurrently does not panic. +// ============================================================================= + +func TestEmptyHandlers_Concurrent(t *testing.T) { + server := createMockServer() + + handlers := []func(*Session, mhfpacket.MHFPacket){ + handleMsgSysEcho, + handleMsgSysUpdateRight, + handleMsgSysAuthQuery, + handleMsgSysAuthTerminal, + handleMsgCaExchangeItem, + handleMsgMhfServerCommand, + handleMsgMhfSetLoginwindow, + handleMsgSysTransBinary, + handleMsgSysCollectBinary, + handleMsgSysGetState, + handleMsgSysSerialize, + handleMsgSysEnumlobby, + handleMsgSysEnumuser, + handleMsgSysInfokyserver, + handleMsgMhfGetCaUniqueID, + handleMsgMhfGetExtraInfo, + handleMsgSysSetStatus, + handleMsgSysDeleteObject, + handleMsgSysRotateObject, + handleMsgSysDuplicateObject, + handleMsgSysGetObjectBinary, + handleMsgSysGetObjectOwner, + handleMsgSysUpdateObjectBinary, + handleMsgSysCleanupObject, + handleMsgMhfShutClient, + handleMsgSysHideClient, + handleMsgSysStageDestruct, + } + + var wg sync.WaitGroup + for _, h := range handlers { + for i := 0; i < 10; i++ { + wg.Add(1) + go func(handler func(*Session, mhfpacket.MHFPacket)) { + defer wg.Done() + session := createMockSession(1, server) + handler(session, nil) + }(h) + } + } + wg.Wait() +} + +// ============================================================================= +// Category 29: stubEnumerateNoResults and stubGetNoResults helper coverage +// These are called by many handlers; test them directly too. +// ============================================================================= + +func TestStubHelpers(t *testing.T) { + server := createMockServer() + + t.Run("stubEnumerateNoResults", func(t *testing.T) { + session := createMockSession(1, server) + stubEnumerateNoResults(session, 1) + select { + case p := <-session.sendPackets: + if len(p.data) == 0 { + t.Error("response should have data") + } + default: + t.Error("no response queued") + } + }) + + t.Run("doAckBufSucceed", func(t *testing.T) { + session := createMockSession(1, server) + doAckBufSucceed(session, 1, []byte{0x01, 0x02, 0x03}) + select { + case p := <-session.sendPackets: + if len(p.data) == 0 { + t.Error("response should have data") + } + default: + t.Error("no response queued") + } + }) + + t.Run("doAckBufFail", func(t *testing.T) { + session := createMockSession(1, server) + doAckBufFail(session, 1, []byte{0x01, 0x02, 0x03}) + select { + case p := <-session.sendPackets: + if len(p.data) == 0 { + t.Error("response should have data") + } + default: + t.Error("no response queued") + } + }) + + t.Run("doAckSimpleSucceed", func(t *testing.T) { + session := createMockSession(1, server) + doAckSimpleSucceed(session, 1, []byte{0x00, 0x00, 0x00, 0x00}) + select { + case p := <-session.sendPackets: + if len(p.data) == 0 { + t.Error("response should have data") + } + default: + t.Error("no response queued") + } + }) + + t.Run("doAckSimpleFail", func(t *testing.T) { + session := createMockSession(1, server) + doAckSimpleFail(session, 1, []byte{0x00, 0x00, 0x00, 0x00}) + select { + case p := <-session.sendPackets: + if len(p.data) == 0 { + t.Error("response should have data") + } + default: + t.Error("no response queued") + } + }) +} diff --git a/server/channelserver/handlers_coverage_test.go b/server/channelserver/handlers_coverage_test.go new file mode 100644 index 000000000..c99676ee4 --- /dev/null +++ b/server/channelserver/handlers_coverage_test.go @@ -0,0 +1,144 @@ +package channelserver + +import ( + "testing" + + "erupe-ce/network/mhfpacket" +) + +// Tests for handlers that do NOT require database access, exercising additional +// code paths not covered by existing test files (handlers_core_test.go, +// handlers_rengoku_test.go, etc.). + +// TestHandleMsgSysPing_DifferentAckHandles verifies ping works with various ack handles. +func TestHandleMsgSysPing_DifferentAckHandles(t *testing.T) { + server := createMockServer() + + ackHandles := []uint32{0, 1, 99999, 0xFFFFFFFF} + for _, ack := range ackHandles { + session := createMockSession(1, server) + pkt := &mhfpacket.MsgSysPing{AckHandle: ack} + + handleMsgSysPing(session, pkt) + + select { + case p := <-session.sendPackets: + if len(p.data) == 0 { + t.Errorf("AckHandle=%d: Response packet should have data", ack) + } + default: + t.Errorf("AckHandle=%d: No response packet queued", ack) + } + } +} + +// TestHandleMsgSysTerminalLog_NoEntries verifies the handler works with nil entries. +func TestHandleMsgSysTerminalLog_NoEntries(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgSysTerminalLog{ + AckHandle: 99999, + LogID: 0, + Entries: nil, + } + + handleMsgSysTerminalLog(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") + } +} + +// TestHandleMsgSysTerminalLog_ManyEntries verifies the handler with many log entries. +func TestHandleMsgSysTerminalLog_ManyEntries(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + entries := make([]mhfpacket.TerminalLogEntry, 20) + for i := range entries { + entries[i] = mhfpacket.TerminalLogEntry{ + Index: uint32(i), + Type1: uint8(i % 256), + Type2: uint8((i + 1) % 256), + } + } + + pkt := &mhfpacket.MsgSysTerminalLog{ + AckHandle: 55555, + LogID: 42, + Entries: entries, + } + + handleMsgSysTerminalLog(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") + } +} + +// TestHandleMsgSysTime_MultipleCalls verifies calling time handler repeatedly. +func TestHandleMsgSysTime_MultipleCalls(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgSysTime{ + GetRemoteTime: false, + Timestamp: 0, + } + + for i := 0; i < 5; i++ { + handleMsgSysTime(session, pkt) + } + + // Should have 5 queued responses + count := 0 + for { + select { + case p := <-session.sendPackets: + if len(p.data) == 0 { + t.Error("Response packet should have data") + } + count++ + default: + goto done + } + } +done: + if count != 5 { + t.Errorf("Expected 5 queued responses, got %d", count) + } +} + +// TestHandleMsgMhfGetRengokuRankingRank_DifferentAck verifies rengoku ranking +// works with different ack handles. +func TestHandleMsgMhfGetRengokuRankingRank_DifferentAck(t *testing.T) { + server := createMockServer() + + ackHandles := []uint32{0, 1, 54321, 0xDEADBEEF} + for _, ack := range ackHandles { + session := createMockSession(1, server) + pkt := &mhfpacket.MsgMhfGetRengokuRankingRank{AckHandle: ack} + + handleMsgMhfGetRengokuRankingRank(session, pkt) + + select { + case p := <-session.sendPackets: + if len(p.data) == 0 { + t.Errorf("AckHandle=%d: Response packet should have data", ack) + } + default: + t.Errorf("AckHandle=%d: No response packet queued", ack) + } + } +} diff --git a/server/channelserver/handlers_discord_test.go b/server/channelserver/handlers_discord_test.go new file mode 100644 index 000000000..9557b5ef1 --- /dev/null +++ b/server/channelserver/handlers_discord_test.go @@ -0,0 +1 @@ +package channelserver diff --git a/server/channelserver/handlers_diva_test.go b/server/channelserver/handlers_diva_test.go new file mode 100644 index 000000000..414078e80 --- /dev/null +++ b/server/channelserver/handlers_diva_test.go @@ -0,0 +1,343 @@ +package channelserver + +import ( + "testing" + + "erupe-ce/network/mhfpacket" +) + +func TestHandleMsgMhfGetUdInfo(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfGetUdInfo{ + AckHandle: 12345, + } + + handleMsgMhfGetUdInfo(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 TestHandleMsgMhfGetKijuInfo(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfGetKijuInfo{ + AckHandle: 12345, + } + + handleMsgMhfGetKijuInfo(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 TestHandleMsgMhfSetKiju(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfSetKiju{ + AckHandle: 12345, + } + + handleMsgMhfSetKiju(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 TestHandleMsgMhfAddUdPoint(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfAddUdPoint{ + AckHandle: 12345, + } + + handleMsgMhfAddUdPoint(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 TestHandleMsgMhfGetUdMyPoint(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfGetUdMyPoint{ + AckHandle: 12345, + } + + handleMsgMhfGetUdMyPoint(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 TestHandleMsgMhfGetUdTotalPointInfo(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfGetUdTotalPointInfo{ + AckHandle: 12345, + } + + handleMsgMhfGetUdTotalPointInfo(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 TestHandleMsgMhfGetUdSelectedColorInfo(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfGetUdSelectedColorInfo{ + AckHandle: 12345, + } + + handleMsgMhfGetUdSelectedColorInfo(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 TestHandleMsgMhfGetUdMonsterPoint(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfGetUdMonsterPoint{ + AckHandle: 12345, + } + + handleMsgMhfGetUdMonsterPoint(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 TestHandleMsgMhfGetUdDailyPresentList(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfGetUdDailyPresentList{ + AckHandle: 12345, + } + + handleMsgMhfGetUdDailyPresentList(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 TestHandleMsgMhfGetUdNormaPresentList(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfGetUdNormaPresentList{ + AckHandle: 12345, + } + + handleMsgMhfGetUdNormaPresentList(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 TestHandleMsgMhfAcquireUdItem(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfAcquireUdItem{ + AckHandle: 12345, + } + + handleMsgMhfAcquireUdItem(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 TestHandleMsgMhfGetUdRanking(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfGetUdRanking{ + AckHandle: 12345, + } + + handleMsgMhfGetUdRanking(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 TestHandleMsgMhfGetUdMyRanking(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfGetUdMyRanking{ + AckHandle: 12345, + } + + handleMsgMhfGetUdMyRanking(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 TestGenerateDivaTimestamps_Debug(t *testing.T) { + // Test debug mode timestamps + tests := []struct { + name string + start uint32 + }{ + {"Debug_Start1", 1}, + {"Debug_Start2", 2}, + {"Debug_Start3", 3}, + } + + server := createMockServer() + session := createMockSession(1, server) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + timestamps := generateDivaTimestamps(session, tt.start, true) + if len(timestamps) != 6 { + t.Errorf("Expected 6 timestamps, got %d", len(timestamps)) + } + // Verify timestamps are non-zero + for i, ts := range timestamps { + if ts == 0 { + t.Errorf("Timestamp %d should not be zero", i) + } + } + }) + } +} + +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_event_test.go b/server/channelserver/handlers_event_test.go new file mode 100644 index 000000000..60fd66713 --- /dev/null +++ b/server/channelserver/handlers_event_test.go @@ -0,0 +1,258 @@ +package channelserver + +import ( + "math/bits" + "testing" + + "erupe-ce/network/mhfpacket" +) + +func TestHandleMsgMhfRegisterEvent(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfRegisterEvent{ + AckHandle: 12345, + WorldID: 1, + LandID: 2, + } + + handleMsgMhfRegisterEvent(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") + } +} + +func TestHandleMsgMhfReleaseEvent(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfReleaseEvent{ + AckHandle: 12345, + } + + handleMsgMhfReleaseEvent(session, pkt) + + // Verify response packet was queued (with special error code 0x41) + 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 TestHandleMsgMhfEnumerateEvent(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfEnumerateEvent{ + AckHandle: 12345, + } + + handleMsgMhfEnumerateEvent(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") + } +} + +func TestHandleMsgMhfGetRestrictionEvent(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + // Should not panic (empty handler) + defer func() { + if r := recover(); r != nil { + t.Errorf("handleMsgMhfGetRestrictionEvent panicked: %v", r) + } + }() + + handleMsgMhfGetRestrictionEvent(session, nil) +} + +func TestHandleMsgMhfSetRestrictionEvent(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfSetRestrictionEvent{ + AckHandle: 12345, + } + + handleMsgMhfSetRestrictionEvent(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") + } +} + +func TestGenerateFeatureWeapons(t *testing.T) { + tests := []struct { + name string + count int + }{ + {"single weapon", 1}, + {"few weapons", 3}, + {"normal count", 7}, + {"max weapons", 14}, + {"over max", 20}, // Should cap at 14 + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := generateFeatureWeapons(tt.count) + + // Result should be non-zero for positive counts + if tt.count > 0 && result.ActiveFeatures == 0 { + t.Error("Expected non-zero ActiveFeatures") + } + + // Should not exceed max value (2^14 - 1 = 16383) + if result.ActiveFeatures > 16383 { + t.Errorf("ActiveFeatures = %d, exceeds max of 16383", result.ActiveFeatures) + } + }) + } +} + +func TestGenerateFeatureWeapons_Randomness(t *testing.T) { + // Generate multiple times and verify some variation + results := make(map[uint32]int) + iterations := 100 + + for i := 0; i < iterations; i++ { + result := generateFeatureWeapons(5) + results[result.ActiveFeatures]++ + } + + // Should have some variation (not all the same) + if len(results) == 1 { + t.Error("Expected some variation in generated weapons") + } +} + +func TestGenerateFeatureWeapons_ZeroCount(t *testing.T) { + result := generateFeatureWeapons(0) + + // Should return 0 for no weapons + if result.ActiveFeatures != 0 { + t.Errorf("Expected 0 for zero count, got %d", result.ActiveFeatures) + } +} + +// --- NEW TESTS --- + +// TestGenerateFeatureWeapons_BitCount verifies that the number of set bits +// in ActiveFeatures matches the requested count (capped at 14). +func TestGenerateFeatureWeapons_BitCount(t *testing.T) { + tests := []struct { + name string + count int + wantBits int + }{ + {"1 weapon", 1, 1}, + {"5 weapons", 5, 5}, + {"10 weapons", 10, 10}, + {"14 weapons", 14, 14}, + {"20 capped to 14", 20, 14}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := generateFeatureWeapons(tt.count) + setBits := bits.OnesCount32(result.ActiveFeatures) + if setBits != tt.wantBits { + t.Errorf("Set bits = %d, want %d (ActiveFeatures=0b%032b)", + setBits, tt.wantBits, result.ActiveFeatures) + } + }) + } +} + +// TestGenerateFeatureWeapons_BitsInRange verifies that all set bits are within +// bits 0-13 (no bits above bit 13 should be set). +func TestGenerateFeatureWeapons_BitsInRange(t *testing.T) { + for i := 0; i < 50; i++ { + result := generateFeatureWeapons(7) + // Bits 14+ should never be set + if result.ActiveFeatures&^uint32(0x3FFF) != 0 { + t.Errorf("Bits above 13 are set: 0x%08X", result.ActiveFeatures) + } + } +} + +// TestGenerateFeatureWeapons_MaxYieldsAllBits verifies that requesting 14 +// weapons sets exactly bits 0-13 (the value 16383 = 0x3FFF). +func TestGenerateFeatureWeapons_MaxYieldsAllBits(t *testing.T) { + result := generateFeatureWeapons(14) + if result.ActiveFeatures != 0x3FFF { + t.Errorf("ActiveFeatures = 0x%04X, want 0x3FFF (all 14 bits set)", result.ActiveFeatures) + } +} + +// TestGenerateFeatureWeapons_StartTimeZero verifies that the returned +// activeFeature has a zero StartTime (not set by generateFeatureWeapons). +func TestGenerateFeatureWeapons_StartTimeZero(t *testing.T) { + result := generateFeatureWeapons(5) + if !result.StartTime.IsZero() { + t.Errorf("StartTime should be zero, got %v", result.StartTime) + } +} + +// TestHandleMsgMhfRegisterEvent_DifferentValues tests with various Unk2/Unk4 values. +func TestHandleMsgMhfRegisterEvent_DifferentValues(t *testing.T) { + server := createMockServer() + + tests := []struct { + name string + worldID uint16 + landID uint16 + }{ + {"zeros", 0, 0}, + {"max values", 65535, 65535}, + {"typical", 5, 10}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + session := createMockSession(1, server) + pkt := &mhfpacket.MsgMhfRegisterEvent{ + AckHandle: 99999, + WorldID: tt.worldID, + LandID: tt.landID, + } + + handleMsgMhfRegisterEvent(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_festa_test.go b/server/channelserver/handlers_festa_test.go new file mode 100644 index 000000000..6898847fe --- /dev/null +++ b/server/channelserver/handlers_festa_test.go @@ -0,0 +1,109 @@ +package channelserver + +import ( + "testing" + + _config "erupe-ce/config" + "erupe-ce/network/mhfpacket" +) + +func TestHandleMsgMhfEnumerateRanking_Default(t *testing.T) { + server := createMockServer() + server.erupeConfig = &_config.Config{ + DebugOptions: _config.DebugOptions{ + TournamentOverride: 0, // Default state + }, + } + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfEnumerateRanking{ + AckHandle: 12345, + } + + handleMsgMhfEnumerateRanking(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 TestHandleMsgMhfEnumerateRanking_State1(t *testing.T) { + server := createMockServer() + server.erupeConfig = &_config.Config{ + DebugOptions: _config.DebugOptions{ + TournamentOverride: 1, + }, + } + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfEnumerateRanking{ + AckHandle: 12345, + } + + handleMsgMhfEnumerateRanking(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 TestHandleMsgMhfEnumerateRanking_State2(t *testing.T) { + server := createMockServer() + server.erupeConfig = &_config.Config{ + DebugOptions: _config.DebugOptions{ + TournamentOverride: 2, + }, + } + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfEnumerateRanking{ + AckHandle: 12345, + } + + handleMsgMhfEnumerateRanking(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 TestHandleMsgMhfEnumerateRanking_State3(t *testing.T) { + server := createMockServer() + server.erupeConfig = &_config.Config{ + DebugOptions: _config.DebugOptions{ + TournamentOverride: 3, + }, + } + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfEnumerateRanking{ + AckHandle: 12345, + } + + handleMsgMhfEnumerateRanking(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_guild_icon_test.go b/server/channelserver/handlers_guild_icon_test.go new file mode 100644 index 000000000..b44520119 --- /dev/null +++ b/server/channelserver/handlers_guild_icon_test.go @@ -0,0 +1,249 @@ +package channelserver + +import ( + "encoding/json" + "testing" +) + +func TestGuildIconScan_Bytes(t *testing.T) { + jsonData := []byte(`{"Parts":[{"Index":1,"ID":100,"Page":2,"Size":3,"Rotation":4,"Red":255,"Green":128,"Blue":0,"PosX":50,"PosY":60}]}`) + + gi := &GuildIcon{} + err := gi.Scan(jsonData) + if err != nil { + t.Fatalf("Scan([]byte) error = %v", err) + } + + if len(gi.Parts) != 1 { + t.Fatalf("Parts length = %d, want 1", len(gi.Parts)) + } + + part := gi.Parts[0] + if part.Index != 1 { + t.Errorf("Index = %d, want 1", part.Index) + } + if part.ID != 100 { + t.Errorf("ID = %d, want 100", part.ID) + } + if part.Page != 2 { + t.Errorf("Page = %d, want 2", part.Page) + } + if part.Size != 3 { + t.Errorf("Size = %d, want 3", part.Size) + } + if part.Rotation != 4 { + t.Errorf("Rotation = %d, want 4", part.Rotation) + } + if part.Red != 255 { + t.Errorf("Red = %d, want 255", part.Red) + } + if part.Green != 128 { + t.Errorf("Green = %d, want 128", part.Green) + } + if part.Blue != 0 { + t.Errorf("Blue = %d, want 0", part.Blue) + } + if part.PosX != 50 { + t.Errorf("PosX = %d, want 50", part.PosX) + } + if part.PosY != 60 { + t.Errorf("PosY = %d, want 60", part.PosY) + } +} + +func TestGuildIconScan_String(t *testing.T) { + jsonStr := `{"Parts":[{"Index":5,"ID":200,"Page":1,"Size":2,"Rotation":0,"Red":100,"Green":50,"Blue":25,"PosX":300,"PosY":400}]}` + + gi := &GuildIcon{} + err := gi.Scan(jsonStr) + if err != nil { + t.Fatalf("Scan(string) error = %v", err) + } + + if len(gi.Parts) != 1 { + t.Fatalf("Parts length = %d, want 1", len(gi.Parts)) + } + if gi.Parts[0].ID != 200 { + t.Errorf("ID = %d, want 200", gi.Parts[0].ID) + } + if gi.Parts[0].PosX != 300 { + t.Errorf("PosX = %d, want 300", gi.Parts[0].PosX) + } +} + +func TestGuildIconScan_MultipleParts(t *testing.T) { + jsonData := []byte(`{"Parts":[{"Index":0,"ID":1,"Page":0,"Size":0,"Rotation":0,"Red":0,"Green":0,"Blue":0,"PosX":0,"PosY":0},{"Index":1,"ID":2,"Page":0,"Size":0,"Rotation":0,"Red":0,"Green":0,"Blue":0,"PosX":0,"PosY":0},{"Index":2,"ID":3,"Page":0,"Size":0,"Rotation":0,"Red":0,"Green":0,"Blue":0,"PosX":0,"PosY":0}]}`) + + gi := &GuildIcon{} + err := gi.Scan(jsonData) + if err != nil { + t.Fatalf("Scan() error = %v", err) + } + + if len(gi.Parts) != 3 { + t.Fatalf("Parts length = %d, want 3", len(gi.Parts)) + } + for i, part := range gi.Parts { + if part.Index != uint16(i) { + t.Errorf("Parts[%d].Index = %d, want %d", i, part.Index, i) + } + } +} + +func TestGuildIconScan_EmptyParts(t *testing.T) { + gi := &GuildIcon{} + err := gi.Scan([]byte(`{"Parts":[]}`)) + if err != nil { + t.Fatalf("Scan() error = %v", err) + } + if len(gi.Parts) != 0 { + t.Errorf("Parts length = %d, want 0", len(gi.Parts)) + } +} + +func TestGuildIconScan_InvalidJSON(t *testing.T) { + gi := &GuildIcon{} + err := gi.Scan([]byte(`{invalid`)) + if err == nil { + t.Error("Scan() with invalid JSON should return error") + } +} + +func TestGuildIconScan_InvalidJSONString(t *testing.T) { + gi := &GuildIcon{} + err := gi.Scan("{invalid") + if err == nil { + t.Error("Scan() with invalid JSON string should return error") + } +} + +func TestGuildIconScan_UnsupportedType(t *testing.T) { + gi := &GuildIcon{} + // Passing an unsupported type should not error (just no-op) + err := gi.Scan(12345) + if err != nil { + t.Errorf("Scan(int) unexpected error = %v", err) + } +} + +func TestGuildIconValue(t *testing.T) { + gi := &GuildIcon{ + Parts: []GuildIconPart{ + {Index: 1, ID: 100, Page: 2, Size: 3, Rotation: 4, Red: 255, Green: 128, Blue: 0, PosX: 50, PosY: 60}, + }, + } + + val, err := gi.Value() + if err != nil { + t.Fatalf("Value() error = %v", err) + } + + jsonBytes, ok := val.([]byte) + if !ok { + t.Fatalf("Value() returned %T, want []byte", val) + } + + // Verify round-trip + gi2 := &GuildIcon{} + err = json.Unmarshal(jsonBytes, gi2) + if err != nil { + t.Fatalf("json.Unmarshal error = %v", err) + } + + if len(gi2.Parts) != 1 { + t.Fatalf("round-trip Parts length = %d, want 1", len(gi2.Parts)) + } + if gi2.Parts[0].ID != 100 { + t.Errorf("round-trip ID = %d, want 100", gi2.Parts[0].ID) + } + if gi2.Parts[0].Red != 255 { + t.Errorf("round-trip Red = %d, want 255", gi2.Parts[0].Red) + } +} + +func TestGuildIconValue_Empty(t *testing.T) { + gi := &GuildIcon{} + val, err := gi.Value() + if err != nil { + t.Fatalf("Value() error = %v", err) + } + + if val == nil { + t.Error("Value() should not return nil") + } +} + +func TestGuildIconScanValueRoundTrip(t *testing.T) { + original := &GuildIcon{ + Parts: []GuildIconPart{ + {Index: 0, ID: 10, Page: 1, Size: 2, Rotation: 45, Red: 200, Green: 150, Blue: 100, PosX: 500, PosY: 600}, + {Index: 1, ID: 20, Page: 3, Size: 4, Rotation: 90, Red: 50, Green: 75, Blue: 255, PosX: 100, PosY: 200}, + }, + } + + // Value -> Scan round trip + val, err := original.Value() + if err != nil { + t.Fatalf("Value() error = %v", err) + } + + restored := &GuildIcon{} + err = restored.Scan(val) + if err != nil { + t.Fatalf("Scan() error = %v", err) + } + + if len(restored.Parts) != len(original.Parts) { + t.Fatalf("Parts length = %d, want %d", len(restored.Parts), len(original.Parts)) + } + + for i := range original.Parts { + if restored.Parts[i] != original.Parts[i] { + t.Errorf("Parts[%d] mismatch: got %+v, want %+v", i, restored.Parts[i], original.Parts[i]) + } + } +} + +func TestFestivalColorCodes(t *testing.T) { + tests := []struct { + colour FestivalColor + code int16 + }{ + {FestivalColorBlue, 0}, + {FestivalColorRed, 1}, + {FestivalColorNone, -1}, + } + + for _, tt := range tests { + t.Run(string(tt.colour), func(t *testing.T) { + code, ok := FestivalColorCodes[tt.colour] + if !ok { + t.Fatalf("FestivalColorCodes missing key %s", tt.colour) + } + if code != tt.code { + t.Errorf("FestivalColorCodes[%s] = %d, want %d", tt.colour, code, tt.code) + } + }) + } +} + +func TestFestivalColorConstants(t *testing.T) { + if FestivalColorNone != "none" { + t.Errorf("FestivalColorNone = %s, want none", FestivalColorNone) + } + if FestivalColorRed != "red" { + t.Errorf("FestivalColorRed = %s, want red", FestivalColorRed) + } + if FestivalColorBlue != "blue" { + t.Errorf("FestivalColorBlue = %s, want blue", FestivalColorBlue) + } +} + +func TestGuildApplicationTypeConstants(t *testing.T) { + if GuildApplicationTypeApplied != "applied" { + t.Errorf("GuildApplicationTypeApplied = %s, want applied", GuildApplicationTypeApplied) + } + if GuildApplicationTypeInvited != "invited" { + t.Errorf("GuildApplicationTypeInvited = %s, want invited", GuildApplicationTypeInvited) + } +} diff --git a/server/channelserver/handlers_guild_member_test.go b/server/channelserver/handlers_guild_member_test.go new file mode 100644 index 000000000..4102ff56c --- /dev/null +++ b/server/channelserver/handlers_guild_member_test.go @@ -0,0 +1,209 @@ +package channelserver + +import ( + "testing" +) + +func TestGuildMember_CanRecruit(t *testing.T) { + tests := []struct { + name string + member GuildMember + expected bool + }{ + { + name: "recruiter flag true", + member: GuildMember{ + Recruiter: true, + OrderIndex: 10, + IsLeader: false, + }, + expected: true, + }, + { + name: "order index 1", + member: GuildMember{ + Recruiter: false, + OrderIndex: 1, + IsLeader: false, + }, + expected: true, + }, + { + name: "order index 2", + member: GuildMember{ + Recruiter: false, + OrderIndex: 2, + IsLeader: false, + }, + expected: true, + }, + { + name: "order index 3", + member: GuildMember{ + Recruiter: false, + OrderIndex: 3, + IsLeader: false, + }, + expected: true, + }, + { + name: "order index 0 (sub-leader)", + member: GuildMember{ + Recruiter: false, + OrderIndex: 0, + IsLeader: false, + }, + expected: true, + }, + { + name: "order index 4 cannot recruit", + member: GuildMember{ + Recruiter: false, + OrderIndex: 4, + IsLeader: false, + }, + expected: false, + }, + { + name: "order index 5 cannot recruit", + member: GuildMember{ + Recruiter: false, + OrderIndex: 5, + IsLeader: false, + }, + expected: false, + }, + { + name: "is leader can recruit", + member: GuildMember{ + Recruiter: false, + OrderIndex: 100, + IsLeader: true, + }, + expected: true, + }, + { + name: "regular member cannot recruit", + member: GuildMember{ + Recruiter: false, + OrderIndex: 10, + IsLeader: false, + }, + expected: false, + }, + { + name: "all flags true", + member: GuildMember{ + Recruiter: true, + OrderIndex: 1, + IsLeader: true, + }, + expected: true, + }, + { + name: "high order index with leader", + member: GuildMember{ + Recruiter: false, + OrderIndex: 255, + IsLeader: true, + }, + expected: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := tt.member.CanRecruit() + if result != tt.expected { + t.Errorf("CanRecruit() = %v, expected %v (Recruiter=%v, OrderIndex=%d, IsLeader=%v)", + result, tt.expected, tt.member.Recruiter, tt.member.OrderIndex, tt.member.IsLeader) + } + }) + } +} + +func TestGuildMember_IsSubLeader(t *testing.T) { + tests := []struct { + name string + orderIndex uint16 + expected bool + }{ + { + name: "order index 0", + orderIndex: 0, + expected: true, + }, + { + name: "order index 1", + orderIndex: 1, + expected: true, + }, + { + name: "order index 2", + orderIndex: 2, + expected: true, + }, + { + name: "order index 3", + orderIndex: 3, + expected: true, + }, + { + name: "order index 4", + orderIndex: 4, + expected: false, + }, + { + name: "order index 5", + orderIndex: 5, + expected: false, + }, + { + name: "order index 100", + orderIndex: 100, + expected: false, + }, + { + name: "order index 255", + orderIndex: 255, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + member := GuildMember{OrderIndex: tt.orderIndex} + result := member.IsSubLeader() + if result != tt.expected { + t.Errorf("IsSubLeader() with OrderIndex=%d = %v, expected %v", + tt.orderIndex, result, tt.expected) + } + }) + } +} + +func TestGuildMember_CanRecruit_Priority(t *testing.T) { + // Test that Recruiter flag takes priority (short-circuit) + member := GuildMember{ + Recruiter: true, + OrderIndex: 100, // Would fail OrderIndex check + IsLeader: false, + } + + if !member.CanRecruit() { + t.Error("Recruiter flag should allow recruiting regardless of OrderIndex") + } +} + +func TestGuildMember_CanRecruit_OrderIndexBoundary(t *testing.T) { + // Test the exact boundary at OrderIndex == 3 vs 4 + member3 := GuildMember{Recruiter: false, OrderIndex: 3, IsLeader: false} + member4 := GuildMember{Recruiter: false, OrderIndex: 4, IsLeader: false} + + if !member3.CanRecruit() { + t.Error("OrderIndex 3 should be able to recruit") + } + if member4.CanRecruit() { + t.Error("OrderIndex 4 should NOT be able to recruit") + } +} 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..8eb1f8444 --- /dev/null +++ b/server/channelserver/handlers_mercenary_test.go @@ -0,0 +1,298 @@ +package channelserver + +import ( + "bytes" + "encoding/binary" + "testing" + + "erupe-ce/common/byteframe" + "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") + } +} + +// --- NEW TESTS --- + +// buildCatBytes constructs a binary cat data payload suitable for GetAirouDetails. +func buildCatBytes(cats []Airou) []byte { + buf := new(bytes.Buffer) + // catCount + buf.WriteByte(byte(len(cats))) + for _, cat := range cats { + catBuf := new(bytes.Buffer) + // ID (uint32) + binary.Write(catBuf, binary.BigEndian, cat.ID) + // 1 byte skip (unknown bool) + catBuf.WriteByte(0) + // Name (18 bytes) + name := make([]byte, 18) + copy(name, cat.Name) + catBuf.Write(name) + // Task (uint8) + catBuf.WriteByte(cat.Task) + // 16 bytes skip (appearance data) + catBuf.Write(make([]byte, 16)) + // Personality (uint8) + catBuf.WriteByte(cat.Personality) + // Class (uint8) + catBuf.WriteByte(cat.Class) + // 5 bytes skip (affection and colour sliders) + catBuf.Write(make([]byte, 5)) + // Experience (uint32) + binary.Write(catBuf, binary.BigEndian, cat.Experience) + // 1 byte skip (bool for weapon equipped) + catBuf.WriteByte(0) + // WeaponType (uint8) + catBuf.WriteByte(cat.WeaponType) + // WeaponID (uint16) + binary.Write(catBuf, binary.BigEndian, cat.WeaponID) + + catData := catBuf.Bytes() + // catDefLen (uint32) - total length of the cat data after this field + binary.Write(buf, binary.BigEndian, uint32(len(catData))) + buf.Write(catData) + } + return buf.Bytes() +} + +func TestGetAirouDetails_Empty(t *testing.T) { + // Zero cats + data := []byte{0x00} + bf := byteframe.NewByteFrameFromBytes(data) + cats := GetAirouDetails(bf) + + if len(cats) != 0 { + t.Errorf("Expected 0 cats, got %d", len(cats)) + } +} + +func TestGetAirouDetails_SingleCat(t *testing.T) { + input := Airou{ + ID: 42, + Name: []byte("TestCat"), + Task: 4, + Personality: 3, + Class: 2, + Experience: 1500, + WeaponType: 6, + WeaponID: 100, + } + + data := buildCatBytes([]Airou{input}) + bf := byteframe.NewByteFrameFromBytes(data) + cats := GetAirouDetails(bf) + + if len(cats) != 1 { + t.Fatalf("Expected 1 cat, got %d", len(cats)) + } + + cat := cats[0] + if cat.ID != 42 { + t.Errorf("ID = %d, want 42", cat.ID) + } + if cat.Task != 4 { + t.Errorf("Task = %d, want 4", cat.Task) + } + if cat.Personality != 3 { + t.Errorf("Personality = %d, want 3", cat.Personality) + } + if cat.Class != 2 { + t.Errorf("Class = %d, want 2", cat.Class) + } + if cat.Experience != 1500 { + t.Errorf("Experience = %d, want 1500", cat.Experience) + } + if cat.WeaponType != 6 { + t.Errorf("WeaponType = %d, want 6", cat.WeaponType) + } + if cat.WeaponID != 100 { + t.Errorf("WeaponID = %d, want 100", cat.WeaponID) + } + // Name should be 18 bytes (padded with nulls) + if len(cat.Name) != 18 { + t.Errorf("Name length = %d, want 18", len(cat.Name)) + } + // First bytes should match "TestCat" + if !bytes.HasPrefix(cat.Name, []byte("TestCat")) { + t.Errorf("Name does not start with 'TestCat', got %v", cat.Name) + } +} + +func TestGetAirouDetails_MultipleCats(t *testing.T) { + inputs := []Airou{ + {ID: 1, Name: []byte("Alpha"), Task: 1, Personality: 0, Class: 0, Experience: 100, WeaponType: 6, WeaponID: 10}, + {ID: 2, Name: []byte("Beta"), Task: 2, Personality: 1, Class: 1, Experience: 200, WeaponType: 6, WeaponID: 20}, + {ID: 3, Name: []byte("Gamma"), Task: 4, Personality: 2, Class: 2, Experience: 300, WeaponType: 6, WeaponID: 30}, + } + + data := buildCatBytes(inputs) + bf := byteframe.NewByteFrameFromBytes(data) + cats := GetAirouDetails(bf) + + if len(cats) != 3 { + t.Fatalf("Expected 3 cats, got %d", len(cats)) + } + + for i, cat := range cats { + if cat.ID != inputs[i].ID { + t.Errorf("Cat %d: CatID = %d, want %d", i, cat.ID, inputs[i].ID) + } + if cat.Task != inputs[i].Task { + t.Errorf("Cat %d: CurrentTask = %d, want %d", i, cat.Task, inputs[i].Task) + } + if cat.Experience != inputs[i].Experience { + t.Errorf("Cat %d: Experience = %d, want %d", i, cat.Experience, inputs[i].Experience) + } + if cat.WeaponID != inputs[i].WeaponID { + t.Errorf("Cat %d: WeaponID = %d, want %d", i, cat.WeaponID, inputs[i].WeaponID) + } + } +} + +func TestGetAirouDetails_ExtraTrailingBytes(t *testing.T) { + // The GetAirouDetails function handles extra bytes by seeking to catStart+catDefLen. + // Simulate a cat definition with extra trailing bytes by increasing catDefLen. + buf := new(bytes.Buffer) + buf.WriteByte(1) // catCount = 1 + + catBuf := new(bytes.Buffer) + binary.Write(catBuf, binary.BigEndian, uint32(99)) // catID + catBuf.WriteByte(0) // skip + catBuf.Write(make([]byte, 18)) // name + catBuf.WriteByte(3) // currentTask + catBuf.Write(make([]byte, 16)) // appearance skip + catBuf.WriteByte(1) // personality + catBuf.WriteByte(2) // class + catBuf.Write(make([]byte, 5)) // affection skip + binary.Write(catBuf, binary.BigEndian, uint32(500)) // experience + catBuf.WriteByte(0) // weapon equipped bool + catBuf.WriteByte(6) // weaponType + binary.Write(catBuf, binary.BigEndian, uint16(50)) // weaponID + + catData := catBuf.Bytes() + // Add 10 extra trailing bytes + extra := make([]byte, 10) + catDataWithExtra := append(catData, extra...) + + binary.Write(buf, binary.BigEndian, uint32(len(catDataWithExtra))) + buf.Write(catDataWithExtra) + + bf := byteframe.NewByteFrameFromBytes(buf.Bytes()) + cats := GetAirouDetails(bf) + + if len(cats) != 1 { + t.Fatalf("Expected 1 cat, got %d", len(cats)) + } + if cats[0].ID != 99 { + t.Errorf("ID = %d, want 99", cats[0].ID) + } + if cats[0].Experience != 500 { + t.Errorf("Experience = %d, want 500", cats[0].Experience) + } +} + +func TestGetAirouDetails_CatNamePadding(t *testing.T) { + // Verify that names shorter than 18 bytes are correctly padded with null bytes. + input := Airou{ + ID: 1, + Name: []byte("Hi"), + } + + data := buildCatBytes([]Airou{input}) + bf := byteframe.NewByteFrameFromBytes(data) + cats := GetAirouDetails(bf) + + if len(cats) != 1 { + t.Fatalf("Expected 1 cat, got %d", len(cats)) + } + if len(cats[0].Name) != 18 { + t.Errorf("Name length = %d, want 18", len(cats[0].Name)) + } + // "Hi" followed by null bytes + if cats[0].Name[0] != 'H' || cats[0].Name[1] != 'i' { + t.Errorf("Name first bytes = %v, want 'Hi...'", cats[0].Name[:2]) + } +} + +// TestHandleMsgMhfMercenaryHuntdata_Unk0_1 tests with Unk0=1 (returns 1 byte) +func TestHandleMsgMhfMercenaryHuntdata_Unk0_1(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfMercenaryHuntdata{ + AckHandle: 12345, + Unk0: 1, + } + + handleMsgMhfMercenaryHuntdata(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") + } +} + +// TestHandleMsgMhfMercenaryHuntdata_Unk0_0 tests with Unk0=0 (returns 0 bytes payload) +func TestHandleMsgMhfMercenaryHuntdata_Unk0_0(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfMercenaryHuntdata{ + AckHandle: 12345, + Unk0: 0, + } + + handleMsgMhfMercenaryHuntdata(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") + } +} + +// TestHandleMsgMhfEnumerateMercenaryLog tests the mercenary log enumeration handler +func TestHandleMsgMhfEnumerateMercenaryLog(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfEnumerateMercenaryLog{ + AckHandle: 12345, + } + + handleMsgMhfEnumerateMercenaryLog(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_misc_test.go b/server/channelserver/handlers_misc_test.go new file mode 100644 index 000000000..26fbd67d5 --- /dev/null +++ b/server/channelserver/handlers_misc_test.go @@ -0,0 +1,601 @@ +package channelserver + +import ( + "testing" + + "erupe-ce/network/mhfpacket" +) + +// Test handlers with simple responses + +func TestHandleMsgMhfGetEarthStatus(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfGetEarthStatus{ + AckHandle: 12345, + } + + handleMsgMhfGetEarthStatus(session, pkt) + + select { + case p := <-session.sendPackets: + if p.data == nil { + t.Error("Response packet data should not be nil") + } + default: + t.Error("No response packet queued") + } +} + +func TestHandleMsgMhfGetEarthValue_Type1(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfGetEarthValue{ + AckHandle: 12345, + ReqType: 1, + } + + handleMsgMhfGetEarthValue(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 TestHandleMsgMhfGetEarthValue_Type2(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfGetEarthValue{ + AckHandle: 12345, + ReqType: 2, + } + + handleMsgMhfGetEarthValue(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 TestHandleMsgMhfGetEarthValue_Type3(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfGetEarthValue{ + AckHandle: 12345, + ReqType: 3, + } + + handleMsgMhfGetEarthValue(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 TestHandleMsgMhfGetEarthValue_UnknownType(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfGetEarthValue{ + AckHandle: 12345, + ReqType: 99, // Unknown type + } + + handleMsgMhfGetEarthValue(session, pkt) + + select { + case p := <-session.sendPackets: + // Should still return a response (empty values) + if p.data == nil { + t.Error("Response packet data should not be nil") + } + default: + t.Error("No response packet queued") + } +} + +func TestHandleMsgMhfReadBeatLevel(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfReadBeatLevel{ + AckHandle: 12345, + ValidIDCount: 2, + IDs: [16]uint32{1, 2}, + } + + handleMsgMhfReadBeatLevel(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 TestHandleMsgMhfReadBeatLevel_NoIDs(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfReadBeatLevel{ + AckHandle: 12345, + ValidIDCount: 0, + IDs: [16]uint32{}, + } + + handleMsgMhfReadBeatLevel(session, pkt) + + select { + case p := <-session.sendPackets: + if p.data == nil { + t.Error("Response packet data should not be nil") + } + default: + t.Error("No response packet queued") + } +} + +func TestHandleMsgMhfUpdateBeatLevel(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfUpdateBeatLevel{ + AckHandle: 12345, + } + + handleMsgMhfUpdateBeatLevel(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 empty handlers don't panic + +func TestHandleMsgMhfStampcardPrize(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + defer func() { + if r := recover(); r != nil { + t.Errorf("handleMsgMhfStampcardPrize panicked: %v", r) + } + }() + + handleMsgMhfStampcardPrize(session, nil) +} + +func TestHandleMsgMhfUnreserveSrg(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfUnreserveSrg{ + AckHandle: 12345, + } + + handleMsgMhfUnreserveSrg(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 TestHandleMsgMhfReadBeatLevelAllRanking(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfReadBeatLevelAllRanking{ + AckHandle: 12345, + } + + handleMsgMhfReadBeatLevelAllRanking(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 TestHandleMsgMhfReadBeatLevelMyRanking(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfReadBeatLevelMyRanking{ + AckHandle: 12345, + } + + handleMsgMhfReadBeatLevelMyRanking(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 TestHandleMsgMhfReadLastWeekBeatRanking(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfReadLastWeekBeatRanking{ + AckHandle: 12345, + } + + handleMsgMhfReadLastWeekBeatRanking(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 TestHandleMsgMhfGetFixedSeibatuRankingTable(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfGetFixedSeibatuRankingTable{ + AckHandle: 12345, + } + + handleMsgMhfGetFixedSeibatuRankingTable(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 TestHandleMsgMhfKickExportForce(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + defer func() { + if r := recover(); r != nil { + t.Errorf("handleMsgMhfKickExportForce panicked: %v", r) + } + }() + + handleMsgMhfKickExportForce(session, nil) +} + +func TestHandleMsgMhfRegistSpabiTime(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + defer func() { + if r := recover(); r != nil { + t.Errorf("handleMsgMhfRegistSpabiTime panicked: %v", r) + } + }() + + handleMsgMhfRegistSpabiTime(session, nil) +} + +func TestHandleMsgMhfDebugPostValue(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + defer func() { + if r := recover(); r != nil { + t.Errorf("handleMsgMhfDebugPostValue panicked: %v", r) + } + }() + + handleMsgMhfDebugPostValue(session, nil) +} + +func TestHandleMsgMhfGetCogInfo(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + defer func() { + if r := recover(); r != nil { + t.Errorf("handleMsgMhfGetCogInfo panicked: %v", r) + } + }() + + handleMsgMhfGetCogInfo(session, nil) +} + +// Additional handler tests for coverage + +func TestHandleMsgMhfGetNotice(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfGetNotice{ + AckHandle: 12345, + } + + handleMsgMhfGetNotice(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 TestHandleMsgMhfPostNotice(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfPostNotice{ + AckHandle: 12345, + } + + handleMsgMhfPostNotice(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 TestHandleMsgMhfGetRandFromTable(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfGetRandFromTable{ + AckHandle: 12345, + Results: 3, + } + + handleMsgMhfGetRandFromTable(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 TestHandleMsgMhfGetSenyuDailyCount(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfGetSenyuDailyCount{ + AckHandle: 12345, + } + + handleMsgMhfGetSenyuDailyCount(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 TestHandleMsgMhfGetSeibattle(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfGetSeibattle{ + AckHandle: 12345, + } + + handleMsgMhfGetSeibattle(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 TestHandleMsgMhfPostSeibattle(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfPostSeibattle{ + AckHandle: 12345, + } + + handleMsgMhfPostSeibattle(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 TestHandleMsgMhfGetDailyMissionMaster(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + defer func() { + if r := recover(); r != nil { + t.Errorf("handleMsgMhfGetDailyMissionMaster panicked: %v", r) + } + }() + + handleMsgMhfGetDailyMissionMaster(session, nil) +} + +func TestHandleMsgMhfGetDailyMissionPersonal(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + defer func() { + if r := recover(); r != nil { + t.Errorf("handleMsgMhfGetDailyMissionPersonal panicked: %v", r) + } + }() + + handleMsgMhfGetDailyMissionPersonal(session, nil) +} + +func TestHandleMsgMhfSetDailyMissionPersonal(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + defer func() { + if r := recover(); r != nil { + t.Errorf("handleMsgMhfSetDailyMissionPersonal panicked: %v", r) + } + }() + + handleMsgMhfSetDailyMissionPersonal(session, nil) +} + +func TestHandleMsgMhfGetUdShopCoin(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfGetUdShopCoin{ + AckHandle: 12345, + } + + handleMsgMhfGetUdShopCoin(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 TestHandleMsgMhfUseUdShopCoin(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + defer func() { + if r := recover(); r != nil { + t.Errorf("handleMsgMhfUseUdShopCoin panicked: %v", r) + } + }() + + handleMsgMhfUseUdShopCoin(session, nil) +} + +func TestHandleMsgMhfGetLobbyCrowd(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfGetLobbyCrowd{ + AckHandle: 12345, + } + + handleMsgMhfGetLobbyCrowd(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") + } +} + +// Distribution struct tests +func TestDistributionStruct(t *testing.T) { + dist := Distribution{ + ID: 1, + MinHR: 1, + MaxHR: 999, + MinSR: 0, + MaxSR: 999, + MinGR: 0, + MaxGR: 999, + TimesAcceptable: 1, + TimesAccepted: 0, + EventName: "Test Event", + Description: "Test Description", + Selection: false, + } + + if dist.ID != 1 { + t.Errorf("ID = %d, want 1", dist.ID) + } + if dist.EventName != "Test Event" { + t.Errorf("EventName = %s, want Test Event", dist.EventName) + } +} + +func TestDistributionItemStruct(t *testing.T) { + item := DistributionItem{ + ItemType: 1, + ID: 100, + ItemID: 1234, + Quantity: 10, + } + + if item.ItemType != 1 { + t.Errorf("ItemType = %d, want 1", item.ItemType) + } + if item.ItemID != 1234 { + t.Errorf("ItemID = %d, want 1234", item.ItemID) + } +} diff --git a/server/channelserver/handlers_mutex_test.go b/server/channelserver/handlers_mutex_test.go new file mode 100644 index 000000000..801706005 --- /dev/null +++ b/server/channelserver/handlers_mutex_test.go @@ -0,0 +1,77 @@ +package channelserver + +import ( + "testing" +) + +// Test that all mutex handlers don't panic (they are empty implementations) + +func TestHandleMsgSysCreateMutex(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + // Should not panic (empty handler) + defer func() { + if r := recover(); r != nil { + t.Errorf("handleMsgSysCreateMutex panicked: %v", r) + } + }() + + handleMsgSysCreateMutex(session, nil) +} + +func TestHandleMsgSysCreateOpenMutex(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + // Should not panic (empty handler) + defer func() { + if r := recover(); r != nil { + t.Errorf("handleMsgSysCreateOpenMutex panicked: %v", r) + } + }() + + handleMsgSysCreateOpenMutex(session, nil) +} + +func TestHandleMsgSysDeleteMutex(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + // Should not panic (empty handler) + defer func() { + if r := recover(); r != nil { + t.Errorf("handleMsgSysDeleteMutex panicked: %v", r) + } + }() + + handleMsgSysDeleteMutex(session, nil) +} + +func TestHandleMsgSysOpenMutex(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + // Should not panic (empty handler) + defer func() { + if r := recover(); r != nil { + t.Errorf("handleMsgSysOpenMutex panicked: %v", r) + } + }() + + handleMsgSysOpenMutex(session, nil) +} + +func TestHandleMsgSysCloseMutex(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + // Should not panic (empty handler) + defer func() { + if r := recover(); r != nil { + t.Errorf("handleMsgSysCloseMutex panicked: %v", r) + } + }() + + handleMsgSysCloseMutex(session, nil) +} diff --git a/server/channelserver/handlers_object_test.go b/server/channelserver/handlers_object_test.go new file mode 100644 index 000000000..52a9dbae9 --- /dev/null +++ b/server/channelserver/handlers_object_test.go @@ -0,0 +1,372 @@ +package channelserver + +import ( + "testing" + + "erupe-ce/network/mhfpacket" +) + +func TestHandleMsgSysCreateObject(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + // Create a stage for the session + stage := NewStage("test_stage") + session.stage = stage + + pkt := &mhfpacket.MsgSysCreateObject{ + AckHandle: 12345, + X: 100.0, + Y: 50.0, + Z: -25.0, + Unk0: 0, + } + + handleMsgSysCreateObject(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") + } + + // Verify object was created in stage + if len(stage.objects) != 1 { + t.Errorf("Stage should have 1 object, got %d", len(stage.objects)) + } +} + +func TestHandleMsgSysCreateObject_MultipleObjects(t *testing.T) { + server := createMockServer() + + // Create multiple sessions that create objects + sessions := make([]*Session, 3) + stage := NewStage("test_stage") + + for i := 0; i < 3; i++ { + sessions[i] = createMockSession(uint32(i+1), server) + sessions[i].stage = stage + + pkt := &mhfpacket.MsgSysCreateObject{ + AckHandle: uint32(12345 + i), + X: float32(i * 10), + Y: float32(i * 20), + Z: float32(i * 30), + } + + handleMsgSysCreateObject(sessions[i], pkt) + + // Drain send queue + select { + case <-sessions[i].sendPackets: + default: + } + } + + // All objects should exist + if len(stage.objects) != 3 { + t.Errorf("Stage should have 3 objects, got %d", len(stage.objects)) + } +} + +func TestHandleMsgSysPositionObject(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + // Create a stage with an existing object + stage := NewStage("test_stage") + session.stage = stage + + // Add another session to receive broadcast + session2 := createMockSession(2, server) + session2.stage = stage + stage.clients[session] = session.charID + stage.clients[session2] = session2.charID + + // Create an object + stage.objects[session.charID] = &Object{ + id: 1, + ownerCharID: session.charID, + x: 0, + y: 0, + z: 0, + } + + pkt := &mhfpacket.MsgSysPositionObject{ + ObjID: 1, + X: 100.0, + Y: 200.0, + Z: 300.0, + } + + handleMsgSysPositionObject(session, pkt) + + // Verify object position was updated + obj := stage.objects[session.charID] + if obj.x != 100.0 || obj.y != 200.0 || obj.z != 300.0 { + t.Errorf("Object position not updated: got (%f, %f, %f), want (100, 200, 300)", + obj.x, obj.y, obj.z) + } + + // Verify broadcast was sent to session2 + select { + case <-session2.sendPackets: + // Good - broadcast received + default: + t.Error("Position update should be broadcast to other sessions") + } +} + +func TestHandleMsgSysPositionObject_NoObject(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + stage := NewStage("test_stage") + session.stage = stage + stage.clients[session] = session.charID + + // Position update for non-existent object - should not panic + pkt := &mhfpacket.MsgSysPositionObject{ + ObjID: 999, + X: 100.0, + Y: 200.0, + Z: 300.0, + } + + // Should not panic + defer func() { + if r := recover(); r != nil { + t.Errorf("handleMsgSysPositionObject panicked with non-existent object: %v", r) + } + }() + + handleMsgSysPositionObject(session, pkt) +} + +func TestHandleMsgSysDeleteObject(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + // Should not panic (empty handler) + defer func() { + if r := recover(); r != nil { + t.Errorf("handleMsgSysDeleteObject panicked: %v", r) + } + }() + + handleMsgSysDeleteObject(session, nil) +} + +func TestHandleMsgSysRotateObject(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + // Should not panic (empty handler) + defer func() { + if r := recover(); r != nil { + t.Errorf("handleMsgSysRotateObject panicked: %v", r) + } + }() + + handleMsgSysRotateObject(session, nil) +} + +func TestHandleMsgSysDuplicateObject(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + // Should not panic (empty handler) + defer func() { + if r := recover(); r != nil { + t.Errorf("handleMsgSysDuplicateObject panicked: %v", r) + } + }() + + handleMsgSysDuplicateObject(session, nil) +} + +func TestHandleMsgSysGetObjectBinary(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + // Should not panic (empty handler) + defer func() { + if r := recover(); r != nil { + t.Errorf("handleMsgSysGetObjectBinary panicked: %v", r) + } + }() + + handleMsgSysGetObjectBinary(session, nil) +} + +func TestHandleMsgSysGetObjectOwner(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + // Should not panic (empty handler) + defer func() { + if r := recover(); r != nil { + t.Errorf("handleMsgSysGetObjectOwner panicked: %v", r) + } + }() + + handleMsgSysGetObjectOwner(session, nil) +} + +func TestHandleMsgSysUpdateObjectBinary(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + // Should not panic (empty handler) + defer func() { + if r := recover(); r != nil { + t.Errorf("handleMsgSysUpdateObjectBinary panicked: %v", r) + } + }() + + handleMsgSysUpdateObjectBinary(session, nil) +} + +func TestHandleMsgSysCleanupObject(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + // Should not panic (empty handler) + defer func() { + if r := recover(); r != nil { + t.Errorf("handleMsgSysCleanupObject panicked: %v", r) + } + }() + + handleMsgSysCleanupObject(session, nil) +} + +func TestHandleMsgSysAddObject(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + // Should not panic (empty handler) + defer func() { + if r := recover(); r != nil { + t.Errorf("handleMsgSysAddObject panicked: %v", r) + } + }() + + handleMsgSysAddObject(session, nil) +} + +func TestHandleMsgSysDelObject(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + // Should not panic (empty handler) + defer func() { + if r := recover(); r != nil { + t.Errorf("handleMsgSysDelObject panicked: %v", r) + } + }() + + handleMsgSysDelObject(session, nil) +} + +func TestHandleMsgSysDispObject(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + // Should not panic (empty handler) + defer func() { + if r := recover(); r != nil { + t.Errorf("handleMsgSysDispObject panicked: %v", r) + } + }() + + handleMsgSysDispObject(session, nil) +} + +func TestHandleMsgSysHideObject(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + // Should not panic (empty handler) + defer func() { + if r := recover(); r != nil { + t.Errorf("handleMsgSysHideObject panicked: %v", r) + } + }() + + handleMsgSysHideObject(session, nil) +} + +func TestObjectHandlers_SequentialCreateObject(t *testing.T) { + server := createMockServer() + stage := NewStage("test_stage") + + // Create objects sequentially from multiple sessions + // Note: handleMsgSysCreateObject has a race condition in NextObjectID + // so we test sequential creation instead + for i := 0; i < 10; i++ { + session := createMockSession(uint32(i), server) + session.stage = stage + + pkt := &mhfpacket.MsgSysCreateObject{ + AckHandle: uint32(i), + X: float32(i), + Y: float32(i * 2), + Z: float32(i * 3), + } + + handleMsgSysCreateObject(session, pkt) + + // Drain send queue + select { + case <-session.sendPackets: + default: + } + } + + // All objects should be created + if len(stage.objects) != 10 { + t.Errorf("Expected 10 objects, got %d", len(stage.objects)) + } +} + +func TestObjectHandlers_SequentialPositionUpdate(t *testing.T) { + server := createMockServer() + stage := NewStage("test_stage") + + session := createMockSession(1, server) + session.stage = stage + stage.clients[session] = session.charID + + // Create an object + stage.objects[session.charID] = &Object{ + id: 1, + ownerCharID: session.charID, + x: 0, + y: 0, + z: 0, + } + + // Sequentially update object position + for i := 0; i < 10; i++ { + pkt := &mhfpacket.MsgSysPositionObject{ + ObjID: 1, + X: float32(i), + Y: float32(i * 2), + Z: float32(i * 3), + } + + handleMsgSysPositionObject(session, pkt) + } + + // Verify final position + obj := stage.objects[session.charID] + if obj.x != 9 || obj.y != 18 || obj.z != 27 { + t.Errorf("Object position not as expected: got (%f, %f, %f), want (9, 18, 27)", + obj.x, obj.y, obj.z) + } +} diff --git a/server/channelserver/handlers_quest_backport_test.go b/server/channelserver/handlers_quest_backport_test.go new file mode 100644 index 000000000..b07bca4c3 --- /dev/null +++ b/server/channelserver/handlers_quest_backport_test.go @@ -0,0 +1,128 @@ +package channelserver + +import ( + "encoding/binary" + "testing" + + _config "erupe-ce/config" +) + +func TestBackportQuest_Basic(t *testing.T) { + // Set up config for the test + oldConfig := _config.ErupeConfig + defer func() { _config.ErupeConfig = oldConfig }() + + _config.ErupeConfig = &_config.Config{} + _config.ErupeConfig.RealClientMode = _config.ZZ + + // Create a quest data buffer large enough for BackportQuest to work with. + // The function reads a uint32 from data[0:4] as offset, then works at offset+96. + // We need at least offset + 96 + 108 + 6*8 bytes. + // Set offset (wp base) = 0, so wp starts at 96, rp at 100. + data := make([]byte, 512) + binary.LittleEndian.PutUint32(data[0:4], 0) // offset = 0 + + // Fill some data at the rp positions so we can verify copies + for i := 100; i < 400; i++ { + data[i] = byte(i & 0xFF) + } + + result := BackportQuest(data) + if result == nil { + t.Fatal("BackportQuest returned nil") + } + if len(result) != len(data) { + t.Errorf("BackportQuest changed data length: got %d, want %d", len(result), len(data)) + } +} + +func TestBackportQuest_S6Mode(t *testing.T) { + oldConfig := _config.ErupeConfig + defer func() { _config.ErupeConfig = oldConfig }() + + _config.ErupeConfig = &_config.Config{} + _config.ErupeConfig.RealClientMode = _config.S6 + + data := make([]byte, 512) + binary.LittleEndian.PutUint32(data[0:4], 0) + + for i := 0; i < len(data); i++ { + data[i+4] = byte(i % 256) + if i+4 >= len(data)-1 { + break + } + } + + // Set some values at data[8:12] so we can check they get copied to data[16:20] + binary.LittleEndian.PutUint32(data[8:12], 0xDEADBEEF) + + result := BackportQuest(data) + if result == nil { + t.Fatal("BackportQuest returned nil") + } + + // In S6 mode, data[16:20] should be copied from data[8:12] + got := binary.LittleEndian.Uint32(result[16:20]) + if got != 0xDEADBEEF { + t.Errorf("S6 mode: data[16:20] = 0x%X, want 0xDEADBEEF", got) + } +} + +func TestBackportQuest_G91Mode_PatternReplacement(t *testing.T) { + oldConfig := _config.ErupeConfig + defer func() { _config.ErupeConfig = oldConfig }() + + _config.ErupeConfig = &_config.Config{} + _config.ErupeConfig.RealClientMode = _config.G91 + + data := make([]byte, 512) + binary.LittleEndian.PutUint32(data[0:4], 0) + + // Insert an armor sphere pattern at a known location + // Pattern: 0x0A, 0x00, 0x01, 0x33 -> should replace bytes at +2 with 0xD7, 0x00 + offset := 300 + data[offset] = 0x0A + data[offset+1] = 0x00 + data[offset+2] = 0x01 + data[offset+3] = 0x33 + + result := BackportQuest(data) + + // After BackportQuest, the pattern's last 2 bytes should be replaced + if result[offset+2] != 0xD7 || result[offset+3] != 0x00 { + t.Errorf("G91 pattern replacement failed: got [0x%X, 0x%X], want [0xD7, 0x00]", + result[offset+2], result[offset+3]) + } +} + +func TestBackportQuest_F5Mode(t *testing.T) { + oldConfig := _config.ErupeConfig + defer func() { _config.ErupeConfig = oldConfig }() + + _config.ErupeConfig = &_config.Config{} + _config.ErupeConfig.RealClientMode = _config.F5 + + data := make([]byte, 512) + binary.LittleEndian.PutUint32(data[0:4], 0) + + result := BackportQuest(data) + if result == nil { + t.Fatal("BackportQuest returned nil") + } +} + +func TestBackportQuest_G101Mode(t *testing.T) { + oldConfig := _config.ErupeConfig + defer func() { _config.ErupeConfig = oldConfig }() + + _config.ErupeConfig = &_config.Config{} + _config.ErupeConfig.RealClientMode = _config.G101 + + data := make([]byte, 512) + binary.LittleEndian.PutUint32(data[0:4], 0) + + result := BackportQuest(data) + if result == nil { + t.Fatal("BackportQuest returned nil") + } +} diff --git a/server/channelserver/handlers_rengoku_test.go b/server/channelserver/handlers_rengoku_test.go new file mode 100644 index 000000000..605068e26 --- /dev/null +++ b/server/channelserver/handlers_rengoku_test.go @@ -0,0 +1,53 @@ +package channelserver + +import ( + "testing" + + "erupe-ce/network/mhfpacket" +) + +func TestHandleMsgMhfGetRengokuRankingRank(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfGetRengokuRankingRank{ + AckHandle: 12345, + } + + handleMsgMhfGetRengokuRankingRank(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") + } +} + +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 new file mode 100644 index 000000000..f031fb15f --- /dev/null +++ b/server/channelserver/handlers_reserve_test.go @@ -0,0 +1,113 @@ +package channelserver + +import ( + "testing" + + "erupe-ce/network/mhfpacket" +) + +func TestReserveHandlersWithAck(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + // Test handleMsgSysReserve188 + handleMsgSysReserve188(session, &mhfpacket.MsgSysReserve188{AckHandle: 12345}) + select { + case p := <-session.sendPackets: + if len(p.data) == 0 { + t.Error("Reserve188: response should have data") + } + default: + t.Error("Reserve188: no response queued") + } + + // Test handleMsgSysReserve18B + handleMsgSysReserve18B(session, &mhfpacket.MsgSysReserve18B{AckHandle: 12345}) + select { + case p := <-session.sendPackets: + if len(p.data) == 0 { + t.Error("Reserve18B: response should have data") + } + default: + t.Error("Reserve18B: no response queued") + } +} + +func TestReserveEmptyHandlers(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + tests := []struct { + name string + handler func(s *Session, p mhfpacket.MHFPacket) + pkt mhfpacket.MHFPacket + }{ + {"Reserve55", handleMsgSysReserve55, &mhfpacket.MsgSysReserve55{}}, + {"Reserve56", handleMsgSysReserve56, &mhfpacket.MsgSysReserve56{}}, + {"Reserve57", handleMsgSysReserve57, &mhfpacket.MsgSysReserve57{}}, + {"Reserve01", handleMsgSysReserve01, &mhfpacket.MsgSysReserve01{}}, + {"Reserve02", handleMsgSysReserve02, &mhfpacket.MsgSysReserve02{}}, + {"Reserve03", handleMsgSysReserve03, &mhfpacket.MsgSysReserve03{}}, + {"Reserve04", handleMsgSysReserve04, &mhfpacket.MsgSysReserve04{}}, + {"Reserve05", handleMsgSysReserve05, &mhfpacket.MsgSysReserve05{}}, + {"Reserve06", handleMsgSysReserve06, &mhfpacket.MsgSysReserve06{}}, + {"Reserve07", handleMsgSysReserve07, &mhfpacket.MsgSysReserve07{}}, + {"Reserve0C", handleMsgSysReserve0C, &mhfpacket.MsgSysReserve0C{}}, + {"Reserve0D", handleMsgSysReserve0D, &mhfpacket.MsgSysReserve0D{}}, + {"Reserve0E", handleMsgSysReserve0E, &mhfpacket.MsgSysReserve0E{}}, + {"Reserve4A", handleMsgSysReserve4A, &mhfpacket.MsgSysReserve4A{}}, + {"Reserve4B", handleMsgSysReserve4B, &mhfpacket.MsgSysReserve4B{}}, + {"Reserve4C", handleMsgSysReserve4C, &mhfpacket.MsgSysReserve4C{}}, + {"Reserve4D", handleMsgSysReserve4D, &mhfpacket.MsgSysReserve4D{}}, + {"Reserve4E", handleMsgSysReserve4E, &mhfpacket.MsgSysReserve4E{}}, + {"Reserve4F", handleMsgSysReserve4F, &mhfpacket.MsgSysReserve4F{}}, + {"Reserve5C", handleMsgSysReserve5C, &mhfpacket.MsgSysReserve5C{}}, + {"Reserve5E", handleMsgSysReserve5E, &mhfpacket.MsgSysReserve5E{}}, + {"Reserve5F", handleMsgSysReserve5F, &mhfpacket.MsgSysReserve5F{}}, + {"Reserve71", handleMsgSysReserve71, &mhfpacket.MsgSysReserve71{}}, + {"Reserve72", handleMsgSysReserve72, &mhfpacket.MsgSysReserve72{}}, + {"Reserve73", handleMsgSysReserve73, &mhfpacket.MsgSysReserve73{}}, + {"Reserve74", handleMsgSysReserve74, &mhfpacket.MsgSysReserve74{}}, + {"Reserve75", handleMsgSysReserve75, &mhfpacket.MsgSysReserve75{}}, + {"Reserve76", handleMsgSysReserve76, &mhfpacket.MsgSysReserve76{}}, + {"Reserve77", handleMsgSysReserve77, &mhfpacket.MsgSysReserve77{}}, + {"Reserve78", handleMsgSysReserve78, &mhfpacket.MsgSysReserve78{}}, + {"Reserve79", handleMsgSysReserve79, &mhfpacket.MsgSysReserve79{}}, + {"Reserve7A", handleMsgSysReserve7A, &mhfpacket.MsgSysReserve7A{}}, + {"Reserve7B", handleMsgSysReserve7B, &mhfpacket.MsgSysReserve7B{}}, + {"Reserve7C", handleMsgSysReserve7C, &mhfpacket.MsgSysReserve7C{}}, + {"Reserve7E", handleMsgSysReserve7E, &mhfpacket.MsgSysReserve7E{}}, + {"Reserve10F", handleMsgMhfReserve10F, &mhfpacket.MsgMhfReserve10F{}}, + {"Reserve180", handleMsgSysReserve180, &mhfpacket.MsgSysReserve180{}}, + {"Reserve18E", handleMsgSysReserve18E, &mhfpacket.MsgSysReserve18E{}}, + {"Reserve18F", handleMsgSysReserve18F, &mhfpacket.MsgSysReserve18F{}}, + {"Reserve19E", handleMsgSysReserve19E, &mhfpacket.MsgSysReserve19E{}}, + {"Reserve19F", handleMsgSysReserve19F, &mhfpacket.MsgSysReserve19F{}}, + {"Reserve1A4", handleMsgSysReserve1A4, &mhfpacket.MsgSysReserve1A4{}}, + {"Reserve1A6", handleMsgSysReserve1A6, &mhfpacket.MsgSysReserve1A6{}}, + {"Reserve1A7", handleMsgSysReserve1A7, &mhfpacket.MsgSysReserve1A7{}}, + {"Reserve1A8", handleMsgSysReserve1A8, &mhfpacket.MsgSysReserve1A8{}}, + {"Reserve1A9", handleMsgSysReserve1A9, &mhfpacket.MsgSysReserve1A9{}}, + {"Reserve1AA", handleMsgSysReserve1AA, &mhfpacket.MsgSysReserve1AA{}}, + {"Reserve1AB", handleMsgSysReserve1AB, &mhfpacket.MsgSysReserve1AB{}}, + {"Reserve1AC", handleMsgSysReserve1AC, &mhfpacket.MsgSysReserve1AC{}}, + {"Reserve1AD", handleMsgSysReserve1AD, &mhfpacket.MsgSysReserve1AD{}}, + {"Reserve1AE", handleMsgSysReserve1AE, &mhfpacket.MsgSysReserve1AE{}}, + {"Reserve1AF", handleMsgSysReserve1AF, &mhfpacket.MsgSysReserve1AF{}}, + {"Reserve19B", handleMsgSysReserve19B, &mhfpacket.MsgSysReserve19B{}}, + {"Reserve192", handleMsgSysReserve192, &mhfpacket.MsgSysReserve192{}}, + {"Reserve193", handleMsgSysReserve193, &mhfpacket.MsgSysReserve193{}}, + {"Reserve194", handleMsgSysReserve194, &mhfpacket.MsgSysReserve194{}}, + } + + 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, tt.pkt) + }) + } +} diff --git a/server/channelserver/handlers_reward_test.go b/server/channelserver/handlers_reward_test.go new file mode 100644 index 000000000..ff2770eb0 --- /dev/null +++ b/server/channelserver/handlers_reward_test.go @@ -0,0 +1,126 @@ +package channelserver + +import ( + "testing" + + "erupe-ce/network/mhfpacket" +) + +func TestHandleMsgMhfGetAdditionalBeatReward(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfGetAdditionalBeatReward{ + AckHandle: 12345, + } + + handleMsgMhfGetAdditionalBeatReward(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 TestHandleMsgMhfGetUdRankingRewardList(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfGetUdRankingRewardList{ + AckHandle: 12345, + } + + handleMsgMhfGetUdRankingRewardList(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 TestHandleMsgMhfGetRewardSong(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfGetRewardSong{ + AckHandle: 12345, + } + + handleMsgMhfGetRewardSong(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 TestHandleMsgMhfUseRewardSong(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + defer func() { + if r := recover(); r != nil { + t.Errorf("handleMsgMhfUseRewardSong panicked: %v", r) + } + }() + + handleMsgMhfUseRewardSong(session, nil) +} + +func TestHandleMsgMhfAddRewardSongCount(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + defer func() { + if r := recover(); r != nil { + t.Errorf("handleMsgMhfAddRewardSongCount panicked: %v", r) + } + }() + + handleMsgMhfAddRewardSongCount(session, nil) +} + +func TestHandleMsgMhfAcquireMonthlyReward(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfAcquireMonthlyReward{ + AckHandle: 12345, + } + + handleMsgMhfAcquireMonthlyReward(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 TestHandleMsgMhfAcceptReadReward(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + defer func() { + if r := recover(); r != nil { + t.Errorf("handleMsgMhfAcceptReadReward panicked: %v", r) + } + }() + + handleMsgMhfAcceptReadReward(session, nil) +} diff --git a/server/channelserver/handlers_semaphore_test.go b/server/channelserver/handlers_semaphore_test.go new file mode 100644 index 000000000..206694394 --- /dev/null +++ b/server/channelserver/handlers_semaphore_test.go @@ -0,0 +1,447 @@ +package channelserver + +import ( + "testing" + + "erupe-ce/network/mhfpacket" +) + +func TestHandleMsgSysCreateSemaphore(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgSysCreateSemaphore{ + AckHandle: 12345, + Unk0: 0, + } + + handleMsgSysCreateSemaphore(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") + } +} + +func TestHandleMsgSysDeleteSemaphore_NoSemaphores(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgSysDeleteSemaphore{ + SemaphoreID: 12345, + } + + // Should not panic when no semaphores exist + defer func() { + if r := recover(); r != nil { + t.Errorf("handleMsgSysDeleteSemaphore panicked: %v", r) + } + }() + + handleMsgSysDeleteSemaphore(session, pkt) +} + +func TestHandleMsgSysDeleteSemaphore_WithSemaphore(t *testing.T) { + server := createMockServer() + server.semaphore = make(map[string]*Semaphore) + session := createMockSession(1, server) + + // Create a semaphore + sema := NewSemaphore(session, "test_sema", 4) + server.semaphore["test_sema"] = sema + + pkt := &mhfpacket.MsgSysDeleteSemaphore{ + SemaphoreID: sema.id, + } + + handleMsgSysDeleteSemaphore(session, pkt) + + // Semaphore should be deleted + if _, exists := server.semaphore["test_sema"]; exists { + t.Error("Semaphore should be deleted") + } +} + +func TestHandleMsgSysCreateAcquireSemaphore_NewSemaphore(t *testing.T) { + server := createMockServer() + server.semaphore = make(map[string]*Semaphore) + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgSysCreateAcquireSemaphore{ + AckHandle: 12345, + Unk0: 0, + PlayerCount: 4, + SemaphoreID: "test_semaphore", + } + + handleMsgSysCreateAcquireSemaphore(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") + } + + // Verify semaphore was created + if _, exists := server.semaphore["test_semaphore"]; !exists { + t.Error("Semaphore should be created") + } +} + +func TestHandleMsgSysCreateAcquireSemaphore_ExistingSemaphore(t *testing.T) { + server := createMockServer() + server.semaphore = make(map[string]*Semaphore) + session := createMockSession(1, server) + + // Pre-create semaphore + sema := NewSemaphore(session, "existing_sema", 4) + server.semaphore["existing_sema"] = sema + + pkt := &mhfpacket.MsgSysCreateAcquireSemaphore{ + AckHandle: 12345, + Unk0: 0, + PlayerCount: 4, + SemaphoreID: "existing_sema", + } + + handleMsgSysCreateAcquireSemaphore(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") + } + + // Verify client was added to semaphore + if len(sema.clients) == 0 { + t.Error("Session should be added to semaphore") + } +} + +func TestHandleMsgSysCreateAcquireSemaphore_RavienteSemaphore(t *testing.T) { + server := createMockServer() + server.semaphore = make(map[string]*Semaphore) + session := createMockSession(1, server) + + // Test raviente semaphore (special prefix) + pkt := &mhfpacket.MsgSysCreateAcquireSemaphore{ + AckHandle: 12345, + Unk0: 0, + PlayerCount: 32, + SemaphoreID: "hs_l0u3B51", // Raviente prefix + suffix + } + + handleMsgSysCreateAcquireSemaphore(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") + } + + // Verify raviente semaphore was created with special settings + if sema, exists := server.semaphore["hs_l0u3B51"]; !exists { + t.Error("Raviente semaphore should be created") + } else if sema.maxPlayers != 127 { + t.Errorf("Raviente semaphore maxPlayers = %d, want 127", sema.maxPlayers) + } +} + +func TestHandleMsgSysCreateAcquireSemaphore_Full(t *testing.T) { + server := createMockServer() + server.semaphore = make(map[string]*Semaphore) + + // Create semaphore with 1 player max + session1 := createMockSession(1, server) + sema := NewSemaphore(session1, "full_sema", 1) + server.semaphore["full_sema"] = sema + + // Fill the semaphore + sema.clients[session1] = session1.charID + + // Try to acquire with another session + session2 := createMockSession(2, server) + pkt := &mhfpacket.MsgSysCreateAcquireSemaphore{ + AckHandle: 12345, + Unk0: 0, + PlayerCount: 1, + SemaphoreID: "full_sema", + } + + handleMsgSysCreateAcquireSemaphore(session2, pkt) + + // Should still respond (with failure indication) + select { + case p := <-session2.sendPackets: + if len(p.data) == 0 { + t.Error("Response packet should have data even for full semaphore") + } + default: + t.Error("No response packet queued") + } +} + +func TestHandleMsgSysAcquireSemaphore_Exists(t *testing.T) { + server := createMockServer() + server.semaphore = make(map[string]*Semaphore) + session := createMockSession(1, server) + + // Create semaphore + sema := NewSemaphore(session, "acquire_test", 4) + server.semaphore["acquire_test"] = sema + + pkt := &mhfpacket.MsgSysAcquireSemaphore{ + AckHandle: 12345, + SemaphoreID: "acquire_test", + } + + handleMsgSysAcquireSemaphore(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") + } + + // Verify host was set + if sema.host != session { + t.Error("Session should be set as semaphore host") + } +} + +func TestHandleMsgSysAcquireSemaphore_NotExists(t *testing.T) { + server := createMockServer() + server.semaphore = make(map[string]*Semaphore) + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgSysAcquireSemaphore{ + AckHandle: 12345, + SemaphoreID: "nonexistent", + } + + handleMsgSysAcquireSemaphore(session, pkt) + + // Should respond with 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") + } +} + +func TestHandleMsgSysReleaseSemaphore(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + // Should not panic (mostly empty handler) + defer func() { + if r := recover(); r != nil { + t.Errorf("handleMsgSysReleaseSemaphore panicked: %v", r) + } + }() + + pkt := &mhfpacket.MsgSysReleaseSemaphore{} + handleMsgSysReleaseSemaphore(session, pkt) +} + +func TestHandleMsgSysCheckSemaphore_Exists(t *testing.T) { + server := createMockServer() + server.semaphore = make(map[string]*Semaphore) + session := createMockSession(1, server) + + // Create semaphore + sema := NewSemaphore(session, "check_test", 4) + server.semaphore["check_test"] = sema + + pkt := &mhfpacket.MsgSysCheckSemaphore{ + AckHandle: 12345, + SemaphoreID: "check_test", + } + + handleMsgSysCheckSemaphore(session, pkt) + + // Verify response indicates semaphore exists + select { + case p := <-session.sendPackets: + if len(p.data) < 4 { + t.Error("Response packet should have at least 4 bytes") + } + default: + t.Error("No response packet queued") + } +} + +func TestHandleMsgSysCheckSemaphore_NotExists(t *testing.T) { + server := createMockServer() + server.semaphore = make(map[string]*Semaphore) + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgSysCheckSemaphore{ + AckHandle: 12345, + SemaphoreID: "nonexistent", + } + + handleMsgSysCheckSemaphore(session, pkt) + + // Verify response indicates semaphore does not exist + select { + case p := <-session.sendPackets: + if len(p.data) < 4 { + t.Error("Response packet should have at least 4 bytes") + } + default: + t.Error("No response packet queued") + } +} + +func TestRemoveSessionFromSemaphore(t *testing.T) { + server := createMockServer() + server.semaphore = make(map[string]*Semaphore) + session := createMockSession(1, server) + + // Create semaphore and add session + sema := NewSemaphore(session, "remove_test", 4) + sema.clients[session] = session.charID + server.semaphore["remove_test"] = sema + + // Remove session + removeSessionFromSemaphore(session) + + // Verify session was removed + if _, exists := sema.clients[session]; exists { + t.Error("Session should be removed from clients") + } +} + +func TestRemoveSessionFromSemaphore_MultipleSemaphores(t *testing.T) { + server := createMockServer() + server.semaphore = make(map[string]*Semaphore) + session := createMockSession(1, server) + + // Create multiple semaphores with the session + for i := 0; i < 3; i++ { + sema := NewSemaphore(session, "multi_test_"+string(rune('a'+i)), 4) + sema.clients[session] = session.charID + server.semaphore["multi_test_"+string(rune('a'+i))] = sema + } + + // Remove session from all + removeSessionFromSemaphore(session) + + // Verify session was removed from all semaphores + for _, sema := range server.semaphore { + if _, exists := sema.clients[session]; exists { + t.Error("Session should be removed from all semaphore clients") + } + } +} + +func TestDestructEmptySemaphores(t *testing.T) { + server := createMockServer() + server.semaphore = make(map[string]*Semaphore) + session := createMockSession(1, server) + + // Create empty semaphore + sema := NewSemaphore(session, "empty_sema", 4) + server.semaphore["empty_sema"] = sema + + // Create non-empty semaphore + semaWithClients := NewSemaphore(session, "with_clients", 4) + semaWithClients.clients[session] = session.charID + server.semaphore["with_clients"] = semaWithClients + + destructEmptySemaphores(session) + + // Empty semaphore should be deleted + if _, exists := server.semaphore["empty_sema"]; exists { + t.Error("Empty semaphore should be deleted") + } + + // Non-empty semaphore should remain + if _, exists := server.semaphore["with_clients"]; !exists { + t.Error("Non-empty semaphore should remain") + } +} + +func TestSemaphoreHandlers_SequentialAcquire(t *testing.T) { + server := createMockServer() + server.semaphore = make(map[string]*Semaphore) + + // Sequentially try to create/acquire the same semaphore + // Note: the handler has race conditions when accessed concurrently + for i := 0; i < 5; i++ { + session := createMockSession(uint32(i), server) + + pkt := &mhfpacket.MsgSysCreateAcquireSemaphore{ + AckHandle: uint32(i), + Unk0: 0, + PlayerCount: 4, + SemaphoreID: "sequential_test", + } + + handleMsgSysCreateAcquireSemaphore(session, pkt) + + // Drain send queue + select { + case <-session.sendPackets: + default: + } + } + + // Semaphore should exist + if _, exists := server.semaphore["sequential_test"]; !exists { + t.Error("Semaphore should exist after sequential acquires") + } +} + +func TestSemaphoreHandlers_MultipleCheck(t *testing.T) { + server := createMockServer() + server.semaphore = make(map[string]*Semaphore) + + // Create semaphore + helperSession := createMockSession(99, server) + sema := NewSemaphore(helperSession, "check_multiple", 4) + server.semaphore["check_multiple"] = sema + + // Check the semaphore from multiple sessions sequentially + for i := 0; i < 5; i++ { + session := createMockSession(uint32(i), server) + + pkt := &mhfpacket.MsgSysCheckSemaphore{ + AckHandle: uint32(i), + SemaphoreID: "check_multiple", + } + + handleMsgSysCheckSemaphore(session, pkt) + + // Drain send queue + select { + case <-session.sendPackets: + default: + } + } +} diff --git a/server/channelserver/handlers_shop_gacha_test.go b/server/channelserver/handlers_shop_gacha_test.go new file mode 100644 index 000000000..787a860bb --- /dev/null +++ b/server/channelserver/handlers_shop_gacha_test.go @@ -0,0 +1,408 @@ +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) + } +} + +func TestGetRandomEntries_ZeroRolls(t *testing.T) { + entries := []GachaEntry{ + {ID: 1, Weight: 1.0}, + } + result, err := getRandomEntries(entries, 0, false) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if len(result) != 0 { + t.Errorf("expected 0 results, got %d", len(result)) + } +} + +func TestGetRandomEntries_SingleEntryNonBox(t *testing.T) { + entries := []GachaEntry{ + {ID: 1, Weight: 1.0, ItemNumber: 100}, + } + result, err := getRandomEntries(entries, 3, false) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if len(result) != 3 { + t.Errorf("expected 3 results, got %d", len(result)) + } + for i, r := range result { + if r.ID != 1 { + t.Errorf("result[%d].ID = %d, expected 1", i, r.ID) + } + } +} + +func TestGetRandomEntries_NonBoxAllowsDuplicates(t *testing.T) { + entries := []GachaEntry{ + {ID: 1, Weight: 1.0}, + } + result, err := getRandomEntries(entries, 5, false) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if len(result) != 5 { + t.Errorf("expected 5 results, got %d", len(result)) + } + // All should be the same since there's only one entry + for i, r := range result { + if r.ID != 1 { + t.Errorf("result[%d].ID = %d, expected 1", i, r.ID) + } + } +} + +func TestGetRandomEntries_BoxModeRemovesSelected(t *testing.T) { + entries := []GachaEntry{ + {ID: 1, Weight: 1.0}, + {ID: 2, Weight: 1.0}, + {ID: 3, Weight: 1.0}, + } + result, err := getRandomEntries(entries, 3, true) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if len(result) != 3 { + t.Errorf("expected 3 results, got %d", len(result)) + } + + // In box mode, all entries should be unique + seen := make(map[uint32]bool) + for _, r := range result { + if seen[r.ID] { + t.Errorf("duplicate entry in box mode: ID=%d", r.ID) + } + seen[r.ID] = true + } +} + +func TestGetRandomEntries_BoxModeMatchingCount(t *testing.T) { + entries := []GachaEntry{ + {ID: 1, Weight: 1.0}, + {ID: 2, Weight: 1.0}, + } + result, err := getRandomEntries(entries, 2, true) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if len(result) != 2 { + t.Errorf("expected 2 results, got %d", len(result)) + } + + // Should contain both entries exactly once + seen := make(map[uint32]bool) + for _, r := range result { + seen[r.ID] = true + } + if !seen[1] || !seen[2] { + t.Errorf("box mode should return all entries when rolls == len(entries)") + } +} + +func TestGetRandomEntries_WeightedSelectionBias(t *testing.T) { + // Test that weighted selection respects weights + entries := []GachaEntry{ + {ID: 1, Weight: 100.0}, // Very high weight + {ID: 2, Weight: 0.001}, // Very low weight + } + + // Run many iterations + counts := make(map[uint32]int) + for i := 0; i < 1000; i++ { + result, _ := getRandomEntries(entries, 1, false) + if len(result) > 0 { + counts[result[0].ID]++ + } + } + + // ID 1 should be selected much more often + if counts[1] <= counts[2] { + t.Errorf("weighted selection not working: high weight count=%d, low weight count=%d", + counts[1], counts[2]) + } +} + +func TestGetRandomEntries_MultipleEntriesMultipleRolls(t *testing.T) { + entries := []GachaEntry{ + {ID: 1, Weight: 1.0}, + {ID: 2, Weight: 1.0}, + {ID: 3, Weight: 1.0}, + } + result, err := getRandomEntries(entries, 10, false) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if len(result) != 10 { + t.Errorf("expected 10 results, got %d", len(result)) + } + + // All results should have valid IDs + for i, r := range result { + if r.ID < 1 || r.ID > 3 { + t.Errorf("result[%d].ID = %d, expected 1, 2, or 3", i, r.ID) + } + } +} + +func TestGetRandomEntries_PreservesEntryData(t *testing.T) { + entries := []GachaEntry{ + { + ID: 1, + Weight: 1.0, + ItemNumber: 100, + ItemQuantity: 5, + Rarity: 3, + FrontierPoints: 500, + }, + } + result, err := getRandomEntries(entries, 1, false) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if len(result) != 1 { + t.Fatalf("expected 1 result, got %d", len(result)) + } + + r := result[0] + if r.ItemNumber != 100 { + t.Errorf("ItemNumber = %d, expected 100", r.ItemNumber) + } + if r.ItemQuantity != 5 { + t.Errorf("ItemQuantity = %d, expected 5", r.ItemQuantity) + } + if r.Rarity != 3 { + t.Errorf("Rarity = %d, expected 3", r.Rarity) + } + if r.FrontierPoints != 500 { + t.Errorf("FrontierPoints = %d, expected 500", r.FrontierPoints) + } +} diff --git a/server/channelserver/handlers_simple_test.go b/server/channelserver/handlers_simple_test.go new file mode 100644 index 000000000..64a284569 --- /dev/null +++ b/server/channelserver/handlers_simple_test.go @@ -0,0 +1,313 @@ +package channelserver + +import ( + "testing" + "time" + + "erupe-ce/network/mhfpacket" +) + +// Test simple handler patterns that don't require database + +func TestHandlerMsgMhfSexChanger(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfSexChanger{ + AckHandle: 12345, + } + + // Should not panic + handleMsgMhfSexChanger(session, pkt) + + // Should queue a response + 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 TestHandlerMsgMhfEnterTournamentQuest(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + // Should not panic with nil packet (empty handler) + defer func() { + if r := recover(); r != nil { + t.Errorf("handleMsgMhfEnterTournamentQuest panicked: %v", r) + } + }() + + handleMsgMhfEnterTournamentQuest(session, nil) +} + +func TestHandlerMsgMhfGetUdBonusQuestInfo(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfGetUdBonusQuestInfo{ + AckHandle: 12345, + } + + handleMsgMhfGetUdBonusQuestInfo(session, pkt) + + // Should queue a response + 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 that acknowledge handlers work correctly + +func TestAckResponseFormats(t *testing.T) { + server := createMockServer() + + tests := []struct { + name string + handler func(s *Session, ackHandle uint32, data []byte) + }{ + {"doAckBufSucceed", doAckBufSucceed}, + {"doAckBufFail", doAckBufFail}, + {"doAckSimpleSucceed", doAckSimpleSucceed}, + {"doAckSimpleFail", doAckSimpleFail}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + session := createMockSession(1, server) + testData := []byte{0x01, 0x02, 0x03, 0x04} + + tt.handler(session, 99999, testData) + + select { + case pkt := <-session.sendPackets: + if pkt.data == nil { + t.Error("Packet data should not be nil") + } + default: + t.Error("Handler should queue a packet") + } + }) + } +} + +func TestStubHandlers(t *testing.T) { + server := createMockServer() + + tests := []struct { + name string + handler func(s *Session, ackHandle uint32) + }{ + {"stubEnumerateNoResults", stubEnumerateNoResults}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + session := createMockSession(1, server) + + tt.handler(session, 12345) + + select { + case pkt := <-session.sendPackets: + if pkt.data == nil { + t.Error("Packet data should not be nil") + } + default: + t.Error("Stub handler should queue a packet") + } + }) + } +} + +// Test packet queueing + +func TestSessionQueueSendMHF(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgSysAck{ + AckHandle: 12345, + IsBufferResponse: false, + ErrorCode: 0, + AckData: []byte{0x00}, + } + + session.QueueSendMHF(pkt) + + select { + case p := <-session.sendPackets: + if len(p.data) == 0 { + t.Error("Queued packet should have data") + } + default: + t.Error("QueueSendMHF should queue a packet") + } +} + +func TestSessionQueueSendNonBlocking(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + data := []byte{0x01, 0x02, 0x03, 0x04} + session.QueueSendNonBlocking(data) + + select { + case p := <-session.sendPackets: + if len(p.data) != 4 { + t.Errorf("Queued data len = %d, want 4", len(p.data)) + } + if p.nonBlocking != true { + t.Error("Packet should be marked as non-blocking") + } + default: + t.Error("QueueSendNonBlocking should queue data") + } +} + +func TestSessionQueueSendNonBlocking_FullQueue(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + // Fill the queue + for i := 0; i < 20; i++ { + session.sendPackets <- packet{data: []byte{byte(i)}, nonBlocking: true} + } + + // Non-blocking send should not block when queue is full + // It should drop the packet instead + done := make(chan bool, 1) + go func() { + session.QueueSendNonBlocking([]byte{0xFF}) + done <- true + }() + + // Wait for completion with a reasonable timeout + // The function should return immediately (dropping the packet) + select { + case <-done: + // Good - didn't block, function completed + case <-time.After(100 * time.Millisecond): + 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}, + {"handleMsgMhfGetExtraInfo", handleMsgMhfGetExtraInfo}, + {"handleMsgMhfGetCogInfo", handleMsgMhfGetCogInfo}, + {"handleMsgMhfStampcardPrize", handleMsgMhfStampcardPrize}, + {"handleMsgMhfKickExportForce", handleMsgMhfKickExportForce}, + {"handleMsgSysSetStatus", handleMsgSysSetStatus}, + {"handleMsgSysEcho", handleMsgSysEcho}, + {"handleMsgMhfUseUdShopCoin", handleMsgMhfUseUdShopCoin}, + {"handleMsgMhfEnterTournamentQuest", handleMsgMhfEnterTournamentQuest}, + } + + 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_tactics_test.go b/server/channelserver/handlers_tactics_test.go new file mode 100644 index 000000000..d3cb5e73c --- /dev/null +++ b/server/channelserver/handlers_tactics_test.go @@ -0,0 +1,193 @@ +package channelserver + +import ( + "testing" + + "erupe-ce/network/mhfpacket" +) + +func TestHandleMsgMhfGetUdTacticsPoint(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfGetUdTacticsPoint{ + AckHandle: 12345, + } + + handleMsgMhfGetUdTacticsPoint(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 TestHandleMsgMhfAddUdTacticsPoint(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfAddUdTacticsPoint{ + AckHandle: 12345, + } + + handleMsgMhfAddUdTacticsPoint(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 TestHandleMsgMhfGetUdTacticsRewardList(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfGetUdTacticsRewardList{ + AckHandle: 12345, + } + + handleMsgMhfGetUdTacticsRewardList(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 TestHandleMsgMhfGetUdTacticsFollower(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfGetUdTacticsFollower{ + AckHandle: 12345, + } + + handleMsgMhfGetUdTacticsFollower(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 TestHandleMsgMhfGetUdTacticsBonusQuest(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfGetUdTacticsBonusQuest{ + AckHandle: 12345, + } + + handleMsgMhfGetUdTacticsBonusQuest(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 TestHandleMsgMhfGetUdTacticsFirstQuestBonus(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfGetUdTacticsFirstQuestBonus{ + AckHandle: 12345, + } + + handleMsgMhfGetUdTacticsFirstQuestBonus(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 TestHandleMsgMhfGetUdTacticsRemainingPoint(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfGetUdTacticsRemainingPoint{ + AckHandle: 12345, + } + + handleMsgMhfGetUdTacticsRemainingPoint(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 TestHandleMsgMhfGetUdTacticsRanking(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfGetUdTacticsRanking{ + AckHandle: 12345, + } + + handleMsgMhfGetUdTacticsRanking(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 TestHandleMsgMhfSetUdTacticsFollower(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + defer func() { + if r := recover(); r != nil { + t.Errorf("handleMsgMhfSetUdTacticsFollower panicked: %v", r) + } + }() + + handleMsgMhfSetUdTacticsFollower(session, nil) +} + +func TestHandleMsgMhfGetUdTacticsLog(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + defer func() { + if r := recover(); r != nil { + t.Errorf("handleMsgMhfGetUdTacticsLog panicked: %v", r) + } + }() + + handleMsgMhfGetUdTacticsLog(session, nil) +} diff --git a/server/channelserver/handlers_test.go b/server/channelserver/handlers_test.go new file mode 100644 index 000000000..b967320df --- /dev/null +++ b/server/channelserver/handlers_test.go @@ -0,0 +1,268 @@ +package channelserver + +import ( + "testing" + + "erupe-ce/network" +) + +func TestHandlerTableInitialized(t *testing.T) { + if handlerTable == nil { + t.Fatal("handlerTable should be initialized by init()") + } +} + +func TestHandlerTableHasEntries(t *testing.T) { + if len(handlerTable) == 0 { + t.Error("handlerTable should have entries") + } + + // Should have many handlers + if len(handlerTable) < 100 { + t.Errorf("handlerTable has %d entries, expected 100+", len(handlerTable)) + } +} + +func TestHandlerTableSystemPackets(t *testing.T) { + // Test that key system packets have handlers + systemPackets := []network.PacketID{ + network.MSG_HEAD, + network.MSG_SYS_END, + network.MSG_SYS_NOP, + network.MSG_SYS_ACK, + network.MSG_SYS_LOGIN, + network.MSG_SYS_LOGOUT, + network.MSG_SYS_PING, + network.MSG_SYS_TIME, + } + + for _, opcode := range systemPackets { + t.Run(opcode.String(), func(t *testing.T) { + if _, ok := handlerTable[opcode]; !ok { + t.Errorf("handler missing for %s", opcode) + } + }) + } +} + +func TestHandlerTableStagePackets(t *testing.T) { + // Test stage-related packet handlers + stagePackets := []network.PacketID{ + network.MSG_SYS_CREATE_STAGE, + network.MSG_SYS_STAGE_DESTRUCT, + network.MSG_SYS_ENTER_STAGE, + network.MSG_SYS_BACK_STAGE, + network.MSG_SYS_MOVE_STAGE, + network.MSG_SYS_LEAVE_STAGE, + network.MSG_SYS_LOCK_STAGE, + network.MSG_SYS_UNLOCK_STAGE, + } + + for _, opcode := range stagePackets { + t.Run(opcode.String(), func(t *testing.T) { + if _, ok := handlerTable[opcode]; !ok { + t.Errorf("handler missing for stage packet %s", opcode) + } + }) + } +} + +func TestHandlerTableBinaryPackets(t *testing.T) { + // Test binary message handlers + binaryPackets := []network.PacketID{ + network.MSG_SYS_CAST_BINARY, + network.MSG_SYS_CASTED_BINARY, + network.MSG_SYS_SET_STAGE_BINARY, + network.MSG_SYS_GET_STAGE_BINARY, + } + + for _, opcode := range binaryPackets { + t.Run(opcode.String(), func(t *testing.T) { + if _, ok := handlerTable[opcode]; !ok { + t.Errorf("handler missing for binary packet %s", opcode) + } + }) + } +} + +func TestHandlerTableReservedPackets(t *testing.T) { + // Reserved packets should still have handlers (usually no-ops) + reservedPackets := []network.PacketID{ + network.MSG_SYS_reserve01, + network.MSG_SYS_reserve02, + network.MSG_SYS_reserve03, + network.MSG_SYS_reserve04, + network.MSG_SYS_reserve05, + network.MSG_SYS_reserve06, + network.MSG_SYS_reserve07, + } + + for _, opcode := range reservedPackets { + t.Run(opcode.String(), func(t *testing.T) { + if _, ok := handlerTable[opcode]; !ok { + t.Errorf("handler missing for reserved packet %s", opcode) + } + }) + } +} + +func TestHandlerFuncType(t *testing.T) { + // Verify all handlers are valid functions + for opcode, handler := range handlerTable { + if handler == nil { + t.Errorf("handler for %s is nil", opcode) + } + } +} + +func TestHandlerTableObjectPackets(t *testing.T) { + objectPackets := []network.PacketID{ + network.MSG_SYS_ADD_OBJECT, + network.MSG_SYS_DEL_OBJECT, + network.MSG_SYS_DISP_OBJECT, + network.MSG_SYS_HIDE_OBJECT, + } + + for _, opcode := range objectPackets { + t.Run(opcode.String(), func(t *testing.T) { + if _, ok := handlerTable[opcode]; !ok { + t.Errorf("handler missing for object packet %s", opcode) + } + }) + } +} + +func TestHandlerTableClientPackets(t *testing.T) { + clientPackets := []network.PacketID{ + network.MSG_SYS_SET_STATUS, + network.MSG_SYS_HIDE_CLIENT, + network.MSG_SYS_ENUMERATE_CLIENT, + } + + for _, opcode := range clientPackets { + t.Run(opcode.String(), func(t *testing.T) { + if _, ok := handlerTable[opcode]; !ok { + t.Errorf("handler missing for client packet %s", opcode) + } + }) + } +} + +func TestHandlerTableSemaphorePackets(t *testing.T) { + semaphorePackets := []network.PacketID{ + network.MSG_SYS_CREATE_ACQUIRE_SEMAPHORE, + network.MSG_SYS_ACQUIRE_SEMAPHORE, + network.MSG_SYS_RELEASE_SEMAPHORE, + } + + for _, opcode := range semaphorePackets { + t.Run(opcode.String(), func(t *testing.T) { + if _, ok := handlerTable[opcode]; !ok { + t.Errorf("handler missing for semaphore packet %s", opcode) + } + }) + } +} + +func TestHandlerTableMHFPackets(t *testing.T) { + // Test some core MHF packets have handlers + mhfPackets := []network.PacketID{ + network.MSG_MHF_SAVEDATA, + network.MSG_MHF_LOADDATA, + } + + for _, opcode := range mhfPackets { + t.Run(opcode.String(), func(t *testing.T) { + if _, ok := handlerTable[opcode]; !ok { + t.Errorf("handler missing for MHF packet %s", opcode) + } + }) + } +} + +func TestHandlerTableEnumeratePackets(t *testing.T) { + enumPackets := []network.PacketID{ + network.MSG_SYS_ENUMERATE_CLIENT, + network.MSG_SYS_ENUMERATE_STAGE, + } + + for _, opcode := range enumPackets { + t.Run(opcode.String(), func(t *testing.T) { + if _, ok := handlerTable[opcode]; !ok { + t.Errorf("handler missing for enumerate packet %s", opcode) + } + }) + } +} + +func TestHandlerTableLogPackets(t *testing.T) { + logPackets := []network.PacketID{ + network.MSG_SYS_TERMINAL_LOG, + network.MSG_SYS_ISSUE_LOGKEY, + network.MSG_SYS_RECORD_LOG, + } + + for _, opcode := range logPackets { + t.Run(opcode.String(), func(t *testing.T) { + if _, ok := handlerTable[opcode]; !ok { + t.Errorf("handler missing for log packet %s", opcode) + } + }) + } +} + +func TestHandlerTableFilePackets(t *testing.T) { + filePackets := []network.PacketID{ + network.MSG_SYS_GET_FILE, + } + + for _, opcode := range filePackets { + t.Run(opcode.String(), func(t *testing.T) { + if _, ok := handlerTable[opcode]; !ok { + t.Errorf("handler missing for file packet %s", opcode) + } + }) + } +} + +func TestHandlerTableEchoPacket(t *testing.T) { + if _, ok := handlerTable[network.MSG_SYS_ECHO]; !ok { + t.Error("handler missing for MSG_SYS_ECHO") + } +} + +func TestHandlerTableReserveStagePackets(t *testing.T) { + reservePackets := []network.PacketID{ + network.MSG_SYS_RESERVE_STAGE, + network.MSG_SYS_UNRESERVE_STAGE, + network.MSG_SYS_SET_STAGE_PASS, + network.MSG_SYS_WAIT_STAGE_BINARY, + } + + for _, opcode := range reservePackets { + t.Run(opcode.String(), func(t *testing.T) { + if _, ok := handlerTable[opcode]; !ok { + t.Errorf("handler missing for reserve stage packet %s", opcode) + } + }) + } +} + +func TestHandlerTableThresholdPacket(t *testing.T) { + if _, ok := handlerTable[network.MSG_SYS_EXTEND_THRESHOLD]; !ok { + t.Error("handler missing for MSG_SYS_EXTEND_THRESHOLD") + } +} + +func TestHandlerTableNoNilValues(t *testing.T) { + nilCount := 0 + for opcode, handler := range handlerTable { + if handler == nil { + nilCount++ + t.Errorf("nil handler for opcode %s", opcode) + } + } + if nilCount > 0 { + t.Errorf("found %d nil handlers in handlerTable", nilCount) + } +} diff --git a/server/channelserver/handlers_tournament_test.go b/server/channelserver/handlers_tournament_test.go new file mode 100644 index 000000000..ef862f3e3 --- /dev/null +++ b/server/channelserver/handlers_tournament_test.go @@ -0,0 +1,91 @@ +package channelserver + +import ( + "testing" + + "erupe-ce/network/mhfpacket" +) + +func TestHandleMsgMhfInfoTournament_Type0(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfInfoTournament{ + AckHandle: 12345, + Unk0: 0, + } + + handleMsgMhfInfoTournament(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") + } +} + +func TestHandleMsgMhfInfoTournament_Type1(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfInfoTournament{ + AckHandle: 12345, + Unk0: 1, + } + + handleMsgMhfInfoTournament(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") + } +} + +func TestHandleMsgMhfEntryTournament(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfEntryTournament{ + AckHandle: 12345, + } + + handleMsgMhfEntryTournament(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 TestHandleMsgMhfAcquireTournament(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfAcquireTournament{ + AckHandle: 12345, + } + + handleMsgMhfAcquireTournament(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_tower_test.go b/server/channelserver/handlers_tower_test.go new file mode 100644 index 000000000..0d3379211 --- /dev/null +++ b/server/channelserver/handlers_tower_test.go @@ -0,0 +1,156 @@ +package channelserver + +import ( + "testing" + + "erupe-ce/network/mhfpacket" +) + +func TestHandleMsgMhfGetTenrouirai_Type1(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfGetTenrouirai{ + AckHandle: 12345, + Unk0: 1, + } + + handleMsgMhfGetTenrouirai(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") + } +} + +func TestHandleMsgMhfGetTenrouirai_Default(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfGetTenrouirai{ + AckHandle: 12345, + Unk0: 0, + Unk1: 0, + } + + handleMsgMhfGetTenrouirai(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") + } +} + +func TestHandleMsgMhfPostTowerInfo(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfPostTowerInfo{ + AckHandle: 12345, + } + + handleMsgMhfPostTowerInfo(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") + } +} + +func TestHandleMsgMhfPostTenrouirai(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfPostTenrouirai{ + AckHandle: 12345, + } + + handleMsgMhfPostTenrouirai(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") + } +} + +func TestHandleMsgMhfGetBreakSeibatuLevelReward(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfGetBreakSeibatuLevelReward{ + AckHandle: 12345, + } + + handleMsgMhfGetBreakSeibatuLevelReward(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 TestHandleMsgMhfGetWeeklySeibatuRankingReward(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfGetWeeklySeibatuRankingReward{ + AckHandle: 12345, + } + + handleMsgMhfGetWeeklySeibatuRankingReward(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") + } +} + +func TestHandleMsgMhfPresentBox(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfPresentBox{ + AckHandle: 12345, + } + + handleMsgMhfPresentBox(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") + } +} diff --git a/server/channelserver/handlers_users_test.go b/server/channelserver/handlers_users_test.go new file mode 100644 index 000000000..5885781cc --- /dev/null +++ b/server/channelserver/handlers_users_test.go @@ -0,0 +1,128 @@ +package channelserver + +import ( + "testing" + + "erupe-ce/network/mhfpacket" +) + +func TestHandleMsgSysInsertUser(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + // Should not panic (empty handler) + defer func() { + if r := recover(); r != nil { + t.Errorf("handleMsgSysInsertUser panicked: %v", r) + } + }() + + handleMsgSysInsertUser(session, nil) +} + +func TestHandleMsgSysDeleteUser(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + // Should not panic (empty handler) + defer func() { + if r := recover(); r != nil { + t.Errorf("handleMsgSysDeleteUser panicked: %v", r) + } + }() + + handleMsgSysDeleteUser(session, nil) +} + +func TestHandleMsgSysNotifyUserBinary(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + // Should not panic (empty handler) + defer func() { + if r := recover(); r != nil { + t.Errorf("handleMsgSysNotifyUserBinary panicked: %v", r) + } + }() + + handleMsgSysNotifyUserBinary(session, nil) +} + +func TestHandleMsgSysGetUserBinary_FromCache(t *testing.T) { + server := createMockServer() + server.userBinaryParts = make(map[userBinaryPartID][]byte) + session := createMockSession(1, server) + + // Pre-populate cache + key := userBinaryPartID{charID: 100, index: 1} + server.userBinaryParts[key] = []byte{0x01, 0x02, 0x03, 0x04} + + pkt := &mhfpacket.MsgSysGetUserBinary{ + AckHandle: 12345, + CharID: 100, + BinaryType: 1, + } + + handleMsgSysGetUserBinary(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") + } +} + +func TestHandleMsgSysGetUserBinary_NotInCache(t *testing.T) { + server := createMockServer() + server.userBinaryParts = make(map[userBinaryPartID][]byte) + session := createMockSession(1, server) + + // Don't populate cache - will fall back to DB (which is nil in test) + pkt := &mhfpacket.MsgSysGetUserBinary{ + AckHandle: 12345, + CharID: 100, + BinaryType: 1, + } + + // This will panic when trying to access nil db, which is expected + // in the test environment without database setup + defer func() { + if r := recover(); r != nil { + // Expected - no database in test + t.Log("Expected panic due to nil database in test") + } + }() + + handleMsgSysGetUserBinary(session, pkt) +} + +func TestUserBinaryPartID_AsMapKey(t *testing.T) { + // Test that userBinaryPartID works as map key + parts := make(map[userBinaryPartID][]byte) + + key1 := userBinaryPartID{charID: 1, index: 0} + key2 := userBinaryPartID{charID: 1, index: 1} + key3 := userBinaryPartID{charID: 2, index: 0} + + parts[key1] = []byte{0x01} + parts[key2] = []byte{0x02} + parts[key3] = []byte{0x03} + + if len(parts) != 3 { + t.Errorf("Expected 3 parts, got %d", len(parts)) + } + + if parts[key1][0] != 0x01 { + t.Error("Key1 data mismatch") + } + if parts[key2][0] != 0x02 { + t.Error("Key2 data mismatch") + } + if parts[key3][0] != 0x03 { + t.Error("Key3 data mismatch") + } +} diff --git a/server/channelserver/handlers_util_test.go b/server/channelserver/handlers_util_test.go new file mode 100644 index 000000000..4c47f326d --- /dev/null +++ b/server/channelserver/handlers_util_test.go @@ -0,0 +1,208 @@ +package channelserver + +import ( + "testing" +) + +func TestStubEnumerateNoResults(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + // Call stubEnumerateNoResults - it queues a packet + stubEnumerateNoResults(session, 12345) + + // Verify packet was queued + select { + case pkt := <-session.sendPackets: + if len(pkt.data) == 0 { + t.Error("Packet data should not be empty") + } + default: + t.Error("No packet was queued") + } +} + +func TestDoAckBufSucceed(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + testData := []byte{0x01, 0x02, 0x03, 0x04} + doAckBufSucceed(session, 12345, testData) + + // Verify packet was queued + select { + case pkt := <-session.sendPackets: + if len(pkt.data) == 0 { + t.Error("Packet data should not be empty") + } + default: + t.Error("No packet was queued") + } +} + +func TestDoAckBufFail(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + testData := []byte{0x01, 0x02, 0x03, 0x04} + doAckBufFail(session, 12345, testData) + + // Verify packet was queued + select { + case pkt := <-session.sendPackets: + if len(pkt.data) == 0 { + t.Error("Packet data should not be empty") + } + default: + t.Error("No packet was queued") + } +} + +func TestDoAckSimpleSucceed(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + testData := []byte{0x00, 0x00, 0x00, 0x00} + doAckSimpleSucceed(session, 12345, testData) + + // Verify packet was queued + select { + case pkt := <-session.sendPackets: + if len(pkt.data) == 0 { + t.Error("Packet data should not be empty") + } + default: + t.Error("No packet was queued") + } +} + +func TestDoAckSimpleFail(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + testData := []byte{0x00, 0x00, 0x00, 0x00} + doAckSimpleFail(session, 12345, testData) + + // Verify packet was queued + select { + case pkt := <-session.sendPackets: + if len(pkt.data) == 0 { + t.Error("Packet data should not be empty") + } + default: + t.Error("No packet was queued") + } +} + +func TestDoAck_EmptyData(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + // Should work with empty data + doAckBufSucceed(session, 0, []byte{}) + + select { + case pkt := <-session.sendPackets: + // Empty data is valid + _ = pkt + default: + t.Error("No packet was queued with empty data") + } +} + +func TestDoAck_NilData(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + // Should work with nil data + doAckBufSucceed(session, 0, nil) + + select { + case pkt := <-session.sendPackets: + // Nil data is valid + _ = pkt + default: + t.Error("No packet was queued with nil data") + } +} + +func TestDoAck_LargeData(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + // Test with large data + largeData := make([]byte, 65536) + for i := range largeData { + largeData[i] = byte(i % 256) + } + + doAckBufSucceed(session, 99999, largeData) + + select { + case pkt := <-session.sendPackets: + if len(pkt.data) == 0 { + t.Error("Packet data should not be empty for large data") + } + default: + t.Error("No packet was queued with large data") + } +} + +func TestDoAck_AckHandleZero(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + // Test with ack handle 0 + doAckSimpleSucceed(session, 0, []byte{0x00}) + + select { + case pkt := <-session.sendPackets: + _ = pkt + default: + t.Error("No packet was queued with zero ack handle") + } +} + +func TestDoAck_AckHandleMax(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + // Test with max uint32 ack handle + doAckSimpleSucceed(session, 0xFFFFFFFF, []byte{0x00}) + + select { + case pkt := <-session.sendPackets: + _ = pkt + default: + t.Error("No packet was queued with max ack handle") + } +} + +// Test that handlers don't panic with empty packets +func TestEmptyHandlers(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + tests := []struct { + name string + handler func(s *Session, p interface{}) + }{ + {"handleMsgHead", func(s *Session, p interface{}) { handleMsgHead(s, nil) }}, + {"handleMsgSysExtendThreshold", func(s *Session, p interface{}) { handleMsgSysExtendThreshold(s, nil) }}, + {"handleMsgSysEnd", func(s *Session, p interface{}) { handleMsgSysEnd(s, nil) }}, + {"handleMsgSysNop", func(s *Session, p interface{}) { handleMsgSysNop(s, nil) }}, + {"handleMsgSysAck", func(s *Session, p interface{}) { handleMsgSysAck(s, nil) }}, + {"handleMsgSysAuthData", func(s *Session, p interface{}) { handleMsgSysAuthData(s, nil) }}, + } + + 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/sys_language_test.go b/server/channelserver/sys_language_test.go new file mode 100644 index 000000000..df3bba9b7 --- /dev/null +++ b/server/channelserver/sys_language_test.go @@ -0,0 +1,94 @@ +package channelserver + +import ( + "testing" + + _config "erupe-ce/config" +) + +func TestGetLangStrings_English(t *testing.T) { + server := &Server{ + erupeConfig: &_config.Config{ + Language: "en", + }, + } + + lang := getLangStrings(server) + + if lang.language != "English" { + t.Errorf("language = %q, want %q", lang.language, "English") + } + + // Verify key strings are not empty + if lang.cafe.reset == "" { + t.Error("cafe.reset should not be empty") + } + if lang.commands.disabled == "" { + t.Error("commands.disabled should not be empty") + } + if lang.commands.reload == "" { + t.Error("commands.reload should not be empty") + } + if lang.commands.ravi.noCommand == "" { + t.Error("commands.ravi.noCommand should not be empty") + } + if lang.guild.invite.title == "" { + t.Error("guild.invite.title should not be empty") + } +} + +func TestGetLangStrings_Japanese(t *testing.T) { + server := &Server{ + erupeConfig: &_config.Config{ + Language: "jp", + }, + } + + lang := getLangStrings(server) + + if lang.language != "日本語" { + t.Errorf("language = %q, want %q", lang.language, "日本語") + } + + // Verify Japanese strings are different from English + enServer := &Server{ + erupeConfig: &_config.Config{ + Language: "en", + }, + } + enLang := getLangStrings(enServer) + + if lang.commands.reload == enLang.commands.reload { + t.Error("Japanese commands.reload should be different from English") + } +} + +func TestGetLangStrings_DefaultToEnglish(t *testing.T) { + server := &Server{ + erupeConfig: &_config.Config{ + Language: "unknown_language", + }, + } + + lang := getLangStrings(server) + + // Unknown language should default to English + if lang.language != "English" { + t.Errorf("Unknown language should default to English, got %q", lang.language) + } +} + +func TestGetLangStrings_EmptyLanguage(t *testing.T) { + server := &Server{ + erupeConfig: &_config.Config{ + Language: "", + }, + } + + lang := getLangStrings(server) + + // Empty language should default to English + if lang.language != "English" { + t.Errorf("Empty language should default to English, got %q", lang.language) + } +} diff --git a/server/channelserver/sys_object_test.go b/server/channelserver/sys_object_test.go new file mode 100644 index 000000000..8f9fcb1a9 --- /dev/null +++ b/server/channelserver/sys_object_test.go @@ -0,0 +1,322 @@ +package channelserver + +import ( + "sync" + "testing" +) + +func TestObjectStruct(t *testing.T) { + obj := &Object{ + id: 12345, + ownerCharID: 67890, + x: 100.5, + y: 50.25, + z: -10.0, + } + + if obj.id != 12345 { + t.Errorf("Object id = %d, want 12345", obj.id) + } + if obj.ownerCharID != 67890 { + t.Errorf("Object ownerCharID = %d, want 67890", obj.ownerCharID) + } + if obj.x != 100.5 { + t.Errorf("Object x = %f, want 100.5", obj.x) + } + if obj.y != 50.25 { + t.Errorf("Object y = %f, want 50.25", obj.y) + } + if obj.z != -10.0 { + t.Errorf("Object z = %f, want -10.0", obj.z) + } +} + +func TestObjectRWMutex(t *testing.T) { + obj := &Object{ + id: 1, + ownerCharID: 100, + x: 0, + y: 0, + z: 0, + } + + // Test read lock + obj.RLock() + _ = obj.x + obj.RUnlock() + + // Test write lock + obj.Lock() + obj.x = 100.0 + obj.Unlock() + + if obj.x != 100.0 { + t.Errorf("Object x = %f, want 100.0 after write", obj.x) + } +} + +func TestObjectConcurrentAccess(t *testing.T) { + obj := &Object{ + id: 1, + ownerCharID: 100, + x: 0, + y: 0, + z: 0, + } + + var wg sync.WaitGroup + + // Concurrent writers + for i := 0; i < 10; i++ { + wg.Add(1) + go func(val float32) { + defer wg.Done() + for j := 0; j < 100; j++ { + obj.Lock() + obj.x = val + obj.y = val + obj.z = val + obj.Unlock() + } + }(float32(i)) + } + + // Concurrent readers + for i := 0; i < 5; i++ { + wg.Add(1) + go func() { + defer wg.Done() + for j := 0; j < 100; j++ { + obj.RLock() + _ = obj.x + _ = obj.y + _ = obj.z + obj.RUnlock() + } + }() + } + + wg.Wait() +} + +func TestStageBinaryKeyStruct(t *testing.T) { + key1 := stageBinaryKey{id0: 1, id1: 2} + key2 := stageBinaryKey{id0: 1, id1: 3} + key3 := stageBinaryKey{id0: 1, id1: 2} + + // Different keys + if key1 == key2 { + t.Error("key1 and key2 should be different") + } + + // Same keys + if key1 != key3 { + t.Error("key1 and key3 should be equal") + } +} + +func TestStageBinaryKeyAsMapKey(t *testing.T) { + data := make(map[stageBinaryKey][]byte) + + key1 := stageBinaryKey{id0: 0, id1: 0} + key2 := stageBinaryKey{id0: 0, id1: 1} + key3 := stageBinaryKey{id0: 1, id1: 0} + + data[key1] = []byte{0x01} + data[key2] = []byte{0x02} + data[key3] = []byte{0x03} + + if len(data) != 3 { + t.Errorf("Expected 3 entries, got %d", len(data)) + } + + if data[key1][0] != 0x01 { + t.Errorf("data[key1] = 0x%02X, want 0x01", data[key1][0]) + } + if data[key2][0] != 0x02 { + t.Errorf("data[key2] = 0x%02X, want 0x02", data[key2][0]) + } + if data[key3][0] != 0x03 { + t.Errorf("data[key3] = 0x%02X, want 0x03", data[key3][0]) + } +} + +func TestNewStageDefaults(t *testing.T) { + stage := NewStage("test_stage_001") + + if stage.id != "test_stage_001" { + t.Errorf("stage.id = %s, want test_stage_001", stage.id) + } + if stage.maxPlayers != 127 { + t.Errorf("stage.maxPlayers = %d, want 127 (default)", stage.maxPlayers) + } + if stage.objectIndex != 0 { + t.Errorf("stage.objectIndex = %d, want 0", stage.objectIndex) + } + if stage.clients == nil { + t.Error("stage.clients should be initialized") + } + if stage.reservedClientSlots == nil { + t.Error("stage.reservedClientSlots should be initialized") + } + if stage.objects == nil { + t.Error("stage.objects should be initialized") + } + if stage.rawBinaryData == nil { + t.Error("stage.rawBinaryData should be initialized") + } + if stage.host != nil { + t.Error("stage.host should be nil initially") + } + if stage.password != "" { + t.Errorf("stage.password should be empty, got %s", stage.password) + } +} + +func TestStageReservedClientSlots(t *testing.T) { + stage := NewStage("test") + + // Reserve some slots + stage.reservedClientSlots[100] = true + stage.reservedClientSlots[200] = false // ready status doesn't matter for presence + stage.reservedClientSlots[300] = true + + if len(stage.reservedClientSlots) != 3 { + t.Errorf("reservedClientSlots count = %d, want 3", len(stage.reservedClientSlots)) + } + + // Check ready status + if !stage.reservedClientSlots[100] { + t.Error("charID 100 should be ready") + } + if stage.reservedClientSlots[200] { + t.Error("charID 200 should not be ready") + } +} + +func TestStageRawBinaryData(t *testing.T) { + stage := NewStage("test") + + key := stageBinaryKey{id0: 5, id1: 10} + data := []byte{0xDE, 0xAD, 0xBE, 0xEF} + + stage.rawBinaryData[key] = data + + retrieved := stage.rawBinaryData[key] + if len(retrieved) != 4 { + t.Fatalf("retrieved data len = %d, want 4", len(retrieved)) + } + if retrieved[0] != 0xDE || retrieved[3] != 0xEF { + t.Error("retrieved data doesn't match stored data") + } +} + +func TestStageObjects(t *testing.T) { + stage := NewStage("test") + + obj := &Object{ + id: 1, + ownerCharID: 12345, + x: 100.0, + y: 200.0, + z: 300.0, + } + + stage.objects[obj.id] = obj + + if len(stage.objects) != 1 { + t.Errorf("objects count = %d, want 1", len(stage.objects)) + } + + retrieved := stage.objects[obj.id] + if retrieved.ownerCharID != 12345 { + t.Errorf("retrieved object ownerCharID = %d, want 12345", retrieved.ownerCharID) + } +} + +func TestStageHost(t *testing.T) { + server := createMockServer() + stage := NewStage("test") + + // Set host + host := createMockSession(100, server) + stage.host = host + + if stage.host != host { + t.Error("stage host not set correctly") + } + if stage.host.charID != 100 { + t.Errorf("stage host charID = %d, want 100", stage.host.charID) + } +} + +func TestStagePassword(t *testing.T) { + stage := NewStage("test") + + // Set password + stage.password = "secret123" + + if stage.password != "secret123" { + t.Errorf("stage password = %s, want secret123", stage.password) + } +} + +func TestStageMaxPlayers(t *testing.T) { + stage := NewStage("test") + + // Change max players + stage.maxPlayers = 16 + + if stage.maxPlayers != 16 { + t.Errorf("stage maxPlayers = %d, want 16", stage.maxPlayers) + } +} + +func TestStageConcurrentClientAccess(t *testing.T) { + server := createMockServer() + stage := NewStage("test") + + var wg sync.WaitGroup + + // Concurrent client additions + for i := 0; i < 10; i++ { + wg.Add(1) + go func(id int) { + defer wg.Done() + for j := 0; j < 10; j++ { + session := createMockSession(uint32(id*100+j), server) + stage.Lock() + stage.clients[session] = session.charID + stage.Unlock() + + stage.Lock() + delete(stage.clients, session) + stage.Unlock() + } + }(i) + } + + // Concurrent reads + for i := 0; i < 5; i++ { + wg.Add(1) + go func() { + defer wg.Done() + for j := 0; j < 50; j++ { + stage.RLock() + _ = len(stage.clients) + stage.RUnlock() + } + }() + } + + wg.Wait() +} + +func TestStageBroadcastMHF_EmptyStage(t *testing.T) { + stage := NewStage("test") + pkt := &mockPacket{opcode: 0x1234} + + // Should not panic with empty stage + stage.BroadcastMHF(pkt, nil) +} + diff --git a/server/channelserver/sys_semaphore_test.go b/server/channelserver/sys_semaphore_test.go new file mode 100644 index 000000000..f0e029abb --- /dev/null +++ b/server/channelserver/sys_semaphore_test.go @@ -0,0 +1,276 @@ +package channelserver + +import ( + "sync" + "testing" +) + +func TestNewSemaphore(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + sema := NewSemaphore(session, "test_semaphore", 16) + + if sema == nil { + t.Fatal("NewSemaphore() returned nil") + } + if sema.name != "test_semaphore" { + t.Errorf("name = %s, want test_semaphore", sema.name) + } + if sema.maxPlayers != 16 { + t.Errorf("maxPlayers = %d, want 16", sema.maxPlayers) + } + if sema.clients == nil { + t.Error("clients map should be initialized") + } + if sema.host != session { + t.Error("host should be set to the creating session") + } +} + +func TestNewSemaphoreIDIncrement(t *testing.T) { + server := createMockServer() + session1 := createMockSession(1, server) + session2 := createMockSession(2, server) + session3 := createMockSession(3, server) + + sema1 := NewSemaphore(session1, "sema1", 4) + sema2 := NewSemaphore(session2, "sema2", 4) + sema3 := NewSemaphore(session3, "sema3", 4) + + // IDs should be set (may or may not be unique depending on session state) + if sema1.id == 0 && sema2.id == 0 && sema3.id == 0 { + t.Error("at least some semaphore IDs should be non-zero") + } +} + +func TestSemaphoreClients(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + sema := NewSemaphore(session, "test", 4) + + session1 := createMockSession(100, server) + session2 := createMockSession(200, server) + + // Add clients + sema.clients[session1] = session1.charID + sema.clients[session2] = session2.charID + + if len(sema.clients) != 2 { + t.Errorf("clients count = %d, want 2", len(sema.clients)) + } + + // Verify client IDs + if sema.clients[session1] != 100 { + t.Errorf("clients[session1] = %d, want 100", sema.clients[session1]) + } + if sema.clients[session2] != 200 { + t.Errorf("clients[session2] = %d, want 200", sema.clients[session2]) + } +} + +func TestSemaphoreRemoveClient(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + sema := NewSemaphore(session, "test", 4) + + clientSession := createMockSession(100, server) + sema.clients[clientSession] = clientSession.charID + + // Remove client + delete(sema.clients, clientSession) + + if len(sema.clients) != 0 { + t.Errorf("clients count = %d, want 0 after delete", len(sema.clients)) + } +} + +func TestSemaphoreMaxPlayers(t *testing.T) { + tests := []struct { + name string + maxPlayers uint16 + }{ + {"quest party", 4}, + {"small event", 16}, + {"raviente", 32}, + {"large event", 64}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + sema := NewSemaphore(session, tt.name, tt.maxPlayers) + + if sema.maxPlayers != tt.maxPlayers { + t.Errorf("maxPlayers = %d, want %d", sema.maxPlayers, tt.maxPlayers) + } + }) + } +} + +func TestSemaphoreBroadcastMHF(t *testing.T) { + server := createMockServer() + hostSession := createMockSession(1, server) + sema := NewSemaphore(hostSession, "test", 4) + + session1 := createMockSession(100, server) + session2 := createMockSession(200, server) + session3 := createMockSession(300, server) + + sema.clients[session1] = session1.charID + sema.clients[session2] = session2.charID + sema.clients[session3] = session3.charID + + pkt := &mockPacket{opcode: 0x1234} + + // Broadcast excluding session1 + sema.BroadcastMHF(pkt, session1) + + // session2 and session3 should receive + select { + case data := <-session2.sendPackets: + if len(data.data) == 0 { + t.Error("session2 received empty data") + } + default: + t.Error("session2 did not receive broadcast") + } + + select { + case data := <-session3.sendPackets: + if len(data.data) == 0 { + t.Error("session3 received empty data") + } + default: + t.Error("session3 did not receive broadcast") + } + + // session1 should NOT receive (it was ignored) + select { + case <-session1.sendPackets: + t.Error("session1 should not receive broadcast (it was ignored)") + default: + // Expected - no data for session1 + } +} + +func TestSemaphoreBroadcastToAll(t *testing.T) { + server := createMockServer() + hostSession := createMockSession(1, server) + sema := NewSemaphore(hostSession, "test", 4) + + session1 := createMockSession(100, server) + session2 := createMockSession(200, server) + + sema.clients[session1] = session1.charID + sema.clients[session2] = session2.charID + + pkt := &mockPacket{opcode: 0x1234} + + // Broadcast to all (nil ignored session) + sema.BroadcastMHF(pkt, nil) + + // Both should receive + count := 0 + select { + case <-session1.sendPackets: + count++ + default: + } + select { + case <-session2.sendPackets: + count++ + default: + } + + if count != 2 { + t.Errorf("expected 2 broadcasts, got %d", count) + } +} + +func TestSemaphoreRWMutex(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + sema := NewSemaphore(session, "test", 4) + + // Test that RWMutex works + sema.RLock() + _ = len(sema.clients) // Read operation + sema.RUnlock() + + sema.Lock() + sema.clients[createMockSession(100, server)] = 100 // Write operation + sema.Unlock() +} + +func TestSemaphoreConcurrentAccess(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + sema := NewSemaphore(session, "test", 100) + + 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 < 100; j++ { + s := createMockSession(uint32(id*100+j), server) + sema.Lock() + sema.clients[s] = s.charID + sema.Unlock() + + sema.Lock() + delete(sema.clients, s) + sema.Unlock() + } + }(i) + } + + // Concurrent readers + for i := 0; i < 5; i++ { + wg.Add(1) + go func() { + defer wg.Done() + for j := 0; j < 100; j++ { + sema.RLock() + _ = len(sema.clients) + sema.RUnlock() + } + }() + } + + wg.Wait() +} + +func TestSemaphoreEmptyBroadcast(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + sema := NewSemaphore(session, "test", 4) + + pkt := &mockPacket{opcode: 0x1234} + + // Should not panic with no clients + sema.BroadcastMHF(pkt, nil) +} + +func TestSemaphoreNameString(t *testing.T) { + server := createMockServer() + + tests := []string{ + "quest_001", + "raviente_phase1", + "tournament_round3", + "diva_defense", + } + + for _, id := range tests { + session := createMockSession(1, server) + sema := NewSemaphore(session, id, 4) + if sema.name != id { + t.Errorf("name = %s, want %s", sema.name, id) + } + } +} diff --git a/server/channelserver/sys_stage_test.go b/server/channelserver/sys_stage_test.go new file mode 100644 index 000000000..be34a292c --- /dev/null +++ b/server/channelserver/sys_stage_test.go @@ -0,0 +1,290 @@ +package channelserver + +import ( + "sync" + "testing" +) + +func TestStageBroadcastMHF(t *testing.T) { + stage := NewStage("test_stage") + server := createMockServer() + + // Add some sessions + session1 := createMockSession(1, server) + session2 := createMockSession(2, server) + session3 := createMockSession(3, server) + + stage.clients[session1] = session1.charID + stage.clients[session2] = session2.charID + stage.clients[session3] = session3.charID + + pkt := &mockPacket{opcode: 0x1234} + + // Should not panic + stage.BroadcastMHF(pkt, session1) + + // Verify session2 and session3 received data + select { + case data := <-session2.sendPackets: + if len(data.data) == 0 { + t.Error("session2 received empty data") + } + default: + t.Error("session2 did not receive data") + } + + select { + case data := <-session3.sendPackets: + if len(data.data) == 0 { + t.Error("session3 received empty data") + } + default: + t.Error("session3 did not receive data") + } +} + +func TestStageBroadcastMHF_NilClientContext(t *testing.T) { + stage := NewStage("test_stage") + server := createMockServer() + + session1 := createMockSession(1, server) + session2 := createMockSession(2, server) + session2.clientContext = nil // Simulate corrupted session + + stage.clients[session1] = session1.charID + stage.clients[session2] = session2.charID + + pkt := &mockPacket{opcode: 0x1234} + + // This should panic with the current implementation + defer func() { + if r := recover(); r != nil { + t.Logf("Caught expected panic: %v", r) + // Test passes - we've confirmed the bug exists + } else { + t.Log("No panic occurred - either the bug is fixed or test is wrong") + } + }() + + stage.BroadcastMHF(pkt, nil) +} + +// TestStageBroadcastMHF_ConcurrentModificationWithLock tests that proper locking +// prevents the race condition between BroadcastMHF and session removal +func TestStageBroadcastMHF_ConcurrentModificationWithLock(t *testing.T) { + stage := NewStage("test_stage") + server := createMockServer() + + // Create many sessions + sessions := make([]*Session, 100) + for i := range sessions { + sessions[i] = createMockSession(uint32(i), server) + stage.clients[sessions[i]] = sessions[i].charID + } + + pkt := &mockPacket{opcode: 0x1234} + + var wg sync.WaitGroup + + // Start goroutines that broadcast + for i := 0; i < 10; i++ { + wg.Add(1) + go func() { + defer wg.Done() + for j := 0; j < 100; j++ { + stage.BroadcastMHF(pkt, nil) + } + }() + } + + // Start goroutines that remove sessions WITH proper locking + // This simulates the fixed logoutPlayer behavior + for i := 0; i < 10; i++ { + wg.Add(1) + idx := i * 10 + go func(startIdx int) { + defer wg.Done() + for j := 0; j < 10; j++ { + sessionIdx := startIdx + j + if sessionIdx < len(sessions) { + // Fixed: modifying stage.clients WITH lock + stage.Lock() + delete(stage.clients, sessions[sessionIdx]) + stage.Unlock() + } + } + }(idx) + } + + wg.Wait() +} + +// TestStageBroadcastMHF_RaceDetectorWithLock verifies no race when +// modifications are done with proper locking +func TestStageBroadcastMHF_RaceDetectorWithLock(t *testing.T) { + stage := NewStage("test_stage") + server := createMockServer() + + session1 := createMockSession(1, server) + session2 := createMockSession(2, server) + + stage.clients[session1] = session1.charID + stage.clients[session2] = session2.charID + + pkt := &mockPacket{opcode: 0x1234} + + var wg sync.WaitGroup + + // Goroutine 1: Continuously broadcast + wg.Add(1) + go func() { + defer wg.Done() + for i := 0; i < 1000; i++ { + stage.BroadcastMHF(pkt, nil) + } + }() + + // Goroutine 2: Add and remove sessions WITH proper locking + // This simulates the fixed logoutPlayer behavior + wg.Add(1) + go func() { + defer wg.Done() + for i := 0; i < 1000; i++ { + newSession := createMockSession(uint32(100+i), server) + // Add WITH lock (fixed) + stage.Lock() + stage.clients[newSession] = newSession.charID + stage.Unlock() + // Remove WITH lock (fixed) + stage.Lock() + delete(stage.clients, newSession) + stage.Unlock() + } + }() + + wg.Wait() +} + +// TestNewStageBasic verifies Stage creation +func TestNewStageBasic(t *testing.T) { + stageID := "test_stage_001" + stage := NewStage(stageID) + + if stage == nil { + t.Fatal("NewStage() returned nil") + } + if stage.id != stageID { + t.Errorf("stage.id = %s, want %s", stage.id, stageID) + } + if stage.clients == nil { + t.Error("stage.clients should not be nil") + } + if stage.reservedClientSlots == nil { + t.Error("stage.reservedClientSlots should not be nil") + } + if stage.objects == nil { + t.Error("stage.objects should not be nil") + } +} + +// TestStageClientCount tests client counting +func TestStageClientCount(t *testing.T) { + stage := NewStage("test_stage") + server := createMockServer() + + if len(stage.clients) != 0 { + t.Errorf("initial client count = %d, want 0", len(stage.clients)) + } + + // Add clients + session1 := createMockSession(1, server) + session2 := createMockSession(2, server) + + stage.clients[session1] = session1.charID + if len(stage.clients) != 1 { + t.Errorf("client count after 1 add = %d, want 1", len(stage.clients)) + } + + stage.clients[session2] = session2.charID + if len(stage.clients) != 2 { + t.Errorf("client count after 2 adds = %d, want 2", len(stage.clients)) + } + + // Remove a client + delete(stage.clients, session1) + if len(stage.clients) != 1 { + t.Errorf("client count after 1 remove = %d, want 1", len(stage.clients)) + } +} + +// TestStageLockUnlock tests stage locking +func TestStageLockUnlock(t *testing.T) { + stage := NewStage("test_stage") + + // Test lock/unlock without deadlock + stage.Lock() + stage.password = "test" + stage.Unlock() + + stage.RLock() + password := stage.password + stage.RUnlock() + + if password != "test" { + t.Error("stage password should be 'test'") + } +} + +// TestStageHostSession tests host session tracking +func TestStageHostSession(t *testing.T) { + stage := NewStage("test_stage") + server := createMockServer() + session := createMockSession(1, server) + + if stage.host != nil { + t.Error("initial host should be nil") + } + + stage.host = session + if stage.host == nil { + t.Error("host should not be nil after setting") + } + if stage.host.charID != 1 { + t.Errorf("host.charID = %d, want 1", stage.host.charID) + } +} + +// TestStageMultipleClients tests stage with multiple clients +func TestStageMultipleClients(t *testing.T) { + stage := NewStage("test_stage") + server := createMockServer() + + // Add many clients + sessions := make([]*Session, 10) + for i := range sessions { + sessions[i] = createMockSession(uint32(i+1), server) + stage.clients[sessions[i]] = sessions[i].charID + } + + if len(stage.clients) != 10 { + t.Errorf("client count = %d, want 10", len(stage.clients)) + } + + // Verify each client is tracked + for _, s := range sessions { + if _, ok := stage.clients[s]; !ok { + t.Errorf("session with charID %d not found in stage", s.charID) + } + } +} + +// TestStageNewMaxPlayers tests default max players +func TestStageNewMaxPlayers(t *testing.T) { + stage := NewStage("test_stage") + + // Default max players is 127 + if stage.maxPlayers != 127 { + t.Errorf("initial maxPlayers = %d, want 127", stage.maxPlayers) + } +} + diff --git a/server/channelserver/sys_time_test.go b/server/channelserver/sys_time_test.go new file mode 100644 index 000000000..6fbb5c645 --- /dev/null +++ b/server/channelserver/sys_time_test.go @@ -0,0 +1,167 @@ +package channelserver + +import ( + "testing" + "time" +) + +func TestTimeAdjusted(t *testing.T) { + result := TimeAdjusted() + + // Should return a time in UTC+9 timezone + _, offset := result.Zone() + expectedOffset := 9 * 60 * 60 // 9 hours in seconds + if offset != expectedOffset { + t.Errorf("TimeAdjusted() zone offset = %d, want %d (UTC+9)", offset, expectedOffset) + } + + // The time should be close to current time (within a few seconds) + now := time.Now() + diff := result.Sub(now.In(time.FixedZone("UTC+9", 9*60*60))) + if diff < -time.Second || diff > time.Second { + t.Errorf("TimeAdjusted() time differs from expected by %v", diff) + } +} + +func TestTimeMidnight(t *testing.T) { + midnight := TimeMidnight() + + // Should be at midnight (hour=0, minute=0, second=0, nanosecond=0) + if midnight.Hour() != 0 { + t.Errorf("TimeMidnight() hour = %d, want 0", midnight.Hour()) + } + if midnight.Minute() != 0 { + t.Errorf("TimeMidnight() minute = %d, want 0", midnight.Minute()) + } + if midnight.Second() != 0 { + t.Errorf("TimeMidnight() second = %d, want 0", midnight.Second()) + } + if midnight.Nanosecond() != 0 { + t.Errorf("TimeMidnight() nanosecond = %d, want 0", midnight.Nanosecond()) + } + + // Should be in UTC+9 timezone + _, offset := midnight.Zone() + expectedOffset := 9 * 60 * 60 + if offset != expectedOffset { + t.Errorf("TimeMidnight() zone offset = %d, want %d (UTC+9)", offset, expectedOffset) + } +} + +func TestTimeWeekStart(t *testing.T) { + weekStart := TimeWeekStart() + + // Should be on Monday (weekday = 1) + if weekStart.Weekday() != time.Monday { + t.Errorf("TimeWeekStart() weekday = %v, want Monday", weekStart.Weekday()) + } + + // Should be at midnight + if weekStart.Hour() != 0 || weekStart.Minute() != 0 || weekStart.Second() != 0 { + t.Errorf("TimeWeekStart() should be at midnight, got %02d:%02d:%02d", + weekStart.Hour(), weekStart.Minute(), weekStart.Second()) + } + + // Should be in UTC+9 timezone + _, offset := weekStart.Zone() + expectedOffset := 9 * 60 * 60 + if offset != expectedOffset { + t.Errorf("TimeWeekStart() zone offset = %d, want %d (UTC+9)", offset, expectedOffset) + } + + // Week start should be before or equal to current midnight + midnight := TimeMidnight() + if weekStart.After(midnight) { + t.Errorf("TimeWeekStart() %v should be <= current midnight %v", weekStart, midnight) + } +} + +func TestTimeWeekNext(t *testing.T) { + weekStart := TimeWeekStart() + weekNext := TimeWeekNext() + + // TimeWeekNext should be exactly 7 days after TimeWeekStart + expectedNext := weekStart.Add(time.Hour * 24 * 7) + if !weekNext.Equal(expectedNext) { + t.Errorf("TimeWeekNext() = %v, want %v (7 days after WeekStart)", weekNext, expectedNext) + } + + // Should also be on Monday + if weekNext.Weekday() != time.Monday { + t.Errorf("TimeWeekNext() weekday = %v, want Monday", weekNext.Weekday()) + } + + // Should be at midnight + if weekNext.Hour() != 0 || weekNext.Minute() != 0 || weekNext.Second() != 0 { + t.Errorf("TimeWeekNext() should be at midnight, got %02d:%02d:%02d", + weekNext.Hour(), weekNext.Minute(), weekNext.Second()) + } + + // Should be in the future relative to week start + if !weekNext.After(weekStart) { + t.Errorf("TimeWeekNext() %v should be after TimeWeekStart() %v", weekNext, weekStart) + } +} + +func TestTimeWeekStartSundayEdge(t *testing.T) { + // When today is Sunday, the calculation should go back to last Monday + // This is tested indirectly by verifying the weekday is always Monday + weekStart := TimeWeekStart() + + // Regardless of what day it is now, week start should be Monday + if weekStart.Weekday() != time.Monday { + t.Errorf("TimeWeekStart() on any day should return Monday, got %v", weekStart.Weekday()) + } +} + +func TestTimeMidnightSameDay(t *testing.T) { + adjusted := TimeAdjusted() + midnight := TimeMidnight() + + // Midnight should be on the same day (year, month, day) + if midnight.Year() != adjusted.Year() || + midnight.Month() != adjusted.Month() || + midnight.Day() != adjusted.Day() { + t.Errorf("TimeMidnight() date = %v, want same day as TimeAdjusted() %v", + midnight.Format("2006-01-02"), adjusted.Format("2006-01-02")) + } +} + +func TestTimeWeekDuration(t *testing.T) { + weekStart := TimeWeekStart() + weekNext := TimeWeekNext() + + // Duration between week boundaries should be exactly 7 days + duration := weekNext.Sub(weekStart) + expectedDuration := time.Hour * 24 * 7 + + if duration != expectedDuration { + t.Errorf("Duration between WeekStart and WeekNext = %v, want %v", duration, expectedDuration) + } +} + +func TestTimeZoneConsistency(t *testing.T) { + adjusted := TimeAdjusted() + midnight := TimeMidnight() + weekStart := TimeWeekStart() + weekNext := TimeWeekNext() + + // All times should be in the same timezone (UTC+9) + times := []struct { + name string + time time.Time + }{ + {"TimeAdjusted", adjusted}, + {"TimeMidnight", midnight}, + {"TimeWeekStart", weekStart}, + {"TimeWeekNext", weekNext}, + } + + expectedOffset := 9 * 60 * 60 + for _, tt := range times { + _, offset := tt.time.Zone() + if offset != expectedOffset { + t.Errorf("%s() zone offset = %d, want %d (UTC+9)", tt.name, offset, expectedOffset) + } + } +} diff --git a/server/channelserver/test_helpers_test.go b/server/channelserver/test_helpers_test.go new file mode 100644 index 000000000..a0ffdb9a3 --- /dev/null +++ b/server/channelserver/test_helpers_test.go @@ -0,0 +1,68 @@ +package channelserver + +import ( + "net" + + "erupe-ce/common/byteframe" + _config "erupe-ce/config" + "erupe-ce/network" + "erupe-ce/network/clientctx" + + "go.uber.org/zap" +) + +// mockPacket implements mhfpacket.MHFPacket for testing. +// Imported from v9.2.x-stable. +type mockPacket struct { + opcode uint16 +} + +func (m *mockPacket) Opcode() network.PacketID { + return network.PacketID(m.opcode) +} + +func (m *mockPacket) Build(bf *byteframe.ByteFrame, ctx *clientctx.ClientContext) error { + if ctx == nil { + panic("clientContext is nil") + } + bf.WriteUint32(0x12345678) + return nil +} + +func (m *mockPacket) Parse(bf *byteframe.ByteFrame, ctx *clientctx.ClientContext) error { + return nil +} + +// createMockServer creates a minimal Server for testing. +// Imported from v9.2.x-stable and adapted for main. +func createMockServer() *Server { + logger, _ := zap.NewDevelopment() + s := &Server{ + logger: logger, + erupeConfig: &_config.Config{}, + stages: make(map[string]*Stage), + sessions: make(map[net.Conn]*Session), + raviente: &Raviente{ + register: make([]uint32, 30), + state: make([]uint32, 30), + support: make([]uint32, 30), + }, + } + s.i18n = getLangStrings(s) + return s +} + +// createMockSession creates a minimal Session for testing. +// Imported from v9.2.x-stable and adapted for main. +func createMockSession(charID uint32, server *Server) *Session { + logger, _ := zap.NewDevelopment() + return &Session{ + charID: charID, + clientContext: &clientctx.ClientContext{}, + sendPackets: make(chan packet, 20), + Name: "TestPlayer", + server: server, + logger: logger, + semaphoreID: make([]uint16, 2), + } +}