import { readFileSync, readdirSync } from "fs"; import path from "path"; import { sql } from "drizzle-orm"; import { afterAll, beforeEach, vi } from "vitest"; import * as schema from "@/lib/db/schema"; /** * Integration database wiring (Drizzle). * * - If TEST_DATABASE_URL is set, the real `@/lib/db` singleton is used unchanged, * pointed at that Postgres. Migrations are applied once by the global setup; * files run serially and truncate between tests. * * - Otherwise, `@/lib/db` is mocked with a Drizzle client backed by an in-process * PGlite database (Postgres compiled to WASM) — real Postgres semantics, fully * isolated per worker, no external server. Production code is never modified. */ const realDbUrl = process.env.TEST_DATABASE_URL?.trim(); if (realDbUrl) { process.env.DATABASE_URL = realDbUrl; } vi.mock("@/lib/db", async () => { if (process.env.TEST_DATABASE_URL?.trim()) { return await vi.importActual("@/lib/db"); } const { PGlite } = await import("@electric-sql/pglite"); const { drizzle } = await import("drizzle-orm/pglite"); const client = new PGlite(); const migrationsDir = path.resolve(process.cwd(), "lib", "db", "migrations"); const files = readdirSync(migrationsDir) .filter((entry) => entry.endsWith(".sql")) .sort(); for (const file of files) { await client.exec(readFileSync(path.join(migrationsDir, file), "utf8")); } const db = drizzle(client, { schema }); return { db, schema }; }); const { db } = await import("@/lib/db"); export { db }; // Truncated in dependency order (children first) between every test for isolation. const TABLES = [ "media_usage", "media_asset", "portfolio_asset", "portfolio_section", "portfolio_project", "category", "app_config", ]; export async function resetDb() { const list = TABLES.map((table) => `"${table}"`).join(", "); await db.execute(sql.raw(`TRUNCATE TABLE ${list} RESTART IDENTITY CASCADE;`)); } beforeEach(async () => { await resetDb(); }); afterAll(async () => { // PGlite is in-process and torn down with the worker; the real postgres.js // client is a shared singleton and is left open on purpose. });