231 lines
7.4 KiB
TypeScript
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',
|
|
)
|
|
})
|
|
})
|