Skip to content

Commit 221c226

Browse files
authored
Support Joi v15 + Tests (#6)
1 parent 5de4151 commit 221c226

File tree

8 files changed

+4359
-26
lines changed

8 files changed

+4359
-26
lines changed

.eslintignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
!*
2+
node_modules/
3+
.idea/

.eslintrc

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"extends": ["airbnb-base", "plugin:jest/recommended"],
3+
"rules": {
4+
"strict": "off",
5+
"max-len": "off",
6+
"no-console": "off",
7+
"no-continue": "off",
8+
"no-plusplus": "off",
9+
"global-require": "off",
10+
"operator-linebreak": ["error", "after"],
11+
"no-underscore-dangle": "off",
12+
"prefer-const": ["error", { "destructuring": "all", "ignoreReadBeforeAssign": true }]
13+
},
14+
"env": {
15+
"jest/globals": true
16+
}
17+
}

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
node_modules/
2+
.idea/

README.md

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,20 @@
22

33
Creates a Joi object that validates password complexity.
44

5+
## Requirements
6+
* Joi v15 or higher
7+
* Nodejs 8 or higher
8+
59
## Installation
610

711
`npm install joi-password-complexity`
812

9-
## Example
13+
## Examples
1014

1115
### No options specified
1216

1317
```javascript
14-
const Joi = require('joi');
18+
const Joi = require('@hapi/joi');
1519
const PasswordComplexity = require('joi-password-complexity');
1620

1721
Joi.validate('aPassword123!', new PasswordComplexity(), (err, value) => {
@@ -28,14 +32,14 @@ When no options are specified, the following are used:
2832
upperCase: 1,
2933
numeric: 1,
3034
symbol: 1,
31-
requirementCount: 3,
35+
requirementCount: 4,
3236
}
3337
```
3438

3539
### Options specified
3640

