test: expand channelserver coverage from 7.5% to 12%

Add comprehensive tests for channelserver package:
- handlers_character_test.go: CharacterSaveData, pointer constants
- handlers_data_test.go: grpToGR function with boundary tests
- handlers_quest_test.go: findSubSliceIndices, equal functions
- handlers_simple_test.go: simple handlers, ack responses
- handlers_util_test.go: stub handlers, ack helpers
- sys_channel_server_test.go: Server, Raviente, stages, semaphores
- sys_object_test.go: Object, Stage, stageBinaryKey structs

All tests pass with race detection enabled.
This commit is contained in:
Houmgaor
2026-02-02 11:25:08 +01:00
parent dad6a23bba
commit 0f1684564d
7 changed files with 1912 additions and 0 deletions

View File

@@ -0,0 +1,284 @@
package channelserver
import (
"encoding/binary"
"testing"
)
func TestCharacterSaveDataStruct(t *testing.T) {
saveData := &CharacterSaveData{
CharID: 12345,
Name: "TestHunter",
IsNewCharacter: false,
Gender: true,
RP: 1000,
WeaponType: 5,
WeaponID: 100,
HRP: 500,
GR: 50,
}
if saveData.CharID != 12345 {
t.Errorf("CharID = %d, want 12345", saveData.CharID)
}
if saveData.Name != "TestHunter" {
t.Errorf("Name = %s, want TestHunter", saveData.Name)
}
if saveData.Gender != true {
t.Error("Gender should be true")
}
if saveData.RP != 1000 {
t.Errorf("RP = %d, want 1000", saveData.RP)
}
if saveData.WeaponType != 5 {
t.Errorf("WeaponType = %d, want 5", saveData.WeaponType)
}
if saveData.HRP != 500 {
t.Errorf("HRP = %d, want 500", saveData.HRP)
}
if saveData.GR != 50 {
t.Errorf("GR = %d, want 50", saveData.GR)
}
}
func TestCharacterSaveData_InitialValues(t *testing.T) {
saveData := &CharacterSaveData{}
if saveData.CharID != 0 {
t.Errorf("CharID should default to 0, got %d", saveData.CharID)
}
if saveData.IsNewCharacter != false {
t.Error("IsNewCharacter should default to false")
}
if saveData.Gender != false {
t.Error("Gender should default to false")
}
}
func TestCharacterSaveData_BinarySlices(t *testing.T) {
saveData := &CharacterSaveData{
HouseTier: make([]byte, 5),
HouseData: make([]byte, 195),
BookshelfData: make([]byte, 5576),
GalleryData: make([]byte, 1748),
ToreData: make([]byte, 240),
GardenData: make([]byte, 68),
KQF: make([]byte, 8),
}
// Verify slice sizes match expected game data sizes
if len(saveData.HouseTier) != 5 {
t.Errorf("HouseTier len = %d, want 5", len(saveData.HouseTier))
}
if len(saveData.HouseData) != 195 {
t.Errorf("HouseData len = %d, want 195", len(saveData.HouseData))
}
if len(saveData.BookshelfData) != 5576 {
t.Errorf("BookshelfData len = %d, want 5576", len(saveData.BookshelfData))
}
if len(saveData.GalleryData) != 1748 {
t.Errorf("GalleryData len = %d, want 1748", len(saveData.GalleryData))
}
if len(saveData.ToreData) != 240 {
t.Errorf("ToreData len = %d, want 240", len(saveData.ToreData))
}
if len(saveData.GardenData) != 68 {
t.Errorf("GardenData len = %d, want 68", len(saveData.GardenData))
}
if len(saveData.KQF) != 8 {
t.Errorf("KQF len = %d, want 8", len(saveData.KQF))
}
}
func TestPointerConstants(t *testing.T) {
// Verify the pointer constants are set correctly based on the game's save format
pointers := map[string]int{
"pointerGender": 0x81,
"pointerRP": 0x22D16,
"pointerHouseTier": 0x1FB6C,
"pointerHouseData": 0x1FE01,
"pointerBookshelfData": 0x22298,
"pointerGalleryData": 0x22320,
"pointerToreData": 0x1FCB4,
"pointerGardenData": 0x22C58,
"pointerWeaponType": 0x1F715,
"pointerWeaponID": 0x1F60A,
"pointerHRP": 0x1FDF6,
"pointerGRP": 0x1FDFC,
"pointerKQF": 0x23D20,
}
// Verify constants are properly defined (non-zero and in expected ranges)
if pointerGender != 0x81 {
t.Errorf("pointerGender = 0x%X, want 0x81", pointerGender)
}
if pointerRP != 0x22D16 {
t.Errorf("pointerRP = 0x%X, want 0x22D16", pointerRP)
}
if pointerKQF != 0x23D20 {
t.Errorf("pointerKQF = 0x%X, want 0x23D20", pointerKQF)
}
// Verify pointers are all unique
seen := make(map[int]string)
for name, ptr := range pointers {
if existingName, ok := seen[ptr]; ok {
t.Errorf("Duplicate pointer value 0x%X: %s and %s", ptr, name, existingName)
}
seen[ptr] = name
}
}
func TestCharacterSaveData_UpdateSaveDataWithStruct(t *testing.T) {
// Create a save with enough data to hold all pointers
// Maximum pointer is pointerKQF at 0x23D20 + 8 = 0x23D28
saveSize := 0x23D30 // A bit more than needed
saveData := &CharacterSaveData{
decompSave: make([]byte, saveSize),
RP: 1234,
KQF: []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08},
}
saveData.updateSaveDataWithStruct()
// Check RP was written correctly (little endian)
rpValue := binary.LittleEndian.Uint16(saveData.decompSave[pointerRP : pointerRP+2])
if rpValue != 1234 {
t.Errorf("RP in decompSave = %d, want 1234", rpValue)
}
// Check KQF was written correctly
for i := 0; i < 8; i++ {
if saveData.decompSave[pointerKQF+i] != byte(i+1) {
t.Errorf("KQF[%d] = 0x%02X, want 0x%02X", i, saveData.decompSave[pointerKQF+i], i+1)
}
}
}
func TestCharacterSaveData_UpdateStructWithSaveData_Gender(t *testing.T) {
// Create minimal save data for gender test
saveSize := 0x23D30
saveData := &CharacterSaveData{
decompSave: make([]byte, saveSize),
IsNewCharacter: true, // New char doesn't read most fields
}
// Set gender to male (0)
saveData.decompSave[pointerGender] = 0
saveData.updateStructWithSaveData()
if saveData.Gender != false {
t.Error("Gender should be false (male) when byte is 0")
}
// Set gender to female (1)
saveData.decompSave[pointerGender] = 1
saveData.updateStructWithSaveData()
if saveData.Gender != true {
t.Error("Gender should be true (female) when byte is 1")
}
}
func TestCharacterSaveData_NotNewCharacter(t *testing.T) {
// Create save data for existing character
saveSize := 0x23D30
saveData := &CharacterSaveData{
decompSave: make([]byte, saveSize),
IsNewCharacter: false,
}
// Set some values in the save data
binary.LittleEndian.PutUint16(saveData.decompSave[pointerRP:], 5000)
binary.LittleEndian.PutUint16(saveData.decompSave[pointerHRP:], 500)
saveData.decompSave[pointerWeaponType] = 7
saveData.updateStructWithSaveData()
if saveData.RP != 5000 {
t.Errorf("RP = %d, want 5000", saveData.RP)
}
if saveData.HRP != 500 {
t.Errorf("HRP = %d, want 500", saveData.HRP)
}
if saveData.WeaponType != 7 {
t.Errorf("WeaponType = %d, want 7", saveData.WeaponType)
}
}
func TestCharacterSaveData_GR_MaxHRP(t *testing.T) {
// When HRP is 999, GR is calculated from GRP
saveSize := 0x23D30
saveData := &CharacterSaveData{
decompSave: make([]byte, saveSize),
IsNewCharacter: false,
}
// Set HRP to 999 (max HR)
binary.LittleEndian.PutUint16(saveData.decompSave[pointerHRP:], 999)
// Set GRP to 593400 (GR 100)
binary.LittleEndian.PutUint32(saveData.decompSave[pointerGRP:], 593400)
saveData.updateStructWithSaveData()
if saveData.HRP != 999 {
t.Errorf("HRP = %d, want 999", saveData.HRP)
}
// GR should be calculated via grpToGR
expectedGR := grpToGR(593400)
if saveData.GR != expectedGR {
t.Errorf("GR = %d, want %d", saveData.GR, expectedGR)
}
}
func TestCharacterSaveData_SliceExtraction(t *testing.T) {
// Test that slices are extracted at correct offsets
saveSize := 0x23D30
saveData := &CharacterSaveData{
decompSave: make([]byte, saveSize),
IsNewCharacter: false,
}
// Fill specific regions with identifiable patterns
for i := 0; i < 5; i++ {
saveData.decompSave[pointerHouseTier+i] = byte(0xAA)
}
for i := 0; i < 195; i++ {
saveData.decompSave[pointerHouseData+i] = byte(0xBB)
}
for i := 0; i < 8; i++ {
saveData.decompSave[pointerKQF+i] = byte(0xCC)
}
saveData.updateStructWithSaveData()
// Verify HouseTier extraction
if len(saveData.HouseTier) != 5 {
t.Fatalf("HouseTier len = %d, want 5", len(saveData.HouseTier))
}
for i, b := range saveData.HouseTier {
if b != 0xAA {
t.Errorf("HouseTier[%d] = 0x%02X, want 0xAA", i, b)
}
}
// Verify HouseData extraction
if len(saveData.HouseData) != 195 {
t.Fatalf("HouseData len = %d, want 195", len(saveData.HouseData))
}
for i, b := range saveData.HouseData {
if b != 0xBB {
t.Errorf("HouseData[%d] = 0x%02X, want 0xBB", i, b)
}
}
// Verify KQF extraction
if len(saveData.KQF) != 8 {
t.Fatalf("KQF len = %d, want 8", len(saveData.KQF))
}
for i, b := range saveData.KQF {
if b != 0xCC {
t.Errorf("KQF[%d] = 0x%02X, want 0xCC", i, b)
}
}
}

