const crypto = require('crypto'); const backupManager = require('../backup-manager'); beforeEach(() => { // Reset singleton state backupManager.history = []; backupManager.config = { backups: {}, defaultRetention: { keep: 7 } }; backupManager.running = false; for (const [, job] of backupManager.scheduledJobs.entries()) { clearInterval(job); } backupManager.scheduledJobs.clear(); }); afterAll(() => { backupManager.stop(); }); describe('calculateChecksum', () => { test('returns SHA-256 hex string', () => { const data = Buffer.from('test data'); const checksum = backupManager.calculateChecksum(data); expect(checksum).toMatch(/^[0-9a-f]{64}$/); }); test('same data produces same checksum', () => { const data = Buffer.from('consistent'); expect(backupManager.calculateChecksum(data)).toBe(backupManager.calculateChecksum(data)); }); test('different data produces different checksum', () => { const a = backupManager.calculateChecksum(Buffer.from('aaa')); const b = backupManager.calculateChecksum(Buffer.from('bbb')); expect(a).not.toBe(b); }); }); describe('compressBackup / decompressBackup', () => { test('round-trip preserves data', async () => { const original = { services: [{ id: 'test', name: 'Test' }], config: { theme: 'dark' } }; const compressed = await backupManager.compressBackup(original); const decompressed = await backupManager.decompressBackup(compressed); expect(decompressed).toEqual(original); }); test('compressed output is a Buffer', async () => { const compressed = await backupManager.compressBackup({ test: true }); expect(Buffer.isBuffer(compressed)).toBe(true); }); }); describe('encryptBackup / decryptBackup', () => { const testKey = crypto.randomBytes(32).toString('hex'); test('round-trip preserves data with valid key', async () => { const original = Buffer.from('backup data here'); const encrypted = await backupManager.encryptBackup(original, testKey); const decrypted = await backupManager.decryptBackup(encrypted, testKey); expect(decrypted.toString()).toBe(original.toString()); }); test('produces a non-empty buffer', async () => { const original = Buffer.from('backup data here'); const encrypted = await backupManager.encryptBackup(original, testKey); expect(Buffer.isBuffer(encrypted)).toBe(true); expect(encrypted.length).toBeGreaterThan(0); }); test('output differs from input', async () => { const original = Buffer.from('backup data here'); const encrypted = await backupManager.encryptBackup(original, testKey); expect(encrypted.toString()).not.toBe(original.toString()); }); test('throws on invalid encrypted format', async () => { await expect(backupManager.decryptBackup(Buffer.from('bad'), testKey)).rejects.toThrow(); }); test('throws on wrong key', async () => { const original = Buffer.from('secret data'); const encrypted = await backupManager.encryptBackup(original, testKey); const wrongKey = crypto.randomBytes(32).toString('hex'); await expect(backupManager.decryptBackup(encrypted, wrongKey)).rejects.toThrow(); }); }); describe('scheduleBackup', () => { beforeEach(() => { jest.useFakeTimers(); }); afterEach(() => { jest.useRealTimers(); }); test('parses hourly schedule', () => { backupManager.scheduleBackup('test', { schedule: 'hourly' }); expect(backupManager.scheduledJobs.has('test')).toBe(true); }); test('parses daily schedule', () => { backupManager.scheduleBackup('test', { schedule: 'daily' }); expect(backupManager.scheduledJobs.has('test')).toBe(true); }); test('parses weekly schedule', () => { backupManager.scheduleBackup('test', { schedule: 'weekly' }); expect(backupManager.scheduledJobs.has('test')).toBe(true); }); test('parses monthly schedule', () => { backupManager.scheduleBackup('test', { schedule: 'monthly' }); expect(backupManager.scheduledJobs.has('test')).toBe(true); }); test('parses custom numeric minute schedule', () => { backupManager.scheduleBackup('test', { schedule: '30' }); expect(backupManager.scheduledJobs.has('test')).toBe(true); }); test('logs error for invalid schedule', () => { backupManager.scheduleBackup('test', { schedule: 'invalid' }); expect(backupManager.scheduledJobs.has('test')).toBe(false); }); }); describe('addToHistory', () => { test('appends entry to history', () => { backupManager.addToHistory({ id: 'b1', status: 'success' }); expect(backupManager.history).toHaveLength(1); }); test('trims history to 100 entries', () => { for (let i = 0; i < 105; i++) { backupManager.addToHistory({ id: `b${i}`, status: 'success' }); } expect(backupManager.history.length).toBeLessThanOrEqual(100); }); }); describe('getHistory', () => { test('returns entries in reverse order', () => { backupManager.addToHistory({ id: 'first' }); backupManager.addToHistory({ id: 'second' }); const history = backupManager.getHistory(); expect(history[0].id).toBe('second'); expect(history[1].id).toBe('first'); }); test('respects limit parameter', () => { for (let i = 0; i < 10; i++) { backupManager.addToHistory({ id: `b${i}` }); } expect(backupManager.getHistory(3)).toHaveLength(3); }); }); describe('getConfig / updateConfig', () => { test('getConfig returns current config', () => { const config = backupManager.getConfig(); expect(config).toHaveProperty('backups'); }); test('updateConfig merges new config', () => { backupManager.updateConfig({ backups: { daily: { enabled: true, schedule: 'daily' } } }); expect(backupManager.config.backups.daily).toBeDefined(); }); }); describe('start / stop', () => { test('start sets running flag', () => { backupManager.start(); expect(backupManager.running).toBe(true); backupManager.stop(); }); test('start is idempotent', () => { backupManager.start(); backupManager.start(); expect(backupManager.running).toBe(true); backupManager.stop(); }); test('stop clears running flag and jobs', () => { backupManager.start(); backupManager.stop(); expect(backupManager.running).toBe(false); expect(backupManager.scheduledJobs.size).toBe(0); }); }); describe('cleanupOldBackups', () => { test('keeps configured number of backups', async () => { // Add 5 successful backups for 'daily' for (let i = 0; i < 5; i++) { backupManager.history.push({ id: `daily-${i}`, name: 'daily', status: 'success', timestamp: new Date(Date.now() - i * 86400000).toISOString(), locations: [{ type: 'local', path: `/tmp/fake-${i}.backup` }] }); } await backupManager.cleanupOldBackups('daily', { keep: 3 }); const remaining = backupManager.history.filter(b => b.name === 'daily' && b.status === 'success'); expect(remaining.length).toBe(3); }); });