[AZ-808] [AZ-811] Strict validation on region POST + lat/lon GET

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>
This commit is contained in:
Oleksandr Bezdieniezhnykh
2026-05-22 16:29:41 +03:00
parent fcd494f67e
commit 34ee1e0b83
35 changed files with 1993 additions and 122 deletions
@@ -0,0 +1,151 @@
# Code Review Report
**Batch**: 02 (cycle 8)
**Tasks**: AZ-808 (Region POST endpoint strict validation) + AZ-811 (lat/lon GET endpoint strict validation)
**Date**: 2026-05-22
**Verdict**: PASS_WITH_NOTES
## Findings
| # | Severity | Category | File:Line | Title |
|---|----------|----------|-----------|-------|
| 1 | Info | Design rationale | `SatelliteProvider.Api/DTOs/GetTileByLatLonQuery.cs:1-30` | DTO uses `double?` / `int?` on purpose to dodge minimal-API "Required parameter not provided" short-circuit |
### Finding Details
**F1: `GetTileByLatLonQuery` uses nullable types on purpose** (Info / Design rationale)
- Location: `SatelliteProvider.Api/DTOs/GetTileByLatLonQuery.cs:17-20`
- Description: The DTO declares `Lat`/`Lon`/`Zoom` as `double?`/`double?`/`int?`. Non-nullable variants would feel simpler but cause the minimal-API parameter binder to throw `BadHttpRequestException` BEFORE endpoint filters run when a query param is missing. That short-circuit produces a plain `ProblemDetails` via `GlobalExceptionHandler` — no `errors{}` envelope, no per-field key — which violates AZ-811 ACs 1 and 4 (every failure mode must surface as `errors.<paramName>`).
- Initial implementation used non-nullable types. Diagnostic instrumentation captured the failing test response body (`{"title":"Bad Request","status":400,"detail":"Required parameter \"double Lat\" was not provided from query string."}`) which proved the binder was short-circuiting. Fix: switch to nullable + add `NotNull()` rule in the validator with `CascadeMode.Stop` ahead of the range rule. The handler dereferences `.Value` only after the validator filter passes.
- Suggestion: NONE — the rationale is now documented in both the DTO XML/doc comment and `api_program.md::Api/DTOs`. Captured here so a future reader doesn't "simplify" the types back to non-nullable.
- Task: AZ-811
## Phase Summary
| Phase | Outcome |
|-------|---------|
| 1. Context Loading | Read AZ-808 + AZ-811 specs, `_docs/02_document/contracts/api/tile-inventory.md` (validator pattern reference from cycle 7), and the cycle-7 `ValidationEndpointFilter<T>` shared infra. Both tasks share batch 2 because both wire `WithValidation<T>()` and reuse the cycle-7 validation envelope. |
| 2. Spec Compliance | **AZ-808**: AC-1..AC-8 all ✓. New `RegionRequestValidator` covers `id`/`lat`/`lon`/`sizeMeters`/`zoomLevel`. `[JsonRequired]` on `RequestRegionRequest` enforces required-field at the deserializer (no defaulting). New contract `region-request.md` v1.0.0 published. Unit + integration tests cover happy path + each rule + missing-required + type-mismatch. Probe script exercises every failure mode via `curl`. **AZ-811**: AC-1..AC-9 all ✓. New `GetTileByLatLonQueryValidator` covers `lat`/`lon`/`zoom` with explicit `NotNull` for missing + `InclusiveBetween` for range (CascadeMode.Stop). New `RejectUnknownQueryParamsEndpointFilter` rejects any query key outside `{lat, lon, zoom}` with `Results.ValidationProblem`. New contract `tile-latlon.md` v1.0.0 published. Unit tests for both validator (7 methods) and filter (4 methods); integration tests cover happy path + 6 failure modes. Probe script exercises every failure mode. |
| 3. Code Quality | Mechanical patterns followed; new validators and filter are minimal and SRP-clean. One Info finding (F1) on the nullable-DTO design — surfaced rather than left implicit. Cycle-7 `AssertErrorsContainsMention` helper promoted to `ProblemDetailsAssertions.cs` (closes the Low-severity DRY warning from batch-1 review). |
| 4. Security | `RejectUnknownQueryParamsEndpointFilter` rejects fingerprinting probes (`?debug=1&admin=true`, `?Latitude=...&Longitude=...`) with HTTP 400 + named keys — no enumeration vector. `RegionRequestValidator` runs BEFORE any DB work (idempotency lookup, queueing). No SQL injection vectors, no hardcoded secrets, no PII in logs. JWT auth retained on both endpoints. |
| 5. Performance | Validators run synchronously against in-memory record fields — negligible cost vs the Google-Maps round-trip or DB write that follows. Endpoint filter inspects `Query.Keys` (in-memory dictionary scan). No N+1, no blocking I/O. |
| 6. Cross-Task Consistency | Both tasks share `ValidationEndpointFilter<T>` infra from cycle 7 and the new shared `ProblemDetailsAssertions.AssertErrorsContainsMention`. `RegionRequestValidator` and `GetTileByLatLonQueryValidator` follow the same `Cascade(Stop).NotNull().InclusiveBetween()` pattern. Both produce identically-shaped `ValidationProblemDetails` per `error-shape.md` v1.0.0. |
| 7. Architecture Compliance | DTOs in `SatelliteProvider.Common/DTO/` (Region) and `SatelliteProvider.Api/DTOs/` (latlon query) — the query record is API-local because its `[FromQuery]` binding semantics are not reusable outside the API layer. Validators co-located with the API at `SatelliteProvider.Api/Validators/`. No layering violations. No cycles, no public-API bypasses, no ADR breaches. |
## Files Reviewed
### AZ-808 (Region POST validator)
- `SatelliteProvider.Common/DTO/RequestRegionRequest.cs``[JsonRequired]` on every property; removed default values for `ZoomLevel` and `StitchTiles` so callers cannot rely on implicit defaults.
- `SatelliteProvider.Api/Validators/RegionRequestValidator.cs`**NEW** — 5 rules (id non-empty + 4 range rules).
- `SatelliteProvider.Api/Program.cs` (lines around `MapPost("/api/satellite/request", ...)`) — added `.WithValidation<RequestRegionRequest>()`, `.Accepts<>`, `.Produces<>`, `.ProducesProblem()`. Removed inline `request.SizeMeters` size check (now in validator).
- `SatelliteProvider.Tests/Validators/RegionRequestValidatorTests.cs`**NEW** — Theory + Fact coverage for each rule, positive and negative.
- `SatelliteProvider.IntegrationTests/RegionRequestValidationTests.cs`**NEW** — Happy + empty body + missing/zero GUID + 4 out-of-range + missing-stitch + type mismatch.
- `SatelliteProvider.IntegrationTests/Program.cs` — wired `RegionRequestValidationTests.RunAll` into smoke + full suites.
- `scripts/probe_region_validation.sh`**NEW** — curl probes for every failure mode.
- `_docs/02_document/contracts/api/region-request.md`**NEW** — v1.0.0 contract (no prior version existed).
- `_docs/02_document/modules/api_program.md` — RequestRegion handler description updated; references new contract.
- `_docs/02_document/system-flows.md::F2` — references new contract + validator.
### AZ-811 (lat/lon GET validator)
- `SatelliteProvider.Api/DTOs/GetTileByLatLonQuery.cs`**NEW** — nullable record with `[FromQuery(Name = ...)]` per property. Rationale documented in-file (F1).
- `SatelliteProvider.Api/Validators/GetTileByLatLonQueryValidator.cs`**NEW**`Cascade(Stop).NotNull().InclusiveBetween` per param.
- `SatelliteProvider.Api/Validators/RejectUnknownQueryParamsEndpointFilter.cs`**NEW** — reusable `IEndpointFilter` parameterised by allowed-keys set (case-insensitive). Returns `Results.ValidationProblem` with one error per unknown key.
- `SatelliteProvider.Api/Program.cs` (lines around `MapGet("/api/satellite/tiles/latlon", ...)`) — added envelope filter + `.WithValidation<GetTileByLatLonQuery>()`. Handler signature now `[AsParameters] GetTileByLatLonQuery query`; dereferences `query.Lat!.Value` etc.
- `SatelliteProvider.Api/Swagger/ParameterDescriptionFilter.cs` — descriptions for `lat`/`lon`/`zoom` (post-rename); removed legacy `Latitude`/`Longitude`/`ZoomLevel` entries.
- `SatelliteProvider.Tests/Validators/GetTileByLatLonQueryValidatorTests.cs`**NEW** — 9 methods incl. null cases.
- `SatelliteProvider.Tests/Validators/RejectUnknownQueryParamsEndpointFilterTests.cs`**NEW** — 4 methods (delegation, unknown-key block, legacy PascalCase, case-insensitive allowed-set).
- `SatelliteProvider.IntegrationTests/GetTileByLatLonValidationTests.cs`**NEW** — Happy + 3 out-of-range + 1 missing-required + 2 unknown-key (legacy + hostile) + 1 type-mismatch = 8 methods.
- `SatelliteProvider.IntegrationTests/Program.cs` — wired `GetTileByLatLonValidationTests.RunAll` into smoke + full suites.
- `SatelliteProvider.IntegrationTests/TileTests.cs` — URL `?Latitude=&Longitude=&ZoomLevel=``?lat=&lon=&zoom=`.
- `SatelliteProvider.IntegrationTests/JwtIntegrationTests.cs``ProtectedTilesPath` const updated.
- `SatelliteProvider.IntegrationTests/SecurityTests.cs` — SQLi probe URL updated.
- `scripts/probe_latlon_validation.sh`**NEW** — curl probes incl. missing-lat, hostile probes, type mismatch.
- `scripts/run-performance-tests.sh` — PT-01 URL updated.
- `README.md` — endpoint example updated.
- `_docs/02_document/contracts/api/tile-latlon.md`**NEW** — v1.0.0 contract (no prior version existed).
- `_docs/02_document/modules/api_program.md` — handler + `Api/Validators` + `Api/DTOs` sections updated.
- `_docs/02_document/modules/common_uuidv5.md` — example URL updated.
- `_docs/02_document/system-flows.md::F1` — references new contract + validation layers.
- `_docs/02_document/tests/blackbox-tests.md` — BT-01, BT-N01, BT-N02, BT-18 triggers updated.
- `_docs/02_document/tests/security-tests.md` — SEC-01, SEC-05 triggers updated.
### Shared (cross-task hygiene)
- `SatelliteProvider.IntegrationTests/ProblemDetailsAssertions.cs``AssertErrorsContainsMention` promoted from per-test-file private helper to public static. Closes batch-1 Low-severity DRY warning.
- `SatelliteProvider.IntegrationTests/TileInventoryValidationTests.cs` — uses shared helper.
- `SatelliteProvider.IntegrationTests/RegionFieldRenameTests.cs` — uses shared helper; removed unused `using` directives.
## Test Evidence
`./scripts/run-tests.sh --smoke` (`integration-tests` container exit code 0):
```
Test: POST /api/satellite/request strict validation (AZ-808)
============================================================
AZ-808 AC-2: well-formed body → HTTP 200
✓ {id,lat,lon,sizeMeters,zoomLevel,stitchTiles} accepted with HTTP 200
AZ-808 rule (id-empty): id=Guid.Empty → HTTP 400
✓ id=Guid.Empty rejected with errors["id"]
AZ-808 rule (id-missing): missing id → HTTP 400 via [JsonRequired]
✓ Missing id rejected via [JsonRequired] (no defaulting to Guid.Empty)
AZ-808 rule (lat-out-of-range): lat=91 → HTTP 400
✓ lat=91 rejected with errors["lat"]
AZ-808 rule (lon-out-of-range): lon=181 → HTTP 400
✓ lon=181 rejected with errors["lon"]
AZ-808 rule (sizeMeters-out-of-range): sizeMeters=50 → HTTP 400
✓ sizeMeters=50 rejected with errors["sizeMeters"]
AZ-808 rule (zoomLevel-out-of-range): zoomLevel=30 → HTTP 400
✓ zoomLevel=30 rejected with errors["zoomLevel"]
AZ-808 rule (stitchTiles-missing): missing stitchTiles → HTTP 400 via [JsonRequired]
✓ Missing stitchTiles rejected via [JsonRequired]
AZ-808 rule (type-mismatch): lat="bad" → HTTP 400
✓ Non-numeric lat rejected with HTTP 400
AZ-808 empty body → HTTP 400
✓ Empty body rejected with HTTP 400
✓ POST /api/satellite/request validation tests: PASSED
Test: GET /api/satellite/tiles/latlon strict validation (AZ-811)
================================================================
AZ-811 AC-2: well-formed query → HTTP 200
✓ {lat,lon,zoom} accepted with HTTP 200
AZ-811 rule 1: lat out of range (-90..90) → HTTP 400
✓ lat=91 rejected with errors["lat"]
AZ-811 rule 2: lon out of range (-180..180) → HTTP 400
✓ lon=181 rejected with errors["lon"]
AZ-811 rule 3: zoom out of range (0..22) → HTTP 400
✓ zoom=30 rejected with errors["zoom"]
AZ-811 rule 1: missing `lat` query param → HTTP 400 with errors.lat
✓ Missing lat rejected with errors["lat"] = `lat` is required
AZ-811 rule 4: legacy `?Latitude=&Longitude=&ZoomLevel=` (pre-AZ-811 wire format) → HTTP 400 (envelope filter)
✓ Legacy ?Latitude=&Longitude=&ZoomLevel= rejected by envelope filter
AZ-811 rule 4: hostile/typo query keys → HTTP 400 (envelope filter)
✓ ?debug=1&admin=true rejected; errors map names BOTH unknown keys
AZ-811 rule 5: lat type mismatch (non-numeric) → HTTP 400
✓ lat=fifty rejected with HTTP 400
✓ GET lat/lon validation tests: PASSED
```
`=== All tests passed (mode=smoke) ===` — no regressions in cycle-7 inventory/idempotent/security/route/tile/leaflet/migration suites.
## Verdict Logic
- No Critical, no High, no Medium findings.
- 1 Info finding (F1) — design rationale captured in code + doc; not a regression.
- **PASS_WITH_NOTES**.