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>
12 KiB
Strict validation for UAV upload metadata (POST /api/satellite/upload)
Task: AZ-810_upload_metadata_validation
Name: Strict validation for UAV upload metadata
Description: Add FluentValidation-backed strict input validation to the metadata DTO layer of POST /api/satellite/upload (UAV batch upload, AZ-488). Reject malformed metadata JSON envelopes with RFC 7807 ValidationProblemDetails (HTTP 400). Fourth concrete child of AZ-795; reuses the shared infra wired in cycle 7. The file-level quality checks (size, luminance, age, future-skew) remain in scope of the existing IUavTileQualityGate.
Complexity: 5 points (multipart envelope requires custom filter, 14 rules, two validator classes, MINOR contract bump, defense-in-depth with existing UavTileQualityGate)
Dependencies: AZ-795 (HARD — shared infra); AZ-796 (single-DTO reference); AZ-809 (nested per-item reference); AZ-488 (original endpoint — must remain green); AZ-503 (flightId semantics)
Component: SatelliteProvider.Api/Validators + SatelliteProvider.Common (UavTileBatchMetadataPayload, UavTileMetadata DTOs)
Tracker: AZ-810 (https://denyspopov.atlassian.net/browse/AZ-810)
Epic: AZ-795 — Strict input validation across all public endpoints
Originating ticket: AZ-795 cycle-7 retro (explicitly names this endpoint as a remaining per-endpoint child)
Scope
Add FluentValidation-backed strict input validation to the metadata DTO layer of POST /api/satellite/upload. Reject malformed metadata JSON envelopes with RFC 7807 ValidationProblemDetails (HTTP 400) per the Epic's error-shape.md v1.0.0 contract.
Important scope boundary: this task is about the metadata envelope — UavTileBatchMetadataPayload and its per-item UavTileMetadata payloads. The file-level quality checks (size, luminance variance, age, future-skew) are already enforced by the existing IUavTileQualityGate per AZ-488 and remain in scope of that gate. The DTO validator runs before the quality gate (per-item bytes inspection) so malformed metadata can short-circuit without ever touching the file bytes.
Originating discovery: AZ-795 cycle-7 retro — the metadata DTO is explicitly named as a remaining gap ("already partly validated by UavTileQualityGate, but the metadata layer is a separate validator").
Jira AZ-810 is the authoritative spec; this file mirrors the in-workspace-only sections that the satellite-provider implementer will need.
Endpoint surface
POST /api/satellite/upload (multipart/form-data, auth: RequiresGpsPermission policy on top of JWT bearer)
Multipart envelope:
metadataform field — JSON string deserialized toUavTileBatchMetadataPayload.filesform field —IFormFileCollection, one entry per metadata item, position-correlated.
UavTileBatchMetadataPayload (current shape, per modules/common_dtos.md):
{
"items": [
{
"lat": 50.10,
"lon": 36.10,
"tileZoom": 18,
"tileSizeMeters": 19.10925707,
"capturedAt": "2026-05-22T08:00:00Z",
"flightId": "a1b2c3d4-..."
}
]
}
Response (current per AZ-488): HTTP 200 {items: [UavTileUploadResultItem[]]} even on per-item failures. Envelope-level failures (oversize batch, malformed metadata, mismatched batch) are HTTP 400 ProblemDetails. This task tightens the "malformed metadata" path.
Required validations
Envelope-level
- Multipart envelope present — missing multipart form → framework-level 400 (unchanged).
metadatafield present — missing form field → 400 witherrors.metadata("required").metadataparses as JSON — malformed JSON → 400 witherrors.metadata("could not be parsed as JSON"). Covered by AZ-795'sGlobalExceptionHandleronce metadata binding routes throughJsonSerializerOptions.metadata.itemsrequired, non-empty — missing or[]→ 400 witherrors.metadata.items.metadata.items.length≤UavQualityConfig.MaxBatchSize— over cap → 400 witherrors.metadata.items. (Existing framework limit handles oversize viaKestrelServerOptions.Limits.MaxRequestBodySizeat the byte layer; this rule guards the item count specifically.)metadata.items.length==files.length— envelope alignment per AZ-488. Already detected by the upload handler; surface via ValidationProblemDetails for consistency with sibling endpoints → 400 witherrors.metadata+errors.files.
Per-item (under metadata.items[i])
latrequired — double, in[-90.0, 90.0]. Missing/out-of-range → 400 witherrors.metadata.items[i].lat.lonrequired — double, in[-180.0, 180.0]. Missing/out-of-range → 400 witherrors.metadata.items[i].lon.tileZoomrequired — int, in[0, 22](align withTileCoordValidator). Missing/out-of-range → 400 witherrors.metadata.items[i].tileZoom.tileSizeMetersrequired — double,> 0.0. Missing/non-positive → 400 witherrors.metadata.items[i].tileSizeMeters. (Tighter range can be added if parent-suite team has a documented expected range; for now just guard> 0.)capturedAtrequired — ISO-8601 UTCDateTime. Must satisfy AZ-488 Rule 4 freshness window:capturedAt ≤ now + UavQualityConfig.CapturedAtFutureSkewSecondsANDcapturedAt ≥ now - UavQualityConfig.MaxAgeDays. Missing/out-of-window → 400 witherrors.metadata.items[i].capturedAt. (Equivalent to AZ-488 Rule 4 but at the API layer; the existing UavTileQualityGate still enforces the same rule for defense-in-depth.)flightIdoptional — if present, must be validGuid(RFC 4122). Malformed UUID → 400 witherrors.metadata.items[i].flightId. (Null/missing is valid — anonymous-flight semantics per AZ-503.)
Cross-cutting
- Unknown fields rejected at root or any nesting level of
metadata— covered by AZ-795'sUnmappedMemberHandling.Disallow. Any unknown field at root or underitems[i]→ 400 witherrors.metadata.<path>("could not be mapped to any .NET member"). - Type mismatch — e.g.
"lat": "fifty"or"tileZoom": 18.5(non-integer double for int) → 400 witherrors.metadata.<path>. Covered by AZ-795'sGlobalExceptionHandler.
Implementation pattern (mirror AZ-796, extended for multipart + per-item)
-
New files (all under
SatelliteProvider.Api/Validators/):UavTileBatchMetadataPayloadValidator.cs— root validator with rules 4–6.UavTileMetadataValidator.cs— per-item validator (rules 7–12); invoked viaRuleForEach(x => x.Items).SetValidator(new UavTileMetadataValidator(uavQualityConfig)).
-
Mark required props on
UavTileBatchMetadataPayload+UavTileMetadatawith[JsonRequired]per the cycle-7TileCoordpattern. -
Wire the validator into the multipart handler in
Program.cs(theUploadUavTileBatchendpoint) — likely a custom endpoint filter that: a. Reads themetadataform field. b. Deserializes via the strictJsonSerializerOptions(already configured by AZ-795). c. ResolvesIValidator<UavTileBatchMetadataPayload>from DI and runs it. d. ReturnsResults.ValidationProblemon failure.This is a more involved wiring than AZ-796 (which uses the bog-standard
.WithValidation<T>()filter for pure JSON bodies). Document the new filter in_docs/02_document/modules/api_program.md. -
Unit tests:
SatelliteProvider.Tests/Validators/UavTileBatchMetadataPayloadValidatorTests.cs+UavTileMetadataValidatorTests.cs(≥ 11 test methods total — one perRuleFor/RuleForEachchain). -
Integration tests:
SatelliteProvider.IntegrationTests/UavUploadValidationTests.cs(new file) — ≥ 13 methods (1 happy + 1 per failure-mode AC + envelope alignment regression). -
Manual probe:
scripts/probe_upload_validation.sh— multipartcurlagainst each failure mode.
Update existing contract doc
Bump _docs/02_document/contracts/api/uav-tile-upload.md from v1.1.0 → v1.2.0 (MINOR). The contract doc exists; this task adds the validation rules + error shape reference. Do NOT change the wire format (no rename like AZ-794); MINOR is correct.
Add a new section: "Validation rules (AZ-810)" that enumerates the 14 rules and references error-shape.md v1.0.0.
Coordination with sibling tickets
- Parent (AZ-795): depends on shared infra already landed in cycle 7.
- AZ-796 (inventory): reference for single-DTO pattern.
- AZ-809 (route): reference for nested per-item validator pattern (RuleForEach).
- AZ-488 (original UAV upload): existing happy-path integration tests +
UavTileQualityGateMUST remain green. - AZ-503 (flightId semantics): rule 12 must respect the anonymous-flight contract —
flightId=nullis a valid case.
Acceptance criteria
AC-1: Each of the 14 validations above rejects with HTTP 400 + ValidationProblemDetails (single-rule precision).
AC-2: Happy path unchanged — valid envelope still returns HTTP 200 + per-item result list; per-item file rejections (existing UavTileQualityGate semantics) still return HTTP 200 with per-item status (unchanged contract).
AC-3: Both validator classes live in their own files under SatelliteProvider.Api/Validators/ and are unit-tested (≥ 11 methods total).
AC-4: SatelliteProvider.IntegrationTests/UavUploadValidationTests.cs covers happy + 12+ failure modes with full ValidationProblemDetails assertion.
AC-5: _docs/02_document/contracts/api/uav-tile-upload.md bumped to v1.2.0 (MINOR) with the new validation section.
AC-6: _docs/02_document/modules/api_program.md updated to document the new multipart-validation endpoint filter.
AC-7: OpenAPI spec marks UavTileBatchMetadataPayload + UavTileMetadata fields required, declares ranges, and documents the 400 response.
AC-8: Manual probe script exercises each failure mode end-to-end via multipart curl + JWT.
AC-9: No regression in any existing AZ-488 integration tests (UavTileBatchUploadTests.cs, UavTileQualityGateTests.cs).
Out of scope
- File-level quality checks (size, luminance, age, future-skew) — already enforced by
IUavTileQualityGateper AZ-488; do NOT duplicate at the validator layer (the validator covers metadata-only). - Per-item file-byte validation — unchanged.
- Auth (
RequiresGpsPermission) — unchanged. - Performance — metadata validation overhead is negligible vs the per-item file decode + DB writes.
Constraints
- Breaking behavior change — callers sending malformed metadata that silently coerces will start getting 400 instead of HTTP 200 with per-item rejections. Known consumer set: gps-denied-onboard (D-PROJ-2 flight-uploader path — not currently active per AZ-777 task spec).
- No regression in any existing
UavTileBatchUploadTests.cshappy-path coverage. - Cross-field rule 6 (alignment) requires access to BOTH
metadata.Items.CountANDfiles.Count— it can't be a pureIValidator<UavTileBatchMetadataPayload>rule. Wire it as a separate envelope-level check inside the endpoint filter, with the same ValidationProblemDetails shape. - The multipart validation filter (item 3 of Implementation pattern above) is a NEW shared piece of infra. Consider whether it should live as a generic
MultipartValidationEndpointFilter<T>for future reuse, or stay specific to this endpoint. Parent-suite team decides.
References
- Jira AZ-810: https://denyspopov.atlassian.net/browse/AZ-810
- Parent Epic: AZ-795
- Reference implementations: AZ-796 (single-DTO pattern), AZ-809 (nested per-item pattern, same batch)
- Cycle-7 retro:
_docs/06_metrics/retro_2026-05-22_cycle7.md(explicitly names this endpoint as a per-endpoint child of AZ-795) - Original endpoint: AZ-488 (UAV batch upload), AZ-503 (flightId semantics)
- Related contract docs:
error-shape.mdv1.0.0,tile-inventory.mdv2.0.0 (both produced by AZ-795+AZ-796 cycle 7),uav-tile-upload.mdv1.1.0 (to be bumped)