Files
satellite-provider/_docs/02_tasks/todo/AZ-808_region_endpoint_validation.md
T
Oleksandr Bezdieniezhnykh 06d160daf0 [AZ-808] [AZ-809] [AZ-810] [AZ-811] [AZ-812] Cycle 8 Step 9 queued
Step 9 (New Task) closure for cycle 8. Queues 5 task specs under the
AZ-795 strict-validation umbrella + OSM-naming harmonization:

- AZ-808 region-request validator (POST /api/satellite/request)   3 pts
- AZ-809 route-creation validator (POST /api/satellite/route)     5 pts
- AZ-810 UAV upload metadata validator (POST /api/satellite/upload) 5 pts
- AZ-811 lat/lon GET validator (GET /api/satellite/tiles/latlon)  2 pts
- AZ-812 Region DTO rename latitude/longitude -> lat/lon          3 pts

Total 18 SP. Origin: cross-repo request from gps-denied-onboard
agent (2026-05-22) after AZ-777 Phase 2 black-box probe of the
Region API surfaced silent-coercion behavior + the lone OSM-deviating
coord naming convention left in the producer's public surface.

Ordering recorded (per /autodev Step 10 dirty-tree decision):
AZ-812 ships first so AZ-808 validator + contract doc + integration
tests are written against the final lat/lon names. AZ-809/AZ-810/AZ-811
are independent of AZ-812 (their DTOs already use OSM short form).

Deps table updated: cycle-8b (AZ-812) folded into cycle-8 ordering as
step 1; AZ-808 dependency upgraded SOFT -> HARD on AZ-812.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-22 13:00:34 +03:00

12 KiB
Raw Blame History

Strict validation for region-request endpoint (POST /api/satellite/request)

