/** @jest-environment node */ import { NextRequest } from 'next/server'; import { getServerSession } from 'next-auth'; import { POST as initTaskPage } from '@/app/api/materials/upload/task-page/init/route'; import { POST as completeTaskPage } from '@/app/api/materials/upload/task-page/complete/route'; import { prisma } from '@/lib/prisma'; import { completeTaskImageMaterial } from '@/lib/task-image-material-completion'; jest.mock('next-auth', () => ({ getServerSession: jest.fn() })); jest.mock('@/lib/auth', () => ({ authOptions: {} })); jest.mock('@/lib/prisma', () => ({ prisma: { user: { findUnique: jest.fn() }, material: { create: jest.fn() }, }, })); jest.mock('@/lib/task-image-material-completion', () => ({ completeTaskImageMaterial: jest.fn(), })); describe('任务-页面上传接口', () => { beforeEach(() => { jest.clearAllMocks(); jest.spyOn(console, 'error').mockImplementation(() => undefined); (getServerSession as jest.Mock).mockResolvedValue({ user: { id: 'user-1' } }); (prisma.user.findUnique as jest.Mock).mockResolvedValue({ username: '张三' }); (prisma.material.create as jest.Mock).mockResolvedValue({ id: 'material-1' }); (completeTaskImageMaterial as jest.Mock).mockResolvedValue({ materialId: 'material-1' }); }); afterEach(() => { jest.restoreAllMocks(); }); test('初始化接口应返回顶层 materialId,并标记为 uploading', async () => { const response = await initTaskPage( new NextRequest('http://localhost/api/materials/upload/task-page/init', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ productModel: 'SJ0371', categoryName: '索菲亚', sceneName: '书房', }), }), ); const body = await response.json(); expect(response.status).toBe(200); expect(body).toEqual({ success: true, materialId: 'material-1' }); expect(prisma.material.create).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ materialType: '任务-页面', taskStatus: 'uploading', isIncompleteShell: true, }), }), ); }); test('完成接口把固定素材类型、用户和原始资产交给共享安全状态机', async () => { const assets = [{ uploadId: 'upload-1', fileName: 'page.jpg', fileSize: 10 }]; const response = await completeTaskPage(makeCompleteRequest(assets)); expect(response.status).toBe(200); expect(completeTaskImageMaterial).toHaveBeenCalledWith({ materialId: 'material-1', userId: 'user-1', materialType: '任务-页面', fallbackProductModel: 'task-page', assets, taskNo: undefined, dingtalkApprovalNo: undefined, }); }); test('共享状态机返回 alreadyCompleted 时保持幂等成功', async () => { (completeTaskImageMaterial as jest.Mock).mockResolvedValue({ materialId: 'material-1', alreadyCompleted: true, }); const response = await completeTaskPage(makeCompleteRequest([])); const body = await response.json(); expect(response.status).toBe(200); expect(body).toMatchObject({ success: true, alreadyCompleted: true }); }); test('共享状态机的限额和并发错误状态应原样返回', async () => { (completeTaskImageMaterial as jest.Mock).mockRejectedValue( Object.assign(new Error('图片超过限制'), { status: 413 }), ); const response = await completeTaskPage(makeCompleteRequest([])); expect(response.status).toBe(413); }); }); function makeCompleteRequest(assets: Array<{ uploadId: string; fileName: string; fileSize: number }>) { return new NextRequest('http://localhost/api/materials/upload/task-page/complete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ materialId: 'material-1', assets: { main: assets } }), }); }