Files
missions/_docs/00_problem/acceptance_criteria.md
T
Oleksandr Bezdieniezhnykh 7025f4d075 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.
2026-05-14 19:48:25 +03:00

14 KiB
Raw Blame History

Acceptance Criteria — Azaion.Missions

Status: derived-from-code (autodev /document Step 6, 2026-05-14). Source: every criterion below is grounded in observable code behaviour, configuration, suite spec, or HTTP contract — none are aspirational. Where the spec and code currently disagree (rename / GPS-Denied / wire shape), the criterion captures today's behaviour with a forward-looking note pointing at the responsible Jira child (B6 / B7 / etc.) under AZ-EPIC AZ-539. No automated tests exist yet, so today the AC must be verified by inspection. The autodev existing-code flow's Phase A Steps 3 → 7 is the planned path to convert these into runnable test cases.


AC-1 — Vehicle CRUD (F1)

# Criterion Verification
AC-1.1 POST /vehicles creates a row in vehicles and returns the created Vehicle (PascalCase JSON today) Inspect VehicleService.CreateVehicle; HTTP POST /vehicles { Type, Model, Name, FuelType, BatteryCapacity, EngineConsumption, EngineConsumptionIdle, IsDefault }
AC-1.2 If IsDefault == true on create or update or SetDefault, the service runs UPDATE vehicles SET is_default = FALSE WHERE is_default = TRUE BEFORE inserting/updating with IsDefault = true VehicleService.{CreateVehicle, UpdateVehicle, SetDefault} — clear-then-set pattern
AC-1.3 "Exactly one default" is stricter than spec (B12 decision pending — _docs/_process_leftovers/2026-05-14_rename-flights-to-missions.md) code reflects current behaviour; B12 ticket AZ-551 records the resolution decision
AC-1.4 The clear-then-set is NOT transaction-wrapped → race window can leave 2+ defaults or zero defaults VehicleService — no db.BeginTransactionAsync; tracked in _docs/02_document/components/01_vehicle_catalog/description.md Caveats #1
AC-1.5 GET /vehicles returns a plain List<Vehicle> (NO pagination, NO total count) — matches spec endpoint 13 VehicleService.GetVehicles
AC-1.6 GET /vehicles?name=&isDefault= filters case-sensitively on Name and exactly on IsDefault VehicleService.GetVehicles query expression
AC-1.7 GET /vehicles/{id} returns 404 (KeyNotFoundExceptionErrorHandlingMiddleware) when id absent VehicleService.GetVehicle
AC-1.8 DELETE /vehicles/{id} returns 409 (InvalidOperationExceptionErrorHandlingMiddleware) when any mission references the vehicle VehicleService.DeleteVehicle IsAny<Mission> check
AC-1.9 Every /vehicles/* route requires JWT with permissions=FL claim [Authorize(Policy="FL")] on VehiclesController

AC-2 — Mission create / read / update (F2)

# Criterion Verification
AC-2.1 POST /missions { Name, VehicleId, CreatedDate? } creates a row and returns the created Mission MissionService.CreateMission; default CreatedDate = UtcNow if null
AC-2.2 POST /missions with non-existent VehicleId returns 400 Bad Request (today, via ArgumentException) — spec wants 404 MissionService.CreateMission existence check; carry-forward divergence
AC-2.3 GET /missions?name=&fromDate=&toDate=&page=&pageSize= returns PaginatedResponse<Mission> (the only paginated endpoint in this service) MissionService.GetMissions; default page=1, pageSize=20
AC-2.4 GET /missions/{id} returns 404 when id absent MissionService.GetMission
AC-2.5 PUT /missions/{id} applies partial update — non-null fields in UpdateMissionRequest overwrite, null fields are preserved MissionService.UpdateMission
AC-2.6 LinqToDB does NOT eager-load [Association]Mission.Vehicle and Mission.Waypoints serialize as null / [] on the wire Database/Entities/Mission.cs; verified observation
AC-2.7 Every /missions/* route requires JWT with permissions=FL claim [Authorize(Policy="FL")] on MissionsController
AC-2.8 TOCTOU on VehicleId deletion between existence check and insert produces Npgsql PostgresException → 500 (UX gap — spec wants 400) MissionService.CreateMission; tracked in _docs/02_document/components/02_mission_planning/description.md Caveats

AC-3 — Mission delete with cross-service cascade (F3) — most critical

# Criterion Verification
AC-3.1 DELETE /missions/{id} walks the cascade in this exact order: map_objects → resolve waypointIds → resolve mediaIds (via media.waypoint_id) → resolve annotationIds (via annotations.media_id) → detection (by annotation_id) → annotations (by id) → media (by id) → waypoints (by mission_id) → missions (by id) MissionService.DeleteMission (post-B6/B7)
AC-3.2 Mission missing → 404 (KeyNotFoundException) before any cascade DELETE runs MissionService.DeleteMission initial existence check
AC-3.3 Cascade is NOT transaction-wrapped today (ADR-006); partial failure leaves orphan rows in any sub-table MissionService.DeleteMission; no db.BeginTransactionAsync
AC-3.4 relation does not exist for any of media / annotations / detection → 500 with LogError; this is an abnormal deployment (some sibling service hasn't migrated) Middleware/ErrorHandlingMiddleware.cs fallthrough
AC-3.5 After B7 the cascade does NOT touch orthophotos or gps_correctionsgps-denied owns those tables and lifecycle post-B7 spec; _docs/02_document/architecture.md ADR-007
AC-3.6 End-to-end latency target: <50ms typical against local PostgreSQL on the same device (47 sequential round-trips) _docs/02_document/architecture.md § 6
AC-3.7 autopilot racing the delete by inserting a map_object AFTER step 1 reads zero rows leaves one orphan; small race window in single-operator workflow _docs/02_document/system-flows.md F3 error-scenario table

AC-4 — Waypoint create / read / update / delete (F4)

# Criterion Verification
AC-4.1 All routes are nested: GET/POST/PUT/DELETE /missions/{missionId}/waypoints[/{wpId}] MissionsController route attributes
AC-4.2 Parent mission missing → 404 (KeyNotFoundException) WaypointService.* initial existence check
AC-4.3 GET /missions/{id}/waypoints is unpaginated, ordered by OrderNum ASC (matches spec endpoint 6) WaypointService.GetWaypoints OrderBy(w => w.OrderNum)
AC-4.4 PUT /missions/{id}/waypoints/{wpId} is a full overwrite of every field even though the request DTO looks "partial-shaped" — non-nullable enums/numerics in UpdateWaypointRequest mean every field gets replaced (inconsistent with vehicle's nullable partial-update pattern) Services/WaypointService.cs UpdateWaypoint + DTOs/UpdateWaypointRequest.cs
AC-4.5 DELETE /missions/{id}/waypoints/{wpId} walks the same cascade as F3, scoped to one waypoint (detectionannotationsmediawaypoints) WaypointService.DeleteWaypoint
AC-4.6 Same NO-transaction caveat as AC-3.3 applies to waypoint delete WaypointService.DeleteWaypoint
AC-4.7 Every waypoint route requires JWT with permissions=FL claim [Authorize(Policy="FL")] on MissionsController

AC-5 — JWT bearer validation (F5)

# Criterion Verification
AC-5.1 Algorithm: HMAC-SHA256 (HS256) with SymmetricSecurityKey(UTF-8(JWT_SECRET)) Auth/JwtExtensions.cs
AC-5.2 ValidateLifetime = true; ClockSkew = TimeSpan.FromMinutes(1) (tighter than .NET's 5-minute default) Auth/JwtExtensions.cs
AC-5.3 ValidateIssuer = false and ValidateAudience = false — known CMMC L2 finding (suite-tracked under AZ-487 / AZ-494) Auth/JwtExtensions.cs; _docs/02_document/architecture.md § 7
AC-5.4 Missing Authorization header → 401 JwtBearerHandler
AC-5.5 Invalid signature → 401 HMAC verify fails
AC-5.6 Expired token (with 1-min skew applied) → 401 ValidateLifetime
AC-5.7 Token signed with old JWT_SECRET (rotation) → 401 across the entire device until coordinated re-deploy shared-secret model
AC-5.8 Valid signature + lifetime, but missing permissions=FL claim → 403 Policy "FL" evaluator (5_identity description.md)
AC-5.9 The local validator never calls back to admin; admin outage does NOT take this service down (until issued tokens expire) Auth/JwtExtensions.cs — pure local validation

AC-6 — Service startup + schema migration (F6)

# Criterion Verification
AC-6.1 Program.cs reads DATABASE_URL (env or fallback) → ConvertPostgresUrl → Npgsql connection string Program.cs ConvertPostgresUrl
AC-6.2 Program.cs reads JWT_SECRET (env or fallback) → AddJwtAuth(jwt) Program.cs AddJwtAuth
AC-6.3 DatabaseMigrator.Migrate runs ONCE at startup, INSIDE a single startup scope (not per-request) Program.cs using var scope = app.Services.CreateScope(); ... DatabaseMigrator.Migrate(db)
AC-6.4 Migrator runs CREATE TABLE IF NOT EXISTS for the 4 owned tables (vehicles, missions, waypoints, map_objects) and CREATE INDEX IF NOT EXISTS for 3 indexes Database/DatabaseMigrator.cs
AC-6.5 Migrator runs DROP TABLE IF EXISTS orthophotos; DROP TABLE IF EXISTS gps_corrections; (B9 one-shot, post-B9 only) Database/DatabaseMigrator.cs post-B9
AC-6.6 Migrator is idempotent — every startup runs the same statements; IF NOT EXISTS makes them safe to re-run Database/DatabaseMigrator.cs
AC-6.7 postgres-local unreachable at startup → process exits non-zero; Watchtower restarts the container; flight-gate prevents restart mid-mission Program.cs (no DB error swallow); suite arch doc
AC-6.8 azaion database does not exist → process exits with Npgsql 3D000; database creation is a provisioning concern, NOT this service suite-level concern
AC-6.9 After migrator, ErrorHandlingMiddleware is registered FIRST in the pipeline — wraps every subsequent middleware exception Program.cs middleware order
AC-6.10 Service serves on port 8080 inside the container (EXPOSE 8080); edge compose maps host 5002:8080 Dockerfile; suite _infra/_compose/

AC-7 — Health probe (F7)

# Criterion Verification
AC-7.1 GET /health is anonymous (no [Authorize]) Program.cs MapGet("/health")
AC-7.2 Returns 200 OK with body { "status": "healthy" } Results.Ok(new { status = "healthy" })
AC-7.3 Latency target: <10ms typical (no DB ping today — process-liveness only) Program.cs
AC-7.4 If pipeline is down, the probe fails at TCP-connect time and Watchtower restarts the container suite arch doc

AC-8 — Wire shape (HTTP contract)

# Criterion Verification
AC-8.1 Entity / DTO bodies serialize as PascalCase today (no JsonNamingPolicy.CamelCase configured) — divergent from suite spec (ADR-002 carry-forward) Program.cs (no JsonSerializerOptions.PropertyNamingPolicy); _docs/02_document/architecture.md ADR-002
AC-8.2 Error envelope is camelCase by accidental match — middleware writes new { statusCode, message } (lowercase property names preserved by System.Text.Json) Middleware/ErrorHandlingMiddleware.cs
AC-8.3 Error envelope misses the spec's errors: object? field today Middleware/ErrorHandlingMiddleware.cs
AC-8.4 The static ErrorResponse DTO is dead on the wire — middleware writes the anonymous object instead. If ErrorResponse were ever used, it would emit PascalCase + the wrong Errors shape (List<string>? instead of spec's object?) DTOs/ErrorResponse.cs
AC-8.5 ErrorHandlingMiddleware mapping: KeyNotFoundException → 404, ArgumentException → 400, InvalidOperationException → 409, fallthrough → 500 (with stack trace logged via LogError) Middleware/ErrorHandlingMiddleware.cs
AC-8.6 500 response body shows Internal server error (generic), NOT the stack trace; the stack trace is logged only Middleware/ErrorHandlingMiddleware.cs
AC-8.7 PaginatedResponse<T> has fields Items / TotalCount / Page / PageSize — PascalCase today, divergent from suite spec DTOs/PaginatedResponse.cs

AC-9 — Authorization (cross-cutting)

# Criterion Verification
AC-9.1 One named policy "FL" is registered in Auth/JwtExtensions.cs; satisfied by a permissions claim equal to "FL" Auth/JwtExtensions.cs
AC-9.2 The string "FL" is hardcoded in feature controllers — a typo silently turns into a permanent 403 (no compile-time check) Controllers/{Vehicles,Missions}Controller.cs; _docs/02_document/module-layout.md § Verification Needed #4
AC-9.3 The policy NAME "FL" retains the legacy "Flight" wording even after the service rename to missions — fleet-wide auth change deferred (NOT in this Epic) Auth/JwtExtensions.cs; ../../suite/_docs/00_roles_permissions.md TODO
AC-9.4 No per-method authz beyond [Authorize(Policy="FL")] — every protected endpoint has the same gate Controllers/{Vehicles,Missions}Controller.cs

AC-10 — Operational invariants

# Criterion Verification
AC-10.1 One container instance per device (vertical scale only) Dockerfile; suite arch doc
AC-10.2 RTO ≈ container restart time (~10s); RPO = device-local backup cadence (suite-level) suite arch doc
AC-10.3 Unhandled 500 exceptions are logged with stack trace via LogError(ex, "Unhandled exception") Middleware/ErrorHandlingMiddleware.cs
AC-10.4 No correlation id, no per-user audit log — supporting a production incident requires grep-by-timestamp _docs/02_document/architecture.md § 7
AC-10.5 The migrator's DROP TABLE IF EXISTS orthophotos / gps_corrections block (B9) MUST NOT run before gps-denied has migrated its own copy of those tables on the device — out-of-band ordering: deploy gps-denied first Database/DatabaseMigrator.cs post-B9; _docs/02_document/system-flows.md F6
AC-10.6 The cross-service cascade (media, annotations, detection) requires annotations and detection pipeline to have migrated their tables on the same device — abnormal deployment otherwise _docs/02_document/components/02_mission_planning/description.md Caveats #6