/** * R41修复:PDF 处理超时控制测试 * * 测试内容: * - pdftoppm 命令的超时控制(10分钟) * - 超时错误检测 * - 命令不存在错误处理 */ import { exec } from 'child_process' import { promisify } from 'util' const execAsync = promisify(exec) // Mock child_process jest.mock('child_process', () => { const originalModule = jest.requireActual('child_process') return { ...originalModule, exec: jest.fn(), } }) // Mock fs.promises for PDF validation jest.mock('fs/promises', () => ({ open: jest.fn(), })) describe('R41: PDF 处理超时控制', () => { beforeEach(() => { jest.clearAllMocks() }) describe('pdftoppm 超时控制', () => { it('应该在正常情况下成功提取 PDF 预览', async () => { const mockExec = exec as jest.MockedFunction mockExec.mockImplementationOnce((command, options, callback) => { if (typeof options === 'function') { callback = options } // 模拟成功输出 ;(callback as Function)(null, { stdout: '', stderr: 'Writing to file test-1.jpg', }) return {} as any }) const command = 'pdftoppm -f 1 -l 1 -scale-to 2400 -jpeg "test.pdf" "test"' const { stdout, stderr } = await execAsync(command, { maxBuffer: 10 * 1024 * 1024, timeout: 10 * 60 * 1000, }) expect(stderr).toContain('Writing') }) it('应该在超时(10分钟)后终止进程', async () => { const mockExec = exec as jest.MockedFunction mockExec.mockImplementationOnce((command, options, callback) => { if (typeof options === 'function') { callback = options } // 模拟超时错误 const error: any = new Error('Command timed out after 600000ms') error.killed = true error.signal = 'SIGTERM' ;(callback as Function)(error, null) return {} as any }) const command = 'pdftoppm -f 1 -l 1 -scale-to 2400 -jpeg "test.pdf" "test"' await expect( execAsync(command, { maxBuffer: 10 * 1024 * 1024, timeout: 10 * 60 * 1000, }) ).rejects.toThrow() }) it('应该检测超时错误并返回友好消息', async () => { const mockExec = exec as jest.MockedFunction mockExec.mockImplementationOnce((command, options, callback) => { if (typeof options === 'function') { callback = options } const error: any = new Error('Timeout') error.killed = true error.signal = 'SIGTERM' ;(callback as Function)(error, null) return {} as any }) const command = 'pdftoppm -f 1 -l 1 -scale-to 2400 -jpeg "test.pdf" "test"' try { await execAsync(command, { maxBuffer: 10 * 1024 * 1024, timeout: 10 * 60 * 1000, }) fail('Should have thrown') } catch (error: any) { expect(error.killed).toBe(true) expect(error.signal).toBe('SIGTERM') } }) }) describe('命令不存在错误处理', () => { it('应该检测 pdftoppm 命令不存在', async () => { const mockExec = exec as jest.MockedFunction mockExec.mockImplementationOnce((command, options, callback) => { if (typeof options === 'function') { callback = options } const error: any = new Error('Command failed: pdftoppm') error.code = 127 ;(callback as Function)(error, null) return {} as any }) const command = 'pdftoppm -f 1 -l 1 -scale-to 2400 -jpeg "test.pdf" "test"' try { await execAsync(command, { maxBuffer: 10 * 1024 * 1024, timeout: 10 * 60 * 1000, }) fail('Should have thrown') } catch (error: any) { expect(error.code).toBe(127) } }) it('应该通过错误消息检测命令不存在', async () => { const mockExec = exec as jest.MockedFunction mockExec.mockImplementationOnce((command, options, callback) => { if (typeof options === 'function') { callback = options } const error: any = new Error('pdftoppm: command not found') ;(callback as Function)(error, null) return {} as any }) const command = 'pdftoppm -f 1 -l 1 -scale-to 2400 -jpeg "test.pdf" "test"' try { await execAsync(command, { maxBuffer: 10 * 1024 * 1024, timeout: 10 * 60 * 1000, }) fail('Should have thrown') } catch (error: any) { expect(error.message).toContain('pdftoppm') } }) }) describe('其他错误处理', () => { it('应该处理磁盘空间不足错误', async () => { const mockExec = exec as jest.MockedFunction mockExec.mockImplementationOnce((command, options, callback) => { if (typeof options === 'function') { callback = options } const error: any = new Error('No space left on device') error.code = 'ENOSPC' ;(callback as Function)(error, null) return {} as any }) const command = 'pdftoppm -f 1 -l 1 -scale-to 2400 -jpeg "test.pdf" "test"' await expect( execAsync(command, { maxBuffer: 10 * 1024 * 1024, timeout: 10 * 60 * 1000, }) ).rejects.toThrow() }) it('应该处理权限错误', async () => { const mockExec = exec as jest.MockedFunction mockExec.mockImplementationOnce((command, options, callback) => { if (typeof options === 'function') { callback = options } const error: any = new Error('Permission denied') error.code = 'EACCES' ;(callback as Function)(error, null) return {} as any }) const command = 'pdftoppm -f 1 -l 1 -scale-to 2400 -jpeg "test.pdf" "test"' await expect( execAsync(command, { maxBuffer: 10 * 1024 * 1024, timeout: 10 * 60 * 1000, }) ).rejects.toThrow() }) }) describe('PDF 文件验证', () => { it('应该正确识别有效的 PDF 文件', async () => { const { open } = await import('fs/promises') const mockFileHandle = { read: jest.fn().mockResolvedValue({ buffer: Buffer.from('%PDF-1.4') }), close: jest.fn(), } ;(open as jest.MockedFunction).mockResolvedValue(mockFileHandle as any) // 这里需要实际实现 isPdfFile 函数的测试 // 简化验证 const header = Buffer.from('%PDF') expect(header.toString('ascii', 0, 4)).toBe('%PDF') }) it('应该拒绝非 PDF 文件', async () => { const header = Buffer.from('GIF8') expect(header.toString('ascii', 0, 4)).not.toBe('%PDF') }) }) describe('超时配置验证', () => { it('PDF 处理超时应该设置为 10 分钟', () => { const PDF_TIMEOUT = 10 * 60 * 1000 // 10分钟 expect(PDF_TIMEOUT).toBe(600000) }) it('maxBuffer 应该设置为 10MB', () => { const MAX_BUFFER = 10 * 1024 * 1024 expect(MAX_BUFFER).toBe(10485760) }) }) })