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.
20 KiB
Traceability Matrix
Status: produced by autodev
/test-specPhase 2 (2026-05-14); re-issued in cycle-update mode after the targeted re-verification of 2026-05-14 (drift findings Phase 2). Naming: post-rename target. Tests written for the post-rename API surface — RED-status until B5–B8 land. The traceability matrix below treats the documented spec as the source of truth. Drift correction: rows for AC-5, AC-6, AC-9, AC-1.5/1.6, AC-2.3, E1/E3/E4/E9 are updated below to reflect the ECDSA+JWKS JWT model, fail-fast configuration resolver, and CORS production-gate validator. SeveralNOT COVEREDitems in the pre-revision matrix are nowCoveredthanks to the new NFT-SEC-10..13 + NFT-RES-05 rewrite + the inverted FT-N-01.
Acceptance Criteria Coverage
AC-1 — Vehicle CRUD
| AC ID | Acceptance Criterion (short) | Test IDs | Coverage |
|---|---|---|---|
| AC-1.1 | Create vehicle | FT-P-01 | Covered |
| AC-1.2 | Default-clear on create/update/setDefault | FT-P-02, FT-P-03 | Covered |
| AC-1.3 | "Exactly one default" stricter than spec (B12 pending) | covered indirectly via FT-P-02, FT-P-03 (assertions on count == 1) |
Covered (carry-forward) |
| AC-1.4 | Default-clear NOT transaction-wrapped → race | NFT-RES-08 | Covered (probabilistic) |
| AC-1.5 | GET /vehicles is plain array (NO pagination), ordered by Name ASC |
FT-P-04 | Covered |
| AC-1.6 | Filter case-INSENSITIVE on name, exact on isDefault |
FT-P-05 (positive + lowercase), FT-N-01 (no-match negative) | Covered |
| AC-1.7 | GET /vehicles/{id} 404 | FT-N-02 | Covered |
| AC-1.8 | DELETE /vehicles/{id} 409 if referenced | FT-N-03 | Covered |
| AC-1.9 | All /vehicles/* require Policy="FL" |
NFT-SEC-01, NFT-SEC-05, NFT-SEC-06 | Covered |
AC-2 — Mission CRUD
| AC ID | Acceptance Criterion (short) | Test IDs | Coverage |
|---|---|---|---|
| AC-2.1 | Create mission, default CreatedDate = UtcNow |
FT-P-07 | Covered |
| AC-2.2 | Non-existent VehicleId → 400 (today; spec wants 404) | FT-N-04 | Covered (carry-forward) |
| AC-2.3 | GET /missions paginated PaginatedResponse<Mission>, ordered by CreatedDate DESC, name filter case-INSENSITIVE |
FT-P-08 (ordering + case-INSENSITIVE), FT-P-09, FT-P-10, NFT-PERF-04 | Covered |
| AC-2.4 | GET /missions/{id} 404 | FT-N-05 | Covered |
| AC-2.5 | PUT partial update (Name update only) | FT-P-11 | Covered |
| AC-2.6 | LinqToDB does NOT eager-load [Association] |
covered indirectly via FT-P-07/FT-P-11 (body shape assertion checks Vehicle == null, Waypoints == null/[]) |
Covered |
| AC-2.7 | All /missions/* require Policy="FL" |
NFT-SEC-01 | Covered |
| AC-2.8 | TOCTOU on FK between existence check and insert — now PARTLY mitigated by DB-level FK (PG 23503); surface today is 500 |
NOT directly covered as a separate test (deterministic reproduction still requires controllable concurrency); the FK mitigation is observable indirectly via 6.10 startup-schema test asserting REFERENCES vehicles(id) exists |
NOT COVERED — see Uncovered Items §1 |
AC-3 — Mission cascade delete F3 (most critical)
| AC ID | Acceptance Criterion (short) | Test IDs | Coverage |
|---|---|---|---|
| AC-3.1 | Cascade walks map_objects → detection → annotations → media → waypoints → missions |
FT-P-12 | Covered |
| AC-3.2 | Mission missing → 404 BEFORE any cascade DELETE | FT-N-06 | Covered |
| AC-3.3 | Cascade NOT transaction-wrapped → orphans | NFT-RES-01, NFT-RES-02 | Covered |
| AC-3.4 | relation does not exist → 500 + log |
NFT-RES-01 (uses media drop) |
Covered |
| AC-3.5 | After B7 cascade does NOT touch orthophotos / gps_corrections |
covered via NFT-RES-04 (post-B9 build asserts tables absent); cascade does not reference them by construction (verified by code-level absence at Step 8) | Partially covered |
| AC-3.6 | <50ms typical (P50) | NFT-PERF-01 | Covered |
| AC-3.7 | autopilot race after step 1 → orphan | NFT-RES-08-style (orphan race, on map_objects insert) — design spec'd; probabilistic implementation deferred |
NOT COVERED — see Uncovered Items §2 |
AC-4 — Waypoint CRUD F4
| AC ID | Acceptance Criterion (short) | Test IDs | Coverage |
|---|---|---|---|
| AC-4.1 | Routes nested under /missions/{id}/waypoints[/{wpId}] |
FT-P-13, FT-P-14, FT-P-15, FT-P-18, FT-N-07 (every endpoint exercised) | Covered |
| AC-4.2 | Parent missing → 404 | FT-N-07 | Covered |
| AC-4.3 | GET unpaginated, ordered by OrderNum ASC |
FT-P-13 | Covered |
| AC-4.4 | PUT is full overwrite | FT-P-15 | Covered |
| AC-4.5 | Scoped cascade (detection → annotations → media → waypoints) | FT-P-18 | Covered |
| AC-4.6 | Same NO-transaction caveat | NFT-RES-02 | Covered |
| AC-4.7 | Require Policy="FL" |
NFT-SEC-01, NFT-SEC-05 | Covered |
AC-5 — JWT validation
| AC ID | Acceptance Criterion (short) | Test IDs | Coverage |
|---|---|---|---|
| AC-5.1 | ECDSA-SHA256 with ValidAlgorithms = [EcdsaSha256] (algorithm pin) |
NFT-SEC-02 (signature reject), NFT-SEC-10 (HS256-confusion defense) | Covered |
| AC-5.2 | ValidateLifetime=true, ClockSkew=30s |
NFT-SEC-03 | Covered |
| AC-5.3 | ValidateIssuer=true + ValidateAudience=true (CMMC L2 row 3 structurally fixed in this service) |
NFT-SEC-04, NFT-SEC-04b | Covered |
| AC-5.4 | Missing header → 401 | NFT-SEC-01 | Covered |
| AC-5.5 | Invalid signature / no matching public key → 401 | NFT-SEC-02 | Covered |
| AC-5.6 | Expired token (outside 30s skew) → 401 | NFT-SEC-03 | Covered |
| AC-5.7 | JWKS key rotation without restart — old kid eventually rejected, new kid eventually accepted | NFT-RES-07, NFT-SEC-11 | Covered |
| AC-5.8 | Missing permissions=FL claim → 403 |
NFT-SEC-05 | Covered |
| AC-5.9 | Request-path validation local after JWKS cached; cold-start synchronously fetches JWKS | NFT-SEC-* all pass with admin not running (only jwks-mock runs); the cold-start failure mode is testable by stopping jwks-mock and restarting missions then issuing the first protected request |
Covered (covered-cold-start case under results_report row 5.10) |
| AC-5.10 | Algorithm pin (alg ∉ [EcdsaSha256] → 401) |
NFT-SEC-10 | Covered |
| AC-5.11 | iss validation (iss != JWT_ISSUER → 401) |
NFT-SEC-04 | Covered |
| AC-5.12 | aud validation (aud != JWT_AUDIENCE → 401) |
NFT-SEC-04b | Covered |
AC-6 — Startup + migration
| AC ID | Acceptance Criterion (short) | Test IDs | Coverage |
|---|---|---|---|
| AC-6.1 | Four required env vars resolved via ResolveRequiredOrThrow (env-first, then IConfiguration, else throw); URL form converted via ConvertPostgresUrl |
results_report 6.1, 6.1b, 6.1c; NFT-SEC-12; NFT-RES-05 | Covered |
| AC-6.2 | DATABASE_URL raw form accepted; no JWT_SECRET legacy env consulted |
results_report 6.2; NFT-SEC-12 row 4 (asserts JWT_JWKS_URL is consulted, not JWT_SECRET) |
Covered |
| AC-6.3 | Migrator runs ONCE at startup, inside scope | NFT-RES-03 (idempotency assertion implies single-run + safe-restart) | Partially covered |
| AC-6.4 | 4 owned tables + 3 indexes created | NFT-RES-03 (asserts schema via \d+ after first start) |
Covered |
| AC-6.5 | Post-B9 one-shot legacy DROP TABLE IF EXISTS |
NFT-RES-04 | Covered |
| AC-6.6 | Migrator idempotent | NFT-RES-03 | Covered |
| AC-6.7 | DB unreachable → process exits non-zero | NFT-RES-05 | Covered |
| AC-6.8 | DB missing (3D000) → process exits | NFT-RES-06 | Covered |
| AC-6.9 | ErrorHandlingMiddleware registered FIRST |
covered indirectly via FT-N-08 + NFT-SEC-08 (any unhandled exception produces the documented envelope) | Covered |
| AC-6.10 | Listens on port 8080; edge maps host 5002:8080 |
covered by every test that connects to port 5002→8080 | Covered |
| AC-6.11 | CORS Production-gate fail-fast (empty allow-list + AllowAnyOrigin != true → throw) |
NFT-SEC-13; results_report 6.10–6.13 | Covered |
| AC-6.12 | JWT_JWKS_URL HTTPS-only at fetch time (passes startup config) |
NFT-SEC-12 row 5; results_report 6.1c | Covered |
AC-7 — Health probe
| AC ID | Acceptance Criterion (short) | Test IDs | Coverage |
|---|---|---|---|
| AC-7.1 | GET /health anonymous |
FT-P-16, NFT-SEC-07 | Covered |
| AC-7.2 | 200 { "status": "healthy" } |
FT-P-16, FT-P-17 | Covered |
| AC-7.3 | <10ms typical | NFT-PERF-03 | Covered |
| AC-7.4 | If pipeline down, TCP connect fails (Watchtower restarts) | container-lifecycle behavior outside the service; out-of-scope at the service test level | Out of scope — see Uncovered Items §4 |
AC-8 — Wire shape
| AC ID | Acceptance Criterion (short) | Test IDs | Coverage |
|---|---|---|---|
| AC-8.1 | Entity bodies PascalCase | FT-P-01, FT-P-04 (key-set assertion) | Covered |
| AC-8.2 | Error envelope camelCase by accidental match | FT-N-02, FT-N-05 (key-set assertion includes statusCode, message) |
Covered |
| AC-8.3 | Envelope MUST NOT include errors field |
FT-N-02 (key-set excludes errors), FT-N-05, NFT-SEC-08 |
Covered |
| AC-8.4 | KeyNotFoundException → 404 | FT-N-02, FT-N-05, FT-N-07 | Covered |
| AC-8.5 | ArgumentException → 400, InvalidOperationException → 409 | FT-N-04 (400), FT-N-03 (409) | Covered |
| AC-8.6 | 500 body redacted, stack only in log | FT-N-08, NFT-SEC-08 | Covered |
| AC-8.7 | PaginatedResponse<T> PascalCase keys |
FT-P-08 (key-set assertion) | Covered |
AC-9 — Authorization
| AC ID | Acceptance Criterion (short) | Test IDs | Coverage |
|---|---|---|---|
| AC-9.1 | Policy "FL" registered as RequireClaim("permissions", "FL") — contains-match: a multi-permission token containing "FL" is accepted |
every protected-endpoint test + NFT-SEC-06 step 7-8 (multi-permission accepted) | Covered |
| AC-9.2 | Hardcoded string mismatch ("fl", "FLight", "ADMIN") → 403 | NFT-SEC-06 steps 2/4/6 | Covered |
| AC-9.3 | Policy NAME "FL" retains legacy wording (deferred) |
not testable at runtime — documentation-only | Documentation only |
| AC-9.4 | No per-method authz beyond [Authorize(Policy="FL")] |
covered by NFT-SEC-01 + NFT-SEC-07 (every endpoint gets the same gate; health is the only exception) | Covered |
AC-10 — Operational invariants (API-observable subset)
| AC ID | Acceptance Criterion (short) | Test IDs | Coverage |
|---|---|---|---|
| AC-10.1 | One container per device | container-orchestration constraint, not API-observable; tracked under H1 + H3 | Out of scope (suite-level) |
| AC-10.2 | RTO ≈ container restart, RPO = device-local backup | suite-level operational concern | Out of scope |
| AC-10.3 | Unhandled 500 logged with stack trace via LogError |
FT-N-08, NFT-SEC-08 | Covered |
| AC-10.4 | No correlation id, no per-user audit | absence-of-feature; NOT directly testable; documented carry-forward | Documentation only |
| AC-10.5 | B9 DROP block ordered AFTER gps-denied migrated |
suite-level deploy ordering, NOT enforced by this service | Out of scope (suite-level) |
| AC-10.6 | Cross-service cascade requires sibling tables present | NFT-RES-01 covers the failure mode (table dropped) — passes when failure produces 500 + partial deletes | Covered |
Restrictions Coverage
Hardware (H1–H6)
| Restriction ID | Restriction (short) | Test IDs | Coverage |
|---|---|---|---|
| H1 | Edge-device, one container per device | NFT-RES-LIM-01 through NFT-RES-LIM-04 (resource budget aligned with device assumption) | Indirectly covered |
| H2 | Multi-arch (ARM64 + AMD64) | NOT testable at the test-spec level — covered by suite-level CI matrix (.woodpecker/build-arm.yml + future build-amd.yml) |
Out of scope |
| H3 | Vertical scale only | implicit in test environment (single missions container) |
Implicitly covered |
| H4 | No managed cloud | architectural constraint; not testable | Documentation only |
| H5 | Watchtower + flight-gate | suite-level orchestration | Out of scope |
| H6 | No container-internal resource limits | NFT-RES-LIM-01–04 (observe baseline so suite-level cgroups can be sized correctly) | Covered |
Software (S1–S15)
| Restriction ID | Restriction (short) | Test IDs | Coverage |
|---|---|---|---|
| S1 | C# / .NET 10 | implicit in test environment | Implicitly covered |
| S2 | ASP.NET Core | implicit | Implicitly covered |
| S3–S5 | Library versions | csproj / lockfile concern; NOT a behavioral test | Out of scope (build-time check) |
| S6 | Swagger NOT gated on IsDevelopment() |
NOT directly tested; could add a single GET /swagger test that asserts 200 in production-like env. Carry-forward divergence |
NOT COVERED — see Uncovered Items §4 |
| S7 | PostgreSQL only | implicit | Implicitly covered |
| S8 | One csproj, one root namespace | csproj structure; NOT a behavioral test | Out of scope (code organization) |
| S9 | No src/ directory |
repo layout; NOT a behavioral test | Out of scope |
| S10 | Layer-organized | code organization; NOT a behavioral test | Out of scope |
| S11 | No automated tests today | this entire spec converts S11 from a constraint into a goal | Resolved by this spec |
| S12 | No migration tool | NFT-RES-03 + NFT-RES-04 (idempotency observed) | Covered |
| S13 | No in-process MQ / event bus | architectural constraint | Out of scope (architecture) |
| S14 | Owned + borrowed tables | covered by FT-P-12, FT-P-18 (cascade walks both owned and borrowed) | Covered |
| S15 | gps-denied decoupled |
covered indirectly by NFT-RES-04 (legacy tables absent post-B9) + AC-3.5 absence of cascade reference | Covered |
Environment (E1–E10)
| Restriction ID | Restriction (short) | Test IDs | Coverage |
|---|---|---|---|
| E1 | Four required env vars (DATABASE_URL, JWT_ISSUER, JWT_AUDIENCE, JWT_JWKS_URL) — fail-fast via ResolveRequiredOrThrow |
NFT-SEC-12 (all 4 rows), NFT-RES-05 (all 5 rows), results_report 6.1b | Covered |
| E2 | DATABASE_URL accepts URL or raw form | URL form covered via every default test; raw form covered by results_report 6.2 | Covered |
| E3 | No hardcoded dev fallbacks — ResolveRequiredOrThrow throws |
NFT-SEC-12, NFT-RES-05 rows 1-5 | Covered |
| E4 | Asymmetric ECDSA: no shared secret on this side; only public-key configuration | NFT-SEC-* all run against jwks-mock (the mock holds the private key, this service holds only public-key config) |
Covered |
| E5 | Container EXPOSE 8080; edge maps 5002:8080 | implicit | Implicitly covered |
| E6 | Image tag post-B10 | build-time concern, not behavior | Out of scope |
| E7 | Entrypoint post-B5 | build-time concern | Out of scope |
| E8 | No appsettings env-specific overrides | code organization; NOT a behavioral test | Out of scope |
| E9 | CORS gated by CorsConfigurationValidator — Production throws on empty allow-list |
NFT-SEC-13 (all 5 rows), results_report 6.10–6.13 | Covered |
| E10 | TLS termination is suite reverse proxy; JWKS independently constrained to HTTPS | NFT-SEC-12 row 5 (JWKS HTTPS-only) | Covered (HTTPS half) |
Operational (O1–O10)
| Restriction ID | Restriction (short) | Test IDs | Coverage |
|---|---|---|---|
| O1 | Migrator at every start, idempotent | NFT-RES-03, NFT-RES-04 | Covered |
| O2 | flight-gate prevents restart mid-mission | suite-level orchestration | Out of scope |
| O3 | No version table | covered indirectly by NFT-RES-03 (no version-table query observed) | Implicitly covered |
| O4 | Single Woodpecker job, no test/security stage | this spec adds a test stage as a follow-up artifact | Resolved by this spec |
| O5 | No structured logging | absence-of-feature; NOT testable directly | Documentation only |
| O6 | No correlation id, no audit | absence-of-feature | Documentation only |
| O7 | Health is process-liveness only | FT-P-17 (PG stopped, health still 200) | Covered |
| O8 | Cascade NOT transaction-wrapped | NFT-RES-01, NFT-RES-02 | Covered |
| O9 | Sibling table absent → cascade fails on relation does not exist |
NFT-RES-01 (uses media drop) |
Covered |
| O10 | One-instance-per-device → no cluster awareness | architectural constraint | Documentation only |
Coverage Summary
| Category | Total Items | Covered | Partially / Implicit | Not Covered | Out of Scope / Doc-only | Coverage % (Covered + Partial of in-scope) |
|---|---|---|---|---|---|---|
| AC-1 Vehicle CRUD | 9 | 8 | 1 (carry-forward) | 0 | 0 | 100% |
| AC-2 Mission CRUD | 8 | 7 | 0 | 1 (AC-2.8 TOCTOU) | 0 | 87% |
| AC-3 Cascade F3 | 7 | 5 | 1 | 1 (AC-3.7 race) | 0 | 86% |
| AC-4 Waypoint CRUD F4 | 7 | 7 | 0 | 0 | 0 | 100% |
| AC-5 JWT | 12 | 12 | 0 | 0 | 0 | 100% |
| AC-6 Startup + migration | 12 | 12 | 0 | 0 | 0 | 100% |
| AC-7 Health | 4 | 3 | 0 | 0 | 1 | 100% (in-scope) |
| AC-8 Wire shape | 7 | 7 | 0 | 0 | 0 | 100% |
| AC-9 Authz | 4 | 3 | 0 | 0 | 1 | 100% (in-scope) |
| AC-10 Operational | 6 | 1 | 0 | 0 | 5 | 100% (in-scope) |
| Restrictions H | 6 | 1 | 2 | 0 | 3 | 100% (in-scope) |
| Restrictions S | 15 | 4 | 2 | 0 | 9 | 100% (in-scope) |
| Restrictions E | 10 | 7 | 1 | 0 | 2 | 100% (in-scope) |
| Restrictions O | 10 | 4 | 2 | 0 | 4 | 100% (in-scope) |
| Total | 117 | 78 | 10 | 3 | 26 | 97% in-scope |
Uncovered Items Analysis
| # | Item | Reason Not Covered | Risk | Mitigation |
|---|---|---|---|---|
| 1 | AC-2.8 — TOCTOU on FK between existence check and insert | Deterministic reproduction requires controllable concurrency primitive (instrumented test build with pg_advisory_lock). Note: DB-level FK now produces PG error 23503 so the failure surface is consistent — only the timing of the race is hard to reproduce |
Low — failure mode is well-documented and produces a 500 (loud failure, not silent corruption); occurs only when admin races with create | Add probabilistic test (similar to NFT-RES-08) under a follow-up ticket. Document as known carry-forward. |
| 2 | AC-3.7 — autopilot orphan race on map_objects insert after step-1 read |
Same as #1 — needs controllable concurrency | Low — leaves at most one orphan row per race; cleanup on next mission delete or via manual sweep | Same mitigation as #1; add to follow-up. |
| 3 | AC-7.4 — TCP connect fails on container down (Watchtower restarts) | Container lifecycle outside service surface | None at service level — testable at suite e2e level | Cover at suite e2e (monorepo-e2e skill scope) |
| 4 | S6 — Swagger NOT gated on IsDevelopment() (surviving branch of ADR-005) |
Carry-forward security finding; not part of AC | Medium — production deploy with Swagger exposed | Add a single test GET /swagger/index.html returns 200 in test env, with explicit comment that this LOCKS the carry-forward divergence (will fail when remediated). Suggest as follow-up. |
Resolved by the 2026-05-14 re-verification:
- E3 (hardcoded dev fallbacks) — structurally fixed in code via
ResolveRequiredOrThrow. Old "Uncovered §6" obsolete; now Covered by NFT-SEC-12 + NFT-RES-05. - E9 (CORS
AllowAnyOrigin/Method/Headerin all environments) — structurally fixed byCorsConfigurationValidator. Old "Uncovered §7" obsolete; now Covered by NFT-SEC-13. - AC-6.2 / E2 (
DATABASE_URLraw form path) — covered by results_report row 6.2 as part of the cycle-update; no longer a gap.
Recommendation: items 1, 2 are deterministic-test improvements to land alongside a future transaction-wrap refactor (closes the carry-forward at the same time as the test improvement). Item 4 is a 1-test addition — add in Step 5 (Decompose Tests) under a "blackbox-lock-carry-forward" task.
Phase 3 Coverage Gate
Threshold: ≥ 75% (per cursor-meta.mdc Quality Thresholds + phases/03-data-validation-gate.md).
Achieved: 97% in-scope (after the 2026-05-14 drift Phase 2 re-issue).
Verdict: PASS — Phase 3 gate cleared. The 4 remaining uncovered items are all low-medium risk with documented mitigations; the previous E3 / E9 / AC-6.2 gaps were closed by the structural code fixes already in Infrastructure/ConfigurationResolver.cs and Infrastructure/CorsConfigurationValidator.cs.