它挡的是「接错了」——过滤逻辑完全正确,但会话里的范围值从来没传进处理函数,只有这一层能发现。
把「别人的 id」换成「不存在的 id」,两次响应的状态码与整段正文是否逐字节相同?
不同就是在泄露存在性——越权请求能探到「这条记录存在」。补一条比较整段正文的断言;只比状态码的写法在这里不算通过。
两者装的是同一条要求的两端:单元测试挡「算错了」,接口测试挡「接错了」。判据不是覆盖多少代码,是这次改动落在哪一层。
| 维度 | 单元测试 | 接口测试 |
|---|---|---|
| 测什么 | 函数的输入与输出 | HTTP 语义:状态码、响应头、正文结构与字段 |
| 不测什么 | 路由、序列化、鉴权中间件 | 函数内部实现与分支覆盖 |
| 依赖 | 无网络无数据库,或全部用替身 | 真实路由 + 测试用存储;会话由测试注入 |
| 典型失误 | 断言的是替身自己设的常量 | 只断言状态码,不比较正文 |
| 改动后触发 | 函数实现变了 | 路由、请求与响应结构、权限规则变了 |
代价也要认:接口测试比单元测试慢、更依赖环境,不要用它覆盖每一个分支。两者都测同一个函数时,是重复,不是保险。
一次接口用例是「会话 + 请求 + 对照」三段:会话决定这次是谁,请求从进程内发出,对照拿两个不同的 id 比同一件事。
会话 注入本范围的会话,不从请求体读范围值 请求 进程内构造,不监听端口 对照 别人的 id 与不存在的 id → 两段响应逐字节相同
要认的是这个形状:越权与不存在必须是同一段响应。一处用 404、一处用 403,或文案里带着「无权访问」,形状上就泄了存在性。
resp := GET("/items/1001") 他范围的 id,带本范围会话
assert.Equal(200, resp.Status) 只断言状态码
(不比较正文,也不与「不存在」那条对照)
认它的办法:看它有没有比两段正文。只断言状态码时,越权返回 403 也绿、返回带「无权访问」字样的 404 也绿——而这两种都在泄露存在性。这条用例跑了,等于没跑。
场景:让助手给详情接口补测试。同一个会话,一次查不存在的 id,一次查属于其他范围的 id。
一个接口至少覆盖这四类;前三类管越权,最后一类管写入。判据的来源写在接口契约里,测试照着它写。
| 判据 | 挡住什么 | 漏了会怎样 |
|---|---|---|
| 没有会话时拿不到数据,正文里不含任何标题 | 未登录也能读 | 鉴权中间件漏了也没人知道 |
| 越权与不存在的响应逐字节相同 | 用状态码或文案区分,泄露存在性 | 外部可以逐条探测哪些 id 存在 |
| 本范围的响应里有自己的数据、没有其他范围的数据 | 过滤没生效,越权放行 | 接口看着正常,数据已经越界 |
| 同一幂等键重复提交只落一条;字段缺失返回 400 并指出字段名 | 重复落库、校验错误变成 500 | 重复扣款、客户端拿到 200 的错误 |
幂等那一条把判定落在数据上:查一次行数,不能只看第二次的状态码——只回同一个 id 却插了两行,同样算不通过。错误必须落在 4xx 或 5xx,不要返回 200 再在正文里放失败标记。
判据写在接口契约里——路径、参数、状态码、字段,它是测试与客户端共同的那一份依据,改字段要走单独评审;契约只写外部可见的部分,内部实现写进契约会让每次重构都改契约。测试在进程内构造请求,不监听端口,会话由测试注入,不从请求体读范围值——否则测试自己就把越权通道留出来了。命令挂进门禁那一条入口命令。