mirror of
https://github.com/Mezeporta/Erupe.git
synced 2026-03-22 15:43:49 +01:00
- Guard nil listener/acceptConns in Server.Shutdown() to prevent panic in test servers that don't bind a network listener - Remove redundant userBinaryPartsLock in TestHandleMsgMhfLoaddata that caused a deadlock with handleMsgMhfLoaddata's own lock acquisition - Increase test save blob size from 200 to 150000 bytes to accommodate ZZ save pointer offsets (up to 146728) - Initialize MHFEquipment.Sigils[].Effects slices in test data to prevent index-out-of-range panic in SerializeWarehouseEquipment - Insert warehouse row before updating it (UPDATE on 0 rows is not an error, so the INSERT fallback never triggered) - Use COALESCE for nullable kouryou_point column in kill counter test - Fix duplicate-add test expectation (CSV helper correctly deduplicates)
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"
|
|
s.server.db = 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
|
|
s.server.db = 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
|
|
s.server.db = 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
|
|
s.server.db = 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"
|
|
s.server.db = 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)
|
|
s.server.db = 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)
|
|
}
|
|
}
|
|
}
|