# 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
