From 6c0269d21f0480597adbfab2b616e5934b396420 Mon Sep 17 00:00:00 2001 From: Houmgaor Date: Sun, 22 Feb 2026 18:55:31 +0100 Subject: [PATCH] test(channelserver): add unit tests for helpers, kouryou, scenario, seibattle, and distitem handlers Cover previously untested handler files with mock-based unit tests: - handlers_helpers: load/save character data, ack helpers, updateRights - handlers_kouryou: get/add/exchange points with success and error paths - handlers_scenario: scenario counter serialization, 128-entry trim, category exchange flags - handlers_seibattle: all type codes, Earth response format, data size validation - handlers_distitem: enumerate/apply/acquire distributions, description retrieval --- .../channelserver/handlers_distitem_test.go | 296 ++++++++++++++++++ server/channelserver/handlers_helpers_test.go | 211 +++++++++++++ server/channelserver/handlers_kouryou_test.go | 186 +++++++++++ .../channelserver/handlers_scenario_test.go | 177 +++++++++++ .../channelserver/handlers_seibattle_test.go | 228 ++++++++++++++ 5 files changed, 1098 insertions(+) create mode 100644 server/channelserver/handlers_distitem_test.go create mode 100644 server/channelserver/handlers_helpers_test.go create mode 100644 server/channelserver/handlers_kouryou_test.go create mode 100644 server/channelserver/handlers_scenario_test.go create mode 100644 server/channelserver/handlers_seibattle_test.go diff --git a/server/channelserver/handlers_distitem_test.go b/server/channelserver/handlers_distitem_test.go new file mode 100644 index 000000000..257560384 --- /dev/null +++ b/server/channelserver/handlers_distitem_test.go @@ -0,0 +1,296 @@ +package channelserver + +import ( + "encoding/binary" + "errors" + "testing" + "time" + + cfg "erupe-ce/config" + "erupe-ce/network/mhfpacket" +) + +// --- mockDistRepo --- + +type mockDistRepo struct { + distributions []Distribution + listErr error + items map[uint32][]DistributionItem + itemsErr error + description string + descErr error + recordedDist uint32 + recordedChar uint32 + recordErr error +} + +func (m *mockDistRepo) List(_ uint32, _ uint8) ([]Distribution, error) { + return m.distributions, m.listErr +} + +func (m *mockDistRepo) GetItems(distID uint32) ([]DistributionItem, error) { + if m.itemsErr != nil { + return nil, m.itemsErr + } + if m.items != nil { + return m.items[distID], nil + } + return nil, nil +} + +func (m *mockDistRepo) RecordAccepted(distID, charID uint32) error { + m.recordedDist = distID + m.recordedChar = charID + return m.recordErr +} + +func (m *mockDistRepo) GetDescription(_ uint32) (string, error) { + return m.description, m.descErr +} + +func TestHandleMsgMhfEnumerateDistItem_Empty(t *testing.T) { + server := createMockServer() + server.erupeConfig.RealClientMode = cfg.S6 + server.distRepo = &mockDistRepo{} + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfEnumerateDistItem{AckHandle: 100, DistType: 0} + handleMsgMhfEnumerateDistItem(session, pkt) + + select { + case p := <-session.sendPackets: + _, errCode, ackData := parseAckBufData(t, p.data) + if errCode != 0 { + t.Errorf("ErrorCode = %d, want 0", errCode) + } + count := binary.BigEndian.Uint16(ackData[:2]) + if count != 0 { + t.Errorf("dist count = %d, want 0", count) + } + default: + t.Fatal("No response queued") + } +} + +func TestHandleMsgMhfEnumerateDistItem_WithDistributions(t *testing.T) { + server := createMockServer() + server.erupeConfig.RealClientMode = cfg.S6 + server.distRepo = &mockDistRepo{ + distributions: []Distribution{ + { + ID: 1, + Deadline: time.Unix(1000000, 0), + Rights: 0, + TimesAcceptable: 1, + TimesAccepted: 0, + MinHR: 1, + MaxHR: 999, + EventName: "Test", + }, + }, + } + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfEnumerateDistItem{AckHandle: 100, DistType: 0} + handleMsgMhfEnumerateDistItem(session, pkt) + + select { + case p := <-session.sendPackets: + _, _, ackData := parseAckBufData(t, p.data) + count := binary.BigEndian.Uint16(ackData[:2]) + if count != 1 { + t.Errorf("dist count = %d, want 1", count) + } + default: + t.Fatal("No response queued") + } +} + +func TestHandleMsgMhfApplyDistItem_Empty(t *testing.T) { + server := createMockServer() + server.erupeConfig.RealClientMode = cfg.S6 + server.distRepo = &mockDistRepo{} + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfApplyDistItem{ + AckHandle: 100, + DistributionID: 42, + } + handleMsgMhfApplyDistItem(session, pkt) + + select { + case p := <-session.sendPackets: + _, _, ackData := parseAckBufData(t, p.data) + // 4 (distID) + 2 (count=0) = 6 + distID := binary.BigEndian.Uint32(ackData[:4]) + if distID != 42 { + t.Errorf("distID = %d, want 42", distID) + } + itemCount := binary.BigEndian.Uint16(ackData[4:6]) + if itemCount != 0 { + t.Errorf("item count = %d, want 0", itemCount) + } + default: + t.Fatal("No response queued") + } +} + +func TestHandleMsgMhfApplyDistItem_WithItems(t *testing.T) { + server := createMockServer() + server.erupeConfig.RealClientMode = cfg.S6 + server.distRepo = &mockDistRepo{ + items: map[uint32][]DistributionItem{ + 10: { + {ItemType: 1, ID: 100, ItemID: 200, Quantity: 5}, + {ItemType: 2, ID: 101, ItemID: 300, Quantity: 3}, + }, + }, + } + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfApplyDistItem{ + AckHandle: 100, + DistributionID: 10, + } + handleMsgMhfApplyDistItem(session, pkt) + + select { + case p := <-session.sendPackets: + _, _, ackData := parseAckBufData(t, p.data) + itemCount := binary.BigEndian.Uint16(ackData[4:6]) + if itemCount != 2 { + t.Errorf("item count = %d, want 2", itemCount) + } + default: + t.Fatal("No response queued") + } +} + +func TestHandleMsgMhfAcquireDistItem_ZeroID(t *testing.T) { + server := createMockServer() + server.distRepo = &mockDistRepo{} + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfAcquireDistItem{ + AckHandle: 100, + DistributionID: 0, + } + handleMsgMhfAcquireDistItem(session, pkt) + + select { + case p := <-session.sendPackets: + if len(p.data) == 0 { + t.Fatal("Should respond") + } + default: + t.Fatal("No response queued") + } +} + +func TestHandleMsgMhfAcquireDistItem_RecordAccepted(t *testing.T) { + server := createMockServer() + distRepo := &mockDistRepo{ + items: map[uint32][]DistributionItem{ + 5: {}, + }, + } + server.distRepo = distRepo + session := createMockSession(1, server) + session.charID = 42 + + pkt := &mhfpacket.MsgMhfAcquireDistItem{ + AckHandle: 100, + DistributionID: 5, + } + handleMsgMhfAcquireDistItem(session, pkt) + + if distRepo.recordedDist != 5 { + t.Errorf("recorded dist ID = %d, want 5", distRepo.recordedDist) + } + if distRepo.recordedChar != 42 { + t.Errorf("recorded char ID = %d, want 42", distRepo.recordedChar) + } + + select { + case p := <-session.sendPackets: + if len(p.data) == 0 { + t.Fatal("Should respond") + } + default: + t.Fatal("No response queued") + } +} + +func TestHandleMsgMhfAcquireDistItem_RecordError(t *testing.T) { + server := createMockServer() + server.distRepo = &mockDistRepo{ + recordErr: errors.New("db error"), + } + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfAcquireDistItem{ + AckHandle: 100, + DistributionID: 5, + } + handleMsgMhfAcquireDistItem(session, pkt) + + // Should still send success ack + select { + case p := <-session.sendPackets: + if len(p.data) == 0 { + t.Fatal("Should respond") + } + default: + t.Fatal("No response queued") + } +} + +func TestHandleMsgMhfGetDistDescription_Success(t *testing.T) { + server := createMockServer() + server.distRepo = &mockDistRepo{description: "Test event description"} + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfGetDistDescription{ + AckHandle: 100, + DistributionID: 1, + } + handleMsgMhfGetDistDescription(session, pkt) + + select { + case p := <-session.sendPackets: + _, errCode, ackData := parseAckBufData(t, p.data) + if errCode != 0 { + t.Errorf("ErrorCode = %d, want 0", errCode) + } + if len(ackData) == 0 { + t.Fatal("AckData should not be empty") + } + default: + t.Fatal("No response queued") + } +} + +func TestHandleMsgMhfGetDistDescription_Error(t *testing.T) { + server := createMockServer() + server.distRepo = &mockDistRepo{descErr: errors.New("not found")} + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfGetDistDescription{ + AckHandle: 100, + DistributionID: 999, + } + handleMsgMhfGetDistDescription(session, pkt) + + select { + case p := <-session.sendPackets: + _, errCode, ackData := parseAckBufData(t, p.data) + if errCode != 0 { + t.Errorf("ErrorCode = %d, want 0 (still buf succeed)", errCode) + } + if len(ackData) != 4 { + t.Errorf("AckData len = %d, want 4 (fallback)", len(ackData)) + } + default: + t.Fatal("No response queued") + } +} diff --git a/server/channelserver/handlers_helpers_test.go b/server/channelserver/handlers_helpers_test.go new file mode 100644 index 000000000..ff7610ac9 --- /dev/null +++ b/server/channelserver/handlers_helpers_test.go @@ -0,0 +1,211 @@ +package channelserver + +import ( + "errors" + "testing" +) + +func TestLoadCharacterData_Success(t *testing.T) { + server := createMockServer() + charRepo := newMockCharacterRepo() + charRepo.columns["test_col"] = []byte{0xAA, 0xBB, 0xCC} + server.charRepo = charRepo + session := createMockSession(1, server) + + loadCharacterData(session, 100, "test_col", nil) + + select { + case pkt := <-session.sendPackets: + if pkt.data == nil { + t.Fatal("Response packet should have data") + } + default: + t.Fatal("No response packet queued") + } +} + +func TestLoadCharacterData_EmptyUsesDefault(t *testing.T) { + server := createMockServer() + charRepo := newMockCharacterRepo() + server.charRepo = charRepo + session := createMockSession(1, server) + + defaultData := []byte{0x01, 0x02, 0x03} + loadCharacterData(session, 100, "missing_col", defaultData) + + select { + case pkt := <-session.sendPackets: + if pkt.data == nil { + t.Fatal("Response packet should have data") + } + default: + t.Fatal("No response packet queued") + } +} + +func TestLoadCharacterData_Error(t *testing.T) { + server := createMockServer() + charRepo := newMockCharacterRepo() + charRepo.loadColumnErr = errors.New("db error") + server.charRepo = charRepo + session := createMockSession(1, server) + + defaultData := []byte{0xFF} + loadCharacterData(session, 100, "test_col", defaultData) + + // Should still send a response (with default data) + select { + case pkt := <-session.sendPackets: + if pkt.data == nil { + t.Fatal("Response packet should have data even on error") + } + default: + t.Fatal("No response packet queued") + } +} + +func TestSaveCharacterData_Success(t *testing.T) { + server := createMockServer() + charRepo := newMockCharacterRepo() + server.charRepo = charRepo + session := createMockSession(1, server) + + data := []byte{0x01, 0x02, 0x03} + saveCharacterData(session, 100, "test_col", data, 100) + + // Should save and ack + if saved := charRepo.columns["test_col"]; saved == nil { + t.Error("Data should be saved to repo") + } + + select { + case pkt := <-session.sendPackets: + if pkt.data == nil { + t.Fatal("Response packet should have data") + } + default: + t.Fatal("No response packet queued") + } +} + +func TestSaveCharacterData_TooLarge(t *testing.T) { + server := createMockServer() + charRepo := newMockCharacterRepo() + server.charRepo = charRepo + session := createMockSession(1, server) + + data := make([]byte, 200) + saveCharacterData(session, 100, "test_col", data, 50) + + // Should fail with ack + if _, ok := charRepo.columns["test_col"]; ok { + t.Error("Data should NOT be saved when too large") + } + + select { + case pkt := <-session.sendPackets: + if pkt.data == nil { + t.Fatal("Response packet should have data") + } + default: + t.Fatal("Should queue a fail ack") + } +} + +func TestSaveCharacterData_SaveError(t *testing.T) { + server := createMockServer() + charRepo := newMockCharacterRepo() + charRepo.saveErr = errors.New("save failed") + server.charRepo = charRepo + session := createMockSession(1, server) + + data := []byte{0x01} + saveCharacterData(session, 100, "test_col", data, 100) + + // Should still queue a fail ack + select { + case pkt := <-session.sendPackets: + if pkt.data == nil { + t.Fatal("Response packet should have data") + } + default: + t.Fatal("Should queue a fail ack on save error") + } +} + +func TestSaveCharacterData_NoMaxSize(t *testing.T) { + server := createMockServer() + charRepo := newMockCharacterRepo() + server.charRepo = charRepo + session := createMockSession(1, server) + + data := make([]byte, 5000) + saveCharacterData(session, 100, "test_col", data, 0) + + // maxSize=0 means no limit + if saved := charRepo.columns["test_col"]; saved == nil { + t.Error("Data should be saved when maxSize is 0 (no limit)") + } + + select { + case <-session.sendPackets: + default: + t.Fatal("Should queue success ack") + } +} + +func TestDoAckEarthSucceed(t *testing.T) { + server := createMockServer() + server.erupeConfig.EarthID = 42 + session := createMockSession(1, server) + + doAckEarthSucceed(session, 100, nil) + + select { + case pkt := <-session.sendPackets: + if pkt.data == nil { + t.Fatal("Response should have data") + } + default: + t.Fatal("Should queue a packet") + } +} + +func TestUpdateRights(t *testing.T) { + server := createMockServer() + userRepo := &mockUserRepoGacha{} + userRepo.rights = 30 + server.userRepo = userRepo + session := createMockSession(1, server) + + updateRights(session) + + select { + case pkt := <-session.sendPackets: + if pkt.data == nil { + t.Fatal("Should queue MsgSysUpdateRight") + } + default: + t.Fatal("updateRights should queue a packet") + } +} + +func TestUpdateRights_Error(t *testing.T) { + server := createMockServer() + userRepo := &mockUserRepoGacha{rightsErr: errors.New("db error")} + server.userRepo = userRepo + session := createMockSession(1, server) + + // Should not panic, falls back to rights=2 + updateRights(session) + + select { + case pkt := <-session.sendPackets: + if pkt.data == nil { + t.Fatal("Should queue MsgSysUpdateRight even on error") + } + default: + t.Fatal("updateRights should queue a packet even on error") + } +} + diff --git a/server/channelserver/handlers_kouryou_test.go b/server/channelserver/handlers_kouryou_test.go new file mode 100644 index 000000000..0433c25bf --- /dev/null +++ b/server/channelserver/handlers_kouryou_test.go @@ -0,0 +1,186 @@ +package channelserver + +import ( + "encoding/binary" + "errors" + "testing" + + "erupe-ce/network/mhfpacket" +) + +// parseAckBufData extracts AckData from a serialized MsgSysAck buffer response. +// Wire format: opcode(2) + ackHandle(4) + isBuffer(1) + errorCode(1) + dataLen(2) + data(N) +func parseAckBufData(t *testing.T, raw []byte) (ackHandle uint32, errorCode uint8, ackData []byte) { + t.Helper() + if len(raw) < 10 { + t.Fatalf("raw packet too short: %d bytes", len(raw)) + } + ackHandle = binary.BigEndian.Uint32(raw[2:6]) + isBuffer := raw[6] + errorCode = raw[7] + if isBuffer == 0 { + t.Fatal("Expected buffer response, got simple ack") + } + dataLen := binary.BigEndian.Uint16(raw[8:10]) + if int(dataLen) > len(raw)-10 { + t.Fatalf("data len %d exceeds remaining bytes %d", dataLen, len(raw)-10) + } + ackData = raw[10 : 10+dataLen] + return +} + +func TestHandleMsgMhfGetKouryouPoint(t *testing.T) { + server := createMockServer() + charRepo := newMockCharacterRepo() + charRepo.ints["kouryou_point"] = 500 + server.charRepo = charRepo + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfGetKouryouPoint{AckHandle: 100} + handleMsgMhfGetKouryouPoint(session, pkt) + + select { + case p := <-session.sendPackets: + _, errCode, ackData := parseAckBufData(t, p.data) + if errCode != 0 { + t.Errorf("ErrorCode = %d, want 0", errCode) + } + if len(ackData) < 4 { + t.Fatal("AckData too short") + } + points := binary.BigEndian.Uint32(ackData[:4]) + if points != 500 { + t.Errorf("points = %d, want 500", points) + } + default: + t.Fatal("No response queued") + } +} + +func TestHandleMsgMhfGetKouryouPoint_Error(t *testing.T) { + server := createMockServer() + charRepo := newMockCharacterRepo() + charRepo.readErr = errors.New("db error") + server.charRepo = charRepo + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfGetKouryouPoint{AckHandle: 100} + handleMsgMhfGetKouryouPoint(session, pkt) + + select { + case p := <-session.sendPackets: + _, _, ackData := parseAckBufData(t, p.data) + points := binary.BigEndian.Uint32(ackData[:4]) + if points != 0 { + t.Errorf("points = %d, want 0 on error", points) + } + default: + t.Fatal("No response queued") + } +} + +func TestHandleMsgMhfAddKouryouPoint(t *testing.T) { + server := createMockServer() + charRepo := newMockCharacterRepo() + charRepo.ints["kouryou_point"] = 100 + server.charRepo = charRepo + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfAddKouryouPoint{ + AckHandle: 200, + KouryouPoints: 50, + } + handleMsgMhfAddKouryouPoint(session, pkt) + + if charRepo.ints["kouryou_point"] != 150 { + t.Errorf("kouryou_point = %d, want 150", charRepo.ints["kouryou_point"]) + } + + select { + case p := <-session.sendPackets: + _, _, ackData := parseAckBufData(t, p.data) + points := binary.BigEndian.Uint32(ackData[:4]) + if points != 150 { + t.Errorf("response points = %d, want 150", points) + } + default: + t.Fatal("No response queued") + } +} + +func TestHandleMsgMhfAddKouryouPoint_Error(t *testing.T) { + server := createMockServer() + charRepo := newMockCharacterRepo() + charRepo.adjustErr = errors.New("db error") + server.charRepo = charRepo + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfAddKouryouPoint{ + AckHandle: 200, + KouryouPoints: 50, + } + handleMsgMhfAddKouryouPoint(session, pkt) + + select { + case p := <-session.sendPackets: + _, _, ackData := parseAckBufData(t, p.data) + points := binary.BigEndian.Uint32(ackData[:4]) + if points != 0 { + t.Errorf("response points = %d, want 0 on error", points) + } + default: + t.Fatal("No response queued") + } +} + +func TestHandleMsgMhfExchangeKouryouPoint(t *testing.T) { + server := createMockServer() + charRepo := newMockCharacterRepo() + charRepo.ints["kouryou_point"] = 10000 + server.charRepo = charRepo + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfExchangeKouryouPoint{ + AckHandle: 300, + KouryouPoints: 10000, + } + handleMsgMhfExchangeKouryouPoint(session, pkt) + + if charRepo.ints["kouryou_point"] != 0 { + t.Errorf("kouryou_point = %d, want 0 after exchange", charRepo.ints["kouryou_point"]) + } + + select { + case p := <-session.sendPackets: + _, _, ackData := parseAckBufData(t, p.data) + points := binary.BigEndian.Uint32(ackData[:4]) + if points != 0 { + t.Errorf("response points = %d, want 0", points) + } + default: + t.Fatal("No response queued") + } +} + +func TestHandleMsgMhfExchangeKouryouPoint_Error(t *testing.T) { + server := createMockServer() + charRepo := newMockCharacterRepo() + charRepo.adjustErr = errors.New("db error") + server.charRepo = charRepo + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfExchangeKouryouPoint{ + AckHandle: 300, + KouryouPoints: 5000, + } + handleMsgMhfExchangeKouryouPoint(session, pkt) + + select { + case p := <-session.sendPackets: + if len(p.data) == 0 { + t.Fatal("Should still respond on error") + } + default: + t.Fatal("No response queued") + } +} diff --git a/server/channelserver/handlers_scenario_test.go b/server/channelserver/handlers_scenario_test.go new file mode 100644 index 000000000..2eac917fe --- /dev/null +++ b/server/channelserver/handlers_scenario_test.go @@ -0,0 +1,177 @@ +package channelserver + +import ( + "encoding/binary" + "errors" + "testing" + + "erupe-ce/network/mhfpacket" +) + +// --- mockScenarioRepo --- + +type mockScenarioRepo struct { + scenarios []Scenario + err error +} + +func (m *mockScenarioRepo) GetCounters() ([]Scenario, error) { + return m.scenarios, m.err +} + +func TestHandleMsgMhfInfoScenarioCounter_Empty(t *testing.T) { + server := createMockServer() + server.scenarioRepo = &mockScenarioRepo{} + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfInfoScenarioCounter{AckHandle: 100} + handleMsgMhfInfoScenarioCounter(session, pkt) + + select { + case p := <-session.sendPackets: + _, errCode, ackData := parseAckBufData(t, p.data) + if errCode != 0 { + t.Errorf("ErrorCode = %d, want 0", errCode) + } + if len(ackData) < 1 { + t.Fatal("AckData too short") + } + if ackData[0] != 0 { + t.Errorf("scenario count = %d, want 0", ackData[0]) + } + default: + t.Fatal("No response queued") + } +} + +func TestHandleMsgMhfInfoScenarioCounter_WithScenarios(t *testing.T) { + server := createMockServer() + server.scenarioRepo = &mockScenarioRepo{ + scenarios: []Scenario{ + {MainID: 1000, CategoryID: 0}, + {MainID: 2000, CategoryID: 3}, + {MainID: 3000, CategoryID: 6}, + {MainID: 4000, CategoryID: 7}, + {MainID: 5000, CategoryID: 1}, + }, + } + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfInfoScenarioCounter{AckHandle: 100} + handleMsgMhfInfoScenarioCounter(session, pkt) + + select { + case p := <-session.sendPackets: + _, _, data := parseAckBufData(t, p.data) + if len(data) < 1 { + t.Fatal("AckData too short") + } + count := data[0] + if count != 5 { + t.Errorf("scenario count = %d, want 5", count) + } + + // Each scenario: mainID(4) + exchange(1) + categoryID(1) = 6 bytes + expectedLen := 1 + 5*6 + if len(data) != expectedLen { + t.Errorf("AckData len = %d, want %d", len(data), expectedLen) + } + + // Verify first scenario (categoryID=0, exchange=false) + mainID := binary.BigEndian.Uint32(data[1:5]) + if mainID != 1000 { + t.Errorf("first mainID = %d, want 1000", mainID) + } + if data[5] != 0 { + t.Errorf("categoryID=0 should have exchange=false, got %d", data[5]) + } + + // Verify second scenario (categoryID=3, exchange=true) + if data[5+6] != 1 { + t.Errorf("categoryID=3 should have exchange=true, got %d", data[5+6]) + } + default: + t.Fatal("No response queued") + } +} + +func TestHandleMsgMhfInfoScenarioCounter_TrimTo128(t *testing.T) { + server := createMockServer() + scenarios := make([]Scenario, 200) + for i := range scenarios { + scenarios[i] = Scenario{MainID: uint32(i), CategoryID: 0} + } + server.scenarioRepo = &mockScenarioRepo{scenarios: scenarios} + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfInfoScenarioCounter{AckHandle: 100} + handleMsgMhfInfoScenarioCounter(session, pkt) + + select { + case p := <-session.sendPackets: + _, _, data := parseAckBufData(t, p.data) + if data[0] != 128 { + t.Errorf("scenario count = %d, want 128 (trimmed)", data[0]) + } + default: + t.Fatal("No response queued") + } +} + +func TestHandleMsgMhfInfoScenarioCounter_DBError(t *testing.T) { + server := createMockServer() + server.scenarioRepo = &mockScenarioRepo{err: errors.New("db error")} + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfInfoScenarioCounter{AckHandle: 100} + handleMsgMhfInfoScenarioCounter(session, pkt) + + select { + case p := <-session.sendPackets: + if len(p.data) == 0 { + t.Fatal("Should still respond on error") + } + default: + t.Fatal("No response queued") + } +} + +func TestHandleMsgMhfInfoScenarioCounter_CategoryExchangeFlags(t *testing.T) { + tests := []struct { + name string + categoryID uint8 + wantExch bool + }{ + {"Basic", 0, false}, + {"Veteran", 1, false}, + {"Other (exchange)", 3, true}, + {"Pallone (exchange)", 6, true}, + {"Diva (exchange)", 7, true}, + {"Unknown category 2", 2, false}, + {"Unknown category 4", 4, false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + server := createMockServer() + server.scenarioRepo = &mockScenarioRepo{ + scenarios: []Scenario{{MainID: 1, CategoryID: tt.categoryID}}, + } + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfInfoScenarioCounter{AckHandle: 100} + handleMsgMhfInfoScenarioCounter(session, pkt) + + select { + case p := <-session.sendPackets: + _, _, data := parseAckBufData(t, p.data) + isExchange := data[5] != 0 + if isExchange != tt.wantExch { + t.Errorf("exchange = %v, want %v for categoryID=%d", isExchange, tt.wantExch, tt.categoryID) + } + default: + t.Fatal("No response queued") + } + }) + } +} diff --git a/server/channelserver/handlers_seibattle_test.go b/server/channelserver/handlers_seibattle_test.go new file mode 100644 index 000000000..6e02141b2 --- /dev/null +++ b/server/channelserver/handlers_seibattle_test.go @@ -0,0 +1,228 @@ +package channelserver + +import ( + "encoding/binary" + "testing" + + "erupe-ce/network/mhfpacket" +) + +func TestHandleMsgMhfGetSeibattle_AllTypes(t *testing.T) { + tests := []struct { + name string + pktType uint8 + }{ + {"Timetable", 1}, + {"KeyScore", 3}, + {"Career", 4}, + {"Opponent", 5}, + {"ConventionResult", 6}, + {"CharScore", 7}, + {"CurResult", 8}, + {"UnknownType", 99}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + server := createMockServer() + server.erupeConfig.EarthID = 1 + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfGetSeibattle{ + AckHandle: 100, + Type: tt.pktType, + } + handleMsgMhfGetSeibattle(session, pkt) + + select { + case p := <-session.sendPackets: + _, errCode, ackData := parseAckBufData(t, p.data) + if errCode != 0 { + t.Errorf("ErrorCode = %d, want 0", errCode) + } + // Earth header: EarthID(4) + 0(4) + 0(4) + count(4) = 16 bytes minimum + if len(ackData) < 16 { + t.Errorf("AckData too short: %d bytes", len(ackData)) + } + earthID := binary.BigEndian.Uint32(ackData[:4]) + if earthID != 1 { + t.Errorf("EarthID = %d, want 1", earthID) + } + default: + t.Fatal("No response queued") + } + }) + } +} + +func TestHandleMsgMhfGetSeibattle_TimetableEntryCount(t *testing.T) { + server := createMockServer() + server.erupeConfig.EarthID = 1 + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfGetSeibattle{ + AckHandle: 100, + Type: 1, // Timetable + } + handleMsgMhfGetSeibattle(session, pkt) + + select { + case p := <-session.sendPackets: + _, _, ackData := parseAckBufData(t, p.data) + count := binary.BigEndian.Uint32(ackData[12:16]) + if count != 3 { + t.Errorf("timetable count = %d, want 3", count) + } + default: + t.Fatal("No response queued") + } +} + +func TestHandleMsgMhfGetBreakSeibatuLevelReward_DataSize(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfGetBreakSeibatuLevelReward{AckHandle: 100} + handleMsgMhfGetBreakSeibatuLevelReward(session, pkt) + + select { + case p := <-session.sendPackets: + _, _, ackData := parseAckBufData(t, p.data) + // 4 × int32 = 16 bytes + if len(ackData) != 16 { + t.Errorf("AckData len = %d, want 16", len(ackData)) + } + default: + t.Fatal("No response queued") + } +} + +func TestHandleMsgMhfGetWeeklySeibatuRankingReward_EarthFormat(t *testing.T) { + server := createMockServer() + server.erupeConfig.EarthID = 42 + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfGetWeeklySeibatuRankingReward{AckHandle: 100} + handleMsgMhfGetWeeklySeibatuRankingReward(session, pkt) + + select { + case p := <-session.sendPackets: + _, _, ackData := parseAckBufData(t, p.data) + earthID := binary.BigEndian.Uint32(ackData[:4]) + if earthID != 42 { + t.Errorf("EarthID = %d, want 42", earthID) + } + count := binary.BigEndian.Uint32(ackData[12:16]) + if count != 1 { + t.Errorf("reward count = %d, want 1", count) + } + default: + t.Fatal("No response queued") + } +} + +func TestHandleMsgMhfGetFixedSeibatuRankingTable_DataSize(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfGetFixedSeibatuRankingTable{AckHandle: 100} + handleMsgMhfGetFixedSeibatuRankingTable(session, pkt) + + select { + case p := <-session.sendPackets: + _, _, ackData := parseAckBufData(t, p.data) + // 4 + 4 + 32 = 40 bytes + if len(ackData) != 40 { + t.Errorf("AckData len = %d, want 40", len(ackData)) + } + default: + t.Fatal("No response queued") + } +} + +func TestHandleMsgMhfReadBeatLevel_VerifyIDEcho(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfReadBeatLevel{ + AckHandle: 100, + ValidIDCount: 2, + IDs: [16]uint32{0x74, 0x6B}, + } + handleMsgMhfReadBeatLevel(session, pkt) + + select { + case p := <-session.sendPackets: + _, _, ackData := parseAckBufData(t, p.data) + // 2 entries × (4+4+4+4) = 32 bytes + if len(ackData) != 32 { + t.Errorf("AckData len = %d, want 32", len(ackData)) + } + firstID := binary.BigEndian.Uint32(ackData[:4]) + if firstID != 0x74 { + t.Errorf("first ID = 0x%x, want 0x74", firstID) + } + secondID := binary.BigEndian.Uint32(ackData[16:20]) + if secondID != 0x6B { + t.Errorf("second ID = 0x%x, want 0x6B", secondID) + } + default: + t.Fatal("No response queued") + } +} + +func TestHandleMsgMhfReadBeatLevelAllRanking_DataSize(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfReadBeatLevelAllRanking{AckHandle: 100} + handleMsgMhfReadBeatLevelAllRanking(session, pkt) + + select { + case p := <-session.sendPackets: + _, _, ackData := parseAckBufData(t, p.data) + // 4+4+4 + 100*(4+4+32) = 4012 bytes + expectedLen := 12 + 100*40 + if len(ackData) != expectedLen { + t.Errorf("AckData len = %d, want %d", len(ackData), expectedLen) + } + default: + t.Fatal("No response queued") + } +} + +func TestHandleMsgMhfReadBeatLevelMyRanking_EmptyResponse(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfReadBeatLevelMyRanking{AckHandle: 100} + handleMsgMhfReadBeatLevelMyRanking(session, pkt) + + select { + case p := <-session.sendPackets: + _, _, ackData := parseAckBufData(t, p.data) + if len(ackData) != 0 { + t.Errorf("AckData len = %d, want 0", len(ackData)) + } + default: + t.Fatal("No response queued") + } +} + +func TestHandleMsgMhfReadLastWeekBeatRanking_DataSize(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfReadLastWeekBeatRanking{AckHandle: 100} + handleMsgMhfReadLastWeekBeatRanking(session, pkt) + + select { + case p := <-session.sendPackets: + _, _, ackData := parseAckBufData(t, p.data) + if len(ackData) != 16 { + t.Errorf("AckData len = %d, want 16", len(ackData)) + } + default: + t.Fatal("No response queued") + } +}