benjivers/notes Public Public: anyone can read this; nobody can change it.

Files 7260e8fc5b5c3ca9332ff3506ef97626f456e658
118 lines · 3.9 KB · ~1.1k tok Raw History
1
/**
2
 * Every door answers as the table says. The test acts as each identity the
3
 * contract declares — on a note that names them and on one that names
4
 * someone else — and calls the real actions. A rule written by hand in an
5
 * action, or a cell changed without its door, fails here.
6
 */
7
import { expect, test } from "bun:test";
8
import { mkdtempSync } from "node:fs";
9
import { tmpdir } from "node:os";
10
import { join } from "node:path";
11
import {
12
  addNote,
13
  deleteNote,
14
  getAudit,
15
  getNote,
16
  listAudit,
17
  listNotes,
18
  updateNote,
19
} from "./actions.ts";
20
import { record } from "./audit.ts";
21
import { actingAs, identities, table, type Verb } from "./can.ts";
22
import { contract } from "./contract.ts";
23
import { openDb } from "./db.ts";
24
import type { Who } from "./who.ts";
25
26
const db = openDb(mkdtempSync(join(tmpdir(), "app-contract-")));
27
28
/** The identity the evaluator acts as, as the forge would have signed it. */
29
function whoIs(identity: string): Who {
30
  const { actor } = actingAs(contract, identity);
31
  const role = actor.roles[0];
32
  if (actor.id === null || (role !== "user" && role !== "admin"))
33
    return { role: "anon" };
34
  return { role, id: actor.id, name: identity };
35
}
36
37
let made = 0;
38
const noteBy = (authorId: string): number =>
39
  db
40
    .query<
41
      { id: number },
42
      [string, string]
43
    >("INSERT INTO note (title, body, author_id, author_name) VALUES (?2, 'n', ?1, ?1) RETURNING id")
44
    .get(authorId, `n${++made}`)!.id;
45
46
const auditRow = (): number => {
47
  record(db, {
48
    who: { role: "anon" },
49
    resource: "note",
50
    verb: "create",
51
    outcome: "signin_required",
52
  });
53
  return db.query<{ id: number }, []>("SELECT max(id) AS id FROM audit").get()!
54
    .id;
55
};
56
57
/** One door per verb an action serves: did it let `who` through, on `row`? */
58
const doors: Record<
59
  string,
60
  Partial<Record<Verb, (who: Who, authorId: string) => boolean>>
61
> = {
62
  note: {
63
    create: (who) => addNote(db, who, { body: "hello" }).ok,
64
    list: (who) => listNotes(db, who).ok,
65
    read: (who, authorId) => getNote(db, who, { id: noteBy(authorId) }).ok,
66
    update: (who, authorId) =>
67
      updateNote(db, who, { id: noteBy(authorId), body: "changed" }).ok,
68
    delete: (who, authorId) => deleteNote(db, who, { id: noteBy(authorId) }).ok,
69
  },
70
  // The trail is written by the app itself, never through a door: there
71
  // is no action that creates, changes or removes a row of it.
72
  audit: {
73
    list: (who) => listAudit(db, who).ok,
74
    read: (who) => getAudit(db, who, { id: auditRow() }).ok,
75
    create: () => false,
76
    update: () => false,
77
    delete: () => false,
78
  },
79
};
80
81
test("the contract names the identities the forge signs", () => {
82
  expect(identities(contract).sort()).toEqual(["admin", "anon", "user"]);
83
});
84
85
test("every resource in the contract has every door here", () => {
86
  expect(Object.keys(doors).sort()).toEqual(
87
    Object.keys(contract.resources).sort(),
88
  );
89
  for (const resource of Object.keys(contract.resources))
90
    expect(Object.keys(doors[resource] ?? {}).sort()).toEqual([
91
      "create",
92
      "delete",
93
      "list",
94
      "read",
95
      "update",
96
    ]);
97
});
98
99
test("every door answers as the table says, as every identity", () => {
100
  let asked = 0;
101
  for (const cell of table(contract)) {
102
    const door = doors[cell.resource]?.[cell.verb];
103
    if (door === undefined) continue;
104
    const who = whoIs(cell.as);
105
    const { mine, theirs } = actingAs(contract, cell.as);
106
    const onMine = door(who, String(mine["author_id"]));
107
    const onTheirs = door(who, String(theirs["author_id"]));
108
    const said = `${cell.resource}.${cell.verb} as ${cell.as}`;
109
    asked++;
110
    if (cell.answer === "yes")
111
      expect([said, onMine, onTheirs]).toEqual([said, true, true]);
112
    else if (cell.answer === "no")
113
      expect([said, onMine, onTheirs]).toEqual([said, false, false]);
114
    else if (cell.verb === "list") expect([said, onMine]).toEqual([said, true]);
115
    else expect([said, onMine, onTheirs]).toEqual([said, true, false]);
116
  }
117
  expect(asked).toBeGreaterThan(0);
118
});
↑↓ navigate ↵ open esc close
↑↓ navigate ↵ open esc close

Keyboard shortcuts