Files
dashback/scripts/test-backup-service.js
2026-10-14 03:07:21 +02:00

204 lines
9.8 KiB
JavaScript

import fs from 'node:fs';
import path from 'node:path';
import assert from 'node:assert';
import env from '../src/config/env.js';
import {
createHourlyBackup,
listBackups,
pruneBackups,
getBackupServiceStatus,
startBackupService,
stopBackupService,
resolveBackupPaths,
listBackupVfsFiles,
recoverBackupVfsFile,
} from '../src/services/backupService.js';
import { closeStore } from '../src/db/store.js';
async function runTests() {
console.log('--- Starting Backup Service Verification ---');
// 1. Verify path resolution and directories
const paths = resolveBackupPaths();
console.log(`Backup root dir: ${paths.rootDir}`);
assert(paths.rootDir.endsWith('data/backups'), 'Backup root should be data/backups');
// 2. Execute a backup
console.log('\n[Test 1] Running createHourlyBackup()...');
const backupResult = await createHourlyBackup();
assert(backupResult.success === true, 'Backup should report success');
assert(backupResult.manifest.manual === undefined, 'manual field should not exist in manifest');
console.log(`Backup created: ${backupResult.id}, size: ${(backupResult.totalSizeBytes / 1024 / 1024).toFixed(2)} MB`);
console.log(`Promoted to daily: ${backupResult.promotedDaily}, Promoted to monthly: ${backupResult.promotedMonthly}`);
// Verify files in the created hourly backup
const hourlyBackupFolder = path.join(paths.hourlyDir, backupResult.id);
assert(fs.existsSync(hourlyBackupFolder), 'Hourly backup folder should exist');
assert(fs.existsSync(path.join(hourlyBackupFolder, 'vfs.tar.gz')), 'vfs.tar.gz should exist');
assert(fs.existsSync(path.join(hourlyBackupFolder, 'storage.tar')), 'storage.tar should exist');
assert(fs.existsSync(path.join(hourlyBackupFolder, 'manifest.json')), 'manifest.json should exist');
if (env.dbProvider === 'sqlite') {
const dbName = path.basename(env.sqlite.filename);
assert(fs.existsSync(path.join(hourlyBackupFolder, dbName)), `${dbName} should exist`);
console.log(`Verified SQLite database backup file: ${dbName}`);
}
// Verify daily backup directory
const { dateKey, monthKey } = backupResult.manifest;
const dailyFolder = path.join(paths.dailyDir, `backup-${dateKey}`);
assert(fs.existsSync(dailyFolder), 'Daily backup folder should exist');
assert(fs.existsSync(path.join(dailyFolder, 'manifest.json')), 'Daily manifest should exist');
// Verify monthly backup directory
const monthlyFolder = path.join(paths.monthlyDir, `backup-${monthKey}`);
assert(fs.existsSync(monthlyFolder), 'Monthly backup folder should exist');
assert(fs.existsSync(path.join(monthlyFolder, 'manifest.json')), 'Monthly manifest should exist');
// 3. Test listBackups and getBackupServiceStatus
console.log('\n[Test 2] Testing listBackups() and getBackupServiceStatus()...');
const status = getBackupServiceStatus();
assert(typeof status.enabled === 'boolean', 'status.enabled should be boolean');
assert(status.retention.hourlyMax === 50, 'hourly retention should be 50');
assert(status.retention.dailyMax === 50, 'daily retention should be 50');
const listing = listBackups();
assert(listing.hourly.length >= 1, 'Should have at least 1 hourly backup');
assert(listing.daily.length >= 1, 'Should have at least 1 daily backup');
assert(listing.monthly.length >= 1, 'Should have at least 1 monthly backup');
console.log(`Counts -> hourly: ${listing.counts.hourly}, daily: ${listing.counts.daily}, monthly: ${listing.counts.monthly}`);
// 4. Test VFS listing and recovery from specific backup
console.log('\n[Test 3] Testing listBackupVfsFiles() and recoverBackupVfsFile()...');
const vfsListing = await listBackupVfsFiles({ backupId: backupResult.id, companyId: 1 });
assert(vfsListing.backupId === backupResult.id, 'backupId should match');
assert(vfsListing.companyId === 1, 'companyId should be 1');
assert(Array.isArray(vfsListing.files), 'files should be an array');
console.log(`Found ${vfsListing.files.length} VFS files in backup for company 1.`);
if (vfsListing.files.length > 0) {
const fileToRecover = vfsListing.files[0];
console.log(`Attempting recovery of ${fileToRecover.name}...`);
const recoverRes = await recoverBackupVfsFile({
backupId: backupResult.id,
companyId: 1,
filePath: fileToRecover.name,
});
assert(recoverRes.success === true, 'Recovery should succeed');
assert(recoverRes.path === fileToRecover.name, 'Recovered path should match');
assert(recoverRes.sizeBytes > 0, 'Recovered size should be > 0');
const recoveredDiskPath = path.resolve(paths.vfsDir, '1', fileToRecover.name);
assert(fs.existsSync(recoveredDiskPath), 'Recovered file should exist on disk');
console.log(`Successfully recovered ${fileToRecover.name} (${recoverRes.sizeBytes} bytes).`);
}
// Test error cases
let backupNotFound = false;
try {
await listBackupVfsFiles({ backupId: 'backup-nonexistent', companyId: 1 });
} catch (e) {
if (e.message === 'Backup not found') backupNotFound = true;
}
assert(backupNotFound, 'Should throw Backup not found for invalid backupId');
let fileNotFound = false;
try {
await recoverBackupVfsFile({
backupId: backupResult.id,
companyId: 1,
filePath: 'completely-nonexistent-file.xyz',
});
} catch (e) {
if (e.message === 'File not found in backup') fileNotFound = true;
}
assert(fileNotFound, 'Should throw File not found in backup for invalid file');
console.log('VFS listing and recovery tests passed.');
// 5. Test Retention Pruning (exceeding 50 entries)
console.log('\n[Test 4] Testing retention pruning for > 50 entries...');
// Create dummy hourly backup folders
for (let i = 1; i <= 55; i++) {
const dummyName = `backup-2020-01-01_${String(i).padStart(2, '0')}-00-00`;
const dummyPath = path.join(paths.hourlyDir, dummyName);
fs.mkdirSync(dummyPath, { recursive: true });
fs.writeFileSync(path.join(dummyPath, 'manifest.json'), '{}');
}
// Create dummy daily backup folders
for (let i = 1; i <= 55; i++) {
const dummyName = `backup-2020-01-${String(i).padStart(2, '0')}`;
const dummyPath = path.join(paths.dailyDir, dummyName);
fs.mkdirSync(dummyPath, { recursive: true });
fs.writeFileSync(path.join(dummyPath, 'manifest.json'), '{}');
}
// Create dummy monthly backup folders (should never be pruned!)
for (let i = 1; i <= 55; i++) {
const dummyName = `backup-2010-${String(i).padStart(2, '0')}`;
const dummyPath = path.join(paths.monthlyDir, dummyName);
fs.mkdirSync(dummyPath, { recursive: true });
fs.writeFileSync(path.join(dummyPath, 'manifest.json'), '{}');
}
const pruneResult = pruneBackups();
console.log(`Pruned ${pruneResult.hourlyPruned} hourly, ${pruneResult.dailyPruned} daily`);
assert(pruneResult.hourlyPruned >= 5, 'Should have pruned at least 5 hourly backups');
assert(pruneResult.dailyPruned >= 5, 'Should have pruned at least 5 daily backups');
const listingAfterPrune = listBackups();
assert(listingAfterPrune.counts.hourly <= 50, `Hourly count should be <= 50, got ${listingAfterPrune.counts.hourly}`);
assert(listingAfterPrune.counts.daily <= 50, `Daily count should be <= 50, got ${listingAfterPrune.counts.daily}`);
assert(listingAfterPrune.counts.monthly >= 55, `Monthly count should NOT be pruned (got ${listingAfterPrune.counts.monthly})`);
console.log('Retention pruning verified: 50 hourly max, 50 daily max, monthly preserved forever.');
// Clean up dummy test backups
for (let i = 1; i <= 55; i++) {
const hPath = path.join(paths.hourlyDir, `backup-2020-01-01_${String(i).padStart(2, '0')}-00-00`);
if (fs.existsSync(hPath)) fs.rmSync(hPath, { recursive: true, force: true });
const dPath = path.join(paths.dailyDir, `backup-2020-01-${String(i).padStart(2, '0')}`);
if (fs.existsSync(dPath)) fs.rmSync(dPath, { recursive: true, force: true });
const mPath = path.join(paths.monthlyDir, `backup-2010-${String(i).padStart(2, '0')}`);
if (fs.existsSync(mPath)) fs.rmSync(mPath, { recursive: true, force: true });
}
// 6. Test MSSQL warning behavior
console.log('\n[Test 5] Testing MSSQL console warning behavior...');
const originalProvider = env.dbProvider;
let warningCaptured = false;
const origWarn = console.warn;
console.warn = (...args) => {
const text = args.join(' ');
if (text.includes('MSSQL database backup is not implemented')) {
warningCaptured = true;
}
origWarn(...args);
};
try {
env.dbProvider = 'mssql';
const mssqlBackup = await createHourlyBackup();
assert(warningCaptured, 'Console warning must be printed when running on MSSQL');
assert(mssqlBackup.manifest.files.database.backedUp === false, 'Database backup must be skipped on MSSQL');
console.log('MSSQL warning and database skip verified.');
} finally {
console.warn = origWarn;
env.dbProvider = originalProvider;
}
// 7. Test Lifecycle methods
console.log('\n[Test 6] Testing startBackupService() and stopBackupService()...');
startBackupService();
assert(getBackupServiceStatus().isRunning === true, 'Service should be running');
await stopBackupService();
assert(getBackupServiceStatus().isRunning === false, 'Service should be stopped');
console.log('Service lifecycle start/stop verified.');
await closeStore();
console.log('\n--- ALL BACKUP SERVICE TESTS PASSED ---');
}
runTests().catch((err) => {
console.error('Test failed:', err);
process.exit(1);
});