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,241 @@
# Input Data Parameters — Azaion.Missions
> **Status**: derived-from-code (autodev `/document` Step 6, 2026-05-14).
> Schemas below match the actual `Database/Entities/*.cs` LinqToDB mappings and `DTOs/*.cs` request shapes (post-B6 names). Today's source still uses pre-rename names; the doc-vs-code mapping is in `_docs/02_document/04_verification_log.md` § 0.
---
## 1. Configuration input (env vars)
| Variable | Type | Required | Default (dev fallback) | Source order | Format / constraints | Used by |
|----------|------|----------|------------------------|--------------|----------------------|---------|
| `DATABASE_URL` | string | yes (production) | `Host=localhost;Database=azaion;Username=postgres;Password=changeme` | `IConfiguration``Environment.GetEnvironmentVariable` → fallback | Either `postgresql://user:pass@host:port/db` (converted via local helper `ConvertPostgresUrl`) OR a raw Npgsql connection string | `Program.cs` (DI registration of `AppDataConnection`) |
| `JWT_SECRET` | string | yes (production) | `development-secret-key-min-32-chars!!` | same as above | UTF-8 string, ≥32 chars (`SymmetricSecurityKey` accepts shorter but `JwtBearer` HS256 requires ≥32 bytes) | `Program.cs` `AddJwtAuth` |
| `AZAION_REVISION` | string | no | (build-time) | Dockerfile `ARG` baked from `CI_COMMIT_SHA` | git SHA | Dockerfile only; surfaced via `docker inspect` |
| `ASPNETCORE_URLS` | string | no | `http://+:8080` | ASP.NET Core convention | URL list | ASP.NET Core host |
**Important**: ADR-005 carry-forward — neither Swagger UI mounting nor the dev fallbacks for `JWT_SECRET` / `DATABASE_URL` are gated on `IsDevelopment()`. A production deploy without the env vars set will silently boot with the well-known dev secret; tracked at suite level (CMMC L2 row 3, AZ-487 / AZ-494).
## 2. HTTP request DTOs (post-B6 shapes)
### 2.1 Vehicle (`/vehicles`)
```csharp
public class CreateVehicleRequest {
public VehicleType Type { get; set; } // enum int: Plane=0, Copter=1, UGV=2, GuidedMissile=3
public string Model { get; set; } = "";
public string Name { get; set; } = "";
public FuelType FuelType { get; set; } // enum int: Electric=0, Gasoline=1, Diesel=2
public decimal BatteryCapacity { get; set; }
public decimal EngineConsumption { get; set; }
public decimal EngineConsumptionIdle { get; set; }
public bool IsDefault { get; set; }
}
public class UpdateVehicleRequest { // all properties nullable -- partial update
public VehicleType? Type;
public string? Model;
public string? Name;
public FuelType? FuelType;
public decimal? BatteryCapacity;
public decimal? EngineConsumption;
public decimal? EngineConsumptionIdle;
public bool? IsDefault;
}
public class GetVehiclesQuery {
public string? Name { get; set; } // case-sensitive contains
public bool? IsDefault { get; set; } // exact match
}
public class SetDefaultRequest {
public bool IsDefault { get; set; }
}
```
**Validation**: NONE today. No `[Required]`, no `[Range]`, no min-length. Empty `Name`, negative `BatteryCapacity`, out-of-range enum int values are accepted. Carry-forward improvement.
### 2.2 Mission (`/missions`)
```csharp
public class CreateMissionRequest {
public Guid VehicleId { get; set; }
public string Name { get; set; } = "";
public DateTime? CreatedDate { get; set; } // defaults to UtcNow if null
}
public class UpdateMissionRequest { // partial update
public string? Name { get; set; }
public Guid? VehicleId { get; set; }
}
public class GetMissionsQuery {
public string? Name { get; set; }
public DateTime? FromDate { get; set; }
public DateTime? ToDate { get; set; }
public int Page { get; set; } = 1;
public int PageSize { get; set; } = 20;
}
```
**Validation**: existence check on `VehicleId` (returns 400 today via `ArgumentException`; spec wants 404 — carry-forward divergence). No bounds on `Page` / `PageSize` (negative or huge values accepted by binding).
### 2.3 Waypoint (`/missions/{id}/waypoints`)
```csharp
public class GeoPoint { // shared value object; all fields nullable
public decimal? Lat { get; set; }
public decimal? Lon { get; set; }
public string? Mgrs { get; set; } // Military Grid Reference System
}
public class CreateWaypointRequest {
public GeoPoint? GeoPoint { get; set; } // nullable: all-null is accepted today (no invariant)
public WaypointSource WaypointSource { get; set; } // enum int
public WaypointObjective WaypointObjective { get; set; } // enum int
public int OrderNum { get; set; }
public decimal Height { get; set; }
}
public class UpdateWaypointRequest { // identical SHAPE to Create -- non-nullable enums/numerics
public GeoPoint? GeoPoint { get; set; }
public WaypointSource WaypointSource { get; set; }
public WaypointObjective WaypointObjective { get; set; }
public int OrderNum { get; set; }
public decimal Height { get; set; }
}
```
**Validation**: NONE. No min-length, no enum range check, no `Lat`/`Lon` bounds, no MGRS format validation. `GeoPoint` may be all-null. **`UpdateWaypoint` is structurally NOT partial** — every field gets overwritten on PUT (inconsistent with vehicle's partial-update pattern).
**Spec divergence (Geopoint)**: spec stores `Waypoints.GPS` as a single `string GPS` field with `Lat <-> MGRS` auto-conversion (`../../suite/_docs/02_missions.md`, `../../suite/_docs/00_database_schema.md`). Code stores 3 separate columns with NO conversion. Carry-forward.
## 3. Persisted data — owned tables (post-B7+B9)
### 3.1 `vehicles` (owned)
| Column | Type | Nullable | Notes |
|--------|------|----------|-------|
| `id` | UUID | NO | primary key |
| `type` | INTEGER | NO | `VehicleType` enum int (Plane / Copter / UGV / GuidedMissile) |
| `model` | TEXT | NO | |
| `name` | TEXT | NO | |
| `fuel_type` | INTEGER | NO | `FuelType` enum int |
| `battery_capacity` | NUMERIC | NO | |
| `engine_consumption` | NUMERIC | NO | |
| `engine_consumption_idle` | NUMERIC | NO | |
| `is_default` | BOOLEAN | NO | "exactly one default" enforced by `VehicleService` (stricter than spec — B12 decision) |
### 3.2 `missions` (owned)
| Column | Type | Nullable | Notes |
|--------|------|----------|-------|
| `id` | UUID | NO | primary key |
| `created_date` | TIMESTAMPTZ | NO | server-assigned `UtcNow` if not supplied |
| `name` | TEXT | NO | |
| `vehicle_id` | UUID | NO | logical FK to `vehicles.id`; existence-checked in service, no DB-level FK constraint declared in migrator |
Index: `ix_missions_vehicle_id` on `vehicle_id`.
### 3.3 `waypoints` (owned)
| Column | Type | Nullable | Notes |
|--------|------|----------|-------|
| `id` | UUID | NO | primary key |
| `mission_id` | UUID | NO | logical FK to `missions.id` |
| `lat` | NUMERIC | YES | spec divergence — see § 2.3 |
| `lon` | NUMERIC | YES | spec divergence |
| `mgrs` | TEXT | YES | spec divergence |
| `waypoint_source` | INTEGER | NO | `WaypointSource` enum int |
| `waypoint_objective` | INTEGER | NO | `WaypointObjective` enum int |
| `order_num` | INTEGER | NO | listing order |
| `height` | NUMERIC | NO | metres |
Index: `ix_waypoints_mission_id` on `mission_id`.
### 3.4 `map_objects` (owned schema; written by `autopilot`)
| Column | Type | Nullable | Notes |
|--------|------|----------|-------|
| `id` | UUID | NO | primary key |
| `mission_id` | UUID | NO | logical FK to `missions.id` |
| `h3_index` | TEXT | NO | Uber H3 hex grid cell |
| `mgrs` | TEXT | NO | |
| `lat` | NUMERIC | YES | |
| `lon` | NUMERIC | YES | |
| `class_num` | INTEGER | NO | detection class id |
| `label` | TEXT | NO | |
| `size_width_m` | NUMERIC | NO | |
| `size_length_m` | NUMERIC | NO | |
| `confidence` | NUMERIC | NO | 0..1 |
| `object_status` | INTEGER | NO | `ObjectStatus` enum int |
| `first_seen_at` | TIMESTAMPTZ | NO | |
| `last_seen_at` | TIMESTAMPTZ | NO | |
Index: `ix_map_objects_mission_id` on `mission_id`.
`autopilot` is the writer (per `../../suite/_docs/06_autopilot_design.md`); this service owns the schema and cascade-deletes only.
## 4. Persisted data — borrowed read-only stubs
| Table | Schema owner | This service uses for |
|-------|--------------|------------------------|
| `media` | `annotations` (per `../../suite/_docs/01_annotations.md`) | id resolution + cascade-delete walk on mission/waypoint delete |
| `annotations` | `annotations` | id resolution + cascade-delete walk |
| `detection` (singular by upstream owner) | Detection pipeline | cascade-delete walk |
Stub schemas (just enough to query / delete by id):
```csharp
[Table("media")] public class Media { [PrimaryKey, Column("id")] public string Id = ""; [Column("waypoint_id")] public Guid? WaypointId; }
[Table("annotations")] public class Annotation { [PrimaryKey, Column("id")] public string Id = ""; [Column("media_id")] public string MediaId = ""; }
[Table("detection")] public class Detection { [PrimaryKey, Column("id")] public Guid Id; [Column("annotation_id")] public string AnnotationId = ""; }
```
Migrations for these tables are owned by the respective sibling services. If they have not migrated on a given device, this service's cascade-delete walk fails on `relation does not exist` (abnormal deployment).
## 5. Removed in B7 (post-B7+B9 schema)
These tables and entities are **out of this repo**; cleanup happens once on legacy devices via the B9 `DROP TABLE IF EXISTS` block in `DatabaseMigrator`:
| Table | Pre-B7 owner | Post-B7 owner |
|-------|--------------|---------------|
| `orthophotos` | this repo (`Orthophoto` entity, 03_gps_denied component) | `gps-denied` service (separate repo) |
| `gps_corrections` | this repo (`GpsCorrection` entity, 03_gps_denied component) | `gps-denied` service |
`gps-denied` references `mission_id` / `waypoint_id` as plain GUIDs in its OWN tables — no runtime coupling, no FK declaration, no cascade by this service.
## 6. Enum values
| Enum | Values | Persisted as | Defined in |
|------|--------|--------------|------------|
| `VehicleType` | `Plane=0`, `Copter=1`, `UGV=2`, `GuidedMissile=3` | INTEGER | `Enums/VehicleType.cs` (post-B6) |
| `FuelType` | `Electric=0`, `Gasoline=1`, `Diesel=2` | INTEGER | `Enums/FuelType.cs` |
| `WaypointSource` | `Operator=0`, `Mission=1`, ... | INTEGER | `Enums/WaypointSource.cs` |
| `WaypointObjective` | `Surveillance=0`, `Strike=1`, ... | INTEGER | `Enums/WaypointObjective.cs` |
| `ObjectStatus` | `Active=0`, `Lost=1`, ... | INTEGER | `Enums/ObjectStatus.cs` (used only by `MapObject`) |
Per `_docs/02_document/modules/enums.md`, integer values are NOT range-validated on input — model binding accepts any int.
## 7. Inbound data shapes (HTTP)
| Endpoint | Method | Body / Query | Returns |
|----------|--------|--------------|---------|
| `/vehicles` | GET | `?name=&isDefault=` | `List<Vehicle>` (PascalCase JSON; not paginated) |
| `/vehicles/{id}` | GET | — | `Vehicle` |
| `/vehicles` | POST | `CreateVehicleRequest` | `Vehicle` (created) |
| `/vehicles/{id}` | PUT | `UpdateVehicleRequest` (partial) | `Vehicle` (updated) |
| `/vehicles/{id}/setDefault` | POST | `SetDefaultRequest` | `Vehicle` |
| `/vehicles/{id}` | DELETE | — | 204 / 409 if referenced |
| `/missions` | GET | `?name=&fromDate=&toDate=&page=&pageSize=` | `PaginatedResponse<Mission>` |
| `/missions/{id}` | GET | — | `Mission` |
| `/missions` | POST | `CreateMissionRequest` | `Mission` (created) |
| `/missions/{id}` | PUT | `UpdateMissionRequest` (partial) | `Mission` (updated) |
| `/missions/{id}` | DELETE | — | 204 / 404; runs F3 cascade |
| `/missions/{id}/waypoints` | GET | — | `List<Waypoint>` (unpaginated, ordered by `OrderNum`) |
| `/missions/{id}/waypoints` | POST | `CreateWaypointRequest` | `Waypoint` (created) |
| `/missions/{id}/waypoints/{wpId}` | PUT | `UpdateWaypointRequest` (full overwrite) | `Waypoint` |
| `/missions/{id}/waypoints/{wpId}` | DELETE | — | 204; runs F4 scoped cascade |
| `/health` | GET | — anonymous | `200 { "status": "healthy" }` |
All routes except `/health` require JWT bearer with `permissions=FL` claim.
@@ -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.