session.test.js 4.5 KB

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