gongxiangp2/lib/__tests__/materials-upload-task-other-route.test.ts
2026-07-24 14:47:27 +08:00

252 lines
7.3 KiB
TypeScript

/** @jest-environment node */
import { getServerSession } from 'next-auth'
import { POST } from '@/app/api/materials/upload/route'
import { compressImage, sanitizeFileName } from '@/lib/image-processor'
import { createMaterialLog } from '@/lib/material-log'
import { prisma } from '@/lib/prisma'
import { uploadPublicDisplayToS3 } from '@/lib/public-display'
jest.mock('next-auth', () => ({
getServerSession: jest.fn(),
}))
jest.mock('@/lib/auth', () => ({
authOptions: {},
}))
jest.mock('fs', () => ({
promises: {
mkdir: jest.fn().mockResolvedValue(undefined),
writeFile: jest.fn().mockResolvedValue(undefined),
stat: jest.fn(),
},
}))
jest.mock('@/lib/prisma', () => ({
prisma: {
user: {
findUnique: jest.fn(),
},
material: {
create: jest.fn(),
update: jest.fn(),
findUnique: jest.fn(),
delete: jest.fn(),
},
materialAsset: {
createMany: jest.fn(),
},
},
}))
jest.mock('@/lib/s3', () => ({
uploadToS3: jest.fn(),
}))
jest.mock('@/lib/audio-processor', () => ({
parseAudioMetadata: jest.fn(),
isAudioFile: jest.fn(() => false),
}))
jest.mock('@/lib/video-transcode-task', () => ({
enqueueVideoTranscodeTask: jest.fn(),
}))
jest.mock('@/lib/image-processor', () => ({
compressImage: jest.fn(),
sanitizeFileName: jest.fn((value: string) => value),
}))
jest.mock('@/lib/pdf-upload-handler', () => ({
processPdfUpload: jest.fn(),
}))
jest.mock('@/lib/file-cleanup', () => ({
deleteFiles: jest.fn().mockResolvedValue({ deletedCount: 0, failedCount: 0 }),
}))
jest.mock('@/lib/material-cleanup', () => ({
deleteMaterialRelatedRecords: jest.fn(),
}))
jest.mock('@/lib/material-log', () => ({
createMaterialLog: jest.fn().mockResolvedValue(undefined),
}))
jest.mock('@/lib/public-display', () => ({
buildMaterialPublicDisplayPath: jest.fn((fileName: string) => `materials/${fileName}`),
uploadPublicDisplayToS3: jest.fn(),
}))
jest.mock('@/lib/product-package', () => ({
validateTaskNo: jest.fn(() => true),
isTaskNoExists: jest.fn().mockResolvedValue(false),
purgeDeletedMaterialsByTaskNo: jest.fn().mockResolvedValue({
deletedMaterials: 0,
deletedFiles: 0,
failedFiles: 0,
}),
}))
describe('materials upload route - 任务-其他', () => {
beforeEach(() => {
jest.clearAllMocks()
;(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-task-other-1',
previewUrl: 'https://s3.jiansun.vip/gongxiangpan/public-display/materials/preview-task-other.webp',
productModel: 'TASK202603310001',
categoryName: '通用',
sceneName: '通用',
platformName: '',
productCategoryName: '',
materialType: '任务-其他',
renderer: '',
authorName: '安乔',
zipFileName: 'source-package.zip',
zipFilePath: 'uploads/material-1.zip',
zipFileSize: BigInt(7),
displayImages: [],
videoSourceUrl: null,
videoProcessingStatus: null,
userId: 'user-1',
user: {
username: '安乔',
},
})
;(prisma.material.update as jest.Mock).mockResolvedValue({})
;(prisma.materialAsset.createMany as jest.Mock).mockResolvedValue({ count: 2 })
;(compressImage as jest.Mock).mockResolvedValue(Buffer.from('compressed-preview'))
;(uploadPublicDisplayToS3 as jest.Mock).mockResolvedValue(
'https://s3.jiansun.vip/gongxiangpan/public-display/materials/preview-task-other.webp',
)
;(sanitizeFileName as jest.Mock).mockImplementation((value: string) => value)
})
function createRequest(formData: FormData) {
return {
formData: jest.fn().mockResolvedValue(formData),
} as any
}
function buildTaskOtherFormData() {
const formData = new FormData()
formData.append('materialType', '任务-其他')
formData.append('taskNo', 'TASK202603310001')
formData.append('productModel', '')
formData.append('categoryName', '')
formData.append('sceneName', '')
formData.append(
'previewImage',
new File([Buffer.from('preview-image')], 'preview.png', { type: 'image/png' }),
)
formData.append(
'zipFile',
new File([Buffer.from('zipdata')], 'source-package.zip', { type: 'application/zip' }),
)
return formData
}
test('缺少任务号时应立即返回失败', async () => {
const formData = buildTaskOtherFormData()
formData.set('taskNo', '')
const response = await POST(createRequest(formData))
const body = await response.json()
expect(response.status).toBe(400)
expect(body.error).toBe('任务-其他必须填写任务号')
expect(prisma.material.create).not.toHaveBeenCalled()
})
test('普通上传入口必须拒绝任务-详情,避免绕过需求运营必填规则', async () => {
const formData = new FormData()
formData.append('materialType', '任务-详情')
const response = await POST(createRequest(formData))
const body = await response.json()
expect(response.status).toBe(400)
expect(body.error).toContain('必须使用专用上传入口')
expect(prisma.material.create).not.toHaveBeenCalled()
})
test('缺少预览图时应立即返回失败', async () => {
const formData = buildTaskOtherFormData()
formData.delete('previewImage')
const response = await POST(createRequest(formData))
const body = await response.json()
expect(response.status).toBe(400)
expect(body.error).toBe('任务-其他必须上传至少一张预览图')
expect(prisma.material.create).not.toHaveBeenCalled()
})
test('正常上传时应自动补通用店铺和场景,并保留原始压缩包文件名', async () => {
const formData = buildTaskOtherFormData()
const response = await POST(createRequest(formData))
const body = await response.json()
expect(response.status).toBe(201)
expect(body.message).toBe('上传成功')
expect(prisma.material.create).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({
productModel: 'TASK202603310001',
categoryName: '通用',
sceneName: '通用',
materialType: '任务-其他',
zipFileName: 'source-package.zip',
zipFilePath: expect.stringContaining('uploads'),
}),
}),
)
expect(prisma.materialAsset.createMany).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.arrayContaining([
expect.objectContaining({
assetType: 'preview',
}),
expect.objectContaining({
assetType: 'zip',
fileName: 'source-package.zip',
}),
]),
}),
)
expect(prisma.material.update).toHaveBeenCalledWith(
expect.objectContaining({
where: { id: 'material-task-other-1' },
data: expect.objectContaining({
taskNo: 'TASK202603310001',
taskStatus: 'completed',
}),
}),
)
expect(createMaterialLog).toHaveBeenCalledWith(
expect.objectContaining({
changes: expect.objectContaining({
categoryName: expect.objectContaining({
newValue: '通用',
}),
sceneName: expect.objectContaining({
newValue: '通用',
}),
}),
}),
)
})
})