123 lines
4.4 KiB
TypeScript
123 lines
4.4 KiB
TypeScript
/** @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<void>((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()
|
||
})
|
||
})
|