Files
Erupe/server/channelserver/sys_channel_server_test.go
Houmgaor 7e9440d8cc test: expand channelserver coverage from 20% to 25%
Add tests for:
- Discord handlers (getPlayerSlice, getCharacterList)
- House handlers (boxToBytes, HouseData, Title structs)
- Mail struct tests
- Mercenary handlers (Partner, HunterNavi structs)
- Shop/Gacha handlers (writeShopItems, ShopItem, Gacha structs)
- Additional handler coverage for guild, tower, and simple handlers
- Stage handler tests for binary operations and enumeration
- Channel server tests for BroadcastMHF and session management
2026-02-02 16:02:01 +01:00

748 lines
16 KiB
Go

package channelserver
import (
"net"
"sync"
"testing"
"time"
"erupe-ce/config"
"erupe-ce/network/mhfpacket"
"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")
}
}
func TestServerBroadcastMHF_NoSessions(t *testing.T) {
logger, _ := zap.NewDevelopment()
cfg := &Config{
ID: 1,
Logger: logger,
ErupeConfig: &config.Config{DevMode: true},
}
s := NewServer(cfg)
// Should not panic with no sessions
pkt := &mhfpacket.MsgSysAck{
AckHandle: 1,
IsBufferResponse: false,
ErrorCode: 0,
AckData: []byte{0x00},
}
s.BroadcastMHF(pkt, nil)
}
func TestServerBroadcastMHF_WithSession(t *testing.T) {
logger, _ := zap.NewDevelopment()
cfg := &Config{
ID: 1,
Logger: logger,
ErupeConfig: &config.Config{DevMode: true},
}
s := NewServer(cfg)
session := createMockSession(1, s)
s.sessions[nil] = session
pkt := &mhfpacket.MsgSysAck{
AckHandle: 1,
IsBufferResponse: false,
ErrorCode: 0,
AckData: []byte{0x00},
}
s.BroadcastMHF(pkt, nil)
// Check if packet was queued
select {
case p := <-session.sendPackets:
if p.data == nil {
t.Error("Packet data should not be nil")
}
default:
t.Error("No packet queued to session")
}
}
func TestServerBroadcastMHF_SkipsOrigin(t *testing.T) {
logger, _ := zap.NewDevelopment()
cfg := &Config{
ID: 1,
Logger: logger,
ErupeConfig: &config.Config{DevMode: true},
}
s := NewServer(cfg)
session1 := createMockSession(1, s)
session2 := createMockSession(2, s)
s.sessions[nil] = session1
s.sessions[session2.rawConn] = session2
pkt := &mhfpacket.MsgSysAck{
AckHandle: 1,
IsBufferResponse: false,
ErrorCode: 0,
AckData: []byte{0x00},
}
// Broadcast from session1 - should skip session1
s.BroadcastMHF(pkt, session1)
// session1 should not receive the packet
select {
case <-session1.sendPackets:
t.Error("Origin session should not receive broadcast")
default:
// Good - no packet for origin
}
}
func TestServerFindSessionByCharID_Found(t *testing.T) {
logger, _ := zap.NewDevelopment()
cfg := &Config{
ID: 1,
Logger: logger,
ErupeConfig: &config.Config{DevMode: true},
}
s := NewServer(cfg)
s.Channels = []*Server{s}
session := createMockSession(12345, s)
s.sessions[nil] = session
// Search for existing character
found := s.FindSessionByCharID(12345)
if found == nil {
t.Error("Expected to find session")
}
if found != session {
t.Error("Found wrong session")
}
}
func TestServerFindObjectByChar_Found(t *testing.T) {
logger, _ := zap.NewDevelopment()
cfg := &Config{
ID: 1,
Logger: logger,
ErupeConfig: &config.Config{DevMode: true},
}
s := NewServer(cfg)
// Add a stage with an object
stage := NewStage("test_obj_stage")
obj := &Object{
id: 1,
ownerCharID: 12345,
x: 100.0,
y: 200.0,
z: 300.0,
}
stage.objects[obj.ownerCharID] = obj
s.stages["test_obj_stage"] = stage
// Search for existing object
found := s.FindObjectByChar(12345)
if found == nil {
t.Error("Expected to find object")
}
if found != obj {
t.Error("Found wrong object")
}
}
func TestServerConcurrentBinaryPartsAccess(t *testing.T) {
logger, _ := zap.NewDevelopment()
cfg := &Config{
ID: 1,
Logger: logger,
ErupeConfig: &config.Config{DevMode: true},
}
s := NewServer(cfg)
var wg sync.WaitGroup
// Concurrent writers
for i := 0; i < 10; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
for j := 0; j < 10; j++ {
partID := userBinaryPartID{charID: uint32(id), index: uint8(j % 4)}
s.userBinaryPartsLock.Lock()
s.userBinaryParts[partID] = []byte{byte(id), byte(j)}
s.userBinaryPartsLock.Unlock()
}
}(i)
}
// Concurrent readers
for i := 0; i < 5; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for j := 0; j < 20; j++ {
s.userBinaryPartsLock.RLock()
_ = len(s.userBinaryParts)
s.userBinaryPartsLock.RUnlock()
}
}()
}
wg.Wait()
}
func TestServerNextSemaphoreID(t *testing.T) {
logger, _ := zap.NewDevelopment()
cfg := &Config{
ID: 1,
Logger: logger,
ErupeConfig: &config.Config{DevMode: true},
}
s := NewServer(cfg)
// Get multiple IDs and verify they're unique
ids := make(map[uint32]bool)
for i := 0; i < 10; i++ {
id := s.NextSemaphoreID()
if ids[id] {
t.Errorf("Duplicate semaphore ID: %d", id)
}
ids[id] = true
// IDs should be >= 7 (reserved indexes skipped)
if id < 7 {
t.Errorf("Semaphore ID %d should be >= 7", id)
}
}
}
func TestServerNextSemaphoreID_WrapAround(t *testing.T) {
logger, _ := zap.NewDevelopment()
cfg := &Config{
ID: 1,
Logger: logger,
ErupeConfig: &config.Config{DevMode: true},
}
s := NewServer(cfg)
// Set semaphoreIndex to near max uint32 to test wrap-around
s.semaphoreIndex = ^uint32(0) - 1 // Max uint32 - 1
id1 := s.NextSemaphoreID()
id2 := s.NextSemaphoreID()
// After wrap-around, should skip to 7
if id1 < 7 && id1 != 0 {
t.Errorf("After wrap-around, first ID should be >= 7, got %d", id1)
}
if id1 == id2 {
t.Error("IDs should be different")
}
}
func TestServerNextSemaphoreID_SkipsExisting(t *testing.T) {
logger, _ := zap.NewDevelopment()
cfg := &Config{
ID: 1,
Logger: logger,
ErupeConfig: &config.Config{DevMode: true},
}
s := NewServer(cfg)
// Pre-populate with some semaphores
for i := uint32(7); i <= 15; i++ {
s.semaphore["test_"+string(rune('a'+i))] = &Semaphore{id: i}
}
// Get a new ID - should skip the existing ones
id := s.NextSemaphoreID()
// Verify ID is not one of the existing ones
for _, sema := range s.semaphore {
if sema.id == id {
t.Errorf("NextSemaphoreID returned existing ID: %d", id)
}
}
}