mirror of
https://github.com/Mezeporta/Erupe.git
synced 2026-03-24 08:33:41 +01:00
MSG_CA_EXCHANGE_ITEM and MSG_MHF_USE_UD_SHOP_COIN had Parse() returning
"NOT IMPLEMENTED". The dispatch loop in handlePacketGroup treats any
Parse error as a silent drop — no ACK is sent, causing the client to
wait indefinitely (softlock). Reported on 9.3.0-rc1 for forge item
purchases and Hunting Road N-point interactions.
Fix follows the pattern from d27da5e: parse only the AckHandle, return
nil from Parse, and respond with doAckBufFail so the client's error
branch exits cleanly without reading response fields.
2127 lines
52 KiB
Go
2127 lines
52 KiB
Go
package channelserver
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"errors"
|
|
"net"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"erupe-ce/common/byteframe"
|
|
"erupe-ce/common/mhfcourse"
|
|
cfg "erupe-ce/config"
|
|
"erupe-ce/network/clientctx"
|
|
"erupe-ce/network/mhfpacket"
|
|
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
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 := &mockGuildRepo{}
|
|
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.Registry = NewLocalChannelRegistry([]*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")
|
|
}
|
|
}
|
|
|
|
// TestHandleMsgSysPing_DifferentAckHandles verifies ping works with various ack handles.
|
|
func TestHandleMsgSysPing_DifferentAckHandles(t *testing.T) {
|
|
server := createMockServer()
|
|
|
|
ackHandles := []uint32{0, 1, 99999, 0xFFFFFFFF}
|
|
for _, ack := range ackHandles {
|
|
session := createMockSession(1, server)
|
|
pkt := &mhfpacket.MsgSysPing{AckHandle: ack}
|
|
|
|
handleMsgSysPing(session, pkt)
|
|
|
|
select {
|
|
case p := <-session.sendPackets:
|
|
if len(p.data) == 0 {
|
|
t.Errorf("AckHandle=%d: Response packet should have data", ack)
|
|
}
|
|
default:
|
|
t.Errorf("AckHandle=%d: No response packet queued", ack)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestHandleMsgSysTerminalLog_NoEntries verifies the handler works with nil entries.
|
|
func TestHandleMsgSysTerminalLog_NoEntries(t *testing.T) {
|
|
server := createMockServer()
|
|
session := createMockSession(1, server)
|
|
|
|
pkt := &mhfpacket.MsgSysTerminalLog{
|
|
AckHandle: 99999,
|
|
LogID: 0,
|
|
Entries: nil,
|
|
}
|
|
|
|
handleMsgSysTerminalLog(session, pkt)
|
|
|
|
select {
|
|
case p := <-session.sendPackets:
|
|
if len(p.data) == 0 {
|
|
t.Error("Response packet should have data")
|
|
}
|
|
default:
|
|
t.Error("No response packet queued")
|
|
}
|
|
}
|
|
|
|
// TestHandleMsgSysTerminalLog_ManyEntries verifies the handler with many log entries.
|
|
func TestHandleMsgSysTerminalLog_ManyEntries(t *testing.T) {
|
|
server := createMockServer()
|
|
session := createMockSession(1, server)
|
|
|
|
entries := make([]mhfpacket.TerminalLogEntry, 20)
|
|
for i := range entries {
|
|
entries[i] = mhfpacket.TerminalLogEntry{
|
|
Index: uint32(i),
|
|
Type1: uint8(i % 256),
|
|
Type2: uint8((i + 1) % 256),
|
|
}
|
|
}
|
|
|
|
pkt := &mhfpacket.MsgSysTerminalLog{
|
|
AckHandle: 55555,
|
|
LogID: 42,
|
|
Entries: entries,
|
|
}
|
|
|
|
handleMsgSysTerminalLog(session, pkt)
|
|
|
|
select {
|
|
case p := <-session.sendPackets:
|
|
if len(p.data) == 0 {
|
|
t.Error("Response packet should have data")
|
|
}
|
|
default:
|
|
t.Error("No response packet queued")
|
|
}
|
|
}
|
|
|
|
// TestHandleMsgSysTime_MultipleCalls verifies calling time handler repeatedly.
|
|
func TestHandleMsgSysTime_MultipleCalls(t *testing.T) {
|
|
server := createMockServer()
|
|
session := createMockSession(1, server)
|
|
|
|
pkt := &mhfpacket.MsgSysTime{
|
|
GetRemoteTime: false,
|
|
Timestamp: 0,
|
|
}
|
|
|
|
for i := 0; i < 5; i++ {
|
|
handleMsgSysTime(session, pkt)
|
|
}
|
|
|
|
// Should have 5 queued responses
|
|
count := 0
|
|
for {
|
|
select {
|
|
case p := <-session.sendPackets:
|
|
if len(p.data) == 0 {
|
|
t.Error("Response packet should have data")
|
|
}
|
|
count++
|
|
default:
|
|
goto done
|
|
}
|
|
}
|
|
done:
|
|
if count != 5 {
|
|
t.Errorf("Expected 5 queued responses, got %d", count)
|
|
}
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// Tests consolidated from handlers_core_test.go
|
|
|
|
func TestHandleMsgHead(t *testing.T) {
|
|
server := createMockServer()
|
|
session := createMockSession(1, server)
|
|
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
t.Errorf("handleMsgHead panicked: %v", r)
|
|
}
|
|
}()
|
|
|
|
handleMsgHead(session, nil)
|
|
}
|
|
|
|
func TestHandleMsgSysExtendThreshold(t *testing.T) {
|
|
server := createMockServer()
|
|
session := createMockSession(1, server)
|
|
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
t.Errorf("handleMsgSysExtendThreshold panicked: %v", r)
|
|
}
|
|
}()
|
|
|
|
handleMsgSysExtendThreshold(session, nil)
|
|
}
|
|
|
|
func TestHandleMsgSysEnd(t *testing.T) {
|
|
server := createMockServer()
|
|
session := createMockSession(1, server)
|
|
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
t.Errorf("handleMsgSysEnd panicked: %v", r)
|
|
}
|
|
}()
|
|
|
|
handleMsgSysEnd(session, nil)
|
|
}
|
|
|
|
func TestHandleMsgSysNop(t *testing.T) {
|
|
server := createMockServer()
|
|
session := createMockSession(1, server)
|
|
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
t.Errorf("handleMsgSysNop panicked: %v", r)
|
|
}
|
|
}()
|
|
|
|
handleMsgSysNop(session, nil)
|
|
}
|
|
|
|
func TestHandleMsgSysAck(t *testing.T) {
|
|
server := createMockServer()
|
|
session := createMockSession(1, server)
|
|
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
t.Errorf("handleMsgSysAck panicked: %v", r)
|
|
}
|
|
}()
|
|
|
|
handleMsgSysAck(session, nil)
|
|
}
|
|
|
|
func TestHandleMsgCaExchangeItem(t *testing.T) {
|
|
server := createMockServer()
|
|
session := createMockSession(1, server)
|
|
|
|
pkt := &mhfpacket.MsgCaExchangeItem{AckHandle: 1}
|
|
handleMsgCaExchangeItem(session, pkt)
|
|
}
|
|
|
|
func TestHandleMsgMhfServerCommand(t *testing.T) {
|
|
server := createMockServer()
|
|
session := createMockSession(1, server)
|
|
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
t.Errorf("handleMsgMhfServerCommand panicked: %v", r)
|
|
}
|
|
}()
|
|
|
|
handleMsgMhfServerCommand(session, nil)
|
|
}
|
|
|
|
func TestHandleMsgMhfSetLoginwindow(t *testing.T) {
|
|
server := createMockServer()
|
|
session := createMockSession(1, server)
|
|
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
t.Errorf("handleMsgMhfSetLoginwindow panicked: %v", r)
|
|
}
|
|
}()
|
|
|
|
handleMsgMhfSetLoginwindow(session, nil)
|
|
}
|
|
|
|
func TestHandleMsgSysTransBinary(t *testing.T) {
|
|
server := createMockServer()
|
|
session := createMockSession(1, server)
|
|
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
t.Errorf("handleMsgSysTransBinary panicked: %v", r)
|
|
}
|
|
}()
|
|
|
|
handleMsgSysTransBinary(session, nil)
|
|
}
|
|
|
|
func TestHandleMsgSysCollectBinary(t *testing.T) {
|
|
server := createMockServer()
|
|
session := createMockSession(1, server)
|
|
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
t.Errorf("handleMsgSysCollectBinary panicked: %v", r)
|
|
}
|
|
}()
|
|
|
|
handleMsgSysCollectBinary(session, nil)
|
|
}
|
|
|
|
func TestHandleMsgSysGetState(t *testing.T) {
|
|
server := createMockServer()
|
|
session := createMockSession(1, server)
|
|
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
t.Errorf("handleMsgSysGetState panicked: %v", r)
|
|
}
|
|
}()
|
|
|
|
handleMsgSysGetState(session, nil)
|
|
}
|
|
|
|
func TestHandleMsgSysSerialize(t *testing.T) {
|
|
server := createMockServer()
|
|
session := createMockSession(1, server)
|
|
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
t.Errorf("handleMsgSysSerialize panicked: %v", r)
|
|
}
|
|
}()
|
|
|
|
handleMsgSysSerialize(session, nil)
|
|
}
|
|
|
|
func TestHandleMsgSysEnumlobby(t *testing.T) {
|
|
server := createMockServer()
|
|
session := createMockSession(1, server)
|
|
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
t.Errorf("handleMsgSysEnumlobby panicked: %v", r)
|
|
}
|
|
}()
|
|
|
|
handleMsgSysEnumlobby(session, nil)
|
|
}
|
|
|
|
func TestHandleMsgSysEnumuser(t *testing.T) {
|
|
server := createMockServer()
|
|
session := createMockSession(1, server)
|
|
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
t.Errorf("handleMsgSysEnumuser panicked: %v", r)
|
|
}
|
|
}()
|
|
|
|
handleMsgSysEnumuser(session, nil)
|
|
}
|
|
|
|
func TestHandleMsgSysInfokyserver(t *testing.T) {
|
|
server := createMockServer()
|
|
session := createMockSession(1, server)
|
|
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
t.Errorf("handleMsgSysInfokyserver panicked: %v", r)
|
|
}
|
|
}()
|
|
|
|
handleMsgSysInfokyserver(session, nil)
|
|
}
|
|
|
|
func TestHandleMsgMhfGetCaUniqueID(t *testing.T) {
|
|
server := createMockServer()
|
|
session := createMockSession(1, server)
|
|
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
t.Errorf("handleMsgMhfGetCaUniqueID panicked: %v", r)
|
|
}
|
|
}()
|
|
|
|
handleMsgMhfGetCaUniqueID(session, nil)
|
|
}
|
|
|
|
func TestHandleMsgSysTerminalLog(t *testing.T) {
|
|
server := createMockServer()
|
|
session := createMockSession(1, server)
|
|
|
|
pkt := &mhfpacket.MsgSysTerminalLog{
|
|
AckHandle: 12345,
|
|
LogID: 100,
|
|
Entries: []mhfpacket.TerminalLogEntry{},
|
|
}
|
|
|
|
handleMsgSysTerminalLog(session, pkt)
|
|
|
|
select {
|
|
case p := <-session.sendPackets:
|
|
if len(p.data) == 0 {
|
|
t.Error("Response packet should have data")
|
|
}
|
|
default:
|
|
t.Error("No response packet queued")
|
|
}
|
|
}
|
|
|
|
func TestHandleMsgSysTerminalLog_WithEntries(t *testing.T) {
|
|
server := createMockServer()
|
|
session := createMockSession(1, server)
|
|
|
|
pkt := &mhfpacket.MsgSysTerminalLog{
|
|
AckHandle: 12345,
|
|
LogID: 100,
|
|
Entries: []mhfpacket.TerminalLogEntry{
|
|
{Type1: 1, Type2: 2},
|
|
{Type1: 3, Type2: 4},
|
|
},
|
|
}
|
|
|
|
handleMsgSysTerminalLog(session, pkt)
|
|
|
|
select {
|
|
case p := <-session.sendPackets:
|
|
if len(p.data) == 0 {
|
|
t.Error("Response packet should have data")
|
|
}
|
|
default:
|
|
t.Error("No response packet queued")
|
|
}
|
|
}
|
|
|
|
func TestHandleMsgSysPing(t *testing.T) {
|
|
server := createMockServer()
|
|
session := createMockSession(1, server)
|
|
|
|
pkt := &mhfpacket.MsgSysPing{
|
|
AckHandle: 12345,
|
|
}
|
|
|
|
handleMsgSysPing(session, pkt)
|
|
|
|
select {
|
|
case p := <-session.sendPackets:
|
|
if len(p.data) == 0 {
|
|
t.Error("Response packet should have data")
|
|
}
|
|
default:
|
|
t.Error("No response packet queued")
|
|
}
|
|
}
|
|
|
|
func TestHandleMsgSysTime(t *testing.T) {
|
|
server := createMockServer()
|
|
session := createMockSession(1, server)
|
|
|
|
pkt := &mhfpacket.MsgSysTime{
|
|
GetRemoteTime: true,
|
|
}
|
|
|
|
handleMsgSysTime(session, pkt)
|
|
|
|
select {
|
|
case p := <-session.sendPackets:
|
|
if len(p.data) == 0 {
|
|
t.Error("Response packet should have data")
|
|
}
|
|
default:
|
|
t.Error("No response packet queued")
|
|
}
|
|
}
|
|
|
|
func TestHandleMsgSysIssueLogkey(t *testing.T) {
|
|
server := createMockServer()
|
|
session := createMockSession(1, server)
|
|
|
|
pkt := &mhfpacket.MsgSysIssueLogkey{
|
|
AckHandle: 12345,
|
|
}
|
|
|
|
handleMsgSysIssueLogkey(session, pkt)
|
|
|
|
// Verify logkey was set
|
|
if len(session.logKey) != 16 {
|
|
t.Errorf("logKey length = %d, want 16", len(session.logKey))
|
|
}
|
|
|
|
select {
|
|
case p := <-session.sendPackets:
|
|
if len(p.data) == 0 {
|
|
t.Error("Response packet should have data")
|
|
}
|
|
default:
|
|
t.Error("No response packet queued")
|
|
}
|
|
}
|
|
|
|
func TestHandleMsgSysRecordLog(t *testing.T) {
|
|
server := createMockServer()
|
|
session := createMockSession(1, server)
|
|
|
|
// Setup stage
|
|
stage := NewStage("test_stage")
|
|
session.stage = stage
|
|
stage.reservedClientSlots[session.charID] = true
|
|
|
|
pkt := &mhfpacket.MsgSysRecordLog{
|
|
AckHandle: 12345,
|
|
Data: make([]byte, 256), // Must be large enough for ByteFrame reads (32 offset + 176 uint8s)
|
|
}
|
|
|
|
handleMsgSysRecordLog(session, pkt)
|
|
|
|
// Verify charID removed from reserved slots
|
|
if _, exists := stage.reservedClientSlots[session.charID]; exists {
|
|
t.Error("charID should be removed from reserved slots")
|
|
}
|
|
|
|
select {
|
|
case p := <-session.sendPackets:
|
|
if len(p.data) == 0 {
|
|
t.Error("Response packet should have data")
|
|
}
|
|
default:
|
|
t.Error("No response packet queued")
|
|
}
|
|
}
|
|
|
|
func TestHandleMsgSysUnlockGlobalSema(t *testing.T) {
|
|
server := createMockServer()
|
|
session := createMockSession(1, server)
|
|
|
|
pkt := &mhfpacket.MsgSysUnlockGlobalSema{
|
|
AckHandle: 12345,
|
|
}
|
|
|
|
handleMsgSysUnlockGlobalSema(session, pkt)
|
|
|
|
select {
|
|
case p := <-session.sendPackets:
|
|
if len(p.data) == 0 {
|
|
t.Error("Response packet should have data")
|
|
}
|
|
default:
|
|
t.Error("No response packet queued")
|
|
}
|
|
}
|
|
|
|
func TestHandleMsgSysSetStatus(t *testing.T) {
|
|
server := createMockServer()
|
|
session := createMockSession(1, server)
|
|
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
t.Errorf("handleMsgSysSetStatus panicked: %v", r)
|
|
}
|
|
}()
|
|
|
|
handleMsgSysSetStatus(session, nil)
|
|
}
|
|
|
|
func TestHandleMsgSysEcho(t *testing.T) {
|
|
server := createMockServer()
|
|
session := createMockSession(1, server)
|
|
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
t.Errorf("handleMsgSysEcho panicked: %v", r)
|
|
}
|
|
}()
|
|
|
|
handleMsgSysEcho(session, nil)
|
|
}
|
|
|
|
func TestHandleMsgSysUpdateRight(t *testing.T) {
|
|
server := createMockServer()
|
|
session := createMockSession(1, server)
|
|
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
t.Errorf("handleMsgSysUpdateRight panicked: %v", r)
|
|
}
|
|
}()
|
|
|
|
handleMsgSysUpdateRight(session, nil)
|
|
}
|
|
|
|
func TestHandleMsgSysAuthQuery(t *testing.T) {
|
|
server := createMockServer()
|
|
session := createMockSession(1, server)
|
|
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
t.Errorf("handleMsgSysAuthQuery panicked: %v", r)
|
|
}
|
|
}()
|
|
|
|
handleMsgSysAuthQuery(session, nil)
|
|
}
|
|
|
|
func TestHandleMsgSysAuthTerminal(t *testing.T) {
|
|
server := createMockServer()
|
|
session := createMockSession(1, server)
|
|
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
t.Errorf("handleMsgSysAuthTerminal panicked: %v", r)
|
|
}
|
|
}()
|
|
|
|
handleMsgSysAuthTerminal(session, nil)
|
|
}
|
|
|
|
func TestHandleMsgSysLockGlobalSema_NoMatch(t *testing.T) {
|
|
server := createMockServer()
|
|
server.GlobalID = "test-server"
|
|
server.Registry = NewLocalChannelRegistry([]*Server{})
|
|
session := createMockSession(1, server)
|
|
|
|
pkt := &mhfpacket.MsgSysLockGlobalSema{
|
|
AckHandle: 12345,
|
|
UserIDString: "user123",
|
|
ServerChannelIDString: "channel1",
|
|
}
|
|
|
|
handleMsgSysLockGlobalSema(session, pkt)
|
|
|
|
select {
|
|
case p := <-session.sendPackets:
|
|
if len(p.data) == 0 {
|
|
t.Error("Response packet should have data")
|
|
}
|
|
default:
|
|
t.Error("No response packet queued")
|
|
}
|
|
}
|
|
|
|
func TestHandleMsgSysLockGlobalSema_WithChannel(t *testing.T) {
|
|
server := createMockServer()
|
|
server.GlobalID = "test-server"
|
|
|
|
// Create a mock channel with stages
|
|
channel := &Server{
|
|
GlobalID: "other-server",
|
|
}
|
|
channel.stages.Store("stage_user123", NewStage("stage_user123"))
|
|
server.Registry = NewLocalChannelRegistry([]*Server{channel})
|
|
|
|
session := createMockSession(1, server)
|
|
|
|
pkt := &mhfpacket.MsgSysLockGlobalSema{
|
|
AckHandle: 12345,
|
|
UserIDString: "user123",
|
|
ServerChannelIDString: "channel1",
|
|
}
|
|
|
|
handleMsgSysLockGlobalSema(session, pkt)
|
|
|
|
select {
|
|
case p := <-session.sendPackets:
|
|
if len(p.data) == 0 {
|
|
t.Error("Response packet should have data")
|
|
}
|
|
default:
|
|
t.Error("No response packet queued")
|
|
}
|
|
}
|
|
|
|
func TestHandleMsgSysLockGlobalSema_SameServer(t *testing.T) {
|
|
server := createMockServer()
|
|
server.GlobalID = "test-server"
|
|
|
|
// Create a mock channel with same GlobalID
|
|
channel := &Server{
|
|
GlobalID: "test-server",
|
|
}
|
|
channel.stages.Store("stage_user456", NewStage("stage_user456"))
|
|
server.Registry = NewLocalChannelRegistry([]*Server{channel})
|
|
|
|
session := createMockSession(1, server)
|
|
|
|
pkt := &mhfpacket.MsgSysLockGlobalSema{
|
|
AckHandle: 12345,
|
|
UserIDString: "user456",
|
|
ServerChannelIDString: "channel2",
|
|
}
|
|
|
|
handleMsgSysLockGlobalSema(session, pkt)
|
|
|
|
select {
|
|
case p := <-session.sendPackets:
|
|
if len(p.data) == 0 {
|
|
t.Error("Response packet should have data")
|
|
}
|
|
default:
|
|
t.Error("No response packet queued")
|
|
}
|
|
}
|
|
|
|
func TestHandleMsgMhfAnnounce(t *testing.T) {
|
|
server := createMockServer()
|
|
session := createMockSession(1, server)
|
|
|
|
pkt := &mhfpacket.MsgMhfAnnounce{
|
|
AckHandle: 12345,
|
|
IPAddress: 0x7F000001, // 127.0.0.1
|
|
Port: 54001,
|
|
StageID: []byte("test_stage"),
|
|
Data: byteframe.NewByteFrameFromBytes([]byte{0x00}),
|
|
}
|
|
|
|
handleMsgMhfAnnounce(session, pkt)
|
|
|
|
select {
|
|
case p := <-session.sendPackets:
|
|
if len(p.data) == 0 {
|
|
t.Error("Response packet should have data")
|
|
}
|
|
default:
|
|
t.Error("No response packet queued")
|
|
}
|
|
}
|
|
|
|
func TestHandleMsgSysRightsReload(t *testing.T) {
|
|
server := createMockServer()
|
|
session := createMockSession(1, server)
|
|
|
|
pkt := &mhfpacket.MsgSysRightsReload{
|
|
AckHandle: 12345,
|
|
}
|
|
|
|
// This will panic due to nil db, which is expected in test
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
t.Log("Expected panic due to nil database in test")
|
|
}
|
|
}()
|
|
|
|
handleMsgSysRightsReload(session, pkt)
|
|
}
|
|
|
|
// Tests consolidated from handlers_coverage3_test.go
|
|
|
|
func TestEmptyHandlers_HandlersGo(t *testing.T) {
|
|
server := createMockServer()
|
|
session := createMockSession(1, server)
|
|
|
|
tests := []struct {
|
|
name string
|
|
fn func()
|
|
}{
|
|
{"handleMsgSysEcho", func() { handleMsgSysEcho(session, nil) }},
|
|
{"handleMsgSysUpdateRight", func() { handleMsgSysUpdateRight(session, nil) }},
|
|
{"handleMsgSysAuthQuery", func() { handleMsgSysAuthQuery(session, nil) }},
|
|
{"handleMsgSysAuthTerminal", func() { handleMsgSysAuthTerminal(session, nil) }},
|
|
{"handleMsgMhfServerCommand", func() { handleMsgMhfServerCommand(session, nil) }},
|
|
{"handleMsgMhfSetLoginwindow", func() { handleMsgMhfSetLoginwindow(session, nil) }},
|
|
{"handleMsgSysTransBinary", func() { handleMsgSysTransBinary(session, nil) }},
|
|
{"handleMsgSysCollectBinary", func() { handleMsgSysCollectBinary(session, nil) }},
|
|
{"handleMsgSysGetState", func() { handleMsgSysGetState(session, nil) }},
|
|
{"handleMsgSysSerialize", func() { handleMsgSysSerialize(session, nil) }},
|
|
{"handleMsgSysEnumlobby", func() { handleMsgSysEnumlobby(session, nil) }},
|
|
{"handleMsgSysEnumuser", func() { handleMsgSysEnumuser(session, nil) }},
|
|
{"handleMsgSysInfokyserver", func() { handleMsgSysInfokyserver(session, nil) }},
|
|
{"handleMsgMhfGetCaUniqueID", func() { handleMsgMhfGetCaUniqueID(session, nil) }},
|
|
{"handleMsgSysSetStatus", func() { handleMsgSysSetStatus(session, nil) }},
|
|
{"handleMsgMhfStampcardPrize", func() { handleMsgMhfStampcardPrize(session, nil) }},
|
|
{"handleMsgMhfKickExportForce", func() { handleMsgMhfKickExportForce(session, nil) }},
|
|
{"handleMsgMhfRegistSpabiTime", func() { handleMsgMhfRegistSpabiTime(session, nil) }},
|
|
{"handleMsgMhfDebugPostValue", func() { handleMsgMhfDebugPostValue(session, nil) }},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
t.Errorf("%s panicked: %v", tt.name, r)
|
|
}
|
|
}()
|
|
tt.fn()
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestEmptyHandlers_Concurrent(t *testing.T) {
|
|
server := createMockServer()
|
|
|
|
handlers := []func(*Session, mhfpacket.MHFPacket){
|
|
handleMsgSysEcho,
|
|
handleMsgSysUpdateRight,
|
|
handleMsgSysAuthQuery,
|
|
handleMsgSysAuthTerminal,
|
|
handleMsgMhfServerCommand,
|
|
handleMsgMhfSetLoginwindow,
|
|
handleMsgSysTransBinary,
|
|
handleMsgSysCollectBinary,
|
|
handleMsgSysGetState,
|
|
handleMsgSysSerialize,
|
|
handleMsgSysEnumlobby,
|
|
handleMsgSysEnumuser,
|
|
handleMsgSysInfokyserver,
|
|
handleMsgMhfGetCaUniqueID,
|
|
handleMsgSysSetStatus,
|
|
handleMsgSysDeleteObject,
|
|
handleMsgSysRotateObject,
|
|
handleMsgSysDuplicateObject,
|
|
handleMsgSysGetObjectBinary,
|
|
handleMsgSysGetObjectOwner,
|
|
handleMsgSysUpdateObjectBinary,
|
|
handleMsgSysCleanupObject,
|
|
handleMsgMhfShutClient,
|
|
handleMsgSysHideClient,
|
|
handleMsgSysStageDestruct,
|
|
}
|
|
|
|
var wg sync.WaitGroup
|
|
for _, h := range handlers {
|
|
for i := 0; i < 10; i++ {
|
|
wg.Add(1)
|
|
go func(handler func(*Session, mhfpacket.MHFPacket)) {
|
|
defer wg.Done()
|
|
session := createMockSession(1, server)
|
|
handler(session, nil)
|
|
}(h)
|
|
}
|
|
}
|
|
wg.Wait()
|
|
}
|
|
|
|
func TestSimpleHandlers_PingAndTime(t *testing.T) {
|
|
server := createMockServer()
|
|
|
|
t.Run("handleMsgSysPing", func(t *testing.T) {
|
|
session := createMockSession(1, server)
|
|
handleMsgSysPing(session, &mhfpacket.MsgSysPing{AckHandle: 1})
|
|
select {
|
|
case p := <-session.sendPackets:
|
|
if len(p.data) == 0 {
|
|
t.Error("response should have data")
|
|
}
|
|
default:
|
|
t.Error("no response queued")
|
|
}
|
|
})
|
|
|
|
t.Run("handleMsgSysTime", func(t *testing.T) {
|
|
session := createMockSession(1, server)
|
|
handleMsgSysTime(session, &mhfpacket.MsgSysTime{})
|
|
select {
|
|
case p := <-session.sendPackets:
|
|
if len(p.data) == 0 {
|
|
t.Error("response should have data")
|
|
}
|
|
default:
|
|
t.Error("no response queued")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestHandleMsgSysIssueLogkey_Coverage3(t *testing.T) {
|
|
server := createMockServer()
|
|
|
|
t.Run("generates_logkey", func(t *testing.T) {
|
|
session := createMockSession(1, server)
|
|
handleMsgSysIssueLogkey(session, &mhfpacket.MsgSysIssueLogkey{AckHandle: 1})
|
|
select {
|
|
case p := <-session.sendPackets:
|
|
if len(p.data) == 0 {
|
|
t.Error("response should have data")
|
|
}
|
|
default:
|
|
t.Error("no response queued")
|
|
}
|
|
if session.logKey == nil {
|
|
t.Error("logKey should be set after IssueLogkey")
|
|
}
|
|
if len(session.logKey) != 16 {
|
|
t.Errorf("logKey length = %d, want 16", len(session.logKey))
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestHandleMsgSysUnlockGlobalSema_Coverage3(t *testing.T) {
|
|
server := createMockServer()
|
|
|
|
t.Run("produces_response", func(t *testing.T) {
|
|
session := createMockSession(1, server)
|
|
handleMsgSysUnlockGlobalSema(session, &mhfpacket.MsgSysUnlockGlobalSema{AckHandle: 1})
|
|
select {
|
|
case p := <-session.sendPackets:
|
|
if len(p.data) == 0 {
|
|
t.Error("response should have data")
|
|
}
|
|
default:
|
|
t.Error("no response queued")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestHandleMsgSysLockGlobalSema_Coverage3(t *testing.T) {
|
|
server := createMockServer()
|
|
server.Registry = NewLocalChannelRegistry(make([]*Server, 0))
|
|
|
|
t.Run("no_channels_returns_response", func(t *testing.T) {
|
|
session := createMockSession(1, server)
|
|
handleMsgSysLockGlobalSema(session, &mhfpacket.MsgSysLockGlobalSema{
|
|
AckHandle: 1,
|
|
UserIDString: "testuser",
|
|
ServerChannelIDString: "ch1",
|
|
})
|
|
select {
|
|
case p := <-session.sendPackets:
|
|
if len(p.data) == 0 {
|
|
t.Error("response should have data")
|
|
}
|
|
default:
|
|
t.Error("no response queued")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestHandlersConcurrentInvocations(t *testing.T) {
|
|
server := createMockServer()
|
|
|
|
done := make(chan struct{})
|
|
const numGoroutines = 10
|
|
|
|
for i := 0; i < numGoroutines; i++ {
|
|
go func(id uint32) {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
t.Errorf("goroutine %d panicked: %v", id, r)
|
|
}
|
|
done <- struct{}{}
|
|
}()
|
|
|
|
session := createMockSession(id, server)
|
|
|
|
// Run several handlers concurrently
|
|
handleMsgSysPing(session, &mhfpacket.MsgSysPing{AckHandle: id})
|
|
<-session.sendPackets
|
|
|
|
handleMsgSysTime(session, &mhfpacket.MsgSysTime{GetRemoteTime: true})
|
|
<-session.sendPackets
|
|
|
|
handleMsgSysIssueLogkey(session, &mhfpacket.MsgSysIssueLogkey{AckHandle: id})
|
|
<-session.sendPackets
|
|
|
|
handleMsgMhfMercenaryHuntdata(session, &mhfpacket.MsgMhfMercenaryHuntdata{AckHandle: id, RequestType: 1})
|
|
<-session.sendPackets
|
|
|
|
handleMsgMhfEnumerateMercenaryLog(session, &mhfpacket.MsgMhfEnumerateMercenaryLog{AckHandle: id})
|
|
<-session.sendPackets
|
|
}(uint32(i + 100))
|
|
}
|
|
|
|
for i := 0; i < numGoroutines; i++ {
|
|
<-done
|
|
}
|
|
}
|
|
|
|
func TestHandleMsgSysRecordLog_RemovesReservation(t *testing.T) {
|
|
server := createMockServer()
|
|
session := createMockSession(1, server)
|
|
|
|
stage := NewStage("test_stage_record")
|
|
session.stage = stage
|
|
stage.reservedClientSlots[session.charID] = true
|
|
|
|
pkt := &mhfpacket.MsgSysRecordLog{
|
|
AckHandle: 55555,
|
|
Data: make([]byte, 256),
|
|
}
|
|
|
|
handleMsgSysRecordLog(session, pkt)
|
|
|
|
if _, exists := stage.reservedClientSlots[session.charID]; exists {
|
|
t.Error("charID should be removed from reserved slots after record log")
|
|
}
|
|
|
|
select {
|
|
case p := <-session.sendPackets:
|
|
if len(p.data) == 0 {
|
|
t.Error("Response packet should have data")
|
|
}
|
|
default:
|
|
t.Error("No response packet queued")
|
|
}
|
|
}
|
|
|
|
func TestHandleMsgSysRecordLog_NoExistingReservation(t *testing.T) {
|
|
server := createMockServer()
|
|
session := createMockSession(1, server)
|
|
|
|
stage := NewStage("test_stage_no_reservation")
|
|
session.stage = stage
|
|
// No reservation exists for this charID
|
|
|
|
pkt := &mhfpacket.MsgSysRecordLog{
|
|
AckHandle: 55556,
|
|
Data: make([]byte, 256),
|
|
}
|
|
|
|
// Should not panic even if charID is not in reservedClientSlots
|
|
handleMsgSysRecordLog(session, pkt)
|
|
|
|
select {
|
|
case p := <-session.sendPackets:
|
|
if len(p.data) == 0 {
|
|
t.Error("Response packet should have data")
|
|
}
|
|
default:
|
|
t.Error("No response packet queued")
|
|
}
|
|
}
|
|
|
|
func TestHandleMsgSysUnlockGlobalSema_Response(t *testing.T) {
|
|
server := createMockServer()
|
|
session := createMockSession(1, server)
|
|
|
|
pkt := &mhfpacket.MsgSysUnlockGlobalSema{
|
|
AckHandle: 66666,
|
|
}
|
|
|
|
handleMsgSysUnlockGlobalSema(session, pkt)
|
|
|
|
select {
|
|
case p := <-session.sendPackets:
|
|
if len(p.data) == 0 {
|
|
t.Error("Response packet should have data")
|
|
}
|
|
default:
|
|
t.Error("No response packet queued")
|
|
}
|
|
}
|
|
|
|
func TestHandleMsgSysTerminalLog_MultipleEntries(t *testing.T) {
|
|
server := createMockServer()
|
|
session := createMockSession(1, server)
|
|
|
|
pkt := &mhfpacket.MsgSysTerminalLog{
|
|
AckHandle: 12345,
|
|
LogID: 200,
|
|
Entries: []mhfpacket.TerminalLogEntry{
|
|
{Type1: 10, Type2: 20},
|
|
{Type1: 11, Type2: 21},
|
|
{Type1: 12, Type2: 22},
|
|
},
|
|
}
|
|
|
|
handleMsgSysTerminalLog(session, pkt)
|
|
|
|
select {
|
|
case p := <-session.sendPackets:
|
|
if len(p.data) == 0 {
|
|
t.Error("Response packet should have data")
|
|
}
|
|
default:
|
|
t.Error("No response packet queued")
|
|
}
|
|
}
|
|
|
|
func TestHandleMsgSysTerminalLog_ZeroLogID(t *testing.T) {
|
|
server := createMockServer()
|
|
session := createMockSession(1, server)
|
|
|
|
pkt := &mhfpacket.MsgSysTerminalLog{
|
|
AckHandle: 12345,
|
|
LogID: 0,
|
|
Entries: []mhfpacket.TerminalLogEntry{},
|
|
}
|
|
|
|
handleMsgSysTerminalLog(session, pkt)
|
|
|
|
select {
|
|
case p := <-session.sendPackets:
|
|
if len(p.data) == 0 {
|
|
t.Error("Response packet should have data")
|
|
}
|
|
default:
|
|
t.Error("No response packet queued")
|
|
}
|
|
}
|
|
|
|
func TestHandleMsgSysPing_DifferentAckHandle(t *testing.T) {
|
|
server := createMockServer()
|
|
session := createMockSession(1, server)
|
|
|
|
pkt := &mhfpacket.MsgSysPing{
|
|
AckHandle: 0xFFFFFFFF,
|
|
}
|
|
|
|
handleMsgSysPing(session, pkt)
|
|
|
|
select {
|
|
case p := <-session.sendPackets:
|
|
if len(p.data) == 0 {
|
|
t.Error("Response packet should have data")
|
|
}
|
|
default:
|
|
t.Error("No response packet queued")
|
|
}
|
|
}
|
|
|
|
func TestHandleMsgSysTime_GetRemoteTimeFalse(t *testing.T) {
|
|
server := createMockServer()
|
|
session := createMockSession(1, server)
|
|
|
|
pkt := &mhfpacket.MsgSysTime{
|
|
GetRemoteTime: false,
|
|
}
|
|
|
|
handleMsgSysTime(session, pkt)
|
|
|
|
select {
|
|
case p := <-session.sendPackets:
|
|
if len(p.data) == 0 {
|
|
t.Error("Response packet should have data")
|
|
}
|
|
default:
|
|
t.Error("No response packet queued")
|
|
}
|
|
}
|
|
|
|
func TestHandleMsgSysIssueLogkey_LogKeyGenerated(t *testing.T) {
|
|
server := createMockServer()
|
|
session := createMockSession(1, server)
|
|
|
|
pkt := &mhfpacket.MsgSysIssueLogkey{
|
|
AckHandle: 77777,
|
|
}
|
|
|
|
handleMsgSysIssueLogkey(session, pkt)
|
|
|
|
// Verify that the logKey was set on the session
|
|
session.Lock()
|
|
keyLen := len(session.logKey)
|
|
session.Unlock()
|
|
|
|
if keyLen != 16 {
|
|
t.Errorf("logKey length = %d, want 16", keyLen)
|
|
}
|
|
|
|
select {
|
|
case p := <-session.sendPackets:
|
|
if len(p.data) == 0 {
|
|
t.Error("Response packet should have data")
|
|
}
|
|
default:
|
|
t.Error("No response packet queued")
|
|
}
|
|
}
|
|
|
|
func TestHandleMsgSysIssueLogkey_Uniqueness(t *testing.T) {
|
|
server := createMockServer()
|
|
|
|
// Generate two logkeys and verify they differ
|
|
session1 := createMockSession(1, server)
|
|
session2 := createMockSession(2, server)
|
|
|
|
pkt1 := &mhfpacket.MsgSysIssueLogkey{AckHandle: 1}
|
|
pkt2 := &mhfpacket.MsgSysIssueLogkey{AckHandle: 2}
|
|
|
|
handleMsgSysIssueLogkey(session1, pkt1)
|
|
handleMsgSysIssueLogkey(session2, pkt2)
|
|
|
|
// Drain send packets
|
|
<-session1.sendPackets
|
|
<-session2.sendPackets
|
|
|
|
session1.Lock()
|
|
key1 := make([]byte, len(session1.logKey))
|
|
copy(key1, session1.logKey)
|
|
session1.Unlock()
|
|
|
|
session2.Lock()
|
|
key2 := make([]byte, len(session2.logKey))
|
|
copy(key2, session2.logKey)
|
|
session2.Unlock()
|
|
|
|
if len(key1) != 16 || len(key2) != 16 {
|
|
t.Fatalf("logKeys should be 16 bytes each, got %d and %d", len(key1), len(key2))
|
|
}
|
|
|
|
same := true
|
|
for i := range key1 {
|
|
if key1[i] != key2[i] {
|
|
same = false
|
|
break
|
|
}
|
|
}
|
|
if same {
|
|
t.Error("Two generated logkeys should differ (extremely unlikely to be the same)")
|
|
}
|
|
}
|
|
|
|
func TestMultipleHandlersOnSameSession(t *testing.T) {
|
|
server := createMockServer()
|
|
session := createMockSession(1, server)
|
|
|
|
// Call multiple handlers in sequence
|
|
handleMsgSysPing(session, &mhfpacket.MsgSysPing{AckHandle: 1})
|
|
select {
|
|
case <-session.sendPackets:
|
|
default:
|
|
t.Fatal("Expected packet from Ping handler")
|
|
}
|
|
|
|
handleMsgSysTime(session, &mhfpacket.MsgSysTime{GetRemoteTime: true})
|
|
select {
|
|
case <-session.sendPackets:
|
|
default:
|
|
t.Fatal("Expected packet from Time handler")
|
|
}
|
|
|
|
handleMsgMhfRegisterEvent(session, &mhfpacket.MsgMhfRegisterEvent{AckHandle: 2, WorldID: 5, LandID: 10})
|
|
select {
|
|
case <-session.sendPackets:
|
|
default:
|
|
t.Fatal("Expected packet from RegisterEvent handler")
|
|
}
|
|
|
|
handleMsgMhfReleaseEvent(session, &mhfpacket.MsgMhfReleaseEvent{AckHandle: 3})
|
|
select {
|
|
case <-session.sendPackets:
|
|
default:
|
|
t.Fatal("Expected packet from ReleaseEvent handler")
|
|
}
|
|
|
|
handleMsgMhfEnumerateEvent(session, &mhfpacket.MsgMhfEnumerateEvent{AckHandle: 4})
|
|
select {
|
|
case <-session.sendPackets:
|
|
default:
|
|
t.Fatal("Expected packet from EnumerateEvent handler")
|
|
}
|
|
|
|
handleMsgMhfSetCaAchievementHist(session, &mhfpacket.MsgMhfSetCaAchievementHist{AckHandle: 5})
|
|
select {
|
|
case <-session.sendPackets:
|
|
default:
|
|
t.Fatal("Expected packet from SetCaAchievementHist handler")
|
|
}
|
|
|
|
handleMsgMhfGetRengokuRankingRank(session, &mhfpacket.MsgMhfGetRengokuRankingRank{AckHandle: 6})
|
|
select {
|
|
case <-session.sendPackets:
|
|
default:
|
|
t.Fatal("Expected packet from GetRengokuRankingRank handler")
|
|
}
|
|
}
|
|
|
|
func TestEmptyHandlers_MiscFiles_Session(t *testing.T) {
|
|
server := createMockServer()
|
|
session := createMockSession(1, server)
|
|
|
|
tests := []struct {
|
|
name string
|
|
fn func()
|
|
}{
|
|
{"handleMsgHead", func() { handleMsgHead(session, nil) }},
|
|
{"handleMsgSysExtendThreshold", func() { handleMsgSysExtendThreshold(session, nil) }},
|
|
{"handleMsgSysEnd", func() { handleMsgSysEnd(session, nil) }},
|
|
{"handleMsgSysNop", func() { handleMsgSysNop(session, nil) }},
|
|
{"handleMsgSysAck", func() { handleMsgSysAck(session, nil) }},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
t.Errorf("%s panicked: %v", tt.name, r)
|
|
}
|
|
}()
|
|
tt.fn()
|
|
})
|
|
}
|
|
}
|
|
|
|
// --- logoutPlayer tests ---
|
|
|
|
// setupLogoutServer creates a server with all repos needed for logoutPlayer.
|
|
func setupLogoutServer() (*Server, *mockCharacterRepo, *mockSessionRepo, *mockGuildRepo) {
|
|
server := createMockServer()
|
|
server.userBinary = NewUserBinaryStore()
|
|
server.semaphore = make(map[string]*Semaphore)
|
|
|
|
charRepo := newMockCharacterRepo()
|
|
server.charRepo = charRepo
|
|
|
|
sessionRepo := &mockSessionRepo{}
|
|
server.sessionRepo = sessionRepo
|
|
|
|
guildRepo := &mockGuildRepo{}
|
|
server.guildRepo = guildRepo
|
|
|
|
return server, charRepo, sessionRepo, guildRepo
|
|
}
|
|
|
|
// setupLogoutSession creates a session registered in the server's sessions map with a mockConn.
|
|
func setupLogoutSession(charID uint32, server *Server) (*Session, *mockConn) {
|
|
logger, _ := zap.NewDevelopment()
|
|
mc := &mockConn{}
|
|
session := &Session{
|
|
charID: charID,
|
|
clientContext: &clientctx.ClientContext{},
|
|
sendPackets: make(chan packet, 20),
|
|
Name: "TestPlayer",
|
|
server: server,
|
|
logger: logger,
|
|
semaphoreID: make([]uint16, 2),
|
|
rawConn: mc,
|
|
token: "test-token",
|
|
sessionStart: time.Now().Unix() - 60, // 60 seconds ago
|
|
}
|
|
server.Lock()
|
|
server.sessions[mc] = session
|
|
server.Unlock()
|
|
return session, mc
|
|
}
|
|
|
|
func TestLogoutPlayer_BasicLogout(t *testing.T) {
|
|
server, _, _, _ := setupLogoutServer()
|
|
session, mc := setupLogoutSession(0, server) // charID=0 → early path, no save
|
|
_ = session
|
|
|
|
logoutPlayer(session)
|
|
|
|
if !mc.WasClosed() {
|
|
t.Error("Expected connection to be closed")
|
|
}
|
|
|
|
server.Lock()
|
|
_, exists := server.sessions[mc]
|
|
server.Unlock()
|
|
if exists {
|
|
t.Error("Expected session to be removed from server.sessions")
|
|
}
|
|
}
|
|
|
|
func TestLogoutPlayer_WithCharacter(t *testing.T) {
|
|
server, charRepo, sessionRepo, _ := setupLogoutServer()
|
|
|
|
// Set up time_played so RP calc works
|
|
charRepo.ints["time_played"] = 100
|
|
// LoadSaveData returns nil data → saveAllCharacterData gets nil CharacterSaveData → skips
|
|
charRepo.loadSaveDataData = nil
|
|
|
|
session, mc := setupLogoutSession(42, server)
|
|
|
|
logoutPlayer(session)
|
|
|
|
if !mc.WasClosed() {
|
|
t.Error("Expected connection to be closed")
|
|
}
|
|
|
|
// Verify session was cleared (db is nil in mock server, so ClearSession won't run)
|
|
// The important thing is that the function completes without error
|
|
_ = sessionRepo
|
|
}
|
|
|
|
func TestLogoutPlayer_WithCafeCourse(t *testing.T) {
|
|
server, charRepo, _, _ := setupLogoutServer()
|
|
charRepo.ints["time_played"] = 0
|
|
charRepo.loadSaveDataData = nil
|
|
|
|
session, _ := setupLogoutSession(42, server)
|
|
session.courses = []mhfcourse.Course{{ID: 30}} // cafe course
|
|
|
|
logoutPlayer(session)
|
|
|
|
// With cafe course, cafe_time should be adjusted
|
|
if charRepo.ints["cafe_time"] == 0 {
|
|
// Session was 60 seconds, so cafe_time should be ~60
|
|
t.Log("cafe_time was not adjusted (may be zero if session time was very short)")
|
|
}
|
|
}
|
|
|
|
func TestLogoutPlayer_WithStage(t *testing.T) {
|
|
server, charRepo, _, _ := setupLogoutServer()
|
|
charRepo.ints["time_played"] = 0
|
|
charRepo.loadSaveDataData = nil
|
|
|
|
session, _ := setupLogoutSession(42, server)
|
|
|
|
// Create a stage with the session as a client
|
|
stage := NewStage("testStage")
|
|
stage.clients[session] = session.charID
|
|
session.stage = stage
|
|
server.stages.Store("testStage", stage)
|
|
|
|
logoutPlayer(session)
|
|
|
|
// Verify client was removed from stage
|
|
stage.RLock()
|
|
_, clientExists := stage.clients[session]
|
|
stage.RUnlock()
|
|
if clientExists {
|
|
t.Error("Expected session to be removed from stage clients")
|
|
}
|
|
}
|
|
|
|
func TestLogoutPlayer_HostDisconnect(t *testing.T) {
|
|
server, charRepo, _, _ := setupLogoutServer()
|
|
charRepo.ints["time_played"] = 0
|
|
charRepo.loadSaveDataData = nil
|
|
|
|
hostSession, _ := setupLogoutSession(42, server)
|
|
|
|
// Create a quest stage with the host
|
|
stage := NewStage("sl2Qs001")
|
|
stage.host = hostSession
|
|
stage.clients[hostSession] = hostSession.charID
|
|
hostSession.stage = stage
|
|
server.stages.Store("sl2Qs001", stage)
|
|
|
|
// Create a reserved player in a non-quest stage
|
|
reservedSession, _ := setupLogoutSession(99, server)
|
|
reservedStage := NewStage("sl2Ls001")
|
|
reservedSession.stage = reservedStage
|
|
server.stages.Store("sl2Ls001", reservedStage)
|
|
|
|
// Reserve the player in the quest stage
|
|
stage.reservedClientSlots[99] = true
|
|
|
|
logoutPlayer(hostSession)
|
|
|
|
// The reserved player should have received MsgSysStageDestruct
|
|
select {
|
|
case p := <-reservedSession.sendPackets:
|
|
if len(p.data) == 0 {
|
|
t.Error("Expected non-empty destruct packet")
|
|
}
|
|
default:
|
|
t.Error("Expected MsgSysStageDestruct to be queued for reserved player")
|
|
}
|
|
}
|
|
|
|
func TestLogoutPlayer_ReadTimePlayedError(t *testing.T) {
|
|
server, charRepo, _, _ := setupLogoutServer()
|
|
charRepo.readErr = errors.New("db error")
|
|
charRepo.loadSaveDataData = nil
|
|
|
|
session, mc := setupLogoutSession(42, server)
|
|
|
|
// Should not panic — continues logout gracefully
|
|
logoutPlayer(session)
|
|
|
|
if !mc.WasClosed() {
|
|
t.Error("Expected connection to be closed despite ReadInt error")
|
|
}
|
|
}
|
|
|
|
func TestLogoutPlayer_SaveError(t *testing.T) {
|
|
server, charRepo, _, _ := setupLogoutServer()
|
|
charRepo.ints["time_played"] = 0
|
|
charRepo.loadSaveDataErr = errors.New("load error")
|
|
|
|
session, mc := setupLogoutSession(42, server)
|
|
|
|
// Should not panic — continues logout gracefully
|
|
logoutPlayer(session)
|
|
|
|
if !mc.WasClosed() {
|
|
t.Error("Expected connection to be closed despite save error")
|
|
}
|
|
}
|
|
|
|
func TestLogoutPlayer_ConcurrentLogout(t *testing.T) {
|
|
server, charRepo, _, _ := setupLogoutServer()
|
|
charRepo.loadSaveDataData = nil
|
|
|
|
const numSessions = 5
|
|
sessions := make([]*Session, numSessions)
|
|
for i := 0; i < numSessions; i++ {
|
|
sessions[i], _ = setupLogoutSession(uint32(100+i), server)
|
|
}
|
|
|
|
var wg sync.WaitGroup
|
|
for _, s := range sessions {
|
|
wg.Add(1)
|
|
go func(sess *Session) {
|
|
defer wg.Done()
|
|
logoutPlayer(sess)
|
|
}(s)
|
|
}
|
|
wg.Wait()
|
|
|
|
server.Lock()
|
|
remaining := len(server.sessions)
|
|
server.Unlock()
|
|
if remaining != 0 {
|
|
t.Errorf("Expected 0 remaining sessions, got %d", remaining)
|
|
}
|
|
}
|
|
|
|
// --- saveAllCharacterData tests ---
|
|
|
|
func TestSaveAllCharacterData_NilSaveData(t *testing.T) {
|
|
server, charRepo, _, _ := setupLogoutServer()
|
|
charRepo.loadSaveDataData = nil // LoadSaveData returns nil data
|
|
|
|
session, _ := setupLogoutSession(42, server)
|
|
|
|
// When LoadSaveData returns nil data, GetCharacterSaveData returns a
|
|
// CharacterSaveData with nil compSave. Save() then errors because there
|
|
// is no decompressed data to write. This is expected behavior.
|
|
err := saveAllCharacterData(session, 0)
|
|
if err == nil {
|
|
t.Error("Expected error for nil compSave (no decompressed save data)")
|
|
}
|
|
}
|
|
|
|
func TestSaveAllCharacterData_LoadError(t *testing.T) {
|
|
server, charRepo, _, _ := setupLogoutServer()
|
|
charRepo.loadSaveDataErr = errors.New("database down")
|
|
|
|
session, _ := setupLogoutSession(42, server)
|
|
|
|
err := saveAllCharacterData(session, 0)
|
|
if err == nil {
|
|
t.Error("Expected error when LoadSaveData fails")
|
|
}
|
|
}
|
|
|
|
func TestSaveAllCharacterData_RPCapping(t *testing.T) {
|
|
server, charRepo, _, _ := setupLogoutServer()
|
|
server.erupeConfig.GameplayOptions.MaximumRP = 100
|
|
charRepo.loadSaveDataData = nil // nil compSave in the returned CharacterSaveData
|
|
|
|
session, _ := setupLogoutSession(42, server)
|
|
|
|
// Save will error due to nil decompressed data, but the RP capping logic
|
|
// is exercised before Save() is called (verifiable via log output).
|
|
err := saveAllCharacterData(session, 999)
|
|
if err == nil {
|
|
t.Error("Expected error due to nil decompressed save data")
|
|
}
|
|
}
|
|
|
|
func TestSaveAllCharacterData_PlaytimeUpdate(t *testing.T) {
|
|
server, charRepo, _, _ := setupLogoutServer()
|
|
charRepo.loadSaveDataData = nil
|
|
|
|
session, _ := setupLogoutSession(42, server)
|
|
session.playtimeTime = time.Now().Add(-30 * time.Second) // 30 seconds of playtime
|
|
session.playtime = 100 // existing playtime
|
|
|
|
// With nil compSave, GetCharacterSaveData returns a CharacterSaveData with nil compSave.
|
|
// saveAllCharacterData updates playtime on session before calling Save, even if Save fails.
|
|
_ = saveAllCharacterData(session, 0)
|
|
|
|
// Playtime should have been updated on the session (even though Save itself errors
|
|
// due to nil decompressed data, the session.playtime field is updated before Save)
|
|
if session.playtime <= 100 {
|
|
t.Errorf("Expected playtime > 100 after update, got %d", session.playtime)
|
|
}
|
|
}
|
|
|
|
// --- handleMsgMhfTransitMessage tests ---
|
|
|
|
func buildTransitSearchByCharID(charID uint32) []byte {
|
|
bf := byteframe.NewByteFrame()
|
|
bf.WriteUint32(charID)
|
|
return bf.Data()
|
|
}
|
|
|
|
func buildTransitSearchByName(name string, maxResults uint16) []byte {
|
|
bf := byteframe.NewByteFrame()
|
|
bf.WriteUint16(uint16(len(name) + 1)) // term length
|
|
bf.WriteUint16(maxResults)
|
|
bf.WriteUint8(0) // Unk
|
|
bf.WriteNullTerminatedBytes([]byte(name))
|
|
return bf.Data()
|
|
}
|
|
|
|
func buildTransitSearchByLobby(ip net.IP, port uint16, stageID string, maxResults uint16) []byte {
|
|
bf := byteframe.NewByteFrame()
|
|
// IP in little-endian (reversed byte order in the packet)
|
|
bf.WriteUint8(ip[3])
|
|
bf.WriteUint8(ip[2])
|
|
bf.WriteUint8(ip[1])
|
|
bf.WriteUint8(ip[0])
|
|
bf.WriteUint16(port)
|
|
bf.WriteUint16(uint16(len(stageID) + 1)) // term length
|
|
bf.WriteUint16(maxResults)
|
|
bf.WriteUint8(0) // Unk
|
|
bf.WriteNullTerminatedBytes([]byte(stageID))
|
|
return bf.Data()
|
|
}
|
|
|
|
// ackBufDataOffset is the byte offset where the buffer ACK payload begins.
|
|
// Layout: opcode(2) + ackHandle(4) + isBuffer(1) + errorCode(1) + dataLen(2) = 10.
|
|
const ackBufDataOffset = 10
|
|
|
|
func setupTransitServer() *Server {
|
|
server := createMockServer()
|
|
server.userBinary = NewUserBinaryStore()
|
|
server.IP = "192.168.1.100"
|
|
server.Port = 54001
|
|
return server
|
|
}
|
|
|
|
func setupTransitSession(charID uint32, server *Server, remoteIP string) *Session {
|
|
session := createMockSession(charID, server)
|
|
mc := &mockConn{
|
|
remoteAddr: &net.TCPAddr{IP: net.ParseIP(remoteIP), Port: 12345},
|
|
}
|
|
session.rawConn = mc
|
|
|
|
// Register in server.sessions for SearchSessions to find
|
|
server.Lock()
|
|
server.sessions[mc] = session
|
|
server.Unlock()
|
|
|
|
return session
|
|
}
|
|
|
|
func TestTransitMessage_SearchByCharID(t *testing.T) {
|
|
server := setupTransitServer()
|
|
|
|
// Add a target session that will be found
|
|
target := setupTransitSession(42, server, "192.168.1.50")
|
|
target.Name = "TargetPlayer"
|
|
|
|
// The searching session
|
|
searcher := setupTransitSession(1, server, "192.168.1.50")
|
|
|
|
pkt := &mhfpacket.MsgMhfTransitMessage{
|
|
AckHandle: 100,
|
|
SearchType: 1,
|
|
MessageData: buildTransitSearchByCharID(42),
|
|
}
|
|
handleMsgMhfTransitMessage(searcher, pkt)
|
|
|
|
select {
|
|
case p := <-searcher.sendPackets:
|
|
if len(p.data) < ackBufDataOffset+2 {
|
|
t.Fatal("Response too short")
|
|
}
|
|
count := binary.BigEndian.Uint16(p.data[ackBufDataOffset : ackBufDataOffset+2])
|
|
if count != 1 {
|
|
t.Errorf("Expected 1 result, got %d", count)
|
|
}
|
|
default:
|
|
t.Error("No response packet queued")
|
|
}
|
|
}
|
|
|
|
func TestTransitMessage_SearchByCharID_NotFound(t *testing.T) {
|
|
server := setupTransitServer()
|
|
|
|
searcher := setupTransitSession(1, server, "192.168.1.50")
|
|
|
|
pkt := &mhfpacket.MsgMhfTransitMessage{
|
|
AckHandle: 100,
|
|
SearchType: 1,
|
|
MessageData: buildTransitSearchByCharID(9999), // No such charID
|
|
}
|
|
handleMsgMhfTransitMessage(searcher, pkt)
|
|
|
|
select {
|
|
case p := <-searcher.sendPackets:
|
|
if len(p.data) < ackBufDataOffset+2 {
|
|
t.Fatal("Response too short")
|
|
}
|
|
count := binary.BigEndian.Uint16(p.data[ackBufDataOffset : ackBufDataOffset+2])
|
|
if count != 0 {
|
|
t.Errorf("Expected 0 results for non-existent charID, got %d", count)
|
|
}
|
|
default:
|
|
t.Error("No response packet queued")
|
|
}
|
|
}
|
|
|
|
func TestTransitMessage_SearchByName(t *testing.T) {
|
|
server := setupTransitServer()
|
|
|
|
target := setupTransitSession(42, server, "192.168.1.50")
|
|
target.Name = "HunterAce"
|
|
|
|
searcher := setupTransitSession(1, server, "192.168.1.50")
|
|
|
|
pkt := &mhfpacket.MsgMhfTransitMessage{
|
|
AckHandle: 100,
|
|
SearchType: 2,
|
|
MessageData: buildTransitSearchByName("HunterAce", 10),
|
|
}
|
|
handleMsgMhfTransitMessage(searcher, pkt)
|
|
|
|
select {
|
|
case p := <-searcher.sendPackets:
|
|
if len(p.data) < ackBufDataOffset+2 {
|
|
t.Fatal("Response too short")
|
|
}
|
|
count := binary.BigEndian.Uint16(p.data[ackBufDataOffset : ackBufDataOffset+2])
|
|
if count != 1 {
|
|
t.Errorf("Expected 1 result for name search, got %d", count)
|
|
}
|
|
default:
|
|
t.Error("No response packet queued")
|
|
}
|
|
}
|
|
|
|
func TestTransitMessage_SearchByLobby(t *testing.T) {
|
|
server := setupTransitServer()
|
|
|
|
target := setupTransitSession(42, server, "192.168.1.50")
|
|
stage := NewStage("testLobby")
|
|
target.stage = stage
|
|
server.stages.Store("testLobby", stage)
|
|
|
|
searcher := setupTransitSession(1, server, "192.168.1.50")
|
|
|
|
pkt := &mhfpacket.MsgMhfTransitMessage{
|
|
AckHandle: 100,
|
|
SearchType: 3,
|
|
MessageData: buildTransitSearchByLobby(net.ParseIP("192.168.1.100").To4(), 54001, "testLobby", 10),
|
|
}
|
|
handleMsgMhfTransitMessage(searcher, pkt)
|
|
|
|
select {
|
|
case p := <-searcher.sendPackets:
|
|
if len(p.data) < ackBufDataOffset+2 {
|
|
t.Fatal("Response too short")
|
|
}
|
|
count := binary.BigEndian.Uint16(p.data[ackBufDataOffset : ackBufDataOffset+2])
|
|
if count != 1 {
|
|
t.Errorf("Expected 1 result for lobby search, got %d", count)
|
|
}
|
|
default:
|
|
t.Error("No response packet queued")
|
|
}
|
|
}
|
|
|
|
func TestTransitMessage_LobbySearch(t *testing.T) {
|
|
server := setupTransitServer()
|
|
server.erupeConfig.RealClientMode = cfg.ZZ
|
|
|
|
// Create a stage with the right prefix and binary data
|
|
stage := NewStage("sl2Ls210_room1")
|
|
// RawBinData3 needs at least 4 + 7*2 = 18 bytes for ZZ (Z1+ reads int16)
|
|
binData := make([]byte, 20)
|
|
// rank restriction at offset 4 (int16) = 0 (must be <= findPartyParams.RankRestriction default of 0)
|
|
binary.BigEndian.PutUint16(binData[4:6], 0)
|
|
// target at offset 6 (int16) = 1
|
|
binary.BigEndian.PutUint16(binData[6:8], 1)
|
|
stage.rawBinaryData[stageBinaryKey{1, 3}] = binData
|
|
stage.maxPlayers = 4
|
|
server.stages.Store("sl2Ls210_room1", stage)
|
|
|
|
// Rebuild registry to include the stage
|
|
server.Registry = NewLocalChannelRegistry([]*Server{server})
|
|
|
|
searcher := setupTransitSession(1, server, "192.168.1.50")
|
|
|
|
// Build search type 4 packet: numParams=0, maxResults=10
|
|
bf := byteframe.NewByteFrame()
|
|
bf.WriteUint8(0) // numParams
|
|
bf.WriteUint16(10) // maxResults
|
|
|
|
pkt := &mhfpacket.MsgMhfTransitMessage{
|
|
AckHandle: 100,
|
|
SearchType: 4,
|
|
MessageData: bf.Data(),
|
|
}
|
|
handleMsgMhfTransitMessage(searcher, pkt)
|
|
|
|
select {
|
|
case p := <-searcher.sendPackets:
|
|
if len(p.data) < ackBufDataOffset+2 {
|
|
t.Fatal("Response too short")
|
|
}
|
|
count := binary.BigEndian.Uint16(p.data[ackBufDataOffset : ackBufDataOffset+2])
|
|
if count != 1 {
|
|
t.Errorf("Expected 1 stage result for lobby search, got %d", count)
|
|
}
|
|
default:
|
|
t.Error("No response packet queued")
|
|
}
|
|
}
|
|
|
|
func TestTransitMessage_LocalhostRewrite(t *testing.T) {
|
|
server := setupTransitServer()
|
|
server.IP = "192.168.1.100"
|
|
|
|
target := setupTransitSession(42, server, "10.0.0.5")
|
|
target.Name = "RemotePlayer"
|
|
|
|
// Searcher is on localhost
|
|
searcher := setupTransitSession(1, server, "127.0.0.1")
|
|
|
|
pkt := &mhfpacket.MsgMhfTransitMessage{
|
|
AckHandle: 100,
|
|
SearchType: 1,
|
|
MessageData: buildTransitSearchByCharID(42),
|
|
}
|
|
handleMsgMhfTransitMessage(searcher, pkt)
|
|
|
|
select {
|
|
case p := <-searcher.sendPackets:
|
|
if len(p.data) < ackBufDataOffset+6 {
|
|
t.Fatal("Response too short")
|
|
}
|
|
count := binary.BigEndian.Uint16(p.data[ackBufDataOffset : ackBufDataOffset+2])
|
|
if count != 1 {
|
|
t.Fatalf("Expected 1 result, got %d", count)
|
|
}
|
|
// Check the IP in the response — written via WriteUint32(localhostAddrLE) which is big-endian
|
|
ipBE := binary.BigEndian.Uint32(p.data[ackBufDataOffset+2 : ackBufDataOffset+6])
|
|
if ipBE != localhostAddrLE {
|
|
t.Errorf("Expected localhost IP rewrite (0x%08X), got 0x%08X", localhostAddrLE, ipBE)
|
|
}
|
|
default:
|
|
t.Error("No response packet queued")
|
|
}
|
|
}
|