timezone-resolver.test.ts 5.5 KB

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