|
|
@@ -0,0 +1,201 @@
|
|
|
+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('备份调度已停止')
|
|
|
+ })
|
|
|
+ })
|
|
|
+})
|