gongxiangp2/lib/__tests__/standard-model-white-background-by-spu-route.test.ts

348 lines
11 KiB
TypeScript

/** @jest-environment node */
import { POST } from '@/app/api/external/standard-model-white-background/by-spu/route'
import { prisma } from '@/lib/prisma'
import { logApiCall, validateApiKey } from '@/lib/external-api'
import {
completeExternalApiIdempotencyRecord,
startExternalApiIdempotencyRecord,
} from '@/lib/external-standard-model-white-background-by-barcode'
import { uploadStandardModelWhiteBackground } from '@/lib/standard-model-white-background-service'
jest.mock('@/lib/external-api', () => ({
getApiKeyFromRequest: jest.fn((request: { headers: Headers }) => request.headers.get('X-API-Key')),
getClientIp: jest.fn(() => '127.0.0.1'),
logApiCall: jest.fn().mockResolvedValue(undefined),
validateApiKey: jest.fn(),
}))
jest.mock('@/lib/external-standard-model-white-background-by-barcode', () => {
const actual = jest.requireActual('@/lib/external-standard-model-white-background-by-barcode')
return {
...actual,
completeExternalApiIdempotencyRecord: jest.fn().mockResolvedValue(undefined),
failExternalApiIdempotencyRecord: jest.fn().mockResolvedValue(undefined),
startExternalApiIdempotencyRecord: jest.fn(),
}
})
jest.mock('@/lib/prisma', () => ({
prisma: {
material: {
create: jest.fn(),
findMany: jest.fn(),
},
materialSku: {
aggregate: jest.fn(),
create: jest.fn(),
findFirst: jest.fn(),
},
materialSkuBarcode: {
create: jest.fn(),
findFirst: jest.fn(),
},
user: {
findUnique: jest.fn(),
},
},
}))
jest.mock('@/lib/standard-model-white-background-service', () => ({
uploadStandardModelWhiteBackground: jest.fn(),
}))
describe('external standard-model-white-background by spu route', () => {
beforeEach(() => {
jest.clearAllMocks()
;(validateApiKey as jest.Mock).mockResolvedValue({
id: 'api-1',
name: 'product-design',
createdBy: 'user-1',
isActive: true,
})
;(startExternalApiIdempotencyRecord as jest.Mock).mockResolvedValue({
mode: 'created',
record: { id: 'idem-1' },
})
;(uploadStandardModelWhiteBackground as jest.Mock).mockResolvedValue({
whiteBackgroundUploaded: true,
uploadedFixedCount: 1,
fixedCount: 1,
otherCount: 0,
whiteBackgroundCount: 1,
isComplete: false,
sourceType: 'AI',
whiteBackgroundUploadedBy: 'user-1',
whiteBackgroundUploadedAt: '2026-06-15T08:00:00.000Z',
downloadFileName: 'SKU001_白底图.zip',
})
;(prisma.materialSku.findFirst as jest.Mock).mockResolvedValue(null)
;(prisma.materialSkuBarcode.findFirst as jest.Mock).mockResolvedValue(null)
;(prisma.materialSkuBarcode.create as jest.Mock).mockResolvedValue({
id: 'barcode-binding-1',
})
;(prisma.materialSku.aggregate as jest.Mock).mockResolvedValue({
_max: { sortOrder: -1 },
})
;(prisma.user.findUnique as jest.Mock).mockResolvedValue({
username: 'tester',
realName: '测试用户',
})
})
function createRequest(formData: FormData) {
return {
headers: new Headers({
'X-API-Key': 'test-key',
'user-agent': 'jest',
}),
formData: jest.fn().mockResolvedValue(formData),
} as any
}
function createBaseForm() {
const formData = new FormData()
formData.append('clientRequestId', 'req-001')
formData.append('barcode', '690000000001')
formData.append('spu', 'spu001')
formData.append('skuName', 'sku001')
formData.append('sourceType', 'AI')
formData.append(
'whiteBgRightFront45',
new File([Buffer.from('png-source')], 'right-front-45.png', { type: 'image/png' }),
)
return formData
}
test('已有标准模型 SPU 和 SKU 时应直接录入该 SKU 的白底图', async () => {
;(prisma.material.findMany as jest.Mock).mockResolvedValue([
{
id: 'material-1',
productModel: 'SPU001',
categoryName: '通用',
productCategoryName: '沙发',
skus: [{ id: 'sku-1', skuName: 'SKU001' }],
},
])
const response = await POST(createRequest(createBaseForm()))
const body = await response.json()
expect(response.status).toBe(200)
expect(body.success).toBe(true)
expect(body.data).toMatchObject({
materialId: 'material-1',
skuId: 'sku-1',
barcode: '690000000001',
barcodeBindingId: 'barcode-binding-1',
resolvedSpu: 'SPU001',
resolvedSkuName: 'SKU001',
createdMaterial: false,
createdSku: false,
createdBarcodeBinding: true,
})
expect(prisma.material.create).not.toHaveBeenCalled()
expect(prisma.materialSku.create).not.toHaveBeenCalled()
expect(prisma.materialSkuBarcode.create).toHaveBeenCalledWith(
expect.objectContaining({
data: {
skuId: 'sku-1',
barcode: '690000000001',
},
}),
)
expect(uploadStandardModelWhiteBackground).toHaveBeenCalledWith(
expect.objectContaining({
materialId: 'material-1',
skuId: 'sku-1',
skuName: 'SKU001',
sourceType: 'AI',
operatorUserId: 'user-1',
}),
)
expect(completeExternalApiIdempotencyRecord).toHaveBeenCalled()
expect(logApiCall).toHaveBeenCalledWith(expect.objectContaining({ statusCode: 200 }))
})
test('已有标准模型 SPU 但没有该 SKU 时应在该 SPU 下新建 SKU 后录入', async () => {
;(prisma.material.findMany as jest.Mock).mockResolvedValue([
{
id: 'material-1',
productModel: 'SPU001',
categoryName: '通用',
productCategoryName: '沙发',
skus: [],
},
])
;(prisma.materialSku.aggregate as jest.Mock).mockResolvedValue({
_max: { sortOrder: 0 },
})
;(prisma.materialSku.create as jest.Mock).mockResolvedValue({
id: 'sku-new',
skuName: 'SKU001',
})
const response = await POST(createRequest(createBaseForm()))
const body = await response.json()
expect(response.status).toBe(200)
expect(body.data).toMatchObject({
materialId: 'material-1',
skuId: 'sku-new',
createdMaterial: false,
createdSku: true,
})
expect(prisma.materialSku.create).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({
materialId: 'material-1',
skuName: 'SKU001',
sortOrder: 1,
}),
}),
)
})
test('标准模型内没有该 SPU 时应要求品类并新建 SPU 和 SKU 后录入', async () => {
const formData = createBaseForm()
formData.append('productCategoryName', '沙发')
;(prisma.material.findMany as jest.Mock).mockResolvedValue([])
;(prisma.material.create as jest.Mock).mockResolvedValue({
id: 'material-new',
productModel: 'SPU001',
categoryName: '通用',
productCategoryName: '沙发',
})
;(prisma.materialSku.create as jest.Mock).mockResolvedValue({
id: 'sku-new',
skuName: 'SKU001',
})
const response = await POST(createRequest(formData))
const body = await response.json()
expect(response.status).toBe(200)
expect(body.data).toMatchObject({
materialId: 'material-new',
skuId: 'sku-new',
createdMaterial: true,
createdSku: true,
})
expect(prisma.material.create).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({
productModel: 'SPU001',
materialType: '标准模型',
productCategoryName: '沙发',
}),
}),
)
})
test('缺少 SKU 时不能把 SPU 当 SKU 兜底', async () => {
const formData = new FormData()
formData.append('clientRequestId', 'req-002')
formData.append('barcode', '690000000002')
formData.append('spu', 'spu001')
formData.append('sourceType', 'AI')
formData.append(
'whiteBgRightFront45',
new File([Buffer.from('png-source')], 'right-front-45.png', { type: 'image/png' }),
)
const response = await POST(createRequest(formData))
const body = await response.json()
expect(response.status).toBe(400)
expect(body).toEqual({
success: false,
error: {
code: 'INVALID_SKU',
message: 'skuName 不能为空,白底图必须明确挂到 SPU 下的某个 SKU',
},
})
expect(uploadStandardModelWhiteBackground).not.toHaveBeenCalled()
expect(prisma.material.findMany).not.toHaveBeenCalled()
})
test('缺少产品条码时应拒绝录入,避免后续唯一值链路缺失', async () => {
const formData = new FormData()
formData.append('clientRequestId', 'req-003')
formData.append('spu', 'spu001')
formData.append('skuName', 'sku001')
formData.append('sourceType', 'AI')
formData.append(
'whiteBgRightFront45',
new File([Buffer.from('png-source')], 'right-front-45.png', { type: 'image/png' }),
)
const response = await POST(createRequest(formData))
const body = await response.json()
expect(response.status).toBe(400)
expect(body).toEqual({
success: false,
error: {
code: 'INVALID_BARCODE',
message: 'barcode/productBarcode 不能为空',
},
})
expect(uploadStandardModelWhiteBackground).not.toHaveBeenCalled()
expect(prisma.material.findMany).not.toHaveBeenCalled()
})
test('产品条码已绑定到其它 SKU 时应返回冲突', async () => {
;(prisma.material.findMany as jest.Mock).mockResolvedValue([
{
id: 'material-1',
productModel: 'SPU001',
categoryName: '通用',
productCategoryName: '沙发',
skus: [{ id: 'sku-1', skuName: 'SKU001' }],
},
])
;(prisma.materialSkuBarcode.findFirst as jest.Mock).mockResolvedValue({
id: 'barcode-binding-old',
skuId: 'sku-other',
sku: {
skuName: 'SKU999',
material: {
productModel: 'SPU999',
materialType: '标准模型',
},
},
})
const response = await POST(createRequest(createBaseForm()))
const body = await response.json()
expect(response.status).toBe(409)
expect(body).toEqual({
success: false,
error: {
code: 'BARCODE_OCCUPIED',
message: '产品条码 690000000001 已绑定到标准模型 SPU SPU999 的 SKU SKU999',
},
})
expect(uploadStandardModelWhiteBackground).not.toHaveBeenCalled()
})
test('新建标准模型 SPU 时缺少品类应返回明确错误', async () => {
;(prisma.material.findMany as jest.Mock).mockResolvedValue([])
const response = await POST(createRequest(createBaseForm()))
const body = await response.json()
expect(response.status).toBe(400)
expect(body).toEqual({
success: false,
error: {
code: 'PRODUCT_CATEGORY_REQUIRED',
message: '该 SPU 尚未在标准模型内存在,新建标准模型 SPU 时必须传 productCategoryName',
},
})
expect(prisma.material.create).not.toHaveBeenCalled()
expect(uploadStandardModelWhiteBackground).not.toHaveBeenCalled()
})
})