import { describe, expect, it } from "@suaveplan/testing/runner"; import type { Rng } from "@suaveplan/types"; import { DIGITS, SYMBOLS } from "../charsets/charsets.js"; import { EFF_LONG_WORDLIST } from "../wordlists/eff-long.js"; import { EFF_SHORT_WORDLIST } from "../wordlists/eff-short.js"; import { generatePassphrase } from "./passphrase.js"; function seededRng(seed: number): Rng { let s = seed >>> 0; return { nextUint32(): number { s = (s + 0x6d2b79f5) >>> 0; let t = s; t = Math.imul(t ^ (t >>> 15), t | 1); t ^= t + Math.imul(t ^ (t >>> 7), t | 61); return ((t ^ (t >>> 14)) >>> 0) >>> 0; }, nextFloat(): number { return this.nextUint32() / 0x1_0000_0000; }, }; } describe("generatePassphrase", () => { it("defaults to EFF Long with length 6", { timeout: 5000 }, () => { const pp = generatePassphrase({}); const words = pp.split("-"); expect(words.length).toBe(6); for (const w of words) expect(EFF_LONG_WORDLIST.includes(w)).toBe(true); }); it("draws from EFF Short when wordlist is short", { timeout: 5000 }, () => { const pp = generatePassphrase({ wordlist: "short", length: 7 }); const words = pp.split("-"); expect(words.length).toBe(7); for (const w of words) expect(EFF_SHORT_WORDLIST.includes(w)).toBe(true); }); it("accepts inline wordlist arrays meeting the min-size guard", { timeout: 5000, }, () => { const inline = Array.from({ length: 256 }, (_, i) => `w${i}`); const pp = generatePassphrase({ wordlist: inline, length: 4 }); const words = pp.split("-"); expect(words.length).toBe(4); for (const w of words) expect(inline.includes(w)).toBe(true); }); it("rejects inline wordlist arrays below 256 entries", { timeout: 5000, }, () => { expect(() => generatePassphrase({ wordlist: ["one", "two"], length: 2 }) ).toThrow(/PWGEN\.WORDLIST_TOO_SHORT/); }); it("capitalize firstChar applies to every word", { timeout: 5000 }, () => { const pp = generatePassphrase({ length: 4, capitalize: "firstChar" }); const words = pp.split("-"); for (const w of words) { expect(w.charAt(0)).toBe(w.charAt(0).toUpperCase()); expect(/^[A-Z]/.test(w)).toBe(true); } }); it("capitalize randomWord applies to exactly one word", { timeout: 5000, }, () => { const pp = generatePassphrase( { length: 4, capitalize: "randomWord" }, seededRng(11) ); const words = pp.split("-"); const upperCount = words.filter((w) => /^[A-Z]/.test(w)).length; expect(upperCount).toBe(1); }); it("addDigits and addSymbols inject the requested counts", { timeout: 5000, }, () => { // Use a separator outside both DIGITS and SYMBOLS so the injected // counts are not confounded by the separator characters. const pp = generatePassphrase({ length: 3, addDigits: 2, addSymbols: 1, separator: " ", }); const digitCount = [...pp].filter((c) => DIGITS.has(c)).length; const symbolCount = [...pp].filter((c) => SYMBOLS.has(c)).length; expect(digitCount).toBe(2); expect(symbolCount).toBe(1); }); it("uses a custom separator", { timeout: 5000 }, () => { const pp = generatePassphrase({ length: 4, separator: "." }); expect(pp.split(".").length).toBe(4); }); it("is deterministic under seeded Rng", { timeout: 5000 }, () => { const a = generatePassphrase( { length: 6, capitalize: "firstChar" }, seededRng(42) ); const b = generatePassphrase( { length: 6, capitalize: "firstChar" }, seededRng(42) ); expect(a).toBe(b); }); it("rejects invalid input with PWGEN.INVALID_OPTIONS", { timeout: 5000, }, () => { expect(() => generatePassphrase({ length: 0 })).toThrow( /PWGEN\.INVALID_OPTIONS/ ); }); it("resolves wordlist via registry name", { timeout: 5000 }, () => { const pp = generatePassphrase({ wordlist: "eff-short", length: 4 }); const words = pp.split("-"); for (const w of words) expect(EFF_SHORT_WORDLIST.includes(w)).toBe(true); }); it("works with no opts argument", { timeout: 5000 }, () => { const pp = generatePassphrase(); expect(pp.split("-").length).toBe(6); }); it("applies capitalize none by default", { timeout: 5000 }, () => { const pp = generatePassphrase({ length: 4 }); const words = pp.split("-"); for (const w of words) expect(/^[a-z]/.test(w)).toBe(true); }); });