12 · 第二阶段验收:怎样证明没有串团队或越权#
权限功能不能只验证“管理员操作成功”。更有价值的问题是:别人登录后能不能读到这张工单?客户能不能直接发请求修改状态?被移除的人还能不能使用原会话?本章把这些问题变成可复现的操作和测试。
以下是供你执行的学习验收步骤;教程内容的构建和链接检查不代表业务数据库测试已经执行。
1. 准备一组不容易混淆的测试身份#
按 搭建章 启动当前业务项目,并使用独立浏览器会话。普通标签页通常共享同一站点 Cookie,不能把两个标签页自然当成两个不同账号;可使用不同浏览器配置或普通与隐私窗口分别登录。
| 测试用户 | 甲团队身份 | 乙团队身份 | 用途 |
|---|---|---|---|
| A | 创建者,ADMIN | CUSTOMER | 验证同用户跨团队角色独立 |
| B | 无成员关系 | 创建者,ADMIN | 验证团队隔离 |
| C | AGENT | 无 | 验证客服可处理工单 |
| D | CUSTOMER | 无 | 验证客户归属 |
| E | CUSTOMER | 无 | 为 D 提供另一位客户的工单 |
先分别注册,再由对应团队管理员添加成员。成员添加不会发送邮件,也不会为尚未注册的邮箱自动建账号。
准备好身份后分别创建工单,并记下团队与工单 ID。仅使用自己的本地测试数据,避免拿真实账号或他人的工单做教学实验。
2. 先验认证,再验业务授权#
| 操作 | 预期现象 | 对应源码 |
|---|---|---|
| 错误密码登录 | 失败,显示通用提示 | AuthForm.submit、Better Auth |
| 正确密码登录 | 建立会话,可看到工作区 | /api/auth 入口 |
| 刷新页面 | 会话有效时仍可继续使用 | authClient.useSession |
| 没有会话调用业务接口 | UNAUTHORIZED |
protectedProcedure |
| A、B 创建各自团队 | 各自成为该团队管理员 | team.create 嵌套写入 |
| A 进入乙 | 显示客户角色 | team.list 与乙的 Membership |
| A 在乙试图自我提权 | FORBIDDEN |
requireAdmin |
页面可能先显示登录表单或隐藏操作,因此某些拒绝场景不能只靠点击按钮验证。已有集成测试使用 createCaller 直接构造调用,验证服务端保护本身。
3. 用三种组合检查资源隔离#
核心场景不要只测试一次“换 teamId”:
- 团队外访问:B 用甲 teamId 查甲工单,成员检查拒绝。
- 借本团队身份读别人的 ID:B 用乙 teamId 查甲工单 ID,资源查询未找到。
- 同团队不同客户:D 查询 E 在甲创建的工单,创建者条件使它不可见。
再验证 C 能查看和处理甲团队工单,D 能创建并查看自己的工单,但不能修改状态。有效权限和拒绝权限都要覆盖,避免把“所有人全部被拒绝”误认为隔离正确。
列表验收还要检查 total,不能只看表格行;测试标题、状态筛选与翻页后,团队边界应始终存在。
4. 验证权限变化,不只验证初始角色#
让 A 将 E 升为客服,确认 E 后续能处理甲的工单;随后将 E 移出甲,E 即使会话还在,后续请求也应因缺少成员关系被拒绝。
再让团队只有一个管理员,验证不能移除或降级自己。并发验证使用测试中的两位管理员分别降级自己场景,确认最终仍有一位管理员。
角色变更提交后,后续权限检查会查新关系。它不等于页面实时推送,也不承诺取消所有已经通过检查的在途读取;若界面仍有旧按钮,应结合请求结果区分缓存陈旧与授权错误。
5. 现有自动测试究竟覆盖了哪一层#
主要阅读 tests/tickets.integration.test.ts:
| 测试方式 | 覆盖的链路 | 没有直接覆盖什么 |
|---|---|---|
appRouter.createCaller({ db, userId }) |
tRPC procedure、schema、服务层、真实数据库 | 浏览器 Cookie 发送与 Next.js HTTP 入口 |
createAuth + auth.handler(new Request(...)) |
真实认证库的注册、密码验证、Cookie 响应和退出 | 页面表单与真实浏览器导航 |
auth.api.getSession({ headers }) |
服务端按 Cookie 读取有效会话 | 页面缓存是否清空 |
| 页面手动验收 | 表单、团队切换、按钮、提示、退出后显示 | 穷尽所有伪造输入与并发路径 |
特别注意:createCaller 里的 userId 是测试夹具显式注入,不是 HTTP 客户端可以自报身份的入口。测试在这里假定请求上下文已经建立,用来聚焦授权;认证测试再独立验证上下文身份来源所依赖的会话机制。
认证用例验证了密码存储值不同于明文、错误密码返回 401、Cookie 有 HttpOnly、退出后重用旧 Cookie 得不到会话。不能据此推导所有安全属性都已测完;业务 Route Handler 的 Origin 分支仍需相应 HTTP 层验证,createCaller 不会经过它。
6. 执行真实数据库测试#
如果测试数据库还不存在,在启动开发 Compose 后首次执行:
pnpm db:up
# 已有 teamdesk_test 时跳过创建;不要把测试连接指向生产。
docker compose exec -T postgres createdb -U teamdesk teamdesk_test
DATABASE_URL=postgresql://teamdesk:teamdesk@localhost:5433/teamdesk_test pnpm db:migrate
pnpm db:generate然后执行本阶段聚焦测试:
TEST_DATABASE_URL=postgresql://teamdesk:teamdesk@localhost:5433/teamdesk_test \
pnpm exec vitest run tests/tickets.integration.test.ts测试创建自己的账号、团队和工单,最后清理自己创建的数据。当前创建工单会保存提醒任务,但这组测试不要求 Redis 在线;不要把开发 Worker 指向测试数据库抢先处理这些数据。
pnpm test 默认未提供测试连接时会跳过数据库集成测试。检查输出里的 skipped,不要把“默认单元测试全绿”写成“真实登录和权限已经通过”。第一阶段的完整测试说明仍可参照 第 06 章。
7. 按失败边界排错#
| 现象 | 先检查 | 常见原因 |
|---|---|---|
| 注册 / 登录接口内部错误 | 服务端日志、getAuth 配置 |
缺少密钥、地址或迁移 |
| 认证成功却没有团队 | team.list 与 Membership |
注册不等于自动加入已有团队 |
| 切换 team 后提示无资格 | URL、团队列表、成员关系 | 选中了不再有权限的团队 |
| POST 直接返回 HTTP 403 | 地址栏 Origin 与 BETTER_AUTH_URL |
localhost / 127.0.0.1 或端口不一致 |
| 客服改角色被拒绝 | 当前团队角色与 requireAdmin |
客服能处理工单,不能管理成员 |
| 跨团队详情未找到 | 输入 teamId 与工单归属 | 正常的资源隔离结果 |
| 移除后旧页面还在显示 | 页面是否重新请求 | 已读取数据与当前访问权不是一回事 |
| 登录另一账号出现旧内容 | 查询 key、退出清理、完整刷新 | 浏览器状态边界需要核对 |
| 旧工单看不到 | 历史归档迁移 | 没有自动认领入口 |
先用 Network 确认请求去了哪个入口,再看错误码,最后查服务端与数据库。排查时不要复制真实会话 Cookie、密码散列或 .env 密钥到聊天和截图里。
8. 三道练习,把规则说清楚#
练习 A:只在布局里判断登录是否足够#
如果 TeamWorkspace 未登录时只显示 AuthForm,是否可以删掉 protectedProcedure?
动手练习查看参考答案
参考答案:不可以。Route Handler 不会因为客户端布局隐藏内容就自动受保护,脚本仍能访问接口;当前身份从 HTTP 会话上下文建立,业务 procedure 独立拒绝未登录。页面控制体验,接口控制是否执行。
练习 B:为什么角色不保存在 User 上#
A 在甲是 ADMIN,在乙是 CUSTOMER,如果只用 User.role,会丢失什么信息?
动手练习查看参考答案
参考答案:会丢失角色所属的团队范围。当前 Membership 把用户、团队和角色绑定,requireAdmin 检查的是本次目标团队内的资格,而不是用户在其他地方拥有的最高权限。
练习 C:如何审查新增“统计数量”接口#
假设要显示待处理工单总数,开发者写了 db.ticket.count({ where: { status: 'OPEN' } })。缺了什么?
动手练习查看参考答案
参考答案:缺少团队条件、客户创建者范围以及调用人的成员检查。统计也是读取数据,必须使用同样的可访问集合;不能只保护详情,放任总数、搜索、导出成为旁路。
9. 第二阶段完成标准与后续边界#
你应能从源码独立说明三条链路:
- 登录:表单 → 认证 HTTP 入口 → 库验证凭据并保存会话 → Cookie → 后续请求得到可信身份。
- 成员变更:表单 → 输入和登录校验 → 团队锁内管理员授权 → 数据库原子变更成员及关联工单 → 返回并刷新缓存。
- 工单访问:团队上下文 → 服务端成员与角色 → 团队 / 创建者过滤 → 数据库查询或条件更新 → 返回可见数据或错误。
当前没有接入邮件邀请、邮箱验证与找回密码,也没有配置持久化认证限流;不是完整的公开注册运营方案。已有 Verification 表、认证库具备某种能力,都不等于本项目启用了它。这里列的是实际实现边界,后续要根据上线需求补齐。
接着进入 第 13 章:工单分配,继续学习分配、回复、URL 状态和草稿;本阶段最重要的收获是:把“谁、在哪个团队、对哪项资源、执行什么动作”落实到每一次服务端查询与写入。