Files
picpeak/backend/src/services/__tests__/databaseBackup.test.js
T
paul 20dd43c093
Mirror to GitHub / mirror (push) Successful in 28s
Test and Lint / backend-test (push) Successful in 1m26s
continuous-integration/drone/push Build is failing
Test and Lint / frontend-test (push) Failing after 2m18s
Version and Release / version-bump (push) Successful in 42s
Version and Release / trigger-drone (push) Successful in 3s
feat: implement comprehensive backup and restore system with S3 support
- Add S3/MinIO storage adapter with multipart upload support
- Implement database backup service for SQLite and PostgreSQL
- Create backup manifest generator for tracking backup contents
- Enhance backup service with S3 integration and incremental backups
- Add restore service with safety measures and rollback capability
- Create comprehensive test suite for all backup functionality
- Add admin API endpoints for backup/restore management
- Implement frontend UI with dashboard, configuration, and restore wizard
- Add roadmap section to README with implemented backup feature

This implementation provides:
- Multiple backup destinations (local, rsync, S3/MinIO)
- Intelligent change detection to minimize backup frequency
- Full database backups with compression
- Manifest-based restore with integrity validation
- Pre-restore safety backups with rollback
- Comprehensive error handling and monitoring
- User-friendly admin interface

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-22 09:05:52 +02:00

251 lines
7.6 KiB
JavaScript

