test(channelserver): add handler tests for session, gacha, shop, and plate

Cover critical paths that previously had no test coverage:
- Session: login success/error paths, ping, logkey, record log,
  global sema lock/unlock, rights reload, announce
- Gacha: point queries, coin deduction, item receive with overflow
  and freeze, normal/stepup/box gacha play, stepup status lifecycle,
  weighted random selection
- Shop: enumeration across all shop types, exchange purchases,
  fpoint-to-item and item-to-fpoint exchange, fpoint exchange list
  with Z2 vs ZZ encoding
- Plate: load/save for platedata, platebox, platemyset with
  oversized payload rejection, diff path, and cache invalidation

Add mockSessionRepo, mockGachaRepo, mockShopRepo, and
mockUserRepoGacha to support the new test scenarios. Add
loadColumnErr field to mockCharacterRepo for diff-path error
testing.
This commit is contained in:
Houmgaor
2026-02-22 16:05:25 +01:00
parent db34cb3f85
commit cd630a7a58
5 changed files with 2095 additions and 4 deletions

View File

@@ -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")
}
}

View File

@@ -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")
}
}

View File

@@ -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
}

View File

@@ -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")
}
}

View File

@@ -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 }