system-settings-cache.test.ts 5.9 KB

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