generate.empty-pool.test.ts

3.9 KB · raw

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});