test(channelserver): add comprehensive handler-level tests for handlers_house

Cover all 14 handler functions in handlers_house.go with 25 new tests:

- 7 unit tests for guard paths (payload size limits, box index
  bounds, no-op handlers) that run without a database
- 18 integration tests against real PostgreSQL covering interior
  updates, house state/password, house enumeration by char ID and
  name, house loading with access control, mission data CRUD,
  title acquisition with dedup, warehouse operations (box names,
  usage limits, rename guards), item storage round-trips, and
  deco myset defaults

Introduces readAck() helper to parse MsgSysAck wire format from
the sendPackets channel, and setupHouseTest() for DB + session
scaffolding with user_binary row initialization.
This commit is contained in:
Houmgaor
2026-02-20 23:46:04 +01:00
parent 339487c3d8
commit f2f31cdfbb

View File

@@ -1,12 +1,74 @@
package channelserver
import (
"erupe-ce/common/byteframe"
_config "erupe-ce/config"
"erupe-ce/common/mhfitem"
"erupe-ce/common/token"
"erupe-ce/network/mhfpacket"
"testing"
"github.com/jmoiron/sqlx"
)
// ackResponse holds parsed fields from a queued MsgSysAck packet.
type ackResponse struct {
AckHandle uint32
IsBufferResponse bool
ErrorCode uint8
PayloadSize uint
Payload []byte
}
// readAck drains one packet from the session's sendPackets channel and
// parses the MsgSysAck wire format that QueueSendMHF produces.
func readAck(t *testing.T, session *Session) ackResponse {
t.Helper()
select {
case p := <-session.sendPackets:
bf := byteframe.NewByteFrameFromBytes(p.data)
_ = bf.ReadUint16() // opcode
ack := ackResponse{}
ack.AckHandle = bf.ReadUint32()
ack.IsBufferResponse = bf.ReadBool()
ack.ErrorCode = bf.ReadUint8()
size := uint(bf.ReadUint16())
if size == 0xFFFF {
size = uint(bf.ReadUint32())
}
ack.PayloadSize = size
if ack.IsBufferResponse {
ack.Payload = bf.ReadBytes(size)
} else {
ack.Payload = bf.ReadBytes(4)
}
return ack
default:
t.Fatal("No response packet queued")
return ackResponse{}
}
}
// setupHouseTest creates DB, server, session, and a character with user_binary row.
func setupHouseTest(t *testing.T) (*sqlx.DB, *Server, *Session, uint32) {
t.Helper()
db := SetupTestDB(t)
server := createMockServer()
server.erupeConfig.RealClientMode = _config.ZZ
SetTestDB(server, db)
userID := CreateTestUser(t, db, "house_test_user")
charID := CreateTestCharacter(t, db, userID, "HousePlayer")
_, err := db.Exec(`INSERT INTO user_binary (id) VALUES ($1) ON CONFLICT DO NOTHING`, charID)
if err != nil {
t.Fatalf("Failed to create user_binary row: %v", err)
}
session := createMockSession(charID, server)
return db, server, session, charID
}
// createTestEquipment creates properly initialized test equipment
func createTestEquipment(itemIDs []uint16, warehouseIDs []uint32) []mhfitem.MHFEquipment {
var equip []mhfitem.MHFEquipment
@@ -26,6 +88,610 @@ func createTestEquipment(itemIDs []uint16, warehouseIDs []uint32) []mhfitem.MHFE
return equip
}
// =============================================================================
// Unit Tests — guard paths, no database
// =============================================================================
func TestUpdateInterior_PayloadTooLarge(t *testing.T) {
server := createMockServer()
session := createMockSession(1, server)
pkt := &mhfpacket.MsgMhfUpdateInterior{
AckHandle: 1,
InteriorData: make([]byte, 65), // > 64 triggers guard
}
handleMsgMhfUpdateInterior(session, pkt)
ack := readAck(t, session)
if ack.ErrorCode != 0 {
t.Errorf("expected success ACK (guard returns succeed), got error code %d", ack.ErrorCode)
}
}
func TestUpdateMyhouseInfo_PayloadTooLarge(t *testing.T) {
server := createMockServer()
session := createMockSession(1, server)
pkt := &mhfpacket.MsgMhfUpdateMyhouseInfo{
AckHandle: 2,
Data: make([]byte, 513), // > 512 triggers guard
}
handleMsgMhfUpdateMyhouseInfo(session, pkt)
ack := readAck(t, session)
if ack.ErrorCode != 0 {
t.Errorf("expected success ACK on oversized payload, got error code %d", ack.ErrorCode)
}
}
func TestSaveDecoMyset_PayloadTooShort(t *testing.T) {
server := createMockServer()
session := createMockSession(1, server)
pkt := &mhfpacket.MsgMhfSaveDecoMyset{
AckHandle: 3,
RawDataPayload: []byte{0x00, 0x01}, // < 3 bytes
}
handleMsgMhfSaveDecoMyset(session, pkt)
ack := readAck(t, session)
if ack.ErrorCode != 0 {
t.Errorf("expected success ACK on short payload, got error code %d", ack.ErrorCode)
}
}
func TestUpdateWarehouse_BoxIndexTooHigh(t *testing.T) {
server := createMockServer()
session := createMockSession(1, server)
pkt := &mhfpacket.MsgMhfUpdateWarehouse{
AckHandle: 4,
BoxIndex: 11, // > 10 triggers fail
}
handleMsgMhfUpdateWarehouse(session, pkt)
ack := readAck(t, session)
if ack.ErrorCode != 1 {
t.Errorf("expected fail ACK for out-of-bounds box index, got error code %d", ack.ErrorCode)
}
}
func TestEnumerateHouse_Method5_EmptyResult(t *testing.T) {
server := createMockServer()
server.erupeConfig.RealClientMode = _config.ZZ
session := createMockSession(1, server)
pkt := &mhfpacket.MsgMhfEnumerateHouse{
AckHandle: 5,
Method: 5, // Recent visitors — always returns empty
}
handleMsgMhfEnumerateHouse(session, pkt)
ack := readAck(t, session)
if ack.ErrorCode != 0 {
t.Fatalf("expected success, got error code %d", ack.ErrorCode)
}
if !ack.IsBufferResponse {
t.Fatal("expected buffer response")
}
// First 2 bytes = count, should be 0
bf := byteframe.NewByteFrameFromBytes(ack.Payload)
count := bf.ReadUint16()
if count != 0 {
t.Errorf("expected 0 houses for method 5, got %d", count)
}
}
func TestResetTitle_NoOp(t *testing.T) {
// handleMsgMhfResetTitle is an empty function — just verify no panic
defer func() {
if r := recover(); r != nil {
t.Errorf("handleMsgMhfResetTitle panicked: %v", r)
}
}()
handleMsgMhfResetTitle(nil, nil)
}
func TestOperateWarehouse_RenameBoxIndexTooHigh(t *testing.T) {
// Operation 2 = Rename. BoxIndex > 9 should skip the rename.
// This needs a DB for initializeWarehouse, so the full test is the
// integration test TestOperateWarehouse_Op2_RenameBoxIndexTooHigh below.
}
// =============================================================================
// Integration Tests — real PostgreSQL via SetupTestDB
// =============================================================================
func TestUpdateInterior_SavesData(t *testing.T) {
_, _, session, charID := setupHouseTest(t)
interiorData := []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A}
pkt := &mhfpacket.MsgMhfUpdateInterior{
AckHandle: 10,
InteriorData: interiorData,
}
handleMsgMhfUpdateInterior(session, pkt)
ack := readAck(t, session)
if ack.ErrorCode != 0 {
t.Fatalf("expected success, got error code %d", ack.ErrorCode)
}
// Verify data was persisted
_, _, furniture, _, _, _, _, err := session.server.houseRepo.GetHouseContents(charID)
if err != nil {
t.Fatalf("GetHouseContents failed: %v", err)
}
if len(furniture) < len(interiorData) {
t.Fatalf("furniture data too short: got %d bytes", len(furniture))
}
for i, b := range interiorData {
if furniture[i] != b {
t.Errorf("furniture[%d] = %#x, want %#x", i, furniture[i], b)
}
}
}
func TestUpdateHouse_SetsStateAndPassword(t *testing.T) {
_, _, session, charID := setupHouseTest(t)
pkt := &mhfpacket.MsgMhfUpdateHouse{
AckHandle: 11,
State: 3,
Password: "secret",
}
handleMsgMhfUpdateHouse(session, pkt)
ack := readAck(t, session)
if ack.ErrorCode != 0 {
t.Fatalf("expected success, got error code %d", ack.ErrorCode)
}
state, password, err := session.server.houseRepo.GetHouseAccess(charID)
if err != nil {
t.Fatalf("GetHouseAccess failed: %v", err)
}
if state != 3 {
t.Errorf("state = %d, want 3", state)
}
if password != "secret" {
t.Errorf("password = %q, want %q", password, "secret")
}
}
func TestEnumerateHouse_Method4_ByCharID(t *testing.T) {
_, _, session, charID := setupHouseTest(t)
pkt := &mhfpacket.MsgMhfEnumerateHouse{
AckHandle: 12,
Method: 4,
CharID: charID,
}
handleMsgMhfEnumerateHouse(session, pkt)
ack := readAck(t, session)
if ack.ErrorCode != 0 {
t.Fatalf("expected success, got error code %d", ack.ErrorCode)
}
bf := byteframe.NewByteFrameFromBytes(ack.Payload)
count := bf.ReadUint16()
if count != 1 {
t.Errorf("expected 1 house for charID lookup, got %d", count)
}
}
func TestEnumerateHouse_Method3_ByName(t *testing.T) {
_, _, session, _ := setupHouseTest(t)
pkt := &mhfpacket.MsgMhfEnumerateHouse{
AckHandle: 13,
Method: 3,
Name: "HousePlayer",
}
handleMsgMhfEnumerateHouse(session, pkt)
ack := readAck(t, session)
if ack.ErrorCode != 0 {
t.Fatalf("expected success, got error code %d", ack.ErrorCode)
}
bf := byteframe.NewByteFrameFromBytes(ack.Payload)
count := bf.ReadUint16()
if count < 1 {
t.Errorf("expected at least 1 house for name search, got %d", count)
}
}
func TestLoadHouse_OwnHouse_Destination9(t *testing.T) {
_, _, session, charID := setupHouseTest(t)
// Set some interior data first
interior := make([]byte, 20)
interior[0] = 0xAB
_ = session.server.houseRepo.UpdateInterior(charID, interior)
pkt := &mhfpacket.MsgMhfLoadHouse{
AckHandle: 14,
CharID: charID,
Destination: 9, // Own house — bypasses access control
}
handleMsgMhfLoadHouse(session, pkt)
ack := readAck(t, session)
if ack.ErrorCode != 0 {
t.Fatalf("expected success loading own house, got error code %d", ack.ErrorCode)
}
if !ack.IsBufferResponse {
t.Fatal("expected buffer response")
}
if len(ack.Payload) == 0 {
t.Error("expected non-empty house data")
}
}
func TestLoadHouse_WrongPassword_Fails(t *testing.T) {
_, _, session, charID := setupHouseTest(t)
// Set a password on the house
_ = session.server.houseRepo.UpdateHouseState(charID, 2, "correct")
pkt := &mhfpacket.MsgMhfLoadHouse{
AckHandle: 15,
CharID: charID,
Destination: 3, // Others house
CheckPass: true,
Password: "wrong",
}
handleMsgMhfLoadHouse(session, pkt)
ack := readAck(t, session)
if ack.ErrorCode != 1 {
t.Errorf("expected fail ACK for wrong password, got error code %d", ack.ErrorCode)
}
}
func TestLoadHouse_CorrectPassword_Succeeds(t *testing.T) {
_, _, session, charID := setupHouseTest(t)
_ = session.server.houseRepo.UpdateHouseState(charID, 2, "correct")
pkt := &mhfpacket.MsgMhfLoadHouse{
AckHandle: 16,
CharID: charID,
Destination: 3,
CheckPass: true,
Password: "correct",
}
handleMsgMhfLoadHouse(session, pkt)
ack := readAck(t, session)
if ack.ErrorCode != 0 {
t.Errorf("expected success for correct password, got error code %d", ack.ErrorCode)
}
if !ack.IsBufferResponse {
t.Fatal("expected buffer response for house data")
}
}
func TestGetMyhouseInfo_NoData(t *testing.T) {
_, _, session, _ := setupHouseTest(t)
pkt := &mhfpacket.MsgMhfGetMyhouseInfo{AckHandle: 17}
handleMsgMhfGetMyhouseInfo(session, pkt)
ack := readAck(t, session)
if ack.ErrorCode != 0 {
t.Fatalf("expected success, got error code %d", ack.ErrorCode)
}
// When no mission data exists, handler returns 9-byte default
if len(ack.Payload) != 9 {
t.Errorf("expected 9-byte default payload, got %d bytes", len(ack.Payload))
}
}
func TestGetMyhouseInfo_WithData(t *testing.T) {
_, _, session, charID := setupHouseTest(t)
missionData := make([]byte, 50)
missionData[0] = 0xDE
missionData[1] = 0xAD
_ = session.server.houseRepo.UpdateMission(charID, missionData)
pkt := &mhfpacket.MsgMhfGetMyhouseInfo{AckHandle: 18}
handleMsgMhfGetMyhouseInfo(session, pkt)
ack := readAck(t, session)
if ack.ErrorCode != 0 {
t.Fatalf("expected success, got error code %d", ack.ErrorCode)
}
if len(ack.Payload) != 50 {
t.Fatalf("expected 50-byte payload, got %d bytes", len(ack.Payload))
}
if ack.Payload[0] != 0xDE || ack.Payload[1] != 0xAD {
t.Errorf("payload mismatch: got %#x %#x, want 0xDE 0xAD", ack.Payload[0], ack.Payload[1])
}
}
func TestUpdateMyhouseInfo_SavesData(t *testing.T) {
_, _, session, charID := setupHouseTest(t)
missionData := make([]byte, 100)
missionData[0] = 0xCA
missionData[1] = 0xFE
pkt := &mhfpacket.MsgMhfUpdateMyhouseInfo{
AckHandle: 19,
Data: missionData,
}
handleMsgMhfUpdateMyhouseInfo(session, pkt)
ack := readAck(t, session)
if ack.ErrorCode != 0 {
t.Fatalf("expected success, got error code %d", ack.ErrorCode)
}
// Verify via repository
data, err := session.server.houseRepo.GetMission(charID)
if err != nil {
t.Fatalf("GetMission failed: %v", err)
}
if len(data) != 100 {
t.Fatalf("mission data length = %d, want 100", len(data))
}
if data[0] != 0xCA || data[1] != 0xFE {
t.Errorf("mission data mismatch: got %#x %#x, want 0xCA 0xFE", data[0], data[1])
}
}
func TestEnumerateTitle_Empty(t *testing.T) {
_, _, session, _ := setupHouseTest(t)
pkt := &mhfpacket.MsgMhfEnumerateTitle{AckHandle: 20}
handleMsgMhfEnumerateTitle(session, pkt)
ack := readAck(t, session)
if ack.ErrorCode != 0 {
t.Fatalf("expected success, got error code %d", ack.ErrorCode)
}
bf := byteframe.NewByteFrameFromBytes(ack.Payload)
count := bf.ReadUint16()
if count != 0 {
t.Errorf("expected 0 titles, got %d", count)
}
}
func TestAcquireTitle_AndEnumerate(t *testing.T) {
_, _, session, _ := setupHouseTest(t)
// Acquire two titles
acquirePkt := &mhfpacket.MsgMhfAcquireTitle{
AckHandle: 21,
TitleIDs: []uint16{100, 200},
}
handleMsgMhfAcquireTitle(session, acquirePkt)
ack := readAck(t, session)
if ack.ErrorCode != 0 {
t.Fatalf("acquire failed: error code %d", ack.ErrorCode)
}
// Enumerate
enumPkt := &mhfpacket.MsgMhfEnumerateTitle{AckHandle: 22}
handleMsgMhfEnumerateTitle(session, enumPkt)
ack = readAck(t, session)
if ack.ErrorCode != 0 {
t.Fatalf("enumerate failed: error code %d", ack.ErrorCode)
}
bf := byteframe.NewByteFrameFromBytes(ack.Payload)
count := bf.ReadUint16()
if count != 2 {
t.Errorf("expected 2 titles, got %d", count)
}
// Read title IDs
_ = bf.ReadUint16() // unk
ids := make(map[uint16]bool)
for i := 0; i < int(count); i++ {
id := bf.ReadUint16()
ids[id] = true
_ = bf.ReadUint16() // unk
_ = bf.ReadUint32() // acquired timestamp
_ = bf.ReadUint32() // updated timestamp
}
if !ids[100] || !ids[200] {
t.Errorf("expected title IDs 100 and 200, got %v", ids)
}
}
func TestAcquireTitle_Duplicate(t *testing.T) {
_, _, session, charID := setupHouseTest(t)
// Acquire title 300
pkt1 := &mhfpacket.MsgMhfAcquireTitle{AckHandle: 23, TitleIDs: []uint16{300}}
handleMsgMhfAcquireTitle(session, pkt1)
_ = readAck(t, session)
// Acquire same title again
pkt2 := &mhfpacket.MsgMhfAcquireTitle{AckHandle: 24, TitleIDs: []uint16{300}}
handleMsgMhfAcquireTitle(session, pkt2)
_ = readAck(t, session)
// Should still have exactly 1 title (upsert)
titles, err := session.server.houseRepo.GetTitles(charID)
if err != nil {
t.Fatalf("GetTitles failed: %v", err)
}
if len(titles) != 1 {
t.Errorf("expected 1 title after duplicate acquire, got %d", len(titles))
}
}
func TestOperateWarehouse_Op0_GetBoxNames(t *testing.T) {
_, _, session, charID := setupHouseTest(t)
// Initialize warehouse and rename a box
session.server.houseRepo.InitializeWarehouse(charID)
_ = session.server.houseRepo.RenameWarehouseBox(charID, 0, 0, "MyItems")
pkt := &mhfpacket.MsgMhfOperateWarehouse{
AckHandle: 25,
Operation: 0,
}
handleMsgMhfOperateWarehouse(session, pkt)
ack := readAck(t, session)
if ack.ErrorCode != 0 {
t.Fatalf("expected success, got error code %d", ack.ErrorCode)
}
if !ack.IsBufferResponse {
t.Fatal("expected buffer response")
}
// Response format: op(1) + renewal(4) + usages(2) + count(1) + entries
if len(ack.Payload) < 8 {
t.Fatalf("payload too short: %d bytes", len(ack.Payload))
}
bf := byteframe.NewByteFrameFromBytes(ack.Payload)
op := bf.ReadUint8()
if op != 0 {
t.Errorf("op = %d, want 0", op)
}
}
func TestOperateWarehouse_Op3_GetUsageLimit(t *testing.T) {
_, _, session, _ := setupHouseTest(t)
pkt := &mhfpacket.MsgMhfOperateWarehouse{
AckHandle: 26,
Operation: 3,
}
handleMsgMhfOperateWarehouse(session, pkt)
ack := readAck(t, session)
if ack.ErrorCode != 0 {
t.Fatalf("expected success, got error code %d", ack.ErrorCode)
}
// Response: op(1) + renewal_time(4) + usages(2) = 7 bytes
bf := byteframe.NewByteFrameFromBytes(ack.Payload)
op := bf.ReadUint8()
if op != 3 {
t.Errorf("op = %d, want 3", op)
}
renewalTime := bf.ReadUint32()
usages := bf.ReadUint16()
if renewalTime != 0 {
t.Errorf("renewal time = %d, want 0", renewalTime)
}
if usages != 10000 {
t.Errorf("usages = %d, want 10000", usages)
}
}
func TestOperateWarehouse_Op2_RenameBoxIndexTooHigh(t *testing.T) {
_, _, session, _ := setupHouseTest(t)
pkt := &mhfpacket.MsgMhfOperateWarehouse{
AckHandle: 27,
Operation: 2,
BoxIndex: 10, // > 9, rename should be skipped
Name: "ShouldNotRename",
}
handleMsgMhfOperateWarehouse(session, pkt)
ack := readAck(t, session)
if ack.ErrorCode != 0 {
t.Fatalf("expected success ACK even with skipped rename, got error code %d", ack.ErrorCode)
}
}
func TestEnumerateWarehouse_EmptyBox(t *testing.T) {
_, _, session, _ := setupHouseTest(t)
pkt := &mhfpacket.MsgMhfEnumerateWarehouse{
AckHandle: 28,
BoxType: 0, // Items
BoxIndex: 0,
}
handleMsgMhfEnumerateWarehouse(session, pkt)
ack := readAck(t, session)
if ack.ErrorCode != 0 {
t.Fatalf("expected success, got error code %d", ack.ErrorCode)
}
if !ack.IsBufferResponse {
t.Fatal("expected buffer response")
}
// Empty box returns serialized empty list: count(2) + unk(2) = 4 bytes minimum
if len(ack.Payload) < 4 {
t.Errorf("expected at least 4-byte payload for empty box, got %d", len(ack.Payload))
}
}
func TestUpdateWarehouse_Items(t *testing.T) {
_, _, session, charID := setupHouseTest(t)
items := []mhfitem.MHFItemStack{
{Item: mhfitem.MHFItem{ItemID: 42}, Quantity: 10, WarehouseID: token.RNG.Uint32()},
{Item: mhfitem.MHFItem{ItemID: 99}, Quantity: 5, WarehouseID: token.RNG.Uint32()},
}
pkt := &mhfpacket.MsgMhfUpdateWarehouse{
AckHandle: 29,
BoxType: 0,
BoxIndex: 0,
UpdatedItems: items,
}
handleMsgMhfUpdateWarehouse(session, pkt)
ack := readAck(t, session)
if ack.ErrorCode != 0 {
t.Fatalf("expected success, got error code %d", ack.ErrorCode)
}
// Read back via enumerate
session2 := createMockSession(charID, session.server)
enumPkt := &mhfpacket.MsgMhfEnumerateWarehouse{
AckHandle: 30,
BoxType: 0,
BoxIndex: 0,
}
handleMsgMhfEnumerateWarehouse(session2, enumPkt)
ack2 := readAck(t, session2)
if ack2.ErrorCode != 0 {
t.Fatalf("enumerate failed: error code %d", ack2.ErrorCode)
}
// Parse the serialized items
bf := byteframe.NewByteFrameFromBytes(ack2.Payload)
count := bf.ReadUint16()
if count != 2 {
t.Errorf("expected 2 items in warehouse, got %d", count)
}
}
func TestLoadDecoMyset_Default(t *testing.T) {
_, _, session, _ := setupHouseTest(t)
pkt := &mhfpacket.MsgMhfLoadDecoMyset{AckHandle: 31}
handleMsgMhfLoadDecoMyset(session, pkt)
ack := readAck(t, session)
if ack.ErrorCode != 0 {
t.Fatalf("expected success, got error code %d", ack.ErrorCode)
}
if !ack.IsBufferResponse {
t.Fatal("expected buffer response")
}
// G10+ mode returns {0x01, 0x00}
if len(ack.Payload) < 2 {
t.Fatalf("expected at least 2-byte payload, got %d", len(ack.Payload))
}
if ack.Payload[0] != 0x01 || ack.Payload[1] != 0x00 {
t.Errorf("expected default {0x01, 0x00}, got {%#x, %#x}", ack.Payload[0], ack.Payload[1])
}
}
// =============================================================================
// Existing pure-logic tests and benchmarks (unchanged)
// =============================================================================
// TestWarehouseItemSerialization verifies warehouse item serialization
func TestWarehouseItemSerialization(t *testing.T) {
tests := []struct {