wren/lost-property Public Public: anyone can read this; nobody can change it.

workbenchthis machine
Files wren/1-build-lost-property
152 lines · 6.2 KB · ~1.8k tok Raw History
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
6
process.env.FORGE_APP_DATA = mkdtempSync(join(tmpdir(), "app-"));
7
process.env.PORT = "0";
8
const { server } = await import("./server.ts");
9
const base = `http://${server.hostname}:${server.port}`;
10
11
async function accession(description: string): Promise<string> {
12
  const form = new FormData();
13
  form.set("description", description);
14
  const posted = await fetch(base + "/items", { method: "POST", body: form, redirect: "manual" });
15
  expect(posted.status).toBe(303);
16
  return posted.headers.get("location")!;
17
}
18
19
test("serves the entrance", async () => {
20
  const response = await fetch(base + "/");
21
  expect(response.status).toBe(200);
22
  expect(await response.text()).toContain("<h1>lost-property</h1>");
23
});
24
25
test("accessions a lost item with a catalogue number and a vitrine", async () => {
26
  const location = await accession("a single brown leather glove, left hand");
27
  expect(location).toMatch(/^\/items\/\d+$/);
28
  const page = await (await fetch(base + location)).text();
29
  expect(page).toContain("a single brown leather glove");
30
  expect(page).toMatch(/LP \d{4}\.\d{4}/);
31
  expect(page).toContain("Outerwear and Umbrellas");
32
});
33
34
test("lists an accessioned item on the entrance", async () => {
35
  await accession("a set of house keys on a red fob");
36
  const page = await (await fetch(base + "/")).text();
37
  expect(page).toContain("a set of house keys on a red fob");
38
});
39
40
test("an empty description is not accessioned", async () => {
41
  const form = new FormData();
42
  form.set("description", "   ");
43
  const posted = await fetch(base + "/items", { method: "POST", body: form, redirect: "manual" });
44
  expect(posted.status).toBe(303);
45
  expect(posted.headers.get("location")).toBe("/");
46
});
47
48
test("a case that does not exist answers 404", async () => {
49
  const response = await fetch(base + "/items/999999");
50
  expect(response.status).toBe(404);
51
});
52
53
test("a visitor leaves a note saying they saw one like it", async () => {
54
  const location = await accession("reading glasses, tortoiseshell frame");
55
  const form = new FormData();
56
  form.set("kind", "sighting");
57
  form.set("body", "Saw one just like this at the market on Tuesday.");
58
  const posted = await fetch(base + location + "/notes", { method: "POST", body: form, redirect: "manual" });
59
  expect(posted.status).toBe(303);
60
  const page = await (await fetch(base + location)).text();
61
  expect(page).toContain("Saw one like it");
62
  expect(page).toContain("Saw one just like this at the market on Tuesday.");
63
});
64
65
test("a visitor leaves a note saying they have the other half", async () => {
66
  const location = await accession("a single earring, gold hoop");
67
  const form = new FormData();
68
  form.set("kind", "match");
69
  form.set("body", "I have the matching earring, email me.");
70
  await fetch(base + location + "/notes", { method: "POST", body: form, redirect: "manual" });
71
  const page = await (await fetch(base + location)).text();
72
  expect(page).toContain("Has the other half");
73
  expect(page).toContain("I have the matching earring, email me.");
74
});
75
76
test("a note needs a valid kind", async () => {
77
  const location = await accession("a paperback book, dog-eared");
78
  const form = new FormData();
79
  form.set("kind", "not-a-real-kind");
80
  form.set("body", "hello");
81
  const posted = await fetch(base + location + "/notes", { method: "POST", body: form, redirect: "manual" });
82
  expect(posted.status).toBe(400);
83
});
84
85
test("a note cannot be left on a case that does not exist", async () => {
86
  const form = new FormData();
87
  form.set("kind", "sighting");
88
  form.set("body", "hello");
89
  const posted = await fetch(base + "/items/999999/notes", { method: "POST", body: form, redirect: "manual" });
90
  expect(posted.status).toBe(404);
91
});
92
93
test("stands on the security floor", async () => {
94
  const response = await fetch(base + "/");
95
  expect(response.headers.get("content-security-policy")).toContain("default-src 'self'");
96
  expect(response.headers.get("x-content-type-options")).toBe("nosniff");
97
});
98
99
test("a bug answers with the floor, not the runtime's page", async () => {
100
  // A percent-escape that is not one makes the search params throw.
101
  const response = await fetch(base + "/items", {
102
    method: "POST",
103
    headers: { "content-type": "application/x-www-form-urlencoded" },
104
    body: "description=%E0%A4%A",
105
  });
106
  expect(response.status).toBe(200);
107
  expect(response.headers.get("content-security-policy")).toContain("default-src 'self'");
108
});
109
110
test("a body is capped by the bytes read, whatever it claims", async () => {
111
  // The cap counts bytes as they arrive and never reads Content-Length, so
112
  // a chunked body with no length is refused the same way this one is.
113
  const response = await fetch(base + "/items", {
114
    method: "POST",
115
    headers: { "content-type": "application/x-www-form-urlencoded" },
116
    body: "description=" + "x".repeat(1024 * 1024),
117
  });
118
  expect(response.status).toBe(413);
119
});
120
121
test("HEAD sees the app up", async () => {
122
  const response = await server.fetch(new Request(base + "/", { method: "HEAD" }));
123
  expect(response.status).toBe(200);
124
  expect(await response.text()).toBe("");
125
});
126
127
test("HEAD on a case answers like GET without a body", async () => {
128
  const location = await accession("a stuffed teddy bear, missing an eye");
129
  const response = await server.fetch(new Request(base + location, { method: "HEAD" }));
130
  expect(response.status).toBe(200);
131
  expect(await response.text()).toBe("");
132
});
133
134
test("refuses a cross-site post", async () => {
135
  const form = new FormData();
136
  form.set("description", "from another site");
137
  const posted = await fetch(base + "/items", {
138
    method: "POST",
139
    body: form,
140
    redirect: "manual",
141
    headers: { origin: "https://evil.example", "sec-fetch-site": "cross-site" },
142
  });
143
  expect(posted.status).toBe(403);
144
  expect(await (await fetch(base + "/")).text()).not.toContain("from another site");
145
});
146
147
test("escapes a description that carries markup", async () => {
148
  const location = await accession("<script>alert(1)</script>");
149
  const page = await (await fetch(base + location)).text();
150
  expect(page).not.toContain("<script>alert(1)</script>");
151
  expect(page).toContain("&lt;script&gt;");
152
});
↑↓ navigate ↵ open esc close
↑↓ navigate ↵ open esc close

Keyboard shortcuts