Files
Oleksandr Bezdieniezhnykh 34ee1e0b83 [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>
2026-05-22 16:29:41 +03:00

12 KiB

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.csNEW — 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.csNEW — Theory + Fact coverage for each rule, positive and negative.
  • SatelliteProvider.IntegrationTests/RegionRequestValidationTests.csNEW — 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.shNEW — curl probes for every failure mode.
  • _docs/02_document/contracts/api/region-request.mdNEW — 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.csNEW — nullable record with [FromQuery(Name = ...)] per property. Rationale documented in-file (F1).
  • SatelliteProvider.Api/Validators/GetTileByLatLonQueryValidator.csNEWCascade(Stop).NotNull().InclusiveBetween per param.
  • SatelliteProvider.Api/Validators/RejectUnknownQueryParamsEndpointFilter.csNEW — 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.csNEW — 9 methods incl. null cases.
  • SatelliteProvider.Tests/Validators/RejectUnknownQueryParamsEndpointFilterTests.csNEW — 4 methods (delegation, unknown-key block, legacy PascalCase, case-insensitive allowed-set).
  • SatelliteProvider.IntegrationTests/GetTileByLatLonValidationTests.csNEW — 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.csProtectedTilesPath const updated.
  • SatelliteProvider.IntegrationTests/SecurityTests.cs — SQLi probe URL updated.
  • scripts/probe_latlon_validation.shNEW — 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.mdNEW — 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.csAssertErrorsContainsMention 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.