Files
satellite-provider/_docs/02_document/tests/blackbox-tests.md
T
Oleksandr Bezdieniezhnykh 61612044fb [AZ-503] [AZ-504] Cycle 5 Steps 11-15 sync
Wrap up cycle 5 verification + documentation:
- Steps 10/11 wrap-up reports (implementation_completeness +
  implementation_report) for the AZ-503-foundation + AZ-504 batch.
- Step 12 test-spec sync: AZ-503-foundation/AZ-504 ACs appended;
  AZ-505 deferred ACs recorded.
- Step 13 update-docs: architecture, data-model, glossary, module-
  layout, uav-tile-upload contract (v1.1.0), DataAccess + Services
  + Tests module docs synced; new common_uuidv5.md module doc.
- Step 14 security audit: PASS_WITH_WARNINGS; 0 new Critical/High;
  2 new Low informational (F1 flightId provenance, F2 pgcrypto
  deploy gap).
- Step 15 performance test: PASS_WITH_INFRA_WARNINGS; PT-08
  passed twice (AZ-504 fix verified); PT-01/02 failed due to
  recurring local Docker/colima DNS cold-start (not an app
  regression). Cycle-3 perf-harness leftover stays OPEN with
  replay #5 documented.
- Autodev state moved to Step 16 (Deploy).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-12 18:01:27 +03:00

