/* @vitest-environment node */ import { describe, it, expect, vi, beforeEach } from "vitest"; vi.mock("@/lib/auth/session", () => ({ getSession: vi.fn(), })); vi.mock("@/lib/db", () => ({ getDb: vi.fn(), })); vi.mock("@/models/user", () => ({ default: { findById: vi.fn(), }, })); vi.mock("bcryptjs", () => { const compare = vi.fn(); const hash = vi.fn(); return { default: { compare, hash }, compare, hash, }; }); import { getSession } from "@/lib/auth/session"; import { getDb } from "@/lib/db"; import User from "@/models/user"; import { compare as bcryptCompare, hash as bcryptHash } from "bcryptjs"; import { POST, dynamic } from "./route.js"; function createRequestStub(body) { return { async json() { return body; }, }; } describe("POST /api/auth/change-password", () => { beforeEach(() => { vi.clearAllMocks(); getDb.mockResolvedValue({}); }); it('exports dynamic="force-dynamic"', () => { expect(dynamic).toBe("force-dynamic"); }); it("returns 401 when unauthenticated", async () => { getSession.mockResolvedValue(null); const res = await POST(createRequestStub({})); expect(res.status).toBe(401); expect(await res.json()).toEqual({ error: { message: "Unauthorized", code: "AUTH_UNAUTHENTICATED" }, }); }); it("returns 400 when JSON parsing fails", async () => { getSession.mockResolvedValue({ userId: "u1", role: "branch", branchId: "NL01", }); const req = { json: vi.fn().mockRejectedValue(new Error("invalid json")), }; const res = await POST(req); expect(res.status).toBe(400); expect(await res.json()).toEqual({ error: { message: "Invalid request body", code: "VALIDATION_INVALID_JSON", }, }); }); it("returns 400 when body is not an object", async () => { getSession.mockResolvedValue({ userId: "u1", role: "branch", branchId: "NL01", }); const res = await POST(createRequestStub("nope")); expect(res.status).toBe(400); expect(await res.json()).toEqual({ error: { message: "Invalid request body", code: "VALIDATION_INVALID_BODY", }, }); }); it("returns 400 when fields are missing", async () => { getSession.mockResolvedValue({ userId: "u1", role: "branch", branchId: "NL01", }); const res = await POST(createRequestStub({ currentPassword: "x" })); expect(res.status).toBe(400); expect(await res.json()).toEqual({ error: { message: "Missing currentPassword or newPassword", code: "VALIDATION_MISSING_FIELD", details: { fields: ["newPassword"] }, }, }); expect(User.findById).not.toHaveBeenCalled(); }); it("returns 401 when user is not found (treat as invalid session)", async () => { getSession.mockResolvedValue({ userId: "u1", role: "branch", branchId: "NL01", }); User.findById.mockReturnValue({ exec: vi.fn().mockResolvedValue(null), }); const res = await POST( createRequestStub({ currentPassword: "OldPassword123", newPassword: "StrongPassword123", }), ); expect(res.status).toBe(401); expect(await res.json()).toEqual({ error: { message: "Unauthorized", code: "AUTH_UNAUTHENTICATED" }, }); }); it("returns 401 when current password is wrong", async () => { getSession.mockResolvedValue({ userId: "u1", role: "branch", branchId: "NL01", }); const user = { _id: "507f1f77bcf86cd799439011", passwordHash: "hash", mustChangePassword: true, passwordResetToken: "tok", passwordResetExpiresAt: new Date(), save: vi.fn().mockResolvedValue(true), }; User.findById.mockReturnValue({ exec: vi.fn().mockResolvedValue(user), }); bcryptCompare.mockResolvedValue(false); const res = await POST( createRequestStub({ currentPassword: "wrong", newPassword: "StrongPassword123", }), ); expect(res.status).toBe(401); expect(await res.json()).toEqual({ error: { message: "Invalid credentials", code: "AUTH_INVALID_CREDENTIALS", }, }); expect(bcryptHash).not.toHaveBeenCalled(); expect(user.save).not.toHaveBeenCalled(); }); it("returns 400 when new password is weak", async () => { getSession.mockResolvedValue({ userId: "u1", role: "branch", branchId: "NL01", }); const user = { _id: "507f1f77bcf86cd799439011", passwordHash: "hash", mustChangePassword: true, passwordResetToken: "tok", passwordResetExpiresAt: new Date(), save: vi.fn().mockResolvedValue(true), }; User.findById.mockReturnValue({ exec: vi.fn().mockResolvedValue(user), }); bcryptCompare.mockResolvedValue(true); const res = await POST( createRequestStub({ currentPassword: "OldPassword123", newPassword: "short", }), ); expect(res.status).toBe(400); const body = await res.json(); expect(body.error.code).toBe("VALIDATION_WEAK_PASSWORD"); expect(body.error.details).toMatchObject({ minLength: 8, requireLetter: true, requireNumber: true, }); expect(Array.isArray(body.error.details.reasons)).toBe(true); expect(bcryptHash).not.toHaveBeenCalled(); expect(user.save).not.toHaveBeenCalled(); }); it("returns 200 and updates passwordHash + clears flags on success", async () => { getSession.mockResolvedValue({ userId: "u1", role: "branch", branchId: "NL01", }); const user = { _id: "507f1f77bcf86cd799439011", passwordHash: "old-hash", mustChangePassword: true, passwordResetToken: "tok", passwordResetExpiresAt: new Date("2030-01-01"), save: vi.fn().mockResolvedValue(true), }; User.findById.mockReturnValue({ exec: vi.fn().mockResolvedValue(user), }); bcryptCompare.mockResolvedValue(true); bcryptHash.mockResolvedValue("new-hash"); const res = await POST( createRequestStub({ currentPassword: "OldPassword123", newPassword: "StrongPassword123", }), ); expect(res.status).toBe(200); expect(await res.json()).toEqual({ ok: true }); expect(bcryptHash).toHaveBeenCalledWith("StrongPassword123", 12); expect(user.passwordHash).toBe("new-hash"); expect(user.mustChangePassword).toBe(false); expect(user.passwordResetToken).toBe(null); expect(user.passwordResetExpiresAt).toBe(null); expect(user.save).toHaveBeenCalledTimes(1); }); });