// Backup Manager Tests // Validates backup/restore lifecycle for DashCaddy configurations jest.mock('fs'); jest.mock('child_process'); jest.mock('../credential-manager', () => ({ exportBackup: jest.fn().mockReturnValue({ encrypted: 'cred-data' }), importBackup: jest.fn() })); jest.mock('../resource-monitor', () => ({ exportStats: jest.fn().mockReturnValue({ stats: [{ cpu: 10 }] }), importStats: jest.fn() })); const fs = require('fs'); const crypto = require('crypto'); const credentialManager = require('../credential-manager'); const resourceMonitor = require('../resource-monitor'); // Setup defaults BEFORE requiring singleton (constructor calls loadConfig/loadHistory) fs.existsSync.mockReturnValue(false); fs.readFileSync.mockReturnValue('{}'); fs.writeFileSync.mockReturnValue(undefined); fs.mkdirSync.mockReturnValue(undefined); fs.unlinkSync.mockReturnValue(undefined); const backupManager = require('../backup-manager'); beforeEach(() => { jest.clearAllMocks(); jest.useFakeTimers(); // Restore defaults fs.existsSync.mockReturnValue(false); fs.readFileSync.mockReturnValue('{}'); fs.writeFileSync.mockReturnValue(undefined); fs.mkdirSync.mockReturnValue(undefined); fs.unlinkSync.mockReturnValue(undefined); // Reset internal state backupManager.history = []; backupManager.config = { backups: {}, defaultRetention: { keep: 7 } }; backupManager.running = false; // Clear all scheduled jobs directly (stop() only clears when running=true) for (const [, job] of backupManager.scheduledJobs.entries()) { clearInterval(job); } backupManager.scheduledJobs.clear(); }); afterEach(() => { backupManager.stop(); jest.useRealTimers(); }); describe('BackupManager — backup/restore lifecycle', () => { describe('constructor and config', () => { it('starts with empty config when no config file exists', () => { const config = backupManager.getConfig(); expect(config.backups).toEqual({}); expect(config.defaultRetention).toEqual({ keep: 7 }); }); it('loadConfig returns saved config when file exists', () => { const savedConfig = { backups: { daily: { enabled: true, schedule: 'daily' } }, defaultRetention: { keep: 14 } }; fs.existsSync.mockReturnValue(true); fs.readFileSync.mockReturnValue(JSON.stringify(savedConfig)); const config = backupManager.loadConfig(); expect(config.backups.daily).toBeDefined(); expect(config.defaultRetention.keep).toBe(14); }); it('loadConfig returns defaults on error', () => { fs.existsSync.mockReturnValue(true); fs.readFileSync.mockImplementation(() => { throw new Error('read error'); }); const config = backupManager.loadConfig(); expect(config.backups).toEqual({}); }); it('loadHistory returns empty array when no file', () => { fs.existsSync.mockReturnValue(false); expect(backupManager.loadHistory()).toEqual([]); }); it('loadHistory loads saved entries', () => { const history = [{ id: 'test-1', status: 'success' }]; fs.existsSync.mockReturnValue(true); fs.readFileSync.mockReturnValue(JSON.stringify(history)); expect(backupManager.loadHistory()).toEqual(history); }); }); describe('start/stop scheduler', () => { it('does nothing on double start', () => { backupManager.start(); backupManager.start(); // should not throw expect(backupManager.running).toBe(true); }); it('does nothing on stop when not running', () => { backupManager.stop(); // should not throw expect(backupManager.running).toBe(false); }); it('clears scheduled jobs on stop', () => { backupManager.scheduledJobs.set('test', setInterval(() => {}, 10000)); backupManager.running = true; backupManager.stop(); expect(backupManager.scheduledJobs.size).toBe(0); expect(backupManager.running).toBe(false); }); }); describe('scheduleBackup intervals', () => { it('schedules hourly backup', () => { backupManager.scheduleBackup('test', { schedule: 'hourly' }); expect(backupManager.scheduledJobs.has('test')).toBe(true); }); it('schedules daily backup', () => { backupManager.scheduleBackup('test', { schedule: 'daily' }); expect(backupManager.scheduledJobs.has('test')).toBe(true); }); it('schedules weekly backup', () => { backupManager.scheduleBackup('test', { schedule: 'weekly' }); expect(backupManager.scheduledJobs.has('test')).toBe(true); }); it('schedules monthly backup', () => { backupManager.scheduleBackup('test', { schedule: 'monthly' }); expect(backupManager.scheduledJobs.has('test')).toBe(true); }); it('accepts custom interval in minutes', () => { backupManager.scheduleBackup('test', { schedule: '30' }); expect(backupManager.scheduledJobs.has('test')).toBe(true); }); it('rejects invalid schedule', () => { backupManager.scheduleBackup('test', { schedule: 'bogus' }); expect(backupManager.scheduledJobs.has('test')).toBe(false); }); }); describe('compress/decompress', () => { it('round-trips data through gzip', async () => { const original = { version: '1.0', data: { services: [{ id: 'plex' }] } }; const compressed = await backupManager.compressBackup(original); expect(Buffer.isBuffer(compressed)).toBe(true); const decompressed = await backupManager.decompressBackup(compressed); expect(decompressed).toEqual(original); }); it('compressed output is smaller than JSON', async () => { const data = { bigArray: Array(100).fill({ id: 'test', name: 'test-service' }) }; const compressed = await backupManager.compressBackup(data); expect(compressed.length).toBeLessThan(JSON.stringify(data).length); }); }); describe('encrypt/decrypt (AES-256-GCM)', () => { const testKey = crypto.randomBytes(32).toString('hex'); it('round-trips data through encryption', async () => { const original = Buffer.from('DashCaddy backup data'); const encrypted = await backupManager.encryptBackup(original, testKey); const decrypted = await backupManager.decryptBackup(encrypted, testKey); expect(decrypted.toString()).toBe('DashCaddy backup data'); }); it('encrypted format is iv:authTag:ciphertext (base64)', async () => { const data = Buffer.from('test'); const encrypted = await backupManager.encryptBackup(data, testKey); const parts = encrypted.toString().split(':'); expect(parts.length).toBeGreaterThanOrEqual(3); }); it('rejects tampered data (auth tag mismatch)', async () => { const data = Buffer.from('test'); const encrypted = await backupManager.encryptBackup(data, testKey); // Corrupt the first character of the IV const str = encrypted.toString(); const tampered = Buffer.from('X' + str.substring(1)); await expect(backupManager.decryptBackup(tampered, testKey)) .rejects.toThrow(); }); it('rejects wrong key', async () => { const data = Buffer.from('test'); const encrypted = await backupManager.encryptBackup(data, testKey); const wrongKey = crypto.randomBytes(32).toString('hex'); await expect(backupManager.decryptBackup(encrypted, wrongKey)) .rejects.toThrow(); }); it('rejects invalid format (fewer than 3 parts)', async () => { await expect(backupManager.decryptBackup(Buffer.from('onlyonepart'), testKey)) .rejects.toThrow('Invalid encrypted backup format'); }); }); describe('calculateChecksum', () => { it('returns SHA-256 hex digest', () => { const data = Buffer.from('test data'); const checksum = backupManager.calculateChecksum(data); expect(checksum).toMatch(/^[a-f0-9]{64}$/); }); it('same data produces same checksum', () => { const data = Buffer.from('DashCaddy'); expect(backupManager.calculateChecksum(data)) .toBe(backupManager.calculateChecksum(data)); }); it('different data produces different checksum', () => { expect(backupManager.calculateChecksum(Buffer.from('A'))) .not.toBe(backupManager.calculateChecksum(Buffer.from('B'))); }); }); describe('saveToLocal', () => { it('creates backup directory if missing', async () => { fs.existsSync.mockReturnValue(false); await backupManager.saveToLocal(Buffer.from('data'), { path: '/custom/backups' }, 'test-123'); expect(fs.mkdirSync).toHaveBeenCalledWith('/custom/backups', { recursive: true }); }); it('writes backup file with correct name', async () => { fs.existsSync.mockReturnValue(true); const result = await backupManager.saveToLocal(Buffer.from('data'), {}, 'daily-1234'); expect(fs.writeFileSync).toHaveBeenCalledWith( expect.stringContaining('daily-1234.backup'), expect.any(Buffer) ); expect(result.type).toBe('local'); expect(result.size).toBe(4); }); }); describe('verifyBackup', () => { it('passes when checksum matches', async () => { const data = Buffer.from('verified'); const checksum = crypto.createHash('sha256').update(data).digest('hex'); fs.readFileSync.mockReturnValue(data); const result = await backupManager.verifyBackup({ type: 'local', path: '/backup.dat' }, checksum); expect(result).toBe(true); }); it('throws on checksum mismatch', async () => { fs.readFileSync.mockReturnValue(Buffer.from('tampered')); await expect(backupManager.verifyBackup( { type: 'local', path: '/backup.dat' }, 'wrong-checksum' )).rejects.toThrow('checksum mismatch'); }); }); describe('history management', () => { it('addToHistory appends and saves', () => { backupManager.addToHistory({ id: 'test-1', status: 'success' }); expect(backupManager.getHistory()).toHaveLength(1); expect(fs.writeFileSync).toHaveBeenCalled(); }); it('caps history at 100 entries', () => { for (let i = 0; i < 110; i++) { backupManager.addToHistory({ id: `test-${i}`, status: 'success' }); } expect(backupManager.history.length).toBe(100); }); it('getHistory returns newest first', () => { backupManager.addToHistory({ id: 'old', status: 'success' }); backupManager.addToHistory({ id: 'new', status: 'success' }); const history = backupManager.getHistory(); expect(history[0].id).toBe('new'); expect(history[1].id).toBe('old'); }); it('getHistory respects limit', () => { for (let i = 0; i < 10; i++) { backupManager.addToHistory({ id: `test-${i}`, status: 'success' }); } expect(backupManager.getHistory(3)).toHaveLength(3); }); }); describe('updateConfig', () => { it('merges new config and saves', () => { backupManager.updateConfig({ customSetting: true }); expect(backupManager.getConfig().customSetting).toBe(true); expect(fs.writeFileSync).toHaveBeenCalled(); }); it('restarts scheduler on config update', () => { backupManager.start(); expect(backupManager.running).toBe(true); backupManager.updateConfig({ backups: {} }); // Should still be running after restart expect(backupManager.running).toBe(true); }); }); describe('backupServices / backupConfig', () => { it('reads services.json when it exists', () => { fs.existsSync.mockReturnValue(true); fs.readFileSync.mockReturnValue(JSON.stringify([{ id: 'plex' }])); const result = backupManager.backupServices(); expect(result).toEqual([{ id: 'plex' }]); }); it('returns null when services.json missing', () => { fs.existsSync.mockReturnValue(false); expect(backupManager.backupServices()).toBeNull(); }); it('returns null on read error', () => { fs.existsSync.mockReturnValue(true); fs.readFileSync.mockImplementation(() => { throw new Error('read error'); }); expect(backupManager.backupServices()).toBeNull(); }); it('reads config.json when it exists', () => { fs.existsSync.mockReturnValue(true); fs.readFileSync.mockReturnValue(JSON.stringify({ tld: '.sami' })); const result = backupManager.backupConfig(); expect(result).toEqual({ tld: '.sami' }); }); }); describe('cleanupOldBackups', () => { it('deletes backups beyond retention limit', async () => { // Add 5 successful backups for (let i = 0; i < 5; i++) { backupManager.history.push({ id: `daily-${i}`, name: 'daily', status: 'success', timestamp: new Date(2026, 0, i + 1).toISOString(), locations: [{ type: 'local', path: `/backups/daily-${i}.backup` }] }); } fs.existsSync.mockReturnValue(true); await backupManager.cleanupOldBackups('daily', { keep: 2 }); // Should delete 3 oldest expect(fs.unlinkSync).toHaveBeenCalledTimes(3); // History should have 2 remaining for 'daily' const remaining = backupManager.history.filter(b => b.name === 'daily'); expect(remaining).toHaveLength(2); }); it('keeps all when under retention limit', async () => { backupManager.history.push({ id: 'daily-1', name: 'daily', status: 'success', timestamp: new Date().toISOString(), locations: [{ type: 'local', path: '/backups/daily-1.backup' }] }); await backupManager.cleanupOldBackups('daily', { keep: 7 }); expect(fs.unlinkSync).not.toHaveBeenCalled(); }); }); describe('backupCredentials / backupStats', () => { it('returns credential export data', () => { const result = backupManager.backupCredentials(); expect(result).toEqual({ encrypted: 'cred-data' }); expect(credentialManager.exportBackup).toHaveBeenCalled(); }); it('returns null on credential export error', () => { credentialManager.exportBackup.mockImplementationOnce(() => { throw new Error('no key'); }); expect(backupManager.backupCredentials()).toBeNull(); }); it('returns stats export data', () => { const result = backupManager.backupStats(); expect(result).toEqual({ stats: [{ cpu: 10 }] }); expect(resourceMonitor.exportStats).toHaveBeenCalled(); }); it('returns null on stats export error', () => { resourceMonitor.exportStats.mockImplementationOnce(() => { throw new Error('no stats'); }); expect(backupManager.backupStats()).toBeNull(); }); }); describe('createBackupData', () => { it('includes all sources when "all" specified', async () => { fs.existsSync.mockReturnValue(true); fs.readFileSync.mockImplementation((filePath) => { if (typeof filePath === 'string') { if (filePath.includes('services')) return JSON.stringify([{ id: 'plex' }]); if (filePath.includes('config')) return JSON.stringify({ tld: '.sami' }); } return '{}'; }); const data = await backupManager.createBackupData(['all']); expect(data.version).toBe('1.0'); expect(data.data.services).toEqual([{ id: 'plex' }]); expect(data.data.config).toEqual({ tld: '.sami' }); expect(data.data.credentials).toEqual({ encrypted: 'cred-data' }); expect(data.data.stats).toEqual({ stats: [{ cpu: 10 }] }); }); it('includes only credentials when specified', async () => { const data = await backupManager.createBackupData(['credentials']); expect(data.data.credentials).toEqual({ encrypted: 'cred-data' }); expect(data.data.services).toBeUndefined(); }); it('includes only stats when specified', async () => { const data = await backupManager.createBackupData(['stats']); expect(data.data.stats).toEqual({ stats: [{ cpu: 10 }] }); expect(data.data.services).toBeUndefined(); }); }); describe('saveToDestination', () => { it('routes to saveToLocal for local type', async () => { fs.existsSync.mockReturnValue(true); const result = await backupManager.saveToDestination(Buffer.from('data'), { type: 'local' }, 'bk-1'); expect(result.type).toBe('local'); expect(fs.writeFileSync).toHaveBeenCalled(); }); it('throws for unsupported destination type', async () => { await expect(backupManager.saveToDestination(Buffer.from('data'), { type: 's3' }, 'bk-1')) .rejects.toThrow('Unsupported destination type: s3'); }); }); describe('executeBackup', () => { it('runs full backup pipeline and records success in history', async () => { fs.existsSync.mockReturnValue(true); fs.readFileSync.mockImplementation((filePath) => { if (typeof filePath === 'string') { if (filePath.includes('services')) return JSON.stringify([{ id: 'plex' }]); if (filePath.includes('config')) return JSON.stringify({ tld: '.sami' }); } return '{}'; }); const events = []; backupManager.on('backup-start', e => events.push({ type: 'start', ...e })); backupManager.on('backup-complete', e => events.push({ type: 'complete', ...e })); const result = await backupManager.executeBackup('daily', { include: ['services', 'config'], destinations: [{ type: 'local' }], verify: false }); expect(result.status).toBe('success'); expect(result.name).toBe('daily'); expect(result.compressed).toBe(true); expect(result.size).toBeGreaterThan(0); expect(backupManager.history).toHaveLength(1); expect(events).toHaveLength(2); expect(events[0].type).toBe('start'); expect(events[1].type).toBe('complete'); backupManager.removeAllListeners(); }); it('runs encrypted backup pipeline', async () => { const key = crypto.randomBytes(32).toString('hex'); fs.existsSync.mockReturnValue(true); fs.readFileSync.mockReturnValue(JSON.stringify([{ id: 'plex' }])); const result = await backupManager.executeBackup('encrypted', { include: ['services'], destinations: [{ type: 'local' }], encrypt: true, encryptionKey: key, verify: false }); expect(result.status).toBe('success'); expect(result.encrypted).toBe(true); }); it('records failure in history when all destinations fail', async () => { fs.existsSync.mockReturnValue(true); fs.readFileSync.mockReturnValue(JSON.stringify([{ id: 'plex' }])); fs.writeFileSync.mockImplementation((path) => { if (typeof path === 'string' && path.includes('.backup')) throw new Error('disk full'); }); const events = []; backupManager.on('backup-failed', e => events.push(e)); await expect(backupManager.executeBackup('daily', { include: ['services'], destinations: [{ type: 'local' }], verify: false })).rejects.toThrow('Failed to save backup to any destination'); expect(backupManager.history).toHaveLength(1); expect(backupManager.history[0].status).toBe('failed'); expect(events).toHaveLength(1); backupManager.removeAllListeners(); }); it('runs cleanup after successful backup with retention', async () => { fs.existsSync.mockReturnValue(true); fs.readFileSync.mockReturnValue(JSON.stringify([{ id: 'plex' }])); // Pre-fill history with old backups for (let i = 0; i < 5; i++) { backupManager.history.push({ id: `daily-old-${i}`, name: 'daily', status: 'success', timestamp: new Date(2026, 0, i + 1).toISOString(), locations: [{ type: 'local', path: `/backups/daily-old-${i}.backup` }] }); } await backupManager.executeBackup('daily', { include: ['services'], destinations: [{ type: 'local' }], verify: false, retention: { keep: 2 } }); // Old backups should be cleaned up (5 old + 1 new = 6 total, keep 2 → delete 4) expect(fs.unlinkSync).toHaveBeenCalled(); }); }); describe('restoreBackup', () => { it('throws when backup not found in history', async () => { await expect(backupManager.restoreBackup('nonexistent')) .rejects.toThrow('Backup not found: nonexistent'); }); it('throws on unsupported backup version', async () => { // Create backup data with wrong version const wrongVersionData = { version: '2.0', data: {} }; const compressed = await backupManager.compressBackup(wrongVersionData); backupManager.history.push({ id: 'test-restore', status: 'success', encrypted: false, locations: [{ type: 'local', path: '/backups/test-restore.backup' }] }); fs.readFileSync.mockReturnValue(compressed); await expect(backupManager.restoreBackup('test-restore')) .rejects.toThrow('Unsupported backup version: 2.0'); }); it('restores services and config from backup', async () => { const backupData = { version: '1.0', data: { services: [{ id: 'plex' }, { id: 'radarr' }], config: { tld: '.sami' } } }; const compressed = await backupManager.compressBackup(backupData); backupManager.history.push({ id: 'test-restore', status: 'success', encrypted: false, locations: [{ type: 'local', path: '/backups/test-restore.backup' }] }); fs.readFileSync.mockReturnValue(compressed); const events = []; backupManager.on('restore-start', e => events.push({ type: 'start', ...e })); backupManager.on('restore-complete', e => events.push({ type: 'complete', ...e })); const result = await backupManager.restoreBackup('test-restore'); expect(result.success).toBe(true); expect(result.restored.services).toBe(true); expect(result.restored.config).toBe(true); expect(fs.writeFileSync).toHaveBeenCalledWith( expect.stringContaining('services'), expect.stringContaining('plex') ); expect(events).toHaveLength(2); backupManager.removeAllListeners(); }); it('restores credentials and stats from backup', async () => { const backupData = { version: '1.0', data: { credentials: { encrypted: 'cred-data' }, stats: { stats: [{ cpu: 10 }] } } }; const compressed = await backupManager.compressBackup(backupData); backupManager.history.push({ id: 'full-restore', status: 'success', encrypted: false, locations: [{ type: 'local', path: '/backups/full-restore.backup' }] }); fs.readFileSync.mockReturnValue(compressed); const result = await backupManager.restoreBackup('full-restore'); expect(result.restored.credentials).toBe(true); expect(result.restored.stats).toBe(true); expect(credentialManager.importBackup).toHaveBeenCalledWith({ encrypted: 'cred-data' }); expect(resourceMonitor.importStats).toHaveBeenCalledWith({ stats: [{ cpu: 10 }] }); }); it('restores encrypted backup', async () => { const key = crypto.randomBytes(32).toString('hex'); const backupData = { version: '1.0', data: { services: [{ id: 'plex' }] } }; const compressed = await backupManager.compressBackup(backupData); const encrypted = await backupManager.encryptBackup(compressed, key); backupManager.history.push({ id: 'enc-restore', status: 'success', encrypted: true, locations: [{ type: 'local', path: '/backups/enc-restore.backup' }] }); fs.readFileSync.mockReturnValue(encrypted); const result = await backupManager.restoreBackup('enc-restore', { encryptionKey: key }); expect(result.success).toBe(true); expect(result.restored.services).toBe(true); }); it('emits restore-failed on error', async () => { backupManager.history.push({ id: 'fail-restore', status: 'success', encrypted: false, locations: [{ type: 'local', path: '/backups/fail-restore.backup' }] }); fs.readFileSync.mockImplementation(() => { throw new Error('read error'); }); const events = []; backupManager.on('restore-failed', e => events.push(e)); await expect(backupManager.restoreBackup('fail-restore')) .rejects.toThrow(); expect(events).toHaveLength(1); expect(events[0].error).toBeDefined(); backupManager.removeAllListeners(); }); it('skips restore of specific sections when options disable them', async () => { const backupData = { version: '1.0', data: { services: [{ id: 'plex' }], config: { tld: '.sami' }, credentials: { encrypted: 'data' }, stats: { stats: [] } } }; const compressed = await backupManager.compressBackup(backupData); backupManager.history.push({ id: 'partial-restore', status: 'success', encrypted: false, locations: [{ type: 'local', path: '/backups/partial.backup' }] }); fs.readFileSync.mockReturnValue(compressed); const result = await backupManager.restoreBackup('partial-restore', { restoreServices: false, restoreConfig: false, restoreCredentials: false, restoreStats: false }); expect(result.success).toBe(true); expect(result.restored.services).toBeUndefined(); expect(result.restored.config).toBeUndefined(); expect(result.restored.credentials).toBeUndefined(); expect(result.restored.stats).toBeUndefined(); }); }); describe('start with configured backups', () => { it('schedules enabled backups on start', () => { backupManager.config = { backups: { daily: { enabled: true, schedule: 'daily' }, disabled: { enabled: false, schedule: 'hourly' } }, defaultRetention: { keep: 7 } }; backupManager.start(); expect(backupManager.scheduledJobs.has('daily')).toBe(true); expect(backupManager.scheduledJobs.has('disabled')).toBe(false); }); }); describe('persistence error handling', () => { it('saveConfig handles write error gracefully', () => { fs.writeFileSync.mockImplementation(() => { throw new Error('disk full'); }); expect(() => backupManager.saveConfig()).not.toThrow(); }); it('saveHistory handles write error gracefully', () => { fs.writeFileSync.mockImplementation(() => { throw new Error('disk full'); }); expect(() => backupManager.saveHistory()).not.toThrow(); }); it('backupConfig returns null on read error', () => { fs.existsSync.mockReturnValue(true); fs.readFileSync.mockImplementation(() => { throw new Error('corrupt'); }); expect(backupManager.backupConfig()).toBeNull(); }); }); describe('verifyBackup edge cases', () => { it('returns true for non-local backup type', async () => { const result = await backupManager.verifyBackup({ type: 'remote', path: 'na' }, 'checksum'); expect(result).toBe(true); }); }); describe('DashCaddy scenarios', () => { it('full backup pipeline: services + config → compress → verify', async () => { fs.existsSync.mockReturnValue(true); fs.readFileSync.mockImplementation((filePath) => { if (typeof filePath === 'string') { if (filePath.includes('services')) return JSON.stringify([{ id: 'plex' }, { id: 'radarr' }]); if (filePath.includes('config')) return JSON.stringify({ tld: '.sami', mode: 'homelab' }); } return '{}'; }); const data = await backupManager.createBackupData(['services', 'config']); expect(data.version).toBe('1.0'); expect(data.data.services).toEqual([{ id: 'plex' }, { id: 'radarr' }]); expect(data.data.config).toEqual({ tld: '.sami', mode: 'homelab' }); // Compress and verify round-trip const compressed = await backupManager.compressBackup(data); const decompressed = await backupManager.decompressBackup(compressed); expect(decompressed.data.services).toEqual(data.data.services); }); it('encrypted backup round-trip with real AES-256-GCM', async () => { const key = crypto.randomBytes(32).toString('hex'); const payload = { version: '1.0', data: { services: [{ id: 'jellyfin' }] } }; const compressed = await backupManager.compressBackup(payload); const encrypted = await backupManager.encryptBackup(compressed, key); const decrypted = await backupManager.decryptBackup(encrypted, key); const restored = await backupManager.decompressBackup(decrypted); expect(restored.data.services[0].id).toBe('jellyfin'); }); }); });