mirror of
https://github.com/IS1DI/x0gp.git
synced 2026-07-18 13:27:03 +00:00
520 lines
16 KiB
Go
520 lines
16 KiB
Go
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. Austria (planned)
|
|
if res[0].TrackID != "austria-redbull-ring" || res[0].Status != "planned" {
|
|
t.Errorf("expected austria-redbull-ring (planned), 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. Monaco (finished)
|
|
if res[2].TrackID != "monaco" || res[2].Status != "finished" {
|
|
t.Errorf("expected monaco (finished), got %s (%s)", res[2].TrackID, res[2].Status)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestGetRaceAndLastFinished(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)
|
|
|
|
cleanup := func() {
|
|
_, _ = pool.Exec(ctx, "DELETE FROM race_drivers WHERE race_id LIKE 'test-get-race-%'")
|
|
_, _ = pool.Exec(ctx, "DELETE FROM races WHERE id LIKE 'test-get-race-%'")
|
|
}
|
|
cleanup()
|
|
defer cleanup()
|
|
|
|
// 1. Insert a finished race
|
|
now := time.Now().UnixMilli()
|
|
race := FinishedRace{
|
|
ID: "test-get-race-1",
|
|
Name: "Test Get Race 1",
|
|
TrackID: "monaco",
|
|
MaxCars: 4,
|
|
Laps: 5,
|
|
TimeLimitS: 300,
|
|
Status: "finished",
|
|
CreatedMs: now - 10000,
|
|
StartedMs: now - 8000,
|
|
FinishedMs: now - 5000,
|
|
DurationMs: 3000,
|
|
TotalLaps: 20,
|
|
TotalDrivers: 1,
|
|
}
|
|
err = store.InsertFinished(ctx, race)
|
|
if err != nil {
|
|
t.Fatalf("failed to insert finished race: %v", err)
|
|
}
|
|
|
|
// 2. Fetch the race by ID
|
|
got, err := store.GetFinished(ctx, "test-get-race-1")
|
|
if err != nil {
|
|
t.Fatalf("GetFinished failed: %v", err)
|
|
}
|
|
if got.ID != "test-get-race-1" || got.Name != "Test Get Race 1" {
|
|
t.Errorf("expected test-get-race-1, got: %+v", got)
|
|
}
|
|
|
|
// 3. Insert a second finished race (newer finished_ms)
|
|
race2 := FinishedRace{
|
|
ID: "test-get-race-2",
|
|
Name: "Test Get Race 2",
|
|
TrackID: "monaco",
|
|
MaxCars: 4,
|
|
Laps: 5,
|
|
TimeLimitS: 300,
|
|
Status: "finished",
|
|
CreatedMs: now - 5000,
|
|
StartedMs: now - 4000,
|
|
FinishedMs: now - 1000,
|
|
DurationMs: 3000,
|
|
TotalLaps: 20,
|
|
TotalDrivers: 1,
|
|
}
|
|
err = store.InsertFinished(ctx, race2)
|
|
if err != nil {
|
|
t.Fatalf("failed to insert finished race 2: %v", err)
|
|
}
|
|
|
|
// 4. Fetch the last finished race (should be test-get-race-2)
|
|
last, err := store.GetLastFinished(ctx)
|
|
if err != nil {
|
|
t.Fatalf("GetLastFinished failed: %v", err)
|
|
}
|
|
if last.ID != "test-get-race-2" {
|
|
t.Errorf("expected test-get-race-2 as last race, got: %s", last.ID)
|
|
}
|
|
}
|
|
|
|
func TestDriverProfileStats(t *testing.T) {
|
|
dsn := os.Getenv("DATABASE_URL")
|
|
if dsn == "" {
|
|
t.Skip("DATABASE_URL is empty; skipping integration test")
|
|
}
|
|
|
|
ctx := context.Background()
|
|
pool, err := pgxpool.New(ctx, dsn)
|
|
if err != nil {
|
|
t.Fatalf("failed to open pool: %v", err)
|
|
}
|
|
defer pool.Close()
|
|
|
|
store := NewPgStore(pool)
|
|
|
|
// Clean up any test records
|
|
_, _ = store.Exec(ctx, `DELETE FROM race_queue WHERE driver_id = 'test-stats-drv'`)
|
|
_, _ = store.Exec(ctx, `DELETE FROM race_drivers WHERE driver_id = 'test-stats-drv'`)
|
|
_, _ = store.Exec(ctx, `DELETE FROM races WHERE id LIKE 'test-stats-race-%'`)
|
|
_, _ = store.Exec(ctx, `DELETE FROM drivers WHERE id = 'test-stats-drv'`)
|
|
|
|
// 1. Create a driver
|
|
_, _ = store.Exec(ctx, `
|
|
INSERT INTO drivers (id, nickname, name, avatar_url, created_ms, updated_ms)
|
|
VALUES ('test-stats-drv', 'TSD', 'Test Stats Driver', '', 1700000000, 1700000000)`)
|
|
|
|
// 2. Insert races and race results
|
|
// Race 1: finished, TSD took 2nd position, best lap 15000, total time 50000
|
|
race1 := FinishedRace{
|
|
ID: "test-stats-race-1",
|
|
Name: "Race 1",
|
|
TrackID: "monaco",
|
|
MaxCars: 4,
|
|
Laps: 5,
|
|
TimeLimitS: 300,
|
|
DriverIDs: []string{"test-stats-drv"},
|
|
Status: "finished",
|
|
CreatedMs: 1700000000,
|
|
StartedMs: 1700000500,
|
|
FinishedMs: 1700060000,
|
|
DurationMs: 59500,
|
|
TotalLaps: 5,
|
|
TotalDrivers: 2,
|
|
Results: []DriverResult{
|
|
{
|
|
DriverID: "test-stats-drv",
|
|
TotalTimeMs: 50000,
|
|
BestLapMs: 15000,
|
|
Position: ptrInt(2),
|
|
},
|
|
},
|
|
}
|
|
if err := store.InsertFinished(ctx, race1); err != nil {
|
|
t.Fatalf("InsertFinished 1 failed: %v", err)
|
|
}
|
|
|
|
// Race 2: finished, TSD took 1st position (Win), best lap 14000 (Best Lap), total time 45000
|
|
race2 := FinishedRace{
|
|
ID: "test-stats-race-2",
|
|
Name: "Race 2",
|
|
TrackID: "monaco",
|
|
MaxCars: 4,
|
|
Laps: 5,
|
|
TimeLimitS: 300,
|
|
DriverIDs: []string{"test-stats-drv"},
|
|
Status: "finished",
|
|
CreatedMs: 1700100000,
|
|
StartedMs: 1700100500,
|
|
FinishedMs: 1700160000,
|
|
DurationMs: 59500,
|
|
TotalLaps: 5,
|
|
TotalDrivers: 2,
|
|
Results: []DriverResult{
|
|
{
|
|
DriverID: "test-stats-drv",
|
|
TotalTimeMs: 45000,
|
|
BestLapMs: 14000,
|
|
Position: ptrInt(1),
|
|
},
|
|
},
|
|
}
|
|
if err := store.InsertFinished(ctx, race2); err != nil {
|
|
t.Fatalf("InsertFinished 2 failed: %v", err)
|
|
}
|
|
|
|
// 3. Query stats
|
|
stats, err := store.GetDriverStats(ctx, "test-stats-drv")
|
|
if err != nil {
|
|
t.Fatalf("GetDriverStats failed: %v", err)
|
|
}
|
|
if stats.TotalRacesStarted != 2 || stats.TotalRacesFinished != 2 {
|
|
t.Errorf("expected 2 races started/finished, got: %+v", stats)
|
|
}
|
|
if stats.Wins != 1 || stats.Podiums != 2 {
|
|
t.Errorf("expected 1 win, 2 podiums, got: %+v", stats)
|
|
}
|
|
if stats.BestLapMs != 14000 {
|
|
t.Errorf("expected best lap 14000, got %d", stats.BestLapMs)
|
|
}
|
|
if stats.TotalPlaytimeMs != 95000 {
|
|
t.Errorf("expected total playtime 95000, got %d", stats.TotalPlaytimeMs)
|
|
}
|
|
|
|
// 4. Query last race (should be Race 2, finished at 1700160000)
|
|
last, err := store.GetDriverLastRace(ctx, "test-stats-drv")
|
|
if err != nil {
|
|
t.Fatalf("GetDriverLastRace failed: %v", err)
|
|
}
|
|
if last == nil || last.RaceID != "test-stats-race-2" {
|
|
t.Errorf("expected last race test-stats-race-2, got: %+v", last)
|
|
}
|
|
|
|
// 5. Query best race (should be Race 2, because position 1 is better than 2)
|
|
best, err := store.GetDriverBestRace(ctx, "test-stats-drv")
|
|
if err != nil {
|
|
t.Fatalf("GetDriverBestRace failed: %v", err)
|
|
}
|
|
if best == nil || best.RaceID != "test-stats-race-2" {
|
|
t.Errorf("expected best race test-stats-race-2, got: %+v", best)
|
|
}
|
|
}
|
|
|
|
func ptrInt(v int) *int { return &v }
|
|
|