فهرست منبع

RHL-009 feat(auth): implement password policy validation and testing

Code_Uwe 6 روز پیش
والد
کامیت
4860b95c39
2فایلهای تغییر یافته به همراه127 افزوده شده و 0 حذف شده
  1. 66 0
      lib/auth/passwordPolicy.js
  2. 61 0
      lib/auth/passwordPolicy.test.js

+ 66 - 0
lib/auth/passwordPolicy.js

@@ -0,0 +1,66 @@
+export const PASSWORD_POLICY = Object.freeze({
+	minLength: 8,
+	requireLetter: true,
+	requireNumber: true,
+	disallowSameAsCurrent: true,
+});
+
+export const PASSWORD_POLICY_REASON = Object.freeze({
+	MIN_LENGTH: "MIN_LENGTH",
+	MISSING_LETTER: "MISSING_LETTER",
+	MISSING_NUMBER: "MISSING_NUMBER",
+	SAME_AS_CURRENT: "SAME_AS_CURRENT",
+});
+
+function hasLetter(value) {
+	return /[A-Za-z]/.test(String(value || ""));
+}
+
+function hasNumber(value) {
+	return /\d/.test(String(value || ""));
+}
+
+/**
+ * Validate a new password against the project's explicit policy.
+ *
+ * @param {{ newPassword?: unknown, currentPassword?: unknown }} input
+ * @returns {{
+ *   ok: boolean,
+ *   reasons: string[],
+ *   policy: { minLength: number, requireLetter: boolean, requireNumber: boolean, disallowSameAsCurrent: boolean }
+ * }}
+ */
+export function validateNewPassword({ newPassword, currentPassword } = {}) {
+	const pw = typeof newPassword === "string" ? newPassword : "";
+	const current = typeof currentPassword === "string" ? currentPassword : null;
+
+	const reasons = [];
+
+	if (pw.length < PASSWORD_POLICY.minLength) {
+		reasons.push(PASSWORD_POLICY_REASON.MIN_LENGTH);
+	}
+
+	if (PASSWORD_POLICY.requireLetter && !hasLetter(pw)) {
+		reasons.push(PASSWORD_POLICY_REASON.MISSING_LETTER);
+	}
+
+	if (PASSWORD_POLICY.requireNumber && !hasNumber(pw)) {
+		reasons.push(PASSWORD_POLICY_REASON.MISSING_NUMBER);
+	}
+
+	if (
+		PASSWORD_POLICY.disallowSameAsCurrent &&
+		current !== null &&
+		pw === current
+	) {
+		reasons.push(PASSWORD_POLICY_REASON.SAME_AS_CURRENT);
+	}
+
+	const uniqueReasons = Array.from(new Set(reasons));
+
+	return {
+		ok: uniqueReasons.length === 0,
+		reasons: uniqueReasons,
+		policy: { ...PASSWORD_POLICY },
+	};
+}

+ 61 - 0
lib/auth/passwordPolicy.test.js

@@ -0,0 +1,61 @@
+/* @vitest-environment node */
+
+import { describe, it, expect } from "vitest";
+import {
+	validateNewPassword,
+	PASSWORD_POLICY,
+	PASSWORD_POLICY_REASON,
+} from "./passwordPolicy.js";
+
+describe("lib/auth/passwordPolicy", () => {
+	it("accepts a strong password", () => {
+		const res = validateNewPassword({
+			newPassword: "StrongPassword123",
+			currentPassword: "OldPassword123",
+		});
+
+		expect(res.ok).toBe(true);
+		expect(res.reasons).toEqual([]);
+		expect(res.policy).toEqual(PASSWORD_POLICY);
+	});
+
+	it("rejects too short passwords", () => {
+		const res = validateNewPassword({
+			newPassword: "Abc1",
+			currentPassword: "OldPassword123",
+		});
+
+		expect(res.ok).toBe(false);
+		expect(res.reasons).toContain(PASSWORD_POLICY_REASON.MIN_LENGTH);
+	});
+
+	it("rejects passwords without numbers", () => {
+		const res = validateNewPassword({
+			newPassword: "VeryStrongPassword",
+			currentPassword: "OldPassword123",
+		});
+
+		expect(res.ok).toBe(false);
+		expect(res.reasons).toContain(PASSWORD_POLICY_REASON.MISSING_NUMBER);
+	});
+
+	it("rejects passwords without letters", () => {
+		const res = validateNewPassword({
+			newPassword: "1234567890123",
+			currentPassword: "OldPassword123",
+		});
+
+		expect(res.ok).toBe(false);
+		expect(res.reasons).toContain(PASSWORD_POLICY_REASON.MISSING_LETTER);
+	});
+
+	it("rejects when new password equals current password", () => {
+		const res = validateNewPassword({
+			newPassword: "SamePassword123",
+			currentPassword: "SamePassword123",
+		});
+
+		expect(res.ok).toBe(false);
+		expect(res.reasons).toContain(PASSWORD_POLICY_REASON.SAME_AS_CURRENT);
+	});
+});