import { randomUUID } from "node:crypto"; import bcrypt from "bcrypt"; import { roles, rolePermissions, tariffs, tariffVersions, users, type Db } from "@parking/db"; import type { Permission, TariffStructure } from "@parking/shared"; import type { FastifyBaseLogger, FastifyInstance } from "fastify"; import { EventLog } from "./event-log.js"; import { SoftwareSigner, buildVerifier } from "./signer.js"; // Shared scaffolding for server tests (NOT a *.test file, so it is not collected as a // suite and stays out of shipped dist via the tsconfig test-exclude). Builds the real // EventLog over a fresh test DB, a silent logger, and a minimal active tariff so the // pay/exit flows have something to price against. const SECRET = "test-event-signing-key-0123456789"; /** Real EventLog (real signer + per-keyId verifier) over a test DB. */ export function makeLog(db: Db): EventLog { return new EventLog(db, new SoftwareSigner(SECRET), buildVerifier); } /** A logger that swallows everything — flows log liberally; tests don't care. */ export function silentLogger(): FastifyBaseLogger { const noop = () => {}; const l: Record = { info: noop, warn: noop, error: noop, debug: noop, fatal: noop, trace: noop, silent: noop, level: "silent", }; l.child = () => l; return l as unknown as FastifyBaseLogger; } /** A simple flat-rate V1 tariff: free under the entry grace, then a fixed price per * increment, with a walk-back exit grace. Returns the tariffVersionId + currency. */ export function seedTariff( db: Db, opts: { pricePerIncrementMinor?: number; incrementMin?: number; gracePeriodEntryMin?: number; gracePeriodExitMin?: number; currency?: string; effectiveFrom?: string } = {}, ): { tariffVersionId: string; currency: string } { const tariffId = randomUUID(); const versionId = randomUUID(); const currency = opts.currency ?? "ALL"; const structure: TariffStructure = { gracePeriodEntryMin: opts.gracePeriodEntryMin ?? 10, incrementMin: opts.incrementMin ?? 60, blocks: [{ uptoMin: null, priceMinorPerIncrement: opts.pricePerIncrementMinor ?? 10000 }], dailyCapMinor: null, lostTicketMinor: 50000, gracePeriodExitMin: opts.gracePeriodExitMin ?? 15, overstay: "reprice", }; db.insert(tariffs).values({ id: tariffId, scope: "site", name: "Test" }).run(); db.insert(tariffVersions).values({ id: versionId, tariffId, effectiveFrom: opts.effectiveFrom ?? "2000-01-01T00:00:00.000Z", currency, structure: structure as unknown as Record, }).run(); return { tariffVersionId: versionId, currency }; } /** ISO string `minutes` ago from now (for entries that should already owe a fee). */ export function minutesAgo(minutes: number): string { return new Date(Date.now() - minutes * 60_000).toISOString(); } // --- HTTP integration scaffolding (route tests via app.inject) ----------------- /** Seed a user with a role. `admin` role grants every permission (ADMIN_PERMS); * any other role gets exactly the `permissions` listed. Returns the credentials. */ export async function seedUser( db: Db, opts: { username?: string; password?: string; roleId?: string; permissions?: Permission[] } = {}, ): Promise<{ username: string; password: string; roleId: string }> { const username = opts.username ?? "tester"; const password = opts.password ?? "test-password-123"; const roleId = opts.roleId ?? "admin"; if (roleId !== "admin") { db.insert(roles).values({ id: roleId, name: roleId, builtin: 0 }).onConflictDoNothing().run(); for (const p of opts.permissions ?? []) { db.insert(rolePermissions).values({ roleId, permission: p }).onConflictDoNothing().run(); } } else { // The admin role row must exist for the FK; ADMIN_PERMS is resolved in code. db.insert(roles).values({ id: "admin", name: "admin", builtin: 1 }).onConflictDoNothing().run(); } db.insert(users).values({ id: randomUUID(), username, passwordHash: await bcrypt.hash(password, 10), roleId, }).run(); return { username, password, roleId }; } /** Log in via the real auth route and return the cookie header + CSRF token to * replay on subsequent requests (mutations need both the cookie and the header). */ export async function login( app: FastifyInstance, username: string, password: string, ): Promise<{ cookie: string; csrf: string }> { const res = await app.inject({ method: "POST", url: "/api/auth/login", payload: { username, password } }); if (res.statusCode !== 200) throw new Error(`login failed: ${res.statusCode} ${res.body}`); const setCookies = res.cookies; const cookie = setCookies.map((c) => `${c.name}=${c.value}`).join("; "); const csrf = setCookies.find((c) => c.name === "parking_csrf")?.value ?? ""; return { cookie, csrf }; }