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

When signup is closed, no new account is made by any door, except the forge's first #2258

Merged opened this

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

Merged

scout/forge:scout/2256-when-signup-is-closed-no-new-account-is → main 3 commits fork 4 threads · 4 unresolved

  • Merged. scout/2256-when-signup-is-closed-no-new-account-is was merged into main as 3ccd4e3.

Review

Sign in to review this change.
1 file changed +118 −0
test/signup-closed.test.ts added +118
@@ -0,0 +1,118 @@
1 /**
2 * With `signup` closed (or `invite`, which acts as closed), no new account is
3 * made by any door; the forge's first always is; a switch is live.
4 */
5
6 import { afterAll, beforeAll, describe, expect, test } from "bun:test";
7 import { mkdtemp, rm } from "node:fs/promises";
8 import { tmpdir } from "node:os";
9 import { join } from "node:path";
10 import { createApp, type App } from "../src/app.ts";
11 import { setSignup, type SignupMode } from "../src/settings/signup.ts";
12 import { signUp, signUpWithPasskey } from "../src/actions/users.ts";
13 import { getUserByName } from "../src/core/users.ts";
14 import { serve, type Servers } from "../src/http/server.ts";
15
16 const TOKEN = "edge-secret-0123456789";
17 const edge = (login: string) => ({ "x-plat-user": login, "x-plat-edge-token": TOKEN });
18
19 describe("closed signup", () => {
20 let dataDir: string;
21 let app: App;
22 let servers: Servers;
23 let base: string;
24
25 beforeAll(async () => {
26 dataDir = await mkdtemp(join(tmpdir(), "forge-signup-closed-"));
27 app = createApp({ dataDir, port: 0, url: "https://forge.example", edgeToken: TOKEN });
28 servers = await serve(app);
29 base = servers.url;
30 });
31 afterAll(async () => {
32 await servers.stop();
33 app.close();
34 await rm(dataDir, { recursive: true, force: true });
35 });
36
37 const mode = (m: SignupMode) => setSignup(app.db, m, "boot");
38 const post = (username: string) =>
39 fetch(`${base}/api/v1/users`, {
40 method: "POST",
41 headers: { "content-type": "application/json", origin: "https://forge.example" },
42 body: JSON.stringify({ username, email: `${username}@example.com`, password: "a-good-passphrase" }),
43 });
44 const me = (login: string) => fetch(`${base}/api/v1/user`, { headers: edge(login) });
45
46 test("the first account is made even when closed", async () => {
47 mode("closed");
48 const first = await me("founder");
49 expect(first.status).toBe(200);
50 expect(((await first.json()) as { isAdmin: boolean }).isAdmin).toBe(true);
51 });
52
53 for (const closed of ["closed", "invite"] as const) {
54 test(`${closed}: page, API and edge make no account`, async () => {
55 mode(closed);
56 const page = await fetch(`${base}/signup`);
57 const html = await page.text();
58 expect(html).toContain("Signup is closed on this forge.");
59 expect(html).not.toContain('name="password"');
60
61 const res = await post(`via-api-${closed}`);
62 expect(res.status).toBe(403);
63 const body = (await res.json()) as { error: string; message: string };
64 expect(body.error).toBe("signup_closed");
65 expect(body.message).toContain("closed");
66 expect(getUserByName(app.db, `via-api-${closed}`)).toBeUndefined();
67
68 expect((await me(`via-edge-${closed}`)).status).toBe(401);
69 expect(getUserByName(app.db, `via-edge-${closed}`)).toBeUndefined();
70 });
71 }
72
73 test("existing accounts still sign in through the edge when closed", async () => {
74 mode("closed");
75 expect((await me("founder")).status).toBe(200);
76 });
77
78 test("open makes accounts; switching takes effect on the next request", async () => {
79 mode("open");
80 expect(await (await fetch(`${base}/signup`)).text()).toContain('name="password"');
81 expect((await post("newcomer")).status).toBe(201);
82 expect((await me("edgy")).status).toBe(200);
83 mode("closed");
84 expect((await post("late")).status).toBe(403);
85 expect((await me("later")).status).toBe(401);
86 mode("open");
87 expect((await post("late")).status).toBe(201);
88 });
89
90 test("closed: the passkey and form doors and the terminal refuse too", async () => {
91 mode("closed");
92 const json = (path: string, body: unknown) =>
93 fetch(`${base}${path}`, {
94 method: "POST",
95 headers: { "content-type": "application/json", origin: "https://forge.example" },
96 body: JSON.stringify(body),
97 });
98 const options = await json("/signup/passkey/options", { username: "pk", email: "[email protected]" });
99 expect(options.status).toBe(403);
100 expect(JSON.stringify(await options.json())).toContain("Signup is closed on this forge.");
101
102 const form = await fetch(`${base}/signup`, {
103 method: "POST",
104 headers: { "content-type": "application/x-www-form-urlencoded", origin: "https://forge.example" },
105 body: new URLSearchParams({ username: "formed", email: "[email protected]", password: "a-good-passphrase" }),
106 });
107 expect(form.status).toBe(403);
108 expect(await form.text()).toContain("Signup is closed on this forge.");
109 expect(getUserByName(app.db, "formed")).toBeUndefined();
110
111 const input = { username: "direct", email: "[email protected]" };
112 const key = await signUpWithPasskey(app, { ...input, passkey: {} as never, passkeyName: "k" });
113 expect(key).toEqual({ ok: false, error: "signup_closed" });
114 const terminal = await signUp(app, { ...input, password: "a-good-passphrase" }, { door: "terminal" });
115 expect(terminal).toEqual({ ok: false, error: "signup_closed" });
116 expect(getUserByName(app.db, "direct")).toBeUndefined();
117 });
118 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs