// T-025: generate.ts empty-pool guard and 128-char boundary // // Why these tests exist: generatePassword has two edge-case behaviors that are // not tested elsewhere: // // 1. All character classes forbidden → the pool falls back to lowercase // ("abcdefghijklmnopqrstuvwxyz") rather than looping forever or throwing. // (line 38: `if (chars.length === 0) chars = filter(...) || "abc..."`) // // 2. 128-character limit (raised in v1.8.0) — the generator must produce // exactly 128 characters when length=128 and must handle the boundary // without a performance cliff or off-by-one. // // These tests also serve as a regression guard against any future refactor of // the rejection-sampling loop: if the loop condition changes, the 128-char // case catches a potential performance regression (loop would run for 30+s). import { describe, expect, it } from "vitest"; import { generatePassword } from "./generate"; describe("T-025: generatePassword edge cases", () => { // T-025a: all character classes excluded via forbiddenChars → falls back to // lowercase alphabet, no infinite loop, no error. // // Bug this catches: removing the `|| "abcde..."` fallback in the empty-pool // guard would cause an infinite loop (the while loop never fills the length). it("T-025a: all chars forbidden via forbiddenChars → falls back to lowercase, no hang", () => { // Forbid every uppercase, digit, and symbol; force-enable all classes. // Only lowercase survives. const allExceptLower = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*"; const password = generatePassword({ length: 10, includeUppercase: true, includeLowercase: true, includeNumbers: true, includeSymbols: true, forbiddenChars: allExceptLower, }); expect(typeof password).toBe("string"); expect(password.length).toBe(10); // Remaining chars should only be lowercase expect(password).toMatch(/^[a-z]+$/); }); // T-025b: all character classes disabled (no uppercase, no lowercase, no // numbers, no symbols) — the pool would be empty, so the fallback ensures // the generator produces something usable. it("T-025b: all boolean classes disabled → fallback lowercase used, no hang", () => { const password = generatePassword({ length: 8, includeUppercase: false, includeLowercase: false, includeNumbers: false, includeSymbols: false, }); expect(typeof password).toBe("string"); expect(password.length).toBe(8); // Fallback to lowercase: output should be alphabetic expect(password).toMatch(/^[a-z]+$/); }); // T-025c: 128-character password generated without performance cliff. // v1.8.0 raised the maximum to 128 chars. The rejection-sampling loop // must complete in reasonable time (the test has a 2-second implicit timeout). // // Bug this catches: if the rejection-sampling limit calculation overflows // or produces 0 for large char pools, every CSPRNG value is rejected and // the loop runs indefinitely. it("T-025c: length=128 produces exactly 128 characters without hanging", () => { const password = generatePassword({ length: 128, includeUppercase: true, includeLowercase: true, includeNumbers: true, includeSymbols: true, }); expect(password.length).toBe(128); }); // T-025d: length=1 (minimum sensible boundary). it("T-025d: length=1 produces exactly 1 character", () => { const password = generatePassword({ length: 1 }); expect(password.length).toBe(1); }); // T-025e: output is CSPRNG-based; calling twice produces different results. // Not a determinism guarantee, but a sanity check that the output is not // a hardcoded/constant string. it("T-025e: two calls produce different passwords (CSPRNG is live)", () => { const a = generatePassword({ length: 32 }); const b = generatePassword({ length: 32 }); // Collision probability: 1/(pool^32) ≈ 0 for any reasonable pool expect(a).not.toBe(b); }); });