backupworkspace.test.ts

3.4 KB · raw

1// Covers §24's backup workspace: every kept backup listed and downloadable, back up now, the last2// failure until a backup succeeds, and the written restore steps.34import { afterAll, beforeAll, describe, expect, setSystemTime, test } from "bun:test";5import { unlinkSync, writeFileSync } from "node:fs";6import { backups, makeBackup, startBackupLoop } from "../src/server/backup.ts";7import { setting } from "../src/server/db.ts";8import { Browser, type Instance, instance } from "./browser.ts";9import { makeRepos } from "./fixture.ts";1011describe("the backup workspace (§24)", () => {12	let inst: Instance;13	let operator: Browser;14	let clock = Date.parse("2026-09-27T03:00:00Z");15	const page = async () => (await operator.get("/admin?section=backups")).text();1617	beforeAll(async () => {18		setSystemTime(new Date(clock));19		const repos = await makeRepos(["app"]);20		inst = await instance(repos.bucketDir);21		for (let day = 3; day > 0; day--) {22			makeBackup(inst.kepr, inst.dataDir, clock - day * 86_400_000);23		}24		operator = new Browser(inst.app);25		await operator.signIn();26	});27	afterAll(() => setSystemTime());2829	test("lists every kept backup, newest first, with its size and its own download", async () => {30		const text = await page();31		const listed = [...text.matchAll(/href="\/admin\/backup\?name=(\d{8}T\d{6}Z)"/g)].map(32			(match) => match[1],33		);34		expect(listed).toEqual(["20260926T030000Z", "20260925T030000Z", "20260924T030000Z"]);35		expect(text).toMatch(/2026-09-24 03:00:00 UTC<\/a>\s*<span class="muted">[\d.]+ KB<\/span>/);36		const download = await operator.request("/admin/backup?name=20260925T030000Z");37		expect(download.status).toBe(200);38		expect(download.headers.get("content-disposition")).toBe(39			'attachment; filename="kepr-backup-20260925T030000Z.tar.gz"',40		);41		expect((await operator.request("/admin/backup?name=20260101T000000Z")).status).toBe(404);42		expect((await operator.request("/admin/backup?name=../kepr.db")).status).toBe(404);43	});4445	test("says how to restore one by hand", async () => {46		const text = await page();47		expect(text).toContain("Stop Kepr.");48		expect(text).toContain("kepr.db and assets/");49	});5051	test("back up now makes one at once, even with today's done", async () => {52		clock += 3600_000;53		setSystemTime(new Date(clock));54		const before = backups(inst.dataDir).length;55		const made = await operator.post("/admin/backup", {});56		expect(made.status).toBe(303);57		expect(made.headers.get("location")).toBe("/admin?section=backups&saved=1");58		expect(backups(inst.dataDir).length).toBe(before + 1);59		expect(backups(inst.dataDir).at(-1)).toBe("20260927T040000Z");60	});6162	test("the last failure stays shown until a backup succeeds", async () => {63		const saved = `${inst.dataDir}/backups-kept`;64		Bun.spawnSync(["mv", `${inst.dataDir}/backups`, saved]);65		writeFileSync(`${inst.dataDir}/backups`, "");66		startBackupLoop(inst.kepr, inst.config);67		expect(setting(inst.kepr, "backup_error")).toContain("ENOTDIR");68		const failed = await operator.post("/admin/backup", {});69		expect(failed.status).toBe(500);70		expect(await page()).toContain("The last backup failed:");71		unlinkSync(`${inst.dataDir}/backups`);72		Bun.spawnSync(["mv", saved, `${inst.dataDir}/backups`]);73		expect((await operator.post("/admin/backup", {})).status).toBe(303);74		expect(setting(inst.kepr, "backup_error")).toBeNull();75		expect(await page()).not.toContain("The last backup failed:");76	});77});