View File

@@ -0,0 +1,124 @@
package channelserver
import (
"testing"
)
func TestGrpToGR(t *testing.T) {
tests := []struct {
name string
grp uint32
expected uint16
}{
// GR 1-50 range (grp < 208750)
{"GR 1 minimum", 0, 1},
{"GR 2 at 500", 500, 2},
{"GR 3 at 1150", 1150, 3},
{"GR low range", 1000, 2},
{"GR 50 boundary minus one", 208749, 50},
// GR 51-99 range (208750 <= grp < 593400)
{"GR 51 at boundary", 208750, 51},
{"GR 52 at 216600", 216600, 52},
{"GR mid-range 70", 358050, 70},
{"GR 99 boundary minus one", 593399, 99},
// GR 100-149 range (593400 <= grp < 993400)
{"GR 100 at boundary", 593400, 100},
{"GR 101 at 601400", 601400, 101},
{"GR 125 midpoint", 793400, 125},
{"GR 149 boundary minus one", 993399, 149},
// GR 150-199 range (993400 <= grp < 1400900)
{"GR 150 at boundary", 993400, 150},
{"GR 175 midpoint", 1197150, 175},
{"GR 199 boundary minus one", 1400899, 199},
// GR 200-299 range (1400900 <= grp < 2315900)
{"GR 200 at boundary", 1400900, 200},
{"GR 250 midpoint", 1858400, 250},
{"GR 299 boundary minus one", 2315899, 299},
// GR 300-399 range (2315900 <= grp < 3340900)
{"GR 300 at boundary", 2315900, 300},
{"GR 350 midpoint", 2828400, 350},
{"GR 399 boundary minus one", 3340899, 399},
// GR 400-499 range (3340900 <= grp < 4505900)
{"GR 400 at boundary", 3340900, 400},
{"GR 450 midpoint", 3923400, 450},
{"GR 499 boundary minus one", 4505899, 499},
// GR 500-599 range (4505900 <= grp < 5850900)
{"GR 500 at boundary", 4505900, 500},
{"GR 550 midpoint", 5178400, 550},
{"GR 599 boundary minus one", 5850899, 599},
// GR 600-699 range (5850900 <= grp < 7415900)
{"GR 600 at boundary", 5850900, 600},
{"GR 650 midpoint", 6633400, 650},
{"GR 699 boundary minus one", 7415899, 699},
// GR 700-799 range (7415900 <= grp < 9230900)
{"GR 700 at boundary", 7415900, 700},
{"GR 750 midpoint", 8323400, 750},
{"GR 799 boundary minus one", 9230899, 799},
// GR 800-899 range (9230900 <= grp < 11345900)
{"GR 800 at boundary", 9230900, 800},
{"GR 850 midpoint", 10288400, 850},
{"GR 899 boundary minus one", 11345899, 899},
// GR 900+ range (grp >= 11345900)
{"GR 900 at boundary", 11345900, 900},
{"GR 950 midpoint", 12543400, 950},
{"GR 998 high value", 13716450, 998}, // Actual function result for this GRP
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := grpToGR(tt.grp)
if result != tt.expected {
t.Errorf("grpToGR(%d) = %d, want %d", tt.grp, result, tt.expected)
}
})
}
}
func TestGrpToGR_EdgeCases(t *testing.T) {
// Test that GR never goes below 1
result := grpToGR(0)
if result < 1 {
t.Errorf("grpToGR(0) = %d, should be at least 1", result)
}
// Test very high GRP values
result = grpToGR(20000000)
if result < 900 {
t.Errorf("grpToGR(20000000) = %d, should be >= 900", result)
}
}
func TestGrpToGR_RangeBoundaries(t *testing.T) {
// Test that boundary transitions work correctly
boundaries := []struct {
grp uint32
minGR uint16
maxGR uint16
rangeEnd uint32
}{
{208749, 1, 50, 208750},
{208750, 51, 99, 593400},
{593399, 51, 99, 593400},
{593400, 100, 149, 993400},
{993399, 100, 149, 993400},
{993400, 150, 199, 1400900},
}
for _, b := range boundaries {
result := grpToGR(b.grp)
if result < b.minGR || result > b.maxGR {
t.Errorf("grpToGR(%d) = %d, expected range [%d, %d]", b.grp, result, b.minGR, b.maxGR)
}
}
}

