81 lines
2.3 KiB
TypeScript
81 lines
2.3 KiB
TypeScript
import { beforeEach, expect, it, vi } from "vitest";
|
|
|
|
const state = vi.hoisted(() => ({
|
|
revision: "0",
|
|
values: new Map<string, unknown>(),
|
|
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<unknown>) => {
|
|
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<string[]>((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();
|
|
});
|