Protect upload cleanup and atomic completion paths, add durable media worker leases and brand asset validation, and introduce verified migration/bootstrap/deployment gates.
117 lines
3.7 KiB
TypeScript
117 lines
3.7 KiB
TypeScript
/** @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')
|
|
})
|
|
})
|