# 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.`). - 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` shared infra. Both tasks share batch 2 because both wire `WithValidation()` 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` 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()`, `.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()`. 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**.