Files
FireAdministration/tests/unit/sicherung-zugang.test.ts
florian.wilde 14cc4de9b8
Pruefung / pruefung (push) Canceled after 0s
Pruefung / oberflaeche (push) Canceled after 0s
Phase 11: Freigabereife (#16)
2026-09-06 20:05:49 +00:00

169 lines
6.4 KiB
TypeScript

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<Array<{ table_name: string }>>(
`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<Array<{ da: boolean }>>(
"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();
}
});
});