"""Tests for canonical MITRE kill-chain ordering in coverage-by-tactic. Block 3 fix: TacticCoverageChart.tsx (dashboard) rendered whatever order the backend happened to return (alphabetical, and only tactics with seeded techniques), while ExecutiveDashboardPage.tsx re-sorted client-side into the correct kill-chain order. get_coverage_by_tactic now returns the canonical order — and all 14 tactics, zero-filled — so both consumers get consistent results without needing their own reordering logic. """ from app.models.technique import Technique from app.services.metrics_query_service import MITRE_TACTIC_ORDER, get_coverage_by_tactic def test_coverage_by_tactic_returns_canonical_kill_chain_order(db): # Seed a few tactics out of order to prove the response isn't alphabetical. db.add(Technique(mitre_id="T3001", name="A", tactic="impact")) db.add(Technique(mitre_id="T3002", name="B", tactic="execution")) db.add(Technique(mitre_id="T3003", name="C", tactic="reconnaissance")) db.commit() rows = get_coverage_by_tactic(db) tactics = [r.tactic for r in rows] canonical_present = [t for t in tactics if t in MITRE_TACTIC_ORDER] assert canonical_present == MITRE_TACTIC_ORDER def test_coverage_by_tactic_zero_fills_tactics_with_no_techniques(db): db.add(Technique(mitre_id="T3004", name="D", tactic="execution")) db.commit() rows = get_coverage_by_tactic(db) by_tactic = {r.tactic: r for r in rows} assert "persistence" in by_tactic assert by_tactic["persistence"].total == 0 assert by_tactic["execution"].total == 1 def test_coverage_by_tactic_unknown_tactic_sorts_last(db): db.add(Technique(mitre_id="T3005", name="E", tactic=None)) db.commit() rows = get_coverage_by_tactic(db) tactics = [r.tactic for r in rows] assert tactics[-1] == "unknown"