mirror of
https://github.com/azaion/satellite-provider.git
synced 2026-06-22 12:11:14 +00:00
[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>
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
# 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:
|
||||
- `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`):
|
||||
|
||||
```json
|
||||
{
|
||||
"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.length` ≤ `UavQualityConfig.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]`)
|
||||
|
||||
7. **`lat` required** — double, in `[-90.0, 90.0]`. Missing/out-of-range → 400 with `errors.metadata.items[i].lat`.
|
||||
8. **`lon` required** — double, in `[-180.0, 180.0]`. Missing/out-of-range → 400 with `errors.metadata.items[i].lon`.
|
||||
9. **`tileZoom` required** — int, in `[0, 22]` (align with `TileCoordValidator`). Missing/out-of-range → 400 with `errors.metadata.items[i].tileZoom`.
|
||||
10. **`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`.)
|
||||
11. **`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.)
|
||||
12. **`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
|
||||
|
||||
13. **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").
|
||||
14. **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 4–6.
|
||||
- `UavTileMetadataValidator.cs` — per-item validator (rules 7–12); 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)
|
||||
Reference in New Issue
Block a user