Updated JWT authentication to use configuration values instead of hardcoded secrets, improving security and flexibility. Enhanced CORS policy to conditionally allow origins based on configuration settings, with logging for permissive defaults. Updated README to reflect project renaming and clarify service context.
21 KiB
Blackbox Tests
Status: produced by autodev
/test-specPhase 2 (2026-05-14). Naming: post-rename target. Pre-rename code path runs the same test against/aircrafts//flights; tests will be RED until B5–B8 land. Cross-references: every test traces to one or more AC IDs from_docs/00_problem/acceptance_criteria.md. Expected results are inline (quantifiable) with a pointer to the corresponding row in_docs/00_problem/input_data/expected_results/results_report.md.
Positive Scenarios
FT-P-01: Create non-default vehicle returns 201 with PascalCase body
Summary: Verifies vehicle CRUD create path and response wire shape. Traces to: AC-1.1, AC-8.1 Category: Vehicle CRUD
Preconditions:
seed_empty
Input data: data_parameters.md § 2.1 — POST /vehicles { Type:0, Model:"Bayraktar", Name:"BR-01", FuelType:1, BatteryCapacity:0, EngineConsumption:5, EngineConsumptionIdle:1, IsDefault:false }, JWT permissions=FL
Steps:
| Step | Consumer Action | Expected System Response |
|---|---|---|
| 1 | POST /vehicles with body above |
201 Created; body Vehicle with PascalCase keys Id, Type, Model, Name, FuelType, BatteryCapacity, EngineConsumption, EngineConsumptionIdle, IsDefault; Id parses as UUID |
| 2 | Side-channel SELECT COUNT(*) FROM vehicles WHERE id={returnedId} |
count == 1 |
Expected outcome: results_report.md AC-1 row 1.1. Max execution time: 5s.
FT-P-02: Create default vehicle demotes prior default
Summary: Verifies the clear-then-set pattern in VehicleService.CreateVehicle when IsDefault=true.
Traces to: AC-1.2
Category: Vehicle CRUD
Preconditions:
seed_one_default_vehicle(prior rowId=P1,is_default=true)
Input data: POST /vehicles { ..., IsDefault:true } (other fields valid)
Steps:
| Step | Consumer Action | Expected System Response |
|---|---|---|
| 1 | POST /vehicles { ..., IsDefault:true } |
201; new row has IsDefault:true |
| 2 | Side-channel SELECT id, is_default FROM vehicles ORDER BY is_default DESC |
New row has is_default=true; row P1 has is_default=false; total is_default=true count == 1 |
Expected outcome: results_report.md AC-1 row 1.2. Max execution time: 5s.
FT-P-03: setDefault promotes existing vehicle
Summary: Verifies POST /vehicles/{id}/setDefault clear-then-set behavior.
Traces to: AC-1.2 (update branch), AC-1.4
Category: Vehicle CRUD
Preconditions:
seed_one_default_vehicle(default rowP1); add a non-default rowP2
Input data: POST /vehicles/{P2}/setDefault { IsDefault:true }
Steps:
| Step | Consumer Action | Expected System Response |
|---|---|---|
| 1 | POST /vehicles/{P2}/setDefault { IsDefault:true } |
200; body Vehicle with Id==P2, IsDefault==true |
| 2 | Side-channel SELECT id, is_default FROM vehicles |
P2.is_default==true, P1.is_default==false, default count == 1 |
Expected outcome: results_report.md AC-1 row 1.4. Max execution time: 5s.
FT-P-04: Vehicle list returns plain JSON array (no pagination)
Summary: Verifies GET /vehicles returns a non-paginated array — distinguishing it from GET /missions.
Traces to: AC-1.5
Category: Vehicle CRUD
Preconditions:
seed_3_vehicles_2_default
Input data: GET /vehicles
Steps:
| Step | Consumer Action | Expected System Response |
|---|---|---|
| 1 | GET /vehicles |
200; body parses as JSON array (NOT object); body.length == 3; each element has PascalCase keys per FT-P-01 |
Expected outcome: results_report.md AC-1 row 1.5. Max execution time: 2s.
FT-P-05: Vehicle filter by name + isDefault
Summary: Verifies query-string filter (case-sensitive substring on name, exact on isDefault).
Traces to: AC-1.6
Category: Vehicle CRUD
Preconditions:
seed_3_vehicles_2_defaultcontainingBR-01(default),BR-02(non-default),MQ-9(default)
Input data: GET /vehicles?name=BR&isDefault=true
Steps:
| Step | Consumer Action | Expected System Response |
|---|---|---|
| 1 | GET /vehicles?name=BR&isDefault=true |
200; body.length == 1; body[0].Name == "BR-01" |
Expected outcome: results_report.md AC-1 row 1.6. Max execution time: 2s.
FT-P-06: Delete vehicle with no references
Summary: Verifies DELETE /vehicles/{id} returns 204 and removes the row.
Traces to: AC-1.10
Category: Vehicle CRUD
Preconditions:
- One vehicle row exists, no missions reference it
Input data: DELETE /vehicles/{id}
Steps:
| Step | Consumer Action | Expected System Response |
|---|---|---|
| 1 | DELETE /vehicles/{id} |
204 No Content; empty body |
| 2 | Side-channel SELECT COUNT(*) FROM vehicles WHERE id={id} |
count == 0 |
Expected outcome: results_report.md AC-1 row 1.10. Max execution time: 2s.
FT-P-07: Create mission with default CreatedDate
Summary: Verifies mission create assigns CreatedDate = UtcNow when null.
Traces to: AC-2.1
Category: Mission CRUD
Preconditions:
seed_one_default_vehicle(use its id asVehicleId)
Input data: POST /missions { Name:"Recon-01", VehicleId:<id>, CreatedDate:null }
Steps:
| Step | Consumer Action | Expected System Response |
|---|---|---|
| 1 | Capture t0 = DateTime.UtcNow |
|
| 2 | POST /missions with body above |
201; body.CreatedDate parses as UTC; abs(body.CreatedDate - t0) ≤ 5s |
Expected outcome: results_report.md AC-2 row 2.1. Max execution time: 5s.
FT-P-08: Mission list paginated default page
Summary: Verifies GET /missions returns PaginatedResponse<Mission> with default page size 20.
Traces to: AC-2.3, AC-8.7
Category: Mission CRUD
Preconditions:
seed_25_missions
Input data: GET /missions
Steps:
| Step | Consumer Action | Expected System Response |
|---|---|---|
| 1 | GET /missions |
200; body parses as object with PascalCase keys Items, TotalCount, Page, PageSize; Page==1; PageSize==20; TotalCount==25; Items.length==20 |
Expected outcome: results_report.md AC-2 row 2.3. Max execution time: 2s.
FT-P-09: Mission list page 2
Summary: Verifies pagination skips correctly to page 2. Traces to: AC-2.3 Category: Mission CRUD
Preconditions:
seed_25_missions
Input data: GET /missions?page=2&pageSize=20
Steps:
| Step | Consumer Action | Expected System Response |
|---|---|---|
| 1 | GET /missions?page=2&pageSize=20 |
200; Page==2; Items.length==5; ids in Items are disjoint from those returned by FT-P-08 (per UUID set check) |
Expected outcome: results_report.md AC-2 row 2.4. Max execution time: 2s.
FT-P-10: Mission list date range
Summary: Verifies ?fromDate=&toDate= filter inclusivity.
Traces to: AC-2.3
Category: Mission CRUD
Preconditions:
seed_25_missions(5 January, 20 February)
Input data: GET /missions?fromDate=2026-01-01T00:00:00Z&toDate=2026-01-31T23:59:59Z
Steps:
| Step | Consumer Action | Expected System Response |
|---|---|---|
| 1 | request as above | 200; TotalCount==5; every Items[i].CreatedDate falls within January 2026 UTC |
Expected outcome: results_report.md AC-2 row 2.5. Max execution time: 2s.
FT-P-11: Mission partial update preserves null fields
Summary: Verifies PUT /missions/{id} only overwrites non-null fields in the request body.
Traces to: AC-2.5
Category: Mission CRUD
Preconditions:
- One mission row with known
Name,VehicleId
Input data: PUT /missions/{id} { Name:"Renamed", VehicleId:null }
Steps:
| Step | Consumer Action | Expected System Response |
|---|---|---|
| 1 | request as above | 200; body.Name == "Renamed"; body.VehicleId == previous_value (preserved) |
Expected outcome: results_report.md AC-2 row 2.7. Max execution time: 2s.
FT-P-12: Mission cascade delete walks every dependency table
Summary: Verifies DELETE /missions/{id} deletes every row in the seeded chain across map_objects, detection, annotations, media, waypoints, missions.
Traces to: AC-3.1
Category: Mission Cascade Delete (F3) — most critical
Preconditions:
fixture_cascade_F3applied (seededmidmission with full chain)
Input data: DELETE /missions/{mid}
Steps:
| Step | Consumer Action | Expected System Response |
|---|---|---|
| 1 | request as above | 204 |
| 2 | Side-channel SELECT COUNT(*) FROM map_objects WHERE mission_id={mid} |
count == 0 |
| 3 | Side-channel SELECT COUNT(*) FROM detection WHERE annotation_id IN (seeded ids) |
count == 0 |
| 4 | Side-channel SELECT COUNT(*) FROM annotations WHERE id IN (seeded ids) |
count == 0 |
| 5 | Side-channel SELECT COUNT(*) FROM media WHERE id IN (seeded ids) |
count == 0 |
| 6 | Side-channel SELECT COUNT(*) FROM waypoints WHERE mission_id={mid} |
count == 0 |
| 7 | Side-channel SELECT COUNT(*) FROM missions WHERE id={mid} |
count == 0 |
| 8 | Compare per-table counts against expected_results/cascade_F3_walk.json |
json_diff matches all expected fields |
Expected outcome: results_report.md AC-3 row 3.1. Max execution time: 10s.
FT-P-13: Waypoint list ordered by OrderNum
Summary: Verifies GET /missions/{id}/waypoints returns waypoints sorted by OrderNum ASC, regardless of insert order.
Traces to: AC-4.3
Category: Waypoint CRUD
Preconditions:
seed_5_waypoints_unorderedunder one mission
Input data: GET /missions/{id}/waypoints
Steps:
| Step | Consumer Action | Expected System Response |
|---|---|---|
| 1 | request as above | 200; body parses as JSON array; body.length == 5; [w.OrderNum for w in body] == [1, 2, 3, 4, 5] |
Expected outcome: results_report.md AC-4 row 4.2. Max execution time: 2s.
FT-P-14: Waypoint create echoes geo fields
Summary: Verifies POST /missions/{id}/waypoints creates a row with the supplied GeoPoint and DOES NOT auto-convert lat/lon ↔ mgrs.
Traces to: AC-4 (data_parameters.md § 2.3 spec divergence)
Category: Waypoint CRUD
Preconditions:
- One mission row exists
Input data: POST /missions/{id}/waypoints { GeoPoint:{Lat:50.45, Lon:30.52, Mgrs:null}, WaypointSource:0, WaypointObjective:0, OrderNum:1, Height:120 }
Steps:
| Step | Consumer Action | Expected System Response |
|---|---|---|
| 1 | request as above | 201; body.GeoPoint.Lat == 50.45; body.GeoPoint.Lon == 30.52; body.GeoPoint.Mgrs == null (NO auto-conversion today) |
Expected outcome: results_report.md AC-4 row 4.3. Max execution time: 2s.
FT-P-15: Waypoint update is full overwrite
Summary: Verifies PUT /missions/{id}/waypoints/{wpId} overwrites every field even though the DTO looks "partial" (non-nullable enums/numerics).
Traces to: AC-4.4
Category: Waypoint CRUD
Preconditions:
- One waypoint exists with
Height=120,OrderNum=1,GeoPoint=(Lat:50.45, ...)
Input data: PUT /missions/{id}/waypoints/{wpId} { GeoPoint:null, WaypointSource:1, WaypointObjective:1, OrderNum:2, Height:0 }
Steps:
| Step | Consumer Action | Expected System Response |
|---|---|---|
| 1 | request as above | 200; body.Height == 0 (overwritten from 120); body.OrderNum == 2; body.GeoPoint == null |
Expected outcome: results_report.md AC-4 row 4.4. Max execution time: 2s.
FT-P-16: Health is 200 anonymous
Summary: Verifies GET /health requires no auth.
Traces to: AC-7.1, AC-7.2
Category: Health probe
Preconditions: any
Input data: GET /health (no Authorization header)
Steps:
| Step | Consumer Action | Expected System Response |
|---|---|---|
| 1 | GET /health with no Authorization header |
200; body == { "status": "healthy" } exactly (case-sensitive key) |
Expected outcome: results_report.md AC-7 row 7.1. Max execution time: 2s.
FT-P-17: Health is 200 with Postgres stopped (process-liveness only)
Summary: Verifies the health probe does NOT ping the DB. Traces to: AC-7.2, AC-7.3 Category: Health probe
Preconditions: missions running; postgres-test docker compose stop postgres-test
Input data: GET /health
Steps:
| Step | Consumer Action | Expected System Response |
|---|---|---|
| 1 | GET /health with PG stopped |
200; body == { "status": "healthy" } |
Expected outcome: results_report.md AC-7 row 7.2. Max execution time: 5s (allow PG-stop time).
FT-P-18: Waypoint cascade delete is scoped to one waypoint
Summary: Verifies DELETE /missions/{mid}/waypoints/{wpId} deletes only the chain rooted at wpId, leaves sibling waypoint chains intact.
Traces to: AC-4.5
Category: Waypoint Cascade Delete (F4)
Preconditions:
fixture_cascade_F4(target waypointwp1with chain; sibling waypointwp2with chain)
Input data: DELETE /missions/{mid}/waypoints/{wp1}
Steps:
| Step | Consumer Action | Expected System Response |
|---|---|---|
| 1 | request as above | 204 |
| 2 | Side-channel SELECT COUNT(*) FROM detection WHERE annotation_id IN (wp1 chain) |
count == 0 |
| 3 | Side-channel SELECT COUNT(*) FROM annotations WHERE id IN (wp1 chain) |
count == 0 |
| 4 | Side-channel SELECT COUNT(*) FROM media WHERE id IN (wp1 chain) |
count == 0 |
| 5 | Side-channel SELECT COUNT(*) FROM waypoints WHERE id={wp1} |
count == 0 |
| 6 | Side-channel SELECT COUNT(*) FROM waypoints WHERE id={wp2} |
count == 1 (sibling intact) |
| 7 | Side-channel counts on media, annotations, detection for wp2 chain |
all > 0 (sibling intact) |
| 8 | Compare against cascade_F4_walk.json |
json_diff matches |
Expected outcome: results_report.md AC-4 row 4.5. Max execution time: 10s.
Negative Scenarios
FT-N-01: Vehicle name filter is case-sensitive
Summary: Verifies name=br does NOT match BR-01 (case sensitivity).
Traces to: AC-1.6
Category: Vehicle CRUD (negative)
Preconditions:
seed_3_vehicles_2_default(only containsBR-*names — nobr-*)
Input data: GET /vehicles?name=br
Steps:
| Step | Consumer Action | Expected System Response |
|---|---|---|
| 1 | request as above | 200; body.length == 0 |
Expected outcome: results_report.md AC-1 row 1.7. Max execution time: 2s.
FT-N-02: GET vehicle 404
Summary: Verifies GET /vehicles/{random} returns 404 with the documented envelope.
Traces to: AC-1.7, AC-8.2
Category: Vehicle CRUD (negative)
Preconditions: any
Input data: GET /vehicles/{random uuid}, JWT FL
Steps:
| Step | Consumer Action | Expected System Response |
|---|---|---|
| 1 | request as above | 404; body parses to JSON object with EXACTLY keys statusCode, message (camelCase by accidental match) |
Expected outcome: results_report.md AC-1 row 1.8. Max execution time: 2s.
FT-N-03: Delete vehicle in use returns 409
Summary: Verifies DELETE /vehicles/{id} returns 409 when at least one mission references the vehicle.
Traces to: AC-1.8, AC-8.5
Category: Vehicle CRUD (negative)
Preconditions:
- Vehicle row exists, ≥1 mission row references it
Input data: DELETE /vehicles/{id}
Steps:
| Step | Consumer Action | Expected System Response |
|---|---|---|
| 1 | DELETE /vehicles/{id} |
409; envelope { statusCode:409, message:<non-empty> } |
| 2 | Side-channel SELECT COUNT(*) FROM vehicles WHERE id={id} |
count == 1 (row NOT deleted) |
Expected outcome: results_report.md AC-1 row 1.9. Max execution time: 2s.
FT-N-04: Create mission with non-existent VehicleId returns 400 (today)
Summary: Verifies the existing ArgumentException → 400 divergence (spec wants 404).
Traces to: AC-2.2 (carry-forward divergence), AC-8.5
Category: Mission CRUD (negative)
Preconditions:
seed_empty(no vehicles exist)
Input data: POST /missions { Name:"x", VehicleId:<random uuid>, CreatedDate:null }
Steps:
| Step | Consumer Action | Expected System Response |
|---|---|---|
| 1 | request as above | 400; envelope { statusCode:400, message:<non-empty> } |
| 2 | Side-channel SELECT COUNT(*) FROM missions |
count == 0 (no row written) |
Expected outcome: results_report.md AC-2 row 2.2. Note: this test will FAIL once the spec divergence is closed (status will become 404). When that work lands, update the expected status here. Max execution time: 2s.
FT-N-05: GET mission 404
Summary: Verifies GET /missions/{random} returns 404.
Traces to: AC-2.4, AC-8.2
Category: Mission CRUD (negative)
Preconditions: any
Input data: GET /missions/{random uuid}, JWT FL
Steps:
| Step | Consumer Action | Expected System Response |
|---|---|---|
| 1 | request as above | 404; envelope { statusCode:404, message:<non-empty> } |
Expected outcome: results_report.md AC-2 row 2.6. Max execution time: 2s.
FT-N-06: Cascade delete short-circuits on missing mission (404 before any DELETE)
Summary: Verifies that mission existence is checked BEFORE any dependency-table DELETE runs (AC-3.2). Traces to: AC-3.2 Category: Cascade delete F3 (negative)
Preconditions:
fixture_cascade_F3applied (seeded chain rooted atmid); test targets a DIFFERENT randommid'not present in the DB
Input data: DELETE /missions/{mid'} (random uuid)
Steps:
| Step | Consumer Action | Expected System Response |
|---|---|---|
| 1 | Enable Postgres log_statement=all (test orchestrator sets this on postgres-test startup) |
|
| 2 | DELETE /missions/{mid'} |
404 |
| 3 | Side-channel SELECT COUNT(*) FROM map_objects |
count unchanged from precondition |
| 4 | Side-channel inspect pg_stat_statements (or scrape pg_log lines logged after the request timestamp) |
NO DELETE FROM map_objects / ... waypoints / ... media / ... annotations / ... detection issued by the request connection |
Expected outcome: results_report.md AC-3 row 3.2. Max execution time: 5s.
FT-N-07: Waypoint operation against missing mission returns 404
Summary: Verifies parent-mission existence check on every waypoint endpoint. Traces to: AC-4.2 Category: Waypoint CRUD (negative)
Preconditions: any
Input data: GET /missions/{random uuid}/waypoints, JWT FL
Steps:
| Step | Consumer Action | Expected System Response |
|---|---|---|
| 1 | request as above | 404; envelope { statusCode:404, message:<non-empty> } |
Expected outcome: results_report.md AC-4 row 4.1. Max execution time: 2s.
FT-N-08: Generic 500 returns redacted body, logs stack
Summary: Verifies Exception fallthrough — body is the generic message, full stack is in the log.
Traces to: AC-8.6, AC-10.3
Category: Wire shape (negative)
Preconditions:
- Inject a divide-by-zero or similar throw in any handler via a test-only middleware OR force the condition (e.g., AC-2.9 TOCTOU triggers
Npgsql.PostgresException→ 500). For deterministic execution, drop thevehiclestable mid-test then callGET /vehicles/{any}.
Input data: as above
Steps:
| Step | Consumer Action | Expected System Response |
|---|---|---|
| 1 | Side-channel DROP TABLE vehicles CASCADE |
|
| 2 | GET /vehicles/{any uuid}, JWT FL |
500; body == { "statusCode":500, "message":"Internal server error" } exactly |
| 3 | docker logs missions | grep "Unhandled exception" |
At least one matching log line emitted within ≤ 2s of the request, containing the exception type name |
Expected outcome: results_report.md AC-8 row 8.7 + AC-10 row 10.1. Max execution time: 5s.
Notes
- Tests that depend on the DB side-channel reading
pg_stat_statementsor query logs (FT-N-06) require a one-time test bootstrap to enable those features onpostgres-test. That bootstrap is part of the docker compose for the test environment (environment.md§ Docker Environment). - Every test enforces
Max execution timevia xUnit[Fact(Timeout = N*1000)]. Default suite timeout (CI gate) is 15 minutes (perenvironment.md).