mirror of
https://github.com/azaion/satellite-provider.git
synced 2026-06-22 14:21:15 +00:00
[AZ-794] [AZ-795] [AZ-796] Strict input validation + z/x/y rename
AZ-794: rename inventory wire fields tileZoom/tileX/tileY -> z/x/y to match the slippy-map URL convention. Contract bumped to v2.0.0. AZ-795: shared validation infrastructure -- FluentValidation + ValidationEndpointFilter + GlobalValidatorConfig (camelCase paths). GlobalExceptionHandler now converts JsonException (UnmappedMember + JsonRequired) into RFC 7807 ValidationProblemDetails. JSON layer hardened with UnmappedMemberHandling.Disallow + camelCase naming policy. New error-shape.md contract. AZ-796: InventoryRequestValidator covers 9 rules (XOR tiles vs locationHashes, cap 1000, z 0..22, x/y in slippy bounds, hash length/charset). 16 unit tests + 16 integration tests + a manual curl probe script. Adjacent fixes uncovered by the new strict layer: - IdempotentPostTests RoutePoint payload corrected to lat/lon (the DTO has used JsonPropertyName for ages; previously silently ignored under PascalCase fallback). - TileInventoryTests slippy x/y reduced to fit z=18 bounds. - docker-compose.yml host port for Postgres moved 5432 -> 5433 to avoid sibling-project conflict; appsettings.Development + README + AGENTS + architecture + containerization docs aligned. New coderule (suite + repo): API consumer-facing OpenAPI descriptions must not contain task IDs, contract filenames, or version-bump history -- internal change tracking belongs in commits/contract docs/changelogs. Existing offending descriptions in Program.cs cleaned up. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -123,9 +123,9 @@ Adopted into satellite-provider cycle 7 with the recommended ordering: shared va
|
||||
|
||||
| Task | Title | Depends On | Points | Status |
|
||||
|------|-------|-----------|--------|--------|
|
||||
| AZ-794 | Inventory body fields: rename `tileZoom/tileX/tileY` → `z/x/y` (OSM convention) | — (coordinate release with AZ-795 / AZ-796) | 3 | To Do (cycle 7) |
|
||||
| AZ-795 | Strict input validation across all public endpoints (FluentValidation + ProblemDetails) — **Epic with shared-infra ship** | — (children gated on shared infra landing first) | — (epic; shared-infra estimate 5–8 pts; per-endpoint children ~3 pts each) | To Do (cycle 7) |
|
||||
| AZ-796 | Strict validation for inventory endpoint (POST /api/satellite/tiles/inventory) | AZ-795 (HARD — shared infra); coordinate with AZ-794 | 3 | To Do (cycle 7) |
|
||||
| AZ-794 | Inventory body fields: rename `tileZoom/tileX/tileY` → `z/x/y` (OSM convention) | — (coordinate release with AZ-795 / AZ-796) | 3 | Done (cycle 7) |
|
||||
| AZ-795 | Strict input validation across all public endpoints (FluentValidation + ProblemDetails) — **Epic with shared-infra ship** | — (children gated on shared infra landing first) | — (epic; shared-infra estimate 5–8 pts; per-endpoint children ~3 pts each) | Done — shared infra shipped (cycle 7); future per-endpoint child tasks open |
|
||||
| AZ-796 | Strict validation for inventory endpoint (POST /api/satellite/tiles/inventory) | AZ-795 (HARD — shared infra); coordinate with AZ-794 | 3 | Done (cycle 7) |
|
||||
|
||||
## Execution Order
|
||||
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
# Inventory API: rename body fields to OSM-style z/x/y
|
||||
|
||||
**Task**: AZ-794_inventory_field_rename_osm
|
||||
**Name**: Rename inventory body fields tileZoom/tileX/tileY → z/x/y (OSM convention)
|
||||
**Description**: Align the `POST /api/satellite/tiles/inventory` body shape with the URL-path slippy-map convention (`{z}/{x}/{y}`). Same coordinate concept is named two different ways inside the same API today; this task makes the body match the URL.
|
||||
**Complexity**: 3 points (recommended; final call by parent-suite team)
|
||||
**Dependencies**: — (coordinate release window with AZ-795 / AZ-796 to bundle the breaking changes)
|
||||
**Component**: SatelliteProvider.Api + SatelliteProvider.Common (DTOs)
|
||||
**Tracker**: AZ-794 (https://denyspopov.atlassian.net/browse/AZ-794)
|
||||
**Epic**: — (related to AZ-795 input-validation epic via Jira "Relates" link)
|
||||
**Originating ticket**: gps-denied-onboard AZ-777 Phase 1 (Jetson probe, 2026-05-22)
|
||||
|
||||
## Origin
|
||||
|
||||
Surfaced during gps-denied-onboard AZ-777 Phase 1 Jetson probing of the parent-suite `satellite-provider` service. The reviewing engineer noted that the inventory endpoint uses two different naming conventions for the same coordinate concept:
|
||||
|
||||
- URL: `GET /tiles/{z}/{x}/{y}` — OSM/XYZ-standard short names.
|
||||
- Body: `{"tiles": [{"tileZoom":12,"tileX":2424,"tileY":1424}]}` — verbose .NET-style names.
|
||||
|
||||
Conformance to the URL convention end-to-end (both URL and body) aligns with 20 years of slippy-map ecosystem norms (OSM, Google, Mapbox, Bing, Leaflet, MapLibre) and removes the need for consumers to translate at the boundary. Jira AZ-794 is the authoritative spec; this file mirrors the in-workspace-only sections that the satellite-provider implementer will need.
|
||||
|
||||
## Problem
|
||||
|
||||
Same coordinate concept; two names; same API:
|
||||
- URL path uses `z`, `x`, `y`.
|
||||
- Request and response bodies use `tileZoom`, `tileX`, `tileY`.
|
||||
|
||||
Every consumer that thinks in slippy-map vocabulary has to translate at the boundary. Wire size is also ~3× larger than necessary on the field names (a 900-tile inventory request carries ~5 KB of `tileZoom`/`tileX`/`tileY` vs ~1.7 KB of `z`/`x`/`y`).
|
||||
|
||||
## Outcome
|
||||
|
||||
- `POST /api/satellite/tiles/inventory` request body uses `{"tiles": [{"z":...,"x":...,"y":...}]}` per tile entry.
|
||||
- Response body uses `{"results": [{"z":...,"x":...,"y":...,"locationHash":...,"present":...,...}]}` per entry. All non-coord fields (`locationHash`, `present`, `id`, `capturedAt`, `source`, `flightId`, `resolutionMPerPx`) unchanged.
|
||||
- OpenAPI / Swagger spec updated to match.
|
||||
- Schema doc `_docs/02_document/contracts/api/tile-inventory.md` bumped (v1.x.0 → next major) with a Migration / Coexistence section.
|
||||
- Integration tests updated.
|
||||
- Release notes / migration guide entry naming AZ-794 as the breaking-rename owner.
|
||||
|
||||
## Scope
|
||||
|
||||
### Included
|
||||
|
||||
- DTOs for the inventory request + response — exact file path to be confirmed by the implementer (likely `SatelliteProvider.Common/DTO/TileInventory.cs` per AZ-505 spec).
|
||||
- The MapPost handler / minimal-api endpoint registration in `SatelliteProvider.Api/Program.cs` (or wherever the inventory endpoint is wired today).
|
||||
- `SatelliteProvider.IntegrationTests/TileInventoryTests.cs` — update payload builders + response assertions.
|
||||
- `_docs/02_document/contracts/api/tile-inventory.md` — major version bump with Change Log entry naming this task.
|
||||
- Release notes / migration guide.
|
||||
|
||||
### Excluded
|
||||
|
||||
- The `GET /tiles/{z}/{x}/{y}` endpoint — already uses the correct names; no change.
|
||||
- Strict input validation — owned by AZ-796 (sibling under epic AZ-795).
|
||||
- Other endpoint renames.
|
||||
- Internal storage / query / repo-method names (`tile_zoom`, `tile_x`, `tile_y` DB columns) — wire-format change only.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
**AC-1: Request body uses short names**
|
||||
Given a POST body `{"tiles":[{"z":12,"x":2424,"y":1424}]}` with valid JWT
|
||||
When `POST /api/satellite/tiles/inventory` is called
|
||||
Then HTTP 200 with `results[0].z == 12`, `results[0].x == 2424`, `results[0].y == 1424` and a deterministic non-zero `locationHash`.
|
||||
|
||||
**AC-2: Response body uses short names**
|
||||
Given a successful inventory call
|
||||
When the response is parsed
|
||||
Then every `results[i]` object contains `z`, `x`, `y` keys (not `tileZoom`, `tileX`, `tileY`). All other fields (`locationHash`, `present`, `id`, `capturedAt`, `source`, `flightId`, `resolutionMPerPx`) are unchanged byte-for-byte from the pre-rename contract.
|
||||
|
||||
**AC-3: OpenAPI spec accuracy**
|
||||
Given `/swagger/v1/swagger.json` (or equivalent)
|
||||
When the InventoryRequest + InventoryEntry schemas are inspected
|
||||
Then they declare `z`, `x`, `y` (not the old names) as the required coordinate properties.
|
||||
|
||||
**AC-4: Migration guidance documented**
|
||||
Given the rename ships
|
||||
Then `_docs/02_document/contracts/api/tile-inventory.md` is bumped to a new major version, the Change Log entry names AZ-794 and the breaking-rename, and a Migration / Coexistence section either (a) names the hard-switch release with the consumer-side bump coordinated, or (b) documents the accept-both transition window.
|
||||
|
||||
## Rollout — pick one
|
||||
|
||||
- **Option 1 — hard switch** (recommended while consumer count is small): rename atomically, bump API version, ship coordinated consumer update in the same release.
|
||||
- **Option 2 — accept-both transition**: server accepts both `z` and `tileZoom` on input for one release; response always uses short names; deprecation notice in release notes; remove long names in next release.
|
||||
|
||||
## Constraints
|
||||
|
||||
- **Breaking change** — coordinate with all known consumers before shipping. Known consumer at filing time: `gps-denied-onboard` `HttpTileDownloader` in `src/gps_denied_onboard/components/c11_tile_manager/tile_downloader.py`. Parent-suite team to enumerate the full consumer set before deciding rollout cadence.
|
||||
- **No internal storage rename** — DB columns (`tile_zoom`, `tile_x`, `tile_y`) stay as named. Wire-format change only; internal Postgres schema is out of scope.
|
||||
- **Coordinate with AZ-795 / AZ-796** — if validation strictness ships in the same release as the rename, the validators must use the new short names from day one. Recommended ordering: ship AZ-794 first, then AZ-795 shared infra, then AZ-796.
|
||||
|
||||
## References
|
||||
|
||||
- Jira AZ-794: https://denyspopov.atlassian.net/browse/AZ-794
|
||||
- Related: AZ-795 (validation epic), AZ-796 (inventory validation child)
|
||||
- Originating discovery: gps-denied-onboard AZ-777 (Phase 1 Jetson probe, 2026-05-22)
|
||||
- Current contract doc: `_docs/02_document/contracts/api/tile-inventory.md` v1.0.0
|
||||
- Known consumer side: `gps-denied-onboard/src/gps_denied_onboard/components/c11_tile_manager/tile_downloader.py`
|
||||
@@ -0,0 +1,153 @@
|
||||
# Strict input validation across all public endpoints (FluentValidation + ProblemDetails)
|
||||
|
||||
**Task**: AZ-795_strict_validation_epic
|
||||
**Name**: Strict input validation across all public endpoints
|
||||
**Type**: Epic
|
||||
**Description**: Every public HTTP endpoint must reject malformed input with structured 4xx errors instead of silently coercing missing fields to zero / ignoring unknown fields. Recommended approach: FluentValidation + global ProblemDetails filter + `JsonSerializerOptions.UnmappedMemberHandling.Disallow`.
|
||||
**Complexity**: — (epic; rolls up children. Estimate: 5–8 pts shared infra + ~3 pts per per-endpoint child)
|
||||
**Dependencies**: — (per-endpoint children depend on shared infra landing first)
|
||||
**Component**: SatelliteProvider.Api (DI wiring + global filter + DTOs + validators)
|
||||
**Tracker**: AZ-795 (https://denyspopov.atlassian.net/browse/AZ-795)
|
||||
**Children**: AZ-796 (inventory endpoint — first concrete child); sibling per-endpoint tasks to be added by parent-suite team
|
||||
**Originating ticket**: gps-denied-onboard AZ-777 Phase 1 (Jetson probe, 2026-05-22)
|
||||
|
||||
## Origin
|
||||
|
||||
Discovered during gps-denied-onboard AZ-777 Phase 1 Jetson probing on 2026-05-22. A hand-typed inventory request with the wrong field names (`{"z","x","y"}` instead of the current `{"tileZoom","tileX","tileY"}`) returned **HTTP 200** with `(0,0,0)` coordinates and an identical `locationHash` for every entry. Real client bugs masquerade as valid results because the deserializer silently treats unknown fields as missing and missing fields as `default(int) = 0`.
|
||||
|
||||
For a service that's the single source of truth about which satellite tiles exist, permissive parsing is actively dangerous: corruption downstream, confident wrong answers, hours of debugging on the consumer side.
|
||||
|
||||
Jira AZ-795 is the authoritative spec; this file mirrors the in-workspace-only sections that the satellite-provider implementer will need.
|
||||
|
||||
## Problem
|
||||
|
||||
Every public-facing JSON endpoint on satellite-provider inherits the same Postel-permissive parsing default:
|
||||
- Missing required fields → silently `default(T)` (e.g. `0` for `int`).
|
||||
- Unknown fields → silently dropped (no `[JsonExtensionData]` capture, no log entry).
|
||||
- Wrong types → silently coerced where possible, silently dropped where not.
|
||||
|
||||
No structured error response. The only contract-level signal a misbehaving client gets today is downstream weirdness (wrong `locationHash`, repeated identical results, etc.) — many hops away from the actual cause.
|
||||
|
||||
## Outcome
|
||||
|
||||
- Every public-facing JSON endpoint rejects malformed input with **HTTP 400 + RFC 7807 ProblemDetails** body naming the offending field(s).
|
||||
- Validators are testable in isolation (unit tests per `RuleFor`) and enforced by the HTTP layer without per-controller try/catch boilerplate.
|
||||
- Unknown-field rejection is wired at the deserializer level so typos can't reach a validator.
|
||||
- Uniform error response shape across all endpoints.
|
||||
- New `_docs/02_document/contracts/api/error-shape.md` v1.0.0 documenting the ProblemDetails contract every endpoint conforms to.
|
||||
|
||||
## Recommended approach
|
||||
|
||||
1. **FluentValidation** for input DTOs (declarative, composable, validators are testable units). Final stack choice belongs to the parent-suite team; if FluentValidation is ruled out by existing constraints, alternatives are stock DataAnnotations + custom model binders or hand-written `IValidator<T>`.
|
||||
2. **Global error filter / ASP.NET model-state behavior** that emits RFC 7807 ProblemDetails for every validation failure. No per-endpoint try/catch boilerplate.
|
||||
3. **Unknown-field rejection** at the deserializer: `JsonSerializerOptions.UnmappedMemberHandling.Disallow` (.NET 8+) or `Newtonsoft.Json` `MissingMemberHandling.Error`. Catches typos like `{"Z":12}` (uppercase) that no validator can catch after deserialization.
|
||||
|
||||
## Error response contract (uniform across all endpoints)
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
|
||||
"title": "One or more validation errors occurred.",
|
||||
"status": 400,
|
||||
"errors": {
|
||||
"tiles[0].z": ["The z field is required."],
|
||||
"tiles[1]": ["Unexpected field: 'tileZoom'."]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Stable enough for consumers to pattern-match. Field names in `errors` paths must use the same casing as the request body (post-AZ-794 short names for the inventory endpoint).
|
||||
|
||||
## Scope
|
||||
|
||||
### Included — Shared infrastructure (this epic owns)
|
||||
|
||||
- DI wiring for FluentValidation (or chosen alternative) in `SatelliteProvider.Api/Program.cs` (or appropriate composition root).
|
||||
- Global error filter / `Configure<ApiBehaviorOptions>(...)` for ProblemDetails formatting.
|
||||
- `JsonSerializerOptions` configuration for unknown-field rejection.
|
||||
- A validator-coverage table in `_docs/02_document/architecture.md` (or equivalent) listing each public endpoint and its validator class.
|
||||
- Shared test fixtures for ProblemDetails assertions in `SatelliteProvider.IntegrationTests`.
|
||||
- New contract artifact: `_docs/02_document/contracts/api/error-shape.md` v1.0.0 (the ProblemDetails shape every endpoint conforms to).
|
||||
|
||||
### Included — Per-endpoint child tasks
|
||||
|
||||
- One child Task per public-facing endpoint that has a JSON body. Each consumes the shared infra.
|
||||
- Each child uses the AC template below.
|
||||
- Parent-suite team enumerates the full endpoint surface from `/swagger/v1/swagger.json` / route map and creates the children.
|
||||
- First child (concrete reference implementation): **AZ-796** — inventory endpoint.
|
||||
|
||||
### Excluded
|
||||
|
||||
- Authentication / authorization changes (JWT contract owned by AZ-494).
|
||||
- Endpoint renaming (**AZ-794** owns the inventory body-field rename).
|
||||
- Rate-limiting / quota (separate concern).
|
||||
- Internal-only admin endpoints, health probes, metrics scrapers (parent-suite team owns in/out decision per endpoint).
|
||||
|
||||
## Acceptance Criteria template (every child task must satisfy)
|
||||
|
||||
**AC-1: Missing required field → 400**
|
||||
Given a POST body that omits a required field
|
||||
When the endpoint is called
|
||||
Then HTTP 400 with `errors.<field>` listing the missing field.
|
||||
|
||||
**AC-2: Unknown field → 400**
|
||||
Given a POST body with an unrecognized field at root or in nested objects
|
||||
When the endpoint is called
|
||||
Then HTTP 400 with `errors[].<location>` naming the unexpected field.
|
||||
|
||||
**AC-3: Wrong type → 400**
|
||||
Given a POST body with a field of unexpected JSON type (e.g. string where integer expected)
|
||||
When the endpoint is called
|
||||
Then HTTP 400 with `errors.<field>` describing the type mismatch.
|
||||
|
||||
**AC-4: Out-of-range value → 400**
|
||||
Given a POST body with a value outside its supported range
|
||||
When the endpoint is called
|
||||
Then HTTP 400 with `errors.<field>` describing the valid range.
|
||||
|
||||
**AC-5: Empty array where non-empty required → 400**
|
||||
Given a POST body where a required non-empty collection is empty
|
||||
When the endpoint is called
|
||||
Then HTTP 400 with `errors.<field>` describing the constraint.
|
||||
|
||||
**AC-6: Validator class is its own file + unit-tested**
|
||||
A `IValidator<RequestDto>` (or equivalent) class exists in its own file under `SatelliteProvider.Api/Validators/` (or per-suite convention), with a unit test per `RuleFor(...)`.
|
||||
|
||||
**AC-7: Integration tests cover one happy + one failure path per AC**
|
||||
`SatelliteProvider.IntegrationTests` adds a fixture that POSTs each bad-payload variant and asserts `status == 400` + ProblemDetails shape + specific `errors[].<location>` path.
|
||||
|
||||
**AC-8: OpenAPI / Swagger spec accuracy**
|
||||
`/swagger/v1/swagger.json` marks required fields, declares ranges, and documents the new 400 response shape.
|
||||
|
||||
## Test requirements
|
||||
|
||||
- **Unit**: one xUnit class per validator. Tests cover each `RuleFor(...)` / equivalent.
|
||||
- **Integration**: `SatelliteProvider.IntegrationTests` adds one fixture per endpoint covering all AC variants (~7–10 new tests per endpoint).
|
||||
- **Contract**: OpenAPI spec snapshot test confirms the published schema rejects what the validator rejects.
|
||||
- **Cross-cutting**: shared `ProblemDetailsAssertions` helper in test infra so every endpoint's failure tests use the same assertion shape.
|
||||
|
||||
## Migration / breaking-change strategy
|
||||
|
||||
Tightening validation is a **breaking behavior change**: clients that today get 200 OK with nonsense will start getting 400. Three approaches the parent-suite team can pick from:
|
||||
|
||||
1. **Hard switch** — ship in one release with a clear "Breaking" note. Cleanest for low-consumer-count (currently 1).
|
||||
2. **Soft warning then enforce** — log warnings for one release when malformed input arrives; enforce in the next.
|
||||
3. **API versioning** — keep `/v1` permissive, add `/v2` strict, migrate consumers, remove `/v1`.
|
||||
|
||||
Recommendation: **#1** while the consumer set is small (currently 1 known: `gps-denied-onboard`).
|
||||
|
||||
## Constraints
|
||||
|
||||
- Shared infra MUST land before any per-endpoint child task — children are gated on it.
|
||||
- Coordinate with **AZ-794** (inventory rename) — recommended ordering ships AZ-794 first so this epic's validators use the final names from day one.
|
||||
- Parent-suite team enumerates the full consumer set before deciding rollout cadence (not just `gps-denied-onboard`).
|
||||
- Per-endpoint child tasks added by parent-suite team after enumerating endpoint surface from OpenAPI / route map. Do NOT create all children up-front — let them be added as the team decomposes.
|
||||
|
||||
## References
|
||||
|
||||
- Jira AZ-795: https://denyspopov.atlassian.net/browse/AZ-795
|
||||
- First child: AZ-796 (inventory endpoint)
|
||||
- Related: AZ-794 (inventory rename), AZ-777 (originating discovery in gps-denied-onboard)
|
||||
- Originating discovery: gps-denied-onboard AZ-777 Phase 1 Jetson probe (2026-05-22)
|
||||
- ASP.NET ProblemDetails reference: https://learn.microsoft.com/en-us/aspnet/core/web-api/handle-errors
|
||||
- FluentValidation reference: https://docs.fluentvalidation.net/
|
||||
@@ -0,0 +1,124 @@
|
||||
# Strict validation for inventory endpoint (POST /api/satellite/tiles/inventory)
|
||||
|
||||
**Task**: AZ-796_inventory_endpoint_validation
|
||||
**Name**: Strict validation for inventory endpoint
|
||||
**Description**: Add FluentValidation-backed strict input validation to `POST /api/satellite/tiles/inventory`. Reject malformed payloads with RFC 7807 ProblemDetails (HTTP 400). First concrete child of the validation-hardening epic (AZ-795); serves as reference implementation pattern for sibling per-endpoint tasks.
|
||||
**Complexity**: 3 points (recommended)
|
||||
**Dependencies**: AZ-795 (HARD — shared FluentValidation + ProblemDetails + unknown-field-rejection infra must land first); coordinate with AZ-794 (rename)
|
||||
**Component**: SatelliteProvider.Api/Validators + SatelliteProvider.Common (DTOs)
|
||||
**Tracker**: AZ-796 (https://denyspopov.atlassian.net/browse/AZ-796)
|
||||
**Epic**: AZ-795 — Strict input validation across all public endpoints
|
||||
**Originating ticket**: gps-denied-onboard AZ-777 Phase 1 (Jetson probe, 2026-05-22)
|
||||
|
||||
## Origin
|
||||
|
||||
Discovered during gps-denied-onboard AZ-777 Phase 1 Jetson probing on 2026-05-22 — see parent epic AZ-795 for full context. This ticket scopes the strict-validation work to the **inventory endpoint** as the first concrete reference implementation; sibling per-endpoint child tasks will follow the same pattern.
|
||||
|
||||
Jira AZ-796 is the authoritative spec; this file mirrors the in-workspace-only sections that the satellite-provider implementer will need.
|
||||
|
||||
## Problem
|
||||
|
||||
`POST /api/satellite/tiles/inventory` today accepts malformed payloads silently:
|
||||
- Missing required fields (`z`, `x`, `y`) → silently coerced to `0`, producing `locationHash` collisions and `(0,0,0)` echoed back as if the client had asked for tile (0,0,0).
|
||||
- Unknown fields (typos like `{"Z":12}` uppercase) → silently dropped, then required field appears missing → silently 0.
|
||||
- Wrong types → silently coerced where possible.
|
||||
- No structured 4xx response. Real client bugs surface downstream as "all my inventory results have the same locationHash" — many hops from the actual cause.
|
||||
|
||||
Concrete reproducer (from the originating probe):
|
||||
|
||||
```bash
|
||||
curl -sk -H "Authorization: Bearer $JWT" -H "Content-Type: application/json" \
|
||||
-d '{"tiles":[{"z":12,"x":2424,"y":1424},{"z":12,"x":2425,"y":1425}]}' \
|
||||
https://satellite-provider:8080/api/satellite/tiles/inventory
|
||||
```
|
||||
|
||||
Returns HTTP 200 with both `results` entries carrying `tileZoom:0, tileX:0, tileY:0` and identical `locationHash`. Expected: HTTP 400 naming `z`, `x`, `y` as unexpected fields (pre-AZ-794) or 200 with correct echo (post-AZ-794) — but never silently-wrong 200.
|
||||
|
||||
## Outcome
|
||||
|
||||
- `POST /api/satellite/tiles/inventory` rejects malformed payloads with HTTP 400 + RFC 7807 ProblemDetails matching the shape defined by the parent epic AZ-795.
|
||||
- An `IValidator<InventoryRequestDto>` (or equivalent) covers all 9 validation rules listed below.
|
||||
- Integration tests cover one happy path + one failure path per validation rule.
|
||||
- OpenAPI spec marks required fields, declares ranges, and documents the new 400 response.
|
||||
- Schema doc `_docs/02_document/contracts/api/tile-inventory.md` updated to reference the validation rules + error contract.
|
||||
|
||||
## Scope
|
||||
|
||||
### Included
|
||||
|
||||
- `InventoryRequestValidator` (or equivalent class) in `SatelliteProvider.Api/Validators/` — full coverage of the 9 validation rules below.
|
||||
- Wiring of the validator into the MapPost / minimal-api endpoint registration in `SatelliteProvider.Api/Program.cs` (or wherever the inventory endpoint is wired today). Wiring leverages the shared infra from AZ-795.
|
||||
- Unit tests for the validator (`SatelliteProvider.UnitTests` or appropriate — one test method per `RuleFor(...)`).
|
||||
- Integration tests in `SatelliteProvider.IntegrationTests/TileInventoryValidationTests.cs` (new file) — happy + failure case per AC.
|
||||
- Update to `_docs/02_document/contracts/api/tile-inventory.md` documenting the validation rules + error shape.
|
||||
- Update to `/swagger/v1/swagger.json` (via XML doc comments / Swashbuckle annotations) marking required fields + ranges + 400 response.
|
||||
|
||||
### Excluded
|
||||
|
||||
- Shared infra wiring (parent epic AZ-795 owns this).
|
||||
- Validation for other endpoints (sibling child tasks under AZ-795 will be added by parent-suite team).
|
||||
- The field rename itself (AZ-794).
|
||||
- Auth / JWT changes.
|
||||
- Performance considerations (existing AZ-505 perf gates remain in effect; validation overhead expected to be negligible vs DB round-trip).
|
||||
|
||||
## Required validations (9 rules)
|
||||
|
||||
Naming below assumes AZ-794 (rename) has shipped. If validators land BEFORE AZ-794, swap `z/x/y` for `tileZoom/tileX/tileY` and re-rename when AZ-794 lands.
|
||||
|
||||
1. **Body present** — null/empty body → 400.
|
||||
2. **`tiles` field required** — missing → 400 with `errors.tiles: ["required"]`.
|
||||
3. **`tiles` non-empty** — empty array → 400 with `errors.tiles: ["must contain at least 1 entry"]`.
|
||||
4. **`tiles` max size** — to be confirmed with parent-suite (existing AZ-505 spec uses 5000 for the EITHER/OR body shape; reaffirm here or align). Over the cap → 400.
|
||||
5. **Each entry has `z`, `x`, `y`** — any missing → 400 with `errors.tiles[i].<field>: ["required"]`.
|
||||
6. **Each field is non-negative integer** — wrong type or negative → 400.
|
||||
7. **`z` within supported zoom range** — out of range → 400 with `errors.tiles[i].z: ["must be between {min} and {max}"]`. Range to be confirmed with parent-suite (existing AZ-484 / AZ-503 schemas suggest 0–22; reaffirm here).
|
||||
8. **`x` / `y` within tile-axis bounds for given `z`** — i.e. `0 <= x,y < 2^z` — out of range → 400 with `errors.tiles[i].x` or `.y`.
|
||||
9. **Unknown fields at root or in tile entries** — 400 with `errors[].<location>: ["unexpected field: '<name>'"]`. Requires `JsonSerializerOptions.UnmappedMemberHandling.Disallow` (.NET 8+) at the deserializer level — this is part of AZ-795 shared infra and must be wired first.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
**AC-1: Each of the 9 validations rejects with HTTP 400 + ProblemDetails**
|
||||
Given a POST body that violates exactly ONE validation rule (one failure case per rule)
|
||||
When `POST /api/satellite/tiles/inventory` is called with valid JWT
|
||||
Then HTTP 400; response body matches the parent epic's ProblemDetails shape; `errors[].<location>` names the specific failing field; `errors[]` array does NOT include unrelated rules (single-rule precision).
|
||||
|
||||
**AC-2: Happy path unchanged**
|
||||
Given a POST body that satisfies all 9 validations
|
||||
When `POST /api/satellite/tiles/inventory` is called with valid JWT
|
||||
Then HTTP 200 with the existing result shape (one entry per requested tile, same ordering, fields preserved from the pre-validation contract). No regression in existing `TileInventoryTests.cs` happy-path assertions.
|
||||
|
||||
**AC-3: Validator class is its own file + unit-tested**
|
||||
A `InventoryRequestValidator` (or equivalent) class exists in its own file under `SatelliteProvider.Api/Validators/` (or per-suite convention). xUnit test class has one test method per `RuleFor(...)` — i.e. ≥ 9 unit-test methods.
|
||||
|
||||
**AC-4: Integration tests cover happy + failure per rule**
|
||||
`SatelliteProvider.IntegrationTests/TileInventoryValidationTests.cs` (new file) has ≥ 10 test methods: 1 happy path + 9 failure cases. Each failure case POSTs the malformed payload, asserts `status == 400`, asserts ProblemDetails shape, asserts the specific `errors[].<location>` matches the rule.
|
||||
|
||||
**AC-5: OpenAPI spec accuracy**
|
||||
Given `/swagger/v1/swagger.json` (or equivalent)
|
||||
When the InventoryRequest schema + endpoint operation are inspected
|
||||
Then required fields are marked `required: true`, integer types are declared with `minimum`/`maximum` per the validation rules, the endpoint declares a 400 response with the ProblemDetails schema.
|
||||
|
||||
**AC-6: Schema doc updated**
|
||||
`_docs/02_document/contracts/api/tile-inventory.md` is updated (Change Log entry naming AZ-796) to document the validation rules + error contract. No version bump required (additive — error shape is a previously-undefined contract; clients that send valid payloads see no change).
|
||||
|
||||
**AC-7: Manual probe captures each failure mode end-to-end**
|
||||
A `scripts/probe_inventory_validation.sh` (or Postman / Bruno collection) is committed that exercises each failure mode via real `curl` with a JWT, capturing the actual response body for documentation/regression.
|
||||
|
||||
## Coordination with sibling tickets
|
||||
|
||||
- **Parent (AZ-795)**: shared FluentValidation + ProblemDetails + unknown-field-rejection infra must land first.
|
||||
- **AZ-794 (inventory rename)**: if it ships first, validators use `z/x/y` from day one. If it ships in the same release, coordinate field names so this ticket lands once with the final names. If it ships later, validators initially use `tileZoom/tileX/tileY` and get renamed at AZ-794 ship time — less ideal but acceptable.
|
||||
|
||||
## Constraints
|
||||
|
||||
- **Breaking behavior change** — clients that today get 200 with nonsense will start getting 400. Coordinate rollout with all known consumers per AZ-795's migration strategy section.
|
||||
- **No regression in existing `TileInventoryTests.cs`** happy-path assertions (AZ-505 AC coverage).
|
||||
- **No change to internal repository / DB query path** — validation lives at the API layer only.
|
||||
|
||||
## References
|
||||
|
||||
- Jira AZ-796: https://denyspopov.atlassian.net/browse/AZ-796
|
||||
- Parent epic: AZ-795 (shared infra; error-shape contract)
|
||||
- Related: AZ-794 (rename), AZ-505 (existing inventory endpoint spec)
|
||||
- Originating discovery: gps-denied-onboard AZ-777 Phase 1 Jetson probe (2026-05-22)
|
||||
- Current contract doc: `_docs/02_document/contracts/api/tile-inventory.md` v1.0.0
|
||||
Reference in New Issue
Block a user