setup.test.ts

4.1 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	});6566	test("refuses a wrong code, a foreign origin, and bad settings, writing nothing", async () => {67		const { configPath, done, post } = await setup();68		const cases: [Record<string, string>, string][] = [69			[{ code: "ZZZZ-ZZZZ" }, "That setup code is not right."],70			[{ password: "short", confirm: "short" }, "at least 12 characters"],71			[{ confirm: "something else" }, "The two passwords differ."],72			[{ domain: "https://nope" }, "The domain is a bare hostname"],73			[{ folder: "/no/such/folder" }, "is not a folder Kepr can read"],74			[{ source: "s3", folder: "" }, "The endpoint is required."],75		];76		for (const [fields, error] of cases) {77			const response = await post(form(fields));78			expect(response.status).toBeGreaterThanOrEqual(400);79			expect(await response.text()).toContain(error);80		}81		expect((await post(form(), "https://evil.example")).status).toBe(403);82		expect(await Bun.file(configPath).exists()).toBe(false);83		expect(done).toEqual([]);84	});8586	test("ten wrong codes lock setup until Kepr restarts", async () => {87		const { post } = await setup();88		for (let i = 0; i < 10; i++) await post(form({ code: "ZZZZZZZZ" }));89		const locked = await post(form());90		expect(locked.status).toBe(429);91		expect(await locked.text()).toContain("Restart Kepr for a new code.");92	});9394	test("the right code writes kepr.toml, hands over, and closes setup", async () => {95		const { configPath, done, post } = await setup();96		const response = await post(form());97		expect(response.status).toBe(303);98		expect(response.headers.get("location")).toBe("/sign-in");99		const config = await loadConfig(configPath);100		expect(config.domain).toBe("localhost:3000");101		expect(config.bucket).toEqual({ kind: "folder", path: bucketDir });102		expect(await Bun.password.verify(PASSWORD, config.adminPasswordHash)).toBe(true);103		expect(done).toHaveLength(1);104		expect(done[0]?.domain).toBe("localhost:3000");105		expect((await post(form())).status).toBe(404);106	});107});