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

A test proves the terminal's app variables screen: an admin adds a masked value that is never drawn, toasted or logged, and unsets one after a confirm #7604

Merged opened this

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

Merged

scout/forge:scout/7591-a-test-proves-the-terminal-s-app-variabl → main 1 commit fork

  • Merged. scout/7591-a-test-proves-the-terminal-s-app-variabl was merged into main as c6855c3.

Review

Sign in to review this change.
1 file changed +125 −0
test/tui-appenv.test.ts added +125
@@ -0,0 +1,125 @@
1 /**
2 * What the terminal's variables screen (src/tui/screens/appenv.ts, #7232)
3 * promises about a secret: the value typed into the masked field is in no
4 * frame, no toast and no log line, and `d` unsets only after a confirm. The
5 * flow itself is in tui-app-env.test.ts; this file watches the console and
6 * every frame while an admin adds, declines, and confirms.
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 { newApp } from "../src/actions/apps.ts";
13 import { APP_ENV_ERRORS } from "../src/actions/app_env_words.ts";
14 import { createApp, type App } from "../src/app.ts";
15 import { listAppEnvNames } from "../src/core/app_env.ts";
16 import { getAppByRepo } from "../src/core/apps.ts";
17 import { getRepo, type Repo } from "../src/core/repos.ts";
18 import { createUser, type User } from "../src/core/users.ts";
19 import { logLevel, setLogLevel, type LogLevel } from "../src/kernel/log.ts";
20 import { createForgeApp } from "../src/tui/forge.ts";
21 import { repoScreen } from "../src/tui/screens/repo.ts";
22 import { local } from "../src/tui/screens/where.ts";
23 import { headlessTerminal } from "../src/tui/term/terminal.ts";
24 import { settleApp } from "./helpers/tui.ts";
25 import { proveAll } from "./helpers/verified.ts";
26
27 const SECRET = "hunter2_Zq9xW";
28 const RIGHT = "\x1b[C";
29 const DOWN = "\x1b[B";
30 const open = [":", "Variables", "\r"];
31
32 let dir: string;
33 let app: App;
34 let admin: User;
35 let writer: User;
36 let shop: Repo;
37 let found: LogLevel;
38 const frames: string[] = [];
39 const logged: string[] = [];
40 const spies: { mockRestore(): void }[] = [];
41
42 const vars = () => listAppEnvNames(app.db, getAppByRepo(app.db, shop.id)!.id);
43
44 async function drive(user: User, keys: string[]): Promise<string> {
45 const terminal = headlessTerminal(120, 40);
46 const { app: tui, session } = createForgeApp(
47 app,
48 { sshUsername: user.username, remoteAddress: "127.0.0.1", fingerprint: "SHA256:test", user },
49 { terminal, exitHooks: false },
50 );
51 tui.start();
52 tui.ctx.push(repoScreen(session, tui.ctx, local(session, shop)));
53 await settleApp(tui);
54 for (const k of keys) {
55 terminal.feed(k);
56 await settleApp(tui);
57 frames.push(tui.screen.toText());
58 }
59 const text = tui.screen.toText();
60 tui.stop?.();
61 return text;
62 }
63
64 beforeAll(async () => {
65 found = logLevel();
66 setLogLevel("debug");
67 for (const m of ["log", "info", "warn", "error", "debug"] as const) {
68 spies.push(spyOn(console, m).mockImplementation((...a: unknown[]) => void logged.push(a.join(" "))));
69 }
70 dir = await mkdtemp(join(tmpdir(), "forge-tui-appenv-"));
71 app = createApp({ dataDir: dir, port: 0 });
72 const mk = async (username: string, first = false) => {
73 const r = await createUser(app.db, {
74 origin: "seed", trusted: true, username, email: `${username}@forge.local`,
75 password: "a-good-passphrase", adminIfFirst: first,
76 });
77 if (!r.ok) throw new Error(r.error);
78 return r.value;
79 };
80 admin = await mk("alice", true);
81 writer = await mk("walt");
82 proveAll(app);
83 const made = await newApp(app, admin, { name: "shop", visibility: "public" });
84 if (!made.ok) throw new Error(String(made.error));
85 shop = getRepo(app.db, admin.username, "shop")!;
86 });
87 afterAll(async () => {
88 for (const s of spies) s.mockRestore();
89 setLogLevel(found);
90 app.db.close();
91 await rm(dir, { recursive: true, force: true });
92 });
93
94 describe("the terminal's variables screen", () => {
95 test("an admin adds a masked value: name and scope listed, the value nowhere", async () => {
96 const text = await drive(admin, [...open, "a", "API_TOKEN", "\r", SECRET, "\r", DOWN, "\r"]);
97 expect(vars().map((v) => [v.name, v.scope])).toEqual([["API_TOKEN", "production"]]);
98 expect(text).toContain("API_TOKEN is set for production.");
99 expect(text).toContain("production · set");
100 expect(frames.some((f) => f.includes("•".repeat(SECRET.length)))).toBe(true);
101 for (const f of frames) expect(f).not.toContain(SECRET);
102 expect(logged.join("\n")).not.toContain(SECRET);
103 });
104
105 test("d asks first; keeping it leaves it, confirming unsets it", async () => {
106 const asked = await drive(admin, [...open, "d"]);
107 expect(asked).toContain("Unset this variable");
108 expect(vars().length).toBe(1);
109 await drive(admin, [...open, "d", "\r"]);
110 expect(vars().length).toBe(1);
111 const text = await drive(admin, [...open, "d", RIGHT, "\r"]);
112 expect(vars()).toEqual([]);
113 expect(text).toContain("API_TOKEN is unset for production.");
114 });
115
116 test("a non-admin is not offered the entry; a bad name is refused in MCP's words", async () => {
117 expect(await drive(writer, [":", "Variables"])).not.toContain("— Variables");
118 expect(await drive(admin, [":", "Variables"])).toContain("— Variables");
119 const text = await drive(admin, [...open, "a", "bad name", "\r", SECRET, "\r", "\r"]);
120 expect(text).toContain(APP_ENV_ERRORS["invalid_name"]!.slice(0, 50));
121 expect(vars()).toEqual([]);
122 for (const f of frames) expect(f).not.toContain(SECRET);
123 expect(logged.join("\n")).not.toContain(SECRET);
124 });
125 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs