Files
missions/_docs/00_problem/acceptance_criteria.md
T
Oleksandr Bezdieniezhnykh 78dea8ebab
ci/woodpecker/push/build-arm Pipeline was successful
chore: update configuration and Docker setup for JWT and test results
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.
2026-05-15 03:23:23 +03:00

19 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) ordered by Name ASC VehicleService.GetVehicles OrderBy(a => a.Name)
AC-1.6 GET /vehicles?name=&isDefault= filters case-INSENSITIVELY on Name (LinqToDB renders LOWER(name) LIKE %lower(input)%) and exactly on IsDefault VehicleService.GetVehicles a.Name.ToLower().Contains(query.Name.ToLower())
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), ordered by CreatedDate DESC (newest first); name filter is case-INSENSITIVE (LOWER(name) LIKE %lower(input)%) MissionService.GetMissions OrderByDescending(f => f.CreatedDate); f.Name.ToLower().Contains(query.Name.ToLower()); 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 is partly mitigated by DB-level FKmissions.vehicle_id REFERENCES vehicles(id) causes PostgreSQL to reject the insert with error code 23503 if the parent was deleted between check and insert. Surface today: Npgsql PostgresException (code 23503) → ErrorHandlingMiddleware fallthrough → 500 (UX gap — spec wants 400). Mitigation in app code (wrap check + insert in a transaction OR map 23503 to 400) is carry-forward — tracked in _docs/02_document/components/02_mission_planning/description.md Caveats MissionService.CreateMission; Database/DatabaseMigrator.cs (FK declaration)

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 Create: parent mission missing → 404 (KeyNotFoundException("Mission not found")) via an explicit db.Missions.AnyAsync(m => m.Id == missionId) check before insert. Update / Delete: the check is collapsed into a single composite WHERE w.MissionId == missionId AND w.Id == waypointId predicate; if no row matches (parent missing OR child missing OR mismatched parent/child pair) → 404 with the same Waypoint not found message. The two error cases (parent vs child) are NOT distinguishable from the response WaypointService.{CreateWaypoint, UpdateWaypoint, DeleteWaypoint}
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: ECDSA-SHA256 asymmetric signature validation against public keys retrieved from admin's JWKS. ValidAlgorithms = [SecurityAlgorithms.EcdsaSha256] is pinned — defends against HS256-confusion (an attacker who learns the JWKS public key cannot forge tokens with alg: HS256 using that key as the HMAC secret) Auth/JwtExtensions.cs TokenValidationParameters.ValidAlgorithms
AC-5.2 ValidateLifetime = true; ClockSkew = TimeSpan.FromSeconds(30) (tighter than .NET's 5-minute default and tighter than the legacy 1-minute setting) Auth/JwtExtensions.cs ClockSkew = TimeSpan.FromSeconds(30)
AC-5.3 ValidateIssuer = true with ValidIssuer = <resolved JWT_ISSUER>; ValidateAudience = true with ValidAudience = <resolved JWT_AUDIENCE>. The CMMC L2 row 3 finding is structurally fixed in this service's code; the suite-level docs may still describe the legacy "iss/aud disabled" model and have a separate sync task pending Auth/JwtExtensions.cs
AC-5.4 Missing Authorization header on a [Authorize] route → 401 JwtBearerHandler
AC-5.5 Invalid signature → 401 (ECDSA verify fails against every cached public key whose kid matches the token header) Auth/JwtExtensions.cs IssuerSigningKeyResolver + JwtBearerHandler
AC-5.6 Expired token (with 30s skew applied) → 401 ValidateLifetime = true
AC-5.7 Token's kid not in cached JWKS → 401. JWKS rotation publishes a new kid; the cached manager refreshes on the default schedule (matches admin's Cache-Control: public, max-age=3600). No coordinated redeploy is needed for rotation ConfigurationManager<JsonWebKeySet> refresh
AC-5.8 Valid signature + lifetime + iss + aud, but missing permissions=FL claim → 403 Policy "FL" evaluator (05_identity/description.md)
AC-5.9 Request-path validation does NOT call admin; only the first protected request after a cold start triggers a synchronous JWKS HTTPS GET against JWT_JWKS_URL (which must be HTTPS — HttpDocumentRetriever { RequireHttps = true }). Once cached, the manager refreshes on its default schedule. admin outage AFTER the JWKS has been cached does NOT take this service down until cache + tokens expire; admin outage AT the time of the first JWKS fetch causes the first protected request to fail 500 Auth/JwtExtensions.cs ConfigurationManager<JsonWebKeySet>
AC-5.10 Token header with alg ∉ [EcdsaSha256] (e.g. forged alg: HS256, or genuine but unsupported alg: RS256) → 401 — algorithm pin defense Auth/JwtExtensions.cs ValidAlgorithms
AC-5.11 iss claim ≠ resolved JWT_ISSUER → 401 Auth/JwtExtensions.cs ValidateIssuer + ValidIssuer
AC-5.12 aud claim ≠ resolved JWT_AUDIENCE → 401 Auth/JwtExtensions.cs ValidateAudience + ValidAudience

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

