route.test.js 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251
  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. };
  52. User.findOne.mockReturnValue({
  53. exec: vi.fn().mockResolvedValue(user),
  54. });
  55. bcryptCompare.mockResolvedValue(true);
  56. const request = createRequestStub({
  57. username: "BranchUser", // mixed case, should be normalized
  58. password: "secret-password",
  59. });
  60. const response = await POST(request);
  61. const json = await response.json();
  62. expect(response.status).toBe(200);
  63. expect(json).toEqual({ ok: true });
  64. expect(getDb).toHaveBeenCalledTimes(1);
  65. expect(User.findOne).toHaveBeenCalledWith({ username: "branchuser" });
  66. expect(bcryptCompare).toHaveBeenCalledWith(
  67. "secret-password",
  68. "hashed-password",
  69. );
  70. expect(createSession).toHaveBeenCalledWith({
  71. userId: "507f1f77bcf86cd799439011",
  72. role: "branch",
  73. branchId: "NL01",
  74. email: "nl01@example.com",
  75. });
  76. });
  77. it("returns 400 when JSON parsing fails", async () => {
  78. // Simulate request.json() throwing (invalid JSON body).
  79. const request = {
  80. json: vi.fn().mockRejectedValue(new Error("invalid json")),
  81. };
  82. const response = await POST(request);
  83. const body = await response.json();
  84. expect(response.status).toBe(400);
  85. expect(body).toEqual({
  86. error: {
  87. message: "Invalid request body",
  88. code: "VALIDATION_INVALID_JSON",
  89. },
  90. });
  91. expect(createSession).not.toHaveBeenCalled();
  92. });
  93. it("returns 401 when user does not exist", async () => {
  94. User.findOne.mockReturnValue({
  95. exec: vi.fn().mockResolvedValue(null),
  96. });
  97. const request = createRequestStub({
  98. username: "unknownuser",
  99. password: "some-password",
  100. });
  101. const response = await POST(request);
  102. const body = await response.json();
  103. expect(response.status).toBe(401);
  104. expect(body).toEqual({
  105. error: {
  106. message: "Invalid credentials",
  107. code: "AUTH_INVALID_CREDENTIALS",
  108. },
  109. });
  110. expect(createSession).not.toHaveBeenCalled();
  111. });
  112. it("returns 401 when passwordHash is missing (defensive)", async () => {
  113. User.findOne.mockReturnValue({
  114. exec: vi.fn().mockResolvedValue({
  115. _id: "507f1f77bcf86cd799439099",
  116. username: "branchuser",
  117. email: "nl01@example.com",
  118. // passwordHash missing on purpose
  119. role: "branch",
  120. branchId: "NL01",
  121. }),
  122. });
  123. const request = createRequestStub({
  124. username: "branchuser",
  125. password: "secret-password",
  126. });
  127. const response = await POST(request);
  128. const body = await response.json();
  129. expect(response.status).toBe(401);
  130. expect(body).toEqual({
  131. error: {
  132. message: "Invalid credentials",
  133. code: "AUTH_INVALID_CREDENTIALS",
  134. },
  135. });
  136. expect(bcryptCompare).not.toHaveBeenCalled();
  137. expect(createSession).not.toHaveBeenCalled();
  138. });
  139. it("returns 401 when password is incorrect", async () => {
  140. const user = {
  141. _id: "507f1f77bcf86cd799439012",
  142. username: "branchuser",
  143. email: "nl02@example.com",
  144. passwordHash: "hashed-password",
  145. role: "branch",
  146. branchId: "NL02",
  147. };
  148. User.findOne.mockReturnValue({
  149. exec: vi.fn().mockResolvedValue(user),
  150. });
  151. bcryptCompare.mockResolvedValue(false);
  152. const request = createRequestStub({
  153. username: "branchuser",
  154. password: "wrong-password",
  155. });
  156. const response = await POST(request);
  157. const body = await response.json();
  158. expect(response.status).toBe(401);
  159. expect(body).toEqual({
  160. error: {
  161. message: "Invalid credentials",
  162. code: "AUTH_INVALID_CREDENTIALS",
  163. },
  164. });
  165. expect(createSession).not.toHaveBeenCalled();
  166. });
  167. it("returns 400 when username or password is missing", async () => {
  168. const request = createRequestStub({
  169. username: "only-username",
  170. });
  171. const response = await POST(request);
  172. const body = await response.json();
  173. expect(response.status).toBe(400);
  174. expect(body).toEqual({
  175. error: {
  176. message: "Missing username or password",
  177. code: "VALIDATION_MISSING_FIELD",
  178. details: { fields: ["username", "password"] },
  179. },
  180. });
  181. expect(User.findOne).not.toHaveBeenCalled();
  182. expect(createSession).not.toHaveBeenCalled();
  183. });
  184. it("returns 500 when an unexpected error occurs", async () => {
  185. User.findOne.mockImplementation(() => {
  186. throw new Error("DB failure");
  187. });
  188. const request = createRequestStub({
  189. username: "branchuser",
  190. password: "secret-password",
  191. });
  192. const response = await POST(request);
  193. const body = await response.json();
  194. expect(response.status).toBe(500);
  195. expect(body).toEqual({
  196. error: {
  197. message: "Internal server error",
  198. code: "INTERNAL_SERVER_ERROR",
  199. },
  200. });
  201. expect(createSession).not.toHaveBeenCalled();
  202. });
  203. });