account.test.ts

4.2 KB · raw

1// Covers the console's Account section (June, 2026-09-30): the admin password, other sessions and2// the domain, all changed without touching kepr.toml by hand, and applied without a restart.34import { 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});