GDPR 模块的测试目标不是“端点返回 200”,而是证明请求不会串租户、不会走错状态机、不会漏掉数据 Owner、不会把不完整导出标成完成,并能在超时和部分失败后恢复。
1. 当前已有测试证据
| 测试 | 已证明 | 未证明 |
|---|---|---|
GdprQueryHandlerTests | Consent/SAR 列表通过 IGdprStore,并传 actor tenant/user | 路由、授权、有效租户、详情安全 |
GdprInfrastructureArchitectureTests | Infrastructure 不依赖具体 ORM;Store fail-closed 注册 | 运行时数据库和合规语义 |
PortRepositoryParityTests | 两个 ORM 的基础 Consent、DSR 更新和 SysUser 匿名化结果一致 | Command 状态机、并发、回滚、关联数据 |
| API Shell Smoke | 无数据库时解析 Unavailable Port | 生产 Adapter Readiness |
| Shared ownership tests | 两张表归 GDPR Owner,旧 1:1 模型不存在 | 数据分类和保留 |
目前没有 GDPR Command 单元测试、HTTP 授权测试、SAR 文件测试或多 Owner 擦除测试。
2. 状态机测试
每种请求类型都应使用表驱动测试固定允许与拒绝矩阵。
[Theory][InlineData("CoolingOff", true, "Executing")][InlineData("Submitted", false, null)][InlineData("Executing", false, null)][InlineData("Completed", false, null)][InlineData("Cancelled", false, null)][InlineData("Rejected", false, null)]public void Execute_transition_is_closed( string from, bool expectedAllowed, string? expectedTo){ // 每个来源状态都必须有显式期望,新增枚举项时测试会要求决策。 var result = ErasureTransitions.TryExecute( ErasureStatus.Parse(from), coolingOffExpired: true);
// 允许路径还要固定唯一目标状态,避免隐式跳转。 result.IsSuccess.ShouldBe(expectedAllowed); if (expectedAllowed) result.Value.ShouldBe(ErasureStatus.Parse(expectedTo!));}当前没有 ErasureTransitions;测试形状表达生产目标。还要逐一把 SAR ID 传给 Erasure 命令、把 Erasure ID 传给 SAR 命令,验证统一失败。
3. 租户与主体测试
至少构造四个维度:
- Actor Tenant;
- Effective Tenant;
- Subject Tenant;
- 记录 Tenant。
using var tenantScope = tenantAccessor.Push( effectiveTenantId: "tenant-b", actorTenantId: "tenant-host");
// 管理员代表目标租户发起,但主体必须是目标租户内的用户。var result = await mediator.Send( new CreateSarForSubject.Command( SubjectUserId: "user-b", RequestId: "sar-b-001"), cancellationToken);
result.IsSuccess.ShouldBeTrue();// 分别断言数据归属、主体和操作人证据。stored.TenantId.ShouldBe("tenant-b");stored.UserId.ShouldBe("user-b");stored.ActorTenantId.ShouldBe("tenant-host");自助端点若不支持 operate-as,应明确拒绝,而不是写入混合上下文。
4. 幂等与并发测试
必须在真实数据库上并发发送相同 RequestId:
- 同租户、同类型、同 RequestId 只产生一行;
- 同租户、不同类型的命名空间策略明确;
- 不同租户可以使用相同 RequestId;
- 两个 Operator 不能同时完成同一请求;
- 重复撤回不改写首次时间;
- 重复 Owner Step 不重复通知或删除。
只用 Mock 顺序调用“查询后插入”无法证明并发幂等。
5. SAR 完整性测试
为每个 Contributor 准备黄金数据集,验证:
- 0、1、1000、1001 和多页记录;
- 稳定游标跨页无重复、无缺失;
- Manifest 中 Owner 数等于冻结清单;
- 每个 Artifact Hash 与实际文件一致;
- 被保留/排除的数据有原因码;
- Schema Version 可由旧客户端识别;
- 生成失败不发布下载票据;
- 票据一次消费、过期、跨主体和跨租户均拒绝。
当前实现会在 1001 条审计记录时静默漏掉后续页,这应作为现状回归测试固定,直到修复。
6. 擦除证据测试
多 Owner 场景需要故障注入:
| 故障 | 期望 |
|---|---|
| Owner A 成功、B 暂时失败 | 请求保持 Partial/Retrying,不得 Completed |
| Legal Hold | 对应数据 Retained,保存原因与期限 |
| 外部 Processor 超时 | 幂等重试并升级,不重复成功 Step |
| Identity 用户不存在 | 按策略记录 NoData 或错误,不泄露跨租户存在性 |
| 提交前进程终止 | 重启后从持久化 Checkpoint 恢复 |
| Evidence Store 不可用 | 高价值执行失败关闭 |
7. 权限与 Feature 测试
HTTP 集成测试应从实际 Permission Catalog 创建角色,再调用所有九条路由。当前最需要捕获的是 privacy.*.manage 与实际 *.update/delete 不一致。
还要覆盖:
- 无 Token 返回 401;
- 已认证无决策 Allow 返回 403;
- 本人权限不允许处理他人请求;
- Operator 权限不能跨 Effective Tenant;
- Feature 关闭时自助、运营和后台路径行为一致;
admin角色名不能替代必要权限。
8. 指标
建议按 Tenant Hash、RequestType、Status 和 OwnerCode 聚合,禁止把 UserId、RequestId 原值放入指标标签。
| 指标 | 用途 |
|---|---|
gdpr_requests_created_total | 请求流量与类型 |
gdpr_requests_in_state | Submitted/CoolingOff/Partial 等积压 |
gdpr_request_age_seconds | 最老未完成请求 |
gdpr_owner_step_duration_seconds | Owner 延迟分布 |
gdpr_owner_step_failures_total | 可重试/永久失败 |
gdpr_export_bytes | 包大小与容量 |
gdpr_download_ticket_rejections_total | 票据攻击与客户端错误 |
gdpr_erasure_retained_total | 法律保全/保留裁决 |
当前模块没有专属指标。
9. SLO 与告警
法定时限由组织配置,不应硬编码成本文数字。工程 SLO 可以定义为法定截止前的内部预算:
- 未确认请求超过接收预算;
- SAR/Erasure 距截止时间不足安全缓冲;
- CoolingOff 已结束但没有进入审批队列;
- Partial Step 重试次数或年龄超阈值;
- 导出文件已到期但对象仍存在;
- 同一请求出现两个 Completed Evidence;
- 权限拒绝率突然变化;
- Unavailable Port 或 Evidence Store Readiness 失败。
告警通知中只使用内部 RecordId 和 CorrelationId,不包含主体姓名、邮箱或导出 URL。
10. 排障路径
在当前实现中若 SAR API 返回 ProcessingFailed 而数据库仍是 Submitted,先检查事务回滚语义,不要手工把状态直接改成 Completed。
11. 发布演练
每次商业发布前至少执行:
- 双 ORM 各完成一份多页 SAR;
- 使用错误类型 ID 攻击三个处理端点;
- 在冷静期前一毫秒、等于截止和后一毫秒执行;
- 模拟 Owner 部分失败、数据库死锁、对象存储超时;
- 下载票据重放与跨租户访问;
- 删除到期包并从对象存储核验;
- 模拟 operate-as 与 Host Caller;
- 恢复备份,验证过期导出不会重新暴露;
- 从证据重建一次请求时间线;
- 由安全、运维、法务共同签署结果。
12. GA 阻断门禁
下列任一项未完成,GDPR 模块不得宣称生产完整:
- 所有处理器验证 RequestType 和集中状态迁移;
- RequestId 唯一约束与并发测试通过;
- 权限目录、实际决策码和 Feature 行为一致;
- Actor、Effective Tenant、Subject 分离;
- SAR Contributor、全量分页、Manifest、加密文件和下载票据落地;
- Erasure Owner、Legal Hold、Checkpoint 和外部 Processor 证据落地;
- 失败状态可持久化,不被 Result 回滚吞掉;
- Identity 关联凭据/会话策略完成;
- 数据分类、保留、销毁和备份策略批准;
- 双 ORM、HTTP、安全、故障恢复和性能测试通过;
- 指标、告警、Runbook 和值班责任人就绪;
- 软件行为与组织法律流程完成联合验收。
13. 本地校验
# 运行现有 GDPR 查询、架构和双 ORM 契约测试。dotnet test tests/BitzOrcas.Application.Tests \ --filter FullyQualifiedName~Gdprdotnet test tests/BitzOrcas.Architecture.Tests \ --filter FullyQualifiedName~Gdprdotnet test tests/BitzOrcas.Integration.Tests \ --filter FullyQualifiedName~GdprStore_Should_Behave
# 全局暴露尚未形成实现的生产合同;修复前预期多数无命中。rg -n "IDataSubjectExportContributor|IDataErasureContributor|LegalHold|ExportManifest" \ src tests -g '*.cs'