import { createHmac } from "node:crypto"; import { describe, expect, it } from "vitest"; import { hostKeyNachricht, signatur, testNachricht, webhookKoerper, zusammenfassungLeer, zusammenfassungNachricht, } from "@/lib/benachrichtigung/nachrichten"; import { hostKeyGruppen, zusammenfassungAus, type Posten } from "@/lib/benachrichtigung/posten"; /** Nachrichtenaufbau (#028): Text- und HTML-Mail auf Deutsch, Webhook-JSON, Signatur. */ const RAHMEN = { appUrl: "https://sshkv.example.de/", jetzt: new Date("2026-10-12T05:00:00Z") }; const POSTEN: Posten[] = [ { art: "drift", anlass: "drift:a1", host: { hostId: "h2", name: "srv-zeta", adresse: "10.0.0.2" }, befund: { art: "RECHTE", zielbenutzer: "root", fingerprint: null, text: "authorized_keys ist 0666" }, }, { art: "drift", anlass: "drift:a2", host: { hostId: "h1", name: "srv-alpha", adresse: "10.0.0.1" }, befund: { art: "UNBEKANNTER_SCHLUESSEL", zielbenutzer: "deploy", fingerprint: "SHA256:abc", text: "Kommentar ", }, }, { art: "drift", anlass: "drift:a3", host: { hostId: "h1", name: "srv-alpha", adresse: "10.0.0.1" }, befund: { art: "FEHLENDER_SCHLUESSEL", zielbenutzer: "deploy", fingerprint: "SHA256:def", text: null }, }, { art: "unerreichbar", anlass: "unerreichbar:h3:nie", host: { hostId: "h3", name: "srv-weg", adresse: "10.0.0.3", zuletztGeprueftAm: null, letzterAbgleichAm: null, }, }, { art: "ablauf", anlass: "ablauf:k1:7:2026-10-17", schluessel: { schluesselId: "k1", name: "Backup-Zugang", fingerprint: "SHA256:k1", ablaufAm: new Date("2026-10-17T10:00:00Z"), tage: 5, stufe: 7, }, }, { art: "abgelaufen", anlass: "abgelaufen:k2:2026-10-01", schluessel: { schluesselId: "k2", name: "Alter Zugang", fingerprint: "SHA256:k2", ablaufAm: new Date("2026-10-01T10:00:00Z"), hosts: ["srv-alpha", "Gruppe pve"], }, }, ]; describe("Zusammenfassung", () => { const z = zusammenfassungAus(POSTEN); const n = zusammenfassungNachricht(z, RAHMEN); it("fasst Drift je Host zusammen und sortiert nach Namen", () => { expect(z.drift.map((h) => [h.name, h.befunde.length])).toEqual([ ["srv-alpha", 2], ["srv-zeta", 1], ]); expect(zusammenfassungLeer(z)).toBe(false); expect(zusammenfassungLeer(zusammenfassungAus([]))).toBe(true); }); it("Betreff zählt die Anlässe auf Deutsch", () => { expect(n.betreff).toBe( "[sshkv] Zusammenfassung: 3 neue Abweichungen, 1 Host nicht erreichbar, 1 Schlüssel läuft ab, 1 abgelaufener Schlüssel ausgerollt", ); }); it("Textfassung mit Umlauten, Links und allen Abschnitten", () => { expect(n.text).toContain("Zusammenfassung vom 12.10.2026, 07:00"); expect(n.text).toContain("Neue Abweichungen (Drift):"); expect(n.text).toContain(" srv-alpha (10.0.0.1):"); expect(n.text).toContain("Unbekannter Schlüssel – Benutzer deploy – SHA256:abc"); expect(n.text).toContain("https://sshkv.example.de/hosts/h1"); expect(n.text).toContain("srv-weg (10.0.0.3) – noch nie erfolgreich abgeglichen"); expect(n.text).toContain("Backup-Zugang – läuft am 17.10.2026 ab (in 5 Tagen)"); expect(n.text).toContain("Alter Zugang – abgelaufen am 01.10.2026, noch auf srv-alpha, Gruppe pve"); }); it("HTML maskiert Inhalte von den Hosts", () => { expect(n.html).toContain(''); expect(n.html).not.toContain("", }, { art: "FEHLENDER_SCHLUESSEL", artText: "Schlüssel fehlt", zielbenutzer: "deploy", fingerprint: "SHA256:def", text: null, }, ], }); expect(json["ablauf"]).toEqual([ { schluessel: { id: "k1", name: "Backup-Zugang", fingerprint: "SHA256:k1" }, ablaufAm: "2026-10-17T10:00:00.000Z", tage: 5, stufe: 7, }, ]); }); }); describe("Host-Key geändert (dringend)", () => { const gruppen = hostKeyGruppen([ { art: "hostkey", anlass: "hostkey:x1", host: { hostId: "h1", name: "srv-alpha", adresse: "10.0.0.1" }, befund: { algorithmus: "ssh-ed25519", erwartet: "SHA256:alt", gesehen: "SHA256:neu" }, }, ...POSTEN, ]); it("je Host eine Nachricht, nur aus Host-Key-Posten", () => { expect(gruppen).toHaveLength(1); expect(gruppen[0]?.anlaesse).toEqual(["hostkey:x1"]); }); it("Betreff, Text und Nutzlast", () => { const n = hostKeyNachricht(gruppen[0]!.aenderung, RAHMEN); expect(n.betreff).toBe("[sshkv] DRINGEND: Host-Key von srv-alpha hat sich geändert"); expect(n.text).toContain("ssh-ed25519: erwartet SHA256:alt, gesehen SHA256:neu"); expect(n.text).toContain("bestätigt"); expect(n.html).toContain("Dringend"); expect(n.nutzlast).toMatchObject({ ereignis: "hostkey.geaendert", dringend: true, host: { id: "h1", name: "srv-alpha" }, befunde: [{ art: "HOSTKEY_GEAENDERT", erwartet: "SHA256:alt", fingerprint: "SHA256:neu" }], }); }); }); describe("Testnachricht und Signatur", () => { it("Testnachricht nennt den Kanal", () => { const n = testNachricht("EMAIL", RAHMEN); expect(n.betreff).toBe("[sshkv] Testnachricht"); expect(n.text).toContain("per E-Mail funktioniert"); expect(n.nutzlast.ereignis).toBe("test"); }); it("HMAC-SHA256 über den rohen Body", () => { const koerper = webhookKoerper(testNachricht("WEBHOOK", RAHMEN)); const erwartet = createHmac("sha256", "geheim").update(koerper).digest("hex"); expect(signatur(koerper, "geheim")).toBe(`sha256=${erwartet}`); expect(signatur(`${koerper} `, "geheim")).not.toBe(`sha256=${erwartet}`); }); });