backup.integration.test.ts 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  1. import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
  2. import { databaseBackup } from '../backup'
  3. import { databaseRestore } from '../restore'
  4. import path from 'path'
  5. // Mock pg-dump-restore for integration tests
  6. vi.mock('pg-dump-restore', async (importOriginal) => {
  7. const actual = await importOriginal() as typeof import('pg-dump-restore')
  8. return {
  9. ...actual,
  10. pgDump: vi.fn().mockResolvedValue({ size: 1024 }),
  11. pgRestore: vi.fn().mockResolvedValue(undefined),
  12. }
  13. })
  14. // Mock fs for integration tests
  15. vi.mock('fs', () => ({
  16. promises: {
  17. mkdir: vi.fn().mockResolvedValue(undefined),
  18. chmod: vi.fn().mockResolvedValue(undefined),
  19. readdir: vi.fn().mockResolvedValue([]),
  20. stat: vi.fn().mockResolvedValue({ size: 1024, mtimeMs: Date.now(), mode: 0o600, mtime: new Date() }),
  21. access: vi.fn().mockResolvedValue(undefined),
  22. unlink: vi.fn().mockResolvedValue(undefined),
  23. writeFile: vi.fn().mockResolvedValue(undefined),
  24. rm: vi.fn().mockResolvedValue(undefined),
  25. utimes: vi.fn().mockResolvedValue(undefined),
  26. }
  27. }))
  28. // Mock node-cron
  29. vi.mock('node-cron', () => ({
  30. default: {
  31. schedule: vi.fn().mockImplementation(() => ({
  32. stop: vi.fn(),
  33. nextDate: vi.fn().mockReturnValue(new Date()),
  34. })),
  35. },
  36. }))
  37. // Mock logger
  38. vi.mock('../logger', () => ({
  39. logger: {
  40. db: vi.fn(),
  41. error: vi.fn(),
  42. api: vi.fn(),
  43. middleware: vi.fn(),
  44. },
  45. }))
  46. describe('Database Backup Integration', () => {
  47. const testBackupDir = './test-backups'
  48. beforeEach(async () => {
  49. vi.clearAllMocks()
  50. // 设置测试环境变量
  51. process.env.BACKUP_DIR = testBackupDir
  52. process.env.BACKUP_RETENTION_DAYS = '1'
  53. // 重置所有mock函数
  54. const { promises } = await import('fs')
  55. const mockedPromises = vi.mocked(promises)
  56. // 重置所有mock实现
  57. mockedPromises.mkdir.mockResolvedValue(undefined)
  58. mockedPromises.chmod.mockResolvedValue(undefined)
  59. mockedPromises.readdir.mockResolvedValue([])
  60. mockedPromises.stat.mockResolvedValue({ size: 1024, mtimeMs: Date.now(), mode: 0o600, mtime: new Date() } as any)
  61. mockedPromises.access.mockResolvedValue(undefined)
  62. mockedPromises.unlink.mockResolvedValue(undefined)
  63. mockedPromises.writeFile.mockResolvedValue(undefined)
  64. mockedPromises.rm.mockResolvedValue(undefined)
  65. mockedPromises.utimes.mockResolvedValue(undefined)
  66. })
  67. afterEach(() => {
  68. vi.restoreAllMocks()
  69. })
  70. describe('Backup Creation', () => {
  71. it('应该成功创建备份文件', async () => {
  72. const backupFile = await databaseBackup.createBackup()
  73. console.debug('backupFile', backupFile)
  74. expect(backupFile).toBeDefined()
  75. expect(backupFile).toContain('.dump')
  76. // 验证文件已创建
  77. const exists = await databaseBackup.backupExists(backupFile)
  78. expect(exists).toBe(true)
  79. // 验证文件权限 - 由于mock环境,我们验证chmod被正确调用
  80. const fs = await import('fs')
  81. expect(vi.mocked(fs.promises.chmod)).toHaveBeenCalledWith(backupFile, 0o600)
  82. })
  83. it('应该设置正确的文件权限', async () => {
  84. const backupFile = await databaseBackup.createBackup()
  85. console.debug('backupFile', backupFile)
  86. // 验证chmod被正确调用
  87. const fs = await import('fs')
  88. expect(vi.mocked(fs.promises.chmod)).toHaveBeenCalledWith(backupFile, 0o600)
  89. })
  90. })
  91. describe('Backup Cleanup', () => {
  92. it('应该清理旧的备份文件', async () => {
  93. const fs = await import('fs')
  94. // 设置readdir返回测试文件
  95. vi.mocked(fs.promises.readdir).mockResolvedValue(['backup-old.dump', 'backup-new.dump'])
  96. // 设置stat返回不同的时间
  97. const now = Date.now()
  98. const oldFileTime = now - (2 * 24 * 60 * 60 * 1000) // 2天前
  99. const newFileTime = now - (12 * 60 * 60 * 1000) // 12小时前
  100. vi.mocked(fs.promises.stat)
  101. .mockResolvedValueOnce({ mtimeMs: oldFileTime } as any)
  102. .mockResolvedValueOnce({ mtimeMs: newFileTime } as any)
  103. // 执行清理
  104. await databaseBackup.cleanupOldBackups()
  105. // 验证unlink被正确调用(只针对旧文件)
  106. expect(vi.mocked(fs.promises.unlink)).toHaveBeenCalledTimes(1)
  107. expect(vi.mocked(fs.promises.unlink)).toHaveBeenCalledWith(path.join('./backups', 'backup-old.dump'))
  108. })
  109. })
  110. describe('Backup Management', () => {
  111. it('应该能够检查备份文件是否存在', async () => {
  112. const backupFile = await databaseBackup.createBackup()
  113. const exists = await databaseBackup.backupExists(backupFile)
  114. expect(exists).toBe(true)
  115. const notExists = await databaseBackup.backupExists('/nonexistent/path.dump')
  116. expect(notExists).toBe(false)
  117. })
  118. it('应该能够获取备份文件信息', async () => {
  119. const backupFile = await databaseBackup.createBackup()
  120. const info = await databaseBackup.getBackupInfo(backupFile)
  121. expect(info).toHaveProperty('size')
  122. expect(info).toHaveProperty('mtime')
  123. expect(info).toHaveProperty('formattedSize')
  124. expect(info.formattedSize).toBe('14 B') // 'mock backup data' 的长度
  125. })
  126. })
  127. describe('Scheduled Backups', () => {
  128. it('应该启动和停止定时备份', async () => {
  129. const cron = await import('node-cron')
  130. databaseBackup.startScheduledBackups()
  131. expect(vi.mocked(cron.default.schedule)).toHaveBeenCalledWith('0 2 * * *', expect.any(Function))
  132. databaseBackup.stopScheduledBackups()
  133. // 验证stop方法被调用
  134. const scheduleInstance = vi.mocked(cron.default.schedule).mock.results[0].value
  135. expect(scheduleInstance.stop).toHaveBeenCalled()
  136. })
  137. it('应该返回备份状态', () => {
  138. databaseBackup.startScheduledBackups()
  139. const status = databaseBackup.getBackupStatus()
  140. expect(status).toHaveProperty('scheduled')
  141. expect(status).toHaveProperty('nextRun')
  142. expect(status).toHaveProperty('lastRun')
  143. expect(status.scheduled).toBe(true)
  144. })
  145. })
  146. describe('Restore Integration', () => {
  147. it('应该能够找到最新备份', async () => {
  148. // 创建多个备份文件
  149. await databaseBackup.createBackup()
  150. await new Promise(resolve => setTimeout(resolve, 100)) // 确保时间戳不同
  151. await databaseBackup.createBackup()
  152. const latestBackup = await databaseRestore.findLatestBackup()
  153. expect(latestBackup).toBeDefined()
  154. expect(latestBackup).toContain('.dump')
  155. })
  156. it('应该能够列出所有备份', async () => {
  157. // 创建多个备份文件
  158. await databaseBackup.createBackup()
  159. await databaseBackup.createBackup()
  160. const backups = await databaseRestore.listBackups()
  161. expect(backups.length).toBe(2)
  162. expect(backups.every(b => b.endsWith('.dump'))).toBe(true)
  163. })
  164. })
  165. })