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

A test pins proposal tokens in the terminal: shown once, minted only by who may, revoked for good #6992

Merged opened this

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

Merged

scout/forge:scout/6979-a-test-pins-proposal-tokens-in-the-termi → main 1 commit fork 1 thread · 1 unresolved

  • Merged. scout/6979-a-test-pins-proposal-tokens-in-the-termi was merged into main as ca73761.

Review

Sign in to review this change.
1 file changed +146 −0
test/tui-proposal-tokens.test.ts added +146
@@ -0,0 +1,146 @@
1 /**
2 * Proposal tokens in the terminal (#6979): the secret shows once on its own
3 * screen, only a writer mints, and a revoked token is refused by the API.
4 */
5 import { afterAll, beforeAll, describe, expect, test } from "bun:test";
6 import { mkdtemp, rm } from "node:fs/promises";
7 import { tmpdir } from "node:os";
8 import { join } from "node:path";
9 import { createRepo } from "../src/actions/repos.ts";
10 import { createApp, type App } from "../src/app.ts";
11 import { createUser, getUserByName } from "../src/core/users.ts";
12 import { serve, type Servers } from "../src/http/server.ts";
13 import { createForgeApp } from "../src/tui/forge.ts";
14 import { headlessTerminal } from "../src/tui/term/terminal.ts";
15 import { quietLogs } from "./helpers/globals.ts";
16 import { settleApp } from "./helpers/tui.ts";
17
18 quietLogs();
19
20 const NOT_VISIBLE = "no such resource here, or it is not visible to you";
21 const count = (app: App) =>
22 app.db.query<{ n: number }, []>("SELECT COUNT(*) AS n FROM proposal_token").get()!.n;
23
24 describe("proposal tokens in the terminal", () => {
25 let dataDir: string;
26 let app: App;
27 let servers: Servers;
28
29 function boot(username: string) {
30 const terminal = headlessTerminal(140, 32);
31 const user = getUserByName(app.db, username)!;
32 const { app: tui } = createForgeApp(
33 app,
34 { sshUsername: "t", remoteAddress: "127.0.0.1", user },
35 { terminal, exitHooks: false },
36 );
37 tui.start();
38 const key = (k: string, ctrl = false) =>
39 tui.dispatch({ type: "key", key: k, ctrl, alt: false, shift: false });
40 return {
41 text: () => tui.screen.toText(),
42 press: async (...keys: string[]) => {
43 keys.forEach((k) => key(k));
44 await settleApp(tui);
45 },
46 type: async (t: string) => {
47 for (const ch of t) key(ch);
48 await settleApp(tui);
49 },
50 open: async function () {
51 key("k", true);
52 await settleApp(tui);
53 await this.type("Proposal tokens");
54 await this.press("enter");
55 await this.type("alice/lib");
56 await this.press("enter");
57 },
58 stop: () => tui.stop(),
59 };
60 }
61
62 const asProposal = (token: string) =>
63 fetch(`${servers.url}/api/v1/user`, { headers: { authorization: `Bearer ${token}` } });
64
65 beforeAll(async () => {
66 dataDir = await mkdtemp(join(tmpdir(), "forge-tui-proposal-"));
67 app = createApp({ dataDir, port: 0, host: "forge.test" });
68 servers = await serve(app);
69 let alice;
70 for (const [username, first] of [["alice", true], ["bob", false]] as const) {
71 const made = await createUser(app.db, {
72 origin: "seed",
73 trusted: true,
74 username,
75 email: `${username}@forge.local`,
76 password: "a-good-passphrase",
77 adminIfFirst: first,
78 });
79 if (!made.ok) throw new Error(made.error);
80 alice ??= made.value;
81 }
82 const repo = await createRepo(app, alice!, { name: "lib", visibility: "public" });
83 if (!repo.ok) throw new Error(String(repo.error));
84 });
85
86 afterAll(async () => {
87 await servers.stop();
88 await rm(dataDir, { recursive: true, force: true });
89 });
90
91 test("n mints; the secret shows once, then x revokes it and the API refuses it", async () => {
92 const h = boot("alice");
93 await h.open();
94 expect(h.text()).toContain("None yet");
95
96 await h.press("n");
97 await h.type("kid-one");
98 await h.press("enter");
99 await h.type("https://kid.example.com/");
100 await h.press("enter");
101
102 const secret = /forge_[A-Za-z0-9_-]+/.exec(h.text())?.[0] ?? "";
103 expect(secret).not.toBe("");
104 expect(h.text()).toContain("Shown once");
105 expect(count(app)).toBe(1);
106 // A live proposal token is known but limited (403); a revoked one is nobody (401).
107 expect((await asProposal(secret)).status).toBe(403);
108
109 await h.press("esc");
110 expect(h.text()).toContain("kid-one");
111 expect(h.text()).toContain("https://kid.example.com/");
112 expect(h.text()).not.toContain(secret);
113 expect(h.text()).not.toContain("Shown once");
114
115 await h.press("x");
116 expect(h.text()).toContain("kid-one: revoked.");
117 expect(h.text()).not.toContain(secret);
118 expect(count(app)).toBe(0);
119 expect((await asProposal(secret)).status).toBe(401);
120 h.stop();
121 });
122
123 test("bad input mints nothing", async () => {
124 const h = boot("alice");
125 await h.open();
126 await h.press("n");
127 await h.type("Bad Name");
128 await h.press("enter");
129 await h.type("https://kid.example.com/");
130 await h.press("enter");
131 expect(h.text()).toContain("lowercase words and hyphens");
132 expect(count(app)).toBe(0);
133 h.stop();
134 });
135
136 test("a person who may not write the repository sees the refusal and mints nothing", async () => {
137 const h = boot("bob");
138 await h.open();
139 expect(h.text()).toContain(NOT_VISIBLE);
140 expect(h.text()).not.toContain("Tokens you minted");
141 await h.press("n");

Minor, non-blocking: Bob's submit is refused, so he never reaches the list screen. The prompt stays open, and n here is typed into its text field. That means this assertion can't fail whatever the mint key does. The real guarantee is the refusal text plus count(app) === 0, and both are checked. Next time this file is touched, drop the n press or assert the prompt still shows the refusal.

142 expect(h.text()).not.toContain("Mint a proposal token");
143 expect(count(app)).toBe(0);
144 h.stop();
145 });
146 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs