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

A test pins verifying a cloud account from Settings: a forge admin only, and a failing provider explained in words #7041

Merged opened this

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

Merged

scout/forge:scout/7028-a-test-pins-verifying-a-cloud-account-fr → main 1 commit fork 2 threads · 2 unresolved

  • Merged. scout/7028-a-test-pins-verifying-a-cloud-account-fr was merged into main as f2e8885.

Review

Sign in to review this change.
1 file changed +110 −0
test/settings-cloud-verify.test.ts added +110
@@ -0,0 +1,110 @@
1 /**
2 * POST /settings/cloud/verify, posted the way the page does: a forge admin
3 * verifies an account against a stubbed provider and is told the result; a
4 * provider that answers 401 is explained in words, with no key and no raw
5 * provider body; a non-admin and a cross-origin post are refused before any
6 * provider call.
7 */
8 import { afterAll, beforeAll, describe, expect, 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 { addAccount, net } from "../src/actions/cloud.ts";
14 import { createUser, getUserByName } from "../src/core/users.ts";
15 import { serve, type Servers } from "../src/http/server.ts";
16 import { quietLogs } from "./helpers/globals.ts";
17
18 quietLogs();
19
20 const TOKEN = "ghp_verify9Zq4TxLm7Kp2Vb8nWcR5";
21 const RAW = "raw-provider-body-bad-credentials-7731";
22 let mode: "ok" | "unauthorized" = "ok";
23 let calls = 0;
24 net.fetch = async () => {
25 calls++;
26 return mode === "ok"
27 ? new Response(JSON.stringify({ login: "octo" }), { status: 200 })

net.fetch is replaced at module scope and never restored. Save the original and put it back in afterAll so other files in the same bun run keep the real network seam.

28 : new Response(JSON.stringify({ message: RAW }), { status: 401 });
29 };
30
31 describe("POST /settings/cloud/verify", () => {
32 let dataDir = "";
33 let app: App;
34 let servers: Servers;
35
36 beforeAll(async () => {
37 dataDir = await mkdtemp(join(tmpdir(), "forge-cloud-verify-"));
38 app = createApp({ dataDir, port: 0 });
39 servers = await serve(app);
40 for (const [username, adminIfFirst] of [["alice", true], ["bob", false]] as const) {
41 const made = await createUser(app.db, { origin: "seed", trusted: true, username, email: `${username}@f.test`, password: "a-good-passphrase", adminIfFirst });
42 if (!made.ok) throw new Error(made.error);
43 }
44 const added = await addAccount(app, getUserByName(app.db, "alice")!, "github", "main", TOKEN);
45 if (!added.ok) throw new Error(added.error);
46 });
47 afterAll(async () => {
48 servers.stop();
49 app.close();
50 await rm(dataDir, { recursive: true, force: true });
51 });
52
53 const login = async (username: string) =>
54 ((await fetch(`${servers.url}/signin`, {
55 method: "POST",
56 redirect: "manual",
57 headers: { "content-type": "application/x-www-form-urlencoded", origin: servers.url },
58 body: new URLSearchParams({ username, password: "a-good-passphrase" }),
59 })).headers.get("set-cookie") ?? "").split(";")[0] ?? "";
60
61 const verify = (cookie: string, origin: string = servers.url) =>
62 fetch(`${servers.url}/settings/cloud/verify`, {
63 method: "POST",
64 redirect: "manual",
65 headers: { cookie, "content-type": "application/x-www-form-urlencoded", origin },
66 body: new URLSearchParams({ provider: "github", name: "main" }),
67 });
68
69 /** Follow the redirect with the flash cookie, as a browser does. */
70 const landing = async (cookie: string, res: Response): Promise<string> => {
71 expect(res.status).toBe(303);
72 expect(res.headers.get("location")).toBe("/settings/cloud");
73 const flash = (res.headers.get("set-cookie") ?? "").split(";")[0] ?? "";
74 return await (await fetch(`${servers.url}/settings/cloud`, { headers: { cookie: `${cookie}; ${flash}` } })).text();
75 };
76
77 test("an admin verifies an account that works and is told so", async () => {
78 mode = "ok";
79 const cookie = await login("alice");
80 const before = calls;
81 const page = await landing(cookie, await verify(cookie));
82 expect(calls).toBeGreaterThan(before);
83 expect(page).toContain("GitHub token belongs to octo");
84 expect(page).not.toContain(TOKEN);
85 });
86
87 test("a provider that answers 401 is explained in words, without key or raw body", async () => {
88 mode = "unauthorized";
89 const cookie = await login("alice");
90 const res = await verify(cookie);
91 expect(res.headers.get("set-cookie") ?? "").not.toContain(TOKEN);
92 const page = await landing(cookie, res);
93 expect(page).toContain("The provider did not accept that credential.");
94 expect(page).not.toContain(TOKEN);
95 expect(page).not.toContain(RAW);
96 expect(page).not.toContain("answered 401");
97 });
98
99 test("a non-admin and a cross-origin post are refused, and no provider call is made", async () => {
100 mode = "ok";
101 const bob = await login("bob");
102 const alice = await login("alice");
103 const before = calls;
104 expect((await verify(bob)).status).toBe(404);
105 expect((await verify(alice, "https://evil.example")).status).toBe(403);
106 const anon = await verify("");
107 expect(anon.headers.get("location") ?? "").not.toBe("/settings/cloud");

not.toBe('/settings/cloud') passes for any location, or none. Assert the actual sign-in redirect (status and location) so the test fails if the guard changes shape.

108 expect(calls).toBe(before);
109 });
110 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs