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