12345import { 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 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});