import { existsSync, readFileSync } from "node:fs"; import type { AddressInfo } from "node:net"; import type { Server } from "node:http"; import { join } from "node:path"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; import { parseTokens } from "../../content/src/config.ts"; import { createHttpServer } from "../../content/src/http.ts"; import { book, rig, type Rig } from "../../content/test/helpers.ts"; import { ContentApi } from "../src/client.ts"; import { createAdminServer } from "../src/server.ts"; const ADMIN = "d".repeat(24); let r: Rig; let content: Server; let admin: Server; let base: string; const listen = async (s: Server) => { await new Promise((ok) => s.listen(0, "127.0.0.1", ok)); return `http://127.0.0.1:${(s.address() as AddressInfo).port}`; }; beforeAll(async () => { r = rig(); r.put({ type: "status-kinds", title: "Status kinds", publish: true, kinds: [{ label: "Reading", icon: "book" }, { label: "Playing", icon: "dice" }] }); r.put( { type: "store-settings", title: "Store settings", publish: true, categories: [ { name: "Tabletop", ordersOpen: true, turnaround: "2 weeks" }, { name: "Paper", ordersOpen: false }, ], }, ); r.put({ type: "product-line", title: "Proxy cards", publish: true, category: "Tabletop", mode: "made to order", channel: "here", summary: "Cards.", slots: { open: 3, total: 5 }, priceTiers: [{ band: "1-10", price: "$1" }], }); r.put(book("Mistborn"), "Allomancy."); r.service.scan(); content = createHttpServer(r.service, { tokens: parseTokens(`admin:${ADMIN}`) }); const contentUrl = await listen(content); admin = createAdminServer(new ContentApi(contentUrl, ADMIN), { now: () => new Date("2026-10-11T12:00:00Z") }); base = await listen(admin); }); afterAll(() => { admin.close(); content.close(); r.done(); }); const get = async (path: string) => { const res = await fetch(base + path, { redirect: "manual" }); return { status: res.status, text: await res.text(), location: res.headers.get("location") }; }; const post = async (path: string, form: Record, headers: Record = {}) => { const res = await fetch(base + path, { method: "POST", redirect: "manual", headers: { "content-type": "application/x-www-form-urlencoded", "sec-fetch-site": "same-origin", ...headers }, body: new URLSearchParams(form).toString(), }); return { status: res.status, location: res.headers.get("location") ?? "" }; }; describe("admin page", () => { it("shows the overview", async () => { const page = await get("/"); expect(page.status).toBe(200); expect(page.text).toContain("live notes of 4"); expect(page.text).toContain("No sync job has reported yet"); }); it("refuses a form posted from another site", async () => { expect((await post("/scan", {}, { "sec-fetch-site": "cross-site" })).status).toBe(403); }); it("captures a Library item with a status, written into the vault", async () => { const res = await post("/capture/library", { title: "Warbreaker", medium: "book", state: "now", publish: "1", alsoStatus: "1", kind: "Reading" }); expect(res.location).toContain("msg="); expect(existsSync(join(r.site, "Library/Books/Warbreaker.md"))).toBe(true); const status = r.store.list({ type: "status" })[0]!; expect(status.data).toMatchObject({ kind: "Reading", subject: "[[Warbreaker]]", date: "2026-10-11" }); expect(status.author).toBe("admin"); }); it("sends a rejected capture back with the reasons", async () => { const res = await post("/capture/library", { title: "Warbreaker", medium: "book", state: "now" }); expect(res.location).toMatch(/err=.*r=/); }); it("reviews a held change with a diff and accepts it", async () => { const o = r.service.apply({ op: "update", path: "Library/Books/Mistborn.md", fields: { verdict: "A heist." } }, "agent"); expect(o.status).toBe("held"); const id = o.status === "held" ? o.held.id : 0; const list = await get("/review"); expect(list.text).toContain(`/review/${id}`); const one = await get(`/review/${id}`); expect(one.text).toContain('
verdict: A heist.
'); expect(one.text).toContain("agent-edit"); const res = await post(`/review/${id}/accept`, {}); expect(res.location).toContain("Accepted"); expect(readFileSync(join(r.site, "Library/Books/Mistborn.md"), "utf8")).toContain("verdict: A heist."); }); it("previews a note, shows its history and rolls it back", async () => { const page = await get("/note?path=Library%2FBooks%2FMistborn.md"); expect(page.text).toContain("

Allomancy.

"); expect(page.text).toContain("Compare and roll back"); const rev = await get("/note/revision?path=Library%2FBooks%2FMistborn.md&rev=1"); expect(rev.text).toContain('
verdict: A heist.
'); await post("/note/rollback", { path: "Library/Books/Mistborn.md", rev: "1" }); expect(r.store.get("Library/Books/Mistborn.md")!.data.verdict).toBeUndefined(); }); it("publishes a draft", async () => { r.put(book("Elantris", { publish: false })); r.service.scan(); expect((await get("/drafts")).text).toContain("Elantris"); await post("/note/publish", { path: "Library/Books/Elantris.md", publish: "1" }); expect(r.store.get("Library/Books/Elantris.md")!.publish).toBe(true); }); it("opens and closes orders and sets slots", async () => { expect((await get("/store")).text).toContain("Proxy cards"); await post("/store/category", { name: "Paper", ordersOpen: "1", turnaround: "3 weeks" }); await post("/store/category", { name: "Tabletop" }); const cats = r.store.get("Store/_settings.md")!.data.categories; expect(cats).toEqual([ { name: "Tabletop", ordersOpen: false }, { name: "Paper", ordersOpen: true, turnaround: "3 weeks" }, ]); await post("/store/stock", { path: "Store/Lines/Proxy cards.md", open: "1" }); expect(r.store.get("Store/Lines/Proxy cards.md")!.data.slots).toEqual({ open: 1, total: 5 }); }); it("shows sync jobs once they report", async () => { r.store.reportSync({ job: "personalreader", at: "2026-10-11T11:00:00Z", ok: false, message: "Suwayomi did not answer", counts: { updated: 0 } }); const page = await get("/"); expect(page.text).toContain("personalreader"); expect(page.text).toContain("Suwayomi did not answer"); }); });