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>
153 lines
5.3 KiB
TypeScript
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);
|
|
});
|
|
});
|