import assert from "node:assert/strict"; import { test } from "node:test"; import request from "supertest"; import { createApp } from "../src/app"; import { createResetToken, resetPassword } from "../src/authService"; import { pool } from "../src/db"; import { extractRaceCoverImageFromHtml } from "../src/raceCoverImage"; import { hashPassword, normalizeEmail } from "../src/security"; const app = createApp(); let userCounter = 0; async function authAgent() { userCounter += 1; const email = normalizeEmail(`runner${userCounter}@example.com`); const password = "correct horse battery staple"; const passwordHash = await hashPassword(password); const inserted = await pool.query<{ id: string }>( `INSERT INTO users (email, password_hash) VALUES ($1, $2) RETURNING id`, [email, passwordHash], ); await pool.query("UPDATE users SET email_verified_at = COALESCE(email_verified_at, NOW()) WHERE id = $1", [ inserted.rows[0].id, ]); const agent = request.agent(app); const login = await agent.post("/api/auth/login").send({ email, password }).expect(200); return { agent, csrfToken: login.body.csrfToken as string }; } async function createVerifiedUser(email: string, password: string) { const passwordHash = await hashPassword(password); const inserted = await pool.query<{ id: string }>( `INSERT INTO users (email, password_hash) VALUES ($1, $2) RETURNING id`, [normalizeEmail(email), passwordHash], ); await pool.query("UPDATE users SET email_verified_at = COALESCE(email_verified_at, NOW()) WHERE id = $1", [ inserted.rows[0].id, ]); return inserted.rows[0].id; } 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 { agent } = await authAgent(); const res = await agent.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 { agent } = await authAgent(); const res = await agent.get("/api/races?month=13").expect(400); assert.equal(res.body.error, "validation_error"); }); test("GET /api/races accepts year and month", async () => { const { agent } = await authAgent(); const res = await agent.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 { agent } = await authAgent(); const res = await agent.get("/api/races/does-not-exist").expect(404); assert.equal(res.body.error, "not_found"); assert.ok(Array.isArray(res.body.details)); }); test("GET /api/races requires authentication", async () => { const res = await request(app).get("/api/races").expect(401); assert.equal(res.body.error, "unauthorized"); }); test("login uses generic response for missing user and wrong password", async () => { const password = "correct horse battery staple"; await createVerifiedUser("generic@example.com", password); const wrongPassword = await request(app) .post("/api/auth/login") .send({ email: "generic@example.com", password: "wrong password" }) .expect(401); const missingUser = await request(app) .post("/api/auth/login") .send({ email: "missing@example.com", password }) .expect(401); assert.deepEqual(missingUser.body, wrongPassword.body); }); test("GET /api/races/:id returns not_found for another user's race", async () => { const first = await authAgent(); const created = await first.agent .post("/api/races") .set("X-CSRF-Token", first.csrfToken) .send({ slug: "2026-07-01-private-race", date: "2026-07-01", title: "Private Race", distanceKm: 10, }) .expect(201); const second = await authAgent(); const res = await second.agent.get(`/api/races/${created.body.id}`).expect(404); assert.equal(res.body.error, "not_found"); }); test("new password reset token invalidates previous token", async () => { const userId = await createVerifiedUser("reset@example.com", "correct horse battery staple"); const client = await pool.connect(); const first = await createResetToken(client, userId); const second = await createResetToken(client, userId); client.release(); assert.equal(await resetPassword(first, "new correct horse battery staple"), false); assert.equal(await resetPassword(second, "new correct horse battery staple"), true); }); test("extractRaceCoverImageFromHtml prefers runc.run intro image", () => { const html = `