generate.empty-pool.test.ts
1// T-025: generate.ts empty-pool guard and maximum-length boundary2//3// Why these tests exist: generatePassword has two edge-case behaviors that are4// not tested elsewhere:5//6// 1. All character classes forbidden → the pool falls back to lowercase7// ("abcdefghijklmnopqrstuvwxyz") rather than looping forever or throwing.8// (line 38: `if (chars.length === 0) chars = filter(...) || "abc..."`)9//10// 2. 256-character limit — the generator must produce exactly 25611// characters and must handle the boundary12// without a performance cliff or off-by-one.13//14// These tests also serve as a regression guard against any future refactor of15// the rejection-sampling loop: if the loop condition changes, the 128-char16// case catches a potential performance regression (loop would run for 30+s).1718import { describe, expect, it } from "vitest";19import { generatePassword } from "./generate";2021describe("T-025: generatePassword edge cases", () => {22 // T-025a: all character classes excluded via forbiddenChars → falls back to23 // lowercase alphabet, no infinite loop, no error.24 //25 // Bug this catches: removing the `|| "abcde..."` fallback in the empty-pool26 // guard would cause an infinite loop (the while loop never fills the length).27 it("T-025a: all chars forbidden via forbiddenChars → falls back to lowercase, no hang", () => {28 // Forbid every uppercase, digit, and symbol; force-enable all classes.29 // Only lowercase survives.30 const allExceptLower = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*";31 const password = generatePassword({32 length: 10,33 includeUppercase: true,34 includeLowercase: true,35 includeNumbers: true,36 includeSymbols: true,37 forbiddenChars: allExceptLower,38 });3940 expect(typeof password).toBe("string");41 expect(password.length).toBe(10);42 // Remaining chars should only be lowercase43 expect(password).toMatch(/^[a-z]+$/);44 });4546 // T-025b: all character classes disabled (no uppercase, no lowercase, no47 // numbers, no symbols) — the pool would be empty, so the fallback ensures48 // the generator produces something usable.49 it("T-025b: all boolean classes disabled → fallback lowercase used, no hang", () => {50 const password = generatePassword({51 length: 8,52 includeUppercase: false,53 includeLowercase: false,54 includeNumbers: false,55 includeSymbols: false,56 });5758 expect(typeof password).toBe("string");59 expect(password.length).toBe(8);60 // Fallback to lowercase: output should be alphabetic61 expect(password).toMatch(/^[a-z]+$/);62 });6364 // T-025c: 256-character password generated without performance cliff.65 // The rejection-sampling loop66 // must complete in reasonable time (the test has a 2-second implicit timeout).67 //68 // Bug this catches: if the rejection-sampling limit calculation overflows69 // or produces 0 for large char pools, every CSPRNG value is rejected and70 // the loop runs indefinitely.71 it("T-025c: length=256 produces exactly 256 characters without hanging", () => {72 const password = generatePassword({73 length: 256,74 includeUppercase: true,75 includeLowercase: true,76 includeNumbers: true,77 includeSymbols: true,78 });7980 expect(password.length).toBe(256);81 });8283 // T-025d: length=1 (minimum sensible boundary).84 it("T-025d: length=1 produces exactly 1 character", () => {85 const password = generatePassword({ length: 1 });86 expect(password.length).toBe(1);87 });8889 // T-025e: output is CSPRNG-based; calling twice produces different results.90 // Not a determinism guarantee, but a sanity check that the output is not91 // a hardcoded/constant string.92 it("T-025e: two calls produce different passwords (CSPRNG is live)", () => {93 const a = generatePassword({ length: 32 });94 const b = generatePassword({ length: 32 });95 // Collision probability: 1/(pool^32) ≈ 0 for any reasonable pool96 expect(a).not.toBe(b);97 });98});