c5c704bae9
- Group/GroupMembership/ModuleGrant models plus MembershipSource enum (D-05), placed under TenantModuleActivation with German block comment - Hand-SQL appended to the generated migration: partial unique index for one default group per tenant (D-13), CHECK num_nonnulls xor-constraint plus two partial unique indexes for ModuleGrant (D-04), and the D-06 backfill (Group -> GroupMembership -> ModuleGrant, each INSERT guarded by WHERE NOT EXISTS for idempotent re-runs on `prisma migrate deploy`) - apps/api/src/groups/migration-sql.spec.ts verifies the hand-SQL by reading migration.sql directly, no DB required - Verified against the local DB: default-group count matches tenant count, membership/grant counts match existing users/active activations, and the XOR constraint rejects a group+user-less insert
69 lines
2.8 KiB
TypeScript
69 lines
2.8 KiB
TypeScript
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);
|
|
});
|
|
});
|