feat(15-01): Group/GroupMembership/ModuleGrant schema + D-06 backfill migration
- 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
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user