File size: 3,069 Bytes
b324c71
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
"""
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