backup.test.ts 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
  1. import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
  2. import { DatabaseBackup } from '../../../../src/server/utils/backup'
  3. import path from 'path'
  4. // Mock pg-dump-restore
  5. vi.mock('pg-dump-restore', () => ({
  6. pgDump: vi.fn().mockResolvedValue(undefined),
  7. pgRestore: vi.fn().mockResolvedValue(undefined),
  8. }))
  9. // Mock fs for tests
  10. vi.mock('fs', () => ({
  11. promises: {
  12. mkdir: vi.fn().mockResolvedValue(undefined),
  13. chmod: vi.fn().mockResolvedValue(undefined),
  14. readdir: vi.fn().mockResolvedValue([]),
  15. stat: vi.fn().mockResolvedValue({ size: 1024, mtimeMs: Date.now(), mode: 0o600, mtime: new Date() }),
  16. access: vi.fn().mockResolvedValue(undefined),
  17. unlink: vi.fn().mockResolvedValue(undefined),
  18. rm: vi.fn().mockResolvedValue(undefined),
  19. writeFile: vi.fn().mockResolvedValue(undefined),
  20. utimes: vi.fn().mockResolvedValue(undefined),
  21. }
  22. }))
  23. // Mock logger
  24. vi.mock('../../../../src/server/utils/logger', () => ({
  25. logger: {
  26. db: vi.fn(),
  27. error: vi.fn(),
  28. api: vi.fn(),
  29. middleware: vi.fn(),
  30. },
  31. }))
  32. describe('DatabaseBackup', () => {
  33. let backup: DatabaseBackup
  34. beforeEach(() => {
  35. vi.clearAllMocks()
  36. backup = DatabaseBackup.getInstance()
  37. })
  38. afterEach(() => {
  39. vi.restoreAllMocks()
  40. })
  41. describe('getInstance', () => {
  42. it('应该返回单例实例', () => {
  43. const instance1 = DatabaseBackup.getInstance()
  44. const instance2 = DatabaseBackup.getInstance()
  45. expect(instance1).toBe(instance2)
  46. })
  47. })
  48. describe('ensureBackupDir', () => {
  49. it('应该创建备份目录并设置权限', async () => {
  50. const fs = await import('fs')
  51. await backup.ensureBackupDir()
  52. expect(fs.promises.mkdir).toHaveBeenCalledWith('./backups', { recursive: true })
  53. expect(fs.promises.chmod).toHaveBeenCalledWith('./backups', 0o700)
  54. })
  55. it('应该在创建目录失败时抛出错误', async () => {
  56. const fs = await import('fs')
  57. const { logger } = await import('../../../../src/server/utils/logger')
  58. vi.mocked(fs.promises.mkdir).mockRejectedValueOnce(new Error('创建目录失败'))
  59. await expect(backup.ensureBackupDir()).rejects.toThrow('创建目录失败')
  60. expect(logger.error).toHaveBeenCalled()
  61. })
  62. })
  63. describe('getDbConfig', () => {
  64. it('应该返回正确的数据库配置', () => {
  65. process.env.DB_HOST = 'test-host'
  66. process.env.DB_PORT = '5433'
  67. process.env.DB_DATABASE = 'test-db'
  68. process.env.DB_USERNAME = 'test-user'
  69. process.env.DB_PASSWORD = 'test-password'
  70. const config = (backup as any).getDbConfig()
  71. expect(config).toEqual({
  72. host: 'test-host',
  73. port: 5433,
  74. database: 'test-db',
  75. username: 'test-user',
  76. password: 'test-password',
  77. })
  78. })
  79. it('应该使用默认值当环境变量未设置时', () => {
  80. delete process.env.DB_HOST
  81. delete process.env.DB_PORT
  82. delete process.env.DB_DATABASE
  83. delete process.env.DB_USERNAME
  84. delete process.env.DB_PASSWORD
  85. const config = (backup as any).getDbConfig()
  86. expect(config).toEqual({
  87. host: 'localhost',
  88. port: 5432,
  89. database: 'postgres',
  90. username: 'postgres',
  91. password: '',
  92. })
  93. })
  94. })
  95. describe('formatFileSize', () => {
  96. it('应该正确格式化文件大小', () => {
  97. const formatFileSize = (backup as any).formatFileSize
  98. expect(formatFileSize(0)).toBe('0 B')
  99. expect(formatFileSize(1024)).toBe('1 KB')
  100. expect(formatFileSize(1048576)).toBe('1 MB')
  101. expect(formatFileSize(1073741824)).toBe('1 GB')
  102. })
  103. })
  104. describe('backupExists', () => {
  105. it('应该返回true当备份文件存在时', async () => {
  106. const fs = await import('fs')
  107. const exists = await backup.backupExists('/path/to/backup.dump')
  108. expect(exists).toBe(true)
  109. expect(fs.promises.access).toHaveBeenCalledWith('/path/to/backup.dump')
  110. })
  111. it('应该返回false当备份文件不存在时', async () => {
  112. const fs = await import('fs')
  113. vi.mocked(fs.promises.access).mockRejectedValueOnce(new Error('文件不存在'))
  114. const exists = await backup.backupExists('/path/to/backup.dump')
  115. expect(exists).toBe(false)
  116. })
  117. })
  118. describe('cleanupOldBackups', () => {
  119. it('应该清理7天前的旧备份', async () => {
  120. const fs = await import('fs')
  121. const { logger } = await import('../../../../src/server/utils/logger')
  122. const now = Date.now()
  123. const oldFileTime = now - (8 * 24 * 60 * 60 * 1000) // 8天前
  124. const newFileTime = now - (6 * 24 * 60 * 60 * 1000) // 6天前
  125. vi.mocked(fs.promises.readdir).mockResolvedValue(['backup-old.dump', 'backup-new.dump'] as any)
  126. vi.mocked(fs.promises.stat)
  127. .mockResolvedValueOnce({ mtimeMs: oldFileTime } as any)
  128. .mockResolvedValueOnce({ mtimeMs: newFileTime } as any)
  129. await backup.cleanupOldBackups()
  130. expect(fs.promises.unlink).toHaveBeenCalledTimes(1)
  131. expect(fs.promises.unlink).toHaveBeenCalledWith(path.join('./backups', 'backup-old.dump'))
  132. expect(logger.db).toHaveBeenCalledWith('删除旧备份文件: backup-old.dump')
  133. })
  134. it('应该在清理失败时记录错误但不抛出', async () => {
  135. const fs = await import('fs')
  136. const { logger } = await import('../../../../src/server/utils/logger')
  137. vi.mocked(fs.promises.readdir).mockRejectedValueOnce(new Error('读取目录失败'))
  138. await expect(backup.cleanupOldBackups()).resolves.not.toThrow()
  139. expect(logger.error).toHaveBeenCalled()
  140. })
  141. })
  142. describe('startScheduledBackups', () => {
  143. it('应该启动定时备份任务', async () => {
  144. const { logger } = await import('../../../../src/server/utils/logger')
  145. backup.startScheduledBackups()
  146. // expect(cron.default.schedule).toHaveBeenCalledWith('0 2 * * *', expect.any(Function))
  147. expect(logger.db).toHaveBeenCalledWith('备份调度已启动: 0 2 * * *')
  148. })
  149. })
  150. describe('stopScheduledBackups', () => {
  151. it('应该停止定时备份任务', async () => {
  152. const { logger } = await import('../../../../src/server/utils/logger')
  153. // 先启动再停止
  154. backup.startScheduledBackups()
  155. backup.stopScheduledBackups()
  156. expect(logger.db).toHaveBeenCalledWith('备份调度已停止')
  157. })
  158. })
  159. })