eq2go/internal/achievements/achievements_test.go

744 lines
20 KiB
Go

package achievements
import (
"context"
"fmt"
"testing"
"time"
)
// MockLogger implements the Logger interface for testing
type MockLogger struct {
InfoMessages []string
ErrorMessages []string
DebugMessages []string
WarningMessages []string
}
func (ml *MockLogger) LogInfo(system, format string, args ...any) {
ml.InfoMessages = append(ml.InfoMessages, fmt.Sprintf(format, args...))
}
func (ml *MockLogger) LogError(system, format string, args ...any) {
ml.ErrorMessages = append(ml.ErrorMessages, fmt.Sprintf(format, args...))
}
func (ml *MockLogger) LogDebug(system, format string, args ...any) {
ml.DebugMessages = append(ml.DebugMessages, fmt.Sprintf(format, args...))
}
func (ml *MockLogger) LogWarning(system, format string, args ...any) {
ml.WarningMessages = append(ml.WarningMessages, fmt.Sprintf(format, args...))
}
// MockDatabase implements basic database operations for testing
type MockDatabase struct {
achievements []Achievement
playerAchievements map[uint32][]PlayerAchievement
requirements map[uint32][]Requirement
rewards map[uint32][]Reward
}
func NewMockDatabase() *MockDatabase {
return &MockDatabase{
achievements: []Achievement{},
playerAchievements: make(map[uint32][]PlayerAchievement),
requirements: make(map[uint32][]Requirement),
rewards: make(map[uint32][]Reward),
}
}
func (db *MockDatabase) Query(query string, args ...any) (*MockRows, error) {
// Simulate database queries based on the query string
if query == `
SELECT id, achievement_id, title, uncompleted_text, completed_text,
category, expansion, icon, point_value, qty_req, hide_achievement,
unknown3a, unknown3b, max_version
FROM achievements
ORDER BY achievement_id
` {
return &MockRows{
achievements: db.achievements,
position: 0,
queryType: "achievements",
}, nil
}
// Handle other query types as needed
return &MockRows{queryType: "unknown"}, nil
}
func (db *MockDatabase) Exec(query string, args ...any) (any, error) {
// Mock exec operations
return nil, nil
}
// MockRows simulates database rows for testing
type MockRows struct {
achievements []Achievement
position int
queryType string
closed bool
}
func (rows *MockRows) Next() bool {
if rows.closed {
return false
}
if rows.queryType == "achievements" {
return rows.position < len(rows.achievements)
}
return false
}
func (rows *MockRows) Scan(dest ...any) error {
if rows.queryType == "achievements" && rows.position < len(rows.achievements) {
achievement := &rows.achievements[rows.position]
// Scan values in order expected by the query
if len(dest) >= 14 {
*dest[0].(*uint32) = achievement.ID
*dest[1].(*uint32) = achievement.AchievementID
*dest[2].(*string) = achievement.Title
*dest[3].(*string) = achievement.UncompletedText
*dest[4].(*string) = achievement.CompletedText
*dest[5].(*string) = achievement.Category
*dest[6].(*string) = achievement.Expansion
*dest[7].(*uint16) = achievement.Icon
*dest[8].(*uint32) = achievement.PointValue
*dest[9].(*uint32) = achievement.QtyRequired
var hideInt int
if achievement.Hide {
hideInt = 1
}
*dest[10].(*int) = hideInt
*dest[11].(*uint32) = achievement.Unknown3A
*dest[12].(*uint32) = achievement.Unknown3B
*dest[13].(*uint32) = achievement.MaxVersion
}
rows.position++
}
return nil
}
func (rows *MockRows) Close() error {
rows.closed = true
return nil
}
func (rows *MockRows) Err() error {
return nil
}
// Test data setup
func createTestAchievements() []Achievement {
return []Achievement{
{
ID: 1,
AchievementID: 100,
Title: "First Kill",
UncompletedText: "Kill your first enemy",
CompletedText: "You have killed your first enemy!",
Category: CategoryCombat,
Expansion: ExpansionBase,
Icon: 1001,
PointValue: 10,
QtyRequired: 1,
Hide: false,
Requirements: []Requirement{
{AchievementID: 100, Name: "Kill Enemy", QtyRequired: 1},
},
Rewards: []Reward{
{AchievementID: 100, Reward: "10 Experience Points"},
},
},
{
ID: 2,
AchievementID: 101,
Title: "Explorer",
UncompletedText: "Discover 5 new locations",
CompletedText: "You have explored many locations!",
Category: CategoryExploration,
Expansion: ExpansionBase,
Icon: 1002,
PointValue: 25,
QtyRequired: 5,
Hide: false,
Requirements: []Requirement{
{AchievementID: 101, Name: "Discover Location", QtyRequired: 5},
},
Rewards: []Reward{
{AchievementID: 101, Reward: "Map Fragment"},
},
},
}
}
func setupTestManager() (*AchievementManager, *MockLogger, *MockDatabase) {
logger := &MockLogger{}
mockDB := NewMockDatabase()
// Add test data
mockDB.achievements = createTestAchievements()
config := AchievementConfig{
EnablePacketUpdates: true,
AutoCompleteOnReached: true,
EnableStatistics: true,
MaxCachedPlayers: 100,
}
// Create manager without database initially for isolated testing
manager := NewAchievementManager(nil, logger, config)
return manager, logger, mockDB
}
func TestNewAchievementManager(t *testing.T) {
logger := &MockLogger{}
config := AchievementConfig{
EnablePacketUpdates: true,
MaxCachedPlayers: 100,
}
manager := NewAchievementManager(nil, logger, config)
if manager == nil {
t.Fatal("NewAchievementManager returned nil")
}
if manager.logger != logger {
t.Error("Logger not set correctly")
}
if manager.config != config {
t.Error("Config not set correctly")
}
if len(manager.achievements) != 0 {
t.Error("Expected empty achievements map")
}
}
func TestAchievementManagerInitializeWithoutDatabase(t *testing.T) {
manager, logger, _ := setupTestManager()
// Initialize with no database (should handle gracefully)
ctx := context.Background()
err := manager.Initialize(ctx)
if err != nil {
t.Fatalf("Initialize failed: %v", err)
}
// Should have no achievements loaded
if len(manager.achievements) != 0 {
t.Error("Expected no achievements without database")
}
// Logger should have recorded the initialization
if len(logger.InfoMessages) == 0 {
t.Error("Expected initialization log message")
}
}
func TestGetAchievement(t *testing.T) {
manager, _, _ := setupTestManager()
// Manually add achievements for testing
testAchievements := createTestAchievements()
for i := range testAchievements {
manager.achievements[testAchievements[i].AchievementID] = &testAchievements[i]
}
// Test existing achievement
achievement, exists := manager.GetAchievement(100)
if !exists {
t.Error("Expected achievement 100 to exist")
}
if achievement.Title != "First Kill" {
t.Errorf("Expected title 'First Kill', got '%s'", achievement.Title)
}
// Test non-existing achievement
_, exists = manager.GetAchievement(999)
if exists {
t.Error("Expected achievement 999 to not exist")
}
}
func TestGetAllAchievements(t *testing.T) {
manager, _, _ := setupTestManager()
// Manually add achievements for testing
testAchievements := createTestAchievements()
for i := range testAchievements {
manager.achievements[testAchievements[i].AchievementID] = &testAchievements[i]
}
// Build indexes
for _, achievement := range manager.achievements {
manager.categoryIndex[achievement.Category] = append(manager.categoryIndex[achievement.Category], achievement)
manager.expansionIndex[achievement.Expansion] = append(manager.expansionIndex[achievement.Expansion], achievement)
}
achievements := manager.GetAllAchievements()
if len(achievements) != 2 {
t.Errorf("Expected 2 achievements, got %d", len(achievements))
}
}
func TestGetAchievementsByCategory(t *testing.T) {
manager, _, _ := setupTestManager()
// Manually add achievements and build indexes
testAchievements := createTestAchievements()
for i := range testAchievements {
manager.achievements[testAchievements[i].AchievementID] = &testAchievements[i]
manager.categoryIndex[testAchievements[i].Category] = append(manager.categoryIndex[testAchievements[i].Category], &testAchievements[i])
}
combatAchievements := manager.GetAchievementsByCategory(CategoryCombat)
if len(combatAchievements) != 1 {
t.Errorf("Expected 1 combat achievement, got %d", len(combatAchievements))
}
explorationAchievements := manager.GetAchievementsByCategory(CategoryExploration)
if len(explorationAchievements) != 1 {
t.Errorf("Expected 1 exploration achievement, got %d", len(explorationAchievements))
}
}
func TestGetAchievementsByExpansion(t *testing.T) {
manager, _, _ := setupTestManager()
// Manually add achievements and build indexes
testAchievements := createTestAchievements()
for i := range testAchievements {
manager.achievements[testAchievements[i].AchievementID] = &testAchievements[i]
manager.expansionIndex[testAchievements[i].Expansion] = append(manager.expansionIndex[testAchievements[i].Expansion], &testAchievements[i])
}
baseAchievements := manager.GetAchievementsByExpansion(ExpansionBase)
if len(baseAchievements) != 2 {
t.Errorf("Expected 2 base expansion achievements, got %d", len(baseAchievements))
}
}
func TestGetCategories(t *testing.T) {
manager, _, _ := setupTestManager()
// Manually build category index
manager.categoryIndex[CategoryCombat] = []*Achievement{}
manager.categoryIndex[CategoryExploration] = []*Achievement{}
categories := manager.GetCategories()
if len(categories) != 2 {
t.Errorf("Expected 2 categories, got %d", len(categories))
}
// Check that both categories exist
categoryMap := make(map[string]bool)
for _, category := range categories {
categoryMap[category] = true
}
if !categoryMap[CategoryCombat] {
t.Error("Expected Combat category")
}
if !categoryMap[CategoryExploration] {
t.Error("Expected Exploration category")
}
}
func TestGetExpansions(t *testing.T) {
manager, _, _ := setupTestManager()
// Manually build expansion index
manager.expansionIndex[ExpansionBase] = []*Achievement{}
expansions := manager.GetExpansions()
if len(expansions) != 1 {
t.Errorf("Expected 1 expansion, got %d", len(expansions))
}
if expansions[0] != ExpansionBase {
t.Errorf("Expected expansion '%s', got '%s'", ExpansionBase, expansions[0])
}
}
func TestUpdatePlayerProgress(t *testing.T) {
manager, logger, _ := setupTestManager()
// Add test achievement
testAchievement := &Achievement{
AchievementID: 100,
Title: "Test Achievement",
QtyRequired: 5,
PointValue: 10,
}
manager.achievements[100] = testAchievement
ctx := context.Background()
characterID := uint32(12345)
achievementID := uint32(100)
// Test updating progress
err := manager.UpdatePlayerProgress(ctx, characterID, achievementID, 3)
if err != nil {
t.Fatalf("UpdatePlayerProgress failed: %v", err)
}
// Verify progress was set
progress, err := manager.GetPlayerAchievementProgress(characterID, achievementID)
if err != nil {
t.Fatalf("GetPlayerAchievementProgress failed: %v", err)
}
if progress != 3 {
t.Errorf("Expected progress 3, got %d", progress)
}
// Test auto-completion when reaching required quantity
err = manager.UpdatePlayerProgress(ctx, characterID, achievementID, 5)
if err != nil {
t.Fatalf("UpdatePlayerProgress failed: %v", err)
}
// Should be completed now
completed, err := manager.IsPlayerAchievementCompleted(characterID, achievementID)
if err != nil {
t.Fatalf("IsPlayerAchievementCompleted failed: %v", err)
}
if !completed {
t.Error("Expected achievement to be completed")
}
// Check that completion was logged
found := false
for _, msg := range logger.InfoMessages {
if msg == fmt.Sprintf("Character %d completed achievement %d", characterID, achievementID) {
found = true
break
}
}
if !found {
t.Error("Expected completion log message")
}
}
func TestCompletePlayerAchievement(t *testing.T) {
manager, logger, _ := setupTestManager()
// Add test achievement
manager.achievements[100] = &Achievement{AchievementID: 100, Title: "Test"}
ctx := context.Background()
characterID := uint32(12345)
achievementID := uint32(100)
// Complete the achievement
err := manager.CompletePlayerAchievement(ctx, characterID, achievementID)
if err != nil {
t.Fatalf("CompletePlayerAchievement failed: %v", err)
}
// Verify completion
completed, err := manager.IsPlayerAchievementCompleted(characterID, achievementID)
if err != nil {
t.Fatalf("IsPlayerAchievementCompleted failed: %v", err)
}
if !completed {
t.Error("Expected achievement to be completed")
}
// Check that completion was logged
found := false
for _, msg := range logger.InfoMessages {
if msg == fmt.Sprintf("Character %d completed achievement %d", characterID, achievementID) {
found = true
break
}
}
if !found {
t.Error("Expected completion log message")
}
// Test completing already completed achievement (should not log again)
originalLogCount := len(logger.InfoMessages)
err = manager.CompletePlayerAchievement(ctx, characterID, achievementID)
if err != nil {
t.Fatalf("CompletePlayerAchievement failed on already completed: %v", err)
}
if len(logger.InfoMessages) != originalLogCount {
t.Error("Expected no additional log message for already completed achievement")
}
}
func TestGetPlayerAchievements(t *testing.T) {
manager, _, _ := setupTestManager()
characterID := uint32(12345)
// Test with no achievements
achievements, err := manager.GetPlayerAchievements(characterID)
if err != nil {
t.Fatalf("GetPlayerAchievements failed: %v", err)
}
if len(achievements) != 0 {
t.Error("Expected empty achievements map")
}
// Add an achievement manually
manager.playerAchievements[characterID] = map[uint32]*PlayerAchievement{
100: {
CharacterID: characterID,
AchievementID: 100,
Progress: 3,
CompletedDate: time.Now(),
},
}
// Test with achievements
achievements, err = manager.GetPlayerAchievements(characterID)
if err != nil {
t.Fatalf("GetPlayerAchievements failed: %v", err)
}
if len(achievements) != 1 {
t.Errorf("Expected 1 achievement, got %d", len(achievements))
}
achievement, exists := achievements[100]
if !exists {
t.Error("Expected achievement 100 to exist")
}
if achievement.Progress != 3 {
t.Errorf("Expected progress 3, got %d", achievement.Progress)
}
}
func TestGetPlayerStatistics(t *testing.T) {
manager, _, _ := setupTestManager()
// Add test achievements
manager.achievements[100] = &Achievement{AchievementID: 100, PointValue: 10, Category: CategoryCombat}
manager.achievements[101] = &Achievement{AchievementID: 101, PointValue: 25, Category: CategoryExploration}
characterID := uint32(12345)
// Add player achievements - one completed, one in progress
manager.playerAchievements[characterID] = map[uint32]*PlayerAchievement{
100: {
CharacterID: characterID,
AchievementID: 100,
Progress: 10,
CompletedDate: time.Now(), // Completed
},
101: {
CharacterID: characterID,
AchievementID: 101,
Progress: 3,
CompletedDate: time.Time{}, // In progress
},
}
stats, err := manager.GetPlayerStatistics(characterID)
if err != nil {
t.Fatalf("GetPlayerStatistics failed: %v", err)
}
if stats.CharacterID != characterID {
t.Errorf("Expected character ID %d, got %d", characterID, stats.CharacterID)
}
if stats.TotalAchievements != 2 {
t.Errorf("Expected 2 total achievements, got %d", stats.TotalAchievements)
}
if stats.CompletedCount != 1 {
t.Errorf("Expected 1 completed achievement, got %d", stats.CompletedCount)
}
if stats.InProgressCount != 1 {
t.Errorf("Expected 1 in-progress achievement, got %d", stats.InProgressCount)
}
if stats.TotalPointsEarned != 10 {
t.Errorf("Expected 10 points earned, got %d", stats.TotalPointsEarned)
}
if stats.TotalPointsAvailable != 35 {
t.Errorf("Expected 35 points available, got %d", stats.TotalPointsAvailable)
}
if stats.CompletedByCategory[CategoryCombat] != 1 {
t.Errorf("Expected 1 combat achievement completed, got %d", stats.CompletedByCategory[CategoryCombat])
}
}
func TestInvalidAchievementOperations(t *testing.T) {
manager, _, _ := setupTestManager()
ctx := context.Background()
characterID := uint32(12345)
invalidAchievementID := uint32(999)
// Test updating progress for non-existent achievement
err := manager.UpdatePlayerProgress(ctx, characterID, invalidAchievementID, 1)
if err == nil {
t.Error("Expected error for invalid achievement ID")
}
// Test completing non-existent achievement
err = manager.CompletePlayerAchievement(ctx, characterID, invalidAchievementID)
if err == nil {
t.Error("Expected error for invalid achievement ID")
}
}
func TestThreadSafety(t *testing.T) {
manager, _, _ := setupTestManager()
// Add test achievement
manager.achievements[100] = &Achievement{
AchievementID: 100,
QtyRequired: 10,
PointValue: 10,
}
ctx := context.Background()
characterID := uint32(12345)
achievementID := uint32(100)
// Test concurrent access
done := make(chan bool, 10)
// Start 10 concurrent operations
for i := 0; i < 10; i++ {
go func(progress uint32) {
defer func() { done <- true }()
// Update progress
err := manager.UpdatePlayerProgress(ctx, characterID, achievementID, progress)
if err != nil {
t.Errorf("UpdatePlayerProgress failed: %v", err)
return
}
// Read progress
_, err = manager.GetPlayerAchievementProgress(characterID, achievementID)
if err != nil {
t.Errorf("GetPlayerAchievementProgress failed: %v", err)
return
}
// Check completion status
_, err = manager.IsPlayerAchievementCompleted(characterID, achievementID)
if err != nil {
t.Errorf("IsPlayerAchievementCompleted failed: %v", err)
return
}
}(uint32(i + 1))
}
// Wait for all operations to complete
for i := 0; i < 10; i++ {
<-done
}
}
func TestPacketBuilding(t *testing.T) {
manager, logger, _ := setupTestManager()
// Add test achievement
manager.achievements[100] = &Achievement{
AchievementID: 100,
Title: "Test Achievement",
CompletedText: "Completed!",
UncompletedText: "Not completed",
Category: CategoryCombat,
Expansion: ExpansionBase,
Icon: 1001,
PointValue: 10,
QtyRequired: 1,
Hide: false,
}
characterID := uint32(12345)
clientVersion := int32(1096)
// Test sending packet with no player achievements (should not error)
err := manager.SendPlayerAchievementsPacket(characterID, clientVersion)
if err != nil {
t.Fatalf("SendPlayerAchievementsPacket failed: %v", err)
}
// Should have debug message about packet building
found := false
expectedMsg := fmt.Sprintf("Built achievement list packet for character %d (0 achievements)", characterID)
for _, msg := range logger.DebugMessages {
if expectedMsg == msg {
found = true
break
}
}
if !found {
t.Errorf("Expected debug message '%s', got messages: %v", expectedMsg, logger.DebugMessages)
}
}
func TestShutdown(t *testing.T) {
manager, logger, _ := setupTestManager()
ctx := context.Background()
err := manager.Shutdown(ctx)
if err != nil {
t.Fatalf("Shutdown failed: %v", err)
}
// Should have info message about shutdown
found := false
for _, msg := range logger.InfoMessages {
if msg == "Shutting down achievement manager" {
found = true
break
}
}
if !found {
t.Error("Expected shutdown log message")
}
}
// Benchmark tests
func BenchmarkGetAchievement(b *testing.B) {
manager, _, _ := setupTestManager()
// Add many achievements
for i := uint32(0); i < 1000; i++ {
manager.achievements[i] = &Achievement{AchievementID: i, Title: fmt.Sprintf("Achievement %d", i)}
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = manager.GetAchievement(uint32(i % 1000))
}
}
func BenchmarkUpdatePlayerProgress(b *testing.B) {
manager, _, _ := setupTestManager()
// Add test achievement
manager.achievements[100] = &Achievement{
AchievementID: 100,
QtyRequired: 1000000, // High value so it doesn't auto-complete
PointValue: 10,
}
ctx := context.Background()
characterID := uint32(12345)
achievementID := uint32(100)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = manager.UpdatePlayerProgress(ctx, characterID, achievementID, uint32(i))
}
}