backup.test.ts 6.2 KB

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