feat: add Campaign/Compliance domain entities and extract users/audit/data_sources to services (LP-2 through LP-6)

This commit is contained in:
2026-02-20 13:28:14 +01:00
parent 44621364be
commit c0c6cda11d
11 changed files with 939 additions and 319 deletions

View File

@@ -0,0 +1,175 @@
"""Tests for CampaignEntity — pure domain logic, no DB."""
import sys
import os
import uuid
from unittest.mock import MagicMock
import pytest
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)
from app.domain.entities.campaign import (
CampaignEntity,
CampaignStatus,
CampaignType,
)
from app.domain.errors import BusinessRuleViolation, InvalidStateTransition
# ── Helpers ──────────────────────────────────────────────────────────
def _entity(status: str = "draft", test_count: int = 0, **overrides) -> CampaignEntity:
defaults = dict(
id=uuid.uuid4(),
name="Test Campaign",
type=CampaignType.custom,
status=CampaignStatus(status),
description=None,
threat_actor_id=None,
created_by=None,
target_platform=None,
tags=[],
test_count=test_count,
)
defaults.update(overrides)
return CampaignEntity(**defaults)
def _fake_orm(status: str = "draft", test_count: int = 0, **overrides) -> MagicMock:
m = MagicMock()
m.id = uuid.uuid4()
m.name = "Test Campaign"
m.type = "custom"
m.status = status
m.description = None
m.threat_actor_id = None
m.created_by = None
m.target_platform = None
m.tags = []
m.campaign_tests = [MagicMock()] * test_count if test_count else []
for k, v in overrides.items():
setattr(m, k, v)
return m
# ── 1. Test activation from draft with tests → success ───────────────
def test_activate_from_draft_with_tests_success():
e = _entity("draft", test_count=1)
e.activate()
assert e.status == CampaignStatus.active
def test_activate_from_draft_with_multiple_tests_success():
e = _entity("draft", test_count=3)
e.activate()
assert e.status == CampaignStatus.active
# ── 2. Test activation from draft with 0 tests → BusinessRuleViolation ───
def test_activate_from_draft_with_zero_tests_raises():
e = _entity("draft", test_count=0)
with pytest.raises(BusinessRuleViolation, match="at least one test"):
e.activate()
assert e.status == CampaignStatus.draft
# ── 3. Test activation from active → InvalidStateTransition ────────────
def test_activate_from_active_raises():
e = _entity("active", test_count=2)
with pytest.raises(InvalidStateTransition) as exc_info:
e.activate()
assert exc_info.value.current_state == "active"
assert exc_info.value.target_state == "active"
assert "completed" in exc_info.value.valid_transitions
# ── 4. Test complete from active → success ──────────────────────────────
def test_complete_from_active_success():
e = _entity("active", test_count=2)
e.complete()
assert e.status == CampaignStatus.completed
# ── 5. Test complete from draft → InvalidStateTransition ────────────────
def test_complete_from_draft_raises():
e = _entity("draft", test_count=1)
with pytest.raises(InvalidStateTransition) as exc_info:
e.complete()
assert exc_info.value.current_state == "draft"
assert exc_info.value.target_state == "completed"
assert "active" in exc_info.value.valid_transitions
# ── 6. Test ensure_modifiable in draft/active → ok ──────────────────────
def test_ensure_modifiable_draft_ok():
e = _entity("draft")
e.ensure_modifiable() # no raise
def test_ensure_modifiable_active_ok():
e = _entity("active", test_count=1)
e.ensure_modifiable() # no raise
# ── 7. Test ensure_modifiable in completed → BusinessRuleViolation ──────
def test_ensure_modifiable_completed_raises():
e = _entity("completed", test_count=1)
with pytest.raises(BusinessRuleViolation, match="Cannot modify"):
e.ensure_modifiable()
def test_ensure_modifiable_archived_raises():
e = _entity("archived", test_count=1)
with pytest.raises(BusinessRuleViolation, match="Cannot modify"):
e.ensure_modifiable()
# ── 8. Test from_orm conversion ──────────────────────────────────────────
def test_from_orm_basic():
orm = _fake_orm("draft", test_count=0)
e = CampaignEntity.from_orm(orm)
assert e.name == "Test Campaign"
assert e.type == CampaignType.custom
assert e.status == CampaignStatus.draft
assert e.id == orm.id
assert e.test_count == 0
def test_from_orm_with_tests():
orm = _fake_orm("draft", test_count=3)
e = CampaignEntity.from_orm(orm)
assert e.test_count == 3
def test_from_orm_coerces_type_and_status():
orm = _fake_orm(status="active", type="apt_emulation", test_count=1)
e = CampaignEntity.from_orm(orm)
assert e.status == CampaignStatus.active
assert e.type == CampaignType.apt_emulation
def test_from_orm_handles_none_tags():
orm = _fake_orm("draft", test_count=0)
orm.tags = None
e = CampaignEntity.from_orm(orm)
assert e.tags == []