setup.test.ts

4.2 KB · raw

1// Covers first-run setup (June, 2026-09-30): with no kepr.toml, Kepr serves only /setup, which takes2// the one-time code printed in the terminal, then the password, domain and bucket, writes kepr.toml3// itself, and hands over to the real app.45import { beforeAll, describe, expect, test } from "bun:test";6import { mkdtemp } from "node:fs/promises";7import { tmpdir } from "node:os";8import { join } from "node:path";9import { createSetupApp } from "../src/admin/setup.ts";10import { type Config, loadConfig } from "../src/server/config.ts";11import { makeRepos } from "./fixture.ts";1213const CODE = "ABCD2345";14const PASSWORD = "a long enough first password";15const ORIGIN = "http://localhost";1617async function setup() {18	const dir = await mkdtemp(join(tmpdir(), "kepr-setup-"));19	const configPath = join(dir, "kepr.toml");20	const done: Config[] = [];21	const app = createSetupApp({22		code: CODE,23		configPath,24		dataDir: join(dir, "data"),25		done: (config) => done.push(config),26	});27	const post = (fields: Record<string, string>, origin = ORIGIN) =>28		app.request("/setup", {29			method: "POST",30			headers: { origin, host: "localhost" },31			body: new URLSearchParams(fields),32		});33	return { app, configPath, done, post };34}3536describe("first-run setup", () => {37	let bucketDir: string;38	const form = (fields: Record<string, string> = {}) => ({39		code: "abcd-2345",40		domain: "localhost:3000",41		password: PASSWORD,42		confirm: PASSWORD,43		source: "folder",44		folder: bucketDir,45		...fields,46	});4748	beforeAll(async () => {49		bucketDir = (await makeRepos(["app"])).bucketDir;50	});5152	test("every other path leads to /setup, which never shows the code", async () => {53		const { app } = await setup();54		const home = await app.request("/");55		expect(home.status).toBe(302);56		expect(home.headers.get("location")).toBe("/setup");57		const page = await app.request("/setup", { headers: { host: "localhost:3000" } });58		expect(page.status).toBe(200);59		const text = await page.text();60		expect(text).toContain('name="code"');61		expect(text).toContain('value="localhost:3000"');62		expect(text).not.toContain(CODE);63		expect(page.headers.get("content-security-policy")).toContain("script-src 'self'");64		// no-referrer makes browsers post the form with `Origin: null`, which the origin check refuses.65		expect(page.headers.get("referrer-policy")).toBe("same-origin");66	});6768	test("refuses a wrong code, a foreign origin, and bad settings, writing nothing", async () => {69		const { configPath, done, post } = await setup();70		const cases: [Record<string, string>, string][] = [71			[{ code: "ZZZZ-ZZZZ" }, "That setup code is not right."],72			[{ password: "short", confirm: "short" }, "at least 12 characters"],73			[{ confirm: "something else" }, "The two passwords differ."],74			[{ domain: "https://nope" }, "The domain is a bare hostname"],75			[{ folder: "/no/such/folder" }, "is not a folder Kepr can read"],76			[{ source: "s3", folder: "" }, "The endpoint is required."],77		];78		for (const [fields, error] of cases) {79			const response = await post(form(fields));80			expect(response.status).toBeGreaterThanOrEqual(400);81			expect(await response.text()).toContain(error);82		}83		expect((await post(form(), "https://evil.example")).status).toBe(403);84		expect(await Bun.file(configPath).exists()).toBe(false);85		expect(done).toEqual([]);86	});8788	test("ten wrong codes lock setup until Kepr restarts", async () => {89		const { post } = await setup();90		for (let i = 0; i < 10; i++) await post(form({ code: "ZZZZZZZZ" }));91		const locked = await post(form());92		expect(locked.status).toBe(429);93		expect(await locked.text()).toContain("Restart Kepr for a new code.");94	});9596	test("the right code writes kepr.toml, hands over, and closes setup", async () => {97		const { configPath, done, post } = await setup();98		const response = await post(form());99		expect(response.status).toBe(303);100		expect(response.headers.get("location")).toBe("/sign-in");101		const config = await loadConfig(configPath);102		expect(config.domain).toBe("localhost:3000");103		expect(config.bucket).toEqual({ kind: "folder", path: bucketDir });104		expect(await Bun.password.verify(PASSWORD, config.adminPasswordHash)).toBe(true);105		expect(done).toHaveLength(1);106		expect(done[0]?.domain).toBe("localhost:3000");107		expect((await post(form())).status).toBe(404);108	});109});