import pytest from unittest.mock import AsyncMock from bson import ObjectId from app.core.security import create_token from app.services.verification import compute_verified_score from app.services import weather as weather_svc from app.services.ai import triage, detect_language, classify_urgency, generate_headline, similarity, is_duplicate def _token(role="volunteer"): return create_token({"sub": str(ObjectId()), "role": role}) def test_verified_score_caps_at_100(): assert compute_verified_score(99, 99, True) == 100 def test_verified_score_zero_when_no_signals(): assert compute_verified_score(0, 0, False) == 0 def test_verified_score_each_source_capped(): # witnesses alone capped at 40 assert compute_verified_score(100, 0, False) == 40 # corroboration alone capped at 40 assert compute_verified_score(0, 100, False) == 40 # weather adds exactly 20 assert compute_verified_score(0, 0, True) == 20 def test_weather_supports_flood_on_heavy_rain(): assert weather_svc.supports_category("flood", {"precipitation_mm": 8, "wind_kph": 0, "code": 0}) assert not weather_svc.supports_category("flood", {"precipitation_mm": 0, "wind_kph": 0, "code": 0}) def test_weather_supports_fire_on_dry_wind(): assert weather_svc.supports_category("fire", {"precipitation_mm": 0, "wind_kph": 30, "code": 0}) assert not weather_svc.supports_category("fire", {"precipitation_mm": 2, "wind_kph": 30, "code": 0}) def test_weather_supports_returns_false_for_unknown_categories(): assert not weather_svc.supports_category("medical", {"precipitation_mm": 100, "wind_kph": 100}) assert not weather_svc.supports_category("flood", None) # --- AI triage tests (use heuristic fallback via NA_DISABLE_AI_MODEL=1) --- def test_triage_critical_detects_unconscious(): t = triage("Man collapsed and is unconscious, not breathing") assert t.urgency == "CRITICAL" assert "unconscious" in t.triggers assert t.priority_score >= 80 def test_triage_detects_child_vulnerability(): t = triage("A bachcha is trapped inside the flooded building") assert t.vulnerability == "child" def test_triage_detects_elderly_vulnerability(): t = triage("Elderly man alone and injured after the accident") assert t.vulnerability == "elderly" def test_triage_time_sensitivity_immediate(): t = triage("Need help immediately, right now") assert t.time_sensitivity == "immediate" def test_triage_time_sensitivity_days(): t = triage("Can someone come tomorrow to check the power line") assert t.time_sensitivity == "days" def test_detect_language_devanagari(): assert detect_language("मदद चाहिए जल्दी") == "hi" def test_detect_language_hinglish(): assert detect_language("aag lagi hai madad karo") == "hi-Latn" def test_detect_language_english(): assert detect_language("fire in the building help") == "en" def test_backcompat_classify_urgency_signature(): urgency, reason = classify_urgency("person is bleeding heavily") assert urgency == "CRITICAL" assert reason # non-empty # --- headline + similarity --- def test_headline_prefers_first_sentence(): assert generate_headline("Fire in the kitchen. Everyone is safe.") == "Fire in the kitchen." def test_headline_truncates_long_text(): long_text = "a very long description " * 20 h = generate_headline(long_text, max_len=50) assert len(h) <= 51 # 50 + ellipsis assert h.endswith("…") def test_headline_empty(): assert generate_headline("") == "" def test_similarity_identical_is_one(): s = "fire near park gate" assert similarity(s, s) == 1.0 def test_similarity_disjoint_is_low(): assert similarity("fire in kitchen", "cat chased mouse") < 0.2 def test_is_duplicate_detects_near_match(): a = "fire in the building near gate 3" b = "there is a fire near gate 3 in the building" assert is_duplicate(a, b) def test_is_duplicate_rejects_unrelated(): assert not is_duplicate("fire near park", "child lost at market") @pytest.mark.asyncio async def test_witness_rejects_own_alert(client): c, db = client alert_id = ObjectId() token = _token("volunteer") # The JWT sub becomes the user id; make the alert owned by the same id import jose.jwt as jj from app.core.config import settings sub = jj.decode(token, settings.JWT_SECRET, algorithms=[settings.JWT_ALGORITHM])["sub"] db.alerts.find_one = AsyncMock( return_value={ "_id": alert_id, "reporter_id": ObjectId(sub), "status": "open", "location": {"type": "Point", "coordinates": [76.7794, 30.7333]}, } ) resp = await c.post( f"/api/alerts/{alert_id}/witness", headers={"Authorization": f"Bearer {token}"}, ) assert resp.status_code == 400 @pytest.mark.asyncio async def test_witness_rejects_far_user(client): c, db = client alert_id = ObjectId() token = _token("volunteer") import jose.jwt as jj from app.core.config import settings sub = jj.decode(token, settings.JWT_SECRET, algorithms=[settings.JWT_ALGORITHM])["sub"] db.alerts.find_one = AsyncMock( return_value={ "_id": alert_id, "reporter_id": ObjectId(), # someone else "status": "open", "location": {"type": "Point", "coordinates": [76.7794, 30.7333]}, } ) db.users.find_one = AsyncMock( return_value={ "_id": ObjectId(sub), # >2 km away "location": {"type": "Point", "coordinates": [77.2090, 28.6139]}, } ) resp = await c.post( f"/api/alerts/{alert_id}/witness", headers={"Authorization": f"Bearer {token}"}, ) assert resp.status_code == 403 @pytest.mark.asyncio async def test_witness_missing_alert(client): c, db = client db.alerts.find_one = AsyncMock(return_value=None) resp = await c.post( f"/api/alerts/{ObjectId()}/witness", headers={"Authorization": f"Bearer {_token()}"}, ) assert resp.status_code == 404