From 6d81eaba4b64c141267f3a1c3128ec50e7f3ba7d Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Wed, 25 Mar 2026 10:34:54 +0300 Subject: [PATCH] feat(auth): add validatePassword function with requirements for min and max characters amount, presence of at least one uppercase letter, one lowercase letter, one digit and one special symbol; cover it with tests --- src/features/auth/lib/validators/index.ts | 1 + .../validatePassword/validatePassword.spec.ts | 66 +++++++++++++++++++ .../validatePassword/validatePassword.ts | 19 ++++++ .../auth/model/stores/authStore/authStore.ts | 6 +- 4 files changed, 87 insertions(+), 5 deletions(-) create mode 100644 src/features/auth/lib/validators/validatePassword/validatePassword.spec.ts create mode 100644 src/features/auth/lib/validators/validatePassword/validatePassword.ts diff --git a/src/features/auth/lib/validators/index.ts b/src/features/auth/lib/validators/index.ts index beaa628..ff7f5c9 100644 --- a/src/features/auth/lib/validators/index.ts +++ b/src/features/auth/lib/validators/index.ts @@ -1 +1,2 @@ export { validateEmail } from "./validateEmail/validateEmail"; +export { validatePassword } from "./validatePassword/validatePassword"; diff --git a/src/features/auth/lib/validators/validatePassword/validatePassword.spec.ts b/src/features/auth/lib/validators/validatePassword/validatePassword.spec.ts new file mode 100644 index 0000000..69dc34c --- /dev/null +++ b/src/features/auth/lib/validators/validatePassword/validatePassword.spec.ts @@ -0,0 +1,66 @@ +import { + MAX_PASSWORD_LENGTH, + MIN_PASSWORD_LENGTH, + validatePassword, +} from "./validatePassword"; + +describe("validatePassword", () => { + describe("Absence of necessary characters", () => { + it("should return false when the password does not contain a lowercase letter", () => { + expect(validatePassword("PASSWORD123!")).toBe(false); + }); + it("should return false when the password does not contain an uppercase letter", () => { + expect(validatePassword("password123!")).toBe(false); + }); + it("should return false when the password does not contain a digit", () => { + expect(validatePassword("Password!")).toBe(false); + }); + it("should return false when the password does not contain a special character", () => { + expect(validatePassword("Password123")).toBe(false); + }); + }); + + describe("Length requirement", () => { + const validChars = "aA1!"; + + it("should return false when the password is less than min length", () => { + const shortPassword = + validChars + + "a".repeat(Math.max(0, MIN_PASSWORD_LENGTH - validChars.length - 1)); + + expect(validatePassword(shortPassword)).toBe(false); + }); + it("should return true when the password is exactly min length", () => { + const exactMinPassword = + validChars + + "a".repeat(Math.max(0, MIN_PASSWORD_LENGTH - validChars.length)); + + expect(validatePassword(exactMinPassword)).toBe(true); + }); + it("should return false when the password is greater than max length", () => { + const longPassword = + validChars + + "a".repeat(Math.max(0, MAX_PASSWORD_LENGTH - validChars.length + 1)); + + expect(validatePassword(longPassword)).toBe(false); + }); + it("should return true when the password is exactly max length", () => { + const exactMaxPassword = + validChars + + "a".repeat(Math.max(0, MAX_PASSWORD_LENGTH - validChars.length)); + + expect(validatePassword(exactMaxPassword)).toBe(true); + }); + }); + + describe("Valid password", () => { + it("should return true when the password is valid", () => { + const validChars = "aA1!"; + const validPassword = + validChars + + "a".repeat(Math.max(0, MIN_PASSWORD_LENGTH - validChars.length)); + + expect(validatePassword(validPassword)).toBe(true); + }); + }); +}); diff --git a/src/features/auth/lib/validators/validatePassword/validatePassword.ts b/src/features/auth/lib/validators/validatePassword/validatePassword.ts new file mode 100644 index 0000000..5c35970 --- /dev/null +++ b/src/features/auth/lib/validators/validatePassword/validatePassword.ts @@ -0,0 +1,19 @@ +export const MIN_PASSWORD_LENGTH = 11; +export const MAX_PASSWORD_LENGTH = 63; + +/** + * The password regex used to validate passwords. + * Uses a case-sensitive regex with at least one lowercase letter, one uppercase letter, one digit, and one special character. + */ +export const PASSWORD_REGEX = new RegExp( + `^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@$!%*?&])[A-Za-z\\d@$!%*?&]{${MIN_PASSWORD_LENGTH},${MAX_PASSWORD_LENGTH}}$`, +); + +/** + * Validates a password against the password regex. + * @param password The password to validate. + * @returns `true` if the password is valid, `false` otherwise. + */ +export function validatePassword(password: string): boolean { + return PASSWORD_REGEX.test(password); +} diff --git a/src/features/auth/model/stores/authStore/authStore.ts b/src/features/auth/model/stores/authStore/authStore.ts index 84300d6..9bdc9b3 100644 --- a/src/features/auth/model/stores/authStore/authStore.ts +++ b/src/features/auth/model/stores/authStore/authStore.ts @@ -4,7 +4,7 @@ import { login, logout, register } from "../../../api"; import { callApi } from "shared/utils"; import { UNEXPECTED_ERROR_MESSAGE } from "shared/api"; import { selectAuthData, selectFormValid } from "../../selectors"; -import { validateEmail } from "../../../lib"; +import { validateEmail, validatePassword } from "../../../lib"; export const defaultStoreState: Readonly = { formData: { @@ -16,10 +16,6 @@ export const defaultStoreState: Readonly = { error: null, }; -function validatePassword(password: string): boolean { - return Boolean(password); -} - export const useAuthStore = create()((set, get) => ({ ...defaultStoreState,