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

A test pins saving and clearing the sealed backup mirror from Settings → Backups, a forge admin's only #7031

Merged opened this

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

Merged

scout/forge:scout/7021-a-test-pins-saving-and-clearing-the-seal → main 2 commits fork

  • Merged. scout/7021-a-test-pins-saving-and-clearing-the-seal was merged into main as 8eb6195.

Review

Sign in to review this change.
1 file changed +137 −0
test/settings-backups-mirror.test.ts added +137
@@ -0,0 +1,137 @@
1 /**
2 * Settings → Backups → Off-box mirror: an admin seals and clears the mirror
3 * through the page's own forms. Nothing a credential says comes back, a
4 * refused destination is a danger flash that saves nothing, and a non-admin or
5 * a cross-origin post changes nothing.
6 */
7 import { afterAll, beforeAll, describe, expect, test } from "bun:test";
8 import { mkdtemp, rm } from "node:fs/promises";
9 import { tmpdir } from "node:os";
10 import { join } from "node:path";
11 import { createApp, type App } from "../src/app.ts";
12 import { createSession } from "../src/core/sessions.ts";
13 import { createUser } from "../src/core/users.ts";
14 import { serve, type Servers } from "../src/http/server.ts";
15 import { mirrorSealedSet } from "../src/snapshot/mirror.ts";
16 import { adminCookie } from "./helpers/admin.ts";
17 import { quietLogs } from "./helpers/globals.ts";
18
19 quietLogs();
20
21 const SECRET = "mirror-s3cr3t-ZZZ-0123456789";
22 const ACCESS = "AKIAMIRRORKEY";
23 const GOOD = {
24 endpoint: "http://127.0.0.1:9100",
25 bucket: "forge-mirror",
26 access_key: ACCESS,
27 secret_key: SECRET,
28 };
29
30 describe("Settings → Backups mirror", () => {
31 let dir = "";
32 let app: App;
33 let servers: Servers;
34 let admin = "";
35 let bob = "";
36
37 const post = (path: string, fields: Record<string, string>, cookie: string, origin?: string) =>
38 fetch(`${servers.url}${path}`, {
39 method: "POST",
40 redirect: "manual",
41 headers: {
42 cookie,
43 origin: origin ?? servers.url,
44 "content-type": "application/x-www-form-urlencoded",
45 },
46 body: new URLSearchParams(fields).toString(),
47 });
48
49 const flashOf = (res: Response): string => {
50 const set = res.headers.getSetCookie().find((c) => c.startsWith("forge_flash=")) ?? "";
51 return decodeURIComponent(set.split(";")[0]!.slice("forge_flash=".length));
52 };
53
54 const view = async (): Promise<string> =>
55 (await fetch(`${servers.url}/settings/backups`, { headers: { cookie: admin } })).text();
56
57 beforeAll(async () => {
58 dir = await mkdtemp(join(tmpdir(), "forge-mirror-page-"));
59 delete process.env["FORGE_SNAPSHOT_DIR"];
60 app = createApp({ dataDir: dir, port: 0 });
61 servers = await serve(app);
62 admin = await adminCookie(app, { username: "alice", email: "[email protected]" });
63 const b = await createUser(app.db, {
64 origin: "seed",
65 username: "bob",
66 email: "[email protected]",
67 password: "a-good-passphrase",
68 trusted: true,
69 });
70 if (!b.ok) throw new Error("bob");
71 bob = `forge_session=${createSession(app.db, b.value.id).token}`;
72 }, 30_000);
73
74 afterAll(async () => {
75 servers.stop();
76 app.close();
77 await rm(dir, { recursive: true, force: true });
78 });
79
80 test("a refused destination is a danger flash and saves nothing", async () => {
81 const res = await post("/settings/backups/mirror", { ...GOOD, bucket: "NOT A BUCKET" }, admin);
82 expect(res.status).toBe(303);
83 expect(res.headers.get("location")).toBe("/settings/backups");
84 const flash = JSON.parse(flashOf(res));
85 expect(flash.tone).toBe("danger");
86 expect(flash.message).toContain("The bucket name is 3–63 lowercase letters");
87 expect(mirrorSealedSet(app.db)).toEqual([]);
88 });
89
90 test("a non-admin and a cross-origin post are refused and change nothing", async () => {
91 const asBob = await post("/settings/backups/mirror", GOOD, bob);
92 expect(asBob.status).toBe(404);
93 const asBobClear = await post("/settings/backups/mirror/clear", {}, bob);
94 expect(asBobClear.status).toBe(404);
95 const cross = await post("/settings/backups/mirror", GOOD, admin, "https://evil.example");
96 expect(cross.status).toBe(403);
97 expect(mirrorSealedSet(app.db)).toEqual([]);
98 });
99
100 test("an admin seals a mirror, the page shows it set, and no credential comes back", async () => {
101 const res = await post("/settings/backups/mirror", GOOD, admin);
102 expect(res.status).toBe(303);
103 expect(res.headers.get("location")).toBe("/settings/backups");
104 const flash = JSON.parse(flashOf(res));
105 expect(flash.tone).not.toBe("danger");
106 expect(flash.message).toBe("Mirror sealed; it is brought level within the hour.");
107 expect(mirrorSealedSet(app.db).sort()).toEqual(
108 [
109 "SNAPSHOT_MIRROR_S3_ACCESS_KEY",
110 "SNAPSHOT_MIRROR_S3_BUCKET",
111 "SNAPSHOT_MIRROR_S3_ENDPOINT",
112 "SNAPSHOT_MIRROR_S3_SECRET_KEY",
113 ],
114 );
115
116 const body = await view();
117 expect(body.split("set — leave blank to keep").length - 1).toBe(4);
118 expect(body).not.toContain(SECRET);
119 expect(body).not.toContain(ACCESS);
120 });
121
122 test("a cross-origin clear leaves the sealed mirror in place", async () => {
123 const res = await post("/settings/backups/mirror/clear", {}, admin, "https://evil.example");
124 expect(res.status).toBe(403);
125 expect(mirrorSealedSet(app.db)).not.toEqual([]);
126 });
127
128 test("clearing says so and the page shows no mirror", async () => {
129 const res = await post("/settings/backups/mirror/clear", {}, admin);
130 expect(res.status).toBe(303);
131 expect(JSON.parse(flashOf(res)).message).toBe("The sealed mirror is cleared.");
132 expect(mirrorSealedSet(app.db)).toEqual([]);
133 const body = await view();
134 expect(body).not.toContain("set — leave blank to keep");
135 expect(body).toContain("No off-box mirror");
136 });
137 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs