Files
familysync/apps/api/test/global-setup.ts
T
Lucas Berger 8453b9751e chore(quick-260613-ndv-01): add globalSetup for familysync_test isolation
- Create apps/api/test/global-setup.ts: root-provisions + grants + migrates
  familysync_test (local only); no-op when process.env.CI is truthy (T-ndv-04)
- Update apps/api/vitest.config.ts: wire globalSetup; add CI-gated test.env
  override (DB_NAME=familysync_test, DB_HOST) so workers never touch dev DB
2026-06-13 16:56:55 -04:00

104 lines
3.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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<void> {
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 13: 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');
}