204 lines
9.8 KiB
JavaScript
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);
|
|
});
|