session.test.js 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255
  1. /* @vitest-environment node */
  2. import { describe, it, expect, vi, beforeEach } from "vitest";
  3. import { SignJWT } from "jose";
  4. // Mock next/headers to provide a simple in-memory cookie store
  5. vi.mock("next/headers", () => {
  6. let store = new Map();
  7. return {
  8. cookies() {
  9. return {
  10. get(name) {
  11. const entry = store.get(name);
  12. if (!entry) return undefined;
  13. return { name, value: entry.value };
  14. },
  15. set(name, value, options) {
  16. store.set(name, { value, options });
  17. },
  18. };
  19. },
  20. __cookieStore: {
  21. clear() {
  22. store = new Map();
  23. },
  24. dump() {
  25. return store;
  26. },
  27. },
  28. };
  29. });
  30. // Import after the mock so the module under test uses the mocked cookies()
  31. import {
  32. createSession,
  33. getSession,
  34. destroySession,
  35. SESSION_COOKIE_NAME,
  36. SESSION_MAX_AGE_SECONDS,
  37. } from "./session";
  38. import { __cookieStore } from "next/headers";
  39. describe("auth session utilities", () => {
  40. beforeEach(() => {
  41. __cookieStore.clear();
  42. process.env.SESSION_SECRET = "x".repeat(64);
  43. process.env.NODE_ENV = "test";
  44. delete process.env.SESSION_COOKIE_SECURE;
  45. });
  46. it("creates a session cookie with a signed JWT", async () => {
  47. const jwt = await createSession({
  48. userId: "user123",
  49. role: "branch",
  50. branchId: "NL01",
  51. });
  52. expect(typeof jwt).toBe("string");
  53. expect(jwt.length).toBeGreaterThan(10);
  54. const store = __cookieStore.dump();
  55. const cookie = store.get(SESSION_COOKIE_NAME);
  56. expect(cookie).toBeDefined();
  57. expect(cookie.value).toBe(jwt);
  58. expect(cookie.options).toMatchObject({
  59. httpOnly: true,
  60. secure: false, // NODE_ENV = "test"
  61. sameSite: "lax",
  62. path: "/",
  63. maxAge: SESSION_MAX_AGE_SECONDS,
  64. });
  65. });
  66. it("reads a valid session from cookie (email defaults to null)", async () => {
  67. await createSession({
  68. userId: "user456",
  69. role: "admin",
  70. branchId: null,
  71. });
  72. const session = await getSession();
  73. expect(session).toEqual({
  74. userId: "user456",
  75. role: "admin",
  76. branchId: null,
  77. email: null,
  78. mustChangePassword: false,
  79. });
  80. });
  81. it("includes email in the session when provided (normalized to lowercase)", async () => {
  82. await createSession({
  83. userId: "user999",
  84. role: "branch",
  85. branchId: "NL01",
  86. email: "User@Example.COM",
  87. });
  88. const session = await getSession();
  89. expect(session).toEqual({
  90. userId: "user999",
  91. role: "branch",
  92. branchId: "NL01",
  93. email: "user@example.com",
  94. mustChangePassword: false,
  95. });
  96. });
  97. it("includes mustChangePassword=true only when explicitly set to true", async () => {
  98. await createSession({
  99. userId: "user-must",
  100. role: "branch",
  101. branchId: "NL01",
  102. mustChangePassword: true,
  103. });
  104. const session = await getSession();
  105. expect(session).toEqual({
  106. userId: "user-must",
  107. role: "branch",
  108. branchId: "NL01",
  109. email: null,
  110. mustChangePassword: true,
  111. });
  112. });
  113. it("defaults mustChangePassword=false for legacy payloads without the field", async () => {
  114. const jwt = await new SignJWT({
  115. userId: "legacy-user",
  116. role: "admin",
  117. branchId: null,
  118. email: "legacy@example.com",
  119. })
  120. .setProtectedHeader({ alg: "HS256", typ: "JWT" })
  121. .setIssuedAt()
  122. .setExpirationTime(`${SESSION_MAX_AGE_SECONDS}s`)
  123. .sign(new TextEncoder().encode(process.env.SESSION_SECRET));
  124. const store = __cookieStore.dump();
  125. store.set(SESSION_COOKIE_NAME, {
  126. value: jwt,
  127. options: {
  128. httpOnly: true,
  129. secure: false,
  130. sameSite: "lax",
  131. path: "/",
  132. maxAge: SESSION_MAX_AGE_SECONDS,
  133. },
  134. });
  135. const session = await getSession();
  136. expect(session).toEqual({
  137. userId: "legacy-user",
  138. role: "admin",
  139. branchId: null,
  140. email: "legacy@example.com",
  141. mustChangePassword: false,
  142. });
  143. });
  144. it("returns null when no session cookie is present", async () => {
  145. const session = await getSession();
  146. expect(session).toBeNull();
  147. });
  148. it("returns null and clears cookie when token is invalid", async () => {
  149. // Manually set an invalid JWT value
  150. const store = __cookieStore.dump();
  151. store.set(SESSION_COOKIE_NAME, {
  152. value: "not-a-valid-jwt",
  153. options: {
  154. httpOnly: true,
  155. secure: false,
  156. sameSite: "lax",
  157. path: "/",
  158. maxAge: SESSION_MAX_AGE_SECONDS,
  159. },
  160. });
  161. const session = await getSession();
  162. expect(session).toBeNull();
  163. const updatedStore = __cookieStore.dump();
  164. const cookie = updatedStore.get(SESSION_COOKIE_NAME);
  165. expect(cookie).toBeDefined();
  166. expect(cookie.value).toBe("");
  167. expect(cookie.options.maxAge).toBe(0);
  168. });
  169. it("destroySession clears the session cookie when it exists", async () => {
  170. await createSession({
  171. userId: "user789",
  172. role: "branch",
  173. branchId: "NL02",
  174. });
  175. await destroySession();
  176. const store = __cookieStore.dump();
  177. const cookie = store.get(SESSION_COOKIE_NAME);
  178. expect(cookie).toBeDefined();
  179. expect(cookie.value).toBe("");
  180. expect(cookie.options.maxAge).toBe(0);
  181. });
  182. it("destroySession sets an empty cookie even if none existed before", async () => {
  183. await destroySession();
  184. const store = __cookieStore.dump();
  185. const cookie = store.get(SESSION_COOKIE_NAME);
  186. expect(cookie).toBeDefined();
  187. expect(cookie.value).toBe("");
  188. expect(cookie.options.maxAge).toBe(0);
  189. });
  190. it('respects SESSION_COOKIE_SECURE override when set to "true"', async () => {
  191. process.env.SESSION_COOKIE_SECURE = "true";
  192. await createSession({
  193. userId: "user-secure",
  194. role: "admin",
  195. branchId: null,
  196. });
  197. const store = __cookieStore.dump();
  198. const cookie = store.get(SESSION_COOKIE_NAME);
  199. expect(cookie.options.secure).toBe(true);
  200. });
  201. it('respects SESSION_COOKIE_SECURE override when set to "false"', async () => {
  202. process.env.SESSION_COOKIE_SECURE = "false";
  203. await createSession({
  204. userId: "user-insecure",
  205. role: "admin",
  206. branchId: null,
  207. });
  208. const store = __cookieStore.dump();
  209. const cookie = store.get(SESSION_COOKIE_NAME);
  210. expect(cookie.options.secure).toBe(false);
  211. });
  212. });