import { describe, expect, it } from "vitest"; import { zugangAus } from "../../scripts/sicherung-zugang"; import { SPERREN } from "../../scripts/sicherung-sperren"; /** * Das Zerlegen der Verbindungszeichenkette. * * Der unangenehmste Fehler, den ein Sicherungsskript machen kann, ist nicht * abzubrechen: es liefe durch, schriebe eine Datei, und die enthielte die * falsche Datenbank. Auffallen würde es beim Zurückspielen — also genau dann, * wenn man die Sicherung braucht. * * Deshalb prüft dieser Test nicht nur, dass richtige Adressen richtig zerlegt * werden, sondern vor allem, dass falsche laut scheitern. */ describe("Verbindungszeichenkette zerlegen", () => { it("liest Benutzer, Passwort und Datenbank", () => { const zugang = zugangAus("postgresql://florian:geheim@localhost:5442/florian?schema=public"); expect(zugang).toEqual({ benutzer: "florian", passwort: "geheim", datenbank: "florian", }); }); it("ignoriert die Prisma-Parameter am Ende", () => { // `?schema=public` ist der Grund, warum die URL nicht als Ganzes an // pg_dump gehen kann — es lehnt sie mit "invalid URI query parameter" ab. const zugang = zugangAus( "postgresql://florian:geheim@localhost:5442/florian?schema=public&connection_limit=5", ); expect(zugang.datenbank).toBe("florian"); }); it("entschlüsselt Sonderzeichen im Passwort", () => { // Ein Passwort mit @ oder / muss in der URL kodiert stehen. Würde es hier // nicht dekodiert, meldete Postgres nur "Passwort falsch". const zugang = zugangAus("postgresql://florian:ge%40heim%2Fx@localhost:5432/florian"); expect(zugang.passwort).toBe("ge@heim/x"); }); it("kommt mit einem leeren Passwort zurecht", () => { const zugang = zugangAus("postgresql://florian@localhost:5432/florian"); expect(zugang.passwort).toBe(""); expect(zugang.benutzer).toBe("florian"); }); it("bricht ab, wenn die Adresse fehlt", () => { expect(() => zugangAus(undefined)).toThrow(/DIRECT_DATABASE_URL fehlt/); expect(() => zugangAus("")).toThrow(/DIRECT_DATABASE_URL fehlt/); }); it("bricht ab, wenn es gar keine URL ist", () => { expect(() => zugangAus("einfach nur text")).toThrow(/keine gültige URL/); }); it("bricht bei „localhost:5432“ ab — über die Protokollprüfung", () => { // Node zerlegt das klaglos: „localhost:“ gilt als Protokoll, „5432“ als // Pfad. Ohne die Protokollprüfung eine Zeile weiter käme hier ein // Zugang mit leerem Benutzer heraus, und die Sicherung liefe ins Leere. expect(() => zugangAus("localhost:5432")).toThrow(/postgresql/); }); it("bricht ab, wenn es nicht Postgres ist", () => { // Sonst würde eine falsch gesetzte Umgebungsvariable eine Sicherung // erzeugen, die nichts enthält. expect(() => zugangAus("mysql://florian:geheim@localhost:3306/florian")).toThrow( /postgresql/, ); }); it("bricht ab, wenn der Datenbankname fehlt", () => { expect(() => zugangAus("postgresql://florian:geheim@localhost:5432/")).toThrow( /Datenbankname/, ); }); }); describe("Die Sperrenliste stimmt mit der Datenbank überein", () => { /** * Der Abgleich gegen die Wirklichkeit. * * `scripts/sicherung-sperren.ts` führt auf, welche Rechte die * Anwendungsrolle nicht haben darf — und stellt sie nach einer * Wiederherstellung wieder her. Diese Liste steht neben den Migrationen, * die dieselben Sperren setzen, und zwei Listen laufen auseinander. * * Läuft sie auseinander, ist der Schaden lautlos: eine Wiederherstellung * würde eine Sperre nicht zurückbringen, von der niemand mehr weiß, dass es * sie gab. Deshalb fragt dieser Test die migrierte Datenbank, statt der * Liste zu glauben. */ it("nennt keine Sperre, die es in der Datenbank nicht gibt", async ({ skip }) => { const url = process.env.DIRECT_DATABASE_URL; if (!url) return skip(); const { PrismaClient } = await import("@prisma/client"); const prisma = new PrismaClient({ datasources: { db: { url } } }); try { const tabellen = await prisma.$queryRawUnsafe>( `SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_type = 'BASE TABLE'`, ); const vorhanden = new Set(tabellen.map((zeile) => zeile.table_name)); const unbekannt = SPERREN.map((eintrag) => eintrag.tabelle).filter( (tabelle) => !vorhanden.has(tabelle), ); expect(unbekannt, `Diese Tabellen gibt es nicht (mehr): ${unbekannt.join(", ")}`).toEqual( [], ); } finally { await prisma.$disconnect(); } }); it("übersieht keine Sperre, die die Migrationen gesetzt haben", async ({ skip }) => { const url = process.env.DIRECT_DATABASE_URL; if (!url) return skip(); const { PrismaClient } = await import("@prisma/client"); const prisma = new PrismaClient({ datasources: { db: { url } } }); try { const rolle = await prisma.$queryRawUnsafe>( "SELECT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'florian_app') AS da", ); if (!rolle[0]?.da) return skip(); // Welche Rechte hat die Anwendungsrolle heute wirklich? const zeilen = await prisma.$queryRawUnsafe< Array<{ table_name: string; privilege_type: string }> >( `SELECT table_name, privilege_type FROM information_schema.role_table_grants WHERE grantee = 'florian_app'`, ); const hat = new Set(zeilen.map((z) => `${z.table_name}:${z.privilege_type}`)); // Jede Tabelle, der ein Schreibrecht fehlt, muss in der Liste stehen — // sonst käme sie nach einer Wiederherstellung ungesperrt zurück. const alleTabellen = [...new Set(zeilen.map((z) => z.table_name))]; const notiert = new Map(SPERREN.map((e) => [e.tabelle, new Set(e.rechte)])); const fehlend: string[] = []; for (const tabelle of alleTabellen) { for (const recht of ["INSERT", "UPDATE", "DELETE"]) { if (!hat.has(`${tabelle}:${recht}`) && !notiert.get(tabelle)?.has(recht)) { fehlend.push(`${tabelle}: ${recht}`); } } } expect( fehlend, "Diese Sperren stehen in der Datenbank, aber nicht in scripts/sicherung-sperren.ts — " + `nach einer Wiederherstellung wären sie weg: ${fehlend.join(", ")}`, ).toEqual([]); } finally { await prisma.$disconnect(); } }); });