feat(platzhalter): Aufloesung, Namensregeln und Shell-Maskierung (#26)
Prüfung / pruefung (push) Canceled after 0s
Prüfung / e2e (push) Canceled after 0s

This commit was merged in pull request #26.
This commit is contained in:
2026-09-25 13:30:31 +00:00
parent f85bc87324
commit eec5de404a
21 changed files with 4069 additions and 166 deletions
+138
View File
@@ -0,0 +1,138 @@
import { describe, expect, it } from "vitest";
import {
PASSWORT_GROSS,
PASSWORT_KLEIN,
PASSWORT_LAENGE,
PASSWORT_SONDER,
PASSWORT_ZIFFERN,
passwortErzeugen,
} from "@/lib/passwort";
/**
* Das generierte Passwort (#012).
*
* Die Zusage lautet: bei jeder Anzeige neu und nirgends gespeichert. Hier steht
* die eine Haelfte davon — dass es wirklich jedes Mal ein anderes ist und dass
* es die Anforderungen erfuellt, an denen ein `New-ADUser` sonst scheitert.
*
* Die andere Haelfte — dass es in keiner Tabelle landet — steht in
* `tests/befehle.test.ts` („schreibt beim Fuellen nichts in die Datenbank“). Sie
* prueft, was heute pruefbar ist: dass der Weg vom Lauf zum fertigen Befehl
* nichts schreibt. `Schrittergebnis` und `Kommentar` gibt es erst mit #013; dort
* gehoert derselbe Beweis noch einmal hin, wenn es sie gibt.
*/
const ALLE = PASSWORT_GROSS + PASSWORT_KLEIN + PASSWORT_ZIFFERN + PASSWORT_SONDER;
describe("Erzeugtes Passwort", () => {
it("ist 20 Zeichen lang", () => {
expect(passwortErzeugen()).toHaveLength(20);
expect(PASSWORT_LAENGE).toBe(20);
});
it("benutzt nur Zeichen aus dem vereinbarten Vorrat", () => {
for (let versuch = 0; versuch < 200; versuch += 1) {
for (const zeichen of passwortErzeugen()) {
expect(ALLE.includes(zeichen), `„${zeichen}“ gehört nicht zum Vorrat`).toBe(true);
}
}
});
it("enthält aus jeder Gruppe mindestens ein Zeichen", () => {
// Die Passwortrichtlinie eines Active Directory verlangt ueblicherweise drei
// von vier Gruppen. Ein abgewiesenes New-ADUser laesst den Techniker vor dem
// Kunden stehen.
for (let versuch = 0; versuch < 200; versuch += 1) {
const passwort = passwortErzeugen();
for (const [name, gruppe] of [
["Großbuchstaben", PASSWORT_GROSS],
["Kleinbuchstaben", PASSWORT_KLEIN],
["Ziffern", PASSWORT_ZIFFERN],
["Sonderzeichen", PASSWORT_SONDER],
] as const) {
expect(
[...passwort].some((z) => gruppe.includes(z)),
`${name} fehlen in „${passwort}“`,
).toBe(true);
}
}
});
it("enthält keine verwechselbaren Zeichen", () => {
/*
* Das Passwort wird vom Bildschirm abgelesen und getippt – oft von der
* Person, der das Konto gehoert. Ein falsch gelesenes Zeichen kostet einen
* Anruf.
*
* Verwechselbar ist immer ein **Paar**: aus jedem Paar fehlt genau einer,
* dann ist der andere eindeutig. `B` ist draussen, also darf `8` bleiben;
* `O` ist draussen, also darf nichts mit `0` verwechselt werden — `0` ist
* trotzdem weg, weil es mit `o` zusammenfaellt.
*/
for (let versuch = 0; versuch < 200; versuch += 1) {
expect(passwortErzeugen()).not.toMatch(/[IlB1|O0]/u);
}
});
it("lässt aus jedem verwechselbaren Paar genau einen übrig", () => {
// Haelt die Begruendung oben fest: `8` ohne `B` ist eindeutig, `1` ohne `l`
// und `I` waere es auch — es ist trotzdem draussen, weil `|` in manchen
// Schriften gleich aussieht.
const vorrat = PASSWORT_GROSS + PASSWORT_KLEIN + PASSWORT_ZIFFERN;
for (const paar of ["I1", "l1", "O0", "o0", "B8", "Il"]) {
const drin = [...paar].filter((zeichen) => vorrat.includes(zeichen));
expect(drin.length, `„${paar}“ ist beides im Vorrat`).toBeLessThanOrEqual(1);
}
});
it("enthält nichts, was einen Befehl oder eine CSV-Zelle zerbricht", () => {
// Die Maskierung faengt das ab – aber ein Passwort, das nur wegen korrekter
// Maskierung funktioniert, ist eine Wette auf jeden kuenftigen Aufrufer.
for (let versuch = 0; versuch < 200; versuch += 1) {
expect(passwortErzeugen()).not.toMatch(/["'$`\\;&|<>%,\n\r]/u);
}
});
it("liefert bei zwei Aufrufen verschiedene Passwörter", () => {
// Genau das Akzeptanzkriterium: ein zweiter Aufruf ist ein **anderes**
// Passwort, nicht dasselbe noch einmal.
const gesehen = new Set<string>();
for (let versuch = 0; versuch < 500; versuch += 1) {
gesehen.add(passwortErzeugen());
}
expect(gesehen.size).toBe(500);
});
it("verteilt die Pflichtzeichen über die ganze Länge", () => {
// Ohne Mischen staenden Gross, Klein, Ziffer und Sonderzeichen immer an den
// ersten vier Stellen — erkennbar, und damit vier Zeichen weniger Zufall.
const stellen = new Set<number>();
for (let versuch = 0; versuch < 300; versuch += 1) {
const passwort = passwortErzeugen();
const stelle = [...passwort].findIndex((z) => PASSWORT_SONDER.includes(z));
stellen.add(stelle);
}
expect(stellen.size).toBeGreaterThan(10);
});
it("weist eine Länge ab, die nicht alle vier Gruppen fassen kann", () => {
expect(() => passwortErzeugen(3)).toThrow();
expect(passwortErzeugen(4)).toHaveLength(4);
});
it("fängt nie mit einem Sonderzeichen an", () => {
// `= + - @` am Anfang einer Zelle liest eine Tabellenkalkulation als Formel,
// und ein Passwort geht seinen Weg oft ueber Zwischenablage und Tabelle.
for (let versuch = 0; versuch < 500; versuch += 1) {
expect(PASSWORT_SONDER.includes(passwortErzeugen().charAt(0))).toBe(false);
}
});
it("enthält kein Ausrufezeichen", () => {
// Bei eingeschalteter verzoegerter Ersetzung frisst cmd.exe es still – das
// Konto bekaeme ein anderes Passwort als angezeigt.
for (let versuch = 0; versuch < 200; versuch += 1) {
expect(passwortErzeugen()).not.toContain("!");
}
});
});