system-settings-cache.test.ts 5.4 KB

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