Initial commit: DashCaddy v1.0

Full codebase including API server (32 modules + routes), dashboard frontend,
DashCA certificate distribution, installer script, and deployment skills.
This commit is contained in:
2026-03-05 02:26:12 -08:00
commit f61e85d9a7
337 changed files with 75282 additions and 0 deletions

View File

@@ -0,0 +1,209 @@
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);
});
});