Das Protokoll war bisher nur in der Datenbank lesbar. Die Ansicht zeigt es jeder Rolle mit lesbaren Aktionstexten, aufklappbarem Diff, Details und Hashes; ein Test haelt fest, dass jede Aktion im Code einen Text hat. Geblaettert wird ueber die ID statt OFFSET, weil das Protokoll waehrend des Lesens waechst. Die Kettenpruefung liest jede Zeile und laeuft deshalb nur auf Knopfdruck, mit 5 Minuten Zwischenspeicher; ein Bruch wird selbst protokolliert. Der CSV-Export streamt in Stapeln, entschaerft Formeln und steht selbst im Protokoll. protokollieren() reicht jeden Eintrag nach dem Schreiben an Syslog weiter, ohne Details und Diff. Kein Weg aendert oder loescht Eintraege; ein struktureller Test sichert das ab, zusaetzlich zum Trigger. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
76 lines
3.2 KiB
TypeScript
76 lines
3.2 KiB
TypeScript
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<string, string> {
|
|
const gefunden = new Map<string, string>();
|
|
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");
|
|
});
|
|
});
|