ZIP 功能不能只用一个正常压缩包测试。它同时涉及不可信输入、流、磁盘、CPU、网络中断和异步清理;很多问题只会出现在损坏 ZIP 或失败路径中。
建议分成三层:
单元测试
路径、大小、文件类型、清单等纯逻辑
Service 集成测试
真正使用 yauzl/archiver 读写临时 ZIP
HTTP 集成测试
Multer + Express Route + 下载响应
不要所有测试都经过 HTTP。路径规范化和 Entry 限制可以作为纯函数快速覆盖;只有库和流的真实行为才需要创建 ZIP。
至少覆盖:
index.html 和资源路径正确;测试不要只检查 HTTP 200,还应重新用 yauzl 打开返回 ZIP,检查内部 Entry:
expect(entryNames).toEqual(
expect.arrayContaining([
"index.html",
"assets/architecture.png",
]),
);
这叫“验证产物”,比只看状态码可靠。
../outside.txt;../../.env;/absolute/path.txt;C:\\app\\server.ts;\\server\\share\\file.txt;/ 和 \;.zip 后缀;单元测试中的正常假数据很难暴露资源泄漏。可以有意识地制造失败:
warning 或 error;finalize() 失败;每个失败用例除了断言错误码,还应验证:
ZipFile 已关闭
流已销毁或结束
archiver 已停止
没有发送第二个响应
临时目录最终不存在
日志不包含密码、Token 或原始敏感内容
使用 Supertest 上传正常文件:
const response = await request(app)
.post("/api/markdown/export")
.attach("archive", fixturePath)
.expect(200)
.expect("Content-Type", /application\/zip/)
.expect("Content-Disposition", /attachment/);
默认解析器可能不适合二进制响应,可以注册 Buffer parser:
function binaryParser(
response: NodeJS.ReadableStream,
callback: (error: Error | null, body?: Buffer) => void,
): void {
const chunks: Buffer[] = [];
response.on("data", (chunk: Buffer) => {
chunks.push(chunk);
});
response.on("end", () => {
callback(null, Buffer.concat(chunks));
});
response.on("error", callback);
}
测试文件应放在明确的 fixtures 目录。小型恶意 ZIP 可以在测试中使用 archiver 或专用脚本生成;不要把来源不明的真实恶意样本放进开发者常用目录并随意解压。
不要只测试“超限会报错”,还要测试边界:
maxEntries - 1 → 成功
maxEntries → 成功(若规则是 <=)
maxEntries + 1 → 拒绝
大小、路径深度、名称长度和压缩比都应该有边界测试。还应确认计数使用的是所有 Entry 还是只使用文件 Entry,这个业务定义必须固定。
单个 20 MiB ZIP 能成功,不代表 30 个同时上传也能成功。观察:
对 CPU 较重的 Markdown 高亮、图片处理和压缩,可以限制并发或转到任务队列/Worker。PM2 cluster 增加进程后,总并发限制也要重新计算,不能只看单个进程。
| 指标 | 用途 |
|---|---|
| 上传 ZIP 字节数 | 了解正常文件范围和异常峰值 |
| Entry 数量 | 发现小文件洪泛 |
| 总声明解压字节数 | 发现 ZIP 炸弹趋势 |
| 最大压缩比 | 发现异常压缩内容 |
| 解压/转换/归档耗时 | 定位慢在哪个阶段 |
| 当前活动任务数 | 控制并发和容量 |
| 临时磁盘占用 | 避免磁盘写满 |
| 客户端中止次数 | 识别接口过慢或前端取消 |
| 按错误码计数 | 发现攻击或产品配置问题 |
| 清理失败次数 | 发现孤儿目录和句柄泄漏 |
日志中建议携带 requestId/jobId,但不要记录完整 Markdown 内容、Token、Cookie 或服务器敏感路径。
Nginx client_max_body_size
公网入口的请求体硬上限
Multer limits.fileSize / files / fields
multipart 上传层限制
yauzl Entry 限制
ZIP 内部数量、大小、路径和压缩比
任务并发限制
控制 CPU、内存和磁盘总消耗
这些限制不是重复。越靠外层越早拒绝明显异常流量,越靠内层越了解 ZIP 的真实结构。
.any();lazyEntries: true 控制处理节奏;mimetype 和原始文件名。warning、error 和目标流监听,再 finalize();warning 分类,不无条件忽略;finalize();finalize();res.attachment() 设置下载名;ZipFile、销毁 Entry 流并 archive.abort();finally;如果清单看起来很多,第一次上线至少完成:
lazyEntries;res.download();finally 清理,并测试失败路径;然后再逐步增加压缩比、Unicode 冲突、主动内容扫描、后台任务和更完整的监控。