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

Make my forge takes a stranger with JavaScript to the claim form already filled in; without JavaScript the answer still shows the link once #11316

Merged opened this

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

Merged

mason/forge:mason/11202-make-my-forge-takes-a-stranger-with-java → main 10 commits fork

  • Merged. mason/11202-make-my-forge-takes-a-stranger-with-java was merged into main as f8b2534.

Review

Sign in to review this change.
8 files changed +204 −95
docs/features/claim-redirect.md added +20
@@ -0,0 +1,20 @@
1 # Claim redirect
2
3 What a stranger sees after pressing Start my forge (on `/`, `POST /forges/start`)
4 or Spawn on `/forges/new` (`POST /forges/new`). Both forms carry `data-js`, and
5 one script (`JS_FLAG_JS`, in `src/web/claim_code.ts`) adds `js=1` to them.
6
7 - **With JavaScript.** The answer is a 303 to `/forges/<name>/waiting#<code>`
8 (`no-store`, its address carries the code). The waiting page is static and
9 never holds the code: a few lines of script read the fragment, clear it from
10 the address bar, poll `/forges/<name>/ready` (at most 120 tries), then go to
11 the child's `/claim#<code>`. No code to copy.
12 - **Without JavaScript, from `/`.** No `js` field: the answer is a page, "Setting
13 up <name>", with one link to the claim form (`no-store`).
14 - **Without JavaScript, from `/forges/new`.** The answer is a 201 page that shows
15 the claim link once (`no-store`), to send or open.
16 - **From the terminal.** Unchanged: the door returns the claim link once and
17 the terminal prints it.
18
19 A signed-in person never gets the redirect. A claim code that is not well formed
20 is never put in an address: the link page shows it instead.
src/http/server.ts +2
@@ -281,6 +281,7 @@ import { forgeLinkRevoke, forgeLinkSubmit, forgeLinkUndo, forgeRemoveConfirm, fo
281281 forgeOwnCodeSubmit, forgesPage } from "../web/pages/forges.ts";
282282 import { forgeItSubmit } from "../web/pages/forge-it.ts";
283283 import { funnelSubmit } from "./funnel.ts";
284 import { forgeWaitingPage } from "../web/pages/forge-waiting.ts";
284285 import { startForgeSubmit } from "../web/pages/start-forge.ts";
285286 import { newForgePage, newForgeSubmit } from "../web/pages/new-forge.ts";
286287 import { askedPage } from "../web/studio/asked_page.ts";
@@ -857,6 +858,7 @@ export function buildRouter(): Router<Handler> {
857858 router.add("GET", "/forges/:name/remove", forgeRemoveConfirm);
858859 router.add("POST", "/forges/:name/remove", forgeRemoveSubmit);
859860 router.add("GET", "/forges/:name/ready", forgeReady);
861 router.add("GET", "/forges/:name/waiting", forgeWaitingPage);
860862 router.add("GET", "/forges/new", newForgePage);
861863 router.add("POST", "/forges/new", newForgeSubmit);
862864 router.add("POST", "/forges/start", startForgeSubmit);
src/web/claim_code.ts added +34
@@ -0,0 +1,34 @@
1 /**
2 * The claim code a new forge hands its first person (#11202): what it looks
3 * like, the address of the waiting page that carries it in the fragment, and
4 * the script that tells the server a browser ran script.
5 */
6 import { isChildName } from "../core/proposal_token.ts";
7
8 /** What the claim code looks like: 32 random bytes, base64url. */
9 export const CLAIM_CODE = /^[A-Za-z0-9_-]{43}$/;
10
11 /**
12 * Sets `js=1` on the forms that ask for it (`data-js`), so the answer can tell
13 * a browser that ran this script (sent to the waiting page, the code in the
14 * fragment) from one that did not (shown the link once).
15 */
16 export const JS_FLAG_JS = /* js */ `
17 (function () {
18 var forms = document.querySelectorAll("form[data-js]");
19 for (var i = 0; i < forms.length; i++) {
20 var f = document.createElement("input");
21 f.type = "hidden"; f.name = "js"; f.value = "1";
22 forms[i].appendChild(f);
23 }
24 })();
25 `;
26
27 /** The waiting page for a new forge, the claim code in the fragment only (never sent to a server). */
28 export function waitingAddress(body: Record<string, unknown>): string | undefined {
29 const link = body["claimUrl"];
30 const name = String(body["name"] ?? "");
31 if (typeof link !== "string" || !isChildName(name)) return undefined;
32 const code = link.split("#")[1] ?? "";
33 return CLAIM_CODE.test(code) ? `/forges/${name}/waiting#${code}` : undefined;
34 }
src/web/pages/forge-waiting.ts added +54
@@ -0,0 +1,54 @@
1 /**
2 * `GET /forges/<name>/waiting`: where a stranger with JavaScript waits for the
3 * forge they just made (#11202). The page is static and holds no claim code:
4 * the code rides the fragment of this address (`#<code>`), which a server never
5 * sees. A few lines of script read it, clear it from the address bar, poll
6 * `/forges/<name>/ready` (capped), then go to the child's `/claim#<code>`.
7 */
8 import { isChildName } from "../../core/proposal_token.ts";
9 import type { Ctx } from "../../http/context.ts";
10 import { childUrlOf } from "../../spawn/door.ts";
11 import { notFoundPage, pageHead } from "../chrome.ts";
12 import { html } from "../html.ts";
13 import { page } from "../layout.ts";
14 import { CLAIM_CODE } from "../claim_code.ts";
15
16 export const WAITING_JS = /* js */ `
17 (function () {
18 var el = document.getElementById("waiting");
19 if (!el) return;
20 var code = location.hash.slice(1), tries = 0;
21 if (!${CLAIM_CODE}.test(code)) { el.textContent = "This link has no claim code. Start again from the new forge page."; return; }
22 history.replaceState(null, "", location.pathname);
23 var name = el.getAttribute("data-name"), base = el.getAttribute("data-claim");
24 function again(ms) {
25 if (tries < 120) setTimeout(poll, ms);
26 else el.textContent = "This is taking longer than usual. Reload this page in a minute.";
27 }
28 function poll() {
29 tries++;
30 fetch("/forges/" + encodeURIComponent(name) + "/ready", { credentials: "same-origin" })
31 .then(function (r) { return r.json(); })
32 .then(function (j) { if (j && j.ready) location.replace(base + "#" + code); else again(1000); })
33 .catch(function () { again(2000); });
34 }
35 poll();
36 })();
37 `;
38
39 export function forgeWaitingPage(ctx: Ctx): Response {
40 const name = ctx.params.name ?? "";
41 if (!isChildName(name)) return notFoundPage(ctx);
42 const claim = `${childUrlOf(ctx.app, name).replace(/\/+$/, "")}/claim`;
43 return page({
44 ctx,
45 title: "Making your forge",
46 scriptJs: WAITING_JS,
47 body: html`<div class="narrow stack">
48 ${pageHead("Making your forge")}
49 <p id="waiting" role="status" data-name="${name}" data-claim="${claim}">It will be ready in a moment.</p>
50 <p>You will then choose a username and a password.</p>
51 <noscript><p>This page needs JavaScript. Go back and make the forge again to get its link.</p></noscript>
52 </div>`,
53 });
54 }
src/web/pages/new-forge.ts +9 −67
@@ -17,8 +17,6 @@ import type { Ctx } from "../../http/context.ts";
1717 import { notePhase, takeSessionTime, timePhase } from "../../core/route_timing.ts";
1818 import { giftOf, REDEEM_WORDS } from "../../actions/codes.ts";
1919 import { quotaFull } from "../../spawn/quota.ts";
20 import { isChildName } from "../../core/proposal_token.ts";
21 import { childReady } from "../../spawn/ready.ts";
2220 import { childHost, childUrlOf, noRoom, spawnDoor, type Door } from "../../spawn/door.ts";
2321 import {
2422 LEFT_BEHIND,
@@ -37,7 +35,7 @@ import { field, readForm, type Form } from "../form.ts";
3735 import { html, type Html } from "../html.ts";
3836 import { noteForgeMade } from "../funnel_door.ts";
3937 import { page, redirect } from "../layout.ts";
40 import { startRefusalHref, startRefusalOf } from "../start_refusal.ts";
38 import { JS_FLAG_JS, waitingAddress } from "../claim_code.ts";
4139 import { pickList } from "../pick.ts";
4240 import { btn, field as formField, row, selectField } from "../ui.ts";
4341
@@ -209,7 +207,7 @@ function formView(
209207 ${plan}
210208 <section class="settings-block">
211209 <h2>Its name and apps</h2>
212 <form method="post" action="/forges/new" class="stack" id="new-forge">
210 <form method="post" action="/forges/new" class="stack" id="new-forge" data-js>
213211 ${formField({
214212 label: "Name",
215213 name: "name",
@@ -473,57 +471,6 @@ function madeView(body: Record<string, unknown>, isAdmin: boolean): Html {
473471 </div>`;
474472 }
475473
476 /** The script that waits for the new forge to answer, then goes on to its claim form. */
477 export const WAIT_JS = /* js */ `
478 (function () {
479 var el = document.getElementById("making");
480 if (!el) return;
481 var name = el.getAttribute("data-name"), go = document.getElementById("making-go"), tries = 0;
482 function late() {
483 el.textContent = "This is taking longer than usual. Press Continue to try the new forge now; if it is not up yet, come back to this page in a minute.";
484 }
485 function again(ms) { if (tries < 120) setTimeout(poll, ms); else late(); }
486 function poll() {
487 tries++;
488 fetch("/forges/" + encodeURIComponent(name) + "/ready", { credentials: "same-origin" })
489 .then(function (r) { return r.json(); })
490 .then(function (j) { if (j && j.ready) location.replace(go.href); else again(1000); })
491 .catch(function () { again(2000); });
492 }
493 poll();
494 })();
495 `;
496
497 /**
498 * Screen two of "Start my forge": the new forge's claim form, where the
499 * person picks a username and password and lands signed in as its admin
500 * (`/claim`). Answering already, the browser is sent straight there; still
501 * booting, a waiting page polls `/forges/<name>/ready` and goes on by itself.
502 * The code travels in the fragment of that one address and is never copied.
503 */
504 async function walkOn(ctx: Ctx, body: Record<string, unknown>): Promise<Response | undefined> {
505 const link = body["claimUrl"];
506 const name = String(body["name"] ?? "");
507 if (typeof link !== "string" || !/^https?:\/\//.test(link) || !isChildName(name)) return undefined;
508 if ((await childReady(ctx.app, name)) === true) {
509 const res = redirect(link);
510 res.headers.set("cache-control", "no-store");
511 return res;
512 }
513 const response = page({
514 ctx,
515 title: "Making your forge",
516 scriptJs: WAIT_JS,
517 body: html`<div class="narrow stack">
518 ${pageHead("Making your forge")}
519 <p id="making" role="status" data-name="${name}">It will be ready in a moment; you will go on by yourself, to choose a username and a password.</p>
520 <p>${btn("Continue", { href: link, variant: "primary", id: "making-go" })}</p>
521 </div>`,
522 });
523 response.headers.set("cache-control", "no-store");
524 return response;
525 }
526
527474 const asBool = (form: Form, name: string): boolean =>
528475 field(form, name) === "on";
529476
@@ -558,6 +505,7 @@ export function newForgePage(ctx: Ctx): Response {
558505 const response = page({
559506 ctx,
560507 title: "New forge",
508 ...(user === undefined ? { scriptJs: JS_FLAG_JS } : {}),
561509 body: formView(ctx, user, { ...EMPTY, code, name: (ctx.url.searchParams.get("name") ?? "").slice(0, 38), ...fromPack(ctx, user) }, errors, gift, reads),
562510 });
563511 notePhase(ctx.request, "render", performance.now() - rendering);
@@ -607,11 +555,6 @@ export async function newForgeSubmit(ctx: Ctx): Promise<Response> {
607555 ...(values.code === "" ? {} : { code: values.code }),
608556 dryRun: planOnly,
609557 });
610 // The home page's own form says it came from there: a refusal goes back to `/` with the name kept.
611 if (door.status >= 400 && user === undefined && field(form, "from") === "home" && door.status !== 401 && door.status !== 403) {
612 const kind = startRefusalOf(door);
613 return redirect(startRefusalHref(kind, values.name));
614 }
615558 if (door.status >= 400)
616559 return page(
617560 {
@@ -628,13 +571,12 @@ export async function newForgeSubmit(ctx: Ctx): Promise<Response> {
628571 body: formView(ctx, user, values, {}, planView(ctx, user, values, door.body), readsFor(ctx, user)),
629572 });
630573 noteForgeMade(ctx);
631 // A stranger from the home page is not handed a link: the browser goes on to the claim form itself (#11242).
632 const going = user === undefined && field(form, "from") === "home" ? await walkOn(ctx, door.body) : undefined;
633 if (going !== undefined) return going;
634 const response = page(
635 { ctx, title: "New forge", body: madeView(door.body, ctx.auth.user?.isAdmin === true) },
636 door.status,
637 );
574 // A stranger whose script ran is not handed a link: the waiting page carries the code in its fragment (#11202).
575 const waiting = user === undefined && field(form, "js") === "1" ? waitingAddress(door.body) : undefined;
576 const response =
577 waiting !== undefined
578 ? redirect(waiting)
579 : page({ ctx, title: "New forge", body: madeView(door.body, ctx.auth.user?.isAdmin === true) }, door.status);
638580 // The link is in this body only: never kept by a cache.
639581 response.headers.set("cache-control", "no-store");
640582 if (values.code !== "") response.headers.append("set-cookie", dropGift(ctx.url, ctx.app.config.url));
src/web/pages/start-forge.ts +9
@@ -6,6 +6,7 @@
66 * then goes straight to the new forge's claim page (username and password,
77 * signed in on landing). The claim link is in this one no-store body, as the
88 * target of a link and a redirect: never shown as text, never to copy.
9 * With JavaScript the answer is a 303 to `/forges/<name>/waiting#<code>` instead.
910 * A signed-in person is sent to the fuller `/forges/new`, which keeps its form.
1011 */
1112 import { notePhase, takeSessionTime } from "../../core/route_timing.ts";
@@ -16,6 +17,7 @@ import { field, readForm } from "../form.ts";
1617 import { html, type Html } from "../html.ts";
1718 import { page, redirect } from "../layout.ts";
1819 import { notFoundPage, pageHead } from "../chrome.ts";
20 import { waitingAddress } from "../claim_code.ts";
1921 import { startRefusalHref, startRefusalOf } from "../start_refusal.ts";
2022
2123 /** The home's live name check: 404 from the ready probe means nothing by that name was made here. */
@@ -73,6 +75,13 @@ export async function startForgeSubmit(ctx: Ctx): Promise<Response> {
7375 const link = door.body["claimUrl"];
7476 if (typeof link !== "string") return redirect(startRefusalHref("taken", name));
7577 noteForgeMade(ctx);
78 // A browser that ran the page's script goes to the static waiting page, the code in the fragment only (#11202).
79 const waiting = field(form, "js") === "1" ? waitingAddress(door.body) : undefined;
80 if (waiting !== undefined) {
81 const to = redirect(waiting);
82 to.headers.set("cache-control", "no-store");
83 return to;
84 }
7685 const body = startWaitBody(name, link);
7786 const response = page({ ctx, title: "Setting up…", body, scriptJs: startWaitJs });
7887 response.headers.set("cache-control", "no-store");
src/web/studio/home.ts +3 −2
@@ -28,6 +28,7 @@ import { plansRow } from "../plans_row.ts";
2828 import { freePlanName, funnelJs, noteHomeVisit } from "../funnel_door.ts";
2929 import { publicPlans } from "../../settings/plans.ts";
3030 import { getDoorSettings } from "../../settings/door.ts";
31 import { JS_FLAG_JS } from "../claim_code.ts";
3132 import { startNameJs } from "../pages/start-forge.ts";
3233 import { GET_A_FORGE_PATH } from "../../billing/plan_words.ts";
3334 import { appsForRepos } from "../../core/apps.ts";
@@ -797,7 +798,7 @@ export function studioDoor(ctx: Ctx): Response {
797798 <div class="door-under">
798799 <div class="door-lead">
799800 <p class="door-support">Say what you need and an AI crew builds it in your own forge, free to try.</p>
800 <form method="post" action="/forges/start" class="door-start" id="start-forge">
801 <form method="post" action="/forges/start" class="door-start" id="start-forge" data-js>
801802 ${refusal === undefined ? null : html`<p class="error" id="start-refused">${START_REFUSALS[refusal]}</p>`}
802803 <div class="field">
803804 <label for="start-name">Forge name</label>
@@ -928,7 +929,7 @@ bun run src/main.ts</code></pre>
928929 title: "forge: software for what you're building, yours to keep",
929930 description: "Your own forge, with a private AI crew that builds your apps, games and business tools. Open source, and yours to keep.",
930931 body,
931 scriptJs: `${liveClientJs}\n${liveRegionsJs}\n${funnelJs}\n${startNameJs}`,
932 scriptJs: `${liveClientJs}\n${liveRegionsJs}\n${funnelJs}\n${startNameJs}\n${JS_FLAG_JS}`,
932933 jsonLd: siteStructuredData(origin, running().version),
933934 ...(visitor
934935 ? {
test/stranger-three-screens.test.ts +73 −26
@@ -12,7 +12,8 @@ import { join } from "node:path";
1212 import { childDirOf, childNamed } from "../src/actions/spawn.ts";
1313 import { createApp, type App } from "../src/app.ts";
1414 import { serve, type Servers } from "../src/http/server.ts";
15 import { WAIT_JS } from "../src/web/pages/new-forge.ts";
15 import { JS_FLAG_JS } from "../src/web/claim_code.ts";
16 import { WAITING_JS } from "../src/web/pages/forge-waiting.ts";
1617 import { useProbe } from "../src/spawn/door.ts";
1718 import { firstBoot } from "../src/spawn/first-boot.ts";
1819 import { getUserByName } from "../src/core/users.ts";
@@ -41,48 +42,89 @@ afterAll(async () => {
4142 await rm(dir, { recursive: true, force: true });
4243 });
4344
44 const start = (name: string) =>
45 fetch(`${parentServers.url}/forges/new`, {
45 const start = (name: string, js = true, path = "/forges/start") =>
46 fetch(`${parentServers.url}${path}`, {
4647 method: "POST",
4748 headers: { "content-type": "application/x-www-form-urlencoded" },
48 body: new URLSearchParams({ name, from: "home" }),
49 body: new URLSearchParams(js ? { name, js: "1" } : { name }),
4950 redirect: "manual",
5051 });
5152
52 test("screen 1 to 2: a forge still booting shows a waiting page that goes on by itself, with no link to copy", async () => {
53 test("screen 1 to 2: with JavaScript the answer is a 303 to the waiting page, the code only in the fragment", async () => {
5354 up = false;
5455 const home = await (await fetch(`${parentServers.url}/`)).text();
5556 expect(home).toContain('id="start-forge"');
5657 const res = await start("walker");
57 expect(res.status).toBe(200);
58 expect(res.status).toBe(303);
59 expect(res.headers.get("cache-control")).toBe("no-store");
60 expect(res.headers.get("location") ?? "").toMatch(/^\/forges\/walker\/waiting#[A-Za-z0-9_-]{43}$/);
61 });
62
63 /** The text of every script a page loads from its own address. */
64 async function pageScripts(html: string): Promise<string> {
65 const srcs = [...html.matchAll(/<script src="([^"]+)"/g)].map((m) => m[1]!);
66 return (await Promise.all(srcs.map(async (u) => (await fetch(`${parentServers.url}${u}`)).text()))).join("\n");
67 }
68
69 test("the home form and /forges/new both carry data-js and load the script that adds js=1", async () => {
70 const home = await (await fetch(`${parentServers.url}/`)).text();
71 expect(home).toMatch(/<form[^>]*id="start-forge"[^>]*data-js/);
72 expect(await pageScripts(home)).toContain("form[data-js]");
73 const form = await (await fetch(`${parentServers.url}/forges/new`)).text();
74 expect(form).toMatch(/<form[^>]*id="new-forge"[^>]*data-js/);
75 expect(await pageScripts(form)).toContain("form[data-js]");
76 });
77
78 test("the flag script appends a hidden js=1 input to each data-js form", () => {
79 const added: Array<{ type?: string; name?: string; value?: string }> = [];
80 const form = { appendChild: (e: { type?: string; name?: string; value?: string }) => added.push(e) };
81 const doc = { querySelectorAll: () => [form], createElement: () => ({}) };
82 new Function("document", JS_FLAG_JS)(doc);
83 expect(added).toEqual([{ type: "hidden", name: "js", value: "1" }]);
84 });
85
86 test("a claim code that is not well formed never goes in an address", async () => {
87 const { waitingAddress } = await import("../src/web/claim_code.ts");
88 expect(waitingAddress({ name: "walker", claimUrl: "https://walker--forge.test/claim#short" })).toBeUndefined();
89 expect(waitingAddress({ name: "Bad_Name", claimUrl: `https://x/claim#${"a".repeat(43)}` })).toBeUndefined();
90 expect(waitingAddress({ name: "walker", claimUrl: `https://x/claim#${"a".repeat(43)}` })).toBe(`/forges/walker/waiting#${"a".repeat(43)}`);
91 });
92
93 test("without JavaScript, /forges/new answers with the page (201) that shows the claim link once", async () => {
94 const res = await start("plainwalker", false, "/forges/new");
95 expect(res.status).toBe(201);
96 expect(res.headers.get("location")).toBeNull();
5897 expect(res.headers.get("cache-control")).toBe("no-store");
5998 const body = await res.text();
60 expect(body).toContain('id="making"');
61 expect(body).toContain('data-name="walker"');
62 expect(body).not.toContain("The link to send");
63 expect(body).not.toContain("data-copy");
64 // The only way on is one Continue link, always rendered (not only without JavaScript), to the claim form.
65 const claims = body.match(/href="https:\/\/walker--forge\.test\/claim#[A-Za-z0-9_-]{43}"/g) ?? [];
66 expect(claims).toHaveLength(1);
67 expect(body).toMatch(/<a[^>]*id="making-go"[^>]*>\s*Continue/);
68 expect(body).not.toContain("<noscript>");
99 expect(body).toContain("The link to send");
100 expect(body).toMatch(/plainwalker--forge\.test\/claim#[A-Za-z0-9_-]{43}/);
101 });
102
103 test("the waiting page is static and never contains the code", async () => {
104 const page = await fetch(`${parentServers.url}/forges/walker/waiting`);
105 expect(page.status).toBe(200);
106 const body = await page.text();
107 expect(body).not.toContain("/claim#");
108 expect(body).toContain('data-claim="https://walker--forge.test/claim"');
109 expect((await fetch(`${parentServers.url}/forges/Bad_Name/waiting`)).status).toBe(404);
69110 });
70111
71112 /** Run the waiting script against stubs; `answers` is what each poll gets. */
72 function runWait(answers: Array<"pending" | "down">) {
73 const status = { textContent: "", getAttribute: () => "walker" };
74 const go = { href: "https://walker--forge.test/claim#x" };
113 function runWait(answers: Array<"pending" | "down" | "ready">) {
114 const code = "a".repeat(43);
115 const status = { textContent: "", getAttribute: (k: string) => (k === "data-name" ? "walker" : "https://walker--forge.test/claim") };
75116 const replaced: string[] = [];
117 const cleared: string[] = [];
76118 let timers = 0;
77119 let n = 0;
78120 const fetchStub = () => {
79121 const a = answers[Math.min(n++, answers.length - 1)];
80 return a === "down" ? Promise.reject(new Error("net")) : Promise.resolve({ json: async () => ({ ready: false }) });
122 return a === "down" ? Promise.reject(new Error("net")) : Promise.resolve({ json: async () => ({ ready: a === "ready" }) });
81123 };
82 const doc = { getElementById: (id: string) => (id === "making" ? status : go) };
124 const doc = { getElementById: () => status };
83125 const timeout = (f: () => void) => { timers++; queueMicrotask(f); return 0; };
84 new Function("document", "fetch", "setTimeout", "location", WAIT_JS)(doc, fetchStub, timeout, { replace: (u: string) => replaced.push(u) });
85 return { status, replaced, polls: () => n, timers: () => timers };
126 new Function("document", "fetch", "setTimeout", "location", "history", WAITING_JS)(doc, fetchStub, timeout, { hash: `#${code}`, pathname: "/forges/walker/waiting", replace: (u: string) => replaced.push(u) }, { replaceState: (_a: unknown, _b: string, u: string) => cleared.push(u) });
127 return { status, replaced, cleared, polls: () => n, timers: () => timers };
86128 }
87129 const settle = () => new Promise((r) => setTimeout(r, 50));
88130
@@ -91,7 +133,7 @@ test("the waiting script gives up after 120 tries and says so in plain words, ne
91133 await settle();
92134 expect(w.polls()).toBe(120);
93135 expect(w.status.textContent).toContain("taking longer than usual");
94 expect(w.status.textContent).toContain("Continue");
136 expect(w.cleared).toEqual(["/forges/walker/waiting"]);
95137 expect(w.replaced).toEqual([]);
96138 });
97139
@@ -102,19 +144,24 @@ test("a network error keeps trying and says the same when it gives up", async ()
102144 expect(w.status.textContent).toContain("taking longer than usual");
103145 });
104146
147 test("the waiting script goes to the child's claim form with the code once the forge answers", async () => {
148 const w = runWait(["ready"]);
149 await settle();
150 expect(w.replaced).toEqual([`https://walker--forge.test/claim#${"a".repeat(43)}`]);
151 });
152
105153 test("a name the door refuses still comes back to `/`, not to the waiting page", async () => {
106154 const res = await start("Bad_Name");
107155 expect(res.status).toBe(303);
108156 expect(res.headers.get("location") ?? "").toStartWith("/?start=");
109157 });
110158
111 test("screen 2 to 3: a forge that answers sends the browser to its claim form; a username and password land signed in as its admin", async () => {
159 test("screen 2 to 3: the waiting page hands on to the claim form; a username and password land signed in as its admin", async () => {
112160 up = true;
113161 const res = await start("runner");
114162 expect(res.status).toBe(303);
115163 const to = res.headers.get("location") ?? "";
116 expect(to).toMatch(/^https:\/\/runner--forge\.test\/claim#[A-Za-z0-9_-]{43}$/);
117 expect(res.headers.get("cache-control")).toBe("no-store");
164 expect(to).toMatch(/^\/forges\/runner\/waiting#[A-Za-z0-9_-]{43}$/);
118165 const token = to.split("#")[1] ?? "";
119166
120167 const made = await childNamed(parent, "runner");

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs