Public: anyone can read this; the crew can change it.

One test proves the Lost test: from production and from a preview, the pill says where you are and links home, to comment and to tell the crew #7763

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

scout/forge:scout/7755-one-test-proves-the-lost-test-from-produ → main 1 commit fork 1 thread · 1 unresolved

  • Merged. scout/7755-one-test-proves-the-lost-test-from-produ was merged into main as e64cf8c.

Review

Sign in to review this change.
1 file changed +163 −0
test/never-lost.test.ts added +163
@@ -0,0 +1,163 @@
1 /**
2 * The Lost test (P6 Never lost): from production and from a preview, the
3 * pill says where you are and links home, to comment, and to tell the crew.
4 * One forge, one app serving one HTML page, asked through the app proxy as
5 * production and as the preview of an open pull, signed in and signed out.
6 */
7
8 import { afterAll, beforeAll, describe, expect, spyOn, test } from "bun:test";
9 import { mkdtemp, rm } from "node:fs/promises";
10 import { tmpdir } from "node:os";
11 import { join } from "node:path";
12 import { createApp, type App } from "../src/app.ts";
13 import { newApp } from "../src/actions/apps.ts";
14 import { signGrant, type AppTarget } from "../src/apps/proxy.ts";
15 import { AppsRuntime, type EnvStatus } from "../src/apps/runtime.ts";
16 import { TOOLBAR_PATH } from "../src/apps/toolbar.ts";
17 import { getAppByRepo, type AppRecord } from "../src/core/apps.ts";
18 import { createIssue } from "../src/core/issues.ts";
19 import { createToken } from "../src/core/tokens.ts";
20 import { createUser } from "../src/core/users.ts";
21 import { serve, type Servers } from "../src/http/server.ts";
22 import { quietLogs } from "./helpers/globals.ts";
23
24 quietLogs();
25
26 const DOMAIN = "apps.test";
27 const HTML = "<html><body><h1>hi</h1></body></html>";
28
29 describe("never lost: the pill from production and from a preview", () => {
30 let dataDir: string;
31 let app: App;
32 let servers: Servers;
33 let runtime: AppsRuntime;
34 let backend: ReturnType<typeof Bun.serve>;
35 let status: ReturnType<typeof spyOn>;
36 let record: AppRecord;
37 let pull: number;
38 let auth: Record<string, string>;
39 let alice: string;
40
41 beforeAll(async () => {
42 backend = Bun.serve({
43 port: 0,
44 fetch: () => new Response(HTML, { headers: { "content-type": "text/html; charset=utf-8" } }),
45 });
46 dataDir = await mkdtemp(join(tmpdir(), "forge-never-lost-"));
47 app = createApp({ dataDir, port: 0, host: "forge.test" });
48 servers = await serve(app);
49 runtime = new AppsRuntime(app, {
50 domain: DOMAIN,
51 scheme: "http",
52 publicPort: servers.port,
53 forgeUrl: servers.url,
54 concurrency: 1,
55 });
56 await runtime.start();
57 status = spyOn(AppsRuntime.prototype, "envStatus").mockImplementation(
58 (_app: unknown, env: string) =>
59 ({
60 env,
61 state: "running",
62 port: backend.port,
63 commitOid: "a".repeat(40),
64 }) as EnvStatus,
65 );
66 const u = await createUser(app.db, {
67 origin: "seed",
68 trusted: true,
69 username: "alice",
70 email: "[email protected]",
71 password: "a-good-passphrase",
72 adminIfFirst: true,
73 });
74 if (!u.ok) throw new Error(u.error);
75 alice = u.value.id;
76 const made = await newApp(app, u.value, { name: "notes", description: "x", visibility: "public" });
77 if (!made.ok) throw new Error(String(made.error));
78 record = getAppByRepo(app.db, made.value.repo.id)!;
79 const p = createIssue(app.db, { repoId: record.repoId, authorId: alice, title: "a change", kind: "pull" });
80 if (!p.ok) throw new Error(p.error);
81 pull = p.value.number;
82 auth = { authorization: `Bearer ${createToken(app.db, alice, "t", "rw").token}` };
83 }, 120_000);
84
85 afterAll(async () => {
86 status.mockRestore();
87 await runtime.stop();
88 servers.stop();
89 backend.stop(true);
90 app.close();
91 await rm(dataDir, { recursive: true, force: true });
92 });
93
94 const cookieFor = (env: string) =>
95 `forge_grant_${record.id.slice(-8)}=${signGrant(dataDir, { app: record, env } as AppTarget, 60_000, Date.now(), {
96 id: alice,
97 name: "alice",
98 role: "admin",
99 })}`;
100 const ask = (host: string, path: string, cookie?: string) =>
101 fetch(`${servers.url}${path}`, {
102 headers: { host, accept: "text/html", ...(cookie === undefined ? {} : { cookie }) },
103 redirect: "manual",
104 });
105
106 const places = [
107 { name: "production", preview: false },
108 { name: "a preview", preview: true },
109 ];
110
111 for (const place of places) {
112 describe(place.name, () => {
113 const host = () => (place.preview ? `notes--${pull}.${DOMAIN}` : `notes.${DOMAIN}`);
114 const env = () => (place.preview ? `pull-${pull}` : "production");
115
116 test("signed in: the page says where you are; the pill's links all answer", async () => {
117 const res = await ask(host(), "/", cookieFor(env()));
118 expect(res.status).toBe(200);
119 const html = await res.text();
120 const tag = /<script src="([^"]+)"([^>]*) defer><\/script>/.exec(html);
121 expect(tag).not.toBeNull();
122 expect(tag![1]).toBe(`${servers.url}${TOOLBAR_PATH}`);
123 expect(tag![2]).toContain(' data-app="notes"');
124 expect(tag![2]).toContain(' data-repo="alice/notes"');
125 expect(tag![2]).toContain(` data-env="${env()}"`);
126 if (place.preview) expect(tag![2]).toContain(` data-pull="${pull}"`);
127 else expect(tag![2]).not.toContain("data-pull");
128
129 // The crew door answers the owner with the forge's own tell page.
130 const crew = await ask(host(), "/__forge/crew", cookieFor(env()));
131 expect(crew.status).toBe(200);
132 const tell = ((await crew.json()) as { tell?: string }).tell;
133 expect(tell).toBe(`${servers.url}/alice/notes/tell`);
134

These links are assembled by the test, not read from the pill. If the script changes its hrefs, this still passes. Parse or execute the script output instead.

135 // Home, comment/tell/make-it-yours, and the tell form: forge origin, 200.
136 const query = new URLSearchParams({ env: env(), page: `http://${host()}/` });
137 const links = [
138 `${servers.url}/`,
139 `${servers.url}/alice/notes/toolbar?${query}`,
140 `${tell}?${new URLSearchParams({ page: `http://${host()}/`, ...(place.preview ? { pull: String(pull) } : {}) })}`,
141 ];
142 for (const link of links) {
143 expect(new URL(link).origin).toBe(servers.url);
144 const got = await fetch(link, { headers: auth, redirect: "manual" });
145 expect(got.status).toBe(200);
146 }
147 const form = await (await fetch(links[2]!, { headers: auth })).text();
148 expect(form).toContain("<form");
149 });
150
151 test("signed out: still says where you are, offers sign-in in place of tell", async () => {
152 const html = await (await ask(host(), "/")).text();
153 expect(html).toContain(' data-app="notes"');
154 expect(html).toContain(` data-env="${env()}"`);
155 if (place.preview) expect(html).toContain(` data-pull="${pull}"`);
156 expect(await (await ask(host(), "/__forge/crew")).json()).toEqual({});
157 const door = await (await ask(host(), "/__forge/")).text();
158 expect(door).toContain("/__forge/signin");
159 expect(door).not.toContain("/alice/notes/tell");
160 });
161 });
162 }
163 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs