eq2go/internal/chat/chat_test.go

907 lines
20 KiB
Go

package chat
import (
"context"
"fmt"
"sync"
"testing"
"time"
)
// EntityWithGetID helper type for testing
type EntityWithGetID struct {
id int32
}
func (e *EntityWithGetID) GetID() int32 {
return e.id
}
// Mock implementations for testing
type MockChannelDatabase struct {
channels map[string]ChatChannelData
mu sync.RWMutex
loadErr error
saveErr error
delErr error
}
func NewMockChannelDatabase() *MockChannelDatabase {
return &MockChannelDatabase{
channels: make(map[string]ChatChannelData),
}
}
func (m *MockChannelDatabase) LoadWorldChannels(ctx context.Context) ([]ChatChannelData, error) {
if m.loadErr != nil {
return nil, m.loadErr
}
m.mu.RLock()
defer m.mu.RUnlock()
var channels []ChatChannelData
for _, channel := range m.channels {
channels = append(channels, channel)
}
return channels, nil
}
func (m *MockChannelDatabase) SaveChannel(ctx context.Context, channel ChatChannelData) error {
if m.saveErr != nil {
return m.saveErr
}
m.mu.Lock()
defer m.mu.Unlock()
m.channels[channel.Name] = channel
return nil
}
func (m *MockChannelDatabase) DeleteChannel(ctx context.Context, channelName string) error {
if m.delErr != nil {
return m.delErr
}
m.mu.Lock()
defer m.mu.Unlock()
if _, exists := m.channels[channelName]; !exists {
return fmt.Errorf("channel not found")
}
delete(m.channels, channelName)
return nil
}
func (m *MockChannelDatabase) SetLoadError(err error) {
m.loadErr = err
}
func (m *MockChannelDatabase) SetSaveError(err error) {
m.saveErr = err
}
func (m *MockChannelDatabase) SetDeleteError(err error) {
m.delErr = err
}
type MockClientManager struct {
sentMessages []ChannelMessage
sentLists map[int32][]ChannelInfo
sentUpdates []ChatUpdate
sentUserLists map[int32][]ChannelMember
connectedClients map[int32]bool
mu sync.RWMutex
}
type ChatUpdate struct {
CharacterID int32
ChannelName string
Action int
CharacterName string
}
func NewMockClientManager() *MockClientManager {
return &MockClientManager{
sentLists: make(map[int32][]ChannelInfo),
sentUserLists: make(map[int32][]ChannelMember),
connectedClients: make(map[int32]bool),
}
}
func (m *MockClientManager) SendChannelList(characterID int32, channels []ChannelInfo) error {
m.mu.Lock()
defer m.mu.Unlock()
m.sentLists[characterID] = channels
return nil
}
func (m *MockClientManager) SendChannelMessage(characterID int32, message ChannelMessage) error {
m.mu.Lock()
defer m.mu.Unlock()
m.sentMessages = append(m.sentMessages, message)
return nil
}
func (m *MockClientManager) SendChannelUpdate(characterID int32, channelName string, action int, characterName string) error {
m.mu.Lock()
defer m.mu.Unlock()
m.sentUpdates = append(m.sentUpdates, ChatUpdate{
CharacterID: characterID,
ChannelName: channelName,
Action: action,
CharacterName: characterName,
})
return nil
}
func (m *MockClientManager) SendChannelUserList(characterID int32, channelName string, members []ChannelMember) error {
m.mu.Lock()
defer m.mu.Unlock()
m.sentUserLists[characterID] = members
return nil
}
func (m *MockClientManager) IsClientConnected(characterID int32) bool {
m.mu.RLock()
defer m.mu.RUnlock()
return m.connectedClients[characterID]
}
func (m *MockClientManager) SetClientConnected(characterID int32, connected bool) {
m.mu.Lock()
defer m.mu.Unlock()
m.connectedClients[characterID] = connected
}
func (m *MockClientManager) GetSentMessages() []ChannelMessage {
m.mu.RLock()
defer m.mu.RUnlock()
return append([]ChannelMessage{}, m.sentMessages...)
}
type MockPlayerManager struct {
players map[int32]PlayerInfo
mu sync.RWMutex
}
func NewMockPlayerManager() *MockPlayerManager {
return &MockPlayerManager{
players: make(map[int32]PlayerInfo),
}
}
func (m *MockPlayerManager) GetPlayerInfo(characterID int32) (PlayerInfo, error) {
m.mu.RLock()
defer m.mu.RUnlock()
if player, exists := m.players[characterID]; exists {
return player, nil
}
return PlayerInfo{}, fmt.Errorf("player not found")
}
func (m *MockPlayerManager) ValidatePlayer(characterID int32, levelReq, raceReq, classReq int32) bool {
player, err := m.GetPlayerInfo(characterID)
if err != nil {
return false
}
if levelReq > 0 && player.Level < levelReq {
return false
}
if raceReq > 0 && (raceReq&(1<<player.Race)) == 0 {
return false
}
if classReq > 0 && (classReq&(1<<player.Class)) == 0 {
return false
}
return true
}
func (m *MockPlayerManager) GetPlayerLanguages(characterID int32) ([]int32, error) {
return []int32{0, 1}, nil // Default languages
}
func (m *MockPlayerManager) AddPlayer(player PlayerInfo) {
m.mu.Lock()
defer m.mu.Unlock()
m.players[player.CharacterID] = player
}
type MockLanguageProcessor struct{}
func (m *MockLanguageProcessor) ProcessMessage(senderID, receiverID int32, message string, languageID int32) (string, error) {
return message, nil // No scrambling for tests
}
func (m *MockLanguageProcessor) CanUnderstand(senderID, receiverID int32, languageID int32) bool {
return true // Everyone understands everything in tests
}
func (m *MockLanguageProcessor) GetDefaultLanguage(characterID int32) int32 {
return 0 // Common tongue
}
// Channel tests
func TestNewChannel(t *testing.T) {
channel := NewChannel("test")
if channel == nil {
t.Fatal("NewChannel returned nil")
}
if channel.GetName() != "test" {
t.Errorf("Channel name = %v, want test", channel.GetName())
}
if channel.GetNumClients() != 0 {
t.Errorf("New channel should have 0 clients, got %v", channel.GetNumClients())
}
}
func TestChannelSettersAndGetters(t *testing.T) {
channel := NewChannel("test")
// Test name
channel.SetName("newname")
if channel.GetName() != "newname" {
t.Errorf("SetName failed: got %v, want newname", channel.GetName())
}
// Test password
channel.SetPassword("secret")
if !channel.HasPassword() {
t.Error("HasPassword should return true after SetPassword")
}
if !channel.PasswordMatches("secret") {
t.Error("PasswordMatches should return true for correct password")
}
if channel.PasswordMatches("wrong") {
t.Error("PasswordMatches should return false for incorrect password")
}
// Test type
channel.SetType(ChannelTypeWorld)
if channel.GetType() != ChannelTypeWorld {
t.Errorf("SetType failed: got %v, want %v", channel.GetType(), ChannelTypeWorld)
}
// Test restrictions
channel.SetLevelRestriction(50)
if !channel.CanJoinChannelByLevel(50) {
t.Error("CanJoinChannelByLevel should allow level 50")
}
if channel.CanJoinChannelByLevel(49) {
t.Error("CanJoinChannelByLevel should not allow level 49")
}
// Test race restriction (bitmask)
channel.SetRacesAllowed(1 << 1) // Only race ID 1 allowed
if !channel.CanJoinChannelByRace(1) {
t.Error("CanJoinChannelByRace should allow race 1")
}
if channel.CanJoinChannelByRace(2) {
t.Error("CanJoinChannelByRace should not allow race 2")
}
// Test class restriction (bitmask)
channel.SetClassesAllowed(1 << 3) // Only class ID 3 allowed
if !channel.CanJoinChannelByClass(3) {
t.Error("CanJoinChannelByClass should allow class 3")
}
if channel.CanJoinChannelByClass(4) {
t.Error("CanJoinChannelByClass should not allow class 4")
}
}
func TestChannelMembership(t *testing.T) {
channel := NewChannel("test")
// Test initial state
if channel.IsInChannel(100) {
t.Error("IsInChannel should return false for new channel")
}
if !channel.IsEmpty() {
t.Error("New channel should be empty")
}
// Test joining
err := channel.JoinChannel(100)
if err != nil {
t.Errorf("JoinChannel failed: %v", err)
}
if !channel.IsInChannel(100) {
t.Error("IsInChannel should return true after joining")
}
if channel.GetNumClients() != 1 {
t.Errorf("GetNumClients = %v, want 1", channel.GetNumClients())
}
if channel.IsEmpty() {
t.Error("Channel should not be empty after joining")
}
// Test duplicate join
err = channel.JoinChannel(100)
if err == nil {
t.Error("JoinChannel should fail for duplicate member")
}
// Test multiple members
err = channel.JoinChannel(200)
if err != nil {
t.Errorf("JoinChannel failed for second member: %v", err)
}
if channel.GetNumClients() != 2 {
t.Errorf("GetNumClients = %v, want 2", channel.GetNumClients())
}
// Test GetMembers
members := channel.GetMembers()
if len(members) != 2 {
t.Errorf("GetMembers returned %v members, want 2", len(members))
}
// Verify members contains both IDs
found100, found200 := false, false
for _, id := range members {
if id == 100 {
found100 = true
}
if id == 200 {
found200 = true
}
}
if !found100 || !found200 {
t.Error("GetMembers should contain both member IDs")
}
// Test leaving
err = channel.LeaveChannel(100)
if err != nil {
t.Errorf("LeaveChannel failed: %v", err)
}
if channel.IsInChannel(100) {
t.Error("IsInChannel should return false after leaving")
}
if channel.GetNumClients() != 1 {
t.Errorf("GetNumClients = %v, want 1 after leaving", channel.GetNumClients())
}
// Test leaving non-member
err = channel.LeaveChannel(300)
if err == nil {
t.Error("LeaveChannel should fail for non-member")
}
// Test leaving last member
err = channel.LeaveChannel(200)
if err != nil {
t.Errorf("LeaveChannel failed for last member: %v", err)
}
if !channel.IsEmpty() {
t.Error("Channel should be empty after all members leave")
}
}
func TestChannelValidateJoin(t *testing.T) {
channel := NewChannel("test")
channel.SetPassword("secret")
channel.SetLevelRestriction(10)
channel.SetRacesAllowed(1 << 1) // Race 1 only
channel.SetClassesAllowed(1 << 2) // Class 2 only
tests := []struct {
name string
level int32
race int32
class int32
password string
wantErr bool
}{
{
name: "valid join",
level: 15,
race: 1,
class: 2,
password: "secret",
wantErr: false,
},
{
name: "wrong password",
level: 15,
race: 1,
class: 2,
password: "wrong",
wantErr: true,
},
{
name: "level too low",
level: 5,
race: 1,
class: 2,
password: "secret",
wantErr: true,
},
{
name: "wrong race",
level: 15,
race: 2,
class: 2,
password: "secret",
wantErr: true,
},
{
name: "wrong class",
level: 15,
race: 1,
class: 3,
password: "secret",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := channel.ValidateJoin(tt.level, tt.race, tt.class, tt.password)
if (err != nil) != tt.wantErr {
t.Errorf("ValidateJoin() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func TestChannelGetChannelInfo(t *testing.T) {
channel := NewChannel("testchannel")
channel.SetType(ChannelTypeWorld)
channel.SetPassword("secret")
channel.SetLevelRestriction(20)
channel.SetRacesAllowed(15) // Multiple races
channel.SetClassesAllowed(31) // Multiple classes
channel.JoinChannel(100)
channel.JoinChannel(200)
info := channel.GetChannelInfo()
if info.Name != "testchannel" {
t.Errorf("ChannelInfo Name = %v, want testchannel", info.Name)
}
if !info.HasPassword {
t.Error("ChannelInfo should indicate has password")
}
if info.MemberCount != 2 {
t.Errorf("ChannelInfo MemberCount = %v, want 2", info.MemberCount)
}
if info.LevelRestriction != 20 {
t.Errorf("ChannelInfo LevelRestriction = %v, want 20", info.LevelRestriction)
}
if info.ChannelType != ChannelTypeWorld {
t.Errorf("ChannelInfo ChannelType = %v, want %v", info.ChannelType, ChannelTypeWorld)
}
}
func TestChannelCopy(t *testing.T) {
original := NewChannel("original")
original.SetPassword("secret")
original.SetType(ChannelTypeCustom)
original.SetLevelRestriction(25)
original.JoinChannel(100)
original.JoinChannel(200)
copy := original.Copy()
if copy == original {
t.Error("Copy should return different instance")
}
if copy.GetName() != original.GetName() {
t.Error("Copy should have same name")
}
if copy.GetType() != original.GetType() {
t.Error("Copy should have same type")
}
if copy.GetNumClients() != original.GetNumClients() {
t.Error("Copy should have same member count")
}
// Test that modifying copy doesn't affect original
copy.JoinChannel(300)
if original.GetNumClients() == copy.GetNumClients() {
t.Error("Modifying copy should not affect original")
}
}
// Database tests
func TestMockChannelDatabase(t *testing.T) {
db := NewMockChannelDatabase()
ctx := context.Background()
// Test empty database
channels, err := db.LoadWorldChannels(ctx)
if err != nil {
t.Errorf("LoadWorldChannels failed: %v", err)
}
if len(channels) != 0 {
t.Errorf("Expected 0 channels, got %v", len(channels))
}
// Test saving channel
testChannel := ChatChannelData{
Name: "testchannel",
Password: "secret",
LevelRestriction: 10,
ClassRestriction: 15,
RaceRestriction: 7,
}
err = db.SaveChannel(ctx, testChannel)
if err != nil {
t.Errorf("SaveChannel failed: %v", err)
}
// Test loading after save
channels, err = db.LoadWorldChannels(ctx)
if err != nil {
t.Errorf("LoadWorldChannels failed: %v", err)
}
if len(channels) != 1 {
t.Errorf("Expected 1 channel, got %v", len(channels))
}
if channels[0].Name != testChannel.Name {
t.Errorf("Channel name = %v, want %v", channels[0].Name, testChannel.Name)
}
// Test delete
err = db.DeleteChannel(ctx, "testchannel")
if err != nil {
t.Errorf("DeleteChannel failed: %v", err)
}
// Test delete non-existent
err = db.DeleteChannel(ctx, "nonexistent")
if err == nil {
t.Error("DeleteChannel should fail for non-existent channel")
}
// Test error conditions
db.SetLoadError(fmt.Errorf("load error"))
_, err = db.LoadWorldChannels(ctx)
if err == nil {
t.Error("LoadWorldChannels should return error when set")
}
db.SetSaveError(fmt.Errorf("save error"))
err = db.SaveChannel(ctx, testChannel)
if err == nil {
t.Error("SaveChannel should return error when set")
}
db.SetDeleteError(fmt.Errorf("delete error"))
err = db.DeleteChannel(ctx, "test")
if err == nil {
t.Error("DeleteChannel should return error when set")
}
}
// Interface tests
func TestEntityChatAdapter(t *testing.T) {
playerManager := NewMockPlayerManager()
playerManager.AddPlayer(PlayerInfo{
CharacterID: 100,
CharacterName: "TestPlayer",
Level: 25,
Race: 1,
Class: 2,
IsOnline: true,
})
entityWithGetID := &EntityWithGetID{id: 100}
adapter := &EntityChatAdapter{
entity: entityWithGetID,
playerManager: playerManager,
}
if adapter.GetCharacterID() != 100 {
t.Errorf("GetCharacterID() = %v, want 100", adapter.GetCharacterID())
}
if adapter.GetCharacterName() != "TestPlayer" {
t.Errorf("GetCharacterName() = %v, want TestPlayer", adapter.GetCharacterName())
}
if adapter.GetLevel() != 25 {
t.Errorf("GetLevel() = %v, want 25", adapter.GetLevel())
}
if adapter.GetRace() != 1 {
t.Errorf("GetRace() = %v, want 1", adapter.GetRace())
}
if adapter.GetClass() != 2 {
t.Errorf("GetClass() = %v, want 2", adapter.GetClass())
}
// Test with non-existent player
entityWithGetID2 := &EntityWithGetID{id: 999}
adapter2 := &EntityChatAdapter{
entity: entityWithGetID2,
playerManager: playerManager,
}
if adapter2.GetCharacterName() != "" {
t.Error("GetCharacterName should return empty string for non-existent player")
}
if adapter2.GetLevel() != 0 {
t.Error("GetLevel should return 0 for non-existent player")
}
}
// Constants tests
func TestConstants(t *testing.T) {
// Test channel types
if ChannelTypeNone != 0 {
t.Errorf("ChannelTypeNone = %v, want 0", ChannelTypeNone)
}
if ChannelTypeWorld != 1 {
t.Errorf("ChannelTypeWorld = %v, want 1", ChannelTypeWorld)
}
if ChannelTypeCustom != 2 {
t.Errorf("ChannelTypeCustom = %v, want 2", ChannelTypeCustom)
}
// Test chat actions
if ChatChannelJoin != 0 {
t.Errorf("ChatChannelJoin = %v, want 0", ChatChannelJoin)
}
if ChatChannelLeave != 1 {
t.Errorf("ChatChannelLeave = %v, want 1", ChatChannelLeave)
}
// Test restrictions
if NoLevelRestriction != 0 {
t.Errorf("NoLevelRestriction = %v, want 0", NoLevelRestriction)
}
if NoRaceRestriction != 0 {
t.Errorf("NoRaceRestriction = %v, want 0", NoRaceRestriction)
}
if NoClassRestriction != 0 {
t.Errorf("NoClassRestriction = %v, want 0", NoClassRestriction)
}
}
// Concurrency tests
func TestChannelConcurrency(t *testing.T) {
channel := NewChannel("concurrent")
var wg sync.WaitGroup
// Concurrent joins
for i := range 100 {
wg.Add(1)
go func(id int32) {
defer wg.Done()
channel.JoinChannel(id)
}(int32(i))
}
// Concurrent reads
for i := range 50 {
wg.Add(1)
go func(id int32) {
defer wg.Done()
channel.IsInChannel(id)
channel.GetMembers()
channel.GetChannelInfo()
}(int32(i))
}
wg.Wait()
// Verify final state
if channel.GetNumClients() != 100 {
t.Errorf("After concurrent joins, got %v members, want 100", channel.GetNumClients())
}
// Concurrent leaves
for i := range 50 {
wg.Add(1)
go func(id int32) {
defer wg.Done()
channel.LeaveChannel(id)
}(int32(i))
}
wg.Wait()
if channel.GetNumClients() != 50 {
t.Errorf("After concurrent leaves, got %v members, want 50", channel.GetNumClients())
}
}
func TestMockClientManager(t *testing.T) {
client := NewMockClientManager()
// Test client connection status
if client.IsClientConnected(100) {
t.Error("IsClientConnected should return false initially")
}
client.SetClientConnected(100, true)
if !client.IsClientConnected(100) {
t.Error("IsClientConnected should return true after setting")
}
// Test channel list
channels := []ChannelInfo{
{Name: "test1", MemberCount: 5},
{Name: "test2", MemberCount: 10},
}
err := client.SendChannelList(100, channels)
if err != nil {
t.Errorf("SendChannelList failed: %v", err)
}
// Test channel message
message := ChannelMessage{
SenderID: 200,
SenderName: "TestSender",
Message: "Hello world",
ChannelName: "test",
Timestamp: time.Now(),
}
err = client.SendChannelMessage(100, message)
if err != nil {
t.Errorf("SendChannelMessage failed: %v", err)
}
sentMessages := client.GetSentMessages()
if len(sentMessages) != 1 {
t.Errorf("Expected 1 sent message, got %v", len(sentMessages))
}
if sentMessages[0].Message != "Hello world" {
t.Errorf("Sent message = %v, want Hello world", sentMessages[0].Message)
}
}
func TestMockPlayerManager(t *testing.T) {
playerMgr := NewMockPlayerManager()
// Test non-existent player
_, err := playerMgr.GetPlayerInfo(999)
if err == nil {
t.Error("GetPlayerInfo should fail for non-existent player")
}
// Add player
player := PlayerInfo{
CharacterID: 100,
CharacterName: "TestPlayer",
Level: 30,
Race: 2,
Class: 4,
IsOnline: true,
}
playerMgr.AddPlayer(player)
// Test existing player
retrieved, err := playerMgr.GetPlayerInfo(100)
if err != nil {
t.Errorf("GetPlayerInfo failed: %v", err)
}
if retrieved.CharacterName != "TestPlayer" {
t.Errorf("Player name = %v, want TestPlayer", retrieved.CharacterName)
}
// Test validation
if !playerMgr.ValidatePlayer(100, 25, 0, 0) {
t.Error("ValidatePlayer should pass for level requirement")
}
if playerMgr.ValidatePlayer(100, 35, 0, 0) {
t.Error("ValidatePlayer should fail for level requirement")
}
// Test languages
languages, err := playerMgr.GetPlayerLanguages(100)
if err != nil {
t.Errorf("GetPlayerLanguages failed: %v", err)
}
if len(languages) == 0 {
t.Error("GetPlayerLanguages should return some languages")
}
}
func TestMockLanguageProcessor(t *testing.T) {
processor := &MockLanguageProcessor{}
// Test message processing
processed, err := processor.ProcessMessage(100, 200, "test message", 0)
if err != nil {
t.Errorf("ProcessMessage failed: %v", err)
}
if processed != "test message" {
t.Errorf("ProcessMessage = %v, want test message", processed)
}
// Test understanding
if !processor.CanUnderstand(100, 200, 0) {
t.Error("CanUnderstand should return true in mock")
}
// Test default language
if processor.GetDefaultLanguage(100) != 0 {
t.Error("GetDefaultLanguage should return 0 in mock")
}
}
// Benchmarks
func BenchmarkChannelJoin(b *testing.B) {
channel := NewChannel("benchmark")
for i := 0; i < b.N; i++ {
channel.JoinChannel(int32(i))
}
}
func BenchmarkChannelIsInChannel(b *testing.B) {
channel := NewChannel("benchmark")
for i := range 1000 {
channel.JoinChannel(int32(i))
}
for i := 0; i < b.N; i++ {
channel.IsInChannel(int32(i % 1000))
}
}
func BenchmarkChannelGetMembers(b *testing.B) {
channel := NewChannel("benchmark")
for i := range 1000 {
channel.JoinChannel(int32(i))
}
for i := 0; i < b.N; i++ {
channel.GetMembers()
}
}