3741
```javascript
38-
const Joi = require('joi');
42+
const Joi = require('@hapi/joi');
3943
const PasswordComplexity = require('joi-password-complexity');
4044

4145
const complexityOptions = {

lib/index.js

Lines changed: 39 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
const path = require('path');
2-
const Any = require(path.join(path.dirname(require.resolve('joi')), 'types/any/index.js'));
3-
const Language = require(path.join(path.dirname(require.resolve('joi')), 'language.js'));
2+
3+
/* eslint-disable import/no-dynamic-require */
4+
const Any = require(path.join(path.dirname(require.resolve('@hapi/joi')), 'types/any/index.js'));
5+
const Language = require(path.join(path.dirname(require.resolve('@hapi/joi')), 'language.js'));
6+
/* eslint-enable */
47

58
const defaultOptions = {
69
min: 8,
@@ -9,7 +12,7 @@ const defaultOptions = {
912
upperCase: 1,
1013
numeric: 1,
1114
symbol: 1,
12-
requirementCount: 3,
15+
requirementCount: 4,
1316
};
1417

1518
const PasswordComplexity = class extends Any {
@@ -20,8 +23,8 @@ const PasswordComplexity = class extends Any {
2023

2124
this._options = options || defaultOptions;
2225
if (options && !options.requirementCount) {
23-
this._options.requirementCount = (options.lowerCase > 0)
24-
+ (options.upperCase > 0) + (options.numeric > 0) + (options.symbol > 0);
26+
this._options.requirementCount = (options.lowerCase > 0) +
27+
(options.upperCase > 0) + (options.numeric > 0) + (options.symbol > 0);
2528
}
2629
}
2730

@@ -42,30 +45,50 @@ const PasswordComplexity = class extends Any {
4245
}
4346

4447
_base(value, state, options) {
45-
let validated = 0;
46-
let matchMin = false;
47-
let matchMax = false;
48+
const errors = [];
4849

4950
if (typeof value === 'string') {
50-
matchMin = this._options.min && value.length >= this._options.min;
51-
matchMax = this._options.max && value.length <= this._options.max;
51+
const lowercaseCount = (value.match(/[a-z]/g) || []).length;
52+
const upperCaseCount = (value.match(/[A-Z]/g) || []).length;
53+
const numericCount = (value.match(/[0-9]/g) || []).length;
54+
const symbolCount = (value.match(/[^a-zA-Z0-9]/g) || []).length;
55+
56+
const meetsMin = this._options.min && value.length >= this._options.min;
57+
const meetsMax = this._options.max && value.length <= this._options.max;
58+
const meetsLowercase = lowercaseCount >= this._options.lowerCase;
59+
const meetsUppercase = upperCaseCount >= this._options.upperCase;
60+
const meetsNumeric = numericCount >= this._options.numeric;
61+
const meetsSymbol = symbolCount >= this._options.symbol;
62+
const meetsRequirementCount = !this._options.requirementCount ||
63+
meetsLowercase + meetsUppercase + meetsNumeric + meetsSymbol >= this._options.requirementCount;
5264

53-
validated += (value.match(/[a-z]/g) || []).length >= this._options.lowerCase;
54-
validated += (value.match(/[A-Z]/g) || []).length >= this._options.upperCase;
55-
validated += (value.match(/[0-9]/g) || []).length >= this._options.numeric;
56-
validated += (value.match(/[^a-zA-Z0-9]/g) || []).length >= this._options.symbol;
65+
if (!meetsMin) errors.push(this.createError('passwordComplexity.tooShort', { value }, state, options));
66+
if (!meetsMax) errors.push(this.createError('passwordComplexity.tooLong', { value }, state, options));
67+
if (!meetsLowercase) errors.push(this.createError('passwordComplexity.lowercase', { value }, state, options));
68+
if (!meetsUppercase) errors.push(this.createError('passwordComplexity.uppercase', { value }, state, options));
69+
if (!meetsNumeric) errors.push(this.createError('passwordComplexity.numeric', { value }, state, options));
70+
if (!meetsSymbol) errors.push(this.createError('passwordComplexity.symbol', { value }, state, options));
71+
if (!meetsRequirementCount) {
72+
errors.push(this.createError('passwordComplexity.requirementCount', { value }, state, options));
73+
}
5774
}
5875

5976
return {
6077
value,
61-
errors: (matchMin && matchMax && validated >= this._options.requirementCount) ? null
62-
: this.createError('passwordComplexity.base', { value }, state, options),
78+
errors: errors.length ? errors : null,
6379
};
6480
}
6581
};
6682

6783
Language.errors.passwordComplexity = {
6884
base: 'must meet password complexity requirements',
85+
tooShort: 'is too short',
86+
tooLong: 'is too long',
87+
lowercase: 'doesn\'t contain the required lowercase characters',
88+
uppercase: 'doesn\'t contain the required uppercase characters',
89+
numeric: 'doesn\'t contain the required numeric characters',
90+
symbol: 'doesn\'t contain the required symbols',
91+
requirementCount: 'must meet enough of the complexity requirements',
6992
};
7093

7194
module.exports = PasswordComplexity;

package.json

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,26 @@
11
{
22
"name": "joi-password-complexity",
3-
"version": "2.0.1",
3+
"version": "3.0.0",
44
"description": "Joi validation for password complexity requirements.",
55
"main": "lib/index.js",
66
"scripts": {
7-
"test": "npm test"
7+
"test": "jest",
8+
"lint": "./node_modules/.bin/eslint --fix --ext .js ."
89
},
910
"repository": {
1011
"type": "git",
1112
"url": "git+https://github.com/kamronbatman/joi-password-complexity.git"
1213
},
13-
"peerDependencies": {
14-
"joi": ">=10.5.0"
14+
"peerDependencies": {},
15+
"devDependencies": {
16+
"eslint": "^6.1.0",
17+
"eslint-config-airbnb-base": "^13.2.0",
18+
"eslint-plugin-import": "^2.18.2",
19+
"eslint-plugin-jest": "^22.14.0",
20+
"jest": "^24.8.0"
1521
},
1622
"engines": {
17-
"node": ">=4.0.0"
23+
"node": ">=8.0.0"
1824
},
1925
"keywords": [
2026
"Joi",
@@ -27,5 +33,11 @@
2733
"bugs": {
2834
"url": "https://github.com/kamronbatman/joi-password-complexity/issues"
2935
},
30-
"homepage": "https://github.com/kamronbatman/joi-password-complexity#readme"
36+
"homepage": "https://github.com/kamronbatman/joi-password-complexity#readme",
37+
"jest": {
38+
"verbose": true
39+
},
40+
"dependencies": {
41+
"@hapi/joi": "^15.1.0"
42+
}
3143
}

test/password.test.js

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
const Joi = require('@hapi/joi');
2+
3+
const PasswordComplexity = require('../lib/index');
4+
5+
describe('JoiPasswordComplexity defaultOptions', () => {
6+
it('should reject a password that is too short', () => {
7+
const password = '123';
8+
9+
const result = Joi.validate(password, new PasswordComplexity());
10+
const errors = result.error.details.filter(e => e.type === 'passwordComplexity.tooShort');
11+
expect(errors.length).toBe(1);
12+
expect(errors[0].message).toBe('"value" is too short');
13+
});
14+
it('should reject a password that is too long', () => {
15+
const password = '123456791234567912345679123';
16+
17+
const result = Joi.validate(password, new PasswordComplexity());
18+
const errors = result.error.details.filter(e => e.type === 'passwordComplexity.tooLong');
19+
20+
expect(errors.length).toBe(1);
21+
expect(errors[0].message).toBe('"value" is too long');
22+
});
23+
24+
it('should reject a password that doesn\'t meet the required lowercase count', () => {
25+
const password = 'ABCDEFG';
26+
27+
const result = Joi.validate(password, new PasswordComplexity());
28+
const errors = result.error.details.filter(e => e.type === 'passwordComplexity.lowercase');
29+
30+
expect(errors.length).toBe(1);
31+
expect(errors[0].message).toBe('"value" doesn\'t contain the required lowercase characters');
32+
});
33+
34+
it('should reject a password that doesn\'t meet the required uppercase count', () => {
35+
const password = 'abcdefg';
36+
37+
const result = Joi.validate(password, new PasswordComplexity());
38+
const errors = result.error.details.filter(e => e.type === 'passwordComplexity.uppercase');
39+
40+
expect(errors.length).toBe(1);
41+
expect(errors[0].message).toBe('"value" doesn\'t contain the required uppercase characters');
42+
});
43+
44+
it('should reject a password that doesn\'t meet the required numeric count', () => {
45+
const password = 'ABCDEFG';
46+
47+
const result = Joi.validate(password, new PasswordComplexity());
48+
const errors = result.error.details.filter(e => e.type === 'passwordComplexity.numeric');
49+
50+
expect(errors.length).toBe(1);
51+
expect(errors[0].message).toBe('"value" doesn\'t contain the required numeric characters');
52+
});
53+
54+
it('should reject a password that doesn\'t meet the required symbol count', () => {
55+
const password = 'ABCDEFG';
56+
57+
const result = Joi.validate(password, new PasswordComplexity());
58+
const errors = result.error.details.filter(e => e.type === 'passwordComplexity.symbol');
59+
60+
expect(errors.length).toBe(1);
61+
expect(errors[0].message).toBe('"value" doesn\'t contain the required symbols');
62+
});
63+
64+
it('should accept a valid password', () => {
65+
const password = 'abCD12#$';
66+
const result = Joi.validate(password, new PasswordComplexity());
67+
68+
expect(result.error).toBeNull();
69+
});
70+
71+
it('should reject a password that doesn\'t meet the requirement count', () => {
72+
const password = 'abCD12';
73+
const result = Joi.validate(password, new PasswordComplexity());
74+
const errors = result.error.details.filter(e => e.type === 'passwordComplexity.requirementCount');
75+
76+
expect(errors.length).toBe(1);
77+
expect(errors[0].message).toBe('"value" must meet enough of the complexity requirements');
78+
});
79+
});

0 commit comments

Comments
 (0)