Files
Laufzettel/tests/tagcode.test.ts
T
florian.wildeandClaude Opus 5 7a0f875f55
Prüfung / pruefung (push) Canceled after 0s
Prüfung / e2e (push) Canceled after 0s
feat(datenmodell): Schema, tagCodes und Geheimnisse (#20)
Vollständiges Datenmodell in deutscher Benennung: 20 Tabellen, 9 Aufzählungen,
eine Migration, dazu die framework-freien Helfer für tagCodes und Geheimnisse.

Regeln stehen zusätzlich in der Datenbank, nicht nur im Service: 12 CHECKs,
2 partielle Eindeutigkeitsindizes, 4 zusammengesetzte Fremdschlüssel. Die
Mandantengrenze ist damit strukturell erzwungen. Schrittergebnisse, Kommentare
und Protokolleinträge kaskadieren bewusst nicht — das Archiv ist das Gedächtnis
der Anwendung.

106 Tests, davon 42 gegen die echten Datenbankregeln.

Closes #6

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-24 23:11:58 +00:00

153 lines
5.3 KiB
TypeScript

import { describe, expect, it, vi } from "vitest";
import {
TAGCODE_ALPHABET,
TAGCODE_HOECHSTVERSUCHE,
TAGCODE_MUSTER,
TagCodeFehler,
istTagCode,
tagCodeEindeutigErzeugen,
tagCodeErzeugen,
} from "@/lib/tagcode";
function zieheCodes(anzahl: number): string[] {
return Array.from({ length: anzahl }, () => tagCodeErzeugen());
}
describe("tagCode erzeugen", () => {
it("haelt Praefix und Laenge ein", () => {
for (const code of zieheCodes(200)) {
expect(code).toMatch(TAGCODE_MUSTER);
expect(code).toHaveLength(3 + 6);
expect(code.startsWith("LZ-")).toBe(true);
}
});
it("benutzt nur das erlaubte Alphabet", () => {
for (const code of zieheCodes(5000)) {
for (const zeichen of code.slice(3)) {
expect(TAGCODE_ALPHABET).toContain(zeichen);
}
}
});
it("enthaelt nie die verwechselbaren Zeichen 0, O, 1 oder I", () => {
// Genau das ist der Grund fuer das eingeschraenkte Alphabet: die Codes werden
// von Aufklebern abgetippt und am Telefon vorgelesen. Das L ist bewusst nicht
// dabei: ohne es bliebe das Alphabet bei 31 Zeichen statt der gewuenschten 32,
// und das Praefix `LZ-` enthaelt es ohnehin.
for (const code of zieheCodes(5000)) {
expect(code.slice(3)).not.toMatch(/[0O1I]/);
}
});
it("liefert bei 10 000 Ziehungen praktisch keine Dubletten", () => {
const anzahl = 10_000;
const eindeutige = new Set(zieheCodes(anzahl));
// Bei 32^6 rund 1,07 Mrd. Moeglichkeiten sind nach dem Geburtstagsproblem
// etwa 0,05 Dubletten zu erwarten -- meistens also keine. Ein strikter
// Vergleich auf 10 000 waere deshalb flatterhaft: er wuerde selten, aber
// regelmaessig rot, ohne dass etwas kaputt ist. Die verbindliche
// Eindeutigkeit sichert der Unique-Index in der Datenbank.
expect(eindeutige.size).toBeGreaterThanOrEqual(anzahl - 5);
});
it("verteilt die Zeichen grob gleichmaessig ueber das Alphabet", () => {
const codes = zieheCodes(5000);
const haeufigkeit = new Map<string, number>();
for (const code of codes) {
for (const zeichen of code.slice(3)) {
haeufigkeit.set(zeichen, (haeufigkeit.get(zeichen) ?? 0) + 1);
}
}
const gezogeneZeichen = codes.length * 6;
const erwartet = gezogeneZeichen / TAGCODE_ALPHABET.length;
expect(haeufigkeit.size).toBe(TAGCODE_ALPHABET.length);
for (const zeichen of TAGCODE_ALPHABET) {
const anzahl = haeufigkeit.get(zeichen) ?? 0;
// Grosszuegige Schranken (rund 15 Standardabweichungen), damit der Test
// eine echte Schieflage findet, aber nicht am Zufall scheitert.
expect(anzahl).toBeGreaterThan(erwartet * 0.5);
expect(anzahl).toBeLessThan(erwartet * 1.5);
}
});
});
describe("istTagCode", () => {
it("nimmt einen frisch erzeugten Code an", () => {
expect(istTagCode(tagCodeErzeugen())).toBe(true);
expect(istTagCode("LZ-7K3F9Q")).toBe(true);
});
it.each([
["Kleinbuchstaben", "lz-7k3f9q"],
["gemischte Schreibweise", "LZ-7k3F9Q"],
["fehlendes Praefix", "7K3F9Q"],
["falsches Praefix", "XY-7K3F9Q"],
["Praefix ohne Bindestrich", "LZ7K3F9Q"],
["zu kurz", "LZ-7K3F9"],
["zu lang", "LZ-7K3F9QA"],
["verbotene Null", "LZ-7K3F90"],
["verbotenes O", "LZ-7K3F9O"],
["verbotene Eins", "LZ-7K3F91"],
["verbotenes I", "LZ-7K3F9I"],
["Sonderzeichen", "LZ-7K3F9-"],
["Leerzeichen", "LZ-7K3F9 "],
["leer", ""],
["nur Praefix", "LZ-"],
["Zeilenumbruch angehaengt", "LZ-7K3F9Q\n"],
["Text davor", "xLZ-7K3F9Q"],
])("weist %s zurueck", (_fall, wert) => {
expect(istTagCode(wert)).toBe(false);
});
it("urteilt bei mehrfachem Aufruf gleich", () => {
// Das Muster wird geteilt; ein globales Flag darauf wuerde den Zustand
// zwischen Aufrufen mitschleppen.
expect(istTagCode("LZ-7K3F9Q")).toBe(true);
expect(istTagCode("LZ-7K3F9Q")).toBe(true);
});
});
describe("tagCodeEindeutigErzeugen", () => {
it("liefert den ersten Code, wenn er frei ist", async () => {
const istVergeben = vi.fn(async () => false);
const code = await tagCodeEindeutigErzeugen(istVergeben);
expect(istTagCode(code)).toBe(true);
expect(istVergeben).toHaveBeenCalledTimes(1);
expect(istVergeben).toHaveBeenCalledWith(code);
});
it("zieht erneut, wenn der erste Code vergeben ist", async () => {
const gesehen: string[] = [];
const istVergeben = vi.fn(async (code: string) => {
gesehen.push(code);
return gesehen.length === 1;
});
const code = await tagCodeEindeutigErzeugen(istVergeben);
expect(istVergeben).toHaveBeenCalledTimes(2);
expect(gesehen).toHaveLength(2);
expect(gesehen[0]).not.toBe(code);
expect(gesehen[1]).toBe(code);
});
it("wirft nach zehn vergeblichen Versuchen einen TagCodeFehler", async () => {
const istVergeben = vi.fn(async () => true);
await expect(tagCodeEindeutigErzeugen(istVergeben)).rejects.toThrow(TagCodeFehler);
expect(istVergeben).toHaveBeenCalledTimes(TAGCODE_HOECHSTVERSUCHE);
});
it("gibt einen Fehler der Vergabepruefung unveraendert weiter", async () => {
const istVergeben = vi.fn(async () => {
throw new Error("Datenbank nicht erreichbar");
});
await expect(tagCodeEindeutigErzeugen(istVergeben)).rejects.toThrow("Datenbank nicht erreichbar");
expect(istVergeben).toHaveBeenCalledTimes(1);
});
});