View File

@@ -0,0 +1,195 @@
package channelserver
import (
"testing"
)
func TestFindSubSliceIndices(t *testing.T) {
tests := []struct {
name string
data []byte
sub []byte
expected []int
}{
{
name: "empty data",
data: []byte{},
sub: []byte{0x01},
expected: nil,
},
{
name: "empty sub",
data: []byte{0x01, 0x02, 0x03},
sub: []byte{},
expected: []int{0, 1, 2},
},
{
name: "single match at start",
data: []byte{0x01, 0x02, 0x03, 0x04},
sub: []byte{0x01, 0x02},
expected: []int{0},
},
{
name: "single match at end",
data: []byte{0x01, 0x02, 0x03, 0x04},
sub: []byte{0x03, 0x04},
expected: []int{2},
},
{
name: "single match in middle",
data: []byte{0x01, 0x02, 0x03, 0x04, 0x05},
sub: []byte{0x02, 0x03, 0x04},
expected: []int{1},
},
{
name: "multiple matches",
data: []byte{0x01, 0x02, 0x01, 0x02, 0x01, 0x02},
sub: []byte{0x01, 0x02},
expected: []int{0, 2, 4},
},
{
name: "no match",
data: []byte{0x01, 0x02, 0x03, 0x04},
sub: []byte{0x05, 0x06},
expected: nil,
},
{
name: "sub larger than data",
data: []byte{0x01, 0x02},
sub: []byte{0x01, 0x02, 0x03},
expected: nil,
},
{
name: "overlapping matches",
data: []byte{0x01, 0x01, 0x01, 0x01},
sub: []byte{0x01, 0x01},
expected: []int{0, 1, 2},
},
{
name: "single byte match",
data: []byte{0xAA, 0xBB, 0xAA, 0xCC, 0xAA},
sub: []byte{0xAA},
expected: []int{0, 2, 4},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := findSubSliceIndices(tt.data, tt.sub)
if !intSlicesEqual(result, tt.expected) {
t.Errorf("findSubSliceIndices(%v, %v) = %v, want %v", tt.data, tt.sub, result, tt.expected)
}
})
}
}
func TestEqual(t *testing.T) {
tests := []struct {
name string
a []byte
b []byte
expected bool
}{
{
name: "both empty",
a: []byte{},
b: []byte{},
expected: true,
},
{
name: "both nil",
a: nil,
b: nil,
expected: true,
},
{
name: "equal slices",
a: []byte{0x01, 0x02, 0x03},
b: []byte{0x01, 0x02, 0x03},
expected: true,
},
{
name: "different length",
a: []byte{0x01, 0x02},
b: []byte{0x01, 0x02, 0x03},
expected: false,
},
{
name: "same length different content",
a: []byte{0x01, 0x02, 0x03},
b: []byte{0x01, 0x02, 0x04},
expected: false,
},
{
name: "single byte equal",
a: []byte{0xFF},
b: []byte{0xFF},
expected: true,
},
{
name: "single byte different",
a: []byte{0xFF},
b: []byte{0xFE},
expected: false,
},
{
name: "one empty one not",
a: []byte{},
b: []byte{0x01},
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := equal(tt.a, tt.b)
if result != tt.expected {
t.Errorf("equal(%v, %v) = %v, want %v", tt.a, tt.b, result, tt.expected)
}
})
}
}
func TestEqual_Symmetry(t *testing.T) {
// equal(a, b) should always equal equal(b, a)
testCases := [][]byte{
{0x01, 0x02, 0x03},
{0x01, 0x02},
{},
{0xFF},
}
for i, a := range testCases {
for j, b := range testCases {
resultAB := equal(a, b)
resultBA := equal(b, a)
if resultAB != resultBA {
t.Errorf("Symmetry failed: equal(case[%d], case[%d])=%v but equal(case[%d], case[%d])=%v",
i, j, resultAB, j, i, resultBA)
}
}
}
}
// Helper function to compare int slices
func intSlicesEqual(a, b []int) bool {
if len(a) != len(b) {
return false
}
if a == nil && b == nil {
return true
}
if a == nil || b == nil {
return false
}
for i, v := range a {
if v != b[i] {
return false
}
}
return true
}
// BackportQuest tests are skipped because they require runtime configuration
// (ErupeConfig.RealClientMode) which is not available in unit tests.
// Integration tests should cover this function.

View File

