Enhanced the .gitignore to exclude test results and updated the Dockerfile to include a new entrypoint script for improved container initialization. Refactored JWT configuration to support additional parameters for automatic refresh intervals, ensuring better control over token management. Updated the ConfigurationResolver to enforce required environment variables without hardcoded fallbacks, enhancing security and flexibility.
14 KiB
Test Data Management
Status: produced by autodev
/test-specPhase 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 inexpected_results/fixture_cascade_*.sqluse post-rename names; pre-rename code path runs the same DDL via theAzaion.Flights.Database.DatabaseMigratoragainst 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<DbResetFixture> |
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<DbResetFixture>): 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 — separatedocker runinvocation), AC-5.7 (JWKS key rotation). - Per-test transaction roll-back is NOT used — the system under test is a separate process and its
DataConnectionis 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:<random>} |
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<Mission>; 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<JsonWebKeySet>), 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):
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": "<encoded JWT>", "kid": "<key id>" }.
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 |