# Test Data Management > **Status**: produced by autodev `/test-spec` Phase 2 (2026-05-14). > **Naming**: post-rename target. Today's API surface is `/aircrafts`/`/flights`; tests will be RED until B5–B8 land. Fixture column names in `expected_results/fixture_cascade_*.sql` use post-rename names; pre-rename code path runs the same DDL via the `Azaion.Flights.Database.DatabaseMigrator` against the test DB before the fixture INSERT — the test orchestrator invokes the service container's startup, then runs fixture SQL via the side channel. ## Seed Data Sets | Data Set | Description | Used by Tests | How Loaded | Cleanup | |----------|-------------|---------------|-----------|---------| | `seed_empty` | Schema migrated, no rows in any table | bootstrap, unauth, 404 scenarios | `docker compose down -v && docker compose up -d` then wait for `missions` startup (which runs the migrator) | `down -v` between scenarios | | `seed_one_default_vehicle` | Schema + 1 row in `vehicles` with `is_default=true` | AC-1.2 (default-clear), AC-1.3 (TOCTOU race), AC-1.4 setDefault, AC-2.1 mission create | side-channel SQL `INSERT INTO vehicles ...` after `seed_empty` | Within scenario class via per-class `IClassFixture` | | `seed_3_vehicles_2_default` | 3 rows in `vehicles`, 2 with `is_default=true` (illegal under AC-1.2) | AC-1.5 list, AC-1.6 filter (with deterministic ordering) | side-channel SQL | per-class fixture | | `seed_25_missions` | 25 `missions` rows (5 in January 2026, 20 in February 2026) referencing one vehicle | AC-2.3, AC-2.4, AC-2.5 pagination + date filter | side-channel SQL with deterministic UUIDs | per-class fixture | | `fixture_cascade_F3` | One mission with full dependency chain: 1 mission → 2 waypoints → 2 media → 2 annotations → 2 detection rows + 3 map_objects | AC-3.1, AC-3.3, AC-3.4, AC-10.2 | `expected_results/fixture_cascade_F3.sql` (referenced from `results_report.md`) — applied via side-channel after `seed_empty` | `down -v` after each scenario in this class | | `fixture_cascade_F4` | One mission with one waypoint that has 1 media → 1 annotation → 1 detection chain; sibling waypoint with NO chain (must remain after delete) | AC-4.5, AC-4.6 | `expected_results/fixture_cascade_F4.sql` | `down -v` after each scenario in this class | | `seed_5_waypoints_unordered` | 5 waypoints with `order_num` `[3, 1, 2, 5, 4]` under one mission | AC-4.3 unpaginated ordering | side-channel SQL | per-class fixture | | `seed_legacy_gps_tables` | Pre-B7 schema: `vehicles`, `missions`, `waypoints` PLUS `orthophotos` and `gps_corrections` populated with 1 row each | AC-3.5 (post-B7 absence), AC-6.5 (one-shot drop), AC-10.5 (legacy device migration) | side-channel SQL `CREATE TABLE` + `INSERT` in a fresh `seed_empty` | `down -v` between scenarios | ## Data Isolation Strategy Three isolation tiers, by scenario type: - **Class-scoped DB reset** (`IClassFixture`): for scenarios that share the same seed within a test class but must not leak to other classes. Used for AC-1, AC-2, AC-4 read paths. - **Scenario-scoped container restart** (`docker compose down -v && up -d`): for scenarios that assert startup-time behavior or migrator side-effects. Used for AC-6.3, AC-6.4 (idempotency), AC-6.5 (legacy drop), AC-6.6 (idempotent re-run), AC-6.7 (DB unreachable), AC-6.11 (CORS Production-gate lock test — separate `docker run` invocation), AC-5.7 (JWKS key rotation). - **Per-test transaction roll-back is NOT used** — the system under test is a separate process and its `DataConnection` is not in the test transaction. ## Input Data Mapping | Input Data File | Source Location | Description | Covers Scenarios | |-----------------|----------------|-------------|-----------------| | `data_parameters.md` § 7 (HTTP table) | `_docs/00_problem/input_data/data_parameters.md` | Documentation of every endpoint + DTO shape; the consumer constructs requests from these shapes | every FT-* and NFT-* scenario | | `fixture_cascade_F3.sql` | `_docs/00_problem/input_data/expected_results/fixture_cascade_F3.sql` | Cascade chain seed for AC-3 | FT-P-12, FT-N-04, NFT-RES-01, NFT-PERF-01 | | `fixture_cascade_F4.sql` | `_docs/00_problem/input_data/expected_results/fixture_cascade_F4.sql` | Cascade chain seed for AC-4 | FT-P-18, NFT-RES-02 | | `cascade_F3_walk.json` | `_docs/00_problem/input_data/expected_results/cascade_F3_walk.json` | Per-table delete-count expectations | FT-P-12 | | `cascade_F4_walk.json` | `_docs/00_problem/input_data/expected_results/cascade_F4_walk.json` | Per-table delete-count expectations | FT-P-18 | ## Expected Results Mapping | Test Scenario ID | Input Data | Expected Result | Comparison Method | Tolerance | Expected Result Source | |-----------------|------------|-----------------|-------------------|-----------|----------------------| | FT-P-01 | `POST /vehicles` body per `data_parameters.md` § 2.1 | `201 Created`, `Vehicle` body, DB row exists | exact + db_query | N/A | `results_report.md` AC-1 row 1.1 | | FT-P-02 | `POST /vehicles` body with `IsDefault:true` against `seed_one_default_vehicle` | `201`; new row default; prior row not default; default count == 1 | exact + db_query | N/A | AC-1 row 1.2 | | FT-P-03 | `POST /vehicles/{id}/setDefault {IsDefault:true}` | `200`; default count == 1; only target row default | exact + db_query | N/A | AC-1 row 1.4 | | FT-P-04 | `GET /vehicles` against `seed_3_vehicles_2_default` | `200`; body length == 3; PascalCase keys; **ordered by `Name` ASC** | exact + schema + ordering | N/A | AC-1 row 1.5 | | FT-P-05 | `GET /vehicles?name=BR&isDefault=true` | `200`; body length == 1; `body[0].Name == "BR-01"` | exact | N/A | AC-1 row 1.6 | | FT-N-01 | `GET /vehicles?name=br` (lowercase filter against `BR-01`) | `200`; body length == 1 (filter is **case-INSENSITIVE** per AC-1.6) | exact | N/A | AC-1 row 1.7 | | FT-N-02 | `GET /vehicles/{random uuid}` | `404`; envelope `{ statusCode:404, message }` | exact + schema | N/A | AC-1 row 1.8 | | FT-N-03 | `DELETE /vehicles/{id}` against vehicle referenced by ≥1 mission | `409`; row not deleted | exact + db_query | N/A | AC-1 row 1.9 | | FT-P-06 | `DELETE /vehicles/{id}` against vehicle with 0 missions | `204`; row deleted | exact + db_query | N/A | AC-1 row 1.10 | | FT-P-07 | `POST /missions` body per `data_parameters.md` § 2.2 | `201`; `CreatedDate ± 5s` of `now` | exact + numeric_tolerance | ±5s | AC-2 row 2.1 | | FT-N-04 | `POST /missions {VehicleId:}` | `400` (today, divergent from spec's 404) | exact | N/A | AC-2 row 2.2 | | FT-P-08 | `GET /missions` against `seed_25_missions` | `200`; `PaginatedResponse`; Page=1, PageSize=20, TotalCount=25, Items.length=20; **ordered by `CreatedDate` DESC** (newest first); `Items[0].CreatedDate >= Items[1].CreatedDate >= ...` | exact + schema + ordering | N/A | AC-2 row 2.3 | | FT-P-09 | `GET /missions?page=2&pageSize=20` | `Page=2, Items.length=5` | exact | N/A | AC-2 row 2.4 | | FT-P-10 | `GET /missions?fromDate=...&toDate=...` | `TotalCount=3` against the 5-row seed | exact | N/A | AC-2 row 2.5 | | FT-P-11 | `PUT /missions/{id} {Name, VehicleId:null}` | `200`; Name updated, VehicleId preserved | exact | N/A | AC-2 row 2.7 | | FT-N-05 | `GET /missions/{random}` | `404` | exact | N/A | AC-2 row 2.6 | | FT-P-12 | `DELETE /missions/{id}` against `fixture_cascade_F3` | `204`; per-table counts == 0 per `cascade_F3_walk.json` | exact + db_query + file_reference | N/A | AC-3 row 3.1 | | FT-N-06 | `DELETE /missions/{random}` | `404`; no DELETE issued against dependency tables (instrument SQL log) | exact + log_assertion | N/A | AC-3 row 3.2 | | FT-P-13 | `GET /missions/{id}/waypoints` against `seed_5_waypoints_unordered` | `200`; ordered `OrderNum [1..5]` ASC | exact | N/A | AC-4 row 4.2 | | FT-N-07 | `GET /missions/{random}/waypoints` | `404` | exact | N/A | AC-4 row 4.1 | | FT-P-14 | `POST /missions/{id}/waypoints` body per `data_parameters.md` § 2.3 with non-null GeoPoint | `201`; `Lat,Lon` echoed; `Mgrs == null` (today, divergent — see §2.3 note) | exact | N/A | AC-4 row 4.3 | | FT-P-15 | `PUT /missions/{id}/waypoints/{wpId}` body resetting Height to 0 | `200`; `Height==0` (full overwrite) | exact | N/A | AC-4 row 4.4 | | FT-P-18 | `DELETE /missions/{id}/waypoints/{wpId}` against `fixture_cascade_F4` | `204`; only target waypoint's chain deleted; sibling chain intact | exact + db_query + file_reference | N/A | AC-4 row 4.5 | | FT-P-16 | `GET /health` no auth | `200 { "status": "healthy" }` | exact | N/A | AC-7 row 7.1 | | FT-P-17 | `GET /health` with PG stopped | `200 { "status": "healthy" }` (no DB ping) | exact | N/A | AC-7 row 7.2 | NFT-* mappings (perf, resilience, security, resource-limit) are inline in the respective test files. ## External Dependency Mocks | External Service | Mock/Stub | How Provided | Behavior | |-----------------|-----------|-------------|----------| | `admin` (JWT issuer + JWKS) | `jwks-mock` container | Built from `tests/Azaion.Missions.JwksMock/`, runs in the same `e2e-net` Docker network. Self-signed TLS cert; CA mounted into `missions` so it trusts the mock's JWKS HTTPS endpoint. Exposes `GET /.well-known/jwks.json` (consumed by `missions`'s `ConfigurationManager`), `POST /sign` (returns ECDSA-signed JWTs to the test consumer for AC-5 + AC-9 scenarios), `POST /rotate-key` (generates a new `kid`, retains the previous public key for `OldKeyGraceSeconds` to support NFT-RES-07 transition assertions). The mock's private key never leaves its container. | Mints valid / expired / wrong-`kid` / wrong-`iss` / wrong-`aud` / wrong-`alg` (HS256 confusion) / claim-missing / claim-typo tokens on demand. Rotation tests trigger key roll without restarting `missions`. | | `annotations` table owner | DB-only stub | Side-channel `CREATE TABLE annotations (id text PRIMARY KEY, media_id text)` then `INSERT` | Provides rows the cascade walk reads + deletes; no service running | | `detection` table owner | DB-only stub | Side-channel `CREATE TABLE detection (id uuid PRIMARY KEY, annotation_id text)` + INSERT | Same as above | | `media` table owner | DB-only stub | Side-channel `CREATE TABLE media (id text PRIMARY KEY, waypoint_id uuid)` + INSERT | Same | | `autopilot` writer of `map_objects` | DB-only stub + race injector | Side-channel; for AC-3.4 race, a parallel goroutine-equivalent inserts a `map_objects` row immediately after the service's first `SELECT` (instrumented via test-only proxy) | One scenario only | | `flight-gate`, Watchtower, suite reverse proxy, suite UI | NOT mocked | n/a | Out of scope for service-level e2e | **JWKS mock token-minting contract** (consumed by the e2e test runner): ```http POST https://jwks-mock:8443/sign HTTP/1.1 Content-Type: application/json { "iss": "https://admin-test.azaion.local", # optional; defaults to the mock's JWT_ISSUER env "aud": "azaion-edge", # optional; defaults to JWT_AUDIENCE env "exp_offset_seconds": 3600, # optional; default 3600 (1h). Negative for expired tokens. "permissions": "FL", # optional; default "FL". Set to omit / "" / "ADMIN" / "fl" / "FLight" to test claim-mismatch "alg_override": null, # optional; "HS256" forces the mock to sign with a static HMAC key for HS256-confusion tests (NFT-SEC-10) "kid_override": null # optional; non-existent kid for unknown-key tests } ``` Response: `{ "token": "", "kid": "" }`. The mock signs every token with the current private key by default. `alg_override: "HS256"` is the only way to obtain an HS256 token in tests — used to verify the algorithm-pin defense (NFT-SEC-10). ## Data Validation Rules | Data Type | Validation today (per AC-* notes) | Invalid Examples | Expected System Behavior | |-----------|-----------------------------------|-----------------|------------------------| | Vehicle.Name | NONE (per data_parameters.md § 2.1 note) | `""` (empty) | accepted; row created; tests assert this is the current state, not the desirable one (carry-forward) | | Vehicle.BatteryCapacity | NONE | `-1` | accepted; carry-forward | | Vehicle.Type | NONE (any int accepted) | `99` | accepted; carry-forward | | Mission.Page / PageSize | NONE | `-1`, `999999` | accepted by binding; carry-forward | | Waypoint.GeoPoint | NONE; all-null accepted | `{Lat:null, Lon:null, Mgrs:null}` | accepted (`OrderNum + Height` still required-by-shape) | | JWT lifetime | `ValidateLifetime=true` with **30s** clock skew | `exp = now-60s` | `401` | | JWT signature | ECDSA-SHA256 + admin's JWKS | tampered payload / signed with non-JWKS key | `401` | | JWT algorithm | `ValidAlgorithms = [EcdsaSha256]` (pinned) | `alg: HS256`, `alg: RS256`, `alg: none` | `401` | | JWT `iss` claim | exact match against `JWT_ISSUER` | anything else | `401` | | JWT `aud` claim | exact match against `JWT_AUDIENCE` | anything else | `401` | | JWT `kid` (header) | must resolve in cached JWKS | unknown `kid` | `401` (until next JWKS refresh tick when rotation publishes it) | | JWT claim `permissions` | contains-match `"FL"` (multi-permission tokens accepted if one entry == `"FL"`) | `"fl"`, `"ADMIN"`, `"FLight"`, missing | `403` | | `Authorization` header | required on all `/vehicles/*`, `/missions/*` | absent | `401` | | `DATABASE_URL` shape | `postgresql://...` URL OR raw Npgsql connection string | unparseable | `Infrastructure/ConfigurationResolver.ResolveRequiredOrThrow` throws `InvalidOperationException`; process exits non-zero before HTTP server binds | | `JWT_ISSUER` / `JWT_AUDIENCE` / `JWT_JWKS_URL` | each required at startup | any of them missing or whitespace-only | `Infrastructure/ConfigurationResolver.ResolveRequiredOrThrow` throws `InvalidOperationException`; process exits non-zero before HTTP server binds | | `JWT_JWKS_URL` scheme | HTTPS-only via `HttpDocumentRetriever { RequireHttps = true }` | `http://...` | passes startup config resolution, but first protected request fails 500 when JWKS fetch rejects the URL | | `CorsConfig:AllowedOrigins` in `Production` | non-empty OR `AllowAnyOrigin == true` | empty AND `AllowAnyOrigin != true` AND `ASPNETCORE_ENVIRONMENT=Production` | `CorsConfigurationValidator.EnsureSafeForEnvironment` throws `InvalidOperationException`; startup aborts | | `CorsConfig` in non-Production | empty allow-list permitted | n/a | falls back to `AllowAnyOrigin/Method/Header` AND logs `PermissiveDefaultWarning` |