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 translations for custom matchers #9

Merged
merged 20 commits into from
Nov 27, 2024
Merged
Show file tree
Hide file tree
Changes from 11 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
16 changes: 11 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,18 @@ 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('lowercase', lowercaseMatcher);
zxcvbnOptions.addMatcher('number', numberMatcher);
zxcvbnOptions.addMatcher('special', specialMatcher);
zxcvbnOptions.addMatcher('uppercase', uppercaseMatcher);
zxcvbnOptions.addMatcher('minLength', minLengthMatcher(10));
zxcvbnOptions.addMatcher('lowercaseRequired', lowercaseMatcher);
zxcvbnOptions.addMatcher('minLength', numberMatcher);
zxcvbnOptions.addMatcher('numberRequired', specialMatcher);
zxcvbnOptions.addMatcher('specialRequired', uppercaseMatcher);
zxcvbnOptions.addMatcher('uppercaseRequired', minLengthMatcher(10));

// Use zxcvbn as usual
import { zxcvbn } from '@zxcvbn-ts/core';
Expand Down
3 changes: 2 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -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';
9 changes: 6 additions & 3 deletions src/matchers/lowercaseMatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: 'lowercaseRequired', 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.lowercaseRequired || 'lowercaseRequired',
suggestions: [],
};
},
scoring(_match) {
return -100;
Expand Down
7 changes: 5 additions & 2 deletions src/matchers/minLengthMatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
9 changes: 6 additions & 3 deletions src/matchers/numberMatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: 'numberRequired', 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.numberRequired || 'numberRequired',
suggestions: [],
};
},
scoring(_match) {
return -100;
Expand Down
9 changes: 6 additions & 3 deletions src/matchers/specialMatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: 'specialRequired', 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.specialRequired || 'specialRequired',
suggestions: [],
};
},
scoring(_match) {
return -100;
Expand Down
9 changes: 6 additions & 3 deletions src/matchers/uppercaseMatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: 'uppercaseRequired', 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.uppercaseRequired || 'uppercaseRequired',
suggestions: [],
};
},
scoring(_match) {
return -100;
Expand Down
17 changes: 17 additions & 0 deletions src/translations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
export const customMatchersTranslations = {
warnings: {
lowercaseRequired: 'At least one lowercase letter is required.',
minLength: 'Password must be at least 12 characters long.',
ilan-kushnir-payu-gpo marked this conversation as resolved.
Show resolved Hide resolved
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.',
numberRequired: 'Include at least one number.',
specialRequired: 'Include at least one special character.',
uppercaseRequired: 'Include at least one uppercase letter.',
},
timeEstimation: {},
};
65 changes: 26 additions & 39 deletions tests/unit/matchers.test.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,20 @@
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: 'uppercase', 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 feedback = uppercaseMatcher.feedback(match);
expect(feedback.warning).to.equal('Include at least one uppercase letter.');
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', () => {
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);
});
Expand All @@ -24,17 +23,15 @@ 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 });
});

it('should provide correct feedback for missing lowercase letters', () => {
const match = { pattern: 'lowercase', 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.');
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', () => {
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);
});
Expand All @@ -43,17 +40,13 @@ 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 });
});

it('should provide correct feedback for missing numbers', () => {
const match = { pattern: 'number', 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.');
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', () => {
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);
});
Expand All @@ -62,42 +55,36 @@ 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 });
});

it('should provide correct feedback for missing special characters', () => {
const match = { pattern: 'special', 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.');
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', () => {
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);
});
});

describe('minLengthMatcher', () => {
const minLength = 10;
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.',
);
});

it('should return no matches for passwords meeting the minimum length', () => {
const result = matcher.Matching.prototype.match({ password: 'longenoughpassword' });
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 ${minLength} 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);
Expand Down