Comments 是小模块,但安全半径不小:自由资源键可以跨业务挂载,正文是用户生成内容,软删除需要保持线程,迁移还会清空旧数据。GA 验收应以访问控制、租户一致性和可恢复性为核心。
1. 当前测试资产
源码中能审计到:
- 1 个 Application 查询测试:验证 store 委派、回复树和删除占位;
- 3 个 Comments Infrastructure 架构测试:ORM 中立、查询 handler 使用 store、fail-closed 持久化端口;
- 1 个双 ORM PortRepositoryParity 场景:Insert、Update、SoftDelete、IncludeDeleted;
- API Shell build smoke 能解析 ICommentStore;
- 持久化元数据注册测试覆盖 CommentThreadSupportRecord。
没有专门覆盖 Create、Update、Delete、迁移、通用权限、HTTP 契约、并发幂等、目标 owner 授权、深树或租户模拟的测试。
2. 测试层级
Handler 单元测试
- Create 的非空/10k、已有结果、父缺失/删除、AuthorId;
- Update 的作者/admin/窗口边界/trim/EditedAt;
- Delete 的作者/admin/重复删除;
- GetComments 的排序、孤儿、删除占位、环保护;
- Migration 的计数、坏 JSON、重复、失败和取消。
持久化与契约测试
- SqlSugar/EF Core 的 unique index、soft delete、include deleted 和 tenant filter;
- CommentId 并发首请求只产生一行并返回一致结果;
- User.TenantId != EffectiveTenantId 的四个用例;
- 每个 HTTP 路由、方法、Problem Details、timeout/rate limit;
- owner policy 对 Documents/Tickets 的 view/create/update/delete;
- 迁移 source/target 对账与事务范围。
3. 目标资源授权测试
[Fact]public async Task Create_Should_Reject_When_Subject_Is_Not_Visible(){ // Arrange:调用者有 Comments Create,但不在目标 Ticket 的可见范围。 using var client = fixture.CreateClient( tenantId: "tenant-a", userId: "outsider", permissions: ["comments.comment.create"]); await fixture.SeedPrivateTicketAsync("tenant-a", "ticket-42", ownerId: "owner");
// Act:自由 EntityType/EntityId 不能绕过 Ticket owner policy。 using var response = await client.PostAsJsonAsync( "/api/comments/", new { commentId = Guid.NewGuid(), entityType = "Ticket", entityId = "ticket-42", body = "probe" });
response.StatusCode.ShouldBe(HttpStatusCode.Forbidden); (await fixture.CountCommentsAsync("tenant-a", "Ticket", "ticket-42")).ShouldBe(0);}当前实现会在通用权限通过后创建记录,所以这是资源策略落地的 red test。
4. 租户一致性测试
await using var scope = fixture.CreateScope( userTenantId: "operator-home", effectiveTenantId: "tenant-target");
var command = new CreateComment.Command( operationId, "Document", "doc-42", null, "tenant-scoped comment");
// 两次调用必须都在 tenant-target 查询和写入,而不是混用 userTenantId。var first = await scope.Mediator.Send(command);var replay = await scope.Mediator.Send(command);
// 幂等重放必须返回同一物理评论。first.Value!.Id.ShouldBe(replay.Value!.Id);// 同时证明 home tenant 没有被意外写入。(await scope.Comments.CountAsync("tenant-target")).ShouldBe(1);(await scope.Comments.CountAsync("operator-home")).ShouldBe(0);同一测试矩阵要覆盖 Get、Update、Delete 和 Ticket migration。
5. 可观测性
正文、作者 Id、原始 EntityId 不应进入高基数 metric 或普通日志。推荐字段:
| 字段 | 示例 |
|---|---|
| use_case | create/get/update/delete/migrate |
| result | success/validation/forbidden/not_found/conflict/dependency |
| entity_type | 注册表中的低基数 canonical code |
| tenant_hash | 不可逆短哈希 |
| actor_type | user/client/moderator |
| operation_kind | top_level/reply/edit/author_delete/moderator_delete |
关键指标:请求耗时/结果、owner policy deny、幂等 replay/conflict、线程记录数/深度、Body 长度、软删除率、迁移剩余/失败/对账差异、数据库唯一冲突和 sanitizer 拒绝数。
6. 告警与事故处置
| 信号 | 可能原因 | 首轮处置 |
|---|---|---|
| 同资源评论量异常上升 | 垃圾请求、无 owner 校验 | 限流/关闭 Create,核对 actor 与 subject |
| 唯一冲突增加 | 并发重试、模拟租户幂等分裂 | 检查 CommentId、User/EffectiveTenant |
| GET P95/响应大小上升 | 热点长线程、无分页 | 对资源临时限流,启用分页/深度限制 |
| StackOverflow/500 | 环或极深回复链 | 下线该 subject,导出并修复 ParentCommentId |
| 非成员读到评论 | owner policy 缺失/缓存过期 | 网关封禁 Get,审计访问,撤销缓存 |
| 迁移后源清空目标缺失 | tenant mismatch/事务错误 | 停迁移、从备份恢复、按哈希对账 |
| XSS 报告 | UI raw render | 立即禁用富文本渲染,清洗输出并排查历史正文 |
7. 容量与分页
当前一个 GET 读取资源全部评论并构建整树。生产容量测试至少覆盖:
- 1/10/100/1000/10000 条评论;
- 10000 层链与宽度 10000 的单根;
- 50% 软删除占位;
- 10k Body 的响应与压缩;
- 同一热点 subject 并发读写;
- Entity/Parent/Author 索引在多租户大表上的执行计划。
目标 API 可采用根评论游标分页,响应每根前 N 条回复与 replyCount,再提供按 ParentCommentId 的分页端点。稳定排序使用 CreateTime + CommentId。
8. 数据对账
日常只读检查:
- EntityType 不在 registry;
- EntityId 对应 owner 资源不存在;
- ParentCommentId 指向另一 subject/tenant 或形成环;
- IsDeleted=true 但 Body 非 null;
- AuthorId=
0; - Body/Id 超过契约或异常 Unicode;
- 同 subject 的重复 CommentId;
- 迁移后 Ticket.CommentsJson 非
[]; - Ticket 源评论与 SysComment 的 CommentId/Body 哈希差异。
9. GA 阻断项
- EntityType 使用 registry,未知值失败关闭并规范化大小写;
- 所有 Get/Create/Update/Delete 调用目标 owner policy;
- Documents.AllowComments 与 Ticket 状态/可见性真实生效;
- User.TenantId/EffectiveTenantId 统一,模拟与后台场景有契约测试;
- Create 要求明确 ActorType/ActorId,不再写 AuthorId=
0; - admin 字符串替换为 moderator 权限/策略与审计;
- 并发幂等冲突归一,软删除后键语义明确;
- 标识/Body 长度在 Application 提前验证;
- GET 有分页、深度/总量限制、环保护和稳定排序;
- 富文本/Markdown 输出经过安全 renderer/sanitizer;
- 创建/编辑/删除的事件、通知和审核明确实现或明确不承诺;
- Ticket migration 的租户、事务、分页、坏数据、报告和对账修复;
- 移除不必要的 Tickets.Infrastructure 长期依赖;
- Handler、HTTP、权限、owner、双 ORM、并发、容量、备份恢复测试通过。
10. 验证命令
# 当前 Comments 相关测试。dotnet test tests/BitzOrcas.Application.Tests/BitzOrcas.Application.Tests.csproj \ --filter 'FullyQualifiedName~Comments'dotnet test tests/BitzOrcas.Architecture.Tests/BitzOrcas.Architecture.Tests.csproj \ --filter 'FullyQualifiedName~CommentsInfrastructureArchitectureTests'dotnet test tests/BitzOrcas.Integration.Tests/BitzOrcas.Integration.Tests.csproj \ --filter 'FullyQualifiedName~CommentStore_Should_Behave'
# 全局扫出仍缺失的目标策略和租户分裂。rg -n 'EnsureCanComment|ICommentSubjectPolicy|AllowComments' src/Platform -g '*.cs'rg -n 'User.TenantId|EffectiveTenantId|AdminRole|\?\? "0"' \ src/Platform/Comments -g '*.cs'