timezone-resolver.test.ts 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  1. /**
  2. * Timezone Resolver Tests (Task 2)
  3. *
  4. * TDD tests for the system timezone resolver:
  5. * - Fallback chain: DB timezone -> env TZ -> UTC
  6. * - Validation of resolved timezone
  7. * - Integration with cached system settings
  8. */
  9. import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
  10. // Mock the system settings cache
  11. vi.mock("@/lib/config/system-settings-cache", () => ({
  12. getCachedSystemSettings: vi.fn(),
  13. }));
  14. // Mock env config
  15. vi.mock("@/lib/config/env.schema", () => ({
  16. getEnvConfig: vi.fn(),
  17. isDevelopment: vi.fn(() => false),
  18. }));
  19. // Mock logger
  20. vi.mock("@/lib/logger", () => ({
  21. logger: {
  22. debug: vi.fn(),
  23. warn: vi.fn(),
  24. info: vi.fn(),
  25. error: vi.fn(),
  26. },
  27. }));
  28. import { getCachedSystemSettings } from "@/lib/config/system-settings-cache";
  29. import { getEnvConfig } from "@/lib/config/env.schema";
  30. import type { SystemSettings } from "@/types/system-config";
  31. const getCachedSystemSettingsMock = vi.mocked(getCachedSystemSettings);
  32. const getEnvConfigMock = vi.mocked(getEnvConfig);
  33. function createSettings(overrides: Partial<SystemSettings> = {}): SystemSettings {
  34. return {
  35. id: 1,
  36. siteTitle: "Claude Code Hub",
  37. allowGlobalUsageView: false,
  38. currencyDisplay: "USD",
  39. billingModelSource: "original",
  40. timezone: null,
  41. enableAutoCleanup: false,
  42. cleanupRetentionDays: 30,
  43. cleanupSchedule: "0 2 * * *",
  44. cleanupBatchSize: 10000,
  45. enableClientVersionCheck: false,
  46. verboseProviderError: false,
  47. enableHttp2: false,
  48. enableResponsesWebSocket: false,
  49. interceptAnthropicWarmupRequests: false,
  50. enableThinkingSignatureRectifier: true,
  51. enableCodexSessionIdCompletion: true,
  52. enableResponseFixer: true,
  53. responseFixerConfig: {
  54. fixTruncatedJson: true,
  55. fixSseFormat: true,
  56. fixEncoding: true,
  57. maxJsonDepth: 200,
  58. maxFixSize: 1024 * 1024,
  59. },
  60. quotaDbRefreshIntervalSeconds: 10,
  61. quotaLeasePercent5h: 0.05,
  62. quotaLeasePercentDaily: 0.05,
  63. quotaLeasePercentWeekly: 0.05,
  64. quotaLeasePercentMonthly: 0.05,
  65. quotaLeaseCapUsd: null,
  66. createdAt: new Date("2026-01-01T00:00:00.000Z"),
  67. updatedAt: new Date("2026-01-01T00:00:00.000Z"),
  68. ...overrides,
  69. };
  70. }
  71. function mockEnvConfig(tz = "Asia/Shanghai") {
  72. getEnvConfigMock.mockReturnValue({
  73. NODE_ENV: "test",
  74. TZ: tz,
  75. PORT: 23000,
  76. AUTO_MIGRATE: true,
  77. ENABLE_RATE_LIMIT: true,
  78. ENABLE_SECURE_COOKIES: true,
  79. SESSION_TTL: 300,
  80. STORE_SESSION_MESSAGES: false,
  81. DEBUG_MODE: false,
  82. LOG_LEVEL: "info",
  83. ENABLE_CIRCUIT_BREAKER_ON_NETWORK_ERRORS: false,
  84. ENABLE_PROVIDER_CACHE: true,
  85. MAX_RETRY_ATTEMPTS_DEFAULT: 2,
  86. FETCH_BODY_TIMEOUT: 600000,
  87. FETCH_HEADERS_TIMEOUT: 600000,
  88. FETCH_CONNECT_TIMEOUT: 30000,
  89. REDIS_TLS_REJECT_UNAUTHORIZED: true,
  90. } as ReturnType<typeof getEnvConfig>);
  91. }
  92. beforeEach(() => {
  93. vi.clearAllMocks();
  94. });
  95. describe("resolveSystemTimezone", () => {
  96. it("should return DB timezone when set and valid", async () => {
  97. const { resolveSystemTimezone } = await import("@/lib/utils/timezone");
  98. getCachedSystemSettingsMock.mockResolvedValue(createSettings({ timezone: "America/New_York" }));
  99. mockEnvConfig("Asia/Shanghai");
  100. const result = await resolveSystemTimezone();
  101. expect(result).toBe("America/New_York");
  102. });
  103. it("should fallback to env TZ when DB timezone is null", async () => {
  104. const { resolveSystemTimezone } = await import("@/lib/utils/timezone");
  105. getCachedSystemSettingsMock.mockResolvedValue(createSettings({ timezone: null }));
  106. mockEnvConfig("Europe/London");
  107. const result = await resolveSystemTimezone();
  108. expect(result).toBe("Europe/London");
  109. });
  110. it("should fallback to env TZ when DB timezone is invalid", async () => {
  111. const { resolveSystemTimezone } = await import("@/lib/utils/timezone");
  112. getCachedSystemSettingsMock.mockResolvedValue(
  113. createSettings({ timezone: "Invalid/Timezone_Zone" })
  114. );
  115. mockEnvConfig("Asia/Tokyo");
  116. const result = await resolveSystemTimezone();
  117. expect(result).toBe("Asia/Tokyo");
  118. });
  119. it("should fallback to UTC when both DB timezone and env TZ are invalid", async () => {
  120. const { resolveSystemTimezone } = await import("@/lib/utils/timezone");
  121. getCachedSystemSettingsMock.mockResolvedValue(createSettings({ timezone: "Invalid/Zone" }));
  122. // Empty string TZ won't pass isValidIANATimezone
  123. mockEnvConfig("");
  124. const result = await resolveSystemTimezone();
  125. expect(result).toBe("UTC");
  126. });
  127. it("should fallback to UTC when getCachedSystemSettings throws", async () => {
  128. const { resolveSystemTimezone } = await import("@/lib/utils/timezone");
  129. getCachedSystemSettingsMock.mockRejectedValue(new Error("DB connection failed"));
  130. mockEnvConfig("Asia/Shanghai");
  131. const result = await resolveSystemTimezone();
  132. // Should still try env TZ fallback
  133. expect(result).toBe("Asia/Shanghai");
  134. });
  135. it("should fallback to UTC when getCachedSystemSettings throws and env TZ is empty", async () => {
  136. const { resolveSystemTimezone } = await import("@/lib/utils/timezone");
  137. getCachedSystemSettingsMock.mockRejectedValue(new Error("DB connection failed"));
  138. mockEnvConfig("");
  139. const result = await resolveSystemTimezone();
  140. expect(result).toBe("UTC");
  141. });
  142. it("should handle empty string DB timezone as null", async () => {
  143. const { resolveSystemTimezone } = await import("@/lib/utils/timezone");
  144. getCachedSystemSettingsMock.mockResolvedValue(
  145. createSettings({ timezone: "" as unknown as null })
  146. );
  147. mockEnvConfig("Europe/Paris");
  148. const result = await resolveSystemTimezone();
  149. expect(result).toBe("Europe/Paris");
  150. });
  151. });