import { beforeEach, expect, it, vi } from "vitest"; const state = vi.hoisted(() => ({ revision: "0", values: new Map(), get: vi.fn(), set: vi.fn(), status: "ready", })); vi.mock("@/lib/redis", () => ({ redis: state })); vi.mock("@/lib/logger", () => ({ logger: { error: vi.fn() } })); vi.mock("@/lib/cache", () => ({ cached: async (key: string, _ttl: number, query: () => Promise) => { if (state.values.has(key)) return state.values.get(key); const result = await query(); state.values.set(key, result); return result; }, })); import { cacheNews, invalidateNewsCache, refreshNewsCacheForDelivery, } from "./news-cache"; beforeEach(() => { state.values.clear(); state.revision = "0"; state.status = "ready"; state.get.mockReset().mockImplementation(async () => state.revision); state.set .mockReset() .mockImplementation(async (_key: string, value: string) => { state.revision = value; }); }); it("invalidates a previously cached public list", async () => { const query = vi .fn() .mockResolvedValueOnce([]) .mockResolvedValueOnce(["new article"]); expect(await cacheNews("list", 60000, query)).toEqual([]); await invalidateNewsCache(); expect(await cacheNews("list", 60000, query)).toEqual(["new article"]); }); it("does not let a stale in-flight read replace a newer revision", async () => { let finish!: (value: string[]) => void; const old = cacheNews( "list", 60000, () => new Promise((resolve) => { finish = resolve; }), ); await vi.waitFor(() => expect(finish).toBeDefined()); await invalidateNewsCache(); expect(await cacheNews("list", 60000, async () => ["new"])).toEqual(["new"]); finish([]); await old; expect(await cacheNews("list", 60000, async () => ["wrong"])).toEqual([ "new", ]); }); it("reads fresh data when Redis is unavailable", async () => { state.get.mockRejectedValue(Error("offline")); expect(await cacheNews("list", 60000, async () => ["fresh"])).toEqual([ "fresh", ]); }); it("keeps failed durable cache invalidations retryable", async () => { state.set.mockRejectedValue(Error("offline")); await expect(refreshNewsCacheForDelivery()).rejects.toThrow("offline"); }); it("does not acknowledge an ended Redis connection as refreshed", async () => { state.status = "end"; await expect(refreshNewsCacheForDelivery()).rejects.toThrow(); });