NeighbourAid / tests /test_websocket_routing.py
Parth Kansal
commit
49e9f9d
"""Tests for the in-memory WebSocket ConnectionManager.
We can't easily exercise full WS connect/disconnect in unit tests without
running uvicorn — but the routing decisions (does this volunteer get this
alert? does the skill match extend the radius?) are async-callable on the
manager directly.
"""
from __future__ import annotations
import json
from typing import Any
import pytest
from app.services.websocket import (
CATEGORY_PREFERRED_SKILLS,
DEFAULT_RADIUS_KM,
SKILL_RADIUS_KM,
ConnectionManager,
)
class FakeWS:
"""Tiny stand-in for FastAPI's WebSocket — records sent payloads."""
def __init__(self):
self.sent: list[dict[str, Any]] = []
self.closed = False
async def send_text(self, text: str) -> None:
if self.closed:
raise RuntimeError("WS closed")
self.sent.append(json.loads(text))
def _alert(category: str, lng: float, lat: float, oid: str = "abc") -> dict:
return {
"id": oid,
"category": category,
"urgency": "HIGH",
"location": {"type": "Point", "coordinates": [lng, lat]},
}
@pytest.fixture
def manager():
return ConnectionManager()
def test_count_reflects_active_connections(manager):
assert manager.count() == 0
manager.register("u1", FakeWS(), [76.7, 30.7])
manager.register("u2", FakeWS(), [76.8, 30.8])
assert manager.count() == 2
manager.disconnect("u1")
assert manager.count() == 1
@pytest.mark.asyncio
async def test_volunteer_within_default_radius_receives_alert(manager):
ws = FakeWS()
# Volunteer at the same coords as the alert
manager.register("v1", ws, [76.7794, 30.7333])
alert = _alert("medical", 76.7794, 30.7333)
await manager.broadcast_nearby(alert)
assert len(ws.sent) == 1
payload = ws.sent[0]
assert payload["id"] == "abc"
assert payload["your_distance_km"] == pytest.approx(0.0, abs=0.01)
assert payload["is_skill_match"] is False # no skills registered
@pytest.mark.asyncio
async def test_volunteer_outside_default_radius_skipped(manager):
ws = FakeWS()
# Place volunteer ~50 km away — well outside the 5 km default
manager.register("v1", ws, [77.5, 30.7])
alert = _alert("medical", 76.7794, 30.7333)
await manager.broadcast_nearby(alert)
assert ws.sent == []
@pytest.mark.asyncio
async def test_skill_match_extends_radius(manager):
"""A medical-tagged volunteer 10 km away (outside default 5 km) should
still get a medical alert because the skill match bumps the radius."""
assert "medical" in CATEGORY_PREFERRED_SKILLS["medical"]
assert DEFAULT_RADIUS_KM < 10 < SKILL_RADIUS_KM
ws = FakeWS()
# 10 km east of the alert (longitude offset ≈ 0.1° at latitude 30°)
manager.register("v1", ws, [76.7794 + 0.105, 30.7333], skills=["medical"])
alert = _alert("medical", 76.7794, 30.7333)
await manager.broadcast_nearby(alert)
assert len(ws.sent) == 1
assert ws.sent[0]["is_skill_match"] is True
@pytest.mark.asyncio
async def test_unrelated_skill_does_not_extend_radius(manager):
ws = FakeWS()
# 10 km away, but volunteer has a swim skill — not preferred for medical
manager.register("v1", ws, [76.7794 + 0.105, 30.7333], skills=["swim"])
alert = _alert("medical", 76.7794, 30.7333)
await manager.broadcast_nearby(alert)
# Outside default radius and skill doesn't help → no broadcast
assert ws.sent == []
@pytest.mark.asyncio
async def test_disconnects_after_send_failure(manager):
ws = FakeWS()
ws.closed = True # any send will raise
manager.register("v1", ws, [76.7794, 30.7333])
alert = _alert("medical", 76.7794, 30.7333)
await manager.broadcast_nearby(alert)
# Manager should have removed the failing connection so it doesn't
# keep raising on subsequent broadcasts.
assert manager.count() == 0
@pytest.mark.asyncio
async def test_payload_includes_vehicle_flag(manager):
ws = FakeWS()
manager.register("v1", ws, [76.7794, 30.7333], has_vehicle=True)
alert = _alert("medical", 76.7794, 30.7333)
await manager.broadcast_nearby(alert)
assert ws.sent[0]["your_has_vehicle"] is True