// Covers the console's Account section (June, 2026-09-30): the admin password, other sessions and // the domain, all changed without touching kepr.toml by hand, and applied without a restart. import { beforeAll, describe, expect, test } from "bun:test"; import { join } from "node:path"; import { createApp } from "../src/server/app.ts"; import { loadConfig, saveConfig } from "../src/server/config.ts"; import { Browser, type Instance, instance, PASSWORD } from "./browser.ts"; import { makeRepos } from "./fixture.ts"; const NEW_PASSWORD = "a much longer passphrase now"; describe("the Account section", () => { let inst: Instance; let operator: Browser; let configPath: string; const change = (fields: Record) => operator.post("/admin/account", { action: "password", ...fields }); beforeAll(async () => { const repos = await makeRepos(["app"]); inst = await instance(repos.bucketDir); configPath = join(inst.dataDir, "kepr.toml"); await saveConfig(configPath, inst.config); inst.app = createApp(inst.config, inst.kepr, undefined, configPath); operator = new Browser(inst.app); await operator.signIn(); }); test("lists the sections's forms and never the hash", async () => { const page = await (await operator.get("/admin/account")).text(); expect(page).toContain('name="current_password"'); expect(page).toContain('value="kepr.example"'); expect(page).not.toContain("$argon2id$"); }); test("refuses a wrong current password, a short one, or a mismatch", async () => { const cases: [Record, string][] = [ [ { current_password: "wrong", password: NEW_PASSWORD, confirm: NEW_PASSWORD }, "The current password is not right.", ], [ { current_password: PASSWORD, password: "short", confirm: "short" }, "at least 12 characters", ], [ { current_password: PASSWORD, password: NEW_PASSWORD, confirm: "different" }, "The two new passwords differ.", ], ]; for (const [fields, error] of cases) { const response = await change(fields); expect(response.status).toBe(400); expect(await response.text()).toContain(error); } expect( await Bun.password.verify(PASSWORD, (await loadConfig(configPath)).adminPasswordHash), ).toBe(true); }); test("a new password is saved, works at once, and signs every other session out", async () => { const other = new Browser(inst.app, "198.51.100.9"); await other.signIn(); const response = await change({ current_password: PASSWORD, password: NEW_PASSWORD, confirm: NEW_PASSWORD, }); expect(response.status).toBe(303); const saved = await loadConfig(configPath); expect(await Bun.password.verify(NEW_PASSWORD, saved.adminPasswordHash)).toBe(true); expect((await other.get("/admin")).status).toBe(401); expect((await operator.get("/admin")).status).toBe(200); expect((await new Browser(inst.app, "198.51.100.10").signIn(PASSWORD)).status).toBe(401); expect((await new Browser(inst.app, "198.51.100.11").signIn(NEW_PASSWORD)).status).toBe(303); }); test("signing out everywhere else keeps only this session", async () => { const other = new Browser(inst.app, "198.51.100.12"); await other.signIn(NEW_PASSWORD); expect(await (await operator.get("/admin/account")).text()).toMatch( /[2-9] sessions? signed in/, ); expect((await operator.post("/admin/account", { action: "sessions" })).status).toBe(303); expect((await other.get("/admin")).status).toBe(401); expect((await operator.get("/admin")).status).toBe(200); }); test("the domain is checked, saved, and used at once", async () => { const bad = await operator.post("/admin/account", { action: "domain", domain: "not a domain" }); expect(bad.status).toBe(400); const moved = await operator.post("/admin/account", { action: "domain", domain: "code.example", }); expect(moved.status).toBe(303); expect((await loadConfig(configPath)).domain).toBe("code.example"); const origin = "https://code.example"; expect((await operator.post("/admin/sync", {}, origin)).status).toBe(303); expect((await operator.post("/admin/sync", {})).status).toBe(403); const page = await (await operator.get("/admin/account")).text(); expect(page).toContain(''); }); });