The goal is not to prove that an endpoint returns 200. GDPR tests must prove that requests cannot cross tenants or state machines, that no owner is omitted, that incomplete exports never report completion, and that partial failure can recover safely.
1. Existing evidence
| Test | Proven | Not proven |
|---|---|---|
GdprQueryHandlerTests | consent/SAR lists call IGdprStore with actor tenant/user | route, permission, effective tenant, detail safety |
GdprInfrastructureArchitectureTests | no concrete ORM dependency; fail-closed registration | runtime database and compliance semantics |
PortRepositoryParityTests | basic consent, DSR update, and SysUser anonymization match across ORMs | commands, transitions, concurrency, rollback, related data |
| API Shell smoke | unavailable port without database | production readiness |
| ownership architecture tests | two tables are GDPR owner-local | classification and retention |
There are no GDPR command unit tests, HTTP authorization tests, export-file tests, or multi-owner erasure tests.
2. State-machine tests
Use table-driven contracts for every allowed and rejected source status.
[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){ // Every source status has an explicit expectation, including future enum additions. var result = ErasureTransitions.TryExecute( ErasureStatus.Parse(from), coolingOffExpired: true);
// An allowed path also has exactly one expected target status. result.IsSuccess.ShouldBe(expectedAllowed); if (expectedAllowed) result.Value.ShouldBe(ErasureStatus.Parse(expectedTo!));}ErasureTransitions is a target type. Also pass SAR IDs to every erasure handler and erasure IDs to every SAR handler.
3. Tenant and subject tests
Construct four independent values:
- Actor Tenant;
- Effective Tenant;
- Subject Tenant;
- stored row Tenant.
using var tenantScope = tenantAccessor.Push( effectiveTenantId: "tenant-b", actorTenantId: "tenant-host");
// The operator acts for the target tenant, but the subject belongs to that tenant.var result = await mediator.Send( new CreateSarForSubject.Command( SubjectUserId: "user-b", RequestId: "sar-b-001"), cancellationToken);
result.IsSuccess.ShouldBeTrue();// Assert data ownership, subject identity, and actor evidence independently.stored.TenantId.ShouldBe("tenant-b");stored.UserId.ShouldBe("user-b");stored.ActorTenantId.ShouldBe("tenant-host");If self-service does not support operate-as, reject it explicitly rather than writing mixed context.
4. Idempotency and concurrency
Run concurrent requests against a real database and prove:
- one row for the same tenant/type/RequestId;
- an explicit namespace policy across request types;
- the same RequestId is valid in different tenants;
- two operators cannot complete one request;
- repeated withdrawal preserves the first time;
- repeated owner steps do not duplicate deletion or notification.
Sequential mock tests cannot prove query-then-insert idempotency.
5. SAR completeness
Build a golden dataset for every contributor:
- 0, 1, 1,000, 1,001, and multi-page records;
- stable cursors without duplicates or gaps;
- owner count matching the frozen manifest;
- artifact hashes matching files;
- omission reasons for retained data;
- compatible schema versions;
- no ticket when generation fails;
- one-time, expired, foreign-subject, and foreign-tenant ticket rejection.
The current implementation silently omits audit data after item 1,000. Preserve that as a failing contract until corrected.
6. Erasure evidence
Use fault injection across owners:
| Fault | Required outcome |
|---|---|
| owner A succeeds, B temporarily fails | Partial/Retrying, never Completed |
| legal hold | Retained with reason and deadline |
| external processor times out | idempotent retry and escalation |
| Identity user absent | policy outcome without cross-tenant disclosure |
| process ends before commit | resume from durable checkpoint |
| evidence store unavailable | high-value action fails closed |
7. Authorization and feature tests
HTTP tests should create roles from the actual Permission Catalog and call all nine routes. They must catch the current manage versus update/delete mismatch.
Cover:
- no token returns 401;
- authenticated without an Allow returns 403;
- self permission cannot process another subject;
- operator permission cannot cross Effective Tenant;
- feature off behaves consistently for HTTP, background, and operator paths;
- role name
admindoes not replace required permission.
8. Metrics
Aggregate by tenant hash, request type, status, and owner code. Never put raw UserId or RequestId in labels.
| Metric | Purpose |
|---|---|
gdpr_requests_created_total | intake by type |
gdpr_requests_in_state | work backlog |
gdpr_request_age_seconds | oldest open request |
gdpr_owner_step_duration_seconds | owner latency |
gdpr_owner_step_failures_total | retryable/permanent failure |
gdpr_export_bytes | package capacity |
gdpr_download_ticket_rejections_total | attack/client failure |
gdpr_erasure_retained_total | legal hold/retention |
The current module emits no dedicated metrics.
9. SLOs and alerts
Legal deadlines belong in organization-approved configuration. Engineering SLOs should reserve a safety margin:
- request not acknowledged within intake budget;
- SAR/erasure approaching the configured deadline;
- expired CoolingOff without operator action;
- old or over-retried Partial steps;
- expired export object still present;
- two Completed evidence records for one request;
- sudden authorization-denial change;
- unavailable port or evidence-store readiness failure.
Notifications use internal RecordId and CorrelationId, not names, emails, or export URLs.
10. Triage
If ProcessSar returns ProcessingFailed while the row remains Submitted, inspect transaction rollback before changing the row manually.
11. Release drills
Before each commercial release:
- complete a multi-page SAR on both ORMs;
- attack each processing endpoint with a wrong-type ID;
- execute just before, exactly at, and after cooling-off;
- inject owner failure, database deadlock, and object-store timeout;
- replay a ticket and attempt cross-tenant download;
- expire and physically verify package deletion;
- exercise operate-as and Host callers;
- restore a backup and ensure expired exports do not reappear;
- reconstruct one request timeline from evidence;
- obtain security, operations, and privacy-owner sign-off.
12. GA blocking gate
- Every handler checks RequestType and a central transition guard.
- RequestId uniqueness and concurrency tests pass.
- Catalog, runtime decisions, and feature behavior agree.
- Actor, Effective Tenant, and Subject are separate.
- SAR contributors, full paging, manifest, encrypted file, and ticket exist.
- Erasure owners, legal hold, checkpoints, and processor evidence exist.
- Failed state persists instead of disappearing on Result rollback.
- Identity credential/session policy is complete.
- Classification, retention, destruction, and backup policy are approved.
- Dual-provider, HTTP, security, recovery, and performance tests pass.
- Metrics, alerts, runbook, and on-call ownership are ready.
- Software behavior and organizational legal process pass joint acceptance.
13. Local verification
# Run existing GDPR query, architecture, and provider contracts.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
# Expose production contracts that are not yet implemented.rg -n "IDataSubjectExportContributor|IDataErasureContributor|LegalHold|ExportManifest" \ src tests -g '*.cs'