gongxiangp2/lib/__tests__/external-material-import.test.ts
chenpei 4712175cb0 feat(素材链路): 发布白底图双轨与AI生图回传能力
- 完成白底图 WebP 双轨接口、上传链路与预览缓存改造
- 新增 AI 生图平台成品回传接口、任务状态与表单规则
- 补齐 AI 生成标签、任务详情补全、相关测试与对接文档
2026-03-25 10:28:02 +08:00

410 lines
11 KiB
TypeScript

/** @jest-environment node */
import {
EXTERNAL_MATERIAL_IMPORT_STATUS,
buildProductPackageImportAssets,
createExternalMaterialImportJob,
getExternalMaterialImportJobStatus,
getExternalMaterialImportSchema,
processExternalMaterialImportJob,
} from '@/lib/external-material-import'
import { prisma } from '@/lib/prisma'
import { completeProductPackageUpload } from '@/lib/product-package-completion'
jest.mock('@/lib/prisma', () => ({
prisma: {
$executeRawUnsafe: jest.fn(),
$transaction: jest.fn(),
category: {
findMany: jest.fn(),
},
scene: {
findMany: jest.fn(),
},
platform: {
findMany: jest.fn(),
},
productCategory: {
findMany: jest.fn(),
},
user: {
findUnique: jest.fn(),
},
material: {
create: jest.fn(),
},
externalMaterialImportJob: {
findFirst: jest.fn(),
findUnique: jest.fn(),
update: jest.fn(),
},
},
}))
jest.mock('@/lib/product-package-completion', () => ({
completeProductPackageUpload: jest.fn(),
}))
describe('external material import', () => {
const tx = {
$executeRaw: jest.fn(),
$queryRaw: jest.fn(),
user: {
findFirst: jest.fn(),
findUnique: jest.fn(),
},
material: {
findFirst: jest.fn(),
create: jest.fn(),
},
externalMaterialImportJob: {
findUnique: jest.fn(),
findFirst: jest.fn(),
create: jest.fn(),
update: jest.fn(),
},
}
const validBody = {
clientRequestId: 'req-001',
materialType: '任务-详情',
requester: {
dingtalkUserId: 'ding-user-1',
},
formData: {
taskNo: 'TASK202603240001',
productModel: 'SPU-001',
categoryName: '旗舰店',
sceneName: '客厅',
platformName: '天猫',
productCategoryName: '沙发',
},
sections: [
{
key: 'detail',
images: [
{
url: 'https://img.example/detail-2.jpg',
fileName: 'detail-2.jpg',
fileSize: 22,
sortOrder: 20,
},
{
url: 'https://img.example/detail-1.jpg',
fileName: 'detail-1.jpg',
fileSize: 11,
sortOrder: 10,
},
],
},
{
key: 'main',
images: [
{
url: 'https://img.example/main-1.jpg',
fileName: 'main-1.jpg',
fileSize: 33,
sortOrder: 1,
},
],
},
],
}
beforeEach(() => {
jest.clearAllMocks()
;(prisma.$transaction as jest.Mock).mockImplementation(async (callback: any) => callback(tx))
;(prisma.$executeRawUnsafe as jest.Mock).mockResolvedValue(undefined)
tx.$executeRaw.mockResolvedValue(1)
tx.$queryRaw.mockResolvedValue(undefined)
tx.user.findFirst.mockResolvedValue({
id: 'user-1',
username: 'zhangsan',
realName: '张三',
dingtalkUserId: 'ding-user-1',
})
tx.user.findUnique.mockResolvedValue({
id: 'user-1',
username: 'zhangsan',
realName: '张三',
})
tx.material.findFirst.mockResolvedValue(null)
tx.material.create.mockResolvedValue({
id: 'material-1',
userId: 'user-1',
})
tx.externalMaterialImportJob.findUnique.mockResolvedValue(null)
tx.externalMaterialImportJob.findFirst.mockResolvedValue(null)
tx.externalMaterialImportJob.create.mockResolvedValue({
id: 'job-1',
apiKeyId: 'api-1',
clientRequestId: 'req-001',
materialType: '任务-详情',
taskNo: 'TASK202603240001',
status: EXTERNAL_MATERIAL_IMPORT_STATUS.PROCESSING,
resultMaterialId: null,
errorCode: null,
errorMessage: null,
createdAt: new Date('2026-03-24T09:00:00.000Z'),
updatedAt: new Date('2026-03-24T09:00:00.000Z'),
completedAt: null,
})
tx.externalMaterialImportJob.update.mockResolvedValue(undefined)
;(prisma.category.findMany as jest.Mock).mockResolvedValue([
{ id: 'category-1', name: '旗舰店' },
])
;(prisma.scene.findMany as jest.Mock).mockResolvedValue([
{ id: 'scene-1', name: '客厅' },
])
;(prisma.platform.findMany as jest.Mock).mockResolvedValue([
{ id: 'platform-1', name: '天猫' },
])
;(prisma.productCategory.findMany as jest.Mock).mockResolvedValue([
{ id: 'pc-1', name: '沙发' },
])
;(prisma.externalMaterialImportJob.findFirst as jest.Mock).mockResolvedValue({
id: 'job-1',
apiKeyId: 'api-1',
clientRequestId: 'req-001',
materialType: '任务-详情',
taskNo: 'TASK202603240001',
status: EXTERNAL_MATERIAL_IMPORT_STATUS.SUCCEEDED,
resultMaterialId: 'material-1',
errorCode: null,
errorMessage: null,
createdAt: new Date('2026-03-24T09:00:00.000Z'),
updatedAt: new Date('2026-03-24T09:02:00.000Z'),
completedAt: new Date('2026-03-24T09:02:00.000Z'),
})
;(prisma.externalMaterialImportJob.findUnique as jest.Mock).mockResolvedValue({
id: 'job-1',
apiKeyId: 'api-1',
clientRequestId: 'req-001',
materialType: '任务-详情',
taskNo: 'TASK202603240001',
status: EXTERNAL_MATERIAL_IMPORT_STATUS.PROCESSING,
resolvedUserId: 'user-1',
requestPayload: JSON.stringify(validBody),
resultMaterialId: null,
errorCode: null,
errorMessage: null,
completedAt: null,
})
;(prisma.externalMaterialImportJob.update as jest.Mock).mockResolvedValue(undefined)
;(prisma.user.findUnique as jest.Mock).mockResolvedValue({
id: 'user-1',
username: 'zhangsan',
realName: '张三',
})
;(prisma.material.create as jest.Mock).mockResolvedValue({
id: 'material-1',
userId: 'user-1',
})
;(completeProductPackageUpload as jest.Mock).mockResolvedValue(undefined)
})
test('schema 应按当前项目真实规则返回任务-详情字段和下拉项', async () => {
const schema = await getExternalMaterialImportSchema('任务-详情')
expect(schema).toEqual({
materialType: '任务-详情',
open: true,
fields: [
{
name: 'taskNo',
label: '任务号',
control: 'text',
required: true,
},
{
name: 'productModel',
label: 'SPU',
control: 'text',
required: true,
},
{
name: 'categoryName',
label: '店铺',
control: 'select',
required: true,
options: [{ label: '旗舰店', value: '旗舰店' }],
},
{
name: 'sceneName',
label: '场景',
control: 'select',
required: true,
options: [{ label: '客厅', value: '客厅' }],
},
{
name: 'platformName',
label: '平台',
control: 'select',
required: false,
options: [{ label: '天猫', value: '天猫' }],
},
{
name: 'productCategoryName',
label: '品类',
control: 'select',
required: false,
options: [{ label: '沙发', value: '沙发' }],
},
],
imageGroups: {
allowedKeys: ['main', 'sku', 'detail'],
minTotalImages: 1,
},
})
})
test('创建导入任务时,钉钉身份映射失败应立即拒绝且不创建任务', async () => {
tx.user.findFirst.mockResolvedValueOnce(null)
await expect(
createExternalMaterialImportJob({
apiKeyId: 'api-1',
body: validBody,
}),
).rejects.toMatchObject({
status: 400,
code: 'USER_NOT_FOUND',
})
expect(tx.$executeRaw).toHaveBeenCalled()
expect(tx.externalMaterialImportJob.create).not.toHaveBeenCalled()
})
test('相同 clientRequestId 重复提交时应直接返回已有任务', async () => {
tx.externalMaterialImportJob.findUnique.mockResolvedValueOnce({
id: 'job-existing',
apiKeyId: 'api-1',
clientRequestId: 'req-001',
materialType: '任务-详情',
taskNo: 'TASK202603240001',
status: EXTERNAL_MATERIAL_IMPORT_STATUS.PROCESSING,
resultMaterialId: null,
errorCode: null,
errorMessage: null,
createdAt: new Date('2026-03-24T09:00:00.000Z'),
updatedAt: new Date('2026-03-24T09:00:00.000Z'),
completedAt: null,
})
const result = await createExternalMaterialImportJob({
apiKeyId: 'api-1',
body: validBody,
})
expect(result.created).toBe(false)
expect(result.job.jobId).toBe('job-existing')
expect(tx.externalMaterialImportJob.create).not.toHaveBeenCalled()
})
test('图片分组应允许非全套提交,但详情图顺序必须按显式顺序排序', () => {
const assets = buildProductPackageImportAssets(validBody.sections)
expect(assets).toEqual({
main: [
{
fileUrl: 'https://img.example/main-1.jpg',
fileName: 'main-1.jpg',
fileSize: 33,
sortOrder: 1,
},
],
sku: [],
detail: [
{
fileUrl: 'https://img.example/detail-1.jpg',
fileName: 'detail-1.jpg',
fileSize: 11,
sortOrder: 10,
},
{
fileUrl: 'https://img.example/detail-2.jpg',
fileName: 'detail-2.jpg',
fileSize: 22,
sortOrder: 20,
},
],
})
})
test('状态查询应返回成功态和结果素材编号', async () => {
const status = await getExternalMaterialImportJobStatus({
apiKeyId: 'api-1',
jobId: 'job-1',
})
expect(status).toEqual({
jobId: 'job-1',
materialType: '任务-详情',
taskNo: 'TASK202603240001',
status: EXTERNAL_MATERIAL_IMPORT_STATUS.SUCCEEDED,
success: true,
resultMaterialId: 'material-1',
errorCode: null,
errorMessage: null,
completedAt: '2026-03-24T09:02:00.000Z',
})
})
test('处理导入任务时应复用任务-详情完成入库能力并保留详情顺序', async () => {
await processExternalMaterialImportJob('job-1')
expect(prisma.material.create).toHaveBeenCalledWith({
data: expect.objectContaining({
productModel: 'SPU-001',
categoryName: '旗舰店',
sceneName: '客厅',
platformName: '天猫',
productCategoryName: '沙发',
materialType: '任务-详情',
userId: 'user-1',
isAiGenerated: true,
}),
})
expect(completeProductPackageUpload).toHaveBeenCalledWith({
materialId: 'material-1',
taskNo: 'TASK202603240001',
operatorUserId: 'user-1',
assets: {
main: [
{
fileUrl: 'https://img.example/main-1.jpg',
fileName: 'main-1.jpg',
fileSize: 33,
sortOrder: 1,
},
],
sku: [],
detail: [
{
fileUrl: 'https://img.example/detail-1.jpg',
fileName: 'detail-1.jpg',
fileSize: 11,
sortOrder: 10,
},
{
fileUrl: 'https://img.example/detail-2.jpg',
fileName: 'detail-2.jpg',
fileSize: 22,
sortOrder: 20,
},
],
},
})
expect(prisma.externalMaterialImportJob.update).toHaveBeenCalledWith({
where: { id: 'job-1' },
data: expect.objectContaining({
status: EXTERNAL_MATERIAL_IMPORT_STATUS.SUCCEEDED,
resultMaterialId: 'material-1',
}),
})
})
})