import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' import { DatabaseBackup } from '@/utils/backup' import path from 'path' // Mock pg-dump-restore vi.mock('pg-dump-restore', () => ({ pgDump: vi.fn().mockResolvedValue(undefined), pgRestore: vi.fn().mockResolvedValue(undefined), })) // Mock fs for tests vi.mock('fs', () => ({ promises: { mkdir: vi.fn().mockResolvedValue(undefined), chmod: vi.fn().mockResolvedValue(undefined), readdir: vi.fn().mockResolvedValue([]), stat: vi.fn().mockResolvedValue({ size: 1024, mtimeMs: Date.now(), mode: 0o600, mtime: new Date() }), access: vi.fn().mockResolvedValue(undefined), unlink: vi.fn().mockResolvedValue(undefined), rm: vi.fn().mockResolvedValue(undefined), writeFile: vi.fn().mockResolvedValue(undefined), utimes: vi.fn().mockResolvedValue(undefined), } })) // Mock logger vi.mock('@/utils/logger', () => ({ logger: { db: vi.fn(), error: vi.fn(), api: vi.fn(), middleware: vi.fn(), }, })) describe('DatabaseBackup', () => { let backup: DatabaseBackup beforeEach(() => { vi.clearAllMocks() backup = DatabaseBackup.getInstance() }) afterEach(() => { vi.restoreAllMocks() }) describe('getInstance', () => { it('应该返回单例实例', () => { const instance1 = DatabaseBackup.getInstance() const instance2 = DatabaseBackup.getInstance() expect(instance1).toBe(instance2) }) }) describe('ensureBackupDir', () => { it('应该创建备份目录并设置权限', async () => { const fs = await import('fs') await backup.ensureBackupDir() expect(fs.promises.mkdir).toHaveBeenCalledWith('./backups', { recursive: true }) expect(fs.promises.chmod).toHaveBeenCalledWith('./backups', 0o700) }) it('应该在创建目录失败时抛出错误', async () => { const fs = await import('fs') const { logger } = await import('@/utils/logger') vi.mocked(fs.promises.mkdir).mockRejectedValueOnce(new Error('创建目录失败')) await expect(backup.ensureBackupDir()).rejects.toThrow('创建目录失败') expect(logger.error).toHaveBeenCalled() }) }) describe('getDbConfig', () => { it('应该返回正确的数据库配置', () => { process.env.DB_HOST = 'test-host' process.env.DB_PORT = '5433' process.env.DB_DATABASE = 'test-db' process.env.DB_USERNAME = 'test-user' process.env.DB_PASSWORD = 'test-password' const config = (backup as any).getDbConfig() expect(config).toEqual({ host: 'test-host', port: 5433, database: 'test-db', username: 'test-user', password: 'test-password', }) }) it('应该使用默认值当环境变量未设置时', () => { delete process.env.DB_HOST delete process.env.DB_PORT delete process.env.DB_DATABASE delete process.env.DB_USERNAME delete process.env.DB_PASSWORD const config = (backup as any).getDbConfig() expect(config).toEqual({ host: 'localhost', port: 5432, database: 'postgres', username: 'postgres', password: '', }) }) }) describe('formatFileSize', () => { it('应该正确格式化文件大小', () => { const formatFileSize = (backup as any).formatFileSize expect(formatFileSize(0)).toBe('0 B') expect(formatFileSize(1024)).toBe('1 KB') expect(formatFileSize(1048576)).toBe('1 MB') expect(formatFileSize(1073741824)).toBe('1 GB') }) }) describe('backupExists', () => { it('应该返回true当备份文件存在时', async () => { const fs = await import('fs') const exists = await backup.backupExists('/path/to/backup.dump') expect(exists).toBe(true) expect(fs.promises.access).toHaveBeenCalledWith('/path/to/backup.dump') }) it('应该返回false当备份文件不存在时', async () => { const fs = await import('fs') vi.mocked(fs.promises.access).mockRejectedValueOnce(new Error('文件不存在')) const exists = await backup.backupExists('/path/to/backup.dump') expect(exists).toBe(false) }) }) describe('cleanupOldBackups', () => { it('应该清理7天前的旧备份', async () => { const fs = await import('fs') const { logger } = await import('@/utils/logger') const now = Date.now() const oldFileTime = now - (8 * 24 * 60 * 60 * 1000) // 8天前 const newFileTime = now - (6 * 24 * 60 * 60 * 1000) // 6天前 vi.mocked(fs.promises.readdir).mockResolvedValue(['backup-old.dump', 'backup-new.dump'] as any) vi.mocked(fs.promises.stat) .mockResolvedValueOnce({ mtimeMs: oldFileTime } as any) .mockResolvedValueOnce({ mtimeMs: newFileTime } as any) await backup.cleanupOldBackups() expect(fs.promises.unlink).toHaveBeenCalledTimes(1) expect(fs.promises.unlink).toHaveBeenCalledWith(path.join('./backups', 'backup-old.dump')) expect(logger.db).toHaveBeenCalledWith('删除旧备份文件: backup-old.dump') }) it('应该在清理失败时记录错误但不抛出', async () => { const fs = await import('fs') const { logger } = await import('@/utils/logger') vi.mocked(fs.promises.readdir).mockRejectedValueOnce(new Error('读取目录失败')) await expect(backup.cleanupOldBackups()).resolves.not.toThrow() expect(logger.error).toHaveBeenCalled() }) }) describe('startScheduledBackups', () => { it('应该启动定时备份任务', async () => { const { logger } = await import('@/utils/logger') backup.startScheduledBackups() // expect(cron.default.schedule).toHaveBeenCalledWith('0 2 * * *', expect.any(Function)) expect(logger.db).toHaveBeenCalledWith('备份调度已启动: 0 2 * * *') }) }) describe('stopScheduledBackups', () => { it('应该停止定时备份任务', async () => { const { logger } = await import('@/utils/logger') // 先启动再停止 backup.startScheduledBackups() backup.stopScheduledBackups() expect(logger.db).toHaveBeenCalledWith('备份调度已停止') }) }) })