Files
Laufzettel/tests/platzhalter-werte.test.ts
T
florian.wildeandClaude Opus 5 c78a6ac1af
Prüfung / pruefung (push) Canceled after 0s
Prüfung / e2e (push) Canceled after 0s
feat(laeufe): Service, Abarbeitung, Kommentare und Archivschutz
Ein Lauf wird vorbereitet, abgearbeitet, abgeschlossen und archiviert. Der
Archiv-Schreibschutz sitzt in genau einer Funktion und wird vor jeder Aenderung
an Kopfdaten, Schrittergebnis und Kommentar gerufen; abgeschlossen bleibt
aenderbar, archiviert nicht.

Die Abarbeitungsansicht speichert jeden Schritt einzeln, frischt sich alle zehn
Sekunden auf und pausiert im Hintergrund. Sie traegt in der Polling-Antwort
keine Befehle und laedt sich auch nicht von selbst neu, solange ein generiertes
Passwort auf der Seite steht: beides tauschte es dem Techniker zwischen Lesen
und Einfuegen aus.

Der unabhaengige Review fand zwei Blocker: ein Befehl konnte zwei verschiedene
Passwoerter tragen (Konto bekommt A, Benutzer erfaehrt B), und „nicht
zutreffend“ liess sich nicht zuruecknehmen, waehrend die Oberflaeche
behauptete, nichts sei gewaehlt. Beides behoben und in Tests festgehalten.

Closes #13

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-25 16:56:33 +00:00

