// Creates a throwaway Postgres database per test-file run against the // docker-compose `db` service, and drops it afterwards. Mirrors the old // sqlite temp-file-per-run isolation, adapted to Postgres (CREATE/DROP // DATABASE instead of a throwaway file). // // Creating/dropping the throwaway database itself stays raw `pg` (test // infra, not the app's data-access layer) — but applying the schema to the // fresh database goes through `prisma migrate deploy`, and the actual // per-file PrismaClient used by both test bodies and app code (route // handlers etc., all of which import `prisma` from "@/lib/prisma") is a // throwaway PrismaClient instance created here, wired up explicitly. // // Root-cause note (2026-08-04): this used to work by mutating the global // `process.env.DATABASE_URL` in a `beforeAll` and relying on the app's // `src/lib/prisma.ts` singleton to pick that up. That's unsafe the moment // two test files' setup/query timing can interleave within the same // worker process — `process.env` is shared across all test files that // happen to run in the same worker (Vitest's `test.isolate` resets each // file's *module registry*, not `process.env`), so one file's mutation can // leak into another's PrismaClient construction/reconnect. That forced // `fileParallelism: false` as a blunt workaround (see git blame on // vitest.config.ts), and even that stopped being sufficient once the // worker pool got reused across enough sequential files (12 test files — // see git history around this comment for the concrete FK-violation // failures that motivated this rewrite). // // Fix: never touch `process.env.DATABASE_URL` at all. Instead, `vi.mock` // "@/lib/prisma" (the same module id every test file and every app module // imports `prisma` from) with a factory that creates this file's own // throwaway database and returns a dedicated `PrismaClient` pointed at it // via the explicit `datasourceUrl` constructor option (Prisma 5.2+) — // never via the ambient env var. `vi.mock` factories are cached per // isolated test-file module registry, so every import of "@/lib/prisma" // within a given test file (the test file itself, and any route handler / // lib module it pulls in) resolves to that one file-scoped client, while // two different test files never share any mutable global state. That // makes `fileParallelism: true` (the default, restored in // vitest.config.ts) safe again. import { randomUUID } from "node:crypto"; import { execSync } from "node:child_process"; import { Client } from "pg"; import { PrismaClient } from "@prisma/client"; import { afterAll, vi } from "vitest"; const baseUrl = new URL( process.env.DATABASE_URL || "postgres://asset_license_mgmt:change-me-local-dev@db:5432/asset_license_mgmt" ); const testDbName = `test_${randomUUID().replace(/-/g, "_")}`; function adminUrl(): string { const url = new URL(baseUrl.toString()); url.pathname = "/postgres"; return url.toString(); } function testDbUrl(): string { const url = new URL(baseUrl.toString()); url.pathname = `/${testDbName}`; return url.toString(); } async function withAdminClient(fn: (client: Client) => Promise): Promise { const client = new Client({ connectionString: adminUrl() }); await client.connect(); try { return await fn(client); } finally { await client.end(); } } // Set by the "@/lib/prisma" mock factory below once it has created this // file's throwaway database and client, so afterAll can tear both down. // Populated synchronously-in-effect before any test body can run, since // the mock factory is awaited as part of resolving the first import of // "@/lib/prisma" (which every test file and every route/lib module under // test does at the top of its own module graph, ahead of any test body). let testPrisma: PrismaClient | undefined; vi.mock("@/lib/prisma", async () => { await withAdminClient((client) => client.query(`CREATE DATABASE ${testDbName}`)); // Apply the Prisma migration history to the fresh throwaway database. // `migrate deploy` replays the same real migrations production uses // (prisma/migrations), rather than a schema-diffing shortcut like // `db push` — keeping this test setup honest about what actually runs // outside tests too. Pass the target database explicitly via the // subprocess's own `env` rather than mutating this process's // `process.env` — the whole point of this rewrite is to avoid any // shared mutable global between test files. execSync("npx prisma migrate deploy", { stdio: "inherit", env: { ...process.env, DATABASE_URL: testDbUrl() }, }); testPrisma = new PrismaClient({ datasourceUrl: testDbUrl() }); return { prisma: testPrisma }; }); afterAll(async () => { await testPrisma?.$disconnect(); await withAdminClient((client) => client.query(`DROP DATABASE IF EXISTS ${testDbName} WITH (FORCE)`) ); });