351 lines
9.8 KiB
TypeScript
351 lines
9.8 KiB
TypeScript
/** @jest-environment node */
|
|
|
|
import {
|
|
completeProductPackageUpload,
|
|
parsePublicProductPackageAssets,
|
|
} from '@/lib/product-package-completion'
|
|
import { prisma } from '@/lib/prisma'
|
|
import {
|
|
queueTaskDetailBarcodeImagesUpdatedNotification,
|
|
queueTaskDetailFullSetNotification,
|
|
} from '@/lib/external-auto-listing'
|
|
import { createMaterialLog } from '@/lib/material-log'
|
|
import { createZipFromFiles } from '@/lib/task-material-processor'
|
|
import { getOwnedUploadSession } from '@/lib/upload-sessions'
|
|
import { assignTaskNoOnComplete } from '@/lib/product-package'
|
|
|
|
jest.mock('@/lib/prisma', () => ({
|
|
prisma: {
|
|
material: {
|
|
findUnique: jest.fn(),
|
|
update: jest.fn(),
|
|
},
|
|
},
|
|
}))
|
|
|
|
jest.mock('@/lib/product-package', () => ({
|
|
assignTaskNoOnComplete: jest.fn().mockResolvedValue(null),
|
|
validateTaskNo: jest.fn(() => true),
|
|
}))
|
|
|
|
jest.mock('@/lib/task-material-processor', () => ({
|
|
cleanupOnError: jest.fn().mockResolvedValue(undefined),
|
|
cleanupTempFiles: jest.fn().mockResolvedValue(undefined),
|
|
createZipFromFiles: jest.fn()
|
|
.mockResolvedValueOnce({ filePath: 'zip-main', fileName: 'main.zip', fileSize: 11 })
|
|
.mockResolvedValueOnce({ filePath: 'zip-sku', fileName: 'sku.zip', fileSize: 22 })
|
|
.mockResolvedValueOnce({ filePath: 'zip-detail', fileName: 'detail.zip', fileSize: 33 }),
|
|
createZipFromZips: jest.fn().mockResolvedValue({ filePath: 'zip-all', fileName: 'all.zip', fileSize: 66 }),
|
|
saveProductPackageAssets: jest.fn().mockResolvedValue({
|
|
createdSkuPreviewAssets: [
|
|
{
|
|
id: 'sku-preview-1',
|
|
fileUrl: 'https://s3.example/sku-preview-1.webp',
|
|
fileName: 'sku-preview-1.webp',
|
|
sortOrder: 0,
|
|
skuCode: 'SKU-001',
|
|
createdAt: new Date('2026-04-22T09:00:00.000Z'),
|
|
updatedAt: new Date('2026-04-22T09:00:00.000Z'),
|
|
barcodes: ['690000000001'],
|
|
},
|
|
],
|
|
}),
|
|
}))
|
|
|
|
jest.mock('@/lib/upload-sessions', () => ({
|
|
getUploadedFile: jest.fn(),
|
|
getOwnedUploadSession: jest.fn().mockResolvedValue({ userId: 'user-1' }),
|
|
}))
|
|
|
|
jest.mock('@/lib/material-log', () => ({
|
|
createMaterialLog: jest.fn().mockResolvedValue(undefined),
|
|
}))
|
|
|
|
jest.mock('@/lib/task-detail-full-set', () => ({
|
|
isTaskDetailFullSetFromUploadGroups: jest.fn(() => true),
|
|
}))
|
|
|
|
jest.mock('@/lib/public-display', () => ({
|
|
buildTaskDetailPublicDisplayPath: jest.fn((_materialId: string, group: string, fileName: string) => `task-detail/${group}/${fileName}`),
|
|
uploadPublicDisplayToS3: jest.fn((_: Buffer, path: string) => Promise.resolve(`https://s3.example/${path}`)),
|
|
}))
|
|
|
|
jest.mock('@/lib/external-auto-listing', () => ({
|
|
queueTaskDetailFullSetNotification: jest.fn().mockResolvedValue([]),
|
|
queueTaskDetailBarcodeImagesUpdatedNotification: jest.fn().mockResolvedValue([]),
|
|
}))
|
|
|
|
jest.mock('@/lib/image-processor', () => ({
|
|
compressImage: jest.fn(async (buffer: Buffer) => buffer),
|
|
sliceImage: jest.fn(async (buffer: Buffer) => [buffer]),
|
|
}))
|
|
|
|
jest.mock('sharp', () => ({
|
|
__esModule: true,
|
|
default: jest.fn(() => ({
|
|
metadata: jest.fn().mockResolvedValue({ width: 800, height: 800 }),
|
|
})),
|
|
}))
|
|
|
|
describe('product-package-completion', () => {
|
|
beforeEach(() => {
|
|
jest.clearAllMocks()
|
|
;(prisma.material.findUnique as jest.Mock).mockResolvedValue({
|
|
id: 'material-1',
|
|
productModel: 'SPU-001',
|
|
categoryName: '旗舰店',
|
|
previewUrl: '',
|
|
user: {
|
|
realName: '张三',
|
|
username: 'zhangsan',
|
|
},
|
|
})
|
|
;(prisma.material.update as jest.Mock).mockResolvedValue(undefined)
|
|
global.fetch = jest.fn(async () =>
|
|
new Response(Buffer.from('image-binary'), {
|
|
status: 200,
|
|
headers: {
|
|
'Content-Type': 'image/png',
|
|
},
|
|
}),
|
|
) as any
|
|
})
|
|
|
|
test('全套任务详情完成后应触发自动上架通知排队', async () => {
|
|
const result = await completeProductPackageUpload({
|
|
materialId: 'material-1',
|
|
taskNo: null,
|
|
dingtalkApprovalNo: '202508211445000157693',
|
|
operatorUserId: 'user-1',
|
|
assets: {
|
|
main: [
|
|
{
|
|
fileUrl: 'https://img.example/main.png',
|
|
fileName: 'main.png',
|
|
fileSize: 10,
|
|
sortOrder: 0,
|
|
},
|
|
],
|
|
sku: [
|
|
{
|
|
fileUrl: 'https://img.example/sku.png',
|
|
fileName: 'sku.png',
|
|
fileSize: 10,
|
|
sortOrder: 0,
|
|
skuCode: 'SKU-001',
|
|
barcodes: ['690000000001'],
|
|
},
|
|
],
|
|
detail: [
|
|
{
|
|
fileUrl: 'https://img.example/detail.png',
|
|
fileName: 'detail.png',
|
|
fileSize: 10,
|
|
sortOrder: 0,
|
|
},
|
|
],
|
|
},
|
|
})
|
|
|
|
expect(result).toEqual({
|
|
materialId: 'material-1',
|
|
taskNo: null,
|
|
dingtalkApprovalNo: '202508211445000157693',
|
|
isFullSet: true,
|
|
})
|
|
expect(assignTaskNoOnComplete).toHaveBeenCalledWith(
|
|
'material-1',
|
|
null,
|
|
'202508211445000157693',
|
|
)
|
|
expect(queueTaskDetailFullSetNotification).toHaveBeenCalledWith('material-1')
|
|
expect(queueTaskDetailBarcodeImagesUpdatedNotification).toHaveBeenCalledWith('material-1', [
|
|
'sku-preview-1',
|
|
])
|
|
expect(createMaterialLog).toHaveBeenCalled()
|
|
})
|
|
|
|
test('SKU 图缺少 skuCode 时应直接报错', async () => {
|
|
await expect(
|
|
completeProductPackageUpload({
|
|
materialId: 'material-1',
|
|
taskNo: null,
|
|
operatorUserId: 'user-1',
|
|
assets: {
|
|
sku: [
|
|
{
|
|
fileUrl: 'https://img.example/sku.png',
|
|
fileName: 'sku.png',
|
|
fileSize: 10,
|
|
sortOrder: 0,
|
|
barcodes: ['690000000001'],
|
|
},
|
|
],
|
|
},
|
|
}),
|
|
).rejects.toThrow('缺少 SKU 编号')
|
|
})
|
|
|
|
test('SKU 图缺少产品条码时应直接报错', async () => {
|
|
await expect(
|
|
completeProductPackageUpload({
|
|
materialId: 'material-1',
|
|
taskNo: null,
|
|
operatorUserId: 'user-1',
|
|
assets: {
|
|
sku: [
|
|
{
|
|
fileUrl: 'https://img.example/sku.png',
|
|
fileName: 'sku.png',
|
|
fileSize: 10,
|
|
sortOrder: 0,
|
|
skuCode: 'SKU-001',
|
|
barcodes: [],
|
|
},
|
|
],
|
|
},
|
|
}),
|
|
).rejects.toThrow('至少需要 1 个产品条码')
|
|
})
|
|
|
|
test('历史导入允许 SKU 图没有产品条码', async () => {
|
|
;(createZipFromFiles as jest.Mock).mockResolvedValue({
|
|
filePath: 'zip-sku-only',
|
|
fileName: 'sku-only.zip',
|
|
fileSize: 22,
|
|
})
|
|
|
|
await expect(
|
|
completeProductPackageUpload({
|
|
materialId: 'material-1',
|
|
taskNo: null,
|
|
operatorUserId: 'user-1',
|
|
assets: {
|
|
sku: [
|
|
{
|
|
fileUrl: 'https://img.example/sku.png',
|
|
fileName: 'sku.png',
|
|
fileSize: 10,
|
|
sortOrder: 0,
|
|
skuCode: 'SKU-001',
|
|
barcodes: [],
|
|
},
|
|
],
|
|
},
|
|
options: {
|
|
allowSkuWithoutBarcodes: true,
|
|
},
|
|
}),
|
|
).resolves.toEqual({
|
|
materialId: 'material-1',
|
|
taskNo: null,
|
|
dingtalkApprovalNo: null,
|
|
isFullSet: true,
|
|
})
|
|
})
|
|
|
|
test('同一批次重复条码绑定两张 SKU 图时应拒绝', async () => {
|
|
await expect(
|
|
completeProductPackageUpload({
|
|
materialId: 'material-1',
|
|
taskNo: null,
|
|
operatorUserId: 'user-1',
|
|
assets: {
|
|
sku: [
|
|
{
|
|
fileUrl: 'https://img.example/sku-a.png',
|
|
fileName: 'sku-a.png',
|
|
fileSize: 10,
|
|
sortOrder: 0,
|
|
skuCode: 'SKU-001',
|
|
barcodes: ['690000000001'],
|
|
},
|
|
{
|
|
fileUrl: 'https://img.example/sku-b.png',
|
|
fileName: 'sku-b.png',
|
|
fileSize: 10,
|
|
sortOrder: 1,
|
|
skuCode: 'SKU-002',
|
|
barcodes: ['690000000001'],
|
|
},
|
|
],
|
|
},
|
|
}),
|
|
).rejects.toThrow('不能同时绑定')
|
|
})
|
|
|
|
test('公开上传资产不得携带 localFilePath', () => {
|
|
expect(() =>
|
|
parsePublicProductPackageAssets({
|
|
main: [
|
|
{
|
|
uploadId: 'upload-1',
|
|
localFilePath: 'C:\\Windows\\win.ini',
|
|
fileName: 'main.png',
|
|
fileSize: 10,
|
|
},
|
|
],
|
|
}),
|
|
).toThrow('只允许使用 uploadId')
|
|
})
|
|
|
|
test('公开上传资产不得使用 fileUrl 触发服务端请求', () => {
|
|
expect(() =>
|
|
parsePublicProductPackageAssets({
|
|
main: [
|
|
{
|
|
fileUrl: 'http://127.0.0.1:3000/api/admin/system-logs',
|
|
fileName: 'main.png',
|
|
fileSize: 10,
|
|
},
|
|
],
|
|
}),
|
|
).toThrow('只允许使用 uploadId')
|
|
})
|
|
|
|
test('公开上传资产只保留完成流程需要的字段', () => {
|
|
expect(
|
|
parsePublicProductPackageAssets({
|
|
sku: [
|
|
{
|
|
uploadId: ' upload-1 ',
|
|
fileName: ' sku.png ',
|
|
fileSize: 10,
|
|
sortOrder: 0,
|
|
skuCode: 'SKU-001',
|
|
barcodes: ['690000000001', 123],
|
|
unexpected: 'ignored',
|
|
},
|
|
],
|
|
}),
|
|
).toEqual({
|
|
main: undefined,
|
|
sku: [
|
|
{
|
|
uploadId: 'upload-1',
|
|
fileName: 'sku.png',
|
|
fileSize: 10,
|
|
sortOrder: 0,
|
|
skuCode: 'SKU-001',
|
|
barcodes: ['690000000001'],
|
|
},
|
|
],
|
|
detail: undefined,
|
|
psd: undefined,
|
|
})
|
|
})
|
|
|
|
test('不能消费其他用户的上传会话', async () => {
|
|
;(getOwnedUploadSession as jest.Mock).mockResolvedValueOnce(null)
|
|
|
|
await expect(completeProductPackageUpload({
|
|
materialId: 'material-1',
|
|
taskNo: null,
|
|
operatorUserId: 'user-1',
|
|
assets: {
|
|
main: [{ uploadId: 'upload-other', fileName: 'main.png', fileSize: 10 }],
|
|
},
|
|
})).rejects.toThrow('不属于当前用户')
|
|
})
|
|
})
|