from datetime import UTC, datetime from uuid import uuid4 from app.schemas import AccessAttempted from app.scoring import HeuristicScorer, risk_band def _evt( *, guest_id=None, fingerprint=None, ip=None, user_agent="Mozilla/5.0", ): return AccessAttempted( event_id=uuid4(), guest_id=guest_id or uuid4(), token_id=uuid4(), access_log_id=uuid4(), fingerprint=fingerprint, ip_address=ip, user_agent=user_agent, occurred_at=datetime.now(UTC), ) def test_first_access_with_full_signals_is_low_risk(): scorer = HeuristicScorer() evt = _evt( fingerprint={"ua": "Chrome", "platform": "macOS"}, ip="203.0.113.7", ) res = scorer.score(evt) assert res.score <= 30 assert risk_band(res.score) == "low" def test_fingerprint_change_drives_score_up(): scorer = HeuristicScorer() guest = uuid4() first = _evt(guest_id=guest, fingerprint={"ua": "Chrome"}, ip="203.0.113.7") scorer.score(first) second = _evt(guest_id=guest, fingerprint={"ua": "Safari"}, ip="203.0.113.7") res = scorer.score(second) assert res.score >= 40 assert any("fingerprint" in r for r in res.reasons) def test_ip_change_and_fingerprint_change_classify_high_or_block(): scorer = HeuristicScorer() guest = uuid4() scorer.score(_evt(guest_id=guest, fingerprint={"ua": "Chrome"}, ip="203.0.113.7")) suspicious = _evt( guest_id=guest, fingerprint={"ua": "Curl/8"}, ip="198.51.100.42", user_agent=None, ) res = scorer.score(suspicious) assert res.score >= 60 assert risk_band(res.score) in {"high", "block"} def test_missing_fingerprint_and_user_agent_flagged(): scorer = HeuristicScorer() res = scorer.score(_evt(fingerprint=None, ip="203.0.113.1", user_agent=None)) assert "no device fingerprint provided" in res.reasons assert "missing user agent" in res.reasons def test_score_clamped_to_0_100(): scorer = HeuristicScorer() # 10 successive accesses with no fingerprint, no UA, changing IPs guest = uuid4() for i in range(12): res = scorer.score(_evt(guest_id=guest, fingerprint=None, ip=f"10.0.{i}.1", user_agent=None)) assert 0 <= res.score <= 100