test: increase total coverage from 40.7% to 46.1%

Add comprehensive mhfpacket Parse/Build/Opcode tests covering nearly
all packet types (78.3% -> 95.7%). Add channelserver tests for
BackportQuest and GuildIcon Scan/Value round-trips.
This commit is contained in:
Houmgaor
2026-02-08 16:17:17 +01:00
parent 81b2b85a8b
commit 6d18de01eb
7 changed files with 4497 additions and 0 deletions

View File

@@ -0,0 +1,238 @@
package mhfpacket
import (
"io"
"testing"
"erupe-ce/common/byteframe"
"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{}},
{"MsgMhfAddRewardSongCount", &MsgMhfAddRewardSongCount{}},
{"MsgMhfCaravanMyRank", &MsgMhfCaravanMyRank{}},
{"MsgMhfCaravanMyScore", &MsgMhfCaravanMyScore{}},
{"MsgMhfCaravanRanking", &MsgMhfCaravanRanking{}},
{"MsgMhfDebugPostValue", &MsgMhfDebugPostValue{}},
{"MsgMhfEnterTournamentQuest", &MsgMhfEnterTournamentQuest{}},
{"MsgMhfGetBreakSeibatuLevelReward", &MsgMhfGetBreakSeibatuLevelReward{}},
{"MsgMhfGetCaAchievementHist", &MsgMhfGetCaAchievementHist{}},
{"MsgMhfGetCaUniqueID", &MsgMhfGetCaUniqueID{}},
{"MsgMhfGetDailyMissionMaster", &MsgMhfGetDailyMissionMaster{}},
{"MsgMhfGetDailyMissionPersonal", &MsgMhfGetDailyMissionPersonal{}},
{"MsgMhfGetExtraInfo", &MsgMhfGetExtraInfo{}},
{"MsgMhfGetFixedSeibatuRankingTable", &MsgMhfGetFixedSeibatuRankingTable{}},
{"MsgMhfGetRandFromTable", &MsgMhfGetRandFromTable{}},
{"MsgMhfGetRestrictionEvent", &MsgMhfGetRestrictionEvent{}},
{"MsgMhfGetSenyuDailyCount", &MsgMhfGetSenyuDailyCount{}},
{"MsgMhfKickExportForce", &MsgMhfKickExportForce{}},
{"MsgMhfPaymentAchievement", &MsgMhfPaymentAchievement{}},
{"MsgMhfPlayFreeGacha", &MsgMhfPlayFreeGacha{}},
{"MsgMhfPostBoostTimeLimit", &MsgMhfPostBoostTimeLimit{}},
{"MsgMhfPostGemInfo", &MsgMhfPostGemInfo{}},
{"MsgMhfPostRyoudama", &MsgMhfPostRyoudama{}},
{"MsgMhfPostSeibattle", &MsgMhfPostSeibattle{}},
{"MsgMhfReadBeatLevelAllRanking", &MsgMhfReadBeatLevelAllRanking{}},
{"MsgMhfReadBeatLevelMyRanking", &MsgMhfReadBeatLevelMyRanking{}},
{"MsgMhfReadLastWeekBeatRanking", &MsgMhfReadLastWeekBeatRanking{}},
{"MsgMhfRegistSpabiTime", &MsgMhfRegistSpabiTime{}},
{"MsgMhfResetAchievement", &MsgMhfResetAchievement{}},
{"MsgMhfResetTitle", &MsgMhfResetTitle{}},
{"MsgMhfSetCaAchievement", &MsgMhfSetCaAchievement{}},
{"MsgMhfSetDailyMissionPersonal", &MsgMhfSetDailyMissionPersonal{}},
{"MsgMhfSetUdTacticsFollower", &MsgMhfSetUdTacticsFollower{}},
{"MsgMhfStampcardPrize", &MsgMhfStampcardPrize{}},
{"MsgMhfUnreserveSrg", &MsgMhfUnreserveSrg{}},
{"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{}},
{"MsgSysGetObjectOwner", &MsgSysGetObjectOwner{}},
{"MsgSysGetState", &MsgSysGetState{}},
{"MsgSysInfokyserver", &MsgSysInfokyserver{}},
{"MsgSysOpenMutex", &MsgSysOpenMutex{}},
{"MsgSysRotateObject", &MsgSysRotateObject{}},
{"MsgSysSerialize", &MsgSysSerialize{}},
{"MsgSysTransBinary", &MsgSysTransBinary{}},
}
ctx := &clientctx.ClientContext{}
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{}
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{}
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.Unk0 != tt.unk0 {
t.Errorf("Unk0 = %d, want %d", pkt.Unk0, 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{}
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.Unk != 100 {
t.Errorf("Unk = %d, want 100", pkt.Unk)
}
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.Unk != 200 {
t.Errorf("Unk = %d, want 200", pkt.Unk)
}
if pkt.Name != "Test" {
t.Errorf("Name = %q, want %q", pkt.Name, "Test")
}
})
}
// TestParseSmallNotImplementedDoesNotPanic ensures that calling Parse on NOT IMPLEMENTED
// packets with a nil ClientContext does not cause a nil pointer dereference panic.
func TestParseSmallNotImplementedDoesNotPanic(t *testing.T) {
packets := []MHFPacket{
&MsgMhfAcceptReadReward{},
&MsgSysAuthData{},
&MsgSysSerialize{},
}
for _, pkt := range packets {
t.Run("nil_ctx", func(t *testing.T) {
bf := byteframe.NewByteFrame()
err := pkt.Parse(bf, nil)
if err == nil {
t.Fatal("expected error, got nil")
}
})
}
}