Comments is small in lines of code, not in blast radius. A free-form subject key can cross business boundaries, bodies contain user-generated content, soft deletion must preserve a tree, and migration clears legacy data. GA acceptance must center on resource access, tenant consistency, and recoverability.
1. Current test assets
The current repository contains:
- one Application query test covering store delegation, reply projection, and a deleted placeholder;
- three Comments Infrastructure architecture tests for ORM neutrality, store usage by the query handler, and fail-closed persistence composition;
- one dual-ORM PortRepositoryParity scenario covering Insert, Update, SoftDelete, and IncludeDeleted;
- API-shell build smoke resolution for ICommentStore;
- persistence-metadata registration coverage for CommentThreadSupportRecord.
There are no dedicated tests for Create, Update, Delete, migration, generic permissions, HTTP contracts, concurrent idempotency, owner authorization, deep trees, or tenant impersonation.
2. Test layers
Handler unit tests
- Create required inputs/10k, existing result, missing/deleted parent, and AuthorId;
- Update author/admin/window boundary/trim/EditedAt;
- Delete author/admin/repeated deletion;
- GetComments ordering, orphan, deleted placeholder, and cycle protection;
- Migration counts, corrupt JSON, replay, failure, and cancellation.
Persistence and contract tests
- unique index, soft delete, include-deleted, and tenant filter in SqlSugar and EF Core;
- one row and one consistent result from concurrent first requests;
- all four use cases where User.TenantId differs from EffectiveTenantId;
- every route, HTTP method, Problem Details shape, timeout, and rate-limit choice;
- Documents/Tickets owner policy for view/create/update/delete;
- migration source/target reconciliation and actual transaction scope.
3. Subject-authorization red test
[Fact]public async Task Create_Should_Reject_When_Subject_Is_Not_Visible(){ // The caller may create comments generally but cannot see this private Ticket. using var client = fixture.CreateClient( tenantId: "tenant-a", userId: "outsider", permissions: ["comments.comment.create"]); await fixture.SeedPrivateTicketAsync( "tenant-a", "ticket-42", ownerId: "owner");
// Free-form EntityType/EntityId must not bypass the Ticket-owned 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);}The current implementation creates the row after generic permission succeeds, so this is intentionally a red test until subject policy exists.
4. Tenant-consistency contract
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");
// Lookup and insert must both use tenant-target rather than mixing tenant sources.var first = await scope.Mediator.Send(command);var replay = await scope.Mediator.Send(command);
// Idempotent replay must return the same physical comment.first.Value!.Id.ShouldBe(replay.Value!.Id);// Prove that the operator's home tenant received no accidental write.(await scope.Comments.CountAsync("tenant-target")).ShouldBe(1);(await scope.Comments.CountAsync("operator-home")).ShouldBe(0);Apply the same tenant matrix to Get, Update, Delete, and Ticket migration.
5. Observability without leaking content
Do not put body, author id, or raw EntityId into normal logs or high-cardinality metrics. Useful dimensions include:
| Dimension | Example values |
|---|---|
| use_case | create/get/update/delete/migrate |
| result | success/validation/forbidden/not_found/conflict/dependency |
| entity_type | low-cardinality canonical registry code |
| tenant_hash | irreversible short hash |
| actor_type | user/client/moderator |
| operation_kind | root/reply/edit/author_delete/moderator_delete |
Track latency/result, owner-policy denials, idempotency replay/conflict, thread size/depth, body-length buckets, soft-delete rate, migration remainder/failure/reconciliation delta, database unique conflicts, and sanitizer rejections.
6. Alerts and incident response
| Signal | Likely cause | First response |
|---|---|---|
| sudden comment growth on one subject | spam or absent owner check | rate-limit/disable Create; inspect actor and subject |
| unique conflicts rise | concurrent retries or tenant-source split | inspect CommentId and User/EffectiveTenant |
| GET P95/response size rises | hot unpaged thread | throttle subject; enable paging/depth limits |
| StackOverflow/500 | cycle or extreme reply chain | isolate subject; export and repair ParentCommentId |
| non-member read a thread | absent/stale owner policy | block GET, audit access, invalidate cache |
| source cleared but target missing | tenant mismatch/transaction error | stop migration, restore backup, reconcile hashes |
| XSS report | raw UI rendering | disable rich rendering; sanitize and inspect history |
7. Capacity and pagination
One current GET loads and builds the entire thread. Capacity tests should cover:
- 1, 10, 100, 1,000, and 10,000 comments;
- a 10,000-deep chain and one root with 10,000 children;
- 50% soft-deleted placeholders;
- 10k-character bodies with response compression;
- concurrent read/write on one hot subject;
- multi-tenant execution plans for subject, parent, and author indexes.
A target API can cursor-page root comments, include only the first N replies plus replyCount, and page children by ParentCommentId. Use CreateTime + CommentId for deterministic order.
8. Reconciliation checks
Run read-only checks for:
- EntityType absent from the registry;
- owner resource missing for EntityId;
- ParentCommentId crossing a subject/tenant or forming a cycle;
- IsDeleted=true with a non-null Body;
- AuthorId=
0; - body/id values beyond the contract or suspicious Unicode;
- duplicate CommentId within a subject;
- non-
[]Ticket.CommentsJson after migration; - CommentId/Body-hash mismatches between the Ticket source and SysComment.
9. GA release blockers
- EntityType uses a registry; unknown values fail closed and casing is canonical.
- Every Get/Create/Update/Delete calls the subject owner’s policy.
- Documents.AllowComments and Ticket visibility/state rules actually execute.
- User.TenantId and EffectiveTenantId are unified with impersonation/background contract tests.
- Create has an explicit ActorType/ActorId and no longer writes AuthorId=
0. - The admin string becomes a moderator capability/policy with audit evidence.
- Concurrent idempotency normalizes unique races and defines soft-delete replay.
- Application validates identifier and body limits before persistence.
- GET has pagination, depth/total limits, cycle protection, and stable ordering.
- Rich-text/Markdown output uses a safe renderer and sanitizer.
- Events, notifications, and moderation are implemented or explicitly excluded from product promises.
- Ticket migration tenant, transaction, paging, corrupt-data, reporting, and reconciliation defects are fixed.
- The long-lived Tickets.Infrastructure dependency is removed.
- Handler, HTTP, permission, owner, dual-ORM, concurrency, capacity, and restore tests pass.
10. Verification commands
# Current Comments-focused tests.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'
# Sweep for a subject policy and for the current tenant/author split.rg -n 'EnsureCanComment|ICommentSubjectPolicy|AllowComments' src/Platform -g '*.cs'rg -n 'User.TenantId|EffectiveTenantId|AdminRole|\?\? "0"' \ src/Platform/Comments -g '*.cs'