170 lines
5.0 KiB
TypeScript
170 lines
5.0 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { test } from "node:test";
|
|
import request from "supertest";
|
|
import { createApp } from "../src/app";
|
|
import { extractRaceCoverImageFromHtml } from "../src/raceCoverImage";
|
|
|
|
const app = createApp();
|
|
|
|
test("GET /api/health returns ok", async () => {
|
|
const res = await request(app).get("/api/health").expect(200);
|
|
assert.equal(res.body.status, "ok");
|
|
assert.equal(typeof res.body.version, "string");
|
|
assert.ok(res.body.version.length > 0);
|
|
});
|
|
|
|
test("GET /api/meta returns version for UI footer", async () => {
|
|
const res = await request(app).get("/api/meta").expect(200);
|
|
assert.equal(typeof res.body.version, "string");
|
|
assert.ok(res.body.version.length > 0);
|
|
});
|
|
|
|
test("GET /api/ready succeeds with mock database", async () => {
|
|
const res = await request(app).get("/api/ready").expect(200);
|
|
assert.equal(res.body.status, "ready");
|
|
assert.equal(res.body.db, "connected");
|
|
});
|
|
|
|
test("GET /api/races rejects invalid year", async () => {
|
|
const res = await request(app).get("/api/races?year=bad").expect(400);
|
|
assert.equal(res.body.error, "validation_error");
|
|
assert.ok(Array.isArray(res.body.details));
|
|
});
|
|
|
|
test("GET /api/races rejects month out of range", async () => {
|
|
const res = await request(app).get("/api/races?month=13").expect(400);
|
|
assert.equal(res.body.error, "validation_error");
|
|
});
|
|
|
|
test("GET /api/races accepts year and month", async () => {
|
|
const res = await request(app).get("/api/races?year=2026&month=5").expect(200);
|
|
assert.ok(Array.isArray(res.body));
|
|
});
|
|
|
|
test("GET /api/races/:id returns not_found", async () => {
|
|
const res = await request(app).get("/api/races/does-not-exist").expect(404);
|
|
assert.equal(res.body.error, "not_found");
|
|
assert.ok(Array.isArray(res.body.details));
|
|
});
|
|
|
|
test("extractRaceCoverImageFromHtml prefers runc.run intro image", () => {
|
|
const html = `
|
|
<meta property="og:image" content="https://example.com/og.jpg">
|
|
<div class="run-intro__image">
|
|
<div class="run-intro__image-left-shadow"></div>
|
|
<img src="/uploads/race_landing_header_backgrounds/header.jpg" alt="">
|
|
<div class="run-intro__image-right-shadow"></div>
|
|
</div>
|
|
`;
|
|
|
|
assert.equal(
|
|
extractRaceCoverImageFromHtml(html, "https://aprilrun5km.runc.run/"),
|
|
"https://aprilrun5km.runc.run/uploads/race_landing_header_backgrounds/header.jpg",
|
|
);
|
|
});
|
|
|
|
test("extractRaceCoverImageFromHtml reads Open Graph and Twitter images", () => {
|
|
assert.equal(
|
|
extractRaceCoverImageFromHtml(
|
|
'<meta property="og:image" content="/cover.png">',
|
|
"https://example.com/race",
|
|
),
|
|
"https://example.com/cover.png",
|
|
);
|
|
assert.equal(
|
|
extractRaceCoverImageFromHtml(
|
|
'<meta name="twitter:image" content="https://cdn.example.com/twitter.jpg">',
|
|
"https://example.com/race",
|
|
),
|
|
"https://cdn.example.com/twitter.jpg",
|
|
);
|
|
});
|
|
|
|
test("POST /api/races stores manual coverImageUrl", async () => {
|
|
const coverImageUrl = "https://example.com/manual.jpg";
|
|
const res = await request(app)
|
|
.post("/api/races")
|
|
.send({
|
|
id: "2026-06-01-manual-cover",
|
|
date: "2026-06-01",
|
|
title: "Manual Cover",
|
|
distanceKm: 10,
|
|
officialUrl: "https://example.com/race",
|
|
coverImageUrl,
|
|
})
|
|
.expect(201);
|
|
|
|
assert.equal(res.body.coverImageUrl, coverImageUrl);
|
|
});
|
|
|
|
test("POST /api/races auto extracts coverImageUrl from officialUrl", async () => {
|
|
const originalFetch = globalThis.fetch;
|
|
globalThis.fetch = async () =>
|
|
new Response('<meta property="og:image" content="/auto.jpg">', {
|
|
status: 200,
|
|
headers: { "content-type": "text/html" },
|
|
});
|
|
|
|
try {
|
|
const res = await request(app)
|
|
.post("/api/races")
|
|
.send({
|
|
id: "2026-06-02-auto-cover",
|
|
date: "2026-06-02",
|
|
title: "Auto Cover",
|
|
distanceKm: 21.1,
|
|
officialUrl: "https://example.com/race",
|
|
})
|
|
.expect(201);
|
|
|
|
assert.equal(res.body.coverImageUrl, "https://example.com/auto.jpg");
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
test("POST /api/races succeeds when cover extraction fails", async () => {
|
|
const originalFetch = globalThis.fetch;
|
|
globalThis.fetch = async () => {
|
|
throw new Error("network down");
|
|
};
|
|
|
|
try {
|
|
const res = await request(app)
|
|
.post("/api/races")
|
|
.send({
|
|
id: "2026-06-03-cover-fail",
|
|
date: "2026-06-03",
|
|
title: "Cover Fail",
|
|
distanceKm: 5,
|
|
officialUrl: "https://example.com/race",
|
|
})
|
|
.expect(201);
|
|
|
|
assert.equal(res.body.coverImageUrl, null);
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
test("PATCH /api/races/:id updates coverImageUrl explicitly", async () => {
|
|
const id = "2026-06-04-patch-cover";
|
|
await request(app)
|
|
.post("/api/races")
|
|
.send({
|
|
id,
|
|
date: "2026-06-04",
|
|
title: "Patch Cover",
|
|
distanceKm: 10,
|
|
})
|
|
.expect(201);
|
|
|
|
const coverImageUrl = "https://example.com/patched.jpg";
|
|
const res = await request(app)
|
|
.patch(`/api/races/${id}`)
|
|
.send({ coverImageUrl })
|
|
.expect(200);
|
|
|
|
assert.equal(res.body.coverImageUrl, coverImageUrl);
|
|
});
|