feat: complete race history sprint

This commit is contained in:
Vakanaut
2026-07-13 08:31:12 +03:00
parent 46998a1a67
commit a8b87fc6b1
22 changed files with 725 additions and 288 deletions

View File

@@ -4,6 +4,7 @@ import path from "node:path";
import { after, test } from "node:test";
import request from "supertest";
import { buildHelmetOptions, createApp } from "../src/app";
import { backfillRaceCovers } from "../src/backfillRaceCovers";
import {
cleanupExpiredAuthRows,
createResetToken,
@@ -161,6 +162,29 @@ test("GET /api/races accepts year and month", async () => {
assert.ok(Array.isArray(res.body));
});
test("GET /api/races filters by status, date and distance", async () => {
const { agent, csrfToken } = await authAgent();
await agent.post("/api/races").set("X-CSRF-Token", csrfToken).send({
slug: "2026-05-01-filter-planned", date: "2026-05-01", title: "Planned 5", distanceKm: 5, status: "planned",
}).expect(201);
await agent.post("/api/races").set("X-CSRF-Token", csrfToken).send({
slug: "2026-06-01-filter-completed", date: "2026-06-01", title: "Completed 10", distanceKm: 10, status: "completed",
}).expect(201);
await agent.post("/api/races").set("X-CSRF-Token", csrfToken).send({
slug: "2026-07-01-filter-completed", date: "2026-07-01", title: "Completed 21", distanceKm: 21.1, status: "completed",
}).expect(201);
const res = await agent.get("/api/races?status=completed&dateFrom=2026-06-01&dateTo=2026-06-30&distanceMax=10").expect(200);
assert.deepEqual(res.body.map((race: { title: string }) => race.title), ["Completed 10"]);
});
test("GET /api/races rejects inverted filter ranges", async () => {
const { agent } = await authAgent();
const res = await agent.get("/api/races?dateFrom=2026-07-01&dateTo=2026-06-01").expect(400);
assert.equal(res.body.error, "validation_error");
assert.ok(res.body.details.some((detail: string) => detail.includes("dateFrom")));
});
test("GET /api/races/:id rejects a non-UUID id", async () => {
const { agent } = await authAgent();
const res = await agent.get("/api/races/does-not-exist").expect(400);
@@ -460,6 +484,39 @@ test("POST /api/races stores manual coverImageUrl", async () => {
assert.equal(res.body.coverImageUrl, coverImageUrl);
});
test("PATCH /api/races refreshes an automatic cover but preserves a manual cover", async () => {
const originalFetch = globalThis.fetch;
let imageName = "first.jpg";
globalThis.fetch = async () => new Response(`<meta property="og:image" content="/${imageName}">`, {
status: 200,
headers: { "content-type": "text/html" },
});
try {
const { agent, csrfToken } = await authAgent();
const created = await agent.post("/api/races").set("X-CSRF-Token", csrfToken).send({
slug: "2026-06-06-auto-refresh", date: "2026-06-06", title: "Auto Refresh", distanceKm: 10, officialUrl: "https://example.com/first",
}).expect(201);
imageName = "second.jpg";
const refreshed = await agent.patch(`/api/races/${created.body.id}`).set("X-CSRF-Token", csrfToken).send({
officialUrl: "https://example.com/second",
}).expect(200);
assert.equal(refreshed.body.coverImageUrl, "https://example.com/second.jpg");
const manual = await agent.patch(`/api/races/${created.body.id}`).set("X-CSRF-Token", csrfToken).send({
officialUrl: "https://example.com/third", coverImageUrl: "https://example.com/manual.jpg",
}).expect(200);
imageName = "fourth.jpg";
const preserved = await agent.patch(`/api/races/${created.body.id}`).set("X-CSRF-Token", csrfToken).send({
officialUrl: "https://example.com/fourth",
}).expect(200);
assert.equal(manual.body.coverImageUrl, "https://example.com/manual.jpg");
assert.equal(preserved.body.coverImageUrl, "https://example.com/manual.jpg");
} finally {
globalThis.fetch = originalFetch;
}
});
test("POST /api/races rejects unknown and malformed fields", async () => {
const { agent, csrfToken } = await authAgent();
const res = await agent
@@ -526,6 +583,58 @@ test("POST /api/races auto extracts coverImageUrl from officialUrl", async () =>
}
});
test("PATCH /api/races auto extracts a missing cover when officialUrl changes", async () => {
const originalFetch = globalThis.fetch;
globalThis.fetch = async () => new Response("", { status: 404 });
try {
const { agent, csrfToken } = await authAgent();
const created = await agent
.post("/api/races")
.set("X-CSRF-Token", csrfToken)
.send({ slug: "2026-06-04-patch-cover", date: "2026-06-04", title: "Patch Cover", distanceKm: 10 })
.expect(201);
globalThis.fetch = async () => new Response('<meta property="og:image" content="/patched.jpg">', {
status: 200,
headers: { "content-type": "text/html" },
});
const updated = await agent
.patch(`/api/races/${created.body.id}`)
.set("X-CSRF-Token", csrfToken)
.send({ officialUrl: "https://example.com/patched" })
.expect(200);
assert.equal(updated.body.coverImageUrl, "https://example.com/patched.jpg");
} finally {
globalThis.fetch = originalFetch;
}
});
test("cover backfill restores only races without a cover", async () => {
const originalFetch = globalThis.fetch;
globalThis.fetch = async () => new Response('<meta property="og:image" content="/backfill.jpg">', {
status: 200,
headers: { "content-type": "text/html" },
});
try {
const { agent, csrfToken } = await authAgent();
await agent.post("/api/races").set("X-CSRF-Token", csrfToken).send({
slug: "2026-06-05-backfill", date: "2026-06-05", title: "Backfill", distanceKm: 10,
}).expect(201);
await pool.query("UPDATE races SET official_url = $1 WHERE slug = $2", ["https://example.com/backfill", "2026-06-05-backfill"]);
const stats = await backfillRaceCovers();
assert.ok(stats.candidates >= 1);
assert.ok(stats.restored >= 1);
const result = await pool.query<{ cover_image_url: string }>("SELECT cover_image_url FROM races WHERE slug = $1", ["2026-06-05-backfill"]);
assert.equal(result.rows[0]?.cover_image_url, "https://example.com/backfill.jpg");
} finally {
globalThis.fetch = originalFetch;
}
});
test("POST /api/races succeeds when cover extraction fails", async () => {
const originalFetch = globalThis.fetch;
globalThis.fetch = async () => {