test: prove module database access with PostgreSQL

Add a real installed-module fixture (core/tests/fixtures/module-database-integration/db-facade)
with its own manifest, Drizzle schema, drizzle-kit-generated migration, and permissioned HTTP
routes, then exercise it end to end in a new apps/api PostgreSQL integration suite: explicit
migration application before the module runtime starts (never auto-created), start/stop/HTTP
handlers sharing one ModuleDatabase facade by reference, authorized read/mutation/transaction
commit and rollback against real PostgreSQL, denied/rejected-origin/OPTIONS/HEAD requests never
reaching the module or its table, and both a module-caught and an uncaught real database failure
sanitizing exactly as pass 1 established. Registered alongside the existing suites in
vitest.migrations.integration.config.ts; no production code, schema, or migration changed.
This commit is contained in:
ekklesia
2026-07-25 21:26:15 -05:00
parent 0e308eb9ad
commit d1b9efd4d2
10 changed files with 1044 additions and 0 deletions
@@ -0,0 +1,211 @@
import { eq, sql } from "drizzle-orm";
import { records } from "../schema/index.js";
// A fixed (not per-run-random) globalThis symbol key: this fixture is a static, committed file
// (not a per-test generated template string), so it cannot bake a unique key into itself the way
// every other dynamically-written fixture in this codebase does. A fixed key is safe here because
// the PostgreSQL integration suite never runs its files in parallel (`fileParallelism: false`,
// `vitest.migrations.integration.config.ts`), so there is never more than one active recorder at a
// time. The test file resets this array immediately before each scenario and deletes the property
// during cleanup.
const RECORDER_KEY = "ekklesia.integration.db-facade.recorder";
// A fixed row id `start()` writes and `stop()` later updates, so both the integration test and the
// module's own HTTP handlers can address the exact same row deterministically, without needing a
// side channel to learn a generated id.
export const STARTUP_RECORD_ID = "11111111-1111-1111-1111-111111111111";
function recordEvent(event) {
const globalValues = globalThis;
if (!Array.isArray(globalValues[RECORDER_KEY])) {
globalValues[RECORDER_KEY] = [];
}
globalValues[RECORDER_KEY].push(event);
}
export default {
id: "db-facade",
version: "0.1.0",
register({ http }) {
// Registration is declarative and database-free: there is no `services` field on this
// context at all (see `ModuleRegistrationContext`), so `context.services` reads as
// `undefined` here — recorded for the integration test to assert on directly.
recordEvent({ services: undefined, type: "register" });
http.route({
method: "GET",
path: "/records/:id",
permission: "db-facade.records.read",
async handler(_request, context) {
recordEvent({
contextKeys: Object.keys(context).sort(),
services: context.services,
type: "handler",
route: "GET /records/:id",
});
const [row] = await context.services.database
.select({ id: records.id, value: records.value })
.from(records)
.where(eq(records.id, context.params.id));
if (!row) {
return Response.json({ code: "RECORD_NOT_FOUND" }, { status: 404 });
}
return Response.json(row, { status: 200 });
},
});
http.route({
method: "GET",
path: "/records-count",
// Deliberately the "write" (authenticated) permission, not "read" (public) — this is the
// fixture's one authenticated-baseline GET route, used to prove HEAD cannot bypass
// authorization the way it could if HEAD skipped the same middleware chain GET uses.
permission: "db-facade.records.write",
async handler(_request, context) {
recordEvent({
contextKeys: Object.keys(context).sort(),
services: context.services,
type: "handler",
route: "GET /records-count",
});
const result = await context.services.database.execute(
sql`select count(*)::int as count from ${records}`,
);
const row = Array.isArray(result) ? result[0] : result.rows?.[0];
return Response.json({ count: row?.count ?? 0 }, { status: 200 });
},
});
http.route({
method: "POST",
path: "/records",
permission: "db-facade.records.write",
async handler(request, context) {
recordEvent({
contextKeys: Object.keys(context).sort(),
services: context.services,
type: "handler",
route: "POST /records",
});
const { id, value } = await request.json();
await context.services.database.insert(records).values({ id, value });
return Response.json({ id, value }, { status: 201 });
},
});
http.route({
method: "PATCH",
path: "/records/:id",
permission: "db-facade.records.write",
async handler(request, context) {
recordEvent({
contextKeys: Object.keys(context).sort(),
services: context.services,
type: "handler",
route: "PATCH /records/:id",
});
const { value } = await request.json();
const updated = await context.services.database
.update(records)
.set({ value })
.where(eq(records.id, context.params.id))
.returning({ id: records.id, value: records.value });
if (updated.length === 0) {
return Response.json({ code: "RECORD_NOT_FOUND" }, { status: 404 });
}
return Response.json(updated[0], { status: 200 });
},
});
http.route({
method: "POST",
path: "/records/transactional",
permission: "db-facade.records.write",
async handler(request, context) {
recordEvent({
contextKeys: Object.keys(context).sort(),
services: context.services,
type: "handler",
route: "POST /records/transactional",
});
const { id, value, conflictId } = await request.json();
try {
await context.services.database.transaction(async (tx) => {
await tx.insert(records).values({ id, value });
if (conflictId) {
// The same primary key inserted twice inside one transaction: PostgreSQL rejects
// the second insert with a unique-violation, and the whole transaction — including
// the first, otherwise-valid insert above — must roll back atomically.
await tx.insert(records).values({ id: conflictId, value: "conflict" });
await tx.insert(records).values({ id: conflictId, value: "conflict-again" });
}
});
return Response.json({ committed: true }, { status: 201 });
} catch {
return Response.json({ committed: false }, { status: 409 });
}
},
});
http.route({
method: "GET",
path: "/boom",
permission: "db-facade.records.read",
async handler(_request, context) {
recordEvent({
contextKeys: Object.keys(context).sort(),
services: context.services,
type: "handler",
route: "GET /boom",
});
// Deliberately uncaught: a real PostgreSQL error (division by zero) must escape this
// handler so the platform's own sanitization boundary is exercised end to end.
await context.services.database.execute(sql`select 1/0`);
return Response.json({ unreachable: true });
},
});
http.route({
method: "GET",
path: "/safe-boom",
permission: "db-facade.records.read",
async handler(_request, context) {
recordEvent({
contextKeys: Object.keys(context).sort(),
services: context.services,
type: "handler",
route: "GET /safe-boom",
});
try {
await context.services.database.execute(sql`select 1/0`);
return Response.json({ unreachable: true });
} catch {
// A module deliberately catching its own database failure and returning a valid,
// sanitized response of its own choosing — the platform must preserve this untouched.
return Response.json({ code: "BOOM_HANDLED" }, { status: 409 });
}
},
});
},
async start(context) {
recordEvent({ services: context.services, type: "start" });
// `onConflictDoNothing` makes repeated startups against an already-started database safe —
// a real module's own idempotency choice, not a platform requirement — so a test can start
// the same migrated database more than once without `start()` itself failing on the fixed
// `STARTUP_RECORD_ID` row a previous run already inserted.
await context.services.database
.insert(records)
.values({ id: STARTUP_RECORD_ID, value: "startup-value" })
.onConflictDoNothing();
},
async stop(context) {
recordEvent({ services: context.services, type: "stop" });
await context.services.database
.update(records)
.set({ value: "stopped-by-shutdown" })
.where(eq(records.id, STARTUP_RECORD_ID));
},
};