// Covers first-run setup (June, 2026-09-30): with no kepr.toml, Kepr serves only /setup, which takes // the one-time code printed in the terminal, then the password, domain and bucket, writes kepr.toml // itself, and hands over to the real app. import { beforeAll, describe, expect, test } from "bun:test"; import { mkdtemp } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { createSetupApp } from "../src/admin/setup.ts"; import { type Config, loadConfig } from "../src/server/config.ts"; import { makeRepos } from "./fixture.ts"; const CODE = "ABCD2345"; const PASSWORD = "a long enough first password"; const ORIGIN = "http://localhost"; async function setup() { const dir = await mkdtemp(join(tmpdir(), "kepr-setup-")); const configPath = join(dir, "kepr.toml"); const done: Config[] = []; const app = createSetupApp({ code: CODE, configPath, dataDir: join(dir, "data"), done: (config) => done.push(config), }); const post = (fields: Record, origin = ORIGIN) => app.request("/setup", { method: "POST", headers: { origin, host: "localhost" }, body: new URLSearchParams(fields), }); return { app, configPath, done, post }; } describe("first-run setup", () => { let bucketDir: string; const form = (fields: Record = {}) => ({ code: "abcd-2345", domain: "localhost:3000", password: PASSWORD, confirm: PASSWORD, source: "folder", folder: bucketDir, ...fields, }); beforeAll(async () => { bucketDir = (await makeRepos(["app"])).bucketDir; }); test("every other path leads to /setup, which never shows the code", async () => { const { app } = await setup(); const home = await app.request("/"); expect(home.status).toBe(302); expect(home.headers.get("location")).toBe("/setup"); const page = await app.request("/setup", { headers: { host: "localhost:3000" } }); expect(page.status).toBe(200); const text = await page.text(); expect(text).toContain('name="code"'); expect(text).toContain('value="localhost:3000"'); expect(text).not.toContain(CODE); expect(page.headers.get("content-security-policy")).toContain("script-src 'self'"); // no-referrer makes browsers post the form with `Origin: null`, which the origin check refuses. expect(page.headers.get("referrer-policy")).toBe("same-origin"); }); test("refuses a wrong code, a foreign origin, and bad settings, writing nothing", async () => { const { configPath, done, post } = await setup(); const cases: [Record, string][] = [ [{ code: "ZZZZ-ZZZZ" }, "That setup code is not right."], [{ password: "short", confirm: "short" }, "at least 12 characters"], [{ confirm: "something else" }, "The two passwords differ."], [{ domain: "https://nope" }, "The domain is a bare hostname"], [{ folder: "/no/such/folder" }, "is not a folder Kepr can read"], [{ source: "s3", folder: "" }, "The endpoint is required."], ]; for (const [fields, error] of cases) { const response = await post(form(fields)); expect(response.status).toBeGreaterThanOrEqual(400); expect(await response.text()).toContain(error); } expect((await post(form(), "https://evil.example")).status).toBe(403); expect(await Bun.file(configPath).exists()).toBe(false); expect(done).toEqual([]); }); test("ten wrong codes lock setup until Kepr restarts", async () => { const { post } = await setup(); for (let i = 0; i < 10; i++) await post(form({ code: "ZZZZZZZZ" })); const locked = await post(form()); expect(locked.status).toBe(429); expect(await locked.text()).toContain("Restart Kepr for a new code."); }); test("the right code writes kepr.toml, hands over, and closes setup", async () => { const { configPath, done, post } = await setup(); const response = await post(form()); expect(response.status).toBe(303); expect(response.headers.get("location")).toBe("/sign-in"); const config = await loadConfig(configPath); expect(config.domain).toBe("localhost:3000"); expect(config.bucket).toEqual({ kind: "folder", path: bucketDir }); expect(await Bun.password.verify(PASSWORD, config.adminPasswordHash)).toBe(true); expect(done).toHaveLength(1); expect(done[0]?.domain).toBe("localhost:3000"); expect((await post(form())).status).toBe(404); }); });