| 1 |
import { expect, test } from "bun:test"; |
| 2 |
import { mkdtempSync } from "node:fs"; |
| 3 |
import { tmpdir } from "node:os"; |
| 4 |
import { join } from "node:path"; |
| 5 |
import { |
| 6 |
AT_ONCE, |
| 7 |
canDelete, |
| 8 |
deleteDrawing, |
| 9 |
getDrawing, |
| 10 |
listDrawings, |
| 11 |
startDrawing, |
| 12 |
svgsOf, |
| 13 |
} from "./actions.ts"; |
| 14 |
import { openDb } from "./db.ts"; |
| 15 |
import { recover, setDrawer, settled } from "./draw.ts"; |
| 16 |
import type { Who } from "./who.ts"; |
| 17 |
|
| 18 |
const db = openDb(mkdtempSync(join(tmpdir(), "app-actions-"))); |
| 19 |
const anon: Who = { role: "anon" }; |
| 20 |
const alice: Who = { role: "user", id: "u1", name: "alice" }; |
| 21 |
const bob: Who = { role: "user", id: "u2", name: "bob" }; |
| 22 |
const root: Who = { role: "admin", id: "u0", name: "root" }; |
| 23 |
const SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><circle cx="32" cy="32" r="9"/></svg>`; |
| 24 |
setDrawer(async () => [SVG]); |
| 25 |
|
| 26 |
test("only a signed-in person draws, as themselves; the drawing fills in after", async () => { |
| 27 |
const refused = startDrawing(db, anon, { prompt: "hello" }); |
| 28 |
expect(refused.ok).toBe(false); |
| 29 |
if (!refused.ok) expect(refused.error).toBe("signin_required"); |
| 30 |
const started = startDrawing(db, alice, { prompt: " a fox " }); |
| 31 |
expect(started.ok).toBe(true); |
| 32 |
if (!started.ok) return; |
| 33 |
expect(started.value).toMatchObject({ prompt: "a fox", kind: "mark", status: "drawing", author_id: "u1" }); |
| 34 |
await settled(); |
| 35 |
const done = getDrawing(db, alice, { id: started.value.id }); |
| 36 |
expect(done.ok && done.value.status).toBe("done"); |
| 37 |
expect(done.ok && svgsOf(done.value)).toEqual([SVG]); |
| 38 |
}); |
| 39 |
|
| 40 |
test("a prompt is 1 to 400 characters and a kind is one of four", () => { |
| 41 |
for (const input of [{ prompt: "" }, { prompt: " " }, { prompt: "x".repeat(401) }, { prompt: "ok", kind: "poster" }]) { |
| 42 |
const r = startDrawing(db, alice, input); |
| 43 |
expect(r.ok).toBe(false); |
| 44 |
if (!r.ok) expect(r.error).toBe("invalid"); |
| 45 |
} |
| 46 |
const icon = startDrawing(db, bob, { prompt: "ok", kind: "icon" }); |
| 47 |
expect(icon.ok && icon.value.kind).toBe("icon"); |
| 48 |
}); |
| 49 |
|
| 50 |
test("drawings are their asker's: listed, read and removed by them or an admin", async () => { |
| 51 |
await settled(); |
| 52 |
const mine = startDrawing(db, alice, { prompt: "mine" }); |
| 53 |
if (!mine.ok) throw new Error(mine.message); |
| 54 |
await settled(); |
| 55 |
const bobs = listDrawings(db, bob); |
| 56 |
expect(bobs.ok && bobs.value.every((d) => d.author_id === "u2")).toBe(true); |
| 57 |
expect(listDrawings(db, anon).ok).toBe(false); |
| 58 |
const all = listDrawings(db, root); |
| 59 |
expect(all.ok && all.value.some((d) => d.author_id === "u1") && all.value.some((d) => d.author_id === "u2")).toBe(true); |
| 60 |
const peek = getDrawing(db, bob, { id: mine.value.id }); |
| 61 |
expect(!peek.ok && peek.error).toBe("forbidden"); |
| 62 |
expect(canDelete(bob, mine.value)).toBe(false); |
| 63 |
expect(canDelete(alice, mine.value)).toBe(true); |
| 64 |
const byBob = deleteDrawing(db, bob, { id: mine.value.id }); |
| 65 |
expect(!byBob.ok && byBob.error).toBe("forbidden"); |
| 66 |
expect(deleteDrawing(db, root, { id: mine.value.id }).ok).toBe(true); |
| 67 |
const gone = deleteDrawing(db, alice, { id: mine.value.id }); |
| 68 |
expect(!gone.ok && gone.error).toBe("not_found"); |
| 69 |
const odd = deleteDrawing(db, root, { id: 1.5 }); |
| 70 |
expect(!odd.ok && odd.error).toBe("invalid"); |
| 71 |
}); |
| 72 |
|
| 73 |
test("one drawing at a time per person, and a few at once for everyone", async () => { |
| 74 |
await settled(); |
| 75 |
const waits: (() => void)[] = []; |
| 76 |
setDrawer(() => new Promise((resolve) => waits.push(() => resolve([SVG])))); |
| 77 |
try { |
| 78 |
expect(startDrawing(db, alice, { prompt: "one" }).ok).toBe(true); |
| 79 |
const again = startDrawing(db, alice, { prompt: "two" }); |
| 80 |
expect(!again.ok && again.error).toBe("busy"); |
| 81 |
for (let i = 1; i < AT_ONCE; i++) |
| 82 |
expect(startDrawing(db, { role: "user", id: `x${i}`, name: `x${i}` }, { prompt: "p" }).ok).toBe(true); |
| 83 |
const full = startDrawing(db, bob, { prompt: "three" }); |
| 84 |
expect(!full.ok && full.message).toContain("others"); |
| 85 |
} finally { |
| 86 |
for (const go of waits) go(); |
| 87 |
await settled(); |
| 88 |
setDrawer(async () => [SVG]); |
| 89 |
} |
| 90 |
expect(startDrawing(db, alice, { prompt: "after" }).ok).toBe(true); |
| 91 |
await settled(); |
| 92 |
}); |
| 93 |
|
| 94 |
test("a drawing a stopped process left half done is failed, not waited on forever", () => { |
| 95 |
const row = db |
| 96 |
.query<{ id: number }, []>("INSERT INTO drawing (prompt, kind, author_id, author_name) VALUES ('p', 'mark', 'u1', 'alice') RETURNING id") |
| 97 |
.get()!; |
| 98 |
expect(recover(db)).toBe(1); |
| 99 |
const after = getDrawing(db, alice, { id: row.id }); |
| 100 |
expect(after.ok && after.value.status).toBe("failed"); |
| 101 |
expect(after.ok && after.value.error).toContain("restarted"); |
| 102 |
}); |