import { env } from "cloudflare:workers"; import { evictDurableObject, runInDurableObject } from "cloudflare:test"; import { describe, expect, it, vi } from "vitest"; import worker from "../src/index.js"; import { toWebkey } from "../src/webkeys.js"; import { FETCH_BUDGET } from "../src/query.js"; // matches tests/fake-webapp.js: 30 items, every third private const WEBAPP_ID = "fake-webapp-id"; type TestEnv = { CACHE_DO: DurableObjectNamespace; PUBLIC_DO: DurableObjectNamespace; WEBAPP: { calls(): Promise }; }; const testEnv = env as unknown as TestEnv; const parse = (items: string[]) => items.map((j) => JSON.parse(j) as Record); describe("PublicCache", () => { it("on a miss asks the webapp and keeps only shared items; a hit makes no call", async () => { const stub = testEnv.PUBLIC_DO.get(testEnv.PUBLIC_DO.newUniqueId()); await stub.setup({ webappId: WEBAPP_ID }); const before = await testEnv.WEBAPP.calls(); const miss = await stub.page(1, 24); expect(miss.cached).toBe(false); expect(miss.total).toBe(30); // diigo page 1 = items 1..24, of which 8 are private expect(parse(miss.items).map((i) => i.private)).toEqual(Array(16).fill(false)); const afterMiss = await testEnv.WEBAPP.calls(); expect(afterMiss).toBeGreaterThan(before); const hit = await stub.page(1, 24); expect(hit.cached).toBe(true); expect(hit.items).toEqual(miss.items); expect(await testEnv.WEBAPP.calls()).toBe(afterMiss); }); it("logs each page read as a hit or a miss (fetched vs kept), without the cache id", async () => { const spy = vi.spyOn(console, "log"); try { const id = testEnv.PUBLIC_DO.newUniqueId(); const stub = testEnv.PUBLIC_DO.get(id); await stub.setup({ webappId: WEBAPP_ID }); await stub.page(1, 24); await stub.page(1, 24); const cacheLogs = spy.mock.calls.map((c) => c[0]).filter((e) => e?.event === "cache"); expect(cacheLogs).toEqual([ { event: "cache", kind: "public", page: 1, outcome: "miss", fetched: 24, kept: 16 }, { event: "cache", kind: "public", page: 1, outcome: "hit", items: 16 }, ]); expect(JSON.stringify(spy.mock.calls)).not.toContain(id.toString()); } finally { spy.mockRestore(); } }); it("stores no private rows at all", async () => { const stub = testEnv.PUBLIC_DO.get(testEnv.PUBLIC_DO.newUniqueId()); await stub.setup({ webappId: WEBAPP_ID }); await stub.page(1, 24); await stub.page(2, 24); await runInDurableObject(stub, (_obj, state) => { const rows = state.storage.sql.exec<{ n: number }>("select count(*) as n from items where private = 1").toArray(); expect(rows[0].n).toBe(0); }); }); }); describe("eviction", () => { it("a cache miss leaves no undisposed RPC stubs to block eviction", async () => { const stub = testEnv.CACHE_DO.get(testEnv.CACHE_DO.newUniqueId()); await stub.setup({ webappId: WEBAPP_ID }); await stub.page(1, 24); await evictDurableObject(stub); expect((await stub.page(1, 24)).cached).toBe(true); }, 3000); }); describe("DiigoCache", () => { it("keeps every item of a page, each under its own link_id", async () => { const stub = testEnv.CACHE_DO.get(testEnv.CACHE_DO.newUniqueId()); await stub.setup({ webappId: WEBAPP_ID }); const page = await stub.page(1, 24); const ids = parse(page.items).map((i) => i.link_id); expect(ids).toHaveLength(24); expect(new Set(ids).size).toBe(24); expect(parse(page.items).filter((i) => i.private)).toHaveLength(8); }); }); describe("page replay order", () => { it("a hit replays diigo's fetch-time order (page_members), not a column sort", async () => { const stub = testEnv.CACHE_DO.get(testEnv.CACHE_DO.newUniqueId()); await stub.setup({ webappId: WEBAPP_ID }); const titles = (items: string[]) => parse(items).map((i) => i.title); const expected = Array.from({ length: 24 }, (_, k) => `bookmark ${k + 1}`); const miss = await stub.page(1, 24); expect(titles(miss.items)).toEqual(expected); // page 2's etch upserts other rows; page 1's replay must not shift await stub.page(2, 24); const hit = await stub.page(1, 24); expect(hit.cached).toBe(true); expect(titles(hit.items)).toEqual(expected); }); }); describe("worker routes", () => { const call = (path: string, init?: RequestInit) => worker.fetch(new Request(`https://diigo-cache.test${path}`, init), env as never); it("wires a public cache whose page fills itself on a miss", async () => { const wired = await call("/_wire", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ webappId: WEBAPP_ID, mode: "public" }), }); expect(wired.status).toBe(200); const { username } = (await wired.json()) as { username: string }; expect(username).toBe("alice_99"); // page 2 was never fetched by anyone: the public cache asks the webapp const html = await (await call(`/public/${username}?page=2`)).text(); expect(html).toContain('
cache: MISS"); const again = await (await call(`/public/${username}?page=2`)).text(); expect(again).toContain("cache: HIT"); expect(html).toContain("bookmark 25"); // the date comes first, in toISOString() form expect(html).toMatch(/