mirror of
https://github.com/azaion/satellite-provider.git
synced 2026-06-21 12:11:14 +00:00
[AZ-794] [AZ-795] [AZ-796] Cycle 7 Steps 12-15 sync (test-spec / docs / security / perf)
Step 12 (Test-Spec Sync): adds BT-27 for the AZ-796 9-rule validation surface and 12 cycle-7 AC rows + Coverage Summary update to traceability-matrix.md. Step 13 (Update Docs): module-layout + module docs for the new SatelliteProvider.Api/Validators namespace + GlobalExceptionHandler + updated TileInventory DTO; tests_unit + tests_integration document the new InventoryRequestValidatorTests (16 unit tests covering all 9 rules) + TileInventoryValidationTests (16 integration tests) + ProblemDetailsAssertions support; glossary entries for Validation Problem Details / FluentValidation / Unmapped Member Handling; system-flows F8 (Tile Inventory Bulk Lookup) expanded with deserializer + validator gates and a 13-row Validation Surface table; data_parameters § Tile Inventory documents the v2 input schema + constraints; ripple_log_cycle7 captures the doc-side ripple decisions. Step 14 (Security Audit): 5-phase audit ran; verdict PASS_WITH_WARNINGS (3 Low findings — D-AZ795-1 FluentValidation 12.0.0 -> 12.1.1 recommended bump, F-AZ795-1 JsonException.Message leak in 400 detail, F-AZ795-2 BadHttpRequestException.Message leak). No Critical / High; auth runs before validation (confirmed in Program.cs); two NuGet additions (FluentValidation 12.0.0 + .DependencyInjectionExtensions 12.0.0) both CVE-clean. Per-phase reports plus consolidated security_report_cycle7.md. Step 15 (Performance Test): docker compose stack used for perf run, scripts/run-performance-tests.sh exited 0 with 8/8 scenarios PASS (second consecutive clean exit-0); added PT-09 cycle-7 smoke probe (v2 z/x/y schema, 2500-tile all-miss batch) measuring min=27ms median=44ms p95=73ms max=86ms (13.7x under AZ-505 AC-4 1000ms budget). PT-07/08 improvements traced to the cycle-6 TLS handshake-overhead identification, not application-side change. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -14,7 +14,7 @@
|
||||
| Layer 1 | Historic name for satellite imagery from external providers (provider-agnostic; first implementation: Google Maps). Generalised in AZ-484 to one of N values of `Tile Source`; the term is retained for continuity with earlier docs and tickets. | user clarification, AZ-484 |
|
||||
| Layer 2 | Historic name for UAV-captured nadir camera imagery (orthogonal tiles uploaded post-flight). Generalised in AZ-484 to the `uav` `Tile Source` value; the term is retained for continuity with earlier docs and tickets. | user clarification, AZ-484 |
|
||||
| Tile Source | The producer of a tile row, persisted in `tiles.source` as a contract-defined string (`google_maps`, `uav`, …). Per AZ-503-foundation: each `(cell, source, flight)` triple may have at most one row; reads return the most-recent across sources AND flights. Adding a new source requires a new `TileSource` enum member and a tile-storage contract version bump. | _docs/02_document/contracts/data-access/tile-storage.md (v2.0.0) |
|
||||
| Tile Inventory | AZ-505 bulk read endpoint (`POST /api/satellite/tiles/inventory`) that returns one metadata entry per requested cell — present/absent + most-recent row's `id`/`capturedAt`/`source`/`flightId`/`resolutionMPerPx` — without streaming any tile bodies. Accepts up to 5000 entries per request in one of two XOR shapes: by-coord (`tiles: [{tileZoom, tileX, tileY}, …]`) or by-hash (`locationHashes: [Guid, …]`). Used by the onboard `gps-denied-onboard` cross-repo path to decide which Google-Maps cells still need download and which UAV variants are already on the server. | _docs/02_document/contracts/api/tile-inventory.md (v1.0.0) |
|
||||
| Tile Inventory | AZ-505 bulk read endpoint (`POST /api/satellite/tiles/inventory`) that returns one metadata entry per requested cell — present/absent + most-recent row's `id`/`capturedAt`/`source`/`flightId`/`resolutionMPerPx` — without streaming any tile bodies. Accepts up to 5000 entries per request in one of two XOR shapes: by-coord (`tiles: [{z, x, y}, …]`; renamed from `tileZoom/tileX/tileY` by AZ-794, cycle 7) or by-hash (`locationHashes: [Guid, …]`). Used by the onboard `gps-denied-onboard` cross-repo path to decide which Google-Maps cells still need download and which UAV variants are already on the server. Strict input validation enforced by `InventoryRequestValidator` (AZ-796, cycle 7) — see `Validation Problem Details`. | _docs/02_document/contracts/api/tile-inventory.md (v2.0.0) |
|
||||
| Captured At | Producer-defined UTC timestamp ("the moment this tile imagery represents") persisted in `tiles.captured_at`. For Google Maps it is `DateTime.UtcNow` at download time (provider does not expose original imagery date); for UAV it is the capture timestamp supplied by the upload client. Drives the most-recent-across-(source, flight) read selection rule. | _docs/02_document/contracts/data-access/tile-storage.md (v2.0.0) |
|
||||
| UAV Tile Upload | `POST /api/satellite/upload` batch endpoint (AZ-488) that ingests UAV-captured tiles. Multipart envelope with a JSON `metadata` field and an aligned `files` collection; per-item results returned in a single HTTP 200 response. | _docs/02_document/contracts/api/uav-tile-upload.md (v1.0.0) |
|
||||
| Quality Gate | The 5-rule validator (`UavTileQualityGate`) applied to every UAV tile before persistence: Format, Size band, Dimensions, Captured-at age, Blank/uniform. The first failing rule produces a reject reason from the closed `UavTileRejectReasons` enumeration. | _docs/02_document/contracts/api/uav-tile-upload.md (v1.0.0) |
|
||||
@@ -44,6 +44,9 @@
|
||||
| Tile Deduplication | Mechanism using DB unique index + ConcurrentDictionary to prevent re-downloading identical tiles | modules/services_google_maps_downloader.md |
|
||||
| UUIDv5 | RFC 9562 §5.5 deterministic UUID derived from a namespace UUID + a UTF-8 name via SHA-1. AZ-503 uses it to produce stable, cross-repo `tiles.id` and `tiles.location_hash` values without coordinating an id allocator between the satellite-provider and `gps-denied-onboard` workspaces. | modules/common_uuidv5.md, AZ-503 |
|
||||
| Legacy ID | Pre-AZ-503 random `tiles.id` value, copied into the `legacy_id` column by migration 014 for one-cycle forensics. To be dropped in a future cycle once the cross-repo cutover settles. | _docs/02_document/data_model.md, AZ-503 |
|
||||
| Validation Problem Details | The uniform RFC 7807 error body shape (`ValidationProblemDetails`) returned by every public HTTP endpoint on 4xx input rejection: `{ type, title, status, errors: { "field.path": ["msg1", ...], ... } }`. Both the FluentValidation business-rule layer (`ValidationEndpointFilter<T>`) and the System.Text.Json deserializer layer (caught by `GlobalExceptionHandler`) produce this exact shape. Error-map keys are camelCase JSON paths (`tiles[0].z`, `locationHashes[3]`) per the global property-name resolver configured in `GlobalValidatorConfig.ApplyOnce`. | _docs/02_document/contracts/api/error-shape.md (v1.0.0), AZ-795 |
|
||||
| FluentValidation | Open-source library (12.0.0 since AZ-795) used to declare business-rule validators (`AbstractValidator<T>`) per request DTO. Registered via `AddValidatorsFromAssemblyContaining<Program>()` in `Program.cs` and consumed by the generic `ValidationEndpointFilter<T>` which an endpoint opts into via `RouteHandlerBuilder.WithValidation<T>()`. | _docs/02_document/architecture.md § 9, AZ-795 |
|
||||
| Unmapped Member Handling | The `System.Text.Json.Serialization.JsonUnmappedMemberHandling.Disallow` mode wired into `ConfigureHttpJsonOptions` in cycle 7 — rejects unknown JSON fields (including legacy renames such as `tileZoom/tileX/tileY` after AZ-794) at deserialisation time with a `JsonException` that `GlobalExceptionHandler` converts to 400 + `ValidationProblemDetails`. Pair-rule with `[JsonRequired]` on TileCoord axes catches missing-axis cases at the same layer. | _docs/02_document/architecture.md § 9, AZ-795 |
|
||||
|
||||
## Abbreviations
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
**Language**: csharp
|
||||
**Layout Convention**: custom (per-component .csproj per logical component)
|
||||
**Root**: ./
|
||||
**Last Updated**: 2026-05-12 (cycle 6 — AZ-505 tile inventory + Leaflet covering index + HTTP/2: new `POST /api/satellite/tiles/inventory` endpoint, new `ITileRepository.GetTilesByLocationHashesAsync`, rewired `GetByTileCoordinatesAsync` to filter on `location_hash`, migration `015_AddTilesLeafletPathIndex.sql`, Kestrel `Http1AndHttp2`, new `TileInventory*` DTOs in Common; cycle 5 — AZ-503 tile-identity foundation added: `SatelliteProvider.Common/Utils/Uuidv5.cs`, migration `014_AddTileIdentityColumns.sql`, 4 new `TileEntity` columns, integer-only flight-aware UPSERT, IntegrationTests → Common ProjectReference)
|
||||
**Last Updated**: 2026-05-22 (cycle 7 — AZ-794 + AZ-795 + AZ-796 strict inventory validation + z/x/y rename: `TileCoord` wire fields renamed `tileZoom/tileX/tileY` → `z/x/y` with `[JsonRequired]`; new `SatelliteProvider.Api/Validators/{InventoryRequestValidator,ValidationEndpointFilter,GlobalValidatorConfig,ValidationEndpointFilterExtensions}.cs`; new `SatelliteProvider.Api/GlobalExceptionHandler.cs` for `JsonException` → `ValidationProblemDetails`; FluentValidation 12.0.0 + `JsonSerializerOptions.UnmappedMemberHandling.Disallow` wired into `Program.cs`; new contract `_docs/02_document/contracts/api/error-shape.md` v1.0.0; `tile-inventory.md` bumped to v2.0.0; new `SatelliteProvider.IntegrationTests/ProblemDetailsAssertions.cs` + `TileInventoryValidationTests.cs`; new `SatelliteProvider.Tests/TestSupport/ValidatorTestModuleInitializer.cs` + `Validators/InventoryRequestValidatorTests.cs`; new `scripts/probe_inventory_validation.sh`; cycle 6 — AZ-505 tile inventory + Leaflet covering index + HTTP/2: new `POST /api/satellite/tiles/inventory` endpoint, new `ITileRepository.GetTilesByLocationHashesAsync`, rewired `GetByTileCoordinatesAsync` to filter on `location_hash`, migration `015_AddTilesLeafletPathIndex.sql`, Kestrel `Http1AndHttp2`, new `TileInventory*` DTOs in Common; cycle 5 — AZ-503 tile-identity foundation added: `SatelliteProvider.Common/Utils/Uuidv5.cs`, migration `014_AddTileIdentityColumns.sql`, 4 new `TileEntity` columns, integer-only flight-aware UPSERT, IntegrationTests → Common ProjectReference)
|
||||
|
||||
## Layout Rules
|
||||
|
||||
@@ -129,6 +129,7 @@ The cycle-1 (AZ-487) and cycle-2 (AZ-488) code reviews each surfaced an F1 (Low
|
||||
- `SatelliteProvider.Api/Validators/ValidationEndpointFilter.cs` + `ValidationEndpointFilterExtensions.cs` (added by AZ-795; generic `IEndpointFilter<T>` that runs the registered `IValidator<T>` and returns `Results.ValidationProblem` on failure; opt-in via `RouteHandlerBuilder.WithValidation<T>()`)
|
||||
- `SatelliteProvider.Api/Validators/InventoryRequestValidator.cs` + `TileCoordValidator` (added by AZ-796; FluentValidation rules for `POST /api/satellite/tiles/inventory` — XOR `tiles`/`locationHashes`, per-array cap, slippy-map range checks)
|
||||
- `SatelliteProvider.Api/Validators/GlobalValidatorConfig.cs` (added by AZ-795/AZ-796; idempotent `ApplyOnce()` configures `ValidatorOptions.Global.PropertyNameResolver` so `errors`-map keys are camelCase per `error-shape.md` Inv-4; called from `Program.cs` and from the test assembly's `ModuleInitializer`)
|
||||
- `SatelliteProvider.Api/GlobalExceptionHandler.cs` (added by AZ-795; `IExceptionHandler` registered via `AddExceptionHandler<GlobalExceptionHandler>()`. Intercepts `BadHttpRequestException(JsonException)` from System.Text.Json's strict-parsing path — unknown-member rejection, missing required field via `[JsonRequired]`, JSON type mismatch — and emits `ValidationProblemDetails` with the same `errors[]` map shape that FluentValidation produces. 5xx errors pass through with sanitised body + `correlationId` per AZ-353.)
|
||||
- **Internal**: (none)
|
||||
- **Owns**: `SatelliteProvider.Api/**`
|
||||
- **PackageReferences (added by AZ-487, bumped by AZ-496, then by AZ-500; AZ-795 added FluentValidation)**: `Microsoft.AspNetCore.Authentication.JwtBearer` 10.0.7 (pinned to the same minor patch as `Microsoft.AspNetCore.OpenApi` 10.0.7; AZ-496 bumped both packages from 8.0.21 → 8.0.25 in cycle 3 to close cycle-1 D1 + cycle-2 D3 supply-chain findings, then AZ-500 bumped both 8.0.25 → 10.0.7 in cycle 4 as part of the .NET 8 → .NET 10 migration; AZ-500 also bumped `Swashbuckle.AspNetCore` 6.6.2 → 10.1.7 here to land Microsoft.OpenApi 2.x compat required by ASP.NET Core 10). `FluentValidation` + `FluentValidation.DependencyInjectionExtensions` 12.0.0 added by AZ-795 to back the strict-input-validation epic.
|
||||
|
||||
@@ -10,7 +10,7 @@ Application entry point. Configures DI container, sets up middleware, defines mi
|
||||
|--------|-------|---------|-------------|
|
||||
| GET | `/tiles/{z}/{x}/{y}` | `ServeTile` | Slippy map tile server with in-memory caching. AZ-505 rewired the DB lookup to filter on `location_hash` (deterministic UUIDv5) so the read becomes an `Index Only Scan` against `tiles_leaflet_path`; the wire response is byte-identical to pre-AZ-505. |
|
||||
| GET | `/api/satellite/tiles/latlon` | `GetTileByLatLon` | Download single tile by lat/lon/zoom |
|
||||
| POST | `/api/satellite/tiles/inventory` | `GetTilesInventory` | Bulk tile-existence/metadata lookup (AZ-505) — body is XOR of `tiles[{tileZoom,tileX,tileY}]` (Form A) and `locationHashes[uuid]` (Form B), each capped at 5000 entries. Response is one entry per request entry, in input order. Contract: `_docs/02_document/contracts/api/tile-inventory.md` v1.0.0. |
|
||||
| POST | `/api/satellite/tiles/inventory` | `GetTilesInventory` | Bulk tile-existence/metadata lookup (AZ-505) — body is XOR of `tiles[{z,x,y}]` (Form A) and `locationHashes[uuid]` (Form B), each capped at 5000 entries. Response is one entry per request entry, in input order. AZ-794 (cycle 7) renamed the coord triple from `tileZoom/tileX/tileY` → `z/x/y` (OSM convention); AZ-796 (cycle 7) added strict input validation via `WithValidation<TileInventoryRequest>()` so malformed payloads return RFC 7807 `ValidationProblemDetails` instead of silently coercing to zero. Contracts: `_docs/02_document/contracts/api/tile-inventory.md` v2.0.0 + `_docs/02_document/contracts/api/error-shape.md` v1.0.0. |
|
||||
| GET | `/api/satellite/tiles/mgrs` | `GetSatelliteTilesByMgrs` | MGRS stub (returns empty) |
|
||||
| POST | `/api/satellite/upload` | `UploadUavTileBatch` | UAV tile batch upload (AZ-488) — multipart envelope, 5-rule quality gate, per-source UPSERT with `source='uav'`. Requires the `RequiresGpsPermission` policy. |
|
||||
| POST | `/api/satellite/request` | `RequestRegion` | Queue region for async tile processing |
|
||||
@@ -32,12 +32,21 @@ Application entry point. Configures DI container, sets up middleware, defines mi
|
||||
- `UavTileBatchUploadResponse`, `UavTileUploadResultItem` — per-item response shape
|
||||
- `UavTileUploadStatus`, `UavTileRejectReasons` — string-constant enumerations exposed in the v1.0.0 contract
|
||||
|
||||
### Common/DTO (AZ-505)
|
||||
### Common/DTO (AZ-505; renamed by AZ-794 in cycle 7)
|
||||
- `TileInventoryRequest` — XOR body envelope with `Tiles` (Form A) OR `LocationHashes` (Form B)
|
||||
- `TileCoord` — `{TileZoom, TileX, TileY}` per-entry coord under Form A
|
||||
- `TileCoord` — `{Z, X, Y}` per-entry coord under Form A. Each property is marked `[JsonRequired]` so missing axes surface as `400` at the deserializer layer (System.Text.Json throws, `GlobalExceptionHandler` converts to `ValidationProblemDetails`).
|
||||
- `TileInventoryResponse` — `{Results: TileInventoryEntry[]}` response shape; ordering matches request
|
||||
- `TileInventoryEntry` — per-entry response shape (`Present`, `LocationHash`, optional `Id`/`CapturedAt`/`Source`/`FlightId`/`ResolutionMPerPx`)
|
||||
- `TileInventoryLimits.MaxEntriesPerRequest` — hard cap (5000) consumed by request validation
|
||||
- `TileInventoryEntry` — per-entry response shape (`Z`, `X`, `Y`, `LocationHash`, `Present`, optional `Id`/`CapturedAt`/`Source`/`FlightId`/`ResolutionMPerPx`)
|
||||
- `TileInventoryLimits.MaxEntriesPerRequest` — hard cap (5000) consumed by `InventoryRequestValidator`
|
||||
|
||||
### Api/Validators (AZ-795 + AZ-796, cycle 7)
|
||||
- `InventoryRequestValidator` — FluentValidation `AbstractValidator<TileInventoryRequest>`. Rules: XOR `tiles`/`locationHashes`, `tiles.Count ≤ MaxEntriesPerRequest`, `locationHashes.Count ≤ MaxEntriesPerRequest`, per-entry `TileCoordValidator`.
|
||||
- `TileCoordValidator` — per-entry rules: `Z` ∈ [0, 22] (slippy-map range), `X` ∈ [0, 2^Z), `Y` ∈ [0, 2^Z).
|
||||
- `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).
|
||||
|
||||
## Internal Logic
|
||||
|
||||
@@ -53,6 +62,9 @@ Application entry point. Configures DI container, sets up middleware, defines mi
|
||||
9. JSON options: camelCase, case-insensitive
|
||||
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`) 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. **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. **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.
|
||||
14. **FluentValidation registration (AZ-795 + AZ-796, cycle 7)**: `AddValidatorsFromAssemblyContaining<Program>()` auto-registers every `IValidator<T>` in the API assembly (currently `InventoryRequestValidator` + `TileCoordValidator`). `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<TileInventoryRequest>()` on the inventory MapPost — the generic `ValidationEndpointFilter<T>` resolves the validator from DI at request time and returns `Results.ValidationProblem` on failure.
|
||||
|
||||
### Startup
|
||||
1. Database migration via `DatabaseMigrator.RunMigrations()` — throws on failure
|
||||
@@ -67,12 +79,12 @@ Application entry point. Configures DI container, sets up middleware, defines mi
|
||||
3. If no DB record: downloads tile via `GoogleMapsDownloaderV2.DownloadSingleTileAsync`, creates `TileEntity`, inserts
|
||||
4. Returns image bytes with cache headers (`Cache-Control: public, max-age=86400`)
|
||||
|
||||
### GetTilesInventory Handler (AZ-505)
|
||||
1. Validates XOR body shape: 400 if both `tiles` and `locationHashes` are populated, 400 if neither is populated, 400 if either exceeds `TileInventoryLimits.MaxEntriesPerRequest` (5000)
|
||||
2. Delegates to `ITileService.GetInventoryAsync(request, ct)`
|
||||
3. Service computes `location_hash` for Form A entries via `Uuidv5.Create(TileNamespace, "{z}/{x}/{y}")`, calls `ITileRepository.GetTilesByLocationHashesAsync(IReadOnlyList<Guid>)`, re-aligns results back to input order
|
||||
4. Returns `TileInventoryResponse` with one entry per input — `present=true` entries carry `id` / `capturedAt` / `source` / `flightId` / `resolutionMPerPx`; `present=false` entries carry only `locationHash`
|
||||
5. Authenticated by `.RequireAuthorization()` (401 before handler for anonymous)
|
||||
### GetTilesInventory Handler (AZ-505 + AZ-796 cycle 7)
|
||||
1. **Pre-handler validation (cycle 7)**: `ValidationEndpointFilter<TileInventoryRequest>` runs BEFORE the handler. Resolves `InventoryRequestValidator` from DI and asserts XOR `tiles`/`locationHashes`, per-array cap (`TileInventoryLimits.MaxEntriesPerRequest = 5000`), `z` ∈ [0, 22], `x` ∈ [0, 2^z), `y` ∈ [0, 2^z) per entry. Any failure short-circuits with HTTP 400 + `ValidationProblemDetails`. Deserializer-layer failures (missing `z/x/y`, unknown root/nested fields, JSON type mismatch) are caught earlier by System.Text.Json and surfaced as identically-shaped `ValidationProblemDetails` via `GlobalExceptionHandler` (AZ-795).
|
||||
2. Handler delegates to `ITileService.GetInventoryAsync(request, ct)` — body of the handler is just the service call + `Results.Ok`.
|
||||
3. Service computes `location_hash` for Form A entries via `Uuidv5.Create(TileNamespace, "{z}/{x}/{y}")`, calls `ITileRepository.GetTilesByLocationHashesAsync(IReadOnlyList<Guid>)`, re-aligns results back to input order.
|
||||
4. Returns `TileInventoryResponse` with one entry per input — `present=true` entries carry `id` / `capturedAt` / `source` / `flightId` / `resolutionMPerPx`; `present=false` entries carry only `locationHash`.
|
||||
5. Authenticated by `.RequireAuthorization()` (401 before validation runs for anonymous requests).
|
||||
|
||||
### GetTileByLatLon Handler
|
||||
Downloads a tile, persists it, returns metadata as `DownloadTileResponse`.
|
||||
@@ -85,7 +97,7 @@ Buffers each `IFormFile` into memory, packages them as `UavUploadFile` records (
|
||||
|
||||
## Dependencies
|
||||
All project references: Common, DataAccess, Services.
|
||||
NuGet: `Serilog.AspNetCore` (8.0.3 — fallback retained on .NET 10 per AZ-500 Risk #4: no 10.x line published as of cycle 4; documented in `AGENTS.md`), `Swashbuckle.AspNetCore` (10.1.7 — bumped from 6.6.2 by AZ-500 to land Microsoft.OpenApi 2.x compat required by ASP.NET Core 10), `Microsoft.AspNetCore.OpenApi` (10.0.7 — bumped from 8.0.25 by AZ-500), `Microsoft.AspNetCore.Authentication.JwtBearer` (10.0.7 — added at 8.0.21 by AZ-487, bumped to 8.0.25 by AZ-496, bumped to 10.0.7 by AZ-500), `SixLabors.ImageSharp`, `Newtonsoft.Json`.
|
||||
NuGet: `Serilog.AspNetCore` (8.0.3 — fallback retained on .NET 10 per AZ-500 Risk #4: no 10.x line published as of cycle 4; documented in `AGENTS.md`), `Swashbuckle.AspNetCore` (10.1.7 — bumped from 6.6.2 by AZ-500 to land Microsoft.OpenApi 2.x compat required by ASP.NET Core 10), `Microsoft.AspNetCore.OpenApi` (10.0.7 — bumped from 8.0.25 by AZ-500), `Microsoft.AspNetCore.Authentication.JwtBearer` (10.0.7 — added at 8.0.21 by AZ-487, bumped to 8.0.25 by AZ-496, bumped to 10.0.7 by AZ-500), `FluentValidation` + `FluentValidation.DependencyInjectionExtensions` (12.0.0 — added by AZ-795 to back the strict-input-validation epic), `SixLabors.ImageSharp`, `Newtonsoft.Json`.
|
||||
|
||||
**Microsoft.OpenApi 2.x refactor note (AZ-500)**: the major bump (1.x → 2.x) drove three internal Swashbuckle-setup edits in this file — `using Microsoft.OpenApi.Models;` → `using Microsoft.OpenApi;`; `AddSecurityRequirement(...)` rewritten to take a `Func<OpenApiDocument, OpenApiSecurityRequirement>` and use `OpenApiSecuritySchemeReference("Bearer")` instead of the removed `OpenApiSecurityScheme.Reference` shape; `MapType<UavTileBatchUploadRequest>` rewritten to use the new `JsonSchemaType` enum and `IDictionary<string, IOpenApiSchema>` properties bag. The Swagger document shape (paths, operations, the Bearer Authorize button, the multipart-batch upload schema) is preserved exactly — `SwaggerDocument_AdvertisesBearerSecurityScheme` and the AZ-353 swagger-ready integration assertions still pass. Eight `ASPDEPR002` deprecation warnings (`WithOpenApi(...)`) remain — they're recorded in `_docs/03_implementation/reviews/batch_01_cycle4_review.md` as a follow-up PBI; the API is still fully functional in .NET 10 (deprecated, not removed).
|
||||
|
||||
|
||||
@@ -110,21 +110,23 @@ Authoritative reject-reason codes for the UAV upload quality gate. Adding a new
|
||||
- `ImageTooUniform = "IMAGE_TOO_UNIFORM"` — Rule 5 (luminance variance below `MinLuminanceVariance`).
|
||||
- `StorageFailure = "STORAGE_FAILURE"` — reserved for the orphan-row-recovery path when the on-disk write succeeds but the DB UPSERT fails; surfaced per-item without failing the envelope (AZ-488 Reliability NFR).
|
||||
|
||||
### TileCoord (added AZ-505)
|
||||
### TileCoord (added AZ-505, renamed AZ-794 cycle 7)
|
||||
Single tile coordinate triple used by the inventory endpoint Form A request shape and as the per-entry input echo on the response.
|
||||
- `TileZoom` (int) — slippy zoom level.
|
||||
- `TileX`, `TileY` (int) — slippy x/y at that zoom.
|
||||
- Defined in `SatelliteProvider.Common/DTO/TileInventory.cs`. Matches `tile-inventory.md` v1.0.0 Shape.
|
||||
- `Z` (int) `[JsonRequired]` — slippy zoom level. Wire name `"z"`.
|
||||
- `X` (int) `[JsonRequired]` — slippy x at that zoom. Wire name `"x"`.
|
||||
- `Y` (int) `[JsonRequired]` — slippy y at that zoom. Wire name `"y"`.
|
||||
- Defined in `SatelliteProvider.Common/DTO/TileInventory.cs`. Matches `tile-inventory.md` v2.0.0 Shape (the rename from `tileZoom/tileX/tileY` shipped in AZ-794; the `[JsonRequired]` markers + the global `UnmappedMemberHandling.Disallow` mean missing axes and the legacy field names both surface as HTTP 400 with `ValidationProblemDetails` per `error-shape.md` v1.0.0).
|
||||
|
||||
### TileInventoryRequest (added AZ-505)
|
||||
API request body for `POST /api/satellite/tiles/inventory`. Carries one of two XOR-exclusive batch shapes.
|
||||
- `Tiles` (`IReadOnlyList<TileCoord>?`) — Form A: coords-by-value. The server computes `location_hash = Uuidv5(TileNamespace, "{z}/{x}/{y}")` per entry.
|
||||
- `LocationHashes` (`IReadOnlyList<Guid>?`) — Form B: hashes-by-reference. Used when the caller already has UUIDv5 location hashes (typical for the onboard cross-repo path).
|
||||
- Exactly one of `Tiles` / `LocationHashes` must be populated and non-empty; both-populated or neither → HTTP 400 (`tile-inventory.md` Inv-1).
|
||||
- Exactly one of `Tiles` / `LocationHashes` must be populated and non-empty; both-populated or neither → HTTP 400 (`tile-inventory.md` v2.0.0 Inv-1, enforced by `InventoryRequestValidator` via `ValidationEndpointFilter<TileInventoryRequest>` in cycle 7).
|
||||
- Total entries (in either field) ≤ `TileInventoryLimits.MaxEntriesPerRequest` (5000); over-cap → HTTP 400 (Inv-7).
|
||||
|
||||
### TileInventoryEntry (added AZ-505)
|
||||
### TileInventoryEntry (added AZ-505, coord fields renamed AZ-794 cycle 7)
|
||||
Per-entry result inside `TileInventoryResponse`. One entry per request entry, in the SAME order as the request (`tile-inventory.md` Inv-2).
|
||||
- `Z`, `X`, `Y` (int) — echoed coord triple matching the request entry; wire names `"z"`, `"x"`, `"y"` (renamed from `"tileZoom"`/`"tileX"`/`"tileY"` by AZ-794). Always populated; when Form B was used, these are 0 (the caller already knows the hash).
|
||||
- `LocationHash` (Guid) — always populated; UUIDv5 of `"{z}/{x}/{y}"` from `Uuidv5.LocationHashForTile` (Form A) or echoed from request (Form B).
|
||||
- `Present` (bool) — `true` iff a row exists in `tiles` with this `location_hash` (Inv-4).
|
||||
- `Id` (Guid?) — `tiles.id` of the most-recent row across sources/flights (`captured_at DESC, updated_at DESC, id DESC`, Inv-5); null when `Present=false` (Inv-6).
|
||||
@@ -135,7 +137,7 @@ API response body for `POST /api/satellite/tiles/inventory`.
|
||||
- `Results` (`IReadOnlyList<TileInventoryEntry>`) — one entry per request entry; `Results.Count` always equals the request entry count (Inv-2).
|
||||
|
||||
### TileInventoryLimits (added AZ-505, static constants)
|
||||
- `MaxEntriesPerRequest = 5000` — request-body cap enforced by the inventory handler (Inv-7).
|
||||
- `MaxEntriesPerRequest = 5000` — request-body cap enforced by `InventoryRequestValidator` (per-array cap; `tile-inventory.md` v2.0.0 Inv-7).
|
||||
|
||||
## Internal Logic
|
||||
- `GeoPoint` uses a precision tolerance of `0.00005` degrees (~5.5 meters) for equality comparison.
|
||||
|
||||
@@ -15,6 +15,9 @@ Console application that runs end-to-end integration tests against a live API in
|
||||
- `JwtIntegrationTests` (added by AZ-487 cycle 2; helpers consolidated by AZ-491 cycle 3; iss/aud scenarios added by AZ-494 cycle 3) — `AnonymousRequest_To_AnyEndpoint_Returns401`, `ExpiredToken_Returns401`, `InvalidSignature_Returns401`, `ValidToken_Returns200_OnHealthyEndpoint`, `WrongIssuer_Returns401` (AZ-494 AC-1), `WrongAudience_Returns401` (AZ-494 AC-2), `SwaggerDocument_AdvertisesBearerSecurityScheme`. HS256 token minting lives in the shared `SatelliteProvider.TestSupport.JwtTokenFactory` (consumed via `ProjectReference`); runner-specific concerns (`JwtTestHelpers.ResolveSecretOrThrow` / `ResolveIssuerOrThrow` / `ResolveAudienceOrThrow`, `MintAuthenticated` / `MintExpired` convenience wrappers that auto-fill iss+aud from env, `AttachDefaultAuthorization`, `DefaultSubject = "integration-tests"`) remain in this project. The test runner sets `JWT_SECRET` + `JWT_ISSUER` + `JWT_AUDIENCE` on the API container and attaches a Bearer token (with matching iss/aud) to every existing test's HTTP requests so the pre-cycle-2 suite continues to pass.
|
||||
- `UavUploadTests` (added by AZ-488, cycle 2; coordinate-counter promoted to defense-in-depth by AZ-493 cycle 3; AZ-503 cycle 5 added 2 more tests) — `HappyPathSingleItem_PersistsRow`, `MixedBatch_ReturnsPerItemResults`, `MultiSourceCoexistence_AZ484_Cycle2`, `SameSourceUpsert_AZ484_Cycle2`, `NoToken_Returns401`, `ValidTokenWithoutGpsPermission_Returns403`, `OversizedBatch_Returns400`, plus AZ-503: `MultiFlightUavRowsCoexist_AZ503_AC3` (two flights at the same cell → two rows, one `location_hash`, two `file_path`s under `./tiles/uav/{flight_id}/...`) and `FloatRoundingDoesNotBreakIdempotence_AZ503_AC4` (two uploads with float-distinct `latitude` recomputed from `TileToWorldPos` collapse to a single row because the conflict key is integer-only). The AZ-503 migration made `location_hash NOT NULL`, so the cycle-2 `MultiSourceCoexistence_AZ484_Cycle2` seeder was updated to compute `location_hash` via `Uuidv5.Create` (canonical name `"{zoom}/0/0"`) before the raw SQL `INSERT` — this required adding a `ProjectReference` from `SatelliteProvider.IntegrationTests` to `SatelliteProvider.Common`. The wall-clock-seeded `_coordinateCounter` is retained as a belt-and-suspenders safeguard alongside the AZ-493 startup DB-reset (below) — if a developer runs with `--keep-state`, or the DB-reset path is skipped for any reason, the wall-clock seed still spreads coordinates across runs so the unique index does not collide.
|
||||
- `StubAndErrorContractTests` (existing) — updated in cycle 2 to drop the legacy `StubUpload_Returns501` expectation since AZ-488 implemented the endpoint.
|
||||
- `TileInventoryTests` (added cycle 6 — AZ-505) — `OrderingAndPresentAbsentShaping_AC1`, `LeafletReadReturnsMostRecentViaLocationHash_AC2`, `ValidationRejectsBothPopulated_AC6`, `ValidationRejectsNeitherPopulated_AC6`, `ValidationRejectsOversizedBatch_AC6`, `UnauthenticatedRequestReturns401_AC6`, `PerformanceBudget_AC4` (full-suite only). Tests are cycle-7-stable — they use the post-AZ-794 `{z, x, y}` wire shape and a minor x/y reduction was applied in cycle 7 to keep the synthetic coords within the z=18 slippy bounds enforced by `TileCoordValidator`.
|
||||
- `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.
|
||||
|
||||
### Supporting Classes
|
||||
- `Models.cs` — HTTP response DTOs for deserialization
|
||||
@@ -30,6 +33,7 @@ Console application that runs end-to-end integration tests against a live API in
|
||||
- Token *minting* lives in the shared `SatelliteProvider.TestSupport.JwtTokenFactory` (AZ-491) — runner-side concerns (env reads, HttpClient mutation, the iss/aud-aware mint wrapper) deliberately stay here.
|
||||
- `IntegrationTestDatabaseReset.cs` (AZ-493) — instance class with a single `EnsureCleanStateAsync()` method that truncates the integration-test target tables in FK-safe order. Guarded via `SatelliteProvider.TestSupport.IntegrationTestResetGuard` (env + Host allowlist) so it cannot run against a non-test database.
|
||||
- `PerfBootstrap.cs` (AZ-492) — static helpers for the perf harness bootstrap subcommands. `MintToken()` mints a 4-hour HS256 token with subject `perf-tests` and a `permissions: GPS` claim via the canonical `SatelliteProvider.TestSupport.JwtTokenFactory.Create`; `GenerateUavFixture(args)` writes a 256×256 random-noise JPEG via `SixLabors.ImageSharp` to the path passed on the CLI. Invoked from `scripts/run-performance-tests.sh` via `dotnet <SatelliteProvider.IntegrationTests.dll> --mint-only` and `--gen-uav-fixture <path>`.
|
||||
- `ProblemDetailsAssertions.cs` (added cycle 7 — AZ-795) — shared static helpers for asserting RFC 7807 ProblemDetails bodies on integration-test responses. `ReadProblemDetailsAsync(HttpResponseMessage, label)` deserialises the response body into a `JsonElement` with helpful failure messages when the content-type / shape doesn't match. `AssertProblemDetails(problem, expectedStatus, label)` asserts the base ProblemDetails shape (`type`, `title`, `status`). `AssertValidationProblem(problem, expectedStatus, label, expectedErrorPath?, expectedErrorContains?)` extends the base assertion to require the `errors` map per `error-shape.md` Inv-2 and optionally checks a specific field path / message substring. Consumed by `TileInventoryValidationTests`; designed to be reused by every future per-endpoint child task under AZ-795.
|
||||
|
||||
## Internal Logic
|
||||
- Makes HTTP calls to the API at `API_URL` environment variable (default: `http://api:8080`)
|
||||
|
||||
@@ -23,17 +23,22 @@ Existing baseline (pre-cycle-2) test classes cover `TileService`, `RegionService
|
||||
- `Uuidv5Tests` — pure-C# UUIDv5 generator parity tests. `Create_MatchesPythonReferenceVectors_AC1` pins 10 reference vectors generated by Python's `uuid.uuid5(TILE_NAMESPACE, name)`; `Create_IsDeterministic` asserts repeated runs return the same `Guid`; `Create_SetsVersionAndVariantBits` asserts the version nibble is `5` and the variant top-2-bits are `10` (RFC 9562 §5.5).
|
||||
- `UavTileFilePathTests` (rewritten for AZ-503 from the cycle-2 placeholder) — covers `BuildUavTileFilePath(Guid? flightId, int z, int x, int y)` across three cases: `BuildUavTileFilePath_AnonymousFlight_UsesNoneSegment` (null `flightId` → literal `none` segment), `BuildUavTileFilePath_PerFlight_UsesFlightIdDirectory` (per-flight segment), `BuildUavTileFilePath_DifferentFlights_ProduceDifferentPaths` (path-distinctness across flights at the same cell). Integer-typed coordinates and the `Guid? flightId` parameter together still preclude string-injection path traversal.
|
||||
|
||||
### AZ-795 + AZ-796 — strict inventory validation (cycle 7)
|
||||
- `Validators/InventoryRequestValidatorTests` (added cycle 7 — AZ-796) — 16 tests against `InventoryRequestValidator` + `TileCoordValidator` in isolation via FluentValidation's `TestValidate(...)` test helper. Covers every `RuleFor(...)`: `Validate_TilesPopulated_LocationHashesNull_Passes`, `Validate_LocationHashesPopulated_TilesNull_Passes`, `Validate_BothPopulated_FailsXorRule`, `Validate_NeitherPopulated_FailsXorRule`, `Validate_BothEmpty_FailsXorRule`, `Validate_TilesAtCap_Passes`, `Validate_TilesOverCap_FailsCapRule`, `Validate_LocationHashesOverCap_FailsCapRule`, `Validate_TileZoomOutOfRange_FailsRangeRule` (`[Theory]` with z ∈ {-1, 23, 100}), `Validate_TileZoomInRange_PassesRangeRule` (`[Theory]` with z ∈ {0, 18, 22}), `Validate_TileXNegative_FailsRangeRule`, `Validate_TileXAtUpperBound_FailsRangeRule`, `Validate_TileYNegative_FailsRangeRule`, `Validate_TileYAtUpperBound_FailsRangeRule`, `Validate_AxesAtMaxForZoom_Passes`.
|
||||
- `TestSupport/ValidatorTestModuleInitializer.cs` (added cycle 7 — AZ-795) — `[ModuleInitializer]` that calls `GlobalValidatorConfig.ApplyOnce()` at test-assembly load time. Ensures unit tests see the same camelCase property-name resolution that `Program.cs` configures for the running API, so validator error keys (e.g., `tiles[0].z`) match the runtime contract per `error-shape.md` v1.0.0 Inv-4 without forcing every test to re-run the setup.
|
||||
|
||||
## Internal Logic
|
||||
- Tests follow Arrange / Act / Assert. Time-dependent paths inject a `FixedTimeProvider` (cycle-2 addition) so Rule 4 has deterministic age windows.
|
||||
- `JwtSecurityTokenHandler.MapInboundClaims = false` is set explicitly in JWT tests so claims read by their original names (`sub`, `permissions`, …) rather than the framework-remapped names.
|
||||
- Cycle 7 also added validator-isolated assertions via FluentValidation's `TestValidate(...)` helper (no HTTP, no DI container) — the matching end-to-end assertions live in `SatelliteProvider.IntegrationTests/TileInventoryValidationTests.cs`.
|
||||
|
||||
## Dependencies
|
||||
- Project references: `SatelliteProvider.Services.TileDownloader`, `SatelliteProvider.Services.RegionProcessing`, `SatelliteProvider.Services.RouteManagement`, `SatelliteProvider.Common`, `SatelliteProvider.DataAccess`, `SatelliteProvider.Api` (for the Authentication tests — added in AZ-487), `SatelliteProvider.TestSupport` (added by AZ-491; provides the canonical `JwtTokenFactory` consumed by both this project and `SatelliteProvider.IntegrationTests`).
|
||||
- NuGet: xUnit (2.5.3), Moq (4.20.72), FluentAssertions (8.8.0), coverlet.collector (6.0.0), Microsoft.NET.Test.Sdk (17.8.0), Microsoft.Extensions.* (Caching.Memory, Configuration, DI, Logging, Options, Http — all bumped from 9.0.10 → 10.0.7 by AZ-500 as a coordinated cycle-4 move), `Microsoft.AspNetCore.Authentication.JwtBearer` 10.0.7 (consumed transitively via the `ProjectReference` to `SatelliteProvider.Api`; AZ-487 added the dependency at 8.0.21, AZ-496 bumped it to 8.0.25, AZ-500 bumped it to 10.0.7), `SixLabors.ImageSharp` 3.1.11 (added by AZ-488 for the gate tests).
|
||||
- NuGet: xUnit (2.5.3), Moq (4.20.72), FluentAssertions (8.8.0), coverlet.collector (6.0.0), Microsoft.NET.Test.Sdk (17.8.0), Microsoft.Extensions.* (Caching.Memory, Configuration, DI, Logging, Options, Http — all bumped from 9.0.10 → 10.0.7 by AZ-500 as a coordinated cycle-4 move), `Microsoft.AspNetCore.Authentication.JwtBearer` 10.0.7 (consumed transitively via the `ProjectReference` to `SatelliteProvider.Api`; AZ-487 added the dependency at 8.0.21, AZ-496 bumped it to 8.0.25, AZ-500 bumped it to 10.0.7), `SixLabors.ImageSharp` 3.1.11 (added by AZ-488 for the gate tests), `FluentValidation` + `FluentValidation.TestHelper` 12.0.0 (added cycle 7 — AZ-795; the test helper drives the `TestValidate(...)` assertions used by `InventoryRequestValidatorTests`).
|
||||
- `appsettings.json` copied to output (used by Authentication tests for the `Jwt` section binding scenario).
|
||||
|
||||
## Consumers
|
||||
- CI pipeline (`01-test.yml`) and `scripts/run-tests.sh --unit-only` run `dotnet test` against this project.
|
||||
|
||||
## Tests
|
||||
This IS the test module. Cycle-2 added ~25 unit tests on top of the existing baseline; cycle-5 (AZ-503) added 6 more (3 in `Uuidv5Tests`, 3 in `UavTileFilePathTests`) plus 2 new methods in `UavTileUploadHandlerTests`. The full project executes in seconds (no external services required).
|
||||
This IS the test module. Cycle-2 added ~25 unit tests on top of the existing baseline; cycle-5 (AZ-503) added 6 more (3 in `Uuidv5Tests`, 3 in `UavTileFilePathTests`) plus 2 new methods in `UavTileUploadHandlerTests`. Cycle 7 (AZ-795 + AZ-796) added 16 more in `InventoryRequestValidatorTests` covering every `RuleFor(...)` in the cycle's new validators. The full project executes in seconds (no external services required). Cycle 7 Step 11 reported the unit suite at 311 tests, all green.
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
# Cycle 7 — Documentation Ripple Log
|
||||
|
||||
**Cycle**: 7 (AZ-794 z/x/y rename + AZ-795 strict-validation epic + AZ-796 inventory-endpoint validation)
|
||||
**Generated by**: `/document` skill (task mode) during autodev Step 13
|
||||
**Resolution method**: `Grep --type cs` against every new or changed symbol introduced by the three tasks. C# `using`-based import analysis on `TileCoord` (renamed fields + `[JsonRequired]`), `InventoryRequestValidator`, `ValidationEndpointFilter<T>`, `GlobalExceptionHandler`, `GlobalValidatorConfig`, plus `ProblemDetailsAssertions` and `ValidatorTestModuleInitializer` in the test projects. No static-analyzer (NDepend, etc.) was used — the new surface is shallow and lives almost entirely behind `Program.cs` + the two new test files, so the literal usage scan is exhaustive.
|
||||
|
||||
## Directly-changed source files (cycle 7)
|
||||
|
||||
- `SatelliteProvider.Common/DTO/TileInventory.cs` (AZ-794, modified) — `TileCoord` properties renamed `TileZoom/TileX/TileY` → `Z/X/Y` with `[JsonRequired]` on each; `TileInventoryEntry` echo fields renamed in lockstep. Wire field names are `z`/`x`/`y` per the camelCase resolver.
|
||||
- `SatelliteProvider.Api/Program.cs` (AZ-795 + AZ-796, modified) —
|
||||
- `ConfigureHttpJsonOptions(o => o.SerializerOptions.UnmappedMemberHandling = JsonUnmappedMemberHandling.Disallow)` (AZ-795).
|
||||
- `AddProblemDetails(...)` global ProblemDetails configurator (AZ-795).
|
||||
- `AddExceptionHandler<GlobalExceptionHandler>()` + `UseExceptionHandler()` middleware order (AZ-795).
|
||||
- `AddValidatorsFromAssemblyContaining<Program>()` + `GlobalValidatorConfig.ApplyOnce()` at startup (AZ-795 + AZ-796).
|
||||
- `.WithValidation<TileInventoryRequest>()` on the `MapPost("/api/satellite/tiles/inventory", …)` builder (AZ-796).
|
||||
- Endpoint summary / description bumped to reference `tile-inventory.md` v2.0.0 + `error-shape.md` v1.0.0.
|
||||
- `SatelliteProvider.Api/Validators/InventoryRequestValidator.cs` (AZ-796, **new**) — `AbstractValidator<TileInventoryRequest>` + nested `TileCoordValidator` with 9 rules (XOR, per-array cap, Z/X/Y ranges).
|
||||
- `SatelliteProvider.Api/Validators/ValidationEndpointFilter.cs` (AZ-795, **new**) — generic `IEndpointFilter<T>` that runs the registered `IValidator<T>` and emits `Results.ValidationProblem(...)` on failure.
|
||||
- `SatelliteProvider.Api/Validators/ValidationEndpointFilterExtensions.cs` (AZ-795, **new**) — opt-in `RouteHandlerBuilder.WithValidation<T>()` extension; intentionally orthogonal to per-endpoint authorization configuration.
|
||||
- `SatelliteProvider.Api/Validators/GlobalValidatorConfig.cs` (AZ-795 + AZ-796, **new**) — idempotent `ApplyOnce()` configures FluentValidation's global `PropertyNameResolver` to camelCase (`tiles[0].z` instead of `Tiles[0].Z`) per `error-shape.md` Inv-4. Called from `Program.cs` and from the unit-test assembly's `[ModuleInitializer]`.
|
||||
- `SatelliteProvider.Api/GlobalExceptionHandler.cs` (AZ-795, **new**) — `IExceptionHandler` that intercepts `BadHttpRequestException(JsonException)` (the System.Text.Json strict-parse path: unknown fields, `[JsonRequired]` violations, type mismatches) and emits the same `ValidationProblemDetails` shape that FluentValidation produces. 5xx paths pass through with sanitised body + correlation id (continuation of the AZ-353 contract).
|
||||
- `_docs/02_document/contracts/api/error-shape.md` (AZ-795, **new**) — v1.0.0 uniform error-body contract. Single source of truth for the `ValidationProblemDetails` wire shape across both layers and across all future child tickets of the AZ-795 epic.
|
||||
- `_docs/02_document/contracts/api/tile-inventory.md` (AZ-794 + AZ-796, modified) — bumped to v2.0.0; documents the 9 validation rules + the `z/x/y` rename; `Producer task` block extended to credit AZ-505 + AZ-794 + AZ-796.
|
||||
- `SatelliteProvider.Tests/Validators/InventoryRequestValidatorTests.cs` (AZ-796, **new**) — 16 unit tests against the validator via `TestValidate(...)`.
|
||||
- `SatelliteProvider.Tests/TestSupport/ValidatorTestModuleInitializer.cs` (AZ-795, **new**) — calls `GlobalValidatorConfig.ApplyOnce()` at test-assembly load.
|
||||
- `SatelliteProvider.IntegrationTests/ProblemDetailsAssertions.cs` (AZ-795, **new**) — shared response-shape helper consumed by every future per-endpoint validation test.
|
||||
- `SatelliteProvider.IntegrationTests/TileInventoryValidationTests.cs` (AZ-796, **new**) — 16 end-to-end tests; one per validation rule (with sub-cases) plus a happy path.
|
||||
- `SatelliteProvider.IntegrationTests/TileInventoryTests.cs` (AZ-794, modified) — updated `tileZoom/tileX/tileY` JSON payloads to `z/x/y`; reduced the synthetic x/y values to stay inside the slippy-map bounds enforced by `TileCoordValidator`.
|
||||
- `SatelliteProvider.IntegrationTests/IdempotentPostTests.cs` (AZ-795, modified) — route-point payload PascalCase → camelCase (`lat`/`lon`) because the post-cycle-7 strict deserializer no longer silently drops the wrong field names that the test had been sending pre-cycle 7.
|
||||
- `scripts/probe_inventory_validation.sh` (AZ-796, **new**) — manual probe script; exercises each failure mode end-to-end and captures responses for change-review evidence.
|
||||
|
||||
## Importer scan results
|
||||
|
||||
| Symbol | Importer count | Importer files | Component touched |
|
||||
|--------|----------------|----------------|-------------------|
|
||||
| `TileCoord.Z` / `TileCoord.X` / `TileCoord.Y` (renamed properties; wire names `z`/`x`/`y`) | 5 | `TileService.cs` (`Uuidv5.LocationHashForTile`), `TileInventoryTests.cs`, `TileInventoryValidationTests.cs`, `InventoryRequestValidatorTests.cs`, `TileInventory.cs` self-references in `TileInventoryEntry` | TileDownloader (production), Tests (unit + integration) |
|
||||
| `[JsonRequired]` on `TileCoord.Z/X/Y` | n/a | enforced at runtime by `System.Text.Json` + caught by `GlobalExceptionHandler` (no compile-time consumer) | WebApi (deserializer + handler) |
|
||||
| `InventoryRequestValidator` / `TileCoordValidator` | 3 | `Program.cs` (assembly-scan registration via `AddValidatorsFromAssemblyContaining<Program>()`), `InventoryRequestValidatorTests.cs`, `TileInventoryValidationTests.cs` (indirect through the running API) | WebApi (production), Tests (unit + integration) |
|
||||
| `ValidationEndpointFilter<T>` / `WithValidation<T>()` | 1 (current) + N-future | `Program.cs` (`MapPost("/api/satellite/tiles/inventory", …).WithValidation<TileInventoryRequest>()`) | WebApi |
|
||||
| `GlobalValidatorConfig.ApplyOnce` | 2 | `Program.cs` (production), `ValidatorTestModuleInitializer.cs` (unit-test assembly load) | WebApi, Tests (unit) |
|
||||
| `GlobalExceptionHandler` | 1 | `Program.cs` (DI registration + middleware order) | WebApi |
|
||||
| `ProblemDetailsAssertions.AssertValidationProblem` | 1 (current) + N-future | `TileInventoryValidationTests.cs`; designed to be reused by every future per-endpoint child task under AZ-795 | Tests (integration) |
|
||||
| `FluentValidation` package (12.0.0) | 4 | `SatelliteProvider.Api.csproj`, `SatelliteProvider.Tests.csproj`, `InventoryRequestValidator.cs`, `InventoryRequestValidatorTests.cs` | WebApi, Tests (unit) |
|
||||
|
||||
## Doc refresh decisions
|
||||
|
||||
All importers land inside components that already received targeted updates during Step 10 (Implement) and this Step 13:
|
||||
|
||||
- **WebApi (`Program.cs`)** — updated `_docs/02_document/modules/api_program.md` with the new endpoint description, the new `Api/Validators` section (filter + extensions + validator + global config), the new `Api/GlobalExceptionHandler` section, expanded DI registration (ProblemDetails + GlobalExceptionHandler + strict JSON + FluentValidation), and the new dependency entries.
|
||||
- **Common (DTOs)** — updated `_docs/02_document/modules/common_dtos.md`: `TileCoord` now documents the rename + `[JsonRequired]` markers + ValidationProblemDetails fallout; `TileInventoryRequest` documents the XOR enforcement by `InventoryRequestValidator`; `TileInventoryEntry` documents the rename echo; `TileInventoryLimits` documents the validator as the enforcer.
|
||||
- **Validators (new subfolder)** — captured under `module-layout.md` with two new entries:
|
||||
- `Api/Validators/{InventoryRequestValidator,TileCoordValidator,ValidationEndpointFilter,ValidationEndpointFilterExtensions,GlobalValidatorConfig}`.
|
||||
- `Api/GlobalExceptionHandler`.
|
||||
- **Tests (unit)** — updated `_docs/02_document/modules/tests_unit.md` with the new "AZ-795 + AZ-796 — strict inventory validation (cycle 7)" subsection, the new `[ModuleInitializer]` helper, the new FluentValidation/TestHelper NuGet entry, and the cycle 7 unit-suite totals (311 tests).
|
||||
- **Tests (integration)** — updated `_docs/02_document/modules/tests_integration.md`: new `TileInventoryValidationTests` entry, new `ProblemDetailsAssertions` helper entry, cycle-7 stability note on `TileInventoryTests`, and the cycle-7 fix on `IdempotentPostTests` (payload rename forced by strict deserializer).
|
||||
|
||||
System-level docs also updated this pass:
|
||||
|
||||
- `architecture.md` — already carries the new "§ 9 Input Validation (AZ-795)" section (was added during the implementation phase along with the validator coverage table). No further changes needed; the AZ-794 wire-format rename is captured at the inventory-contract level rather than in architecture prose.
|
||||
- `system-flows.md` — F8 flow header updated to credit cycle 7; sequence diagram annotated with the two new validation gates (deserializer + filter); Validation Surface table expanded from 4 rows to 13 rows covering every failure mode from `error-shape.md`.
|
||||
- `glossary.md` — `Tile Inventory` entry updated to v2.0.0 wire shape + cite the cycle-7 validator; added three new entries: `Validation Problem Details`, `FluentValidation`, `Unmapped Member Handling`.
|
||||
- `module-layout.md` — Last Updated bumped + cycle-7 changelog line prepended.
|
||||
- `tests/blackbox-tests.md` and `tests/traceability-matrix.md` — updated during Step 12 (Test-Spec Sync): BT-27 added + 12 AC rows added (AZ-794 AC-1..AC-4 + AZ-795 epic-level + AZ-796 AC-1..AC-7) + Coverage Summary refresh.
|
||||
|
||||
## No-ripple components
|
||||
|
||||
These components were NOT touched by cycle-7 changes and require no doc update:
|
||||
|
||||
- **DataAccess** — no schema or repository signature changes in cycle 7. The cycle-6 `tiles_leaflet_path` covering index and the cycle-5 identity columns are unaffected by the wire-format rename or by the new validators.
|
||||
- **TileDownloader (`TileService.GetInventoryAsync`)** — the algorithm is unchanged: it still computes `Uuidv5.LocationHashForTile(z, x, y)` per coord. Only the *property names* on the DTO changed (`TileZoom` → `Z`, etc.); the value contract is identical.
|
||||
- **RegionProcessing / RouteManagement** — no imports against cycle-7 symbols.
|
||||
- **DataAccess migrations** — no new migration in cycle 7; the existing identity columns and indices already carry the production load.
|
||||
|
||||
## Parse-failure / heuristic notes
|
||||
|
||||
None — every symbol resolved via direct `Grep --type cs`. No fallback heuristic was needed. The cycle 7 surface is intentionally narrow (3 tasks, all WebApi-layer concerns) which keeps the ripple log short.
|
||||
|
||||
## AZ-795 epic posture
|
||||
|
||||
The `architecture.md` § 9 table classifies all other public endpoints as `partial` and tags them as "future AZ-795 child" — the epic remains open. Cycle 7 lands the shared infrastructure + the first per-endpoint application (AZ-796). Subsequent child tickets will reuse `ValidationEndpointFilter<T>`, `ProblemDetailsAssertions`, and the `error-shape.md` contract without adding new infrastructure.
|
||||
@@ -327,11 +327,11 @@ sequenceDiagram
|
||||
|
||||
---
|
||||
|
||||
## Flow F8: Tile Inventory Bulk Lookup (added AZ-505)
|
||||
## Flow F8: Tile Inventory Bulk Lookup (added AZ-505; renamed + strict-validated AZ-794+AZ-795+AZ-796, cycle 7)
|
||||
|
||||
### Description
|
||||
|
||||
Programmatic clients (httpx `http2=True`, .NET `HttpClient`, onboard cross-repo callers) post a batch of up to 5000 `(z, x, y)` triples (Form A) or up to 5000 pre-computed `location_hash` UUIDs (Form B) and get one inventory entry per input slot, in the same order. Each entry says whether the cell is present and — when present — the most-recent row's `id`, `capturedAt`, `source`, `flightId`, and `resolutionMPerPx`. No tile bodies are returned; the caller subsequently fetches bodies via F7. This is the read-half of the bulk-list contract that the onboard `gps-denied-onboard` workspace consumes to decide which Google-Maps cells it needs and which UAV variants are already on the server.
|
||||
Programmatic clients (httpx `http2=True`, .NET `HttpClient`, onboard cross-repo callers) post a batch of up to 5000 `{z, x, y}` triples (Form A; the wire field names were renamed from `tileZoom/tileX/tileY` by AZ-794, cycle 7) or up to 5000 pre-computed `location_hash` UUIDs (Form B) and get one inventory entry per input slot, in the same order. Each entry says whether the cell is present and — when present — the most-recent row's `id`, `capturedAt`, `source`, `flightId`, and `resolutionMPerPx`. No tile bodies are returned; the caller subsequently fetches bodies via F7. This is the read-half of the bulk-list contract that the onboard `gps-denied-onboard` workspace consumes to decide which Google-Maps cells it needs and which UAV variants are already on the server.
|
||||
|
||||
### Sequence Diagram
|
||||
|
||||
@@ -345,8 +345,9 @@ sequenceDiagram
|
||||
|
||||
Client->>Kestrel: POST /api/satellite/tiles/inventory (JWT, Form A or B)
|
||||
Kestrel->>GetTilesInventory: route match
|
||||
GetTilesInventory->>GetTilesInventory: XOR check (both/neither populated → 400)
|
||||
GetTilesInventory->>GetTilesInventory: cap check (count > 5000 → 400)
|
||||
Note over Kestrel,GetTilesInventory: AZ-795 deserializer guards (UnmappedMemberHandling.Disallow + [JsonRequired])<br/>catch unknown / missing / type-mismatched fields → 400 ValidationProblemDetails<br/>via GlobalExceptionHandler (cycle 7)
|
||||
GetTilesInventory->>GetTilesInventory: ValidationEndpointFilter<TileInventoryRequest><br/>(InventoryRequestValidator — AZ-796 cycle 7)
|
||||
Note over GetTilesInventory: 9 business rules: XOR / non-empty / per-array cap / Z range / X+Y range
|
||||
GetTilesInventory->>TileService: GetInventoryAsync(request)
|
||||
Note over TileService: Form A: compute location_hash per coord<br/>via Uuidv5.LocationHashForTile<br/>Form B: echo caller-supplied hashes
|
||||
TileService->>TileRepo: GetTilesByLocationHashesAsync(hashes)
|
||||
@@ -357,15 +358,26 @@ sequenceDiagram
|
||||
GetTilesInventory-->>Client: 200 OK, JSON (results in input order)
|
||||
```
|
||||
|
||||
### Validation Surface
|
||||
### Validation Surface (post-cycle 7 — AZ-795 + AZ-796)
|
||||
|
||||
| Input | Detection | Response |
|
||||
|-------|-----------|----------|
|
||||
| Both `tiles` and `locationHashes` populated | Handler XOR check | 400 + ProblemDetails (`tile-inventory.md` Inv-1) |
|
||||
| Neither populated | Handler XOR check | 400 + ProblemDetails |
|
||||
| `count > 5000` (`TileInventoryLimits.MaxEntriesPerRequest`) | Handler cap check | 400 + ProblemDetails (Inv-7) |
|
||||
| Empty / missing body | System.Text.Json (`[JsonRequired]` on `Tiles`/`LocationHashes` covered indirectly via `InventoryRequestValidator`) | 400 + `ValidationProblemDetails` |
|
||||
| Both `tiles` and `locationHashes` populated | `InventoryRequestValidator` `.Must(...)` (Rule 1, XOR — `tile-inventory.md` v2.0.0 Inv-1) | 400 + `ValidationProblemDetails`, key `""` |
|
||||
| Neither populated | `InventoryRequestValidator` `.Must(...)` (Rule 1, XOR) | 400 + `ValidationProblemDetails`, key `""` |
|
||||
| `tiles` or `locationHashes` array is empty | `InventoryRequestValidator` `.Must(...)` (Rule 1, XOR — non-empty arm) | 400 + `ValidationProblemDetails`, key `""` |
|
||||
| `tiles.Count > 5000` (`TileInventoryLimits.MaxEntriesPerRequest`) | `InventoryRequestValidator` `.Must(t => t.Count <= 5000)` (Rule 6 — Inv-7) | 400 + `ValidationProblemDetails`, key `tiles` |
|
||||
| `locationHashes.Count > 5000` | `InventoryRequestValidator` `.Must(h => h.Count <= 5000)` (Rule 7 — Inv-7) | 400 + `ValidationProblemDetails`, key `locationHashes` |
|
||||
| `tiles[i].z` missing OR out of range (must be `0..22` inclusive) | `[JsonRequired]` on `Z` (deserializer) + `TileCoordValidator` Rule 4 | 400 + `ValidationProblemDetails`, key `tiles[i].z` |
|
||||
| `tiles[i].x` missing OR `< 0` OR `>= 2^z` | `[JsonRequired]` on `X` (deserializer) + `TileCoordValidator` Rule 5 | 400 + `ValidationProblemDetails`, key `tiles[i].x` |
|
||||
| `tiles[i].y` missing OR `< 0` OR `>= 2^z` | `[JsonRequired]` on `Y` (deserializer) + `TileCoordValidator` Rule 5 | 400 + `ValidationProblemDetails`, key `tiles[i].y` |
|
||||
| Legacy `tileZoom/tileX/tileY` field names | `UnmappedMemberHandling.Disallow` (deserializer; AZ-794 + AZ-795) | 400 + `ValidationProblemDetails`, key `tiles[i].tileZoom` (etc.) |
|
||||
| Unknown root or nested field | `UnmappedMemberHandling.Disallow` (deserializer; AZ-795) | 400 + `ValidationProblemDetails`, key on the unknown path |
|
||||
| Wrong JSON type (e.g. `"z": "18"`) | System.Text.Json type-mismatch (deserializer; AZ-795) | 400 + `ValidationProblemDetails`, key on the offending path |
|
||||
| No `Authorization: Bearer …` header | `.RequireAuthorization()` | 401 before handler runs |
|
||||
|
||||
All 4xx bodies conform to `error-shape.md` v1.0.0. The same `ValidationProblemDetails` shape is emitted whether the failure was caught by the FluentValidation business-rule layer (`InventoryRequestValidator`) or by the deserializer layer (via `GlobalExceptionHandler`). Both layers are unit + integration tested in `SatelliteProvider.Tests/Validators/InventoryRequestValidatorTests` and `SatelliteProvider.IntegrationTests/TileInventoryValidationTests`.
|
||||
|
||||
### Performance
|
||||
|
||||
p95 ≤ 1000 ms for 2500-coord batches (AZ-505 AC-4). Cycle-6 measured: p95=66ms — well under budget. The covering index (`tiles_leaflet_path`) supplies the leading `location_hash` lookup; the projection's columns beyond the INCLUDE list (`id`, `captured_at`, `flight_id`, ...) trigger a bounded heap fetch which is documented and accepted per the AZ-505 NFR.
|
||||
|
||||
@@ -244,3 +244,40 @@ All Cycle-5 UAV scenarios reuse the AZ-488 envelope. The new observable surface
|
||||
**Pass criterion**: All four expected status codes returned; no response leaks server internals.
|
||||
**AC trace**: AZ-505 AC-6.
|
||||
|
||||
---
|
||||
|
||||
## Cycle 7 — AZ-794 / AZ-795 / AZ-796 Strict Validation + z/x/y Rename
|
||||
|
||||
Cycle 7 hardens `POST /api/satellite/tiles/inventory` by combining (a) the OSM-convention rename of body fields from `tileZoom/tileX/tileY` to `z/x/y` (AZ-794), (b) the shared input-validation infrastructure (AZ-795 — FluentValidation + `ValidationEndpointFilter<T>` + `GlobalExceptionHandler` + `JsonSerializerOptions.UnmappedMemberHandling.Disallow`), and (c) the inventory endpoint's nine concrete validation rules (AZ-796). The cycle's wire-shape contract is `tile-inventory.md` v2.0.0; the failure-response contract is `error-shape.md` v1.0.0.
|
||||
|
||||
The cycle introduces no new HTTP routes. Functional positive coverage is unchanged from cycle 6 — `TileInventoryTests.OrderingAndPresentAbsentShaping_AC1` and the rest of the AZ-505 suite continue to assert ordering, present/absent shaping, leaflet selection, HTTP/2, and the perf budget against the post-rename body shape. The new tests below cover the strict-rejection behaviour that pre-cycle-7 silently coerced.
|
||||
|
||||
## BT-27: Inventory Endpoint — Nine Validation Rules with RFC 7807 ProblemDetails
|
||||
|
||||
**Trigger**: A family of `POST /api/satellite/tiles/inventory` calls, each violating exactly ONE validation rule from AZ-796 §"Required validations (9 rules)". One additional sub-case asserts the legacy `tileZoom/tileX/tileY` field names are now rejected (intersection of AZ-794 + AZ-796).
|
||||
**Precondition**: API up; valid JWT attached on every sub-case. `error-shape.md` v1.0.0 + `tile-inventory.md` v2.0.0 frozen.
|
||||
**Expected**: HTTP 400 with `Content-Type: application/problem+json` for every sub-case. The body conforms to the validation-failure shape from `error-shape.md` v1.0.0 (Inv-1 .. Inv-7); the `errors` map names the offending field path using the request body's camelCase. Sub-cases:
|
||||
|
||||
| # | Rule | Trigger excerpt | Expected `errors` key | Test method |
|
||||
|---|------|-----------------|-----------------------|-------------|
|
||||
| 1 | Body present | empty body (zero bytes, `Content-Type: application/json`) | (no `errors` map — framework-level ProblemDetails) | `EmptyBody_Returns400` |
|
||||
| 2a | XOR of `tiles` / `locationHashes` | `{}` (neither populated) | `$` | `NeitherPopulated_Returns400` |
|
||||
| 2b | XOR (both populated) | `{"tiles":[…],"locationHashes":[…]}` | `$` | `BothPopulated_Returns400` |
|
||||
| 3 | `tiles` non-empty | `{"tiles":[]}` | `$` (treated as not-populated by XOR) | `EmptyTilesArray_Returns400` |
|
||||
| 4 | `tiles` ≤ `MaxEntriesPerRequest` (5000) | 5001-entry `tiles` array | `tiles` | `TilesOverCap_Returns400` |
|
||||
| 5a | Required `z` on each entry | `{"tiles":[{"x":1,"y":1}]}` | path mentioning `z` | `MissingZ_Returns400WithFieldPath` |
|
||||
| 5b | Required `x` / `y` on each entry | `{"tiles":[{"z":18}]}` | (validator + JsonRequired report missing axes) | `MissingXAndY_Returns400` |
|
||||
| 6a | Non-negative axis | `{"tiles":[{"z":18,"x":-1,"y":0}]}` | `tiles[0].x` | `NegativeAxis_Returns400` |
|
||||
| 6b | Integer type | `{"tiles":[{"z":"eighteen","x":1,"y":1}]}` | path mentioning the axis | `TypeMismatch_Returns400` |
|
||||
| 7 | `z` in slippy-map range 0..22 | `{"tiles":[{"z":30,"x":0,"y":0}]}` | `tiles[0].z`; message mentions "between 0 and 22" | `ZoomOutOfRange_Returns400WithFieldPath` |
|
||||
| 8a | `x` < 2^z | `{"tiles":[{"z":2,"x":4,"y":0}]}` | `tiles[0].x` | `XBeyondZoomBounds_Returns400` |
|
||||
| 8b | `y` < 2^z | `{"tiles":[{"z":0,"x":0,"y":1}]}` | `tiles[0].y` | `YBeyondZoomBounds_Returns400` |
|
||||
| 9a | Unknown root field | `{"unknownField":42,"tiles":[…]}` | path mentioning `unknownField` | `UnknownRootField_Returns400` |
|
||||
| 9b | Unknown nested field on tile entry | `{"tiles":[{"z":18,"x":1,"y":1,"foo":42}]}` | path mentioning `foo` | `UnknownNestedField_Returns400` |
|
||||
| 9c | Legacy v1 names (`tileZoom/tileX/tileY`) | exact AZ-777 Phase 1 reproduction body | path mentioning `tileZoom` | `OldV1FieldName_Returns400` (cross-listed under AZ-794) |
|
||||
| pos | Happy path with z/x/y | `{"tiles":[{"z":18,"x":1,"y":1}]}` | HTTP 200 — no body shape change vs AZ-505 baseline | `HappyPath_Returns200` |
|
||||
|
||||
**Pass criterion**: Every failure sub-case returns HTTP 400 with the expected `errors` key OR an equivalent RFC 7807 ProblemDetails for the empty-body case; the happy path returns HTTP 200. No sub-case leaks server-internal state (DB strings, secrets, stack traces) per `error-shape.md` Inv-5.
|
||||
**AC trace**: AZ-796 AC-1 (all 9 rules + ProblemDetails shape), AC-2 (happy path); AZ-794 AC-1 (positive z/x/y acceptance — sub-case `pos`), AZ-794 AC-2 (sub-case `9c` proves the old names produce a structured 400, eliminating the silent-coerce-to-0 footgun); AZ-795 (epic-level — every sub-case exercises the shared `ValidationEndpointFilter` + `GlobalExceptionHandler` + `UnmappedMemberHandling.Disallow` infra).
|
||||
**Notes**: The 9 rules split across two enforcement layers — rules 5/6/9 are enforced by the deserializer (`JsonRequired` + `UnmappedMemberHandling.Disallow` + native JSON type validation, see AZ-795 shared infra) and surface as `BadHttpRequestException` → `GlobalExceptionHandler.JsonException` branch; rules 2/3/4/7/8 are enforced by `InventoryRequestValidator` (FluentValidation) via `ValidationEndpointFilter<TileInventoryRequest>`. Both paths produce identically-shaped `ValidationProblemDetails` bodies (`error-shape.md` v1.0.0 invariant).
|
||||
|
||||
|
||||
@@ -109,6 +109,18 @@
|
||||
| AZ-504 AC-2 | PT-08 completes on zero-accepted response (defensive) | Same standalone shell harness (case 4) — `accepted=0, rejected=N` path no longer kills the script | ✓ |
|
||||
| AZ-504 AC-3 | PT-08 summary line prints in full default-parameter perf run | Verified at autodev Step 15 (Performance Test) by running `scripts/run-performance-tests.sh` with `PERF_REPEAT_COUNT=20 PERF_UAV_BATCH_SIZE=10`; pass criterion is the `PT-08 UAV batch upload: PASS p95=Xms / 2000ms (...)` line in the run output | ◐ gate at Step 15 |
|
||||
| AZ-504 AC-4 | Leftover `_docs/_process_leftovers/2026-05-12_perf-cycle3-harness-execution.md` deleted on green full run | Verified at autodev Step 15 by `test -f _docs/_process_leftovers/2026-05-12_perf-cycle3-harness-execution.md` returning non-zero after the green run + commit | ◐ gate at Step 15 |
|
||||
| AZ-794 AC-1 | Inventory request body uses short names `{z, x, y}` (OSM convention) | BT-27 sub-case `pos` (`TileInventoryValidationTests.HappyPath_Returns200`); existing AZ-505 integration suite (`TileInventoryTests.OrderingAndPresentAbsentShaping_AC1` + the AC-6 validation tests already use z/x/y after the rename); deserializer-level proof via BT-27 sub-case `9c` (`OldV1FieldName_Returns400`) — old names now hard-fail | ✓ |
|
||||
| AZ-794 AC-2 | Inventory response body uses short names `{z, x, y}`; all other fields (`locationHash`, `present`, `id`, `capturedAt`, `source`, `flightId`, `resolutionMPerPx`) unchanged byte-for-byte from the pre-rename contract | `TileInventoryTests.OrderingAndPresentAbsentShaping_AC1` (integration; asserts `entry.Z`, `entry.X`, `entry.Y`, `entry.LocationHash`, `entry.Present`, `entry.Id`, `entry.CapturedAt`, `entry.Source` for 25 mixed present/absent entries against the v2.0.0 wire shape) | ✓ |
|
||||
| AZ-794 AC-3 | OpenAPI / Swagger spec declares `z, x, y` (not the old names) as the required coordinate properties | Doc-state AC — verified at Step 13 (Update Docs) review against `/swagger/v1/swagger.json` schema definitions for `TileInventoryRequest` + `TileCoord` | ◐ doc-verified at Step 13 |
|
||||
| AZ-794 AC-4 | `_docs/02_document/contracts/api/tile-inventory.md` bumped to v2.0.0; Migration / Coexistence section names AZ-794 and the breaking-rename | Doc-state AC — `_docs/02_document/contracts/api/tile-inventory.md` v2.0.0 Change Log entry naming AZ-794 (verified at Step 13 Update Docs review) | ✓ |
|
||||
| AZ-795 (epic) | Shared input-validation infra in place: FluentValidation 12.0.0 + `ValidationEndpointFilter<T>` + `GlobalExceptionHandler` + `JsonSerializerOptions.UnmappedMemberHandling.Disallow` + camelCase naming policy + new `error-shape.md` v1.0.0 contract | Structural: `SatelliteProvider.Api/Validators/{ValidationEndpointFilter,GlobalValidatorConfig,ValidationEndpointFilterExtensions}.cs` + `SatelliteProvider.Api/GlobalExceptionHandler.cs` exist; `error-shape.md` v1.0.0 frozen with 8 documented test cases; end-to-end exercise via the AZ-796 test suite (every BT-27 sub-case routes through this infra). Per-endpoint child task tracker (`AZ-796` is the first; siblings to follow) is owned by Jira AZ-795. | ✓ (infra + contract; per-endpoint child coverage tracked individually) |
|
||||
| AZ-796 AC-1 | Each of the 9 validation rules rejects with HTTP 400 + RFC 7807 ProblemDetails; `errors[]` array has single-rule precision (no unrelated rules) | BT-27 (blackbox; sub-cases 1, 2a, 2b, 3, 4, 5a, 5b, 6a, 6b, 7, 8a, 8b, 9a, 9b, 9c — one per rule); `TileInventoryValidationTests.*` (integration: 15 failure tests) + `InventoryRequestValidatorTests.*` (unit: covers the rules expressible at the validator layer in isolation) | ✓ |
|
||||
| AZ-796 AC-2 | Happy path unchanged (HTTP 200 with existing result shape; one entry per requested tile, same ordering, fields preserved) | BT-27 sub-case `pos` (`TileInventoryValidationTests.HappyPath_Returns200`); no regression in existing `TileInventoryTests.OrderingAndPresentAbsentShaping_AC1` (which still passes at cycle 7 Step 11) | ✓ |
|
||||
| AZ-796 AC-3 | `InventoryRequestValidator` lives in its own file under `SatelliteProvider.Api/Validators/`; xUnit class has one test method per `RuleFor(...)` (≥ 9 unit-test methods) | Structural: `SatelliteProvider.Api/Validators/InventoryRequestValidator.cs` exists (74 lines, isolated validator + `TileCoordValidator`); `SatelliteProvider.Tests/Validators/InventoryRequestValidatorTests.cs` contains 16 test methods (`Validate_TilesPopulated_LocationHashesNull_Passes`, `Validate_LocationHashesPopulated_TilesNull_Passes`, `Validate_BothPopulated_FailsXorRule`, `Validate_NeitherPopulated_FailsXorRule`, `Validate_BothEmpty_FailsXorRule`, `Validate_TilesAtCap_Passes`, `Validate_TilesOverCap_FailsCapRule`, `Validate_LocationHashesOverCap_FailsCapRule`, `Validate_TileZoomOutOfRange_FailsRangeRule` ×3, `Validate_TileZoomInRange_PassesRangeRule` ×3, `Validate_TileXNegative_FailsRangeRule`, `Validate_TileXAtUpperBound_FailsRangeRule`, `Validate_TileYNegative_FailsRangeRule`, `Validate_TileYAtUpperBound_FailsRangeRule`, `Validate_AxesAtMaxForZoom_Passes`) | ✓ |
|
||||
| AZ-796 AC-4 | Integration tests cover happy + failure per rule (≥ 10 methods) in `SatelliteProvider.IntegrationTests/TileInventoryValidationTests.cs` | `TileInventoryValidationTests` contains 16 integration test methods (1 happy + 15 failure: `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`, `TypeMismatch_Returns400`) — 16 ≥ 10. Cycle 7 Step 11 full run reports all 16 passing. | ✓ |
|
||||
| AZ-796 AC-5 | `/swagger/v1/swagger.json` marks required fields, declares integer ranges per validation rules, declares 400 response with ProblemDetails schema | Doc-state AC — verified at Step 13 (Update Docs) review against the published OpenAPI document; integration smoke is the existing `JwtIntegrationTests.SwaggerDocument_AdvertisesBearerSecurityScheme` pattern (a future analogous test against the validation schema is out-of-scope this cycle) | ◐ doc-verified at Step 13 |
|
||||
| AZ-796 AC-6 | `_docs/02_document/contracts/api/tile-inventory.md` updated to document the 9 validation rules + error contract reference | Doc-state AC — `_docs/02_document/contracts/api/tile-inventory.md` v2.0.0 Change Log entry naming AZ-796 (verified at Step 13 Update Docs review) | ✓ |
|
||||
| AZ-796 AC-7 | `scripts/probe_inventory_validation.sh` committed; exercises each failure mode via `curl` + JWT for documentation / regression | Structural: `scripts/probe_inventory_validation.sh` exists in repo and is manually runnable | ✓ |
|
||||
|
||||
## Restrictions → Test Mapping
|
||||
|
||||
@@ -158,7 +170,8 @@
|
||||
| Cycle 5 — AZ-503 foundation (integration + unit + blackbox) | 2 integration + 6 unit + 4 blackbox | 7/12 in-scope (AC-1, 2, 3, 4, 7, 8, 11); 5 ACs deferred → AZ-505 (now resolved in cycle 6) | — |
|
||||
| Cycle 5 — AZ-504 perf-script fix (shell harness + Step-15 gate) | 1 standalone shell harness (4 cases) | 2/4 verified now (AC-1, AC-2); 2/4 gated at Step 15 (AC-3, AC-4) | — |
|
||||
| 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). | — |
|
||||
| **Total** | **94** | **63/63 in-scope (100%); 2 AZ-504 ACs gated at Step 15** | **8/8 (100%)** |
|
||||
| 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`. | — |
|
||||
| **Total** | **126** | **75/75 in-scope (100%); 2 AZ-504 ACs gated at Step 15; 2 cycle-7 ACs doc-verified at Step 13** | **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.
|
||||
@@ -186,3 +199,13 @@
|
||||
- AZ-505 AC-5 originally specified h2c (HTTP/2 over plaintext). Kestrel was switched to TLS+ALPN on `https://+:8080` during the cycle-6 Run Tests step because `HttpProtocols.Http1AndHttp2` silently downgrades to HTTP/1.1 over plaintext (no ALPN). The functional gate (multiplexing semantics) is unchanged — the test still asserts `HttpResponseMessage.Version == 2.0` over 20 concurrent GETs on a single connection. The deployment caveat (dev cert vs. production TLS termination at the ingress) is documented in `tile-inventory.md` Non-Goals.
|
||||
- AZ-505 NFRs propagate as follows: Performance (AC-3, AC-4) ⇒ PT-09 entry (full PT-09 row in `performance-tests.md`); Compatibility (existing `GET /tiles/{z}/{x}/{y}` byte-identical) ⇒ no new test — the AZ-484 / AZ-503-foundation selection rule is unchanged, and the test that exercised it under the old `(z, x, y)`-keyed SELECT now exercises it under the `location_hash`-keyed SELECT via AC-2; Security (JWT + `RequireAuthorization()`) ⇒ AC-6 anonymous-401 case, BT-26.
|
||||
- Cycle-update rule check: no NFR conflicts surfaced. The 500 ms → 1000 ms perf budget relaxation between AZ-503 AC-9 and AZ-505 AC-4 is **not** a conflict in the cycle-update sense — AZ-503 AC-9 was explicitly deferred (`◐ deferred → AZ-505`) so AZ-505 owns the binding budget; AZ-503's number was a pre-implementation estimate. The matrix records both numbers and the rationale so the budget history stays auditable.
|
||||
|
||||
**Coverage shape notes (Cycle 7 — AZ-794 + AZ-795 + AZ-796 strict inventory validation + z/x/y rename):**
|
||||
- Cycle 7 is the **first** application of the AZ-795 shared validation infrastructure (FluentValidation 12.0.0 + `ValidationEndpointFilter<T>` + `GlobalExceptionHandler` + `JsonSerializerOptions.UnmappedMemberHandling.Disallow` + camelCase naming policy). The infra is exercised end-to-end by every AZ-796 sub-case — the matrix records `AZ-795 (epic)` as `✓` because the infra is in place and the first concrete child (AZ-796) demonstrates it functions correctly. Sibling per-endpoint child tasks (other public-facing JSON endpoints) will land under AZ-795 in future cycles and each will get its own AC row at that time.
|
||||
- AZ-794 (z/x/y rename) and AZ-796 (strict validation) shipped in the same commit (`865dfdb`). The matrix surfaces this coupling at AZ-794 AC-2 / AZ-796 AC-1 sub-case `9c` — the BT-27 sub-case that POSTs the legacy `{"tileZoom","tileX","tileY"}` payload now returns HTTP 400 with `errors[…]` naming `tileZoom`, proving (a) AZ-794's rename is observable on the wire and (b) AZ-795's `UnmappedMemberHandling.Disallow` catches the old names instead of silently coercing to `(0,0,0)`. The same sub-case carries the AZ-777 Phase 1 reproducer body verbatim — that exact request now fails-fast, closing the original discovery loop.
|
||||
- AZ-794 has no perf, security, or resilience NFRs distinct from AZ-505's. Wire size is reduced ~3× on field names (per AZ-794 spec); not separately measured because the AZ-505 AC-4 p95 budget (1000 ms / 2500 tiles, measured 66 ms in cycle 6) already absorbs the rename with margin. Cycle 7 Step 11 reran the full integration suite (311 unit + integration) green; the AZ-505 perf budget is re-measured at Step 15 of cycle 7 per the existing `◐ gate at Step 15` rows.
|
||||
- AZ-796 AC-3 (validator unit-tested) and AC-4 (integration-tested) each specify a minimum count (≥ 9 unit, ≥ 10 integration). Cycle 7 delivered 16 + 16 = 32 — comfortably over the floor — covering every `RuleFor(…)` in `InventoryRequestValidator` and `TileCoordValidator` plus the JSON-deserializer-level rules (`JsonRequired`, `UnmappedMemberHandling.Disallow`, type mismatch) that don't reach the validator. The split is documented in BT-27 §Notes.
|
||||
- Doc-only ACs (AZ-794 AC-3, AZ-796 AC-5 — OpenAPI / Swagger spec accuracy) are marked `◐ doc-verified at Step 13` because they require inspection of the generated `/swagger/v1/swagger.json` during the Update Docs step. Cycle 7's Swashbuckle output reflects the rename + ranges automatically via the DTOs' `[JsonRequired]` and the validator's `RuleFor` constraints — no manual OpenAPI XML doc edits were needed. Step 13 will verify against the running container's swagger document.
|
||||
- AZ-505 AC-6's existing row (cycle 6 — "Request validation — 400 on both populated, 400 on neither, 400 on > 5000 entries, 401 on anonymous") remains accurate. Its 4 cases overlap with AZ-796 AC-1 sub-cases 2a, 2b, 4, and the anonymous case (also SEC-05). Both rows are kept per cycle-update rule 4 ("Preserve existing traceability IDs"); the duplication is by design — AZ-505 AC-6 was the cycle-6 contract (status-code-only), AZ-796 AC-1 is the cycle-7 contract (status code + ProblemDetails shape + field-path errors). The cycle-7 row is the binding one going forward; the cycle-6 row stays as historical record.
|
||||
- Cycle-update rule check: no NFR conflicts. The 5000-entry cap is reaffirmed (matches AZ-505); the supported zoom range 0..22 is reaffirmed (matches `tile-inventory.md` Inv-7); the error shape contract is **new** (`error-shape.md` v1.0.0) — but no prior cycle declared a different error shape, so this is greenfield content, not a conflict.
|
||||
- Step 10 artifact gap (cycle 7): no `implementation_report_*_cycle7.md` was produced in `_docs/03_implementation/`. The actual implementation evidence lives in commits `dceaddc` (cycle 7 task adoption) + `865dfdb` (cycle 7 Step 10 implementation), in the state file's `detail` field (which recorded the test-run outcome), and in the new test artifacts themselves (`InventoryRequestValidator.cs`, `InventoryRequestValidatorTests.cs`, `TileInventoryValidationTests.cs`, `ProblemDetailsAssertions.cs`, `error-shape.md` v1.0.0). This artifact gap is recorded here for cycle 7 retrospective follow-up — the matrix itself is unaffected because cycle-update mode's source-of-truth is the task specs in `_docs/02_tasks/done/`, not the implementation report.
|
||||
|
||||
Reference in New Issue
Block a user