From 1ee864186ce3667b204c44392beefc7b1d6e3a9f Mon Sep 17 00:00:00 2001 From: Ilan Kushnir Date: Mon, 25 Nov 2024 15:27:51 +0200 Subject: [PATCH 01/20] feat: get feedback from translations --- src/matchers/lowercaseMatcher.ts | 9 ++++++--- src/matchers/minLengthMatcher.ts | 7 +++++-- src/matchers/numberMatcher.ts | 9 ++++++--- src/matchers/specialMatcher.ts | 9 ++++++--- src/matchers/uppercaseMatcher.ts | 9 ++++++--- 5 files changed, 29 insertions(+), 14 deletions(-) diff --git a/src/matchers/lowercaseMatcher.ts b/src/matchers/lowercaseMatcher.ts index c0078e8..3d36032 100644 --- a/src/matchers/lowercaseMatcher.ts +++ b/src/matchers/lowercaseMatcher.ts @@ -9,12 +9,15 @@ export const lowercaseMatcher: Matcher = { return matches; } } - matches.push({ pattern: 'lowercase', token: password, i: 0, j: password.length - 1 }); + matches.push({ pattern: 'oneLowercase', token: password, i: 0, j: password.length - 1 }); return matches; } }, - feedback(_match) { - return { warning: 'Include at least one lowercase letter.', suggestions: [] }; + feedback: options => { + return { + warning: options.translations.warnings.oneLowercase || 'oneLowercase', + suggestions: [], + }; }, scoring(_match) { return -100; diff --git a/src/matchers/minLengthMatcher.ts b/src/matchers/minLengthMatcher.ts index c406809..429a768 100644 --- a/src/matchers/minLengthMatcher.ts +++ b/src/matchers/minLengthMatcher.ts @@ -10,8 +10,11 @@ export const minLengthMatcher = (minLength: number): Matcher => ({ return matches; } }, - feedback(_match) { - return { warning: `Password may not be shorter than ${minLength} characters.`, suggestions: [] }; + feedback: options => { + return { + warning: (options.translations.warnings.minLength = 'minLength'), + suggestions: [], + }; }, scoring(_match) { return -100; diff --git a/src/matchers/numberMatcher.ts b/src/matchers/numberMatcher.ts index 2f652d6..e920d07 100644 --- a/src/matchers/numberMatcher.ts +++ b/src/matchers/numberMatcher.ts @@ -9,12 +9,15 @@ export const numberMatcher: Matcher = { return matches; } } - matches.push({ pattern: 'number', token: password, i: 0, j: password.length - 1 }); + matches.push({ pattern: 'oneNumber', token: password, i: 0, j: password.length - 1 }); return matches; } }, - feedback(_match) { - return { warning: 'Include at least one number.', suggestions: [] }; + feedback: options => { + return { + warning: options.translations.warnings.oneNumber || 'oneNumber', + suggestions: [], + }; }, scoring(_match) { return -100; diff --git a/src/matchers/specialMatcher.ts b/src/matchers/specialMatcher.ts index 834ed98..21ac4b9 100644 --- a/src/matchers/specialMatcher.ts +++ b/src/matchers/specialMatcher.ts @@ -10,12 +10,15 @@ export const specialMatcher: Matcher = { return matches; } } - matches.push({ pattern: 'special', token: password, i: 0, j: password.length - 1 }); + matches.push({ pattern: 'oneSpecial', token: password, i: 0, j: password.length - 1 }); return matches; } }, - feedback(_match) { - return { warning: 'Include at least one special character.', suggestions: [] }; + feedback: options => { + return { + warning: options.translations.warnings.oneSpecial || 'oneSpecial', + suggestions: [], + }; }, scoring(_match) { return -100; diff --git a/src/matchers/uppercaseMatcher.ts b/src/matchers/uppercaseMatcher.ts index 9ed1518..7e6c2ab 100644 --- a/src/matchers/uppercaseMatcher.ts +++ b/src/matchers/uppercaseMatcher.ts @@ -9,12 +9,15 @@ export const uppercaseMatcher: Matcher = { return matches; } } - matches.push({ pattern: 'uppercase', token: password, i: 0, j: password.length - 1 }); + matches.push({ pattern: 'oneUppercase', token: password, i: 0, j: password.length - 1 }); return matches; } }, - feedback(_match) { - return { warning: 'Include at least one uppercase letter.', suggestions: [] }; + feedback: options => { + return { + warning: options.translations.warnings.oneUppercase || 'oneUppercase', + suggestions: [], + }; }, scoring(_match) { return -100; From 8d6936e76736ae20f9bf309f69481c21853aeddf Mon Sep 17 00:00:00 2001 From: Ilan Kushnir Date: Mon, 25 Nov 2024 15:28:04 +0200 Subject: [PATCH 02/20] feat: add translations --- src/index.ts | 3 ++- src/translations.ts | 11 +++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 src/translations.ts diff --git a/src/index.ts b/src/index.ts index 02cde8e..2d4ea18 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,6 @@ -export { uppercaseMatcher } from './matchers/uppercaseMatcher'; export { minLengthMatcher } from './matchers/minLengthMatcher'; +export { uppercaseMatcher } from './matchers/uppercaseMatcher'; export { lowercaseMatcher } from './matchers/lowercaseMatcher'; export { numberMatcher } from './matchers/numberMatcher'; export { specialMatcher } from './matchers/specialMatcher'; +export { customMatchersTranslations } from './translations'; diff --git a/src/translations.ts b/src/translations.ts new file mode 100644 index 0000000..c8afe70 --- /dev/null +++ b/src/translations.ts @@ -0,0 +1,11 @@ +export const customMatchersTranslations = { + warnings: { + oneLowercase: 'Include at least one lowercase letter.', + minLength: 'Password may not be shorter than ${minLength} characters.', + oneNumber: 'Include at least one number.', + oneSpecial: 'Include at least one special character.', + oneUppercase: 'Include at least one uppercase letter.', + }, + suggestions: {}, + timeEstimation: {}, +}; From 9f740cb91e0b68b6ba366957ae383b62f0ea18e4 Mon Sep 17 00:00:00 2001 From: Ilan Kushnir Date: Mon, 25 Nov 2024 15:28:13 +0200 Subject: [PATCH 03/20] chore: update readme --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index e9588c8..1464602 100644 --- a/README.md +++ b/README.md @@ -19,11 +19,11 @@ import { } from 'zxcvbn-custom-matchers'; // Add the matchers -zxcvbnOptions.addMatcher('lowercase', lowercaseMatcher); -zxcvbnOptions.addMatcher('number', numberMatcher); -zxcvbnOptions.addMatcher('special', specialMatcher); -zxcvbnOptions.addMatcher('uppercase', uppercaseMatcher); -zxcvbnOptions.addMatcher('minLength', minLengthMatcher(10)); +zxcvbnOptions.addMatcher('oneLowercase', lowercaseMatcher); +zxcvbnOptions.addMatcher('minLength', numberMatcher); +zxcvbnOptions.addMatcher('oneNumber', specialMatcher); +zxcvbnOptions.addMatcher('oneSpecial', uppercaseMatcher); +zxcvbnOptions.addMatcher('oneUppercase', minLengthMatcher(10)); // Use zxcvbn as usual import { zxcvbn } from '@zxcvbn-ts/core'; From 8be39f168dbb5be63f13bf7fae6a6603d1594dbe Mon Sep 17 00:00:00 2001 From: Ilan Kushnir Date: Mon, 25 Nov 2024 17:58:37 +0200 Subject: [PATCH 04/20] refactor: change patterns names --- src/matchers/lowercaseMatcher.ts | 4 ++-- src/matchers/numberMatcher.ts | 4 ++-- src/matchers/specialMatcher.ts | 4 ++-- src/matchers/uppercaseMatcher.ts | 4 ++-- src/translations.ts | 8 ++++---- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/matchers/lowercaseMatcher.ts b/src/matchers/lowercaseMatcher.ts index 3d36032..8104f78 100644 --- a/src/matchers/lowercaseMatcher.ts +++ b/src/matchers/lowercaseMatcher.ts @@ -9,13 +9,13 @@ export const lowercaseMatcher: Matcher = { return matches; } } - matches.push({ pattern: 'oneLowercase', token: password, i: 0, j: password.length - 1 }); + matches.push({ pattern: 'lowercaseRequired', token: password, i: 0, j: password.length - 1 }); return matches; } }, feedback: options => { return { - warning: options.translations.warnings.oneLowercase || 'oneLowercase', + warning: options.translations.warnings.lowercaseRequired || 'lowercaseRequired', suggestions: [], }; }, diff --git a/src/matchers/numberMatcher.ts b/src/matchers/numberMatcher.ts index e920d07..3b6a51f 100644 --- a/src/matchers/numberMatcher.ts +++ b/src/matchers/numberMatcher.ts @@ -9,13 +9,13 @@ export const numberMatcher: Matcher = { return matches; } } - matches.push({ pattern: 'oneNumber', token: password, i: 0, j: password.length - 1 }); + matches.push({ pattern: 'numberRequired', token: password, i: 0, j: password.length - 1 }); return matches; } }, feedback: options => { return { - warning: options.translations.warnings.oneNumber || 'oneNumber', + warning: options.translations.warnings.numberRequired || 'numberRequired', suggestions: [], }; }, diff --git a/src/matchers/specialMatcher.ts b/src/matchers/specialMatcher.ts index 21ac4b9..d0b1306 100644 --- a/src/matchers/specialMatcher.ts +++ b/src/matchers/specialMatcher.ts @@ -10,13 +10,13 @@ export const specialMatcher: Matcher = { return matches; } } - matches.push({ pattern: 'oneSpecial', token: password, i: 0, j: password.length - 1 }); + matches.push({ pattern: 'specialRequired', token: password, i: 0, j: password.length - 1 }); return matches; } }, feedback: options => { return { - warning: options.translations.warnings.oneSpecial || 'oneSpecial', + warning: options.translations.warnings.specialRequired || 'specialRequired', suggestions: [], }; }, diff --git a/src/matchers/uppercaseMatcher.ts b/src/matchers/uppercaseMatcher.ts index 7e6c2ab..0ceb959 100644 --- a/src/matchers/uppercaseMatcher.ts +++ b/src/matchers/uppercaseMatcher.ts @@ -9,13 +9,13 @@ export const uppercaseMatcher: Matcher = { return matches; } } - matches.push({ pattern: 'oneUppercase', token: password, i: 0, j: password.length - 1 }); + matches.push({ pattern: 'uppercaseRequired', token: password, i: 0, j: password.length - 1 }); return matches; } }, feedback: options => { return { - warning: options.translations.warnings.oneUppercase || 'oneUppercase', + warning: options.translations.warnings.uppercaseRequired || 'uppercaseRequired', suggestions: [], }; }, diff --git a/src/translations.ts b/src/translations.ts index c8afe70..39709d3 100644 --- a/src/translations.ts +++ b/src/translations.ts @@ -1,10 +1,10 @@ export const customMatchersTranslations = { warnings: { - oneLowercase: 'Include at least one lowercase letter.', + lowercaseRequired: 'Include at least one lowercase letter.', minLength: 'Password may not be shorter than ${minLength} characters.', - oneNumber: 'Include at least one number.', - oneSpecial: 'Include at least one special character.', - oneUppercase: 'Include at least one uppercase letter.', + numberRequired: 'Include at least one number.', + specialRequired: 'Include at least one special character.', + uppercaseRequired: 'Include at least one uppercase letter.', }, suggestions: {}, timeEstimation: {}, From 087c8626cce1a1211b040f8edca9cb06691024ea Mon Sep 17 00:00:00 2001 From: Ilan Kushnir Date: Mon, 25 Nov 2024 18:11:59 +0200 Subject: [PATCH 05/20] chore: update README --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 1464602..39d3105 100644 --- a/README.md +++ b/README.md @@ -19,11 +19,11 @@ import { } from 'zxcvbn-custom-matchers'; // Add the matchers -zxcvbnOptions.addMatcher('oneLowercase', lowercaseMatcher); +zxcvbnOptions.addMatcher('lowercaseRequired', lowercaseMatcher); zxcvbnOptions.addMatcher('minLength', numberMatcher); -zxcvbnOptions.addMatcher('oneNumber', specialMatcher); -zxcvbnOptions.addMatcher('oneSpecial', uppercaseMatcher); -zxcvbnOptions.addMatcher('oneUppercase', minLengthMatcher(10)); +zxcvbnOptions.addMatcher('numberRequired', specialMatcher); +zxcvbnOptions.addMatcher('specialRequired', uppercaseMatcher); +zxcvbnOptions.addMatcher('uppercaseRequired', minLengthMatcher(10)); // Use zxcvbn as usual import { zxcvbn } from '@zxcvbn-ts/core'; From c185443327d8aa87caf925710c93f69287d691e9 Mon Sep 17 00:00:00 2001 From: Ilan Kushnir Date: Tue, 26 Nov 2024 16:29:14 +0200 Subject: [PATCH 06/20] chore: update README --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 39d3105..668c988 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,12 @@ import { minLengthMatcher } from 'zxcvbn-custom-matchers'; +// Add the matchers' translations +const options = { + translations: merge({}, zxcvbnEnPackage.translations, customMatchersTranslations) +}; +zxcvbnOptions.setOptions(options); + // Add the matchers zxcvbnOptions.addMatcher('lowercaseRequired', lowercaseMatcher); zxcvbnOptions.addMatcher('minLength', numberMatcher); From b66e978a0f2d97e28e9c92e065589f89a9f77c2a Mon Sep 17 00:00:00 2001 From: Ilan Kushnir Date: Tue, 26 Nov 2024 16:32:56 +0200 Subject: [PATCH 07/20] tests: update test matcher strings --- tests/unit/matchers.test.ts | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/tests/unit/matchers.test.ts b/tests/unit/matchers.test.ts index 6f0e95e..1bafd89 100644 --- a/tests/unit/matchers.test.ts +++ b/tests/unit/matchers.test.ts @@ -5,17 +5,17 @@ import { uppercaseMatcher, lowercaseMatcher, numberMatcher, specialMatcher, minL describe('uppercaseMatcher', () => { it('should return a match for missing uppercase letters', () => { const result = uppercaseMatcher.Matching.prototype.match({ password: 'password123!' }); - expect(result).to.deep.include({ pattern: 'uppercase', token: 'password123!', i: 0, j: 11 }); + expect(result).to.deep.include({ pattern: 'uppercaseRequired', token: 'password123!', i: 0, j: 11 }); }); it('should provide correct feedback for missing uppercase letters', () => { - const match = { pattern: 'uppercase', token: 'password123!', i: 0, j: 11, guesses: 1, guessesLog10: 0 }; + const match = { pattern: 'uppercaseRequired', token: 'password123!', i: 0, j: 11, guesses: 1, guessesLog10: 0 }; const feedback = uppercaseMatcher.feedback(match); expect(feedback.warning).to.equal('Include at least one uppercase letter.'); }); it('should return a score of -100 for missing uppercase letters', () => { - const match = { pattern: 'uppercase', token: 'password123!', i: 0, j: 11 }; + const match = { pattern: 'uppercaseRequired', token: 'password123!', i: 0, j: 11 }; const score = uppercaseMatcher.scoring(match); expect(score).to.equal(-100); }); @@ -24,17 +24,17 @@ describe('uppercaseMatcher', () => { describe('lowercaseMatcher', () => { it('should return a match for missing lowercase letters', () => { const result = lowercaseMatcher.Matching.prototype.match({ password: 'PASSWORD123!' }); - expect(result).to.deep.include({ pattern: 'lowercase', token: 'PASSWORD123!', i: 0, j: 11 }); + expect(result).to.deep.include({ pattern: 'lowercaseRequired', token: 'PASSWORD123!', i: 0, j: 11 }); }); it('should provide correct feedback for missing lowercase letters', () => { - const match = { pattern: 'lowercase', token: 'PASSWORD123!', i: 0, j: 11, guesses: 1, guessesLog10: 0 }; + const match = { pattern: 'lowercaseRequired', token: 'PASSWORD123!', i: 0, j: 11, guesses: 1, guessesLog10: 0 }; const feedback = lowercaseMatcher.feedback(match); expect(feedback.warning).to.equal('Include at least one lowercase letter.'); }); it('should return a score of -100 for missing lowercase letters', () => { - const match = { pattern: 'lowercase', token: 'PASSWORD123!', i: 0, j: 11 }; + const match = { pattern: 'lowercaseRequired', token: 'PASSWORD123!', i: 0, j: 11 }; const score = lowercaseMatcher.scoring(match); expect(score).to.equal(-100); }); @@ -43,17 +43,17 @@ describe('lowercaseMatcher', () => { describe('numberMatcher', () => { it('should return a match for missing numbers', () => { const result = numberMatcher.Matching.prototype.match({ password: 'Password!' }); - expect(result).to.deep.include({ pattern: 'number', token: 'Password!', i: 0, j: 8 }); + expect(result).to.deep.include({ pattern: 'numberRequired', token: 'Password!', i: 0, j: 8 }); }); it('should provide correct feedback for missing numbers', () => { - const match = { pattern: 'number', token: 'Password!', i: 0, j: 8, guesses: 1, guessesLog10: 0 }; + const match = { pattern: 'numberRequired', token: 'Password!', i: 0, j: 8, guesses: 1, guessesLog10: 0 }; const feedback = numberMatcher.feedback(match); expect(feedback.warning).to.equal('Include at least one number.'); }); it('should return a score of -100 for missing numbers', () => { - const match = { pattern: 'number', token: 'Password!', i: 0, j: 8 }; + const match = { pattern: 'numberRequired', token: 'Password!', i: 0, j: 8 }; const score = numberMatcher.scoring(match); expect(score).to.equal(-100); }); @@ -62,17 +62,17 @@ describe('numberMatcher', () => { describe('specialMatcher', () => { it('should return a match for missing special characters', () => { const result = specialMatcher.Matching.prototype.match({ password: 'Password123' }); - expect(result).to.deep.include({ pattern: 'special', token: 'Password123', i: 0, j: 10 }); + expect(result).to.deep.include({ pattern: 'specialRequired', token: 'Password123', i: 0, j: 10 }); }); it('should provide correct feedback for missing special characters', () => { - const match = { pattern: 'special', token: 'Password123', i: 0, j: 10, guesses: 1, guessesLog10: 0 }; + const match = { pattern: 'specialRequired', token: 'Password123', i: 0, j: 10, guesses: 1, guessesLog10: 0 }; const feedback = specialMatcher.feedback(match); expect(feedback.warning).to.equal('Include at least one special character.'); }); it('should return a score of -100 for missing special characters', () => { - const match = { pattern: 'special', token: 'Password123', i: 0, j: 10 }; + const match = { pattern: 'specialRequired', token: 'Password123', i: 0, j: 10 }; const score = specialMatcher.scoring(match); expect(score).to.equal(-100); }); @@ -95,7 +95,7 @@ describe('minLengthMatcher', () => { it('should provide correct feedback for passwords shorter than the minimum length', () => { const match = { pattern: 'minLength', token: 'short', i: 0, j: 4, guesses: 1, guessesLog10: 0 }; const feedback = matcher.feedback(match); - expect(feedback.warning).to.equal(`Password may not be shorter than ${minLength} characters.`); + expect(feedback.warning).to.equal('Password may not be shorter than 10 characters.'); }); it('should return a score of -100 for passwords shorter than the minimum length', () => { From 867b92c23eb984eb7a7cdfa923a29ecaaff19657 Mon Sep 17 00:00:00 2001 From: Ilan Kushnir Date: Wed, 27 Nov 2024 11:03:37 +0200 Subject: [PATCH 08/20] fix: update translations + suggestions --- src/translations.ts | 10 ++++++++-- tests/unit/matchers.test.ts | 32 +------------------------------- 2 files changed, 9 insertions(+), 33 deletions(-) diff --git a/src/translations.ts b/src/translations.ts index 39709d3..33aa1c8 100644 --- a/src/translations.ts +++ b/src/translations.ts @@ -1,11 +1,17 @@ export const customMatchersTranslations = { warnings: { + lowercaseRequired: 'At least one lowercase letter is required.', + minLength: 'Password must be at least 12 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.', + }, + suggestions: { lowercaseRequired: 'Include at least one lowercase letter.', - minLength: 'Password may not be shorter than ${minLength} characters.', + minLength: 'Password may not be shorter than 12 characters.', numberRequired: 'Include at least one number.', specialRequired: 'Include at least one special character.', uppercaseRequired: 'Include at least one uppercase letter.', }, - suggestions: {}, timeEstimation: {}, }; diff --git a/tests/unit/matchers.test.ts b/tests/unit/matchers.test.ts index 1bafd89..59e908c 100644 --- a/tests/unit/matchers.test.ts +++ b/tests/unit/matchers.test.ts @@ -8,12 +8,6 @@ describe('uppercaseMatcher', () => { expect(result).to.deep.include({ pattern: 'uppercaseRequired', token: 'password123!', i: 0, j: 11 }); }); - it('should provide correct feedback for missing uppercase letters', () => { - const match = { pattern: 'uppercaseRequired', token: 'password123!', i: 0, j: 11, guesses: 1, guessesLog10: 0 }; - const feedback = uppercaseMatcher.feedback(match); - expect(feedback.warning).to.equal('Include at least one uppercase letter.'); - }); - it('should return a score of -100 for missing uppercase letters', () => { const match = { pattern: 'uppercaseRequired', token: 'password123!', i: 0, j: 11 }; const score = uppercaseMatcher.scoring(match); @@ -27,12 +21,6 @@ describe('lowercaseMatcher', () => { expect(result).to.deep.include({ pattern: 'lowercaseRequired', token: 'PASSWORD123!', i: 0, j: 11 }); }); - it('should provide correct feedback for missing lowercase letters', () => { - const match = { pattern: 'lowercaseRequired', token: 'PASSWORD123!', i: 0, j: 11, guesses: 1, guessesLog10: 0 }; - const feedback = lowercaseMatcher.feedback(match); - expect(feedback.warning).to.equal('Include at least one lowercase letter.'); - }); - it('should return a score of -100 for missing lowercase letters', () => { const match = { pattern: 'lowercaseRequired', token: 'PASSWORD123!', i: 0, j: 11 }; const score = lowercaseMatcher.scoring(match); @@ -46,12 +34,6 @@ describe('numberMatcher', () => { expect(result).to.deep.include({ pattern: 'numberRequired', token: 'Password!', i: 0, j: 8 }); }); - it('should provide correct feedback for missing numbers', () => { - const match = { pattern: 'numberRequired', token: 'Password!', i: 0, j: 8, guesses: 1, guessesLog10: 0 }; - const feedback = numberMatcher.feedback(match); - expect(feedback.warning).to.equal('Include at least one number.'); - }); - it('should return a score of -100 for missing numbers', () => { const match = { pattern: 'numberRequired', token: 'Password!', i: 0, j: 8 }; const score = numberMatcher.scoring(match); @@ -65,12 +47,6 @@ describe('specialMatcher', () => { expect(result).to.deep.include({ pattern: 'specialRequired', token: 'Password123', i: 0, j: 10 }); }); - it('should provide correct feedback for missing special characters', () => { - const match = { pattern: 'specialRequired', token: 'Password123', i: 0, j: 10, guesses: 1, guessesLog10: 0 }; - const feedback = specialMatcher.feedback(match); - expect(feedback.warning).to.equal('Include at least one special character.'); - }); - it('should return a score of -100 for missing special characters', () => { const match = { pattern: 'specialRequired', token: 'Password123', i: 0, j: 10 }; const score = specialMatcher.scoring(match); @@ -79,7 +55,7 @@ describe('specialMatcher', () => { }); describe('minLengthMatcher', () => { - const minLength = 10; + const minLength = 12; const matcher = minLengthMatcher(minLength); it('should return a match for passwords shorter than the minimum length', () => { @@ -92,12 +68,6 @@ describe('minLengthMatcher', () => { expect(result).to.be.empty; }); - it('should provide correct feedback for passwords shorter than the minimum length', () => { - const match = { pattern: 'minLength', token: 'short', i: 0, j: 4, guesses: 1, guessesLog10: 0 }; - const feedback = matcher.feedback(match); - expect(feedback.warning).to.equal('Password may not be shorter than 10 characters.'); - }); - it('should return a score of -100 for passwords shorter than the minimum length', () => { const match = { pattern: 'minLength', token: 'short', i: 0, j: 4 }; const score = matcher.scoring(match); From dc224d80c1758c849baebf21a6e17d19def1963d Mon Sep 17 00:00:00 2001 From: Ilan Kushnir Date: Wed, 27 Nov 2024 11:05:34 +0200 Subject: [PATCH 09/20] tests: add translations assertions --- tests/unit/matchers.test.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/unit/matchers.test.ts b/tests/unit/matchers.test.ts index 59e908c..421c8a2 100644 --- a/tests/unit/matchers.test.ts +++ b/tests/unit/matchers.test.ts @@ -1,11 +1,14 @@ import 'mocha'; import { expect } from 'chai'; import { uppercaseMatcher, lowercaseMatcher, numberMatcher, specialMatcher, minLengthMatcher } from '../../src'; +import { customMatchersTranslations } from '../../src/translations'; describe('uppercaseMatcher', () => { it('should return a match for missing uppercase letters', () => { const result = uppercaseMatcher.Matching.prototype.match({ password: 'password123!' }); expect(result).to.deep.include({ pattern: 'uppercaseRequired', token: 'password123!', i: 0, j: 11 }); + expect(customMatchersTranslations.warnings.uppercaseRequired).to.equal('At least one uppercase letter is required.'); + expect(customMatchersTranslations.suggestions.uppercaseRequired).to.equal('Include at least one uppercase letter.'); }); it('should return a score of -100 for missing uppercase letters', () => { @@ -19,6 +22,8 @@ describe('lowercaseMatcher', () => { it('should return a match for missing lowercase letters', () => { const result = lowercaseMatcher.Matching.prototype.match({ password: 'PASSWORD123!' }); expect(result).to.deep.include({ pattern: 'lowercaseRequired', token: 'PASSWORD123!', i: 0, j: 11 }); + expect(customMatchersTranslations.warnings.lowercaseRequired).to.equal('At least one lowercase letter is required.'); + expect(customMatchersTranslations.suggestions.lowercaseRequired).to.equal('Include at least one lowercase letter.'); }); it('should return a score of -100 for missing lowercase letters', () => { @@ -32,6 +37,8 @@ describe('numberMatcher', () => { it('should return a match for missing numbers', () => { const result = numberMatcher.Matching.prototype.match({ password: 'Password!' }); expect(result).to.deep.include({ pattern: 'numberRequired', token: 'Password!', i: 0, j: 8 }); + expect(customMatchersTranslations.warnings.numberRequired).to.equal('At least one number is required.'); + expect(customMatchersTranslations.suggestions.numberRequired).to.equal('Include at least one number.'); }); it('should return a score of -100 for missing numbers', () => { @@ -45,6 +52,8 @@ describe('specialMatcher', () => { it('should return a match for missing special characters', () => { const result = specialMatcher.Matching.prototype.match({ password: 'Password123' }); expect(result).to.deep.include({ pattern: 'specialRequired', token: 'Password123', i: 0, j: 10 }); + expect(customMatchersTranslations.warnings.specialRequired).to.equal('At least one special character is required.'); + expect(customMatchersTranslations.suggestions.specialRequired).to.equal('Include at least one special character.'); }); it('should return a score of -100 for missing special characters', () => { @@ -61,6 +70,8 @@ describe('minLengthMatcher', () => { it('should return a match for passwords shorter than the minimum length', () => { const result = matcher.Matching.prototype.match({ password: 'short' }); expect(result).to.deep.include({ pattern: 'minLength', token: 'short', i: 0, j: 4 }); + expect(customMatchersTranslations.warnings.minLength).to.equal('Password must be at least 12 characters long.'); + expect(customMatchersTranslations.suggestions.minLength).to.equal('Password may not be shorter than 12 characters.'); }); it('should return no matches for passwords meeting the minimum length', () => { From 06a1e4843207b28492641ff69961fcdf02c49b54 Mon Sep 17 00:00:00 2001 From: Ilan Kushnir Date: Wed, 27 Nov 2024 11:30:20 +0200 Subject: [PATCH 10/20] fix: minor bug fix --- src/matchers/minLengthMatcher.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matchers/minLengthMatcher.ts b/src/matchers/minLengthMatcher.ts index 429a768..cd19178 100644 --- a/src/matchers/minLengthMatcher.ts +++ b/src/matchers/minLengthMatcher.ts @@ -12,7 +12,7 @@ export const minLengthMatcher = (minLength: number): Matcher => ({ }, feedback: options => { return { - warning: (options.translations.warnings.minLength = 'minLength'), + warning: options.translations.warnings.minLength || 'minLength', suggestions: [], }; }, From feaed48c56b1f3d462828ecdb79b92d18c8e3af2 Mon Sep 17 00:00:00 2001 From: Ilan Kushnir Date: Wed, 27 Nov 2024 11:31:41 +0200 Subject: [PATCH 11/20] fix: minor bug fix --- tests/unit/matchers.test.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/tests/unit/matchers.test.ts b/tests/unit/matchers.test.ts index 421c8a2..e481e5d 100644 --- a/tests/unit/matchers.test.ts +++ b/tests/unit/matchers.test.ts @@ -7,7 +7,9 @@ describe('uppercaseMatcher', () => { it('should return a match for missing uppercase letters', () => { const result = uppercaseMatcher.Matching.prototype.match({ password: 'password123!' }); expect(result).to.deep.include({ pattern: 'uppercaseRequired', token: 'password123!', i: 0, j: 11 }); - expect(customMatchersTranslations.warnings.uppercaseRequired).to.equal('At least one uppercase letter is required.'); + expect(customMatchersTranslations.warnings.uppercaseRequired).to.equal( + 'At least one uppercase letter is required.', + ); expect(customMatchersTranslations.suggestions.uppercaseRequired).to.equal('Include at least one uppercase letter.'); }); @@ -22,7 +24,9 @@ describe('lowercaseMatcher', () => { it('should return a match for missing lowercase letters', () => { const result = lowercaseMatcher.Matching.prototype.match({ password: 'PASSWORD123!' }); expect(result).to.deep.include({ pattern: 'lowercaseRequired', token: 'PASSWORD123!', i: 0, j: 11 }); - expect(customMatchersTranslations.warnings.lowercaseRequired).to.equal('At least one lowercase letter is required.'); + expect(customMatchersTranslations.warnings.lowercaseRequired).to.equal( + 'At least one lowercase letter is required.', + ); expect(customMatchersTranslations.suggestions.lowercaseRequired).to.equal('Include at least one lowercase letter.'); }); @@ -71,7 +75,9 @@ describe('minLengthMatcher', () => { const result = matcher.Matching.prototype.match({ password: 'short' }); expect(result).to.deep.include({ pattern: 'minLength', token: 'short', i: 0, j: 4 }); expect(customMatchersTranslations.warnings.minLength).to.equal('Password must be at least 12 characters long.'); - expect(customMatchersTranslations.suggestions.minLength).to.equal('Password may not be shorter than 12 characters.'); + expect(customMatchersTranslations.suggestions.minLength).to.equal( + 'Password may not be shorter than 12 characters.', + ); }); it('should return no matches for passwords meeting the minimum length', () => { From dc5e6d50813baa22712ab34cfe63a979a5921ffe Mon Sep 17 00:00:00 2001 From: Ilan Kushnir Date: Wed, 27 Nov 2024 11:39:03 +0200 Subject: [PATCH 12/20] chore: update readme --- README.md | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 668c988..87ec2f5 100644 --- a/README.md +++ b/README.md @@ -9,31 +9,36 @@ npm install zxcvbn-custom-matchers ### Usage ```ts -import { zxcvbnOptions } from '@zxcvbn-ts/core'; +import { ZxcvbnFactory } from '@zxcvbn-ts/core'; +import zxcvbnCommonPackage from '@zxcvbn-ts/language-common'; +import zxcvbnEnPackage from '@zxcvbn-ts/language-en'; import { - lowercaseMatcher, numberMatcher, specialMatcher, + lowercaseMatcher, uppercaseMatcher, - minLengthMatcher + minLengthMatcher, + customMatchersTranslations } from 'zxcvbn-custom-matchers'; +import { merge } from 'lodash'; // Add the matchers' translations const options = { + dictionary: merge({}, zxcvbnCommonPackage.dictionary, zxcvbnEnPackage.dictionary), translations: merge({}, zxcvbnEnPackage.translations, customMatchersTranslations) }; -zxcvbnOptions.setOptions(options); - -// Add the matchers -zxcvbnOptions.addMatcher('lowercaseRequired', lowercaseMatcher); -zxcvbnOptions.addMatcher('minLength', numberMatcher); -zxcvbnOptions.addMatcher('numberRequired', specialMatcher); -zxcvbnOptions.addMatcher('specialRequired', uppercaseMatcher); -zxcvbnOptions.addMatcher('uppercaseRequired', minLengthMatcher(10)); +const customMatchers = { + minLength: minLengthMatcher(commons.MIN_PASSWORD_LENGTH), + specialRequired: specialMatcher, + numberRequired: numberMatcher, + lowercaseRequired: lowercaseMatcher, + uppercaseRequired: uppercaseMatcher +}; +const zxcvbn = new ZxcvbnFactory(options, customMatchers); // Use zxcvbn as usual import { zxcvbn } from '@zxcvbn-ts/core'; -const result = zxcvbn('password123'); +const result = zxcvbn.check('password123'); console.log(result); ``` From ee19b85e616d5320a58417631f6809cfab0a9bb0 Mon Sep 17 00:00:00 2001 From: Ilan Kushnir Date: Wed, 27 Nov 2024 11:39:42 +0200 Subject: [PATCH 13/20] chore: update readme --- README.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/README.md b/README.md index 87ec2f5..e8dc8ea 100644 --- a/README.md +++ b/README.md @@ -10,8 +10,6 @@ npm install zxcvbn-custom-matchers ### Usage ```ts import { ZxcvbnFactory } from '@zxcvbn-ts/core'; -import zxcvbnCommonPackage from '@zxcvbn-ts/language-common'; -import zxcvbnEnPackage from '@zxcvbn-ts/language-en'; import { numberMatcher, specialMatcher, @@ -22,9 +20,8 @@ import { } from 'zxcvbn-custom-matchers'; import { merge } from 'lodash'; -// Add the matchers' translations +// Add the custom matchers and their translations const options = { - dictionary: merge({}, zxcvbnCommonPackage.dictionary, zxcvbnEnPackage.dictionary), translations: merge({}, zxcvbnEnPackage.translations, customMatchersTranslations) }; const customMatchers = { From 0db747fad7da3fef57c0f0acc1e579fbeefeacdc Mon Sep 17 00:00:00 2001 From: Ilan Kushnir Date: Wed, 27 Nov 2024 11:47:20 +0200 Subject: [PATCH 14/20] fix: update matcher suggestions --- src/matchers/lowercaseMatcher.ts | 2 +- src/matchers/minLengthMatcher.ts | 4 ++-- src/matchers/numberMatcher.ts | 2 +- src/matchers/specialMatcher.ts | 2 +- src/matchers/uppercaseMatcher.ts | 2 +- src/translations.ts | 4 ++-- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/matchers/lowercaseMatcher.ts b/src/matchers/lowercaseMatcher.ts index 8104f78..3b0180c 100644 --- a/src/matchers/lowercaseMatcher.ts +++ b/src/matchers/lowercaseMatcher.ts @@ -16,7 +16,7 @@ export const lowercaseMatcher: Matcher = { feedback: options => { return { warning: options.translations.warnings.lowercaseRequired || 'lowercaseRequired', - suggestions: [], + suggestions: [options.translations.suggestions.lowercaseRequired || 'lowercaseRequired'], }; }, scoring(_match) { diff --git a/src/matchers/minLengthMatcher.ts b/src/matchers/minLengthMatcher.ts index cd19178..cf2df12 100644 --- a/src/matchers/minLengthMatcher.ts +++ b/src/matchers/minLengthMatcher.ts @@ -12,8 +12,8 @@ export const minLengthMatcher = (minLength: number): Matcher => ({ }, feedback: options => { return { - warning: options.translations.warnings.minLength || 'minLength', - suggestions: [], + warning: options.translations.warnings.minLength?.replace('%s', minLength) || 'minLength', + suggestions: [options.translations.suggestions.minLength?.replace('%s', minLength) || 'minLength'], }; }, scoring(_match) { diff --git a/src/matchers/numberMatcher.ts b/src/matchers/numberMatcher.ts index 3b6a51f..7407623 100644 --- a/src/matchers/numberMatcher.ts +++ b/src/matchers/numberMatcher.ts @@ -16,7 +16,7 @@ export const numberMatcher: Matcher = { feedback: options => { return { warning: options.translations.warnings.numberRequired || 'numberRequired', - suggestions: [], + suggestions: [options.translations.suggestions.numberRequired || 'numberRequired'], }; }, scoring(_match) { diff --git a/src/matchers/specialMatcher.ts b/src/matchers/specialMatcher.ts index d0b1306..a407382 100644 --- a/src/matchers/specialMatcher.ts +++ b/src/matchers/specialMatcher.ts @@ -17,7 +17,7 @@ export const specialMatcher: Matcher = { feedback: options => { return { warning: options.translations.warnings.specialRequired || 'specialRequired', - suggestions: [], + suggestions: [options.translations.suggestions.specialRequired || 'specialRequired'], }; }, scoring(_match) { diff --git a/src/matchers/uppercaseMatcher.ts b/src/matchers/uppercaseMatcher.ts index 0ceb959..cdafc0e 100644 --- a/src/matchers/uppercaseMatcher.ts +++ b/src/matchers/uppercaseMatcher.ts @@ -16,7 +16,7 @@ export const uppercaseMatcher: Matcher = { feedback: options => { return { warning: options.translations.warnings.uppercaseRequired || 'uppercaseRequired', - suggestions: [], + suggestions: [options.translations.suggestions.uppercaseRequired || 'uppercaseRequired'], }; }, scoring(_match) { diff --git a/src/translations.ts b/src/translations.ts index 33aa1c8..5f8cca8 100644 --- a/src/translations.ts +++ b/src/translations.ts @@ -1,14 +1,14 @@ export const customMatchersTranslations = { warnings: { lowercaseRequired: 'At least one lowercase letter is required.', - minLength: 'Password must be at least 12 characters long.', + minLength: 'Password must be at least %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.', }, suggestions: { lowercaseRequired: 'Include at least one lowercase letter.', - minLength: 'Password may not be shorter than 12 characters.', + minLength: 'Password may not be shorter than %s characters.', numberRequired: 'Include at least one number.', specialRequired: 'Include at least one special character.', uppercaseRequired: 'Include at least one uppercase letter.', From 762a8295edfad4790da692b4eefbe80e29152e7a Mon Sep 17 00:00:00 2001 From: Ilan Kushnir Date: Wed, 27 Nov 2024 11:47:37 +0200 Subject: [PATCH 15/20] fix: add v4 beta as peer dependenvy --- package-lock.json | 13 ++++++++----- package.json | 5 ++++- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index fb55021..739137d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,12 +7,12 @@ "": { "name": "zxcvbn-custom-matchers", "version": "0.0.0", - "license": "ISC", + "license": "MIT", "devDependencies": { "@types/mocha": "^10.0.10", "@typescript-eslint/eslint-plugin": "^6.7.5", "@typescript-eslint/parser": "^6.7.5", - "@zxcvbn-ts/core": "^3.0.4", + "@zxcvbn-ts/core": "^4.0.0-beta.2", "chai": "^4.5.0", "depcheck": "^1.4.3", "eslint": "^8.18.0", @@ -24,6 +24,9 @@ "nyc": "^17.1.0", "ts-node": "^10.9.2", "typescript": "^5.3.3" + }, + "peerDependencies": { + "@zxcvbn-ts/core": "^4.0.0-beta.2" } }, "node_modules/@ampproject/remapping": { @@ -1044,9 +1047,9 @@ "license": "MIT" }, "node_modules/@zxcvbn-ts/core": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@zxcvbn-ts/core/-/core-3.0.4.tgz", - "integrity": "sha512-aQeiT0F09FuJaAqNrxynlAwZ2mW/1MdXakKWNmGM1Qp/VaY6CnB/GfnMS2T8gB2231Esp1/maCWd8vTG4OuShw==", + "version": "4.0.0-beta.2", + "resolved": "https://registry.npmjs.org/@zxcvbn-ts/core/-/core-4.0.0-beta.2.tgz", + "integrity": "sha512-sG38FCt5+TYZ5qZ8Kl8lzX0bk2eq2yvLdfvFofY7MDDOesr1y0aR7OM2lU4kutJkZxWxV4/KXJSzinzpZv/Tbg==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 8a30121..596744c 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "@types/mocha": "^10.0.10", "@typescript-eslint/eslint-plugin": "^6.7.5", "@typescript-eslint/parser": "^6.7.5", - "@zxcvbn-ts/core": "^3.0.4", + "@zxcvbn-ts/core": "^4.0.0-beta.2", "chai": "^4.5.0", "depcheck": "^1.4.3", "eslint": "^8.18.0", @@ -40,5 +40,8 @@ "nyc": "^17.1.0", "ts-node": "^10.9.2", "typescript": "^5.3.3" + }, + "peerDependencies": { + "@zxcvbn-ts/core": "^4.0.0-beta.2" } } From 7cbcb41339c95a4fd9eac3112b648949850fcd4b Mon Sep 17 00:00:00 2001 From: Ilan Kushnir Date: Wed, 27 Nov 2024 11:48:27 +0200 Subject: [PATCH 16/20] chore: update readme regarding version --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index e8dc8ea..b464b4d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # @zxcvbn-custom-matchers The package is adding custom matchers to the zxcvbn-ts package. The matchers enforce specific character requirements in passwords and provide feedback and scoring. +> Please note! this package works only with zxcvbn-ts v.4 and above. ## Installation ```sh From 2f0cee3de8f4e9d7b5352a5d7bb36d7177193005 Mon Sep 17 00:00:00 2001 From: Ilan Kushnir Date: Wed, 27 Nov 2024 12:18:34 +0200 Subject: [PATCH 17/20] fix: add translated suggestions to matchers --- package-lock.json | 9 +++++++++ package.json | 2 ++ src/matchers/lowercaseMatcher.ts | 8 ++++---- src/matchers/minLengthMatcher.ts | 8 ++++---- src/matchers/numberMatcher.ts | 8 ++++---- src/matchers/specialMatcher.ts | 8 ++++---- src/matchers/uppercaseMatcher.ts | 8 ++++---- 7 files changed, 31 insertions(+), 20 deletions(-) diff --git a/package-lock.json b/package-lock.json index 739137d..191e3b0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@typescript-eslint/eslint-plugin": "^6.7.5", "@typescript-eslint/parser": "^6.7.5", "@zxcvbn-ts/core": "^4.0.0-beta.2", + "@zxcvbn-ts/language-en": "^3.0.2", "chai": "^4.5.0", "depcheck": "^1.4.3", "eslint": "^8.18.0", @@ -20,6 +21,7 @@ "eslint-plugin-prettier": "^5.0.1", "husky": "^8.0.3", "lint-staged": "^15.0.1", + "lodash": "^4.17.21", "mocha": "^10.2.0", "nyc": "^17.1.0", "ts-node": "^10.9.2", @@ -1056,6 +1058,13 @@ "fastest-levenshtein": "1.0.16" } }, + "node_modules/@zxcvbn-ts/language-en": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@zxcvbn-ts/language-en/-/language-en-3.0.2.tgz", + "integrity": "sha512-Zp+zL+I6Un2Bj0tRXNs6VUBq3Djt+hwTwUz4dkt2qgsQz47U0/XthZ4ULrT/RxjwJRl5LwiaKOOZeOtmixHnjg==", + "dev": true, + "license": "MIT" + }, "node_modules/acorn": { "version": "8.14.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", diff --git a/package.json b/package.json index 596744c..d62be6d 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "@typescript-eslint/eslint-plugin": "^6.7.5", "@typescript-eslint/parser": "^6.7.5", "@zxcvbn-ts/core": "^4.0.0-beta.2", + "@zxcvbn-ts/language-en": "^3.0.2", "chai": "^4.5.0", "depcheck": "^1.4.3", "eslint": "^8.18.0", @@ -36,6 +37,7 @@ "eslint-plugin-prettier": "^5.0.1", "husky": "^8.0.3", "lint-staged": "^15.0.1", + "lodash": "^4.17.21", "mocha": "^10.2.0", "nyc": "^17.1.0", "ts-node": "^10.9.2", diff --git a/src/matchers/lowercaseMatcher.ts b/src/matchers/lowercaseMatcher.ts index 3b0180c..a30fd88 100644 --- a/src/matchers/lowercaseMatcher.ts +++ b/src/matchers/lowercaseMatcher.ts @@ -1,4 +1,4 @@ -import { Matcher, Match } from '@zxcvbn-ts/core/dist/types'; +import { Matcher, Match } from '@zxcvbn-ts/core'; export const lowercaseMatcher: Matcher = { Matching: class LowercaseMatcher { @@ -15,11 +15,11 @@ export const lowercaseMatcher: Matcher = { }, feedback: options => { return { - warning: options.translations.warnings.lowercaseRequired || 'lowercaseRequired', - suggestions: [options.translations.suggestions.lowercaseRequired || 'lowercaseRequired'], + warning: options.translations.warnings['lowercaseRequired'] || 'lowercaseRequired', + suggestions: [options.translations.suggestions['lowercaseRequired'] || 'lowercaseRequired'], }; }, - scoring(_match) { + scoring() { return -100; }, }; diff --git a/src/matchers/minLengthMatcher.ts b/src/matchers/minLengthMatcher.ts index cf2df12..92b3233 100644 --- a/src/matchers/minLengthMatcher.ts +++ b/src/matchers/minLengthMatcher.ts @@ -1,4 +1,4 @@ -import { Matcher, Match } from '@zxcvbn-ts/core/dist/types'; +import { Matcher, Match } from '@zxcvbn-ts/core'; export const minLengthMatcher = (minLength: number): Matcher => ({ Matching: class MinLengthMatcher { @@ -12,11 +12,11 @@ export const minLengthMatcher = (minLength: number): Matcher => ({ }, 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['minLength']?.replace('%s', minLength) || 'minLength', + suggestions: [options.translations.suggestions['minLength']?.replace('%s', minLength) || 'minLength'], }; }, - scoring(_match) { + scoring() { return -100; }, }); diff --git a/src/matchers/numberMatcher.ts b/src/matchers/numberMatcher.ts index 7407623..4e2ea3f 100644 --- a/src/matchers/numberMatcher.ts +++ b/src/matchers/numberMatcher.ts @@ -1,4 +1,4 @@ -import { Matcher, Match } from '@zxcvbn-ts/core/dist/types'; +import { Matcher, Match } from '@zxcvbn-ts/core'; export const numberMatcher: Matcher = { Matching: class NumberMatcher { @@ -15,11 +15,11 @@ export const numberMatcher: Matcher = { }, feedback: options => { return { - warning: options.translations.warnings.numberRequired || 'numberRequired', - suggestions: [options.translations.suggestions.numberRequired || 'numberRequired'], + warning: options.translations.warnings['numberRequired'] || 'numberRequired', + suggestions: [options.translations.suggestions['numberRequired'] || 'numberRequired'], }; }, - scoring(_match) { + scoring() { return -100; }, }; diff --git a/src/matchers/specialMatcher.ts b/src/matchers/specialMatcher.ts index a407382..a0a5fc9 100644 --- a/src/matchers/specialMatcher.ts +++ b/src/matchers/specialMatcher.ts @@ -1,4 +1,4 @@ -import { Matcher, Match } from '@zxcvbn-ts/core/dist/types'; +import { Matcher, Match } from '@zxcvbn-ts/core'; export const specialMatcher: Matcher = { Matching: class SpecialMatcher { @@ -16,11 +16,11 @@ export const specialMatcher: Matcher = { }, feedback: options => { return { - warning: options.translations.warnings.specialRequired || 'specialRequired', - suggestions: [options.translations.suggestions.specialRequired || 'specialRequired'], + warning: options.translations.warnings['specialRequired'] || 'specialRequired', + suggestions: [options.translations.suggestions['specialRequired'] || 'specialRequired'], }; }, - scoring(_match) { + scoring() { return -100; }, }; diff --git a/src/matchers/uppercaseMatcher.ts b/src/matchers/uppercaseMatcher.ts index cdafc0e..ae7627b 100644 --- a/src/matchers/uppercaseMatcher.ts +++ b/src/matchers/uppercaseMatcher.ts @@ -1,4 +1,4 @@ -import { Matcher, Match } from '@zxcvbn-ts/core/dist/types'; +import { Matcher, Match } from '@zxcvbn-ts/core'; export const uppercaseMatcher: Matcher = { Matching: class UppercaseMatcher { @@ -15,11 +15,11 @@ export const uppercaseMatcher: Matcher = { }, feedback: options => { return { - warning: options.translations.warnings.uppercaseRequired || 'uppercaseRequired', - suggestions: [options.translations.suggestions.uppercaseRequired || 'uppercaseRequired'], + warning: options.translations.warnings['uppercaseRequired'] || 'uppercaseRequired', + suggestions: [options.translations.suggestions['uppercaseRequired'] || 'uppercaseRequired'], }; }, - scoring(_match) { + scoring() { return -100; }, }; From 4de47d0ebd3d03851457f51c77580ac4360c4a8f Mon Sep 17 00:00:00 2001 From: Ilan Kushnir Date: Wed, 27 Nov 2024 12:19:00 +0200 Subject: [PATCH 18/20] tests: refactor tests to go through the zxcvbn-ts/core package --- tests/unit/matchers.test.ts | 201 ++++++++++++++++++++---------------- 1 file changed, 114 insertions(+), 87 deletions(-) diff --git a/tests/unit/matchers.test.ts b/tests/unit/matchers.test.ts index e481e5d..b584b8e 100644 --- a/tests/unit/matchers.test.ts +++ b/tests/unit/matchers.test.ts @@ -1,107 +1,134 @@ import 'mocha'; import { expect } from 'chai'; -import { uppercaseMatcher, lowercaseMatcher, numberMatcher, specialMatcher, minLengthMatcher } from '../../src'; -import { customMatchersTranslations } from '../../src/translations'; - -describe('uppercaseMatcher', () => { - it('should return a match for missing uppercase letters', () => { - const result = uppercaseMatcher.Matching.prototype.match({ password: 'password123!' }); - expect(result).to.deep.include({ pattern: 'uppercaseRequired', token: 'password123!', i: 0, j: 11 }); - expect(customMatchersTranslations.warnings.uppercaseRequired).to.equal( - 'At least one uppercase letter is required.', - ); - expect(customMatchersTranslations.suggestions.uppercaseRequired).to.equal('Include at least one uppercase letter.'); - }); +import { ZxcvbnFactory, type OptionsType } from '@zxcvbn-ts/core'; +import { + uppercaseMatcher, + lowercaseMatcher, + numberMatcher, + specialMatcher, + minLengthMatcher, + customMatchersTranslations, +} from '../../src'; +import { translations as baseTranslations } from '@zxcvbn-ts/language-en'; +import { merge } from 'lodash'; - it('should return a score of -100 for missing uppercase letters', () => { - const match = { pattern: 'uppercaseRequired', token: 'password123!', i: 0, j: 11 }; - const score = uppercaseMatcher.scoring(match); - expect(score).to.equal(-100); - }); -}); +const MIN_LENGTH = 12; +const MIN_SECURE_SCORE = 3; +const PERFECT_SCORE = 4; +const SAMPLE_STRONG_PASSWORD = 'de#dSh251dft!'; -describe('lowercaseMatcher', () => { - it('should return a match for missing lowercase letters', () => { - const result = lowercaseMatcher.Matching.prototype.match({ password: 'PASSWORD123!' }); - expect(result).to.deep.include({ pattern: 'lowercaseRequired', token: 'PASSWORD123!', i: 0, j: 11 }); - expect(customMatchersTranslations.warnings.lowercaseRequired).to.equal( - 'At least one lowercase letter is required.', - ); - expect(customMatchersTranslations.suggestions.lowercaseRequired).to.equal('Include at least one lowercase letter.'); - }); +// Package setup +const customMatchers = { + minLength: minLengthMatcher(MIN_LENGTH), + specialRequired: specialMatcher, + numberRequired: numberMatcher, + lowercaseRequired: lowercaseMatcher, + uppercaseRequired: uppercaseMatcher, +}; +const mergedTranslations = merge({}, baseTranslations, customMatchersTranslations); +const options: OptionsType = { translations: mergedTranslations }; +const zxcvbn = new ZxcvbnFactory(options, customMatchers); - it('should return a score of -100 for missing lowercase letters', () => { - const match = { pattern: 'lowercaseRequired', token: 'PASSWORD123!', i: 0, j: 11 }; - const score = lowercaseMatcher.scoring(match); - expect(score).to.equal(-100); - }); -}); +describe('Password Validation Requirements', () => { + describe('Uppercase Character Requirement', () => { + it('should detect when uppercase letters are missing', () => { + const result = zxcvbn.check('password123!'); + const hasUppercaseWarning = result.feedback.warning === customMatchersTranslations.warnings.uppercaseRequired; -describe('numberMatcher', () => { - it('should return a match for missing numbers', () => { - const result = numberMatcher.Matching.prototype.match({ password: 'Password!' }); - expect(result).to.deep.include({ pattern: 'numberRequired', token: 'Password!', i: 0, j: 8 }); - expect(customMatchersTranslations.warnings.numberRequired).to.equal('At least one number is required.'); - expect(customMatchersTranslations.suggestions.numberRequired).to.equal('Include at least one number.'); - }); + expect(hasUppercaseWarning).to.be.true; + expect(result.score).to.be.lessThan(MIN_SECURE_SCORE); + }); - it('should return a score of -100 for missing numbers', () => { - const match = { pattern: 'numberRequired', token: 'Password!', i: 0, j: 8 }; - const score = numberMatcher.scoring(match); - expect(score).to.equal(-100); - }); -}); + it('should pass when uppercase letter is included', () => { + const result = zxcvbn.check('Password123!'); + const hasUppercaseWarning = result.feedback.warning === customMatchersTranslations.warnings.uppercaseRequired; -describe('specialMatcher', () => { - it('should return a match for missing special characters', () => { - const result = specialMatcher.Matching.prototype.match({ password: 'Password123' }); - expect(result).to.deep.include({ pattern: 'specialRequired', token: 'Password123', i: 0, j: 10 }); - expect(customMatchersTranslations.warnings.specialRequired).to.equal('At least one special character is required.'); - expect(customMatchersTranslations.suggestions.specialRequired).to.equal('Include at least one special character.'); + expect(hasUppercaseWarning).to.be.false; + }); }); - it('should return a score of -100 for missing special characters', () => { - const match = { pattern: 'specialRequired', token: 'Password123', i: 0, j: 10 }; - const score = specialMatcher.scoring(match); - expect(score).to.equal(-100); + describe('Lowercase Character Requirement', () => { + it('should detect when lowercase letters are missing', () => { + const result = zxcvbn.check('PASSWORD123!'); + const hasLowercaseWarning = result.feedback.warning === customMatchersTranslations.warnings.lowercaseRequired; + + expect(hasLowercaseWarning).to.be.true; + expect(result.score).to.be.lessThan(MIN_SECURE_SCORE); + }); + + it('should pass when lowercase letter is included', () => { + const result = zxcvbn.check('PASSWORd123!'); + const hasLowercaseWarning = result.feedback.warning === customMatchersTranslations.warnings.lowercaseRequired; + + expect(hasLowercaseWarning).to.be.false; + }); }); -}); -describe('minLengthMatcher', () => { - const minLength = 12; - const matcher = minLengthMatcher(minLength); - - it('should return a match for passwords shorter than the minimum length', () => { - const result = matcher.Matching.prototype.match({ password: 'short' }); - expect(result).to.deep.include({ pattern: 'minLength', token: 'short', i: 0, j: 4 }); - expect(customMatchersTranslations.warnings.minLength).to.equal('Password must be at least 12 characters long.'); - expect(customMatchersTranslations.suggestions.minLength).to.equal( - 'Password may not be shorter than 12 characters.', - ); + describe('Number Requirement', () => { + it('should detect when numbers are missing', () => { + const result = zxcvbn.check('Passwdfsgsdfgdsfgord!'); + const hasNumberWarning = result.feedback.warning === customMatchersTranslations.warnings.numberRequired; + + expect(hasNumberWarning).to.be.true; + expect(result.score).to.be.lessThan(MIN_SECURE_SCORE); + }); + + it('should pass when number is included', () => { + const result = zxcvbn.check('Password1!'); + const hasNumberWarning = result.feedback.warning === customMatchersTranslations.warnings.numberRequired; + + expect(hasNumberWarning).to.be.false; + }); }); - it('should return no matches for passwords meeting the minimum length', () => { - const result = matcher.Matching.prototype.match({ password: 'longenoughpassword' }); - expect(result).to.be.empty; + describe('Special Character Requirement', () => { + it('should detect when special characters are missing', () => { + const result = zxcvbn.check('Password0123456'); + const hasSpecialWarning = result.feedback.warning === customMatchersTranslations.warnings.specialRequired; + + expect(hasSpecialWarning).to.be.true; + expect(result.score).to.be.lessThan(MIN_SECURE_SCORE); + }); + + it('should pass when special character is included', () => { + const result = zxcvbn.check('Password123!'); + const hasSpecialWarning = result.feedback.warning === customMatchersTranslations.warnings.specialRequired; + + expect(hasSpecialWarning).to.be.false; + }); }); - it('should return a score of -100 for passwords shorter than the minimum length', () => { - const match = { pattern: 'minLength', token: 'short', i: 0, j: 4 }; - const score = matcher.scoring(match); - expect(score).to.equal(-100); + describe('Minimum Length Requirement', () => { + it('should detect passwords shorter than minimum length', () => { + const result = zxcvbn.check('short'); + const expectedWarning = customMatchersTranslations.warnings.minLength.replace('%s', String(MIN_LENGTH)); + const hasLengthWarning = result.feedback.warning === expectedWarning; + + expect(hasLengthWarning).to.be.true; + expect(result.score).to.be.lessThan(MIN_SECURE_SCORE); + }); + + it('should pass for passwords meeting minimum length', () => { + const result = zxcvbn.check('longenoughpassword'); + const hasLengthWarning = result.feedback.warning === customMatchersTranslations.warnings.minLength; + + expect(hasLengthWarning).to.be.false; + }); }); -}); -describe('multiple matchers', () => { - it('should return no matches for a password meeting all requirements', () => { - const resultUppercase = uppercaseMatcher.Matching.prototype.match({ password: 'Password123!' }); - const resultLowercase = lowercaseMatcher.Matching.prototype.match({ password: 'Password123!' }); - const resultNumber = numberMatcher.Matching.prototype.match({ password: 'Password123!' }); - const resultSpecial = specialMatcher.Matching.prototype.match({ password: 'Password123!' }); - - expect(resultUppercase).to.be.empty; - expect(resultLowercase).to.be.empty; - expect(resultNumber).to.be.empty; - expect(resultSpecial).to.be.empty; + describe('Combined Requirements', () => { + it('should pass all requirements for a compliant password', () => { + const result = zxcvbn.check(SAMPLE_STRONG_PASSWORD); + + expect(result.feedback.warning).to.be.null; + expect(result.score).to.equal(PERFECT_SCORE); + }); + + it('should detect multiple missing requirements', () => { + const result = zxcvbn.check('password'); + + expect(result.feedback.warning).to.not.be.undefined; + expect(result.score).to.be.lessThan(MIN_SECURE_SCORE); + }); }); }); From cc3da8bb9bca26b7fd36d8e115079ca676607903 Mon Sep 17 00:00:00 2001 From: Ilan Kushnir Date: Wed, 27 Nov 2024 12:20:50 +0200 Subject: [PATCH 19/20] tests: add suggestions assertions --- tests/unit/matchers.test.ts | 118 ++++++++++++++++++++++-------------- 1 file changed, 72 insertions(+), 46 deletions(-) diff --git a/tests/unit/matchers.test.ts b/tests/unit/matchers.test.ts index b584b8e..d24b8d8 100644 --- a/tests/unit/matchers.test.ts +++ b/tests/unit/matchers.test.ts @@ -7,127 +7,153 @@ import { numberMatcher, specialMatcher, minLengthMatcher, - customMatchersTranslations, + customMatchersTranslations } from '../../src'; import { translations as baseTranslations } from '@zxcvbn-ts/language-en'; import { merge } from 'lodash'; +// Constants const MIN_LENGTH = 12; const MIN_SECURE_SCORE = 3; const PERFECT_SCORE = 4; const SAMPLE_STRONG_PASSWORD = 'de#dSh251dft!'; -// Package setup +// Configuration setup const customMatchers = { minLength: minLengthMatcher(MIN_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); +/** + * Test suite for password validation requirements, warnings, and suggestions + */ describe('Password Validation Requirements', () => { describe('Uppercase Character Requirement', () => { - it('should detect when uppercase letters are missing', () => { - const result = zxcvbn.check('password123!'); - const hasUppercaseWarning = result.feedback.warning === customMatchersTranslations.warnings.uppercaseRequired; + const testPassword = 'password123!'; + const validPassword = 'Password123!'; + + it('should provide appropriate warning and suggestion when uppercase letters are missing', () => { + const result = zxcvbn.check(testPassword); - expect(hasUppercaseWarning).to.be.true; + expect(result.feedback.warning).to.equal(customMatchersTranslations.warnings.uppercaseRequired); + expect(result.feedback.suggestions).to.include(customMatchersTranslations.suggestions.uppercaseRequired); expect(result.score).to.be.lessThan(MIN_SECURE_SCORE); }); - it('should pass when uppercase letter is included', () => { - const result = zxcvbn.check('Password123!'); - const hasUppercaseWarning = result.feedback.warning === customMatchersTranslations.warnings.uppercaseRequired; + it('should not show uppercase warnings or suggestions when requirement is met', () => { + const result = zxcvbn.check(validPassword); - expect(hasUppercaseWarning).to.be.false; + expect(result.feedback.warning).to.not.equal(customMatchersTranslations.warnings.uppercaseRequired); + expect(result.feedback.suggestions).to.not.include(customMatchersTranslations.suggestions.uppercaseRequired); }); }); describe('Lowercase Character Requirement', () => { - it('should detect when lowercase letters are missing', () => { - const result = zxcvbn.check('PASSWORD123!'); - const hasLowercaseWarning = result.feedback.warning === customMatchersTranslations.warnings.lowercaseRequired; + const testPassword = 'PASSWORD123!'; + const validPassword = 'PASSWORd123!'; + + it('should provide appropriate warning and suggestion when lowercase letters are missing', () => { + const result = zxcvbn.check(testPassword); - expect(hasLowercaseWarning).to.be.true; + expect(result.feedback.warning).to.equal(customMatchersTranslations.warnings.lowercaseRequired); + expect(result.feedback.suggestions).to.include(customMatchersTranslations.suggestions.lowercaseRequired); expect(result.score).to.be.lessThan(MIN_SECURE_SCORE); }); - it('should pass when lowercase letter is included', () => { - const result = zxcvbn.check('PASSWORd123!'); - const hasLowercaseWarning = result.feedback.warning === customMatchersTranslations.warnings.lowercaseRequired; + it('should not show lowercase warnings or suggestions when requirement is met', () => { + const result = zxcvbn.check(validPassword); - expect(hasLowercaseWarning).to.be.false; + expect(result.feedback.warning).to.not.equal(customMatchersTranslations.warnings.lowercaseRequired); + expect(result.feedback.suggestions).to.not.include(customMatchersTranslations.suggestions.lowercaseRequired); }); }); describe('Number Requirement', () => { - it('should detect when numbers are missing', () => { - const result = zxcvbn.check('Passwdfsgsdfgdsfgord!'); - const hasNumberWarning = result.feedback.warning === customMatchersTranslations.warnings.numberRequired; + const testPassword = 'Passwdfsgsdfgdsfgord!'; + const validPassword = 'Password1!'; - expect(hasNumberWarning).to.be.true; + 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.score).to.be.lessThan(MIN_SECURE_SCORE); }); - it('should pass when number is included', () => { - const result = zxcvbn.check('Password1!'); - const hasNumberWarning = result.feedback.warning === customMatchersTranslations.warnings.numberRequired; + it('should not show number warnings or suggestions when requirement is met', () => { + const result = zxcvbn.check(validPassword); - expect(hasNumberWarning).to.be.false; + expect(result.feedback.warning).to.not.equal(customMatchersTranslations.warnings.numberRequired); + expect(result.feedback.suggestions).to.not.include(customMatchersTranslations.suggestions.numberRequired); }); }); describe('Special Character Requirement', () => { - it('should detect when special characters are missing', () => { - const result = zxcvbn.check('Password0123456'); - const hasSpecialWarning = result.feedback.warning === customMatchersTranslations.warnings.specialRequired; + const testPassword = 'Password0123456'; + const validPassword = 'Password123!'; + + it('should provide appropriate warning and suggestion when special characters are missing', () => { + const result = zxcvbn.check(testPassword); - expect(hasSpecialWarning).to.be.true; + expect(result.feedback.warning).to.equal(customMatchersTranslations.warnings.specialRequired); + expect(result.feedback.suggestions).to.include(customMatchersTranslations.suggestions.specialRequired); expect(result.score).to.be.lessThan(MIN_SECURE_SCORE); }); - it('should pass when special character is included', () => { - const result = zxcvbn.check('Password123!'); - const hasSpecialWarning = result.feedback.warning === customMatchersTranslations.warnings.specialRequired; + it('should not show special character warnings or suggestions when requirement is met', () => { + const result = zxcvbn.check(validPassword); - expect(hasSpecialWarning).to.be.false; + expect(result.feedback.warning).to.not.equal(customMatchersTranslations.warnings.specialRequired); + expect(result.feedback.suggestions).to.not.include(customMatchersTranslations.suggestions.specialRequired); }); }); describe('Minimum Length Requirement', () => { - it('should detect passwords shorter than minimum length', () => { - const result = zxcvbn.check('short'); + const testPassword = 'short'; + const validPassword = 'longenoughpassword'; + + 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 hasLengthWarning = result.feedback.warning === expectedWarning; + const expectedSuggestion = customMatchersTranslations.suggestions.minLength.replace('%s', String(MIN_LENGTH)); - expect(hasLengthWarning).to.be.true; + 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 pass for passwords meeting minimum length', () => { - const result = zxcvbn.check('longenoughpassword'); - const hasLengthWarning = result.feedback.warning === customMatchersTranslations.warnings.minLength; + 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)); - expect(hasLengthWarning).to.be.false; + expect(result.feedback.warning).to.not.equal(unexpectedWarning); + expect(result.feedback.suggestions).to.not.include(unexpectedSuggestion); }); }); describe('Combined Requirements', () => { - it('should pass all requirements for a compliant password', () => { + it('should not show any warnings or suggestions for a fully compliant password', () => { const result = zxcvbn.check(SAMPLE_STRONG_PASSWORD); expect(result.feedback.warning).to.be.null; + expect(result.feedback.suggestions).to.be.empty; expect(result.score).to.equal(PERFECT_SCORE); }); - it('should detect multiple missing requirements', () => { + it('should provide multiple suggestions for a weak password', () => { const result = zxcvbn.check('password'); - expect(result.feedback.warning).to.not.be.undefined; + expect(result.feedback.warning).to.not.be.null; + expect(result.feedback.suggestions).to.not.be.empty; + expect(result.feedback.suggestions.length).to.be.greaterThan(1); expect(result.score).to.be.lessThan(MIN_SECURE_SCORE); }); }); From 1bbd6315fced691695fcf7cc6048d68a8fb11f92 Mon Sep 17 00:00:00 2001 From: Ilan Kushnir Date: Wed, 27 Nov 2024 12:23:01 +0200 Subject: [PATCH 20/20] tests: remove redundant lines --- tests/unit/matchers.test.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/tests/unit/matchers.test.ts b/tests/unit/matchers.test.ts index d24b8d8..6dfee92 100644 --- a/tests/unit/matchers.test.ts +++ b/tests/unit/matchers.test.ts @@ -12,13 +12,12 @@ import { import { translations as baseTranslations } from '@zxcvbn-ts/language-en'; import { merge } from 'lodash'; -// Constants const MIN_LENGTH = 12; const MIN_SECURE_SCORE = 3; const PERFECT_SCORE = 4; const SAMPLE_STRONG_PASSWORD = 'de#dSh251dft!'; -// Configuration setup +// Package setup const customMatchers = { minLength: minLengthMatcher(MIN_LENGTH), specialRequired: specialMatcher, @@ -31,9 +30,7 @@ const mergedTranslations = merge({}, baseTranslations, customMatchersTranslation const options: OptionsType = { translations: mergedTranslations }; const zxcvbn = new ZxcvbnFactory(options, customMatchers); -/** - * Test suite for password validation requirements, warnings, and suggestions - */ + describe('Password Validation Requirements', () => { describe('Uppercase Character Requirement', () => { const testPassword = 'password123!';