Files
satellite-provider/_docs/02_tasks/done/AZ-810_upload_metadata_validation.md
T
Oleksandr Bezdieniezhnykh 490902c80a [AZ-810] Strict validation for POST /api/satellite/upload metadata
Adds the per-endpoint child of AZ-795 ("Strict Input Validation Epic")
for the UAV upload multipart endpoint. Three new validators land under
SatelliteProvider.Api/Validators/:

- UavTileBatchMetadataPayloadValidator: items NotNull + NotEmpty +
  count <= MaxBatchSize + RuleForEach dispatching to the per-item
  validator.
- UavTileMetadataValidator: lat / lon / tileZoom range, tileSizeMeters
  > 0, capturedAt within [now - MaxAgeDays, now + future-skew]; uses an
  injectable TimeProvider so unit tests can drive a fixed clock.
- UavUploadValidationFilter: IEndpointFilter that reads the multipart
  `metadata` form field, deserializes it with the strict global
  JsonSerializerOptions (so UnmappedMemberHandling.Disallow +
  [JsonRequired] from AZ-795 are honored), runs the FluentValidation
  chain, and enforces the cross-field `items.Count == files.Count`
  envelope rule. FluentValidation errors are prefixed with `metadata.`
  so wire keys look like `errors["metadata.items[0].latitude"]`.

[JsonRequired] is added to every non-optional axis on
UavTileMetadata and UavTileBatchMetadataPayload; FlightId stays
nullable per AZ-503 anonymous-flight semantics.

Coverage: 13 unit tests + 16 integration tests + 1 curl probe script
exercise the happy path and every failure mode. All 9 ACs covered;
no regression in AZ-488 UavUploadTests payloads (traced against the
new rules).

Documentation: uav-tile-upload.md bumped v1.1.0 -> v1.2.0 with the
new validation rules section + 400-shape examples + changelog entry.
api_program.md updated to describe the three new validators + filter
+ the AddTransient<UavUploadValidationFilter>() DI registration.

Reports: batch_04_cycle8_report.md + reviews/batch_04_cycle8_review.md
record the PASS_WITH_WARNINGS verdict (2 Low DRY-in-tests findings:
FixedTimeProvider duplication crossed the cycle-2 "promote to shared"
threshold; PostBatch helper duplicated between two integration
suites). Both deferred to follow-up PBIs.

Task spec archived: _docs/02_tasks/todo/AZ-810... -> done/.
Jira: AZ-810 transitioned In Progress -> In Testing.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 13:32:19 +03:00

12 KiB
Raw Blame History

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 envelopeUavTileBatchMetadataPayload 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:

  • metadata form field — JSON string deserialized to UavTileBatchMetadataPayload.
  • files form 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

  1. Multipart envelope present — missing multipart form → framework-level 400 (unchanged).
  2. metadata field present — missing form field → 400 with errors.metadata ("required").
  3. metadata parses as JSON — malformed JSON → 400 with errors.metadata ("could not be parsed as JSON"). Covered by AZ-795's GlobalExceptionHandler once metadata binding routes through JsonSerializerOptions.
  4. metadata.items required, non-empty — missing or [] → 400 with errors.metadata.items.
  5. metadata.items.lengthUavQualityConfig.MaxBatchSize — over cap → 400 with errors.metadata.items. (Existing framework limit handles oversize via KestrelServerOptions.Limits.MaxRequestBodySize at the byte layer; this rule guards the item count specifically.)
  6. 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 with errors.metadata + errors.files.

Per-item (under metadata.items[i])

  1. lat required — double, in [-90.0, 90.0]. Missing/out-of-range → 400 with errors.metadata.items[i].lat.
  2. lon required — double, in [-180.0, 180.0]. Missing/out-of-range → 400 with errors.metadata.items[i].lon.
  3. tileZoom required — int, in [0, 22] (align with TileCoordValidator). Missing/out-of-range → 400 with errors.metadata.items[i].tileZoom.
  4. tileSizeMeters required — double, > 0.0. Missing/non-positive → 400 with errors.metadata.items[i].tileSizeMeters. (Tighter range can be added if parent-suite team has a documented expected range; for now just guard > 0.)
  5. capturedAt required — ISO-8601 UTC DateTime. Must satisfy AZ-488 Rule 4 freshness window: capturedAt ≤ now + UavQualityConfig.CapturedAtFutureSkewSeconds AND capturedAt ≥ now - UavQualityConfig.MaxAgeDays. Missing/out-of-window → 400 with errors.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.)
  6. flightId optional — if present, must be valid Guid (RFC 4122). Malformed UUID → 400 with errors.metadata.items[i].flightId. (Null/missing is valid — anonymous-flight semantics per AZ-503.)

Cross-cutting

  1. Unknown fields rejected at root or any nesting level of metadata — covered by AZ-795's UnmappedMemberHandling.Disallow. Any unknown field at root or under items[i] → 400 with errors.metadata.<path> ("could not be mapped to any .NET member").
  2. Type mismatch — e.g. "lat": "fifty" or "tileZoom": 18.5 (non-integer double for int) → 400 with errors.metadata.<path>. Covered by AZ-795's GlobalExceptionHandler.

Implementation pattern (mirror AZ-796, extended for multipart + per-item)

  1. New files (all under SatelliteProvider.Api/Validators/):

    • UavTileBatchMetadataPayloadValidator.cs — root validator with rules 46.
    • UavTileMetadataValidator.cs — per-item validator (rules 712); invoked via RuleForEach(x => x.Items).SetValidator(new UavTileMetadataValidator(uavQualityConfig)).
  2. Mark required props on UavTileBatchMetadataPayload + UavTileMetadata with [JsonRequired] per the cycle-7 TileCoord pattern.

  3. Wire the validator into the multipart handler in Program.cs (the UploadUavTileBatch endpoint) — likely a custom endpoint filter that: a. Reads the metadata form field. b. Deserializes via the strict JsonSerializerOptions (already configured by AZ-795). c. Resolves IValidator<UavTileBatchMetadataPayload> from DI and runs it. d. Returns Results.ValidationProblem on 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.

  4. Unit tests: SatelliteProvider.Tests/Validators/UavTileBatchMetadataPayloadValidatorTests.cs + UavTileMetadataValidatorTests.cs (≥ 11 test methods total — one per RuleFor/RuleForEach chain).

  5. Integration tests: SatelliteProvider.IntegrationTests/UavUploadValidationTests.cs (new file) — ≥ 13 methods (1 happy + 1 per failure-mode AC + envelope alignment regression).

  6. Manual probe: scripts/probe_upload_validation.sh — multipart curl against 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 + UavTileQualityGate MUST remain green.
  • AZ-503 (flightId semantics): rule 12 must respect the anonymous-flight contract — flightId=null is 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 IUavTileQualityGate per 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.cs happy-path coverage.
  • Cross-field rule 6 (alignment) requires access to BOTH metadata.Items.Count AND files.Count — it can't be a pure IValidator<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.md v1.0.0, tile-inventory.md v2.0.0 (both produced by AZ-795+AZ-796 cycle 7), uav-tile-upload.md v1.1.0 (to be bumped)