09. 测试策略与生产上线清单

ZIP 功能不能只用一个正常压缩包测试。它同时涉及不可信输入、流、磁盘、CPU、网络中断和异步清理;很多问题只会出现在损坏 ZIP 或失败路径中。

1. 测试分层

建议分成三层:

单元测试
  路径、大小、文件类型、清单等纯逻辑

Service 集成测试
  真正使用 yauzl/archiver 读写临时 ZIP

HTTP 集成测试
  Multer + Express Route + 下载响应

不要所有测试都经过 HTTP。路径规范化和 Entry 限制可以作为纯函数快速覆盖;只有库和流的真实行为才需要创建 ZIP。

2. 正常功能用例

至少覆盖:

测试不要只检查 HTTP 200,还应重新用 yauzl 打开返回 ZIP,检查内部 Entry:

expect(entryNames).toEqual(
  expect.arrayContaining([
    "index.html",
    "assets/architecture.png",
  ]),
);

这叫“验证产物”,比只看状态码可靠。

3. 恶意和异常 ZIP 用例

路径类

容量类

格式类

内容类

4. 故障注入

单元测试中的正常假数据很难暴露资源泄漏。可以有意识地制造失败:

每个失败用例除了断言错误码,还应验证:

ZipFile 已关闭
流已销毁或结束
archiver 已停止
没有发送第二个响应
临时目录最终不存在
日志不包含密码、Token 或原始敏感内容

5. HTTP 集成测试示意

使用 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 或专用脚本生成;不要把来源不明的真实恶意样本放进开发者常用目录并随意解压。

6. 测试资源上限本身

不要只测试“超限会报错”,还要测试边界:

maxEntries - 1  → 成功
maxEntries      → 成功(若规则是 <=)
maxEntries + 1  → 拒绝

大小、路径深度、名称长度和压缩比都应该有边界测试。还应确认计数使用的是所有 Entry 还是只使用文件 Entry,这个业务定义必须固定。

7. 并发和资源测试

单个 20 MiB ZIP 能成功,不代表 30 个同时上传也能成功。观察:

对 CPU 较重的 Markdown 高亮、图片处理和压缩,可以限制并发或转到任务队列/Worker。PM2 cluster 增加进程后,总并发限制也要重新计算,不能只看单个进程。

8. 建议监控的业务指标

指标 用途
上传 ZIP 字节数 了解正常文件范围和异常峰值
Entry 数量 发现小文件洪泛
总声明解压字节数 发现 ZIP 炸弹趋势
最大压缩比 发现异常压缩内容
解压/转换/归档耗时 定位慢在哪个阶段
当前活动任务数 控制并发和容量
临时磁盘占用 避免磁盘写满
客户端中止次数 识别接口过慢或前端取消
按错误码计数 发现攻击或产品配置问题
清理失败次数 发现孤儿目录和句柄泄漏

日志中建议携带 requestId/jobId,但不要记录完整 Markdown 内容、Token、Cookie 或服务器敏感路径。

9. Nginx、Multer 和业务层各自的限制

Nginx client_max_body_size
  公网入口的请求体硬上限

Multer limits.fileSize / files / fields
  multipart 上传层限制

yauzl Entry 限制
  ZIP 内部数量、大小、路径和压缩比

任务并发限制
  控制 CPU、内存和磁盘总消耗

这些限制不是重复。越靠外层越早拒绝明显异常流量,越靠内层越了解 ZIP 的真实结构。

10. 生产上线清单

上传入口

yauzl 导入

Markdown 与 HTML

archiver 导出

中止与清理

运维

11. 新人上线的最小安全版本

如果清单看起来很多,第一次上线至少完成:

  1. Nginx 与 Multer 限制上传大小;
  2. 使用 yauzl lazyEntries
  3. 阻止路径穿越和符号链接;
  4. 限制 Entry 数、单项大小和总解压大小;
  5. 每请求独立临时目录;
  6. 先生成结果 ZIP,成功后再 res.download()
  7. finally 清理,并测试失败路径;
  8. 限制接口并发,记录耗时和清理失败。

然后再逐步增加压缩比、Unicode 冲突、主动内容扫描、后台任务和更完整的监控。

复盘题

  1. 为什么 ZIP 接口测试不能只断言 HTTP 200?
  2. 路径攻击、容量攻击和格式损坏分别应有哪些测试样例?
  3. 故障注入后,除了错误响应还应验证哪些资源状态?
  4. 为什么单请求压力测试不能代替并发测试?
  5. Nginx、Multer 与 yauzl 的大小限制各解决什么问题?
  6. 为什么要监控临时目录清理失败次数?
  7. PM2 cluster 增加实例后,为什么需要重新评估总并发?
  8. 如果只能完成八项措施,新人最小安全版本应包括哪些内容?

官方参考