Add races results GET endpoints (by ID and last finished) and regenerate swagger specs

This commit is contained in:
2026-07-17 22:41:55 +04:00
parent c8ff4c04ad
commit 9c9632343d
10 changed files with 812 additions and 318 deletions
+85
View File
@@ -308,3 +308,88 @@ func TestLeaderboard(t *testing.T) {
}
})
}
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)
}
}
+25
View File
@@ -812,6 +812,31 @@ func (s *Service) GetCalendarLeaderboard(ctx context.Context, year int, currentD
return out, nil
}
// GetRace fetches a single race by id (either live from lobby or finished from DB).
func (s *Service) GetRace(ctx context.Context, id string) (FinishedRace, error) {
meta, err := s.lobby.GetRace(id)
if err == nil {
return FinishedRace{
ID: meta.ID,
Name: meta.Name,
TrackID: meta.TrackID,
MaxCars: meta.MaxCars,
Laps: meta.Laps,
TimeLimitS: meta.TimeLimitS,
DriverIDs: meta.DriverIDs,
Status: string(meta.Status),
CreatedMs: meta.CreatedMs,
StartedMs: meta.StartedMs,
}, nil
}
return s.pg.GetFinished(ctx, id)
}
// GetLastFinished fetches the most recently finished race.
func (s *Service) GetLastFinished(ctx context.Context) (FinishedRace, error) {
return s.pg.GetLastFinished(ctx)
}
// ---------------------------------------------------------------------------
// Scheduler
// ---------------------------------------------------------------------------
+55
View File
@@ -696,6 +696,61 @@ func (s *PgStore) hydrateFinished(ctx context.Context, r *FinishedRace) error {
return nil
}
// GetFinished fetches a single finished race by id.
func (s *PgStore) GetFinished(ctx context.Context, id string) (FinishedRace, error) {
row := s.pool.QueryRow(ctx, `
SELECT id, name, track_id, max_cars, laps, time_limit_s,
status, created_ms, started_ms, finished_ms, duration_ms,
total_laps, total_drivers
FROM races
WHERE id = $1 AND status IN ('finished', 'cancelled')`, id)
var r FinishedRace
if err := row.Scan(
&r.ID, &r.Name, &r.TrackID,
&r.MaxCars, &r.Laps, &r.TimeLimitS, &r.Status,
&r.CreatedMs, &r.StartedMs, &r.FinishedMs, &r.DurationMs,
&r.TotalLaps, &r.TotalDrivers,
); err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return FinishedRace{}, ErrNotFound
}
return FinishedRace{}, err
}
if err := s.hydrateFinished(ctx, &r); err != nil {
return FinishedRace{}, err
}
return r, nil
}
// GetLastFinished fetches the most recently finished race.
func (s *PgStore) GetLastFinished(ctx context.Context) (FinishedRace, error) {
row := s.pool.QueryRow(ctx, `
SELECT id, name, track_id, max_cars, laps, time_limit_s,
status, created_ms, started_ms, finished_ms, duration_ms,
total_laps, total_drivers
FROM races
WHERE status IN ('finished', 'cancelled')
ORDER BY finished_ms DESC, id DESC
LIMIT 1`)
var r FinishedRace
if err := row.Scan(
&r.ID, &r.Name, &r.TrackID,
&r.MaxCars, &r.Laps, &r.TimeLimitS, &r.Status,
&r.CreatedMs, &r.StartedMs, &r.FinishedMs, &r.DurationMs,
&r.TotalLaps, &r.TotalDrivers,
); err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return FinishedRace{}, ErrNotFound
}
return FinishedRace{}, err
}
if err := s.hydrateFinished(ctx, &r); err != nil {
return FinishedRace{}, err
}
return r, nil
}
func joinAnd(parts []string) string {
out := ""
for i, p := range parts {