Initial Commit
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user