This commit is contained in:
seb
2026-10-14 00:08:11 +02:00
parent 11f6a8fc0f
commit 74b24686bc
14 changed files with 962 additions and 12 deletions

View File

@@ -0,0 +1,153 @@
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,
} 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({ manual: true });
assert(backupResult.success === true, 'Backup should report success');
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 Retention Pruning (exceeding 50 entries)
console.log('\n[Test 3] 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 });
}
// 5. Test MSSQL warning behavior
console.log('\n[Test 4] 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({ manual: true });
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;
}
// 6. Test Lifecycle methods
console.log('\n[Test 5] 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);
});