169 lines
5.7 KiB
Python
169 lines
5.7 KiB
Python
"""Tests for TechniqueEntity — pure domain logic, no DB."""
|
|
|
|
import uuid
|
|
import sys
|
|
import os
|
|
from datetime import datetime
|
|
from unittest.mock import MagicMock
|
|
|
|
backend_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
|
if backend_dir not in sys.path:
|
|
sys.path.insert(0, backend_dir)
|
|
|
|
import pytest
|
|
|
|
from app.domain.entities.technique import TechniqueEntity
|
|
from app.domain.enums import TechniqueStatus
|
|
|
|
|
|
# ── Helpers ──────────────────────────────────────────────────────────
|
|
|
|
|
|
def _entity(**overrides) -> TechniqueEntity:
|
|
defaults = dict(
|
|
id=uuid.uuid4(),
|
|
mitre_id="T1059",
|
|
name="Command and Scripting Interpreter",
|
|
tactic="execution",
|
|
status_global=TechniqueStatus.not_evaluated,
|
|
)
|
|
defaults.update(overrides)
|
|
return TechniqueEntity(**defaults)
|
|
|
|
|
|
def _fake_orm(**overrides) -> MagicMock:
|
|
m = MagicMock()
|
|
m.id = uuid.uuid4()
|
|
m.mitre_id = "T1059"
|
|
m.name = "Command and Scripting Interpreter"
|
|
m.tactic = "execution"
|
|
m.description = None
|
|
m.platforms = ["windows", "linux"]
|
|
m.is_subtechnique = False
|
|
m.parent_mitre_id = None
|
|
m.status_global = "not_evaluated"
|
|
m.review_required = False
|
|
m.last_review_date = None
|
|
for k, v in overrides.items():
|
|
setattr(m, k, v)
|
|
return m
|
|
|
|
|
|
# ── 1. Factory: create() ────────────────────────────────────────────
|
|
|
|
|
|
class TestCreate:
|
|
|
|
def test_valid_technique(self):
|
|
e = TechniqueEntity.create(
|
|
mitre_id="T1059",
|
|
name="Command and Scripting Interpreter",
|
|
tactic="execution",
|
|
)
|
|
assert e.mitre_id == "T1059"
|
|
assert e.name == "Command and Scripting Interpreter"
|
|
assert e.status_global == TechniqueStatus.not_evaluated
|
|
assert not e.is_subtechnique
|
|
|
|
def test_valid_subtechnique(self):
|
|
e = TechniqueEntity.create(
|
|
mitre_id="T1059.001",
|
|
name="PowerShell",
|
|
tactic="execution",
|
|
)
|
|
assert e.is_subtechnique
|
|
assert e.parent_mitre_id == "T1059"
|
|
|
|
def test_invalid_mitre_id_raises(self):
|
|
with pytest.raises(ValueError, match="Invalid MITRE"):
|
|
TechniqueEntity.create(mitre_id="INVALID", name="Bad", tactic="x")
|
|
|
|
def test_platforms_default_to_empty(self):
|
|
e = TechniqueEntity.create(mitre_id="T1059", name="Test")
|
|
assert e.platforms == []
|
|
|
|
|
|
# ── 2. from_orm / apply_to ──────────────────────────────────────────
|
|
|
|
|
|
class TestOrmRoundTrip:
|
|
|
|
def test_from_orm_basic(self):
|
|
orm = _fake_orm()
|
|
e = TechniqueEntity.from_orm(orm)
|
|
assert e.mitre_id == "T1059"
|
|
assert e.status_global == TechniqueStatus.not_evaluated
|
|
|
|
def test_from_orm_coerces_string_status(self):
|
|
orm = _fake_orm(status_global="validated")
|
|
e = TechniqueEntity.from_orm(orm)
|
|
assert e.status_global == TechniqueStatus.validated
|
|
|
|
def test_apply_to_updates_model(self):
|
|
orm = _fake_orm()
|
|
e = TechniqueEntity.from_orm(orm)
|
|
e.status_global = TechniqueStatus.validated
|
|
e.review_required = True
|
|
e.apply_to(orm)
|
|
assert orm.status_global == TechniqueStatus.validated
|
|
assert orm.review_required is True
|
|
|
|
|
|
# ── 3. recalculate_status ───────────────────────────────────────────
|
|
|
|
|
|
class TestRecalculateStatus:
|
|
|
|
def test_no_tests_gives_not_evaluated(self):
|
|
e = _entity()
|
|
result = e.recalculate_status([])
|
|
assert result == TechniqueStatus.not_evaluated
|
|
assert e.status_global == TechniqueStatus.not_evaluated
|
|
|
|
def test_all_validated_all_detected_gives_validated(self):
|
|
e = _entity()
|
|
tests = [("validated", "detected"), ("validated", "detected")]
|
|
assert e.recalculate_status(tests) == TechniqueStatus.validated
|
|
|
|
def test_all_validated_some_partially_gives_partial(self):
|
|
e = _entity()
|
|
tests = [("validated", "detected"), ("validated", "partially_detected")]
|
|
assert e.recalculate_status(tests) == TechniqueStatus.partial
|
|
|
|
def test_all_validated_none_detected_gives_not_covered(self):
|
|
e = _entity()
|
|
tests = [("validated", "not_detected"), ("validated", "not_detected")]
|
|
assert e.recalculate_status(tests) == TechniqueStatus.not_covered
|
|
|
|
def test_all_validated_no_results_gives_not_covered(self):
|
|
e = _entity()
|
|
tests = [("validated", None)]
|
|
assert e.recalculate_status(tests) == TechniqueStatus.not_covered
|
|
|
|
def test_mixed_validated_and_in_progress_gives_partial(self):
|
|
e = _entity()
|
|
tests = [("validated", "detected"), ("draft", None)]
|
|
assert e.recalculate_status(tests) == TechniqueStatus.partial
|
|
|
|
def test_all_in_progress_gives_in_progress(self):
|
|
e = _entity()
|
|
tests = [("draft", None), ("red_executing", None)]
|
|
assert e.recalculate_status(tests) == TechniqueStatus.in_progress
|
|
|
|
|
|
# ── 4. mark_reviewed / flag_for_review ──────────────────────────────
|
|
|
|
|
|
class TestReviewCycle:
|
|
|
|
def test_mark_reviewed_clears_flag(self):
|
|
e = _entity(review_required=True)
|
|
e.mark_reviewed()
|
|
assert e.review_required is False
|
|
assert e.last_review_date is not None
|
|
|
|
def test_flag_for_review(self):
|
|
e = _entity(review_required=False)
|
|
e.flag_for_review()
|
|
assert e.review_required is True
|