Files
Laufzettel/tests/platzhalter-werte.test.ts
T
Florian WildeandClaude Opus 5 b59fbb4645
Prüfung / pruefung (pull_request) Canceled after 0s
Prüfung / pruefung (push) Canceled after 0s
Prüfung / e2e (pull_request) Canceled after 0s
Prüfung / e2e (push) Canceled after 0s
feat(platzhalter): Aufloesung, Namensregeln und Shell-Maskierung
Aus einer Vorlage wird hier der PowerShell-Befehl, den ein Techniker kopiert und
auf einem Domaenencontroller ausfuehrt. Entsprechend dicht ist die Abdeckung:
810 Tests, und die Maskierung wird **ausgefuehrt** geprueft, nicht gelesen.

- Platzhalter aus Geraet, Lauf, Mitarbeiter und Abteilung, dazu die Variablen
  der Vorlage; ein unbekannter bleibt sichtbar stehen
- `{{ou}}` ist der Eltern-Container des DN — genau der Pfad fuer New-ADUser
- Namensregeln fuer upn, sam, mail und hostname mit Transliteration, `{v}`,
  `{nnn}` und `{kundenkuerzel}`; Kundenregeln ueberlagern die globalen feldweise
- Maskierung je Shell, Skript-Kopie je Abschnitt mit Kommentarzeilen
- `{{generiertes_passwort}}`: 20 Zeichen, bei jeder Anzeige neu, nirgends
  gespeichert; im Druck `[Passwort – nicht gespeichert]`

Der unabhaengige Review fand drei Blocker. Der schwerste war eine
Codeausfuehrung: PowerShell liest auch `“`, `”` und `„` als Begrenzer einer
Zeichenkette, die Maskierung kannte nur das ASCII-Zeichen. Ein Abteilungsname
aus Word — Word wandelt `"` von selbst um — reichte. Gefunden hat es der Review
nicht durch Lesen, sondern indem er sich PowerShell in einen Container geholt
und die Markierungsdatei hat entstehen lassen. Der Test dagegen hatte sich still
uebersprungen; er nimmt jetzt selbst das Abbild und bricht in CI ab.

Dazu: zwei verschiedene Passwoerter in einer Antwort (Anzeige und Skript wurden
getrennt gefuellt), eine Vorlagenvariable, die `{{techniker}}` und `{{kunde}}`
ueberschreiben konnte, und die tragende Annahme der Maskierung — dass der
Platzhalter in Anfuehrungszeichen steht —, die niemand geprueft hat.

Schliesst #012

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-25 15:29:43 +02:00

176 lines
7.1 KiB
TypeScript
Raw 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("erzeugt für zwei Vorkommen im selben Text zwei verschiedene Passwörter", () => {
/*
* Fachlich fragwuerdig, aber die ehrliche Folge der Regel „bei jeder Anzeige
* neu“: wer den Platzhalter zweimal in einen Befehl schreibt, meint
* vermutlich dasselbe Passwort. Dass es zwei sind, faellt beim Lesen auf —
* eines still zu speichern und wiederzuverwenden fiele nicht auf.
*/
const ergebnis = platzhalterErsetzen("{{generiertes_passwort}} und {{generiertes_passwort}}", {});
const [erstes, zweites] = ergebnis.text.split(" und ");
expect(erstes).not.toBe(zweites);
});
});