mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-03-28 17:43:05 +01:00
179 lines
7.2 KiB
Python
179 lines
7.2 KiB
Python
"""Tests for the WebSocket route endpoint (/api/ws).
|
|
|
|
These integration tests verify the WebSocket endpoint behavior:
|
|
- Initial health message sent on connect
|
|
- Ping/pong keepalive mechanism
|
|
- Clean disconnect handling
|
|
|
|
Uses FastAPI's TestClient synchronous WebSocket support with mocked
|
|
radio_manager and health data dependencies.
|
|
"""
|
|
|
|
from unittest.mock import AsyncMock, patch
|
|
|
|
import pytest
|
|
from fastapi.testclient import TestClient
|
|
|
|
from app.websocket import ws_manager
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _clean_ws_manager():
|
|
"""Ensure ws_manager has no stale connections between tests."""
|
|
ws_manager.active_connections.clear()
|
|
yield
|
|
ws_manager.active_connections.clear()
|
|
|
|
|
|
class TestWebSocketEndpoint:
|
|
"""Tests for the /api/ws WebSocket endpoint."""
|
|
|
|
def test_receives_initial_health_on_connect(self):
|
|
"""Client receives a health event with radio status immediately after connecting."""
|
|
with (
|
|
patch("app.routers.ws.radio_manager") as mock_ws_rm,
|
|
patch("app.routers.health.radio_manager") as mock_health_rm,
|
|
patch("app.routers.health.RawPacketRepository") as mock_repo,
|
|
patch("app.routers.health.settings") as mock_settings,
|
|
patch("app.routers.health.os.path.getsize", return_value=1024 * 1024),
|
|
):
|
|
mock_ws_rm.is_connected = True
|
|
mock_ws_rm.connection_info = "Serial: /dev/ttyUSB0"
|
|
mock_health_rm.is_connected = True
|
|
mock_health_rm.connection_info = "Serial: /dev/ttyUSB0"
|
|
mock_repo.get_oldest_undecrypted = AsyncMock(return_value=None)
|
|
mock_settings.database_path = "/tmp/test.db"
|
|
|
|
from app.main import app
|
|
|
|
client = TestClient(app)
|
|
|
|
with client.websocket_connect("/api/ws") as ws:
|
|
data = ws.receive_json()
|
|
|
|
assert data["type"] == "health"
|
|
assert "data" in data
|
|
|
|
health = data["data"]
|
|
assert health["radio_connected"] is True
|
|
assert health["connection_info"] == "Serial: /dev/ttyUSB0"
|
|
assert health["status"] == "ok"
|
|
assert "database_size_mb" in health
|
|
assert "oldest_undecrypted_timestamp" in health
|
|
|
|
def test_initial_health_reflects_disconnected_radio(self):
|
|
"""Health event reflects degraded status when radio is not connected."""
|
|
with (
|
|
patch("app.routers.ws.radio_manager") as mock_ws_rm,
|
|
patch("app.routers.health.radio_manager") as mock_health_rm,
|
|
patch("app.routers.health.RawPacketRepository") as mock_repo,
|
|
patch("app.routers.health.settings") as mock_settings,
|
|
patch("app.routers.health.os.path.getsize", return_value=0),
|
|
):
|
|
mock_ws_rm.is_connected = False
|
|
mock_ws_rm.connection_info = None
|
|
mock_health_rm.is_connected = False
|
|
mock_health_rm.connection_info = None
|
|
mock_repo.get_oldest_undecrypted = AsyncMock(return_value=None)
|
|
mock_settings.database_path = "/tmp/test.db"
|
|
|
|
from app.main import app
|
|
|
|
client = TestClient(app)
|
|
|
|
with client.websocket_connect("/api/ws") as ws:
|
|
data = ws.receive_json()
|
|
|
|
assert data["type"] == "health"
|
|
health = data["data"]
|
|
assert health["radio_connected"] is False
|
|
assert health["connection_info"] is None
|
|
assert health["status"] == "degraded"
|
|
|
|
def test_ping_returns_pong(self):
|
|
"""Sending 'ping' text receives a JSON pong response."""
|
|
with (
|
|
patch("app.routers.ws.radio_manager") as mock_ws_rm,
|
|
patch("app.routers.health.radio_manager") as mock_health_rm,
|
|
patch("app.routers.health.RawPacketRepository") as mock_repo,
|
|
patch("app.routers.health.settings") as mock_settings,
|
|
patch("app.routers.health.os.path.getsize", return_value=0),
|
|
):
|
|
mock_ws_rm.is_connected = True
|
|
mock_ws_rm.connection_info = "TCP: 192.168.1.1:4000"
|
|
mock_health_rm.is_connected = True
|
|
mock_health_rm.connection_info = "TCP: 192.168.1.1:4000"
|
|
mock_repo.get_oldest_undecrypted = AsyncMock(return_value=None)
|
|
mock_settings.database_path = "/tmp/test.db"
|
|
|
|
from app.main import app
|
|
|
|
client = TestClient(app)
|
|
|
|
with client.websocket_connect("/api/ws") as ws:
|
|
# Consume the initial health message
|
|
ws.receive_json()
|
|
|
|
# Send ping and verify pong
|
|
ws.send_text("ping")
|
|
pong = ws.receive_json()
|
|
|
|
assert pong == {"type": "pong"}
|
|
|
|
def test_non_ping_message_does_not_produce_response(self):
|
|
"""Messages other than 'ping' are silently ignored (no response sent)."""
|
|
with (
|
|
patch("app.routers.ws.radio_manager") as mock_ws_rm,
|
|
patch("app.routers.health.radio_manager") as mock_health_rm,
|
|
patch("app.routers.health.RawPacketRepository") as mock_repo,
|
|
patch("app.routers.health.settings") as mock_settings,
|
|
patch("app.routers.health.os.path.getsize", return_value=0),
|
|
):
|
|
mock_ws_rm.is_connected = True
|
|
mock_ws_rm.connection_info = "Serial: /dev/ttyUSB0"
|
|
mock_health_rm.is_connected = True
|
|
mock_health_rm.connection_info = "Serial: /dev/ttyUSB0"
|
|
mock_repo.get_oldest_undecrypted = AsyncMock(return_value=None)
|
|
mock_settings.database_path = "/tmp/test.db"
|
|
|
|
from app.main import app
|
|
|
|
client = TestClient(app)
|
|
|
|
with client.websocket_connect("/api/ws") as ws:
|
|
ws.receive_json() # consume health
|
|
|
|
# Send a non-ping message, then a ping to verify the connection
|
|
# is still alive and only the ping produces a response
|
|
ws.send_text("hello")
|
|
ws.send_text("ping")
|
|
pong = ws.receive_json()
|
|
assert pong == {"type": "pong"}
|
|
|
|
def test_disconnect_removes_client_from_manager(self):
|
|
"""Closing the WebSocket removes the connection from ws_manager."""
|
|
with (
|
|
patch("app.routers.ws.radio_manager") as mock_ws_rm,
|
|
patch("app.routers.health.radio_manager") as mock_health_rm,
|
|
patch("app.routers.health.RawPacketRepository") as mock_repo,
|
|
patch("app.routers.health.settings") as mock_settings,
|
|
patch("app.routers.health.os.path.getsize", return_value=0),
|
|
):
|
|
mock_ws_rm.is_connected = True
|
|
mock_ws_rm.connection_info = "Serial: /dev/ttyUSB0"
|
|
mock_health_rm.is_connected = True
|
|
mock_health_rm.connection_info = "Serial: /dev/ttyUSB0"
|
|
mock_repo.get_oldest_undecrypted = AsyncMock(return_value=None)
|
|
mock_settings.database_path = "/tmp/test.db"
|
|
|
|
from app.main import app
|
|
|
|
client = TestClient(app)
|
|
|
|
with client.websocket_connect("/api/ws") as ws:
|
|
ws.receive_json() # consume health
|
|
assert len(ws_manager.active_connections) == 1
|
|
|
|
# After context manager exits, the WebSocket is closed
|
|
assert len(ws_manager.active_connections) == 0
|