Files
group_fqcd_jr/tests/unit/service/test_risk_questionnaire_service.py
T

119 lines
4.0 KiB
Python

from typing import Any, cast
import pytest
from pydantic import ValidationError
from app.core.contracts import RequestContext
from app.core.risk_questionnaire_contracts import RiskQuestionnaireSubmission
from app.model.memory import MemorySyncOutbox
from app.model.risk_questionnaire import ProfileSnapshot, RiskAssessment
from app.service.api_transaction_service import ApiTransactionService
from app.service.risk_questionnaire_service import RiskQuestionnaireService
def context() -> RequestContext:
return RequestContext(
user_id="7", trace_id="questionnaire-test", roles=("customer",)
)
def lowest_risk_answers() -> dict[str, int]:
return {
"q1": 12, "q2": 4, "q3": 5, "q4": 4, "q5": 4, "q6": 5, "q7": 5,
"q8": 4, "q9": 4, "q10": 1, "q11": 5, "q12": 4, "q13": 4,
}
def highest_risk_answers() -> dict[str, int]:
return {
"q1": 1, "q2": 1, "q3": 4, "q4": 1, "q5": 1, "q6": 1, "q7": 1,
"q8": 1, "q9": 1, "q10": 4, "q11": 1, "q12": 1, "q13": 1,
}
def submission() -> RiskQuestionnaireSubmission:
return RiskQuestionnaireSubmission(
answers=lowest_risk_answers(), declaration_accepted=True
)
class FakeSession:
def __init__(self) -> None:
self.items: list[object] = []
def add(self, item: object) -> None:
self.items.append(item)
class FakeRepository:
def __init__(self, session: FakeSession) -> None:
self.session = session
self.deactivated = False
async def assessments_since(self, _customer_id: int, _since: object) -> int:
return 0
async def next_profile_version(self, _customer_id: int) -> int:
return 1
async def deactivate_current_profile(self, _customer_id: int, _now: object) -> None:
self.deactivated = True
def add_assessment(self, assessment: RiskAssessment) -> None:
self.session.add(assessment)
def add_profile(self, profile: ProfileSnapshot) -> None:
self.session.add(profile)
def add_sync_event(self, event: MemorySyncOutbox) -> None:
self.session.add(event)
def test_submission_requires_all_questions_and_declaration() -> None:
with pytest.raises(ValidationError, match="every questionnaire question"):
RiskQuestionnaireSubmission(answers={"q1": 1}, declaration_accepted=True)
with pytest.raises(ValidationError):
RiskQuestionnaireSubmission.model_validate({
"answers": lowest_risk_answers(), "declaration_accepted": False,
})
def test_score_bands_are_deterministic_and_server_only() -> None:
low = RiskQuestionnaireService.score(lowest_risk_answers())
high = RiskQuestionnaireService.score(highest_risk_answers())
assert (low.total, low.risk_level) == (13, "C1")
assert (high.total, high.risk_level) == (57, "C5")
@pytest.mark.asyncio
async def test_submission_persists_assessment_and_profile_without_exposing_them(
monkeypatch: pytest.MonkeyPatch,
) -> None:
session = FakeSession()
async def execute(
_self: ApiTransactionService,
_context: RequestContext,
_scope: str,
_key: str | None,
_body: object,
operation: Any,
) -> dict[str, Any]:
return cast(dict[str, Any], await operation(session))
monkeypatch.setattr(ApiTransactionService, "execute", execute)
monkeypatch.setattr(
"app.service.risk_questionnaire_service.RiskQuestionnaireRepository", FakeRepository
)
response = await RiskQuestionnaireService().submit(
submission(), context(), "questionnaire-key-0001"
)
data = cast(dict[str, object], response["data"])
assert set(data) == {"completed", "questionnaire_version", "valid_until"}
assert data["completed"] is True
assert not {"total_score", "risk_level", "risk_profile", "profile"}.intersection(data)
assert any(isinstance(item, RiskAssessment) for item in session.items)
assert any(isinstance(item, ProfileSnapshot) for item in session.items)
assert len([item for item in session.items if isinstance(item, MemorySyncOutbox)]) == 2