Skip to content

Commit

Permalink
feat: add max length custom matcher
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
ilan-kushnir-payu-gpo and Ilan Kushnir authored Nov 27, 2024
1 parent 69e1b95 commit 777fb3d
Show file tree
Hide file tree
Showing 5 changed files with 66 additions and 6 deletions.
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
lowercaseMatcher,
uppercaseMatcher,
minLengthMatcher,
maxLengthMatcher,
customMatchersTranslations
} from 'zxcvbn-custom-matchers';
import { merge } from 'lodash';
Expand All @@ -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,
Expand Down Expand Up @@ -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.
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
22 changes: 22 additions & 0 deletions src/matchers/maxLengthMatcher.ts
Original file line number Diff line number Diff line change
@@ -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;
},
});
2 changes: 2 additions & 0 deletions src/translations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ 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.',
},
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.',
Expand Down
36 changes: 31 additions & 5 deletions tests/unit/matchers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,35 +2,37 @@ 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!';

// 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!';
Expand Down Expand Up @@ -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);
Expand Down

0 comments on commit 777fb3d

Please sign in to comment.