Skip to content

Commit

Permalink
Merge pull request #8 from PayU/min-len
Browse files Browse the repository at this point in the history
feat: min length matcher
  • Loading branch information
ilan-kushnir-payu-gpo authored Nov 25, 2024
2 parents 4a4a9d7 + 5e621fa commit 7e19b06
Show file tree
Hide file tree
Showing 8 changed files with 139 additions and 61 deletions.
35 changes: 25 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,49 +10,64 @@ npm install zxcvbn-custom-matchers
### Usage
```ts
import { zxcvbnOptions } from '@zxcvbn-ts/core';
import {
import {
lowercaseMatcher,
numberMatcher,
specialMatcher,
uppercaseMatcher
uppercaseMatcher,
minLengthMatcher
} from 'zxcvbn-custom-matchers';

// Add the matchers
zxcvbnOptions.addMatcher('lowercaseMatcher', lowercaseMatcher);
zxcvbnOptions.addMatcher('numberMatcher', numberMatcher);
zxcvbnOptions.addMatcher('specialMatcher', specialMatcher);
zxcvbnOptions.addMatcher('uppercaseMatcher', uppercaseMatcher);
zxcvbnOptions.addMatcher('lowercase', lowercaseMatcher);
zxcvbnOptions.addMatcher('number', numberMatcher);
zxcvbnOptions.addMatcher('special', specialMatcher);
zxcvbnOptions.addMatcher('uppercase', uppercaseMatcher);
zxcvbnOptions.addMatcher('minLength', minLengthMatcher(10));

// Use zxcvbn as usual
import { zxcvbn } from '@zxcvbn-ts/core';
const result = zxcvbn('password123');
console.log(result);
```

>Note: When adding a custom matcher with addMatcher, the first parameter (a string) should be the same as the matcher's pattern.
## Matchers Description

This project includes several matchers that enforce specific character requirements in passwords. Each matcher checks for a particular type of character and provides feedback and scoring.

### Uppercase Matcher

- **Pattern**: `uppercase`
- **Purpose**: Ensures the password contains at least one uppercase letter.
- **Feedback**: Suggests including at least one uppercase letter if missing.
- **Scoring**: Returns a score of `-100` if missing uppercase letters.
- **Scoring**: Returns a score of `1` if missing uppercase letters.

### Lowercase Matcher

- **Pattern**: `lowercase`
- **Purpose**: Ensures the password contains at least one lowercase letter.
- **Feedback**: Suggests including at least one lowercase letter if missing.
- **Scoring**: Returns a score of `-100` if missing lowercase letters.
- **Scoring**: Returns a score of `1` if missing lowercase letters.

### Number Matcher

- **Pattern**: `number`
- **Purpose**: Ensures the password contains at least one number.
- **Feedback**: Suggests including at least one number if missing.
- **Scoring**: Returns a score of `-100` if missing numbers.
- **Scoring**: Returns a score of `1` if missing numbers.

### Special Character Matcher

