mirror of
https://github.com/Mezeporta/Erupe.git
synced 2026-03-22 15:43:49 +01:00
Move all direct DB calls from handlers_festa.go (23 calls across 8 tables) and handlers_tower.go (16 calls across 4 tables) into dedicated repository structs following the established pattern. FestaRepository (14 methods): lifecycle cleanup, event management, team souls, trial stats/rankings, user state, voting, registration, soul submission, prize claiming/enumeration. TowerRepository (12 methods): personal tower data (skills, progress, gems), guild tenrouirai progress/scores/page advancement, tower RP. Also fix pre-existing nil pointer panics in integration tests by adding SetTestDB helper that initializes both the DB connection and all repositories, and wire the done channel in createTestServerWithDB to prevent Shutdown panics.
654 lines
18 KiB
Go
654 lines
18 KiB
Go
package channelserver
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/binary"
|
|
"fmt"
|
|
|
|
"erupe-ce/common/byteframe"
|
|
"erupe-ce/network"
|
|
"erupe-ce/network/clientctx"
|
|
"erupe-ce/network/mhfpacket"
|
|
"erupe-ce/server/channelserver/compression/nullcomp"
|
|
"testing"
|
|
)
|
|
|
|
// MockMsgMhfSavedata creates a mock save data packet for testing
|
|
type MockMsgMhfSavedata struct {
|
|
SaveType uint8
|
|
AckHandle uint32
|
|
RawDataPayload []byte
|
|
}
|
|
|
|
func (m *MockMsgMhfSavedata) Opcode() network.PacketID {
|
|
return network.MSG_MHF_SAVEDATA
|
|
}
|
|
|
|
func (m *MockMsgMhfSavedata) Parse(bf *byteframe.ByteFrame, ctx *clientctx.ClientContext) error {
|
|
return nil
|
|
}
|
|
|
|
func (m *MockMsgMhfSavedata) Build(bf *byteframe.ByteFrame, ctx *clientctx.ClientContext) error {
|
|
return nil
|
|
}
|
|
|
|
// MockMsgMhfSaveScenarioData creates a mock scenario data packet for testing
|
|
type MockMsgMhfSaveScenarioData struct {
|
|
AckHandle uint32
|
|
RawDataPayload []byte
|
|
}
|
|
|
|
func (m *MockMsgMhfSaveScenarioData) Opcode() network.PacketID {
|
|
return network.MSG_MHF_SAVE_SCENARIO_DATA
|
|
}
|
|
|
|
func (m *MockMsgMhfSaveScenarioData) Parse(bf *byteframe.ByteFrame, ctx *clientctx.ClientContext) error {
|
|
return nil
|
|
}
|
|
|
|
func (m *MockMsgMhfSaveScenarioData) Build(bf *byteframe.ByteFrame, ctx *clientctx.ClientContext) error {
|
|
return nil
|
|
}
|
|
|
|
// TestSaveDataDecompressionFailureSendsFailAck verifies that decompression
|
|
// failures result in a failure ACK, not a success ACK
|
|
func TestSaveDataDecompressionFailureSendsFailAck(t *testing.T) {
|
|
t.Skip("skipping test - nullcomp doesn't validate input data as expected")
|
|
tests := []struct {
|
|
name string
|
|
saveType uint8
|
|
invalidData []byte
|
|
expectFailAck bool
|
|
}{
|
|
{
|
|
name: "invalid_diff_data",
|
|
saveType: 1,
|
|
invalidData: []byte{0xFF, 0xFF, 0xFF, 0xFF},
|
|
expectFailAck: true,
|
|
},
|
|
{
|
|
name: "invalid_blob_data",
|
|
saveType: 0,
|
|
invalidData: []byte{0xFF, 0xFF, 0xFF, 0xFF},
|
|
expectFailAck: true,
|
|
},
|
|
{
|
|
name: "empty_diff_data",
|
|
saveType: 1,
|
|
invalidData: []byte{},
|
|
expectFailAck: true,
|
|
},
|
|
{
|
|
name: "empty_blob_data",
|
|
saveType: 0,
|
|
invalidData: []byte{},
|
|
expectFailAck: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// This test verifies the fix we made where decompression errors
|
|
// should send doAckSimpleFail instead of doAckSimpleSucceed
|
|
|
|
// Create a valid compressed payload for comparison
|
|
validData := []byte{0x01, 0x02, 0x03, 0x04}
|
|
compressedValid, err := nullcomp.Compress(validData)
|
|
if err != nil {
|
|
t.Fatalf("failed to compress test data: %v", err)
|
|
}
|
|
|
|
// Test that valid data can be decompressed
|
|
_, err = nullcomp.Decompress(compressedValid)
|
|
if err != nil {
|
|
t.Fatalf("valid data failed to decompress: %v", err)
|
|
}
|
|
|
|
// Test that invalid data fails to decompress
|
|
_, err = nullcomp.Decompress(tt.invalidData)
|
|
if err == nil {
|
|
t.Error("expected decompression to fail for invalid data, but it succeeded")
|
|
}
|
|
|
|
// The actual handler test would require a full session mock,
|
|
// but this verifies the nullcomp behavior that our fix depends on
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestScenarioSaveErrorHandling verifies that database errors
|
|
// result in failure ACKs
|
|
func TestScenarioSaveErrorHandling(t *testing.T) {
|
|
// This test documents the expected behavior after our fix:
|
|
// 1. If db.Exec returns an error, doAckSimpleFail should be called
|
|
// 2. If db.Exec succeeds, doAckSimpleSucceed should be called
|
|
// 3. The function should return early after sending fail ACK
|
|
|
|
tests := []struct {
|
|
name string
|
|
scenarioData []byte
|
|
wantError bool
|
|
}{
|
|
{
|
|
name: "valid_scenario_data",
|
|
scenarioData: []byte{0x01, 0x02, 0x03},
|
|
wantError: false,
|
|
},
|
|
{
|
|
name: "empty_scenario_data",
|
|
scenarioData: []byte{},
|
|
wantError: false, // Empty data is valid
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// Verify data format is reasonable
|
|
if len(tt.scenarioData) > 1000000 {
|
|
t.Error("scenario data suspiciously large")
|
|
}
|
|
|
|
// The actual database interaction test would require a mock DB
|
|
// This test verifies data constraints
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestAckPacketStructure verifies the structure of ACK packets
|
|
func TestAckPacketStructure(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
ackHandle uint32
|
|
data []byte
|
|
}{
|
|
{
|
|
name: "simple_ack",
|
|
ackHandle: 0x12345678,
|
|
data: []byte{0x00, 0x00, 0x00, 0x00},
|
|
},
|
|
{
|
|
name: "ack_with_data",
|
|
ackHandle: 0xABCDEF01,
|
|
data: []byte{0x01, 0x02, 0x03, 0x04, 0x05},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// Simulate building an ACK packet
|
|
var buf bytes.Buffer
|
|
|
|
// Write opcode (2 bytes, big endian)
|
|
_ = binary.Write(&buf, binary.BigEndian, uint16(network.MSG_SYS_ACK))
|
|
|
|
// Write ack handle (4 bytes, big endian)
|
|
_ = binary.Write(&buf, binary.BigEndian, tt.ackHandle)
|
|
|
|
// Write data
|
|
buf.Write(tt.data)
|
|
|
|
// Verify packet structure
|
|
packet := buf.Bytes()
|
|
|
|
if len(packet) != 2+4+len(tt.data) {
|
|
t.Errorf("expected packet length %d, got %d", 2+4+len(tt.data), len(packet))
|
|
}
|
|
|
|
// Verify opcode
|
|
opcode := binary.BigEndian.Uint16(packet[0:2])
|
|
if opcode != uint16(network.MSG_SYS_ACK) {
|
|
t.Errorf("expected opcode 0x%04X, got 0x%04X", network.MSG_SYS_ACK, opcode)
|
|
}
|
|
|
|
// Verify ack handle
|
|
handle := binary.BigEndian.Uint32(packet[2:6])
|
|
if handle != tt.ackHandle {
|
|
t.Errorf("expected ack handle 0x%08X, got 0x%08X", tt.ackHandle, handle)
|
|
}
|
|
|
|
// Verify data
|
|
dataStart := 6
|
|
for i, b := range tt.data {
|
|
if packet[dataStart+i] != b {
|
|
t.Errorf("data mismatch at index %d: got 0x%02X, want 0x%02X", i, packet[dataStart+i], b)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestNullcompRoundTrip verifies compression and decompression work correctly
|
|
func TestNullcompRoundTrip(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
data []byte
|
|
}{
|
|
{
|
|
name: "small_data",
|
|
data: []byte{0x01, 0x02, 0x03, 0x04},
|
|
},
|
|
{
|
|
name: "repeated_data",
|
|
data: bytes.Repeat([]byte{0xAA}, 100),
|
|
},
|
|
{
|
|
name: "mixed_data",
|
|
data: []byte{0x00, 0x01, 0x02, 0x03, 0xFF, 0xFE, 0xFD, 0xFC},
|
|
},
|
|
{
|
|
name: "single_byte",
|
|
data: []byte{0x42},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// Compress
|
|
compressed, err := nullcomp.Compress(tt.data)
|
|
if err != nil {
|
|
t.Fatalf("compression failed: %v", err)
|
|
}
|
|
|
|
// Decompress
|
|
decompressed, err := nullcomp.Decompress(compressed)
|
|
if err != nil {
|
|
t.Fatalf("decompression failed: %v", err)
|
|
}
|
|
|
|
// Verify round trip
|
|
if !bytes.Equal(tt.data, decompressed) {
|
|
t.Errorf("round trip failed: got %v, want %v", decompressed, tt.data)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestSaveDataValidation verifies save data validation logic
|
|
func TestSaveDataValidation(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
data []byte
|
|
isValid bool
|
|
}{
|
|
{
|
|
name: "valid_save_data",
|
|
data: bytes.Repeat([]byte{0x00}, 100),
|
|
isValid: true,
|
|
},
|
|
{
|
|
name: "empty_save_data",
|
|
data: []byte{},
|
|
isValid: true, // Empty might be valid depending on context
|
|
},
|
|
{
|
|
name: "large_save_data",
|
|
data: bytes.Repeat([]byte{0x00}, 1000000),
|
|
isValid: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// Basic validation checks
|
|
if len(tt.data) == 0 && len(tt.data) > 0 {
|
|
t.Error("negative data length")
|
|
}
|
|
|
|
// Verify data is not nil if we expect valid data
|
|
if tt.isValid && len(tt.data) > 0 && tt.data == nil {
|
|
t.Error("expected non-nil data for valid case")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestErrorRecovery verifies that errors don't leave the system in a bad state
|
|
func TestErrorRecovery(t *testing.T) {
|
|
t.Skip("skipping test - nullcomp doesn't validate input data as expected")
|
|
|
|
// This test verifies that after an error:
|
|
// 1. A proper error ACK is sent
|
|
// 2. The function returns early
|
|
// 3. No further processing occurs
|
|
// 4. The session remains in a valid state
|
|
|
|
t.Run("early_return_after_error", func(t *testing.T) {
|
|
// Create invalid compressed data
|
|
invalidData := []byte{0xFF, 0xFF, 0xFF, 0xFF}
|
|
|
|
// Attempt decompression
|
|
_, err := nullcomp.Decompress(invalidData)
|
|
|
|
// Should error
|
|
if err == nil {
|
|
t.Error("expected decompression error for invalid data")
|
|
}
|
|
|
|
// After error, the handler should:
|
|
// - Call doAckSimpleFail (our fix)
|
|
// - Return immediately
|
|
// - NOT call doAckSimpleSucceed (the bug we fixed)
|
|
})
|
|
}
|
|
|
|
// BenchmarkPacketQueueing benchmarks the packet queueing performance
|
|
func BenchmarkPacketQueueing(b *testing.B) {
|
|
// This test is skipped because it requires a mock that implements the network.CryptConn interface
|
|
// The current architecture doesn't easily support interface-based testing
|
|
b.Skip("benchmark requires interface-based CryptConn mock")
|
|
}
|
|
|
|
// ============================================================================
|
|
// Integration Tests (require test database)
|
|
// Run with: docker-compose -f docker/docker-compose.test.yml up -d
|
|
// ============================================================================
|
|
|
|
// TestHandleMsgMhfSavedata_Integration tests the actual save data handler with database
|
|
func TestHandleMsgMhfSavedata_Integration(t *testing.T) {
|
|
db := SetupTestDB(t)
|
|
defer TeardownTestDB(t, db)
|
|
|
|
// Create test user and character
|
|
userID := CreateTestUser(t, db, "testuser")
|
|
charID := CreateTestCharacter(t, db, userID, "TestChar")
|
|
|
|
// Create test session
|
|
mock := &MockCryptConn{sentPackets: make([][]byte, 0)}
|
|
s := createTestSession(mock)
|
|
s.charID = charID
|
|
s.Name = "TestChar"
|
|
SetTestDB(s.server, db)
|
|
|
|
tests := []struct {
|
|
name string
|
|
saveType uint8
|
|
payloadFunc func() []byte
|
|
wantSuccess bool
|
|
}{
|
|
{
|
|
name: "blob_save",
|
|
saveType: 0,
|
|
payloadFunc: func() []byte {
|
|
// Create minimal valid savedata (large enough for all game mode pointers)
|
|
data := make([]byte, 150000)
|
|
copy(data[88:], []byte("TestChar\x00")) // Name at offset 88
|
|
compressed, _ := nullcomp.Compress(data)
|
|
return compressed
|
|
},
|
|
wantSuccess: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
payload := tt.payloadFunc()
|
|
pkt := &mhfpacket.MsgMhfSavedata{
|
|
SaveType: tt.saveType,
|
|
AckHandle: 1234,
|
|
AllocMemSize: uint32(len(payload)),
|
|
DataSize: uint32(len(payload)),
|
|
RawDataPayload: payload,
|
|
}
|
|
|
|
handleMsgMhfSavedata(s, pkt)
|
|
|
|
// Check if ACK was sent
|
|
if len(s.sendPackets) == 0 {
|
|
t.Error("no ACK packet was sent")
|
|
} else {
|
|
// Drain the channel
|
|
<-s.sendPackets
|
|
}
|
|
|
|
// Verify database was updated (for success case)
|
|
if tt.wantSuccess {
|
|
var savedData []byte
|
|
err := db.QueryRow("SELECT savedata FROM characters WHERE id = $1", charID).Scan(&savedData)
|
|
if err != nil {
|
|
t.Errorf("failed to query saved data: %v", err)
|
|
}
|
|
if len(savedData) == 0 {
|
|
t.Error("savedata was not written to database")
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestHandleMsgMhfLoaddata_Integration tests loading character data
|
|
func TestHandleMsgMhfLoaddata_Integration(t *testing.T) {
|
|
db := SetupTestDB(t)
|
|
defer TeardownTestDB(t, db)
|
|
|
|
// Create test user and character
|
|
userID := CreateTestUser(t, db, "testuser")
|
|
|
|
// Create savedata
|
|
saveData := make([]byte, 200)
|
|
copy(saveData[88:], []byte("LoadTest\x00"))
|
|
compressed, _ := nullcomp.Compress(saveData)
|
|
|
|
var charID uint32
|
|
err := db.QueryRow(`
|
|
INSERT INTO characters (user_id, is_female, is_new_character, name, unk_desc_string, gr, hr, weapon_type, last_login, savedata, decomyset, savemercenary)
|
|
VALUES ($1, false, false, 'LoadTest', '', 0, 0, 0, 0, $2, '', '')
|
|
RETURNING id
|
|
`, userID, compressed).Scan(&charID)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create test character: %v", err)
|
|
}
|
|
|
|
// Create test session
|
|
mock := &MockCryptConn{sentPackets: make([][]byte, 0)}
|
|
s := createTestSession(mock)
|
|
s.charID = charID
|
|
SetTestDB(s.server, db)
|
|
s.server.userBinaryParts = make(map[userBinaryPartID][]byte)
|
|
|
|
pkt := &mhfpacket.MsgMhfLoaddata{
|
|
AckHandle: 5678,
|
|
}
|
|
|
|
handleMsgMhfLoaddata(s, pkt)
|
|
|
|
// Verify ACK was sent
|
|
if len(s.sendPackets) == 0 {
|
|
t.Error("no ACK packet was sent")
|
|
}
|
|
|
|
// Verify name was extracted
|
|
if s.Name != "LoadTest" {
|
|
t.Errorf("character name not loaded, got %q, want %q", s.Name, "LoadTest")
|
|
}
|
|
}
|
|
|
|
// TestHandleMsgMhfSaveScenarioData_Integration tests scenario data saving
|
|
func TestHandleMsgMhfSaveScenarioData_Integration(t *testing.T) {
|
|
db := SetupTestDB(t)
|
|
defer TeardownTestDB(t, db)
|
|
|
|
// Create test user and character
|
|
userID := CreateTestUser(t, db, "testuser")
|
|
charID := CreateTestCharacter(t, db, userID, "ScenarioTest")
|
|
|
|
// Create test session
|
|
mock := &MockCryptConn{sentPackets: make([][]byte, 0)}
|
|
s := createTestSession(mock)
|
|
s.charID = charID
|
|
SetTestDB(s.server, db)
|
|
|
|
scenarioData := []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A}
|
|
|
|
pkt := &mhfpacket.MsgMhfSaveScenarioData{
|
|
AckHandle: 9999,
|
|
DataSize: uint32(len(scenarioData)),
|
|
RawDataPayload: scenarioData,
|
|
}
|
|
|
|
handleMsgMhfSaveScenarioData(s, pkt)
|
|
|
|
// Verify ACK was sent
|
|
if len(s.sendPackets) == 0 {
|
|
t.Error("no ACK packet was sent")
|
|
} else {
|
|
<-s.sendPackets
|
|
}
|
|
|
|
// Verify scenario data was saved
|
|
var saved []byte
|
|
err := db.QueryRow("SELECT scenariodata FROM characters WHERE id = $1", charID).Scan(&saved)
|
|
if err != nil {
|
|
t.Fatalf("failed to query scenario data: %v", err)
|
|
}
|
|
|
|
if !bytes.Equal(saved, scenarioData) {
|
|
t.Errorf("scenario data mismatch: got %v, want %v", saved, scenarioData)
|
|
}
|
|
}
|
|
|
|
// TestHandleMsgMhfLoadScenarioData_Integration tests scenario data loading
|
|
func TestHandleMsgMhfLoadScenarioData_Integration(t *testing.T) {
|
|
db := SetupTestDB(t)
|
|
defer TeardownTestDB(t, db)
|
|
|
|
// Create test user and character
|
|
userID := CreateTestUser(t, db, "testuser")
|
|
|
|
scenarioData := []byte{0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x11, 0x22, 0x33, 0x44}
|
|
|
|
var charID uint32
|
|
err := db.QueryRow(`
|
|
INSERT INTO characters (user_id, is_female, is_new_character, name, unk_desc_string, gr, hr, weapon_type, last_login, savedata, decomyset, savemercenary, scenariodata)
|
|
VALUES ($1, false, false, 'ScenarioLoad', '', 0, 0, 0, 0, $2, '', '', $3)
|
|
RETURNING id
|
|
`, userID, []byte{0x00, 0x00, 0x00, 0x00}, scenarioData).Scan(&charID)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create test character: %v", err)
|
|
}
|
|
|
|
// Create test session
|
|
mock := &MockCryptConn{sentPackets: make([][]byte, 0)}
|
|
s := createTestSession(mock)
|
|
s.charID = charID
|
|
SetTestDB(s.server, db)
|
|
|
|
pkt := &mhfpacket.MsgMhfLoadScenarioData{
|
|
AckHandle: 1111,
|
|
}
|
|
|
|
handleMsgMhfLoadScenarioData(s, pkt)
|
|
|
|
// Verify ACK was sent
|
|
if len(s.sendPackets) == 0 {
|
|
t.Fatal("no ACK packet was sent")
|
|
}
|
|
|
|
// The ACK should contain the scenario data
|
|
ackPkt := <-s.sendPackets
|
|
if len(ackPkt.data) < len(scenarioData) {
|
|
t.Errorf("ACK packet too small: got %d bytes, expected at least %d", len(ackPkt.data), len(scenarioData))
|
|
}
|
|
}
|
|
|
|
// TestSaveDataCorruptionDetection_Integration tests that corrupted saves are rejected
|
|
func TestSaveDataCorruptionDetection_Integration(t *testing.T) {
|
|
db := SetupTestDB(t)
|
|
defer TeardownTestDB(t, db)
|
|
|
|
// Create test user and character
|
|
userID := CreateTestUser(t, db, "testuser")
|
|
charID := CreateTestCharacter(t, db, userID, "OriginalName")
|
|
|
|
// Create test session
|
|
mock := &MockCryptConn{sentPackets: make([][]byte, 0)}
|
|
s := createTestSession(mock)
|
|
s.charID = charID
|
|
s.Name = "OriginalName"
|
|
SetTestDB(s.server, db)
|
|
s.server.erupeConfig.DeleteOnSaveCorruption = false
|
|
|
|
// Create save data with a DIFFERENT name (corruption)
|
|
// Must be large enough for ZZ save pointer offsets (highest: pKQF at 146728)
|
|
corruptedData := make([]byte, 150000)
|
|
copy(corruptedData[88:], []byte("HackedName\x00"))
|
|
compressed, _ := nullcomp.Compress(corruptedData)
|
|
|
|
pkt := &mhfpacket.MsgMhfSavedata{
|
|
SaveType: 0,
|
|
AckHandle: 4444,
|
|
AllocMemSize: uint32(len(compressed)),
|
|
DataSize: uint32(len(compressed)),
|
|
RawDataPayload: compressed,
|
|
}
|
|
|
|
handleMsgMhfSavedata(s, pkt)
|
|
|
|
// The save should be rejected, connection should be closed
|
|
// In a real scenario, s.rawConn.Close() is called
|
|
// We can't easily test that, but we can verify the data wasn't saved
|
|
|
|
// Check that database wasn't updated with corrupted data
|
|
var savedName string
|
|
_ = db.QueryRow("SELECT name FROM characters WHERE id = $1", charID).Scan(&savedName)
|
|
if savedName == "HackedName" {
|
|
t.Error("corrupted save data was incorrectly written to database")
|
|
}
|
|
}
|
|
|
|
// TestConcurrentSaveData_Integration tests concurrent save operations
|
|
func TestConcurrentSaveData_Integration(t *testing.T) {
|
|
db := SetupTestDB(t)
|
|
defer TeardownTestDB(t, db)
|
|
|
|
// Create test user and multiple characters
|
|
userID := CreateTestUser(t, db, "testuser")
|
|
charIDs := make([]uint32, 5)
|
|
for i := 0; i < 5; i++ {
|
|
charIDs[i] = CreateTestCharacter(t, db, userID, fmt.Sprintf("Char%d", i))
|
|
}
|
|
|
|
// Run concurrent saves
|
|
done := make(chan bool, 5)
|
|
for i := 0; i < 5; i++ {
|
|
go func(index int) {
|
|
mock := &MockCryptConn{sentPackets: make([][]byte, 0)}
|
|
s := createTestSession(mock)
|
|
s.charID = charIDs[index]
|
|
s.Name = fmt.Sprintf("Char%d", index)
|
|
SetTestDB(s.server, db)
|
|
|
|
saveData := make([]byte, 150000)
|
|
copy(saveData[88:], []byte(fmt.Sprintf("Char%d\x00", index)))
|
|
compressed, _ := nullcomp.Compress(saveData)
|
|
|
|
pkt := &mhfpacket.MsgMhfSavedata{
|
|
SaveType: 0,
|
|
AckHandle: uint32(index),
|
|
AllocMemSize: uint32(len(compressed)),
|
|
DataSize: uint32(len(compressed)),
|
|
RawDataPayload: compressed,
|
|
}
|
|
|
|
handleMsgMhfSavedata(s, pkt)
|
|
done <- true
|
|
}(i)
|
|
}
|
|
|
|
// Wait for all saves to complete
|
|
for i := 0; i < 5; i++ {
|
|
<-done
|
|
}
|
|
|
|
// Verify all characters were saved
|
|
for i := 0; i < 5; i++ {
|
|
var saveData []byte
|
|
err := db.QueryRow("SELECT savedata FROM characters WHERE id = $1", charIDs[i]).Scan(&saveData)
|
|
if err != nil {
|
|
t.Errorf("character %d: failed to load savedata: %v", i, err)
|
|
}
|
|
if len(saveData) == 0 {
|
|
t.Errorf("character %d: savedata is empty", i)
|
|
}
|
|
}
|
|
}
|