wren/glyph Public Public: anyone can read this; nobody can change it.

forked from apps/glyph

workbenchthis machine
Files acd87b6b6146da11e0c9272c92a02a8e707adf4a
342 lines · 11.2 KB · ~3.3k tok Raw History
1
import { afterAll, 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 { env } from "./env.ts";
6
import { serve } from "./server.ts";
7
import { sign } from "./who.ts";
8
9
const KEY = "a-key-only-the-forge-holds";
10
const server = serve({
11
  port: 0,
12
  dataDir: mkdtempSync(join(tmpdir(), "app-")),
13
  identityKey: KEY,
14
});
15
const base = `http://${server.hostname}:${server.port}`;
16
afterAll(() => server.stop(true));
17
18
/** The headers the forge sends for a signed-in person. */
19
const as = (
20
  id: string,
21
  name: string,
22
  role: "user" | "admin" = "user",
23
): Record<string, string> => {
24
  const identity = `${id}|${name}|${role}|${Date.now() + 60_000}`;
25
  return {
26
    "x-forge-identity": identity,
27
    "x-forge-signature": sign(identity, KEY),
28
  };
29
};
30
const alice = as("u1", "alice");
31
const bob = as("u2", "bob");
32
const root = as("u0", "root", "admin");
33
const jsonBody = (body: unknown): RequestInit => ({
34
  headers: { "content-type": "application/json" },
35
  body: JSON.stringify(body),
36
});
37
const api = (
38
  path: string,
39
  init: RequestInit = {},
40
  who: Record<string, string> = {},
41
) =>
42
  fetch(base + path, {
43
    ...init,
44
    headers: { ...(init.headers as Record<string, string>), ...who },
45
  });
46
const rpc = (message: unknown, who: Record<string, string> = {}) =>
47
  api("/mcp", { method: "POST", ...jsonBody(message) }, who).then(
48
    (r) => r.json() as Promise<any>,
49
  );
50
51
test("the page knows who you are and offers the right door", async () => {
52
  const guest = await (await fetch(base + "/")).text();
53
  expect(guest).toContain("<h1>");
54
  expect(guest).toContain("/__forge/signin?to=/");
55
  expect(guest).toContain('href="/__forge/report"');
56
  expect(guest).not.toContain('action="/notes"');
57
  const signed = await (await api("/", {}, alice)).text();
58
  expect(signed).toContain("<b>alice</b>");
59
  expect(signed).toContain("/__forge/signout");
60
  expect(signed).toContain('action="/notes"');
61
});
62
63
test("a form post adds as the person; a guest is sent to sign in", async () => {
64
  const form = new FormData();
65
  form.set("body", "hello from a form");
66
  const refused = await api("/notes", {
67
    method: "POST",
68
    body: form,
69
    redirect: "manual",
70
  });
71
  expect(refused.status).toBe(303);
72
  expect(refused.headers.get("set-cookie")).toContain("notice=");
73
  expect(refused.headers.get("set-cookie")).toContain("SameSite=Lax");
74
  expect(await (await fetch(base + "/")).text()).not.toContain(
75
    "hello from a form",
76
  );
77
  const posted = await api(
78
    "/notes",
79
    { method: "POST", body: form, redirect: "manual" },
80
    alice,
81
  );
82
  expect(posted.status).toBe(303);
83
  expect(posted.headers.get("set-cookie")).toBeNull();
84
  const page = await (await api("/", {}, alice)).text();
85
  expect(page).toContain("hello from a form");
86
  expect(page).toContain("alice ·");
87
});
88
89
test("the JSON API is the same actions with the same identity", async () => {
90
  const created = await api(
91
    "/api/notes",
92
    { method: "POST", ...jsonBody({ body: "over the api" }) },
93
    alice,
94
  );
95
  expect(created.status).toBe(201);
96
  const note = (await created.json()) as { id: number; author_name: string };
97
  expect(note.author_name).toBe("alice");
98
  const listed = (await (await fetch(base + "/api/notes")).json()) as {
99
    id: number;
100
  }[];
101
  expect(listed.map((n) => n.id)).toContain(note.id);
102
  expect(
103
    (await api("/api/notes", { method: "POST", ...jsonBody({ body: "anon" }) }))
104
      .status,
105
  ).toBe(401);
106
  expect(
107
    (await api(`/api/notes/${note.id}`, { method: "DELETE" }, bob)).status,
108
  ).toBe(403);
109
  expect(
110
    (await api(`/api/notes/${note.id}`, { method: "DELETE" }, alice)).status,
111
  ).toBe(200);
112
  expect(
113
    (await api(`/api/notes/${note.id}`, { method: "DELETE" }, alice)).status,
114
  ).toBe(404);
115
  expect(
116
    (
117
      await api(
118
        "/api/notes",
119
        { method: "POST", ...jsonBody({ body: "" }) },
120
        alice,
121
      )
122
    ).status,
123
  ).toBe(422);
124
});
125
126
test("the MCP server lists the tools and calls them as the caller", async () => {
127
  const hello = await rpc({
128
    jsonrpc: "2.0",
129
    id: 1,
130
    method: "initialize",
131
    params: {},
132
  });
133
  expect(hello.result.capabilities).toEqual({ tools: {} });
134
  const listed = await rpc({ jsonrpc: "2.0", id: 2, method: "tools/list" });
135
  expect(listed.result.tools.map((t: { name: string }) => t.name)).toEqual([
136
    "list_notes",
137
    "add_note",
138
    "delete_note",
139
  ]);
140
  const call = (name: string, args: unknown, who = {}) =>
141
    rpc(
142
      {
143
        jsonrpc: "2.0",
144
        id: 3,
145
        method: "tools/call",
146
        params: { name, arguments: args },
147
      },
148
      who,
149
    );
150
  const refused = await call("add_note", { body: "as nobody" });
151
  expect(refused.result.isError).toBe(true);
152
  const added = await call("add_note", { body: "over mcp" }, root);
153
  expect(added.result.isError).toBeUndefined();
154
  const note = JSON.parse(added.result.content[0].text);
155
  expect(note.author_name).toBe("root");
156
  const notes = JSON.parse(
157
    (await call("list_notes", {})).result.content[0].text,
158
  );
159
  expect(notes.map((n: { id: number }) => n.id)).toContain(note.id);
160
  expect(
161
    (await call("delete_note", { id: note.id }, root)).result.isError,
162
  ).toBeUndefined();
163
  expect(
164
    (await rpc({ jsonrpc: "2.0", id: 4, method: "nothing" })).error.code,
165
  ).toBe(-32601);
166
  const notified = await api("/mcp", {
167
    method: "POST",
168
    ...jsonBody({ jsonrpc: "2.0", method: "notifications/initialized" }),
169
  });
170
  expect(notified.status).toBe(202);
171
});
172
173
test("a change is on the event stream, and a late client hears what it missed", async () => {
174
  const stream = await fetch(base + "/events");
175
  expect(stream.headers.get("content-type")).toBe("text/event-stream");
176
  const reader = stream.body!.getReader();
177
  const next = async (): Promise<{ id: string; type: string }> => {
178
    let buffer = "";
179
    for (;;) {
180
      const { value } = await reader.read();
181
      buffer += new TextDecoder().decode(value);
182
      const frame = /id: (\d+)\ndata: (.*)\n\n/.exec(buffer);
183
      if (frame) return { id: frame[1]!, type: JSON.parse(frame[2]!).type };
184
    }
185
  };
186
  await api(
187
    "/api/notes",
188
    { method: "POST", ...jsonBody({ body: "live" }) },
189
    alice,
190
  );
191
  const heard = await next();
192
  expect(heard.type).toBe("note.added");
193
  await reader.cancel();
194
  const late = await fetch(base + "/events", {
195
    headers: { "last-event-id": String(Number(heard.id) - 1) },
196
  });
197
  const replay = new TextDecoder().decode(
198
    (await late.body!.getReader().read()).value,
199
  );
200
  expect(replay).toContain(`id: ${heard.id}\n`);
201
  expect(replay).toContain("note.added");
202
});
203
204
test("writes are rationed per person", async () => {
205
  const flood = as("u9", "flood");
206
  const answers = await Promise.all(
207
    Array.from({ length: 40 }, () =>
208
      api(
209
        "/api/notes",
210
        { method: "POST", ...jsonBody({ body: "again" }) },
211
        flood,
212
      ),
213
    ),
214
  );
215
  const codes = answers.map((r) => r.status);
216
  expect(codes).toContain(201);
217
  expect(codes).toContain(429);
218
});
219
220
test("stands on the security floor", async () => {
221
  const response = await fetch(base + "/");
222
  expect(response.headers.get("content-security-policy")).toContain(
223
    "default-src 'self'",
224
  );
225
  expect(response.headers.get("x-content-type-options")).toBe("nosniff");
226
  expect(
227
    (await fetch(base + "/api/notes")).headers.get("content-security-policy"),
228
  ).toContain("default-src");
229
});
230
231
test("a body is capped by the bytes read, whatever it claims", async () => {
232
  const response = await api(
233
    "/notes",
234
    {
235
      method: "POST",
236
      headers: { "content-type": "application/x-www-form-urlencoded" },
237
      body: "body=" + "x".repeat(1024 * 1024),
238
    },
239
    alice,
240
  );
241
  expect(response.status).toBe(413);
242
});
243
244
test("HEAD sees the app up", async () => {
245
  const response = await fetch(base + "/", { method: "HEAD" });
246
  expect(response.status).toBe(200);
247
  expect(await response.text()).toBe("");
248
});
249
250
test("refuses a cross-site post", async () => {
251
  const form = new FormData();
252
  form.set("body", "from another site");
253
  const posted = await api(
254
    "/notes",
255
    {
256
      method: "POST",
257
      body: form,
258
      redirect: "manual",
259
      headers: {
260
        origin: "https://evil.example",
261
        "sec-fetch-site": "cross-site",
262
      },
263
    },
264
    alice,
265
  );
266
  expect(posted.status).toBe(403);
267
});
268
269
test("static files are cached for a year when linked by content", async () => {
270
  const page = await (await fetch(base + "/")).text();
271
  const href = /href="(\/static\/app\.css\?v=[0-9a-f]{8})"/.exec(page)?.[1];
272
  expect(href).toBeDefined();
273
  const css = await fetch(base + href!);
274
  expect(css.headers.get("cache-control")).toContain("immutable");
275
  expect(
276
    (await fetch(base + "/static/app.css")).headers.get("cache-control"),
277
  ).toBe("no-cache");
278
  expect((await fetch(base + "/static/..%2Fpackage.json")).status).toBe(404);
279
});
280
281
test("/healthz says up, apart from the page", async () => {
282
  const response = await fetch(base + "/healthz");
283
  expect(response.status).toBe(200);
284
  expect(response.headers.get("cache-control")).toBe("no-store");
285
  const health = (await response.json()) as Record<string, unknown>;
286
  expect(health.ok).toBe(true);
287
  expect(health.version).toMatch(/^\d+\.\d+\.\d+/);
288
  expect(Number.isInteger(health.uptime)).toBe(true);
289
  expect((await fetch(base + "/healthz", { method: "HEAD" })).status).toBe(200);
290
});
291
292
test("a link to the page unfurls: description, Open Graph, a card", async () => {
293
  const page = await (await fetch(base + "/")).text();
294
  for (const meta of [
295
    'name="description"',
296
    'property="og:title"',
297
    'property="og:description"',
298
    'property="og:url"',
299
    'property="og:type" content="website"',
300
    'name="twitter:card" content="summary"',
301
    'name="theme-color"',
302
  ])
303
    expect([meta, page.includes(meta)]).toEqual([meta, true]);
304
});
305
306
test("the look is the tokens: /tokens.css is linked by content and cached for a year", async () => {
307
  const page = await (await fetch(base + "/")).text();
308
  const href = /href="(\/tokens\.css\?v=[0-9a-f]{8})"/.exec(page)?.[1];
309
  expect(href).toBeDefined();
310
  const css = await fetch(base + href!);
311
  expect(css.headers.get("content-type")).toContain("text/css");
312
  expect(css.headers.get("cache-control")).toContain("immutable");
313
  expect(await css.text()).toContain("--color-bg:");
314
  // The tokens come first, so every rule after them can use them.
315
  expect(page.indexOf("/tokens.css")).toBeLessThan(
316
    page.indexOf("/static/app.css"),
317
  );
318
});
319
320
test("what the web expects: robots.txt, and security.txt from the forge that runs us", async () => {
321
  // Production is read by crawlers, the API is not; anything else is a
322
  // draft and asks not to be indexed at all.
323
  const robots = await (await fetch(base + "/robots.txt")).text();
324
  const noindex = (await fetch(base + "/")).headers.get("x-robots-tag");
325
  if (env === "production") {
326
    expect(robots).toContain("Disallow: /api/\n");
327
    expect(noindex).toBeNull();
328
  } else {
329
    expect(robots).toBe("User-agent: *\nDisallow: /\n");
330
    expect(noindex).toBe("noindex");
331
  }
332
  const security = await fetch(base + "/.well-known/security.txt", {
333
    redirect: "manual",
334
  });
335
  if (process.env.FORGE_URL === undefined) expect(security.status).toBe(404);
336
  else {
337
    expect(security.status).toBe(302);
338
    expect(security.headers.get("location")).toBe(
339
      new URL("/.well-known/security.txt", process.env.FORGE_URL).toString(),
340
    );
341
  }
342
});
↑↓ navigate ↵ open esc close
↑↓ navigate ↵ open esc close

Keyboard shortcuts