412 lines
11 KiB
TypeScript
412 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,
|
|
taskDetailRequesterUserId: 'user-1',
|
|
taskDetailRequesterName: '张三',
|
|
}),
|
|
})
|
|
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',
|
|
}),
|
|
})
|
|
})
|
|
})
|