200 lines
6.8 KiB
TypeScript
200 lines
6.8 KiB
TypeScript
/** @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 },
|
||
}),
|
||
})
|
||
}
|