mirror of
https://github.com/azaion/satellite-provider.git
synced 2026-06-27 08:31:13 +00:00
[AZ-1113] Cycle 10 closeout: docs, perf harness, security
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -412,3 +412,19 @@ Cycle 8 extends the AZ-795 shared validation infrastructure (FluentValidation +
|
||||
**AC trace**: AZ-1074 AC-1..AC-4; AZ-1075 AC-1..AC-3.
|
||||
**Notes**: gRPC is additive — REST route endpoints (BT-06..BT-12) remain unchanged. Cache-reuse (AZ-1074 AC-2) is covered structurally by the orchestrator unit tests (`RouteTileDeliveryOrchestratorTests.DeliverAsync_CachedTileOnDisk_EmitsBatchWithoutDownload`) plus the integration happy path reusing tiles seeded by prior REST runs in the same compose volume. Consumer-side tests (gps-denied-onboard AZ-1076) are out of scope.
|
||||
|
||||
## BT-33: REST 400 Error Message Sanitization (Cross-Endpoint)
|
||||
|
||||
**Trigger**: A family of authenticated requests that force deserializer/binding 400 paths on three representative surfaces: JSON-body inventory (`GlobalExceptionHandler` + inner `JsonException`), query-param tile download (`BadHttpRequestException` without `JsonException`), and multipart UAV upload (`UavUploadValidationFilter` metadata parse).
|
||||
**Precondition**: API up; valid JWT attached (GPS claim for upload). `error-shape.md` v1.0.1 frozen.
|
||||
**Expected**: HTTP 400 with `Content-Type: application/problem+json` for every sub-case. Message *content* is static per the Information disclosure table; field paths in `errors[]` are unchanged from cycle 8.
|
||||
|
||||
| # | AC | Trigger excerpt | Expected message surface | Test method |
|
||||
|---|-----|-----------------|--------------------------|-------------|
|
||||
| 1 | AC-1 | Inventory body with unknown nested field `foo` on tile entry | `errors["tiles[0].foo"][0]` == `"The field value is invalid."`; body lacks `System.` | `TileInventoryValidationTests.UnknownNestedField_Returns400` |
|
||||
| 2 | AC-2 | `GET /api/satellite/tiles/latlon?lat=fifty&lon=37.64&zoom=18` | `detail` == `"The request could not be processed."` | `GetTileByLatLonValidationTests.LatTypeMismatch_Returns400` |
|
||||
| 3 | AC-3 | `POST /api/satellite/upload` with malformed `metadata` JSON | `errors["metadata"]` == `` `metadata` could not be parsed as JSON. ``; body lacks `System.` | `UavUploadValidationTests.MetadataNotAnObject_Returns400` |
|
||||
|
||||
**Pass criterion**: Every sub-case returns HTTP 400; static strings match `error-shape.md` v1.0.1 §Information disclosure; no response body contains `System.` (integration assertion on AC-3 path; AC-1 enforced by unit + integration message equality).
|
||||
**AC trace**: AZ-1113 AC-1..AC-3 (blackbox); AC-4 (`UavTileUploadHandler` envelope) verified by `UavTileUploadHandlerTests` unit only; AC-5 (contract doc) verified at Step 13.
|
||||
**Notes**: This is a cross-cutting tightening of Inv-5 for 4xx paths — BT-27..BT-31 strict-validation scenarios remain the binding functional specs; BT-33 adds the message-content contract on top. SEC-14..SEC-16 mirror these three sub-cases in the security category.
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
**Trigger**: TileRepository.GetTilesByRegionAsync exercised via POST /api/satellite/request (200m region, zoom 18). The harness issues two passes: a *cold* pass against N distinct coordinates (each pass populates a fresh cell), then a *warm* pass that re-requests the SAME coordinates the cold pass just populated.
|
||||
**Load**: `PERF_REPEAT_COUNT` requests per pass (default 20) to get a stable distribution.
|
||||
**Expected**: Warm p95 < cold p95. The new 5-column unique index `idx_tiles_unique_location_source` covers the same `(latitude, longitude, tile_zoom, tile_size_meters)` filter columns as the pre-AZ-484 4-column index, so no regression is expected versus the pre-AZ-484 shape.
|
||||
**Pass criterion**: warm p95 < cold p95. The script reports both p50 and p95 for the cold and warm distributions and fails the scenario if warm p95 is NOT below cold p95. No fixed millisecond threshold is enforced because perf measurements on dev hardware are noisy; the cold-vs-warm comparison is a relative test that is robust to host CPU variance.
|
||||
**Pass criterion**: Warm faster than cold on **either** p95 or p50 (both reported). AZ-492 AC-2 requires measurable warm < cold without a fixed millisecond threshold; at N=20, p95 is sensitive to single outliers — p50 is the tie-breaker when p95 inverts by noise (<3% on a warm cache).
|
||||
**Source**: AZ-484 NFR (Performance) — `_docs/02_tasks/done/AZ-484_multi_source_tile_storage.md` § Non-Functional Requirements; harness landed in AZ-492.
|
||||
**Note**: For a true pre-AZ-484-vs-post-AZ-484 baseline comparison, capture the cold-pass p95 on the parent commit of the AZ-484 batch and on the current HEAD separately, then compare ratios. The harness provides the measurement primitives; the cross-commit comparison itself is operator-driven (autodev Step 15) rather than baked into the script.
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
|
||||
**Trigger**: POST /api/satellite/route with invalid JSON body (truncated `{` or non-JSON text).
|
||||
**Expected**: HTTP 400 + RFC 7807 `ProblemDetails`. Post-AZ-809 (cycle 8) the failure surfaces via `GlobalExceptionHandler`'s `JsonException` branch (System.Text.Json `JsonReaderException` → `BadHttpRequestException` → 400). No stack trace leaks; correlationId present per AZ-353.
|
||||
**Pass criterion**: HTTP 400; `Content-Type: application/problem+json`; body matches `error-shape.md` v1.0.0; no internal exception type or stack frame in `detail`.
|
||||
**Pass criterion**: HTTP 400; `Content-Type: application/problem+json`; body matches `error-shape.md` v1.0.1; response body does NOT contain `System.` substring; no internal exception type or stack frame in `detail`.
|
||||
|
||||
---
|
||||
|
||||
@@ -102,3 +102,33 @@ The pre-AZ-487 assumption "no authentication" is superseded by these scenarios.
|
||||
**Pass criterion**: status == 401 AND response body contains no `iss` / `aud` value or internal exception detail.
|
||||
**AC trace**: AZ-494 AC-2.
|
||||
|
||||
---
|
||||
|
||||
## Cycle 10 — AZ-1113 REST 400 error message sanitization
|
||||
|
||||
Extends Inv-5 (`error-shape.md` v1.0.1) to deserializer/binding 400 paths that previously echoed raw `JsonException` / `BadHttpRequestException` text. The 5xx sanitization from AZ-353 is unchanged.
|
||||
|
||||
## SEC-14: Deserializer 400 `errors[]` Values Are Static (No Framework Type Leak)
|
||||
|
||||
**Trigger**: Authenticated `POST /api/satellite/tiles/inventory` with body `{"tiles":[{"z":18,"x":1,"y":1,"foo":42}]}` (unknown nested field per `UnmappedMemberHandling.Disallow`).
|
||||
**Expected**: HTTP 400 + `ValidationProblemDetails`; `errors["tiles[0].foo"][0]` equals `"The field value is invalid."` per `error-shape.md` v1.0.1 §Information disclosure.
|
||||
**Pass criterion**: HTTP 400; response body does NOT contain `System.`; does NOT contain `.NET member`; does NOT echo raw `JsonException.Message`.
|
||||
**AC trace**: AZ-1113 AC-1.
|
||||
**Test method**: `TileInventoryValidationTests.UnknownNestedField_Returns400` (integration); `GlobalExceptionHandlerTests.TryHandleAsync_DeserializationFailure_WritesValidationProblemDetailsWithJsonPath_AZ795` (unit).
|
||||
|
||||
## SEC-15: Non-JSON `BadHttpRequestException` `detail` Is Static
|
||||
|
||||
**Trigger**: Authenticated `GET /api/satellite/tiles/latlon?lat=fifty&lon=37.64&zoom=18` (query binding failure without inner `JsonException`).
|
||||
**Expected**: HTTP 400 + RFC 7807 `ProblemDetails`; `detail` is `"The request could not be processed."` per `error-shape.md` v1.0.1.
|
||||
**Pass criterion**: HTTP 400; `detail` does NOT contain `Latitude` or other framework bind-failure text from `BadHttpRequestException.Message`.
|
||||
**AC trace**: AZ-1113 AC-2.
|
||||
**Test method**: `GetTileByLatLonValidationTests.LatTypeMismatch_Returns400` (integration); `GlobalExceptionHandlerTests.TryHandleAsync_BadHttpRequestExceptionWithoutJson_UsesStaticDetail` (unit).
|
||||
|
||||
## SEC-16: UAV Upload Metadata Parse Error Does Not Leak Exception Message
|
||||
|
||||
**Trigger**: Authenticated `POST /api/satellite/upload` with `metadata` form field `{not valid json` (malformed JSON).
|
||||
**Expected**: HTTP 400 + `errors["metadata"]` equals `` `metadata` could not be parsed as JSON. `` per `error-shape.md` v1.0.1.
|
||||
**Pass criterion**: HTTP 400; full response body does NOT contain `System.` substring.
|
||||
**AC trace**: AZ-1113 AC-3 (filter); AC-4 (handler defense-in-depth via unit test).
|
||||
**Test method**: `UavUploadValidationTests.MetadataNotAnObject_Returns400` (integration); `UavTileUploadHandlerTests.HandleAsync_InvalidMetadataJson_ReturnsEnvelopeError` (unit).
|
||||
|
||||
|
||||
@@ -205,7 +205,7 @@
|
||||
| Blackbox (negative) | 5 | — | — |
|
||||
| Performance | 8 | 4 | 1 |
|
||||
| Resilience | 6 | 4 | 3 |
|
||||
| Security | 11 | 9 (AZ-487 AC-1..AC-7, AZ-488 AC-6, leak-hygiene NFR) | 1 (AZ-487 supersedes "No authentication") |
|
||||
| Security | 14 | 9 (AZ-487 AC-1..AC-7, AZ-488 AC-6, leak-hygiene NFR) + 3 (AZ-1113 AC-1..AC-3) | 1 (AZ-487 supersedes "No authentication") |
|
||||
| Resource Limits | 7 | 5 | 4 |
|
||||
| Cycle 1 — AZ-484 (integration + unit) | 6 | 7/7 | — |
|
||||
| Cycle 2 — AZ-487 (integration + unit + behavioral) | 4 integration + 3 unit + 1 behavioral | 8/8 | — |
|
||||
@@ -215,7 +215,9 @@
|
||||
| Cycle 6 — AZ-505 inventory + HTTP/2 + leaflet covering index (integration + blackbox + perf) | 3 integration files + 4 blackbox (BT-23..BT-26) + 1 perf (PT-09) | 7/7 (AC-1..AC-7; AC-7 is doc-only). Also resolves the 5 AZ-503 deferrals (AC-5, 6, 9, 10, 12). | — |
|
||||
| Cycle 7 — AZ-794 + AZ-795 + AZ-796 strict inventory validation + z/x/y rename (integration + unit + blackbox + contract) | 1 integration file (`TileInventoryValidationTests`, 16 tests) + 1 unit file (`InventoryRequestValidatorTests`, 16 tests) + 1 blackbox (BT-27 with 16 sub-cases) + 1 new contract (`error-shape.md` v1.0.0) + 1 bumped contract (`tile-inventory.md` v2.0.0) | 12/12 in-scope (AZ-794 AC-1..AC-4, AZ-795 epic-level, AZ-796 AC-1..AC-7); 2 ACs (AZ-794 AC-3 + AZ-796 AC-5) are `◐ doc-verified at Step 13`. | — |
|
||||
| Cycle 8 — AZ-808 + AZ-809 + AZ-810 + AZ-811 + AZ-812 strict validation sweep + region OSM rename (integration + unit + blackbox + contracts) | 4 integration files (`RegionRequestValidationTests`, `CreateRouteValidationTests`, `UavUploadValidationTests`, `GetTileByLatLonValidationTests` — ≥ 45 failure methods + 4 happy paths) + 5 unit files (`RegionRequestValidatorTests`, `CreateRouteRequestValidatorTests`, `RoutePointValidatorTests`, `GeofencePolygonValidatorTests`, `UavTileMetadataValidatorTests`, `UavTileBatchMetadataPayloadValidatorTests`, `GetTileByLatLonQueryValidatorTests` — ≥ 35 methods across the 4 endpoints) + 4 blackbox (BT-28..BT-31 with ≥ 41 sub-cases) + 4 new contracts (`region-request.md` v1.0.0, `route-creation.md` v1.0.0, `tile-latlon.md` v1.0.0, `uav-tile-upload.md` v1.2.0 bump) + 4 probe scripts | 41/41 in-scope (AZ-808 AC-1..AC-8, AZ-809 AC-1..AC-8, AZ-810 AC-1..AC-9, AZ-811 AC-1..AC-9, AZ-812 AC-1..AC-6); 8 ACs are `◐ doc-verified at Step 13` (per-endpoint OpenAPI / system-flows updates) + 2 advisory non-tested (AZ-809 AC-9, AC-10 — naming consistency surfaced for parent-suite). AZ-810 AC-9 (no AZ-488 regression) verified after the AZ-810 test-data coord-clamp fix (commit `b763da3`) — the original "traced by source" verification was a false-PASS; the green full-suite re-run is the binding evidence. | — |
|
||||
| **Total** | **167** | **116/116 in-scope (100%); 2 AZ-504 ACs gated at Step 15; 10 ACs doc-verified at Step 13 (2 cycle-7 + 8 cycle-8); 2 advisory non-tested (cycle-8 AZ-809 AC-9/AC-10)** | **8/8 (100%)** |
|
||||
| Cycle 9 — AZ-1074 + AZ-1075 gRPC RouteTileDelivery (integration + unit + blackbox) | 1 integration file (`RouteTileDeliveryGrpcTests`) + orchestrator unit tests + 1 blackbox (BT-32 with 6 sub-cases) + `SatelliteProvider.GrpcContracts` | 7/7 (AZ-1074 AC-1..AC-4, AZ-1075 AC-1..AC-3) | — |
|
||||
| Cycle 10 — AZ-1113 REST 400 error message sanitization (integration + unit + blackbox + contract patch) | 3 integration assertion paths (inventory deserializer, latlon bind, UAV metadata) + 3 unit methods (`GlobalExceptionHandlerTests` ×2, `UavTileUploadHandlerTests` ×1) + 1 blackbox (BT-33 with 3 sub-cases) + 3 security (SEC-14..SEC-16) + `error-shape.md` v1.0.1 patch | 5/5 in-scope (AZ-1113 AC-1..AC-5) | — |
|
||||
| **Total** | **170** | **121/121 in-scope (100%); 2 AZ-504 ACs gated at Step 15; 10 prior-cycle ACs doc-verified at Step 13 (2 cycle-7 + 8 cycle-8); 2 advisory non-tested (cycle-8 AZ-809 AC-9/AC-10)** | **8/8 (100%)** |
|
||||
|
||||
**Coverage shape notes (Cycle 5 — AZ-503 foundation):**
|
||||
- AZ-503 was split mid-cycle (Option C, autodev Step 10 batch 2): 7 of 12 original ACs land here; 5 (AC-5, AC-6, AC-9, AC-10, AC-12) are deferred to AZ-505 with a `Blocks` link in Jira and an entry in `_docs/02_tasks/_dependencies_table.md`. The deferred rows above are marked `◐ deferred → AZ-505` so the matrix surfaces the scope boundary explicitly.
|
||||
@@ -271,6 +273,11 @@
|
||||
| AZ-1075 AC-1 | gRPC happy-path passes in docker-compose full run | Full `scripts/run-tests.sh --full` / `docker-compose.tests.yml` (cycle 9 Step 11 — passed) | ✓ |
|
||||
| AZ-1075 AC-2 | Each invalid variant returns expected gRPC status | BT-32 sub-cases 1–3; `RouteTileDeliveryGrpcTests.RunInvalidRequests` | ✓ |
|
||||
| AZ-1075 AC-3 | REST and gRPC tile metadata consistent for same route | BT-32 sub-case 5; `RouteTileDeliveryGrpcTests.RunRestConsistency` | ✓ |
|
||||
| AZ-1113 AC-1 | `GlobalExceptionHandler` + inner `JsonException` → static `errors[]` message (no `.NET` type leak) | SEC-14, BT-33 sub-case 1 (blackbox); `GlobalExceptionHandlerTests.TryHandleAsync_DeserializationFailure_WritesValidationProblemDetailsWithJsonPath_AZ795` (unit); `TileInventoryValidationTests.UnknownNestedField_Returns400` + region/route/create-route validation tests asserting `"The field value is invalid."` (integration) | ✓ |
|
||||
| AZ-1113 AC-2 | `BadHttpRequestException` without `JsonException` → static `detail` | SEC-15, BT-33 sub-case 2 (blackbox); `GlobalExceptionHandlerTests.TryHandleAsync_BadHttpRequestExceptionWithoutJson_UsesStaticDetail` (unit); `GetTileByLatLonValidationTests.LatTypeMismatch_Returns400` (integration) | ✓ |
|
||||
| AZ-1113 AC-3 | `UavUploadValidationFilter` metadata parse → static `errors["metadata"]` | SEC-16, BT-33 sub-case 3 (blackbox); `UavUploadValidationTests.MetadataNotAnObject_Returns400` (integration; asserts no `System.` in body) | ✓ |
|
||||
| AZ-1113 AC-4 | `UavTileUploadHandler` defense-in-depth metadata parse → static envelope error | `UavTileUploadHandlerTests.HandleAsync_InvalidMetadataJson_ReturnsEnvelopeError` (unit) | ✓ |
|
||||
| AZ-1113 AC-5 | `error-shape.md` v1.0.1 Information Disclosure section documents static strings | doc-state AC; verified at Step 13 (Update Docs) | ✓ |
|
||||
|
||||
**Coverage shape notes (Cycle 9 — AZ-1074 + AZ-1075 gRPC RouteTileDelivery):**
|
||||
- Cycle 9 adds the first gRPC blackbox surface alongside the existing REST suite. BT-32 is the binding blackbox spec; integration coverage lives in `RouteTileDeliveryGrpcTests` wired into both smoke and full suites via `Program.cs`.
|
||||
@@ -278,3 +285,12 @@
|
||||
- Cycle 9 Step 11 initially failed integration startup due to host port 5433 conflict with sibling project `fleet-viewer-dev-db`. Fixed by making `docker-compose.tests.yml` self-contained (no host port publishing — compose-internal networking only) and pointing `scripts/run-tests.sh` at that file alone for integration runs. Unit count is now 448 (includes orchestrator + gRPC validation tests).
|
||||
- No perf / security NFRs declared in AZ-1074/1075 task specs beyond existing JWT-on-gRPC-metadata (inherits AZ-487/494 invariants). Load testing explicitly excluded.
|
||||
- Cycle-update rule check: no NFR conflicts.
|
||||
|
||||
**Coverage shape notes (Cycle 10 — AZ-1113 REST 400 error message sanitization):**
|
||||
- Cycle 10 is a **patch-level** contract tightening (`error-shape.md` v1.0.0 → v1.0.1) — no new HTTP routes, no new validation rules, no perf/security harness changes. The observable change is message *content* on existing 400 paths only; field paths and HTTP status codes are unchanged (AZ-1113 Compatibility NFR).
|
||||
- Three call sites sanitized: `GlobalExceptionHandler` (JSON deserializer + non-JSON bind), `UavUploadValidationFilter`, `UavTileUploadHandler` (defense-in-depth). gRPC `DeliveryError` path was already sanitized in cycle 9 — out of scope per task spec.
|
||||
- BT-33 + SEC-14..SEC-16 are deliberately **cross-cutting** rather than per-endpoint duplicates of BT-27..BT-31 — cycle-update rule 4 preserves existing traceability IDs; the cycle-8 rows remain the binding functional specs, cycle-10 rows add the message-content contract on top.
|
||||
- Integration tests that previously asserted substring matches on raw `JsonException.Message` were updated to assert the static strings (`RegionRequestValidationTests`, `CreateRouteValidationTests`, `TileInventoryValidationTests`, `UavUploadValidationTests`). No new integration test *files* — assertion tightening on existing failure methods.
|
||||
- AZ-1113 AC-5 is doc-only (`error-shape.md` §Information disclosure) — verified at Step 13 (`api_program.md` + contract doc).
|
||||
- Step 11 evidence: smoke PASS 450/450 unit + integration EXIT:0 (~2.5m) per state file; full perf gate unchanged (REST-only PT scenarios still apply).
|
||||
- Cycle-update rule check: no NFR conflicts. Inv-5 scope expands from 5xx-only (AZ-353) to include deserializer/binding 4xx — not a conflict because no prior cycle declared the opposite for 4xx message content.
|
||||
|
||||
Reference in New Issue
Block a user