mirror of
https://github.com/azaion/missions.git
synced 2026-06-21 16:51:08 +00:00
78dea8ebab
ci/woodpecker/push/build-arm Pipeline was successful
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.
613 lines
22 KiB
Markdown
613 lines
22 KiB
Markdown
# 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 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 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`).
|