mirror of
https://github.com/azaion/satellite-provider.git
synced 2026-06-22 11:31:14 +00:00
[AZ-808] [AZ-809] [AZ-810] [AZ-811] [AZ-812] Cycle 8 test-spec sync
Phase 12 of autodev existing-code flow — cycle-update mode of the
test-spec skill. Append cycle-8 coverage to the documentation suite
without rewriting any pre-cycle-8 content.
blackbox-tests.md
- Add 4 new BT entries (BT-28..BT-31) — one per cycle-8 endpoint:
- BT-28: Region request endpoint strict validation + OSM rename
(AZ-808 + AZ-812; 11 sub-cases through the new `RegionRequest
Validator` + the AZ-795 deserializer infra; sub-case `pos` proves
the new `lat`/`lon` names accepted, sub-case `9` proves the old
`latitude`/`longitude` rejected by `UnmappedMemberHandling.Disallow`).
- BT-29: Create route endpoint nested + cross-field validation
(AZ-809; 15 sub-cases covering nested per-point validators,
geofence cross-field invariants, and the `createTilesZip` /
`requestMaps` cross-field rule; advisory ACs 9 + 10 explicitly
NOT tested per spec).
- BT-30: UAV upload metadata multipart validation (AZ-810; 14
sub-cases across the three-layer composition: deserializer,
FluentValidation, envelope cross-field; documents the unique
`errors["metadata"]` vs `errors["metadata.items[i].field"]` key
convention for multipart endpoints).
- BT-31: GET tiles/latlon query-param validation + unknown-param
rejection (AZ-811; 8 sub-cases; sub-cases 4b + 4c prove the
novel `UnknownQueryParameterEndpointFilter` rejects both
legacy and hostile unknown query keys).
traceability-matrix.md
- Append 41 AC rows (AZ-808 AC-1..AC-8, AZ-809 AC-1..AC-10,
AZ-810 AC-1..AC-9, AZ-811 AC-1..AC-9, AZ-812 AC-1..AC-6).
- Update Coverage Summary: cycle-8 row added; Total moves from
126 tests / 75 ACs to 167 tests / 116 ACs.
- Add "Coverage shape notes (Cycle 8 ...)" section explaining the
multipart enforcement shape (AZ-810), the new query-param filter
(AZ-811), the AZ-808 + AZ-812 same-cycle coordination, and the
AZ-810 AC-9 process annotation (false-PASS by source tracing →
bound to green full-suite re-run after the test-data coord-clamp
fix in commit b763da3).
- AZ-809 AC-9 + AC-10 marked as `◐ advisory (not tested)` —
naming-consistency concerns surfaced for parent-suite team
decision.
State
- Advance autodev to Step 13 (Update Docs), sub_step phase 0
awaiting-invocation.
No production code change; no contract change; no test code change.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -284,3 +284,111 @@ The cycle introduces no new HTTP routes. Functional positive coverage is unchang
|
||||
**AC trace**: AZ-796 AC-1 (all 9 rules + ProblemDetails shape), AC-2 (happy path); AZ-794 AC-1 (positive z/x/y acceptance — sub-case `pos`), AZ-794 AC-2 (sub-case `9c` proves the old names produce a structured 400, eliminating the silent-coerce-to-0 footgun); AZ-795 (epic-level — every sub-case exercises the shared `ValidationEndpointFilter` + `GlobalExceptionHandler` + `UnmappedMemberHandling.Disallow` infra).
|
||||
**Notes**: The 9 rules split across two enforcement layers — rules 5/6/9 are enforced by the deserializer (`JsonRequired` + `UnmappedMemberHandling.Disallow` + native JSON type validation, see AZ-795 shared infra) and surface as `BadHttpRequestException` → `GlobalExceptionHandler.JsonException` branch; rules 2/3/4/7/8 are enforced by `InventoryRequestValidator` (FluentValidation) via `ValidationEndpointFilter<TileInventoryRequest>`. Both paths produce identically-shaped `ValidationProblemDetails` bodies (`error-shape.md` v1.0.0 invariant).
|
||||
|
||||
---
|
||||
|
||||
## Cycle 8 — AZ-808 / AZ-809 / AZ-810 / AZ-811 / AZ-812 Strict Validation Sweep + Region Rename
|
||||
|
||||
Cycle 8 extends the AZ-795 shared validation infrastructure (FluentValidation + `ValidationEndpointFilter<T>` + `GlobalExceptionHandler` + `JsonSerializerOptions.UnmappedMemberHandling.Disallow`) to the four remaining public-facing endpoints, and lands the OSM-convention `Lat`/`Lon` rename on the Region API in the same commit set. Every cycle-8 endpoint emits the same `ValidationProblemDetails` shape (`error-shape.md` v1.0.0) used by AZ-796. No new HTTP routes; existing positive coverage (BT-01..BT-18 region/route/tile flows, BT-13..BT-17 UAV upload, BT-N01..BT-N05 negatives) is preserved. The new tests below cover the strict-rejection behaviour that pre-cycle-8 either silently coerced (missing `id` → zero-Guid; unknown `?latitude=` → `lat=0`) or accepted out-of-range (lat > 90, zoom > 22) values that downstream code couldn't render.
|
||||
|
||||
## BT-28: Region Request Endpoint — Strict Validation + OSM Rename
|
||||
|
||||
**Trigger**: A family of `POST /api/satellite/request` calls. AZ-812's `Lat`/`Lon` rename ships in the same commit as AZ-808; every sub-case uses the post-rename wire shape `{"id":"<guid>","lat":..,"lon":..,"sizeMeters":..,"zoomLevel":..,"stitchTiles":..}`.
|
||||
**Precondition**: API up; valid JWT attached. `error-shape.md` v1.0.0 + `region-request.md` v1.0.0 frozen (v1.0.0 published directly with the post-AZ-812 names per AZ-812 AC-6 coordination).
|
||||
**Expected**: HTTP 400 with `Content-Type: application/problem+json` for every failure sub-case; HTTP 200 + `RegionStatusResponse` for the happy path. `errors[]` names the offending field path in request-body camelCase.
|
||||
|
||||
| # | Rule | Trigger excerpt | Expected `errors` key | Test method |
|
||||
|---|------|-----------------|-----------------------|-------------|
|
||||
| pos | Happy path with `lat`/`lon` | full valid body | HTTP 200 — body shape unchanged from pre-AZ-808 (`{regionId,status}`) | `HappyPath_Returns200` |
|
||||
| 1 | Empty body | zero bytes, `Content-Type: application/json` | (framework-level ProblemDetails) | `EmptyBody_Returns400` |
|
||||
| 2a | Required `id` (silent-coercion fix) | omit `id` entirely | `id` — message states `id` is required (NOT zero-Guid coercion) | `Post_WithMissingId_ReturnsBadRequest` |
|
||||
| 2b | Zero-Guid `id` | `"id":"00000000-0000-0000-0000-000000000000"` | `id` | `ZeroGuidId_Returns400` |
|
||||
| 3 | `lat` in range `[-90, 90]` | `"lat":91` | `lat` | `LatOutOfRange_Returns400` |
|
||||
| 4 | `lon` in range `[-180, 180]` | `"lon":181` | `lon` | `LonOutOfRange_Returns400` |
|
||||
| 5 | `sizeMeters` in range `[100, 10_000]` | `"sizeMeters":1000000` | `sizeMeters` | `SizeMetersOutOfRange_Returns400` |
|
||||
| 6 | `zoomLevel` in range `[0, 22]` | `"zoomLevel":30` | `zoomLevel` | `ZoomOutOfRange_Returns400` |
|
||||
| 7 | Required `stitchTiles` | omit `stitchTiles` | `stitchTiles` | `MissingStitchTiles_Returns400` |
|
||||
| 8 | Unknown root field (`UnmappedMemberHandling.Disallow`) | `"debug":42` | path mentioning `debug` | `UnknownRootField_Returns400` |
|
||||
| 9 | Legacy v1 names (`latitude`/`longitude`) | exact AZ-777 Phase 2 reproduction body | path mentioning `latitude` (treated as unknown post-AZ-812) | `OldLatLongNames_Returns400` |
|
||||
| 10 | Type mismatch on `lat` | `"lat":"fifty"` | path mentioning `lat` | `LatTypeMismatch_Returns400` |
|
||||
|
||||
**Pass criterion**: Every failure sub-case returns HTTP 400 with the expected `errors` key OR an equivalent RFC 7807 body for the empty-body case; the happy path returns HTTP 200 + a non-empty `regionId`. AZ-812 AC-4 is verified by sub-cases `pos` (new names accepted) and `9` (old names rejected by `UnmappedMemberHandling.Disallow`) on the same endpoint. No regression in `RegionRequestTests.cs` (cycle 8 Step 11 — green).
|
||||
**AC trace**: AZ-808 AC-1 (8 rules + ProblemDetails shape), AC-2 (happy path); AZ-812 AC-2 (wire format end-to-end), AC-3 (existing happy-path tests pass — Step 11 green), AC-4 (post-rename accepted, pre-rename rejected — sub-cases `pos` + `9`).
|
||||
**Notes**: The 8 validations split between the deserializer (rule 7 missing-required, rule 8 unknown-root, rule 10 type-mismatch — surface via `GlobalExceptionHandler`) and `RegionRequestValidator` (rules 2b/3/4/5/6 — surface via `ValidationEndpointFilter<RequestRegionRequest>`). The silent-coercion case (rule 2a) is the AZ-777 Phase 2 reproducer: pre-cycle-8 a missing `id` deserialized to `Guid.Empty` and the handler silently created a region with id `00000000-...`; post-cycle-8 it fails-fast at the deserializer.
|
||||
|
||||
## BT-29: Create Route Endpoint — Nested + Cross-Field Strict Validation
|
||||
|
||||
**Trigger**: A family of `POST /api/satellite/route` calls covering AZ-809's 14 rules. Bodies use the post-AZ-809 wire shape `{id, name, description?, regionSizeMeters, zoomLevel, points: [{lat, lon}, …], requestMaps, createTilesZip, geofences?: {polygons: [{northWest, southEast}]}}`.
|
||||
**Precondition**: API up; valid JWT attached. `error-shape.md` v1.0.0 + `route-creation.md` v1.0.0 frozen.
|
||||
**Expected**: HTTP 400 with `Content-Type: application/problem+json` for every failure sub-case; HTTP 200 + `RouteResponse` for the happy path. `errors[]` keys use the nested path (e.g. `points[1].lat`, `geofences.polygons[0].northWest`).
|
||||
|
||||
| # | Rule | Trigger excerpt | Expected `errors` key | Test method |
|
||||
|---|------|-----------------|-----------------------|-------------|
|
||||
| pos | Happy path (`requestMaps=false`, no background processing) | minimal 2-point valid body | HTTP 200 — body shape unchanged | `HappyPath_Returns200` |
|
||||
| 1 | Empty body | zero bytes | (framework-level ProblemDetails) | `EmptyBody_Returns400` |
|
||||
| 2a | Required `id` (silent-coercion fix) | omit `id` | `id` is required (no zero-Guid coercion) | `MissingId_Returns400` |
|
||||
| 2b | Zero-Guid `id` | `"id":"00000000-..."` | `id` | `ZeroGuidId_Returns400` |
|
||||
| 3 | Required `name` non-empty | `"name":""` | `name` | `EmptyName_Returns400` |
|
||||
| 5 | `regionSizeMeters` in `[100, 10_000]` | `1000000` | `regionSizeMeters` | `RegionSizeMetersOutOfRange_Returns400` |
|
||||
| 6 | `zoomLevel` in `[0, 22]` | `30` | `zoomLevel` | `ZoomOutOfRange_Returns400` |
|
||||
| 7 | `points` count in `[2, 500]` | 1-point array | `points` | `PointsCountBelowMin_Returns400` |
|
||||
| 8a | Per-point `lat` in `[-90, 90]` | `points[1].lat=91` | `points[1].lat` | `PointLatOutOfRange_Returns400` |
|
||||
| 8b | Per-point `lon` in `[-180, 180]` | `points[1].lon=181` | `points[1].lon` | `PointLonOutOfRange_Returns400` |
|
||||
| 9 | Geofence cross-field invariant (`NW.lat > SE.lat` AND `NW.lon < SE.lon`) | NW.lat ≤ SE.lat | `geofences.polygons[0].northWest` | `GeofenceNwSeInvariantViolated_Returns400` |
|
||||
| 10 | Required `requestMaps` (no defaulting) | omit `requestMaps` | `requestMaps` | `MissingRequestMaps_Returns400` |
|
||||
| 12 | Cross-field: `createTilesZip=true` requires `requestMaps=true` | `{createTilesZip:true, requestMaps:false}` | top-level message (no per-field key) | `CreateTilesZipRequiresRequestMaps_Returns400` |
|
||||
| 13 | Unknown root field | `"debug":42` | path mentioning `debug` | `UnknownRootField_Returns400` |
|
||||
| 14 | Nested type mismatch | `"points[0].lat":"fifty"` | path mentioning `points[0].lat` | `NestedTypeMismatch_Returns400` |
|
||||
|
||||
**Pass criterion**: Every failure sub-case returns HTTP 400 with the expected `errors` key OR an equivalent RFC 7807 body for the empty/cross-field cases; the happy path returns HTTP 200 + a non-empty `routeId`. No regression in `RouteCreationTests.cs` (cycle 8 Step 11 — green). The AZ-779-class footgun (silent coercion of missing `id` to zero-Guid creating untracked routes) is closed by sub-case 2a.
|
||||
**AC trace**: AZ-809 AC-1 (14 rules + ProblemDetails shape), AC-2 (happy path). Advisory ACs AC-9 (`sizeMeters` vs `regionSizeMeters` naming) and AC-10 (input `lat`/`lon` vs output `latitude`/`longitude` asymmetry) are explicitly **not** tested — surfaced to parent-suite team for follow-up only.
|
||||
**Notes**: The 14 rules split across three layers — deserializer (rules 3 type-mismatch, 13 unknown-root, 14 nested type mismatch, 10 missing-required), `CreateRouteRequestValidator` (rules 2b/5/6/7, plus orchestration of nested validators), `RoutePointValidator` (rules 8a/8b — applied per-element via `RuleForEach(x => x.Points).SetValidator`), `GeofencePolygonValidator` (rule 9 cross-field), and a top-level `Must()` (rule 12). Rule 4 (`description` length cap if present) is enforced by `CreateRouteRequestValidator.RuleFor(x => x.Description).MaximumLength(...)` and is not exercised by an explicit sub-case because the existing `pos` body omits `description` and there is no probe-confirmed footgun there. The cross-field rule 12 surfaces under a top-level `errors` map entry (no field key) per FluentValidation convention for `RuleFor(x => x).Must(...)`.
|
||||
|
||||
## BT-30: UAV Upload Metadata Endpoint — Multipart Strict Validation
|
||||
|
||||
**Trigger**: A family of `POST /api/satellite/upload` multipart calls covering AZ-810's 14 rules. The endpoint is `multipart/form-data`, so the validator wires through a **custom** `UavUploadValidationFilter` (NOT the generic `ValidationEndpointFilter<T>` used by JSON-body endpoints) which deserializes the `metadata` form field via the strict global `JsonSerializerOptions` and routes errors through three composed layers (deserializer → FluentValidation → envelope cross-field check).
|
||||
**Precondition**: API up; valid JWT with `permissions:["GPS"]` attached. `error-shape.md` v1.0.0 + `uav-tile-upload.md` v1.2.0 frozen.
|
||||
**Expected**: HTTP 400 with `Content-Type: application/problem+json` for every failure sub-case; HTTP 200 + per-item result list for the happy path (per-item file rejections still return HTTP 200 — file-byte semantics unchanged from AZ-488). `errors[]` keys are prefixed with `metadata.` (e.g. `metadata.items[0].latitude`) for FluentValidation-layer rules; deserializer-layer failures surface under `errors["metadata"]` (per `UavUploadValidationFilter` design — the deserializer doesn't know the per-field path inside the form value).
|
||||
|
||||
| # | Rule | Layer | Trigger excerpt | Expected `errors` key | Test method |
|
||||
|---|------|-------|-----------------|-----------------------|-------------|
|
||||
| pos | Happy path | — | well-formed multipart envelope with valid 256×256 JPEG | HTTP 200, per-item `status=accepted` | `HappyPath_Returns200` |
|
||||
| 1 | `metadata` form field present | deserializer | omit `metadata` field | `metadata` | `MissingMetadataField_Returns400` |
|
||||
| 2 | `metadata` deserializes to `UavTileBatchMetadataPayload` | deserializer | `"items":"notanarray"` | `metadata` | `MetadataNotAnObject_Returns400` |
|
||||
| 3 | `metadata.items` count in `[1, 100]` | FluentValidation | 101-item array | `metadata.items` | `OversizedItemsArray_Returns400` |
|
||||
| 4 | `metadata.items` count == file count (envelope cross-field) | envelope filter | 2 items, 1 file | both `metadata.items` and `files` | `ItemsFileCountMismatch_Returns400` |
|
||||
| 5 | per-item `latitude` in `[-90, 90]` | FluentValidation | `items[1].latitude=91` | `metadata.items[1].latitude` | `LatitudeOutOfRange_Returns400` |
|
||||
| 6 | per-item `longitude` in `[-180, 180]` | FluentValidation | `items[0].longitude=181` | `metadata.items[0].longitude` | `LongitudeOutOfRange_Returns400` |
|
||||
| 7 | per-item `tileZoom` in `[0, 22]` | FluentValidation | `items[0].tileZoom=30` | `metadata.items[0].tileZoom` | `TileZoomOutOfRange_Returns400` |
|
||||
| 8 | per-item `tileSizeMeters` > 0 | FluentValidation | `items[0].tileSizeMeters=0` | `metadata.items[0].tileSizeMeters` | `TileSizeMetersZero_Returns400` |
|
||||
| 9a | per-item `capturedAt` not in future (skew = 30s) | FluentValidation | `capturedAt=UtcNow+1h` | `metadata.items[0].capturedAt` | `CapturedAtFuture_Returns400` |
|
||||
| 9b | per-item `capturedAt` not older than `MaxAgeDays` (default 7d) | FluentValidation | `capturedAt=UtcNow-60d` | `metadata.items[0].capturedAt` | `CapturedAtTooOld_Returns400` |
|
||||
| 10 | per-item required fields (`latitude`/`longitude`/`tileZoom`/`tileSizeMeters`/`capturedAt`) present (`[JsonRequired]`) | deserializer | omit `latitude` from one item | `metadata` (deserializer-level; per-field path not available inside form value) | `MissingRequiredField_Returns400` |
|
||||
| 11 | Unknown root field on `UavTileBatchMetadataPayload` | deserializer | `"debug":42` at metadata root | `metadata` | `UnknownRootField_Returns400` |
|
||||
| 12 | Unknown nested field on `UavTileMetadata` | deserializer | `"altitude":100` on an item | `metadata` | `UnknownItemField_Returns400` |
|
||||
| 13 | Type mismatch on per-item field | deserializer | `"latitude":"fifty"` | `metadata` | `LatitudeTypeMismatch_Returns400` |
|
||||
|
||||
**Pass criterion**: Every failure sub-case returns HTTP 400 with the expected `errors` key. The happy path returns HTTP 200 + a per-item result list with `status=accepted`. Per-item file rejections (existing `IUavTileQualityGate` semantics: `INVALID_FORMAT`, `WRONG_DIMENSIONS`, `SIZE_OUT_OF_BAND`, `CAPTURED_AT_FUTURE`, `CAPTURED_AT_TOO_OLD`, `IMAGE_TOO_UNIFORM`) still return HTTP 200 with per-item `status=rejected` (AZ-488 contract preserved). **AZ-810 AC-9 verified** by full integration suite green at cycle 8 Step 11 (after the test-data clamp fix — see `_docs/03_implementation/batch_04_cycle8_report.md` AC-9 row + commit `b763da3`).
|
||||
**AC trace**: AZ-810 AC-1 (14 rules + ProblemDetails shape), AC-2 (happy path unchanged), AC-9 (no AZ-488 regression).
|
||||
**Notes**: This is the first endpoint where the validation infra had to step **outside** the generic `ValidationEndpointFilter<T>` — multipart form-data is not a JSON body, so the metadata field has to be extracted from `IFormCollection` and deserialized inside a custom filter (`UavUploadValidationFilter`). Three layers compose: (1) deserializer with `JsonSerializerOptions.UnmappedMemberHandling.Disallow` + `[JsonRequired]` — surfaces under `errors["metadata"]`; (2) `UavTileBatchMetadataPayloadValidator` + `UavTileMetadataValidator` (FluentValidation) — error paths prefixed with `metadata.`; (3) envelope cross-field check (`items.Count == files.Count`) inside the filter — surfaces under BOTH `errors["metadata.items"]` AND `errors["files"]`. The `metadata.` prefix and the bare-`"metadata"` key for deserializer-level errors are documented in `_docs/02_document/contracts/api/uav-tile-upload.md` v1.2.0 §Validation Rules.
|
||||
|
||||
## BT-31: GET tiles/latlon — Query-Param Strict Validation + Unknown-Param Rejection
|
||||
|
||||
**Trigger**: A family of `GET /api/satellite/tiles/latlon?lat=&lon=&zoom=` calls covering AZ-811's 5 rules. The endpoint takes **query parameters** (not a JSON body), so the validator wires through a **novel** `UnknownQueryParameterEndpointFilter` (envelope filter, item 4 of AZ-811's implementation pattern) plus `GetTileByLatLonQueryValidator` (FluentValidation for the typed query params).
|
||||
**Precondition**: API up; valid JWT attached. `error-shape.md` v1.0.0 + `tile-latlon.md` v1.0.0 frozen.
|
||||
**Expected**: HTTP 400 with `Content-Type: application/problem+json` for every failure sub-case; HTTP 200 + `DownloadTileResponse` for the happy path. `errors[]` keys name the offending query parameter (e.g. `lat`).
|
||||
|
||||
| # | Rule | Trigger excerpt | Expected `errors` key | Test method |
|
||||
|---|------|-----------------|-----------------------|-------------|
|
||||
| pos | Happy path | `?lat=47.46&lon=37.64&zoom=18` | HTTP 200 — body shape unchanged | `HappyPath_Returns200` |
|
||||
| 1 | `lat` in range `[-90, 90]` | `?lat=91` | `lat` | `LatOutOfRange_Returns400` |
|
||||
| 2 | `lon` in range `[-180, 180]` | `?lon=181` | `lon` | `LonOutOfRange_Returns400` |
|
||||
| 3 | `zoom` in range `[0, 22]` | `?zoom=30` | `zoom` | `ZoomOutOfRange_Returns400` |
|
||||
| 4a | `lat` required | omit `?lat=` | `lat` ("`lat` is required") | `MissingLat_Returns400` |
|
||||
| 4b | Legacy v1 names (`?Latitude=&Longitude=&ZoomLevel=`) | exact pre-AZ-811 wire format | `errors` map names BOTH unknown keys | `LegacyLatitudeLongitudeNames_Returns400` |
|
||||
| 4c | Hostile / typo query keys | `?debug=1&admin=true` | `errors` map names BOTH unknown keys | `UnknownQueryKeys_Returns400` |
|
||||
| 5 | `lat` type mismatch | `?lat=fifty` | `lat` | `LatTypeMismatch_Returns400` |
|
||||
|
||||
**Pass criterion**: Every failure sub-case returns HTTP 400 with the expected `errors` key. The happy path returns HTTP 200 + a non-empty `tileId` and a path of shape `tiles/{zoom}/{x}/{y}.jpg`. No regression in `TileByLatLonTests.cs` (cycle 8 Step 11 — green). BT-N01 (`lat=91&lon=181&zoom=18`) and BT-N02 (`zoom=25`) — pre-cycle-8 negative scenarios that only asserted "HTTP 4xx or error in body" — are now strictly bound: BT-N01 produces HTTP 400 with `errors["lat"]` AND `errors["lon"]`; BT-N02 produces HTTP 400 with `errors["zoom"]`.
|
||||
**AC trace**: AZ-811 AC-1 (5 rules + ProblemDetails shape), AC-2 (happy path), AC-9 (the novel unknown-query-param envelope filter is documented in `_docs/02_document/modules/api_program.md` for reuse).
|
||||
**Notes**: This is the first endpoint to need a generic **unknown-query-param rejection** layer — ASP.NET's default model binder silently ignores unknown query parameters (parallel to `UnmappedMemberHandling.Disallow` for JSON bodies, but no built-in equivalent exists for query strings). The new `UnknownQueryParameterEndpointFilter` introspects the route's declared parameters and rejects any extra keys. Sub-cases `4b` and `4c` exercise this filter: `4b` proves the pre-AZ-811 wire format (`?Latitude=&Longitude=&ZoomLevel=`) that silently fell back to `lat=0, lon=0, zoom=0` now fails fast with HTTP 400 naming all three unknown keys; `4c` proves the same path catches arbitrary hostile / typo keys. The filter is designed for reuse by any future query-param endpoint (AZ-811 AC-9).
|
||||
|
||||
|
||||
Reference in New Issue
Block a user