mirror of
https://github.com/IS1DI/x0gp.git
synced 2026-07-19 05:47:02 +00:00
Add races results GET endpoints (by ID and last finished) and regenerate swagger specs
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user