From 777fb3d1caffe000c7634a6f415bd0c9ad6b16a4 Mon Sep 17 00:00:00 2001 From: ilan-kushnir-payu-gpo Date: Wed, 27 Nov 2024 17:07:21 +0200 Subject: [PATCH] feat: add max length custom matcher * feat: get feedback from translations * feat: add translations * chore: update readme * refactor: change patterns names * chore: update README * chore: update README * tests: update test matcher strings * fix: update translations + suggestions * tests: add translations assertions * fix: minor bug fix * fix: minor bug fix * chore: update readme * chore: update readme * fix: update matcher suggestions * fix: add v4 beta as peer dependenvy * chore: update readme regarding version * fix: add translated suggestions to matchers * tests: refactor tests to go through the zxcvbn-ts/core package * tests: add suggestions assertions * tests: remove redundant lines * feat: add max length matcher * tests: max length unit tests --------- Co-authored-by: Ilan Kushnir --- README.md | 11 +++++++++- src/index.ts | 1 + src/matchers/maxLengthMatcher.ts | 22 +++++++++++++++++++ src/translations.ts | 2 ++ tests/unit/matchers.test.ts | 36 +++++++++++++++++++++++++++----- 5 files changed, 66 insertions(+), 6 deletions(-) create mode 100644 src/matchers/maxLengthMatcher.ts diff --git a/README.md b/README.md index b464b4d..e50859c 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ import { lowercaseMatcher, uppercaseMatcher, minLengthMatcher, + maxLengthMatcher, customMatchersTranslations } from 'zxcvbn-custom-matchers'; import { merge } from 'lodash'; @@ -26,7 +27,8 @@ const options = { translations: merge({}, zxcvbnEnPackage.translations, customMatchersTranslations) }; const customMatchers = { - minLength: minLengthMatcher(commons.MIN_PASSWORD_LENGTH), + minLength: minLengthMatcher(MIN_PASSWORD_LENGTH), + maxLength: maxLengthMatcher(MAX_PASSWORD_LENGTH), specialRequired: specialMatcher, numberRequired: numberMatcher, lowercaseRequired: lowercaseMatcher, @@ -80,3 +82,10 @@ This project includes several matchers that enforce specific character requireme - **Purpose**: Ensures the password meets the minimum length requirement. - **Feedback**: Suggests the password must be at least the specified length if shorter. - **Scoring**: Returns a score of `1` if the password is shorter than the specified length. + +### Maximum Length Matcher + +- **Pattern**: `maxLength` +- **Purpose**: Ensures the password meets the max length requirement. +- **Feedback**: Suggests the password must be at least the specified length if longer. +- **Scoring**: Returns a score of `1` if the password is longer than the specified length. diff --git a/src/index.ts b/src/index.ts index 2d4ea18..e452d40 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,5 @@ export { minLengthMatcher } from './matchers/minLengthMatcher'; +export { maxLengthMatcher } from './matchers/maxLengthMatcher'; export { uppercaseMatcher } from './matchers/uppercaseMatcher'; export { lowercaseMatcher } from './matchers/lowercaseMatcher'; export { numberMatcher } from './matchers/numberMatcher'; diff --git a/src/matchers/maxLengthMatcher.ts b/src/matchers/maxLengthMatcher.ts new file mode 100644 index 0000000..55682c1 --- /dev/null +++ b/src/matchers/maxLengthMatcher.ts @@ -0,0 +1,22 @@ +import { Matcher, Match } from '@zxcvbn-ts/core'; + +export const maxLengthMatcher = (maxLength: number): Matcher => ({ + Matching: class MaxLengthMatcher { + match({ password }: { password: string }): Match[] { + const matches: Match[] = []; + if (password.length > maxLength) { + matches.push({ pattern: 'maxLength', token: password, i: 0, j: password.length - 1 }); + } + return matches; + } + }, + feedback: options => { + return { + warning: options.translations.warnings['maxLength']?.replace('%s', maxLength) || 'maxLength', + suggestions: [options.translations.suggestions['maxLength']?.replace('%s', maxLength) || 'maxLength'], + }; + }, + scoring() { + return -100; + }, +}); diff --git a/src/translations.ts b/src/translations.ts index 5f8cca8..37e6044 100644 --- a/src/translations.ts +++ b/src/translations.ts @@ -2,6 +2,7 @@ export const customMatchersTranslations = { warnings: { lowercaseRequired: 'At least one lowercase letter is required.', minLength: 'Password must be at least %s characters long.', + maxLength: 'Password must be no more than %s characters long.', numberRequired: 'At least one number is required.', specialRequired: 'At least one special character is required.', uppercaseRequired: 'At least one uppercase letter is required.', @@ -9,6 +10,7 @@ export const customMatchersTranslations = { suggestions: { lowercaseRequired: 'Include at least one lowercase letter.', minLength: 'Password may not be shorter than %s characters.', + maxLength: 'Password may not be longer than %s characters.', numberRequired: 'Include at least one number.', specialRequired: 'Include at least one special character.', uppercaseRequired: 'Include at least one uppercase letter.', diff --git a/tests/unit/matchers.test.ts b/tests/unit/matchers.test.ts index 6dfee92..f6256ad 100644 --- a/tests/unit/matchers.test.ts +++ b/tests/unit/matchers.test.ts @@ -2,17 +2,19 @@ import 'mocha'; import { expect } from 'chai'; import { ZxcvbnFactory, type OptionsType } from '@zxcvbn-ts/core'; import { + specialMatcher, + numberMatcher, uppercaseMatcher, lowercaseMatcher, - numberMatcher, - specialMatcher, minLengthMatcher, - customMatchersTranslations + maxLengthMatcher, + customMatchersTranslations, } from '../../src'; import { translations as baseTranslations } from '@zxcvbn-ts/language-en'; import { merge } from 'lodash'; const MIN_LENGTH = 12; +const MAX_LENGTH = 50; const MIN_SECURE_SCORE = 3; const PERFECT_SCORE = 4; const SAMPLE_STRONG_PASSWORD = 'de#dSh251dft!'; @@ -20,17 +22,17 @@ const SAMPLE_STRONG_PASSWORD = 'de#dSh251dft!'; // Package setup const customMatchers = { minLength: minLengthMatcher(MIN_LENGTH), + maxLength: maxLengthMatcher(MAX_LENGTH), specialRequired: specialMatcher, numberRequired: numberMatcher, lowercaseRequired: lowercaseMatcher, - uppercaseRequired: uppercaseMatcher + uppercaseRequired: uppercaseMatcher, }; const mergedTranslations = merge({}, baseTranslations, customMatchersTranslations); const options: OptionsType = { translations: mergedTranslations }; const zxcvbn = new ZxcvbnFactory(options, customMatchers); - describe('Password Validation Requirements', () => { describe('Uppercase Character Requirement', () => { const testPassword = 'password123!'; @@ -136,6 +138,30 @@ describe('Password Validation Requirements', () => { }); }); + describe('Maximum Length Requirement', () => { + const testPassword = 'a'.repeat(MAX_LENGTH) + '@A3'; + const validPassword = 'longenougS2@hpassword'; + + it('should provide appropriate warning and suggestion for long passwords', () => { + const result = zxcvbn.check(testPassword); + const expectedWarning = customMatchersTranslations.warnings.maxLength.replace('%s', String(MAX_LENGTH)); + const expectedSuggestion = customMatchersTranslations.suggestions.maxLength.replace('%s', String(MAX_LENGTH)); + + expect(result.feedback.warning).to.equal(expectedWarning); + expect(result.feedback.suggestions).to.include(expectedSuggestion); + expect(result.score).to.be.lessThan(MIN_SECURE_SCORE); + }); + + it('should not show length warnings or suggestions when requirement is met', () => { + const result = zxcvbn.check(validPassword); + const unexpectedWarning = customMatchersTranslations.warnings.minLength.replace('%s', String(MAX_LENGTH)); + const unexpectedSuggestion = customMatchersTranslations.suggestions.minLength.replace('%s', String(MAX_LENGTH)); + + expect(result.feedback.warning).to.not.equal(unexpectedWarning); + expect(result.feedback.suggestions).to.not.include(unexpectedSuggestion); + }); + }); + describe('Combined Requirements', () => { it('should not show any warnings or suggestions for a fully compliant password', () => { const result = zxcvbn.check(SAMPLE_STRONG_PASSWORD);