mirror of
https://github.com/azaion/satellite-provider.git
synced 2026-06-21 22:01:14 +00:00
5e056b2334
Third concrete child of AZ-795 (cycle 8 batch 3). FluentValidation +
[JsonRequired] + UnmappedMemberHandling.Disallow combine to reject every
malformed payload at the API boundary with RFC 7807 ValidationProblemDetails.
Validators (SatelliteProvider.Api/Validators/, all new)
- CreateRouteRequestValidator: id non-empty, name/description length,
regionSizeMeters/zoomLevel ranges, points count [2, 500], cross-field
createTilesZip => requestMaps. Chains RoutePointValidator (per-point)
and GeofencePolygonValidator (per-polygon, guarded by When(Geofences != null)).
OverridePropertyName("geofences.polygons") on the geofences chain so
FluentValidation's default leaf-only key policy doesn't drop the parent
path on deep expressions like req.Geofences!.Polygons.
- RoutePointValidator: lat/lon ranges; OverridePropertyName("lat"/"lon")
chained AFTER InclusiveBetween (the extension is defined on
IRuleBuilderOptions<T, TProperty>, so the generic type is only
inferable after the first concrete rule) so error keys match the
wire format (`points[i].lat`) rather than the C# property name
(`points[i].latitude`).
- GeofencePolygonValidator: per-corner range checks via private nested
GeoCornerValidator; cross-field NW.Lat > SE.Lat and NW.Lon < SE.Lon
invariants emit at errors["geofences.polygons[i].northWest"].
DTOs (SatelliteProvider.Common/DTO/, [JsonRequired] additions only)
- CreateRouteRequest: id, name, regionSizeMeters, zoomLevel, points,
requestMaps, createTilesZip
- RoutePoint: Latitude, Longitude
- GeofencePolygon: NorthWest, SouthEast; Geofences: Polygons
- GeoPoint: Lat, Lon
Tests
- Unit: 26 methods total — 16 in CreateRouteRequestValidatorTests, 6 in
GeofencePolygonValidatorTests, 4 in RoutePointValidatorTests. Each
RuleFor/RuleForEach chain has at least one positive + one negative case.
- Integration: CreateRouteValidationTests.cs — 16 methods (happy + 15
failure modes) wired into smoke + full suites. Covers empty body,
missing/zero id, empty name, out-of-range regionSizeMeters/zoomLevel,
points count < 2, per-point lat/lon out-of-range, geofence invariants,
missing requestMaps, cross-field createTilesZip, unknown root field,
nested type mismatch.
- Manual probe: scripts/probe_route_validation.sh curl-exercises every
failure mode end-to-end + happy path.
Docs
- New contract _docs/02_document/contracts/api/route-creation.md v1.0.0
with nested DTO chain, invariants, per-field test cases table, and
advisories on the legacy service-layer RouteValidator + the
input/output RoutePoint vs RoutePointDto naming asymmetry.
- system-flows.md F4 sequence diagram extended with the validation-filter
branch; preconditions + error scenarios reference the new contract.
- modules/api_program.md: CreateRoute handler section added; Api/Validators
bumped to AZ-808/AZ-809/AZ-811.
- modules/common_dtos.md: DTO descriptions updated with [JsonRequired]
annotations and constraint summaries.
- tests/blackbox-tests.md BT-06/BT-N03/BT-N04/BT-N05 align with the new
wire format and named error keys.
- tests/security-tests.md SEC-04 references GlobalExceptionHandler's
JsonException branch + AZ-353 correlationId.
- _docs/03_implementation/batch_03_cycle8_report.md + reviews/batch_03_cycle8_review.md
(PASS_WITH_NOTES — F1 Low: OverridePropertyName documented inline,
F2 + F3 Info: pre-existing advisories for follow-up).
Smoke green (mode=smoke, exit 0). AZ-809 transitioned to In Testing on Jira.
Task file moved to _docs/02_tasks/done/.
Co-authored-by: Cursor <cursoragent@cursor.com>
14 KiB
14 KiB
Code Review Report
Batch: 03 (cycle 8) Tasks: AZ-809 (POST /api/satellite/route strict validation) Date: 2026-05-22 Verdict: PASS_WITH_NOTES
Findings
| # | Severity | Category | File:Line | Title |
|---|---|---|---|---|
| 1 | Low | API alignment | SatelliteProvider.Api/Validators/CreateRouteRequestValidator.cs:68-77 |
OverridePropertyName is required on deep expressions because FluentValidation drops the parent path on req.Geofences!.Polygons |
| 2 | Info | Defence-in-depth | SatelliteProvider.Services.RouteManagement/RouteValidator.cs (existing) |
Service-layer RouteValidator is now strictly weaker than the cycle-8 CreateRouteRequestValidator and could be deleted, but is retained as a defence-in-depth backstop |
| 3 | Info | Wire-shape asymmetry | SatelliteProvider.Common/DTO/RoutePoint.cs (input) vs SatelliteProvider.Common/DTO/RoutePointDto.cs (output) |
The input wire uses short OSM lat/lon; the response wire uses long latitude/longitude. Pre-existing — AZ-809 documented but did not unify |
Finding Details
F1: OverridePropertyName is mandatory on the geofences chain (Low / API alignment)
- Location:
SatelliteProvider.Api/Validators/CreateRouteRequestValidator.cs:68-77andSatelliteProvider.Api/Validators/RoutePointValidator.cs:30-40. - Description: FluentValidation's default property-name policy drops the parent on deep member expressions like
req => req.Geofences!.Polygons. WithoutOverridePropertyName("geofences.polygons"), the error keys emitted are leaf-only (polygons,polygons[0].northWest) instead of the full wire path the spec mandates (geofences.polygons,geofences.polygons[0].northWest). The fix lives in code AND in a comment explaining WHY; without the comment a future reader would "simplify" the rule chain and silently break wire compatibility. Same pattern applies toRoutePointValidatorwhere C# propertyLatitudemust surface as wirelat— handled byOverridePropertyNamechained AFTER the first concrete rule (a generic-type-inference quirk: the extension is defined onIRuleBuilderOptions<T, TProperty>, which only becomes inferable after the first.InclusiveBetween()etc.). - Suggestion: NONE — rationale captured in code comments AND in
api_program.md::Api/Validatorsso the next reader cannot break it by accident. - Task: AZ-809
F2: Service-layer RouteValidator retained as defence-in-depth (Info / Defence-in-depth)
- Location:
SatelliteProvider.Services.RouteManagement/RouteValidator.cs. - Description: The pre-cycle-8 service-layer validator (
RouteValidator) covered approximately the same surface as the newCreateRouteRequestValidator(id non-empty, points count ≥ 2, geofence corner sanity). Now that the API layer rejects every invalid request before the service runs,RouteValidatoris strictly redundant for HTTP-driven paths. It is, however, also called from the route processing service (background queue) where some bypass path could in principle smuggle an invalid payload — keeping it as a backstop costs ~30 lines and one extra unit-test pass. Removal is tracked as an advisory inroute-creation.md("Validator Cleanup Advisory") so the next cycle can decide whether to consolidate. - Suggestion: Defer to a follow-up PBI. Do NOT delete in this batch.
- Task: AZ-809
F3: Input/output naming asymmetry on route points (Info / Wire-shape asymmetry)
- Location:
SatelliteProvider.Common/DTO/RoutePoint.cs(input) vsSatelliteProvider.Common/DTO/RoutePointDto.cs(output). - Description: Request points use
[JsonPropertyName("lat")]/[JsonPropertyName("lon")]; response points serialize the underlying C#Latitude/Longitudeproperties verbatim. This asymmetry existed before cycle 8. AZ-809 documents it inroute-creation.mdv1.0.0 andcommon_dtos.md, but does not unify because changing the response wire would be a breaking change to existing clients ofGET /api/satellite/route/{id}. Tracked as an advisory. - Suggestion: Open a successor PBI to consider unifying via a
lat/lonrename onRoutePointDto(would be a v2.0.0 ofroute-creation.md). - Task: AZ-809
Phase Summary
| Phase | Outcome |
|---|---|
| 1. Context Loading | Read AZ-809 spec, _docs/02_document/contracts/api/region-request.md (batch-2 pattern), _docs/02_document/contracts/api/error-shape.md (failure shape), and the cycle-7 + cycle-8 (batch-2) validation infra. The route endpoint differs from batch-2 endpoints because it has nested DTOs (RoutePoint, Geofences/GeofencePolygon/GeoPoint) requiring child validators. |
| 2. Spec Compliance | All 9 ACs ✓. New CreateRouteRequestValidator covers the 14 documented rules across deserializer-layer + validator-layer. Nested RoutePointValidator + GeofencePolygonValidator co-validators wired via RuleForEach.SetValidator(...). Cross-field invariants enforced at both the root (createTilesZip ⇒ requestMaps) and per-polygon (NW.Lat > SE.Lat, NW.Lon < SE.Lon). New contract route-creation.md v1.0.0 published. 16 integration tests + 26 unit tests cover happy path + each rule + missing-required + type-mismatch + cross-field. Probe script exercises every failure mode via curl. |
| 3. Code Quality | Mechanical patterns followed; three new validators are minimal and SRP-clean. The OverridePropertyName requirement on deep expressions (F1) is non-obvious and was discovered via failing unit tests + diagnostic instrumentation; the workaround is captured in both code comments and module docs so it cannot be silently regressed. RoutePointValidator and GeofencePolygonValidator are file-private — inner GeoCornerValidator is nested inside GeofencePolygonValidator because the polygon corners are its only consumer; if a future sibling endpoint needs point-shape validation, the spec says to promote and rename. |
| 4. Security | Validators run BEFORE any DB work (route persistence, intermediate-point computation, queue enqueue). The cross-field invariants prevent NaN-geometry payloads from reaching the GeoUtils interpolator (which is not designed for NW=SE corners). No SQL injection vectors, no hardcoded secrets, no PII in logs. JWT auth retained on the endpoint. Probe script tests ?debug=1 / extra root fields → all rejected. |
| 5. Performance | Validators run synchronously against in-memory record fields — negligible cost (microseconds) vs the route's interpolation pass + DB inserts. Even worst-case points.Count = 500 with all geofences.polygons.Count runs ~500 microsecond. No N+1, no blocking I/O. |
| 6. Cross-Task Consistency | Uses the same ValidationEndpointFilter<T> infra from cycle 7 + batch-2 of cycle 8 and the shared ProblemDetailsAssertions.AssertErrorsContainsMention. Error keys follow the same camelCase JSON-path policy (points[i].lat, geofences.polygons[i].northWest) per error-shape.md v1.0.0 Inv-4. All produce identically-shaped ValidationProblemDetails bodies. |
| 7. Architecture Compliance | Route DTOs live in SatelliteProvider.Common/DTO/ (shared with the service layer + integration tests). Validators co-located with the API at SatelliteProvider.Api/Validators/. No layering violations. The service-layer RouteValidator retention is documented as defence-in-depth (F2). No cycles, no public-API bypasses, no ADR breaches. |
Files Reviewed
AZ-809 (route-creation validator)
SatelliteProvider.Common/DTO/CreateRouteRequest.cs—[JsonRequired]on every non-optional axis. Removed implicit defaults so callers cannot rely on them.SatelliteProvider.Common/DTO/RoutePoint.cs—[JsonRequired]on Latitude/Longitude.SatelliteProvider.Common/DTO/GeofencePolygon.cs—[JsonRequired]onNorthWest/SouthEastinGeofencePolygon;[JsonRequired]onPolygonsinGeofences.SatelliteProvider.Common/DTO/GeoPoint.cs—[JsonRequired]on Lat/Lon (used byGeofencePolygoncorners).SatelliteProvider.Api/Validators/CreateRouteRequestValidator.cs— NEW — 7 root rules (id non-empty + 4 range rules + points count + cross-field) plusRuleForEach(req => req.Points).SetValidator(...)andRuleForEach(req => req.Geofences!.Polygons).SetValidator(...).OverridePropertyName(...)for nested chains.SatelliteProvider.Api/Validators/RoutePointValidator.cs— NEW —OverridePropertyName("lat"/"lon")chained AFTER.InclusiveBetween()so the type parameter is inferable.SatelliteProvider.Api/Validators/GeofencePolygonValidator.cs— NEW —CascadeMode.Stop+NotNull+ nestedGeoCornerValidatorfor per-corner ranges; cross-fieldMustrules.WithName("northWest")for the invariant errors.SatelliteProvider.Api/Program.cs(lines aroundMapPost("/api/satellite/route", ...)) — added.WithValidation<CreateRouteRequest>(),.Accepts<>,.Produces<>,.ProducesProblem().SatelliteProvider.Tests/Validators/CreateRouteRequestValidatorTests.cs— NEW — Theory + Fact coverage for each rule, positive and negative; 16 methods. Diagnostic-led: two assertions were converted frompolygons/polygons[0].northWesttogeofences.polygons/geofences.polygons[0].northWestafterOverridePropertyNamewas added.SatelliteProvider.Tests/Validators/RoutePointValidatorTests.cs— NEW — 4 methods (lat/lon range, positive + negative).SatelliteProvider.Tests/Validators/GeofencePolygonValidatorTests.cs— NEW — 6 methods incl. NotNull on corners, range on corners, NW-of-SE invariants.SatelliteProvider.IntegrationTests/CreateRouteValidationTests.cs— NEW — Happy + empty body + missing/zero GUID + 4 out-of-range + insufficient-points + per-point lat/lon out-of-range + geofence invariant + missing-requestMaps + cross-field createTilesZip + unknown-root + nested type-mismatch = 16 methods.SatelliteProvider.IntegrationTests/Program.cs— wiredCreateRouteValidationTests.RunAllinto smoke + full suites.scripts/probe_route_validation.sh— NEW — curl probes for every failure mode + happy path._docs/02_document/contracts/api/route-creation.md— NEW — v1.0.0 contract (no prior version existed). Includes the nested DTO chain + invariants + per-field test cases table + advisory on the legacyRouteValidator+ the input/output naming asymmetry._docs/02_document/modules/api_program.md—CreateRoute Handlersection added;Api/Validatorssection bumped to AZ-808/AZ-809/AZ-811._docs/02_document/modules/common_dtos.md—CreateRouteRequest/RoutePoint/Geofences/GeofencePolygon/GeoPointdescriptions updated with[JsonRequired]markers and constraint summaries._docs/02_document/system-flows.md::F4— sequence diagram extended with the validation-filter branch; preconditions + error scenarios reference the new contract._docs/02_document/tests/blackbox-tests.md::BT-06/BT-N03/BT-N04/BT-N05— triggers and pass criteria align with the new wire format + named error keys._docs/02_document/tests/security-tests.md::SEC-04— referencesGlobalExceptionHandler's JsonException branch + AZ-353 correlationId.
Test Evidence
./scripts/run-tests.sh --smoke (integration-tests container exit code 0):
Test: POST /api/satellite/route strict validation (AZ-809)
==========================================================
AZ-809 AC-2: well-formed body → HTTP 200 (no background processing — requestMaps=false)
✓ Well-formed body accepted with HTTP 200
AZ-809 rule 1: empty body → HTTP 400
✓ Empty body rejected with HTTP 400
AZ-809 rule 2 (probe-confirmed gap): missing `id` → HTTP 400 (no silent zero-Guid coercion)
✓ Missing `id` rejected with HTTP 400 (no silent coercion)
AZ-809 rule 2: zero-Guid `id` → HTTP 400
✓ Zero-Guid `id` rejected with errors["id"]
AZ-809 rule 3: empty `name` → HTTP 400
✓ Empty `name` rejected with errors["name"]
AZ-809 rule 5: `regionSizeMeters` out of range (100..10000) → HTTP 400
✓ `regionSizeMeters=1000000` rejected with errors["regionSizeMeters"]
AZ-809 rule 6: `zoomLevel` out of range (0..22) → HTTP 400
✓ `zoomLevel=30` rejected with errors["zoomLevel"]
AZ-809 rule 7: `points` count < 2 → HTTP 400
✓ `points` count=1 rejected with errors["points"]
AZ-809 rule 8: per-point `lat` out of range → HTTP 400 (errors[points[i].lat])
✓ `points[1].lat=91` rejected with errors["points[1].lat"]
AZ-809 rule 8: per-point `lon` out of range → HTTP 400 (errors[points[i].lon])
✓ `points[1].lon=181` rejected with errors["points[1].lon"]
AZ-809 rule 9: geofence NW.lat <= SE.lat → HTTP 400 (cross-field invariant)
✓ NW.lat <= SE.lat rejected by cross-field invariant
AZ-809 rule 10: missing `requestMaps` → HTTP 400 (no defaulting)
✓ Missing `requestMaps` rejected
AZ-809 rule 12: `createTilesZip=true` AND `requestMaps=false` → HTTP 400 (cross-field invariant)
✓ `createTilesZip=true requestMaps=false` rejected by cross-field invariant
AZ-809 rule 13: unknown root field → HTTP 400 (UnmappedMemberHandling.Disallow)
✓ Unknown root field `debug` rejected with errors mention
AZ-809 rule 14: nested type mismatch (`points[0].lat` as string) → HTTP 400
✓ `points[0].lat:"fifty"` rejected with HTTP 400
✓ Create-route validation tests: PASSED
=== All tests passed (mode=smoke) === — no regressions in cycle-7 inventory or batch-1/batch-2 cycle-8 (AZ-812/AZ-808/AZ-811) tests, no regressions in the migration/leaflet/route/tile/security suites.
Verdict Logic
- No Critical, no High, no Medium findings.
- 1 Low finding (F1) — the
OverridePropertyNamerequirement is captured in code comments + module docs; not a regression, but worth flagging so it cannot be silently regressed. - 2 Info findings (F2 defence-in-depth retention, F3 input/output naming asymmetry) — both pre-existing, documented as advisories for a follow-up PBI.
- PASS_WITH_NOTES.