@@ -0,0 +1,202 @@
package channelserver
import (
"testing"
"time"
"erupe-ce/network/mhfpacket"
)
// Test simple handler patterns that don't require database
func TestHandlerMsgMhfSexChanger(t *testing.T) {
server := createMockServer()
session := createMockSession(1, server)
pkt := &mhfpacket.MsgMhfSexChanger{
AckHandle: 12345,
}
// Should not panic
handleMsgMhfSexChanger(session, pkt)
// Should queue a response
select {
case p := <-session.sendPackets:
if len(p.data) == 0 {
t.Error("Response packet should have data")
}
default:
t.Error("No response packet queued")
}
}
func TestHandlerMsgMhfEnterTournamentQuest(t *testing.T) {
server := createMockServer()
session := createMockSession(1, server)
// Should not panic with nil packet (empty handler)
defer func() {
if r := recover(); r != nil {
t.Errorf("handleMsgMhfEnterTournamentQuest panicked: %v", r)
}
}()
handleMsgMhfEnterTournamentQuest(session, nil)
}
func TestHandlerMsgMhfGetUdBonusQuestInfo(t *testing.T) {
server := createMockServer()
session := createMockSession(1, server)
pkt := &mhfpacket.MsgMhfGetUdBonusQuestInfo{
AckHandle: 12345,
}
handleMsgMhfGetUdBonusQuestInfo(session, pkt)
// Should queue a response
select {
case p := <-session.sendPackets:
if len(p.data) == 0 {
t.Error("Response packet should have data")
}
default:
t.Error("No response packet queued")
}
}
// Test that acknowledge handlers work correctly
func TestAckResponseFormats(t *testing.T) {
server := createMockServer()
tests := []struct {
name string
handler func(s *Session, ackHandle uint32, data []byte)
}{
{"doAckBufSucceed", doAckBufSucceed},
{"doAckBufFail", doAckBufFail},
{"doAckSimpleSucceed", doAckSimpleSucceed},
{"doAckSimpleFail", doAckSimpleFail},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
session := createMockSession(1, server)
testData := []byte{0x01, 0x02, 0x03, 0x04}
tt.handler(session, 99999, testData)
select {
case pkt := <-session.sendPackets:
if pkt.data == nil {
t.Error("Packet data should not be nil")
}
default:
t.Error("Handler should queue a packet")
}
})
}
}
func TestStubHandlers(t *testing.T) {
server := createMockServer()
tests := []struct {
name string
handler func(s *Session, ackHandle uint32)
}{
{"stubEnumerateNoResults", stubEnumerateNoResults},
{"stubGetNoResults", stubGetNoResults},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
session := createMockSession(1, server)
tt.handler(session, 12345)
select {
case pkt := <-session.sendPackets:
if pkt.data == nil {
t.Error("Packet data should not be nil")
}
default:
t.Error("Stub handler should queue a packet")
}
})
}
}
// Test packet queueing
func TestSessionQueueSendMHF(t *testing.T) {
server := createMockServer()
session := createMockSession(1, server)
pkt := &mhfpacket.MsgSysAck{
AckHandle: 12345,
IsBufferResponse: false,
ErrorCode: 0,
AckData: []byte{0x00},
}
session.QueueSendMHF(pkt)
select {
case p := <-session.sendPackets:
if len(p.data) == 0 {
t.Error("Queued packet should have data")
}
default:
t.Error("QueueSendMHF should queue a packet")
}
}
func TestSessionQueueSendNonBlocking(t *testing.T) {
server := createMockServer()
session := createMockSession(1, server)
data := []byte{0x01, 0x02, 0x03, 0x04}
session.QueueSendNonBlocking(data)
select {
case p := <-session.sendPackets:
if len(p.data) != 4 {
t.Errorf("Queued data len = %d, want 4", len(p.data))
}
if p.nonBlocking != true {
t.Error("Packet should be marked as non-blocking")
}
default:
t.Error("QueueSendNonBlocking should queue data")
}
}
func TestSessionQueueSendNonBlocking_FullQueue(t *testing.T) {
server := createMockServer()
session := createMockSession(1, server)
// Fill the queue
for i := 0; i < 20; i++ {
session.sendPackets <- packet{data: []byte{byte(i)}, nonBlocking: true}
}
// Non-blocking send should not block when queue is full
// It should drop the packet instead
done := make(chan bool, 1)
go func() {
session.QueueSendNonBlocking([]byte{0xFF})
done <- true
}()
// Wait for completion with a reasonable timeout
// The function should return immediately (dropping the packet)
select {
case <-done:
// Good - didn't block, function completed
case <-time.After(100 * time.Millisecond):
t.Error("QueueSendNonBlocking blocked on full queue")
}
}

View File

