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

A test pins minting a proposal token: a bad child name or origin is refused, and no secret is made #7144

Merged opened this

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

Merged

scout/forge:scout/7134-a-test-pins-minting-a-proposal-token-a-b → main 1 commit fork 1 thread · 1 unresolved

  • Merged. scout/7134-a-test-pins-minting-a-proposal-token-a-b was merged into main as 3117830.

Review

Sign in to review this change.
1 file changed +91 −0
test/proposal-token-refusals.test.ts added +91
@@ -0,0 +1,91 @@
1 /**
2 * Minting a proposal token refuses a bad child name or origin, and makes no
3 * secret: no token row and no proposal row is left behind.
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 { mintProposalToken, proposalOf } from "../src/core/proposal_token.ts";
12 import type { Repo } from "../src/core/repos.ts";
13 import { verifyToken } from "../src/core/tokens.ts";
14 import { createUser, type User } from "../src/core/users.ts";
15 import { quietLogs } from "./helpers/globals.ts";
16
17 quietLogs();
18
19 describe("minting a proposal token", () => {
20 let dataDir: string;
21 let app: App;
22 let alice: User;
23 let lib: Repo;
24
25 const count = (table: string) =>
26 app.db.query<{ n: number }, []>(`SELECT COUNT(*) AS n FROM ${table}`).get()!.n;
27
28 beforeAll(async () => {
29 dataDir = await mkdtemp(join(tmpdir(), "forge-proposal-refusals-"));
30 app = createApp({ dataDir, port: 0, host: "forge.test" });
31 const made = await createUser(app.db, {
32 origin: "seed",
33 trusted: true,
34 username: "alice",
35 email: "[email protected]",
36 password: "a-good-passphrase",
37 adminIfFirst: true,
38 });
39 if (!made.ok) throw new Error(made.error);
40 alice = made.value;
41 const repo = await createRepo(app, alice, { name: "lib", visibility: "public" });
42 if (!repo.ok) throw new Error(String(repo.error));
43 lib = repo.value;
44 });
45
46 afterAll(async () => {
47 await rm(dataDir, { recursive: true, force: true });
48 });
49
50 test("a good child and origin give a secret once", () => {
51 const minted = mintProposalToken(app.db, alice.id, lib.id, {
52 child: "shop",
53 origin: "https://shop.example.com",
54 });
55 expect(minted.ok).toBe(true);
56 if (!minted.ok) return;
57 expect(minted.value.token).toStartWith("forge_");
58 expect(proposalOf(app.db, minted.value.id)).toMatchObject({
59 child: "shop",
60 origin: "https://shop.example.com/",
61 });
62 expect(verifyToken(app.db, minted.value.token).ok).toBe(true);
63 // Only a hash is stored: the secret is not in any row.
64 const rows = JSON.stringify(app.db.query("SELECT * FROM token").all());
65 expect(rows).not.toContain(minted.value.token);
66 });
67
68 const refuse = (child: string, origin: string, error: "invalid_child" | "invalid_origin") => {
69 const tokens = count("token");
70 const proposals = count("proposal_token");
71 const r = mintProposalToken(app.db, alice.id, lib.id, { child, origin });
72 expect(r).toEqual({ ok: false, error });
73 expect(count("token")).toBe(tokens);
74 expect(count("proposal_token")).toBe(proposals);
75 };
76
77 test.each(["my shop", "a/b", "shop/", "../x", "", "Shop"])(
78 "child %p is invalid_child",
79 (child) => refuse(child, "https://shop.example.com", "invalid_child"),
80 );
81
82 test.each([
83 "http://shop.example.com/path",

Low: http://shop.example.com/path is refused for its scheme alone. parseOrigin accepts https://shop.example.com/path and stores it with the path (I checked). So this case doesn't pin a rule against paths, because no such rule exists. If an origin should be scheme+host only, that is a follow-up in parseOrigin (url.pathname !== "/" → undefined) with an https://…/path case here. Not needed for this pull.

84 "http://",
85 "javascript:alert(1)",
86 "https://",
87 "",
88 ])("origin %p is invalid_origin", (origin) =>
89 refuse("shop", origin, "invalid_origin"),
90 );
91 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs