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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
f85bc87324
commit
b59fbb4645
@@ -0,0 +1,175 @@
|
||||
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 `&` 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; 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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user