/** @jest-environment node */ import AdmZip from 'adm-zip' import { mkdtemp, readFile, readdir, rm } from 'fs/promises' import { tmpdir } from 'os' import path from 'path' import { createTaskDetailEditWorkspace, extractZipEntriesToDirectory, listZipEntries, } from '@/lib/zip-entry-reader' jest.mock('@/lib/upload-paths', () => ({ getUploadRootDir: jest.fn(() => tempRoot), resolveStoredUploadPath: jest.fn((stored: string) => stored), })) let tempRoot: string beforeEach(async () => { tempRoot = await mkdtemp(path.join(tmpdir(), 'zip-reader-test-')) }) afterEach(async () => { await rm(tempRoot, { recursive: true, force: true }) }) async function writeFixtureZip(name: string, entries: Array<{ name: string; data: Buffer | string; store?: boolean }>) { const zipPath = path.join(tempRoot, name) if (entries.some((entry) => entry.store)) { // store 条目走 archiver,覆盖 method 0 分支 const archiver = (await import('archiver')).default const { createWriteStream } = await import('fs') const output = createWriteStream(zipPath) const archive = archiver('zip', { zlib: { level: 0 } }) const done = new Promise((resolve, reject) => { output.on('close', () => resolve()) output.on('error', reject) archive.on('error', reject) }) archive.pipe(output) for (const entry of entries) { const filePath = path.join(tempRoot, `src-${entry.name}`) await (await import('fs/promises')).writeFile(filePath, entry.data) archive.file(filePath, { name: entry.name, ...(entry.store ? { store: true } : {}) }) } await archive.finalize() await done return zipPath } const zip = new AdmZip() for (const entry of entries) { zip.addFile(entry.name, entry.data as Buffer) } zip.writeZip(zipPath) return zipPath } describe('zip-entry-reader', () => { test('按中央目录顺序列出条目并解压内容一致(deflate)', async () => { const zipPath = await writeFixtureZip('deflate.zip', [ { name: 'b-second.jpg', data: Buffer.from('second-image-bytes') }, { name: 'a-first.jpg', data: Buffer.alloc(64 * 1024, 7) }, { name: '中文图.png', data: Buffer.from([1, 2, 3, 4, 5]) }, ]) const entries = await listZipEntries(zipPath) // adm-zip 写包时的目录顺序与插入顺序不保证一致,这里只校验完整性; // 顺序一致性由 list 与 extract 共用同一中央目录保证。 expect([...entries.map((entry) => entry.name)].sort()).toEqual( ['a-first.jpg', 'b-second.jpg', '中文图.png'].sort(), ) const destination = path.join(tempRoot, 'out-deflate') const extracted = await extractZipEntriesToDirectory(zipPath, destination) expect(extracted.length).toBe(3) // 列表与解压保持同一顺序 expect(extracted.map((item) => item.entryName)).toEqual(entries.map((entry) => entry.name)) const byName = new Map(extracted.map((item) => [item.entryName, item])) const second = await readFile(byName.get('b-second.jpg')!.filePath) expect(second.toString()).toBe('second-image-bytes') const big = await readFile(byName.get('a-first.jpg')!.filePath) expect(big.length).toBe(64 * 1024) expect(big[0]).toBe(7) const chinese = await readFile(byName.get('中文图.png')!.filePath) expect(Array.from(chinese)).toEqual([1, 2, 3, 4, 5]) }) test('store 条目按原始字节拷贝', async () => { const payload = Buffer.alloc(32 * 1024, 9) const zipPath = await writeFixtureZip('store.zip', [ { name: 'raw.bin', data: payload, store: true }, ]) const destination = path.join(tempRoot, 'out-store') const [extracted] = await extractZipEntriesToDirectory(zipPath, destination) expect(extracted.entryName).toBe('raw.bin') expect((await readFile(extracted.filePath)).equals(payload)).toBe(true) }) test('空压缩包返回空列表', async () => { const zipPath = await writeFixtureZip('empty.zip', []) expect(await listZipEntries(zipPath)).toEqual([]) }) test('非 ZIP 文件报错', async () => { const notZip = path.join(tempRoot, 'not.zip') await (await import('fs/promises')).writeFile(notZip, Buffer.from('plain text, not a zip')) await expect(listZipEntries(notZip)).rejects.toThrow('ZIP') }) test('编辑工作目录创建后可清理', async () => { const workspace = await createTaskDetailEditWorkspace() await workspace.cleanup() await expect(readdir(workspace.dir)).rejects.toBeDefined() }) })