/** @jest-environment node */ import { access, mkdtemp, mkdir, rm, writeFile, } from 'fs/promises' import os from 'os' import path from 'path' import { deleteFileIfUnreferenced, deleteLocalFile, } from '@/lib/file-cleanup' jest.mock('@/lib/prisma', () => ({ prisma: { materialAsset: { count: jest.fn() }, materialSku: { count: jest.fn() }, material: { count: jest.fn() }, }, })) jest.mock('@/lib/s3', () => ({ deleteFromS3: jest.fn(), isS3Url: jest.fn(() => false), })) describe('file cleanup security boundaries', () => { let sandboxRoot: string let uploadRoot: string let previousUploadDir: string | undefined beforeEach(async () => { previousUploadDir = process.env.UPLOAD_DIR sandboxRoot = await mkdtemp(path.join(os.tmpdir(), 'gongxiangp2-cleanup-')) uploadRoot = path.join(sandboxRoot, 'uploads') await mkdir(uploadRoot, { recursive: true }) process.env.UPLOAD_DIR = uploadRoot }) afterEach(async () => { if (previousUploadDir === undefined) { delete process.env.UPLOAD_DIR } else { process.env.UPLOAD_DIR = previousUploadDir } await rm(sandboxRoot, { recursive: true, force: true }) }) test('拒绝删除受控上传目录之外的文件', async () => { const sentinelPath = path.join(sandboxRoot, 'sentinel.txt') await writeFile(sentinelPath, 'keep') const result = await deleteLocalFile(sentinelPath) expect(result).toEqual({ success: false, error: '拒绝删除受控上传目录之外的路径', }) await expect(access(sentinelPath)).resolves.toBeUndefined() }) test('允许删除受控上传目录内的普通文件', async () => { const filePath = path.join(uploadRoot, 'material-1', 'preview.webp') await mkdir(path.dirname(filePath), { recursive: true }) await writeFile(filePath, 'preview') const result = await deleteLocalFile(filePath) expect(result).toEqual({ success: true, isDirectory: false }) await expect(access(filePath)).rejects.toMatchObject({ code: 'ENOENT' }) }) test('目录默认不能递归删除,受信任调用方必须明确允许', async () => { const directoryPath = path.join(uploadRoot, 'material-1') const sentinelPath = path.join(directoryPath, 'sentinel.txt') await mkdir(directoryPath, { recursive: true }) await writeFile(sentinelPath, 'keep') const deniedResult = await deleteLocalFile(directoryPath) expect(deniedResult).toEqual({ success: false, isDirectory: true, error: '目录递归删除必须由受信任调用方明确允许', }) await expect(access(sentinelPath)).resolves.toBeUndefined() const allowedResult = await deleteLocalFile(directoryPath, { allowDirectoryDelete: true, }) expect(allowedResult).toEqual({ success: true, isDirectory: true }) await expect(access(directoryPath)).rejects.toMatchObject({ code: 'ENOENT' }) }) test('显式禁用物理删除时不触碰历史不可信 fileUrl', async () => { const sentinelPath = path.join(sandboxRoot, 'legacy-target.txt') await writeFile(sentinelPath, 'keep') const result = await deleteFileIfUnreferenced(sentinelPath, { allowPhysicalDelete: false, }) expect(result).toEqual({ success: true, skipped: true, reason: 'physical_delete_disabled', }) await expect(access(sentinelPath)).resolves.toBeUndefined() }) })