mirror of
https://github.com/Mezeporta/Erupe.git
synced 2026-03-22 15:43:49 +01:00
SaveColumn and SaveMercenary now check RowsAffected() and return a wrapped ErrCharacterNotFound when 0 rows are updated, preventing silent data loss when a character ID is missing or mismatched. AdjustInt already detects this via its RETURNING scan — no change. Daily mission packet structs (Get/SetDailyMission*) now parse the AckHandle instead of returning NOT IMPLEMENTED, letting handlers send empty-list success ACKs and avoiding client softlocks. Also adds tests for dashboard stats endpoint and for five guild repo methods (SetAllianceRecruiting, RolloverDailyRP, AddWeeklyBonusUsers, InsertKillLog, ClearTreasureHunt) that had no coverage.
246 lines
7.4 KiB
Go
246 lines
7.4 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{}},
|
|
{"MsgMhfGetRestrictionEvent", &MsgMhfGetRestrictionEvent{}},
|
|
{"MsgMhfKickExportForce", &MsgMhfKickExportForce{}},
|
|
{"MsgMhfPaymentAchievement", &MsgMhfPaymentAchievement{}},
|
|
{"MsgMhfPostRyoudama", &MsgMhfPostRyoudama{}},
|
|
{"MsgMhfRegistSpabiTime", &MsgMhfRegistSpabiTime{}},
|
|
{"MsgMhfResetAchievement", &MsgMhfResetAchievement{}},
|
|
{"MsgMhfResetTitle", &MsgMhfResetTitle{}},
|
|
{"MsgMhfSetCaAchievement", &MsgMhfSetCaAchievement{}},
|
|
{"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")
|
|
}
|
|
})
|
|
}
|
|
}
|