mirror of
https://github.com/Mezeporta/Erupe.git
synced 2026-03-22 07:32:32 +01:00
Several handlers discarded errors from rows.Scan() and db.Exec(), masking data corruption or connection issues. Scan failures in diva schedule, event quests, and trend weapons are now logged or returned. InitializeWarehouse now surfaces its insert error to the caller.
1150 lines
30 KiB
Go
1150 lines
30 KiB
Go
package channelserver
|
|
|
|
import (
|
|
"erupe-ce/common/byteframe"
|
|
cfg "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 = cfg.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
|
|
for i, itemID := range itemIDs {
|
|
e := mhfitem.MHFEquipment{
|
|
ItemID: itemID,
|
|
WarehouseID: warehouseIDs[i],
|
|
Decorations: make([]mhfitem.MHFItem, 3),
|
|
Sigils: make([]mhfitem.MHFSigil, 3),
|
|
}
|
|
// Initialize Sigils Effects arrays
|
|
for j := 0; j < 3; j++ {
|
|
e.Sigils[j].Effects = make([]mhfitem.MHFSigilEffect, 3)
|
|
}
|
|
equip = append(equip, e)
|
|
}
|
|
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 = cfg.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 {
|
|
name string
|
|
items []mhfitem.MHFItemStack
|
|
}{
|
|
{
|
|
name: "empty_warehouse",
|
|
items: []mhfitem.MHFItemStack{},
|
|
},
|
|
{
|
|
name: "single_item",
|
|
items: []mhfitem.MHFItemStack{
|
|
{Item: mhfitem.MHFItem{ItemID: 1}, Quantity: 10},
|
|
},
|
|
},
|
|
{
|
|
name: "multiple_items",
|
|
items: []mhfitem.MHFItemStack{
|
|
{Item: mhfitem.MHFItem{ItemID: 1}, Quantity: 10},
|
|
{Item: mhfitem.MHFItem{ItemID: 2}, Quantity: 20},
|
|
{Item: mhfitem.MHFItem{ItemID: 3}, Quantity: 30},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// Serialize
|
|
serialized := mhfitem.SerializeWarehouseItems(tt.items)
|
|
|
|
// Basic validation
|
|
if serialized == nil {
|
|
t.Error("serialization returned nil")
|
|
}
|
|
|
|
// Verify we can work with the serialized data
|
|
if serialized == nil {
|
|
t.Error("invalid serialized length")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestWarehouseEquipmentSerialization verifies warehouse equipment serialization
|
|
func TestWarehouseEquipmentSerialization(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
equipment []mhfitem.MHFEquipment
|
|
}{
|
|
{
|
|
name: "empty_equipment",
|
|
equipment: []mhfitem.MHFEquipment{},
|
|
},
|
|
{
|
|
name: "single_equipment",
|
|
equipment: createTestEquipment([]uint16{100}, []uint32{1}),
|
|
},
|
|
{
|
|
name: "multiple_equipment",
|
|
equipment: createTestEquipment([]uint16{100, 101, 102}, []uint32{1, 2, 3}),
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// Serialize
|
|
serialized := mhfitem.SerializeWarehouseEquipment(tt.equipment, cfg.ZZ)
|
|
|
|
// Basic validation
|
|
if serialized == nil {
|
|
t.Error("serialization returned nil")
|
|
}
|
|
|
|
// Verify we can work with the serialized data
|
|
if serialized == nil {
|
|
t.Error("invalid serialized length")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestWarehouseItemDiff verifies the item diff calculation
|
|
func TestWarehouseItemDiff(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
oldItems []mhfitem.MHFItemStack
|
|
newItems []mhfitem.MHFItemStack
|
|
wantDiff bool
|
|
}{
|
|
{
|
|
name: "no_changes",
|
|
oldItems: []mhfitem.MHFItemStack{{Item: mhfitem.MHFItem{ItemID: 1}, Quantity: 10}},
|
|
newItems: []mhfitem.MHFItemStack{{Item: mhfitem.MHFItem{ItemID: 1}, Quantity: 10}},
|
|
wantDiff: false,
|
|
},
|
|
{
|
|
name: "quantity_changed",
|
|
oldItems: []mhfitem.MHFItemStack{{Item: mhfitem.MHFItem{ItemID: 1}, Quantity: 10}},
|
|
newItems: []mhfitem.MHFItemStack{{Item: mhfitem.MHFItem{ItemID: 1}, Quantity: 15}},
|
|
wantDiff: true,
|
|
},
|
|
{
|
|
name: "item_added",
|
|
oldItems: []mhfitem.MHFItemStack{{Item: mhfitem.MHFItem{ItemID: 1}, Quantity: 10}},
|
|
newItems: []mhfitem.MHFItemStack{
|
|
{Item: mhfitem.MHFItem{ItemID: 1}, Quantity: 10},
|
|
{Item: mhfitem.MHFItem{ItemID: 2}, Quantity: 5},
|
|
},
|
|
wantDiff: true,
|
|
},
|
|
{
|
|
name: "item_removed",
|
|
oldItems: []mhfitem.MHFItemStack{
|
|
{Item: mhfitem.MHFItem{ItemID: 1}, Quantity: 10},
|
|
{Item: mhfitem.MHFItem{ItemID: 2}, Quantity: 5},
|
|
},
|
|
newItems: []mhfitem.MHFItemStack{{Item: mhfitem.MHFItem{ItemID: 1}, Quantity: 10}},
|
|
wantDiff: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
diff := mhfitem.DiffItemStacks(tt.oldItems, tt.newItems)
|
|
|
|
// Verify that diff returns a valid result (not nil)
|
|
if diff == nil {
|
|
t.Error("diff should not be nil")
|
|
}
|
|
|
|
// The diff function returns items where Quantity > 0
|
|
// So with no changes (all same quantity), diff should have same items
|
|
if tt.name == "no_changes" {
|
|
if len(diff) == 0 {
|
|
t.Error("no_changes should return items")
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestWarehouseEquipmentMerge verifies equipment merging logic
|
|
func TestWarehouseEquipmentMerge(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
oldEquip []mhfitem.MHFEquipment
|
|
newEquip []mhfitem.MHFEquipment
|
|
wantMerged int
|
|
}{
|
|
{
|
|
name: "merge_empty",
|
|
oldEquip: []mhfitem.MHFEquipment{},
|
|
newEquip: []mhfitem.MHFEquipment{},
|
|
wantMerged: 0,
|
|
},
|
|
{
|
|
name: "add_new_equipment",
|
|
oldEquip: []mhfitem.MHFEquipment{
|
|
{ItemID: 100, WarehouseID: 1},
|
|
},
|
|
newEquip: []mhfitem.MHFEquipment{
|
|
{ItemID: 101, WarehouseID: 0}, // New item, no warehouse ID yet
|
|
},
|
|
wantMerged: 2, // Old + new
|
|
},
|
|
{
|
|
name: "update_existing_equipment",
|
|
oldEquip: []mhfitem.MHFEquipment{
|
|
{ItemID: 100, WarehouseID: 1},
|
|
},
|
|
newEquip: []mhfitem.MHFEquipment{
|
|
{ItemID: 101, WarehouseID: 1}, // Update existing
|
|
},
|
|
wantMerged: 1, // Updated in place
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// Simulate the merge logic from handleMsgMhfUpdateWarehouse
|
|
var finalEquip []mhfitem.MHFEquipment
|
|
oEquips := tt.oldEquip
|
|
|
|
for _, uEquip := range tt.newEquip {
|
|
exists := false
|
|
for i := range oEquips {
|
|
if oEquips[i].WarehouseID == uEquip.WarehouseID && uEquip.WarehouseID != 0 {
|
|
exists = true
|
|
oEquips[i].ItemID = uEquip.ItemID
|
|
break
|
|
}
|
|
}
|
|
if !exists {
|
|
// Generate new warehouse ID
|
|
uEquip.WarehouseID = token.RNG.Uint32()
|
|
finalEquip = append(finalEquip, uEquip)
|
|
}
|
|
}
|
|
|
|
for _, oEquip := range oEquips {
|
|
if oEquip.ItemID > 0 {
|
|
finalEquip = append(finalEquip, oEquip)
|
|
}
|
|
}
|
|
|
|
// Verify merge result count
|
|
if len(finalEquip) != tt.wantMerged {
|
|
t.Errorf("expected %d merged equipment, got %d", tt.wantMerged, len(finalEquip))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestWarehouseIDGeneration verifies warehouse ID uniqueness
|
|
func TestWarehouseIDGeneration(t *testing.T) {
|
|
// Generate multiple warehouse IDs and verify they're unique
|
|
idCount := 100
|
|
ids := make(map[uint32]bool)
|
|
|
|
for i := 0; i < idCount; i++ {
|
|
id := token.RNG.Uint32()
|
|
if id == 0 {
|
|
t.Error("generated warehouse ID is 0 (invalid)")
|
|
}
|
|
if ids[id] {
|
|
// While collisions are possible with random IDs,
|
|
// they should be extremely rare
|
|
t.Logf("Warning: duplicate warehouse ID generated: %d", id)
|
|
}
|
|
ids[id] = true
|
|
}
|
|
|
|
if len(ids) < idCount*90/100 {
|
|
t.Errorf("too many duplicate IDs: got %d unique out of %d", len(ids), idCount)
|
|
}
|
|
}
|
|
|
|
// TestWarehouseItemRemoval verifies item removal logic
|
|
func TestWarehouseItemRemoval(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
items []mhfitem.MHFItemStack
|
|
removeID uint16
|
|
wantRemain int
|
|
}{
|
|
{
|
|
name: "remove_existing",
|
|
items: []mhfitem.MHFItemStack{
|
|
{Item: mhfitem.MHFItem{ItemID: 1}, Quantity: 10},
|
|
{Item: mhfitem.MHFItem{ItemID: 2}, Quantity: 20},
|
|
},
|
|
removeID: 1,
|
|
wantRemain: 1,
|
|
},
|
|
{
|
|
name: "remove_non_existing",
|
|
items: []mhfitem.MHFItemStack{
|
|
{Item: mhfitem.MHFItem{ItemID: 1}, Quantity: 10},
|
|
},
|
|
removeID: 999,
|
|
wantRemain: 1,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
var remaining []mhfitem.MHFItemStack
|
|
for _, item := range tt.items {
|
|
if item.Item.ItemID != tt.removeID {
|
|
remaining = append(remaining, item)
|
|
}
|
|
}
|
|
|
|
if len(remaining) != tt.wantRemain {
|
|
t.Errorf("expected %d remaining items, got %d", tt.wantRemain, len(remaining))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestWarehouseEquipmentRemoval verifies equipment removal logic
|
|
func TestWarehouseEquipmentRemoval(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
equipment []mhfitem.MHFEquipment
|
|
setZeroID uint32
|
|
wantActive int
|
|
}{
|
|
{
|
|
name: "remove_by_setting_zero",
|
|
equipment: []mhfitem.MHFEquipment{
|
|
{ItemID: 100, WarehouseID: 1},
|
|
{ItemID: 101, WarehouseID: 2},
|
|
},
|
|
setZeroID: 1,
|
|
wantActive: 1,
|
|
},
|
|
{
|
|
name: "all_active",
|
|
equipment: []mhfitem.MHFEquipment{
|
|
{ItemID: 100, WarehouseID: 1},
|
|
{ItemID: 101, WarehouseID: 2},
|
|
},
|
|
setZeroID: 999,
|
|
wantActive: 2,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// Simulate removal by setting ItemID to 0
|
|
equipment := make([]mhfitem.MHFEquipment, len(tt.equipment))
|
|
copy(equipment, tt.equipment)
|
|
|
|
for i := range equipment {
|
|
if equipment[i].WarehouseID == tt.setZeroID {
|
|
equipment[i].ItemID = 0
|
|
}
|
|
}
|
|
|
|
// Count active equipment (ItemID > 0)
|
|
activeCount := 0
|
|
for _, eq := range equipment {
|
|
if eq.ItemID > 0 {
|
|
activeCount++
|
|
}
|
|
}
|
|
|
|
if activeCount != tt.wantActive {
|
|
t.Errorf("expected %d active equipment, got %d", tt.wantActive, activeCount)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestWarehouseBoxIndexValidation verifies box index bounds
|
|
func TestWarehouseBoxIndexValidation(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
boxIndex uint8
|
|
isValid bool
|
|
}{
|
|
{
|
|
name: "box_0",
|
|
boxIndex: 0,
|
|
isValid: true,
|
|
},
|
|
{
|
|
name: "box_1",
|
|
boxIndex: 1,
|
|
isValid: true,
|
|
},
|
|
{
|
|
name: "box_9",
|
|
boxIndex: 9,
|
|
isValid: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// Verify box index is within reasonable bounds
|
|
if tt.isValid && tt.boxIndex > 100 {
|
|
t.Error("box index unreasonably high")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestWarehouseErrorRecovery verifies error handling doesn't corrupt state
|
|
func TestWarehouseErrorRecovery(t *testing.T) {
|
|
t.Run("database_error_handling", func(t *testing.T) {
|
|
// After our fix, database errors should:
|
|
// 1. Be logged with s.logger.Error()
|
|
// 2. Send doAckSimpleFail()
|
|
// 3. Return immediately
|
|
// 4. NOT send doAckSimpleSucceed() (the bug we fixed)
|
|
|
|
// This test documents the expected behavior
|
|
})
|
|
|
|
t.Run("serialization_error_handling", func(t *testing.T) {
|
|
// Test that serialization errors are handled gracefully
|
|
emptyItems := []mhfitem.MHFItemStack{}
|
|
serialized := mhfitem.SerializeWarehouseItems(emptyItems)
|
|
|
|
// Should handle empty gracefully
|
|
if serialized == nil {
|
|
t.Error("serialization of empty items should not return nil")
|
|
}
|
|
})
|
|
}
|
|
|
|
// BenchmarkWarehouseSerialization benchmarks warehouse serialization performance
|
|
func BenchmarkWarehouseSerialization(b *testing.B) {
|
|
items := []mhfitem.MHFItemStack{
|
|
{Item: mhfitem.MHFItem{ItemID: 1}, Quantity: 10},
|
|
{Item: mhfitem.MHFItem{ItemID: 2}, Quantity: 20},
|
|
{Item: mhfitem.MHFItem{ItemID: 3}, Quantity: 30},
|
|
{Item: mhfitem.MHFItem{ItemID: 4}, Quantity: 40},
|
|
{Item: mhfitem.MHFItem{ItemID: 5}, Quantity: 50},
|
|
}
|
|
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
_ = mhfitem.SerializeWarehouseItems(items)
|
|
}
|
|
}
|
|
|
|
// BenchmarkWarehouseEquipmentMerge benchmarks equipment merge performance
|
|
func BenchmarkWarehouseEquipmentMerge(b *testing.B) {
|
|
oldEquip := make([]mhfitem.MHFEquipment, 50)
|
|
for i := range oldEquip {
|
|
oldEquip[i] = mhfitem.MHFEquipment{
|
|
ItemID: uint16(100 + i),
|
|
WarehouseID: uint32(i + 1),
|
|
}
|
|
}
|
|
|
|
newEquip := make([]mhfitem.MHFEquipment, 10)
|
|
for i := range newEquip {
|
|
newEquip[i] = mhfitem.MHFEquipment{
|
|
ItemID: uint16(200 + i),
|
|
WarehouseID: uint32(i + 1),
|
|
}
|
|
}
|
|
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
var finalEquip []mhfitem.MHFEquipment
|
|
oEquips := oldEquip
|
|
|
|
for _, uEquip := range newEquip {
|
|
exists := false
|
|
for j := range oEquips {
|
|
if oEquips[j].WarehouseID == uEquip.WarehouseID {
|
|
exists = true
|
|
oEquips[j].ItemID = uEquip.ItemID
|
|
break
|
|
}
|
|
}
|
|
if !exists {
|
|
finalEquip = append(finalEquip, uEquip)
|
|
}
|
|
}
|
|
|
|
for _, oEquip := range oEquips {
|
|
if oEquip.ItemID > 0 {
|
|
finalEquip = append(finalEquip, oEquip)
|
|
}
|
|
}
|
|
_ = finalEquip // Use finalEquip to avoid unused variable warning
|
|
}
|
|
}
|