|
@@ -1,49 +1,40 @@
|
|
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
|
import { databaseBackup } from '../backup'
|
|
import { databaseBackup } from '../backup'
|
|
|
import { databaseRestore } from '../restore'
|
|
import { databaseRestore } from '../restore'
|
|
|
-import { promises as fs } from 'fs'
|
|
|
|
|
import path from 'path'
|
|
import path from 'path'
|
|
|
|
|
|
|
|
// Mock pg-dump-restore for integration tests
|
|
// 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()
|
|
|
|
|
|
|
+vi.mock('pg-dump-restore', async (importOriginal) => {
|
|
|
|
|
+ const actual = await importOriginal() as typeof import('pg-dump-restore')
|
|
|
return {
|
|
return {
|
|
|
...actual,
|
|
...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),
|
|
|
|
|
- },
|
|
|
|
|
|
|
+ pgDump: vi.fn().mockResolvedValue({ size: 1024 }),
|
|
|
|
|
+ pgRestore: vi.fn().mockResolvedValue(undefined),
|
|
|
}
|
|
}
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
|
|
+// Mock fs for integration 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),
|
|
|
|
|
+ writeFile: vi.fn().mockResolvedValue(undefined),
|
|
|
|
|
+ rm: vi.fn().mockResolvedValue(undefined),
|
|
|
|
|
+ utimes: vi.fn().mockResolvedValue(undefined),
|
|
|
|
|
+ }
|
|
|
|
|
+}))
|
|
|
|
|
+
|
|
|
// Mock node-cron
|
|
// Mock node-cron
|
|
|
vi.mock('node-cron', () => ({
|
|
vi.mock('node-cron', () => ({
|
|
|
default: {
|
|
default: {
|
|
|
- schedule: vi.fn().mockReturnValue({
|
|
|
|
|
|
|
+ schedule: vi.fn().mockImplementation(() => ({
|
|
|
stop: vi.fn(),
|
|
stop: vi.fn(),
|
|
|
nextDate: vi.fn().mockReturnValue(new Date()),
|
|
nextDate: vi.fn().mockReturnValue(new Date()),
|
|
|
- }),
|
|
|
|
|
|
|
+ })),
|
|
|
},
|
|
},
|
|
|
}))
|
|
}))
|
|
|
|
|
|
|
@@ -52,6 +43,8 @@ vi.mock('../logger', () => ({
|
|
|
logger: {
|
|
logger: {
|
|
|
db: vi.fn(),
|
|
db: vi.fn(),
|
|
|
error: vi.fn(),
|
|
error: vi.fn(),
|
|
|
|
|
+ api: vi.fn(),
|
|
|
|
|
+ middleware: vi.fn(),
|
|
|
},
|
|
},
|
|
|
}))
|
|
}))
|
|
|
|
|
|
|
@@ -65,22 +58,23 @@ describe('Database Backup Integration', () => {
|
|
|
process.env.BACKUP_DIR = testBackupDir
|
|
process.env.BACKUP_DIR = testBackupDir
|
|
|
process.env.BACKUP_RETENTION_DAYS = '1'
|
|
process.env.BACKUP_RETENTION_DAYS = '1'
|
|
|
|
|
|
|
|
- // 清理测试目录
|
|
|
|
|
- try {
|
|
|
|
|
- await fs.rm(testBackupDir, { recursive: true, force: true })
|
|
|
|
|
- } catch (error) {
|
|
|
|
|
- // 目录可能不存在
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ // 重置所有mock函数
|
|
|
|
|
+ const { promises } = await import('fs')
|
|
|
|
|
+ const mockedPromises = vi.mocked(promises)
|
|
|
|
|
+
|
|
|
|
|
+ // 重置所有mock实现
|
|
|
|
|
+ mockedPromises.mkdir.mockResolvedValue(undefined)
|
|
|
|
|
+ mockedPromises.chmod.mockResolvedValue(undefined)
|
|
|
|
|
+ mockedPromises.readdir.mockResolvedValue([])
|
|
|
|
|
+ mockedPromises.stat.mockResolvedValue({ size: 1024, mtimeMs: Date.now(), mode: 0o600, mtime: new Date() } as any)
|
|
|
|
|
+ mockedPromises.access.mockResolvedValue(undefined)
|
|
|
|
|
+ mockedPromises.unlink.mockResolvedValue(undefined)
|
|
|
|
|
+ mockedPromises.writeFile.mockResolvedValue(undefined)
|
|
|
|
|
+ mockedPromises.rm.mockResolvedValue(undefined)
|
|
|
|
|
+ mockedPromises.utimes.mockResolvedValue(undefined)
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
- afterEach(async () => {
|
|
|
|
|
- // 清理测试目录
|
|
|
|
|
- try {
|
|
|
|
|
- await fs.rm(testBackupDir, { recursive: true, force: true })
|
|
|
|
|
- } catch (error) {
|
|
|
|
|
- // 忽略错误
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
|
|
+ afterEach(() => {
|
|
|
vi.restoreAllMocks()
|
|
vi.restoreAllMocks()
|
|
|
})
|
|
})
|
|
|
|
|
|
|
@@ -88,6 +82,8 @@ describe('Database Backup Integration', () => {
|
|
|
it('应该成功创建备份文件', async () => {
|
|
it('应该成功创建备份文件', async () => {
|
|
|
const backupFile = await databaseBackup.createBackup()
|
|
const backupFile = await databaseBackup.createBackup()
|
|
|
|
|
|
|
|
|
|
+ console.debug('backupFile', backupFile)
|
|
|
|
|
+
|
|
|
expect(backupFile).toBeDefined()
|
|
expect(backupFile).toBeDefined()
|
|
|
expect(backupFile).toContain('.dump')
|
|
expect(backupFile).toContain('.dump')
|
|
|
|
|
|
|
@@ -95,46 +91,44 @@ describe('Database Backup Integration', () => {
|
|
|
const exists = await databaseBackup.backupExists(backupFile)
|
|
const exists = await databaseBackup.backupExists(backupFile)
|
|
|
expect(exists).toBe(true)
|
|
expect(exists).toBe(true)
|
|
|
|
|
|
|
|
- // 验证文件权限
|
|
|
|
|
- const stats = await fs.stat(backupFile)
|
|
|
|
|
- expect(stats.mode & 0o777).toBe(0o600) // 应该只有用户可读写
|
|
|
|
|
|
|
+ // 验证文件权限 - 由于mock环境,我们验证chmod被正确调用
|
|
|
|
|
+ const fs = await import('fs')
|
|
|
|
|
+ expect(vi.mocked(fs.promises.chmod)).toHaveBeenCalledWith(backupFile, 0o600)
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
it('应该设置正确的文件权限', async () => {
|
|
it('应该设置正确的文件权限', async () => {
|
|
|
const backupFile = await databaseBackup.createBackup()
|
|
const backupFile = await databaseBackup.createBackup()
|
|
|
|
|
|
|
|
- const stats = await fs.stat(backupFile)
|
|
|
|
|
- expect(stats.mode & 0o777).toBe(0o600)
|
|
|
|
|
|
|
+ console.debug('backupFile', backupFile)
|
|
|
|
|
+
|
|
|
|
|
+ // 验证chmod被正确调用
|
|
|
|
|
+ const fs = await import('fs')
|
|
|
|
|
+ expect(vi.mocked(fs.promises.chmod)).toHaveBeenCalledWith(backupFile, 0o600)
|
|
|
})
|
|
})
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
describe('Backup Cleanup', () => {
|
|
describe('Backup Cleanup', () => {
|
|
|
it('应该清理旧的备份文件', async () => {
|
|
it('应该清理旧的备份文件', async () => {
|
|
|
- // 创建一些测试备份文件
|
|
|
|
|
|
|
+ const fs = await import('fs')
|
|
|
|
|
+
|
|
|
|
|
+ // 设置readdir返回测试文件
|
|
|
|
|
+ vi.mocked(fs.promises.readdir).mockResolvedValue(['backup-old.dump', 'backup-new.dump'])
|
|
|
|
|
+
|
|
|
|
|
+ // 设置stat返回不同的时间
|
|
|
const now = Date.now()
|
|
const now = Date.now()
|
|
|
const oldFileTime = now - (2 * 24 * 60 * 60 * 1000) // 2天前
|
|
const oldFileTime = now - (2 * 24 * 60 * 60 * 1000) // 2天前
|
|
|
const newFileTime = now - (12 * 60 * 60 * 1000) // 12小时前
|
|
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))
|
|
|
|
|
|
|
+ vi.mocked(fs.promises.stat)
|
|
|
|
|
+ .mockResolvedValueOnce({ mtimeMs: oldFileTime } as any)
|
|
|
|
|
+ .mockResolvedValueOnce({ mtimeMs: newFileTime } as any)
|
|
|
|
|
|
|
|
// 执行清理
|
|
// 执行清理
|
|
|
await databaseBackup.cleanupOldBackups()
|
|
await databaseBackup.cleanupOldBackups()
|
|
|
|
|
|
|
|
- // 验证只有旧文件被删除
|
|
|
|
|
- const oldExists = await databaseBackup.backupExists(oldBackup)
|
|
|
|
|
- const newExists = await databaseBackup.backupExists(newBackup)
|
|
|
|
|
-
|
|
|
|
|
- expect(oldExists).toBe(false)
|
|
|
|
|
- expect(newExists).toBe(true)
|
|
|
|
|
|
|
+ // 验证unlink被正确调用(只针对旧文件)
|
|
|
|
|
+ expect(vi.mocked(fs.promises.unlink)).toHaveBeenCalledTimes(1)
|
|
|
|
|
+ expect(vi.mocked(fs.promises.unlink)).toHaveBeenCalledWith(path.join('./backups', 'backup-old.dump'))
|
|
|
})
|
|
})
|
|
|
})
|
|
})
|
|
|
|
|
|
|
@@ -162,15 +156,16 @@ describe('Database Backup Integration', () => {
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
describe('Scheduled Backups', () => {
|
|
describe('Scheduled Backups', () => {
|
|
|
- it('应该启动和停止定时备份', () => {
|
|
|
|
|
- const cron = require('node-cron')
|
|
|
|
|
|
|
+ it('应该启动和停止定时备份', async () => {
|
|
|
|
|
+ const cron = await import('node-cron')
|
|
|
|
|
|
|
|
databaseBackup.startScheduledBackups()
|
|
databaseBackup.startScheduledBackups()
|
|
|
- expect(cron.schedule).toHaveBeenCalled()
|
|
|
|
|
|
|
+ expect(vi.mocked(cron.default.schedule)).toHaveBeenCalledWith('0 2 * * *', expect.any(Function))
|
|
|
|
|
|
|
|
databaseBackup.stopScheduledBackups()
|
|
databaseBackup.stopScheduledBackups()
|
|
|
// 验证stop方法被调用
|
|
// 验证stop方法被调用
|
|
|
- expect(cron.schedule().stop).toHaveBeenCalled()
|
|
|
|
|
|
|
+ const scheduleInstance = vi.mocked(cron.default.schedule).mock.results[0].value
|
|
|
|
|
+ expect(scheduleInstance.stop).toHaveBeenCalled()
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
it('应该返回备份状态', () => {
|
|
it('应该返回备份状态', () => {
|