import { expect, test } from "bun:test"; import { mkdtempSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { AT_ONCE, canDelete, deleteDrawing, getDrawing, listDrawings, startDrawing, svgsOf, } from "./actions.ts"; import { openDb } from "./db.ts"; import { recover, setDrawer, settled } from "./draw.ts"; import type { Who } from "./who.ts"; const db = openDb(mkdtempSync(join(tmpdir(), "app-actions-"))); const anon: Who = { role: "anon" }; const alice: Who = { role: "user", id: "u1", name: "alice" }; const bob: Who = { role: "user", id: "u2", name: "bob" }; const root: Who = { role: "admin", id: "u0", name: "root" }; const SVG = ``; setDrawer(async () => [SVG]); test("only a signed-in person draws, as themselves; the drawing fills in after", async () => { const refused = startDrawing(db, anon, { prompt: "hello" }); expect(refused.ok).toBe(false); if (!refused.ok) expect(refused.error).toBe("signin_required"); const started = startDrawing(db, alice, { prompt: " a fox " }); expect(started.ok).toBe(true); if (!started.ok) return; expect(started.value).toMatchObject({ prompt: "a fox", kind: "mark", status: "drawing", author_id: "u1" }); await settled(); const done = getDrawing(db, alice, { id: started.value.id }); expect(done.ok && done.value.status).toBe("done"); expect(done.ok && svgsOf(done.value)).toEqual([SVG]); }); test("a prompt is 1 to 400 characters and a kind is one of four", () => { for (const input of [{ prompt: "" }, { prompt: " " }, { prompt: "x".repeat(401) }, { prompt: "ok", kind: "poster" }]) { const r = startDrawing(db, alice, input); expect(r.ok).toBe(false); if (!r.ok) expect(r.error).toBe("invalid"); } const icon = startDrawing(db, bob, { prompt: "ok", kind: "icon" }); expect(icon.ok && icon.value.kind).toBe("icon"); }); test("drawings are their asker's: listed, read and removed by them or an admin", async () => { await settled(); const mine = startDrawing(db, alice, { prompt: "mine" }); if (!mine.ok) throw new Error(mine.message); await settled(); const bobs = listDrawings(db, bob); expect(bobs.ok && bobs.value.every((d) => d.author_id === "u2")).toBe(true); expect(listDrawings(db, anon).ok).toBe(false); const all = listDrawings(db, root); expect(all.ok && all.value.some((d) => d.author_id === "u1") && all.value.some((d) => d.author_id === "u2")).toBe(true); const peek = getDrawing(db, bob, { id: mine.value.id }); expect(!peek.ok && peek.error).toBe("forbidden"); expect(canDelete(bob, mine.value)).toBe(false); expect(canDelete(alice, mine.value)).toBe(true); const byBob = deleteDrawing(db, bob, { id: mine.value.id }); expect(!byBob.ok && byBob.error).toBe("forbidden"); expect(deleteDrawing(db, root, { id: mine.value.id }).ok).toBe(true); const gone = deleteDrawing(db, alice, { id: mine.value.id }); expect(!gone.ok && gone.error).toBe("not_found"); const odd = deleteDrawing(db, root, { id: 1.5 }); expect(!odd.ok && odd.error).toBe("invalid"); }); test("one drawing at a time per person, and a few at once for everyone", async () => { await settled(); const waits: (() => void)[] = []; setDrawer(() => new Promise((resolve) => waits.push(() => resolve([SVG])))); try { expect(startDrawing(db, alice, { prompt: "one" }).ok).toBe(true); const again = startDrawing(db, alice, { prompt: "two" }); expect(!again.ok && again.error).toBe("busy"); for (let i = 1; i < AT_ONCE; i++) expect(startDrawing(db, { role: "user", id: `x${i}`, name: `x${i}` }, { prompt: "p" }).ok).toBe(true); const full = startDrawing(db, bob, { prompt: "three" }); expect(!full.ok && full.message).toContain("others"); } finally { for (const go of waits) go(); await settled(); setDrawer(async () => [SVG]); } expect(startDrawing(db, alice, { prompt: "after" }).ok).toBe(true); await settled(); }); test("a drawing a stopped process left half done is failed, not waited on forever", () => { const row = db .query<{ id: number }, []>("INSERT INTO drawing (prompt, kind, author_id, author_name) VALUES ('p', 'mark', 'u1', 'alice') RETURNING id") .get()!; expect(recover(db)).toBe(1); const after = getDrawing(db, alice, { id: row.id }); expect(after.ok && after.value.status).toBe("failed"); expect(after.ok && after.value.error).toContain("restarted"); });