210 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Blackbox Test Scenarios
## BT-01: Single Tile Download
**Trigger**: GET /api/satellite/tiles/latlon?Latitude=47.461747&Longitude=37.647063&ZoomLevel=18
**Precondition**: Tile not in cache
**Expected**: HTTP 200; JSON with zoomLevel=18, tileSizePixels=256, imageType="jpg", filePath matching pattern `tiles/18/*/...`
**Pass criterion**: All fields present and correct values
## BT-02: Tile Cache Reuse
**Trigger**: Same GET as BT-01 repeated
**Precondition**: BT-01 completed (tile now cached)
**Expected**: HTTP 200; same tile ID returned; no new file created
**Pass criterion**: tile.Id matches first request's tile.Id
## BT-03: Region Request (200m, zoom 18, no stitch)
**Trigger**: POST /api/satellite/request with lat=47.461747, lon=37.647063, sizeMeters=200, zoomLevel=18, stitchTiles=false
**Expected**: HTTP 200 immediately; status transitions: pending → processing → completed
**Pass criterion**: Final status="completed"; csvFilePath non-empty; summaryFilePath non-empty; tilesDownloaded + tilesReused > 0
**Timeout**: 240s
## BT-04: Region Request (400m, zoom 17, no stitch)
**Trigger**: POST /api/satellite/request with lat=47.461747, lon=37.647063, sizeMeters=400, zoomLevel=17, stitchTiles=false
**Expected**: Same as BT-03
**Pass criterion**: Same as BT-03
**Timeout**: 240s
## BT-05: Region with Stitching (500m, zoom 18)
**Trigger**: POST /api/satellite/request with lat=47.461747, lon=37.647063, sizeMeters=500, zoomLevel=18, stitchTiles=true
**Expected**: Completes with stitched image generated
**Pass criterion**: status="completed"; stitched image file exists and size > 1024 bytes
**Timeout**: 240s
## BT-06: Simple Route Creation (2 points)
**Trigger**: POST /api/satellite/route with 2 waypoints (48.276067,37.384458) → (48.270740,37.374029), regionSize=500, zoom=18
**Expected**: Route created with interpolated intermediate points
**Pass criterion**: totalPoints > 2; every point spacing ≤ 200m; first point type="original"; last point type="original"; intermediates type="intermediate"
## BT-07: Route Retrieval by ID
**Trigger**: GET /api/satellite/route/{id} after BT-06
**Expected**: Same route returned with all points
**Pass criterion**: route.Id matches; points count matches creation response
## BT-08: Route with Map Processing
**Trigger**: POST /api/satellite/route with requestMaps=true, 2 points, regionSize=300
**Expected**: Route maps processed, stitched image and CSV created
**Pass criterion**: mapsReady=true; stitchedImagePath non-empty; csvFilePath non-empty; stitched image > 1024 bytes
**Timeout**: 180s
## BT-09: Route with Tiles ZIP
**Trigger**: POST /api/satellite/route with requestMaps=true, createTilesZip=true, 2 points
**Expected**: ZIP file created with tiles
**Pass criterion**: tilesZipPath non-empty; ZIP > 1024 bytes; ZIP entry count = unique tiles in CSV; entries start with "tiles/"; path has ≥5 parts (directory structure preserved)
**Timeout**: 180s
## BT-10: Complex Route (10 points, maps)
**Trigger**: POST /api/satellite/route with 10 waypoints, requestMaps=true, regionSize=300
**Expected**: All points interpolated; map tiles processed
**Pass criterion**: mapsReady=true; uniqueTileCount ≥ 10; stitched image > 1024 bytes
**Timeout**: 240s
## BT-11: Route with Geofences (10 points + 2 rectangles)
**Trigger**: POST /api/satellite/route with 10 waypoints + 2 geofence polygons, requestMaps=true
**Expected**: Geofence regions created and processed
**Pass criterion**: mapsReady=true; uniqueTileCount ≥ 10; stitched image > 1024 bytes; geofence regions linked to route
**Timeout**: 240s
## BT-12: Extended Route (20 points, maps)
**Trigger**: POST /api/satellite/route with 20 waypoints in separate geographic area, requestMaps=true
**Expected**: Large route processed completely
**Pass criterion**: mapsReady=true; uniqueTileCount ≥ 20; stitched image > 1024 bytes
**Timeout**: 360s
## Negative Scenarios
## BT-N01: Invalid Coordinates (out of range)
**Trigger**: GET /api/satellite/tiles/latlon?Latitude=91&Longitude=181&ZoomLevel=18
**Expected**: Error response
**Pass criterion**: HTTP 4xx or error in response body
## BT-N02: Invalid Zoom Level
**Trigger**: GET /api/satellite/tiles/latlon?Latitude=47.46&Longitude=37.64&ZoomLevel=25
**Expected**: Error response
**Pass criterion**: HTTP 4xx or error indicating invalid zoom
## BT-N03: Route with < 2 Points
**Trigger**: POST /api/satellite/route with only 1 point
**Expected**: Validation error
**Pass criterion**: HTTP 400 or validation error message
## BT-N04: Geofence with Invalid Coordinates (0,0)
**Trigger**: POST /api/satellite/route with geofence NW=(0,0) SE=(0,0)
**Expected**: Validation error
**Pass criterion**: Error message mentioning coordinates cannot be (0,0)
## BT-N05: Geofence with Inverted Corners
**Trigger**: POST /api/satellite/route with geofence NW.lat < SE.lat
**Expected**: Validation error
**Pass criterion**: Error message about northWest latitude > southEast latitude
---
## Cycle 2 — AZ-488 UAV Tile Upload (POST /api/satellite/upload)
All Cycle-2 UAV scenarios run with a JWT containing `permissions: ["GPS"]` (per AZ-487 + AZ-488). Files use the contract at `_docs/02_document/contracts/api/uav-tile-upload.md` v1.0.0; per-item correlation is by ordinal index between metadata array and `IFormFileCollection`.
## BT-13: UAV Upload — Happy-Path 1-Item Batch Persists `source='uav'`
**Trigger**: POST `/api/satellite/upload` with a 1-item batch — a 256×256 JPEG (~50 KiB), `capturedAt = now`, valid coordinates inside the test region.
**Precondition**: Empty `tiles` table for the chosen cell; valid `GPS` JWT.
**Expected**: HTTP 200; response body has `items[0].status == "accepted"` and a non-empty `tileId`; a new row exists in `tiles` with `source='uav'`, `captured_at` matching the request (UTC, ≤ 1 s drift), `file_path == 'tiles/uav/{z}/{x}/{y}.jpg'`; the file exists on disk at that path with the uploaded bytes.
**Pass criterion**: All of the above true.
**AC trace**: AZ-488 AC-1.
## BT-14: UAV Upload — 3-Item Mixed Batch Returns Per-Item Results
**Trigger**: POST `/api/satellite/upload` with a 3-item batch where item-1 is a valid 256×256 JPEG, item-2 is a 512×512 JPEG (wrong dimensions), item-3 has PNG magic bytes (wrong format).
**Precondition**: Empty `tiles` table; valid `GPS` JWT.
**Expected**: HTTP 200; `items[0].status == "accepted"` with a `tileId`; `items[1].status == "rejected"` with `rejectReason == "WRONG_DIMENSIONS"`; `items[2].status == "rejected"` with `rejectReason == "INVALID_FORMAT"`. Exactly one new row appears in `tiles` (for item-1 only). No file written for items 2 or 3.
**Pass criterion**: status array matches `[accepted, rejected, rejected]` AND reasons match exactly AND `COUNT(*) WHERE source='uav'` == 1 for the test region.
**AC trace**: AZ-488 AC-2, AC-7a, AC-7c.
## BT-15: UAV Upload — Multi-Source Coexistence with `google_maps`
**Trigger**: Pre-seed `tiles` (raw INSERT) with a `source='google_maps'` row at `(L, Ln, z=18, size_m=200)` and `captured_at = T1 = now 2h`. Then POST `/api/satellite/upload` with a UAV tile for the same cell and `capturedAt = T2 = now`.
**Precondition**: AZ-484 migration 013 applied (5-column unique index in place); valid `GPS` JWT.
**Expected**: HTTP 200; both rows exist in `tiles` after upload (no overwrite of the google_maps row); a follow-up `GetByTileCoordinatesAsync(L, Ln, 18, 200)` returns the `source='uav'` row (per AZ-484 selection rule: max `captured_at` across sources).
**Pass criterion**: `SELECT source FROM tiles WHERE ...` returns both `'google_maps'` AND `'uav'`; the repository read returns the UAV row.
**AC trace**: AZ-488 AC-3; cross-references AZ-484 AC-1 (storage Inv-3) and AZ-484 AC-2 (selection rule).
## BT-16: UAV Upload — Same-Source UPSERT Collapses to One Row
**Trigger**: POST `/api/satellite/upload` with a UAV tile for cell `(L, Ln, 18, 200)` at `capturedAt = T1 = now 30m`, then a second POST for the same cell at `capturedAt = T2 = now` with different image bytes (different `seed`).
**Precondition**: Cell is empty for `source='uav'` before T1; valid `GPS` JWT.
**Expected**: HTTP 200 for both calls. After the second call, exactly one `source='uav'` row remains for the cell with `captured_at == T2`; the JPEG at `./tiles/uav/{z}/{x}/{y}.jpg` is overwritten with the T2 bytes. Any pre-existing `source='google_maps'` row is untouched.
**Pass criterion**: `SELECT COUNT(*) FROM tiles WHERE source='uav' AND (L, Ln, 18, 200)` == 1 AND `MAX(captured_at) ≈ T2` AND on-disk JPEG checksum matches the T2 upload.
**AC trace**: AZ-488 AC-4; cross-references AZ-484 AC-3.
## BT-17: UAV Upload — Quality-Gate Rule-Ordering Determinism
**Trigger**: POST `/api/satellite/upload` with a single item that violates BOTH Rule 1 (PNG magic instead of JPEG) AND Rule 3 (512×512 dimensions). Authenticated with `GPS` permission.
**Expected**: HTTP 200; `items[0].status == "rejected"` with `rejectReason == "INVALID_FORMAT"` (Rule 1 fires first; Rule 3 never evaluated).
**Pass criterion**: rejectReason equals exactly `INVALID_FORMAT`; never `WRONG_DIMENSIONS`.
**AC trace**: AZ-488 AC-7; rule-ordering invariant from `_docs/02_document/contracts/api/uav-tile-upload.md` v1.0.0.
## Cycle 2 — AZ-487 Endpoint Parity (existing endpoints with Bearer token)
## BT-18: Existing Tile Endpoint Returns Identical Body with Valid Bearer
**Trigger**: GET `/api/satellite/tiles/latlon?Latitude=47.461747&Longitude=37.647063&ZoomLevel=18` with a valid Bearer token.
**Precondition**: Tile may or may not be cached.
**Expected**: Response body is structurally identical to BT-01 (`tileId`, `zoomLevel == 18`, `tileSizePixels == 256`, `imageType == "jpg"`, `filePath` matches `tiles/18/*/*`).
**Pass criterion**: status == 200 AND BT-01's pass criterion AND no behavioral change vs pre-AZ-487 baseline.
**AC trace**: AZ-487 AC-4 (handler unchanged); validates AZ-487 AC-8 (existing suite parity).
---
## Cycle 5 — AZ-503 Tile Identity → UUIDv5 + integer UPSERT (foundation)
All Cycle-5 UAV scenarios reuse the AZ-488 envelope. The new observable surface is: a `flightId` field in `UavTileMetadata`, deterministic `tileId` / `locationHash` values, and a per-flight on-disk layout `./tiles/uav/{flight_id|none}/{z}/{x}/{y}.jpg`. No new HTTP route or wire change beyond the optional metadata field.
## BT-19: UAV Upload — Multi-Flight Coexistence with Shared `location_hash`
**Trigger**: POST `/api/satellite/upload` twice for the SAME `(z=18, tile_x, tile_y, tile_size_meters)` from two different `flight_id` values `F1 ≠ F2` (each upload sends `metadata.flightId = F`, valid 256×256 JPEG, fresh `capturedAt`).
**Precondition**: Empty `tiles` table for the chosen cell; valid `GPS` JWT.
**Expected**: HTTP 200 for both calls. After the second call, exactly TWO rows exist in `tiles` for `source='uav'` at the cell — one per flight. Both rows share the same `location_hash` (deterministic per `{z}/{x}/{y}`); each has a distinct `id` (deterministic per `{z}/{x}/{y}/uav/{flight_id}`). On disk, two files exist at `./tiles/uav/{F1}/{z}/{x}/{y}.jpg` and `./tiles/uav/{F2}/{z}/{x}/{y}.jpg`; `rm -rf ./tiles/uav/{F1}/` removes ONLY Flight F1's evidence.
**Pass criterion**: `SELECT COUNT(*) FROM tiles WHERE source='uav' AND (z,x,y,size_m)=(...)` == 2 AND `COUNT(DISTINCT location_hash)` == 1 AND `COUNT(DISTINCT id)` == 2 AND `COUNT(DISTINCT file_path)` == 2 AND both files present on disk.
**AC trace**: AZ-503 AC-3, AC-11.
## BT-20: UAV Upload — Idempotent Re-Insert Preserves Deterministic `tileId` and `content_sha256`
**Trigger**: POST `/api/satellite/upload` for cell `(z=18, x, y, size_m)` with `flightId = F` and JPEG body `B` and `capturedAt = T1`. Repeat the SAME body and `flightId` with `capturedAt = T2 > T1`.
**Precondition**: Empty `tiles` table for the cell; valid `GPS` JWT.
**Expected**: HTTP 200 for both calls. Exactly ONE row exists for the cell after both inserts. The row's `id` is identical before and after the second insert (deterministic UUIDv5 from `{z}/{x}/{y}/uav/{F}`). `updated_at` advances to T2; `created_at` is NOT regenerated. `content_sha256` equals `SHA-256(B)` externally computed; the second insert's `content_sha256` matches the first (byte-identical body).
**Pass criterion**: `SELECT COUNT(*)` == 1 AND `id` is stable AND `content_sha256 == sha256(B)` AND `created_at` unchanged AND `updated_at == T2`.
**AC trace**: AZ-503 AC-2, AC-7.
## BT-21: UAV Upload — Float-Rounded Coordinates Collapse to a Single Row
**Trigger**: POST `/api/satellite/upload` for cell `(z=18, x, y, size_m)` with `latitude = 47.123456789012345` and `flightId = F`. Repeat with the SAME `(x, y, z)` but `latitude` recomputed from `tile_center = TileToWorldPos(x, y, z)` (slightly different float representation).
**Precondition**: Empty `tiles` table for the cell; valid `GPS` JWT.
**Expected**: HTTP 200 for both calls. Exactly ONE row results — the integer-only UPSERT conflict key (`tile_zoom, tile_x, tile_y, tile_size_meters, source, COALESCE(flight_id, ...)`) triggers despite the float-different `latitude` values.
**Pass criterion**: `SELECT COUNT(*) FROM tiles WHERE source='uav' AND tile_zoom=18 AND tile_x=x AND tile_y=y AND tile_size_meters=size_m` == 1.
**AC trace**: AZ-503 AC-4.
## BT-22: Migration 014 — Identity Columns Land and Backfill Is Deterministic
**Trigger**: Run `dotnet ef migrations apply` (via API container startup) against a DB with cycle-4 schema; then query `information_schema.columns` and `pg_indexes` for `tiles`.
**Precondition**: DB starts with migration 013 applied (cycle 4 baseline); `pgcrypto` available.
**Expected**: `tiles` table has `flight_id uuid NULL`, `location_hash uuid NOT NULL`, `content_sha256 bytea NULL`, `legacy_id uuid NULL`. `idx_tiles_unique_identity` exists as a UNIQUE index over `(tile_zoom, tile_x, tile_y, tile_size_meters, source, COALESCE(flight_id, '00000000-...'::uuid))`. AZ-484 index `idx_tiles_unique_location_source` is dropped. For any pre-existing row, `location_hash` equals `uuidv5(TILE_NAMESPACE, '{tile_zoom}/{tile_x}/{tile_y}')` byte-identically (validated against a SQL `pg_temp.uuidv5` reference function).
**Pass criterion**: All column / index assertions pass AND the deterministic backfill matches the reference function on 100% of sampled rows.
**AC trace**: AZ-503 AC-8.