From 5e4d755c5ee234adbc7f15d467497da56e81e969 Mon Sep 17 00:00:00 2001 From: Kobi Carmeli Date: Mon, 2 Dec 2024 12:43:07 +0200 Subject: [PATCH 1/2] refactor: use MatcherNames const for setting up custom matchers --- README.md | 17 ++--- src/index.ts | 5 +- src/matcherNames.ts | 8 +++ src/matchers/lowercaseMatcher.ts | 8 ++- src/matchers/maxLengthMatcher.ts | 8 ++- src/matchers/minLengthMatcher.ts | 8 ++- src/matchers/numberMatcher.ts | 8 ++- ...pecialMatcher.ts => specialCharMatcher.ts} | 8 ++- src/matchers/uppercaseMatcher.ts | 9 ++- src/translations.ts | 29 ++++---- tests/unit/matchers.test.ts | 66 ++++++++++--------- 11 files changed, 101 insertions(+), 73 deletions(-) create mode 100644 src/matcherNames.ts rename src/matchers/{specialMatcher.ts => specialCharMatcher.ts} (61%) diff --git a/README.md b/README.md index e50859c..5d5402b 100644 --- a/README.md +++ b/README.md @@ -18,21 +18,22 @@ import { uppercaseMatcher, minLengthMatcher, maxLengthMatcher, - customMatchersTranslations + MatchersTranslations, + MatcherNames } from 'zxcvbn-custom-matchers'; import { merge } from 'lodash'; // Add the custom matchers and their translations const options = { - translations: merge({}, zxcvbnEnPackage.translations, customMatchersTranslations) + translations: merge({}, zxcvbnEnPackage.translations, MatchersTranslations) }; const customMatchers = { - minLength: minLengthMatcher(MIN_PASSWORD_LENGTH), - maxLength: maxLengthMatcher(MAX_PASSWORD_LENGTH), - specialRequired: specialMatcher, - numberRequired: numberMatcher, - lowercaseRequired: lowercaseMatcher, - uppercaseRequired: uppercaseMatcher + [MatcherNames.minLengh]: minLengthMatcher(MIN_PASSWORD_LENGTH), + [MatcherNames.maxLength]: maxLengthMatcher(MAX_PASSWORD_LENGTH), + [MatcherNames.special]: specialMatcher, + [MatcherNames.numberRequired]: numberMatcher, + [MatcherNames.lowercase]: lowercaseMatcher, + [MatcherNames.uppercase]: uppercaseMatcher }; const zxcvbn = new ZxcvbnFactory(options, customMatchers); diff --git a/src/index.ts b/src/index.ts index e452d40..8f01a93 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,5 +3,6 @@ export { maxLengthMatcher } from './matchers/maxLengthMatcher'; export { uppercaseMatcher } from './matchers/uppercaseMatcher'; export { lowercaseMatcher } from './matchers/lowercaseMatcher'; export { numberMatcher } from './matchers/numberMatcher'; -export { specialMatcher } from './matchers/specialMatcher'; -export { customMatchersTranslations } from './translations'; +export { specialMatcher } from './matchers/specialCharMatcher'; +export { MatchersTranslations } from './translations'; +export { MatcherNames } from './matcherNames'; diff --git a/src/matcherNames.ts b/src/matcherNames.ts new file mode 100644 index 0000000..72639c6 --- /dev/null +++ b/src/matcherNames.ts @@ -0,0 +1,8 @@ +export const MatcherNames = { + lowercase: 'lowercase', + uppercase: 'uppercase', + number: 'number', + specialChar: 'specialChar', + minLength: 'minLength', + maxLength: 'maxLength', +}; diff --git a/src/matchers/lowercaseMatcher.ts b/src/matchers/lowercaseMatcher.ts index a30fd88..ad8aae4 100644 --- a/src/matchers/lowercaseMatcher.ts +++ b/src/matchers/lowercaseMatcher.ts @@ -1,5 +1,7 @@ import { Matcher, Match } from '@zxcvbn-ts/core'; +import { MatcherNames } from '../matcherNames'; +const matcher = MatcherNames.lowercase; export const lowercaseMatcher: Matcher = { Matching: class LowercaseMatcher { match({ password }: { password: string }): Match[] { @@ -9,14 +11,14 @@ export const lowercaseMatcher: Matcher = { return matches; } } - matches.push({ pattern: 'lowercaseRequired', token: password, i: 0, j: password.length - 1 }); + matches.push({ pattern: matcher, token: password, i: 0, j: password.length - 1 }); return matches; } }, feedback: options => { return { - warning: options.translations.warnings['lowercaseRequired'] || 'lowercaseRequired', - suggestions: [options.translations.suggestions['lowercaseRequired'] || 'lowercaseRequired'], + warning: options.translations.warnings[matcher] || matcher, + suggestions: [options.translations.suggestions[matcher] || matcher], }; }, scoring() { diff --git a/src/matchers/maxLengthMatcher.ts b/src/matchers/maxLengthMatcher.ts index 55682c1..d62903c 100644 --- a/src/matchers/maxLengthMatcher.ts +++ b/src/matchers/maxLengthMatcher.ts @@ -1,19 +1,21 @@ import { Matcher, Match } from '@zxcvbn-ts/core'; +import { MatcherNames } from '../matcherNames'; +const matcher = MatcherNames.maxLength; 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 }); + matches.push({ pattern: matcher, 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'], + warning: options.translations.warnings[matcher]?.replace('%s', maxLength) || matcher, + suggestions: [options.translations.suggestions[matcher]?.replace('%s', maxLength) || matcher], }; }, scoring() { diff --git a/src/matchers/minLengthMatcher.ts b/src/matchers/minLengthMatcher.ts index 92b3233..99bd138 100644 --- a/src/matchers/minLengthMatcher.ts +++ b/src/matchers/minLengthMatcher.ts @@ -1,19 +1,21 @@ import { Matcher, Match } from '@zxcvbn-ts/core'; +import { MatcherNames } from '../matcherNames'; +const matcher = MatcherNames.minLength; export const minLengthMatcher = (minLength: number): Matcher => ({ Matching: class MinLengthMatcher { match({ password }: { password: string }): Match[] { const matches: Match[] = []; if (password.length < minLength) { - matches.push({ pattern: 'minLength', token: password, i: 0, j: password.length - 1 }); + matches.push({ pattern: matcher, token: password, i: 0, j: password.length - 1 }); } return matches; } }, feedback: options => { return { - warning: options.translations.warnings['minLength']?.replace('%s', minLength) || 'minLength', - suggestions: [options.translations.suggestions['minLength']?.replace('%s', minLength) || 'minLength'], + warning: options.translations.warnings[matcher]?.replace('%s', minLength) || matcher, + suggestions: [options.translations.suggestions[matcher]?.replace('%s', minLength) || matcher], }; }, scoring() { diff --git a/src/matchers/numberMatcher.ts b/src/matchers/numberMatcher.ts index 4e2ea3f..9fad622 100644 --- a/src/matchers/numberMatcher.ts +++ b/src/matchers/numberMatcher.ts @@ -1,5 +1,7 @@ import { Matcher, Match } from '@zxcvbn-ts/core'; +import { MatcherNames } from '../matcherNames'; +const matcher = MatcherNames.number; export const numberMatcher: Matcher = { Matching: class NumberMatcher { match({ password }: { password: string }): Match[] { @@ -9,14 +11,14 @@ export const numberMatcher: Matcher = { return matches; } } - matches.push({ pattern: 'numberRequired', token: password, i: 0, j: password.length - 1 }); + matches.push({ pattern: matcher, token: password, i: 0, j: password.length - 1 }); return matches; } }, feedback: options => { return { - warning: options.translations.warnings['numberRequired'] || 'numberRequired', - suggestions: [options.translations.suggestions['numberRequired'] || 'numberRequired'], + warning: options.translations.warnings[matcher] || matcher, + suggestions: [options.translations.suggestions[matcher] || matcher], }; }, scoring() { diff --git a/src/matchers/specialMatcher.ts b/src/matchers/specialCharMatcher.ts similarity index 61% rename from src/matchers/specialMatcher.ts rename to src/matchers/specialCharMatcher.ts index a0a5fc9..13c2ae4 100644 --- a/src/matchers/specialMatcher.ts +++ b/src/matchers/specialCharMatcher.ts @@ -1,5 +1,7 @@ import { Matcher, Match } from '@zxcvbn-ts/core'; +import { MatcherNames } from '../matcherNames'; +const matcher = MatcherNames.specialChar; export const specialMatcher: Matcher = { Matching: class SpecialMatcher { match({ password }: { password: string }): Match[] { @@ -10,14 +12,14 @@ export const specialMatcher: Matcher = { return matches; } } - matches.push({ pattern: 'specialRequired', token: password, i: 0, j: password.length - 1 }); + matches.push({ pattern: matcher, token: password, i: 0, j: password.length - 1 }); return matches; } }, feedback: options => { return { - warning: options.translations.warnings['specialRequired'] || 'specialRequired', - suggestions: [options.translations.suggestions['specialRequired'] || 'specialRequired'], + warning: options.translations.warnings[matcher] || matcher, + suggestions: [options.translations.suggestions[matcher] || matcher], }; }, scoring() { diff --git a/src/matchers/uppercaseMatcher.ts b/src/matchers/uppercaseMatcher.ts index ae7627b..22b3312 100644 --- a/src/matchers/uppercaseMatcher.ts +++ b/src/matchers/uppercaseMatcher.ts @@ -1,5 +1,7 @@ import { Matcher, Match } from '@zxcvbn-ts/core'; +import { MatcherNames } from '../matcherNames'; +const matcher = MatcherNames.uppercase; export const uppercaseMatcher: Matcher = { Matching: class UppercaseMatcher { match({ password }: { password: string }): Match[] { @@ -9,14 +11,15 @@ export const uppercaseMatcher: Matcher = { return matches; } } - matches.push({ pattern: 'uppercaseRequired', token: password, i: 0, j: password.length - 1 }); + + matches.push({ pattern: matcher, token: password, i: 0, j: password.length - 1 }); return matches; } }, feedback: options => { return { - warning: options.translations.warnings['uppercaseRequired'] || 'uppercaseRequired', - suggestions: [options.translations.suggestions['uppercaseRequired'] || 'uppercaseRequired'], + warning: options.translations.warnings[matcher] || matcher, + suggestions: [options.translations.suggestions[matcher] || matcher], }; }, scoring() { diff --git a/src/translations.ts b/src/translations.ts index 7be5dc0..fcb00d0 100644 --- a/src/translations.ts +++ b/src/translations.ts @@ -1,19 +1,22 @@ -export const customMatchersTranslations = { +// translations.ts +import { MatcherNames } from './matcherNames'; + +export const MatchersTranslations = { 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.', + [MatcherNames.lowercase]: 'At least one lowercase letter is required.', + [MatcherNames.minLength]: 'Password must be at least %s characters long.', + [MatcherNames.maxLength]: 'Password must be no more than %s characters long.', + [MatcherNames.number]: 'At least one number is required.', + [MatcherNames.specialChar]: 'At least one special character is required.', + [MatcherNames.uppercase]: 'At least one uppercase letter is required.', }, suggestions: { - lowercaseRequired: 'Include at least one lowercase letter.', - minLength: 'Make your password at least %s characters long.', - maxLength: 'Reduce your password to no more than %s characters.', - numberRequired: 'Include at least one number.', - specialRequired: 'Include at least one special character.', - uppercaseRequired: 'Include at least one uppercase letter.', + [MatcherNames.lowercase]: 'Include at least one lowercase letter.', + [MatcherNames.minLength]: 'Make your password at least %s characters long.', + [MatcherNames.maxLength]: 'Reduce your password to no more than %s characters.', + [MatcherNames.number]: 'Include at least one number.', + [MatcherNames.specialChar]: 'Include at least one special character.', + [MatcherNames.uppercase]: 'Include at least one uppercase letter.', }, timeEstimation: {}, }; diff --git a/tests/unit/matchers.test.ts b/tests/unit/matchers.test.ts index f6256ad..cf8ab4b 100644 --- a/tests/unit/matchers.test.ts +++ b/tests/unit/matchers.test.ts @@ -8,7 +8,8 @@ import { lowercaseMatcher, minLengthMatcher, maxLengthMatcher, - customMatchersTranslations, + MatchersTranslations, + MatcherNames, } from '../../src'; import { translations as baseTranslations } from '@zxcvbn-ts/language-en'; import { merge } from 'lodash'; @@ -21,15 +22,15 @@ 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, + [MatcherNames.minLength]: minLengthMatcher(MIN_LENGTH), + [MatcherNames.maxLength]: maxLengthMatcher(MAX_LENGTH), + [MatcherNames.specialChar]: specialMatcher, + [MatcherNames.number]: numberMatcher, + [MatcherNames.lowercase]: lowercaseMatcher, + [MatcherNames.uppercase]: uppercaseMatcher, }; -const mergedTranslations = merge({}, baseTranslations, customMatchersTranslations); +const mergedTranslations = merge({}, baseTranslations, MatchersTranslations); const options: OptionsType = { translations: mergedTranslations }; const zxcvbn = new ZxcvbnFactory(options, customMatchers); @@ -41,16 +42,17 @@ describe('Password Validation Requirements', () => { it('should provide appropriate warning and suggestion when uppercase letters are missing', () => { const result = zxcvbn.check(testPassword); - expect(result.feedback.warning).to.equal(customMatchersTranslations.warnings.uppercaseRequired); - expect(result.feedback.suggestions).to.include(customMatchersTranslations.suggestions.uppercaseRequired); + console.info(result.feedback); + expect(result.feedback.warning).to.equal(MatchersTranslations.warnings.uppercase); + expect(result.feedback.suggestions).to.include(MatchersTranslations.suggestions.uppercase); expect(result.score).to.be.lessThan(MIN_SECURE_SCORE); }); it('should not show uppercase warnings or suggestions when requirement is met', () => { const result = zxcvbn.check(validPassword); - expect(result.feedback.warning).to.not.equal(customMatchersTranslations.warnings.uppercaseRequired); - expect(result.feedback.suggestions).to.not.include(customMatchersTranslations.suggestions.uppercaseRequired); + expect(result.feedback.warning).to.not.equal(MatchersTranslations.warnings.uppercase); + expect(result.feedback.suggestions).to.not.include(MatchersTranslations.suggestions.uppercase); }); }); @@ -61,16 +63,16 @@ describe('Password Validation Requirements', () => { it('should provide appropriate warning and suggestion when lowercase letters are missing', () => { const result = zxcvbn.check(testPassword); - expect(result.feedback.warning).to.equal(customMatchersTranslations.warnings.lowercaseRequired); - expect(result.feedback.suggestions).to.include(customMatchersTranslations.suggestions.lowercaseRequired); + expect(result.feedback.warning).to.equal(MatchersTranslations.warnings.lowercase); + expect(result.feedback.suggestions).to.include(MatchersTranslations.suggestions.lowercase); expect(result.score).to.be.lessThan(MIN_SECURE_SCORE); }); it('should not show lowercase warnings or suggestions when requirement is met', () => { const result = zxcvbn.check(validPassword); - expect(result.feedback.warning).to.not.equal(customMatchersTranslations.warnings.lowercaseRequired); - expect(result.feedback.suggestions).to.not.include(customMatchersTranslations.suggestions.lowercaseRequired); + expect(result.feedback.warning).to.not.equal(MatchersTranslations.warnings.lowercase); + expect(result.feedback.suggestions).to.not.include(MatchersTranslations.suggestions.lowercase); }); }); @@ -81,16 +83,16 @@ describe('Password Validation Requirements', () => { it('should provide appropriate warning and suggestion when numbers are missing', () => { const result = zxcvbn.check(testPassword); - expect(result.feedback.warning).to.equal(customMatchersTranslations.warnings.numberRequired); - expect(result.feedback.suggestions).to.include(customMatchersTranslations.suggestions.numberRequired); + expect(result.feedback.warning).to.equal(MatchersTranslations.warnings.number); + expect(result.feedback.suggestions).to.include(MatchersTranslations.suggestions.number); expect(result.score).to.be.lessThan(MIN_SECURE_SCORE); }); it('should not show number warnings or suggestions when requirement is met', () => { const result = zxcvbn.check(validPassword); - expect(result.feedback.warning).to.not.equal(customMatchersTranslations.warnings.numberRequired); - expect(result.feedback.suggestions).to.not.include(customMatchersTranslations.suggestions.numberRequired); + expect(result.feedback.warning).to.not.equal(MatchersTranslations.warnings.number); + expect(result.feedback.suggestions).to.not.include(MatchersTranslations.suggestions.number); }); }); @@ -101,16 +103,16 @@ describe('Password Validation Requirements', () => { it('should provide appropriate warning and suggestion when special characters are missing', () => { const result = zxcvbn.check(testPassword); - expect(result.feedback.warning).to.equal(customMatchersTranslations.warnings.specialRequired); - expect(result.feedback.suggestions).to.include(customMatchersTranslations.suggestions.specialRequired); + expect(result.feedback.warning).to.equal(MatchersTranslations.warnings.specialChar); + expect(result.feedback.suggestions).to.include(MatchersTranslations.suggestions.specialChar); expect(result.score).to.be.lessThan(MIN_SECURE_SCORE); }); it('should not show special character warnings or suggestions when requirement is met', () => { const result = zxcvbn.check(validPassword); - expect(result.feedback.warning).to.not.equal(customMatchersTranslations.warnings.specialRequired); - expect(result.feedback.suggestions).to.not.include(customMatchersTranslations.suggestions.specialRequired); + expect(result.feedback.warning).to.not.equal(MatchersTranslations.warnings.specialChar); + expect(result.feedback.suggestions).to.not.include(MatchersTranslations.suggestions.specialChar); }); }); @@ -120,8 +122,8 @@ describe('Password Validation Requirements', () => { it('should provide appropriate warning and suggestion for short passwords', () => { const result = zxcvbn.check(testPassword); - const expectedWarning = customMatchersTranslations.warnings.minLength.replace('%s', String(MIN_LENGTH)); - const expectedSuggestion = customMatchersTranslations.suggestions.minLength.replace('%s', String(MIN_LENGTH)); + const expectedWarning = MatchersTranslations.warnings.minLength.replace('%s', String(MIN_LENGTH)); + const expectedSuggestion = MatchersTranslations.suggestions.minLength.replace('%s', String(MIN_LENGTH)); expect(result.feedback.warning).to.equal(expectedWarning); expect(result.feedback.suggestions).to.include(expectedSuggestion); @@ -130,8 +132,8 @@ describe('Password Validation Requirements', () => { 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(MIN_LENGTH)); - const unexpectedSuggestion = customMatchersTranslations.suggestions.minLength.replace('%s', String(MIN_LENGTH)); + const unexpectedWarning = MatchersTranslations.warnings.minLength.replace('%s', String(MIN_LENGTH)); + const unexpectedSuggestion = MatchersTranslations.suggestions.minLength.replace('%s', String(MIN_LENGTH)); expect(result.feedback.warning).to.not.equal(unexpectedWarning); expect(result.feedback.suggestions).to.not.include(unexpectedSuggestion); @@ -144,8 +146,8 @@ describe('Password Validation Requirements', () => { 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)); + const expectedWarning = MatchersTranslations.warnings.maxLength.replace('%s', String(MAX_LENGTH)); + const expectedSuggestion = MatchersTranslations.suggestions.maxLength.replace('%s', String(MAX_LENGTH)); expect(result.feedback.warning).to.equal(expectedWarning); expect(result.feedback.suggestions).to.include(expectedSuggestion); @@ -154,8 +156,8 @@ describe('Password Validation Requirements', () => { 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)); + const unexpectedWarning = MatchersTranslations.warnings.minLength.replace('%s', String(MAX_LENGTH)); + const unexpectedSuggestion = MatchersTranslations.suggestions.minLength.replace('%s', String(MAX_LENGTH)); expect(result.feedback.warning).to.not.equal(unexpectedWarning); expect(result.feedback.suggestions).to.not.include(unexpectedSuggestion); From d3ca174be7afc2d8dca13c7a4ca9bf2ad5392b5d Mon Sep 17 00:00:00 2001 From: Kobi Carmeli Date: Mon, 2 Dec 2024 12:53:00 +0200 Subject: [PATCH 2/2] fix readme typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5d5402b..8ee1a7a 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ const options = { translations: merge({}, zxcvbnEnPackage.translations, MatchersTranslations) }; const customMatchers = { - [MatcherNames.minLengh]: minLengthMatcher(MIN_PASSWORD_LENGTH), + [MatcherNames.minLength]: minLengthMatcher(MIN_PASSWORD_LENGTH), [MatcherNames.maxLength]: maxLengthMatcher(MAX_PASSWORD_LENGTH), [MatcherNames.special]: specialMatcher, [MatcherNames.numberRequired]: numberMatcher,