/** @jest-environment node */ import { mkdir, mkdtemp, rm } from 'fs/promises' import os from 'os' import path from 'path' import { NextRequest } from 'next/server' import { getServerSession } from 'next-auth' import { GET, POST } from './route' import { hasSystemAdminPermission } from '@/lib/department-permission' import { safelyCleanupMaterialUploadSession } from '@/lib/temp-cleanup' jest.mock('next-auth', () => ({ getServerSession: jest.fn(), })) jest.mock('@/lib/auth', () => ({ authOptions: {} })) jest.mock('@/lib/department-permission', () => ({ hasSystemAdminPermission: jest.fn(), })) jest.mock('@/lib/temp-cleanup', () => ({ safelyCleanupMaterialUploadSession: jest.fn(), })) jest.mock('@/lib/upload-sessions', () => ({ isSafeUploadId: (value: string) => /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/.test(value), })) describe('material upload cleanup route helper coverage', () => { let uploadRoot: string beforeEach(async () => { jest.clearAllMocks() uploadRoot = await mkdtemp(path.join(os.tmpdir(), 'cleanup-route-')) process.env.UPLOAD_DIR = uploadRoot ;(getServerSession as jest.Mock).mockResolvedValue({ user: { id: 'user-1', role: 'SYSTEM_ADMIN', isSuperAdmin: true }, }) ;(hasSystemAdminPermission as jest.Mock).mockReturnValue(true) ;(safelyCleanupMaterialUploadSession as jest.Mock).mockResolvedValue({ status: 'deleted', success: true, deletedFiles: 2, freedSpace: 4, }) }) afterEach(async () => { delete process.env.UPLOAD_DIR await rm(uploadRoot, { recursive: true, force: true }) }) test('single-id GET delegates cancellation to the exclusive-lock helper', async () => { const response = await GET(new NextRequest( 'http://localhost/api/materials/upload/cleanup?uploadId=single-upload', )) expect(response.status).toBe(200) expect(safelyCleanupMaterialUploadSession).toHaveBeenCalledWith({ uploadId: 'single-upload', intent: 'cancel', }) }) test('batch GET delegates every safe candidate to sweep helper', async () => { await mkdir(path.join(uploadRoot, 'temp', 'batch-upload'), { recursive: true }) const response = await GET(new NextRequest( 'http://localhost/api/materials/upload/cleanup?maxAge=12&expiredOnly=false', )) expect(response.status).toBe(200) expect(safelyCleanupMaterialUploadSession).toHaveBeenCalledWith({ uploadId: 'batch-upload', intent: 'sweep', minAgeMs: 12 * 60 * 60 * 1000, allowYoungOrphaned: true, }) }) test('POST delegates owner recheck to the exclusive-lock helper', async () => { const request = new NextRequest('http://localhost/api/materials/upload/cleanup', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ uploadId: 'owned-upload' }), }) const response = await POST(request) expect(response.status).toBe(200) expect(safelyCleanupMaterialUploadSession).toHaveBeenCalledWith({ uploadId: 'owned-upload', intent: 'cancel', expectedOwnerId: 'user-1', }) }) test('helper uncertainty is surfaced as 503 without route-side deletion', async () => { ;(safelyCleanupMaterialUploadSession as jest.Mock).mockResolvedValue({ status: 'unavailable', success: false, deletedFiles: 0, freedSpace: 0, error: 'Redis 不可用,已保留数据', code: 'UPLOAD_CLEANUP_REDIS_UNAVAILABLE', httpStatus: 503, }) const response = await GET(new NextRequest( 'http://localhost/api/materials/upload/cleanup?uploadId=uncertain-upload', )) const body = await response.json() expect(response.status).toBe(503) expect(body.code).toBe('UPLOAD_CLEANUP_REDIS_UNAVAILABLE') }) })