Ein Server, mehrere Wehren. Ein Konto, mehrere Zugehoerigkeiten. Eine Kreisebene, die zusieht. Datenmodell und RLS trugen seit Phase 0 mehrere Wehren. Was fehlte, war alles darueber: bis hierher war die Personenakte zugleich das Anmeldekonto, und die gehoert genau einer Wehr. Wer in zweien gefuehrt wurde, brauchte zwei Passwoerter, und die Anmeldung per E-Mail war mehrdeutig. Neu sind `Benutzer` (das Konto) und `Mitgliedschaft` (die Zugehoerigkeit). Das Konto traegt bewusst keine Mandantenkennung und kann keine tragen - es ist der Angelpunkt, an dem ein Mensch zu mehreren Wehren gehoert. Geschuetzt wird es deshalb ueber Rechte statt ueber RLS, wie `Berechtigung` seit Phase 0. Was es in einer Wehr darf, entscheidet seine Mitgliedschaft. Die Sitzung behaelt ihre Form und entsteht nur anders - aus der aktiven Mitgliedschaft statt aus der Person. Von 414 Fundstellen in 61 Dateien musste dadurch keine angefasst werden. Die eine Ausnahme ist Absicht: `personId` ist nullbar geworden, weil eine Aufsichtssitzung keine Akte hat, und der Typechecker hat daraus genau die fuenf Stellen gemacht, an denen die Kreisaufsicht nichts verloren hat. Ihre Rechte werden gefiltert statt aufgezaehlt: alles, was `.lesen` heisst, minus Kasse, Audit-Log, KI-Protokoll und Personenexport. So bekommt sie neue Leserechte automatisch und neue Schreibrechte nie. Die Migration ist datenerhaltend und bricht ab, statt einen halben Zustand zu hinterlassen. Nebenbei behoben: `db:test-reset` liess die Nachbarwehr stehen, der Fehlversuch-Test sperrte das gemeinsame Testkonto, ein Testaufbau war nicht wiederholbar, und die Wartelogik der Bildschirmfotos konnte ausloesen, bevor eine Seite ueberhaupt geladen war. 581 Unit-Tests und 266 E2E-Tests gruen. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
151 lines
5.3 KiB
TypeScript
151 lines
5.3 KiB
TypeScript
import "dotenv/config";
|
|
import { spawnSync } from "node:child_process";
|
|
import { PrismaClient } from "@prisma/client";
|
|
|
|
/**
|
|
* Die Testwehr zurücksetzen.
|
|
*
|
|
* npm run db:test-reset
|
|
*
|
|
* Legt einen eigenen Mandanten für die E2E-Tests an — und setzt ihn bei jedem
|
|
* Aufruf auf den Ausgangsstand zurück.
|
|
*
|
|
* **Warum es das gibt.** Bis Phase 8 schrieben die Playwright-Tests in die
|
|
* Beispielwehr `ff-musterhausen`, also in genau den Bestand, den die Anleitung
|
|
* zeigt. Nach fünf Phasen standen dort 94 Atemschutz-Überwachungen statt
|
|
* einer, 49 Geräte statt 45 und Termine mit Namen wie „E2E-Absage-m1x8kq".
|
|
* Zweimal fielen Tests aus, weil ein gesuchter Eintrag von Seite 1 gerutscht
|
|
* war — und bei jedem Testlauf unterschieden sich alle 72 Bildschirmfotos.
|
|
*
|
|
* **Warum Löschen und nicht Aufräumen.** Ein Lauf, der abbricht, lässt beim
|
|
* Aufräumen einen Bestand zurück, den niemand erwartet. Ein vollständiger
|
|
* Neuaufbau ist der einzige Zustand, den man kennt — und er dauert
|
|
* Sekunden, weil der Seed ohnehin schon alles kann.
|
|
*
|
|
* **Die Beispielwehr wird nicht angefasst.** Weder gelesen noch geschrieben:
|
|
* gelöscht wird ausschließlich der Mandant mit der Testkennung.
|
|
*/
|
|
|
|
const SLUG = process.env.E2E_SLUG ?? "ff-teststelle";
|
|
const NAME = process.env.E2E_NAME ?? "Freiwillige Feuerwehr Teststelle";
|
|
const KURZNAME = process.env.E2E_KURZNAME ?? "FF Teststelle";
|
|
const EMAIL = process.env.E2E_ADMIN_EMAIL ?? "e2e@teststelle.de";
|
|
const PASSWORT = process.env.E2E_ADMIN_PASSWORT ?? "Feuerwehr2026";
|
|
|
|
/** Die Beispielwehr. Sie darf unter keinen Umständen getroffen werden. */
|
|
const GESCHUETZT = process.env.SEED_SLUG ?? "ff-musterhausen";
|
|
|
|
const prisma = new PrismaClient({
|
|
datasources: {
|
|
db: { url: process.env.DIRECT_DATABASE_URL ?? process.env.DATABASE_URL ?? "" },
|
|
},
|
|
});
|
|
|
|
function log(nachricht: string): void {
|
|
console.info(`[testdaten] ${nachricht}`);
|
|
}
|
|
|
|
async function main(): Promise<void> {
|
|
if (SLUG === GESCHUETZT) {
|
|
throw new Error(
|
|
`Die Testkennung "${SLUG}" ist dieselbe wie die der Beispielwehr. ` +
|
|
"Das würde den Bestand löschen, den die Anleitung zeigt — abgebrochen.",
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Auch die Nachbarwehr aus Phase 10 und das Konto der Kreisebene.
|
|
*
|
|
* Beide gehören zur Testwehr, liegen aber nicht in ihr: die Nachbarwehr ist
|
|
* eine eigene Organisation, das Konto gehört gar keiner. Blieben sie beim
|
|
* Zurücksetzen stehen, sähe der Seed die Nachbarwehr, hielte alles für
|
|
* erledigt und legte die Mitgliedschaft in der neuen Testwehr nie wieder an
|
|
* — der Wehrwähler zeigte danach nur noch die betreute Wehr.
|
|
*/
|
|
const NACHBAR_SLUG = `${SLUG}-nachbar`;
|
|
const KBI_EMAIL = process.env.SEED_KBI_EMAIL ?? `kbi@${SLUG}.de`;
|
|
|
|
const vorhanden = await prisma.organisation.findFirst({
|
|
where: { slug: { in: [SLUG, NACHBAR_SLUG] } },
|
|
select: { id: true, name: true },
|
|
});
|
|
|
|
if (vorhanden) {
|
|
log(`Entferne die bisherige Testwehr "${vorhanden.name}" ...`);
|
|
|
|
// Das Audit-Log ist append-only; für das Abräumen einer ganzen Wehr wird
|
|
// die Ausnahme gesetzt, die auch der Aufbewahrungslauf des Workers
|
|
// benutzt. Alles Übrige hängt am Fremdschlüssel und geht mit.
|
|
await prisma.$transaction(async (tx) => {
|
|
await tx.$executeRaw`SELECT set_config('app.audit_bereinigung', 'an', true)`;
|
|
|
|
// Die Trigger, die Protokollzeilen unveränderlich machen, stehen dem
|
|
// Löschen im Weg. Im Betrieb passiert das nie — hier wird eine ganze
|
|
// Wehr entfernt, und zwar eine, die es nur für Tests gibt.
|
|
for (const tabelle of [
|
|
"druckmessung",
|
|
"atemschutz_ereignis",
|
|
"ki_vorgang",
|
|
"kiosk_vorgang",
|
|
]) {
|
|
await tx.$executeRawUnsafe(`ALTER TABLE "${tabelle}" DISABLE TRIGGER USER`);
|
|
}
|
|
|
|
await tx.organisation.deleteMany({ where: { slug: { in: [SLUG, NACHBAR_SLUG] } } });
|
|
await tx.benutzer.deleteMany({ where: { email: KBI_EMAIL } });
|
|
|
|
for (const tabelle of [
|
|
"druckmessung",
|
|
"atemschutz_ereignis",
|
|
"ki_vorgang",
|
|
"kiosk_vorgang",
|
|
]) {
|
|
await tx.$executeRawUnsafe(`ALTER TABLE "${tabelle}" ENABLE TRIGGER USER`);
|
|
}
|
|
});
|
|
}
|
|
|
|
log(`Lege "${NAME}" neu an ...`);
|
|
|
|
// Der Seed kann das alles schon — er bekommt nur eine andere Kennung.
|
|
//
|
|
// Über die Shell aufgerufen, weil `npx` auf Windows eine Batchdatei ist:
|
|
// ohne `shell: true` findet spawnSync sie nicht und liefert stillschweigend
|
|
// den Status null statt einer Fehlermeldung.
|
|
const ergebnis = spawnSync("npx tsx prisma/seed.ts", {
|
|
stdio: "inherit",
|
|
shell: true,
|
|
env: {
|
|
...process.env,
|
|
SEED_SLUG: SLUG,
|
|
SEED_NAME: NAME,
|
|
SEED_KURZNAME: KURZNAME,
|
|
SEED_ADMIN_EMAIL: EMAIL,
|
|
SEED_ADMIN_PASSWORT: PASSWORT,
|
|
},
|
|
});
|
|
|
|
if (ergebnis.status !== 0) {
|
|
throw new Error(
|
|
`Der Seed ist mit Code ${ergebnis.status ?? "unbekannt"} abgebrochen.` +
|
|
(ergebnis.error ? ` (${ergebnis.error.message})` : ""),
|
|
);
|
|
}
|
|
|
|
log("");
|
|
log("Testwehr steht. Die E2E-Tests melden sich damit an:");
|
|
log(` E-Mail: ${EMAIL}`);
|
|
log(` Passwort: ${PASSWORT}`);
|
|
log("");
|
|
log(`Die Beispielwehr "${GESCHUETZT}" wurde nicht angefasst.`);
|
|
}
|
|
|
|
main()
|
|
.catch((fehler) => {
|
|
console.error("[testdaten] Abbruch:", fehler instanceof Error ? fehler.message : fehler);
|
|
process.exit(1);
|
|
})
|
|
.finally(async () => {
|
|
await prisma.$disconnect();
|
|
});
|