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}`);
});
});