/** @jest-environment node */ import { NextRequest } from 'next/server' import { getServerSession } from 'next-auth' import { POST } from '@/app/api/admin/materials/batch-delete/route' import { enqueueMaterialPurge, MaterialPurgeError } from '@/lib/material-purge' import { readFile } from 'fs/promises' import path from 'path' jest.mock('next-auth', () => ({ getServerSession: jest.fn() })) jest.mock('@/lib/auth', () => ({ authOptions: {} })) jest.mock('@/lib/prisma', () => ({ prisma: {} })) jest.mock('@/lib/material-log', () => ({ createMaterialLog: jest.fn() })) jest.mock('@/lib/department-permission', () => ({ isSystemAdmin: (role: string) => ['ADMIN', 'SYSTEM_ADMIN'].includes(role) })) jest.mock('@/lib/material-purge', () => ({ enqueueMaterialPurge: jest.fn(), ensureMaterialPurgeWorkerStarted: jest.fn(), MaterialPurgeError: class extends Error { constructor(message: string, readonly status = 409) { super(message) } }, })) const id = '11111111-1111-4111-8111-111111111111' const request = (body: unknown) => new NextRequest('http://localhost/api/admin/materials/batch-delete', { method: 'POST', body: JSON.stringify(body), headers: { 'Content-Type': 'application/json' }, }) beforeEach(() => { jest.clearAllMocks() ;(getServerSession as jest.Mock).mockResolvedValue({ user: { id: 'admin', role: 'ADMIN' } }) }) test.each([null, { user: { id: 'user', role: 'USER' } }, { user: { id: 'department', role: 'DEPARTMENT_ADMIN' } }])('永久删除拒绝非系统管理员', async session => { ;(getServerSession as jest.Mock).mockResolvedValue(session) expect((await POST(request({ ids: [id], permanent: true }))).status).toBe(403) expect(enqueueMaterialPurge).not.toHaveBeenCalled() }) test('只在任务可靠提交后返回 202 和真实任务,不宣称文件已经清理', async () => { const jobs = [{ id: 'job', status: 'queued' }] ;(enqueueMaterialPurge as jest.Mock).mockResolvedValue(jobs) const result = await POST(request({ ids: [id], permanent: true })) expect(result.status).toBe(202) expect(await result.json()).toEqual({ success: true, jobs }) expect(enqueueMaterialPurge).toHaveBeenCalledWith([id], 'admin') }) test.each([{ ids: [id], permanent: 'true' }, { ids: ['invalid'], permanent: true }, { ids: Array(101).fill(id), permanent: true }])('拒绝错误删除方式和超限批量输入', async body => { expect((await POST(request(body))).status).toBe(400) expect(enqueueMaterialPurge).not.toHaveBeenCalled() }) test('批量条件冲突返回可恢复的上下文错误', async () => { ;(enqueueMaterialPurge as jest.Mock).mockRejectedValue(new MaterialPurgeError('只能彻底删除回收站中的素材')) const result = await POST(request({ ids: [id], permanent: true })) expect(result.status).toBe(409) expect(await result.json()).toEqual({ error: '只能彻底删除回收站中的素材' }) }) test.each(['app/api/materials/route.ts', 'app/api/admin/materials/batch-delete/route.ts'])('%s 的请求路径不直接做物理删除', async file => { const source = await readFile(path.join(process.cwd(), file), 'utf8') expect(source).toContain('enqueueMaterialPurge(') expect(source).not.toMatch(/deleteFiles\(|deleteMaterialRelatedRecords\(|prisma\.material\.delete\(/) })