| 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 |
|
|
|
|
| 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 |
|
|
|
|
| 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 |
| assert s.verify_attempts == 4 |
| assert s.locked_until is not None |
|
|