gongxiangp2/lib/__tests__/material-update.test.ts

1001 lines
29 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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('旧编辑调用未提交报告名称时应保留证书原值', async () => {
const existingCertificate = {
...baseMaterial,
materialType: '证书-检测报告',
categoryName: '通用',
sceneName: '通用',
productCategoryName: '板材',
productModel: '',
reportTitle: '原检测报告',
renderer: '',
}
;(prisma.material.findFirst as jest.Mock).mockResolvedValue(existingCertificate)
;(prisma.material.update as jest.Mock).mockImplementation(async ({ data }) => ({
...existingCertificate,
...data,
}))
const result = await updateMaterialWithSharedRules({
materialId: 'material-1',
operatorUserId: 'user-1',
operatorRole: 'USER',
input: {
productModel: '',
categoryName: '',
sceneName: '',
platformName: '',
productCategoryName: '板材',
materialType: '证书-检测报告',
renderer: '',
authorName: '测试作者',
},
})
expect('error' in result).toBe(false)
expect(prisma.material.update).toHaveBeenCalledWith({
where: { id: 'material-1' },
data: expect.objectContaining({
reportTitle: '原检测报告',
}),
})
})
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 expect(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: '',
},
})).resolves.toEqual({
error: '任务-详情必须选择出图方式(实拍、渲染或 AI)',
status: 400,
})
expect(prisma.material.update).toHaveBeenCalledTimes(1)
})
test('旧编辑调用未提交出图方式时应保留任务详情原值', async () => {
const existingTaskDetail = {
...baseMaterial,
materialType: '任务-详情',
taskDetailSourceType: '实拍',
categoryName: '店铺A',
sceneName: '场景A',
}
;(prisma.material.findFirst as jest.Mock).mockResolvedValue(existingTaskDetail)
;(prisma.material.update as jest.Mock).mockImplementation(async ({ data }) => ({
...existingTaskDetail,
...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: '任务作者',
},
})
expect(prisma.material.update).toHaveBeenCalledWith({
where: { id: 'material-1' },
data: expect.objectContaining({
taskDetailSourceType: '实拍',
}),
})
})
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')
})
})