gongxiangp2/lib/__tests__/white-background-upload-route.test.ts
2026-05-07 16:37:22 +08:00

231 lines
7.4 KiB
TypeScript

/** @jest-environment node */
import { POST } from '@/app/api/materials/[id]/white-background/route'
import { getServerSession } from 'next-auth'
import { prisma } from '@/lib/prisma'
import { deleteFileIfUnreferenced } from '@/lib/file-cleanup'
import { buildWhiteBackgroundWebpVariants } from '@/lib/white-background-preview'
import { uploadPublicDisplayToS3 } from '@/lib/public-display'
import { deleteFromS3 } from '@/lib/s3'
jest.mock('next-auth', () => ({
getServerSession: jest.fn(),
}))
jest.mock('@/lib/auth', () => ({
authOptions: {},
}))
jest.mock('fs', () => ({
promises: {
mkdir: jest.fn().mockResolvedValue(undefined),
writeFile: jest.fn().mockResolvedValue(undefined),
unlink: jest.fn().mockResolvedValue(undefined),
},
}))
jest.mock('@/lib/prisma', () => ({
prisma: {
material: {
findUnique: jest.fn(),
},
user: {
findUnique: jest.fn(),
},
materialSku: {
findFirst: jest.fn(),
},
materialAsset: {
findMany: jest.fn(),
},
$transaction: jest.fn(),
},
}))
jest.mock('@/lib/image-processor', () => ({
sanitizeFileName: jest.fn((value: string) => value),
}))
jest.mock('@/lib/file-cleanup', () => ({
deleteFileIfUnreferenced: jest.fn().mockResolvedValue({ success: true }),
}))
jest.mock('@/lib/material-log', () => ({
createMaterialLog: jest.fn().mockResolvedValue(undefined),
}))
jest.mock('@/lib/white-background-preview', () => ({
buildWhiteBackgroundWebpVariants: jest.fn(),
}))
jest.mock('@/lib/public-display', () => ({
...jest.requireActual('@/lib/public-display'),
uploadPublicDisplayToS3: jest.fn(),
}))
jest.mock('@/lib/s3', () => ({
deleteFromS3: jest.fn().mockResolvedValue(undefined),
}))
describe('white-background upload route', () => {
const fsPromises = jest.requireMock('fs').promises as {
mkdir: jest.Mock
writeFile: jest.Mock
unlink: jest.Mock
}
let consoleErrorSpy: jest.SpyInstance
beforeEach(() => {
jest.clearAllMocks()
consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => undefined)
fsPromises.mkdir.mockResolvedValue(undefined)
fsPromises.writeFile.mockResolvedValue(undefined)
fsPromises.unlink.mockResolvedValue(undefined)
;(getServerSession as jest.Mock).mockResolvedValue({
user: {
id: 'user-1',
},
})
;(prisma.material.findUnique as jest.Mock).mockResolvedValue({
id: 'material-1',
materialType: '标准模型',
userId: 'user-1',
productCategoryName: '鞋柜',
categoryName: '旗舰店',
})
;(prisma.user.findUnique as jest.Mock).mockResolvedValue({ role: 'USER' })
;(prisma.materialSku.findFirst as jest.Mock).mockResolvedValue({
id: 'sku-1',
skuName: 'XG014799',
})
;(prisma.materialAsset.findMany as jest.Mock).mockResolvedValue([])
;(prisma.$transaction as jest.Mock).mockImplementation(async (callback: any) =>
callback({
materialAsset: {
deleteMany: jest.fn().mockResolvedValue({ count: 0 }),
createMany: jest.fn().mockResolvedValue({ count: 1 }),
},
materialSku: {
update: jest.fn().mockResolvedValue({
id: 'sku-1',
whiteBackgroundSourceType: '渲染',
whiteBackgroundUploadedBy: 'user-1',
whiteBackgroundUploadedAt: new Date('2026-04-30T08:00:00.000Z'),
}),
},
}),
)
;(buildWhiteBackgroundWebpVariants as jest.Mock).mockResolvedValue({
original: Buffer.from('original-webp'),
preview: Buffer.from('preview-webp'),
})
;(uploadPublicDisplayToS3 as jest.Mock).mockResolvedValue(
'https://s3.jiansun.vip/gongxiangpan/public-display/white-background/material-1/sku-1/XG014799-鞋柜-右前45°.webp',
)
;(deleteFileIfUnreferenced as jest.Mock).mockResolvedValue({ success: true })
})
afterEach(() => {
consoleErrorSpy.mockRestore()
})
function createRequest(formData: FormData) {
return {
formData: jest.fn().mockResolvedValue(formData),
} as any
}
test('uploads fixed white background image as local original webp and public-display preview', async () => {
const formData = new FormData()
formData.append('skuId', 'sku-1')
formData.append('skuName', 'XG014799')
formData.append('sourceType', '渲染')
formData.append(
'whiteBgRightFront45',
new File([Buffer.from('png-source')], 'right-front-45.png', { type: 'image/png' }),
)
const response = await POST(createRequest(formData), {
params: { id: 'material-1' },
})
const body = await response.json()
expect(response.status).toBe(200)
expect(body.success).toBe(true)
expect(buildWhiteBackgroundWebpVariants).toHaveBeenCalledTimes(1)
expect(uploadPublicDisplayToS3).toHaveBeenCalledWith(
Buffer.from('preview-webp'),
'white-background/material-1/sku-1/XG014799-鞋柜-右前45°.webp',
'image/webp',
)
expect(fsPromises.writeFile).toHaveBeenCalledTimes(1)
expect(fsPromises.writeFile).toHaveBeenCalledWith(
expect.stringContaining('uploads\\skus\\material-1\\white-background\\sku-1\\XG014799-鞋柜-右前45°.webp'),
Buffer.from('original-webp'),
)
expect(prisma.$transaction).toHaveBeenCalled()
})
test('sourceType 缺失时应返回 400', async () => {
const formData = new FormData()
formData.append('skuId', 'sku-1')
formData.append('skuName', 'XG014799')
formData.append(
'whiteBgRightFront45',
new File([Buffer.from('png-source')], 'right-front-45.png', { type: 'image/png' }),
)
const response = await POST(createRequest(formData), {
params: { id: 'material-1' },
})
const body = await response.json()
expect(response.status).toBe(400)
expect(body).toEqual({ error: '白底图出图方式必填,且只能是 实拍、渲染、AI' })
})
test('sourceType 非法时应返回 400', async () => {
const formData = new FormData()
formData.append('skuId', 'sku-1')
formData.append('skuName', 'XG014799')
formData.append('sourceType', '手绘')
formData.append(
'whiteBgRightFront45',
new File([Buffer.from('png-source')], 'right-front-45.png', { type: 'image/png' }),
)
const response = await POST(createRequest(formData), {
params: { id: 'material-1' },
})
const body = await response.json()
expect(response.status).toBe(400)
expect(body).toEqual({ error: '白底图出图方式必填,且只能是 实拍、渲染、AI' })
})
test('cleans up written files and uploaded public-display preview when persistence fails', async () => {
const formData = new FormData()
formData.append('skuId', 'sku-1')
formData.append('skuName', 'XG014799')
formData.append('sourceType', 'AI')
formData.append(
'whiteBgRightFront45',
new File([Buffer.from('png-source')], 'right-front-45.png', { type: 'image/png' }),
)
;(prisma.$transaction as jest.Mock).mockRejectedValueOnce(new Error('db failed'))
const response = await POST(createRequest(formData), {
params: { id: 'material-1' },
})
expect(response.status).toBe(500)
expect(fsPromises.unlink).toHaveBeenCalledTimes(1)
expect(fsPromises.unlink).toHaveBeenCalledWith(
expect.stringContaining('uploads\\skus\\material-1\\white-background\\sku-1\\XG014799-鞋柜-右前45°.webp'),
)
expect(deleteFromS3).toHaveBeenCalledWith(
'https://s3.jiansun.vip/gongxiangpan/public-display/white-background/material-1/sku-1/XG014799-鞋柜-右前45°.webp',
)
})
})