#!/usr/bin/env bash set -euo pipefail # Manual end-to-end probe for POST /api/satellite/route strict validation # (AZ-809). Each failure call should return HTTP 400 with an # `application/problem+json` body. The happy path should return HTTP 200. # # Two enforcement layers: # 1. UnmappedMemberHandling.Disallow + [JsonRequired] — deserializer rejects # missing-required and unknown fields with errors via GlobalExceptionHandler. # 2. WithValidation — runs CreateRouteRequestValidator + # RoutePointValidator + GeofencePolygonValidator (range, count, cross-field). # # Usage: # API_URL=https://localhost:8080 JWT="" ./scripts/probe_route_validation.sh API_URL="${API_URL:-https://localhost:8080}" JWT="${JWT:-}" ENDPOINT="${API_URL%/}/api/satellite/route" if [[ -z "${JWT}" ]]; then echo "ERROR: set JWT env var to a bearer token. Mint one via:" echo " dotnet run --project SatelliteProvider.IntegrationTests -- --mint-only" exit 2 fi curl_args=(-sS -k -H "Authorization: Bearer ${JWT}" -H "Content-Type: application/json") probe() { local label="$1" local body="$2" local expected_status="$3" echo "----- ${label} (expecting HTTP ${expected_status}) -----" local response response=$(curl "${curl_args[@]}" -X POST -d "${body}" "${ENDPOINT}" -w "\nHTTP_STATUS=%{http_code}\n") echo "${response}" local actual_status actual_status=$(echo "${response}" | tail -n 1 | sed 's/HTTP_STATUS=//') if [[ "${actual_status}" != "${expected_status}" ]]; then echo "FAIL: expected HTTP ${expected_status}, got ${actual_status}" return 1 fi echo "OK: HTTP ${expected_status}" echo } route_id=$(uuidgen | tr '[:upper:]' '[:lower:]') probe "happy-path-no-maps" '{ "id": "'"${route_id}"'", "name": "probe-route-1", "regionSizeMeters": 1000, "zoomLevel": 18, "points": [ { "lat": 50.10, "lon": 36.10 }, { "lat": 50.11, "lon": 36.11 } ], "requestMaps": false, "createTilesZip": false }' 200 # Rule 2: missing id (probe-confirmed gap) probe "missing-id" '{ "name": "probe-missing-id", "regionSizeMeters": 1000, "zoomLevel": 18, "points": [ { "lat": 50.10, "lon": 36.10 }, { "lat": 50.11, "lon": 36.11 } ], "requestMaps": false, "createTilesZip": false }' 400 # Rule 2: zero-Guid id probe "zero-guid-id" '{ "id": "00000000-0000-0000-0000-000000000000", "name": "probe-zero-id", "regionSizeMeters": 1000, "zoomLevel": 18, "points": [ { "lat": 50.10, "lon": 36.10 }, { "lat": 50.11, "lon": 36.11 } ], "requestMaps": false, "createTilesZip": false }' 400 # Rule 3: empty name probe "empty-name" '{ "id": "'$(uuidgen | tr '[:upper:]' '[:lower:]')'", "name": "", "regionSizeMeters": 1000, "zoomLevel": 18, "points": [ { "lat": 50.10, "lon": 36.10 }, { "lat": 50.11, "lon": 36.11 } ], "requestMaps": false, "createTilesZip": false }' 400 # Rule 7: points too few (1) probe "points-too-few" '{ "id": "'$(uuidgen | tr '[:upper:]' '[:lower:]')'", "name": "probe-1-point", "regionSizeMeters": 1000, "zoomLevel": 18, "points": [ { "lat": 50.10, "lon": 36.10 } ], "requestMaps": false, "createTilesZip": false }' 400 # Rule 8: nested point lat out of range probe "point-lat-out-of-range" '{ "id": "'$(uuidgen | tr '[:upper:]' '[:lower:]')'", "name": "probe-point-lat", "regionSizeMeters": 1000, "zoomLevel": 18, "points": [ { "lat": 50.10, "lon": 36.10 }, { "lat": 91.0, "lon": 36.11 } ], "requestMaps": false, "createTilesZip": false }' 400 # Rule 9: geofence NW not north-of SE (cross-field invariant) probe "geofence-nw-not-north" '{ "id": "'$(uuidgen | tr '[:upper:]' '[:lower:]')'", "name": "probe-geofence-inverted", "regionSizeMeters": 1000, "zoomLevel": 18, "points": [ { "lat": 50.10, "lon": 36.10 }, { "lat": 50.11, "lon": 36.11 } ], "geofences": { "polygons": [ { "northWest": { "lat": 50.05, "lon": 36.05 }, "southEast": { "lat": 50.05, "lon": 36.15 } } ] }, "requestMaps": false, "createTilesZip": false }' 400 # Rule 12: cross-field createTilesZip without requestMaps probe "createTilesZip-without-requestMaps" '{ "id": "'$(uuidgen | tr '[:upper:]' '[:lower:]')'", "name": "probe-cross-field", "regionSizeMeters": 1000, "zoomLevel": 18, "points": [ { "lat": 50.10, "lon": 36.10 }, { "lat": 50.11, "lon": 36.11 } ], "requestMaps": false, "createTilesZip": true }' 400 # Rule 13: unknown root field probe "unknown-root-field" '{ "id": "'$(uuidgen | tr '[:upper:]' '[:lower:]')'", "name": "probe-unknown", "regionSizeMeters": 1000, "zoomLevel": 18, "points": [ { "lat": 50.10, "lon": 36.10 }, { "lat": 50.11, "lon": 36.11 } ], "requestMaps": false, "createTilesZip": false, "debug": "fingerprint-probe" }' 400 # Rule 14: nested type mismatch probe "point-lat-type-mismatch" '{ "id": "'$(uuidgen | tr '[:upper:]' '[:lower:]')'", "name": "probe-type-mismatch", "regionSizeMeters": 1000, "zoomLevel": 18, "points": [ { "lat": "fifty", "lon": 36.10 }, { "lat": 50.11, "lon": 36.11 } ], "requestMaps": false, "createTilesZip": false }' 400 echo "All probes passed."