134 lines
4.8 KiB
TypeScript
134 lines
4.8 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
|
import os from 'os'
|
|
import path from 'path'
|
|
import { promises as fs } from 'fs'
|
|
import {
|
|
getBackupDirectoryForDataFile,
|
|
resolveDataFileBackupPath,
|
|
listDataFileBackups,
|
|
restoreDataFileBackup,
|
|
writeDataFileWithRotation
|
|
} from '../server/utils/data-file-rotation.js'
|
|
|
|
describe('Data file rotation utility', () => {
|
|
let tempRoot = ''
|
|
let backupRoot = ''
|
|
let previousBackupDir = ''
|
|
let dataFile = ''
|
|
|
|
beforeEach(async () => {
|
|
tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'harheimertc-data-rotation-'))
|
|
backupRoot = path.join(tempRoot, 'backup-store')
|
|
dataFile = path.join(tempRoot, 'server', 'data', 'users.json')
|
|
|
|
previousBackupDir = process.env.DATA_FILE_BACKUP_DIR || ''
|
|
process.env.DATA_FILE_BACKUP_DIR = backupRoot
|
|
})
|
|
|
|
afterEach(async () => {
|
|
if (previousBackupDir) {
|
|
process.env.DATA_FILE_BACKUP_DIR = previousBackupDir
|
|
} else {
|
|
delete process.env.DATA_FILE_BACKUP_DIR
|
|
}
|
|
|
|
if (tempRoot) {
|
|
await fs.rm(tempRoot, { recursive: true, force: true })
|
|
}
|
|
})
|
|
|
|
it('writes initial file without creating backup', async () => {
|
|
const result = await writeDataFileWithRotation(dataFile, 'v1', { maxBackups: 5 })
|
|
|
|
expect(result.changed).toBe(true)
|
|
expect(result.backupPath).toBe(null)
|
|
|
|
const content = await fs.readFile(dataFile, 'utf-8')
|
|
expect(content).toBe('v1')
|
|
|
|
const backups = await listDataFileBackups(dataFile)
|
|
expect(backups).toEqual([])
|
|
})
|
|
|
|
it('creates backup from previous content on change', async () => {
|
|
await writeDataFileWithRotation(dataFile, 'v1', { maxBackups: 5 })
|
|
const result = await writeDataFileWithRotation(dataFile, 'v2', { maxBackups: 5 })
|
|
|
|
expect(result.changed).toBe(true)
|
|
expect(result.backupPath).toBeTruthy()
|
|
|
|
const backups = await listDataFileBackups(dataFile)
|
|
expect(backups.length).toBe(1)
|
|
|
|
const backupDir = getBackupDirectoryForDataFile(dataFile)
|
|
const backupContent = await fs.readFile(resolveDataFileBackupPath(backupDir, backups[0]), 'utf-8')
|
|
expect(backupContent).toBe('v1')
|
|
|
|
const currentContent = await fs.readFile(dataFile, 'utf-8')
|
|
expect(currentContent).toBe('v2')
|
|
})
|
|
|
|
it('does not create backup when content is unchanged', async () => {
|
|
await writeDataFileWithRotation(dataFile, 'stable', { maxBackups: 5 })
|
|
await writeDataFileWithRotation(dataFile, 'next', { maxBackups: 5 })
|
|
|
|
const before = await listDataFileBackups(dataFile)
|
|
const result = await writeDataFileWithRotation(dataFile, 'next', { maxBackups: 5 })
|
|
const after = await listDataFileBackups(dataFile)
|
|
|
|
expect(result.changed).toBe(false)
|
|
expect(result.backupPath).toBe(null)
|
|
expect(after).toEqual(before)
|
|
})
|
|
|
|
it('rotates old backups and keeps configured maximum', async () => {
|
|
await writeDataFileWithRotation(dataFile, 'v1', { maxBackups: 2 })
|
|
await writeDataFileWithRotation(dataFile, 'v2', { maxBackups: 2 })
|
|
await writeDataFileWithRotation(dataFile, 'v3', { maxBackups: 2 })
|
|
await writeDataFileWithRotation(dataFile, 'v4', { maxBackups: 2 })
|
|
|
|
const backups = await listDataFileBackups(dataFile)
|
|
expect(backups.length).toBe(2)
|
|
|
|
const backupDir = getBackupDirectoryForDataFile(dataFile)
|
|
const backupContents = await Promise.all(
|
|
backups.map((name) => fs.readFile(resolveDataFileBackupPath(backupDir, name), 'utf-8'))
|
|
)
|
|
|
|
expect(backupContents).toEqual(expect.arrayContaining(['v2', 'v3']))
|
|
expect(backupContents).not.toContain('v1')
|
|
})
|
|
|
|
it('restores selected backup and keeps pre-restore state as backup', async () => {
|
|
await writeDataFileWithRotation(dataFile, 'v1', { maxBackups: 10 })
|
|
await writeDataFileWithRotation(dataFile, 'v2', { maxBackups: 10 })
|
|
await writeDataFileWithRotation(dataFile, 'v3', { maxBackups: 10 })
|
|
|
|
const beforeRestoreBackups = await listDataFileBackups(dataFile)
|
|
const backupDir = getBackupDirectoryForDataFile(dataFile)
|
|
|
|
const resolved = await Promise.all(
|
|
beforeRestoreBackups.map(async (name) => ({
|
|
name,
|
|
content: await fs.readFile(resolveDataFileBackupPath(backupDir, name), 'utf-8')
|
|
}))
|
|
)
|
|
const v1Entry = resolved.find((entry) => entry.content === 'v1')
|
|
expect(v1Entry).toBeTruthy()
|
|
|
|
const restoreResult = await restoreDataFileBackup(dataFile, v1Entry!.name, { maxBackups: 10 })
|
|
expect(restoreResult.changed).toBe(true)
|
|
expect(restoreResult.backupPath).toBeTruthy()
|
|
|
|
const restoredContent = await fs.readFile(dataFile, 'utf-8')
|
|
expect(restoredContent).toBe('v1')
|
|
|
|
const afterRestoreBackups = await listDataFileBackups(dataFile)
|
|
const afterContents = await Promise.all(
|
|
afterRestoreBackups.map((name) => fs.readFile(resolveDataFileBackupPath(backupDir, name), 'utf-8'))
|
|
)
|
|
|
|
expect(afterContents).toContain('v3')
|
|
})
|
|
})
|