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(); 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(); 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("!"); } }); });