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:
@@ -34,7 +34,7 @@ Application entry point. Configures DI container, sets up middleware, defines mi
|
||||
- `RegionRequestValidator` (AZ-808 cycle 8) — `AbstractValidator<RequestRegionRequest>`. Post-deserialization business rules: non-zero `id`, `lat`/`lon` ranges, `sizeMeters` ∈ \[100, 10000\], `zoomLevel` ∈ \[0, 22\]. Required-field detection lives at the deserializer layer (`[JsonRequired]` + `UnmappedMemberHandling.Disallow`).
|
||||
- `CreateRouteRequestValidator` + `RoutePointValidator` + `GeofencePolygonValidator` (AZ-809 cycle 8) — three FluentValidation validators for the route-creation endpoint. The root validator chains `RuleForEach(req => req.Points).SetValidator(new RoutePointValidator())` for per-point checks and `RuleForEach(req => req.Geofences!.Polygons).SetValidator(new GeofencePolygonValidator()).OverridePropertyName("geofences.polygons")` for per-polygon checks. The `OverridePropertyName` on the geofences chain restores the full wire path (`geofences.polygons[i].northWest`) because FluentValidation's default name policy drops the parent on deep expressions like `req.Geofences!.Polygons`. `RoutePointValidator` uses `OverridePropertyName("lat"/"lon")` after each range rule so error keys match the wire format (`lat`/`lon`) rather than the camelCased C# names (`latitude`/`longitude`). The cross-field rule `createTilesZip ⇒ requestMaps` lives on the root via `Must(req => !(req.CreateTilesZip && !req.RequestMaps)).WithName("createTilesZip")`.
|
||||
- `UavTileBatchMetadataPayloadValidator` + `UavTileMetadataValidator` (AZ-810 cycle 8) — FluentValidation validators for the UAV upload metadata envelope. Root validator runs `items` count rules (non-null, non-empty, ≤ `UavQualityConfig.MaxBatchSize`) then `RuleForEach(p => p.Items).SetValidator(new UavTileMetadataValidator(...))` so per-item errors come out as `items[i].<field>` (then prefixed with `metadata.` by `UavUploadValidationFilter`). Per-item rules: `latitude` ∈ \[-90, 90\], `longitude` ∈ \[-180, 180\], `tileZoom` ∈ \[0, 22\], `tileSizeMeters` > 0, `capturedAt` within `\[now - MaxAgeDays, now + CapturedAtFutureSkewSeconds\]`. `flightId` is intentionally NOT validated beyond JSON shape — AZ-503 anonymous-flight semantics require `null` to be valid, and malformed UUID strings are already rejected at the deserializer with a JsonException. The freshness check uses an injectable `TimeProvider` (defaults to `TimeProvider.System`) so unit tests can drive it with a fixed clock.
|
||||
- `UavUploadValidationFilter` (AZ-810 cycle 8) — endpoint filter for `POST /api/satellite/upload`. The endpoint is `multipart/form-data` so the generic `WithValidation<T>()` JSON-body filter cannot bind directly; this filter reads the `metadata` form field, deserializes it via the strict global `JsonSerializerOptions` (so `UnmappedMemberHandling.Disallow` + `[JsonRequired]` from AZ-795 are honored), runs `IValidator<UavTileBatchMetadataPayload>` from DI, and enforces the cross-field `items.Count == files.Count` rule. Error-map keys from the per-item validator are prefixed with `metadata.` so paths surface to the caller as `errors["metadata.items[0].latitude"]`. Registered as a transient via `AddTransient<UavUploadValidationFilter>()` and wired on the endpoint with `.AddEndpointFilter<UavUploadValidationFilter>()`. The downstream `IUavTileUploadHandler` retains its own envelope checks as defence-in-depth (covers direct handler callers in unit tests).
|
||||
- `UavUploadValidationFilter` (AZ-810 cycle 8; AZ-1113 cycle 10) — endpoint filter for `POST /api/satellite/upload`. The endpoint is `multipart/form-data` so the generic `WithValidation<T>()` JSON-body filter cannot bind directly; this filter reads the `metadata` form field, deserializes it via the strict global `JsonSerializerOptions` (so `UnmappedMemberHandling.Disallow` + `[JsonRequired]` from AZ-795 are honored), runs `IValidator<UavTileBatchMetadataPayload>` from DI, and enforces the cross-field `items.Count == files.Count` rule. Error-map keys from the per-item validator are prefixed with `metadata.` so paths surface to the caller as `errors["metadata.items[0].latitude"]`. **AZ-1113**: metadata `JsonException` paths set `errors["metadata"]` to the static string `` `metadata` could not be parsed as JSON. `` (no `ex.Message` echo). Registered as a transient via `AddTransient<UavUploadValidationFilter>()` and wired on the endpoint with `.AddEndpointFilter<UavUploadValidationFilter>()`. The downstream `IUavTileUploadHandler` retains its own envelope checks as defence-in-depth (covers direct handler callers in unit tests).
|
||||
|
||||
### Api/DTOs (AZ-811 cycle 8)
|
||||
- `GetTileByLatLonQuery` — `record GetTileByLatLonQuery(double? Lat, double? Lon, int? Zoom)` with `[FromQuery(Name="lat"|"lon"|"zoom")]` on each property. Bound via `[AsParameters]` on the `GetTileByLatLon` handler. **Nullable on purpose**: minimal-API binding throws `BadHttpRequestException` for missing non-nullable query params BEFORE endpoint filters run; that short-circuit produces a plain `ProblemDetails` via `GlobalExceptionHandler` with no `errors{}` envelope. Nullable types let binding always succeed so the envelope filter + validator handle the failure surface uniformly per `error-shape.md` v1.0.0. The handler dereferences `.Value` only after the validator filter passes.
|
||||
@@ -63,8 +63,8 @@ Application entry point. Configures DI container, sets up middleware, defines mi
|
||||
- `ValidationEndpointFilter<T>` — generic minimal-API filter that resolves `IValidator<T>` from DI, runs it against the bound argument, and returns `Results.ValidationProblem(result.ToDictionary())` on failure. Wired per-endpoint via `RouteHandlerBuilder.WithValidation<T>()`.
|
||||
- `GlobalValidatorConfig.ApplyOnce()` — idempotent process-wide FluentValidation configuration. Sets `ValidatorOptions.Global.PropertyNameResolver` so error map keys are camelCase per `error-shape.md` Inv-4. Called from `Program.cs` and from the test assembly's `ValidatorTestModuleInitializer` so both contexts see identical key shapes.
|
||||
|
||||
### Api/GlobalExceptionHandler (AZ-795, cycle 7)
|
||||
- `GlobalExceptionHandler : IExceptionHandler` — registered via `AddExceptionHandler<GlobalExceptionHandler>()` + `AddProblemDetails()`. Intercepts unhandled exceptions and converts `BadHttpRequestException(JsonException)` (unknown-member rejection, missing-required-field, type mismatch) into RFC 7807 `ValidationProblemDetails` matching the FluentValidation output shape (single source of truth — see `error-shape.md` v1.0.0 §"Both paths produce identically-shaped bodies"). 5xx errors pass through with sanitised body + `correlationId` (preserves AZ-353).
|
||||
### Api/GlobalExceptionHandler (AZ-795 cycle 7; AZ-1113 cycle 10)
|
||||
- `GlobalExceptionHandler : IExceptionHandler` — registered via `AddExceptionHandler<GlobalExceptionHandler>()` + `AddProblemDetails()`. Intercepts unhandled exceptions and converts `BadHttpRequestException(JsonException)` (unknown-member rejection, missing-required-field, type mismatch) into RFC 7807 `ValidationProblemDetails` matching the FluentValidation output shape (single source of truth — see `error-shape.md` v1.0.1 §"Both paths produce identically-shaped bodies"). **AZ-1113 (cycle 10)**: `errors[]` values for deserializer failures use the static string `"The field value is invalid."` (no raw `JsonException.Message` / `.NET` type names). Non-JSON `BadHttpRequestException` paths emit `detail: "The request could not be processed."` instead of echoing `badRequest.Message`. 5xx errors pass through with sanitised body + `correlationId` (preserves AZ-353).
|
||||
|
||||
## Internal Logic
|
||||
|
||||
@@ -81,7 +81,7 @@ Application entry point. Configures DI container, sets up middleware, defines mi
|
||||
10. **JWT authentication (AZ-487 + AZ-494)**: `AddSatelliteJwt(builder.Configuration)` (extension in `SatelliteProvider.Api.Authentication`) registers `JwtBearer` with `TokenValidationParameters` set per the suite auth contract: signature + lifetime + issuer + audience validation, 30 s clock skew, ≥ 32-byte HMAC key. The `iss` value comes from `JWT_ISSUER` env (fallback `Jwt:Issuer` config); the `aud` value comes from `JWT_AUDIENCE` env (fallback `Jwt:Audience` config). All three values (secret, iss, aud) are fail-fast — the API throws `InvalidOperationException` at startup if any is unset or whitespace-only. Production deploys MUST set the env vars with admin-team-confirmed values; `appsettings.json` ships empty so the fail-fast triggers. `appsettings.Development.json` ships clearly-tagged DEV-ONLY values (`DEV-ONLY-iss-admin-azaion-local` / `DEV-ONLY-aud-satellite-provider`) so local dev works out-of-the-box. Followed by `AddAuthorization` with the `RequiresGpsPermission` policy (AZ-488).
|
||||
11. **Kestrel HTTP/2 (AZ-505)**: `builder.WebHost.ConfigureKestrel(opts => opts.ConfigureEndpointDefaults(lo => lo.Protocols = HttpProtocols.Http1AndHttp2))`. The dev listener is now `https://+:8080` with a self-signed cert (`./certs/api.pfx`, generated idempotently by `scripts/run-tests.sh` and bound via `ASPNETCORE_Kestrel__Certificates__Default__Path` / `__Password` in `docker-compose.yml`). Kestrel needs TLS for HTTP/2 protocol negotiation; ALPN advertises both `h2` and `http/1.1` so HTTP/2-capable clients (browser Leaflet, `HttpClient` with `Version20` + `RequestVersionExact`, httpx `http2=True`, gRPC over HTTP/2) multiplex tile reads on a single TLS connection, and legacy clients fall back to HTTP/1.1. The integration-test container trusts the dev cert via `/usr/local/share/ca-certificates/` + `update-ca-certificates`. AZ-505 AC-5 verifies the multiplex semantics here; production termination is expected at the ingress (Envoy / nginx / ALB) — Kestrel can then drop to HTTP/2 cleartext behind it without changing this code.
|
||||
12. **gRPC (AZ-1074, cycle 9)**: `AddGrpc()` + `MapGrpcService<RouteTileDeliveryGrpcService>()`. Shares JWT auth middleware with REST — callers pass `authorization: Bearer <token>` in gRPC metadata. Server-streaming RPC delegates to `IRouteTileDeliveryOrchestrator.DeliverAsync`.
|
||||
13. **ProblemDetails + global exception handler (AZ-795, cycle 7)**: `AddProblemDetails()` + `AddExceptionHandler<GlobalExceptionHandler>()` register the uniform RFC 7807 error pipeline. `app.UseExceptionHandler()` (in the middleware chain) routes unhandled exceptions through `GlobalExceptionHandler`, which converts `BadHttpRequestException(JsonException)` (unknown-member rejection, missing-required-field, JSON type mismatch) into `ValidationProblemDetails` with the same `errors[]` map shape that FluentValidation produces. This is the deserializer-layer half of the strict-validation contract — `error-shape.md` v1.0.0 §"Two collaborating pieces of shared infrastructure".
|
||||
13. **ProblemDetails + global exception handler (AZ-795 cycle 7; AZ-1113 cycle 10)**: `AddProblemDetails()` + `AddExceptionHandler<GlobalExceptionHandler>()` register the uniform RFC 7807 error pipeline. `app.UseExceptionHandler()` (in the middleware chain) routes unhandled exceptions through `GlobalExceptionHandler`, which converts `BadHttpRequestException(JsonException)` (unknown-member rejection, missing-required-field, JSON type mismatch) into `ValidationProblemDetails` with the same `errors[]` map shape that FluentValidation produces. Deserializer/binding 400 message content is static per `error-shape.md` v1.0.1 §Information disclosure — this is the deserializer-layer half of the strict-validation contract.
|
||||
14. **Strict JSON parsing (AZ-795, cycle 7)**: `ConfigureHttpJsonOptions` sets `PropertyNamingPolicy = CamelCase`, `PropertyNameCaseInsensitive = true`, `UnmappedMemberHandling = Disallow`, and adds `JsonStringEnumConverter` with camelCase naming. `UnmappedMemberHandling.Disallow` is the key strict-parsing knob: any unknown root or nested field is rejected at the deserializer rather than silently dropped. Catches typos (`{"Z":12}` uppercase, `{"tileZoom":...}` post-rename) that no FluentValidation rule can see after deserialization.
|
||||
15. **FluentValidation registration (AZ-795 + AZ-796, cycle 7)**: `AddValidatorsFromAssemblyContaining<Program>()` auto-registers every `IValidator<T>` in the API assembly (currently `InventoryRequestValidator` + `TileCoordValidator`, AZ-808 `RegionRequestValidator`, AZ-809 `CreateRouteRequestValidator` + `RoutePointValidator` + `GeofencePolygonValidator`, AZ-810 `UavTileBatchMetadataPayloadValidator` + `UavTileMetadataValidator`, AZ-811 `GetTileByLatLonQueryValidator`). `GlobalValidatorConfig.ApplyOnce()` runs the idempotent process-wide config — sets `ValidatorOptions.Global.PropertyNameResolver` so `errors` map keys are camelCase (matches the request body's casing per `error-shape.md` Inv-4). Per-endpoint opt-in via `.WithValidation<T>()` on the JSON-body endpoints — the generic `ValidationEndpointFilter<T>` resolves the validator from DI at request time and returns `Results.ValidationProblem` on failure.
|
||||
16. **AZ-810 multipart validation filter (cycle 8)**: `AddTransient<UavUploadValidationFilter>()` registers the bespoke filter used by `POST /api/satellite/upload`. The endpoint is `multipart/form-data` so the generic `.WithValidation<T>()` JSON-body filter cannot bind; this filter reads the `metadata` form field, deserializes it via the strict global `JsonSerializerOptions`, runs the FluentValidation chain, and enforces the cross-field `items.Count == files.Count` envelope rule. Wired on the endpoint with `.AddEndpointFilter<UavUploadValidationFilter>()` between `.RequireAuthorization(SatellitePermissions.UavUploadPolicy)` and the metadata accept/produces annotations.
|
||||
|
||||
@@ -19,6 +19,7 @@ Console application that runs end-to-end integration tests against a live API in
|
||||
- `TileInventoryValidationTests` (added cycle 7 — AZ-796) — 16 tests: `HappyPath_Returns200`, `EmptyBody_Returns400`, `NeitherPopulated_Returns400`, `BothPopulated_Returns400`, `EmptyTilesArray_Returns400`, `TilesOverCap_Returns400`, `MissingZ_Returns400WithFieldPath`, `MissingXAndY_Returns400`, `ZoomOutOfRange_Returns400WithFieldPath`, `XBeyondZoomBounds_Returns400`, `YBeyondZoomBounds_Returns400`, `NegativeAxis_Returns400`, `UnknownRootField_Returns400`, `UnknownNestedField_Returns400`, `OldV1FieldName_Returns400` (AZ-794 + AZ-796 intersection — exact AZ-777 Phase 1 reproducer body, asserts legacy `tileZoom/tileX/tileY` now yields 400), `TypeMismatch_Returns400`. Each test exercises one of the 9 validation rules end-to-end through `ValidationEndpointFilter<TileInventoryRequest>` + `GlobalExceptionHandler`, asserts HTTP 400 + RFC 7807 `ValidationProblemDetails` shape via the shared `ProblemDetailsAssertions` helper.
|
||||
- `IdempotentPostTests` — pre-existing; cycle 7 adjusted the route-point payload from PascalCase (`Latitude`/`Longitude`) to camelCase (`lat`/`lon`) because the post-AZ-795 `UnmappedMemberHandling.Disallow` would otherwise reject the previously-silently-ignored fields. The `RoutePoint` DTO has carried `JsonPropertyName("lat"/"lon")` since AZ-309; cycle 7's strict JSON parsing exposed the test was sending the wrong shape and getting away with it via the pre-cycle-7 permissive deserializer.
|
||||
- `RouteTileDeliveryGrpcTests` (added cycle 9 — AZ-1074/AZ-1075) — `RunHappyPath`, `RunInvalidRequests` (single waypoint / lat out of range / zoom out of range → `InvalidArgument`), `RunBackpressureSafe` (slow consumer preserves JPEG + SHA256), `RunRestConsistency` (REST route CSV tile keys overlap gRPC stream keys). Wired into both smoke and full suites via `Program.cs`.
|
||||
- `RegionRequestValidationTests`, `CreateRouteValidationTests`, `UavUploadValidationTests`, `GetTileByLatLonValidationTests` (added cycle 8 — AZ-808..AZ-811) — per-endpoint strict-validation integration suites exercising `ValidationEndpointFilter<T>` / `UavUploadValidationFilter` / `RejectUnknownQueryParamsEndpointFilter` + `GlobalExceptionHandler` end-to-end. AZ-1113 (cycle 10) tightened assertions on deserializer/binding 400 paths to expect static messages per `error-shape.md` v1.0.1; `UavUploadValidationTests.MetadataNotAnObject_Returns400` additionally asserts the response body contains no `System.` substring.
|
||||
|
||||
### Supporting Classes
|
||||
- `Models.cs` — HTTP response DTOs for deserialization
|
||||
|
||||
@@ -15,7 +15,10 @@ Existing baseline (pre-cycle-2) test classes cover `TileService`, `RegionService
|
||||
|
||||
### AZ-488 — UAV tile upload
|
||||
- `UavTileQualityGateTests` — one happy path + ≥ 1 reject path per rule (Rule 1 INVALID_FORMAT × 2, Rule 2 SIZE_OUT_OF_BAND × 2, Rule 3 WRONG_DIMENSIONS × 1, Rule 4 CAPTURED_AT_FUTURE / _TOO_OLD × 2, Rule 5 IMAGE_TOO_UNIFORM × 1) + rule-ordering determinism. Uses a `FixedTimeProvider` for Rule-4 isolation and `UavTileImageFactory` for deterministic JPEG fixtures.
|
||||
- `UavTileUploadHandlerTests` — end-to-end with a mocked `ITileRepository`. Cycle-2 baseline: 1-item happy path, 3-item mixed batch (file written + `InsertAsync` called only for accepted), per-source UPSERT pass-through. AZ-503 additions: `HandleAsync_TwoFlightsSameCell_ProduceDistinctIdsAndPathsButSameLocationHash` (multi-flight coexistence with shared `location_hash`); `HandleAsync_IdenticalUpload_ProducesIdenticalIdAndDeterministicContentSha` (idempotent re-insert preserves deterministic `id` + `content_sha256`).
|
||||
- `UavTileUploadHandlerTests` — end-to-end with a mocked `ITileRepository`. Cycle-2 baseline: 1-item happy path, 3-item mixed batch (file written + `InsertAsync` called only for accepted), per-source UPSERT pass-through. AZ-503 additions: `HandleAsync_TwoFlightsSameCell_ProduceDistinctIdsAndPathsButSameLocationHash` (multi-flight coexistence with shared `location_hash`); `HandleAsync_IdenticalUpload_ProducesIdenticalIdAndDeterministicContentSha` (idempotent re-insert preserves deterministic `id` + `content_sha256`). AZ-1113 (cycle 10): `HandleAsync_InvalidMetadataJson_ReturnsEnvelopeError` — defense-in-depth metadata parse returns static envelope error (no `ex.Message` echo).
|
||||
|
||||
### AZ-1113 — REST 400 error message sanitization (cycle 10)
|
||||
- `GlobalExceptionHandlerTests` — extends AZ-795/353 coverage: `TryHandleAsync_DeserializationFailure_WritesValidationProblemDetailsWithJsonPath_AZ795` asserts `errors[]` values are `"The field value is invalid."` with no `.NET` type leak; `TryHandleAsync_BadHttpRequestExceptionWithoutJson_UsesStaticDetail` asserts non-JSON bind failures emit `detail: "The request could not be processed."` (existing 5xx sanitization tests unchanged).
|
||||
- `Authentication/PermissionsRequirementTests` — `PermissionsAuthorizationHandler` correctly accepts a `permissions` claim shaped as a single string OR as a JSON array, rejects when the requested permission is absent, and short-circuits when the principal has no `permissions` claim at all.
|
||||
- `TestUtilities/UavTileImageFactory` — programmatic JPEG factories used by the gate + handler tests: `CreateValidJpeg(width, height, seed)`, `CreateUniformJpeg`, `CreatePng` (for Rule 1 negative path).
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
# Ripple Log — Cycle 10
|
||||
|
||||
Tasks: AZ-1113 (REST 400 error message sanitization)
|
||||
|
||||
- `_docs/02_document/modules/api_program.md` — GlobalExceptionHandler + UavUploadValidationFilter AZ-1113 sanitization notes (changed by AZ-1113)
|
||||
- `_docs/02_document/modules/tests_unit.md` — GlobalExceptionHandlerTests + UavTileUploadHandlerTests cycle-10 entries (changed by AZ-1113)
|
||||
- `_docs/02_document/modules/tests_integration.md` — validation-test static-message assertions (changed by AZ-1113)
|
||||
- `_docs/02_document/tests/security-tests.md` — SEC-14..SEC-16 + SEC-04 pass-criterion bump (test-spec sync)
|
||||
- `_docs/02_document/tests/blackbox-tests.md` — BT-33 cross-endpoint sanitization scenarios (test-spec sync)
|
||||
- `_docs/02_document/tests/traceability-matrix.md` — AZ-1113 AC-1..AC-5 rows + cycle-10 coverage notes (test-spec sync)
|
||||
|
||||
No new HTTP routes or perf scenarios. Existing integration/unit tests gained static-message assertions; no runner-script changes (cycle-update skips Phase 4).
|
||||
@@ -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