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 { 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", "VorlagenAbschnitt_unveraenderlich", "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", "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); }); });