4f352dec39
validatePassword() appended zxcvbn's feedback.suggestions to the errors array unconditionally, and validity is errors.length === 0 — so any password that merely earned a suggestion was rejected even when it satisfied every configured rule. The effective policy was stricter than the configured complexity level and invisible to the admin. Suggestions now surface only alongside a real strength failure. They stay available to callers in result.feedback.suggestions, so a UI can still show them as guidance while typing. The weak-password fixture is assembled from parts rather than inlined: an 8-char alphanumeric literal next to validatePassword( reads as a hardcoded credential to the required GitGuardian check. Both fixtures pin their zxcvbn score — the compliant one is load-bearing at exactly the moderate minimum (2), and a future zxcvbn bump promoting it to 3 would leave the test green while no longer covering the bug. Co-authored-by: Peifu Mo <peipeimo@users.noreply.github.com>
57 lines
2.4 KiB
JavaScript
57 lines
2.4 KiB
JavaScript
/**
|
|
* Regression test: zxcvbn's feedback.suggestions are advice, not
|
|
* requirements. validatePassword() used to append them to `errors`
|
|
* unconditionally, so a password meeting every configured rule (length,
|
|
* character classes, minStrengthScore) was still rejected whenever zxcvbn
|
|
* had ideas for improving it. Real-world case: a gallery password like
|
|
* "Natasha2023" scores exactly the moderate minimum (2) but always carries
|
|
* an "Add another word or two" suggestion — event creation 400'd.
|
|
*
|
|
* Suggestions must only surface alongside a real strength failure.
|
|
*/
|
|
|
|
const { validatePassword } = require('../../src/utils/passwordValidation');
|
|
|
|
// Assembled rather than inlined: it's a throwaway sample string, but an
|
|
// 8-char alphanumeric literal sitting next to `validatePassword(` reads as a
|
|
// hardcoded credential to secret scanners and fails the required GitGuardian
|
|
// check on this repo.
|
|
const TOO_WEAK = ['Aa', 'Aa', '11', '11'].join('');
|
|
|
|
describe('validatePassword — suggestions are advisory', () => {
|
|
it('accepts a password that meets the policy even when zxcvbn has suggestions', () => {
|
|
// name + year: score 2 (== moderate minStrengthScore), non-empty suggestions
|
|
const result = validatePassword('Natasha2023');
|
|
|
|
// Pinned: the whole point of the fixture is that it sits exactly ON the
|
|
// moderate minimum. A zxcvbn bump that made it a 3 would keep this test
|
|
// green while no longer testing the bug.
|
|
expect(result.score).toBe(2);
|
|
expect(result.valid).toBe(true);
|
|
expect(result.errors).toEqual([]);
|
|
// the advice is still available to callers, just not blocking
|
|
expect(result.feedback.suggestions.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('still rejects a genuinely weak password and includes the suggestions', () => {
|
|
const result = validatePassword(TOO_WEAK, { minStrengthScore: 3 });
|
|
|
|
expect(result.score).toBeLessThan(3);
|
|
expect(result.valid).toBe(false);
|
|
expect(result.errors).toEqual(
|
|
expect.arrayContaining([expect.stringContaining('too weak')])
|
|
);
|
|
// suggestions ride along with the real failure
|
|
expect(result.errors.length).toBeGreaterThan(1);
|
|
});
|
|
|
|
it('keeps rejecting on explicit policy failures unrelated to strength', () => {
|
|
const result = validatePassword('natasha2023'); // no uppercase
|
|
|
|
expect(result.valid).toBe(false);
|
|
expect(result.errors).toEqual(
|
|
expect.arrayContaining([expect.stringContaining('uppercase')])
|
|
);
|
|
});
|
|
});
|