Files
Erupe/network/mhfpacket/msg_parse_small_test.go
Houmgaor c43be33680 feat(shutdown): graceful drain + configurable countdown
Add ShutdownAndDrain to the channel server (issue #179 non-breaking
subset): on SIGTERM/SIGINT, force-close all active sessions so that
logoutPlayer runs for each one (saves character data, cleans up stages
and semaphores), then poll until the session map empties or a 30-second
context deadline passes.  Existing Shutdown() is unchanged.

Add ShutdownCountdownSeconds int config field (default 10) alongside
DisableSoftCrash so operators can tune the broadcast countdown without
patching code.  A zero value falls back to 10 for safety.

Fix pre-existing test failures: MsgMhfAddRewardSongCount has a complete
Parse() implementation so it no longer belongs in the "NOT IMPLEMENTED"
parse test list; its handler test is updated to pass a real packet and
assert an ACK response instead of calling with nil.
2026-03-21 01:36:31 +01:00

249 lines
7.6 KiB
Go

package mhfpacket
import (
"io"
"testing"
"erupe-ce/common/byteframe"
cfg "erupe-ce/config"
"erupe-ce/network/clientctx"
)
// TestParseSmallNotImplemented tests Parse for packets whose Parse method returns
// "NOT IMPLEMENTED". We verify that Parse returns a non-nil error and does not panic.
func TestParseSmallNotImplemented(t *testing.T) {
packets := []struct {
name string
pkt MHFPacket
}{
// MHF packets - NOT IMPLEMENTED
{"MsgMhfAcceptReadReward", &MsgMhfAcceptReadReward{}},
{"MsgMhfDebugPostValue", &MsgMhfDebugPostValue{}},
{"MsgMhfEnterTournamentQuest", &MsgMhfEnterTournamentQuest{}},
{"MsgMhfGetCaAchievementHist", &MsgMhfGetCaAchievementHist{}},
{"MsgMhfGetCaUniqueID", &MsgMhfGetCaUniqueID{}},
{"MsgMhfGetDailyMissionMaster", &MsgMhfGetDailyMissionMaster{}},
{"MsgMhfGetDailyMissionPersonal", &MsgMhfGetDailyMissionPersonal{}},
{"MsgMhfGetRestrictionEvent", &MsgMhfGetRestrictionEvent{}},
{"MsgMhfKickExportForce", &MsgMhfKickExportForce{}},
{"MsgMhfPaymentAchievement", &MsgMhfPaymentAchievement{}},
{"MsgMhfPostRyoudama", &MsgMhfPostRyoudama{}},
{"MsgMhfRegistSpabiTime", &MsgMhfRegistSpabiTime{}},
{"MsgMhfResetAchievement", &MsgMhfResetAchievement{}},
{"MsgMhfResetTitle", &MsgMhfResetTitle{}},
{"MsgMhfSetCaAchievement", &MsgMhfSetCaAchievement{}},
{"MsgMhfSetDailyMissionPersonal", &MsgMhfSetDailyMissionPersonal{}},
{"MsgMhfSetUdTacticsFollower", &MsgMhfSetUdTacticsFollower{}},
{"MsgMhfStampcardPrize", &MsgMhfStampcardPrize{}},
{"MsgMhfUpdateForceGuildRank", &MsgMhfUpdateForceGuildRank{}},
{"MsgMhfUseUdShopCoin", &MsgMhfUseUdShopCoin{}},
// SYS packets - NOT IMPLEMENTED
{"MsgSysAuthData", &MsgSysAuthData{}},
{"MsgSysAuthQuery", &MsgSysAuthQuery{}},
{"MsgSysAuthTerminal", &MsgSysAuthTerminal{}},
{"MsgSysCloseMutex", &MsgSysCloseMutex{}},
{"MsgSysCollectBinary", &MsgSysCollectBinary{}},
{"MsgSysCreateMutex", &MsgSysCreateMutex{}},
{"MsgSysCreateOpenMutex", &MsgSysCreateOpenMutex{}},
{"MsgSysDeleteMutex", &MsgSysDeleteMutex{}},
{"MsgSysEnumlobby", &MsgSysEnumlobby{}},
{"MsgSysEnumuser", &MsgSysEnumuser{}},
{"MsgSysGetObjectBinary", &MsgSysGetObjectBinary{}},
{"MsgSysGetState", &MsgSysGetState{}},
{"MsgSysInfokyserver", &MsgSysInfokyserver{}},
{"MsgSysOpenMutex", &MsgSysOpenMutex{}},
{"MsgSysRotateObject", &MsgSysRotateObject{}},
{"MsgSysSerialize", &MsgSysSerialize{}},
{"MsgSysTransBinary", &MsgSysTransBinary{}},
}
ctx := &clientctx.ClientContext{RealClientMode: cfg.ZZ}
for _, tc := range packets {
t.Run(tc.name, func(t *testing.T) {
bf := byteframe.NewByteFrame()
// Write some padding bytes so Parse has data available if it tries to read.
bf.WriteUint32(0)
_, _ = bf.Seek(0, io.SeekStart)
err := tc.pkt.Parse(bf, ctx)
if err == nil {
t.Fatalf("Parse() expected error for NOT IMPLEMENTED packet, got nil")
}
if err.Error() != "NOT IMPLEMENTED" {
t.Fatalf("Parse() error = %q, want %q", err.Error(), "NOT IMPLEMENTED")
}
})
}
}
// TestParseSmallNoData tests Parse for packets with no fields that return nil.
func TestParseSmallNoData(t *testing.T) {
packets := []struct {
name string
pkt MHFPacket
}{
{"MsgSysCleanupObject", &MsgSysCleanupObject{}},
{"MsgSysUnreserveStage", &MsgSysUnreserveStage{}},
}
ctx := &clientctx.ClientContext{RealClientMode: cfg.ZZ}
for _, tc := range packets {
t.Run(tc.name, func(t *testing.T) {
bf := byteframe.NewByteFrame()
err := tc.pkt.Parse(bf, ctx)
if err != nil {
t.Fatalf("Parse() error = %v, want nil", err)
}
})
}
}
// TestParseSmallLogout tests Parse for MsgSysLogout which reads a single uint8 field.
func TestParseSmallLogout(t *testing.T) {
tests := []struct {
name string
unk0 uint8
}{
{"hardcoded 1", 1},
{"zero", 0},
{"max", 255},
}
ctx := &clientctx.ClientContext{RealClientMode: cfg.ZZ}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
bf := byteframe.NewByteFrame()
bf.WriteUint8(tt.unk0)
_, _ = bf.Seek(0, io.SeekStart)
pkt := &MsgSysLogout{}
err := pkt.Parse(bf, ctx)
if err != nil {
t.Fatalf("Parse() error = %v", err)
}
if pkt.LogoutType != tt.unk0 {
t.Errorf("Unk0 = %d, want %d", pkt.LogoutType, tt.unk0)
}
})
}
}
// TestParseSmallEnumerateHouse tests Parse for MsgMhfEnumerateHouse which reads
// AckHandle, CharID, Method, Unk, lenName, and optional Name.
func TestParseSmallEnumerateHouse(t *testing.T) {
ctx := &clientctx.ClientContext{RealClientMode: cfg.ZZ}
t.Run("no name", func(t *testing.T) {
bf := byteframe.NewByteFrame()
bf.WriteUint32(0x11223344) // AckHandle
bf.WriteUint32(0xDEADBEEF) // CharID
bf.WriteUint8(2) // Method
bf.WriteUint16(100) // Unk
bf.WriteUint8(0) // lenName = 0 (no name)
_, _ = bf.Seek(0, io.SeekStart)
pkt := &MsgMhfEnumerateHouse{}
err := pkt.Parse(bf, ctx)
if err != nil {
t.Fatalf("Parse() error = %v", err)
}
if pkt.AckHandle != 0x11223344 {
t.Errorf("AckHandle = 0x%X, want 0x11223344", pkt.AckHandle)
}
if pkt.CharID != 0xDEADBEEF {
t.Errorf("CharID = 0x%X, want 0xDEADBEEF", pkt.CharID)
}
if pkt.Method != 2 {
t.Errorf("Method = %d, want 2", pkt.Method)
}
if pkt.Name != "" {
t.Errorf("Name = %q, want empty", pkt.Name)
}
})
t.Run("with name", func(t *testing.T) {
bf := byteframe.NewByteFrame()
bf.WriteUint32(1) // AckHandle
bf.WriteUint32(42) // CharID
bf.WriteUint8(1) // Method
bf.WriteUint16(200) // Unk
// The name is SJIS null-terminated bytes. Use ASCII-compatible bytes.
nameBytes := []byte("Test\x00")
bf.WriteUint8(uint8(len(nameBytes))) // lenName > 0
bf.WriteBytes(nameBytes) // null-terminated name
_, _ = bf.Seek(0, io.SeekStart)
pkt := &MsgMhfEnumerateHouse{}
err := pkt.Parse(bf, ctx)
if err != nil {
t.Fatalf("Parse() error = %v", err)
}
if pkt.AckHandle != 1 {
t.Errorf("AckHandle = %d, want 1", pkt.AckHandle)
}
if pkt.CharID != 42 {
t.Errorf("CharID = %d, want 42", pkt.CharID)
}
if pkt.Method != 1 {
t.Errorf("Method = %d, want 1", pkt.Method)
}
if pkt.Name != "Test" {
t.Errorf("Name = %q, want %q", pkt.Name, "Test")
}
})
}
// TestParseSmallGetExtraInfoAndCogInfo tests that MsgMhfGetExtraInfo and
// MsgMhfGetCogInfo correctly parse their AckHandle field.
func TestParseSmallGetExtraInfoAndCogInfo(t *testing.T) {
ctx := &clientctx.ClientContext{RealClientMode: cfg.ZZ}
t.Run("GetExtraInfo", func(t *testing.T) {
bf := byteframe.NewByteFrame()
bf.WriteUint32(0xDEADBEEF)
_, _ = bf.Seek(0, io.SeekStart)
pkt := &MsgMhfGetExtraInfo{}
if err := pkt.Parse(bf, ctx); err != nil {
t.Fatalf("Parse() error = %v", err)
}
if pkt.AckHandle != 0xDEADBEEF {
t.Errorf("AckHandle = 0x%X, want 0xDEADBEEF", pkt.AckHandle)
}
})
t.Run("GetCogInfo", func(t *testing.T) {
bf := byteframe.NewByteFrame()
bf.WriteUint32(0xCAFEBABE)
_, _ = bf.Seek(0, io.SeekStart)
pkt := &MsgMhfGetCogInfo{}
if err := pkt.Parse(bf, ctx); err != nil {
t.Fatalf("Parse() error = %v", err)
}
if pkt.AckHandle != 0xCAFEBABE {
t.Errorf("AckHandle = 0x%X, want 0xCAFEBABE", pkt.AckHandle)
}
})
}
// TestParseSmallNotImplementedDoesNotPanic ensures that calling Parse on NOT IMPLEMENTED
// packets returns an error and does not panic.
func TestParseSmallNotImplementedDoesNotPanic(t *testing.T) {
packets := []MHFPacket{
&MsgMhfAcceptReadReward{},
&MsgSysAuthData{},
&MsgSysSerialize{},
}
ctx := &clientctx.ClientContext{RealClientMode: cfg.ZZ}
for _, pkt := range packets {
t.Run("not_implemented", func(t *testing.T) {
bf := byteframe.NewByteFrame()
err := pkt.Parse(bf, ctx)
if err == nil {
t.Fatal("expected error, got nil")
}
})
}
}