197 lines
8.3 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, expect, it } from "vitest";
import { PASSWORT_IM_DRUCK } from "@/lib/passwort";
import { platzhalterErsetzen, platzhalterErsetzenRoh } from "@/lib/platzhalter-werte";
/**
* Das Ersetzen der Platzhalter (#012).
*
* Hier entsteht der Text, den ein Techniker kopiert und auf einem
* Domaenencontroller ausfuehrt. Entsprechend dicht ist die Abdeckung: jede
* Regel, die der Auftrag nennt, hat einen Test, und jeder Weg, auf dem ein Wert
* einen Befehl zerbrechen koennte, auch.
*/
const WERTE = {
vorname: "Maria",
nachname: "Huber",
mitarbeiter: "Maria Huber",
abteilung: "Buchhaltung",
hostname: "PC-001",
};
describe("Ersetzen", () => {
it("ersetzt einen bekannten Platzhalter", () => {
expect(platzhalterErsetzen("Hallo {{vorname}}", WERTE).text).toBe("Hallo Maria");
});
it("erlaubt Leerzeichen in den Klammern", () => {
expect(platzhalterErsetzen("{{ vorname }} {{ nachname }}", WERTE).text).toBe("Maria Huber");
});
it("ersetzt denselben Platzhalter mehrfach", () => {
expect(platzhalterErsetzen("{{vorname}} und nochmal {{vorname}}", WERTE).text).toBe(
"Maria und nochmal Maria",
);
});
it("lässt einen unbekannten Platzhalter stehen und meldet ihn", () => {
// Akzeptanzkriterium: ein Tippfehler bleibt sichtbar. Eine leere Stelle im
// Befehl faellt niemandem auf, `{{tipfehler}}` schon.
const ergebnis = platzhalterErsetzen("New-ADUser -Name {{tipfehler}}", WERTE);
expect(ergebnis.text).toBe("New-ADUser -Name {{tipfehler}}");
expect(ergebnis.unbekannt).toEqual(["tipfehler"]);
});
it("macht einen bekannten Platzhalter ohne Wert leer", () => {
// Der Unterschied zum unbekannten: „hier gibt es das nicht“ ist etwas
// anderes als „falsch geschrieben“. `{{kunde}}` im Modus Intern ist leer.
const ergebnis = platzhalterErsetzen("Kunde: {{kunde}}!", WERTE);
expect(ergebnis.text).toBe("Kunde: !");
expect(ergebnis.unbekannt).toEqual([]);
});
it("rührt Text ohne Platzhalter nicht an", () => {
expect(platzhalterErsetzen("Nur Text { kein } Platzhalter", WERTE).text).toBe(
"Nur Text { kein } Platzhalter",
);
});
it("lässt einen falsch geschriebenen Platzhalter stehen und meldet ihn", () => {
/*
* Dieselbe Schluesselregel wie bei den Variablen (docs/vorlagen-format.md).
* Gemeldet wird er trotzdem: ein falsch geschriebener Platzhalter ist
* gefaehrlicher als ein unbekannter, weil er richtig aussieht. Bis zum
* Review von #012 blieb er still stehen.
*/
const text = "{{Vorname}} {{vor-name}} {{2te}}";
const ergebnis = platzhalterErsetzen(text, WERTE);
expect(ergebnis.text).toBe(text);
expect([...ergebnis.unbekannt].sort()).toEqual(["2te", "Vorname", "vor-name"]);
});
it("stolpert nicht über eine einzelne geschweifte Klammer", () => {
const text = "Pfad {C:\\} und {{vorname}}";
expect(platzhalterErsetzen(text, WERTE).text).toBe("Pfad {C:\\} und Maria");
});
it("setzt einen Wert mit $-Zeichen wörtlich ein", () => {
// `String.replace` deutet `$&`, `` $` ``, `$'` und `$1` im Ersatztext als
// Rueckverweise. Ein Nachname „A$&B“ haette sonst den gefundenen
// Platzhalter wieder eingesetzt.
for (const wert of ["A$&B", "vor $` danach", "A $' B", "Gruppe $1"]) {
expect(platzhalterErsetzenRoh("Name: {{nachname}}", { nachname: wert }).text, wert).toBe(
`Name: ${wert}`,
);
}
});
});
describe("Maskierung für einen Befehl", () => {
it("reicht jeden Wert durch die mitgegebene Maskierung", () => {
const ergebnis = platzhalterErsetzen(
'New-ADUser -Name "{{mitarbeiter}}"',
{ mitarbeiter: 'Anna "Anni" Meier' },
{ art: "befehl", maskieren: (wert) => wert.replaceAll('"', '""') },
);
expect(ergebnis.text).toBe('New-ADUser -Name "Anna ""Anni"" Meier"');
});
it("maskiert im Befehl nicht zusätzlich für HTML", () => {
// Ein `&amp;` in einem PowerShell-Befehl waere schlicht falsch.
const ergebnis = platzhalterErsetzen(
"Set-Name {{abteilung}}",
{ abteilung: "Forschung & Entwicklung" },
{ art: "befehl", maskieren: (wert) => wert },
);
expect(ergebnis.text).toBe("Set-Name Forschung & Entwicklung");
});
it("lässt einen Wert für die Anzeige unverändert", () => {
/*
* Hier wird **nicht** HTML-maskiert: React maskiert Textknoten selbst, und
* eine zweite Maskierung machte aus „Forschung & Entwicklung“ sichtbar
* „Forschung &amp;amp; Entwicklung“. Dass die Maskierung wirklich
* stattfindet, beweist tests/platzhalter-anzeige.test.tsx am gerenderten
* HTML — nicht dieser Test hier.
*/
expect(platzhalterErsetzen("{{abteilung}}", { abteilung: "Forschung & Entwicklung" }).text).toBe(
"Forschung & Entwicklung",
);
});
});
describe("Generiertes Passwort", () => {
const BEFEHL = '-AccountPassword "{{generiertes_passwort}}"';
it("erscheint im angezeigten Befehl", () => {
const ergebnis = platzhalterErsetzen(BEFEHL, {});
expect(ergebnis.mitPasswort).toBe(true);
expect(ergebnis.text).not.toContain("{{generiertes_passwort}}");
expect(ergebnis.text).toMatch(/-AccountPassword "\S{20}"/u);
});
it("ist bei zwei Aufrufen verschieden", () => {
// Akzeptanzkriterium: je Anzeige neu. Dasselbe Passwort zweimal waere eine
// stillschweigende Speicherung.
const erster = platzhalterErsetzen(BEFEHL, {}).text;
const zweiter = platzhalterErsetzen(BEFEHL, {}).text;
expect(erster).not.toBe(zweiter);
});
it("steht in der Druckansicht als Hinweis statt als Passwort", () => {
// Ein Ausdruck liegt herum – und das Passwort ist nirgends gespeichert,
// also stuende dort ein Geheimnis, das niemand mehr zuordnen kann.
const ergebnis = platzhalterErsetzen(BEFEHL, {}, { art: "druck" });
expect(ergebnis.text).toBe(`-AccountPassword "${PASSWORT_IM_DRUCK}"`);
expect(ergebnis.mitPasswort).toBe(true);
});
it("gibt das Passwort nicht neben dem Text zurück", () => {
/*
* Die harte Zusage des Meilensteins: es gibt **keinen** Weg, das erzeugte
* Passwort ausser im Text zu bekommen. Gaebe es ein Feld dafuer, waere die
* Versuchung gross, es „nur fuers Protokoll“ mitzuschreiben.
*/
const ergebnis = platzhalterErsetzen(BEFEHL, {});
expect(Object.keys(ergebnis).sort()).toEqual(["mitPasswort", "text", "unbekannt"]);
expect(JSON.stringify(ergebnis)).not.toMatch(/passwort":\s*"[A-Za-z0-9]/u);
});
it("meldet kein Passwort, wenn keines vorkam", () => {
expect(platzhalterErsetzen("Hallo {{vorname}}", WERTE).mitPasswort).toBe(false);
});
it("setzt in einem Text überall dasselbe Passwort ein", () => {
/*
* Der Fall, den der Review zu #013 gefunden hat: ein `New-ADUser` setzt das
* Passwort und teilt es in derselben Zeile mit. Zwei verschiedene hiessen,
* dass das Konto A bekommt und der Benutzer B erfaehrt — ein Konto, in das
* niemand hineinkommt, und niemand sieht warum. Frueher stand hier ein Test,
* der genau das festschrieb; er war falsch.
*/
const ergebnis = platzhalterErsetzen("{{generiertes_passwort}} und {{generiertes_passwort}}", {});
const [erstes, zweites] = ergebnis.text.split(" und ");
expect(erstes).toBe(zweites);
expect(erstes).toHaveLength(20);
});
it("erzeugt bei jedem Aufruf ein neues", () => {
// „Bei jeder Anzeige neu“ bleibt: dieselbe Zeile, zweimal gefuellt, ergibt
// zwei Passwoerter. Sonst waere es gespeichert, und genau das darf es nicht.
const einmal = platzhalterErsetzen("{{generiertes_passwort}}", {}).text;
const nochmal = platzhalterErsetzen("{{generiertes_passwort}}", {}).text;
expect(einmal).not.toBe(nochmal);
});
it("nimmt das Passwort des Seitenaufbaus, wenn eines hereingereicht wird", () => {
// So haelt ein Lauf ueber alle seine Befehlsschritte hinweg dasselbe
// Passwort — der Schritt „Konto anlegen“ und der Schritt „Passwort
// uebergeben“ meinen dasselbe.
const eines = platzhalterErsetzen("{{generiertes_passwort}}", {}, { art: "anzeige" }).text;
const erster = platzhalterErsetzen("a {{generiertes_passwort}}", {}, { art: "anzeige" }, eines).text;
const zweiter = platzhalterErsetzen("b {{generiertes_passwort}}", {}, { art: "anzeige" }, eines).text;
expect(erster).toBe(`a ${eines}`);
expect(zweiter).toBe(`b ${eines}`);
});
});