generate.empty-pool.test.ts
1// T-025: generate.ts empty-pool guard, 128-char boundary, minLength > maxLength2//3// Why these tests exist: generatePassword and generateSmartPassword have three4// edge-case behaviors that are not tested: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. 128-character limit (raised in v1.8.0) — the generator must produce11// exactly 128 characters when length=128 and must handle the boundary12// without a performance cliff or off-by-one.13//14// 3. minLength > maxLength in a site policy — generate.ts clamps in15// declaration order: raise to minLength first, then cap to maxLength.16// When minLength=50 and maxLength=16, the result must be exactly 16 chars.17//18// These tests also serve as a regression guard against any future refactor of19// the rejection-sampling loop: if the loop condition changes, the 128-char20// case catches a potential performance regression (loop would run for 30+s).2122import { describe, expect, it } from "vitest";23import { generatePassword, generateSmartPassword } from "./generate";2425describe("T-025: generatePassword edge cases", () => {26 // T-025a: all character classes excluded via forbiddenChars → falls back to27 // lowercase alphabet, no infinite loop, no error.28 //29 // Bug this catches: removing the `|| "abcde..."` fallback in the empty-pool30 // guard would cause an infinite loop (the while loop never fills the length).31 it("T-025a: all chars forbidden via forbiddenChars → falls back to lowercase, no hang", () => {32 // Forbid every uppercase, digit, and symbol; force-enable all classes.33 // Only lowercase survives.34 const allExceptLower = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*";35 const password = generatePassword({36 length: 10,37 includeUppercase: true,38 includeLowercase: true,39 includeNumbers: true,40 includeSymbols: true,41 forbiddenChars: allExceptLower,42 });4344 expect(typeof password).toBe("string");45 expect(password.length).toBe(10);46 // Remaining chars should only be lowercase47 expect(password).toMatch(/^[a-z]+$/);48 });4950 // T-025b: all character classes disabled (no uppercase, no lowercase, no51 // numbers, no symbols) — the pool would be empty, so the fallback ensures52 // the generator produces something usable.53 it("T-025b: all boolean classes disabled → fallback lowercase used, no hang", () => {54 const password = generatePassword({55 length: 8,56 includeUppercase: false,57 includeLowercase: false,58 includeNumbers: false,59 includeSymbols: false,60 });6162 expect(typeof password).toBe("string");63 expect(password.length).toBe(8);64 // Fallback to lowercase: output should be alphabetic65 expect(password).toMatch(/^[a-z]+$/);66 });6768 // T-025c: 128-character password generated without performance cliff.69 // v1.8.0 raised the maximum to 128 chars. The rejection-sampling loop70 // must complete in reasonable time (the test has a 2-second implicit timeout).71 //72 // Bug this catches: if the rejection-sampling limit calculation overflows73 // or produces 0 for large char pools, every CSPRNG value is rejected and74 // the loop runs indefinitely.75 it("T-025c: length=128 produces exactly 128 characters without hanging", () => {76 const password = generatePassword({77 length: 128,78 includeUppercase: true,79 includeLowercase: true,80 includeNumbers: true,81 includeSymbols: true,82 });8384 expect(password.length).toBe(128);85 });8687 // T-025d: length=1 (minimum sensible boundary).88 it("T-025d: length=1 produces exactly 1 character", () => {89 const password = generatePassword({ length: 1 });90 expect(password.length).toBe(1);91 });9293 // T-025e: output is CSPRNG-based; calling twice produces different results.94 // Not a determinism guarantee, but a sanity check that the output is not95 // a hardcoded/constant string.96 it("T-025e: two calls produce different passwords (CSPRNG is live)", () => {97 const a = generatePassword({ length: 32 });98 const b = generatePassword({ length: 32 });99 // Collision probability: 1/(pool^32) ≈ 0 for any reasonable pool100 expect(a).not.toBe(b);101 });102});103104describe("T-025: generateSmartPassword policy clamping", () => {105 // T-025f: minLength > maxLength — generateSmartPassword clamps to maxLength.106 // The evaluation order is: raise to minLength first (→ 50), then cap to107 // maxLength (→ 16). Site policy maxLength must win.108 //109 // Bug this catches: swapping the clamp order so minLength wins over maxLength110 // would produce a password that violates the site's stated maximum, causing111 // sign-up failure on strict sites.112 it("T-025f: minLength > maxLength → maxLength wins (site maximum clamped)", () => {113 const password = generateSmartPassword({ minLength: 50, maxLength: 16 });114 expect(password.length).toBeLessThanOrEqual(16);115 });116117 // T-025g: maxLength below the safety floor of 8 → floor applies.118 // generateSmartPassword has `if (length < 8) length = 8` to prevent119 // trivially weak passwords being auto-generated for restrictive sites.120 it("T-025g: maxLength=4 → safety floor raises output to at least 8 chars", () => {121 const password = generateSmartPassword({ maxLength: 4 });122 expect(password.length).toBeGreaterThanOrEqual(8);123 });124125 // T-025h: forbidsSymbols=true → no symbol characters in output.126 it("T-025h: forbidsSymbols=true → no !@#$%^&* in password", () => {127 const password = generateSmartPassword({ forbidsSymbols: true, minLength: 20 });128 expect(password).not.toMatch(/[!@#$%^&*]/);129 });130});