From f39bd308b29dd514fdb80fc5e3f288500a9bab07 Mon Sep 17 00:00:00 2001 From: Lucas Berger Date: Sat, 13 Jun 2026 17:02:50 -0400 Subject: [PATCH] chore(quick-260613-ndv-02): clean-slate comment in setup.ts + README local-test docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Update apps/api/test/setup.ts header: clarify tests run against familysync_test (provisioned by global-setup.ts), document users-cleanup decision (intact across tests), and note CI-vs-local env difference - Add apps/api/README.md "Running API tests locally" section: documents the test DB isolation, run command, DB_ROOT_PASSWORD requirement, and CI no-op behaviour - Fix apps/api/test/global-setup.ts: switch from drizzle({ client, mode }) to drizzle(pool, { mode }) — drizzle-orm@0.45.2 isConfig() has a tautological OR in the `mode` branch that always returns false, causing the combined-config form to pass the config object as the client (client.query is not a function); two-arg form routes correctly; 244/244 tests pass against familysync_test --- apps/api/README.md | 20 ++++++++++++++++++++ apps/api/test/global-setup.ts | 16 +++++++++++++--- apps/api/test/setup.ts | 27 +++++++++++++++++---------- 3 files changed, 50 insertions(+), 13 deletions(-) diff --git a/apps/api/README.md b/apps/api/README.md index 6ac07dd..18aa160 100644 --- a/apps/api/README.md +++ b/apps/api/README.md @@ -144,6 +144,26 @@ pnpm --filter @familysync/api typecheck Integration tests that hit MariaDB require a running dev DB with `DB_HOST=127.0.0.1` and credentials from your `.env`. See [../../docs/TESTING.md](../../docs/TESTING.md) for the full setup. +## Running API tests locally + +Local test runs use a dedicated `familysync_test` database so the dev `familysync` database is never mutated. `test/global-setup.ts` creates and migrates `familysync_test` automatically on the first run. + +**Prerequisites:** + +- Dev MariaDB running and port-bound (`127.0.0.1:3306`) — start with `docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d mariadb` +- `.env` sourced in your shell (provides `DB_PASSWORD`, `DB_ROOT_PASSWORD`, and other credentials) + +**Run command:** + +```bash +set -a; source .env; set +a +DB_HOST=127.0.0.1 pnpm --filter @familysync/api test +``` + +`DB_ROOT_PASSWORD` must be set in `.env` for the one-time `CREATE DATABASE` / `GRANT` that provisions `familysync_test`. Subsequent runs skip the provisioning step if the database already exists (`CREATE DATABASE IF NOT EXISTS`). + +**CI is unaffected.** `test/global-setup.ts` returns immediately when `CI` is set (the CI `api` job provisions its own `familysync` service DB and runs `db:migrate` before the test step). The `test.env` DB override in `vitest.config.ts` is also a no-op under CI. + ## Further reading - [Architecture](../../docs/ARCHITECTURE.md) — system overview and component diagram diff --git a/apps/api/test/global-setup.ts b/apps/api/test/global-setup.ts index d6ffd01..ec67000 100644 --- a/apps/api/test/global-setup.ts +++ b/apps/api/test/global-setup.ts @@ -80,23 +80,33 @@ export default async function setup(): Promise { } // ── Step 4: Apply committed migrations to familysync_test ────────────────── - const appConn = await mysql.createConnection({ + // Use a Pool (not a Connection) — drizzle-orm/mysql2 session.all() calls + // client.execute() and transaction() calls client.getConnection(), both of + // which are pool methods. createPool with connectionLimit:1 is the minimal form. + const appPool = mysql.createPool({ host, port, user: appUser, password: appPassword, database: TEST_DB, + connectionLimit: 1, multipleStatements: true, // required by drizzle migrator for multi-statement SQL files }); try { - const db = drizzle({ client: appConn, mode: 'default' }); + // Pass pool as the first arg + config as the second. Do NOT use the + // { client: pool, mode } combined-config form — drizzle-orm@0.45.2 has a + // bug in isConfig() where the `mode` branch always returns false (its OR + // condition is a tautology), so the combined form falls through to + // construct({ client, mode }, undefined) and the session client becomes + // the plain config object (no .query()). The two-arg form is safe. + const db = drizzle(appPool, { mode: 'default' }); const migrationsFolder = fileURLToPath( new URL('../src/db/migrations', import.meta.url), ); await migrate(db, { migrationsFolder }); } finally { - await appConn.end(); + await appPool.end(); } console.log('[global-setup] provisioned + migrated familysync_test'); diff --git a/apps/api/test/setup.ts b/apps/api/test/setup.ts index 2787a5d..f6af415 100644 --- a/apps/api/test/setup.ts +++ b/apps/api/test/setup.ts @@ -1,18 +1,25 @@ /** - * Vitest global test setup for apps/api. + * Vitest per-file test setup for apps/api. * - * Establishes shared test infrastructure for API tests: - * - DB pool access via the existing client.ts (DB_HOST/DB_NAME from env) - * - Per-test cleanup for list tables (truncate between tests so state is isolated) + * Tests now run against the isolated `familysync_test` database, which is + * auto-provisioned and migrated by `test/global-setup.ts` before the suite + * starts. The dev `familysync` database is never touched by a local test run. * - * Usage: - * This file is referenced in vitest.config.ts via test.setupFiles. - * Pure-logic tests (listEmitter, fractional rank) do NOT require DB — the - * cleanup function is a no-op when the tables are empty. + * Cleanup strategy: + * - afterEach truncates list/push tables in FK-safe order so each test + * starts with a clean slate for those tables. + * - `users` is intentionally left intact across tests within a single run. + * Many tests seed user id=1 once and reuse it; deleting users between tests + * would break FK-dependent rows mid-suite. The globalSetup provides a fresh + * migrated `familysync_test` at run start, so `users` starts empty and any + * seed inserted by the first test that needs it persists for the session. + * If a specific test leaks `users` rows that affect another test, scope a + * targeted delete inside that test's own beforeEach/afterEach instead. * * Environment: - * Set DB_HOST, DB_USER, DB_PASSWORD, DB_NAME in the test environment. - * Tests run against a local MariaDB dev database; production data is never touched. + * DB_HOST, DB_USER, DB_PASSWORD, and DB_NAME=familysync_test are injected by + * vitest.config.ts (test.env) for local runs. Under CI, DB_NAME=familysync is + * preserved from the job-level env and globalSetup is a no-op. */ import { afterEach } from 'vitest';