mirror of
https://github.com/azaion/satellite-provider.git
synced 2026-06-21 22:41:14 +00:00
34ee1e0b83
AZ-808: FluentValidation for POST /api/satellite/request - RegionRequestValidator: id non-empty, lat/lon/sizeMeters/zoomLevel ranges - RequestRegionRequest: [JsonRequired] on every property, no implicit defaults - Wired via .WithValidation<RequestRegionRequest>() in MapPost chain - Unit + integration tests + curl probe script - New contract: contracts/api/region-request.md v1.0.0 AZ-811: FluentValidation + envelope filter for GET /api/satellite/tiles/latlon - GetTileByLatLonQuery: nullable record (double?/int?) so the minimal-API binder never short-circuits with BadHttpRequestException before filters - GetTileByLatLonQueryValidator: Cascade(Stop) + NotNull + InclusiveBetween per param; missing surfaces as `\`<name>\` is required.` - RejectUnknownQueryParamsEndpointFilter: reusable IEndpointFilter that rejects any query key outside the allowed set with errors[<key>] map; catches legacy `?Latitude=` typos and hostile probes (`?debug=1&admin=1`) - Handler: [AsParameters] GetTileByLatLonQuery + .Value deref post-validator - Unit (validator + filter) + integration tests + curl probe script - New contract: contracts/api/tile-latlon.md v1.0.0 Shared hygiene - Promote AssertErrorsContainsMention from per-test-file private helpers to ProblemDetailsAssertions (closes batch-1 Low-severity DRY warning) - Sync Swagger param descriptions, README, blackbox/security/perf scripts, uuidv5 doc with the new lat/lon/zoom query-param names Docs - system-flows.md F1/F2 reference the new contracts + validation layers - modules/api_program.md adds Api/Validators + Api/DTOs sections - _autodev_state.md: batch 2 of 4 complete; next batch = AZ-809 All smoke tests green (mode=smoke, exit 0). AZ-808 + AZ-811 transitioned to In Testing on Jira. Co-authored-by: Cursor <cursoragent@cursor.com>
166 lines
10 KiB
Markdown
166 lines
10 KiB
Markdown
# Contract: tile-latlon
|
|
|
|
**Component**: WebApi (`SatelliteProvider.Api`) producing rows via TileDownloader (`SatelliteProvider.Services.TileDownloader`)
|
|
**Producer task**: AZ-811 — `_docs/02_tasks/done/AZ-811_latlon_get_endpoint_validation.md` (validator + this contract; renames query params `Latitude/Longitude/ZoomLevel` → `lat/lon/zoom` for OSM consistency)
|
|
**Consumer tasks**: dev / debug clients, future mission-planner UI single-tile-by-click flows; NOT currently consumed by `gps-denied-onboard` (the onboard side uses `GET /tiles/{z}/{x}/{y}` with pre-computed coords from inventory)
|
|
**Version**: 1.0.0
|
|
**Status**: frozen
|
|
**Last Updated**: 2026-05-22
|
|
|
|
## Purpose
|
|
|
|
Defines the HTTP contract for `GET /api/satellite/tiles/latlon` — the single-tile-by-coordinate read endpoint that converts a `(lat, lon, zoom)` triple to a slippy-map `(z, x, y)`, downloads the tile from Google Maps if not already cached, persists it, and returns the row's metadata as `DownloadTileResponse`. The actual tile bytes are served separately via `GET /tiles/{z}/{x}/{y}` once the caller has the resulting `(z, x, y)` (or the equivalent `tilePath` from the response).
|
|
|
|
This is the v1.0.0 of the contract — published alongside AZ-811's validator landing. There is no prior contract document; the producer-doc surface before AZ-811 was `modules/api_program.md::GetTileByLatLon Handler` only.
|
|
|
|
## Endpoint
|
|
|
|
```
|
|
GET /api/satellite/tiles/latlon?lat=<float>&lon=<float>&zoom=<int>
|
|
Authorization: Bearer <JWT>
|
|
```
|
|
|
|
The request MUST carry a valid JWT (AZ-487). No `permissions` claim is required. Anonymous requests are rejected with HTTP 401.
|
|
|
|
## Shape
|
|
|
|
### Query parameters
|
|
|
|
```
|
|
?lat=47.461747&lon=37.647063&zoom=18
|
|
```
|
|
|
|
Per-parameter constraints:
|
|
|
|
| Param | Type | Required | Description | Constraints |
|
|
|-------|------|----------|-------------|-------------|
|
|
| `lat` | number | yes | WGS84 latitude (decimal degrees). | `[-90.0, 90.0]`. |
|
|
| `lon` | number | yes | WGS84 longitude (decimal degrees). | `[-180.0, 180.0]`. |
|
|
| `zoom` | integer | yes | Slippy-map zoom level. | `[0, 22]`. |
|
|
|
|
Strict shape: any query-string parameter outside `{lat, lon, zoom}` is rejected by `RejectUnknownQueryParamsEndpointFilter` with HTTP 400 + the unknown key name in `errors`. This catches typos like `?latitude=` (pre-AZ-811 wire name) that ASP.NET model binding would otherwise silently ignore, and it also rejects hostile fingerprinting probes like `?debug=1&admin=true`.
|
|
|
|
**Required-field detection**: the bound DTO (`GetTileByLatLonQuery`) declares `lat` / `lon` / `zoom` as nullable (`double?`, `double?`, `int?`). Missing a query param therefore binds to `null` rather than throwing `BadHttpRequestException` from the framework binder — the request reaches the endpoint filters in all cases. `GetTileByLatLonQueryValidator` then enforces `NotNull` (chained `CascadeMode.Stop` ahead of the range rule) so a missing param surfaces as `errors[<paramName>]: ["\`<paramName>\` is required."]` exactly like any other validation failure. The handler dereferences `.Value` only after the validator filter has passed, guaranteed by the filter ordering.
|
|
|
|
### Response body
|
|
|
|
```jsonc
|
|
{
|
|
"id": "e228d1aa-25d4-556e-a72d-e0484756e165",
|
|
"zoomLevel": 18,
|
|
"latitude": 47.461747,
|
|
"longitude": 37.647063,
|
|
"tileSizeMeters": 39.84,
|
|
"tileSizePixels": 256,
|
|
"imageType": "jpg",
|
|
"version": 1,
|
|
"filePath": "tiles/18/158485/91707.jpg",
|
|
"createdAt": "2026-05-22T12:34:56.789Z",
|
|
"updatedAt": "2026-05-22T12:34:56.789Z"
|
|
}
|
|
```
|
|
|
|
Per-field semantics:
|
|
|
|
| Field | Type | Description |
|
|
|-------|------|-------------|
|
|
| `id` | UUID | Deterministic UUIDv5 of the tile (`Uuidv5.TileNamespace, "{z}/{x}/{y}"`). |
|
|
| `zoomLevel` | integer | Echoes the request `zoom` param. |
|
|
| `latitude` | number | Tile centre latitude (server-resolved from slippy `(z,x,y)`; may differ from the request `lat` by up to half a tile). |
|
|
| `longitude` | number | Tile centre longitude. |
|
|
| `tileSizeMeters` | number | Approximate ground footprint of the tile at this zoom and latitude. |
|
|
| `tileSizePixels` | integer | Fixed at 256 (slippy-map convention). |
|
|
| `imageType` | string | Always `"jpg"`. |
|
|
| `version` | integer | Tile row version (bumps on each refresh). |
|
|
| `filePath` | string | Relative path under the tile cache root (`tiles/{z}/{x}/{y}.jpg`). |
|
|
| `createdAt` | ISO-8601 UTC | Tile row creation timestamp. |
|
|
| `updatedAt` | ISO-8601 UTC | Tile row last-modification timestamp. |
|
|
|
|
Response field names are intentionally LEGACY (`zoomLevel`, `latitude`, `longitude`) — only the request shape (query params) was renamed by AZ-811. The response is shared with `tile-storage.md` for caller consistency.
|
|
|
|
### Endpoint summary
|
|
|
|
| Method | Path | Request | Response | Status codes |
|
|
|--------|------|---------|----------|--------------|
|
|
| `GET` | `/api/satellite/tiles/latlon` | query string `?lat&lon&zoom` | `DownloadTileResponse` | 200, 400, 401 |
|
|
|
|
## Error shape
|
|
|
|
All `400` responses conform to `_docs/02_document/contracts/api/error-shape.md` v1.0.0. Two enforcement layers produce identically-shaped bodies:
|
|
|
|
1. **`RejectUnknownQueryParamsEndpointFilter`** (envelope, runs first) — rejects any query key outside `{lat, lon, zoom}` with `errors[<paramName>]: ["Unknown query parameter ..."]`. Catches typos and hostile probes.
|
|
2. **`GetTileByLatLonQueryValidator`** (FluentValidation, runs second) — range-checks `lat` / `lon` / `zoom` with `errors[<paramName>]: ["... must be between ..."]`.
|
|
|
|
Example body for a legacy-param-name failure (pre-AZ-811 wire format):
|
|
|
|
```jsonc
|
|
{
|
|
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
|
|
"title": "One or more validation errors occurred.",
|
|
"status": 400,
|
|
"errors": {
|
|
"Latitude": ["Unknown query parameter `Latitude`. Allowed: `lat`, `lon`, `zoom`."],
|
|
"Longitude": ["Unknown query parameter `Longitude`. Allowed: `lat`, `lon`, `zoom`."],
|
|
"ZoomLevel": ["Unknown query parameter `ZoomLevel`. Allowed: `lat`, `lon`, `zoom`."]
|
|
}
|
|
}
|
|
```
|
|
|
|
Example body for an out-of-range failure:
|
|
|
|
```jsonc
|
|
{
|
|
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
|
|
"title": "One or more validation errors occurred.",
|
|
"status": 400,
|
|
"errors": {
|
|
"lat": ["`lat` must be between -90 and 90."]
|
|
}
|
|
}
|
|
```
|
|
|
|
## Invariants
|
|
|
|
- **Inv-1**: `lat ∈ [-90.0, 90.0]`. Out-of-range → 400 with `errors["lat"]`.
|
|
- **Inv-2**: `lon ∈ [-180.0, 180.0]`. Out-of-range → 400 with `errors["lon"]`.
|
|
- **Inv-3**: `zoom ∈ [0, 22]` (slippy-map zoom range, matching `tile-inventory.md` Inv-8 and `region-request.md` Inv-5).
|
|
- **Inv-4** (AZ-811 envelope filter): Any query-string key outside `{lat, lon, zoom}` → 400 with `errors[<key>]`. This is the novel envelope-strictness layer introduced by AZ-811; reuse the filter on future query-string endpoints by passing a fresh allowed-keys set.
|
|
- **Inv-5** (deterministic mapping): `(lat, lon, zoom)` deterministically resolves to a single slippy-map `(z, x, y)` and therefore to a single `Uuidv5.TileNamespace`-derived tile `id`. Re-requesting the same triple returns the SAME `id` (cache hit if the tile already exists). Cross-referenced from `common_uuidv5.md`.
|
|
- **Inv-6** (cache reuse): If the resolved `(z, x, y)` already has a row in `tiles`, no new Google-Maps fetch occurs; the existing row's metadata is returned. The handler delegates this decision to `ITileService.DownloadAndStoreSingleTileAsync`.
|
|
|
|
## Non-Goals
|
|
|
|
- **Not covered**: tile body fetch. This endpoint returns metadata only. Bytes are served via `GET /tiles/{z}/{x}/{y}` (slippy-map URL).
|
|
- **Not covered**: bulk download. Use `POST /api/satellite/tiles/inventory` for batch-lookup or `POST /api/satellite/request` for region pre-fetch.
|
|
- **Not covered**: MGRS-based input. See `GET /api/satellite/tiles/mgrs` (stub, 501).
|
|
- **Not covered**: backward-compatibility shim for `Latitude/Longitude/ZoomLevel` query param names. AZ-811 ships v1.0.0 directly with the post-rename names; pre-rename callers receive HTTP 400 from the envelope filter naming each unknown key. There is no transitional accept-both period.
|
|
- **Not covered**: path-parameter validation on `GET /tiles/{z}/{x}/{y}` (the slippy-map body endpoint). That endpoint uses integer-binding which framework-validates the type but not the range; a separate task may add range checks if needed.
|
|
|
|
## Versioning Rules
|
|
|
|
- **Patch (1.0.x)**: Documentation clarifications, additional invariants that do not change wire behaviour.
|
|
- **Minor (1.x.0)**: Adding an optional query param consumers may safely omit (e.g. `?format=png` if a non-jpg variant is later supported); adding an optional response field.
|
|
- **Major (2.0.0)**: Changing any query-param name; tightening a range constraint that breaks current callers; removing `tileSizeMeters` from the response.
|
|
|
|
## Test Cases
|
|
|
|
| Case | Input | Expected | Notes |
|
|
|------|-------|----------|-------|
|
|
| happy-path | `?lat=47.461747&lon=37.647063&zoom=18` | HTTP 200 + DownloadTileResponse | AC-2 |
|
|
| missing-lat | `?lon=37.647063&zoom=18` | HTTP 400 + `errors["lat"]: ["\`lat\` is required."]` | Inv-1 (NotNull rule) |
|
|
| lat-out-of-range | `?lat=91&lon=37.647063&zoom=18` | HTTP 400 + `errors["lat"]` | Inv-1 (range rule) |
|
|
| lon-out-of-range | `?lat=47.461747&lon=181&zoom=18` | HTTP 400 + `errors["lon"]` | Inv-2 |
|
|
| zoom-out-of-range | `?lat=47.461747&lon=37.647063&zoom=30` | HTTP 400 + `errors["zoom"]` | Inv-3 |
|
|
| legacy-param-names | `?Latitude=47.46&Longitude=37.64&ZoomLevel=18` (pre-AZ-811 wire format) | HTTP 400 + `errors["Latitude","Longitude","ZoomLevel"]` | Inv-4 (AZ-811 envelope) |
|
|
| hostile-extra-keys | `?lat=...&lon=...&zoom=18&debug=1&admin=true` | HTTP 400 + `errors["debug","admin"]` | Inv-4 |
|
|
| typo-zooom | `?lat=...&lon=...&zooom=18` | HTTP 400 + `errors["zooom"]` | Inv-4 |
|
|
| lat-type-mismatch | `?lat=fifty&lon=...&zoom=18` | HTTP 400 (model-binder JsonException-equivalent) | Wire-format failure |
|
|
| cache-reuse | repeat happy-path | HTTP 200; same `id`; no new GET to Google Maps | Inv-5 + Inv-6 |
|
|
| auth-anonymous | no Bearer token | HTTP 401 | Standard `.RequireAuthorization()` baseline |
|
|
|
|
## Change Log
|
|
|
|
| Version | Date | Change | Author |
|
|
|---------|------|--------|--------|
|
|
| 1.0.0 | 2026-05-22 | Initial contract for `GET /api/satellite/tiles/latlon`. Publishes the post-AZ-811 OSM-convention query params (`lat`/`lon`/`zoom`) and the AZ-811 two-layer strict validation (envelope filter for unknown-keys + value-validator for range checks). References `error-shape.md` v1.0.0 for the 400 body shape and `tile-inventory.md` v2.0.0 for the bulk-lookup alternative. Pre-AZ-811 query-param names (`Latitude/Longitude/ZoomLevel`) are explicitly rejected by the envelope filter — no transitional shim. | autodev (Step 10, cycle 8) |
|