- 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
254 lines
7.1 KiB
TypeScript
254 lines
7.1 KiB
TypeScript
/**
|
||
* 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)
|
||
})
|
||
})
|
||
})
|