Identity 的测试不能只证明“正确密码能登录”。真正的交付证据来自失败分支、跨租户隔离、票据重放、平台 Token 隔离、适配器覆盖和生产告警。
1. 测试分层
越往上越接近真实交付,也越慢。下层负责穷举状态,上层负责证明装配和基础设施没有改变语义。
2. 聚合与纯策略测试
| 测试类 | 重点 |
|---|---|
UserAggregateTests | 改密刷新安全戳、失败锁定、启停、激活、Host 语义 |
OrganizationUnitAggregateTests | 路径、移动、后代判断、启停和排序 |
PlatformTenantLifecycleTests | 供给步骤、CompleteProvisioning、暂停和终态 |
TenantTransitionGuardTests | 所有合法边、同状态幂等、非法跳转错误码 |
PasswordHasherTests | BCrypt 验证与 Rehash 判断 |
PasswordValidatorTests | 每个复杂度规则的独立失败 |
| FIDO2 / MFA tests | Credential clone detection、缓存和风险策略 |
聚合测试使用固定 DateTimeOffset,不要依赖系统时间。业务错误断言 Error Code 和类型,不用本地化消息作为稳定契约。
3. 登录深模块测试
LoginFlowTests 直接驱动 ILoginFlow,当前覆盖:
- 正常登录与 Token。
- 用户 2FA 和风险 MFA。
- MFA Challenge 签发失败时失败关闭。
- Refresh Token Store 失败不返回成功。
- Session Store 失败时撤销刚签发的 Refresh Token。
- Captcha 首次挑战、正确答案、缺失答案、校验和生成失败。
- 风险 Block 在密码前拒绝。
- 错误密码、不存在用户、锁定、禁用和锁定到期自动解锁。
- 风险引擎失败时当前的 fail-open 行为。
- 强制改密标志、跨租户邮箱查找和取消传播。
运行这一组:
# 用 FullyQualifiedName 固定 Identity 深模块,不混入无关应用测试。dotnet test tests/BitzOrcas.Application.Tests/BitzOrcas.Application.Tests.csproj \ --configuration Release \ --no-restore \ --filter 'FullyQualifiedName~Identity.LoginFlowTests|FullyQualifiedName~Identity.LoginRiskIntegrationTests|FullyQualifiedName~Identity.VerifyMfaLoginCompletionTests'修改签发顺序时,至少增加一个“前一步成功、后一步失败”的补偿测试。只有 Happy Path 无法证明不会泄露有效 Refresh Token。
4. 账号准入测试
AccountAdmissionFlowTests 是连续业务案例,不应拆成互不相干的 Mock 测试。它覆盖:
- 定向邀请只接受目标邮箱。
- 开放链接限制邮箱域、次数和到期时间。
- Approval 创建 PendingActivation,Activation 激活并可登录。
- 默认角色被应用,失效角色拒绝审批。
- Reject 后不能激活。
- 缺失或重放 Token 返回通用安全错误。
新增事务适配器后要在真实数据库验证:接受邀请失败时,Admission 与 UsedCount 不分叉;激活删除 Token 失败时,不能留下 Active 用户和可重放 Token。
5. 平台 Token 隔离
PlatformTokenIsolationTests 使用真实 JwtTokenService 和内存 Store,固定三条契约:
- 刷新 Web Token 不改变 App 槽位。
- 刷新 App Token 不改变 Web 槽位。
- 新
RefreshTokenRecord继承原始平台。
# 使用真实 JwtTokenService,固定 Claim 与 Web/App 隔离契约。dotnet test tests/BitzOrcas.Unit.Tests/BitzOrcas.Unit.Tests.csproj \ --configuration Release \ --no-restore \ --filter 'FullyQualifiedName~Identity.TokenServiceClaimsTests|FullyQualifiedName~Identity.PlatformTokenIsolationTests'生产 Store 必须再执行同一行为契约。内存 Dictionary 的隔离不能证明 ORM 唯一键和查询过滤器正确。
6. 持久化与双 ORM
Identity 同时包含统一聚合仓储和专用 Query Store。持久化测试至少覆盖:
| 契约 | SqlSugar | EF Core |
|---|---|---|
| User TenantId 过滤 | 必测 | 必测 |
| Host 用户全局查询 | 必测 | 必测 |
| Invitation TokenHash 唯一查找 | 必测 | 必测 |
| RefreshToken 平台 + Hash 索引 | 必测 | 必测 |
| OU 树查询与分页 | 必测 | 必测 |
| Admission + User + Token 事务 | 必测 | 必测 |
ProductionIdentityStoreTests、RepositoryIdentityStoreTests、IdentityQueryHandlerTests 与读模型 Adapter parity 测试是现有入口。新增 Store 不能只测“能保存”,还要测跨租户标识被拒绝和软删除数据不可见。
7. API Shell 边界
API Shell 没有生产数据库时使用失败关闭默认 Store,但路由仍应可启动。API 测试证明:
- 接受邀请与完成激活不是 401;它们在 Store 不可用时返回业务/基础设施失败。
- 邀请管理没有认证时是 401。
- Login/Refresh 是匿名,Me/Logout 需要认证。
- SmartEnum JSON 以名称序列化并可按名称或数值读取。
- Result/Error 统一映射 Problem Details。
# Shell 不要求生产数据库,但必须证明公开与管理路由的认证边界。dotnet test tests/BitzOrcas.Integration.Tests/BitzOrcas.Integration.Tests.csproj \ --configuration Release \ --no-restore \ --filter 'FullyQualifiedName~ApiShellTests'公开端点“不是 401”不等于允许成功。缺少 Store、Token 无效或租户不可信时必须失败关闭。
8. 生产适配器门禁的覆盖范围
ProductionAdapterReadinessGuard 当前会阻止 NullUnitOfWork、InMemoryTenantStore、InMemoryApiClientStore 等默认实现进入 Production/Staging。它还检查 Redis、文件、通知 Publisher、Feature 和 Webhook 关键端口。
它当前没有完整覆盖 Identity 的 Email/SMS Delivery、IRefreshTokenStore、外部身份 Provider、MFA 配置 Store 和分布式 Challenge Cache。因此 Identity GA 还需要补充探针,不能把 Host 成功通过通用 Guard 当作全部就绪。
9. 观测信号
| 信号 | 维度 | 告警建议 |
|---|---|---|
| 登录失败率 | Tenant、Provider、Reason,不含用户名明文 | 基线突增和密码爆破模式 |
| 风控降级次数 | 风控错误码、实例 | 任何持续增长都需告警 |
| Captcha 生成/验证失败 | Provider、阶段 | Provider 故障与攻击流量分开看 |
| MFA Challenge 失败 | 方法、缓存实现 | 多实例跨节点失败 |
| Refresh Token 重用 | Tenant、UserId Hash、平台 | 高优先级安全告警 |
| 会话写入补偿 | Store 错误码 | Token 已发后补偿风险 |
| 激活通知失败 | Channel、Provider、错误类 | 入职流程阻塞 |
| 租户非法转换 | 当前状态、目标状态 | 运维脚本或并发问题 |
日志可记录 UserId 或 Token Hash 的诊断标识,但不得记录密码、Raw Token、完整手机号、JWT、Refresh Token 或连接字符串。
10. 登录故障排查
返回 InvalidCredentials
按顺序检查租户解析结果、Host/邮箱/租户内查找分支、用户状态和密码验证。对外保持通用错误,对内通过 CorrelationId 关联脱敏登录日志。
一直返回 Captcha
确认二次请求带回原 captchaChallengeId 与答案;检查票据是否被第一次校验消费;检查反向代理是否导致 IP、租户或设备上下文改变。
密码正确但没有 Token
检查 requiresMfa、requiresCaptcha、用户 TwoFactorEnabled 和风险 Challenge。再检查角色查询、Refresh Token Store 与 Session Store 的失败日志。
一个平台刷新导致另一个平台退出
检查 Token Name 是否为 RefreshToken:Web / RefreshToken:App,Store 唯一键是否包含 Name,轮换记录是否继承 Platform。
11. 邀请和激活故障排查
| 现象 | 优先检查 |
|---|---|
| 所有邀请都 Invalid | 当前租户、Token Hash 算法、系统时间和 Store 查询 |
| 定向邀请邮箱不匹配 | 规范化规则和候选人实际提交邮箱 |
| 审批后收不到邮件 | 最终 IEmailDeliveryPort 类型、Frontend URL、供应商回执 |
| 激活一直 Invalid | 72h 到期、Token 是否被编码改变、UserToken Provider/Name |
| 审批产生半成品用户 | UoW 范围、角色写入、通知失败和恢复脚本 |
| 重复激活成功 | Token 删除、Admission Consume 与并发唯一约束 |
修复数据前先保存审计证据。不要手工把用户直接改成 Active 而跳过 Admission 和 Token 状态;恢复操作应通过受审计用例或一次性运维命令执行。
12. 安全事件处置
疑似 Refresh Token 被盗
- 按 Token Hash 定位
RefreshTokenRecord和族链。 - 撤销族链与相关 Session。
- 根据风险提升要求用户重新认证或重置密码。
- 检查不同平台是否受影响,不要无证据扩大范围。
- 保留平台、IP、DeviceId、时间和 CorrelationId,脱敏后进入安全审计。
邀请链接泄露
- 撤销 Invitation,保留
RevokedByUserId和时间。 - 查询已产生 Admission 和审核状态。
- 对异常 Admission 执行拒绝;若已经激活,按账号事件处理。
- 创建新 Token,不能恢复旧 Raw Token。
JWT 签名密钥泄露
- 立即启用新 Kid,停止旧密钥签发。
- 视事件等级缩短或取消旧密钥验证宽限。
- 撤销 Refresh Token 与高风险会话。
- 验证所有实例已加载同一密钥环。
- 记录轮换证据并复盘 Secret 来源。
13. 发布前全量命令
# Restore 和 Build 使用同一 Release 输入,先固定依赖与编译产物。dotnet restore BitzOrcas.Modern.slnx --locked-modedotnet build BitzOrcas.Modern.slnx --configuration Release --no-restore
# 分层执行 Identity 相关测试,失败时能直接定位责任层。dotnet test tests/BitzOrcas.Unit.Tests/BitzOrcas.Unit.Tests.csproj \ --configuration Release --no-build --no-restore \ --filter 'FullyQualifiedName~Identity|FullyQualifiedName~Platform.Tenancy'
dotnet test tests/BitzOrcas.Application.Tests/BitzOrcas.Application.Tests.csproj \ --configuration Release --no-build --no-restore \ --filter 'FullyQualifiedName~Identity'
dotnet test tests/BitzOrcas.Architecture.Tests/BitzOrcas.Architecture.Tests.csproj \ --configuration Release --no-build --no-restore \ --filter 'FullyQualifiedName~Identity'Integration Tests 是否能使用 --no-build 取决于前面的解决方案构建是否包含目标;CI 应使用仓库已经验证的标准入口。
14. 全局扫尾与预期
# 不应出现复制登录编排或在 Host 直接验证密码。rg -n "VerifyPasswordFailed|DummyPasswordHash|IssueMfaChallenge" \ src/Hosts src/Platform/Identity -g '*.cs'
# 除 LoginFlow 外的命中必须逐项解释,不能形成第二套密码登录状态机。
# Identity 统一聚合不应出现一对一 Entity/Mapper 复制。find src/Platform/Identity -type f \ \( -name 'UserEntity.cs' -o -name 'AccountInvitationEntity.cs' -o -name 'PlatformTenantEntity.cs' \)
# 预期没有输出;若出现,必须有记录在 docs/architecture 的非对称例外。15. GA 签字清单
- 所有上层测试通过,且真实 ORM/消息/缓存/通知适配器参与集成测试。
- 风控 fail-open 是经过产品和安全批准的显式策略。
- 改密是否撤销全部会话有明确产品决定,测试与 UI 一致。
- 通知、外部身份、MFA、Refresh Token Store 的最终实现可观测且非占位。
- 跨租户、Host、平台 Token、票据重放和状态终态均有负向测试。
- 密钥泄露、Token 盗用、邀请泄露和半成品审批都有演练手册。