import assert from "node:assert/strict"; import test from "node:test"; import { BIO_MAX_LENGTH, DISCRIMINATOR_ALPHABET, DISCRIMINATOR_LENGTH, formatHandle, generateDiscriminator, generateUniqueDiscriminator, handleToSlug, parseHandleSlug, validateUsername, } from "../lib/handle"; /** Liefert eine deterministische Random-Funktion, die der Reihe nach `values` ausgibt. */ function seededRandom(values: number[]): () => number { let i = 0; return () => values[i++ % values.length]; } test("generateDiscriminator hat die konfigurierte Länge", () => { assert.equal(generateDiscriminator().length, DISCRIMINATOR_LENGTH); }); test("generateDiscriminator nutzt nur Zeichen aus dem Alphabet", () => { for (let i = 0; i < 200; i++) { for (const ch of generateDiscriminator()) { assert.ok(DISCRIMINATOR_ALPHABET.includes(ch), `unerwartetes Zeichen: ${ch}`); } } }); test("Alphabet enthält keine verwechselbaren Zeichen (0 O 1 I L)", () => { for (const forbidden of ["0", "O", "1", "I", "L"]) { assert.ok(!DISCRIMINATOR_ALPHABET.includes(forbidden), `verboten: ${forbidden}`); } }); test("generateDiscriminator ist deterministisch bei fester Random-Quelle", () => { // random()=0 -> immer erstes Zeichen des Alphabets assert.equal( generateDiscriminator(() => 0), DISCRIMINATOR_ALPHABET[0].repeat(DISCRIMINATOR_LENGTH), ); }); test("generateUniqueDiscriminator umgeht bereits vergebene Discriminatoren", () => { const first = DISCRIMINATOR_ALPHABET[0].repeat(DISCRIMINATOR_LENGTH); const second = DISCRIMINATOR_ALPHABET[1] + DISCRIMINATOR_ALPHABET[0].repeat(DISCRIMINATOR_LENGTH - 1); // Erster Versuch kollidiert (alles idx 0), zweiter Versuch erstes Zeichen = idx 1 const random = seededRandom([0, 0, 0, 0, 1 / DISCRIMINATOR_ALPHABET.length, 0, 0, 0]); assert.equal(generateUniqueDiscriminator([first], random), second); }); test("generateUniqueDiscriminator ignoriert Groß-/Kleinschreibung", () => { const taken = DISCRIMINATOR_ALPHABET[0].repeat(DISCRIMINATOR_LENGTH).toLowerCase(); const random = seededRandom([0, 0, 0, 0, 1 / DISCRIMINATOR_ALPHABET.length, 0, 0, 0]); const result = generateUniqueDiscriminator([taken], random); assert.notEqual(result, DISCRIMINATOR_ALPHABET[0].repeat(DISCRIMINATOR_LENGTH)); }); test("generateUniqueDiscriminator wirft, wenn nach maxAttempts kein freier gefunden wird", () => { const onlyZero = () => 0; // erzeugt immer denselben Discriminator const taken = [DISCRIMINATOR_ALPHABET[0].repeat(DISCRIMINATOR_LENGTH)]; assert.throws(() => generateUniqueDiscriminator(taken, onlyZero, 10)); }); test("formatHandle baut Username#Discriminator", () => { assert.equal(formatHandle("Held", "A23B"), "Held#A23B"); }); test("formatHandle liefert null ohne Username", () => { assert.equal(formatHandle(null, "A23B"), null); }); test("formatHandle liefert null ohne Discriminator", () => { assert.equal(formatHandle("Held", null), null); }); test("validateUsername akzeptiert gültige Namen und trimmt", () => { assert.deepEqual(validateUsername(" Held_2 "), { ok: true, value: "Held_2" }); }); test("validateUsername lehnt zu kurze Namen ab", () => { assert.equal(validateUsername("ab").ok, false); }); test("validateUsername lehnt zu lange Namen ab", () => { assert.equal(validateUsername("a".repeat(25)).ok, false); }); test("validateUsername lehnt '#' und Leerzeichen ab", () => { assert.equal(validateUsername("Held#1").ok, false); assert.equal(validateUsername("Held Held").ok, false); }); test("validateUsername lehnt Umlaute ab (Handle bleibt URL-tauglich)", () => { assert.equal(validateUsername("Hähnchen").ok, false); }); test("Bio-Limit ist 280", () => { assert.equal(BIO_MAX_LENGTH, 280); }); test("handleToSlug baut Username-Discriminator (URL-tauglich, kein #)", () => { assert.equal(handleToSlug("KochHeld", "A23B"), "KochHeld-A23B"); }); test("handleToSlug liefert null ohne vollstaendigen Handle", () => { assert.equal(handleToSlug(null, "A23B"), null); assert.equal(handleToSlug("KochHeld", null), null); }); test("parseHandleSlug zerlegt am letzten Bindestrich", () => { assert.deepEqual(parseHandleSlug("KochHeld-A23B"), { username: "KochHeld", discriminator: "A23B", }); }); test("parseHandleSlug behaelt Bindestriche im Username", () => { assert.deepEqual(parseHandleSlug("Koch-Held-A23B"), { username: "Koch-Held", discriminator: "A23B", }); }); test("parseHandleSlug normalisiert den Discriminator auf Grossschreibung", () => { assert.deepEqual(parseHandleSlug("KochHeld-a23b"), { username: "KochHeld", discriminator: "A23B", }); }); test("parseHandleSlug lehnt ungueltige Slugs ab", () => { assert.equal(parseHandleSlug("KochHeld"), null); // kein Discriminator assert.equal(parseHandleSlug("KochHeld-"), null); // leerer Discriminator assert.equal(parseHandleSlug("-A23B"), null); // leerer Username assert.equal(parseHandleSlug("KochHeld-A1"), null); // Discriminator zu kurz assert.equal(parseHandleSlug("KochHeld-A1IB"), null); // I nicht im Alphabet }); test("handleToSlug und parseHandleSlug sind invers", () => { const slug = handleToSlug("Koch-Held", "A23B"); assert.ok(slug); assert.deepEqual(parseHandleSlug(slug), { username: "Koch-Held", discriminator: "A23B", }); });