1234import { beforeAll, describe, expect, test } from "bun:test";5import { join } from "node:path";6import { createApp } from "../src/server/app.ts";7import { loadConfig, saveConfig } from "../src/server/config.ts";8import { Browser, type Instance, instance, PASSWORD } from "./browser.ts";9import { makeRepos } from "./fixture.ts";1011const NEW_PASSWORD = "a much longer passphrase now";1213describe("the Account section", () => {14 let inst: Instance;15 let operator: Browser;16 let configPath: string;17 const change = (fields: Record<string, string>) =>18 operator.post("/admin/account", { action: "password", ...fields });1920 beforeAll(async () => {21 const repos = await makeRepos(["app"]);22 inst = await instance(repos.bucketDir);23 configPath = join(inst.dataDir, "kepr.toml");24 await saveConfig(configPath, inst.config);25 inst.app = createApp(inst.config, inst.kepr, undefined, configPath);26 operator = new Browser(inst.app);27 await operator.signIn();28 });2930 test("lists the sections's forms and never the hash", async () => {31 const page = await (await operator.get("/admin/account")).text();32 expect(page).toContain('name="current_password"');33 expect(page).toContain('value="kepr.example"');34 expect(page).not.toContain("$argon2id$");35 });3637 test("refuses a wrong current password, a short one, or a mismatch", async () => {38 const cases: [Record<string, string>, string][] = [39 [40 { current_password: "wrong", password: NEW_PASSWORD, confirm: NEW_PASSWORD },41 "The current password is not right.",42 ],43 [44 { current_password: PASSWORD, password: "short", confirm: "short" },45 "at least 12 characters",46 ],47 [48 { current_password: PASSWORD, password: NEW_PASSWORD, confirm: "different" },49 "The two new passwords differ.",50 ],51 ];52 for (const [fields, error] of cases) {53 const response = await change(fields);54 expect(response.status).toBe(400);55 expect(await response.text()).toContain(error);56 }57 expect(58 await Bun.password.verify(PASSWORD, (await loadConfig(configPath)).adminPasswordHash),59 ).toBe(true);60 });6162 test("a new password is saved, works at once, and signs every other session out", async () => {63 const other = new Browser(inst.app, "198.51.100.9");64 await other.signIn();65 const response = await change({66 current_password: PASSWORD,67 password: NEW_PASSWORD,68 confirm: NEW_PASSWORD,69 });70 expect(response.status).toBe(303);71 const saved = await loadConfig(configPath);72 expect(await Bun.password.verify(NEW_PASSWORD, saved.adminPasswordHash)).toBe(true);73 expect((await other.get("/admin")).status).toBe(401);74 expect((await operator.get("/admin")).status).toBe(200);75 expect((await new Browser(inst.app, "198.51.100.10").signIn(PASSWORD)).status).toBe(401);76 expect((await new Browser(inst.app, "198.51.100.11").signIn(NEW_PASSWORD)).status).toBe(303);77 });7879 test("signing out everywhere else keeps only this session", async () => {80 const other = new Browser(inst.app, "198.51.100.12");81 await other.signIn(NEW_PASSWORD);82 expect(await (await operator.get("/admin/account")).text()).toMatch(83 /[2-9] sessions? signed in/,84 );85 expect((await operator.post("/admin/account", { action: "sessions" })).status).toBe(303);86 expect((await other.get("/admin")).status).toBe(401);87 expect((await operator.get("/admin")).status).toBe(200);88 });8990 test("the domain is checked, saved, and used at once", async () => {91 const bad = await operator.post("/admin/account", { action: "domain", domain: "not a domain" });92 expect(bad.status).toBe(400);93 const moved = await operator.post("/admin/account", {94 action: "domain",95 domain: "code.example",96 });97 expect(moved.status).toBe(303);98 expect((await loadConfig(configPath)).domain).toBe("code.example");99 const origin = "https://code.example";100 expect((await operator.post("/admin/sync", {}, origin)).status).toBe(303);101 expect((await operator.post("/admin/sync", {})).status).toBe(403);102 const page = await (await operator.get("/admin/account")).text();103 expect(page).toContain('<link rel="canonical" href="https://code.example/admin/account">');104 });105});