""" Unit tests for the Business Strength Archetype: server-side scoring (bsa/scoring.py) and the public results route (routes/bsa.py). Firestore is mocked. """ from unittest.mock import MagicMock, patch import pytest from fastapi import HTTPException from bsa import scoring from routes import bsa as bsa_routes from routes.bsa import ResultIn, submit_result @pytest.fixture(autouse=True) def _reset_limits(): bsa_routes.limiter.reset() def _natural(): return {qid: q["naturalFit"] for qid, q in scoring.QUESTIONS.items()} def _body(answers=None, **over): answers = answers or _natural() base = dict(firstName=" Ada ", email=" Ada@Example.com ", phone=" +234 803 123 4567 ", ageRange=scoring.AGES[1], stage=scoring.STAGES[2], consent=True, answers=[{"q": q, "dim": d} for q, d in answers.items()], startedAt=1, sessionId="s-1") base.update(over) return ResultIn(**base) def test_scoring_matches_the_documented_rules(): r = scoring.score(_natural()) assert set(r["raw"].values()) == {6} assert (r["primary"], r["secondary"], r["blendKey"]) == ("ST", "AN", "ST-AN") assert sum(r["pct"].values()) == 100 op = {qid: ("OP" if "OP" in q["dims"] else q["dims"][0]) for qid, q in scoring.QUESTIONS.items()} r = scoring.score(op) assert r["primary"] == "OP" and r["raw"]["OP"] == 24 def test_rejects_impossible_or_missing_answers(): answers = _natural() first = min(answers) bad = next(d for d in scoring.ORDER if d not in scoring.QUESTIONS[first]["dims"]) with pytest.raises(ValueError): scoring.score({**answers, first: bad}) answers.pop(first) with pytest.raises(ValueError): scoring.score(answers) def test_stores_details_and_server_computed_score(): db = MagicMock() db.collection.return_value.add.return_value = (None, MagicMock(id="doc1")) with patch.object(bsa_routes.firestore, "client", return_value=db): out = submit_result(_body()) assert out["id"] == "doc1" and out["blendKey"] == "ST-AN" db.collection.assert_called_with("bsa_results") stored = db.collection.return_value.add.call_args[0][0] assert stored["firstName"] == "Ada" and stored["email"] == "ada@example.com" assert stored["phone"] == "+234 803 123 4567" assert len(stored["answers"]) == 42 and stored["primary"] == "ST" @pytest.mark.parametrize("over", [{"consent": False}, {"email": "not-an-email"}, {"phone": "call me maybe"}, {"phone": "123456"}, {"stage": "made up"}]) def test_rejects_bad_details(over): with pytest.raises(HTTPException) as e: submit_result(_body(**over)) assert e.value.status_code == 400 def test_rate_limits_per_browser(): db = MagicMock() db.collection.return_value.add.return_value = (None, MagicMock(id="x")) with patch.object(bsa_routes.firestore, "client", return_value=db): for _ in range(5): submit_result(_body()) with pytest.raises(HTTPException) as e: submit_result(_body()) assert e.value.status_code == 429