119 lines
4.0 KiB
Python
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
|