|
@@ -0,0 +1,210 @@
|
|
|
|
|
+import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
|
|
|
+import { databaseBackup } from '../backup'
|
|
|
|
|
+import { databaseRestore } from '../restore'
|
|
|
|
|
+import { promises as fs } from 'fs'
|
|
|
|
|
+import path from 'path'
|
|
|
|
|
+
|
|
|
|
|
+// Mock pg-dump-restore for integration tests
|
|
|
|
|
+vi.mock('pg-dump-restore', () => ({
|
|
|
|
|
+ pgDump: vi.fn().mockImplementation(async (connectionOptions, dumpOptions) => {
|
|
|
|
|
+ // 模拟创建备份文件
|
|
|
|
|
+ const { filePath } = dumpOptions
|
|
|
|
|
+ if (filePath) {
|
|
|
|
|
+ const fs = await import('fs')
|
|
|
|
|
+ await fs.promises.writeFile(filePath, 'mock backup data')
|
|
|
|
|
+ }
|
|
|
|
|
+ }),
|
|
|
|
|
+ pgRestore: vi.fn().mockResolvedValue(undefined),
|
|
|
|
|
+}))
|
|
|
|
|
+
|
|
|
|
|
+// Mock fs with importOriginal for integration tests
|
|
|
|
|
+vi.mock('fs', async (importOriginal) => {
|
|
|
|
|
+ const actual = await importOriginal()
|
|
|
|
|
+ return {
|
|
|
|
|
+ ...actual,
|
|
|
|
|
+ promises: {
|
|
|
|
|
+ ...actual.promises,
|
|
|
|
|
+ mkdir: vi.fn().mockResolvedValue(undefined),
|
|
|
|
|
+ chmod: vi.fn().mockResolvedValue(undefined),
|
|
|
|
|
+ readdir: vi.fn().mockResolvedValue([]),
|
|
|
|
|
+ stat: vi.fn().mockResolvedValue({ size: 1024, mtimeMs: Date.now() }),
|
|
|
|
|
+ access: vi.fn().mockResolvedValue(undefined),
|
|
|
|
|
+ unlink: vi.fn().mockResolvedValue(undefined),
|
|
|
|
|
+ writeFile: vi.fn().mockResolvedValue(undefined),
|
|
|
|
|
+ rm: vi.fn().mockResolvedValue(undefined),
|
|
|
|
|
+ utimes: vi.fn().mockResolvedValue(undefined),
|
|
|
|
|
+ },
|
|
|
|
|
+ }
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+// Mock node-cron
|
|
|
|
|
+vi.mock('node-cron', () => ({
|
|
|
|
|
+ default: {
|
|
|
|
|
+ schedule: vi.fn().mockReturnValue({
|
|
|
|
|
+ stop: vi.fn(),
|
|
|
|
|
+ nextDate: vi.fn().mockReturnValue(new Date()),
|
|
|
|
|
+ }),
|
|
|
|
|
+ },
|
|
|
|
|
+}))
|
|
|
|
|
+
|
|
|
|
|
+// Mock logger
|
|
|
|
|
+vi.mock('../logger', () => ({
|
|
|
|
|
+ logger: {
|
|
|
|
|
+ db: vi.fn(),
|
|
|
|
|
+ error: vi.fn(),
|
|
|
|
|
+ },
|
|
|
|
|
+}))
|
|
|
|
|
+
|
|
|
|
|
+describe('Database Backup Integration', () => {
|
|
|
|
|
+ const testBackupDir = './test-backups'
|
|
|
|
|
+
|
|
|
|
|
+ beforeEach(async () => {
|
|
|
|
|
+ vi.clearAllMocks()
|
|
|
|
|
+
|
|
|
|
|
+ // 设置测试环境变量
|
|
|
|
|
+ process.env.BACKUP_DIR = testBackupDir
|
|
|
|
|
+ process.env.BACKUP_RETENTION_DAYS = '1'
|
|
|
|
|
+
|
|
|
|
|
+ // 清理测试目录
|
|
|
|
|
+ try {
|
|
|
|
|
+ await fs.rm(testBackupDir, { recursive: true, force: true })
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ // 目录可能不存在
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ afterEach(async () => {
|
|
|
|
|
+ // 清理测试目录
|
|
|
|
|
+ try {
|
|
|
|
|
+ await fs.rm(testBackupDir, { recursive: true, force: true })
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ // 忽略错误
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ vi.restoreAllMocks()
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ describe('Backup Creation', () => {
|
|
|
|
|
+ it('应该成功创建备份文件', async () => {
|
|
|
|
|
+ const backupFile = await databaseBackup.createBackup()
|
|
|
|
|
+
|
|
|
|
|
+ expect(backupFile).toBeDefined()
|
|
|
|
|
+ expect(backupFile).toContain('.dump')
|
|
|
|
|
+
|
|
|
|
|
+ // 验证文件已创建
|
|
|
|
|
+ const exists = await databaseBackup.backupExists(backupFile)
|
|
|
|
|
+ expect(exists).toBe(true)
|
|
|
|
|
+
|
|
|
|
|
+ // 验证文件权限
|
|
|
|
|
+ const stats = await fs.stat(backupFile)
|
|
|
|
|
+ expect(stats.mode & 0o777).toBe(0o600) // 应该只有用户可读写
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ it('应该设置正确的文件权限', async () => {
|
|
|
|
|
+ const backupFile = await databaseBackup.createBackup()
|
|
|
|
|
+
|
|
|
|
|
+ const stats = await fs.stat(backupFile)
|
|
|
|
|
+ expect(stats.mode & 0o777).toBe(0o600)
|
|
|
|
|
+ })
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ describe('Backup Cleanup', () => {
|
|
|
|
|
+ it('应该清理旧的备份文件', async () => {
|
|
|
|
|
+ // 创建一些测试备份文件
|
|
|
|
|
+ const now = Date.now()
|
|
|
|
|
+ const oldFileTime = now - (2 * 24 * 60 * 60 * 1000) // 2天前
|
|
|
|
|
+ const newFileTime = now - (12 * 60 * 60 * 1000) // 12小时前
|
|
|
|
|
+
|
|
|
|
|
+ const oldBackup = path.join(testBackupDir, 'backup-old.dump')
|
|
|
|
|
+ const newBackup = path.join(testBackupDir, 'backup-new.dump')
|
|
|
|
|
+
|
|
|
|
|
+ await fs.mkdir(testBackupDir, { recursive: true })
|
|
|
|
|
+ await fs.writeFile(oldBackup, 'old backup data')
|
|
|
|
|
+ await fs.writeFile(newBackup, 'new backup data')
|
|
|
|
|
+
|
|
|
|
|
+ // 修改文件时间
|
|
|
|
|
+ await fs.utimes(oldBackup, new Date(oldFileTime), new Date(oldFileTime))
|
|
|
|
|
+ await fs.utimes(newBackup, new Date(newFileTime), new Date(newFileTime))
|
|
|
|
|
+
|
|
|
|
|
+ // 执行清理
|
|
|
|
|
+ await databaseBackup.cleanupOldBackups()
|
|
|
|
|
+
|
|
|
|
|
+ // 验证只有旧文件被删除
|
|
|
|
|
+ const oldExists = await databaseBackup.backupExists(oldBackup)
|
|
|
|
|
+ const newExists = await databaseBackup.backupExists(newBackup)
|
|
|
|
|
+
|
|
|
|
|
+ expect(oldExists).toBe(false)
|
|
|
|
|
+ expect(newExists).toBe(true)
|
|
|
|
|
+ })
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ describe('Backup Management', () => {
|
|
|
|
|
+ it('应该能够检查备份文件是否存在', async () => {
|
|
|
|
|
+ const backupFile = await databaseBackup.createBackup()
|
|
|
|
|
+
|
|
|
|
|
+ const exists = await databaseBackup.backupExists(backupFile)
|
|
|
|
|
+ expect(exists).toBe(true)
|
|
|
|
|
+
|
|
|
|
|
+ const notExists = await databaseBackup.backupExists('/nonexistent/path.dump')
|
|
|
|
|
+ expect(notExists).toBe(false)
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ it('应该能够获取备份文件信息', async () => {
|
|
|
|
|
+ const backupFile = await databaseBackup.createBackup()
|
|
|
|
|
+
|
|
|
|
|
+ const info = await databaseBackup.getBackupInfo(backupFile)
|
|
|
|
|
+
|
|
|
|
|
+ expect(info).toHaveProperty('size')
|
|
|
|
|
+ expect(info).toHaveProperty('mtime')
|
|
|
|
|
+ expect(info).toHaveProperty('formattedSize')
|
|
|
|
|
+ expect(info.formattedSize).toBe('14 B') // 'mock backup data' 的长度
|
|
|
|
|
+ })
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ describe('Scheduled Backups', () => {
|
|
|
|
|
+ it('应该启动和停止定时备份', () => {
|
|
|
|
|
+ const cron = require('node-cron')
|
|
|
|
|
+
|
|
|
|
|
+ databaseBackup.startScheduledBackups()
|
|
|
|
|
+ expect(cron.schedule).toHaveBeenCalled()
|
|
|
|
|
+
|
|
|
|
|
+ databaseBackup.stopScheduledBackups()
|
|
|
|
|
+ // 验证stop方法被调用
|
|
|
|
|
+ expect(cron.schedule().stop).toHaveBeenCalled()
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ it('应该返回备份状态', () => {
|
|
|
|
|
+ databaseBackup.startScheduledBackups()
|
|
|
|
|
+
|
|
|
|
|
+ const status = databaseBackup.getBackupStatus()
|
|
|
|
|
+
|
|
|
|
|
+ expect(status).toHaveProperty('scheduled')
|
|
|
|
|
+ expect(status).toHaveProperty('nextRun')
|
|
|
|
|
+ expect(status).toHaveProperty('lastRun')
|
|
|
|
|
+ expect(status.scheduled).toBe(true)
|
|
|
|
|
+ })
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ describe('Restore Integration', () => {
|
|
|
|
|
+ it('应该能够找到最新备份', async () => {
|
|
|
|
|
+ // 创建多个备份文件
|
|
|
|
|
+ await databaseBackup.createBackup()
|
|
|
|
|
+ await new Promise(resolve => setTimeout(resolve, 100)) // 确保时间戳不同
|
|
|
|
|
+ await databaseBackup.createBackup()
|
|
|
|
|
+
|
|
|
|
|
+ const latestBackup = await databaseRestore.findLatestBackup()
|
|
|
|
|
+ expect(latestBackup).toBeDefined()
|
|
|
|
|
+ expect(latestBackup).toContain('.dump')
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ it('应该能够列出所有备份', async () => {
|
|
|
|
|
+ // 创建多个备份文件
|
|
|
|
|
+ await databaseBackup.createBackup()
|
|
|
|
|
+ await databaseBackup.createBackup()
|
|
|
|
|
+
|
|
|
|
|
+ const backups = await databaseRestore.listBackups()
|
|
|
|
|
+ expect(backups.length).toBe(2)
|
|
|
|
|
+ expect(backups.every(b => b.endsWith('.dump'))).toBe(true)
|
|
|
|
|
+ })
|
|
|
|
|
+ })
|
|
|
|
|
+})
|