import { describe, it, expect } from 'vitest'; import { generatePassword } from './password-gen'; const CHARSET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!#$%&*+-?@'; describe('generatePassword', () => { it('rejects lengths under 8', () => { expect(() => generatePassword(7)).toThrowError(/at least 8/); }); it('respects the requested length', () => { for (const len of [8, 16, 32, 64]) { expect(generatePassword(len)).toHaveLength(len); } }); it('uses only characters from the documented charset', () => { const set = new Set(CHARSET); for (let i = 0; i < 1000; i++) { for (const c of generatePassword(32)) { expect(set.has(c)).toBe(true); } } }); // Rejection-sampling sanity. With N = 71 the expected count per // char over 100k samples is ~1408 (σ ≈ 37). A 6σ band catches // any byte-level bias (biased modulo would push the first 38 // chars by ~16 ppm — too small for this band to flag on its // own, but a regression to `% N` over Uint16/Uint32 with a // non-power-of-two charset would still produce visible drift in // pathological codepaths). Mostly this guards against // fundamental mistakes (off-by-one in the loop, returning the // same byte stream every time, etc.). it('distribution stays within a wide tolerance band', () => { const samples = 100_000; const counts = new Map(); for (let i = 0; i < samples; i++) { const c = generatePassword(8)[0]; counts.set(c, (counts.get(c) ?? 0) + 1); } const expected = samples / CHARSET.length; const sigma = Math.sqrt(expected); const band = 6 * sigma; for (const c of CHARSET) { const observed = counts.get(c) ?? 0; const drift = Math.abs(observed - expected); expect( drift, `char "${c}": observed ${observed}, expected ~${Math.round(expected)} (drift ${drift.toFixed(0)} > ${band.toFixed(0)})` ).toBeLessThan(band); } }); });