import { existsSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' import Database from 'better-sqlite3' import { afterEach, describe, expect, it } from 'vitest' import { backupDatabase, checkIntegrity, closeDb, createDb, createUser, getUserByEmail, listBackups, migrateSchema, } from '../src/index.js' const dirs: string[] = [] const dbs: Array['sqlite']> = [] afterEach(() => { for (const s of dbs.splice(0)) closeDb(s) for (const d of dirs.splice(0)) rmSync(d, { recursive: true, force: true }) }) function fileDb() { const dir = mkdtempSync(join(tmpdir(), 'ap-bk-')) dirs.push(dir) const r = createDb(join(dir, 'app.db')) migrateSchema(r.sqlite) dbs.push(r.sqlite) return { dir, ...r } } describe('backupDatabase', () => { it('создаёт консистентную копию с данными из WAL', async () => { const { dir, db, sqlite } = fileDb() createUser(db, { email: 'a@x.io', name: 'A', passwordHash: 'h', isAdmin: false }) const res = await backupDatabase(sqlite, { dir: join(dir, 'backups'), keep: 3 }) expect(existsSync(res.file)).toBe(true) expect(checkIntegrity(res.file)).toBe(true) const copy = new Database(res.file, { readonly: true }) const row = copy.prepare('SELECT email FROM users').get() copy.close() expect(row).toEqual({ email: 'a@x.io' }) }) it('бэкап во время записи остаётся валидным', async () => { const { dir, db, sqlite } = fileDb() const writer = setInterval(() => { try { createUser(db, { email: `u${Math.random()}@x.io`, name: 'U', passwordHash: 'h', isAdmin: false, }) } catch { /* БД могла закрыться */ } }, 1) const res = await backupDatabase(sqlite, { dir, keep: 3 }) clearInterval(writer) expect(checkIntegrity(res.file)).toBe(true) }) it('ротация оставляет keep последних', async () => { const { dir, sqlite } = fileDb() const bdir = join(dir, 'b') for (let i = 0; i < 5; i++) { await backupDatabase(sqlite, { dir: bdir, keep: 2, now: new Date(Date.UTC(2026, 0, 1, 0, 0, i)), }) } const left = listBackups(bdir) expect(left).toHaveLength(2) expect(left[0]).toContain('000004') }) it('восстановленная копия открывается и содержит данные', async () => { const { dir, db, sqlite } = fileDb() createUser(db, { email: 'r@x.io', name: 'R', passwordHash: 'h', isAdmin: false }) const res = await backupDatabase(sqlite, { dir, keep: 1 }) const restored = createDb(res.file) dbs.push(restored.sqlite) migrateSchema(restored.sqlite) expect(getUserByEmail(restored.db, 'r@x.io')?.name).toBe('R') }) it('битый файл не проходит integrity_check', () => { const dir = mkdtempSync(join(tmpdir(), 'ap-bad-')) dirs.push(dir) const p = join(dir, 'bad.db') writeFileSync(p, 'not a sqlite file'.repeat(100)) expect(() => checkIntegrity(p)).toThrow() }) it('ошибка бэкапа не оставляет .partial', async () => { const { dir, sqlite } = fileDb() closeDb(sqlite) await expect(backupDatabase(sqlite, { dir: join(dir, 'x'), keep: 1 })).rejects.toThrow() expect(listBackups(join(dir, 'x'))).toHaveLength(0) }) })