timezone-resolver.test.ts 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186
  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. codexPriorityBillingSource: "requested",
  41. timezone: null,
  42. enableAutoCleanup: false,
  43. cleanupRetentionDays: 30,
  44. cleanupSchedule: "0 2 * * *",
  45. cleanupBatchSize: 10000,
  46. enableClientVersionCheck: false,
  47. verboseProviderError: false,
  48. enableHttp2: false,
  49. enableHighConcurrencyMode: false,
  50. interceptAnthropicWarmupRequests: false,
  51. enableThinkingBudgetRectifier: true,
  52. enableBillingHeaderRectifier: true,
  53. enableResponseInputRectifier: true,
  54. enableClaudeMetadataUserIdInjection: true,
  55. enableThinkingSignatureRectifier: true,
  56. enableCodexSessionIdCompletion: true,
  57. enableResponseFixer: true,
  58. responseFixerConfig: {
  59. fixTruncatedJson: true,
  60. fixSseFormat: true,
  61. fixEncoding: true,
  62. maxJsonDepth: 200,
  63. maxFixSize: 1024 * 1024,
  64. },
  65. quotaDbRefreshIntervalSeconds: 10,
  66. quotaLeasePercent5h: 0.05,
  67. quotaLeasePercentDaily: 0.05,
  68. quotaLeasePercentWeekly: 0.05,
  69. quotaLeasePercentMonthly: 0.05,
  70. quotaLeaseCapUsd: null,
  71. createdAt: new Date("2026-01-01T00:00:00.000Z"),
  72. updatedAt: new Date("2026-01-01T00:00:00.000Z"),
  73. ...overrides,
  74. };
  75. }
  76. function mockEnvConfig(tz = "Asia/Shanghai") {
  77. getEnvConfigMock.mockReturnValue({
  78. NODE_ENV: "test",
  79. TZ: tz,
  80. PORT: 23000,
  81. AUTO_MIGRATE: true,
  82. ENABLE_RATE_LIMIT: true,
  83. ENABLE_SECURE_COOKIES: true,
  84. SESSION_TTL: 300,
  85. STORE_SESSION_MESSAGES: false,
  86. DEBUG_MODE: false,
  87. LOG_LEVEL: "info",
  88. ENABLE_CIRCUIT_BREAKER_ON_NETWORK_ERRORS: false,
  89. ENABLE_PROVIDER_CACHE: true,
  90. MAX_RETRY_ATTEMPTS_DEFAULT: 2,
  91. FETCH_BODY_TIMEOUT: 600000,
  92. FETCH_HEADERS_TIMEOUT: 600000,
  93. FETCH_CONNECT_TIMEOUT: 30000,
  94. REDIS_TLS_REJECT_UNAUTHORIZED: true,
  95. } as ReturnType<typeof getEnvConfig>);
  96. }
  97. beforeEach(() => {
  98. vi.clearAllMocks();
  99. });
  100. describe("resolveSystemTimezone", () => {
  101. it("should return DB timezone when set and valid", async () => {
  102. const { resolveSystemTimezone } = await import("@/lib/utils/timezone");
  103. getCachedSystemSettingsMock.mockResolvedValue(createSettings({ timezone: "America/New_York" }));
  104. mockEnvConfig("Asia/Shanghai");
  105. const result = await resolveSystemTimezone();
  106. expect(result).toBe("America/New_York");
  107. });
  108. it("should fallback to env TZ when DB timezone is null", async () => {
  109. const { resolveSystemTimezone } = await import("@/lib/utils/timezone");
  110. getCachedSystemSettingsMock.mockResolvedValue(createSettings({ timezone: null }));
  111. mockEnvConfig("Europe/London");
  112. const result = await resolveSystemTimezone();
  113. expect(result).toBe("Europe/London");
  114. });
  115. it("should fallback to env TZ when DB timezone is invalid", async () => {
  116. const { resolveSystemTimezone } = await import("@/lib/utils/timezone");
  117. getCachedSystemSettingsMock.mockResolvedValue(
  118. createSettings({ timezone: "Invalid/Timezone_Zone" })
  119. );
  120. mockEnvConfig("Asia/Tokyo");
  121. const result = await resolveSystemTimezone();
  122. expect(result).toBe("Asia/Tokyo");
  123. });
  124. it("should fallback to UTC when both DB timezone and env TZ are invalid", async () => {
  125. const { resolveSystemTimezone } = await import("@/lib/utils/timezone");
  126. getCachedSystemSettingsMock.mockResolvedValue(createSettings({ timezone: "Invalid/Zone" }));
  127. // Empty string TZ won't pass isValidIANATimezone
  128. mockEnvConfig("");
  129. const result = await resolveSystemTimezone();
  130. expect(result).toBe("UTC");
  131. });
  132. it("should fallback to UTC when getCachedSystemSettings throws", async () => {
  133. const { resolveSystemTimezone } = await import("@/lib/utils/timezone");
  134. getCachedSystemSettingsMock.mockRejectedValue(new Error("DB connection failed"));
  135. mockEnvConfig("Asia/Shanghai");
  136. const result = await resolveSystemTimezone();
  137. // Should still try env TZ fallback
  138. expect(result).toBe("Asia/Shanghai");
  139. });
  140. it("should fallback to UTC when getCachedSystemSettings throws and env TZ is empty", async () => {
  141. const { resolveSystemTimezone } = await import("@/lib/utils/timezone");
  142. getCachedSystemSettingsMock.mockRejectedValue(new Error("DB connection failed"));
  143. mockEnvConfig("");
  144. const result = await resolveSystemTimezone();
  145. expect(result).toBe("UTC");
  146. });
  147. it("should handle empty string DB timezone as null", async () => {
  148. const { resolveSystemTimezone } = await import("@/lib/utils/timezone");
  149. getCachedSystemSettingsMock.mockResolvedValue(
  150. createSettings({ timezone: "" as unknown as null })
  151. );
  152. mockEnvConfig("Europe/Paris");
  153. const result = await resolveSystemTimezone();
  154. expect(result).toBe("Europe/Paris");
  155. });
  156. });