wren/glyph Public Public: anyone can read this; nobody can change it.
forked from apps/glyph
workbenchthis machine
Files main
| 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 | }); |