Repository: typestack/class-validator (ts)

Issue:
feature: add IsStrongPassword decorator
### Description
Validating strength of passwords is a very common requirement for applications. At this moment `class-validator` doesn't support that even though `validator.js` already does.

### Proposed solution
Add the `IsStrongPassword` decorator to wrap the functionality already provided by `validator.js`.

Pull request description:
feat: add IsStrongPassword decorator
## Description
Add support to password strength validation.

## Checklist
- [x] the pull request title describes what this PR does (not a vague title like `Update index.md`)
- [x] the pull request targets the *default* branch of the repository (`develop`)
- [x] the code follows the established code style of the repository
  - `npm run prettier:check` passes
  - `npm run lint:check` passes
- [x] tests are added for the changes I made (if any source code was modified)
- [x] documentation added or updated
- [x] I have run the project locally and verified that there are no errors

### Fixes
fixes #1024


Patch:
diff --git a/README.md b/README.md
index 44fde88..24cd128 100644
--- a/README.md
+++ b/README.md
@@ -899,6 +899,7 @@ isBoolean(value);
 | `@IsISSN(options?: IsISSNOptions)`              | Checks if the string is a ISSN. |
 | `@IsISRC()`                                     | Checks if the string is a [ISRC](https://en.wikipedia.org/wiki/International_Standard_Recording_Code). |
 | `@IsRFC3339()`                                  | Checks if the string is a valid [RFC 3339](https://tools.ietf.org/html/rfc3339) date. |
+| `@IsStrongPassword(options?: IsStrongPasswordOptions)` | Checks if the string is a strong password. |
 | **Array validation decorators**                 | |
 | `@ArrayContains(values: any[])`                 | Checks if array contains all values from the given array of values. |
 | `@ArrayNotContains(values: any[])`              | Checks if array does not contain any of the given values. |
diff --git a/package-lock.json b/package-lock.json
index 75f93fc..1e7e4f4 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -17,7 +17,7 @@
         "@rollup/plugin-commonjs": "^23.0.3",
         "@rollup/plugin-node-resolve": "^15.0.1",
         "@types/jest": "^29.2.3",
-        "@types/node": "^18.11.9",
+        "@types/node": "^18.11.10",
         "@typescript-eslint/eslint-plugin": "^5.45.0",
         "@typescript-eslint/parser": "^5.45.0",
         "eslint": "^8.28.0",
@@ -1463,9 +1463,9 @@
       "dev": true
     },
     "node_modules/@types/node": {
-      "version": "18.11.9",
-      "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz",
-      "integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==",
+      "version": "18.11.10",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.10.tgz",
+      "integrity": "sha512-juG3RWMBOqcOuXC643OAdSA525V44cVgGV6dUDuiFtss+8Fk5x1hI93Rsld43VeJVIeqlP9I7Fn9/qaVqoEAuQ==",
       "dev": true
     },
     "node_modules/@types/parse-json": {
@@ -7266,9 +7266,9 @@
       "dev": true
     },
     "@types/node": {
-      "version": "18.11.9",
-      "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz",
-      "integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==",
+      "version": "18.11.10",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.10.tgz",
+      "integrity": "sha512-juG3RWMBOqcOuXC643OAdSA525V44cVgGV6dUDuiFtss+8Fk5x1hI93Rsld43VeJVIeqlP9I7Fn9/qaVqoEAuQ==",
       "dev": true
     },
     "@types/parse-json": {
diff --git a/package.json b/package.json
index 668889b..cb0de80 100644
--- a/package.json
+++ b/package.json
@@ -44,7 +44,7 @@
     "@rollup/plugin-commonjs": "^23.0.3",
     "@rollup/plugin-node-resolve": "^15.0.1",
     "@types/jest": "^29.2.3",
-    "@types/node": "^18.11.9",
+    "@types/node": "^18.11.10",
     "@typescript-eslint/eslint-plugin": "^5.45.0",
     "@typescript-eslint/parser": "^5.45.0",
     "eslint": "^8.28.0",
diff --git a/src/decorator/decorators.ts b/src/decorator/decorators.ts
index d8e9b56..0798325 100644
--- a/src/decorator/decorators.ts
+++ b/src/decorator/decorators.ts
@@ -111,6 +111,7 @@ export * from './string/IsPostalCode';
 export * from './string/IsRFC3339';
 export * from './string/IsRgbColor';
 export * from './string/IsSemVer';
+export * from './string/IsStrongPassword';
 export * from './string/IsTimeZone';
 
 // -------------------------------------------------------------------------
diff --git a/src/decorator/string/IsStrongPassword.ts b/src/decorator/string/IsStrongPassword.ts
new file mode 100644
index 0000000..9d17e69
--- /dev/null
+++ b/src/decorator/string/IsStrongPassword.ts
@@ -0,0 +1,42 @@
+import validator from 'validator';
+import { ValidationOptions } from '../ValidationOptions';
+import { buildMessage, ValidateBy } from '../common/ValidateBy';
+
+export const IS_STRONG_PASSWORD = 'isStrongPassword';
+
+/**
+ * Options to be passed to IsStrongPassword decorator.
+ */
+export type IsStrongPasswordOptions = Pick<
+  validator.StrongPasswordOptions,
+  'minLength' | 'minLowercase' | 'minUppercase' | 'minNumbers' | 'minSymbols'
+>;
+
+/**
+ * Checks if the string is a strong password.
+ * If given value is not a string, then it returns false.
+ */
+export function isStrongPassword(value: unknown, options?: IsStrongPasswordOptions): boolean {
+  return typeof value === 'string' && validator.isStrongPassword(value, options);
+}
+
+/**
+ * Checks if the string is a strong password.
+ * If given value is not a string, then it returns false.
+ */
+export function IsStrongPassword(
+  options?: IsStrongPasswordOptions,
+  validationOptions?: ValidationOptions
+): PropertyDecorator {
+  return ValidateBy(
+    {
+      name: IS_STRONG_PASSWORD,
+      constraints: [options],
+      validator: {
+        validate: (value, args): boolean => isStrongPassword(value, args.constraints[0]),
+        defaultMessage: buildMessage(eachPrefix => eachPrefix + '$property is not strong enough', validationOptions),
+      },
+    },
+    validationOptions
+  );
+}


Test patch:
diff --git a/test/functional/validation-functions-and-decorators.spec.ts b/test/functional/validation-functions-and-decorators.spec.ts
index 1418235..14b92b3 100644
--- a/test/functional/validation-functions-and-decorators.spec.ts
+++ b/test/functional/validation-functions-and-decorators.spec.ts
@@ -184,6 +184,9 @@ import {
   isPostalCode,
   IsSemVer,
   isSemVer,
+  IsStrongPassword,
+  isStrongPassword,
+  IsStrongPasswordOptions,
   IsTimeZone,
 } from '../../src/decorator/decorators';
 import { Validator } from '../../src/validation/Validator';
@@ -4577,3 +4580,83 @@ describe('isInstance', () => {
     return checkReturnedError(new MyClass(), invalidValues, validationType, message);
   });
 });
