923 lines
27 KiB
TypeScript
923 lines
27 KiB
TypeScript
import { prisma } from '@/lib/prisma'
|
|
import { createMaterialLog } from '@/lib/material-log'
|
|
import { compressImage, sanitizeFileName } from '@/lib/image-processor'
|
|
import { deleteFromS3, uploadToS3 } from '@/lib/s3'
|
|
import { uploadPublicDisplayToS3 } from '@/lib/public-display'
|
|
import { isSuperAdmin, isSystemAdmin } from '@/lib/department-permission'
|
|
import { queueTaskDetailFullSetNotification } from '@/lib/external-auto-listing'
|
|
import { updateMaterialWithSharedRules } from '@/lib/material-update'
|
|
|
|
jest.mock('@/lib/prisma', () => ({
|
|
prisma: {
|
|
material: {
|
|
findFirst: jest.fn(),
|
|
findMany: jest.fn(),
|
|
update: jest.fn(),
|
|
},
|
|
user: {
|
|
findUnique: jest.fn(),
|
|
findFirst: jest.fn(),
|
|
},
|
|
},
|
|
}))
|
|
|
|
jest.mock('@/lib/material-log', () => ({
|
|
createMaterialLog: jest.fn(),
|
|
}))
|
|
|
|
jest.mock('@/lib/s3', () => ({
|
|
deleteFromS3: jest.fn(),
|
|
uploadToS3: jest.fn(),
|
|
}))
|
|
|
|
jest.mock('@/lib/image-processor', () => ({
|
|
compressImage: jest.fn(),
|
|
sanitizeFileName: jest.fn(),
|
|
}))
|
|
|
|
jest.mock('@/lib/public-display', () => ({
|
|
buildMaterialPublicDisplayPath: jest.fn((fileName: string) => `materials/${fileName}`),
|
|
uploadPublicDisplayToS3: jest.fn(),
|
|
}))
|
|
|
|
jest.mock('@/lib/department-permission', () => ({
|
|
isSuperAdmin: jest.fn(),
|
|
isSystemAdmin: jest.fn(),
|
|
}))
|
|
|
|
jest.mock('@/lib/external-auto-listing', () => ({
|
|
queueTaskDetailFullSetNotification: jest.fn().mockResolvedValue(undefined),
|
|
}))
|
|
|
|
describe('material-update shared rules', () => {
|
|
const baseMaterial = {
|
|
id: 'material-1',
|
|
userId: 'user-1',
|
|
previewUrl: 'https://old-preview.example/cover.webp',
|
|
productModel: 'OLD-SPU',
|
|
categoryName: '旧店铺',
|
|
sceneName: '旧场景',
|
|
platformName: '旧平台',
|
|
productCategoryName: '旧品类',
|
|
materialType: '3D场景',
|
|
renderer: 'CR',
|
|
authorName: '旧作者',
|
|
taskNo: 'TASK202601141713525624',
|
|
dingtalkApprovalNo: null,
|
|
taskDetailRequesterUserId: null,
|
|
taskDetailRequesterName: null,
|
|
isFullSet: true,
|
|
assets: [],
|
|
deletedAt: null,
|
|
}
|
|
|
|
beforeEach(() => {
|
|
jest.clearAllMocks()
|
|
;(isSuperAdmin as jest.Mock).mockResolvedValue(false)
|
|
;(isSystemAdmin as jest.Mock).mockReturnValue(false)
|
|
;(sanitizeFileName as jest.Mock).mockImplementation((name: string) => name)
|
|
;(compressImage as jest.Mock).mockResolvedValue(Buffer.from('compressed'))
|
|
;(uploadPublicDisplayToS3 as jest.Mock).mockResolvedValue('https://new-preview.example/cover.webp')
|
|
;(uploadToS3 as jest.Mock).mockResolvedValue('https://new-preview.example/cover.webp')
|
|
;(deleteFromS3 as jest.Mock).mockResolvedValue(undefined)
|
|
})
|
|
|
|
test('切换到标准模型时应自动回写通用店铺和场景', async () => {
|
|
;(prisma.material.findFirst as jest.Mock).mockResolvedValue(baseMaterial)
|
|
;(prisma.material.update as jest.Mock).mockImplementation(async ({ data }) => ({
|
|
...baseMaterial,
|
|
...data,
|
|
}))
|
|
|
|
await updateMaterialWithSharedRules({
|
|
materialId: 'material-1',
|
|
operatorUserId: 'user-1',
|
|
operatorRole: 'USER',
|
|
input: {
|
|
productModel: 'SPU-001',
|
|
categoryName: '',
|
|
sceneName: '',
|
|
platformName: 'Amazon',
|
|
productCategoryName: '椅子',
|
|
materialType: '标准模型',
|
|
renderer: '',
|
|
authorName: '新作者',
|
|
userId: 'user-1',
|
|
},
|
|
})
|
|
|
|
expect(prisma.material.update).toHaveBeenCalledWith({
|
|
where: { id: 'material-1' },
|
|
data: expect.objectContaining({
|
|
productModel: 'SPU-001',
|
|
categoryName: '通用',
|
|
sceneName: '通用',
|
|
platformName: 'Amazon',
|
|
productCategoryName: '椅子',
|
|
materialType: '标准模型',
|
|
}),
|
|
})
|
|
expect(createMaterialLog).toHaveBeenCalled()
|
|
})
|
|
|
|
test('音乐类型编辑时不应强制要求店铺和场景,并应自动回写通用', async () => {
|
|
;(prisma.material.findFirst as jest.Mock).mockResolvedValue({
|
|
...baseMaterial,
|
|
materialType: '音乐',
|
|
categoryName: '',
|
|
sceneName: '',
|
|
productCategoryName: '',
|
|
renderer: '',
|
|
})
|
|
;(prisma.material.update as jest.Mock).mockImplementation(async ({ data }) => ({
|
|
...baseMaterial,
|
|
...data,
|
|
}))
|
|
|
|
const result = await updateMaterialWithSharedRules({
|
|
materialId: 'material-1',
|
|
operatorUserId: 'user-1',
|
|
operatorRole: 'USER',
|
|
input: {
|
|
productModel: 'MUSIC-001',
|
|
categoryName: '',
|
|
sceneName: '',
|
|
platformName: 'Spotify',
|
|
productCategoryName: '',
|
|
materialType: '音乐',
|
|
renderer: '',
|
|
authorName: '音乐作者',
|
|
userId: 'user-1',
|
|
},
|
|
})
|
|
|
|
expect('error' in result).toBe(false)
|
|
expect(prisma.material.update).toHaveBeenCalledWith({
|
|
where: { id: 'material-1' },
|
|
data: expect.objectContaining({
|
|
categoryName: '通用',
|
|
sceneName: '通用',
|
|
materialType: '音乐',
|
|
platformName: 'Spotify',
|
|
}),
|
|
})
|
|
})
|
|
|
|
test('标准模型缺少品类时应返回 400', async () => {
|
|
;(prisma.material.findFirst as jest.Mock).mockResolvedValue(baseMaterial)
|
|
|
|
const result = await updateMaterialWithSharedRules({
|
|
materialId: 'material-1',
|
|
operatorUserId: 'user-1',
|
|
operatorRole: 'USER',
|
|
input: {
|
|
productModel: 'SPU-001',
|
|
categoryName: '',
|
|
sceneName: '',
|
|
platformName: '',
|
|
productCategoryName: '',
|
|
materialType: '标准模型',
|
|
renderer: '',
|
|
authorName: '测试作者',
|
|
userId: 'user-1',
|
|
},
|
|
})
|
|
|
|
expect(result).toEqual({
|
|
error: '标准模型类型必须选择品类',
|
|
status: 400,
|
|
})
|
|
expect(prisma.material.update).not.toHaveBeenCalled()
|
|
})
|
|
|
|
test('证书-检测报告允许只填写报告名称 / 检测对象', async () => {
|
|
;(prisma.material.findFirst as jest.Mock).mockResolvedValue({
|
|
...baseMaterial,
|
|
materialType: '证书-检测报告',
|
|
categoryName: '通用',
|
|
sceneName: '通用',
|
|
productCategoryName: '板材',
|
|
productModel: '',
|
|
reportTitle: null,
|
|
renderer: '',
|
|
})
|
|
;(prisma.material.update as jest.Mock).mockImplementation(async ({ data }) => ({
|
|
...baseMaterial,
|
|
...data,
|
|
}))
|
|
|
|
const result = await updateMaterialWithSharedRules({
|
|
materialId: 'material-1',
|
|
operatorUserId: 'user-1',
|
|
operatorRole: 'USER',
|
|
input: {
|
|
productModel: '',
|
|
reportTitle: 'E0级板材检测报告',
|
|
categoryName: '',
|
|
sceneName: '',
|
|
platformName: '',
|
|
productCategoryName: '板材',
|
|
materialType: '证书-检测报告',
|
|
renderer: '',
|
|
authorName: '测试作者',
|
|
userId: 'user-1',
|
|
},
|
|
})
|
|
|
|
expect('error' in result).toBe(false)
|
|
expect(prisma.material.update).toHaveBeenCalledWith({
|
|
where: { id: 'material-1' },
|
|
data: expect.objectContaining({
|
|
productModel: '',
|
|
reportTitle: 'E0级板材检测报告',
|
|
categoryName: '通用',
|
|
sceneName: '通用',
|
|
materialType: '证书-检测报告',
|
|
}),
|
|
})
|
|
})
|
|
|
|
test('证书-检测报告同时缺少 SPU 和报告名称时应返回 400', async () => {
|
|
;(prisma.material.findFirst as jest.Mock).mockResolvedValue({
|
|
...baseMaterial,
|
|
materialType: '证书-检测报告',
|
|
categoryName: '通用',
|
|
sceneName: '通用',
|
|
productCategoryName: '板材',
|
|
productModel: '',
|
|
reportTitle: null,
|
|
renderer: '',
|
|
})
|
|
|
|
const result = await updateMaterialWithSharedRules({
|
|
materialId: 'material-1',
|
|
operatorUserId: 'user-1',
|
|
operatorRole: 'USER',
|
|
input: {
|
|
productModel: '',
|
|
reportTitle: '',
|
|
categoryName: '',
|
|
sceneName: '',
|
|
platformName: '',
|
|
productCategoryName: '板材',
|
|
materialType: '证书-检测报告',
|
|
renderer: '',
|
|
authorName: '测试作者',
|
|
userId: 'user-1',
|
|
},
|
|
})
|
|
|
|
expect(result).toEqual({
|
|
error: 'SPU 和 报告名称/检测对象 至少填写一个',
|
|
status: 400,
|
|
})
|
|
expect(prisma.material.update).not.toHaveBeenCalled()
|
|
})
|
|
|
|
test('非任务详情类型应强制将 isFullSet 保存为 false', async () => {
|
|
;(prisma.material.findFirst as jest.Mock).mockResolvedValue(baseMaterial)
|
|
;(prisma.material.update as jest.Mock).mockImplementation(async ({ data }) => ({
|
|
...baseMaterial,
|
|
...data,
|
|
}))
|
|
|
|
await updateMaterialWithSharedRules({
|
|
materialId: 'material-1',
|
|
operatorUserId: 'user-1',
|
|
operatorRole: 'USER',
|
|
input: {
|
|
productModel: 'MUSIC-001',
|
|
categoryName: '',
|
|
sceneName: '',
|
|
platformName: '',
|
|
productCategoryName: '',
|
|
materialType: '音乐',
|
|
renderer: '',
|
|
authorName: '音乐作者',
|
|
userId: 'user-1',
|
|
isFullSet: true,
|
|
},
|
|
})
|
|
|
|
expect(prisma.material.update).toHaveBeenCalledWith({
|
|
where: { id: 'material-1' },
|
|
data: expect.objectContaining({
|
|
categoryName: '通用',
|
|
sceneName: '通用',
|
|
isFullSet: false,
|
|
}),
|
|
})
|
|
})
|
|
|
|
test('任务-页面编辑时应保留任务号能力并要求店铺和场景', async () => {
|
|
;(prisma.material.findFirst as jest.Mock).mockResolvedValue({
|
|
...baseMaterial,
|
|
materialType: '任务-页面',
|
|
categoryName: '旧店铺',
|
|
sceneName: '旧场景',
|
|
taskNo: 'TASK202601141713525624-01',
|
|
})
|
|
;(prisma.material.findFirst as jest.Mock)
|
|
.mockResolvedValueOnce({
|
|
...baseMaterial,
|
|
materialType: '任务-页面',
|
|
categoryName: '旧店铺',
|
|
sceneName: '旧场景',
|
|
taskNo: 'TASK202601141713525624-01',
|
|
})
|
|
.mockResolvedValueOnce(null)
|
|
;(prisma.material.update as jest.Mock).mockImplementation(async ({ data }) => ({
|
|
...baseMaterial,
|
|
...data,
|
|
}))
|
|
|
|
const result = await updateMaterialWithSharedRules({
|
|
materialId: 'material-1',
|
|
operatorUserId: 'user-1',
|
|
operatorRole: 'USER',
|
|
input: {
|
|
productModel: 'TASK-SPU-001',
|
|
categoryName: '新店铺',
|
|
sceneName: '新场景',
|
|
platformName: '',
|
|
productCategoryName: '',
|
|
materialType: '任务-页面',
|
|
renderer: '',
|
|
authorName: '任务作者',
|
|
userId: 'user-1',
|
|
taskNo: 'task202601141713525624-02',
|
|
},
|
|
})
|
|
|
|
expect('error' in result).toBe(false)
|
|
expect(prisma.material.update).toHaveBeenCalledWith({
|
|
where: { id: 'material-1' },
|
|
data: expect.objectContaining({
|
|
categoryName: '新店铺',
|
|
sceneName: '新场景',
|
|
taskNo: 'TASK202601141713525624-02',
|
|
materialType: '任务-页面',
|
|
}),
|
|
})
|
|
})
|
|
|
|
test('任务类素材任务号重复时应返回 409', async () => {
|
|
;(prisma.material.findFirst as jest.Mock)
|
|
.mockResolvedValueOnce({
|
|
...baseMaterial,
|
|
materialType: '任务-页面',
|
|
categoryName: '店铺A',
|
|
sceneName: '场景A',
|
|
taskNo: null,
|
|
})
|
|
.mockResolvedValueOnce({ id: 'material-2' })
|
|
|
|
const result = await updateMaterialWithSharedRules({
|
|
materialId: 'material-1',
|
|
operatorUserId: 'user-1',
|
|
operatorRole: 'USER',
|
|
input: {
|
|
productModel: 'TASK-SPU-001',
|
|
categoryName: '店铺A',
|
|
sceneName: '场景A',
|
|
platformName: '',
|
|
productCategoryName: '',
|
|
materialType: '任务-页面',
|
|
renderer: '',
|
|
authorName: '任务作者',
|
|
userId: 'user-1',
|
|
taskNo: 'TASK202601141713525624-01',
|
|
},
|
|
})
|
|
|
|
expect(result).toEqual({
|
|
error: '任务号已存在',
|
|
status: 409,
|
|
})
|
|
expect(prisma.material.update).not.toHaveBeenCalled()
|
|
})
|
|
|
|
test('任务-详情编辑时应支持保存和清空出图方式', async () => {
|
|
;(prisma.material.findFirst as jest.Mock).mockResolvedValue({
|
|
...baseMaterial,
|
|
materialType: '任务-详情',
|
|
taskDetailSourceType: '实拍',
|
|
categoryName: '店铺A',
|
|
sceneName: '场景A',
|
|
})
|
|
;(prisma.material.update as jest.Mock).mockImplementation(async ({ data }) => ({
|
|
...baseMaterial,
|
|
...data,
|
|
}))
|
|
|
|
await updateMaterialWithSharedRules({
|
|
materialId: 'material-1',
|
|
operatorUserId: 'user-1',
|
|
operatorRole: 'USER',
|
|
input: {
|
|
productModel: 'TASK-SPU-001',
|
|
categoryName: '店铺A',
|
|
sceneName: '场景A',
|
|
platformName: '',
|
|
productCategoryName: '',
|
|
materialType: '任务-详情',
|
|
renderer: '',
|
|
authorName: '任务作者',
|
|
userId: 'user-1',
|
|
taskDetailSourceType: 'AI',
|
|
},
|
|
})
|
|
|
|
expect(prisma.material.update).toHaveBeenCalledWith({
|
|
where: { id: 'material-1' },
|
|
data: expect.objectContaining({
|
|
taskDetailSourceType: 'AI',
|
|
}),
|
|
})
|
|
|
|
await updateMaterialWithSharedRules({
|
|
materialId: 'material-1',
|
|
operatorUserId: 'user-1',
|
|
operatorRole: 'USER',
|
|
input: {
|
|
productModel: 'TASK-SPU-001',
|
|
categoryName: '店铺A',
|
|
sceneName: '场景A',
|
|
platformName: '',
|
|
productCategoryName: '',
|
|
materialType: '任务-详情',
|
|
renderer: '',
|
|
authorName: '任务作者',
|
|
userId: 'user-1',
|
|
taskDetailSourceType: '',
|
|
},
|
|
})
|
|
|
|
expect(prisma.material.update).toHaveBeenLastCalledWith({
|
|
where: { id: 'material-1' },
|
|
data: expect.objectContaining({
|
|
taskDetailSourceType: null,
|
|
}),
|
|
})
|
|
})
|
|
|
|
test('历史任务详情没有需求运营时,编辑其他字段仍可继续留空', async () => {
|
|
;(prisma.material.findFirst as jest.Mock).mockResolvedValue({
|
|
...baseMaterial,
|
|
materialType: '任务-详情',
|
|
categoryName: '店铺A',
|
|
sceneName: '场景A',
|
|
taskDetailRequesterUserId: null,
|
|
taskDetailRequesterName: null,
|
|
})
|
|
;(prisma.material.update as jest.Mock).mockImplementation(async ({ data }) => ({
|
|
...baseMaterial,
|
|
...data,
|
|
}))
|
|
|
|
const result = await updateMaterialWithSharedRules({
|
|
materialId: 'material-1',
|
|
operatorUserId: 'user-1',
|
|
operatorRole: 'USER',
|
|
input: {
|
|
productModel: 'TASK-SPU-001',
|
|
categoryName: '店铺A',
|
|
sceneName: '场景A',
|
|
materialType: '任务-详情',
|
|
renderer: '',
|
|
authorName: '任务作者',
|
|
userId: 'user-1',
|
|
taskDetailRequesterUserId: '',
|
|
},
|
|
})
|
|
|
|
expect('error' in result).toBe(false)
|
|
expect(prisma.material.update).toHaveBeenCalledWith({
|
|
where: { id: 'material-1' },
|
|
data: expect.objectContaining({
|
|
taskDetailRequesterUserId: null,
|
|
taskDetailRequesterName: null,
|
|
}),
|
|
})
|
|
})
|
|
|
|
test('其他素材切换为任务详情时必须选择需求运营', async () => {
|
|
;(prisma.material.findFirst as jest.Mock).mockResolvedValue(baseMaterial)
|
|
|
|
const result = await updateMaterialWithSharedRules({
|
|
materialId: 'material-1',
|
|
operatorUserId: 'user-1',
|
|
operatorRole: 'USER',
|
|
input: {
|
|
productModel: 'TASK-SPU-001',
|
|
categoryName: '店铺A',
|
|
sceneName: '场景A',
|
|
materialType: '任务-详情',
|
|
renderer: '',
|
|
authorName: '任务作者',
|
|
userId: 'user-1',
|
|
taskDetailRequesterUserId: '',
|
|
},
|
|
})
|
|
|
|
expect(result).toEqual({
|
|
error: '请选择需求运营人员',
|
|
status: 400,
|
|
})
|
|
expect(prisma.material.update).not.toHaveBeenCalled()
|
|
})
|
|
|
|
test('已有需求运营的任务详情不能清空该人员', async () => {
|
|
;(prisma.material.findFirst as jest.Mock).mockResolvedValue({
|
|
...baseMaterial,
|
|
materialType: '任务-详情',
|
|
categoryName: '店铺A',
|
|
sceneName: '场景A',
|
|
taskDetailRequesterUserId: 'requester-old',
|
|
taskDetailRequesterName: '运营旧',
|
|
})
|
|
|
|
const result = await updateMaterialWithSharedRules({
|
|
materialId: 'material-1',
|
|
operatorUserId: 'user-1',
|
|
operatorRole: 'USER',
|
|
input: {
|
|
productModel: 'TASK-SPU-001',
|
|
categoryName: '店铺A',
|
|
sceneName: '场景A',
|
|
materialType: '任务-详情',
|
|
renderer: '',
|
|
authorName: '任务作者',
|
|
userId: 'user-1',
|
|
taskDetailRequesterUserId: '',
|
|
},
|
|
})
|
|
|
|
expect(result).toEqual({
|
|
error: '需求运营人员不能清空,只能更换',
|
|
status: 400,
|
|
})
|
|
expect(prisma.material.update).not.toHaveBeenCalled()
|
|
})
|
|
|
|
test('更换需求运营时应校验有效人员并由服务端保存姓名快照', async () => {
|
|
;(prisma.material.findFirst as jest.Mock).mockResolvedValue({
|
|
...baseMaterial,
|
|
materialType: '任务-详情',
|
|
categoryName: '店铺A',
|
|
sceneName: '场景A',
|
|
taskDetailRequesterUserId: 'requester-old',
|
|
taskDetailRequesterName: '运营旧',
|
|
})
|
|
;(prisma.user.findFirst as jest.Mock).mockResolvedValue({
|
|
id: 'requester-new',
|
|
username: 'new-operator',
|
|
realName: '运营新',
|
|
})
|
|
;(prisma.material.update as jest.Mock).mockImplementation(async ({ data }) => ({
|
|
...baseMaterial,
|
|
...data,
|
|
}))
|
|
|
|
const result = await updateMaterialWithSharedRules({
|
|
materialId: 'material-1',
|
|
operatorUserId: 'user-1',
|
|
operatorRole: 'USER',
|
|
input: {
|
|
productModel: 'TASK-SPU-001',
|
|
categoryName: '店铺A',
|
|
sceneName: '场景A',
|
|
materialType: '任务-详情',
|
|
renderer: '',
|
|
authorName: '任务作者',
|
|
userId: 'user-1',
|
|
taskDetailRequesterUserId: 'requester-new',
|
|
},
|
|
})
|
|
|
|
expect('error' in result).toBe(false)
|
|
expect(prisma.user.findFirst).toHaveBeenCalledWith({
|
|
where: {
|
|
id: 'requester-new',
|
|
isActive: true,
|
|
status: 'active',
|
|
},
|
|
select: {
|
|
id: true,
|
|
username: true,
|
|
realName: true,
|
|
},
|
|
})
|
|
expect(prisma.material.update).toHaveBeenCalledWith({
|
|
where: { id: 'material-1' },
|
|
data: expect.objectContaining({
|
|
taskDetailRequesterUserId: 'requester-new',
|
|
taskDetailRequesterName: '运营新',
|
|
}),
|
|
})
|
|
})
|
|
|
|
test('任务详情切换为其他素材类型时应清空需求运营字段', async () => {
|
|
;(prisma.material.findFirst as jest.Mock).mockResolvedValue({
|
|
...baseMaterial,
|
|
materialType: '任务-详情',
|
|
taskDetailRequesterUserId: 'requester-old',
|
|
taskDetailRequesterName: '运营旧',
|
|
})
|
|
;(prisma.material.update as jest.Mock).mockImplementation(async ({ data }) => ({
|
|
...baseMaterial,
|
|
...data,
|
|
}))
|
|
|
|
await updateMaterialWithSharedRules({
|
|
materialId: 'material-1',
|
|
operatorUserId: 'user-1',
|
|
operatorRole: 'USER',
|
|
input: {
|
|
productModel: 'MUSIC-001',
|
|
categoryName: '',
|
|
sceneName: '',
|
|
materialType: '音乐',
|
|
renderer: '',
|
|
authorName: '任务作者',
|
|
userId: 'user-1',
|
|
taskDetailRequesterUserId: 'requester-old',
|
|
},
|
|
})
|
|
|
|
expect(prisma.material.update).toHaveBeenCalledWith({
|
|
where: { id: 'material-1' },
|
|
data: expect.objectContaining({
|
|
taskDetailRequesterUserId: null,
|
|
taskDetailRequesterName: null,
|
|
}),
|
|
})
|
|
})
|
|
|
|
test('任务-其他编辑时不应强制要求店铺和场景,并应自动回写通用', async () => {
|
|
;(prisma.material.findFirst as jest.Mock)
|
|
.mockResolvedValueOnce({
|
|
...baseMaterial,
|
|
materialType: '任务-其他',
|
|
categoryName: '通用',
|
|
sceneName: '通用',
|
|
taskNo: 'TASK202603310001',
|
|
productModel: 'TASK202603310001',
|
|
})
|
|
.mockResolvedValueOnce(null)
|
|
;(prisma.material.update as jest.Mock).mockImplementation(async ({ data }) => ({
|
|
...baseMaterial,
|
|
...data,
|
|
}))
|
|
|
|
const result = await updateMaterialWithSharedRules({
|
|
materialId: 'material-1',
|
|
operatorUserId: 'user-1',
|
|
operatorRole: 'USER',
|
|
input: {
|
|
productModel: 'TASK202603310001',
|
|
categoryName: '',
|
|
sceneName: '',
|
|
platformName: '',
|
|
productCategoryName: '',
|
|
materialType: '任务-其他',
|
|
renderer: '',
|
|
authorName: '任务作者',
|
|
userId: 'user-1',
|
|
taskNo: 'task202603310001',
|
|
dingtalkApprovalNo: '202508211445000157693',
|
|
},
|
|
})
|
|
|
|
expect('error' in result).toBe(false)
|
|
expect(prisma.material.update).toHaveBeenCalledWith({
|
|
where: { id: 'material-1' },
|
|
data: expect.objectContaining({
|
|
productModel: 'TASK202603310001',
|
|
categoryName: '通用',
|
|
sceneName: '通用',
|
|
taskNo: 'TASK202603310001',
|
|
dingtalkApprovalNo: '202508211445000157693',
|
|
materialType: '任务-其他',
|
|
}),
|
|
})
|
|
})
|
|
|
|
test('任务素材编辑时应拒绝无效的钉钉审批编号', async () => {
|
|
;(prisma.material.findFirst as jest.Mock).mockResolvedValue({
|
|
...baseMaterial,
|
|
materialType: '任务-页面',
|
|
})
|
|
|
|
const result = await updateMaterialWithSharedRules({
|
|
materialId: 'material-1',
|
|
operatorUserId: 'user-1',
|
|
operatorRole: 'USER',
|
|
input: {
|
|
productModel: 'SPU-001',
|
|
categoryName: '店铺A',
|
|
sceneName: '场景A',
|
|
materialType: '任务-页面',
|
|
renderer: '',
|
|
authorName: '任务作者',
|
|
dingtalkApprovalNo: '20250230123',
|
|
},
|
|
})
|
|
|
|
expect(result).toEqual({
|
|
error: '钉钉审批编号前8位不是有效日期',
|
|
status: 400,
|
|
})
|
|
expect(prisma.material.update).not.toHaveBeenCalled()
|
|
})
|
|
|
|
test('非任务素材不能写入钉钉审批编号', async () => {
|
|
;(prisma.material.findFirst as jest.Mock).mockResolvedValue(baseMaterial)
|
|
|
|
const result = await updateMaterialWithSharedRules({
|
|
materialId: 'material-1',
|
|
operatorUserId: 'user-1',
|
|
operatorRole: 'USER',
|
|
input: {
|
|
productModel: 'SPU-001',
|
|
categoryName: '店铺A',
|
|
sceneName: '场景A',
|
|
materialType: '3D场景',
|
|
renderer: 'CR',
|
|
authorName: '作者',
|
|
dingtalkApprovalNo: '202508211445000157693',
|
|
},
|
|
})
|
|
|
|
expect(result).toEqual({
|
|
error: '钉钉审批编号仅适用于“任务-”素材',
|
|
status: 400,
|
|
})
|
|
expect(prisma.material.update).not.toHaveBeenCalled()
|
|
})
|
|
|
|
test('任务-详情编辑时应按已有资产自动回写 isFullSet', async () => {
|
|
;(prisma.material.findFirst as jest.Mock).mockResolvedValue({
|
|
...baseMaterial,
|
|
materialType: '任务-详情',
|
|
isFullSet: false,
|
|
categoryName: '店铺A',
|
|
sceneName: '场景A',
|
|
assets: [
|
|
{ assetType: 'main-preview' },
|
|
{ assetType: 'sku-preview' },
|
|
{ assetType: 'detail-preview' },
|
|
],
|
|
})
|
|
;(prisma.material.update as jest.Mock).mockImplementation(async ({ data }) => ({
|
|
...baseMaterial,
|
|
...data,
|
|
}))
|
|
|
|
await updateMaterialWithSharedRules({
|
|
materialId: 'material-1',
|
|
operatorUserId: 'user-1',
|
|
operatorRole: 'USER',
|
|
input: {
|
|
productModel: 'SPU-001',
|
|
categoryName: '店铺A',
|
|
sceneName: '场景A',
|
|
platformName: '',
|
|
productCategoryName: '',
|
|
materialType: '任务-详情',
|
|
renderer: '',
|
|
authorName: '任务作者',
|
|
userId: 'user-1',
|
|
} as any,
|
|
})
|
|
|
|
expect(prisma.material.update).toHaveBeenCalledWith({
|
|
where: { id: 'material-1' },
|
|
data: expect.objectContaining({
|
|
isFullSet: true,
|
|
taskNo: 'TASK202601141713525624',
|
|
}),
|
|
})
|
|
expect(queueTaskDetailFullSetNotification).toHaveBeenCalledWith('material-1')
|
|
})
|
|
|
|
test('已是全套的任务-详情再次编辑时不应重复触发自动上架通知', async () => {
|
|
;(prisma.material.findFirst as jest.Mock).mockResolvedValue({
|
|
...baseMaterial,
|
|
materialType: '任务-详情',
|
|
isFullSet: true,
|
|
categoryName: '店铺A',
|
|
sceneName: '场景A',
|
|
assets: [
|
|
{ assetType: 'main-preview' },
|
|
{ assetType: 'sku-preview' },
|
|
{ assetType: 'detail-preview' },
|
|
],
|
|
})
|
|
;(prisma.material.update as jest.Mock).mockImplementation(async ({ data }) => ({
|
|
...baseMaterial,
|
|
...data,
|
|
isFullSet: true,
|
|
}))
|
|
|
|
await updateMaterialWithSharedRules({
|
|
materialId: 'material-1',
|
|
operatorUserId: 'user-1',
|
|
operatorRole: 'USER',
|
|
input: {
|
|
productModel: 'SPU-001',
|
|
categoryName: '店铺A',
|
|
sceneName: '场景A',
|
|
platformName: '',
|
|
productCategoryName: '',
|
|
materialType: '任务-详情',
|
|
renderer: '',
|
|
authorName: '任务作者',
|
|
userId: 'user-1',
|
|
} as any,
|
|
})
|
|
|
|
expect(queueTaskDetailFullSetNotification).not.toHaveBeenCalled()
|
|
})
|
|
|
|
test('任务类素材不允许手工替换预览图', async () => {
|
|
;(prisma.material.findFirst as jest.Mock).mockResolvedValue({
|
|
...baseMaterial,
|
|
materialType: '任务-详情',
|
|
categoryName: '店铺A',
|
|
sceneName: '场景A',
|
|
isFullSet: true,
|
|
assets: [
|
|
{ assetType: 'main-preview' },
|
|
{ assetType: 'sku-preview' },
|
|
{ assetType: 'detail-preview' },
|
|
],
|
|
})
|
|
|
|
const result = await updateMaterialWithSharedRules({
|
|
materialId: 'material-1',
|
|
operatorUserId: 'user-1',
|
|
operatorRole: 'USER',
|
|
input: {
|
|
productModel: 'SPU-001',
|
|
categoryName: '店铺A',
|
|
sceneName: '场景A',
|
|
platformName: '',
|
|
productCategoryName: '',
|
|
materialType: '任务-详情',
|
|
renderer: '',
|
|
authorName: '任务作者',
|
|
userId: 'user-1',
|
|
isFullSet: false,
|
|
},
|
|
previewImage: new File(['preview'], 'preview.png', { type: 'image/png' }),
|
|
})
|
|
|
|
expect(result).toEqual({
|
|
error: '当前素材类型不支持手工替换预览图',
|
|
status: 400,
|
|
})
|
|
expect(prisma.material.update).not.toHaveBeenCalled()
|
|
})
|
|
|
|
test('可手工替换的预览图应写入 public-display materials 前缀', async () => {
|
|
;(prisma.material.findFirst as jest.Mock).mockResolvedValue(baseMaterial)
|
|
;(prisma.material.update as jest.Mock).mockImplementation(async ({ data }) => ({
|
|
...baseMaterial,
|
|
...data,
|
|
}))
|
|
|
|
const previewImage = {
|
|
name: 'cover.png',
|
|
size: 7,
|
|
arrayBuffer: jest.fn().mockResolvedValue(Buffer.from('preview')),
|
|
} as any
|
|
const result = await updateMaterialWithSharedRules({
|
|
materialId: 'material-1',
|
|
operatorUserId: 'user-1',
|
|
operatorRole: 'USER',
|
|
input: {
|
|
productModel: 'SPU-001',
|
|
categoryName: '新店铺',
|
|
sceneName: '新场景',
|
|
platformName: 'Amazon',
|
|
productCategoryName: '',
|
|
materialType: '3D场景',
|
|
renderer: 'CR',
|
|
authorName: '新作者',
|
|
userId: 'user-1',
|
|
},
|
|
previewImage,
|
|
})
|
|
|
|
expect('error' in result).toBe(false)
|
|
expect(uploadPublicDisplayToS3).toHaveBeenCalledWith(
|
|
Buffer.from('compressed'),
|
|
expect.stringMatching(/^materials\/preview-\d+-cover\.png$/),
|
|
'image/webp',
|
|
)
|
|
expect(uploadToS3).not.toHaveBeenCalled()
|
|
expect(deleteFromS3).toHaveBeenCalledWith('https://old-preview.example/cover.webp')
|
|
})
|
|
})
|