@@ -0,0 +1,226 @@
package channelserver
import (
"testing"
)
func TestStubEnumerateNoResults(t *testing.T) {
server := createMockServer()
session := createMockSession(1, server)
// Call stubEnumerateNoResults - it queues a packet
stubEnumerateNoResults(session, 12345)
// Verify packet was queued
select {
case pkt := <-session.sendPackets:
if len(pkt.data) == 0 {
t.Error("Packet data should not be empty")
}
default:
t.Error("No packet was queued")
}
}
func TestStubGetNoResults(t *testing.T) {
server := createMockServer()
session := createMockSession(1, server)
// Call stubGetNoResults - it queues a packet
stubGetNoResults(session, 12345)
// Verify packet was queued
select {
case pkt := <-session.sendPackets:
if len(pkt.data) == 0 {
t.Error("Packet data should not be empty")
}
default:
t.Error("No packet was queued")
}
}
func TestDoAckBufSucceed(t *testing.T) {
server := createMockServer()
session := createMockSession(1, server)
testData := []byte{0x01, 0x02, 0x03, 0x04}
doAckBufSucceed(session, 12345, testData)
// Verify packet was queued
select {
case pkt := <-session.sendPackets:
if len(pkt.data) == 0 {
t.Error("Packet data should not be empty")
}
default:
t.Error("No packet was queued")
}
}
func TestDoAckBufFail(t *testing.T) {
server := createMockServer()
session := createMockSession(1, server)
testData := []byte{0x01, 0x02, 0x03, 0x04}
doAckBufFail(session, 12345, testData)
// Verify packet was queued
select {
case pkt := <-session.sendPackets:
if len(pkt.data) == 0 {
t.Error("Packet data should not be empty")
}
default:
t.Error("No packet was queued")
}
}
func TestDoAckSimpleSucceed(t *testing.T) {
server := createMockServer()
session := createMockSession(1, server)
testData := []byte{0x00, 0x00, 0x00, 0x00}
doAckSimpleSucceed(session, 12345, testData)
// Verify packet was queued
select {
case pkt := <-session.sendPackets:
if len(pkt.data) == 0 {
t.Error("Packet data should not be empty")
}
default:
t.Error("No packet was queued")
}
}
func TestDoAckSimpleFail(t *testing.T) {
server := createMockServer()
session := createMockSession(1, server)
testData := []byte{0x00, 0x00, 0x00, 0x00}
doAckSimpleFail(session, 12345, testData)
// Verify packet was queued
select {
case pkt := <-session.sendPackets:
if len(pkt.data) == 0 {
t.Error("Packet data should not be empty")
}
default:
t.Error("No packet was queued")
}
}
func TestDoAck_EmptyData(t *testing.T) {
server := createMockServer()
session := createMockSession(1, server)
// Should work with empty data
doAckBufSucceed(session, 0, []byte{})
select {
case pkt := <-session.sendPackets:
// Empty data is valid
_ = pkt
default:
t.Error("No packet was queued with empty data")
}
}
func TestDoAck_NilData(t *testing.T) {
server := createMockServer()
session := createMockSession(1, server)
// Should work with nil data
doAckBufSucceed(session, 0, nil)
select {
case pkt := <-session.sendPackets:
// Nil data is valid
_ = pkt
default:
t.Error("No packet was queued with nil data")
}
}
func TestDoAck_LargeData(t *testing.T) {
server := createMockServer()
session := createMockSession(1, server)
// Test with large data
largeData := make([]byte, 65536)
for i := range largeData {
largeData[i] = byte(i % 256)
}
doAckBufSucceed(session, 99999, largeData)
select {
case pkt := <-session.sendPackets:
if len(pkt.data) == 0 {
t.Error("Packet data should not be empty for large data")
}
default:
t.Error("No packet was queued with large data")
}
}
func TestDoAck_AckHandleZero(t *testing.T) {
server := createMockServer()
session := createMockSession(1, server)
// Test with ack handle 0
doAckSimpleSucceed(session, 0, []byte{0x00})
select {
case pkt := <-session.sendPackets:
_ = pkt
default:
t.Error("No packet was queued with zero ack handle")
}
}
func TestDoAck_AckHandleMax(t *testing.T) {
server := createMockServer()
session := createMockSession(1, server)
// Test with max uint32 ack handle
doAckSimpleSucceed(session, 0xFFFFFFFF, []byte{0x00})
select {
case pkt := <-session.sendPackets:
_ = pkt
default:
t.Error("No packet was queued with max ack handle")
}
}
// Test that handlers don't panic with empty packets
func TestEmptyHandlers(t *testing.T) {
server := createMockServer()
session := createMockSession(1, server)
tests := []struct {
name string
handler func(s *Session, p interface{})
}{
{"handleMsgHead", func(s *Session, p interface{}) { handleMsgHead(s, nil) }},
{"handleMsgSysExtendThreshold", func(s *Session, p interface{}) { handleMsgSysExtendThreshold(s, nil) }},
{"handleMsgSysEnd", func(s *Session, p interface{}) { handleMsgSysEnd(s, nil) }},
{"handleMsgSysNop", func(s *Session, p interface{}) { handleMsgSysNop(s, nil) }},
{"handleMsgSysAck", func(s *Session, p interface{}) { handleMsgSysAck(s, nil) }},
{"handleMsgSysAuthData", func(s *Session, p interface{}) { handleMsgSysAuthData(s, nil) }},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
defer func() {
if r := recover(); r != nil {
t.Errorf("%s panicked: %v", tt.name, r)
}
}()
tt.handler(session, nil)
})
}
}

View File

