route.test.js 5.5 KB

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