route.test.js 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253
  1. /* @vitest-environment node */
  2. import { describe, it, expect, vi, beforeEach } from "vitest";
  3. // 1) Mocks
  4. vi.mock("@/lib/db", () => ({
  5. getDb: vi.fn(),
  6. }));
  7. vi.mock("@/models/user", () => ({
  8. default: {
  9. findOne: vi.fn(),
  10. },
  11. }));
  12. vi.mock("@/lib/auth/session", () => ({
  13. createSession: vi.fn(),
  14. }));
  15. vi.mock("bcryptjs", () => {
  16. const compare = vi.fn();
  17. return {
  18. default: { compare },
  19. compare,
  20. };
  21. });
  22. // 2) Imports AFTER the mocks
  23. import { getDb } from "@/lib/db";
  24. import User from "@/models/user";
  25. import { createSession } from "@/lib/auth/session";
  26. import { compare as bcryptCompare } 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/login", () => {
  36. beforeEach(() => {
  37. vi.clearAllMocks();
  38. getDb.mockResolvedValue({}); // we only need it to "connect"
  39. });
  40. it('exports dynamic="force-dynamic" (RHL-006)', () => {
  41. expect(dynamic).toBe("force-dynamic");
  42. });
  43. it("logs in successfully with correct credentials", async () => {
  44. const user = {
  45. _id: "507f1f77bcf86cd799439011",
  46. username: "branchuser",
  47. email: "nl01@example.com",
  48. passwordHash: "hashed-password",
  49. role: "branch",
  50. branchId: "NL01",
  51. mustChangePassword: true,
  52. };
  53. User.findOne.mockReturnValue({
  54. exec: vi.fn().mockResolvedValue(user),
  55. });
  56. bcryptCompare.mockResolvedValue(true);
  57. const request = createRequestStub({
  58. username: "BranchUser", // mixed case, should be normalized
  59. password: "secret-password",
  60. });
  61. const response = await POST(request);
  62. const json = await response.json();
  63. expect(response.status).toBe(200);
  64. expect(json).toEqual({ ok: true });
  65. expect(getDb).toHaveBeenCalledTimes(1);
  66. expect(User.findOne).toHaveBeenCalledWith({ username: "branchuser" });
  67. expect(bcryptCompare).toHaveBeenCalledWith(
  68. "secret-password",
  69. "hashed-password",
  70. );
  71. expect(createSession).toHaveBeenCalledWith({
  72. userId: "507f1f77bcf86cd799439011",
  73. role: "branch",
  74. branchId: "NL01",
  75. email: "nl01@example.com",
  76. mustChangePassword: true,
  77. });
  78. });
  79. it("returns 400 when JSON parsing fails", async () => {
  80. // Simulate request.json() throwing (invalid JSON body).
  81. const request = {
  82. json: vi.fn().mockRejectedValue(new Error("invalid json")),
  83. };
  84. const response = await POST(request);
  85. const body = await response.json();
  86. expect(response.status).toBe(400);
  87. expect(body).toEqual({
  88. error: {
  89. message: "Invalid request body",
  90. code: "VALIDATION_INVALID_JSON",
  91. },
  92. });
  93. expect(createSession).not.toHaveBeenCalled();
  94. });
  95. it("returns 401 when user does not exist", async () => {
  96. User.findOne.mockReturnValue({
  97. exec: vi.fn().mockResolvedValue(null),
  98. });
  99. const request = createRequestStub({
  100. username: "unknownuser",
  101. password: "some-password",
  102. });
  103. const response = await POST(request);
  104. const body = await response.json();
  105. expect(response.status).toBe(401);
  106. expect(body).toEqual({
  107. error: {
  108. message: "Invalid credentials",
  109. code: "AUTH_INVALID_CREDENTIALS",
  110. },
  111. });
  112. expect(createSession).not.toHaveBeenCalled();
  113. });
  114. it("returns 401 when passwordHash is missing (defensive)", async () => {
  115. User.findOne.mockReturnValue({
  116. exec: vi.fn().mockResolvedValue({
  117. _id: "507f1f77bcf86cd799439099",
  118. username: "branchuser",
  119. email: "nl01@example.com",
  120. // passwordHash missing on purpose
  121. role: "branch",
  122. branchId: "NL01",
  123. }),
  124. });
  125. const request = createRequestStub({
  126. username: "branchuser",
  127. password: "secret-password",
  128. });
  129. const response = await POST(request);
  130. const body = await response.json();
  131. expect(response.status).toBe(401);
  132. expect(body).toEqual({
  133. error: {
  134. message: "Invalid credentials",
  135. code: "AUTH_INVALID_CREDENTIALS",
  136. },
  137. });
  138. expect(bcryptCompare).not.toHaveBeenCalled();
  139. expect(createSession).not.toHaveBeenCalled();
  140. });
  141. it("returns 401 when password is incorrect", async () => {
  142. const user = {
  143. _id: "507f1f77bcf86cd799439012",
  144. username: "branchuser",
  145. email: "nl02@example.com",
  146. passwordHash: "hashed-password",
  147. role: "branch",
  148. branchId: "NL02",
  149. };
  150. User.findOne.mockReturnValue({
  151. exec: vi.fn().mockResolvedValue(user),
  152. });
  153. bcryptCompare.mockResolvedValue(false);
  154. const request = createRequestStub({
  155. username: "branchuser",
  156. password: "wrong-password",
  157. });
  158. const response = await POST(request);
  159. const body = await response.json();
  160. expect(response.status).toBe(401);
  161. expect(body).toEqual({
  162. error: {
  163. message: "Invalid credentials",
  164. code: "AUTH_INVALID_CREDENTIALS",
  165. },
  166. });
  167. expect(createSession).not.toHaveBeenCalled();
  168. });
  169. it("returns 400 when username or password is missing", async () => {
  170. const request = createRequestStub({
  171. username: "only-username",
  172. });
  173. const response = await POST(request);
  174. const body = await response.json();
  175. expect(response.status).toBe(400);
  176. expect(body).toEqual({
  177. error: {
  178. message: "Missing username or password",
  179. code: "VALIDATION_MISSING_FIELD",
  180. details: { fields: ["username", "password"] },
  181. },
  182. });
  183. expect(User.findOne).not.toHaveBeenCalled();
  184. expect(createSession).not.toHaveBeenCalled();
  185. });
  186. it("returns 500 when an unexpected error occurs", async () => {
  187. User.findOne.mockImplementation(() => {
  188. throw new Error("DB failure");
  189. });
  190. const request = createRequestStub({
  191. username: "branchuser",
  192. password: "secret-password",
  193. });
  194. const response = await POST(request);
  195. const body = await response.json();
  196. expect(response.status).toBe(500);
  197. expect(body).toEqual({
  198. error: {
  199. message: "Internal server error",
  200. code: "INTERNAL_SERVER_ERROR",
  201. },
  202. });
  203. expect(createSession).not.toHaveBeenCalled();
  204. });
  205. });