/** * Every door answers as the table says. The test acts as each identity the * contract declares — on a drawing that names them and on one that names * someone else — and calls the real actions. A rule written by hand in an * action, or a cell changed without its door, fails here. */ import { expect, test } from "bun:test"; import { mkdtempSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { deleteDrawing, getAudit, getDrawing, listAudit, listDrawings, startDrawing, } from "./actions.ts"; import { record } from "./audit.ts"; import { actingAs, identities, table, type Verb } from "./can.ts"; import { contract } from "./contract.ts"; import { openDb } from "./db.ts"; import { setDrawer } from "./draw.ts"; import type { Who } from "./who.ts"; const db = openDb(mkdtempSync(join(tmpdir(), "app-contract-"))); /** The identity the evaluator acts as, as the forge would have signed it. */ function whoIs(identity: string): Who { const { actor } = actingAs(contract, identity); const role = actor.roles[0]; if (actor.id === null || (role !== "user" && role !== "admin")) return { role: "anon" }; return { role, id: actor.id, name: identity }; } // Drawings finish at once here; a row made by hand is already done. setDrawer(async () => []); const drawingBy = (authorId: string): number => db .query< { id: number }, [string] >("INSERT INTO drawing (prompt, kind, status, author_id, author_name) VALUES ('p', 'mark', 'done', ?1, ?1) RETURNING id") .get(authorId)!.id; /** Each create is a new person, so "one at a time" never answers for the table. */ let fresh = 0; const auditRow = (): number => { record(db, { who: { role: "anon" }, resource: "drawing", verb: "create", outcome: "signin_required", }); return db.query<{ id: number }, []>("SELECT max(id) AS id FROM audit").get()! .id; }; /** One door per verb an action serves: did it let `who` through, on `row`? */ const doors: Record< string, Partial boolean>> > = { drawing: { create: (who) => { const made = startDrawing(db, who.role === "anon" ? who : { ...who, id: `${who.id}-${++fresh}` }, { prompt: "hello" }); // Settled at once, so the cap on drawings in flight never answers for the table. db.exec("UPDATE drawing SET status = 'done' WHERE status = 'drawing'"); return made.ok; }, list: (who) => listDrawings(db, who).ok, read: (who, authorId) => getDrawing(db, who, { id: drawingBy(authorId) }).ok, // There is no action that changes a drawing: Claude's answer is kept as it came. update: () => false, delete: (who, authorId) => deleteDrawing(db, who, { id: drawingBy(authorId) }).ok, }, // The trail is written by the app itself, never through a door: there // is no action that creates, changes or removes a row of it. audit: { list: (who) => listAudit(db, who).ok, read: (who) => getAudit(db, who, { id: auditRow() }).ok, create: () => false, update: () => false, delete: () => false, }, }; test("the contract names the identities the forge signs", () => { expect(identities(contract).sort()).toEqual(["admin", "anon", "user"]); }); test("every resource in the contract has every door here", () => { expect(Object.keys(doors).sort()).toEqual( Object.keys(contract.resources).sort(), ); for (const resource of Object.keys(contract.resources)) expect(Object.keys(doors[resource] ?? {}).sort()).toEqual([ "create", "delete", "list", "read", "update", ]); }); test("every door answers as the table says, as every identity", () => { let asked = 0; for (const cell of table(contract)) { const door = doors[cell.resource]?.[cell.verb]; if (door === undefined) continue; const who = whoIs(cell.as); const { mine, theirs } = actingAs(contract, cell.as); const onMine = door(who, String(mine["author_id"])); const onTheirs = door(who, String(theirs["author_id"])); const said = `${cell.resource}.${cell.verb} as ${cell.as}`; asked++; if (cell.answer === "yes") expect([said, onMine, onTheirs]).toEqual([said, true, true]); else if (cell.answer === "no") expect([said, onMine, onTheirs]).toEqual([said, false, false]); else if (cell.verb === "list") expect([said, onMine]).toEqual([said, true]); else expect([said, onMine, onTheirs]).toEqual([said, true, false]); } expect(asked).toBeGreaterThan(0); });