Files
captoys/diigo-cache/tests/caches.test.ts
T
dckc 80c9279825 diigo-cache: test that a cached page replays fetch-time order
A hit must replay diigo's order as fetched (page_members.pos), not a
column sort. The fake webapp's link_id and updated_at are scrambled so
neither column sorts back into diigo's order; ordering the replay by
updated or item_id fails the new test.

Generated-by: Claude Opus 5.5
2026-10-07 04:19:54 -05:00

140 lines
5.4 KiB
TypeScript

import { env } from "cloudflare:workers";
import { runInDurableObject } from "cloudflare:test";
import { describe, expect, it, vi } from "vitest";
import worker from "../src/index.js";
import { toWebkey } from "../src/webkeys.js";
// matches tests/fake-webapp.js: 30 items, every third private
const WEBAPP_ID = "fake-webapp-id";
type TestEnv = {
CACHE_DO: DurableObjectNamespace<import("../src/cache.js").DiigoCache>;
PUBLIC_DO: DurableObjectNamespace<import("../src/cache.js").PublicCache>;
WEBAPP: { calls(): Promise<number> };
};
const testEnv = env as unknown as TestEnv;
const parse = (items: string[]) => items.map((j) => JSON.parse(j) as Record<string, unknown>);
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("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('<form method="get"');
expect(html).toContain("<strong>cache: MISS");
const again = await (await call(`/public/${username}?page=2`)).text();
expect(again).toContain("<strong>cache: HIT");
expect(html).toContain("bookmark 25");
expect(html).not.toContain("bookmark 27"); // private (27 % 3 === 0)
const json = (await (
await call(`/public/${username}?page=2`, { headers: { accept: "application/json" } })
).json()) as { pages: number; items: unknown[] };
expect(json.pages).toBe(2);
expect(json.items).toHaveLength(4); // 25..30 minus 27, 30
});
it("publishes a private cache later, without the cache id in the URL", async () => {
const cacheId = toWebkey(testEnv.CACHE_DO.newUniqueId());
const res = await call("/_publish", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ cacheId }),
});
// never wired: refused, not crashed
expect(res.status).toBe(404);
});
});