package guilds import ( "context" "fmt" "path/filepath" "testing" "time" "eq2emu/internal/database" ) // createTestDB creates a temporary test database func createTestDB(t *testing.T) *database.DB { // Create temporary directory for test database tempDir := t.TempDir() dbPath := filepath.Join(tempDir, "test_guilds.db") // Create and initialize database db, err := database.Open(dbPath) if err != nil { t.Fatalf("Failed to create test database: %v", err) } // Create guild tables for testing err = createGuildTables(db) if err != nil { t.Fatalf("Failed to create guild tables: %v", err) } return db } // createGuildTables creates the necessary tables for guild testing func createGuildTables(db *database.DB) error { tables := []string{ `CREATE TABLE IF NOT EXISTS guilds ( id INTEGER PRIMARY KEY, name TEXT NOT NULL, motd TEXT, level INTEGER DEFAULT 1, xp INTEGER DEFAULT 111, xp_needed INTEGER DEFAULT 2521, formed_on INTEGER DEFAULT 0 )`, `CREATE TABLE IF NOT EXISTS guild_members ( char_id INTEGER PRIMARY KEY, guild_id INTEGER NOT NULL, account_id INTEGER NOT NULL, recruiter_id INTEGER DEFAULT 0, name TEXT NOT NULL, guild_status INTEGER DEFAULT 0, points REAL DEFAULT 0.0, adventure_class INTEGER DEFAULT 0, adventure_level INTEGER DEFAULT 1, tradeskill_class INTEGER DEFAULT 0, tradeskill_level INTEGER DEFAULT 1, rank INTEGER DEFAULT 7, member_flags INTEGER DEFAULT 0, zone TEXT DEFAULT '', join_date INTEGER DEFAULT 0, last_login_date INTEGER DEFAULT 0, note TEXT DEFAULT '', officer_note TEXT DEFAULT '', recruiter_description TEXT DEFAULT '', recruiter_picture_data BLOB, recruiting_show_adventure_class INTEGER DEFAULT 0, FOREIGN KEY (guild_id) REFERENCES guilds(id) )`, `CREATE TABLE IF NOT EXISTS guild_events ( event_id INTEGER PRIMARY KEY, guild_id INTEGER NOT NULL, date INTEGER NOT NULL, type INTEGER NOT NULL, description TEXT NOT NULL, locked INTEGER DEFAULT 0, FOREIGN KEY (guild_id) REFERENCES guilds(id) )`, `CREATE TABLE IF NOT EXISTS guild_ranks ( guild_id INTEGER NOT NULL, rank INTEGER NOT NULL, name TEXT NOT NULL, PRIMARY KEY (guild_id, rank), FOREIGN KEY (guild_id) REFERENCES guilds(id) )`, `CREATE TABLE IF NOT EXISTS guild_permissions ( guild_id INTEGER NOT NULL, rank INTEGER NOT NULL, permission INTEGER NOT NULL, value INTEGER NOT NULL, PRIMARY KEY (guild_id, rank, permission), FOREIGN KEY (guild_id) REFERENCES guilds(id) )`, `CREATE TABLE IF NOT EXISTS guild_event_filters ( guild_id INTEGER NOT NULL, event_id INTEGER NOT NULL, category INTEGER NOT NULL, value INTEGER NOT NULL, PRIMARY KEY (guild_id, event_id, category), FOREIGN KEY (guild_id) REFERENCES guilds(id) )`, `CREATE TABLE IF NOT EXISTS guild_recruiting ( guild_id INTEGER NOT NULL, flag INTEGER NOT NULL, value INTEGER NOT NULL, PRIMARY KEY (guild_id, flag), FOREIGN KEY (guild_id) REFERENCES guilds(id) )`, `CREATE TABLE IF NOT EXISTS guild_point_history ( character_id INTEGER NOT NULL, date INTEGER NOT NULL, modified_by TEXT NOT NULL, comment TEXT NOT NULL, points REAL NOT NULL, FOREIGN KEY (character_id) REFERENCES guild_members(char_id) )`, } for _, sql := range tables { if err := db.Exec(sql); err != nil { return err } } return nil } // TestDatabaseGuildManager_LoadGuilds tests loading guilds from database func TestDatabaseGuildManager_LoadGuilds(t *testing.T) { db := createTestDB(t) defer db.Close() dgm := NewDatabaseGuildManager(db) ctx := context.Background() // Insert test data insertGuildSQL := `INSERT INTO guilds (id, name, motd, level, xp, xp_needed, formed_on) VALUES (1, 'Test Guild', 'Welcome!', 5, 1000, 5000, ?)` formedTime := time.Now().Unix() err := db.Exec(insertGuildSQL, formedTime) if err != nil { t.Fatalf("Failed to insert test guild: %v", err) } // Test loading guilds guilds, err := dgm.LoadGuilds(ctx) if err != nil { t.Fatalf("Failed to load guilds: %v", err) } if len(guilds) != 1 { t.Errorf("Expected 1 guild, got %d", len(guilds)) } guild := guilds[0] if guild.ID != 1 { t.Errorf("Expected guild ID 1, got %d", guild.ID) } if guild.Name != "Test Guild" { t.Errorf("Expected guild name 'Test Guild', got '%s'", guild.Name) } if guild.MOTD != "Welcome!" { t.Errorf("Expected MOTD 'Welcome!', got '%s'", guild.MOTD) } if guild.Level != 5 { t.Errorf("Expected level 5, got %d", guild.Level) } if guild.EXPCurrent != 1000 { t.Errorf("Expected current exp 1000, got %d", guild.EXPCurrent) } if guild.EXPToNextLevel != 5000 { t.Errorf("Expected next level exp 5000, got %d", guild.EXPToNextLevel) } } // TestDatabaseGuildManager_LoadGuild tests loading a specific guild func TestDatabaseGuildManager_LoadGuild(t *testing.T) { db := createTestDB(t) defer db.Close() dgm := NewDatabaseGuildManager(db) ctx := context.Background() // Insert test data insertGuildSQL := `INSERT INTO guilds (id, name, motd, level, xp, xp_needed, formed_on) VALUES (123, 'Specific Guild', 'Test MOTD', 10, 2000, 8000, ?)` formedTime := time.Now().Unix() err := db.Exec(insertGuildSQL, formedTime) if err != nil { t.Fatalf("Failed to insert test guild: %v", err) } // Test loading specific guild guild, err := dgm.LoadGuild(ctx, 123) if err != nil { t.Fatalf("Failed to load guild: %v", err) } if guild.ID != 123 { t.Errorf("Expected guild ID 123, got %d", guild.ID) } if guild.Name != "Specific Guild" { t.Errorf("Expected guild name 'Specific Guild', got '%s'", guild.Name) } if guild.MOTD != "Test MOTD" { t.Errorf("Expected MOTD 'Test MOTD', got '%s'", guild.MOTD) } // Test loading non-existent guild _, err = dgm.LoadGuild(ctx, 999) if err == nil { t.Error("Expected error when loading non-existent guild") } } // TestDatabaseGuildManager_LoadGuildMembers tests loading guild members func TestDatabaseGuildManager_LoadGuildMembers(t *testing.T) { db := createTestDB(t) defer db.Close() dgm := NewDatabaseGuildManager(db) ctx := context.Background() // Insert test guild err := db.Exec(`INSERT INTO guilds (id, name) VALUES (1, 'Test Guild')`) if err != nil { t.Fatalf("Failed to insert test guild: %v", err) } // Insert test members joinTime := time.Now().Unix() memberSQL := `INSERT INTO guild_members (char_id, guild_id, account_id, name, rank, adventure_level, join_date, last_login_date) VALUES (?, 1, 100, ?, ?, 50, ?, ?)` err = db.Exec(memberSQL, 1, "Player1", RankLeader, joinTime, joinTime) if err != nil { t.Fatalf("Failed to insert member 1: %v", err) } err = db.Exec(memberSQL, 2, "Player2", RankMember, joinTime, joinTime) if err != nil { t.Fatalf("Failed to insert member 2: %v", err) } // Test loading members members, err := dgm.LoadGuildMembers(ctx, 1) if err != nil { t.Fatalf("Failed to load guild members: %v", err) } if len(members) != 2 { t.Errorf("Expected 2 members, got %d", len(members)) } // Verify first member member1 := members[0] if member1.CharacterID != 1 { t.Errorf("Expected character ID 1, got %d", member1.CharacterID) } if member1.Name != "Player1" { t.Errorf("Expected name 'Player1', got '%s'", member1.Name) } if member1.Rank != RankLeader { t.Errorf("Expected rank %d, got %d", RankLeader, member1.Rank) } } // TestDatabaseGuildManager_SaveGuild tests saving guild data func TestDatabaseGuildManager_SaveGuild(t *testing.T) { db := createTestDB(t) defer db.Close() dgm := NewDatabaseGuildManager(db) ctx := context.Background() // Create test guild guild := NewGuild() guild.SetID(1) guild.SetName("Saved Guild", false) guild.SetLevel(15, false) guild.SetMOTD("Saved MOTD", false) guild.SetEXPCurrent(3000, false) guild.SetEXPToNextLevel(9000, false) guild.SetFormedDate(time.Now()) // Test saving guild err := dgm.SaveGuild(ctx, guild) if err != nil { t.Fatalf("Failed to save guild: %v", err) } // Verify the guild was saved savedGuild, err := dgm.LoadGuild(ctx, 1) if err != nil { t.Fatalf("Failed to load saved guild: %v", err) } if savedGuild.Name != "Saved Guild" { t.Errorf("Expected saved name 'Saved Guild', got '%s'", savedGuild.Name) } if savedGuild.Level != 15 { t.Errorf("Expected saved level 15, got %d", savedGuild.Level) } if savedGuild.MOTD != "Saved MOTD" { t.Errorf("Expected saved MOTD 'Saved MOTD', got '%s'", savedGuild.MOTD) } } // TestDatabaseGuildManager_CreateGuild tests creating a new guild func TestDatabaseGuildManager_CreateGuild(t *testing.T) { db := createTestDB(t) defer db.Close() dgm := NewDatabaseGuildManager(db) ctx := context.Background() // Create guild data guildData := GuildData{ Name: "New Guild", Level: 1, FormedDate: time.Now(), MOTD: "New guild MOTD", EXPCurrent: 111, EXPToNextLevel: 2521, RecruitingShortDesc: "Short description", RecruitingFullDesc: "Full description", RecruitingMinLevel: 1, RecruitingPlayStyle: 0, } // Test creating guild guildID, err := dgm.CreateGuild(ctx, guildData) if err != nil { t.Fatalf("Failed to create guild: %v", err) } if guildID <= 0 { t.Errorf("Expected positive guild ID, got %d", guildID) } // Verify the guild was created createdGuild, err := dgm.LoadGuild(ctx, guildID) if err != nil { t.Fatalf("Failed to load created guild: %v", err) } if createdGuild.Name != "New Guild" { t.Errorf("Expected created name 'New Guild', got '%s'", createdGuild.Name) } } // TestDatabaseGuildManager_DeleteGuild tests deleting a guild func TestDatabaseGuildManager_DeleteGuild(t *testing.T) { db := createTestDB(t) defer db.Close() dgm := NewDatabaseGuildManager(db) ctx := context.Background() // Insert test guild err := db.Exec(`INSERT INTO guilds (id, name) VALUES (1, 'To Delete')`) if err != nil { t.Fatalf("Failed to insert test guild: %v", err) } // Insert test member err = db.Exec(`INSERT INTO guild_members (char_id, guild_id, account_id, name) VALUES (1, 1, 100, 'Member')`) if err != nil { t.Fatalf("Failed to insert test member: %v", err) } // Verify guild exists _, err = dgm.LoadGuild(ctx, 1) if err != nil { t.Fatalf("Guild should exist before deletion: %v", err) } // Test deleting guild err = dgm.DeleteGuild(ctx, 1) if err != nil { t.Fatalf("Failed to delete guild: %v", err) } // Verify guild was deleted _, err = dgm.LoadGuild(ctx, 1) if err == nil { t.Error("Guild should not exist after deletion") } // Verify members were deleted members, err := dgm.LoadGuildMembers(ctx, 1) if err != nil { t.Fatalf("Failed to check members after guild deletion: %v", err) } if len(members) != 0 { t.Errorf("Expected 0 members after guild deletion, got %d", len(members)) } } // TestDatabaseGuildManager_GetGuildIDByCharacterID tests getting guild ID by character ID func TestDatabaseGuildManager_GetGuildIDByCharacterID(t *testing.T) { db := createTestDB(t) defer db.Close() dgm := NewDatabaseGuildManager(db) ctx := context.Background() // Insert test guild err := db.Exec(`INSERT INTO guilds (id, name) VALUES (1, 'Test Guild')`) if err != nil { t.Fatalf("Failed to insert test guild: %v", err) } // Insert test member err = db.Exec(`INSERT INTO guild_members (char_id, guild_id, account_id, name) VALUES (123, 1, 100, 'TestPlayer')`) if err != nil { t.Fatalf("Failed to insert test member: %v", err) } // Test getting guild ID for character guildID, err := dgm.GetGuildIDByCharacterID(ctx, 123) if err != nil { t.Fatalf("Failed to get guild ID by character ID: %v", err) } if guildID != 1 { t.Errorf("Expected guild ID 1, got %d", guildID) } // Test getting guild ID for non-existent character _, err = dgm.GetGuildIDByCharacterID(ctx, 999) if err == nil { t.Error("Expected error for non-existent character") } } // TestDatabaseGuildManager_ConcurrentOperations tests concurrent database operations // Now enabled with sqlitex.Pool for proper connection management func TestDatabaseGuildManager_ConcurrentOperations(t *testing.T) { db := createTestDB(t) defer db.Close() dgm := NewDatabaseGuildManager(db) ctx := context.Background() // Insert initial guild err := db.Exec(`INSERT INTO guilds (id, name) VALUES (1, 'Concurrent Test')`) if err != nil { t.Fatalf("Failed to insert test guild: %v", err) } const numGoroutines = 5 // Reduce concurrency to avoid SQLite issues done := make(chan error, numGoroutines) // Test concurrent reads for i := 0; i < numGoroutines; i++ { go func(id int) { // Add small delay to reduce contention time.Sleep(time.Duration(id) * time.Millisecond) _, err := dgm.LoadGuild(ctx, 1) done <- err }(i) } // Wait for all reads to complete for i := 0; i < numGoroutines; i++ { if err := <-done; err != nil { t.Errorf("Concurrent read failed: %v", err) } } // Test concurrent member additions for i := 0; i < numGoroutines; i++ { go func(id int) { memberSQL := `INSERT INTO guild_members (char_id, guild_id, account_id, name) VALUES (?, 1, 100, ?)` err := db.Exec(memberSQL, 100+id, fmt.Sprintf("Player%d", id)) done <- err }(i) } // Wait for all insertions successCount := 0 for i := 0; i < numGoroutines; i++ { if err := <-done; err == nil { successCount++ } } if successCount != numGoroutines { t.Logf("Only %d out of %d concurrent insertions succeeded (some conflicts expected)", successCount, numGoroutines) } } // TestDatabaseGuildManager_TransactionRollback tests transaction rollback on errors func TestDatabaseGuildManager_TransactionRollback(t *testing.T) { db := createTestDB(t) defer db.Close() dgm := NewDatabaseGuildManager(db) ctx := context.Background() // Try to create a guild with invalid data that should trigger a rollback invalidGuildData := GuildData{ Name: "", // Empty name should cause validation error Level: 1, FormedDate: time.Now(), } // This should fail but we test that it handles errors gracefully _, err := dgm.CreateGuild(ctx, invalidGuildData) // We don't expect specific error behavior here since the implementation // may or may not have validation, but we test that it doesn't crash t.Logf("Create guild with invalid data returned: %v", err) // Verify no partial data was left behind guilds, err := dgm.LoadGuilds(ctx) if err != nil { t.Fatalf("Failed to load guilds after rollback test: %v", err) } // Should have no guilds (or any existing test data, but no new invalid guild) t.Logf("Found %d guilds after invalid creation attempt", len(guilds)) } // BenchmarkDatabaseGuildManager_LoadGuilds benchmarks loading guilds func BenchmarkDatabaseGuildManager_LoadGuilds(b *testing.B) { // Create test database in temp directory tempDir := b.TempDir() dbPath := filepath.Join(tempDir, "bench_guilds.db") db, err := database.Open(dbPath) if err != nil { b.Fatalf("Failed to create benchmark database: %v", err) } defer db.Close() if err := createGuildTables(db); err != nil { b.Fatalf("Failed to create guild tables: %v", err) } dgm := NewDatabaseGuildManager(db) ctx := context.Background() // Insert test guilds for i := 0; i < 100; i++ { insertSQL := `INSERT INTO guilds (id, name, level, xp, xp_needed, formed_on) VALUES (?, ?, 1, 111, 2521, ?)` formedTime := time.Now().Unix() if err := db.Exec(insertSQL, i+1, fmt.Sprintf("Guild%d", i+1), formedTime); err != nil { b.Fatalf("Failed to insert test guild %d: %v", i+1, err) } } b.ResetTimer() for i := 0; i < b.N; i++ { _, err := dgm.LoadGuilds(ctx) if err != nil { b.Fatalf("Failed to load guilds in benchmark: %v", err) } } } // BenchmarkDatabaseGuildManager_LoadGuildMembers benchmarks loading guild members func BenchmarkDatabaseGuildManager_LoadGuildMembers(b *testing.B) { // Create test database in temp directory tempDir := b.TempDir() dbPath := filepath.Join(tempDir, "bench_members.db") db, err := database.Open(dbPath) if err != nil { b.Fatalf("Failed to create benchmark database: %v", err) } defer db.Close() if err := createGuildTables(db); err != nil { b.Fatalf("Failed to create guild tables: %v", err) } dgm := NewDatabaseGuildManager(db) ctx := context.Background() // Insert test guild if err := db.Exec(`INSERT INTO guilds (id, name) VALUES (1, 'Benchmark Guild')`); err != nil { b.Fatalf("Failed to insert benchmark guild: %v", err) } // Insert test members joinTime := time.Now().Unix() for i := 0; i < 100; i++ { memberSQL := `INSERT INTO guild_members (char_id, guild_id, account_id, name, rank, join_date, last_login_date) VALUES (?, 1, 100, ?, ?, ?, ?)` if err := db.Exec(memberSQL, i+1, fmt.Sprintf("Player%d", i+1), RankMember, joinTime, joinTime); err != nil { b.Fatalf("Failed to insert test member %d: %v", i+1, err) } } b.ResetTimer() for i := 0; i < b.N; i++ { _, err := dgm.LoadGuildMembers(ctx, 1) if err != nil { b.Fatalf("Failed to load guild members in benchmark: %v", err) } } }