system-settings-cache.test.ts 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
  1. import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
  2. import type { SystemSettings } from "@/types/system-config";
  3. const getSystemSettingsMock = vi.fn();
  4. const loggerDebugMock = vi.fn();
  5. const loggerWarnMock = vi.fn();
  6. const loggerInfoMock = vi.fn();
  7. vi.mock("server-only", () => ({}));
  8. vi.mock("@/repository/system-config", () => ({
  9. getSystemSettings: () => getSystemSettingsMock(),
  10. }));
  11. vi.mock("@/lib/logger", () => ({
  12. logger: {
  13. debug: loggerDebugMock,
  14. warn: loggerWarnMock,
  15. info: loggerInfoMock,
  16. trace: vi.fn(),
  17. error: vi.fn(),
  18. },
  19. }));
  20. function createSettings(overrides: Partial<SystemSettings> = {}): SystemSettings {
  21. const base: SystemSettings = {
  22. id: 1,
  23. siteTitle: "Claude Code Hub",
  24. allowGlobalUsageView: false,
  25. currencyDisplay: "USD",
  26. billingModelSource: "original",
  27. timezone: null,
  28. enableAutoCleanup: false,
  29. cleanupRetentionDays: 30,
  30. cleanupSchedule: "0 2 * * *",
  31. cleanupBatchSize: 10000,
  32. enableClientVersionCheck: false,
  33. verboseProviderError: false,
  34. enableHttp2: false,
  35. enableResponsesWebSocket: false,
  36. interceptAnthropicWarmupRequests: false,
  37. enableThinkingSignatureRectifier: true,
  38. enableThinkingBudgetRectifier: true,
  39. enableBillingHeaderRectifier: true,
  40. enableCodexSessionIdCompletion: true,
  41. enableClaudeMetadataUserIdInjection: true,
  42. enableResponseFixer: true,
  43. responseFixerConfig: {
  44. fixTruncatedJson: true,
  45. fixSseFormat: true,
  46. fixEncoding: true,
  47. maxJsonDepth: 200,
  48. maxFixSize: 1024 * 1024,
  49. },
  50. quotaDbRefreshIntervalSeconds: 10,
  51. quotaLeasePercent5h: 0.05,
  52. quotaLeasePercentDaily: 0.05,
  53. quotaLeasePercentWeekly: 0.05,
  54. quotaLeasePercentMonthly: 0.05,
  55. quotaLeaseCapUsd: null,
  56. createdAt: new Date("2026-01-01T00:00:00.000Z"),
  57. updatedAt: new Date("2026-01-01T00:00:00.000Z"),
  58. };
  59. return { ...base, ...overrides };
  60. }
  61. async function loadCache() {
  62. const mod = await import("@/lib/config/system-settings-cache");
  63. return {
  64. getCachedSystemSettings: mod.getCachedSystemSettings,
  65. isHttp2Enabled: mod.isHttp2Enabled,
  66. invalidateSystemSettingsCache: mod.invalidateSystemSettingsCache,
  67. };
  68. }
  69. beforeEach(() => {
  70. vi.clearAllMocks();
  71. vi.resetModules();
  72. vi.useFakeTimers();
  73. vi.setSystemTime(new Date("2026-01-03T00:00:00.000Z"));
  74. });
  75. afterEach(() => {
  76. vi.useRealTimers();
  77. });
  78. describe("SystemSettingsCache", () => {
  79. test("首次调用应从数据库获取并缓存;TTL 内再次调用应直接返回缓存", async () => {
  80. getSystemSettingsMock.mockResolvedValueOnce(createSettings({ id: 101 }));
  81. const { getCachedSystemSettings } = await loadCache();
  82. const first = await getCachedSystemSettings();
  83. const second = await getCachedSystemSettings();
  84. expect(first).toEqual(expect.objectContaining({ id: 101 }));
  85. // 缓存返回应保持引用一致,避免不必要的对象创建
  86. expect(second).toBe(first);
  87. expect(getSystemSettingsMock).toHaveBeenCalledTimes(1);
  88. expect(loggerDebugMock).toHaveBeenCalledTimes(1);
  89. expect(loggerWarnMock).not.toHaveBeenCalled();
  90. });
  91. test("TTL 过期后应重新获取并更新缓存", async () => {
  92. const settingsA = createSettings({ id: 201, enableHttp2: false });
  93. const settingsB = createSettings({ id: 202, enableHttp2: true });
  94. getSystemSettingsMock.mockResolvedValueOnce(settingsA).mockResolvedValueOnce(settingsB);
  95. const { getCachedSystemSettings } = await loadCache();
  96. const first = await getCachedSystemSettings();
  97. expect(first).toBe(settingsA);
  98. expect(getSystemSettingsMock).toHaveBeenCalledTimes(1);
  99. vi.setSystemTime(new Date("2026-01-03T00:01:00.001Z"));
  100. const second = await getCachedSystemSettings();
  101. expect(second).toBe(settingsB);
  102. expect(getSystemSettingsMock).toHaveBeenCalledTimes(2);
  103. });
  104. test("当获取失败且已有缓存时,应 fail-open 返回上一份缓存", async () => {
  105. const cached = createSettings({ id: 301, interceptAnthropicWarmupRequests: true });
  106. getSystemSettingsMock.mockResolvedValueOnce(cached);
  107. const { getCachedSystemSettings } = await loadCache();
  108. const first = await getCachedSystemSettings();
  109. expect(first).toBe(cached);
  110. vi.setSystemTime(new Date("2026-01-03T00:01:00.001Z"));
  111. getSystemSettingsMock.mockRejectedValueOnce(new Error("db down"));
  112. const second = await getCachedSystemSettings();
  113. expect(second).toBe(cached);
  114. expect(loggerWarnMock).toHaveBeenCalledTimes(1);
  115. });
  116. test("当获取失败且无缓存时,应返回最小默认设置,并显式关闭 warmup 拦截", async () => {
  117. getSystemSettingsMock.mockRejectedValueOnce(new Error("db down"));
  118. const { getCachedSystemSettings } = await loadCache();
  119. const settings = await getCachedSystemSettings();
  120. expect(settings).toEqual(
  121. expect.objectContaining({
  122. siteTitle: "Claude Code Hub",
  123. enableHttp2: false,
  124. enableResponsesWebSocket: false,
  125. interceptAnthropicWarmupRequests: false,
  126. })
  127. );
  128. expect(loggerWarnMock).toHaveBeenCalledTimes(1);
  129. });
  130. test("invalidateSystemSettingsCache 应清空缓存并触发下一次重新获取", async () => {
  131. const settingsA = createSettings({ id: 401 });
  132. const settingsB = createSettings({ id: 402 });
  133. getSystemSettingsMock.mockResolvedValueOnce(settingsA).mockResolvedValueOnce(settingsB);
  134. const { getCachedSystemSettings, invalidateSystemSettingsCache } = await loadCache();
  135. expect(await getCachedSystemSettings()).toBe(settingsA);
  136. invalidateSystemSettingsCache();
  137. expect(loggerInfoMock).toHaveBeenCalledTimes(1);
  138. expect(await getCachedSystemSettings()).toBe(settingsB);
  139. expect(getSystemSettingsMock).toHaveBeenCalledTimes(2);
  140. });
  141. test("isHttp2Enabled 应读取缓存并返回 enableHttp2", async () => {
  142. getSystemSettingsMock.mockResolvedValueOnce(createSettings({ id: 501, enableHttp2: true }));
  143. const { isHttp2Enabled } = await loadCache();
  144. expect(await isHttp2Enabled()).toBe(true);
  145. });
  146. });