Files
missions/_docs/02_document/tests/blackbox-tests.md
T
Oleksandr Bezdieniezhnykh 78dea8ebab
ci/woodpecker/push/build-arm Pipeline was successful
chore: update configuration and Docker setup for JWT and test results
Enhanced the .gitignore to exclude test results and updated the Dockerfile to include a new entrypoint script for improved container initialization. Refactored JWT configuration to support additional parameters for automatic refresh intervals, ensuring better control over token management. Updated the ConfigurationResolver to enforce required environment variables without hardcoded fallbacks, enhancing security and flexibility.
2026-05-15 03:23:23 +03:00

22 KiB
Raw Blame History

Blackbox Tests

Status: produced by autodev /test-spec Phase 2 (2026-05-14). Naming: post-rename target. Pre-rename code path runs the same test against /aircrafts//flights; tests will be RED until B5B8 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 row Id=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 row P1); add a non-default row P2

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), ordered by Name ASC

Summary: Verifies GET /vehicles returns a non-paginated array — distinguishing it from GET /missions — and that results are ordered alphabetically by Name ASC (per AircraftService.GetVehicles OrderBy(a => a.Name)). Traces to: AC-1.5 Category: Vehicle CRUD

Preconditions:

  • seed_3_vehicles_2_default containing BR-01, BR-02, MQ-9 (any insert order)

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; [v.Name for v in body] == ["BR-01", "BR-02", "MQ-9"] (alphabetical ASC)

Expected outcome: results_report.md AC-1 row 1.5. Max execution time: 2s.


FT-P-05: Vehicle filter by name + isDefault (case-INSENSITIVE name)

Summary: Verifies query-string filter — case-INSENSITIVE substring on name (LinqToDB renders LOWER(name) LIKE %lower(input)%), exact on isDefault. Traces to: AC-1.6 Category: Vehicle CRUD

Preconditions:

  • seed_3_vehicles_2_default containing BR-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"
2 GET /vehicles?name=br&isDefault=true (lowercase) 200; body.length == 1; body[0].Name == "BR-01" (case-INSENSITIVE match)

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 as VehicleId)

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, ordered by CreatedDate DESC

Summary: Verifies GET /missions returns PaginatedResponse<Mission> with default page size 20, ordered by CreatedDate DESC (newest first per FlightService.GetMissions OrderByDescending(f => f.CreatedDate)). Traces to: AC-2.3, AC-8.7 Category: Mission CRUD

Preconditions:

  • seed_25_missions with deterministic CreatedDate values spanning January-February 2026

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; for every i in [0..18]: Items[i].CreatedDate >= Items[i+1].CreatedDate (DESC ordering)
2 GET /missions?name=re (lowercase) against missions containing "Recon-*" names 200; body.TotalCount > 0 — case-INSENSITIVE name filter matches Mission Name "Recon-*"

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_F3 applied (seeded mid mission 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_unordered under 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 waypoint wp1 with chain; sibling waypoint wp2 with 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 returns empty when no row matches case-insensitively

Summary: Verifies that ?name= returns an empty body when no row's Name contains the substring (case-insensitive). This is the "no-match" half of AC-1.6 — distinct from FT-P-05 which asserts that lowercase input DOES match BR-01. Traces to: AC-1.6 Category: Vehicle CRUD (negative)

Preconditions:

  • seed_3_vehicles_2_default (BR-01, BR-02, MQ-9)

Input data: GET /vehicles?name=ZZ (substring ZZ is absent from every name)

Steps:

Step Consumer Action Expected System Response
1 GET /vehicles?name=ZZ 200; body.length == 0
2 GET /vehicles?name=zz (lowercase) 200; body.length == 0 (still no match)

Expected outcome: results_report.md AC-1 row 1.7. Note (drift, 2026-05-14): this test was previously titled "Vehicle name filter is case-sensitive" and asserted ?name=br → length 0. That assertion was WRONG against the actual code (a.Name.ToLower().Contains(query.Name.ToLower()) — case-insensitive). The test is rewritten to assert the genuine no-match case. 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_F3 applied (seeded chain rooted at mid); test targets a DIFFERENT random mid' 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 the vehicles table mid-test then call GET /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_statements or query logs (FT-N-06) require a one-time test bootstrap to enable those features on postgres-test. That bootstrap is part of the docker compose for the test environment (environment.md § Docker Environment).
  • Every test enforces Max execution time via xUnit [Fact(Timeout = N*1000)]. Default suite timeout (CI gate) is 15 minutes (per environment.md).