session-ttl-validation.test.ts 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166
  1. import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
  2. import { getGlobalActiveSessionsKey } from "@/lib/redis/active-session-keys";
  3. /**
  4. * Tests for SESSION_TTL environment variable validation
  5. *
  6. * These tests verify that invalid SESSION_TTL values (NaN, 0, negative)
  7. * are properly handled with fallback to default 300 seconds.
  8. */
  9. let redisClientRef: any;
  10. const pipelineCalls: Array<unknown[]> = [];
  11. const makePipeline = () => {
  12. const pipeline = {
  13. zadd: vi.fn((...args: unknown[]) => {
  14. pipelineCalls.push(["zadd", ...args]);
  15. return pipeline;
  16. }),
  17. expire: vi.fn((...args: unknown[]) => {
  18. pipelineCalls.push(["expire", ...args]);
  19. return pipeline;
  20. }),
  21. setex: vi.fn((...args: unknown[]) => {
  22. pipelineCalls.push(["setex", ...args]);
  23. return pipeline;
  24. }),
  25. zremrangebyscore: vi.fn((...args: unknown[]) => {
  26. pipelineCalls.push(["zremrangebyscore", ...args]);
  27. return pipeline;
  28. }),
  29. zrange: vi.fn((...args: unknown[]) => {
  30. pipelineCalls.push(["zrange", ...args]);
  31. return pipeline;
  32. }),
  33. exists: vi.fn((...args: unknown[]) => {
  34. pipelineCalls.push(["exists", ...args]);
  35. return pipeline;
  36. }),
  37. exec: vi.fn(async () => {
  38. pipelineCalls.push(["exec"]);
  39. return [];
  40. }),
  41. };
  42. return pipeline;
  43. };
  44. vi.mock("@/lib/logger", () => ({
  45. logger: {
  46. debug: vi.fn(),
  47. info: vi.fn(),
  48. warn: vi.fn(),
  49. error: vi.fn(),
  50. trace: vi.fn(),
  51. },
  52. }));
  53. vi.mock("@/lib/redis", () => ({
  54. getRedisClient: () => redisClientRef,
  55. }));
  56. describe("SESSION_TTL environment variable validation", () => {
  57. const nowMs = 1_700_000_000_000;
  58. const ORIGINAL_SESSION_TTL = process.env.SESSION_TTL;
  59. beforeEach(() => {
  60. vi.resetAllMocks();
  61. vi.resetModules();
  62. pipelineCalls.length = 0;
  63. vi.useFakeTimers();
  64. vi.setSystemTime(new Date(nowMs));
  65. redisClientRef = {
  66. status: "ready",
  67. exists: vi.fn(async () => 1),
  68. type: vi.fn(async () => "zset"),
  69. del: vi.fn(async () => 1),
  70. zremrangebyscore: vi.fn(async () => 0),
  71. zrange: vi.fn(async () => []),
  72. pipeline: vi.fn(() => makePipeline()),
  73. };
  74. });
  75. afterEach(() => {
  76. vi.useRealTimers();
  77. if (ORIGINAL_SESSION_TTL === undefined) {
  78. delete process.env.SESSION_TTL;
  79. } else {
  80. process.env.SESSION_TTL = ORIGINAL_SESSION_TTL;
  81. }
  82. });
  83. describe("SessionTracker TTL parsing", () => {
  84. it("should use default 300 when SESSION_TTL is empty string", async () => {
  85. process.env.SESSION_TTL = "";
  86. const { SessionTracker } = await import("@/lib/session-tracker");
  87. await SessionTracker.getGlobalSessionCount();
  88. // Default: 300 seconds = 300000 ms
  89. const expectedCutoff = nowMs - 300 * 1000;
  90. expect(redisClientRef.zremrangebyscore).toHaveBeenCalledWith(
  91. getGlobalActiveSessionsKey(),
  92. "-inf",
  93. expectedCutoff
  94. );
  95. });
  96. it("should use default 300 when SESSION_TTL is NaN", async () => {
  97. process.env.SESSION_TTL = "not-a-number";
  98. const { SessionTracker } = await import("@/lib/session-tracker");
  99. await SessionTracker.getGlobalSessionCount();
  100. const expectedCutoff = nowMs - 300 * 1000;
  101. expect(redisClientRef.zremrangebyscore).toHaveBeenCalledWith(
  102. getGlobalActiveSessionsKey(),
  103. "-inf",
  104. expectedCutoff
  105. );
  106. });
  107. it("should use default 300 when SESSION_TTL is 0", async () => {
  108. process.env.SESSION_TTL = "0";
  109. const { SessionTracker } = await import("@/lib/session-tracker");
  110. await SessionTracker.getGlobalSessionCount();
  111. const expectedCutoff = nowMs - 300 * 1000;
  112. expect(redisClientRef.zremrangebyscore).toHaveBeenCalledWith(
  113. getGlobalActiveSessionsKey(),
  114. "-inf",
  115. expectedCutoff
  116. );
  117. });
  118. it("should use default 300 when SESSION_TTL is negative", async () => {
  119. process.env.SESSION_TTL = "-100";
  120. const { SessionTracker } = await import("@/lib/session-tracker");
  121. await SessionTracker.getGlobalSessionCount();
  122. const expectedCutoff = nowMs - 300 * 1000;
  123. expect(redisClientRef.zremrangebyscore).toHaveBeenCalledWith(
  124. getGlobalActiveSessionsKey(),
  125. "-inf",
  126. expectedCutoff
  127. );
  128. });
  129. it("should use provided value when SESSION_TTL is valid positive integer", async () => {
  130. process.env.SESSION_TTL = "600";
  131. const { SessionTracker } = await import("@/lib/session-tracker");
  132. await SessionTracker.getGlobalSessionCount();
  133. // Custom: 600 seconds = 600000 ms
  134. const expectedCutoff = nowMs - 600 * 1000;
  135. expect(redisClientRef.zremrangebyscore).toHaveBeenCalledWith(
  136. getGlobalActiveSessionsKey(),
  137. "-inf",
  138. expectedCutoff
  139. );
  140. });
  141. });
  142. });