package races import ( "context" "os" "testing" "time" "github.com/jackc/pgx/v5/pgxpool" ) func TestLeaderboard(t *testing.T) { dsn := os.Getenv("DATABASE_URL") if dsn == "" { t.Skip("DATABASE_URL is not set, skipping database integration tests") } ctx := context.Background() pool, err := pgxpool.New(ctx, dsn) if err != nil { t.Fatalf("failed to connect to test database: %v", err) } defer pool.Close() store := NewPgStore(pool) // Clean up any left-overs with our test prefix cleanup := func() { _, _ = pool.Exec(ctx, "DELETE FROM race_drivers WHERE race_id LIKE 'test-race-%'") _, _ = pool.Exec(ctx, "DELETE FROM races WHERE id LIKE 'test-race-%'") _, _ = pool.Exec(ctx, "DELETE FROM drivers WHERE id LIKE 'test-driver-%'") _, _ = pool.Exec(ctx, "DELETE FROM clans WHERE id LIKE 'test-clan-%'") } cleanup() defer cleanup() // 1. Create a test clan and test drivers nowMs := time.Now().UnixMilli() _, err = pool.Exec(ctx, ` INSERT INTO clans (id, tag, name, avatar_url, created_ms, updated_ms) VALUES ('test-clan-1', 'TST', 'Test Clan', 'http://avatar.clan', $1, $1)`, nowMs) if err != nil { t.Fatalf("failed to insert test clan: %v", err) } driversData := []struct { id string nick string name string }{ {"test-driver-1", "DRV", "Driver One"}, {"test-driver-2", "TWO", "Driver Two"}, {"test-driver-3", "THR", "Driver Three"}, {"test-driver-4", "FOR", "Driver Four"}, } for _, d := range driversData { _, err = pool.Exec(ctx, ` INSERT INTO drivers (id, nickname, name, avatar_url, clan_id, created_ms, updated_ms) VALUES ($1, $2, $3, 'http://avatar', 'test-clan-1', $4, $4)`, d.id, d.nick, d.name, nowMs) if err != nil { t.Fatalf("failed to insert test driver %s: %v", d.id, err) } } // 2. Create test races on different tracks for current year currentYear := time.Now().Year() testYearMs := time.Date(currentYear, time.June, 1, 12, 0, 0, 0, time.UTC).UnixMilli() racesData := []struct { id string trackID string }{ {"test-race-1", "test-track-A"}, {"test-race-2", "test-track-A"}, {"test-race-3", "test-track-B"}, } for _, r := range racesData { _, err = pool.Exec(ctx, ` INSERT INTO races (id, name, track_id, max_cars, laps, time_limit_s, status, created_ms, started_ms, finished_ms, updated_ms) VALUES ($1, 'Test Race', $2, 4, 10, 60, 'finished', $3, $3, $3, $3)`, r.id, r.trackID, testYearMs) if err != nil { t.Fatalf("failed to insert test race %s: %v", r.id, err) } } // 3. Insert race drivers results // Race 1 (Track A): // - test-driver-1: best_lap 100 (1st fastest -> 25 points) // - test-driver-2: best_lap 400 (currently 3rd fastest -> 25 points) // - test-driver-3: best_lap 300 (currently 2nd fastest -> 25 points) // - test-driver-4: best_lap 0 (DNF, completed 0 laps -> 1 point) resultsRace1 := []struct { driverID string bestLapMs int64 pos any }{ {"test-driver-1", 100, 1}, {"test-driver-2", 400, 2}, {"test-driver-3", 300, 3}, {"test-driver-4", 0, nil}, } for _, res := range resultsRace1 { _, err = pool.Exec(ctx, ` INSERT INTO race_drivers (race_id, driver_id, slot, joined_ms, total_time_ms, best_lap_ms, position) VALUES ('test-race-1', $1, 0, $2, 1000, $3, $4)`, res.driverID, testYearMs, res.bestLapMs, res.pos) if err != nil { t.Fatalf("failed to insert race driver result: %v", err) } } // Race 2 (Track A - testing best results): // - test-driver-2: best_lap 150 (2nd fastest -> 25 points, overrides 400ms lap time) // This makes the times: // - test-driver-1: 100ms (rank 1 -> 25 points) // - test-driver-2: 150ms (rank 2 -> 25 points) // - test-driver-3: 300ms (rank 3 -> 25 points) // - test-driver-4: NULL (rank 4 -> 1 point) // Exactly 3 drivers have 25 points. _, err = pool.Exec(ctx, ` INSERT INTO race_drivers (race_id, driver_id, slot, joined_ms, total_time_ms, best_lap_ms, position) VALUES ('test-race-2', 'test-driver-2', 0, $1, 1000, 150, 1)`, testYearMs) if err != nil { t.Fatalf("failed to insert race 2 result: %v", err) } // Race 3 (Track B): // - test-driver-1: best_lap 500 (rank 2 on Track B -> 25 points) // - test-driver-3: best_lap 200 (rank 1 on Track B -> 25 points) resultsRace3 := []struct { driverID string bestLapMs int64 pos any }{ {"test-driver-1", 500, 2}, {"test-driver-3", 200, 1}, } for _, res := range resultsRace3 { _, err = pool.Exec(ctx, ` INSERT INTO race_drivers (race_id, driver_id, slot, joined_ms, total_time_ms, best_lap_ms, position) VALUES ('test-race-3', $1, 0, $2, 1000, $3, $4)`, res.driverID, testYearMs, res.bestLapMs, res.pos) if err != nil { t.Fatalf("failed to insert race 3 driver result: %v", err) } } // Test Track A Leaderboard t.Run("Track A Leaderboard", func(t *testing.T) { entries, total, err := store.GetTrackLeaderboard(ctx, "test-track-A", currentYear, 10, 0) if err != nil { t.Fatalf("failed to get track leaderboard: %v", err) } if total != 4 { t.Errorf("expected total 4, got %d", total) } if len(entries) != 4 { t.Fatalf("expected 4 entries, got %d", len(entries)) } // Rank 1: test-driver-1 (25 pts, best_time 100ms) // Rank 2: test-driver-2 (25 pts, best_time 150ms) // Rank 3: test-driver-3 (25 pts, best_time 300ms) // Rank 4: test-driver-4 (1 pt, best_time NULL) drv1 := entries[0] if drv1.DriverID != "test-driver-1" || drv1.Points != 25 || *drv1.BestTimeMs != 100 || drv1.Rank != 1 { t.Errorf("invalid rank 1: %+v", drv1) } drv2 := entries[1] if drv2.DriverID != "test-driver-2" || drv2.Points != 25 || *drv2.BestTimeMs != 150 || drv2.Rank != 2 { t.Errorf("invalid rank 2: %+v", drv2) } // Check entries by looping to find specific drivers for _, e := range entries { if e.DriverID == "test-driver-3" { if e.Points != 25 || *e.BestTimeMs != 300 || e.Rank != 3 { t.Errorf("invalid test-driver-3: %+v", e) } } if e.DriverID == "test-driver-4" { if e.Points != 1 || e.BestTimeMs != nil || e.Rank != 4 { t.Errorf("invalid test-driver-4: %+v", e) } } } }) // Test Track A Driver Rank t.Run("Track A Driver Rank", func(t *testing.T) { entry, err := store.GetTrackLeaderboardDriver(ctx, "test-track-A", currentYear, "test-driver-2") if err != nil { t.Fatalf("failed to get driver rank: %v", err) } if entry == nil || entry.Rank != 2 || entry.Points != 25 || *entry.BestTimeMs != 150 { t.Errorf("expected rank 2, points 25, best_time 150, got %+v", entry) } }) // Test Overall Leaderboard t.Run("Overall Leaderboard", func(t *testing.T) { // Overall points calculation: // - test-driver-1: Track A (25) + Track B (25) = 50 points // - test-driver-3: Track A (25) + Track B (25) = 50 points // - test-driver-2: Track A (25) + Track B (0) = 25 points // - test-driver-4: Track A (1) + Track B (0) = 1 point entries, _, err := store.GetOverallLeaderboard(ctx, currentYear, 200, 0) if err != nil { t.Fatalf("failed to get overall leaderboard: %v", err) } // Filter to only our test drivers var testEntries []LeaderboardEntry for _, entry := range entries { if len(entry.DriverID) >= 11 && entry.DriverID[:12] == "test-driver-" { testEntries = append(testEntries, entry) } } if len(testEntries) != 4 { t.Fatalf("expected 4 test entries, got %d", len(testEntries)) } // test-driver-1 and test-driver-3 should both have 50 points if testEntries[0].Points != 50 || testEntries[1].Points != 50 { t.Errorf("expected top 2 test drivers to have 50 points, got: %+v and %+v", testEntries[0], testEntries[1]) } if testEntries[2].DriverID != "test-driver-2" || testEntries[2].Points != 25 { t.Errorf("invalid overall rank 3: %+v", testEntries[2]) } if testEntries[3].DriverID != "test-driver-4" || testEntries[3].Points != 1 { t.Errorf("invalid overall rank 4: %+v", testEntries[3]) } }) // Test Overall Driver Rank t.Run("Overall Driver Rank", func(t *testing.T) { entry, err := store.GetOverallLeaderboardDriver(ctx, currentYear, "test-driver-3") if err != nil { t.Fatalf("failed to get overall driver rank: %v", err) } if entry == nil || entry.Points != 50 || entry.Rank <= 0 { t.Errorf("expected rank > 0, points 50, got %+v", entry) } }) // Test Fallback Profile t.Run("Fallback Profile", func(t *testing.T) { entry, err := store.GetDriverLeaderboardProfile(ctx, "test-driver-1") if err != nil { t.Fatalf("failed to get profile: %v", err) } if entry == nil || entry.Name != "Driver One" || entry.Nickname != "DRV" || entry.ClanTag != "TST" { t.Errorf("expected Driver One, DRV, TST, got %+v", entry) } }) // Test Calendar Leaderboard t.Run("Calendar Leaderboard", func(t *testing.T) { _, _ = pool.Exec(ctx, "DELETE FROM track_calendar") now := time.Now() _, err = pool.Exec(ctx, ` INSERT INTO track_calendar (track_id, start_at, end_at) VALUES ('monaco', $1, $2), ('barcelona-catalunya', $3, $4), ('austria-redbull-ring', $5, $6) `, now.Add(-10*time.Hour), now.Add(-5*time.Hour), now.Add(-2*time.Hour), now.Add(2*time.Hour), now.Add(5*time.Hour), now.Add(10*time.Hour), ) if err != nil { t.Fatalf("failed to insert track calendar: %v", err) } defer func() { _, _ = pool.Exec(ctx, "DELETE FROM track_calendar") }() svc := NewService(store, nil) svc.now = func() time.Time { return now } res, err := svc.GetCalendarLeaderboard(ctx, currentYear, "test-driver-1") if err != nil { t.Fatalf("failed to get calendar leaderboard: %v", err) } if len(res) != 3 { t.Fatalf("expected 3 calendar entries, got %d", len(res)) } // 1. Monaco (finished) if res[0].TrackID != "monaco" || res[0].Status != "finished" { t.Errorf("expected monaco (finished), got %s (%s)", res[0].TrackID, res[0].Status) } // 2. Barcelona (active) if res[1].TrackID != "barcelona-catalunya" || res[1].Status != "active" { t.Errorf("expected barcelona-catalunya (active), got %s (%s)", res[1].TrackID, res[1].Status) } // 3. Austria (planned) if res[2].TrackID != "austria-redbull-ring" || res[2].Status != "planned" { t.Errorf("expected austria-redbull-ring (planned), got %s (%s)", res[2].TrackID, res[2].Status) } }) }