mirror of
https://github.com/azaion/satellite-provider.git
synced 2026-06-22 11:31:14 +00:00
[AZ-809] Strict validation for POST /api/satellite/route
Third concrete child of AZ-795 (cycle 8 batch 3). FluentValidation +
[JsonRequired] + UnmappedMemberHandling.Disallow combine to reject every
malformed payload at the API boundary with RFC 7807 ValidationProblemDetails.
Validators (SatelliteProvider.Api/Validators/, all new)
- CreateRouteRequestValidator: id non-empty, name/description length,
regionSizeMeters/zoomLevel ranges, points count [2, 500], cross-field
createTilesZip => requestMaps. Chains RoutePointValidator (per-point)
and GeofencePolygonValidator (per-polygon, guarded by When(Geofences != null)).
OverridePropertyName("geofences.polygons") on the geofences chain so
FluentValidation's default leaf-only key policy doesn't drop the parent
path on deep expressions like req.Geofences!.Polygons.
- RoutePointValidator: lat/lon ranges; OverridePropertyName("lat"/"lon")
chained AFTER InclusiveBetween (the extension is defined on
IRuleBuilderOptions<T, TProperty>, so the generic type is only
inferable after the first concrete rule) so error keys match the
wire format (`points[i].lat`) rather than the C# property name
(`points[i].latitude`).
- GeofencePolygonValidator: per-corner range checks via private nested
GeoCornerValidator; cross-field NW.Lat > SE.Lat and NW.Lon < SE.Lon
invariants emit at errors["geofences.polygons[i].northWest"].
DTOs (SatelliteProvider.Common/DTO/, [JsonRequired] additions only)
- CreateRouteRequest: id, name, regionSizeMeters, zoomLevel, points,
requestMaps, createTilesZip
- RoutePoint: Latitude, Longitude
- GeofencePolygon: NorthWest, SouthEast; Geofences: Polygons
- GeoPoint: Lat, Lon
Tests
- Unit: 26 methods total — 16 in CreateRouteRequestValidatorTests, 6 in
GeofencePolygonValidatorTests, 4 in RoutePointValidatorTests. Each
RuleFor/RuleForEach chain has at least one positive + one negative case.
- Integration: CreateRouteValidationTests.cs — 16 methods (happy + 15
failure modes) wired into smoke + full suites. Covers empty body,
missing/zero id, empty name, out-of-range regionSizeMeters/zoomLevel,
points count < 2, per-point lat/lon out-of-range, geofence invariants,
missing requestMaps, cross-field createTilesZip, unknown root field,
nested type mismatch.
- Manual probe: scripts/probe_route_validation.sh curl-exercises every
failure mode end-to-end + happy path.
Docs
- New contract _docs/02_document/contracts/api/route-creation.md v1.0.0
with nested DTO chain, invariants, per-field test cases table, and
advisories on the legacy service-layer RouteValidator + the
input/output RoutePoint vs RoutePointDto naming asymmetry.
- system-flows.md F4 sequence diagram extended with the validation-filter
branch; preconditions + error scenarios reference the new contract.
- modules/api_program.md: CreateRoute handler section added; Api/Validators
bumped to AZ-808/AZ-809/AZ-811.
- modules/common_dtos.md: DTO descriptions updated with [JsonRequired]
annotations and constraint summaries.
- tests/blackbox-tests.md BT-06/BT-N03/BT-N04/BT-N05 align with the new
wire format and named error keys.
- tests/security-tests.md SEC-04 references GlobalExceptionHandler's
JsonException branch + AZ-353 correlationId.
- _docs/03_implementation/batch_03_cycle8_report.md + reviews/batch_03_cycle8_review.md
(PASS_WITH_NOTES — F1 Low: OverridePropertyName documented inline,
F2 + F3 Info: pre-existing advisories for follow-up).
Smoke green (mode=smoke, exit 0). AZ-809 transitioned to In Testing on Jira.
Task file moved to _docs/02_tasks/done/.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,156 +0,0 @@
|
||||
# Strict validation for route-creation endpoint (POST /api/satellite/route)
|
||||
|
||||
**Task**: AZ-809_route_endpoint_validation
|
||||
**Name**: Strict validation for route-creation endpoint
|
||||
**Description**: Add FluentValidation-backed strict input validation to `POST /api/satellite/route` (route creation — client submits ordered waypoints + optional geofence polygons; producer interpolates intermediate points every ≈ 200 m and — if `requestMaps=true` — enqueues a region request per route point for async tile backfill). Reject malformed payloads with RFC 7807 ValidationProblemDetails (HTTP 400). Third concrete child of AZ-795; reuses the shared infra wired in cycle 7.
|
||||
**Complexity**: 5 points (14 rules — was 13 before the 2026-05-22 probe added the `Id` rule; 3 validator classes; cross-field constraint; new contract doc)
|
||||
**Dependencies**: AZ-795 (HARD — shared infra); AZ-796 (single-DTO reference); AZ-808 (no-prior-contract pattern, same batch)
|
||||
**Component**: SatelliteProvider.Api/Validators + SatelliteProvider.Common (CreateRouteRequest, RoutePoint, GeofencePolygon, GeoPoint DTOs)
|
||||
**Tracker**: AZ-809 (https://denyspopov.atlassian.net/browse/AZ-809)
|
||||
**Epic**: AZ-795 — Strict input validation across all public endpoints
|
||||
**Originating ticket**: gps-denied-onboard AZ-777 Phase 2 (cross-repo, 2026-05-22) — route-based seeding is the consumer's preferred imagery seeding path; black-box probe surfaced silent-coercion + input/output naming asymmetry
|
||||
|
||||
## Scope
|
||||
|
||||
Add FluentValidation-backed strict input validation to `POST /api/satellite/route`. Reject malformed payloads with **RFC 7807 ValidationProblemDetails** (HTTP 400) per the Epic's `error-shape.md` v1.0.0 contract.
|
||||
|
||||
Originating discovery: AZ-777 Phase 2 (gps-denied-onboard) — the consumer's preferred imagery seeding path is route-based (flight-track waypoints) rather than bbox-based, so this endpoint is the primary integration target for the Derkachi reference tile catalog. A black-box probe (2026-05-22) confirmed real silent-coercion behavior and an input/output naming asymmetry (see *Probe-confirmed gaps* below).
|
||||
|
||||
Jira AZ-809 is the authoritative spec; this file mirrors the in-workspace-only sections that the satellite-provider implementer will need.
|
||||
|
||||
## Probe-confirmed gaps (2026-05-22)
|
||||
|
||||
A black-box probe of the running producer captured these concrete behaviors:
|
||||
|
||||
1. **`Id` silently coerces to zero-Guid when omitted.** Same gap as the Region endpoint (AZ-808). `CreateRouteRequest.Id` has no `[Required]` and no `[JsonRequired]`, so the deserializer yields zero-Guid. Validator must reject missing/zero Id.
|
||||
2. **Happy path works end-to-end** for both `requestMaps:false` (route storage only, instant) and `requestMaps:true` (route + background tile backfill, ~15s for a 2-point 132m route at z=18). Validator must NOT regress.
|
||||
3. **Input/output naming asymmetry on points** (new finding). Input `points: [{"lat":..,"lon":..}]` (OSM short form, per `[JsonPropertyName("lat")]` on `RoutePoint`). But the **response** echoes points as `{"latitude":..,"longitude":..,"pointType":..,"sequenceNumber":..,"segmentIndex":..,"distanceFromPrevious":..}`. This is a DTO round-trip inconsistency on the same object type. NOT in scope for this validation task, but surfaced as **AC-10** (advisory) so the parent-suite team can decide whether to file a follow-up.
|
||||
4. **`UnmappedMemberHandling.Disallow` is active globally** (verified via AZ-808 probe), so unknown-field rejection (rule 13) will work out-of-the-box once `WithValidation<T>()` is wired.
|
||||
|
||||
## Endpoint surface
|
||||
|
||||
`POST /api/satellite/route`
|
||||
|
||||
Current wire format (per `CreateRouteRequest`, probe-confirmed 2026-05-22):
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "a1b2c3d4-...",
|
||||
"name": "derkachi-flight-1",
|
||||
"description": "AZ-777 Phase 2 seed route",
|
||||
"regionSizeMeters": 1000,
|
||||
"zoomLevel": 18,
|
||||
"points": [
|
||||
{ "lat": 50.10, "lon": 36.10 },
|
||||
{ "lat": 50.11, "lon": 36.11 }
|
||||
],
|
||||
"geofences": {
|
||||
"polygons": [
|
||||
{ "northWest": { "lat": 50.15, "lon": 36.05 },
|
||||
"southEast": { "lat": 50.05, "lon": 36.15 } }
|
||||
]
|
||||
},
|
||||
"requestMaps": true,
|
||||
"createTilesZip": false
|
||||
}
|
||||
```
|
||||
|
||||
Response (current, probe-confirmed): HTTP 200 with `RouteResponse` (id, name, description, regionSizeMeters, zoomLevel, totalDistanceMeters, totalPoints, points[], requestMaps, mapsReady, csvFilePath, summaryFilePath, stitchedImagePath, tilesZipPath, createdAt, updatedAt). Note response uses `latitude`/`longitude` for echoed points — see AC-10.
|
||||
|
||||
Background processing per Flow F5 if `requestMaps=true`; client polls `GET /api/satellite/route/{id}` until `mapsReady:true`.
|
||||
|
||||
## Required validations
|
||||
|
||||
1. **Body present** — null/empty body → 400 (`errors.$`).
|
||||
2. **`id` required, non-zero Guid** — NEW (probe-confirmed gap, same as AZ-808). Missing or `00000000-...` → 400 with `errors.id`.
|
||||
3. **`name` required** — non-empty string, length `[1, 200]`. Missing/empty → 400 with `errors.name`.
|
||||
4. **`description` optional** — if present, length `[0, 1000]`. Over cap → 400 with `errors.description`.
|
||||
5. **`regionSizeMeters` required** — double, in `[100.0, 10000.0]` (align with region endpoint). Out-of-range or missing → 400 with `errors.regionSizeMeters`.
|
||||
6. **`zoomLevel` required** — int, in `[0, 22]` (align with `TileCoordValidator`). Out-of-range or missing → 400 with `errors.zoomLevel`.
|
||||
7. **`points` required, non-empty** — at least **2 entries** (current `Flow F4` precondition), at most **500 entries** (cap to prevent runaway region-enqueue — confirm cap with parent-suite team). Below 2 or above 500 → 400 with `errors.points`.
|
||||
8. **Per-point**: `lat` required, double, in `[-90.0, 90.0]`; `lon` required, double, in `[-180.0, 180.0]`. Missing/out-of-range → 400 with `errors.points[i].lat` or `.lon`.
|
||||
9. **`geofences` optional** — if present:
|
||||
- `polygons` required, non-empty.
|
||||
- Per-polygon: `northWest` + `southEast` both required, each with valid `lat`/`lon`.
|
||||
- Cross-field invariant: `northWest.lat > southEast.lat` AND `northWest.lon < southEast.lon` (i.e. NW is genuinely north-of and west-of SE).
|
||||
- Violations → 400 with `errors.geofences.polygons[i].<field>`.
|
||||
10. **`requestMaps` required** — bool. Missing → 400 with `errors.requestMaps`.
|
||||
11. **`createTilesZip` required** — bool. Missing → 400 with `errors.createTilesZip`.
|
||||
12. **Cross-field constraint**: `createTilesZip == true` implies `requestMaps == true` (can't zip what wasn't downloaded). Violation → 400 with `errors.$` or `errors.createTilesZip`.
|
||||
13. **Unknown root or nested fields rejected** — covered by AZ-795's `UnmappedMemberHandling.Disallow` (probe-confirmed active globally via AZ-808). Any unknown field at any nesting level → 400 with `errors.<path>` ("could not be mapped to any .NET member").
|
||||
14. **Type mismatch** — e.g. `"lat": "fifty"` at any nesting level → 400 with `errors.<path>`. Covered by AZ-795's `GlobalExceptionHandler`.
|
||||
|
||||
## Implementation pattern (mirror AZ-796, extended for nesting)
|
||||
|
||||
1. New files (all under `SatelliteProvider.Api/Validators/`):
|
||||
- `CreateRouteRequestValidator.cs` — root validator with rules 2–7, 10–12.
|
||||
- `RoutePointValidator.cs` — per-point validator (rule 8); invoked via `RuleForEach(x => x.Points).SetValidator(new RoutePointValidator())`.
|
||||
- `GeofencePolygonValidator.cs` — per-polygon validator (rule 9); invoked via `RuleForEach(x => x.Geofences.Polygons).SetValidator(new GeofencePolygonValidator())` (guarded by `When(x => x.Geofences != null)`).
|
||||
2. Mark required props on `CreateRouteRequest`, `RoutePoint`, `Geofences`, `GeofencePolygon`, `GeoPoint` with `[JsonRequired]` per the cycle-7 `TileCoord` pattern. Pay special attention to `Id` (probe confirmed it's not enforced today).
|
||||
3. Add `.WithValidation<CreateRouteRequest>()` to the `MapPost("/api/satellite/route", ...)` chain.
|
||||
4. Unit tests: `SatelliteProvider.Tests/Validators/CreateRouteRequestValidatorTests.cs` + `RoutePointValidatorTests.cs` + `GeofencePolygonValidatorTests.cs` (≥ 13 test methods total — one per `RuleFor`/`RuleForEach` chain; new id-rule method must reproduce the probe's missing-id case).
|
||||
5. Integration tests: `SatelliteProvider.IntegrationTests/CreateRouteValidationTests.cs` (new file) — ≥ 14 methods (1 happy + 1 per failure-mode AC).
|
||||
6. Manual probe: `scripts/probe_route_validation.sh`. MUST include missing-id, NW-southeast-inverted polygon, points-too-few, createTilesZip-without-requestMaps.
|
||||
|
||||
## New contract doc
|
||||
|
||||
Create `_docs/02_document/contracts/api/route-creation.md` v1.0.0. Like the region endpoint, this has **no formal contract** today. Cover:
|
||||
|
||||
- Endpoint, auth, request body (with nested DTO recursion), response body (`RouteResponse` shape — acknowledge the input/output point-naming asymmetry; reference AC-10 advisory), error shape (reference `error-shape.md` v1.0.0).
|
||||
- Invariants (client-provided non-zero Id; one routeId per request; min 2 points; max 500 points; polygon NW>SE; cross-field createTilesZip implies requestMaps).
|
||||
- Test cases table (same format as `tile-inventory.md` v2.0.0). MUST include missing-id, geofence NW/SE inversion, createTilesZip cross-field, points-too-few cases.
|
||||
- Cross-link to Flow F4 (Route Creation) + Flow F5 (Route Map Processing background) + `region-request.md` (referenced by F5 enqueue path).
|
||||
|
||||
## Coordination with sibling tickets
|
||||
|
||||
- **Parent (AZ-795)**: depends on shared infra already landed in cycle 7.
|
||||
- **AZ-796 (inventory)**: reference for single-DTO validator pattern.
|
||||
- **AZ-808 (region)**: reference for endpoint without prior contract doc (same precondition: must create new `region-request.md`); coordinate field-name conventions across the two contracts. The naming inconsistency `RequestRegionRequest.sizeMeters` vs `CreateRouteRequest.regionSizeMeters` (same concept, different names) is flagged in AC-9.
|
||||
- **AZ-812 (region field rename)**: tangentially related — AZ-812 is bringing Region into the lat/lon convention that Route already uses. No direct dependency on this task.
|
||||
- **AZ-777 (gps-denied-onboard)**: consumer-side dependency — Phase 2 cannot proceed safely until this validator lands AND `route-creation.md` exists.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
**AC-1**: Each of the 14 validations above rejects with HTTP 400 + ValidationProblemDetails (single-rule precision).
|
||||
|
||||
**AC-2**: Happy path unchanged — a valid body still returns HTTP 200 + `RouteResponse`; background F5 processing still runs when `requestMaps=true`; probe's 2-point 132m route still completes (`mapsReady:true`) in under 20 seconds.
|
||||
|
||||
**AC-3**: All three validators (`CreateRouteRequestValidator`, `RoutePointValidator`, `GeofencePolygonValidator`) live in their own files under `SatelliteProvider.Api/Validators/` and are unit-tested (≥ 1 test per `RuleFor`/`RuleForEach` chain, ≥ 13 methods total).
|
||||
|
||||
**AC-4**: `SatelliteProvider.IntegrationTests/CreateRouteValidationTests.cs` covers happy + 13+ failure modes with full ValidationProblemDetails assertion. MUST include `Post_WithMissingId_ReturnsBadRequest` (reproducing the 2026-05-22 probe's silent-coercion case).
|
||||
|
||||
**AC-5**: `_docs/02_document/contracts/api/route-creation.md` v1.0.0 created and published.
|
||||
|
||||
**AC-6**: `_docs/02_document/system-flows.md` F4 + F5 updated to reference the new contract doc + error shape.
|
||||
|
||||
**AC-7**: OpenAPI spec marks all required fields at every nesting level, declares ranges, and documents the 400 response.
|
||||
|
||||
**AC-8**: Manual probe script exercises each failure mode end-to-end via `curl` + JWT.
|
||||
|
||||
**AC-9** (advisory — surface in PR, parent-suite to decide): the inconsistency `RequestRegionRequest.sizeMeters` vs `CreateRouteRequest.regionSizeMeters` is named differently for the same concept. Either keep the discrepancy and document why, or harmonize to a single name in a follow-up MAJOR contract bump for both.
|
||||
|
||||
**AC-10** (advisory — surface in PR, parent-suite to decide): the **input/output point-naming asymmetry** on this endpoint (input `points: [{"lat":..,"lon":..}]`, response `points: [{"latitude":..,"longitude":..}]` for the same `RoutePoint` round-trip) is a DTO inconsistency. Probe-confirmed 2026-05-22. Either keep + document, or file a follow-up to harmonize.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Route processing semantics (Flow F5 background, ZIP creation, point-in-polygon geofence filtering) — validation lives at the API layer only.
|
||||
- `GET /api/satellite/route/{id}` status endpoint (separate task if needed; Guid binding is framework-handled).
|
||||
- Performance — nested validation overhead is negligible vs interpolation + background region enqueue.
|
||||
- Route interpolation algorithm — unchanged.
|
||||
- Input/output point-naming asymmetry fix — surfaced as AC-10 advisory only.
|
||||
|
||||
## Constraints
|
||||
|
||||
- **Breaking behavior change** — callers today omitting `id` (silently getting zero-Guid) or sending malformed nested bodies will start getting 400. Known consumer set: gps-denied-onboard (uses correct body shape with id and lat/lon points, per black-box probe 2026-05-22). Other consumers TBD by parent-suite team.
|
||||
- No regression in any existing `RouteCreationTests.cs` happy-path coverage.
|
||||
- Cross-field constraint (rule 12) requires custom `When/Otherwise` or a top-level `Must()` rule — FluentValidation 12.0.0 supports both; pick the more readable one.
|
||||
|
||||
## References
|
||||
|
||||
- Jira AZ-809: https://denyspopov.atlassian.net/browse/AZ-809
|
||||
- Parent Epic: AZ-795
|
||||
- Reference implementations: AZ-796 (single-DTO pattern), AZ-808 (no-prior-contract pattern, same batch)
|
||||
- Tangentially related: AZ-812 (region field rename to OSM)
|
||||
- Cycle-7 retro: `_docs/06_metrics/retro_2026-05-22_cycle7.md` (flagged this endpoint as a per-endpoint child of AZ-795)
|
||||
- Originating consumer discovery: gps-denied-onboard AZ-777 Phase 2 (route-based seeding is the consumer's preferred path; 2026-05-22 black-box probe surfaced silent-coercion + naming asymmetry)
|
||||
- Related contract docs: `error-shape.md` v1.0.0, `tile-inventory.md` v2.0.0 (both produced by AZ-795+AZ-796 cycle 7)
|
||||
Reference in New Issue
Block a user