import { readdirSync, readFileSync } from 'node:fs'; import { join } from 'node:path'; import { describe, expect, it } from 'vitest'; /** * Prueft die neue Rollen-Migration (Task 1, WINDOWS #18) rein textuell gegen * die generierte migration.sql — keine Datenbank noetig. Baut die * Hilfsfunktion aus apps/api/src/groups/migration-sql.spec.ts bewusst nach, * statt sie zu importieren: die vorhandene ist in ihrer Datei privat, und * eine Kopie von zwoelf Zeilen ist billiger als eine neue Abhaengigkeit * zwischen zwei Testdateien. */ const MIGRATIONS_DIR = join(__dirname, '../../prisma/migrations'); function readMigrationSql(suffix: string): string { const dirs = readdirSync(MIGRATIONS_DIR, { withFileTypes: true }) .filter((entry) => entry.isDirectory() && entry.name.endsWith(suffix)) .map((entry) => entry.name); if (dirs.length !== 1) { throw new Error( `Expected exactly one migration directory ending in "${suffix}", found ${dirs.length}: ${dirs.join(', ')}`, ); } return readFileSync(join(MIGRATIONS_DIR, dirs[0], 'migration.sql'), 'utf-8'); } describe('rls_app_role migration.sql (WINDOWS #18, T-DGJ-01/T-DGJ-04/T-DGJ-05)', () => { const sql = readMigrationSql('_rls_app_role'); it('nennt die Rolle tessera_app', () => { expect(sql).toContain('tessera_app'); }); it('entzieht ausdruecklich beide Umgehungswege (NOSUPERUSER, NOBYPASSRLS) — BYPASSRLS kommt nie ohne vorangestelltes NO vor', () => { expect(sql).toContain('NOSUPERUSER'); expect(sql).toContain('NOBYPASSRLS'); const bypassOccurrences = sql.match(/BYPASSRLS/g) ?? []; for (const _ of bypassOccurrences) { // jedes Vorkommen von BYPASSRLS muss Teil von NOBYPASSRLS sein } const bareBypassrls = sql.match(/(? { expect(sql).toContain('pg_roles'); expect(sql).toContain('DO $$'); expect(sql).toMatch(/IF NOT EXISTS/); }); it('bildet den Datenbanknamen dynamisch ueber current_database(), nicht fest verdrahtet', () => { expect(sql).toContain('current_database()'); }); it('setzt die Vorgaberechte fuer current_user, nicht fuer einen fest verdrahteten Rollennamen', () => { expect(sql).toContain('FOR ROLE'); expect(sql).toContain('current_user'); }); it('enthaelt kein Kennwort — kein PASSWORD gefolgt von einem Hochkomma', () => { expect(sql).not.toMatch(/PASSWORD\s*'/); }); it('erteilt alle vier Datenzugriffsarten (SELECT, INSERT, UPDATE, DELETE)', () => { for (const verb of ['SELECT', 'INSERT', 'UPDATE', 'DELETE']) { expect(sql).toContain(verb); } expect(sql).toMatch(/GRANT[^;]*SELECT[^;]*INSERT[^;]*UPDATE[^;]*DELETE/); }); });