gongxiangp2/lib/__tests__/pdf-timeout.test.ts
chenpei 4e89b57798 feat(upload): optimize upload UI and performance
- Expand batch upload list area and optimize preview image layout
- Improve upload speed tracking and display (server-side & client-side)
- Reduce network detection log noise
- Optimize Docker build process (caching, env validation)
- Add shadow environment deployment support
2026-01-28 15:07:50 +08:00

254 lines
7.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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<typeof exec>
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<typeof exec>
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<typeof exec>
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<typeof exec>
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<typeof exec>
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<typeof exec>
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<typeof exec>
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<typeof open>).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)
})
})
})