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