@@ -0,0 +1,484 @@
package channelserver
import (
"net"
"sync"
"testing"
"time"
"erupe-ce/config"
"go.uber.org/zap"
)
func TestNewServer(t *testing.T) {
logger, _ := zap.NewDevelopment()
cfg := &Config{
ID: 1,
Logger: logger,
DB: nil,
DiscordBot: nil,
ErupeConfig: &config.Config{DevMode: true},
Name: "TestServer",
Enable: true,
}
s := NewServer(cfg)
if s == nil {
t.Fatal("NewServer returned nil")
}
// Check ID assignment
if s.ID != 1 {
t.Errorf("Server ID = %d, want 1", s.ID)
}
// Check name assignment
if s.name != "TestServer" {
t.Errorf("Server name = %s, want TestServer", s.name)
}
// Check channels are created
if s.acceptConns == nil {
t.Error("acceptConns channel is nil")
}
if s.deleteConns == nil {
t.Error("deleteConns channel is nil")
}
// Check maps are initialized
if s.sessions == nil {
t.Error("sessions map is nil")
}
if s.stages == nil {
t.Error("stages map is nil")
}
if s.userBinaryParts == nil {
t.Error("userBinaryParts map is nil")
}
if s.semaphore == nil {
t.Error("semaphore map is nil")
}
// Check semaphore index starts at 7 (skips reserved IDs)
if s.semaphoreIndex != 7 {
t.Errorf("semaphoreIndex = %d, want 7", s.semaphoreIndex)
}
// Check Raviente is initialized
if s.raviente == nil {
t.Error("raviente is nil")
}
}
func TestNewServer_DefaultStages(t *testing.T) {
logger, _ := zap.NewDevelopment()
cfg := &Config{
ID: 1,
Logger: logger,
ErupeConfig: &config.Config{DevMode: true},
}
s := NewServer(cfg)
// Check persistent stages are created
expectedStages := []string{
"sl1Ns200p0a0u0", // Mezeporta
"sl1Ns211p0a0u0", // Rasta bar
"sl1Ns260p0a0u0", // Pallone Caravan
"sl1Ns262p0a0u0", // Pallone Guest House 1st Floor
"sl1Ns263p0a0u0", // Pallone Guest House 2nd Floor
"sl2Ns379p0a0u0", // Diva fountain
"sl1Ns462p0a0u0", // MezFes
}
for _, stageID := range expectedStages {
if _, ok := s.stages[stageID]; !ok {
t.Errorf("Expected default stage %s not found", stageID)
}
}
if len(s.stages) != len(expectedStages) {
t.Errorf("Server has %d stages, expected %d", len(s.stages), len(expectedStages))
}
}
func TestNewRaviente(t *testing.T) {
r := NewRaviente()
if r == nil {
t.Fatal("NewRaviente returned nil")
}
// Check register initialization
if r.register == nil {
t.Fatal("Raviente register is nil")
}
if r.register.nextTime != 0 {
t.Errorf("nextTime = %d, want 0", r.register.nextTime)
}
if r.register.maxPlayers != 0 {
t.Errorf("maxPlayers = %d, want 0", r.register.maxPlayers)
}
if len(r.register.register) != 5 {
t.Errorf("register array length = %d, want 5", len(r.register.register))
}
// Check state initialization
if r.state == nil {
t.Fatal("Raviente state is nil")
}
if len(r.state.stateData) != 29 {
t.Errorf("stateData length = %d, want 29", len(r.state.stateData))
}
// Check support initialization
if r.support == nil {
t.Fatal("Raviente support is nil")
}
if len(r.support.supportData) != 25 {
t.Errorf("supportData length = %d, want 25", len(r.support.supportData))
}
}
func TestRavienteRegister_InitialValues(t *testing.T) {
r := NewRaviente()
// All register slots should be 0 initially
for i, v := range r.register.register {
if v != 0 {
t.Errorf("register[%d] = %d, want 0", i, v)
}
}
// All state data should be 0 initially
for i, v := range r.state.stateData {
if v != 0 {
t.Errorf("stateData[%d] = %d, want 0", i, v)
}
}
// All support data should be 0 initially
for i, v := range r.support.supportData {
if v != 0 {
t.Errorf("supportData[%d] = %d, want 0", i, v)
}
}
}
func TestServerMutex(t *testing.T) {
logger, _ := zap.NewDevelopment()
cfg := &Config{
ID: 1,
Logger: logger,
ErupeConfig: &config.Config{DevMode: true},
}
s := NewServer(cfg)
// Test that mutex works and doesn't deadlock
s.Lock()
s.isShuttingDown = true
s.Unlock()
s.Lock()
if !s.isShuttingDown {
t.Error("isShuttingDown should be true")
}
s.Unlock()
}
func TestServerStagesLock(t *testing.T) {
logger, _ := zap.NewDevelopment()
cfg := &Config{
ID: 1,
Logger: logger,
ErupeConfig: &config.Config{DevMode: true},
}
s := NewServer(cfg)
// Test RWMutex for stages
s.stagesLock.RLock()
count := len(s.stages)
s.stagesLock.RUnlock()
if count < 7 {
t.Errorf("Expected at least 7 default stages, got %d", count)
}
// Test write lock
s.stagesLock.Lock()
s.stages["test_stage"] = NewStage("test_stage")
s.stagesLock.Unlock()
s.stagesLock.RLock()
if _, ok := s.stages["test_stage"]; !ok {
t.Error("test_stage not found after adding")
}
s.stagesLock.RUnlock()
}
func TestServerConcurrentStageAccess(t *testing.T) {
logger, _ := zap.NewDevelopment()
cfg := &Config{
ID: 1,
Logger: logger,
ErupeConfig: &config.Config{DevMode: true},
}
s := NewServer(cfg)
var wg sync.WaitGroup
// Multiple concurrent readers
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for j := 0; j < 100; j++ {
s.stagesLock.RLock()
_ = len(s.stages)
s.stagesLock.RUnlock()
}
}()
}
// Concurrent writer
wg.Add(1)
go func() {
defer wg.Done()
for j := 0; j < 50; j++ {
s.stagesLock.Lock()
stageID := "concurrent_test_" + string(rune('A'+j%26))
s.stages[stageID] = NewStage(stageID)
s.stagesLock.Unlock()
}
}()
wg.Wait()
}
func TestNextSemaphoreID(t *testing.T) {
logger, _ := zap.NewDevelopment()
cfg := &Config{
ID: 1,
Logger: logger,
ErupeConfig: &config.Config{DevMode: true},
}
s := NewServer(cfg)
// Initial index should be 7
if s.semaphoreIndex != 7 {
t.Errorf("Initial semaphoreIndex = %d, want 7", s.semaphoreIndex)
}
// Get next IDs
id1 := s.NextSemaphoreID()
id2 := s.NextSemaphoreID()
id3 := s.NextSemaphoreID()
// IDs should be unique and incrementing
if id1 == id2 || id2 == id3 || id1 == id3 {
t.Errorf("Semaphore IDs should be unique: %d, %d, %d", id1, id2, id3)
}
if id2 <= id1 || id3 <= id2 {
t.Errorf("Semaphore IDs should be incrementing: %d, %d, %d", id1, id2, id3)
}
}
func TestNextSemaphoreID_SkipsExisting(t *testing.T) {
logger, _ := zap.NewDevelopment()
cfg := &Config{
ID: 1,
Logger: logger,
ErupeConfig: &config.Config{DevMode: true},
}
s := NewServer(cfg)
// Pre-populate some semaphores
s.semaphore["test1"] = &Semaphore{id: 8}
s.semaphore["test2"] = &Semaphore{id: 9}
id := s.NextSemaphoreID()
// Should skip 8 and 9 since they exist
if id == 8 || id == 9 {
t.Errorf("NextSemaphoreID should skip existing IDs, got %d", id)
}
}
func TestUserBinaryPartID(t *testing.T) {
id1 := userBinaryPartID{charID: 100, index: 1}
id2 := userBinaryPartID{charID: 100, index: 2}
id3 := userBinaryPartID{charID: 200, index: 1}
// Same char, different index should be different keys
if id1 == id2 {
t.Error("Different indices should produce different keys")
}
// Different char, same index should be different keys
if id1 == id3 {
t.Error("Different charIDs should produce different keys")
}
// Same values should be equal
id1copy := userBinaryPartID{charID: 100, index: 1}
if id1 != id1copy {
t.Error("Same values should be equal")
}
}
func TestServerUserBinaryParts(t *testing.T) {
logger, _ := zap.NewDevelopment()
cfg := &Config{
ID: 1,
Logger: logger,
ErupeConfig: &config.Config{DevMode: true},
}
s := NewServer(cfg)
testData := []byte{0x01, 0x02, 0x03}
partID := userBinaryPartID{charID: 12345, index: 1}
// Store data
s.userBinaryPartsLock.Lock()
s.userBinaryParts[partID] = testData
s.userBinaryPartsLock.Unlock()
// Retrieve data
s.userBinaryPartsLock.RLock()
data, ok := s.userBinaryParts[partID]
s.userBinaryPartsLock.RUnlock()
if !ok {
t.Error("Failed to retrieve stored binary part")
}
if len(data) != 3 || data[0] != 0x01 {
t.Errorf("Retrieved data doesn't match: %v", data)
}
}
func TestServerShutdown(t *testing.T) {
logger, _ := zap.NewDevelopment()
cfg := &Config{
ID: 1,
Logger: logger,
ErupeConfig: &config.Config{DevMode: true},
}
s := NewServer(cfg)
// Create a test listener
listener, err := net.Listen("tcp", ":0")
if err != nil {
t.Fatalf("Failed to create test listener: %v", err)
}
s.listener = listener
// Shutdown should not panic
s.Shutdown()
// Check shutdown flag is set
s.Lock()
if !s.isShuttingDown {
t.Error("isShuttingDown should be true after Shutdown()")
}
s.Unlock()
}
func TestServerFindSessionByCharID_NotFound(t *testing.T) {
logger, _ := zap.NewDevelopment()
cfg := &Config{
ID: 1,
Logger: logger,
ErupeConfig: &config.Config{DevMode: true},
}
s := NewServer(cfg)
s.Channels = []*Server{s}
// Search for non-existent character
session := s.FindSessionByCharID(99999)
if session != nil {
t.Error("Expected nil for non-existent character")
}
}
func TestServerFindObjectByChar_NotFound(t *testing.T) {
logger, _ := zap.NewDevelopment()
cfg := &Config{
ID: 1,
Logger: logger,
ErupeConfig: &config.Config{DevMode: true},
}
s := NewServer(cfg)
// Search for non-existent object
obj := s.FindObjectByChar(99999)
if obj != nil {
t.Error("Expected nil for non-existent object owner")
}
}
func TestServerStartAndShutdown(t *testing.T) {
logger, _ := zap.NewDevelopment()
cfg := &Config{
ID: 1,
Logger: logger,
ErupeConfig: &config.Config{DevMode: true},
}
s := NewServer(cfg)
s.Port = 0 // Use any available port
err := s.Start()
if err != nil {
t.Fatalf("Server.Start() failed: %v", err)
}
// Give goroutines time to start
time.Sleep(10 * time.Millisecond)
// Verify listener is created
if s.listener == nil {
t.Error("Listener should be created after Start()")
}
// Shutdown
s.Shutdown()
// Give time for cleanup
time.Sleep(10 * time.Millisecond)
}
func TestConfigStruct(t *testing.T) {
logger, _ := zap.NewDevelopment()
cfg := &Config{
ID: 42,
Logger: logger,
DB: nil,
DiscordBot: nil,
ErupeConfig: &config.Config{},
Name: "Test Channel",
Enable: true,
}
if cfg.ID != 42 {
t.Errorf("Config ID = %d, want 42", cfg.ID)
}
if cfg.Name != "Test Channel" {
t.Errorf("Config Name = %s, want 'Test Channel'", cfg.Name)
}
if !cfg.Enable {
t.Error("Config Enable should be true")
}
}