const { DatabaseBackupService } = require('../databaseBackup');
const { db } = require('../../database/db');
const fs = require('fs').promises;
const path = require('path');
const crypto = require('crypto');
// Mock dependencies
jest.mock('../../database/db');
jest.mock('../../utils/logger');
jest.mock('../emailProcessor');
jest.mock('child_process');
describe('DatabaseBackupService', () => {
let service;
let mockExecAsync;
beforeEach(() => {
service = new DatabaseBackupService();
mockExecAsync = jest.fn();
// Reset mocks
jest.clearAllMocks();
// Mock execAsync
const childProcess = require('child_process');
childProcess.exec = jest.fn((cmd, opts, callback) => {
if (callback) {
callback(null, { stdout: 'ok', stderr: '' });
}
});
});
afterEach(async () => {
// Cleanup test files
try {
await fs.rmdir('/tmp/test-backup', { recursive: true });
} catch (e) {
// Ignore
}
});
describe('calculateChecksum', () => {
it('should calculate SHA256 checksum of a file', async () => {
const testFile = '/tmp/test-checksum.txt';
const testContent = 'Hello, World!';
await fs.writeFile(testFile, testContent);
const checksum = await service.calculateChecksum(testFile);
// Expected checksum for "Hello, World!"
const expectedChecksum = crypto
.createHash('sha256')
.update(testContent)
.digest('hex');
expect(checksum).toBe(expectedChecksum);
await fs.unlink(testFile);
});
});
describe('getTableChecksums', () => {
it('should get checksums for all tables', async () => {
// Mock getTables
service.getTables = jest.fn().mockResolvedValue(['events', 'photos']);
// Mock SQLite response
db.raw = jest.fn()
.mockResolvedValueOnce([{ row_count: 10, data_sum: 1000 }])
.mockResolvedValueOnce([{ row_count: 20, data_sum: 2000 }]);
const checksums = await service.getTableChecksums();
expect(checksums).toHaveProperty('events');
expect(checksums).toHaveProperty('photos');
expect(checksums.events.rowCount).toBe(10);
expect(checksums.photos.rowCount).toBe(20);
expect(checksums.events.checksum).toBeDefined();
expect(checksums.photos.checksum).toBeDefined();
});
});
describe('getTables', () => {
it('should get list of tables for SQLite', async () => {
service.dbType = 'sqlite';
db.raw = jest.fn().mockResolvedValue([
{ name: 'events' },
{ name: 'photos' },
{ name: 'admin_users' }
]);
const tables = await service.getTables();
expect(tables).toEqual(['events', 'photos', 'admin_users']);
expect(db.raw).toHaveBeenCalledWith(expect.stringContaining('sqlite_master'));
});
it('should get list of tables for PostgreSQL', async () => {
service.dbType = 'postgresql';
db.raw = jest.fn().mockResolvedValue({
rows: [
{ table_name: 'events' },
{ table_name: 'photos' },
{ table_name: 'admin_users' }
]
});
const tables = await service.getTables();
expect(tables).toEqual(['events', 'photos', 'admin_users']);
expect(db.raw).toHaveBeenCalledWith(expect.stringContaining('information_schema.tables'));
});
});
describe('getDatabaseSize', () => {
it('should get database size for SQLite', async () => {
service.dbType = 'sqlite';
const mockSize = 1024 * 1024 * 10; // 10MB
// Mock fs.stat
const originalStat = fs.stat;
fs.stat = jest.fn().mockResolvedValue({ size: mockSize });
const size = await service.getDatabaseSize();
expect(size).toBe(mockSize);
fs.stat = originalStat;
});
it('should get database size for PostgreSQL', async () => {
service.dbType = 'postgresql';
const mockSize = 1024 * 1024 * 100; // 100MB
db.raw = jest.fn().mockResolvedValue({
rows: [{ size: mockSize.toString() }]
});
const size = await service.getDatabaseSize();
expect(size).toBe(mockSize);
expect(db.raw).toHaveBeenCalledWith(expect.stringContaining('pg_database_size'));
});
});
describe('compressFile', () => {
it('should compress file and return stats', async () => {
const testFile = '/tmp/test-compress.txt';
const compressedFile = '/tmp/test-compress.txt.gz';
// Create test file with repetitive content (compresses well)
const testContent = 'Hello, World! '.repeat(1000);
await fs.writeFile(testFile, testContent);
const stats = await service.compressFile(testFile, compressedFile);
expect(stats.originalSize).toBeGreaterThan(0);
expect(stats.compressedSize).toBeGreaterThan(0);
expect(stats.compressedSize).toBeLessThan(stats.originalSize);
expect(parseFloat(stats.compressionRatio)).toBeGreaterThan(0);
// Cleanup
await fs.unlink(testFile);
await fs.unlink(compressedFile);
});
});
describe('backup configuration', () => {
it('should get backup configuration from database', async () => {
const mockConfig = [
{ setting_key: 'database_backup_enabled', setting_value: 'true' },
{ setting_key: 'database_backup_compress', setting_value: 'true' },
{ setting_key: 'database_backup_retention_days', setting_value: '30' }
];
db.mockReturnValue({
where: jest.fn().mockReturnThis(),
select: jest.fn().mockResolvedValue(mockConfig)
});
const config = await service.getBackupConfig();
expect(config.database_backup_enabled).toBe(true);
expect(config.database_backup_compress).toBe(true);
expect(config.database_backup_retention_days).toBe(30);
});
});
describe('cleanupOldBackups', () => {
it('should delete old backup files and records', async () => {
const oldBackups = [
{ id: 1, file_path: '/backup/old1.sql.gz' },
{ id: 2, file_path: '/backup/old2.sql.gz' }
];
db.mockReturnValue({
where: jest.fn().mockReturnThis(),
select: jest.fn().mockResolvedValue(oldBackups),
delete: jest.fn().mockResolvedValue(1)
});
// Mock fs.unlink
fs.unlink = jest.fn().mockResolvedValue(undefined);
await service.cleanupOldBackups(30);
expect(fs.unlink).toHaveBeenCalledTimes(2);
expect(fs.unlink).toHaveBeenCalledWith('/backup/old1.sql.gz');
expect(fs.unlink).toHaveBeenCalledWith('/backup/old2.sql.gz');
});
});
describe('progress tracking', () => {
it('should update and retrieve progress', () => {
expect(service.getProgress()).toBeNull();
service.updateProgress('Testing...', { step: 1 });
const progress = service.getProgress();
expect(progress.message).toBe('Testing...');
expect(progress.details.step).toBe(1);
expect(progress.timestamp).toBeDefined();
});
});
describe('backup history', () => {
it('should retrieve backup history', async () => {
const mockHistory = [
{
id: 1,
started_at: new Date(),
completed_at: new Date(),
status: 'completed',
file_size_bytes: 1024000
}
];
db.mockReturnValue({
orderBy: jest.fn().mockReturnThis(),
limit: jest.fn().mockResolvedValue(mockHistory)
});
const history = await service.getBackupHistory(10);
expect(history).toEqual(mockHistory);
expect(history.length).toBe(1);
});
});
});