import { afterEach, beforeEach, expect, test } from "bun:test"; import { mkdtemp, readdir, rm, stat } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { forEachDialect } from "@sttracker/db/testing"; import { backupHandler, runBackup } from "../src/backup.ts"; import { makeUser } from "./user-layer-fixtures.ts"; forEachDialect("backup", (getDb) => { let dir: string; beforeEach(async () => { dir = await mkdtemp(join(tmpdir(), "sttracker-bak-")); }); afterEach(async () => { await rm(dir, { recursive: true, force: true }); }); test("backup writes a bundle per user (and the db on sqlite)", async () => { const db = getDb(); await makeUser(db, "ada"); await makeUser(db, "bob"); const out = await runBackup(db, dir, { keep: 14 }); expect(out.startsWith(join(dir, "backups"))).toBe(true); const files = (await readdir(out)).sort(); expect(files).toEqual(db.dialect === "sqlite" ? ["ada.zip", "bob.zip", "sttracker.db"] : ["ada.zip", "bob.zip"]); }); test("backup prunes to keep", async () => { const db = getDb(); await makeUser(db, "ada"); const made: string[] = []; for (let i = 0; i < 4; i++) made.push(await runBackup(db, dir, { keep: 2 })); const left = (await readdir(join(dir, "backups"))).sort(); expect(left).toHaveLength(2); expect(left.map((n) => join(dir, "backups", n))).toEqual(made.slice(2)); }); test("backupHandler runs a backup", async () => { const db = getDb(); await makeUser(db, "ada"); await backupHandler({ db, dataDir: dir, keep: 3 })(); expect(await readdir(join(dir, "backups"))).toHaveLength(1); }); test("backup files are private and no partial directory remains", async () => { const db = getDb(); await makeUser(db, "ada"); const out = await runBackup(db, dir, { keep: 5 }); expect((await stat(out)).mode & 0o777).toBe(0o700); expect((await stat(join(out, "ada.zip"))).mode & 0o777).toBe(0o600); expect((await readdir(join(dir, "backups"))).some((n) => n.endsWith(".tmp"))).toBe(false); }); });