Task: AZ-808_region_endpoint_validation Name: Strict validation for region-request endpoint Description: Add FluentValidation-backed strict input validation to POST /api/satellite/request (region onboarding — enqueues a square region of tiles for async Google-Maps backfill). Reject malformed payloads with RFC 7807 ValidationProblemDetails (HTTP 400). Second concrete child of AZ-795; reuses the shared infra wired in cycle 7. Complexity: 3 points (7 validation rules — was 6 before the 2026-05-22 probe added the Id rule) Dependencies: AZ-795 (HARD — shared infra already landed in cycle 7); AZ-796 (reference implementation pattern); AZ-812 (field-naming coordination — see below) Component: SatelliteProvider.Api/Validators + SatelliteProvider.Common (RequestRegionRequest DTO) Tracker: AZ-808 (https://denyspopov.atlassian.net/browse/AZ-808) Epic: AZ-795 — Strict input validation across all public endpoints Originating ticket: gps-denied-onboard AZ-777 Phase 2 (cross-repo, 2026-05-22) — consumer needs this endpoint to seed Derkachi reference tile catalog; black-box probe surfaced concrete silent-coercion behavior

Scope

Add FluentValidation-backed strict input validation to POST /api/satellite/request (region onboarding — enqueues a square region of tiles for async Google-Maps backfill). 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 needs to call this endpoint to seed the Derkachi reference tile catalog. A black-box probe (2026-05-22) confirmed real silent-coercion behavior that this task fixes (see Probe-confirmed gaps below).

Jira AZ-808 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 that this task must close:

  1. Id silently coerces to zero-Guid when omitted. Body {"latitude":49.94,"longitude":36.31,"sizeMeters":200,"zoomLevel":18,"stitchTiles":false} (no id) returned HTTP 200 with "id":"00000000-0000-0000-0000-000000000000" and status:queued. The [Required] DataAnnotation on RequestRegionRequest.Id is NOT enforced — the deserializer just yields the default Guid. This is the same silent-coercion class that motivated AZ-795. Validator must reject zero-Guid + missing-Id with the same RFC 7807 shape as the inventory validator.
  2. UnmappedMemberHandling.Disallow IS active for this endpoint. Sending the wrong field name ({"lat":49.94,...}) returned HTTP 400 with the proper ValidationProblemDetails shape: {"errors":{"lat":["The JSON property 'lat' could not be mapped to any .NET member contained in type 'SatelliteProvider.Common.DTO.RequestRegionRequest'."]}}. So rule 8 (unknown-field rejection) is already covered by AZ-795 cycle-7 shared infra; this task only needs to verify it stays active after wiring WithValidation<T>().
  3. Happy path works end-to-end. With the correct shape {"id":"<guid>","latitude":..,"longitude":..,"sizeMeters":200,"zoomLevel":18,"stitchTiles":false}: HTTP 200 + regionId + 9 tiles downloaded from Google Maps + accessible via GET /tiles/{z}/{x}/{y} (13 KB JPEG verified). Validator must NOT regress this path.

Field-naming coordination with AZ-812

This spec uses the current wire format (latitude, longitude) because that's what the DTO ships today and that's what the validator must reject malformed values for. AZ-812 (mirror of AZ-794 for inventory) is filed to rename these to lat/lon for OSM-style consistency across all satellite-provider endpoints.

If AZ-812 lands before this task, rewrite all field references in this spec from latitude/longitude to lat/lon before implementing. If AZ-812 lands after this task, AZ-812 must also update the validator + contract doc + integration tests. Pick the ordering during planning to avoid double migration.

Endpoint surface

POST /api/satellite/request

Current wire format (per RequestRegionRequest.cs, probe-confirmed 2026-05-22):

{
  "id": "<guid>",
  "latitude": 50.10,
  "longitude": 36.10,
  "sizeMeters": 5000,
  "zoomLevel": 18,
  "stitchTiles": false
}

Response: HTTP 200 with RegionStatusResponse (id, status, csvFilePath, summaryFilePath, tilesDownloaded, tilesReused, createdAt, updatedAt). Async — the actual tile downloads happen in the background via RegionProcessingService (Flow F3). Caller polls GET /api/satellite/region/{id} until status:completed.

Required validations

  1. Body present — null/empty body → 400 (errors.$).
  2. id required, non-zero Guid — NEW (probe-confirmed gap). Missing or 00000000-... → 400 with errors.id. Use RuleFor(x => x.Id).NotEmpty() (FluentValidation's NotEmpty() rejects default-Guid).
  3. latitude required — double, in [-90.0, 90.0]. Out-of-range or missing → 400 with errors.latitude.
  4. longitude required — double, in [-180.0, 180.0]. Out-of-range or missing → 400 with errors.longitude.
  5. sizeMeters required — double, in [100.0, 10000.0] (matches current inline check in RequestRegion Handler per api_program.md). Out-of-range or missing → 400 with errors.sizeMeters.
  6. zoomLevel required — int, in [0, 22] (align with TileCoordValidator slippy-map range used by AZ-796 for the inventory endpoint). Out-of-range or missing → 400 with errors.zoomLevel.
  7. stitchTiles required — bool. Missing → 400 with errors.stitchTiles (no defaulting to false — force the caller to declare intent).
  8. Unknown root fields rejected — already covered by AZ-795's UnmappedMemberHandling.Disallow (probe-confirmed active). Verify it stays active after wiring WithValidation<T>().
  9. Type mismatch — e.g. "latitude": "fifty" → 400 with errors.latitude ("could not be parsed"). Already covered by AZ-795's GlobalExceptionHandler; verify it triggers for this endpoint.

Implementation pattern (mirror AZ-796)

  1. New file: SatelliteProvider.Api/Validators/RegionRequestValidator.csAbstractValidator<RequestRegionRequest> with rules 27.
  2. Mark RequestRegionRequest props with [JsonRequired] (replacing or supplementing the existing [Required] DataAnnotation — the latter is not enforced by System.Text.Json, as the probe confirmed). Apply to Id, Latitude, Longitude, SizeMeters, ZoomLevel, StitchTiles.
  3. Add .WithValidation<RequestRegionRequest>() to the MapPost("/api/satellite/request", ...) chain in Program.cs.
  4. Unit tests: SatelliteProvider.Tests/Validators/RegionRequestValidatorTests.cs — one test per RuleFor(...) (≥ 6 methods covering id, latitude, longitude, sizeMeters, zoomLevel, stitchTiles).
  5. Integration tests: SatelliteProvider.IntegrationTests/RegionRequestValidationTests.cs (new file) — ≥ 9 methods (1 happy + 1 per failure-mode AC — including missing-id reproducing the probe's silent-coercion case).
  6. Manual probe: scripts/probe_region_validation.sh (mirrors scripts/probe_inventory_validation.sh from AZ-796). MUST include the missing-id test case.

New contract doc

Create _docs/02_document/contracts/api/region-request.md v1.0.0. The region endpoint has no formal contract today (only system-flows.md F2 + module docs). The contract doc must cover:

  • Endpoint, auth, request body, response body (use the actual RegionStatusResponse shape: id, status, csvFilePath, summaryFilePath, tilesDownloaded, tilesReused, createdAt, updatedAt), error shape (reference error-shape.md v1.0.0).
  • Invariants (one regionId per request; client-provided non-zero Id; size cap; async semantics — caller must poll GET /api/satellite/region/{id}).
  • Test cases mirroring the validator rules (same Case | Input | Expected | Notes table format as tile-inventory.md v2.0.0). MUST include the missing-id case.
  • Cross-link to RegionStatus flow (F3) and the consumer-facing inventory contract (tile-inventory.md — callers seed via region, then read via inventory).
  • Reference to AZ-812 (field-naming follow-up).

Coordination with sibling tickets

  • Parent (AZ-795): depends on shared infra already landed in cycle 7.
  • AZ-796 (inventory): reference implementation — copy the validator + integration-test layout 1:1.
  • AZ-812 (region field rename): hard coordination on field names. See Field-naming coordination with AZ-812 above.
  • AZ-777 (gps-denied-onboard): consumer-side dependency — Phase 2 cannot proceed safely until this validator lands AND the contract doc exists. Consumer has black-box-probed the endpoint and can use it today, but silent-coercion bugs make Phase 2 fragile until validation is in place.
  • Sibling validation tasks created in the same batch: AZ-809 (route), AZ-810 (UAV upload metadata), AZ-811 (lat/lon GET).

Acceptance criteria

AC-1: Each of the 9 validations above rejects with HTTP 400 + ValidationProblemDetails (single-rule precision; unrelated rules NOT in the errors map).

AC-2: Happy path unchanged — a valid body still returns HTTP 200 + RegionStatusResponse; background processing still runs; the probe's 9-tile Derkachi case ({"id":"<guid>","latitude":49.94,"longitude":36.31,"sizeMeters":200,"zoomLevel":18,"stitchTiles":false}) still completes in under 10 seconds.

AC-3: RegionRequestValidator lives in its own file under SatelliteProvider.Api/Validators/ and is unit-tested (≥ 1 test per RuleFor).

AC-4: SatelliteProvider.IntegrationTests/RegionRequestValidationTests.cs covers happy + 8+ failure modes with full ValidationProblemDetails assertion (use the existing ProblemDetailsAssertions helper from AZ-795). MUST include Post_WithMissingId_ReturnsBadRequest (reproducing the 2026-05-22 probe's silent-coercion case).

AC-5: _docs/02_document/contracts/api/region-request.md v1.0.0 created and published.

AC-6: _docs/02_document/system-flows.md F2 updated to reference the new contract doc + error shape.

AC-7: OpenAPI spec marks RequestRegionRequest fields required, declares ranges, and documents the 400 response (matches AZ-796 Swashbuckle annotations).

AC-8: Manual probe script exercises each failure mode end-to-end via curl + JWT.

Out of scope

  • The Region API's processing semantics (Flow F3 — RegionProcessingService) — validation lives at the API layer only.
  • Any change to IRegionService.RequestRegionAsync signature beyond accepting the validated DTO.
  • GET /api/satellite/region/{id} status endpoint (separate task if path-parameter validation needed; current Guid binding is framework-handled).
  • The field-name rename (Latitude/LongitudeLat/Lon) — handled by AZ-812.
  • Performance — validation overhead is negligible vs the async enqueue + Google Maps round-trip.

Constraints

  • Breaking behavior change — any consumer today omitting id (silently getting zero-Guid) or sending malformed values will start getting 400. Known consumer set: gps-denied-onboard (currently uses correct body shape with id, per black-box probe 2026-05-22). Other consumers TBD by parent-suite team.
  • No regression in any existing RegionRequestTests.cs happy-path coverage.

References

  • Jira AZ-808: https://denyspopov.atlassian.net/browse/AZ-808
  • Parent Epic: AZ-795 (shared infra; error-shape contract)
  • Reference implementation: AZ-796 (inventory endpoint)
  • Coordination: AZ-812 (region field-name rename to OSM convention)
  • Cycle-7 retro: _docs/06_metrics/retro_2026-05-22_cycle7.md (flagged this endpoint as next in line)
  • Originating consumer discovery: gps-denied-onboard AZ-777 Phase 2 (2026-05-22 black-box probe)
  • Related contract docs: error-shape.md v1.0.0, tile-inventory.md v2.0.0 (both produced by AZ-795+AZ-796 cycle 7)