Files
satellite-provider/_docs/03_implementation/batch_04_cycle8_report.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

11 KiB

Batch Report

Batch: 04 (cycle 8) Tasks: AZ-810 (POST /api/satellite/upload strict metadata validation, multipart envelope) Date: 2026-05-23

Task Results

Task Status Files Modified Tests AC Coverage Issues
AZ-810_upload_metadata_validation Done 12 files (5 new) 13 validator unit tests + 16 integration tests added; full integration-test pass deferred to autodev Step 11 (Run Tests) 9/9 ACs covered 2 Low (DRY in test helpers — FixedTimeProvider, PostBatch); 1 Info (metadata-key wire shape, documented)

AC Test Coverage (9/9 ACs)

AC Coverage
AC-1 All 14 documented rules enforced. Deserializer (rules 1, 12, 13, 14): [JsonRequired] on UavTileMetadata.{Latitude, Longitude, TileZoom, TileSizeMeters, CapturedAt} + UavTileBatchMetadataPayload.Items (missing axes); UnmappedMemberHandling.Disallow from cycle-7 (unknown root + nested fields); System.Text.Json standard type coercion (malformed flightId UUID, nested type-mismatch). Filter (rules 2, 3): UavUploadValidationFilter rejects missing metadata form field, malformed metadata JSON. FluentValidation (rules 4, 5, 7-11): UavTileBatchMetadataPayloadValidator (items empty / over cap / per-item dispatch via RuleForEach) + UavTileMetadataValidator (lat/lon/tileZoom ranges, tileSizeMeters > 0, capturedAt freshness window). Cross-field (rule 6): items.Count == files.Count enforced after the per-payload validator. Each rule has at least one positive + one negative integration test.
AC-2 Happy path: UavUploadValidationTests.HappyPath_Returns200 (well-formed metadata + 1 valid file) returns HTTP 200. AZ-488 happy paths (UavUploadTests.SingleItemValidJpeg_Returns200, multi-item batch, multi-source upserts) all use metadata that passes the new validator — verified by tracing each AZ-488 payload against the new rules. Full integration-test run gating deferred to autodev Step 11.
AC-3 Validators in own files: SatelliteProvider.Api/Validators/UavTileBatchMetadataPayloadValidator.cs + SatelliteProvider.Api/Validators/UavTileMetadataValidator.cs. Unit tests in SatelliteProvider.Tests/Validators/UavTileBatchMetadataPayloadValidatorTests.cs (4 methods) + SatelliteProvider.Tests/Validators/UavTileMetadataValidatorTests.cs (9 methods) = 13 total (≥11 required).
AC-4 Integration tests in SatelliteProvider.IntegrationTests/UavUploadValidationTests.cs — 16 methods (≥13 required): happy + 15 failure modes covering rules 2-14 + AC-4-mandated nested type-mismatch.
AC-5 Contract _docs/02_document/contracts/api/uav-tile-upload.md bumped v1.1.0 → v1.2.0. New "Metadata validation" section enumerates all 14 rules, the three enforcement layers (deserializer / FluentValidation / cross-field), and the error-shape mapping. v1.2.0 changelog entry references AZ-810.
AC-6 _docs/02_document/modules/api_program.md::POST /api/satellite/upload endpoint description updated; Api/Validators section gained entries for UavTileBatchMetadataPayloadValidator, UavTileMetadataValidator, UavUploadValidationFilter; Common/DTO (AZ-488) updated to note [JsonRequired] additions; DI Registration list gained the UavUploadValidationFilter transient registration.
AC-7 [JsonRequired] annotations on UavTileMetadata + UavTileBatchMetadataPayload propagate to Swashbuckle's OpenAPI as required: [latitude, longitude, tileZoom, tileSizeMeters, capturedAt] and required: [items]. Endpoint chain in Program.cs declares .Accepts<UavTileBatchUploadRequest>("multipart/form-data") + .Produces<UavTileBatchUploadResponse>(200) + .ProducesProblem(400). Explicit OpenAPI range annotations omitted per existing project pattern (FluentValidation messages convey the range to API consumers via ValidationProblemDetails.errors).
AC-8 Probe script scripts/probe_upload_validation.sh — happy + 14 failure modes via curl. Reuses probe_route_validation.sh structure (JWT mint, status-code assertion, --exit-on-fail driver).
AC-9 No regression in AZ-488: validator rules all align with the legal payloads UavUploadTests already sends (lat/lon in range, tileZoom = 18, tileSizeMeters = 200.0, capturedAt = UtcNow or recent past, items.Count ∈ [1, 100]). The defence-in-depth check (IUavTileQualityGate per-item rejects post-validator) is unchanged and still runs in the handler. Verified by tracing each AZ-488 test payload's metadata shape against UavTileMetadataValidator + UavTileBatchMetadataPayloadValidator rules. Full integration-test pass gating deferred to autodev Step 11.

Code Review Verdict: PASS_WITH_WARNINGS

See _docs/03_implementation/reviews/batch_04_cycle8_review.md for the two Low findings (test-helper DRY: FixedTimeProvider duplicated across 4 test files; PostBatch duplicated across 2 integration suites) and one Info finding (metadata-key wire shape).

Cumulative Code Review: PASS_WITH_WARNINGS

See _docs/03_implementation/cumulative_review_batches_01-04_cycle8_report.md for the cycle-8 cross-batch consistency check. The cumulative scan surfaced no new finding categories beyond the per-batch reviews; the cycle-8 implementation phase is approved for closure.

Auto-Fix Attempts: 0

