253 lines
7.3 KiB
TypeScript
253 lines
7.3 KiB
TypeScript
/** @jest-environment node */
|
|
|
|
import { NextRequest } from 'next/server'
|
|
import { GET as getMaterialById, PUT as putMaterialById } from '@/app/api/materials/[id]/route'
|
|
import { PUT as putMaterialUpdate } from '@/app/api/materials/update/route'
|
|
import { getServerSession } from 'next-auth'
|
|
import { updateMaterialWithSharedRules } from '@/lib/material-update'
|
|
import { prisma } from '@/lib/prisma'
|
|
import {
|
|
canUserDownloadMaterial,
|
|
canUserDownloadSource,
|
|
canUserViewMaterial,
|
|
isShareLinkAccess,
|
|
isSuperAdmin,
|
|
isSystemAdmin,
|
|
} from '@/lib/department-permission'
|
|
import { deriveTaskDetailIsFullSet } from '@/lib/task-detail-full-set'
|
|
|
|
jest.mock('next-auth', () => ({
|
|
getServerSession: jest.fn(),
|
|
}))
|
|
|
|
jest.mock('@/lib/auth', () => ({
|
|
authOptions: {},
|
|
}))
|
|
|
|
jest.mock('@/lib/prisma', () => ({
|
|
prisma: {
|
|
material: {
|
|
findFirst: jest.fn(),
|
|
},
|
|
user: {
|
|
findUnique: jest.fn(),
|
|
},
|
|
},
|
|
}))
|
|
|
|
jest.mock('@/lib/department-permission', () => ({
|
|
canUserDownloadMaterial: jest.fn(),
|
|
canUserDownloadSource: jest.fn(),
|
|
canUserViewMaterial: jest.fn(),
|
|
isShareLinkAccess: jest.fn(),
|
|
isSuperAdmin: jest.fn(),
|
|
isSystemAdmin: jest.fn(),
|
|
}))
|
|
|
|
jest.mock('@/lib/task-detail-full-set', () => ({
|
|
deriveTaskDetailIsFullSet: jest.fn(),
|
|
}))
|
|
|
|
jest.mock('@/lib/material-update', () => ({
|
|
updateMaterialWithSharedRules: jest.fn(),
|
|
}))
|
|
|
|
describe('material edit routes', () => {
|
|
const originalFile = globalThis.File
|
|
|
|
beforeEach(() => {
|
|
jest.clearAllMocks()
|
|
;(getServerSession as jest.Mock).mockResolvedValue({
|
|
user: {
|
|
id: 'user-1',
|
|
role: 'USER',
|
|
department: '测试部门',
|
|
managedDepartment: null,
|
|
},
|
|
})
|
|
;(isShareLinkAccess as jest.Mock).mockReturnValue(false)
|
|
;(isSuperAdmin as jest.Mock).mockResolvedValue(false)
|
|
;(isSystemAdmin as jest.Mock).mockReturnValue(false)
|
|
;(canUserViewMaterial as jest.Mock).mockResolvedValue(true)
|
|
;(canUserDownloadMaterial as jest.Mock).mockResolvedValue(true)
|
|
;(canUserDownloadSource as jest.Mock).mockResolvedValue(true)
|
|
;(prisma.user.findUnique as jest.Mock).mockResolvedValue({ canDownload: true })
|
|
;(deriveTaskDetailIsFullSet as jest.Mock).mockReturnValue(false)
|
|
;(updateMaterialWithSharedRules as jest.Mock).mockResolvedValue({
|
|
material: {
|
|
id: 'material-1',
|
|
productModel: 'SPU-001',
|
|
categoryName: '店铺A',
|
|
sceneName: '场景A',
|
|
platformName: 'Amazon',
|
|
productCategoryName: '椅子',
|
|
materialType: '标准模型',
|
|
renderer: 'CR',
|
|
authorName: '测试作者',
|
|
taskNo: null,
|
|
taskDetailSourceType: null,
|
|
taskStatus: null,
|
|
taskFetchedAt: null,
|
|
isFullSet: false,
|
|
zipFileSize: 0,
|
|
},
|
|
})
|
|
Object.defineProperty(globalThis, 'File', {
|
|
configurable: true,
|
|
writable: true,
|
|
value: undefined,
|
|
})
|
|
})
|
|
|
|
afterAll(() => {
|
|
Object.defineProperty(globalThis, 'File', {
|
|
configurable: true,
|
|
writable: true,
|
|
value: originalFile,
|
|
})
|
|
})
|
|
|
|
function createFormData() {
|
|
const formData = new FormData()
|
|
formData.append('productModel', 'SPU-001')
|
|
formData.append('categoryName', '店铺A')
|
|
formData.append('sceneName', '场景A')
|
|
formData.append('platformName', 'Amazon')
|
|
formData.append('productCategoryName', '椅子')
|
|
formData.append('materialType', '标准模型')
|
|
formData.append('renderer', 'CR')
|
|
formData.append('authorName', '测试作者')
|
|
formData.append('userId', 'user-1')
|
|
formData.append('taskNo', '')
|
|
formData.append('taskDetailSourceType', 'AI')
|
|
return formData
|
|
}
|
|
|
|
test('PUT /api/materials/[id] 在没有全局 File 时仍可正常处理表单更新', async () => {
|
|
const request = new NextRequest('http://localhost/api/materials/material-1', {
|
|
method: 'PUT',
|
|
body: createFormData(),
|
|
})
|
|
|
|
const response = await putMaterialById(request, {
|
|
params: { id: 'material-1' },
|
|
})
|
|
const body = await response.json()
|
|
|
|
expect(response.status).toBe(200)
|
|
expect(body.success).toBe(true)
|
|
expect(updateMaterialWithSharedRules).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
materialId: 'material-1',
|
|
previewImage: null,
|
|
}),
|
|
)
|
|
})
|
|
|
|
test('PUT /api/materials/update 在没有全局 File 时仍可正常处理表单更新', async () => {
|
|
const formData = createFormData()
|
|
formData.append('id', 'material-1')
|
|
|
|
const request = new NextRequest('http://localhost/api/materials/update', {
|
|
method: 'PUT',
|
|
body: formData,
|
|
})
|
|
|
|
const response = await putMaterialUpdate(request)
|
|
const body = await response.json()
|
|
|
|
expect(response.status).toBe(200)
|
|
expect(body.message).toBe('更新成功')
|
|
expect(updateMaterialWithSharedRules).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
materialId: 'material-1',
|
|
previewImage: null,
|
|
}),
|
|
)
|
|
})
|
|
|
|
test('GET /api/materials/[id] 应将 assets 中的 BigInt fileSize 序列化为字符串', async () => {
|
|
;(prisma.material.findFirst as jest.Mock).mockResolvedValue({
|
|
id: 'material-1',
|
|
userId: 'user-1',
|
|
productModel: 'SPU-001',
|
|
categoryName: '店铺A',
|
|
sceneName: '场景A',
|
|
platformName: 'Amazon',
|
|
productCategoryName: '椅子',
|
|
materialType: '任务-AI原图',
|
|
isAiGenerated: false,
|
|
renderer: 'CR',
|
|
authorName: '测试作者',
|
|
previewUrl: 'https://s3.jiansun.vip/preview.webp',
|
|
zipFileName: 'material.zip',
|
|
zipFilePath: 'uploads/brand-vi/original.pdf',
|
|
zipFileSize: BigInt(123),
|
|
downloadCount: 1,
|
|
likeCount: 2,
|
|
displayImages: [],
|
|
createdAt: new Date('2026-04-03T00:00:00.000Z'),
|
|
videoSourceUrl: null,
|
|
videoPreview540Url: null,
|
|
videoPreviewClipUrl: null,
|
|
videoCoverUrl: null,
|
|
videoDuration: null,
|
|
videoProcessingStatus: null,
|
|
taskNo: 'TASK202603261129596600-01-01',
|
|
taskStatus: 'fetched',
|
|
taskFetchedAt: new Date('2026-04-03T00:00:01.000Z'),
|
|
isFullSet: false,
|
|
taskDetailSourceType: '渲染',
|
|
user: {
|
|
id: 'user-1',
|
|
username: '测试用户',
|
|
realName: '测试用户',
|
|
},
|
|
assets: [
|
|
{
|
|
id: 'asset-1',
|
|
assetType: 'zip',
|
|
fileUrl: 'uploads/material.zip',
|
|
fileName: 'material.zip',
|
|
fileSize: BigInt(456),
|
|
sortOrder: 0,
|
|
},
|
|
],
|
|
})
|
|
|
|
const request = new NextRequest('http://localhost/api/materials/material-1', {
|
|
method: 'GET',
|
|
})
|
|
|
|
const response = await getMaterialById(request, {
|
|
params: { id: 'material-1' },
|
|
})
|
|
const body = await response.json()
|
|
|
|
expect(response.status).toBe(200)
|
|
expect(body.zipFileSize).toBe('123')
|
|
expect(body.assets[0].fileSize).toBe('456')
|
|
expect(body.taskDetailSourceType).toBe('渲染')
|
|
expect(body.sourceFileUrl).toBe('/api/materials/download?id=material-1')
|
|
})
|
|
|
|
test('PUT 路由应透传任务详情出图方式字段', async () => {
|
|
const request = new NextRequest('http://localhost/api/materials/material-1', {
|
|
method: 'PUT',
|
|
body: createFormData(),
|
|
})
|
|
|
|
await putMaterialById(request, {
|
|
params: { id: 'material-1' },
|
|
})
|
|
|
|
expect(updateMaterialWithSharedRules).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
input: expect.objectContaining({
|
|
taskDetailSourceType: 'AI',
|
|
}),
|
|
}),
|
|
)
|
|
})
|
|
})
|