/** * TASK-0092 slice 2 — HttpDocStore unit tests. * * A real `node:http` server standing in for the API's internal endpoints * (`/internal/blocks/*`): the cursor-paginated read, the flush's CAS * success + conflict responses, and the ping semantics (404 = up, network * failure = down). No Docker, no Prisma, no real API — the store is * tested against the documented wire contract. */ import { createServer, type IncomingMessage, type Server, type ServerResponse } from "node:http"; import { AddressInfo } from "node:net"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; import { HttpDocStore } from "../src/http-store.js"; const TOKEN = "svc-token"; interface FakeState { /** All live blocks (position-ordered by `position`). */ blocks: Array<{ id: string; parentId: string | null; type: string; position: string; content: Record; properties?: Record; }>; version: number; /** The version a flush's baseVersion must match (the CAS base). */ casBase: number; requests: Array<{ method: string; path: string; auth: string | undefined }>; /** A set the flush must NOT clobber (the concurrent external state). */ external: Array | null; } function seedBlocks(n: number): FakeState["blocks"] { const out: FakeState["blocks"] = []; for (let i = 0; i < n; i += 1) { out.push({ id: `blk_${String(i).padStart(3, "0")}`, parentId: null, type: i % 2 === 0 ? "paragraph" : "heading_1", position: `p${String(i).padStart(4, "0")}`, content: { text: `block ${i}` }, }); } return out; } async function startFakeApi(state: FakeState): Promise<{ server: Server; url: string }> { const server = createServer((req: IncomingMessage, res: ServerResponse) => { state.requests.push({ method: req.method ?? "", path: req.url ?? "", auth: req.headers.authorization, }); const send = (status: number, body: unknown) => { res.writeHead(status, { "content-type": "application/json" }); res.end(JSON.stringify(body)); }; // Auth: the guard's contract (missing/mismatch → 401). if (req.headers.authorization !== `Bearer ${TOKEN}`) { return send(401, { error: { code: "UNAUTHORIZED", message: "bad token" } }); } if (req.method === "GET" && req.url !== null) { const u = new URL(req.url, "http://x"); const m = u.pathname.match(/^\/internal\/blocks\/([^/]+)$/); if (m) { const pageId = decodeURIComponent(m[1]); if (pageId !== "pg_1") { return send(404, { error: { code: "NOT_FOUND", message: "no page" } }); } const pageSize = Number(u.searchParams.get("pageSize") ?? "500"); const cursor = u.searchParams.get("cursor"); let rows = state.blocks; if (cursor !== null) rows = rows.filter((b) => b.position > (cursor as string)); const slice = rows.slice(0, pageSize); const hasMore = rows.length > pageSize; return send(200, { pageId, version: state.version, blocks: slice, nextCursor: hasMore ? slice[slice.length - 1].position : null, hasMore, }); } return send(404, { error: { code: "NOT_FOUND", message: "no route" } }); } if (req.method === "POST" && req.url === "/internal/blocks/flush") { let raw = ""; req.on("data", (c) => (raw += c)); req.on("end", () => { const body = JSON.parse(raw) as { pageId: string; baseVersion: number; blocks: FakeState["blocks"]; }; if (body.pageId !== "pg_1") { return send(404, { error: { code: "NOT_FOUND", message: "no page" } }); } if (body.baseVersion !== state.casBase) { // The conflict fast path (the room's rebase input). return send(200, { version: state.version, conflict: { externalBlocks: state.external ?? [] }, }); } state.blocks = body.blocks; state.version = state.casBase + 1; state.casBase = state.version; return send(200, { version: state.version }); }); return; } send(404, { error: { code: "NOT_FOUND", message: "no route" } }); }); await new Promise((r) => server.listen(0, "127.0.0.1", () => r())); const addr = server.address() as AddressInfo; return { server, url: `http://127.0.0.1:${addr.port}` }; } describe("HttpDocStore", () => { const state: FakeState = { blocks: seedBlocks(1200), version: 5, casBase: 5, requests: [], external: null, }; let server: Server; let url: string; let store: HttpDocStore; beforeAll(async () => { ({ server, url } = await startFakeApi(state)); store = new HttpDocStore({ apiUrl: url, serviceToken: TOKEN }); }); afterAll(async () => { await new Promise((r) => server.close(() => r())); }); it("hydrate: cursor-paginates a 1200-block page (3 × 500) in order", async () => { const result = await store.hydrate("pg_1"); expect(result.version).toBe(5); expect(result.blocks).not.toBeNull(); expect((result.blocks as unknown[]).length).toBe(1200); const first = (result.blocks as { id: string }[])[0]; const last = (result.blocks as { id: string }[])[1199]; expect(first.id).toBe("blk_000"); expect(last.id).toBe("blk_1199"); // Strictly position-ordered across pages (the cursor contract — the // `position >` comparison is lexicographic on the zero-padded strings). const positions = (result.blocks as { position: string }[]).map((b) => b.position); for (let i = 1; i < positions.length; i += 1) { expect(positions[i] > positions[i - 1]).toBe(true); } // Authenticated every page. const reads = state.requests.filter((r) => r.path.startsWith("/internal/blocks/pg_1")); expect(reads).toHaveLength(3); expect(reads.every((r) => r.auth === `Bearer ${TOKEN}`)).toBe(true); }); it("hydrate: an empty page → null blocks (the DocStore empty sentinel)", async () => { const saved = state.blocks; state.blocks = []; try { const result = await store.hydrate("pg_1"); expect(result.blocks).toBeNull(); } finally { state.blocks = saved; } }); it("hydrate: 404 page → error (no silent empty doc)", async () => { await expect(store.hydrate("pg_missing")).rejects.toThrow(/404/); }); it("persist: success — the full set is flushed, the new version returned", async () => { const before = state.version; const result = await store.persist("pg_1", state.blocks, state.casBase); expect(result.conflict).toBeUndefined(); expect(result.version).toBe(before + 1); expect(state.version).toBe(before + 1); }); it("persist: stale baseVersion → conflict with the external blocks (room rebase)", async () => { state.external = [{ id: "ext_1", parentId: null, type: "paragraph", position: "z9", content: { text: "external" } }]; try { const result = await store.persist("pg_1", state.blocks, 1 /* stale */); expect(result.version).toBe(state.version); expect(result.conflict?.externalBlocks).toEqual(state.external); // The clobber did NOT happen (version unchanged from the last flush). expect(state.version).toBe(6); } finally { state.external = null; } }); it("ping: any HTTP answer (404) = up; a rejected auth still = up (the API is reachable)", async () => { expect(await store.ping()).toBe(true); }); it("ping: network failure = down", async () => { const down = new HttpDocStore({ apiUrl: "http://127.0.0.1:1", serviceToken: TOKEN, timeoutMs: 500 }); expect(await down.ping()).toBe(false); }); it("wrong token → 401 surfaces as a readable error (no token leak)", async () => { const bad = new HttpDocStore({ apiUrl: url, serviceToken: "wrong" }); await expect(bad.hydrate("pg_1")).rejects.toThrow(/401/); }); });