252 lines
7.3 KiB
TypeScript
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: '通用',
|
|
}),
|
|
}),
|
|
}),
|
|
)
|
|
})
|
|
})
|