session-ttl-validation.test.ts 4.6 KB

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