diff --git a/server/channelserver/handlers_gacha_test.go b/server/channelserver/handlers_gacha_test.go new file mode 100644 index 000000000..a0430af13 --- /dev/null +++ b/server/channelserver/handlers_gacha_test.go @@ -0,0 +1,660 @@ +package channelserver + +import ( + "database/sql" + "errors" + "testing" + "time" + + "erupe-ce/common/byteframe" + "erupe-ce/network/mhfpacket" +) + +func TestHandleMsgMhfGetGachaPlayHistory_StubResponse(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfGetGachaPlayHistory{AckHandle: 100, GachaID: 1} + handleMsgMhfGetGachaPlayHistory(session, pkt) + + select { + case p := <-session.sendPackets: + if len(p.data) == 0 { + t.Fatal("Empty response") + } + default: + t.Error("No response packet queued") + } +} + +func TestHandleMsgMhfGetGachaPoint(t *testing.T) { + server := createMockServer() + userRepo := &mockUserRepoGacha{ + gachaFP: 100, + gachaGP: 200, + gachaGT: 300, + } + server.userRepo = userRepo + + session := createMockSession(1, server) + session.userID = 1 + + pkt := &mhfpacket.MsgMhfGetGachaPoint{AckHandle: 100} + handleMsgMhfGetGachaPoint(session, pkt) + + select { + case p := <-session.sendPackets: + if len(p.data) == 0 { + t.Fatal("Empty response") + } + default: + t.Error("No response packet queued") + } +} + +func TestHandleMsgMhfUseGachaPoint_TrialCoins(t *testing.T) { + server := createMockServer() + userRepo := &mockUserRepoGacha{} + server.userRepo = userRepo + + session := createMockSession(1, server) + session.userID = 1 + + pkt := &mhfpacket.MsgMhfUseGachaPoint{ + AckHandle: 100, + TrialCoins: 10, + PremiumCoins: 0, + } + handleMsgMhfUseGachaPoint(session, pkt) + + select { + case <-session.sendPackets: + // success + default: + t.Error("No response packet queued") + } +} + +func TestHandleMsgMhfUseGachaPoint_PremiumCoins(t *testing.T) { + server := createMockServer() + userRepo := &mockUserRepoGacha{} + server.userRepo = userRepo + + session := createMockSession(1, server) + session.userID = 1 + + pkt := &mhfpacket.MsgMhfUseGachaPoint{ + AckHandle: 100, + TrialCoins: 0, + PremiumCoins: 5, + } + handleMsgMhfUseGachaPoint(session, pkt) + + select { + case <-session.sendPackets: + // success + default: + t.Error("No response packet queued") + } +} + +func TestHandleMsgMhfReceiveGachaItem_Normal(t *testing.T) { + server := createMockServer() + charRepo := newMockCharacterRepo() + // Store 2 items: count byte + 2 * 5 bytes each + data := []byte{2, 1, 0, 100, 0, 5, 2, 0, 200, 0, 10} + charRepo.columns["gacha_items"] = data + server.charRepo = charRepo + + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfReceiveGachaItem{AckHandle: 100, Freeze: false} + handleMsgMhfReceiveGachaItem(session, pkt) + + select { + case p := <-session.sendPackets: + if len(p.data) == 0 { + t.Fatal("Empty response") + } + default: + t.Error("No response packet queued") + } + + // After non-freeze receive, gacha_items should be cleared + if charRepo.columns["gacha_items"] != nil { + t.Error("Expected gacha_items to be cleared after receive") + } +} + +func TestHandleMsgMhfReceiveGachaItem_Overflow(t *testing.T) { + server := createMockServer() + charRepo := newMockCharacterRepo() + // Build data with >36 items (overflow scenario): count=37, 37*5=185 bytes + 1 count byte = 186 + data := make([]byte, 186) + data[0] = 37 + for i := 1; i < 186; i++ { + data[i] = byte(i % 256) + } + charRepo.columns["gacha_items"] = data + server.charRepo = charRepo + + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfReceiveGachaItem{AckHandle: 100, Freeze: false} + handleMsgMhfReceiveGachaItem(session, pkt) + + select { + case p := <-session.sendPackets: + if len(p.data) == 0 { + t.Fatal("Empty response") + } + default: + t.Error("No response packet queued") + } + + // After overflow, remaining items should be saved + saved := charRepo.columns["gacha_items"] + if saved == nil { + t.Error("Expected overflow items to be saved") + } +} + +func TestHandleMsgMhfReceiveGachaItem_Freeze(t *testing.T) { + server := createMockServer() + charRepo := newMockCharacterRepo() + data := []byte{1, 1, 0, 100, 0, 5} + charRepo.columns["gacha_items"] = data + server.charRepo = charRepo + + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfReceiveGachaItem{AckHandle: 100, Freeze: true} + handleMsgMhfReceiveGachaItem(session, pkt) + + select { + case <-session.sendPackets: + // success + default: + t.Error("No response packet queued") + } + + // Freeze should NOT clear the items + if charRepo.columns["gacha_items"] == nil { + t.Error("Expected gacha_items to be preserved on freeze") + } +} + +func TestHandleMsgMhfPlayNormalGacha_TransactError(t *testing.T) { + server := createMockServer() + gachaRepo := &mockGachaRepo{txErr: errors.New("transact failed")} + server.gachaRepo = gachaRepo + server.userRepo = &mockUserRepoGacha{} + + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfPlayNormalGacha{AckHandle: 100, GachaID: 1, RollType: 0} + handleMsgMhfPlayNormalGacha(session, pkt) + + select { + case p := <-session.sendPackets: + if len(p.data) == 0 { + t.Fatal("Empty response") + } + default: + t.Error("No response packet queued") + } +} + +func TestHandleMsgMhfPlayNormalGacha_RewardPoolError(t *testing.T) { + server := createMockServer() + gachaRepo := &mockGachaRepo{ + txRolls: 1, + rewardPoolErr: errors.New("pool error"), + } + server.gachaRepo = gachaRepo + server.userRepo = &mockUserRepoGacha{} + + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfPlayNormalGacha{AckHandle: 100, GachaID: 1, RollType: 0} + handleMsgMhfPlayNormalGacha(session, pkt) + + select { + case <-session.sendPackets: + // success - returns empty result + default: + t.Error("No response packet queued") + } +} + +func TestHandleMsgMhfPlayNormalGacha_Success(t *testing.T) { + server := createMockServer() + charRepo := newMockCharacterRepo() + server.charRepo = charRepo + + gachaRepo := &mockGachaRepo{ + txRolls: 1, + rewardPool: []GachaEntry{ + {ID: 10, Weight: 100, Rarity: 3}, + }, + entryItems: map[uint32][]GachaItem{ + 10: {{ItemType: 1, ItemID: 500, Quantity: 1}}, + }, + } + server.gachaRepo = gachaRepo + server.userRepo = &mockUserRepoGacha{} + + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfPlayNormalGacha{AckHandle: 100, GachaID: 1, RollType: 0} + handleMsgMhfPlayNormalGacha(session, pkt) + + select { + case p := <-session.sendPackets: + if len(p.data) == 0 { + t.Fatal("Empty response") + } + default: + t.Error("No response packet queued") + } + + // Verify gacha items were stored + if charRepo.columns["gacha_items"] == nil { + t.Error("Expected gacha items to be saved") + } +} + +func TestHandleMsgMhfPlayStepupGacha_TransactError(t *testing.T) { + server := createMockServer() + gachaRepo := &mockGachaRepo{txErr: errors.New("transact failed")} + server.gachaRepo = gachaRepo + server.userRepo = &mockUserRepoGacha{} + + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfPlayStepupGacha{AckHandle: 100, GachaID: 1, RollType: 0} + handleMsgMhfPlayStepupGacha(session, pkt) + + select { + case <-session.sendPackets: + // success - returns empty result + default: + t.Error("No response packet queued") + } +} + +func TestHandleMsgMhfPlayStepupGacha_Success(t *testing.T) { + server := createMockServer() + charRepo := newMockCharacterRepo() + server.charRepo = charRepo + + gachaRepo := &mockGachaRepo{ + txRolls: 1, + rewardPool: []GachaEntry{ + {ID: 10, Weight: 100, Rarity: 2}, + }, + entryItems: map[uint32][]GachaItem{ + 10: {{ItemType: 1, ItemID: 600, Quantity: 2}}, + }, + guaranteedItems: []GachaItem{ + {ItemType: 1, ItemID: 700, Quantity: 1}, + }, + } + server.gachaRepo = gachaRepo + server.userRepo = &mockUserRepoGacha{} + + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfPlayStepupGacha{AckHandle: 100, GachaID: 1, RollType: 0} + handleMsgMhfPlayStepupGacha(session, pkt) + + if !gachaRepo.deletedStepup { + t.Error("Expected stepup to be deleted") + } + if gachaRepo.insertedStep != 1 { + t.Errorf("Expected insertedStep=1, got %d", gachaRepo.insertedStep) + } + + select { + case <-session.sendPackets: + // success + default: + t.Error("No response packet queued") + } +} + +func TestHandleMsgMhfGetStepupStatus_FreshStep(t *testing.T) { + server := createMockServer() + gachaRepo := &mockGachaRepo{ + stepupStep: 2, + stepupTime: time.Now(), // recent, not stale + hasEntryType: true, + } + server.gachaRepo = gachaRepo + + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfGetStepupStatus{AckHandle: 100, GachaID: 1} + handleMsgMhfGetStepupStatus(session, pkt) + + select { + case p := <-session.sendPackets: + if len(p.data) == 0 { + t.Fatal("Empty response") + } + default: + t.Error("No response packet queued") + } +} + +func TestHandleMsgMhfGetStepupStatus_StaleStep(t *testing.T) { + server := createMockServer() + gachaRepo := &mockGachaRepo{ + stepupStep: 3, + stepupTime: time.Now().Add(-48 * time.Hour), // stale + } + server.gachaRepo = gachaRepo + + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfGetStepupStatus{AckHandle: 100, GachaID: 1} + handleMsgMhfGetStepupStatus(session, pkt) + + if !gachaRepo.deletedStepup { + t.Error("Expected stale stepup to be deleted") + } + + select { + case <-session.sendPackets: + // success + default: + t.Error("No response packet queued") + } +} + +func TestHandleMsgMhfGetStepupStatus_NoRows(t *testing.T) { + server := createMockServer() + gachaRepo := &mockGachaRepo{ + stepupErr: sql.ErrNoRows, + } + server.gachaRepo = gachaRepo + + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfGetStepupStatus{AckHandle: 100, GachaID: 1} + handleMsgMhfGetStepupStatus(session, pkt) + + select { + case <-session.sendPackets: + // success + default: + t.Error("No response packet queued") + } +} + +func TestHandleMsgMhfGetStepupStatus_NoEntryType(t *testing.T) { + server := createMockServer() + gachaRepo := &mockGachaRepo{ + stepupStep: 2, + stepupTime: time.Now(), + hasEntryType: false, // no matching entry type -> reset + } + server.gachaRepo = gachaRepo + + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfGetStepupStatus{AckHandle: 100, GachaID: 1} + handleMsgMhfGetStepupStatus(session, pkt) + + if !gachaRepo.deletedStepup { + t.Error("Expected stepup to be reset when no entry type") + } + + select { + case <-session.sendPackets: + // success + default: + t.Error("No response packet queued") + } +} + +func TestHandleMsgMhfGetBoxGachaInfo_Error(t *testing.T) { + server := createMockServer() + gachaRepo := &mockGachaRepo{ + boxEntryIDsErr: errors.New("db error"), + } + server.gachaRepo = gachaRepo + + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfGetBoxGachaInfo{AckHandle: 100, GachaID: 1} + handleMsgMhfGetBoxGachaInfo(session, pkt) + + select { + case <-session.sendPackets: + // returns empty + default: + t.Error("No response packet queued") + } +} + +func TestHandleMsgMhfGetBoxGachaInfo_Success(t *testing.T) { + server := createMockServer() + gachaRepo := &mockGachaRepo{ + boxEntryIDs: []uint32{10, 20, 30}, + } + server.gachaRepo = gachaRepo + + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfGetBoxGachaInfo{AckHandle: 100, GachaID: 1} + handleMsgMhfGetBoxGachaInfo(session, pkt) + + select { + case p := <-session.sendPackets: + if len(p.data) == 0 { + t.Fatal("Empty response") + } + default: + t.Error("No response packet queued") + } +} + +func TestHandleMsgMhfPlayBoxGacha_TransactError(t *testing.T) { + server := createMockServer() + gachaRepo := &mockGachaRepo{txErr: errors.New("transact failed")} + server.gachaRepo = gachaRepo + server.userRepo = &mockUserRepoGacha{} + + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfPlayBoxGacha{AckHandle: 100, GachaID: 1, RollType: 0} + handleMsgMhfPlayBoxGacha(session, pkt) + + select { + case <-session.sendPackets: + // success + default: + t.Error("No response packet queued") + } +} + +func TestHandleMsgMhfPlayBoxGacha_Success(t *testing.T) { + server := createMockServer() + charRepo := newMockCharacterRepo() + server.charRepo = charRepo + + gachaRepo := &mockGachaRepo{ + txRolls: 1, + rewardPool: []GachaEntry{ + {ID: 10, Weight: 100, Rarity: 1}, + }, + entryItems: map[uint32][]GachaItem{ + 10: {{ItemType: 1, ItemID: 800, Quantity: 1}}, + }, + } + server.gachaRepo = gachaRepo + server.userRepo = &mockUserRepoGacha{} + + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfPlayBoxGacha{AckHandle: 100, GachaID: 1, RollType: 0} + handleMsgMhfPlayBoxGacha(session, pkt) + + if len(gachaRepo.insertedBoxIDs) == 0 { + t.Error("Expected box entry to be inserted") + } + + select { + case <-session.sendPackets: + // success + default: + t.Error("No response packet queued") + } +} + +func TestHandleMsgMhfResetBoxGachaInfo(t *testing.T) { + server := createMockServer() + gachaRepo := &mockGachaRepo{} + server.gachaRepo = gachaRepo + + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfResetBoxGachaInfo{AckHandle: 100, GachaID: 1} + handleMsgMhfResetBoxGachaInfo(session, pkt) + + if !gachaRepo.deletedBox { + t.Error("Expected box entries to be deleted") + } + + select { + case <-session.sendPackets: + // success + default: + t.Error("No response packet queued") + } +} + +func TestHandleMsgMhfPlayFreeGacha_StubACK(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfPlayFreeGacha{AckHandle: 100, GachaID: 1} + handleMsgMhfPlayFreeGacha(session, pkt) + + select { + case <-session.sendPackets: + // success + default: + t.Error("No response packet queued") + } +} + +func TestGetRandomEntries_NonBox(t *testing.T) { + entries := []GachaEntry{ + {ID: 1, Weight: 50}, + {ID: 2, Weight: 50}, + } + result, err := getRandomEntries(entries, 3, false) + if err != nil { + t.Fatal(err) + } + if len(result) != 3 { + t.Errorf("Expected 3 entries, got %d", len(result)) + } +} + +func TestGetRandomEntries_Box(t *testing.T) { + entries := []GachaEntry{ + {ID: 1, Weight: 50}, + {ID: 2, Weight: 50}, + {ID: 3, Weight: 50}, + } + result, err := getRandomEntries(entries, 2, true) + if err != nil { + t.Fatal(err) + } + if len(result) != 2 { + t.Errorf("Expected 2 entries, got %d", len(result)) + } + // Box mode removes entries without replacement — all IDs should be unique + if result[0].ID == result[1].ID { + t.Error("Box mode should return unique entries") + } +} + +func TestHandleMsgMhfPlayStepupGacha_RewardPoolError(t *testing.T) { + server := createMockServer() + charRepo := newMockCharacterRepo() + server.charRepo = charRepo + + gachaRepo := &mockGachaRepo{ + txRolls: 1, + rewardPoolErr: errors.New("pool error"), + } + server.gachaRepo = gachaRepo + server.userRepo = &mockUserRepoGacha{} + + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfPlayStepupGacha{AckHandle: 100, GachaID: 1, RollType: 0} + handleMsgMhfPlayStepupGacha(session, pkt) + + select { + case p := <-session.sendPackets: + // Verify minimal response (1 byte) + _ = p + default: + t.Error("No response packet queued") + } +} + +// Verify the response payload of GetGachaPoint contains the expected values +func TestHandleMsgMhfGetGachaPoint_ResponsePayload(t *testing.T) { + server := createMockServer() + userRepo := &mockUserRepoGacha{ + gachaFP: 111, + gachaGP: 222, + gachaGT: 333, + } + server.userRepo = userRepo + + session := createMockSession(1, server) + session.userID = 1 + + pkt := &mhfpacket.MsgMhfGetGachaPoint{AckHandle: 100} + handleMsgMhfGetGachaPoint(session, pkt) + + select { + case p := <-session.sendPackets: + // The ack wraps the payload. The handler writes gp, gt, fp (12 bytes). + // Just verify we got a reasonable-sized response. + if len(p.data) < 12 { + t.Errorf("Expected at least 12 bytes of gacha point data in response, got %d", len(p.data)) + } + default: + t.Error("No response packet queued") + } +} + +// Verify the response when no gacha items exist (default column) +func TestHandleMsgMhfReceiveGachaItem_Empty(t *testing.T) { + server := createMockServer() + charRepo := newMockCharacterRepo() + // No gacha_items set — will return default {0x00} + server.charRepo = charRepo + + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfReceiveGachaItem{AckHandle: 100, Freeze: false} + handleMsgMhfReceiveGachaItem(session, pkt) + + select { + case p := <-session.sendPackets: + // The response should contain the default byte + bf := byteframe.NewByteFrameFromBytes(p.data) + _ = bf + default: + t.Error("No response packet queued") + } +} diff --git a/server/channelserver/handlers_plate_test.go b/server/channelserver/handlers_plate_test.go new file mode 100644 index 000000000..640194bdd --- /dev/null +++ b/server/channelserver/handlers_plate_test.go @@ -0,0 +1,381 @@ +package channelserver + +import ( + "errors" + "testing" + + "erupe-ce/network/mhfpacket" + "erupe-ce/server/channelserver/compression/nullcomp" +) + +func TestHandleMsgMhfLoadPlateData(t *testing.T) { + server := createMockServer() + charRepo := newMockCharacterRepo() + charRepo.columns["platedata"] = []byte{0x01, 0x02, 0x03} + server.charRepo = charRepo + + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfLoadPlateData{AckHandle: 100} + handleMsgMhfLoadPlateData(session, pkt) + + select { + case p := <-session.sendPackets: + if len(p.data) == 0 { + t.Fatal("Empty response") + } + default: + t.Error("No response packet queued") + } +} + +func TestHandleMsgMhfLoadPlateData_Empty(t *testing.T) { + server := createMockServer() + charRepo := newMockCharacterRepo() + // No platedata column set — loadCharacterData uses nil default + server.charRepo = charRepo + + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfLoadPlateData{AckHandle: 100} + handleMsgMhfLoadPlateData(session, pkt) + + select { + case <-session.sendPackets: + // success + default: + t.Error("No response packet queued") + } +} + +func TestHandleMsgMhfSavePlateData_OversizedPayload(t *testing.T) { + server := createMockServer() + server.userBinary = NewUserBinaryStore() + charRepo := newMockCharacterRepo() + server.charRepo = charRepo + + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfSavePlateData{ + AckHandle: 100, + RawDataPayload: make([]byte, plateDataMaxPayload+1), + IsDataDiff: false, + } + handleMsgMhfSavePlateData(session, pkt) + + // Should still get ACK + select { + case <-session.sendPackets: + // success + default: + t.Error("No response packet queued") + } + + // Data should NOT have been saved + if charRepo.columns["platedata"] != nil { + t.Error("Expected platedata to NOT be saved when oversized") + } +} + +func TestHandleMsgMhfSavePlateData_FullSave(t *testing.T) { + server := createMockServer() + server.userBinary = NewUserBinaryStore() + charRepo := newMockCharacterRepo() + server.charRepo = charRepo + + session := createMockSession(1, server) + + payload := []byte{0x10, 0x20, 0x30, 0x40} + pkt := &mhfpacket.MsgMhfSavePlateData{ + AckHandle: 100, + RawDataPayload: payload, + IsDataDiff: false, + } + handleMsgMhfSavePlateData(session, pkt) + + select { + case <-session.sendPackets: + // success + default: + t.Error("No response packet queued") + } + + saved := charRepo.columns["platedata"] + if saved == nil { + t.Fatal("Expected platedata to be saved") + } + if len(saved) != len(payload) { + t.Errorf("Expected saved data length %d, got %d", len(payload), len(saved)) + } +} + +func TestHandleMsgMhfSavePlateData_DiffPath_LoadError(t *testing.T) { + server := createMockServer() + server.userBinary = NewUserBinaryStore() + charRepo := newMockCharacterRepo() + charRepo.loadColumnErr = errors.New("load failed") + server.charRepo = charRepo + + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfSavePlateData{ + AckHandle: 100, + RawDataPayload: []byte{0x01}, + IsDataDiff: true, + } + handleMsgMhfSavePlateData(session, pkt) + + select { + case <-session.sendPackets: + // returns ACK even on error + default: + t.Error("No response packet queued") + } +} + +func TestHandleMsgMhfSavePlateData_DiffPath_SaveError(t *testing.T) { + server := createMockServer() + server.userBinary = NewUserBinaryStore() + charRepo := newMockCharacterRepo() + // Provide compressed data so decompress works + original := make([]byte, 100) + compressed, _ := nullcomp.Compress(original) + charRepo.columns["platedata"] = compressed + charRepo.saveErr = errors.New("save failed") + server.charRepo = charRepo + + session := createMockSession(1, server) + + // Build a valid diff payload: matchCount=2 (offset becomes 1), diffCount=2 (means 1 byte), then 1 data byte + diffPayload := []byte{2, 2, 0xAA} + pkt := &mhfpacket.MsgMhfSavePlateData{ + AckHandle: 100, + RawDataPayload: diffPayload, + IsDataDiff: true, + } + handleMsgMhfSavePlateData(session, pkt) + + select { + case <-session.sendPackets: + // returns ACK even on save error + default: + t.Error("No response packet queued") + } +} + +func TestHandleMsgMhfLoadPlateBox(t *testing.T) { + server := createMockServer() + charRepo := newMockCharacterRepo() + charRepo.columns["platebox"] = []byte{0xAA, 0xBB} + server.charRepo = charRepo + + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfLoadPlateBox{AckHandle: 100} + handleMsgMhfLoadPlateBox(session, pkt) + + select { + case p := <-session.sendPackets: + if len(p.data) == 0 { + t.Fatal("Empty response") + } + default: + t.Error("No response packet queued") + } +} + +func TestHandleMsgMhfSavePlateBox_OversizedPayload(t *testing.T) { + server := createMockServer() + server.userBinary = NewUserBinaryStore() + charRepo := newMockCharacterRepo() + server.charRepo = charRepo + + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfSavePlateBox{ + AckHandle: 100, + RawDataPayload: make([]byte, plateBoxMaxPayload+1), + IsDataDiff: false, + } + handleMsgMhfSavePlateBox(session, pkt) + + select { + case <-session.sendPackets: + // success + default: + t.Error("No response packet queued") + } + + if charRepo.columns["platebox"] != nil { + t.Error("Expected platebox to NOT be saved when oversized") + } +} + +func TestHandleMsgMhfSavePlateBox_FullSave(t *testing.T) { + server := createMockServer() + server.userBinary = NewUserBinaryStore() + charRepo := newMockCharacterRepo() + server.charRepo = charRepo + + session := createMockSession(1, server) + + payload := []byte{0xCC, 0xDD} + pkt := &mhfpacket.MsgMhfSavePlateBox{ + AckHandle: 100, + RawDataPayload: payload, + IsDataDiff: false, + } + handleMsgMhfSavePlateBox(session, pkt) + + select { + case <-session.sendPackets: + // success + default: + t.Error("No response packet queued") + } + + if charRepo.columns["platebox"] == nil { + t.Fatal("Expected platebox to be saved") + } +} + +func TestHandleMsgMhfSavePlateBox_DiffPath(t *testing.T) { + server := createMockServer() + server.userBinary = NewUserBinaryStore() + charRepo := newMockCharacterRepo() + // Provide compressed data + original := make([]byte, 100) + compressed, _ := nullcomp.Compress(original) + charRepo.columns["platebox"] = compressed + server.charRepo = charRepo + + session := createMockSession(1, server) + + // Valid diff: matchCount=2 (offset becomes 1), diffCount=2 (1 byte), data byte + diffPayload := []byte{2, 2, 0xBB} + pkt := &mhfpacket.MsgMhfSavePlateBox{ + AckHandle: 100, + RawDataPayload: diffPayload, + IsDataDiff: true, + } + handleMsgMhfSavePlateBox(session, pkt) + + select { + case <-session.sendPackets: + // success + default: + t.Error("No response packet queued") + } +} + +func TestHandleMsgMhfLoadPlateMyset(t *testing.T) { + server := createMockServer() + charRepo := newMockCharacterRepo() + server.charRepo = charRepo + + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfLoadPlateMyset{AckHandle: 100} + handleMsgMhfLoadPlateMyset(session, pkt) + + select { + case p := <-session.sendPackets: + if len(p.data) == 0 { + t.Fatal("Empty response") + } + default: + t.Error("No response packet queued") + } +} + +func TestHandleMsgMhfSavePlateMyset_OversizedPayload(t *testing.T) { + server := createMockServer() + server.userBinary = NewUserBinaryStore() + charRepo := newMockCharacterRepo() + server.charRepo = charRepo + + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfSavePlateMyset{ + AckHandle: 100, + RawDataPayload: make([]byte, plateMysetMaxPayload+1), + } + handleMsgMhfSavePlateMyset(session, pkt) + + select { + case <-session.sendPackets: + // success + default: + t.Error("No response packet queued") + } + + if charRepo.columns["platemyset"] != nil { + t.Error("Expected platemyset to NOT be saved when oversized") + } +} + +func TestHandleMsgMhfSavePlateMyset_Success(t *testing.T) { + server := createMockServer() + server.userBinary = NewUserBinaryStore() + charRepo := newMockCharacterRepo() + server.charRepo = charRepo + + session := createMockSession(1, server) + + payload := make([]byte, plateMysetDefaultLen) + payload[0] = 0xFF + pkt := &mhfpacket.MsgMhfSavePlateMyset{ + AckHandle: 100, + RawDataPayload: payload, + } + handleMsgMhfSavePlateMyset(session, pkt) + + select { + case <-session.sendPackets: + // success + default: + t.Error("No response packet queued") + } + + if charRepo.columns["platemyset"] == nil { + t.Fatal("Expected platemyset to be saved") + } + if charRepo.columns["platemyset"][0] != 0xFF { + t.Error("Expected first byte to be 0xFF") + } +} + +func TestHandleMsgMhfSavePlateData_CacheInvalidation(t *testing.T) { + server := createMockServer() + server.userBinary = NewUserBinaryStore() + charRepo := newMockCharacterRepo() + server.charRepo = charRepo + + session := createMockSession(42, server) + + // Pre-populate the cache + server.userBinary.Set(42, 2, []byte{0x01}) + server.userBinary.Set(42, 3, []byte{0x02}) + + pkt := &mhfpacket.MsgMhfSavePlateData{ + AckHandle: 100, + RawDataPayload: []byte{0x10}, + IsDataDiff: false, + } + handleMsgMhfSavePlateData(session, pkt) + + // Verify cache was invalidated + if data := server.userBinary.GetCopy(42, 2); len(data) > 0 { + t.Error("Expected user binary type 2 to be invalidated") + } + if data := server.userBinary.GetCopy(42, 3); len(data) > 0 { + t.Error("Expected user binary type 3 to be invalidated") + } + + select { + case <-session.sendPackets: + // success + default: + t.Error("No response packet queued") + } +} diff --git a/server/channelserver/handlers_session_test.go b/server/channelserver/handlers_session_test.go new file mode 100644 index 000000000..0e7beb99c --- /dev/null +++ b/server/channelserver/handlers_session_test.go @@ -0,0 +1,372 @@ +package channelserver + +import ( + "encoding/binary" + "errors" + "testing" + + "erupe-ce/common/byteframe" + cfg "erupe-ce/config" + "erupe-ce/network/mhfpacket" +) + +func TestHandleMsgSysTerminalLog_ReturnsLogIDPlusOne(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgSysTerminalLog{ + AckHandle: 100, + LogID: 5, + Entries: []mhfpacket.TerminalLogEntry{ + {Type1: 1, Type2: 2, Unk0: 3, Unk1: 4, Unk2: 5, Unk3: 6}, + }, + } + handleMsgSysTerminalLog(session, pkt) + + select { + case p := <-session.sendPackets: + if len(p.data) < 4 { + t.Fatal("Response too short") + } + default: + t.Error("No response packet queued") + } +} + +func TestHandleMsgSysLogin_Success(t *testing.T) { + server := createMockServer() + server.erupeConfig.DebugOptions.DisableTokenCheck = true + server.userBinary = NewUserBinaryStore() + + charRepo := newMockCharacterRepo() + server.charRepo = charRepo + + sessionRepo := &mockSessionRepo{} + server.sessionRepo = sessionRepo + + userRepo := &mockUserRepoGacha{} + server.userRepo = userRepo + + session := createMockSession(0, server) + + pkt := &mhfpacket.MsgSysLogin{ + AckHandle: 100, + CharID0: 42, + LoginTokenString: "test-token", + } + handleMsgSysLogin(session, pkt) + + if session.charID != 42 { + t.Errorf("Expected charID 42, got %d", session.charID) + } + if session.token != "test-token" { + t.Errorf("Expected token 'test-token', got %q", session.token) + } + if sessionRepo.boundToken != "test-token" { + t.Errorf("Expected BindSession called with 'test-token', got %q", sessionRepo.boundToken) + } + + select { + case <-session.sendPackets: + // success + default: + t.Error("No response packet queued") + } +} + +func TestHandleMsgSysLogin_GetUserIDError(t *testing.T) { + server := createMockServer() + server.erupeConfig.DebugOptions.DisableTokenCheck = true + + charRepo := newMockCharacterRepo() + server.charRepo = &mockCharRepoGetUserIDErr{ + mockCharacterRepo: charRepo, + getUserIDErr: errors.New("user not found"), + } + + sessionRepo := &mockSessionRepo{} + server.sessionRepo = sessionRepo + + userRepo := &mockUserRepoGacha{} + server.userRepo = userRepo + + session := createMockSession(0, server) + + pkt := &mhfpacket.MsgSysLogin{ + AckHandle: 100, + CharID0: 42, + LoginTokenString: "test-token", + } + handleMsgSysLogin(session, pkt) + + select { + case <-session.sendPackets: + // got a response (fail ACK) + default: + t.Error("No response packet queued on GetUserID error") + } +} + +func TestHandleMsgSysLogin_BindSessionError(t *testing.T) { + server := createMockServer() + server.erupeConfig.DebugOptions.DisableTokenCheck = true + + charRepo := newMockCharacterRepo() + server.charRepo = charRepo + + sessionRepo := &mockSessionRepo{bindErr: errors.New("bind failed")} + server.sessionRepo = sessionRepo + + userRepo := &mockUserRepoGacha{} + server.userRepo = userRepo + + session := createMockSession(0, server) + + pkt := &mhfpacket.MsgSysLogin{ + AckHandle: 100, + CharID0: 42, + LoginTokenString: "test-token", + } + handleMsgSysLogin(session, pkt) + + select { + case <-session.sendPackets: + // got a response (fail ACK) + default: + t.Error("No response packet queued on BindSession error") + } +} + +func TestHandleMsgSysLogin_SetLastCharacterError(t *testing.T) { + server := createMockServer() + server.erupeConfig.DebugOptions.DisableTokenCheck = true + + charRepo := newMockCharacterRepo() + server.charRepo = charRepo + + sessionRepo := &mockSessionRepo{} + server.sessionRepo = sessionRepo + + userRepo := &mockUserRepoGacha{setLastCharErr: errors.New("set failed")} + server.userRepo = userRepo + + session := createMockSession(0, server) + + pkt := &mhfpacket.MsgSysLogin{ + AckHandle: 100, + CharID0: 42, + LoginTokenString: "test-token", + } + handleMsgSysLogin(session, pkt) + + select { + case <-session.sendPackets: + // got a response (fail ACK) + default: + t.Error("No response packet queued on SetLastCharacter error") + } +} + +func TestHandleMsgSysPing_Session(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgSysPing{AckHandle: 100} + handleMsgSysPing(session, pkt) + + select { + case p := <-session.sendPackets: + if len(p.data) == 0 { + t.Fatal("Empty response") + } + default: + t.Error("No response packet queued") + } +} + +func TestHandleMsgSysIssueLogkey_GeneratesKey(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgSysIssueLogkey{AckHandle: 100} + handleMsgSysIssueLogkey(session, pkt) + + if len(session.logKey) != 16 { + t.Errorf("Expected 16-byte log key, got %d bytes", len(session.logKey)) + } + + select { + case p := <-session.sendPackets: + if len(p.data) == 0 { + t.Fatal("Empty response") + } + default: + t.Error("No response packet queued") + } +} + +func TestHandleMsgSysRecordLog_ZZMode(t *testing.T) { + server := createMockServer() + server.erupeConfig.RealClientMode = cfg.ZZ + server.userBinary = NewUserBinaryStore() + + guildRepo := &mockGuildRepoForMail{} + server.guildRepo = guildRepo + + session := createMockSession(1, server) + + // Create a stage for the session (handler accesses s.stage.reservedClientSlots) + stage := &Stage{ + id: "testStage", + clients: make(map[*Session]uint32), + reservedClientSlots: make(map[uint32]bool), + } + stage.reservedClientSlots[1] = true + session.stage = stage + + // Build kill log data: 32 header bytes + 176 monster bytes + data := make([]byte, 32+176) + // Set monster index 5 to have 2 kills (a large monster per mhfmon) + data[32+5] = 2 + + pkt := &mhfpacket.MsgSysRecordLog{ + AckHandle: 100, + Data: data, + } + handleMsgSysRecordLog(session, pkt) + + // Check that reserved slot was cleaned up + if _, exists := stage.reservedClientSlots[1]; exists { + t.Error("Expected reserved client slot to be removed") + } + + select { + case <-session.sendPackets: + // success + default: + t.Error("No response packet queued") + } +} + +func TestHandleMsgSysLockGlobalSema_LocalChannel(t *testing.T) { + server := createMockServer() + server.GlobalID = "ch1" + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgSysLockGlobalSema{ + AckHandle: 100, + UserIDString: "someStage", + ServerChannelIDString: "ch1", + } + handleMsgSysLockGlobalSema(session, pkt) + + select { + case p := <-session.sendPackets: + if len(p.data) == 0 { + t.Fatal("Empty response") + } + default: + t.Error("No response packet queued") + } +} + +func TestHandleMsgSysLockGlobalSema_RemoteMatch(t *testing.T) { + server := createMockServer() + server.GlobalID = "ch1" + + otherChannel := createMockServer() + otherChannel.GlobalID = "ch2" + otherChannel.stages.Store("prefix_testStage", &Stage{ + id: "prefix_testStage", + clients: make(map[*Session]uint32), + reservedClientSlots: make(map[uint32]bool), + }) + server.Channels = []*Server{server, otherChannel} + + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgSysLockGlobalSema{ + AckHandle: 100, + UserIDString: "testStage", + ServerChannelIDString: "ch1", + } + handleMsgSysLockGlobalSema(session, pkt) + + select { + case p := <-session.sendPackets: + if len(p.data) == 0 { + t.Fatal("Empty response") + } + _ = byteframe.NewByteFrameFromBytes(p.data) + default: + t.Error("No response packet queued") + } +} + +func TestHandleMsgSysUnlockGlobalSema_Session(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgSysUnlockGlobalSema{AckHandle: 100} + handleMsgSysUnlockGlobalSema(session, pkt) + + select { + case <-session.sendPackets: + // success + default: + t.Error("No response packet queued") + } +} + +func TestHandleMsgSysRightsReload_Session(t *testing.T) { + server := createMockServer() + userRepo := &mockUserRepoGacha{rights: 0x02} + server.userRepo = userRepo + + session := createMockSession(1, server) + session.userID = 1 + + pkt := &mhfpacket.MsgSysRightsReload{AckHandle: 100} + handleMsgSysRightsReload(session, pkt) + + select { + case <-session.sendPackets: + // success + default: + t.Error("No response packet queued") + } +} + +func TestHandleMsgMhfAnnounce_Session(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + dataBf := byteframe.NewByteFrame() + dataBf.WriteUint8(2) // type = berserk + + pkt := &mhfpacket.MsgMhfAnnounce{ + AckHandle: 100, + IPAddress: binary.LittleEndian.Uint32([]byte{127, 0, 0, 1}), + Port: 54001, + StageID: make([]byte, 32), + Data: byteframe.NewByteFrameFromBytes(dataBf.Data()), + } + handleMsgMhfAnnounce(session, pkt) + + select { + case <-session.sendPackets: + // success + default: + t.Error("No response packet queued") + } +} + +// mockCharRepoGetUserIDErr wraps mockCharacterRepo to return an error from GetUserID +type mockCharRepoGetUserIDErr struct { + *mockCharacterRepo + getUserIDErr error +} + +func (m *mockCharRepoGetUserIDErr) GetUserID(_ uint32) (uint32, error) { + return 0, m.getUserIDErr +} diff --git a/server/channelserver/handlers_shop_test.go b/server/channelserver/handlers_shop_test.go new file mode 100644 index 000000000..d58b2d123 --- /dev/null +++ b/server/channelserver/handlers_shop_test.go @@ -0,0 +1,476 @@ +package channelserver + +import ( + "errors" + "testing" + + "erupe-ce/common/byteframe" + cfg "erupe-ce/config" + "erupe-ce/network/mhfpacket" +) + +func TestHandleMsgMhfEnumerateShop_Case1_G7EarlyReturn(t *testing.T) { + server := createMockServer() + server.erupeConfig.RealClientMode = cfg.G7 + + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfEnumerateShop{ + AckHandle: 100, + ShopType: 1, + ShopID: 0, + } + handleMsgMhfEnumerateShop(session, pkt) + + select { + case p := <-session.sendPackets: + if len(p.data) == 0 { + t.Fatal("Empty response") + } + default: + t.Error("No response packet queued") + } +} + +func TestHandleMsgMhfEnumerateShop_Case1_GachaList(t *testing.T) { + server := createMockServer() + server.erupeConfig.RealClientMode = cfg.ZZ + + gachaRepo := &mockGachaRepo{ + gachas: []Gacha{ + {ID: 1, Name: "TestGacha", MinGR: 0, MinHR: 0, GachaType: 1}, + }, + } + server.gachaRepo = gachaRepo + + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfEnumerateShop{ + AckHandle: 100, + ShopType: 1, + ShopID: 0, + } + handleMsgMhfEnumerateShop(session, pkt) + + select { + case p := <-session.sendPackets: + if len(p.data) == 0 { + t.Fatal("Empty response") + } + default: + t.Error("No response packet queued") + } +} + +func TestHandleMsgMhfEnumerateShop_Case1_ListShopError(t *testing.T) { + server := createMockServer() + server.erupeConfig.RealClientMode = cfg.ZZ + + gachaRepo := &mockGachaRepo{ + listShopErr: errors.New("db error"), + } + server.gachaRepo = gachaRepo + + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfEnumerateShop{ + AckHandle: 100, + ShopType: 1, + ShopID: 0, + } + handleMsgMhfEnumerateShop(session, pkt) + + select { + case <-session.sendPackets: + // returns empty on error + default: + t.Error("No response packet queued") + } +} + +func TestHandleMsgMhfEnumerateShop_Case2_GachaDetail(t *testing.T) { + server := createMockServer() + server.erupeConfig.RealClientMode = cfg.ZZ + + gachaRepo := &mockGachaRepo{ + shopType: 1, // non-box + allEntries: []GachaEntry{ + {ID: 10, EntryType: 1, ItemType: 1, ItemNumber: 100, ItemQuantity: 5, + Weight: 50, Rarity: 2, Rolls: 1, FrontierPoints: 10, DailyLimit: 3, Name: "Item1"}, + }, + entryItems: map[uint32][]GachaItem{ + 10: {{ItemType: 1, ItemID: 500, Quantity: 1}}, + }, + weightDivisor: 1.0, + } + server.gachaRepo = gachaRepo + + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfEnumerateShop{ + AckHandle: 100, + ShopType: 2, + ShopID: 1, + } + handleMsgMhfEnumerateShop(session, pkt) + + select { + case p := <-session.sendPackets: + if len(p.data) == 0 { + t.Fatal("Empty response") + } + default: + t.Error("No response packet queued") + } +} + +func TestHandleMsgMhfEnumerateShop_Case2_AllEntriesError(t *testing.T) { + server := createMockServer() + server.erupeConfig.RealClientMode = cfg.ZZ + + gachaRepo := &mockGachaRepo{ + allEntriesErr: errors.New("db error"), + } + server.gachaRepo = gachaRepo + + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfEnumerateShop{ + AckHandle: 100, + ShopType: 2, + ShopID: 1, + } + handleMsgMhfEnumerateShop(session, pkt) + + select { + case <-session.sendPackets: + // returns empty on error + default: + t.Error("No response packet queued") + } +} + +func TestHandleMsgMhfEnumerateShop_Case10_ShopItems(t *testing.T) { + server := createMockServer() + server.erupeConfig.RealClientMode = cfg.ZZ + + shopRepo := &mockShopRepo{ + shopItems: []ShopItem{ + {ID: 1, ItemID: 100, Cost: 500, Quantity: 10, MinHR: 1}, + {ID: 2, ItemID: 200, Cost: 1000, Quantity: 5, MinHR: 3}, + {ID: 3, ItemID: 300, Cost: 2000, Quantity: 1, MinHR: 5}, + }, + } + server.shopRepo = shopRepo + + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfEnumerateShop{ + AckHandle: 100, + ShopType: 10, + ShopID: 0, + Limit: 2, // Limit to 2 items + } + handleMsgMhfEnumerateShop(session, pkt) + + select { + case p := <-session.sendPackets: + if len(p.data) == 0 { + t.Fatal("Empty response") + } + default: + t.Error("No response packet queued") + } +} + +func TestHandleMsgMhfEnumerateShop_Cases3to9(t *testing.T) { + for _, shopType := range []uint8{3, 4, 5, 6, 7, 8, 9} { + server := createMockServer() + server.erupeConfig.RealClientMode = cfg.ZZ + + shopRepo := &mockShopRepo{ + shopItems: []ShopItem{ + {ID: 1, ItemID: 100, Cost: 500, Quantity: 10}, + }, + } + server.shopRepo = shopRepo + + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfEnumerateShop{ + AckHandle: 100, + ShopType: shopType, + ShopID: 0, + Limit: 100, + } + handleMsgMhfEnumerateShop(session, pkt) + + select { + case <-session.sendPackets: + // success + default: + t.Errorf("No response for shop type %d", shopType) + } + } +} + +func TestHandleMsgMhfAcquireExchangeShop_RecordsPurchases(t *testing.T) { + server := createMockServer() + shopRepo := &mockShopRepo{} + server.shopRepo = shopRepo + + session := createMockSession(1, server) + + // Build payload: 2 exchanges, one with non-zero hash, one with zero hash + payload := byteframe.NewByteFrame() + payload.WriteUint16(2) // count + payload.WriteUint32(12345) // itemHash 1 + payload.WriteUint32(3) // buyCount 1 + payload.WriteUint32(0) // itemHash 2 (zero, should be skipped) + payload.WriteUint32(1) // buyCount 2 + + pkt := &mhfpacket.MsgMhfAcquireExchangeShop{ + AckHandle: 100, + RawDataPayload: payload.Data(), + } + handleMsgMhfAcquireExchangeShop(session, pkt) + + if len(shopRepo.purchases) != 1 { + t.Errorf("Expected 1 purchase recorded (skipping zero hash), got %d", len(shopRepo.purchases)) + } + if len(shopRepo.purchases) > 0 && shopRepo.purchases[0].itemHash != 12345 { + t.Errorf("Expected itemHash=12345, got %d", shopRepo.purchases[0].itemHash) + } + + select { + case <-session.sendPackets: + // success + default: + t.Error("No response packet queued") + } +} + +func TestHandleMsgMhfExchangeFpoint2Item_Success(t *testing.T) { + server := createMockServer() + shopRepo := &mockShopRepo{ + fpointQuantity: 1, + fpointValue: 100, + } + server.shopRepo = shopRepo + + userRepo := &mockUserRepoGacha{fpDeductBalance: 900} + server.userRepo = userRepo + + session := createMockSession(1, server) + session.userID = 1 + + pkt := &mhfpacket.MsgMhfExchangeFpoint2Item{ + AckHandle: 100, + TradeID: 1, + Quantity: 1, + } + handleMsgMhfExchangeFpoint2Item(session, pkt) + + select { + case p := <-session.sendPackets: + if len(p.data) == 0 { + t.Fatal("Empty response") + } + default: + t.Error("No response packet queued") + } +} + +func TestHandleMsgMhfExchangeFpoint2Item_GetFpointItemError(t *testing.T) { + server := createMockServer() + shopRepo := &mockShopRepo{ + fpointItemErr: errors.New("not found"), + } + server.shopRepo = shopRepo + server.userRepo = &mockUserRepoGacha{} + + session := createMockSession(1, server) + session.userID = 1 + + pkt := &mhfpacket.MsgMhfExchangeFpoint2Item{ + AckHandle: 100, + TradeID: 999, + Quantity: 1, + } + handleMsgMhfExchangeFpoint2Item(session, pkt) + + select { + case <-session.sendPackets: + // returns fail + default: + t.Error("No response packet queued") + } +} + +func TestHandleMsgMhfExchangeFpoint2Item_DeductError(t *testing.T) { + server := createMockServer() + shopRepo := &mockShopRepo{ + fpointQuantity: 1, + fpointValue: 100, + } + server.shopRepo = shopRepo + + userRepo := &mockUserRepoGacha{fpDeductErr: errors.New("insufficient")} + server.userRepo = userRepo + + session := createMockSession(1, server) + session.userID = 1 + + pkt := &mhfpacket.MsgMhfExchangeFpoint2Item{ + AckHandle: 100, + TradeID: 1, + Quantity: 1, + } + handleMsgMhfExchangeFpoint2Item(session, pkt) + + select { + case <-session.sendPackets: + // returns fail + default: + t.Error("No response packet queued") + } +} + +func TestHandleMsgMhfExchangeItem2Fpoint_Success(t *testing.T) { + server := createMockServer() + shopRepo := &mockShopRepo{ + fpointQuantity: 1, + fpointValue: 50, + } + server.shopRepo = shopRepo + + userRepo := &mockUserRepoGacha{fpCreditBalance: 1050} + server.userRepo = userRepo + + session := createMockSession(1, server) + session.userID = 1 + + pkt := &mhfpacket.MsgMhfExchangeItem2Fpoint{ + AckHandle: 100, + TradeID: 1, + Quantity: 1, + } + handleMsgMhfExchangeItem2Fpoint(session, pkt) + + select { + case p := <-session.sendPackets: + if len(p.data) == 0 { + t.Fatal("Empty response") + } + default: + t.Error("No response packet queued") + } +} + +func TestHandleMsgMhfExchangeItem2Fpoint_GetFpointItemError(t *testing.T) { + server := createMockServer() + shopRepo := &mockShopRepo{ + fpointItemErr: errors.New("not found"), + } + server.shopRepo = shopRepo + server.userRepo = &mockUserRepoGacha{} + + session := createMockSession(1, server) + session.userID = 1 + + pkt := &mhfpacket.MsgMhfExchangeItem2Fpoint{ + AckHandle: 100, + TradeID: 999, + Quantity: 1, + } + handleMsgMhfExchangeItem2Fpoint(session, pkt) + + select { + case <-session.sendPackets: + // returns fail + default: + t.Error("No response packet queued") + } +} + +func TestHandleMsgMhfExchangeItem2Fpoint_CreditError(t *testing.T) { + server := createMockServer() + shopRepo := &mockShopRepo{ + fpointQuantity: 1, + fpointValue: 50, + } + server.shopRepo = shopRepo + + userRepo := &mockUserRepoGacha{fpCreditErr: errors.New("credit error")} + server.userRepo = userRepo + + session := createMockSession(1, server) + session.userID = 1 + + pkt := &mhfpacket.MsgMhfExchangeItem2Fpoint{ + AckHandle: 100, + TradeID: 1, + Quantity: 1, + } + handleMsgMhfExchangeItem2Fpoint(session, pkt) + + select { + case <-session.sendPackets: + // returns fail + default: + t.Error("No response packet queued") + } +} + +func TestHandleMsgMhfGetFpointExchangeList_Z2Mode(t *testing.T) { + server := createMockServer() + server.erupeConfig.RealClientMode = cfg.Z2 + + shopRepo := &mockShopRepo{ + fpointExchanges: []FPointExchange{ + {ID: 1, ItemType: 1, ItemID: 100, Quantity: 5, FPoints: 10, Buyable: true}, + {ID: 2, ItemType: 2, ItemID: 200, Quantity: 1, FPoints: 50, Buyable: false}, + }, + } + server.shopRepo = shopRepo + + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfGetFpointExchangeList{AckHandle: 100} + handleMsgMhfGetFpointExchangeList(session, pkt) + + select { + case p := <-session.sendPackets: + if len(p.data) == 0 { + t.Fatal("Empty response") + } + default: + t.Error("No response packet queued") + } +} + +func TestHandleMsgMhfGetFpointExchangeList_ZZMode(t *testing.T) { + server := createMockServer() + server.erupeConfig.RealClientMode = cfg.ZZ + + shopRepo := &mockShopRepo{ + fpointExchanges: []FPointExchange{ + {ID: 1, ItemType: 1, ItemID: 100, Quantity: 5, FPoints: 10, Buyable: true}, + }, + } + server.shopRepo = shopRepo + + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfGetFpointExchangeList{AckHandle: 100} + handleMsgMhfGetFpointExchangeList(session, pkt) + + select { + case p := <-session.sendPackets: + if len(p.data) == 0 { + t.Fatal("Empty response") + } + default: + t.Error("No response packet queued") + } +} diff --git a/server/channelserver/repo_mocks_test.go b/server/channelserver/repo_mocks_test.go index e1cf43600..df49fc6d8 100644 --- a/server/channelserver/repo_mocks_test.go +++ b/server/channelserver/repo_mocks_test.go @@ -110,9 +110,10 @@ type mockCharacterRepo struct { strings map[string]string bools map[string]bool - adjustErr error - readErr error - saveErr error + adjustErr error + readErr error + saveErr error + loadColumnErr error // LoadSaveData mock fields loadSaveDataID uint32 @@ -167,7 +168,12 @@ func (m *mockCharacterRepo) SaveTime(_ uint32, column string, value time.Time) e return m.saveErr } -func (m *mockCharacterRepo) LoadColumn(_ uint32, column string) ([]byte, error) { return m.columns[column], nil } +func (m *mockCharacterRepo) LoadColumn(_ uint32, column string) ([]byte, error) { + if m.loadColumnErr != nil { + return nil, m.loadColumnErr + } + return m.columns[column], nil +} func (m *mockCharacterRepo) SaveColumn(_ uint32, column string, data []byte) error { m.columns[column] = data; return m.saveErr } func (m *mockCharacterRepo) GetName(_ uint32) (string, error) { return "TestChar", nil } func (m *mockCharacterRepo) GetUserID(_ uint32) (uint32, error) { return 1, nil } @@ -735,3 +741,199 @@ func (m *mockHouseRepoForItems) GetWarehouseEquipData(_ uint32, _ uint8) ([]byte func (m *mockHouseRepoForItems) SetWarehouseEquipData(_ uint32, _ uint8, _ []byte) error { return nil } func (m *mockHouseRepoForItems) GetTitles(_ uint32) ([]Title, error) { return nil, nil } func (m *mockHouseRepoForItems) AcquireTitle(_ uint16, _ uint32) error { return nil } + +// --- mockSessionRepo --- + +type mockSessionRepo struct { + validateErr error + bindErr error + clearErr error + updateErr error + + boundToken string + clearedToken string +} + +func (m *mockSessionRepo) ValidateLoginToken(_ string, _ uint32, _ uint32) error { return m.validateErr } +func (m *mockSessionRepo) BindSession(token string, _ uint16, _ uint32) error { + m.boundToken = token + return m.bindErr +} +func (m *mockSessionRepo) ClearSession(token string) error { + m.clearedToken = token + return m.clearErr +} +func (m *mockSessionRepo) UpdatePlayerCount(_ uint16, _ int) error { return m.updateErr } + +// --- mockGachaRepo --- + +type mockGachaRepo struct { + // GetEntryForTransaction + txItemType uint8 + txItemNumber uint16 + txRolls int + txErr error + + // GetRewardPool + rewardPool []GachaEntry + rewardPoolErr error + + // GetItemsForEntry + entryItems map[uint32][]GachaItem + entryItemsErr error + + // GetGuaranteedItems + guaranteedItems []GachaItem + + // Stepup + stepupStep uint8 + stepupTime time.Time + stepupErr error + hasEntryType bool + deletedStepup bool + insertedStep uint8 + + // Box + boxEntryIDs []uint32 + boxEntryIDsErr error + insertedBoxIDs []uint32 + deletedBox bool + + // Shop + gachas []Gacha + listShopErr error + shopType int + allEntries []GachaEntry + allEntriesErr error + weightDivisor float64 + + // FrontierPoints from gacha + addFPErr error +} + +func (m *mockGachaRepo) GetEntryForTransaction(_ uint32, _ uint8) (uint8, uint16, int, error) { + return m.txItemType, m.txItemNumber, m.txRolls, m.txErr +} +func (m *mockGachaRepo) GetRewardPool(_ uint32) ([]GachaEntry, error) { + return m.rewardPool, m.rewardPoolErr +} +func (m *mockGachaRepo) GetItemsForEntry(entryID uint32) ([]GachaItem, error) { + if m.entryItemsErr != nil { + return nil, m.entryItemsErr + } + if m.entryItems != nil { + return m.entryItems[entryID], nil + } + return nil, nil +} +func (m *mockGachaRepo) GetGuaranteedItems(_ uint8, _ uint32) ([]GachaItem, error) { + return m.guaranteedItems, nil +} +func (m *mockGachaRepo) GetStepupStep(_ uint32, _ uint32) (uint8, error) { + return m.stepupStep, m.stepupErr +} +func (m *mockGachaRepo) GetStepupWithTime(_ uint32, _ uint32) (uint8, time.Time, error) { + return m.stepupStep, m.stepupTime, m.stepupErr +} +func (m *mockGachaRepo) HasEntryType(_ uint32, _ uint8) (bool, error) { + return m.hasEntryType, nil +} +func (m *mockGachaRepo) DeleteStepup(_ uint32, _ uint32) error { + m.deletedStepup = true + return nil +} +func (m *mockGachaRepo) InsertStepup(_ uint32, step uint8, _ uint32) error { + m.insertedStep = step + return nil +} +func (m *mockGachaRepo) GetBoxEntryIDs(_ uint32, _ uint32) ([]uint32, error) { + return m.boxEntryIDs, m.boxEntryIDsErr +} +func (m *mockGachaRepo) InsertBoxEntry(_ uint32, entryID uint32, _ uint32) error { + m.insertedBoxIDs = append(m.insertedBoxIDs, entryID) + return nil +} +func (m *mockGachaRepo) DeleteBoxEntries(_ uint32, _ uint32) error { + m.deletedBox = true + return nil +} +func (m *mockGachaRepo) ListShop() ([]Gacha, error) { return m.gachas, m.listShopErr } +func (m *mockGachaRepo) GetShopType(_ uint32) (int, error) { return m.shopType, nil } +func (m *mockGachaRepo) GetAllEntries(_ uint32) ([]GachaEntry, error) { + return m.allEntries, m.allEntriesErr +} +func (m *mockGachaRepo) GetWeightDivisor(_ uint32) (float64, error) { return m.weightDivisor, nil } + +// --- mockShopRepo --- + +type mockShopRepo struct { + shopItems []ShopItem + shopItemsErr error + purchases []shopPurchaseRecord + recordErr error + fpointQuantity int + fpointValue int + fpointItemErr error + fpointExchanges []FPointExchange +} + +type shopPurchaseRecord struct { + charID, itemHash, quantity uint32 +} + +func (m *mockShopRepo) GetShopItems(_ uint8, _ uint32, _ uint32) ([]ShopItem, error) { + return m.shopItems, m.shopItemsErr +} +func (m *mockShopRepo) RecordPurchase(charID, itemHash, quantity uint32) error { + m.purchases = append(m.purchases, shopPurchaseRecord{charID, itemHash, quantity}) + return m.recordErr +} +func (m *mockShopRepo) GetFpointItem(_ uint32) (int, int, error) { + return m.fpointQuantity, m.fpointValue, m.fpointItemErr +} +func (m *mockShopRepo) GetFpointExchangeList() ([]FPointExchange, error) { + return m.fpointExchanges, nil +} + +// --- mockUserRepoGacha (UserRepo with configurable gacha fields) --- + +type mockUserRepoGacha struct { + mockUserRepoForItems + + gachaFP, gachaGP, gachaGT uint32 + trialCoins uint16 + deductTrialErr error + deductPremiumErr error + deductFPErr error + addFPFromGachaErr error + + fpDeductBalance uint32 + fpDeductErr error + fpCreditBalance uint32 + fpCreditErr error + + setLastCharErr error + rights uint32 + rightsErr error +} + +func (m *mockUserRepoGacha) GetGachaPoints(_ uint32) (uint32, uint32, uint32, error) { + return m.gachaFP, m.gachaGP, m.gachaGT, nil +} +func (m *mockUserRepoGacha) GetTrialCoins(_ uint32) (uint16, error) { return m.trialCoins, nil } +func (m *mockUserRepoGacha) DeductTrialCoins(_ uint32, _ uint32) error { return m.deductTrialErr } +func (m *mockUserRepoGacha) DeductPremiumCoins(_ uint32, _ uint32) error { + return m.deductPremiumErr +} +func (m *mockUserRepoGacha) DeductFrontierPoints(_ uint32, _ uint32) error { return m.deductFPErr } +func (m *mockUserRepoGacha) AddFrontierPointsFromGacha(_ uint32, _ uint32, _ uint8) error { + return m.addFPFromGachaErr +} +func (m *mockUserRepoGacha) AdjustFrontierPointsDeduct(_ uint32, _ int) (uint32, error) { + return m.fpDeductBalance, m.fpDeductErr +} +func (m *mockUserRepoGacha) AdjustFrontierPointsCredit(_ uint32, _ int) (uint32, error) { + return m.fpCreditBalance, m.fpCreditErr +} +func (m *mockUserRepoGacha) SetLastCharacter(_ uint32, _ uint32) error { return m.setLastCharErr } +func (m *mockUserRepoGacha) GetRights(_ uint32) (uint32, error) { return m.rights, m.rightsErr }