import {execFile} from 'child_process'; import {promisify} from 'util'; import {createRequire} from 'module'; const run = promisify(execFile); const require = createRequire(import.meta.url); /** * vitest globalSetup for the admin integration tests: starts a throwaway * MariaDB before the suite and removes it afterwards, so a run leaves nothing * behind and never touches a shared database. * * The container is started directly rather than through compose, because * `podman compose` needs a separate compose provider that neither podman nor * docker ships. One container needs no orchestration, and this works with * whichever of the two runtimes is installed. */ export const CONTAINER_NAME = 'nachklang-admin-test-db'; export const TEST_DB_PORT = 3307; const IMAGE = 'docker.io/library/mariadb:11'; const runtime = async (): Promise => { for (const candidate of ['docker', 'podman']) { try { await run(candidate, ['info'], {timeout: 60_000}); return candidate; } catch { // Not installed, or its daemon/machine is not running - try the next. } } throw new Error( 'The admin integration tests need a container runtime. Install docker or podman ' + '(with podman: `podman machine start`), then re-run npm run test:integration.' ); }; /** * Ready means "the entrypoint has applied 001_init.sql", not just "the port * answers": MariaDB accepts connections while it is still running its init * scripts, and a test that started then would fail on a missing table. */ const waitForSchema = async (): Promise => { const mysql = require('mysql2/promise'); const deadline = Date.now() + 120_000; let lastError: unknown; while (Date.now() < deadline) { try { const connection = await mysql.createConnection({ host: '127.0.0.1', port: TEST_DB_PORT, user: 'nachklang', password: 'testpassword', database: 'nachklang_admin', connectTimeout: 5_000 }); const [rows] = await connection.query( "SELECT COUNT(*) AS n FROM information_schema.tables " + "WHERE table_schema = 'nachklang_admin' AND table_name IN ('user', 'user_app_permissions', 'invitations')" ); await connection.end(); if (Number((rows as any[])[0]?.n) === 3) { return; } lastError = new Error('schema not applied yet'); } catch (e) { lastError = e; } await new Promise(resolve => setTimeout(resolve, 1_000)); } throw new Error(`Test database never became ready: ${(lastError as any)?.message}`); }; export const setup = async () => { const engine = await runtime(); // A container left behind by an interrupted run would still hold the old // schema and rows, so always start from scratch. await run(engine, ['rm', '-f', CONTAINER_NAME], {timeout: 60_000}).catch(() => undefined); await run(engine, [ 'run', '-d', '--name', CONTAINER_NAME, '-e', 'MARIADB_ROOT_PASSWORD=roottestpassword', '-e', 'MARIADB_DATABASE=nachklang_admin', '-e', 'MARIADB_USER=nachklang', '-e', 'MARIADB_PASSWORD=testpassword', '-p', `${TEST_DB_PORT}:3306`, // The very migration production runs, applied by the entrypoint on first // boot - so a mistake in it fails the test run rather than the deploy. '-v', `${process.cwd()}/sql/admin/001_init.sql:/docker-entrypoint-initdb.d/001_init.sql:ro`, // Data lives in the container layer and dies with it. IMAGE ], {timeout: 300_000}); await waitForSchema(); }; export const teardown = async () => { const engine = await runtime().catch(() => null); if (engine) { await run(engine, ['rm', '-f', CONTAINER_NAME], {timeout: 60_000}).catch(() => undefined); } };