/** * Vitest globalSetup — provisions and migrates familysync_test for local runs. * * Runs ONCE in the main vitest process before any test file. * Under CI (process.env.CI truthy), returns immediately — CI provisions its own * `familysync` service DB and must not be touched. * * Local flow: * 1. Root-connects → CREATE DATABASE IF NOT EXISTS familysync_test * 2. GRANT ALL PRIVILEGES ON familysync_test.* to the app user * 3. App-user connects → drizzle migrate() applies committed SQL migrations * * Security: * - Root password read from process.env.DB_ROOT_PASSWORD (default 'root' matches dev compose only) * - App user validated against /^[A-Za-z0-9_]+$/ before GRANT interpolation (T-ndv-03) * - This file never runs in CI (T-ndv-04) */ import mysql from 'mysql2/promise'; import { drizzle } from 'drizzle-orm/mysql2'; import { migrate } from 'drizzle-orm/mysql2/migrator'; import { fileURLToPath } from 'node:url'; const TEST_DB = 'familysync_test'; export default async function setup(): Promise { const isCI = !!process.env.CI; if (isCI) { // CI provisions its own database via the mariadb service + db:migrate step. // Do nothing here so the CI flow is completely unaffected (T-ndv-04). return; } // ── Connection params (dev defaults match docker-compose.dev.yml) ────────── const host = process.env.DB_HOST ?? '127.0.0.1'; const port = Number(process.env.DB_PORT ?? 3306); const appUser = process.env.DB_USER ?? 'familysync'; const appPassword = process.env.DB_PASSWORD ?? ''; // ── Root creds — read from env; dev default 'root' only for convenience ──── const rootUser = process.env.DB_ROOT_USER ?? 'root'; const rootPassword = process.env.DB_ROOT_PASSWORD ?? 'root'; // ── Security: validate appUser before interpolating into GRANT ────────────── if (!/^[A-Za-z0-9_]+$/.test(appUser)) { throw new Error( `[global-setup] DB_USER '${appUser}' contains characters not allowed in a GRANT statement. Aborting.`, ); } // ── Step 1–3: Root connection — CREATE DATABASE + GRANT ──────────────────── const rootConn = await mysql.createConnection({ host, port, user: rootUser, password: rootPassword, // No database selected — we are creating one }); try { await rootConn.query(`CREATE DATABASE IF NOT EXISTS \`${TEST_DB}\``); // GRANT does not accept parameterised identifiers; user is validated above (T-ndv-03). // Try @'%' first (Docker/network access); fall back to @'localhost' if the user was // created with a localhost host qualifier. try { await rootConn.query( `GRANT ALL PRIVILEGES ON \`${TEST_DB}\`.* TO '${appUser}'@'%'`, ); } catch { // If @'%' fails (user only exists as @'localhost'), try that form. await rootConn.query( `GRANT ALL PRIVILEGES ON \`${TEST_DB}\`.* TO '${appUser}'@'localhost'`, ); } await rootConn.query('FLUSH PRIVILEGES'); } finally { await rootConn.end(); } // ── Step 4: Apply committed migrations to familysync_test ────────────────── const appConn = await mysql.createConnection({ host, port, user: appUser, password: appPassword, database: TEST_DB, multipleStatements: true, // required by drizzle migrator for multi-statement SQL files }); try { const db = drizzle({ client: appConn, mode: 'default' }); const migrationsFolder = fileURLToPath( new URL('../src/db/migrations', import.meta.url), ); await migrate(db, { migrationsFolder }); } finally { await appConn.end(); } console.log('[global-setup] provisioned + migrated familysync_test'); }