import { describe, it, expect, beforeEach, vi } from "vitest"; // Mock safeRedis 以控制 Redis 行为(单元测试不依赖真实 Redis) vi.mock("../../src/shared/cache/redis.client.js", () => ({ safeRedis: vi.fn().mockResolvedValue(null), getRedisClient: vi.fn(() => null), closeRedisClient: vi.fn(), })); import { withCacheFallback, invalidateCache, } from "../../src/aggregation/fallback-strategy.js"; import { safeRedis } from "../../src/shared/cache/redis.client.js"; const mockSafeRedis = vi.mocked(safeRedis); describe("FallbackStrategy(三级降级)", () => { beforeEach(() => { vi.clearAllMocks(); mockSafeRedis.mockResolvedValue(null); }); describe("Redis 缓存命中(第一级)", () => { it("Redis 命中时返回缓存数据,fromCache=true", async () => { const cachedData = { id: "test-1", name: "测试" }; mockSafeRedis.mockResolvedValueOnce(JSON.stringify(cachedData)); const result = await withCacheFallback( "test-redis-hit", () => Promise.resolve({ id: "test-1", name: "不应该被调用" }), 30, "test", ); expect(result.data).toEqual(cachedData); expect(result.fromCache).toBe(true); expect(result.stale).toBe(false); }); }); describe("Redis 未命中 → fn 成功(第三级直连)", () => { it("Redis miss 后调 fn 获取数据并写入缓存", async () => { const fnData = { id: "test-2", value: 42 }; mockSafeRedis .mockResolvedValueOnce(null) // get → miss .mockResolvedValueOnce(undefined); // set → ok const result = await withCacheFallback( "test-redis-miss-fn-success", () => Promise.resolve(fnData), 30, "test", ); expect(result.data).toEqual(fnData); expect(result.fromCache).toBe(false); expect(result.stale).toBe(false); // safeRedis 被调用 2 次:get + set expect(mockSafeRedis).toHaveBeenCalledTimes(2); }); }); describe("LRU 缓存命中(第二级降级)", () => { it("Redis 不可用时从 LRU 缓存返回数据", async () => { // 第一次调用:miss Redis + miss LRU → 调 fn → 写入 LRU const fnData = { id: "test-3", label: "LRU测试" }; mockSafeRedis .mockResolvedValueOnce(null) // get → miss .mockResolvedValueOnce(undefined); // set → ok await withCacheFallback( "test-lru-hit-key", () => Promise.resolve(fnData), 60, "test", ); // 第二次调用:miss Redis → hit LRU mockSafeRedis.mockResolvedValueOnce(null); // get → miss const result = await withCacheFallback( "test-lru-hit-key", () => Promise.resolve({ id: "test-3", label: "不应该被调用" }), 60, "test", ); expect(result.data).toEqual(fnData); expect(result.fromCache).toBe(true); expect(result.stale).toBe(false); }); }); describe("fn 失败降级", () => { it("fn 抛错时返回 null", async () => { mockSafeRedis.mockResolvedValueOnce(null); // get → miss const result = await withCacheFallback( "test-fn-fail", () => Promise.reject(new Error("downstream error")), 30, "test", ); expect(result.data).toBeNull(); expect(result.fromCache).toBe(false); expect(result.stale).toBe(false); }); }); describe("Redis JSON 解析失败", () => { it("Redis 返回非法 JSON 时降级到 fn", async () => { mockSafeRedis .mockResolvedValueOnce("not-valid-json") // get → 返回非法 JSON .mockResolvedValueOnce(null) // LRU miss .mockResolvedValueOnce(undefined); // set → ok const result = await withCacheFallback( "test-bad-json", () => Promise.resolve({ data: "from-fn" }), 30, "test", ); expect(result.data).toEqual({ data: "from-fn" }); expect(result.fromCache).toBe(false); }); }); describe("invalidateCache", () => { it("调用 safeRedis del + LRU delete", async () => { // 先写入 LRU mockSafeRedis .mockResolvedValueOnce(null) // get → miss .mockResolvedValueOnce(undefined); // set → ok await withCacheFallback( "test-invalidate", () => Promise.resolve({ val: 1 }), 30, "test", ); // 失效缓存 mockSafeRedis.mockResolvedValueOnce(undefined); // del → ok await invalidateCache("test-invalidate"); // 再次查询,应该 miss LRU mockSafeRedis .mockResolvedValueOnce(null) // get → miss .mockResolvedValueOnce(undefined); // set → ok const result = await withCacheFallback( "test-invalidate", () => Promise.resolve({ val: 2 }), 30, "test", ); expect(result.data).toEqual({ val: 2 }); expect(result.fromCache).toBe(false); }); }); });