# Criterion Verification
AC-6.1 Program.cs resolves four required configuration values via Infrastructure/ConfigurationResolver.csResolveRequiredOrThrow: DATABASE_URL, JWT_ISSUER, JWT_AUDIENCE, JWT_JWKS_URL. Resolution order per key is env-var-first, then IConfiguration config key (Database:Url / Jwt:Issuer / Jwt:Audience / Jwt:JwksUrl), else THROW InvalidOperationException at startup. No hardcoded development fallbacks — ADR-005's "dev fallback secret" branch is obsolete; only the Swagger-unconditional branch remains Program.cs, Infrastructure/ConfigurationResolver.cs
AC-6.2 Program.cs calls AddJwtAuth(issuer, audience, jwksUrl) (NOT AddJwtAuth(secret)). The legacy JWT_SECRET env var / config key is no longer consulted anywhere in the codebase. JWKS is fetched lazily on the first protected request via Microsoft.IdentityModel.Protocols.ConfigurationManager<JsonWebKeySet> with HttpDocumentRetriever { RequireHttps = true } Program.cs, Auth/JwtExtensions.cs
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) using PostgreSQL TIMESTAMP (no timezone) for date columns, with explicit REFERENCES for FKs (missions.vehicle_id → vehicles(id), waypoints.mission_id → missions(id), map_objects.waypoint_id → waypoints(id)), 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; unconditionally (B9 one-shot kept idempotent indefinitely — re-running on a fresh DB is a no-op) Database/DatabaseMigrator.cs
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-6.11 CORS is gated by Infrastructure/CorsConfigurationValidator.cs at startup: in Production (case-insensitive on ASPNETCORE_ENVIRONMENT) the host THROWS when CorsConfig:AllowedOrigins is empty AND CorsConfig:AllowAnyOrigin != true; in non-Production environments the same empty allow-list falls back to permissive (AllowAnyOrigin/Method/Header) AND emits a PermissiveDefaultWarning startup log. The pre-B11 "all environments permissive" assumption no longer holds Program.cs, Infrastructure/CorsConfigurationValidator.cs
AC-6.12 The JWKS HTTPS-only constraint (HttpDocumentRetriever { RequireHttps = true }) means a misconfigured JWT_JWKS_URL = http://... will pass startup config resolution (any non-empty string is accepted by ResolveRequiredOrThrow) but cause the first protected request to fail at JWKS-fetch time → 500. Detected only at runtime, not at startup Auth/JwtExtensions.cs HttpDocumentRetriever

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 containing "FL" (AuthorizationPolicyBuilder.RequireClaim("permissions", "FL") matches when ANY permissions claim value equals "FL", so a multi-permission token permissions: ["FL","SOMETHING_ELSE"] is accepted) Auth/JwtExtensions.cs AddPolicy("FL")
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