mirror of
https://github.com/Mezeporta/Erupe.git
synced 2026-03-22 07:32:32 +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.
605 lines
15 KiB
Go
605 lines
15 KiB
Go
package channelserver
|
|
|
|
import (
|
|
"fmt"
|
|
"testing"
|
|
|
|
_config "erupe-ce/config"
|
|
"erupe-ce/common/byteframe"
|
|
"erupe-ce/network/mhfpacket"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
// TestHandleMsgSysEnumerateClient tests client enumeration in stages
|
|
func TestHandleMsgSysEnumerateClient(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
stageID string
|
|
getType uint8
|
|
setupStage func(*Server, string)
|
|
wantClientCount int
|
|
wantFailure bool
|
|
}{
|
|
{
|
|
name: "enumerate_all_clients",
|
|
stageID: "test_stage_1",
|
|
getType: 0, // All clients
|
|
setupStage: func(server *Server, stageID string) {
|
|
stage := NewStage(stageID)
|
|
mock1 := &MockCryptConn{sentPackets: make([][]byte, 0)}
|
|
mock2 := &MockCryptConn{sentPackets: make([][]byte, 0)}
|
|
s1 := createTestSession(mock1)
|
|
s2 := createTestSession(mock2)
|
|
s1.charID = 100
|
|
s2.charID = 200
|
|
stage.clients[s1] = 100
|
|
stage.clients[s2] = 200
|
|
server.stagesLock.Lock()
|
|
server.stages[stageID] = stage
|
|
server.stagesLock.Unlock()
|
|
},
|
|
wantClientCount: 2,
|
|
wantFailure: false,
|
|
},
|
|
{
|
|
name: "enumerate_not_ready_clients",
|
|
stageID: "test_stage_2",
|
|
getType: 1, // Not ready
|
|
setupStage: func(server *Server, stageID string) {
|
|
stage := NewStage(stageID)
|
|
stage.reservedClientSlots[100] = false // Not ready
|
|
stage.reservedClientSlots[200] = true // Ready
|
|
stage.reservedClientSlots[300] = false // Not ready
|
|
server.stagesLock.Lock()
|
|
server.stages[stageID] = stage
|
|
server.stagesLock.Unlock()
|
|
},
|
|
wantClientCount: 2, // Only not-ready clients
|
|
wantFailure: false,
|
|
},
|
|
{
|
|
name: "enumerate_ready_clients",
|
|
stageID: "test_stage_3",
|
|
getType: 2, // Ready
|
|
setupStage: func(server *Server, stageID string) {
|
|
stage := NewStage(stageID)
|
|
stage.reservedClientSlots[100] = false // Not ready
|
|
stage.reservedClientSlots[200] = true // Ready
|
|
stage.reservedClientSlots[300] = true // Ready
|
|
server.stagesLock.Lock()
|
|
server.stages[stageID] = stage
|
|
server.stagesLock.Unlock()
|
|
},
|
|
wantClientCount: 2, // Only ready clients
|
|
wantFailure: false,
|
|
},
|
|
{
|
|
name: "enumerate_empty_stage",
|
|
stageID: "test_stage_empty",
|
|
getType: 0,
|
|
setupStage: func(server *Server, stageID string) {
|
|
stage := NewStage(stageID)
|
|
server.stagesLock.Lock()
|
|
server.stages[stageID] = stage
|
|
server.stagesLock.Unlock()
|
|
},
|
|
wantClientCount: 0,
|
|
wantFailure: false,
|
|
},
|
|
{
|
|
name: "enumerate_nonexistent_stage",
|
|
stageID: "nonexistent_stage",
|
|
getType: 0,
|
|
setupStage: func(server *Server, stageID string) {
|
|
// Don't create the stage
|
|
},
|
|
wantClientCount: 0,
|
|
wantFailure: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// Create test session (which creates a server with erupeConfig)
|
|
mock := &MockCryptConn{sentPackets: make([][]byte, 0)}
|
|
s := createTestSession(mock)
|
|
|
|
// Initialize stages map if needed
|
|
if s.server.stages == nil {
|
|
s.server.stages = make(map[string]*Stage)
|
|
}
|
|
|
|
// Setup stage
|
|
tt.setupStage(s.server, tt.stageID)
|
|
|
|
pkt := &mhfpacket.MsgSysEnumerateClient{
|
|
AckHandle: 1234,
|
|
StageID: tt.stageID,
|
|
Get: tt.getType,
|
|
}
|
|
|
|
handleMsgSysEnumerateClient(s, pkt)
|
|
|
|
// Check if ACK was sent
|
|
if len(s.sendPackets) == 0 {
|
|
t.Fatal("no ACK packet was sent")
|
|
}
|
|
|
|
// Read the ACK packet
|
|
ackPkt := <-s.sendPackets
|
|
if tt.wantFailure {
|
|
// For failures, we can't easily check the exact format
|
|
// Just verify something was sent
|
|
return
|
|
}
|
|
|
|
// Parse the response to count clients
|
|
// The ackPkt.data contains the full packet structure:
|
|
// [opcode:2 bytes][ack_handle:4 bytes][is_buffer:1 byte][error_code:1 byte][payload_size:2 bytes][data...]
|
|
// Total header size: 2 + 4 + 1 + 1 + 2 = 10 bytes
|
|
if len(ackPkt.data) < 10 {
|
|
t.Fatal("ACK packet too small")
|
|
}
|
|
|
|
// The response data starts after the 10-byte header
|
|
// Response format is: [count:uint16][charID1:uint32][charID2:uint32]...
|
|
bf := byteframe.NewByteFrameFromBytes(ackPkt.data[10:]) // Skip full ACK header
|
|
count := bf.ReadUint16()
|
|
|
|
if int(count) != tt.wantClientCount {
|
|
t.Errorf("client count = %d, want %d", count, tt.wantClientCount)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestHandleMsgMhfListMember tests listing blacklisted members
|
|
func TestHandleMsgMhfListMember_Integration(t *testing.T) {
|
|
db := SetupTestDB(t)
|
|
defer TeardownTestDB(t, db)
|
|
|
|
tests := []struct {
|
|
name string
|
|
blockedCSV string
|
|
wantBlockCount int
|
|
}{
|
|
{
|
|
name: "no_blocked_users",
|
|
blockedCSV: "",
|
|
wantBlockCount: 0,
|
|
},
|
|
{
|
|
name: "single_blocked_user",
|
|
blockedCSV: "2",
|
|
wantBlockCount: 1,
|
|
},
|
|
{
|
|
name: "multiple_blocked_users",
|
|
blockedCSV: "2,3,4",
|
|
wantBlockCount: 3,
|
|
},
|
|
}
|
|
|
|
for i, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// Create test user and character (use short names to avoid 15 char limit)
|
|
userID := CreateTestUser(t, db, "user_"+tt.name)
|
|
charName := fmt.Sprintf("Char%d", i)
|
|
charID := CreateTestCharacter(t, db, userID, charName)
|
|
|
|
// Create blocked characters
|
|
if tt.blockedCSV != "" {
|
|
// Create the blocked users
|
|
for i := 2; i <= 4; i++ {
|
|
blockedUserID := CreateTestUser(t, db, "blocked_user_"+tt.name+"_"+string(rune(i)))
|
|
CreateTestCharacter(t, db, blockedUserID, "BlockedChar_"+string(rune(i)))
|
|
}
|
|
}
|
|
|
|
// Set blocked list
|
|
_, err := db.Exec("UPDATE characters SET blocked = $1 WHERE id = $2", tt.blockedCSV, charID)
|
|
if err != nil {
|
|
t.Fatalf("Failed to update blocked list: %v", err)
|
|
}
|
|
|
|
// Create test session
|
|
mock := &MockCryptConn{sentPackets: make([][]byte, 0)}
|
|
s := createTestSession(mock)
|
|
s.charID = charID
|
|
SetTestDB(s.server, db)
|
|
|
|
pkt := &mhfpacket.MsgMhfListMember{
|
|
AckHandle: 5678,
|
|
}
|
|
|
|
handleMsgMhfListMember(s, pkt)
|
|
|
|
// Verify ACK was sent
|
|
if len(s.sendPackets) == 0 {
|
|
t.Fatal("no ACK packet was sent")
|
|
}
|
|
|
|
// Parse response
|
|
// The ackPkt.data contains the full packet structure:
|
|
// [opcode:2 bytes][ack_handle:4 bytes][is_buffer:1 byte][error_code:1 byte][payload_size:2 bytes][data...]
|
|
// Total header size: 2 + 4 + 1 + 1 + 2 = 10 bytes
|
|
ackPkt := <-s.sendPackets
|
|
if len(ackPkt.data) < 10 {
|
|
t.Fatal("ACK packet too small")
|
|
}
|
|
bf := byteframe.NewByteFrameFromBytes(ackPkt.data[10:]) // Skip full ACK header
|
|
count := bf.ReadUint32()
|
|
|
|
if int(count) != tt.wantBlockCount {
|
|
t.Errorf("blocked count = %d, want %d", count, tt.wantBlockCount)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestHandleMsgMhfOprMember tests blacklist/friendlist operations
|
|
func TestHandleMsgMhfOprMember_Integration(t *testing.T) {
|
|
db := SetupTestDB(t)
|
|
defer TeardownTestDB(t, db)
|
|
|
|
tests := []struct {
|
|
name string
|
|
isBlacklist bool
|
|
operation bool // true = remove, false = add
|
|
initialList string
|
|
targetCharIDs []uint32
|
|
wantList string
|
|
}{
|
|
{
|
|
name: "add_to_blacklist",
|
|
isBlacklist: true,
|
|
operation: false,
|
|
initialList: "",
|
|
targetCharIDs: []uint32{2},
|
|
wantList: "2",
|
|
},
|
|
{
|
|
name: "remove_from_blacklist",
|
|
isBlacklist: true,
|
|
operation: true,
|
|
initialList: "2,3,4",
|
|
targetCharIDs: []uint32{3},
|
|
wantList: "2,4",
|
|
},
|
|
{
|
|
name: "add_to_friendlist",
|
|
isBlacklist: false,
|
|
operation: false,
|
|
initialList: "10",
|
|
targetCharIDs: []uint32{20},
|
|
wantList: "10,20",
|
|
},
|
|
{
|
|
name: "remove_from_friendlist",
|
|
isBlacklist: false,
|
|
operation: true,
|
|
initialList: "10,20,30",
|
|
targetCharIDs: []uint32{20},
|
|
wantList: "10,30",
|
|
},
|
|
{
|
|
name: "add_multiple_to_blacklist",
|
|
isBlacklist: true,
|
|
operation: false,
|
|
initialList: "1",
|
|
targetCharIDs: []uint32{2, 3},
|
|
wantList: "1,2,3",
|
|
},
|
|
}
|
|
|
|
for i, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// Create test user and character (use short names to avoid 15 char limit)
|
|
userID := CreateTestUser(t, db, "user_"+tt.name)
|
|
charName := fmt.Sprintf("OpChar%d", i)
|
|
charID := CreateTestCharacter(t, db, userID, charName)
|
|
|
|
// Set initial list
|
|
column := "blocked"
|
|
if !tt.isBlacklist {
|
|
column = "friends"
|
|
}
|
|
_, err := db.Exec("UPDATE characters SET "+column+" = $1 WHERE id = $2", tt.initialList, charID)
|
|
if err != nil {
|
|
t.Fatalf("Failed to set initial list: %v", err)
|
|
}
|
|
|
|
// Create test session
|
|
mock := &MockCryptConn{sentPackets: make([][]byte, 0)}
|
|
s := createTestSession(mock)
|
|
s.charID = charID
|
|
SetTestDB(s.server, db)
|
|
|
|
pkt := &mhfpacket.MsgMhfOprMember{
|
|
AckHandle: 9999,
|
|
Blacklist: tt.isBlacklist,
|
|
Operation: tt.operation,
|
|
CharIDs: tt.targetCharIDs,
|
|
}
|
|
|
|
handleMsgMhfOprMember(s, pkt)
|
|
|
|
// Verify ACK was sent
|
|
if len(s.sendPackets) == 0 {
|
|
t.Fatal("no ACK packet was sent")
|
|
}
|
|
<-s.sendPackets
|
|
|
|
// Verify the list was updated
|
|
var gotList string
|
|
err = db.QueryRow("SELECT "+column+" FROM characters WHERE id = $1", charID).Scan(&gotList)
|
|
if err != nil {
|
|
t.Fatalf("Failed to query updated list: %v", err)
|
|
}
|
|
|
|
if gotList != tt.wantList {
|
|
t.Errorf("list = %q, want %q", gotList, tt.wantList)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestHandleMsgMhfShutClient tests the shut client handler
|
|
func TestHandleMsgMhfShutClient(t *testing.T) {
|
|
mock := &MockCryptConn{sentPackets: make([][]byte, 0)}
|
|
s := createTestSession(mock)
|
|
|
|
pkt := &mhfpacket.MsgMhfShutClient{}
|
|
|
|
// Should not panic (handler is empty)
|
|
handleMsgMhfShutClient(s, pkt)
|
|
}
|
|
|
|
// TestHandleMsgSysHideClient tests the hide client handler
|
|
func TestHandleMsgSysHideClient(t *testing.T) {
|
|
mock := &MockCryptConn{sentPackets: make([][]byte, 0)}
|
|
s := createTestSession(mock)
|
|
|
|
tests := []struct {
|
|
name string
|
|
hide bool
|
|
}{
|
|
{
|
|
name: "hide_client",
|
|
hide: true,
|
|
},
|
|
{
|
|
name: "show_client",
|
|
hide: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
pkt := &mhfpacket.MsgSysHideClient{
|
|
Hide: tt.hide,
|
|
}
|
|
|
|
// Should not panic (handler is empty)
|
|
handleMsgSysHideClient(s, pkt)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestEnumerateClient_ConcurrentAccess tests concurrent stage access
|
|
func TestEnumerateClient_ConcurrentAccess(t *testing.T) {
|
|
logger, _ := zap.NewDevelopment()
|
|
server := &Server{
|
|
logger: logger,
|
|
stages: make(map[string]*Stage),
|
|
erupeConfig: &_config.Config{
|
|
DebugOptions: _config.DebugOptions{
|
|
LogOutboundMessages: false,
|
|
},
|
|
},
|
|
}
|
|
|
|
stageID := "concurrent_test_stage"
|
|
stage := NewStage(stageID)
|
|
|
|
// Add some clients to the stage
|
|
for i := uint32(1); i <= 10; i++ {
|
|
mock := &MockCryptConn{sentPackets: make([][]byte, 0)}
|
|
sess := createTestSession(mock)
|
|
sess.charID = i * 100
|
|
stage.clients[sess] = i * 100
|
|
}
|
|
|
|
server.stagesLock.Lock()
|
|
server.stages[stageID] = stage
|
|
server.stagesLock.Unlock()
|
|
|
|
// Run concurrent enumerations
|
|
done := make(chan bool, 5)
|
|
for i := 0; i < 5; i++ {
|
|
go func() {
|
|
mock := &MockCryptConn{sentPackets: make([][]byte, 0)}
|
|
s := createTestSession(mock)
|
|
s.server = server
|
|
|
|
pkt := &mhfpacket.MsgSysEnumerateClient{
|
|
AckHandle: 3333,
|
|
StageID: stageID,
|
|
Get: 0, // All clients
|
|
}
|
|
|
|
handleMsgSysEnumerateClient(s, pkt)
|
|
done <- true
|
|
}()
|
|
}
|
|
|
|
// Wait for all goroutines to complete
|
|
for i := 0; i < 5; i++ {
|
|
<-done
|
|
}
|
|
}
|
|
|
|
// TestListMember_EmptyDatabase tests listing members when database is empty
|
|
func TestListMember_EmptyDatabase_Integration(t *testing.T) {
|
|
db := SetupTestDB(t)
|
|
defer TeardownTestDB(t, db)
|
|
|
|
// Create test user and character
|
|
userID := CreateTestUser(t, db, "emptytest")
|
|
charID := CreateTestCharacter(t, db, userID, "EmptyChar")
|
|
|
|
// Create test session
|
|
mock := &MockCryptConn{sentPackets: make([][]byte, 0)}
|
|
s := createTestSession(mock)
|
|
s.charID = charID
|
|
SetTestDB(s.server, db)
|
|
|
|
pkt := &mhfpacket.MsgMhfListMember{
|
|
AckHandle: 4444,
|
|
}
|
|
|
|
handleMsgMhfListMember(s, pkt)
|
|
|
|
// Verify ACK was sent
|
|
if len(s.sendPackets) == 0 {
|
|
t.Fatal("no ACK packet was sent")
|
|
}
|
|
|
|
ackPkt := <-s.sendPackets
|
|
if len(ackPkt.data) < 10 {
|
|
t.Fatal("ACK packet too small")
|
|
}
|
|
bf := byteframe.NewByteFrameFromBytes(ackPkt.data[10:]) // Skip full ACK header
|
|
count := bf.ReadUint32()
|
|
|
|
if count != 0 {
|
|
t.Errorf("empty blocked list should have count 0, got %d", count)
|
|
}
|
|
}
|
|
|
|
// TestOprMember_EdgeCases tests edge cases for member operations
|
|
func TestOprMember_EdgeCases_Integration(t *testing.T) {
|
|
db := SetupTestDB(t)
|
|
defer TeardownTestDB(t, db)
|
|
|
|
tests := []struct {
|
|
name string
|
|
initialList string
|
|
operation bool
|
|
targetCharIDs []uint32
|
|
wantList string
|
|
}{
|
|
{
|
|
name: "add_duplicate_to_list",
|
|
initialList: "1,2,3",
|
|
operation: false, // add
|
|
targetCharIDs: []uint32{2},
|
|
wantList: "1,2,3", // CSV helper deduplicates
|
|
},
|
|
{
|
|
name: "remove_nonexistent_from_list",
|
|
initialList: "1,2,3",
|
|
operation: true, // remove
|
|
targetCharIDs: []uint32{99},
|
|
wantList: "1,2,3",
|
|
},
|
|
{
|
|
name: "operate_on_empty_list",
|
|
initialList: "",
|
|
operation: false,
|
|
targetCharIDs: []uint32{1},
|
|
wantList: "1",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// Create test user and character
|
|
userID := CreateTestUser(t, db, "edge_"+tt.name)
|
|
charID := CreateTestCharacter(t, db, userID, "EdgeChar")
|
|
|
|
// Set initial blocked list
|
|
_, err := db.Exec("UPDATE characters SET blocked = $1 WHERE id = $2", tt.initialList, charID)
|
|
if err != nil {
|
|
t.Fatalf("Failed to set initial list: %v", err)
|
|
}
|
|
|
|
// Create test session
|
|
mock := &MockCryptConn{sentPackets: make([][]byte, 0)}
|
|
s := createTestSession(mock)
|
|
s.charID = charID
|
|
SetTestDB(s.server, db)
|
|
|
|
pkt := &mhfpacket.MsgMhfOprMember{
|
|
AckHandle: 7777,
|
|
Blacklist: true,
|
|
Operation: tt.operation,
|
|
CharIDs: tt.targetCharIDs,
|
|
}
|
|
|
|
handleMsgMhfOprMember(s, pkt)
|
|
|
|
// Verify ACK was sent
|
|
if len(s.sendPackets) == 0 {
|
|
t.Fatal("no ACK packet was sent")
|
|
}
|
|
<-s.sendPackets
|
|
|
|
// Verify the list
|
|
var gotList string
|
|
err = db.QueryRow("SELECT blocked FROM characters WHERE id = $1", charID).Scan(&gotList)
|
|
if err != nil {
|
|
t.Fatalf("Failed to query list: %v", err)
|
|
}
|
|
|
|
if gotList != tt.wantList {
|
|
t.Errorf("list = %q, want %q", gotList, tt.wantList)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// BenchmarkEnumerateClients benchmarks client enumeration
|
|
func BenchmarkEnumerateClients(b *testing.B) {
|
|
logger, _ := zap.NewDevelopment()
|
|
server := &Server{
|
|
logger: logger,
|
|
stages: make(map[string]*Stage),
|
|
}
|
|
|
|
stageID := "bench_stage"
|
|
stage := NewStage(stageID)
|
|
|
|
// Add 100 clients to the stage
|
|
for i := uint32(1); i <= 100; i++ {
|
|
mock := &MockCryptConn{sentPackets: make([][]byte, 0)}
|
|
sess := createTestSession(mock)
|
|
sess.charID = i
|
|
stage.clients[sess] = i
|
|
}
|
|
|
|
server.stages[stageID] = stage
|
|
|
|
mock := &MockCryptConn{sentPackets: make([][]byte, 0)}
|
|
s := createTestSession(mock)
|
|
s.server = server
|
|
|
|
pkt := &mhfpacket.MsgSysEnumerateClient{
|
|
AckHandle: 8888,
|
|
StageID: stageID,
|
|
Get: 0,
|
|
}
|
|
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
// Clear the packet channel
|
|
select {
|
|
case <-s.sendPackets:
|
|
default:
|
|
}
|
|
|
|
handleMsgSysEnumerateClient(s, pkt)
|
|
<-s.sendPackets
|
|
}
|
|
}
|