refactor: enhance JWT authentication and CORS configuration

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.
This commit is contained in:
Oleksandr Bezdieniezhnykh
2026-05-14 19:48:25 +03:00
parent 2fe394d732
commit 7025f4d075
74 changed files with 8494 additions and 19 deletions
@@ -0,0 +1,62 @@
{
"$comment": "Expected per-table delete counts and cascade order for FT-P-12 (mission cascade delete F3). Used as the file_reference comparison for the cascade walk.",
"input_fixture": "fixture_cascade_F3.sql",
"trigger": "DELETE /missions/22222222-0000-0000-0000-000000000001",
"expected_response": {
"status_code": 204,
"body_length": 0
},
"expected_cascade_order": [
"SELECT FROM map_objects WHERE mission_id = M1",
"DELETE FROM map_objects WHERE mission_id = M1",
"SELECT FROM waypoints WHERE mission_id = M1",
"SELECT FROM media WHERE waypoint_id IN (WP1, WP2)",
"SELECT FROM annotations WHERE media_id IN (ME1, ME2)",
"DELETE FROM detection WHERE annotation_id IN (AN1, AN2)",
"DELETE FROM annotations WHERE id IN (AN1, AN2)",
"DELETE FROM media WHERE id IN (ME1, ME2)",
"DELETE FROM waypoints WHERE mission_id = M1",
"DELETE FROM missions WHERE id = M1"
],
"expected_per_table_post_state": {
"missions": {
"filter": "id = '22222222-0000-0000-0000-000000000001'",
"expected_count": 0,
"comparison": "exact"
},
"waypoints": {
"filter": "mission_id = '22222222-0000-0000-0000-000000000001'",
"expected_count": 0,
"comparison": "exact"
},
"map_objects": {
"filter": "mission_id = '22222222-0000-0000-0000-000000000001'",
"expected_count": 0,
"comparison": "exact"
},
"media": {
"filter": "id IN ('media-fixture-001', 'media-fixture-002')",
"expected_count": 0,
"comparison": "exact"
},
"annotations": {
"filter": "id IN ('anno-fixture-001', 'anno-fixture-002')",
"expected_count": 0,
"comparison": "exact"
},
"detection": {
"filter": "annotation_id IN ('anno-fixture-001', 'anno-fixture-002')",
"expected_count": 0,
"comparison": "exact"
}
},
"expected_per_table_pre_state_for_safety_check": {
"missions": 1,
"waypoints": 2,
"map_objects": 3,
"media": 2,
"annotations": 2,
"detection": 2
},
"expected_total_round_trips": "between 6 and 9 (4 SELECT + 5 DELETE per the documented walk; allow ±1 for collapsed/skipped phases when chains are empty)"
}
@@ -0,0 +1,69 @@
{
"$comment": "Expected per-table delete counts and cascade order for FT-P-18 (waypoint cascade delete F4). Asserts that the SIBLING waypoint chain remains untouched.",
"input_fixture": "fixture_cascade_F4.sql",
"trigger": "DELETE /missions/22222222-0000-0000-0000-000000000004/waypoints/33333333-0000-0000-0000-00000000F4A1",
"expected_response": {
"status_code": 204,
"body_length": 0
},
"expected_cascade_order": [
"SELECT FROM media WHERE waypoint_id = WP1",
"SELECT FROM annotations WHERE media_id = ME1",
"DELETE FROM detection WHERE annotation_id = AN1",
"DELETE FROM annotations WHERE id = AN1",
"DELETE FROM media WHERE id = ME1",
"DELETE FROM waypoints WHERE id = WP1"
],
"expected_per_table_post_state_target_chain": {
"waypoints": {
"filter": "id = '33333333-0000-0000-0000-00000000F4A1'",
"expected_count": 0,
"comparison": "exact"
},
"media": {
"filter": "id = 'media-F4-target-001'",
"expected_count": 0,
"comparison": "exact"
},
"annotations": {
"filter": "id = 'anno-F4-target-001'",
"expected_count": 0,
"comparison": "exact"
},
"detection": {
"filter": "annotation_id = 'anno-F4-target-001'",
"expected_count": 0,
"comparison": "exact"
}
},
"expected_per_table_post_state_sibling_chain_must_remain": {
"waypoints": {
"filter": "id = '33333333-0000-0000-0000-00000000F4B2'",
"expected_count": 1,
"comparison": "exact"
},
"media": {
"filter": "id = 'media-F4-sibling-002'",
"expected_count": 1,
"comparison": "exact"
},
"annotations": {
"filter": "id = 'anno-F4-sibling-002'",
"expected_count": 1,
"comparison": "exact"
},
"detection": {
"filter": "annotation_id = 'anno-F4-sibling-002'",
"expected_count": 1,
"comparison": "exact"
}
},
"expected_per_table_pre_state_for_safety_check": {
"missions": 1,
"waypoints": 2,
"media": 2,
"annotations": 2,
"detection": 2
},
"expected_total_round_trips": "5 to 6 (2 SELECT + 4 DELETE for the target chain; mission row is NOT touched)"
}
@@ -0,0 +1,54 @@
-- Fixture: full F3 cascade chain rooted at one mission.
-- Used by: blackbox-tests.md FT-P-12, FT-N-04 (variant), resilience-tests.md NFT-RES-01, security-tests.md NFT-SEC-08 (variant)
-- Naming: post-rename target. Pre-rename code path runs the same DDL via Azaion.Flights.Database.DatabaseMigrator;
-- this file ASSUMES the schema is already in place (the missions container's startup runs the migrator).
--
-- Deterministic UUIDs so tests can assert against known IDs.
--
-- Chain shape:
-- 1 vehicle (V1)
-- 1 mission (M1) → references V1
-- 2 waypoints (WP1, WP2) → both reference M1
-- 2 media rows (ME1 ↔ WP1, ME2 ↔ WP2)
-- 2 annotations (AN1 ↔ ME1, AN2 ↔ ME2)
-- 2 detection rows (DT1 ↔ AN1, DT2 ↔ AN2)
-- 3 map_objects (MO1, MO2, MO3) → all reference M1
BEGIN;
-- Vehicle (1 row)
INSERT INTO vehicles (id, type, model, name, fuel_type, battery_capacity, engine_consumption, engine_consumption_idle, is_default)
VALUES ('11111111-0000-0000-0000-000000000001', 0, 'Bayraktar', 'BR-test', 1, 0, 5, 1, true);
-- Mission (1 row) — id M1
INSERT INTO missions (id, created_date, name, vehicle_id)
VALUES ('22222222-0000-0000-0000-000000000001', '2026-05-14T00:00:00Z', 'cascade-F3-fixture', '11111111-0000-0000-0000-000000000001');
-- Waypoints (2 rows) — ids WP1, WP2
INSERT INTO waypoints (id, mission_id, lat, lon, mgrs, waypoint_source, waypoint_objective, order_num, height) VALUES
('33333333-0000-0000-0000-000000000001', '22222222-0000-0000-0000-000000000001', 50.45, 30.52, NULL, 0, 0, 1, 100),
('33333333-0000-0000-0000-000000000002', '22222222-0000-0000-0000-000000000001', 50.46, 30.53, NULL, 0, 0, 2, 110);
-- Media (2 rows) — borrowed-table stubs; the test side-channel CREATEs these tables before the test class runs
-- IMPORTANT: media/annotations/detection are owned by sibling services in production; in tests, side-channel CREATEs them.
INSERT INTO media (id, waypoint_id) VALUES
('media-fixture-001', '33333333-0000-0000-0000-000000000001'),
('media-fixture-002', '33333333-0000-0000-0000-000000000002');
-- Annotations (2 rows)
INSERT INTO annotations (id, media_id) VALUES
('anno-fixture-001', 'media-fixture-001'),
('anno-fixture-002', 'media-fixture-002');
-- Detection (2 rows; uuid PK)
INSERT INTO detection (id, annotation_id) VALUES
('44444444-0000-0000-0000-000000000001', 'anno-fixture-001'),
('44444444-0000-0000-0000-000000000002', 'anno-fixture-002');
-- Map objects (3 rows; written by autopilot in production)
INSERT INTO map_objects (id, mission_id, h3_index, mgrs, lat, lon, class_num, label, size_width_m, size_length_m, confidence, object_status, first_seen_at, last_seen_at) VALUES
('55555555-0000-0000-0000-000000000001', '22222222-0000-0000-0000-000000000001', '8a2a107255dffff', '38UPV1234567890', 50.45, 30.52, 1, 'truck', 3.0, 6.0, 0.91, 0, '2026-05-14T00:00:01Z', '2026-05-14T00:00:02Z'),
('55555555-0000-0000-0000-000000000002', '22222222-0000-0000-0000-000000000001', '8a2a107255bffff', '38UPV1234567891', 50.46, 30.53, 2, 'armor', 4.0, 8.0, 0.88, 0, '2026-05-14T00:00:03Z', '2026-05-14T00:00:04Z'),
('55555555-0000-0000-0000-000000000003', '22222222-0000-0000-0000-000000000001', '8a2a107255affff', '38UPV1234567892', 50.47, 30.54, 1, 'truck', 3.0, 6.0, 0.93, 1, '2026-05-14T00:00:05Z', '2026-05-14T00:00:06Z');
COMMIT;
@@ -0,0 +1,39 @@
-- Fixture: scoped F4 cascade chain rooted at one waypoint, with a sibling waypoint that has its own chain
-- (so the test asserts the sibling chain is INTACT after deleting the target waypoint).
-- Used by: blackbox-tests.md FT-P-18, resilience-tests.md NFT-RES-02
--
-- Chain shape:
-- 1 vehicle (V1)
-- 1 mission (M1) → references V1
-- 2 waypoints:
-- WP1 (target) → 1 media (ME1) → 1 annotation (AN1) → 1 detection (DT1)
-- WP2 (sibling) → 1 media (ME2) → 1 annotation (AN2) → 1 detection (DT2)
-- No map_objects (F4 cascade does not touch map_objects per the documented walk).
BEGIN;
INSERT INTO vehicles (id, type, model, name, fuel_type, battery_capacity, engine_consumption, engine_consumption_idle, is_default)
VALUES ('11111111-0000-0000-0000-000000000004', 0, 'Bayraktar', 'BR-F4-test', 1, 0, 5, 1, false);
INSERT INTO missions (id, created_date, name, vehicle_id)
VALUES ('22222222-0000-0000-0000-000000000004', '2026-05-14T00:00:00Z', 'cascade-F4-fixture', '11111111-0000-0000-0000-000000000004');
-- Waypoints — WP1 is the delete target, WP2 is the sibling that must remain after delete
INSERT INTO waypoints (id, mission_id, lat, lon, mgrs, waypoint_source, waypoint_objective, order_num, height) VALUES
('33333333-0000-0000-0000-00000000F4A1', '22222222-0000-0000-0000-000000000004', 50.45, 30.52, NULL, 0, 0, 1, 100), -- WP1 target
('33333333-0000-0000-0000-00000000F4B2', '22222222-0000-0000-0000-000000000004', 50.46, 30.53, NULL, 0, 0, 2, 110); -- WP2 sibling
-- Media chain for both waypoints
INSERT INTO media (id, waypoint_id) VALUES
('media-F4-target-001', '33333333-0000-0000-0000-00000000F4A1'),
('media-F4-sibling-002', '33333333-0000-0000-0000-00000000F4B2');
INSERT INTO annotations (id, media_id) VALUES
('anno-F4-target-001', 'media-F4-target-001'),
('anno-F4-sibling-002', 'media-F4-sibling-002');
INSERT INTO detection (id, annotation_id) VALUES
('44444444-0000-0000-0000-00000000F4D1', 'anno-F4-target-001'),
('44444444-0000-0000-0000-00000000F4D2', 'anno-F4-sibling-002');
COMMIT;
@@ -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.