import { describe, it, beforeEach, afterEach } from 'node:test'; import assert from 'node:assert/strict'; import { join } from 'node:path'; import { writeFile, readFile, mkdir, rm, stat } from 'node:fs/promises'; import { mkdirSync, writeFileSync } from 'node:fs'; import { tmpdir, homedir } from 'node:os'; import { createBackup, getBackupDir, checksum } from '../../scanners/lib/backup.mjs'; import { listBackups, restoreBackup, deleteBackup } from '../../scanners/rollback-engine.mjs'; /** Create a temp file and back it up, returning paths and content. */ async function setupTestBackup() { const tmpDir = join(tmpdir(), `config-audit-rb-test-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`); mkdirSync(tmpDir, { recursive: true }); const testFile = join(tmpDir, 'test-settings.json'); const originalContent = '{"original": true, "key": "value"}'; writeFileSync(testFile, originalContent); const backup = createBackup([testFile]); // Now modify the file to simulate a change writeFileSync(testFile, '{"modified": true}'); return { tmpDir, testFile, originalContent, backup }; } describe('listBackups', () => { it('returns an array of backups', async () => { const result = await listBackups(); assert.ok(Array.isArray(result.backups), 'Should return backups array'); }); it('backups are sorted newest first', async () => { const result = await listBackups(); if (result.backups.length >= 2) { assert.ok(result.backups[0].id >= result.backups[1].id, 'First backup should be newer'); } }); it('each backup has required fields', async () => { const { tmpDir, backup } = await setupTestBackup(); try { const result = await listBackups(); const found = result.backups.find(b => b.id === backup.backupId); assert.ok(found, 'Should find our test backup'); assert.ok(found.id, 'Backup should have id'); assert.ok(found.createdAt, 'Backup should have createdAt'); assert.ok(Array.isArray(found.files), 'Backup should have files array'); assert.ok(found.files.length > 0, 'Backup should have at least one file'); assert.ok(found.files[0].originalPath, 'File entry should have originalPath'); assert.ok(found.files[0].checksum, 'File entry should have checksum'); } finally { await rm(tmpDir, { recursive: true, force: true }); } }); }); describe('restoreBackup', () => { let tmpDir, testFile, originalContent, backup; beforeEach(async () => { ({ tmpDir, testFile, originalContent, backup } = await setupTestBackup()); }); afterEach(async () => { if (tmpDir) await rm(tmpDir, { recursive: true, force: true }); // Cleanup our test backup try { await deleteBackup(backup.backupId); } catch {} }); it('restores files to original content', async () => { const result = await restoreBackup(backup.backupId); assert.ok(result.restored.length > 0, 'Should restore at least one file'); assert.strictEqual(result.failed.length, 0, 'No failures'); const restoredContent = await readFile(testFile, 'utf-8'); assert.strictEqual(restoredContent, originalContent, 'Content should match original'); }); it('verifies checksums after restore', async () => { const result = await restoreBackup(backup.backupId, { verify: true }); for (const r of result.restored) { assert.strictEqual(r.status, 'restored'); } }); it('dry-run returns plan without writing', async () => { const result = await restoreBackup(backup.backupId, { dryRun: true }); assert.ok(result.restored.length > 0); for (const r of result.restored) { assert.strictEqual(r.status, 'dry-run'); } // File should still be modified const content = await readFile(testFile, 'utf-8'); assert.strictEqual(content, '{"modified": true}', 'File should not be restored in dry-run'); }); it('throws for invalid backup-id', async () => { await assert.rejects( () => restoreBackup('nonexistent_99999999_999999'), { message: /Backup not found/ }, ); }); }); describe('deleteBackup', () => { it('deletes an existing backup', async () => { const { tmpDir, backup } = await setupTestBackup(); try { const result = await deleteBackup(backup.backupId); assert.strictEqual(result.deleted, true); // Verify it's gone from the list const list = await listBackups(); const found = list.backups.find(b => b.id === backup.backupId); assert.ok(!found, 'Deleted backup should not appear in list'); } finally { await rm(tmpDir, { recursive: true, force: true }); } }); it('returns error for nonexistent backup', async () => { const result = await deleteBackup('nonexistent_99999999_999999'); assert.strictEqual(result.deleted, false); assert.ok(result.error, 'Should have error message'); }); });