route.test.js 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277
  1. /* @vitest-environment node */
  2. import { describe, it, expect, vi, beforeEach } from "vitest";
  3. vi.mock("@/lib/auth/session", () => ({
  4. getSession: vi.fn(),
  5. }));
  6. vi.mock("@/lib/db", () => ({
  7. getDb: vi.fn(),
  8. }));
  9. vi.mock("@/models/user", () => ({
  10. default: {
  11. findById: vi.fn(),
  12. },
  13. }));
  14. vi.mock("bcryptjs", () => {
  15. const compare = vi.fn();
  16. const hash = vi.fn();
  17. return {
  18. default: { compare, hash },
  19. compare,
  20. hash,
  21. };
  22. });
  23. import { getSession } from "@/lib/auth/session";
  24. import { getDb } from "@/lib/db";
  25. import User from "@/models/user";
  26. import { compare as bcryptCompare, hash as bcryptHash } from "bcryptjs";
  27. import { POST, dynamic } from "./route.js";
  28. function createRequestStub(body) {
  29. return {
  30. async json() {
  31. return body;
  32. },
  33. };
  34. }
  35. describe("POST /api/auth/change-password", () => {
  36. beforeEach(() => {
  37. vi.clearAllMocks();
  38. getDb.mockResolvedValue({});
  39. });
  40. it('exports dynamic="force-dynamic"', () => {
  41. expect(dynamic).toBe("force-dynamic");
  42. });
  43. it("returns 401 when unauthenticated", async () => {
  44. getSession.mockResolvedValue(null);
  45. const res = await POST(createRequestStub({}));
  46. expect(res.status).toBe(401);
  47. expect(await res.json()).toEqual({
  48. error: { message: "Unauthorized", code: "AUTH_UNAUTHENTICATED" },
  49. });
  50. });
  51. it("returns 400 when JSON parsing fails", async () => {
  52. getSession.mockResolvedValue({
  53. userId: "u1",
  54. role: "branch",
  55. branchId: "NL01",
  56. });
  57. const req = {
  58. json: vi.fn().mockRejectedValue(new Error("invalid json")),
  59. };
  60. const res = await POST(req);
  61. expect(res.status).toBe(400);
  62. expect(await res.json()).toEqual({
  63. error: {
  64. message: "Invalid request body",
  65. code: "VALIDATION_INVALID_JSON",
  66. },
  67. });
  68. });
  69. it("returns 400 when body is not an object", async () => {
  70. getSession.mockResolvedValue({
  71. userId: "u1",
  72. role: "branch",
  73. branchId: "NL01",
  74. });
  75. const res = await POST(createRequestStub("nope"));
  76. expect(res.status).toBe(400);
  77. expect(await res.json()).toEqual({
  78. error: {
  79. message: "Invalid request body",
  80. code: "VALIDATION_INVALID_BODY",
  81. },
  82. });
  83. });
  84. it("returns 400 when fields are missing", async () => {
  85. getSession.mockResolvedValue({
  86. userId: "u1",
  87. role: "branch",
  88. branchId: "NL01",
  89. });
  90. const res = await POST(createRequestStub({ currentPassword: "x" }));
  91. expect(res.status).toBe(400);
  92. expect(await res.json()).toEqual({
  93. error: {
  94. message: "Missing currentPassword or newPassword",
  95. code: "VALIDATION_MISSING_FIELD",
  96. details: { fields: ["newPassword"] },
  97. },
  98. });
  99. expect(User.findById).not.toHaveBeenCalled();
  100. });
  101. it("returns 401 when user is not found (treat as invalid session)", async () => {
  102. getSession.mockResolvedValue({
  103. userId: "u1",
  104. role: "branch",
  105. branchId: "NL01",
  106. });
  107. User.findById.mockReturnValue({
  108. exec: vi.fn().mockResolvedValue(null),
  109. });
  110. const res = await POST(
  111. createRequestStub({
  112. currentPassword: "OldPassword123",
  113. newPassword: "StrongPassword123",
  114. }),
  115. );
  116. expect(res.status).toBe(401);
  117. expect(await res.json()).toEqual({
  118. error: { message: "Unauthorized", code: "AUTH_UNAUTHENTICATED" },
  119. });
  120. });
  121. it("returns 401 when current password is wrong", async () => {
  122. getSession.mockResolvedValue({
  123. userId: "u1",
  124. role: "branch",
  125. branchId: "NL01",
  126. });
  127. const user = {
  128. _id: "507f1f77bcf86cd799439011",
  129. passwordHash: "hash",
  130. mustChangePassword: true,
  131. passwordResetToken: "tok",
  132. passwordResetExpiresAt: new Date(),
  133. save: vi.fn().mockResolvedValue(true),
  134. };
  135. User.findById.mockReturnValue({
  136. exec: vi.fn().mockResolvedValue(user),
  137. });
  138. bcryptCompare.mockResolvedValue(false);
  139. const res = await POST(
  140. createRequestStub({
  141. currentPassword: "wrong",
  142. newPassword: "StrongPassword123",
  143. }),
  144. );
  145. expect(res.status).toBe(401);
  146. expect(await res.json()).toEqual({
  147. error: {
  148. message: "Invalid credentials",
  149. code: "AUTH_INVALID_CREDENTIALS",
  150. },
  151. });
  152. expect(bcryptHash).not.toHaveBeenCalled();
  153. expect(user.save).not.toHaveBeenCalled();
  154. });
  155. it("returns 400 when new password is weak", async () => {
  156. getSession.mockResolvedValue({
  157. userId: "u1",
  158. role: "branch",
  159. branchId: "NL01",
  160. });
  161. const user = {
  162. _id: "507f1f77bcf86cd799439011",
  163. passwordHash: "hash",
  164. mustChangePassword: true,
  165. passwordResetToken: "tok",
  166. passwordResetExpiresAt: new Date(),
  167. save: vi.fn().mockResolvedValue(true),
  168. };
  169. User.findById.mockReturnValue({
  170. exec: vi.fn().mockResolvedValue(user),
  171. });
  172. bcryptCompare.mockResolvedValue(true);
  173. const res = await POST(
  174. createRequestStub({
  175. currentPassword: "OldPassword123",
  176. newPassword: "short",
  177. }),
  178. );
  179. expect(res.status).toBe(400);
  180. const body = await res.json();
  181. expect(body.error.code).toBe("VALIDATION_WEAK_PASSWORD");
  182. expect(body.error.details).toMatchObject({
  183. minLength: 8,
  184. requireLetter: true,
  185. requireNumber: true,
  186. });
  187. expect(Array.isArray(body.error.details.reasons)).toBe(true);
  188. expect(bcryptHash).not.toHaveBeenCalled();
  189. expect(user.save).not.toHaveBeenCalled();
  190. });
  191. it("returns 200 and updates passwordHash + clears flags on success", async () => {
  192. getSession.mockResolvedValue({
  193. userId: "u1",
  194. role: "branch",
  195. branchId: "NL01",
  196. });
  197. const user = {
  198. _id: "507f1f77bcf86cd799439011",
  199. passwordHash: "old-hash",
  200. mustChangePassword: true,
  201. passwordResetToken: "tok",
  202. passwordResetExpiresAt: new Date("2030-01-01"),
  203. save: vi.fn().mockResolvedValue(true),
  204. };
  205. User.findById.mockReturnValue({
  206. exec: vi.fn().mockResolvedValue(user),
  207. });
  208. bcryptCompare.mockResolvedValue(true);
  209. bcryptHash.mockResolvedValue("new-hash");
  210. const res = await POST(
  211. createRequestStub({
  212. currentPassword: "OldPassword123",
  213. newPassword: "StrongPassword123",
  214. }),
  215. );
  216. expect(res.status).toBe(200);
  217. expect(await res.json()).toEqual({ ok: true });
  218. expect(bcryptHash).toHaveBeenCalledWith("StrongPassword123", 12);
  219. expect(user.passwordHash).toBe("new-hash");
  220. expect(user.mustChangePassword).toBe(false);
  221. expect(user.passwordResetToken).toBe(null);
  222. expect(user.passwordResetExpiresAt).toBe(null);
  223. expect(user.save).toHaveBeenCalledTimes(1);
  224. });
  225. });