No mid-batch failures required auto-fix. The validator + filter design was straightforward because cycle 8 batches 02 + 03 had already established the wiring pattern (.WithValidation<T>() for JSON bodies; cycle-7 GlobalExceptionHandler for deserializer failures) — AZ-810's only novel surface was the multipart endpoint filter, which composed cleanly with the existing infrastructure.

Stuck Agents: None

Files Modified

AZ-810 (UAV upload validator + multipart filter)

Path Kind
SatelliteProvider.Common/DTO/UavTileMetadata.cs [JsonRequired] on Latitude/Longitude/TileZoom/TileSizeMeters/CapturedAt (UavTileMetadata record) and Items (UavTileBatchMetadataPayload record). FlightId stays nullable per AZ-503 anonymous-flight semantics. File-comment block updated with the AZ-810 rationale.
SatelliteProvider.Api/Validators/UavTileBatchMetadataPayloadValidator.cs NEW — root validator: Items NotNull + NotEmpty + Must(<= MaxBatchSize) + RuleForEach.SetValidator(new UavTileMetadataValidator(...)). TimeProvider threaded through to the per-item validator.
SatelliteProvider.Api/Validators/UavTileMetadataValidator.cs NEW — per-item validator: lat ∈ [-90, 90], lon ∈ [-180, 180], tileZoom ∈ [0, 22], tileSizeMeters > 0, capturedAt within [now - MaxAgeDays, now + CapturedAtFutureSkewSeconds]. FlightId deliberately not validated (shape-only via the deserializer).
SatelliteProvider.Api/Validators/UavUploadValidationFilter.cs NEWIEndpointFilter for the multipart endpoint. Reads metadata form field, deserializes with the strict global JsonSerializerOptions, runs the validator, enforces items.Count == files.Count. FluentValidation errors prefixed with metadata. so the wire key is metadata.items[0].latitude. Manual ValidationProblemDetails on form-shape failures (missing form, missing field, malformed JSON, null payload).
SatelliteProvider.Api/Program.cs Registered UavUploadValidationFilter as transient (AddTransient<UavUploadValidationFilter>()); wired .AddEndpointFilter<UavUploadValidationFilter>() + .Accepts<UavTileBatchUploadRequest>("multipart/form-data") + .Produces<UavTileBatchUploadResponse>(200) + .ProducesProblem(400) onto the MapPost("/api/satellite/upload", ...) chain. Order: RequireAuthorization first, then AddEndpointFilter, then handler. Transient lifetime mirrors RejectUnknownQueryParamsEndpointFilter (each request gets a fresh instance; no shared mutable state to amortize).
SatelliteProvider.Tests/Validators/UavTileBatchMetadataPayloadValidatorTests.cs NEW — 4 unit tests covering: happy single-item, items NotEmpty, items count > MaxBatchSize, per-item failure propagation with indexed paths (items[1].latitude).
SatelliteProvider.Tests/Validators/UavTileMetadataValidatorTests.cs NEW — 9 unit tests covering: all valid → pass, lat out of range, lon out of range, tileZoom out of range, tileSizeMeters non-positive, capturedAt future, capturedAt too old, flightId null → pass, flightId set → pass. Uses local FixedTimeProvider (see review F1 for DRY follow-up).
SatelliteProvider.IntegrationTests/UavUploadValidationTests.cs NEW — 16 end-to-end tests against the live endpoint. Happy + 15 failure modes (rules 2-14 + AC-4 nested type-mismatch). Uses ProblemDetailsAssertions.AssertValidationProblem + AssertErrorsContainsMention.
SatelliteProvider.IntegrationTests/Program.cs Wired UavUploadValidationTests.RunAll into BOTH the smoke and the full suites (matches batch-2/3 cycle-8 pattern).
scripts/probe_upload_validation.sh NEW — bash + curl probe of happy + 14 failure modes. Reuses probe_route_validation.sh structure (JWT mint, status-code assertion driver).
_docs/02_document/contracts/api/uav-tile-upload.md Version bumped v1.1.0 → v1.2.0. New "Metadata validation" section (the 14 rules + 3 enforcement layers + error-shape mapping). Expanded "HTTP 400 — envelope error" section with the new failure shapes. v1.2.0 changelog entry.
_docs/02_document/modules/api_program.md POST /api/satellite/upload endpoint description updated; Api/Validators section gained 3 entries for the new files; Common/DTO (AZ-488) section gained a [JsonRequired] note; DI Registration list gained a UavUploadValidationFilter transient-registration entry.

Tracker

  • AZ-810: To Do → In Progress (batch 4 start) → In Testing (post-implementation, post-cumulative-review, pre-commit). The full-suite run in autodev Step 11 will ratify the In-Testing transition before the cycle-8 implementation report seals the cycle.

Next Batch

None — batch 4 was the final batch of cycle 8. Cycle 8's strict-validation theme is fully wrapped:

Endpoint Validator Cycle 8 batch
POST /api/satellite/request RegionRequestValidator 02 (AZ-808)
POST /api/satellite/route CreateRouteRequestValidator + nested chain 03 (AZ-809)
POST /api/satellite/upload UavTileBatchMetadataPayloadValidator + UavUploadValidationFilter 04 (AZ-810)
GET /api/satellite/tiles/latlon GetTileByLatLonQueryValidator + RejectUnknownQueryParamsEndpointFilter 02 (AZ-811)
POST /api/satellite/tiles/inventory InventoryRequestValidator (cycle 7)
GET /api/satellite/region/{id} (read-only by path Guid; strict-validation N/A)
GET /api/satellite/route/{id} (read-only by path Guid; strict-validation N/A)

Implement skill should hand back to autodev for Step 11 (Run Tests) → Step 12 (tracker transition) → Step 13 (archive) → cycle implementation report → Step 14 loop exit.