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') }) })