106 lines
3.5 KiB
TypeScript
106 lines
3.5 KiB
TypeScript
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<ReturnType<typeof createDb>['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: '[email protected]', 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: '[email protected]' })
|
|
})
|
|
|
|
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: '[email protected]', 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, '[email protected]')?.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)
|
|
})
|
|
})
|