mirror of
https://github.com/azaion/satellite-provider.git
synced 2026-06-21 22:51:15 +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>
12 KiB
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/Zoomasdouble?/double?/int?. Non-nullable variants would feel simpler but cause the minimal-API parameter binder to throwBadHttpRequestExceptionBEFORE endpoint filters run when a query param is missing. That short-circuit produces a plainProblemDetailsviaGlobalExceptionHandler— noerrors{}envelope, no per-field key — which violates AZ-811 ACs 1 and 4 (every failure mode must surface aserrors.<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 + addNotNull()rule in the validator withCascadeMode.Stopahead of the range rule. The handler dereferences.Valueonly 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 forZoomLevelandStitchTilesso 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 aroundMapPost("/api/satellite/request", ...)) — added.WithValidation<RequestRegionRequest>(),.Accepts<>,.Produces<>,.ProducesProblem(). Removed inlinerequest.SizeMeterssize 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— wiredRegionRequestValidationTests.RunAllinto 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().InclusiveBetweenper param.SatelliteProvider.Api/Validators/RejectUnknownQueryParamsEndpointFilter.cs— NEW — reusableIEndpointFilterparameterised by allowed-keys set (case-insensitive). ReturnsResults.ValidationProblemwith one error per unknown key.SatelliteProvider.Api/Program.cs(lines aroundMapGet("/api/satellite/tiles/latlon", ...)) — added envelope filter +.WithValidation<GetTileByLatLonQuery>(). Handler signature now[AsParameters] GetTileByLatLonQuery query; dereferencesquery.Lat!.Valueetc.SatelliteProvider.Api/Swagger/ParameterDescriptionFilter.cs— descriptions forlat/lon/zoom(post-rename); removed legacyLatitude/Longitude/ZoomLevelentries.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— wiredGetTileByLatLonValidationTests.RunAllinto smoke + full suites.SatelliteProvider.IntegrationTests/TileTests.cs— URL?Latitude=&Longitude=&ZoomLevel=→?lat=&lon=&zoom=.SatelliteProvider.IntegrationTests/JwtIntegrationTests.cs—ProtectedTilesPathconst 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/DTOssections 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—AssertErrorsContainsMentionpromoted 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 unusedusingdirectives.
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.