diff --git a/scripts/run-tests.ts b/scripts/run-tests.ts index e2f6487..b3d2bc7 100644 --- a/scripts/run-tests.ts +++ b/scripts/run-tests.ts @@ -8,6 +8,7 @@ * Lauf: npm run test (alle) * npm run test -- mail tenant (nur Tests, deren Name einen der Filter enthält) */ +import "dotenv/config"; import { spawnSync } from "node:child_process"; import { readdirSync } from "node:fs"; import { join, dirname } from "node:path"; @@ -26,6 +27,15 @@ if (tests.length === 0) { process.exit(1); } +// RLS runs (RLS_ENFORCED=true): db.ts refuses to start without RLS_DATABASE_URL. Default it to the +// same database as DATABASE_URL with the restricted role (as scripts/test-rls-enforcement.ts does). +if (process.env.RLS_ENFORCED === "true" && !process.env.RLS_DATABASE_URL && process.env.DATABASE_URL) { + const url = new URL(process.env.DATABASE_URL); + url.username = "craftvia_app"; + url.password = "craftvia_app_local"; + process.env.RLS_DATABASE_URL = url.toString(); +} + const results: { name: string; ok: boolean; ms: number }[] = []; for (const file of tests) { const started = Date.now(); diff --git a/scripts/test-tenant-isolation.ts b/scripts/test-tenant-isolation.ts index 4db9194..4cc30dc 100644 --- a/scripts/test-tenant-isolation.ts +++ b/scripts/test-tenant-isolation.ts @@ -29,6 +29,21 @@ async function expectThrow(fn: () => Promise, msg: string) { } } +/** + * Compound keys with a foreign tenantId: in owner mode the tenant guard throws (fail-closed); + * with RLS_ENFORCED=true the database already hides the row, so the query returns null. + * Both outcomes mean "no data leak" — anything else fails. + */ +async function expectThrowOrNullUnderRls(fn: () => Promise, msg: string) { + if (process.env.RLS_ENFORCED !== "true") return expectThrow(fn, msg); + try { + const r = await fn(); + ok(r === null, `${msg} (RLS: null)${r === null ? "" : ` — statt null: ${JSON.stringify(r)}`}`); + } catch { + ok(true, `${msg} (RLS: Throw)`); + } +} + /** Erwartet, dass `fn` `null` liefert (kein Datensatz, kein Abfluss). */ async function expectNull(fn: () => Promise, msg: string) { const r = await fn(); @@ -98,7 +113,7 @@ async function main() { ); // (6) Compound-Unique-Key (tenantId_key) mit fremdem tenantId, select ohne tenantId. - await expectThrow( + await expectThrowOrNullUnderRls( () => dbA.role.findUnique({ where: { tenantId_key: { tenantId: tenantB.id, key: roleB.key } }, @@ -108,7 +123,7 @@ async function main() { ); // (7) Compound-Unique-Key mit fremdem tenantId, ohne select. - await expectThrow( + await expectThrowOrNullUnderRls( () => dbA.role.findUnique({ where: { tenantId_key: { tenantId: tenantB.id, key: roleB.key } },