"""Tests for health endpoint fanout status fields. Verifies that build_health_data correctly reports fanout module statuses via the fanout_manager. """ from unittest.mock import patch import pytest from app.routers.health import build_health_data from app.version_info import AppBuildInfo class TestHealthFanoutStatus: """Test fanout_statuses in build_health_data.""" @pytest.mark.asyncio async def test_no_fanout_modules_returns_empty(self, test_db): """fanout_statuses should be empty dict when no modules are running.""" with patch("app.fanout.manager.fanout_manager") as mock_fm: mock_fm.get_statuses.return_value = {} data = await build_health_data(True, "TCP: 1.2.3.4:4000") assert data["fanout_statuses"] == {} @pytest.mark.asyncio async def test_fanout_statuses_reflect_manager(self, test_db): """fanout_statuses should return whatever the manager reports.""" mock_statuses = { "uuid-1": {"name": "Private MQTT", "type": "mqtt_private", "status": "connected"}, "uuid-2": { "name": "Community MQTT", "type": "mqtt_community", "status": "disconnected", }, } with patch("app.fanout.manager.fanout_manager") as mock_fm: mock_fm.get_statuses.return_value = mock_statuses data = await build_health_data(True, "Serial: /dev/ttyUSB0") assert data["fanout_statuses"] == mock_statuses @pytest.mark.asyncio async def test_health_status_ok_when_connected(self, test_db): """Health status is 'ok' when radio is connected.""" with ( patch( "app.routers.health.RawPacketRepository.get_oldest_undecrypted", return_value=None ), patch("app.routers.health.radio_manager") as mock_rm, patch( "app.routers.health.get_app_build_info", return_value=AppBuildInfo( version="3.4.1", version_source="pyproject", commit_hash="abcdef12", commit_source="git", ), ), ): mock_rm.is_setup_in_progress = False mock_rm.is_setup_complete = True data = await build_health_data(True, "Serial: /dev/ttyUSB0") assert data["status"] == "ok" assert data["radio_connected"] is True assert data["radio_initializing"] is False assert data["radio_state"] == "connected" assert data["connection_info"] == "Serial: /dev/ttyUSB0" assert data["app_info"] == { "version": "3.4.1", "commit_hash": "abcdef12", } @pytest.mark.asyncio async def test_health_includes_cached_radio_device_info(self, test_db): """Health includes device metadata captured during post-connect setup.""" with ( patch( "app.routers.health.RawPacketRepository.get_oldest_undecrypted", return_value=None ), patch("app.routers.health.radio_manager") as mock_rm, ): mock_rm.is_setup_in_progress = False mock_rm.is_setup_complete = True mock_rm.connection_desired = True mock_rm.is_reconnecting = False mock_rm.device_info_loaded = True mock_rm.device_model = "T-Echo" mock_rm.firmware_build = "2025-02-01" mock_rm.firmware_version = "1.2.3" mock_rm.max_contacts = 350 mock_rm.max_channels = 64 data = await build_health_data(True, "Serial: /dev/ttyUSB0") assert data["radio_device_info"] == { "model": "T-Echo", "firmware_build": "2025-02-01", "firmware_version": "1.2.3", "max_contacts": 350, "max_channels": 64, } @pytest.mark.asyncio async def test_health_status_degraded_when_disconnected(self, test_db): """Health status is 'degraded' when radio is disconnected.""" with patch( "app.routers.health.RawPacketRepository.get_oldest_undecrypted", return_value=None ): data = await build_health_data(False, None) assert data["status"] == "degraded" assert data["radio_connected"] is False assert data["radio_initializing"] is False assert data["radio_state"] == "disconnected" assert data["connection_info"] is None @pytest.mark.asyncio async def test_health_status_degraded_while_radio_initializing(self, test_db): """Health stays degraded while transport is up but post-connect setup is incomplete.""" with ( patch( "app.routers.health.RawPacketRepository.get_oldest_undecrypted", return_value=None ), patch("app.routers.health.radio_manager") as mock_rm, ): mock_rm.is_setup_in_progress = True mock_rm.is_setup_complete = False data = await build_health_data(True, "Serial: /dev/ttyUSB0") assert data["status"] == "degraded" assert data["radio_connected"] is True assert data["radio_initializing"] is True assert data["radio_state"] == "initializing" @pytest.mark.asyncio async def test_health_state_paused_when_operator_disabled_connection(self, test_db): """Health reports paused when the operator has disabled reconnect attempts.""" with ( patch( "app.routers.health.RawPacketRepository.get_oldest_undecrypted", return_value=None ), patch("app.routers.health.radio_manager") as mock_rm, ): mock_rm.is_setup_in_progress = False mock_rm.is_setup_complete = False mock_rm.connection_desired = False mock_rm.is_reconnecting = False data = await build_health_data(False, "BLE: AA:BB:CC:DD:EE:FF") assert data["radio_state"] == "paused" assert data["radio_connected"] is False @pytest.mark.asyncio async def test_health_state_connecting_while_reconnect_in_progress(self, test_db): """Health reports connecting while retries are active but transport is not up yet.""" with ( patch( "app.routers.health.RawPacketRepository.get_oldest_undecrypted", return_value=None ), patch("app.routers.health.radio_manager") as mock_rm, ): mock_rm.is_setup_in_progress = False mock_rm.is_setup_complete = False mock_rm.connection_desired = True mock_rm.is_reconnecting = True data = await build_health_data(False, None) assert data["radio_state"] == "connecting" assert data["radio_connected"] is False