/** * Every door answers as the table says. The test acts as each identity the * contract declares — on a note 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 { addNote, deleteNote, getAudit, getNote, listAudit, listNotes, updateNote, } 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 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 }; } const noteBy = (authorId: string): number => db .query< { id: number }, [string] >("INSERT INTO note (body, author_id, author_name) VALUES ('n', ?1, ?1) RETURNING id") .get(authorId)!.id; const auditRow = (): number => { record(db, { who: { role: "anon" }, resource: "note", 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>> > = { note: { create: (who) => addNote(db, who, { body: "hello" }).ok, list: (who) => listNotes(db, who).ok, read: (who, authorId) => getNote(db, who, { id: noteBy(authorId) }).ok, update: (who, authorId) => updateNote(db, who, { id: noteBy(authorId), body: "changed" }).ok, delete: (who, authorId) => deleteNote(db, who, { id: noteBy(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); });