51 Tests, unter einer Sekunde, ohne DOM, Datenbank oder laufenden Server. Abgedeckt sind die Stellen, an denen ein Fehler teuer waere: - Slug-Erzeugung mit Umlauten, Tag-Aufbereitung, Textkuerzung - URL-Zerlegung fuer https, SSH, verschachtelte GitLab-Gruppen und Oberflaechenpfade; Provider-Erkennung; Bildung der Aktivitaetskurve - SSRF-Sperrlisten: Loopback, Link-Local samt Metadaten-Endpunkt, CGNAT, Multicast, IPv4-gemappte IPv6-Adressen; Formpruefung; alle drei Umgebungsschalter - Passwoerter: scrypt-Rundlauf, unterschiedliches Salz je Hash, Ablageformat, Verhalten bei kaputten Hashes, Unicode-Normalisierung, Passwortregeln Bewusst ohne Datenbank- und Browsertests - die brauchen Infrastruktur und gehoeren in eine Pipeline. Steht als naechster Punkt in der ROADMAP. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
67 lines
2.6 KiB
TypeScript
67 lines
2.6 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
||
import { PASSWORD_MIN_LENGTH } from "./constants";
|
||
import { hashPassword, validatePasswordStrength, verifyPassword } from "./password";
|
||
|
||
describe("hashPassword und verifyPassword", () => {
|
||
it("erkennt das richtige Passwort wieder", async () => {
|
||
const hash = await hashPassword("ein-langes-testpasswort");
|
||
await expect(verifyPassword("ein-langes-testpasswort", hash)).resolves.toBe(true);
|
||
});
|
||
|
||
it("weist ein falsches Passwort ab", async () => {
|
||
const hash = await hashPassword("ein-langes-testpasswort");
|
||
await expect(verifyPassword("etwas-anderes", hash)).resolves.toBe(false);
|
||
});
|
||
|
||
it("erzeugt fuer dasselbe Passwort unterschiedliche Hashes", async () => {
|
||
// Unterschiedliches Salz – sonst waere an gleichen Hashes ablesbar,
|
||
// wer dasselbe Passwort benutzt.
|
||
const a = await hashPassword("ein-langes-testpasswort");
|
||
const b = await hashPassword("ein-langes-testpasswort");
|
||
expect(a).not.toBe(b);
|
||
});
|
||
|
||
it("haelt das erwartete Ablageformat ein", async () => {
|
||
const hash = await hashPassword("ein-langes-testpasswort");
|
||
expect(hash).toMatch(/^scrypt\$\d+\$\d+\$\d+\$[\w-]+\$[\w-]+$/);
|
||
});
|
||
|
||
it("stuerzt bei kaputten oder fehlenden Hashes nicht ab", async () => {
|
||
await expect(verifyPassword("x", null)).resolves.toBe(false);
|
||
await expect(verifyPassword("x", "")).resolves.toBe(false);
|
||
await expect(verifyPassword("x", "voelliger-unsinn")).resolves.toBe(false);
|
||
await expect(verifyPassword("x", "scrypt$a$b$c$d$e")).resolves.toBe(false);
|
||
});
|
||
|
||
it("behandelt gleichwertige Unicode-Schreibweisen gleich", async () => {
|
||
// "é" als ein Zeichen und als e + Akzent kombiniert.
|
||
const hash = await hashPassword("passwort-café-lang");
|
||
await expect(verifyPassword("passwort-café-lang", hash)).resolves.toBe(true);
|
||
});
|
||
});
|
||
|
||
describe("validatePasswordStrength", () => {
|
||
it("nimmt ein ausreichend langes Passwort an", () => {
|
||
expect(validatePasswordStrength("ein-langes-testpasswort")).toBeNull();
|
||
});
|
||
|
||
it("weist zu kurze Passwoerter ab", () => {
|
||
expect(validatePasswordStrength("a".repeat(PASSWORD_MIN_LENGTH - 1))).toMatch(
|
||
/mindestens/,
|
||
);
|
||
});
|
||
|
||
it("weist naheliegende Passwoerter ab", () => {
|
||
expect(validatePasswordStrength("password")).toBeTruthy();
|
||
expect(validatePasswordStrength("Passwort")).toBeTruthy();
|
||
});
|
||
|
||
it("weist reine Ziffernfolgen ab", () => {
|
||
expect(validatePasswordStrength("1234567890123")).toMatch(/Ziffern/);
|
||
});
|
||
|
||
it("weist uebermaessig lange Passwoerter ab", () => {
|
||
expect(validatePasswordStrength("a".repeat(300))).toMatch(/hoechstens/);
|
||
});
|
||
});
|