gongxiangp2/lib/__tests__/task-page-upload-routes.test.ts

200 lines
6.8 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.

/** @jest-environment node */
import { NextRequest } from 'next/server'
import { getServerSession } from 'next-auth'
import { POST as initTaskPage } from '@/app/api/materials/upload/task-page/init/route'
import { POST as completeTaskPage } from '@/app/api/materials/upload/task-page/complete/route'
import { prisma } from '@/lib/prisma'
import { cleanupOnError, createZipFromFiles } from '@/lib/task-material-processor'
import { getOwnedUploadedFile } from '@/lib/upload-sessions'
import { stat } from 'fs/promises'
import { TASK_DETAIL_UPLOAD_LIMITS } from '@/lib/task-detail-upload-policy'
jest.mock('next-auth', () => ({ getServerSession: jest.fn() }))
jest.mock('@/lib/auth', () => ({ authOptions: {} }))
jest.mock('@/lib/prisma', () => ({
prisma: {
user: { findUnique: jest.fn() },
material: {
create: jest.fn(),
findFirst: jest.fn(),
findUnique: jest.fn(),
updateMany: jest.fn(),
},
},
}))
jest.mock('@/lib/product-package', () => ({
assignTaskNoOnComplete: jest.fn(),
purgeDeletedMaterialsByTaskNo: jest.fn(),
validateTaskNo: jest.fn(() => true),
}))
jest.mock('@/lib/dingtalk-approval-no', () => ({
getDingtalkApprovalNoError: jest.fn(() => null),
}))
jest.mock('@/lib/task-material-processor', () => ({
createZipFromFiles: jest.fn(),
generatePreviewImages: jest.fn(),
cleanupTempFiles: jest.fn(),
cleanupOnError: jest.fn(),
saveAssets: jest.fn(),
}))
jest.mock('@/lib/material-log', () => ({ createMaterialLog: jest.fn() }))
jest.mock('@/lib/upload-sessions', () => ({ getOwnedUploadedFile: jest.fn() }))
jest.mock('fs/promises', () => ({ stat: jest.fn() }))
describe('任务-页面上传接口', () => {
beforeEach(() => {
jest.clearAllMocks()
jest.spyOn(console, 'log').mockImplementation(() => undefined)
jest.spyOn(console, 'error').mockImplementation(() => undefined)
;(getServerSession as jest.Mock).mockResolvedValue({ user: { id: 'user-1' } })
;(prisma.user.findUnique as jest.Mock).mockResolvedValue({ username: '张三' })
;(prisma.material.create as jest.Mock).mockResolvedValue({ id: 'material-1' })
;(prisma.material.updateMany as jest.Mock).mockResolvedValue({ count: 1 })
;(stat as jest.Mock).mockResolvedValue({ isFile: () => true, size: 10 })
})
afterEach(() => {
jest.restoreAllMocks()
})
test('初始化接口应返回顶层 materialId,并标记为 uploading', async () => {
const response = await initTaskPage(
new NextRequest('http://localhost/api/materials/upload/task-page/init', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
productModel: 'SJ0371',
categoryName: '索菲亚',
sceneName: '书房',
}),
}),
)
const body = await response.json()
expect(response.status).toBe(200)
expect(body).toEqual({ success: true, materialId: 'material-1' })
expect(prisma.material.create).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({
materialType: '任务-页面',
taskStatus: 'uploading',
}),
}),
)
})
test('完成接口不能覆盖其它素材类型', async () => {
;(prisma.material.findUnique as jest.Mock).mockResolvedValue({
id: 'material-1',
userId: 'user-1',
materialType: '任务-详情',
})
const response = await completeTaskPage(
makeCompleteRequest([{ uploadId: 'upload-1', fileName: 'page.jpg', fileSize: 10 }]),
)
expect(response.status).toBe(400)
expect(getOwnedUploadedFile).not.toHaveBeenCalled()
})
test('打包失败时应把上传会话一并交给统一清理', async () => {
;(prisma.material.findUnique as jest.Mock).mockResolvedValue({
id: 'material-1',
userId: 'user-1',
materialType: '任务-页面',
productModel: 'SJ0371',
})
;(getOwnedUploadedFile as jest.Mock).mockResolvedValue('uploads/temp/upload-1/merged.jpg')
;(createZipFromFiles as jest.Mock).mockRejectedValue(new Error('打包失败'))
const response = await completeTaskPage(
makeCompleteRequest([{ uploadId: 'upload-1', fileName: 'page.jpg', fileSize: 10 }]),
)
expect(response.status).toBe(500)
expect(cleanupOnError).toHaveBeenCalledWith(
[],
null,
['uploads/temp/upload-1/merged.jpg'],
'material-1',
['upload-1'],
true,
{ taskStatus: 'processing' },
)
})
test('重复完成请求应返回 409,且不能进入清理删除素材', async () => {
;(prisma.material.findUnique as jest.Mock).mockResolvedValue({
id: 'material-1',
userId: 'user-1',
materialType: '任务-页面',
taskStatus: 'completed',
})
;(prisma.material.updateMany as jest.Mock).mockResolvedValue({ count: 0 })
const response = await completeTaskPage(
makeCompleteRequest([{ uploadId: 'upload-1', fileName: 'page.jpg', fileSize: 10 }]),
)
expect(response.status).toBe(409)
expect(cleanupOnError).not.toHaveBeenCalled()
expect(getOwnedUploadedFile).not.toHaveBeenCalled()
})
test('应按磁盘实际大小在 readFile 前拒绝超大图片并清理本请求', async () => {
;(prisma.material.findUnique as jest.Mock).mockResolvedValue({
id: 'material-1',
userId: 'user-1',
materialType: '任务-页面',
taskStatus: 'uploading',
productModel: 'SJ0371',
})
;(getOwnedUploadedFile as jest.Mock).mockResolvedValue('uploads/temp/upload-1/merged.jpg')
;(stat as jest.Mock).mockResolvedValue({
isFile: () => true,
size: TASK_DETAIL_UPLOAD_LIMITS.maxSingleImageBytes + 1,
})
const response = await completeTaskPage(
makeCompleteRequest([{ uploadId: 'upload-1', fileName: 'large.jpg', fileSize: 1 }]),
)
expect(response.status).toBe(413)
expect(createZipFromFiles).not.toHaveBeenCalled()
expect(cleanupOnError).toHaveBeenCalledWith(
[],
null,
['uploads/temp/upload-1/merged.jpg'],
'material-1',
['upload-1'],
true,
{ taskStatus: 'processing' },
)
})
test('素材查询失败时不能按请求中的 materialId 执行删除清理', async () => {
;(prisma.material.findUnique as jest.Mock).mockRejectedValue(new Error('database unavailable'))
const response = await completeTaskPage(
makeCompleteRequest([{ uploadId: 'upload-1', fileName: 'page.jpg', fileSize: 10 }]),
)
expect(response.status).toBe(500)
expect(cleanupOnError).not.toHaveBeenCalled()
})
})
function makeCompleteRequest(assets: Array<{ uploadId: string; fileName: string; fileSize: number }>) {
return new NextRequest('http://localhost/api/materials/upload/task-page/complete', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
materialId: 'material-1',
assets: { main: assets },
}),
})
}