generate.empty-pool.test.ts

5.7 KB · raw

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 "../../shared/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});