Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add max length custom matcher #10

Merged
merged 23 commits into from
Nov 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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