View File

@@ -0,0 +1,397 @@
package channelserver
import (
"sync"
"testing"
)
func TestObjectStruct(t *testing.T) {
obj := &Object{
id: 12345,
ownerCharID: 67890,
x: 100.5,
y: 50.25,
z: -10.0,
}
if obj.id != 12345 {
t.Errorf("Object id = %d, want 12345", obj.id)
}
if obj.ownerCharID != 67890 {
t.Errorf("Object ownerCharID = %d, want 67890", obj.ownerCharID)
}
if obj.x != 100.5 {
t.Errorf("Object x = %f, want 100.5", obj.x)
}
if obj.y != 50.25 {
t.Errorf("Object y = %f, want 50.25", obj.y)
}
if obj.z != -10.0 {
t.Errorf("Object z = %f, want -10.0", obj.z)
}
}
func TestObjectRWMutex(t *testing.T) {
obj := &Object{
id: 1,
ownerCharID: 100,
x: 0,
y: 0,
z: 0,
}
// Test read lock
obj.RLock()
_ = obj.x
obj.RUnlock()
// Test write lock
obj.Lock()
obj.x = 100.0
obj.Unlock()
if obj.x != 100.0 {
t.Errorf("Object x = %f, want 100.0 after write", obj.x)
}
}
func TestObjectConcurrentAccess(t *testing.T) {
obj := &Object{
id: 1,
ownerCharID: 100,
x: 0,
y: 0,
z: 0,
}
var wg sync.WaitGroup
// Concurrent writers
for i := 0; i < 10; i++ {
wg.Add(1)
go func(val float32) {
defer wg.Done()
for j := 0; j < 100; j++ {
obj.Lock()
obj.x = val
obj.y = val
obj.z = val
obj.Unlock()
}
}(float32(i))
}
// Concurrent readers
for i := 0; i < 5; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for j := 0; j < 100; j++ {
obj.RLock()
_ = obj.x
_ = obj.y
_ = obj.z
obj.RUnlock()
}
}()
}
wg.Wait()
}
func TestStageBinaryKeyStruct(t *testing.T) {
key1 := stageBinaryKey{id0: 1, id1: 2}
key2 := stageBinaryKey{id0: 1, id1: 3}
key3 := stageBinaryKey{id0: 1, id1: 2}
// Different keys
if key1 == key2 {
t.Error("key1 and key2 should be different")
}
// Same keys
if key1 != key3 {
t.Error("key1 and key3 should be equal")
}
}
func TestStageBinaryKeyAsMapKey(t *testing.T) {
data := make(map[stageBinaryKey][]byte)
key1 := stageBinaryKey{id0: 0, id1: 0}
key2 := stageBinaryKey{id0: 0, id1: 1}
key3 := stageBinaryKey{id0: 1, id1: 0}
data[key1] = []byte{0x01}
data[key2] = []byte{0x02}
data[key3] = []byte{0x03}
if len(data) != 3 {
t.Errorf("Expected 3 entries, got %d", len(data))
}
if data[key1][0] != 0x01 {
t.Errorf("data[key1] = 0x%02X, want 0x01", data[key1][0])
}
if data[key2][0] != 0x02 {
t.Errorf("data[key2] = 0x%02X, want 0x02", data[key2][0])
}
if data[key3][0] != 0x03 {
t.Errorf("data[key3] = 0x%02X, want 0x03", data[key3][0])
}
}
func TestNewStageDefaults(t *testing.T) {
stage := NewStage("test_stage_001")
if stage.id != "test_stage_001" {
t.Errorf("stage.id = %s, want test_stage_001", stage.id)
}
if stage.maxPlayers != 4 {
t.Errorf("stage.maxPlayers = %d, want 4 (default)", stage.maxPlayers)
}
if stage.objectIndex != 0 {
t.Errorf("stage.objectIndex = %d, want 0", stage.objectIndex)
}
if stage.clients == nil {
t.Error("stage.clients should be initialized")
}
if stage.reservedClientSlots == nil {
t.Error("stage.reservedClientSlots should be initialized")
}
if stage.objects == nil {
t.Error("stage.objects should be initialized")
}
if stage.rawBinaryData == nil {
t.Error("stage.rawBinaryData should be initialized")
}
if stage.host != nil {
t.Error("stage.host should be nil initially")
}
if stage.password != "" {
t.Errorf("stage.password should be empty, got %s", stage.password)
}
}
func TestStageNextObjectID(t *testing.T) {
stage := NewStage("test")
// First ID should be 1 (index 0 is skipped)
id1 := stage.NextObjectID()
if stage.objectIndex != 1 {
t.Errorf("objectIndex after first call = %d, want 1", stage.objectIndex)
}
// Get several IDs and ensure they increment
id2 := stage.NextObjectID()
id3 := stage.NextObjectID()
if id1 == id2 || id2 == id3 {
t.Error("Object IDs should be unique")
}
}
func TestStageNextObjectID_WrapAround(t *testing.T) {
stage := NewStage("test")
stage.objectIndex = 125
// Get ID at 126
stage.NextObjectID()
if stage.objectIndex != 126 {
t.Errorf("objectIndex = %d, want 126", stage.objectIndex)
}
// Next should wrap to 1 (skipping 0 and 127)
stage.NextObjectID()
if stage.objectIndex != 1 {
t.Errorf("objectIndex = %d, want 1 (wrapped around)", stage.objectIndex)
}
}
func TestStageIsQuest(t *testing.T) {
stage := NewStage("test")
// Initially not a quest
if stage.isQuest() {
t.Error("New stage should not be a quest")
}
// Add reserved slot
stage.reservedClientSlots[100] = true
if !stage.isQuest() {
t.Error("Stage with reserved slots should be a quest")
}
}
func TestStageReservedClientSlots(t *testing.T) {
stage := NewStage("test")
// Reserve some slots
stage.reservedClientSlots[100] = true
stage.reservedClientSlots[200] = false // ready status doesn't matter for presence
stage.reservedClientSlots[300] = true
if len(stage.reservedClientSlots) != 3 {
t.Errorf("reservedClientSlots count = %d, want 3", len(stage.reservedClientSlots))
}
// Check ready status
if !stage.reservedClientSlots[100] {
t.Error("charID 100 should be ready")
}
if stage.reservedClientSlots[200] {
t.Error("charID 200 should not be ready")
}
}
func TestStageRawBinaryData(t *testing.T) {
stage := NewStage("test")
key := stageBinaryKey{id0: 5, id1: 10}
data := []byte{0xDE, 0xAD, 0xBE, 0xEF}
stage.rawBinaryData[key] = data
retrieved := stage.rawBinaryData[key]
if len(retrieved) != 4 {
t.Fatalf("retrieved data len = %d, want 4", len(retrieved))
}
if retrieved[0] != 0xDE || retrieved[3] != 0xEF {
t.Error("retrieved data doesn't match stored data")
}
}
func TestStageObjects(t *testing.T) {
stage := NewStage("test")
obj := &Object{
id: stage.NextObjectID(),
ownerCharID: 12345,
x: 100.0,
y: 200.0,
z: 300.0,
}
stage.objects[obj.id] = obj
if len(stage.objects) != 1 {
t.Errorf("objects count = %d, want 1", len(stage.objects))
}
retrieved := stage.objects[obj.id]
if retrieved.ownerCharID != 12345 {
t.Errorf("retrieved object ownerCharID = %d, want 12345", retrieved.ownerCharID)
}
}
func TestStageHost(t *testing.T) {
server := createMockServer()
stage := NewStage("test")
// Set host
host := createMockSession(100, server)
stage.host = host
if stage.host != host {
t.Error("stage host not set correctly")
}
if stage.host.charID != 100 {
t.Errorf("stage host charID = %d, want 100", stage.host.charID)
}
}
func TestStagePassword(t *testing.T) {
stage := NewStage("test")
// Set password
stage.password = "secret123"
if stage.password != "secret123" {
t.Errorf("stage password = %s, want secret123", stage.password)
}
}
func TestStageMaxPlayers(t *testing.T) {
stage := NewStage("test")
// Change max players
stage.maxPlayers = 16
if stage.maxPlayers != 16 {
t.Errorf("stage maxPlayers = %d, want 16", stage.maxPlayers)
}
}
func TestStageConcurrentClientAccess(t *testing.T) {
server := createMockServer()
stage := NewStage("test")
var wg sync.WaitGroup
// Concurrent client additions
for i := 0; i < 10; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
for j := 0; j < 10; j++ {
session := createMockSession(uint32(id*100+j), server)
stage.Lock()
stage.clients[session] = session.charID
stage.Unlock()
stage.Lock()
delete(stage.clients, session)
stage.Unlock()
}
}(i)
}
// Concurrent reads
for i := 0; i < 5; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for j := 0; j < 50; j++ {
stage.RLock()
_ = len(stage.clients)
stage.RUnlock()
}
}()
}
wg.Wait()
}
func TestStageBroadcastMHF_EmptyStage(t *testing.T) {
stage := NewStage("test")
pkt := &mockPacket{opcode: 0x1234}
// Should not panic with empty stage
stage.BroadcastMHF(pkt, nil)
}
func TestStageBroadcastMHF_SkipsNilClientContext(t *testing.T) {
server := createMockServer()
stage := NewStage("test")
session1 := createMockSession(1, server)
session2 := createMockSession(2, server)
session2.clientContext = nil // Nil context should be skipped
stage.clients[session1] = session1.charID
stage.clients[session2] = session2.charID
pkt := &mockPacket{opcode: 0x1234}
// Should not panic
stage.BroadcastMHF(pkt, nil)
// Only session1 should receive
select {
case <-session1.sendPackets:
// Good
default:
t.Error("session1 should receive broadcast")
}
}