import { readdirSync, readFileSync } from 'node:fs'; import { join } from 'node:path'; import { describe, expect, it } from 'vitest'; /** * Prüft die hand-editierten SQL-Ergänzungen in den beiden Phase-15- * Migrationen (Plan 15-01, Task 1 + Task 3), ohne eine Datenbank zu * brauchen — reiner Textabgleich der generierten migration.sql-Dateien. * Stil folgt dem Repo-Muster hand-editierter Migrationen * (20260618112133_rls_policies, 20260721150000_tender_cpv_divisions_backfill). */ 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('add_groups_and_module_grants migration.sql (D-04, D-06, D-13)', () => { const sql = readMigrationSql('_add_groups_and_module_grants'); it('erzwingt genau eine Standardgruppe pro Mandant (D-13, partieller Unique-Index)', () => { expect(sql).toContain('Group_one_default_per_tenant'); expect(sql).toContain('WHERE "isDefault" = true'); }); it('erzwingt die Entweder-oder-Beziehung Gruppe XOR Benutzer per CHECK-Constraint (D-04)', () => { expect(sql).toContain('ModuleGrant_group_xor_user'); expect(sql).toContain('num_nonnulls("groupId", "userId") = 1'); }); it('schützt beide ModuleGrant-Varianten (Gruppe/Benutzer) je Modul über partielle Unique-Indizes', () => { expect(sql).toContain('ModuleGrant_tenant_module_group_unique'); expect(sql).toContain('ModuleGrant_tenant_module_user_unique'); }); it('D-06-Backfill: alle drei INSERT-Statements verwenden gen_random_uuid() für neue IDs', () => { const occurrences = sql.match(/gen_random_uuid\(\)/g) ?? []; expect(occurrences.length).toBe(3); }); it('D-06-Backfill: alle drei INSERT-Statements tragen einen NOT-EXISTS-Wächter (idempotent bei Wiederholungslauf)', () => { const occurrences = sql.match(/NOT EXISTS/g) ?? []; expect(occurrences.length).toBe(3); }); it('D-06-Backfill läuft in der Reihenfolge Group vor GroupMembership vor ModuleGrant', () => { const groupIdx = sql.indexOf('INSERT INTO "Group"'); const membershipIdx = sql.indexOf('INSERT INTO "GroupMembership"'); const grantIdx = sql.indexOf('INSERT INTO "ModuleGrant"'); expect(groupIdx).toBeGreaterThan(-1); expect(membershipIdx).toBeGreaterThan(-1); expect(grantIdx).toBeGreaterThan(-1); expect(groupIdx).toBeLessThan(membershipIdx); expect(membershipIdx).toBeLessThan(grantIdx); }); });