File size: 2,880 Bytes
187966e
 
 
 
 
 
 
db8a51a
 
 
 
 
 
 
 
 
187966e
 
 
db8a51a
187966e
db8a51a
187966e
 
db8a51a
187966e
 
 
db8a51a
187966e
db8a51a
 
 
 
 
 
 
 
 
 
 
 
 
187966e
db8a51a
 
 
 
 
 
 
 
 
187966e
 
 
db8a51a
187966e
db8a51a
 
187966e
 
 
 
 
 
 
 
db8a51a
187966e
 
 
 
 
 
db8a51a
90bea9c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
85
86
87
88
89
90
91
92
from __future__ import annotations

from datetime import UTC, datetime, timedelta

from app.models import ChatSession
from app.verification import is_locked, mark_trusted, verify

ORDERS = [
    {
        "email": "a@b.c",
        "name": "#1001",
        "zip": "08480",
        "customer_name": "Ana García",
        "phone": "600111222",
    }
]
NOW = datetime(2026, 6, 9, tzinfo=UTC)


def test_verify_success_with_order_number():
    s = ChatSession()
    r = verify(s, "A@B.C", ORDERS, order_number="1001", now=NOW)
    assert r.ok is True
    assert r.reason == "verified"
    assert r.matched["name"] == "#1001"
    assert s.verified is True


def test_verify_success_with_postal_code():
    s = ChatSession()
    r = verify(s, "a@b.c", ORDERS, zip_code="08480", now=NOW)
    assert r.ok is True  # email + postal code is enough (no order number needed)


def test_verify_success_with_name():
    s = ChatSession()
    r = verify(s, "a@b.c", ORDERS, name="ana garcia", now=NOW)
    assert r.ok is True


def test_email_alone_asks_for_second_factor_no_attempt():
    s = ChatSession()
    r = verify(s, "a@b.c", ORDERS, now=NOW)
    assert r.ok is False
    assert r.reason == "need_info"
    assert (s.verify_attempts or 0) == 0  # not counted as a failed attempt


def test_wrong_secondary_is_generic_and_counts():
    s = ChatSession()
    r = verify(s, "a@b.c", ORDERS, zip_code="99999", now=NOW, max_attempts=3)
    assert r.ok is False
    assert r.reason == "not_found"
    assert s.verify_attempts == 1


def test_lockout_after_max_attempts():
    s = ChatSession()
    verify(s, "a@b.c", ORDERS, order_number="9999", now=NOW, max_attempts=2)
    r2 = verify(s, "a@b.c", ORDERS, order_number="9999", now=NOW, max_attempts=2)
    assert r2.locked is True
    assert is_locked(s, NOW) is True
    assert is_locked(s, NOW + timedelta(seconds=901)) is False


def test_locked_session_short_circuits():
    s = ChatSession()
    s.locked_until = NOW + timedelta(seconds=500)
    r = verify(s, "a@b.c", ORDERS, order_number="1001", now=NOW)
    assert r.locked is True


def test_mark_trusted():
    s = ChatSession()
    r = mark_trusted(s)
    assert r.ok is True and r.reason == "trusted" and s.verified is True


def test_mark_trusted_does_not_clear_order_lockout():
    """A logged-in customer must not reset the order-verification brute-force lockout
    (else they could keep guessing other people's orders every turn)."""
    from datetime import UTC, datetime, timedelta

    from app.models import ChatSession
    from app.verification import mark_trusted

    s = ChatSession(verify_attempts=4, locked_until=datetime.now(UTC) + timedelta(minutes=10))
    mark_trusted(s)
    assert s.verified is True            # recorded as trusted
    assert s.verify_attempts == 4        # lockout counter untouched
    assert s.locked_until is not None    # still locked