backup.integration.test.ts 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229
  1. import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
  2. import { databaseBackup } from '@d8d/server/utils/backup'
  3. import { databaseRestore } from '@d8d/server/utils/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 with importOriginal for proper partial mocking
  29. vi.mock('node-cron', async (importOriginal) => {
  30. const actual = await importOriginal() as typeof import('node-cron')
  31. return {
  32. ...actual,
  33. default: {
  34. schedule: vi.fn().mockImplementation(() => ({
  35. stop: vi.fn(),
  36. nextDate: vi.fn().mockReturnValue(new Date()),
  37. })),
  38. },
  39. }
  40. })
  41. // Mock logger
  42. vi.mock('@d8d/server/utils/logger', () => ({
  43. logger: {
  44. db: vi.fn(),
  45. error: vi.fn(),
  46. api: vi.fn(),
  47. middleware: vi.fn(),
  48. },
  49. }))
  50. describe('Database Backup Integration', () => {
  51. const testBackupDir = './test-backups'
  52. beforeEach(async () => {
  53. vi.clearAllMocks()
  54. // 设置测试环境变量
  55. process.env.BACKUP_DIR = testBackupDir
  56. process.env.BACKUP_RETENTION_DAYS = '1'
  57. // 重置所有mock函数
  58. const { promises } = await import('fs')
  59. const mockedPromises = vi.mocked(promises)
  60. // 重置所有mock实现
  61. mockedPromises.mkdir.mockResolvedValue(undefined)
  62. mockedPromises.chmod.mockResolvedValue(undefined)
  63. mockedPromises.readdir.mockResolvedValue([])
  64. mockedPromises.stat.mockResolvedValue({ size: 1024, mtimeMs: Date.now(), mode: 0o600, mtime: new Date() } as any)
  65. mockedPromises.access.mockResolvedValue(undefined)
  66. mockedPromises.unlink.mockResolvedValue(undefined)
  67. mockedPromises.writeFile.mockResolvedValue(undefined)
  68. mockedPromises.rm.mockResolvedValue(undefined)
  69. mockedPromises.utimes.mockResolvedValue(undefined)
  70. })
  71. afterEach(() => {
  72. vi.restoreAllMocks()
  73. })
  74. describe('Backup Creation', () => {
  75. it('应该成功创建备份文件', async () => {
  76. const backupFile = await databaseBackup.createBackup()
  77. console.debug('backupFile', backupFile)
  78. expect(backupFile).toBeDefined()
  79. expect(backupFile).toContain('.dump')
  80. // 验证文件已创建
  81. const exists = await databaseBackup.backupExists(backupFile)
  82. expect(exists).toBe(true)
  83. // 验证文件权限 - 由于mock环境,我们验证chmod被正确调用
  84. const fs = await import('fs')
  85. expect(vi.mocked(fs.promises.chmod)).toHaveBeenCalledWith(backupFile, 0o600)
  86. })
  87. it('应该设置正确的文件权限', async () => {
  88. const backupFile = await databaseBackup.createBackup()
  89. console.debug('backupFile', backupFile)
  90. // 验证chmod被正确调用
  91. const fs = await import('fs')
  92. expect(vi.mocked(fs.promises.chmod)).toHaveBeenCalledWith(backupFile, 0o600)
  93. })
  94. })
  95. describe('Backup Cleanup', () => {
  96. it('应该清理旧的备份文件', async () => {
  97. const fs = await import('fs')
  98. // 设置readdir返回测试文件
  99. vi.mocked(fs.promises.readdir).mockResolvedValue(['backup-old.dump', 'backup-new.dump'] as any)
  100. // 设置stat返回不同的时间
  101. const now = Date.now()
  102. const oldFileTime = now - (8 * 24 * 60 * 60 * 1000) // 8天前(超过保留期)
  103. const newFileTime = now - (6 * 24 * 60 * 60 * 1000) // 6天前(在保留期内)
  104. vi.mocked(fs.promises.stat)
  105. .mockResolvedValueOnce({ size: 1024, mtimeMs: oldFileTime, mtime: new Date(oldFileTime), mode: 0o600 } as any)
  106. .mockResolvedValueOnce({ size: 1024, mtimeMs: newFileTime, mtime: new Date(newFileTime), mode: 0o600 } as any)
  107. // 执行清理
  108. await databaseBackup.cleanupOldBackups()
  109. // 验证unlink被正确调用(只针对旧文件)
  110. expect(vi.mocked(fs.promises.unlink)).toHaveBeenCalledTimes(1)
  111. expect(vi.mocked(fs.promises.unlink)).toHaveBeenCalledWith(path.join('./backups', 'backup-old.dump'))
  112. })
  113. })
  114. describe('Backup Management', () => {
  115. it('应该能够检查备份文件是否存在', async () => {
  116. const backupFile = await databaseBackup.createBackup()
  117. const exists = await databaseBackup.backupExists(backupFile)
  118. expect(exists).toBe(true)
  119. // 对于不存在的文件,mock应该返回rejected
  120. const fs = await import('fs')
  121. vi.mocked(fs.promises.access).mockRejectedValueOnce(new Error('文件不存在'))
  122. const notExists = await databaseBackup.backupExists('/nonexistent/path.dump')
  123. expect(notExists).toBe(false)
  124. })
  125. it('应该能够获取备份文件信息', async () => {
  126. const backupFile = await databaseBackup.createBackup()
  127. const info = await databaseBackup.getBackupInfo(backupFile)
  128. expect(info).toHaveProperty('size')
  129. expect(info).toHaveProperty('mtime')
  130. expect(info).toHaveProperty('formattedSize')
  131. expect(info.formattedSize).toBe('1 KB') // mock stat返回1024字节
  132. })
  133. })
  134. describe('Scheduled Backups', () => {
  135. it('应该启动和停止定时备份', async () => {
  136. const cron = await import('node-cron')
  137. databaseBackup.startScheduledBackups()
  138. expect(vi.mocked(cron.default.schedule)).toHaveBeenCalledWith('0 2 * * *', expect.any(Function))
  139. databaseBackup.stopScheduledBackups()
  140. // 验证schedule方法被调用
  141. expect(vi.mocked(cron.default.schedule)).toHaveBeenCalledTimes(1)
  142. // 验证返回的mock实例的stop方法被调用
  143. const mockCalls = vi.mocked(cron.default.schedule).mock.calls
  144. const mockReturnValue = vi.mocked(cron.default.schedule).mock.results[0]?.value
  145. if (mockReturnValue) {
  146. expect(mockReturnValue.stop).toHaveBeenCalled()
  147. } else {
  148. // 如果mock没有正确返回实例,至少验证schedule被调用
  149. expect(mockCalls.length).toBe(1)
  150. }
  151. })
  152. it('应该返回备份状态', () => {
  153. databaseBackup.startScheduledBackups()
  154. const status = databaseBackup.getBackupStatus()
  155. expect(status).toHaveProperty('scheduled')
  156. expect(status).toHaveProperty('nextRun')
  157. expect(status).toHaveProperty('lastRun')
  158. expect(status.scheduled).toBe(true)
  159. })
  160. })
  161. describe('Restore Integration', () => {
  162. it('应该能够找到最新备份', async () => {
  163. const fs = await import('fs')
  164. // 设置readdir返回测试文件(字符串数组)
  165. vi.mocked(fs.promises.readdir).mockResolvedValue([
  166. 'backup-2024-01-01T00-00-00Z.dump',
  167. 'backup-2024-01-03T00-00-00Z.dump',
  168. ] as any)
  169. const latestBackup = await databaseRestore.findLatestBackup()
  170. expect(latestBackup).toBeDefined()
  171. expect(latestBackup).toBe(path.join('./backups', 'backup-2024-01-03T00-00-00Z.dump'))
  172. })
  173. it('应该能够列出所有备份', async () => {
  174. const fs = await import('fs')
  175. // 设置readdir返回测试文件(字符串数组)
  176. vi.mocked(fs.promises.readdir).mockResolvedValue([
  177. 'backup-1.dump',
  178. 'backup-2.dump',
  179. 'other-file.txt'
  180. ] as any)
  181. const backups = await databaseRestore.listBackups()
  182. expect(backups).toEqual(['backup-1.dump', 'backup-2.dump'])
  183. })
  184. })
  185. })