Ein Abschnitt oder ein einzelner Schritt erscheint nur, wenn seine Bedingung zutrifft — abhaengig von einer Variable des Laufs oder vom Zustand eines anderen Schritts. Der echte Fall steht im Seed: "Mitarbeiter-Onboarding" traegt beide Konto-Abschnitte, fuenf AD-Schritte und vier fuer Entra ID, und `konto_typ` sagt beim Start, welcher gilt. Bisher klickte ein Techniker die anderen neun bei jedem Onboarding weg. Entscheidungen, alle im Issue begruendet: - Laufend ausgewertet, nicht beim Start festgeschrieben. Fuer die Abteilungsabschnitte (#031) ist Festschreiben richtig; fuer Bedingungen nicht — die Antwort auf "ist Pruefen fehlgeschlagen?" entsteht erst waehrend der Arbeit. Ein struktureller Test prueft, dass alle drei Schichten (Abschlussregel, Abarbeitung, Druck) die Auswertung rufen. - Nicht entscheidbar heisst: gilt. Ein Lauf ohne konto_typ zeigt beide Abschnitte — genau wie vorher. Ein Abschnitt zu viel ist sichtbar und korrigierbar, ein fehlender faellt niemandem auf. - OFFEN ist eine Antwort, keine Luecke: waere es unentscheidbar, waere ein bedingter Abschnitt von Anfang an sichtbar. - Eine Bedingung, kein Ausdrucksbaum. Kein und, kein oder. - Verwiesen wird ueber Titel-Pfade, nicht ueber Kennungen: vier Wege schreiben eine Struktur neu und geben neue Kennungen. Zwei stille Datenverluste gefunden, der zweite aelter als dieser Meilenstein: - Der Import verwarf `nurFuerAbteilungen`. Der Export schreibt es seit #031, das Schema nimmt es an, der Import setzte es nicht — eine exportierte Vorlage verlor ihre Abteilungsabschnitte und galt dort fuer alle. - Die tiefe Kopie verwarf es ebenfalls, womit jedes "Bearbeiten" einer Vorlage alle Einschraenkungen verlor. Ursache war ein von Hand getippter Zwischentyp, der weniger Felder nannte als die Abfrage liefert; jetzt aus Prisma abgeleitet, beide Kopierwege teilen eine Stelle, und ein Test vergleicht Kopie und Original Feld fuer Feld. Gefunden hat beides die Klickprobe am gebauten Stand, nicht die Suite: im Editor stand "Bedingung gesetzt", nach "Bearbeiten" war sie weg. Dreizehn Gegenproben liefen, alle wurden rot — und der teuerste Befund lag ausserhalb von allen. 1499 Vitest-Tests (vorher 1457), 115 E2E-Ablaeufe gruen. Closes #39 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
195 lines
7.9 KiB
TypeScript
195 lines
7.9 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { datenbankVerfuegbar, testPrisma } from "./helpers/db";
|
|
|
|
/**
|
|
* Wächter über die Datenbankobjekte, die Prisma nicht kennt (#009).
|
|
*
|
|
* Zusammengesetzte Fremdschlüssel, Trigramm-Indizes und Trigger lassen sich im
|
|
* Prisma-Schema nicht ausdrücken. Deshalb erzeugt `prisma migrate dev` bei
|
|
* **jeder** künftigen Migration Löschbefehle für sie — sie sind aus Prismas
|
|
* Sicht Abweichungen von der Vorgabe. Wer die Löschbefehle einmal übersieht,
|
|
* entfernt stillschweigend die Mandantengrenze, die Archivsuche oder den Schutz
|
|
* der Unveränderlichkeit.
|
|
*
|
|
* Dieser Test macht das laut. Er prüft nicht, dass die Regeln *wirken* — das tun
|
|
* `tests/datenmodell.test.ts` und `tests/vorlagen.test.ts` —, sondern nur, dass
|
|
* die Objekte überhaupt noch da sind.
|
|
*/
|
|
|
|
async function namen(abfrage: string): Promise<string[]> {
|
|
const zeilen = await testPrisma.$queryRawUnsafe<{ name: string }[]>(abfrage);
|
|
return zeilen.map((zeile) => zeile.name).sort();
|
|
}
|
|
|
|
describe.runIf(datenbankVerfuegbar)("Handgeschriebene Datenbankobjekte", () => {
|
|
it("hat die zusammengesetzten Fremdschlüssel der Mandantengrenze", async () => {
|
|
// Ohne sie kann ein Lauf ein Gerät eines fremden Kunden ziehen — und ein
|
|
// Anhang an einem Schritt hängen, der zu einem anderen Lauf gehört.
|
|
expect(
|
|
await namen(
|
|
`SELECT conname AS name FROM pg_constraint
|
|
WHERE conname LIKE '%gleicher_kunde' OR conname LIKE '%gleicher_lauf'`,
|
|
),
|
|
).toEqual([
|
|
"Anhang_ergebnis_gleicher_lauf",
|
|
"Anhang_lauf_gleicher_kunde",
|
|
"Geraet_besitzer_gleicher_kunde",
|
|
"Lauf_abteilung_gleicher_kunde",
|
|
"Lauf_geraet_gleicher_kunde",
|
|
"Lauf_mitarbeiter_gleicher_kunde",
|
|
"Mitarbeiter_vorgesetzter_gleicher_kunde",
|
|
]);
|
|
});
|
|
|
|
it("hat die Trigramm-Indizes für die Archivsuche", async () => {
|
|
// Ohne sie wird jede Archivsuche zum vollständigen Durchlauf der Tabelle.
|
|
expect(
|
|
await namen(
|
|
`SELECT indexname AS name FROM pg_indexes WHERE schemaname='public' AND indexdef LIKE '%gin_trgm_ops%'`,
|
|
),
|
|
).toEqual([
|
|
"Kommentar_rumpf_trgm_idx",
|
|
"Lauf_titel_trgm_idx",
|
|
"Schrittergebnis_notiz_trgm_idx",
|
|
"Schrittergebnis_wert_trgm_idx",
|
|
]);
|
|
});
|
|
|
|
it("hat die Trigger, die eine veröffentlichte Version schützen", async () => {
|
|
expect(
|
|
await namen(
|
|
`SELECT tgname AS name FROM pg_trigger WHERE NOT tgisinternal AND tgname NOT LIKE '%_melden'`,
|
|
),
|
|
).toEqual([
|
|
"BausteinSchritt_unveraenderlich",
|
|
"BausteinVersion_status_einbahn",
|
|
/*
|
|
* Zieht die Rolle mit, wenn eine Person geloescht wird (#035). Ohne ihn
|
|
* scheiterte das `ON DELETE SET NULL` des Fremdschluessels am CHECK, und
|
|
* das Loeschen eines Mitarbeiters waere unmoeglich statt folgerichtig.
|
|
*/
|
|
"Mitarbeiter_konto_mitziehen",
|
|
"VorlagenAbschnitt_baustein_veroeffentlicht",
|
|
/*
|
|
* Eine Schritt-Bedingung zeigt auf einen Schritt **derselben Version**
|
|
* (#039). Ein CHECK kann das nicht: es verlangt einen Blick in zwei andere
|
|
* Tabellen. Ohne diese Pruefung zeigt eine Bedingung nach dem
|
|
* Veroeffentlichen ins Leere — „Bearbeiten“ erzeugt eine tiefe Kopie mit
|
|
* neuen Kennungen —, und der Abschnitt erscheint dann stillschweigend
|
|
* immer.
|
|
*/
|
|
"VorlagenAbschnitt_bedingung_gleiche_version",
|
|
"VorlagenAbschnitt_unveraenderlich",
|
|
"VorlagenSchritt_bedingung_gleiche_version",
|
|
"VorlagenSchritt_unveraenderlich",
|
|
"VorlagenVariable_unveraenderlich",
|
|
"VorlagenVersion_status_einbahn",
|
|
]);
|
|
});
|
|
|
|
it("hat die Trigger, die eine Änderung am Lauf melden", async () => {
|
|
/*
|
|
* Ohne sie bleibt die Live-Aktualisierung still, und zwar **lautlos**: die
|
|
* Seite sieht aus wie immer, nur sieht niemand mehr die Arbeit des anderen.
|
|
* Welche Tabellen dabei sind, haengt an dem, was `laufZustand` liefert —
|
|
* fehlte `Kommentar` oder `Anhang`, waere der Strom gegenueber dem alten
|
|
* Polling ein Rueckschritt (#033).
|
|
*/
|
|
expect(
|
|
await namen(`SELECT tgname AS name FROM pg_trigger WHERE NOT tgisinternal AND tgname LIKE '%_melden'`),
|
|
).toEqual([
|
|
"Anhang_geaendert_melden",
|
|
"Kommentar_geaendert_melden",
|
|
"Lauf_geaendert_melden",
|
|
"Schrittergebnis_geaendert_melden",
|
|
]);
|
|
});
|
|
|
|
it("hat die partiellen Eindeutigkeitsindizes", async () => {
|
|
expect(
|
|
await namen(
|
|
`SELECT indexname AS name FROM pg_indexes WHERE schemaname='public' AND indexdef LIKE '%WHERE%'`,
|
|
),
|
|
).toEqual([
|
|
"BausteinVersion_ein_entwurf",
|
|
"Kunde_eigene_organisation_einmalig",
|
|
"Vorlage_seedSchluessel_einmalig",
|
|
"VorlagenVersion_ein_entwurf",
|
|
/*
|
|
* Ohne diesen bekaeme der Zustaendige fuer einen Lauf, der ueber das
|
|
* Wochenende ueberfaellig steht, 2880 Mails — und zwar ohne dass ein
|
|
* einziger Test rot waere, denn jede davon ist fuer sich korrekt (#034).
|
|
*/
|
|
"Zustellung_ueberfaellig_einmal",
|
|
]);
|
|
});
|
|
|
|
it("hat alle Prüfbedingungen", async () => {
|
|
const vorhanden = await namen(
|
|
`SELECT conname AS name FROM pg_constraint WHERE contype='c' AND connamespace='public'::regnamespace`,
|
|
);
|
|
// Namentlich aufgezählt, damit auch das Verschwinden einer einzelnen auffällt.
|
|
expect(vorhanden).toEqual([
|
|
"Anhang_groesse_positiv",
|
|
"Anhang_pruefsumme_form",
|
|
/*
|
|
* Die Rolle MITARBEITER und ihre Zuordnung gehoeren zusammen, in beide
|
|
* Richtungen (#035). Ohne das waere ein MITARBEITER ohne Person moeglich —
|
|
* ein Recht, das sich auf niemanden bezieht, und eine Einschraenkung, die
|
|
* je nach Formulierung „nichts“ oder „alles“ bedeutet.
|
|
*/
|
|
"Benutzer_mitarbeiter_nur_bei_rolle",
|
|
"Einstellung_einzelne_zeile",
|
|
"Einstellung_smtp_passwort_verschluesselt",
|
|
"Einstellung_smtp_port_bereich",
|
|
"Einstellung_webhook_geheimnis_verschluesselt",
|
|
"Geraet_registriert_braucht_namen",
|
|
"Geraet_tagcode_form",
|
|
"Kunde_kuerzel_form",
|
|
"Lauf_abgeschlossen_mit_zeitpunkt",
|
|
"Lauf_archiviert_mit_zeitpunkt",
|
|
"Lauf_begonnen_mit_zeitpunkt",
|
|
"Lauf_mitarbeiter_eindeutig",
|
|
"Mitarbeiter_vorgesetzter_nicht_selbst",
|
|
"Schrittergebnis_begruendung_noetig",
|
|
"TagCodeVergabe_form",
|
|
"Verzeichnisquelle_bindpasswort_verschluesselt",
|
|
/*
|
|
* Seit #028 hat eine Quelle eine **Art**, und welche Felder sie braucht,
|
|
* haengt daran. Ohne diese Bedingung waere eine Graph-Quelle mit
|
|
* `basisDn` moeglich — eine Zeile, die zwei Dinge gleichzeitig behauptet.
|
|
*/
|
|
"Verzeichnisquelle_clientsecret_verschluesselt",
|
|
"Verzeichnisquelle_felder_je_art",
|
|
/*
|
|
* Das SCIM-Token steht nur als argon2-Hash da (#029). Ein Token, das
|
|
* jemand versehentlich roh speichert, faellt hier auf — und zwar in der
|
|
* Datenbank, also auch dann, wenn jemand am Dienst vorbei schreibt.
|
|
*/
|
|
"Verzeichnisquelle_scimtoken_gehasht",
|
|
// Und ein Token gehoert zu einem Zeitpunkt, in beide Richtungen.
|
|
"Verzeichnisquelle_scimtoken_mit_zeitpunkt",
|
|
/*
|
|
* Die Form einer Bedingung (#039). Eine mit unbekannter `art` wuerde beim
|
|
* Lesen als „keine Bedingung“ gelten — der Abschnitt waere sichtbar, und
|
|
* niemand koennte sagen, warum seine Bedingung nicht wirkt.
|
|
*/
|
|
"VorlagenAbschnitt_bedingung_form",
|
|
"VorlagenSchritt_bedingung_form",
|
|
"VorlagenVersion_veroeffentlicht_mit_zeitpunkt",
|
|
"Zustellung_bezug_passt_zum_anlass",
|
|
"Zustellung_empfaenger_nicht_leer",
|
|
"Zustellung_gesendet_mit_zeitpunkt",
|
|
"Zustellung_versuche_nicht_negativ",
|
|
"Zustellung_webhook_ohne_benutzer",
|
|
]);
|
|
});
|
|
|
|
it("hat die Erweiterung für die Trigramm-Suche", async () => {
|
|
const zeilen = await testPrisma.$queryRaw<{ extname: string }[]>`
|
|
SELECT extname FROM pg_extension WHERE extname = 'pg_trgm'
|
|
`;
|
|
expect(zeilen).toHaveLength(1);
|
|
});
|
|
});
|