|
|
|
@@ -0,0 +1,207 @@
|
|
|
|
|
# Expected Results — Azaion.Missions
|
|
|
|
|
|
|
|
|
|
> **Status**: derived-from-spec (autodev `/test-spec` Step 3 prerequisite, 2026-05-14).
|
|
|
|
|
> **Source**: every row below is grounded in `_docs/00_problem/acceptance_criteria.md` (AC-1…AC-10), `_docs/00_problem/input_data/data_parameters.md` (HTTP shapes), and `_docs/00_problem/restrictions.md`.
|
|
|
|
|
> **Naming convention**: rows describe the **post-rename target** (`/vehicles`, `/missions`, `Vehicle`, `Mission`, `VehicleType { Plane, Copter, UGV, GuidedMissile }`). Where today's pre-rename code diverges, the row carries a `today:` note. The B-tickets `AZ-544 / AZ-545 / AZ-546 / AZ-547 / AZ-548 / AZ-549 / AZ-550 / AZ-551` are the planned converger; the leftover index is `_docs/_process_leftovers/2026-05-14_rename-flights-to-missions.md`.
|
|
|
|
|
> **Input shape**: this is an HTTP API service, not a data-processing pipeline. "Input" rows describe HTTP requests (method + path + JWT claim + body / query); reference files only appear when a scenario needs a fixture row (e.g., a pre-existing mission row in the DB).
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## Result Format Legend
|
|
|
|
|
|
|
|
|
|
| Result Type | When to Use | Example |
|
|
|
|
|
|-------------|-------------|---------|
|
|
|
|
|
| Exact value | Output must match precisely | `status_code: 200`, `body.IsDefault: true` |
|
|
|
|
|
| Tolerance range | Numeric output with acceptable variance | `latency: ≤ 50ms` |
|
|
|
|
|
| Threshold | Output must exceed or stay below a limit | `latency < 500ms` |
|
|
|
|
|
| Pattern match | Output must match a string/regex pattern | `body.message contains "Internal server error"` |
|
|
|
|
|
| File reference | Complex output compared against a reference file | `match expected_results/cascade_F3_walk.json` |
|
|
|
|
|
| Schema match | Output structure must conform to a schema | `body matches PaginatedResponse<Mission>` |
|
|
|
|
|
| Set/count | Output must contain specific items or counts | `body.length == 0`, `body.Items.length ≤ 20` |
|
|
|
|
|
| DB state | Side effect on persisted rows must hold post-call | `db.vehicles WHERE is_default=true count == 1` |
|
|
|
|
|
| Log assertion | Side effect on logger must hold post-call | `logger emits "Unhandled exception" with stack trace` |
|
|
|
|
|
|
|
|
|
|
## Comparison Methods
|
|
|
|
|
|
|
|
|
|
| Method | Description | Tolerance Syntax |
|
|
|
|
|
|--------|-------------|-----------------|
|
|
|
|
|
| `exact` | Actual == Expected | N/A |
|
|
|
|
|
| `numeric_tolerance` | abs(actual - expected) ≤ tolerance | `± <value>` |
|
|
|
|
|
| `threshold_min` | actual ≥ threshold | `≥ <value>` |
|
|
|
|
|
| `threshold_max` | actual ≤ threshold | `≤ <value>` |
|
|
|
|
|
| `regex` | actual matches regex pattern | regex string |
|
|
|
|
|
| `substring` | actual contains substring | substring |
|
|
|
|
|
| `json_diff` | structural comparison against reference JSON | diff tolerance per field |
|
|
|
|
|
| `set_contains` | actual output set contains expected items | subset notation |
|
|
|
|
|
| `set_equals` | actual output set equals expected exactly | set equality |
|
|
|
|
|
| `db_query` | result of a `SELECT` against a controlled test DB equals expected | exact / count |
|
|
|
|
|
| `file_reference` | compare against reference file in `expected_results/` | file path |
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## Input → Expected Result Mapping
|
|
|
|
|
|
|
|
|
|
### AC-1 — Vehicle CRUD (`/vehicles`)
|
|
|
|
|
|
|
|
|
|
| # | Input | Input Description | Expected Result | Comparison | Tolerance | Reference File |
|
|
|
|
|
|---|-------|-------------------|-----------------|------------|-----------|---------------|
|
|
|
|
|
| 1.1 | `POST /vehicles` body `{ Type:0, Model:"Bayraktar", Name:"BR-01", FuelType:1, BatteryCapacity:0, EngineConsumption:5, EngineConsumptionIdle:1, IsDefault:false }`, JWT `permissions=FL` | Create non-default Plane | `status_code: 201`; body `Vehicle` with `Id` (UUID), `Type:0`, `Name:"BR-01"`, `IsDefault:false` (PascalCase per AC-8.1); `db.vehicles.count == prev+1` | exact (status, body fields), db_query (count) | N/A | N/A |
|
|
|
|
|
| 1.2 | Same as 1.1 but `IsDefault:true` against a DB containing one prior `vehicles` row with `is_default=true` | Create default — must demote prior default first (AC-1.2) | `status_code: 201`; new row has `IsDefault:true`; the prior default row now has `is_default=false`; `SELECT COUNT(*) FROM vehicles WHERE is_default=true == 1` | exact (status), db_query (count, prior row state) | N/A | N/A |
|
|
|
|
|
| 1.3 | Same as 1.2 but inject a concurrent `INSERT vehicles (..., is_default=true)` between the service's `UPDATE … SET is_default=FALSE` and its `INSERT` | TOCTOU race window (AC-1.4) | `status_code: 201`; `SELECT COUNT(*) FROM vehicles WHERE is_default=true >= 2` is observable in at least one race interleaving | db_query (count) | N/A | N/A |
|
|
|
|
|
| 1.4 | `POST /vehicles/{id}/setDefault` body `{ IsDefault:true }` against `id` of a non-default row | Promote existing vehicle to default (AC-1.2) | `status_code: 200`; body `Vehicle` with `IsDefault:true`; previous default has `is_default=false`; default count == 1 | exact (status, body), db_query (count) | N/A | N/A |
|
|
|
|
|
| 1.5 | `GET /vehicles` no query, JWT `permissions=FL`, DB has 3 rows | List all vehicles (AC-1.5) | `status_code: 200`; body is JSON `array` (NOT `PaginatedResponse`); `body.length == 3`; PascalCase property names | exact (status, length), schema (array, not paginated), exact (case) | N/A | N/A |
|
|
|
|
|
| 1.6 | `GET /vehicles?name=BR&isDefault=true` against DB with `["BR-01" default, "BR-02" non-default, "MQ-9" default]` | Filter by name substring + is_default (AC-1.6) | `status_code: 200`; `body.length == 1`; `body[0].Name == "BR-01"` | exact (status, length, value) | N/A | N/A |
|
|
|
|
|
| 1.7 | `GET /vehicles?name=br` against DB with `"BR-01"` only | Case-sensitive name filter (AC-1.6) | `status_code: 200`; `body.length == 0` | exact (status, length) | N/A | N/A |
|
|
|
|
|
| 1.8 | `GET /vehicles/{id}` with `id` not in DB | Vehicle not found (AC-1.7) | `status_code: 404`; body matches `{ statusCode:404, message: <non-empty string> }` (camelCase by accidental match per AC-8.2) | exact (status), schema (envelope shape), exact (case) | N/A | N/A |
|
|
|
|
|
| 1.9 | `DELETE /vehicles/{id}` against vehicle referenced by ≥1 mission | Vehicle in use → 409 (AC-1.8) | `status_code: 409`; body envelope `{ statusCode:409, message:<non-empty> }`; `db.vehicles WHERE id={id}` still exists (count==1) | exact (status, envelope shape), db_query | N/A | N/A |
|
|
|
|
|
| 1.10 | `DELETE /vehicles/{id}` against vehicle referenced by 0 missions | Vehicle deletable | `status_code: 204`; `db.vehicles WHERE id={id}` count == 0 | exact (status), db_query | N/A | N/A |
|
|
|
|
|
| 1.11 | `GET /vehicles` without `Authorization` header | Unauthenticated (AC-1.9, AC-5.4) | `status_code: 401` | exact (status) | N/A | N/A |
|
|
|
|
|
| 1.12 | `GET /vehicles` with JWT having `permissions="OTHER"` | Wrong permission (AC-1.9, AC-5.8) | `status_code: 403` | exact (status) | N/A | N/A |
|
|
|
|
|
|
|
|
|
|
### AC-2 — Mission create / read / update (`/missions`)
|
|
|
|
|
|
|
|
|
|
| # | Input | Input Description | Expected Result | Comparison | Tolerance | Reference File |
|
|
|
|
|
|---|-------|-------------------|-----------------|------------|-----------|---------------|
|
|
|
|
|
| 2.1 | `POST /missions` body `{ Name:"Recon-01", VehicleId:<existing>, CreatedDate:null }`, JWT `FL` | Create mission with default created date (AC-2.1) | `status_code: 201`; body `Mission` with server-assigned `Id`, `CreatedDate` set to a UTC timestamp within `now ± 5s`; `Name == "Recon-01"`; `VehicleId` echoes input | exact (status, fields), numeric_tolerance (CreatedDate ± 5s) | ±5s | N/A |
|
|
|
|
|
| 2.2 | `POST /missions` body `{ Name:"Recon-02", VehicleId:<random uuid>, CreatedDate:null }` | Vehicle not found (AC-2.2) | `status_code: 400` (today via `ArgumentException`; spec wants 404 — divergence carry-forward) | exact (status) | N/A | N/A |
|
|
|
|
|
| 2.3 | `GET /missions` no query, DB has 25 missions | Default pagination (AC-2.3) | `status_code: 200`; body matches `PaginatedResponse<Mission>` schema; `body.Page == 1`; `body.PageSize == 20`; `body.TotalCount == 25`; `body.Items.length == 20` | schema, exact (counts) | N/A | N/A |
|
|
|
|
|
| 2.4 | `GET /missions?page=2&pageSize=20` against same 25-row DB | Second page | `body.Page == 2`; `body.PageSize == 20`; `body.Items.length == 5` | exact (counts) | N/A | N/A |
|
|
|
|
|
| 2.5 | `GET /missions?fromDate=2026-01-01T00:00:00Z&toDate=2026-01-31T23:59:59Z` against DB with 3 January missions and 2 February missions | Date range filter | `body.TotalCount == 3`; `body.Items.length == 3` | exact (counts) | N/A | N/A |
|
|
|
|
|
| 2.6 | `GET /missions/{id}` with `id` not in DB | Not found (AC-2.4) | `status_code: 404`; envelope `{ statusCode:404, message:<non-empty> }` | exact (status, envelope) | N/A | N/A |
|
|
|
|
|
| 2.7 | `PUT /missions/{id}` body `{ Name:"Recon-01-renamed", VehicleId:null }` against existing mission | Partial update — only Name (AC-2.5) | `status_code: 200`; `body.Name == "Recon-01-renamed"`; `body.VehicleId == <previous>` (preserved) | exact (status, fields) | N/A | N/A |
|
|
|
|
|
| 2.8 | `GET /missions/{id}` against mission with 2 waypoints | LinqToDB does NOT eager-load (AC-2.6) | `body.Vehicle == null`; `body.Waypoints` is `null` or `[]` (depending on JSON null serialization) | exact (null/empty) | N/A | N/A |
|
|
|
|
|
| 2.9 | `POST /missions` simulating TOCTOU: vehicle exists at check time, deleted before insert | TOCTOU FK race (AC-2.8) | `status_code: 500`; logger emits `LogError(ex, "Unhandled exception")` with `Npgsql.PostgresException` in the stack | exact (status), log_assertion (substring) | N/A | N/A |
|
|
|
|
|
| 2.10 | `GET /missions` without `Authorization` | Unauthenticated (AC-2.7, AC-5.4) | `status_code: 401` | exact (status) | N/A | N/A |
|
|
|
|
|
|
|
|
|
|
### AC-3 — Mission cascade delete (`DELETE /missions/{id}`) — most critical
|
|
|
|
|
|
|
|
|
|
Test data fixtures live in `expected_results/fixture_cascade_F3.sql` (seed script that creates one mission with the full dependency chain across `map_objects`, `waypoints`, `media`, `annotations`, `detection`).
|
|
|
|
|
|
|
|
|
|
| # | Input | Input Description | Expected Result | Comparison | Tolerance | Reference File |
|
|
|
|
|
|---|-------|-------------------|-----------------|------------|-----------|---------------|
|
|
|
|
|
| 3.1 | Apply `fixture_cascade_F3.sql`, then `DELETE /missions/{seeded_id}`, JWT `FL` | Full cascade walk (AC-3.1) | `status_code: 204`; rows in `map_objects`, `waypoints`, `media`, `annotations`, `detection`, `missions` matching the seeded chain are all deleted; cascade order is `map_objects → detection → annotations → media → waypoints → missions` (validated via per-statement instrumentation in tests) | exact (status), db_query (each table count == 0 for seeded ids), file_reference (cascade order log) | N/A | `expected_results/cascade_F3_walk.json` |
|
|
|
|
|
| 3.2 | `DELETE /missions/{id}` with `id` not in DB | Mission not found before any cascade runs (AC-3.2) | `status_code: 404`; NO `DELETE` statement issued against `map_objects`, `waypoints`, etc. (validated via SQL query log instrumentation) | exact (status), log_assertion (no DELETE on dependency tables) | N/A | N/A |
|
|
|
|
|
| 3.3 | Apply `fixture_cascade_F3.sql`, drop `media` table from test DB, then `DELETE /missions/{id}` | Cascade fails mid-walk on missing dep table (AC-3.4) | `status_code: 500`; logger emits `Unhandled exception` with `relation "media" does not exist`; `db.missions WHERE id={id}` still exists (cascade NOT transaction-wrapped per AC-3.3, partial deletes remain) | exact (status), log_assertion (regex), db_query (target row remains) | N/A | N/A |
|
|
|
|
|
| 3.4 | Apply `fixture_cascade_F3.sql`, then `DELETE /missions/{id}` while a parallel `INSERT INTO map_objects … mission_id={id}` runs immediately after the service's `SELECT FROM map_objects` step | Orphan-row race (AC-3.7) | `status_code: 204`; `SELECT COUNT(*) FROM map_objects WHERE mission_id={id} >= 1` is observable in at least one race interleaving | db_query | N/A | N/A |
|
|
|
|
|
| 3.5 | `DELETE /missions/{id}` with `id` of a 1-waypoint mission against local PostgreSQL on the same device, no map_objects/media/annotations/detection rows | Latency target (AC-3.6) | end-to-end latency `≤ 50ms` (P50 across 100 invocations) | threshold_max | ≤ 50ms (P50) | N/A |
|
|
|
|
|
| 3.6 | After `B7+B9` migration ran, `SELECT to_regclass('orthophotos')` and `SELECT to_regclass('gps_corrections')` | Tables removed (AC-3.5) | both queries return `NULL` | exact | N/A | N/A |
|
|
|
|
|
|
|
|
|
|
### AC-4 — Waypoint CRUD (`/missions/{id}/waypoints`)
|
|
|
|
|
|
|
|
|
|
| # | Input | Input Description | Expected Result | Comparison | Tolerance | Reference File |
|
|
|
|
|
|---|-------|-------------------|-----------------|------------|-----------|---------------|
|
|
|
|
|
| 4.1 | `GET /missions/{nonexistent}/waypoints`, JWT `FL` | Parent mission missing (AC-4.2) | `status_code: 404`; envelope `{ statusCode:404, message:<non-empty> }` | exact (status, envelope) | N/A | N/A |
|
|
|
|
|
| 4.2 | `GET /missions/{id}/waypoints` against mission with 5 waypoints having `OrderNum [3, 1, 2, 5, 4]` | Unpaginated, ordered (AC-4.3) | `status_code: 200`; body is JSON array; `body.length == 5`; `[w.OrderNum for w in body] == [1, 2, 3, 4, 5]` | exact (status, length, ordering) | N/A | N/A |
|
|
|
|
|
| 4.3 | `POST /missions/{id}/waypoints` body `{ GeoPoint:{Lat:50.45, Lon:30.52, Mgrs:null}, WaypointSource:0, WaypointObjective:0, OrderNum:1, Height:120 }` | Create waypoint with lat/lon | `status_code: 201`; body `Waypoint` with server-assigned `Id`; `body.GeoPoint.Lat == 50.45`; `body.Mgrs == null` (no auto-conversion today, divergent from spec — see data_parameters.md § 2.3) | exact (status, fields) | N/A | N/A |
|
|
|
|
|
| 4.4 | `PUT /missions/{id}/waypoints/{wpId}` body `{ GeoPoint:null, WaypointSource:1, WaypointObjective:1, OrderNum:2, Height:0 }` against waypoint that previously had `Height=120` | Full overwrite (AC-4.4) — every field replaced including `Height: 120 → 0` | `status_code: 200`; `body.Height == 0` (overwritten); `body.OrderNum == 2`; `body.GeoPoint == null` | exact (status, every field) | N/A | N/A |
|
|
|
|
|
| 4.5 | Apply `fixture_cascade_F4.sql` (waypoint with media→annotations→detection chain), then `DELETE /missions/{mid}/waypoints/{wpId}` | Scoped cascade (AC-4.5) | `status_code: 204`; rows for that waypoint's `detection`, `annotations`, `media`, `waypoints` are all deleted; rows belonging to OTHER waypoints in the same mission are untouched | exact (status), db_query (per table) | N/A | `expected_results/cascade_F4_walk.json` |
|
|
|
|
|
| 4.6 | `DELETE` as in 4.5 with `media` table dropped | Same NO-transaction caveat as AC-3.3 (AC-4.6) | `status_code: 500`; partial deletes remain | exact (status), db_query | N/A | N/A |
|
|
|
|
|
| 4.7 | `GET /missions/{id}/waypoints` without `Authorization` | Unauthenticated (AC-4.7) | `status_code: 401` | exact (status) | N/A | N/A |
|
|
|
|
|
|
|
|
|
|
### AC-5 — JWT bearer validation
|
|
|
|
|
|
|
|
|
|
JWT fixtures use `JWT_SECRET=test-secret-32-chars-min!!!!!!!!!`, HS256, claims include `permissions=FL` unless noted.
|
|
|
|
|
|
|
|
|
|
| # | Input | Input Description | Expected Result | Comparison | Tolerance | Reference File |
|
|
|
|
|
|---|-------|-------------------|-----------------|------------|-----------|---------------|
|
|
|
|
|
| 5.1 | `GET /vehicles` without `Authorization` header | Missing token (AC-5.4) | `status_code: 401` | exact (status) | N/A | N/A |
|
|
|
|
|
| 5.2 | `GET /vehicles` with `Authorization: Bearer <token signed by different secret>` | Invalid signature (AC-5.5) | `status_code: 401` | exact (status) | N/A | N/A |
|
|
|
|
|
| 5.3 | `GET /vehicles` with token where `exp` is `now - 120s` (outside 1-min skew) | Expired (AC-5.6) | `status_code: 401` | exact (status) | N/A | N/A |
|
|
|
|
|
| 5.4 | `GET /vehicles` with token where `exp` is `now - 30s` (inside 1-min skew per AC-5.2) | Within skew | `status_code: 200` | exact (status) | N/A | N/A |
|
|
|
|
|
| 5.5 | `GET /vehicles` with valid HS256 signature + lifetime, `permissions` claim absent | Missing claim (AC-5.8) | `status_code: 403` | exact (status) | N/A | N/A |
|
|
|
|
|
| 5.6 | `GET /vehicles` with valid HS256 signature + lifetime, `permissions == "ADMIN"` | Wrong claim value | `status_code: 403` | exact (status) | N/A | N/A |
|
|
|
|
|
| 5.7 | `GET /vehicles` with no `iss` and no `aud` claim, otherwise valid | `ValidateIssuer/ValidateAudience = false` (AC-5.3) | `status_code: 200` | exact (status) | N/A | N/A |
|
|
|
|
|
| 5.8 | Restart service with rotated `JWT_SECRET`, replay a previously valid token | Cross-rotation invalidation (AC-5.7) | `status_code: 401` | exact (status) | N/A | N/A |
|
|
|
|
|
|
|
|
|
|
### AC-6 — Service startup + schema migration
|
|
|
|
|
|
|
|
|
|
Bootstrap fixtures use a Postgres container started fresh per scenario.
|
|
|
|
|
|
|
|
|
|
| # | Input | Input Description | Expected Result | Comparison | Tolerance | Reference File |
|
|
|
|
|
|---|-------|-------------------|-----------------|------------|-----------|---------------|
|
|
|
|
|
| 6.1 | Start service with `DATABASE_URL=postgresql://u:p@h:5432/d` (URL form) | URL conversion (AC-6.1) | service binds `:8080`; `GET /health` returns `200`; logger does NOT emit a connection error | log_assertion (no error), exact (health 200) | N/A | N/A |
|
|
|
|
|
| 6.2 | Start service with `DATABASE_URL=Host=h;Database=d;Username=u;Password=p` (raw form) | Raw connection string accepted (AC-6.1) | same as 6.1 | log_assertion, exact | N/A | N/A |
|
|
|
|
|
| 6.3 | Start service against an empty `azaion` database, inspect schema after startup | Migrator creates 4 owned tables + 3 indexes (AC-6.4) | `SELECT to_regclass(t)` returns non-NULL for each of `vehicles, missions, waypoints, map_objects`; index list contains `ix_missions_vehicle_id, ix_waypoints_mission_id, ix_map_objects_mission_id` | set_equals (table set), set_contains (index set) | N/A | N/A |
|
|
|
|
|
| 6.4 | Start service twice in a row against the same DB | Idempotency (AC-6.6) | second startup completes with same exit code as first; no `relation already exists` error in logs | exact (exit code), log_assertion (no error) | N/A | N/A |
|
|
|
|
|
| 6.5 | Pre-create `orthophotos` and `gps_corrections` tables, then start a post-B9 service | One-shot legacy drop (AC-6.5, AC-10.5) | both tables are absent after startup; `SELECT to_regclass('orthophotos')` and `SELECT to_regclass('gps_corrections')` both return NULL | exact | N/A | N/A |
|
|
|
|
|
| 6.6 | Start service with `DATABASE_URL` pointing at unreachable host | DB unreachable (AC-6.7) | process exits with non-zero exit code within `≤ 30s` | exact (non-zero), threshold_max (≤ 30s) | ≤ 30s | N/A |
|
|
|
|
|
| 6.7 | Start service against a postgres instance where the `azaion` database does NOT exist | DB missing (AC-6.8) | process exits with non-zero exit code; logger emits message containing Npgsql `3D000` | exact (non-zero), log_assertion (substring `3D000`) | N/A | N/A |
|
|
|
|
|
| 6.8 | Make any handler throw `InvalidOperationException`, observe response | `ErrorHandlingMiddleware` registered FIRST (AC-6.9) | response: `status_code: 409`; envelope is the camelCase `{ statusCode, message }`; logger captured stack | exact (status, envelope), log_assertion | N/A | N/A |
|
|
|
|
|
| 6.9 | Start service, run `curl localhost:8080` from inside container | Listens on port 8080 (AC-6.10) | TCP connect succeeds; `/health` returns `200` | exact | N/A | N/A |
|
|
|
|
|
|
|
|
|
|
### AC-7 — Health probe
|
|
|
|
|
|
|
|
|
|
| # | Input | Input Description | Expected Result | Comparison | Tolerance | Reference File |
|
|
|
|
|
|---|-------|-------------------|-----------------|------------|-----------|---------------|
|
|
|
|
|
| 7.1 | `GET /health` without `Authorization` (AC-7.1) | Anonymous health probe | `status_code: 200`; body equals `{ "status": "healthy" }` exactly (case-sensitive property names) | exact (status, body) | N/A | N/A |
|
|
|
|
|
| 7.2 | `GET /health` with PostgreSQL stopped | Probe is process-liveness only (AC-7.3) | `status_code: 200`; body equals `{ "status": "healthy" }` (no DB ping today) | exact | N/A | N/A |
|
|
|
|
|
| 7.3 | `GET /health` measured locally, 100 sequential calls | Latency target (AC-7.3) | P50 latency `≤ 10ms` | threshold_max | ≤ 10ms (P50) | N/A |
|
|
|
|
|
|
|
|
|
|
### AC-8 — Wire shape (HTTP contract)
|
|
|
|
|
|
|
|
|
|
| # | Input | Input Description | Expected Result | Comparison | Tolerance | Reference File |
|
|
|
|
|
|---|-------|-------------------|-----------------|------------|-----------|---------------|
|
|
|
|
|
| 8.1 | `GET /vehicles/{id}` with valid id, JWT `FL` | Entity body case (AC-8.1) | response body has top-level keys `Id, Type, Model, Name, FuelType, BatteryCapacity, EngineConsumption, EngineConsumptionIdle, IsDefault` (PascalCase, NO `id`/`type`/etc.) | set_equals (key set, case-sensitive) | N/A | N/A |
|
|
|
|
|
| 8.2 | `GET /missions/{nonexistent}`, JWT `FL` | Error envelope case (AC-8.2) | response body has exactly the keys `statusCode, message` (lowercase `s`/`m`) | set_equals (key set, case-sensitive) | N/A | N/A |
|
|
|
|
|
| 8.3 | Same as 8.2 | Error envelope MUST NOT include spec's `errors` field today (AC-8.3) | response body MUST NOT contain key `errors` | exact (key absence) | N/A | N/A |
|
|
|
|
|
| 8.4 | `GET /missions/{nonexistent}` | KeyNotFoundException → 404 (AC-8.5) | `status_code: 404` | exact (status) | N/A | N/A |
|
|
|
|
|
| 8.5 | `POST /missions` with `VehicleId = <random uuid>` (existence check fails) | ArgumentException → 400 (AC-8.5) | `status_code: 400` | exact (status) | N/A | N/A |
|
|
|
|
|
| 8.6 | `DELETE /vehicles/{id}` against vehicle in use | InvalidOperationException → 409 (AC-8.5) | `status_code: 409` | exact (status) | N/A | N/A |
|
|
|
|
|
| 8.7 | Force a generic `Exception` (e.g., divide-by-zero in a handler) | Fallthrough → 500 + body redaction (AC-8.6) | `status_code: 500`; body equals `{ "statusCode":500, "message":"Internal server error" }` exactly; logger captures the stack via `LogError` | exact (status, body), log_assertion | N/A | N/A |
|
|
|
|
|
| 8.8 | `GET /missions?page=1&pageSize=10` against 5-mission DB | `PaginatedResponse<Mission>` PascalCase (AC-8.7) | response body has top-level keys `Items, TotalCount, Page, PageSize` (PascalCase) | set_equals (key set) | N/A | N/A |
|
|
|
|
|
|
|
|
|
|
### AC-9 — Authorization (cross-cutting)
|
|
|
|
|
|
|
|
|
|
| # | Input | Input Description | Expected Result | Comparison | Tolerance | Reference File |
|
|
|
|
|
|---|-------|-------------------|-----------------|------------|-----------|---------------|
|
|
|
|
|
| 9.1 | Each protected endpoint (`/vehicles`, `/missions`, `/missions/{id}/waypoints`) called with token having `permissions == "FL"` | Policy "FL" satisfies (AC-9.1, AC-9.4) | each call: `status_code` ∈ `{200, 201, 204}` (no 401/403) | exact (status set) | N/A | N/A |
|
|
|
|
|
| 9.2 | Any protected endpoint called with `permissions == "fl"` (lowercase) or `"FLight"` | Hardcoded string mismatch (AC-9.2) | `status_code: 403` | exact (status) | N/A | N/A |
|
|
|
|
|
| 9.3 | `GET /health` with NO `Authorization` header | Health is exempt (AC-9.4 contrast) | `status_code: 200` | exact (status) | N/A | N/A |
|
|
|
|
|
|
|
|
|
|
### AC-10 — Operational invariants (verifiable observables)
|
|
|
|
|
|
|
|
|
|
| # | Input | Input Description | Expected Result | Comparison | Tolerance | Reference File |
|
|
|
|
|
|---|-------|-------------------|-----------------|------------|-----------|---------------|
|
|
|
|
|
| 10.1 | Force any handler to throw `Exception` | Stack trace logged, NOT returned (AC-10.3, AC-8.6) | logger output contains the type name of the thrown exception AND the file path of the throw site; HTTP body equals `{ "statusCode":500, "message":"Internal server error" }` (no stack in body) | log_assertion (substring), exact (body) | N/A | N/A |
|
|
|
|
|
| 10.2 | Apply `fixture_cascade_F3.sql`, then `DELETE /missions/{id}` with `media` table dropped | Cascade NOT transaction-wrapped (AC-3.3, AC-10.[8 in restrictions]) | `map_objects` rows for `mission_id` are deleted (work done before failure remains); only post-failure work is missing — `media`/`annotations`/`detection`/`waypoints`/`missions` rows still present | db_query (per table) | N/A | N/A |
|
|
|
|
|
| 10.3 | After `B9` migrator runs once on a device with legacy `orthophotos` and `gps_corrections` rows | One-shot destructive step (AC-10.5, AC-6.5) | both tables are absent post-startup; second startup leaves DB unchanged (idempotent because `IF EXISTS`) | exact, db_query | N/A | N/A |
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## Expected Result Reference Files
|
|
|
|
|
|
|
|
|
|
The reference files below are required for cascade-walk and fixture-seeding scenarios. They live alongside this report under `_docs/00_problem/input_data/expected_results/`.
|
|
|
|
|
|
|
|
|
|
| File | Purpose | Used by |
|
|
|
|
|
|------|---------|---------|
|
|
|
|
|
| `cascade_F3_walk.json` | Cascade order + per-table delete-count expectations for AC-3.1 | 3.1 |
|
|
|
|
|
| `cascade_F4_walk.json` | Same for the waypoint-scoped F4 cascade | 4.5 |
|
|
|
|
|
| `fixture_cascade_F3.sql` | Seed script for AC-3 cascade scenarios (creates 1 mission with full chain across `map_objects`, `waypoints`, `media`, `annotations`, `detection`) | 3.1, 3.3, 3.4, 10.2 |
|
|
|
|
|
| `fixture_cascade_F4.sql` | Seed script for AC-4 cascade scenarios (single waypoint with media chain) | 4.5, 4.6 |
|
|
|
|
|
|
|
|
|
|
These reference files are **not yet produced** in this turn — they are listed here so the test-spec skill (Phase 1) can confirm coverage. Step 5 (Decompose Tests) and Step 6 (Implement Tests) will materialise them as concrete fixtures.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## Coverage Summary
|
|
|
|
|
|
|
|
|
|
| AC Group | # Test Inputs | Quantifiable Pass/Fail | Notes |
|
|
|
|
|
|----------|---------------|------------------------|-------|
|
|
|
|
|
| AC-1 Vehicle CRUD | 12 | 100% | 1 row covers TOCTOU race (1.3) |
|
|
|
|
|
| AC-2 Mission CRUD | 10 | 100% | 1 row covers TOCTOU race (2.9) |
|
|
|
|
|
| AC-3 Cascade delete F3 | 6 | 100% | Fixture-seed scenarios + latency P50 |
|
|
|
|
|
| AC-4 Waypoint CRUD F4 | 7 | 100% | Includes full-overwrite vs partial divergence (4.4) |
|
|
|
|
|
| AC-5 JWT validation | 8 | 100% | Skew, rotation, missing/invalid claim |
|
|
|
|
|
| AC-6 Startup + migration | 9 | 100% | Idempotency, B9 legacy drop, bootstrap failure modes |
|
|
|
|
|
| AC-7 Health probe | 3 | 100% | Anonymous, no DB ping, latency P50 |
|
|
|
|
|
| AC-8 Wire shape | 8 | 100% | PascalCase + camelCase divergence locked in (today) |
|
|
|
|
|
| AC-9 Authorization | 3 | 100% | Hardcoded `"FL"` typo coverage |
|
|
|
|
|
| AC-10 Operational invariants | 3 | 100% | Subset that is API-observable; non-observable rows (RTO/RPO, hardware) tracked under restrictions |
|
|
|
|
|
|
|
|
|
|
**Total**: 69 input rows; every row has at least one quantifiable comparison method. Cascade walk + bootstrap rows depend on test fixtures listed above; those will be created by the test implementation step.
|
|
|
|
|
|
|
|
|
|
## Open questions (carry-forward)
|
|
|
|
|
|
|
|
|
|
1. AC-3.6 latency target `<50ms` is documented for "local PostgreSQL on the same device" — the test environment must mirror this (PostgreSQL container on the same host as the service container, no inter-host network) for the threshold to be meaningful. Decision deferred to test-spec Phase 2 environment design.
|
|
|
|
|
2. AC-1.3 / AC-2.9 / AC-3.4 race-window scenarios require a controllable concurrency primitive (parallel client, instrumented transaction barrier). Deferred to test-spec Phase 2 environment design.
|
|
|
|
|
3. AC-5.7 secret-rotation scenario requires service restart between requests; this is "container-restart" semantics in production. Deferred to test-spec Phase 2.
|