import { env } from "cloudflare:workers"; import { evictDurableObject, runDurableObjectAlarm, runInDurableObject } from "cloudflare:test"; import { describe, expect, it, vi } from "vitest"; import worker from "../src/index.js"; import { toWebkey } from "../src/webkeys.js"; // goal 1e (capsite checklist): a job that grabs every page. Fake // accounts: tests/fake-webapp.js ("fake-webapp-id": 30 items = 2 pages // of 24, every 3rd private). type TestEnv = { CACHE_DO: DurableObjectNamespace; PUBLIC_DO: DurableObjectNamespace; WEBAPP: { calls(): Promise }; }; const testEnv = env as unknown as TestEnv; const wired = async (webappId = "fake-webapp-id") => { const stub = testEnv.CACHE_DO.get(testEnv.CACHE_DO.newUniqueId()); await stub.setup({ webappId }); return stub; }; /** run alarms until none is scheduled (bounded, so a runaway job fails) */ const drain = async (stub: DurableObjectStub, max = 20) => { let ran = 0; while (ran < max && (await runDurableObjectAlarm(stub))) ran += 1; expect(ran).toBeLessThan(max); return ran; }; describe("fill job (goal 1e)", () => { it("fills every page; afterwards every page is a hit with no webapp call", async () => { const stub = await wired(); const atStart = await testEnv.WEBAPP.calls(); expect((await stub.startFill()).state).toBe("running"); await drain(stub); const status = await stub.fillStatus(); expect(status).toMatchObject({ state: "done", pages: 2, fetched: 2 }); // one diigo call per page fetched (no extra page-1 fetch per miss) expect((await testEnv.WEBAPP.calls()) - atStart).toBe(2); const before = await testEnv.WEBAPP.calls(); for (const n of [1, 2]) expect((await stub.page(n, 24)).cached).toBe(true); expect(await testEnv.WEBAPP.calls()).toBe(before); }); it("advances pagesPerStep pages per alarm, with progress readable between steps", async () => { const stub = await wired(); await stub.startFill({ pagesPerStep: 1 }); expect(await runDurableObjectAlarm(stub)).toBe(true); expect(await stub.fillStatus()).toMatchObject({ state: "running", nextPage: 2, pages: 2 }); await drain(stub); expect((await stub.fillStatus())?.state).toBe("done"); }); it("survives eviction: progress is in storage, not memory", async () => { const stub = await wired(); await stub.startFill({ pagesPerStep: 1 }); await runDurableObjectAlarm(stub); await evictDurableObject(stub); expect(await stub.fillStatus()).toMatchObject({ state: "running", nextPage: 2 }); await drain(stub); expect((await stub.fillStatus())?.state).toBe("done"); }); it("starting again while running rejoins the same job", async () => { const stub = await wired(); const first = await stub.startFill({ pagesPerStep: 1 }); await runDurableObjectAlarm(stub); const again = await stub.startFill({ pagesPerStep: 1 }); expect(again.startedAt).toBe(first.startedAt); expect(again.nextPage).toBe(2); }); it("skips pages already cached", async () => { const stub = await wired(); await stub.page(1, 24); await stub.startFill(); await drain(stub); expect(await stub.fillStatus()).toMatchObject({ state: "done", fetched: 1 }); }); it("stops when asked; cached pages stay", async () => { const stub = await wired(); await stub.startFill({ pagesPerStep: 1 }); await runDurableObjectAlarm(stub); expect((await stub.stopFill()).state).toBe("stopped"); expect(await runDurableObjectAlarm(stub)).toBe(false); expect((await stub.page(1, 24)).cached).toBe(true); }); it("a webapp failure fails the job loudly, once: status says why, the log too", async () => { const spy = vi.spyOn(console, "error"); try { const stub = await wired("no-such-webapp"); await stub.startFill(); await drain(stub); const status = await stub.fillStatus(); expect(status?.state).toBe("failed"); expect(status?.error).toMatch(/unknown id/); expect(spy.mock.calls.map((c) => c[0]).filter((e) => e?.event === "fill")).not.toHaveLength(0); } finally { spy.mockRestore(); } }); it("a public cache's fill stores no private rows", async () => { const stub = testEnv.PUBLIC_DO.get(testEnv.PUBLIC_DO.newUniqueId()); await stub.setup({ webappId: "fake-webapp-id" }); await stub.startFill(); await drain(stub); 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("fill routes (goal 1e)", () => { const call = (path: string, init?: RequestInit) => worker.fetch(new Request(`https://diigo-cache.test${path}`, init), env as never); const post = (body: Record) => call("/_fill", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) }); it("POST /_fill starts the owner's job (cache id in the body) and answers 202 with its status", async () => { const id = testEnv.CACHE_DO.newUniqueId(); await testEnv.CACHE_DO.get(id).setup({ webappId: "fake-webapp-id" }); const res = await post({ cacheId: toWebkey(id) }); expect(res.status).toBe(202); expect(await res.json()).toMatchObject({ state: "running" }); }); it("target=public fills the cache's public companion; action=stop stops", async () => { const wiredRes = await call("/_wire", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ webappId: "fake-webapp-id", mode: "public" }), }); const { cacheId } = (await wiredRes.json()) as { cacheId: string }; expect((await post({ cacheId, target: "public" })).status).toBe(202); const stopped = await post({ cacheId, target: "public", action: "stop" }); expect(await stopped.json()).toMatchObject({ state: "stopped" }); }); it("refuses an unknown cache id (404) and never takes the id from the URL", async () => { expect((await post({ cacheId: toWebkey(testEnv.CACHE_DO.newUniqueId()) })).status).toBe(404); expect((await call(`/_fill/${toWebkey(testEnv.CACHE_DO.newUniqueId())}`, { method: "POST" })).status).toBe(404); }); it("the created page offers a fetch-everything button, the id in a hidden field", async () => { const res = await call("/_wire", { method: "POST", headers: { "content-type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ webappId: "fake-webapp-id", mode: "private" }), }); const html = await res.text(); expect(html).toMatch(/
/); expect(html).toMatch(/ { const wiredRes = await call("/_wire", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ webappId: "fake-webapp-id", mode: "public" }), }); const { cacheId, username } = (await wiredRes.json()) as { cacheId: string; username: string }; await post({ cacheId, target: "public" }); const html = await (await call(`/public/${username}`)).text(); expect(html).toMatch(/fill(ed|ing)[^<]*\d+ of \d+ pages/); }); });