+
+describe('IsStrongPassword', () => {
+  class MyClass {
+    @IsStrongPassword()
+    someProperty: string;
+  }
+
+  const validValues = ['Abcdef1!'];
+  const invalidValues = [null, undefined, 'Abcde1!', 'abcdef1!', 'ABCDEF1!', 'Abcdefg!', 'Abcdefg1'];
+
+  it('should not fail if validator.validate said that its valid', () => {
+    return checkValidValues(new MyClass(), validValues);
+  });
+
+  it('should fail if validator.validate said that its invalid', () => {
+    return checkInvalidValues(new MyClass(), invalidValues);
+  });
+
+  it('should not fail if method in validator said that its valid', () => {
+    validValues.forEach(value => expect(isStrongPassword(value)).toBeTruthy());
+  });
+
+  it('should fail if method in validator said that its invalid', () => {
+    invalidValues.forEach(value => expect(isStrongPassword(value)).toBeFalsy());
+  });
+
+  it('should return error object with proper data', () => {
+    const validationType = 'isStrongPassword';
+    const message = 'someProperty is not strong enough';
+    return checkReturnedError(new MyClass(), invalidValues, validationType, message);
+  });
+});
+
+describe('IsStrongPassword with options', () => {
+  const options: IsStrongPasswordOptions = {
+    minLength: 12,
+    minLowercase: 2,
+    minUppercase: 2,
+    minNumbers: 2,
+    minSymbols: 2,
+  };
+
+  class MyClass {
+    @IsStrongPassword(options)
+    someProperty: string;
+  }
+
+  const validValues = ['ABcdefgh12!#'];
+  const invalidValues = [
+    null,
+    undefined,
+    'ABcdefg12!#',
+    'Abcdefgh12!#',
+    'ABcDEFGH12!#',
+    'ABcdefghi1!#',
+    'ABcdefghi12!',
+  ];
+
+  it('should not fail if validator.validate said that its valid', () => {
+    return checkValidValues(new MyClass(), validValues);
+  });
+
+  it('should fail if validator.validate said that its invalid', () => {
+    return checkInvalidValues(new MyClass(), invalidValues);
+  });
+
+  it('should not fail if method in validator said that its valid', () => {
+    validValues.forEach(value => expect(isStrongPassword(value, options)).toBeTruthy());
+  });
+
+  it('should fail if method in validator said that its invalid', () => {
+    invalidValues.forEach(value => expect(isStrongPassword(value, options)).toBeFalsy());
+  });
+
+  it('should return error object with proper data', () => {
+    const validationType = 'isStrongPassword';
+    const message = 'someProperty is not strong enough';
+    return checkReturnedError(new MyClass(), invalidValues, validationType, message);
+  });
+});