/** * Recurrence gate for celilo#1074. * * `db/client.ts` runs `PRAGMA foreign_keys = ON` before handing back the real * database. The test helpers did not, and SQLite defaults the pragma OFF per * connection, so every one of the schema's `onDelete: 'cascade'` declarations * was enforced in production and inert in the suite. * * The direction is what made it invisible. An unenforced cascade can only ever * make a test pass that should have failed, so nothing has ever gone red over * it and nothing ever could. `consumer-cleanup.ts`'s whole design reasons about * what the cascade removes and when, and no test could observe any of it. * * Two assertions, because the pragma buys two different things: rows go away * when their parent does, and a row referencing an absent parent is refused. * The second is what stops a structurally impossible fixture from reading as * valid. */ import { afterEach, beforeEach, describe, expect, test } from 'bun:test'; import { mkdtemp, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { cleanupTestDatabase, setupTestDatabase, setupTestDatabaseAt, setupTestDatabaseFile, } from '../test-utils/database'; import type { DbClient } from './client'; describe('test databases enforce foreign keys', () => { let db: DbClient; beforeEach(async () => { db = await setupTestDatabase(); }); afterEach(async () => { await cleanupTestDatabase(db); }); test('deleting a module takes its dependent rows with it', () => { db.$client.run( `INSERT INTO modules (id, name, version, source_path, manifest_data) VALUES ('caddy-consumer', 'consumer', '1.0.0', '/tmp/c', '{}')`, ); db.$client.run( `INSERT INTO web_routes (slug, module_id, type, path, hostname) VALUES ('s', 'caddy-consumer', 'reverse_proxy', '/', 'example.com')`, ); db.$client.run(`DELETE FROM modules WHERE id = 'caddy-consumer'`); const rows = db.$client .query(`SELECT id FROM web_routes WHERE module_id = 'caddy-consumer'`) .all(); expect(rows).toHaveLength(0); }); test('a row referencing a module that does not exist is refused', () => { expect(() => db.$client.run( `INSERT INTO capabilities (module_id, capability_name, version, data) VALUES ('nonexistent', 'public_web', '1.0.0', '{}')`, ), ).toThrow(); }); }); /** * Every way to get a test database, checked directly. * * `test-utils/database.ts` is now the only module that hands one out — the * second one collided with it on two exported names and disagreed with it on * this pragma, which is why the gap was invisible from any call site. A helper * added here later must be added to this list; there is no longer another module * for it to hide in. */ describe('every test-database helper enforces foreign keys', () => { const enforced = (client: DbClient): boolean => (client.$client.query('PRAGMA foreign_keys').get() as { foreign_keys: number }).foreign_keys === 1; test('setupTestDatabase (in memory)', async () => { const memory = await setupTestDatabase(); expect(enforced(memory)).toBe(true); await cleanupTestDatabase(memory); }); test('setupTestDatabaseFile (temp directory it owns)', async () => { const { db: file, cleanup } = await setupTestDatabaseFile(); expect(enforced(file)).toBe(true); await cleanup(); }); test('setupTestDatabaseAt (path the caller chooses)', async () => { const dir = await mkdtemp(join(tmpdir(), 'celilo-fk-')); const at = await setupTestDatabaseAt(join(dir, 'celilo.db')); expect(enforced(at)).toBe(true); at.$client.close(); await rm(dir, { recursive: true, force: true }); }); });