身份与认证测试
这是 youteacher 集成测试套件里专门面向认证的辅助层。它的核心主张是:认证只有在被真人使用的方式下走通,才算被证明——走真实的 UI、打真实的后端服务、再回到那些服务真正写下的状态去核对。认证服务和它们各自的存储从不被 mock;唯一的替身是外部邮件投递和 OAuth 提供方——这两样测试无法真的去驱动。辅助函数通过渲染出来的页面完成注册和登录,像认证器 App 那样生成第二因子验证码,用浏览器里真实的凭据设备走通 passkey,最后读取每个服务各自的数据库,确认该有的行存在、该删的行已消失。
走真实 UI,不抄近路
注册和登录辅助函数点击的是访客看到的同一批对话框和页面。它们打开登录框、填入凭据、提交,然后等到页头的账户控件报告"已认证"状态才继续。注册辅助函数走完整的"先验邮箱、再设密码"流程:填地址、触发一次性验证码邮件、从 mock 邮件服务里取回验证码、提交、设置密码、创建账户。
这些辅助函数里的大部分代码,不是主流程本身,而是围着主流程的时序纪律。页面在服务端 HTML 到达后才 hydrate,有些列表会自动选中一行并悄悄改写 URL,而闸门控件在挑战 token 就绪前一直是禁用的。点击早了一瞬,要么毫无反应,要么把正在输入的字段拆掉。所以辅助函数等的是具体信号——某个元素的认证状态属性、某个特定的网络响应、一个已稳定的 URL——而不是挂钟延时。这是反复出现的教训:一个 e2e 辅助函数的篇幅,大半花在把流程变得确定上,整套测试的可靠性就压在这些等待上。
当 UI 登出路径失败时,辅助函数会退回到直接清掉会话 cookie,避免一个陈旧会话泄漏进下一个测试。测试隔离在这里被当作正确性属性,而不是可有可无的讲究。
第二因子,是生成而非造假
基于时间的一次性密码由一个小而合标准的生成器产出(RFC 6238 构造),它把一个共享密钥变成认证器 App 会显示的同一串滚动验证码。设置辅助函数在安全页里注册第二因子,读回服务端下发的密钥,立刻据此算出一个有效码提交以确认注册;后续登录再用同一个生成器回答第二因子提示。因为验证码是算出来的而不是伪造的,服务端真实的校验路径原封不动地跑了一遍。
passkey 通过一个挂在 Chrome DevTools Protocol 上的虚拟认证器来走通。这不是对凭据 API 的打桩——它是浏览器提供的真实 WebAuthn 设备,所以 navigator.credentials 的行为和生产环境完全一致,只是背后换成了虚拟设备而非物理硬件。辅助函数可以添加和移除该设备、列出它存的凭据、清空它们、切换用户验证是否成功——这样一个测试就能覆盖生物识别成功和失败两种情形。
测试直接发起(而非经浏览器)的服务间调用,带一个短时效的 bearer token,用来证明请求来自可信服务。这个 token 刻意不携带任何用户身份——用户会话活在会话 cookie 里,服务信任是另一件事。该 token 的签名与过期校验交给后端单元和集成测试;e2e 层只需要一个有效 token 就能触达 admin 和 profile 接口。
回到真相之源去核对
每个后端服务各自拥有自己的存储,数据库辅助层伸进每一个去核对结果:auth 服务里的用户账户、会话、第二因子密钥、passkey 与 OAuth 关联;profile 服务里的人才、雇主、招聘方与管理员记录;job 服务里的职位与投递;搜索索引里的文档;以及 Redis 里的限流与缓存计数。断言辅助函数把期望表述成状态——这个用户存在且带这些属性、恰好新建了一个会话、只剩下这一个会话、被删用户的所有行在每个服务里都已清空——并在不匹配时报出指名的错误。
这就补上了只看 UI 的断言留下的缺口。屏幕可以显示"已登录"而后端什么都没写;删除可以看起来成功而孤儿行还活在兄弟服务里。读取真相之源,才让一个绿测意味着能力真的跑过了。两个习惯值得注意:会话断言比对的是动作前后的两张快照,所以量的是"这一次动作"而非累积状态;删除检查按 user id 直接查每张表,而不是走 join,这样一个没有父行的孤儿行也仍会被抓到。
为什么这样建
整个这一层是在论证一件事:在多服务系统里,身份是测试中最不能假造的东西。认证横跨浏览器、多个服务、多个存储;任何一处接缝上的 mock,恰恰盖住了最伤人的那个集成 bug。通过驱动真实 UI、生成真实因子、使用真实凭据设备、读取真实存储,这些辅助函数让测试对每个其他测试都依赖的那条流程保持诚实。
另见
- youteacher_integral ——本层所属的集成测试套件
- e2e-must-be-blackbox ——为什么测试驱动用户动作、断言可见状态
- guard-must-fail-on-the-bug ——断言只有在 bug 能让它变红时才有价值