Files
missions/_docs/02_document/tests/traceability-matrix.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

229 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Traceability Matrix
> **Status**: produced by autodev `/test-spec` Phase 2 (2026-05-14).
> **Naming**: post-rename target. Tests written for the post-rename API surface — RED-status until B5B8 land. The traceability matrix below treats the documented spec as the source of truth.
## 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) | FT-P-04 | Covered |
| AC-1.6 | Filter case-sensitive on `name`, exact on `isDefault` | FT-P-05, FT-N-01 | 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>` | FT-P-08, 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 → 500 | NOT directly covered as a separate test (deterministic reproduction is hard); falls under NFT-RES-08-style probabilistic family | 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 | HS256 + `SymmetricSecurityKey(UTF-8(JWT_SECRET))` | covered indirectly via NFT-SEC-02 (different secret rejected) and NFT-SEC-03 (correct secret accepted) | Covered |
| AC-5.2 | `ValidateLifetime=true`, `ClockSkew=1min` | NFT-SEC-03 | Covered |
| AC-5.3 | `ValidateIssuer=false`, `ValidateAudience=false` (today) | NFT-SEC-04 | Covered (locks today's behavior) |
| AC-5.4 | Missing header → 401 | NFT-SEC-01 | Covered |
| AC-5.5 | Invalid signature → 401 | NFT-SEC-02 | Covered |
| AC-5.6 | Expired token (outside skew) → 401 | NFT-SEC-03 | Covered |
| AC-5.7 | Old `JWT_SECRET` after rotation → 401 | NFT-RES-07 | Covered |
| AC-5.8 | Missing `permissions=FL` claim → 403 | NFT-SEC-05 | Covered |
| AC-5.9 | Local validator never calls `admin` | NOT directly observable from outside the process; covered indirectly by `admin` not running in the test env (NFT-SEC-* still pass) | Partially covered |
### AC-6 — Startup + migration
| AC ID | Acceptance Criterion (short) | Test IDs | Coverage |
|-------|------------------------------|----------|----------|
| AC-6.1 | DATABASE_URL URL form converted via `ConvertPostgresUrl` | covered indirectly via every test that depends on a working DB connection (compose env uses URL form) | Covered |
| AC-6.2 | DATABASE_URL raw form accepted | NOT directly covered — the test environment uses URL form; can be added by an extra startup scenario with the raw form | NOT COVERED — see Uncovered Items §3 |
| 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-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, satisfied by `permissions == "FL"` | every protected-endpoint test | Covered |
| AC-9.2 | Hardcoded string mismatch ("fl", "FLight") → 403 | NFT-SEC-06 | 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 (H1H6)
| 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-0104 (observe baseline so suite-level cgroups can be sized correctly) | Covered |
### Software (S1S15)
| Restriction ID | Restriction (short) | Test IDs | Coverage |
|---------------|---------------------|----------|----------|
| S1 | C# / .NET 10 | implicit in test environment | Implicitly covered |
| S2 | ASP.NET Core | implicit | Implicitly covered |
| S3S5 | 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 §5 |
| 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 (E1E10)
| Restriction ID | Restriction (short) | Test IDs | Coverage |
|---------------|---------------------|----------|----------|
| E1 | Two required env vars | implicit; NFT-RES-05 (DB unreachable) + NFT-SEC-* (JWT_SECRET behavior) | Covered |
| E2 | DATABASE_URL accepts URL or raw form | URL form covered via NFT-RES-05 path; raw form NOT covered | NOT COVERED — see Uncovered Items §3 |
| E3 | Hardcoded dev fallbacks NOT gated on IsDevelopment() | a startup test with NO env vars set could verify fallback boot — security risk gate; carry-forward | NOT COVERED — see Uncovered Items §6 |
| E4 | JWT_SECRET shared across services | suite-level concern | Out of scope |
| 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 `AllowAnyOrigin/Method/Header` | could add a single CORS preflight test that asserts the documented permissive behavior | NOT COVERED — see Uncovered Items §7 |
| E10 | TLS termination is suite reverse proxy | suite-level concern | Out of scope |
### Operational (O1O10)
| 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 | 9 | 8 | 1 | 0 | 0 | 100% |
| AC-6 Startup + migration | 10 | 8 | 1 | 1 (AC-6.2 raw conn) | 0 | 90% |
| 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 | 1 | 1 | 3 (E2, E3, E9) | 5 | 60% (in-scope) |
| Restrictions O | 10 | 4 | 2 | 0 | 4 | 100% (in-scope) |
| **Total** | 112 | 67 | 11 | 6 | 28 | **93%** 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 that doesn't exist today (instrumented test build with `pg_advisory_lock`) | 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-6.2 / E2 — `DATABASE_URL` raw form path | Test env uses URL form; raw form is the alternate adapter branch | Low — branch is small, well-localised in `ConvertPostgresUrl` | Add a single startup scenario with raw form. Single-line config change in test compose. |
| 4 | 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) |
| 5 | S6 — Swagger NOT gated on `IsDevelopment()` | 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. |
| 6 | E3 — Hardcoded dev fallbacks NOT gated | Carry-forward security finding | Medium — production deploy without env vars boots with well-known secret | Add a startup test with NO env vars set, assert `JWT_SECRET` claim ladder still works (locks the divergence). Suggest as follow-up. |
| 7 | E9 — CORS `AllowAnyOrigin/Method/Header` | Carry-forward; assumed safe behind reverse proxy | Low — assumed deployment topology mitigates | Add CORS preflight test that locks current behavior. Suggest as follow-up. |
**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). Items 3, 5, 6, 7 are 1-test additions each — add them 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**: 93% in-scope.
**Verdict**: **PASS** — Phase 3 gate cleared on first iteration. The 6 uncovered items above are all low-medium risk with documented mitigations.