- **Pattern**: `special`
- **Purpose**: Ensures the password contains at least one special character (e.g., !, @, #, $, etc.).
- **Feedback**: Suggests including at least one special character if missing.
- **Scoring**: Returns a score of `-100` if missing special characters.
- **Scoring**: Returns a score of `1` if missing special characters.

### Minimum Length Matcher

- **Pattern**: `minLength`
- **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.
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export { uppercaseMatcher } from './matchers/uppercaseMatcher';
export { minLengthMatcher } from './matchers/minLengthMatcher';
export { lowercaseMatcher } from './matchers/lowercaseMatcher';
export { numberMatcher } from './matchers/numberMatcher';
export { specialMatcher } from './matchers/specialMatcher';
6 changes: 4 additions & 2 deletions src/matchers/lowercaseMatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ import { Matcher, Match } from '@zxcvbn-ts/core/dist/types';
export const lowercaseMatcher: Matcher = {
Matching: class LowercaseMatcher {
match({ password }: { password: string }): Match[] {
const matches: Match[] = [];
for (const char of password) {
if (char >= 'a' && char <= 'z') {
return [];
return matches;
}
}
return [{ pattern: 'lowercase', token: password, i: 0, j: password.length - 1 }];
matches.push({ pattern: 'lowercase', token: password, i: 0, j: password.length - 1 });
return matches;
}
},
feedback(_match) {
Expand Down
19 changes: 19 additions & 0 deletions src/matchers/minLengthMatcher.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Matcher, Match } from '@zxcvbn-ts/core/dist/types';

export const minLengthMatcher = (minLength: number): Matcher => ({
Matching: class MinLengthMatcher {
match({ password }: { password: string }): Match[] {
const matches: Match[] = [];
if (password.length < minLength) {
matches.push({ pattern: 'minLength', token: password, i: 0, j: password.length - 1 });
}
return matches;
}
},
feedback(_match) {
return { warning: `Password may not be shorter than ${minLength} characters.`, suggestions: [] };
},
scoring(_match) {
return -100;
},
});
6 changes: 4 additions & 2 deletions src/matchers/numberMatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ import { Matcher, Match } from '@zxcvbn-ts/core/dist/types';
export const numberMatcher: Matcher = {
Matching: class NumberMatcher {
match({ password }: { password: string }): Match[] {
const matches: Match[] = [];
for (const char of password) {
if (char >= '0' && char <= '9') {
return [];
return matches;
}
}
return [{ pattern: 'number', token: password, i: 0, j: password.length - 1 }];
matches.push({ pattern: 'number', token: password, i: 0, j: password.length - 1 });
return matches;
}
},
feedback(_match) {
Expand Down
6 changes: 4 additions & 2 deletions src/matchers/specialMatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ import { Matcher, Match } from '@zxcvbn-ts/core/dist/types';
export const specialMatcher: Matcher = {
Matching: class SpecialMatcher {
match({ password }: { password: string }): Match[] {
const matches: Match[] = [];
const specialChars = "!@#$%^&*()_+[]{}|;:',.<>?/";
for (const char of password) {
if (specialChars.includes(char)) {
return [];
return matches;
}
}
return [{ pattern: 'special', token: password, i: 0, j: password.length - 1 }];
matches.push({ pattern: 'special', token: password, i: 0, j: password.length - 1 });
return matches;
}
},
feedback(_match) {
Expand Down
6 changes: 4 additions & 2 deletions src/matchers/uppercaseMatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ import { Matcher, Match } from '@zxcvbn-ts/core/dist/types';
export const uppercaseMatcher: Matcher = {
Matching: class UppercaseMatcher {
match({ password }: { password: string }): Match[] {
const matches: Match[] = [];
for (const char of password) {
if (char >= 'A' && char <= 'Z') {
return [];
return matches;
}
}
return [{ pattern: 'uppercase', token: password, i: 0, j: password.length - 1 }];
matches.push({ pattern: 'uppercase', token: password, i: 0, j: password.length - 1 });
return matches;
}
},
feedback(_match) {
Expand Down
121 changes: 78 additions & 43 deletions tests/unit/matchers.test.ts
Original file line number Diff line number Diff line change
@@ -1,85 +1,120 @@
import 'mocha';
import { expect } from 'chai';
import { uppercaseMatcher, lowercaseMatcher, numberMatcher, specialMatcher } from '../../src';
import { uppercaseMatcher, lowercaseMatcher, numberMatcher, specialMatcher, minLengthMatcher } from '../../src';

describe('character requirements matchers and feedback', () => {
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 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 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 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 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;
});

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.');
});

it('should return a score of -100 for missing uppercase letters', () => {
const match = { pattern: 'uppercase', token: 'password123!', i: 0, j: 11 };
const score = uppercaseMatcher.scoring(match);
expect(score).to.equal(-100);
});
});

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.');
});

it('should return a score of -100 for missing lowercase letters', () => {
const match = { pattern: 'lowercase', token: 'PASSWORD123!', i: 0, j: 11 };
const score = lowercaseMatcher.scoring(match);
expect(score).to.equal(-100);
});
});

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.');
});

it('should return a score of -100 for missing numbers', () => {
const match = { pattern: 'number', token: 'Password!', i: 0, j: 8 };
const score = numberMatcher.scoring(match);
expect(score).to.equal(-100);
});
});

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.');
});

it('should return a score of -100 for missing uppercase letters', () => {
const match = { pattern: 'uppercase', token: 'password123!', i: 0, j: 11 };
const score = uppercaseMatcher.scoring(match);
it('should return a score of -100 for missing special characters', () => {
const match = { pattern: 'special', token: 'Password123', i: 0, j: 10 };
const score = specialMatcher.scoring(match);
expect(score).to.equal(-100);
});
});

it('should return a score of -100 for missing lowercase letters', () => {
const match = { pattern: 'lowercase', token: 'PASSWORD123!', i: 0, j: 11 };
const score = lowercaseMatcher.scoring(match);
expect(score).to.equal(-100);
describe('minLengthMatcher', () => {
const minLength = 10;
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 });
});

it('should return a score of -100 for missing numbers', () => {
const match = { pattern: 'number', token: 'Password!', i: 0, j: 8 };
const score = numberMatcher.scoring(match);
expect(score).to.equal(-100);
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 return a score of -100 for missing special characters', () => {
const match = { pattern: 'special', token: 'Password123', i: 0, j: 10 };
const score = specialMatcher.scoring(match);
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);
expect(score).to.equal(-100);
});
});

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;
});
});

0 comments on commit 7e19b06

Please sign in to comment.