import { readFileSync, readdirSync, statSync } from "node:fs"; import path from "node:path"; import { describe, expect, it } from "vitest"; import { AKTIONEN, AKTIONSGRUPPEN, aktionenDerGruppe, aktionText, aktionTon } from "@/lib/protokoll-aktionen"; /** * Jede Protokoll-Aktion im Quellcode hat einen lesbaren Text (#027). Gesucht wird in allen * Dateien, die `protokollieren(` rufen, nach `aktion: …`/`aktion = …` (auch in Ternaries), und * ueberall nach Literalen, die mit einem bekannten Praefix beginnen (`"schluessel.uebernommen"` * wird z. B. als Argument durchgereicht). */ function dateien(verzeichnis: string): string[] { return readdirSync(verzeichnis).flatMap((name) => { const pfad = path.join(verzeichnis, name); if (statSync(pfad).isDirectory()) return name === "generated" ? [] : dateien(pfad); return /\.tsx?$/.test(name) ? [pfad] : []; }); } const QUELLEN = dateien("src").map((datei) => ({ datei, inhalt: readFileSync(datei, "utf8") })); const LITERAL = /"([^"\n]+)"/g; function aktionenImCode(): Map { const gefunden = new Map(); const praefixe = new Set(Object.keys(AKTIONEN).map((a) => a.split(".")[0]!)); for (const { datei, inhalt } of QUELLEN) { if (inhalt.includes("protokollieren(")) { for (const treffer of inhalt.matchAll(/\baktion\s*[:=]\s*([^,\n]+)/g)) { // Bei `a === "x" ? "eins" : "zwei"` zaehlen nur die Zweige, nicht die Bedingung. const ausdruck = treffer[1]!.includes("?") ? treffer[1]!.slice(treffer[1]!.indexOf("?")) : treffer[1]!; for (const [, literal] of ausdruck.matchAll(LITERAL)) gefunden.set(literal!, datei); } } for (const [, literal] of inhalt.matchAll(/"([a-z_]+(?:\.[a-z_]+)+)"/g)) { if (praefixe.has(literal!.split(".")[0]!) && !literal!.endsWith(".ts")) gefunden.set(literal!, datei); } } return gefunden; } describe("Protokoll-Aktionen", () => { const imCode = aktionenImCode(); it("findet die Aktionen im Quellcode", () => { expect(imCode.size).toBeGreaterThan(40); expect(imCode.has("authorized_keys.geschrieben")).toBe(true); expect(imCode.has("hostkey.ersetzt")).toBe(true); expect(imCode.has("privatekey.angezeigt")).toBe(true); expect(imCode.has("schluessel.uebernommen")).toBe(true); }); it.each([...imCode])("%s hat einen lesbaren Text (%s)", (aktion) => { expect(AKTIONEN[aktion], `${aktion} fehlt in src/lib/protokoll-aktionen.ts`).toBeDefined(); }); it("jede Gruppe hat Aktionen", () => { for (const gruppe of Object.keys(AKTIONSGRUPPEN) as (keyof typeof AKTIONSGRUPPEN)[]) { expect(aktionenDerGruppe(gruppe).length, gruppe).toBeGreaterThan(0); } }); it("faellt fuer unbekannte Aktionen auf den technischen Namen zurueck", () => { expect(aktionText("hostkey.ersetzt")).toBe("Host-Key ersetzt"); expect(aktionText("gibt.es.nicht")).toBe("gibt.es.nicht"); }); it("faerbt nach Ergebnis und Art", () => { expect(aktionTon("authorized_keys.geschrieben", "ERFOLG")).toBe("ok"); expect(aktionTon("anmeldung.oidc", "ERFOLG")).toBe("neutral"); expect(aktionTon("anmeldung.verweigert", "VERWEIGERT")).toBe("gefahr"); expect(aktionTon("verbindungstest", "FEHLER")).toBe("gefahr"); }); });