import { describe, expect, it } from "vitest"; import { EreignisTyp, Kanal } from "@prisma/client"; import { KANAL_EXTERN, PLATZHALTER, STANDARD, einsetzen, nachrichtBauen, unbekanntePlatzhalter, } from "@/lib/benachrichtigung/vorlagen"; /** * Die Textbausteine. * * Zwei Dinge werden hier scharf geprüft, weil beide Datenschutzfragen sind * und nicht nur Formatierung: * * 1. Über externe Kanäle geht die Kurzfassung — ohne Namen, ohne Details. * 2. Ein unbekannter Platzhalter bleibt sichtbar stehen, statt still zu * verschwinden. Eine Lücke, die niemand bemerkt, ist schlimmer als ein * seltsam aussehender Text, den jemand korrigiert. */ describe("einsetzen", () => { it("ersetzt bekannte Platzhalter", () => { expect(einsetzen("{anzahl} Prüfungen bei der {wehr}", { anzahl: 3, wehr: "FF Musterhausen" })).toBe( "3 Prüfungen bei der FF Musterhausen", ); }); it("lässt einen unbekannten Platzhalter stehen", () => { expect(einsetzen("Hallo {tippfehler}", { name: "Max" })).toBe("Hallo {tippfehler}"); }); it("macht aus null einen Gedankenstrich, nicht aus leerer Luft", () => { // Ein leerer Ort soll als "—" sichtbar sein: "Ort: " ohne Angabe sieht // aus wie ein Fehler in der Software, "Ort: —" wie eine fehlende Angabe. expect(einsetzen("Ort: {ort}", { ort: null })).toBe("Ort: —"); }); it("kommt mit Umlauten im Platzhalternamen zurecht", () => { expect(einsetzen("{faelligAm}", { faelligAm: "01.10.2026" })).toBe("01.10.2026"); }); it("ersetzt denselben Platzhalter mehrfach", () => { expect(einsetzen("{a} und nochmal {a}", { a: "x" })).toBe("x und nochmal x"); }); it("lässt Text ohne Platzhalter unverändert", () => { expect(einsetzen("Nichts zu ersetzen.", {})).toBe("Nichts zu ersetzen."); }); }); describe("nachrichtBauen", () => { it("nimmt für interne Kanäle den ausführlichen Text", () => { const nachricht = nachrichtBauen({ ereignis: EreignisTyp.PRUEFUNG_FAELLIG, kanal: Kanal.MAIL, werte: { wehr: "FF Musterhausen", anzahl: 3, geraet: "Pressluftatmer", inventarnummer: "INV-0042", faelligAm: "01.10.2026", tage: 7, }, }); expect(nachricht.text).toContain("INV-0042"); expect(nachricht.text).toContain("FF Musterhausen"); }); /** * Der wichtigste Test der Datei. Telegram und Web-Push laufen über fremde * Server — dorthin gehört kein Gerät, kein Name, keine Inventarnummer. */ it("nimmt für externe Kanäle die Kurzfassung ohne Details", () => { for (const kanal of Object.values(Kanal).filter((wert) => KANAL_EXTERN[wert])) { const nachricht = nachrichtBauen({ ereignis: EreignisTyp.PRUEFUNG_FAELLIG, kanal, werte: { wehr: "FF Musterhausen", anzahl: 3, geraet: "Pressluftatmer", inventarnummer: "INV-0042", faelligAm: "01.10.2026", tage: 7, }, }); expect(nachricht.text, `Kanal ${kanal}`).not.toContain("INV-0042"); expect(nachricht.text, `Kanal ${kanal}`).not.toContain("Pressluftatmer"); expect(nachricht.text).toContain("3 Prüfungen"); } }); it("nennt bei einem Mangel über einen externen Kanal die Beschreibung nicht", () => { const nachricht = nachrichtBauen({ ereignis: EreignisTyp.MANGEL_GEMELDET, kanal: Kanal.TELEGRAM, werte: { wehr: "FF Musterhausen", geraet: "LF 20", inventarnummer: "INV-0001", beschreibung: "Bremsleitung undicht", prioritaet: "HOCH", melder: "Huber, Andreas", }, }); expect(nachricht.text).not.toContain("Huber"); expect(nachricht.text).not.toContain("Bremsleitung"); expect(nachricht.text).toContain("HOCH"); }); it("lässt eine eigene Vorlage gewinnen", () => { const nachricht = nachrichtBauen({ ereignis: EreignisTyp.TERMIN_NEU, kanal: Kanal.MAIL, werte: { titel: "Übungsabend", datum: "15.09.2026" }, vorlage: { betreff: "Neu: {titel}", text: "{titel} am {datum}. Kommst du?" }, }); expect(nachricht.betreff).toBe("Neu: Übungsabend"); expect(nachricht.text).toBe("Übungsabend am 15.09.2026. Kommst du?"); }); it("greift ohne Vorlage auf den Standardbetreff zurück", () => { const nachricht = nachrichtBauen({ ereignis: EreignisTyp.TERMIN_ABGESAGT, kanal: Kanal.MAIL, werte: { titel: "Waldfest", datum: "30.09.2026", uhrzeit: "14:00", grund: "Regen" }, }); expect(nachricht.betreff).toBe("Abgesagt: Waldfest am 30.09.2026"); expect(nachricht.text).toContain("Regen"); }); it("liefert für jedes Ereignis einen Standardtext", () => { for (const ereignis of Object.values(EreignisTyp)) { expect(STANDARD[ereignis].text.length, ereignis).toBeGreaterThan(0); expect(STANDARD[ereignis].kurz.length, ereignis).toBeGreaterThan(0); expect(STANDARD[ereignis].betreff.length, ereignis).toBeGreaterThan(0); } }); /** * Ein Standardtext, der einen Platzhalter benutzt, den es für sein Ereignis * nicht gibt, würde bei jedem Empfänger als `{foo}` ankommen. */ it("benutzt in den Standardtexten nur erlaubte Platzhalter", () => { for (const ereignis of Object.values(EreignisTyp)) { const vorlage = STANDARD[ereignis]; expect(unbekanntePlatzhalter(ereignis, vorlage.betreff), ereignis).toEqual([]); expect(unbekanntePlatzhalter(ereignis, vorlage.text), ereignis).toEqual([]); expect(unbekanntePlatzhalter(ereignis, vorlage.kurz), ereignis).toEqual([]); } }); }); describe("unbekanntePlatzhalter", () => { it("findet einen Tippfehler", () => { expect(unbekanntePlatzhalter(EreignisTyp.TERMIN_NEU, "Am {datm} um {uhrzeit}")).toEqual([ "datm", ]); }); it("meldet jeden Fehler nur einmal", () => { expect(unbekanntePlatzhalter(EreignisTyp.TERMIN_NEU, "{foo} und {foo}")).toEqual(["foo"]); }); it("akzeptiert alle für das Ereignis vorgesehenen Platzhalter", () => { for (const ereignis of Object.values(EreignisTyp)) { const alle = PLATZHALTER[ereignis].map((name) => `{${name}}`).join(" "); expect(unbekanntePlatzhalter(ereignis, alle), ereignis).toEqual([]); } }); it("beanstandet einen Platzhalter, den es nur bei einem anderen Ereignis gibt", () => { // {inventarnummer} gibt es bei Prüfungen, nicht bei Terminen. expect(unbekanntePlatzhalter(EreignisTyp.TERMIN_NEU, "{inventarnummer}")).toEqual([ "inventarnummer", ]); }); });