"""Tests for HAClient using unittest.mock to avoid aioresponses/aiohttp version coupling.""" from __future__ import annotations from unittest.mock import AsyncMock, MagicMock import pytest from ha_diag.ha_client import HAClient, make_session HA_URL = "http://homeassistant.test:8123" TOKEN = "test-token" def _mock_resp(payload=None, text=None, status=200): """Return a mock that behaves like an aiohttp response context manager.""" resp = MagicMock() resp.status = status if status >= 400: resp.raise_for_status.side_effect = Exception(f"HTTP {status}") else: resp.raise_for_status = MagicMock() resp.json = AsyncMock(return_value=payload if payload is not None else {}) resp.text = AsyncMock(return_value=text or "") resp.__aenter__ = AsyncMock(return_value=resp) resp.__aexit__ = AsyncMock(return_value=False) return resp def _mock_session(get_resp=None): session = MagicMock() session.get.return_value = get_resp or _mock_resp() return session @pytest.mark.asyncio async def test_get_api_status_ok(): session = _mock_session(_mock_resp({"message": "API running."})) client = HAClient(HA_URL, session) result = await client.get_api_status() assert result == {"message": "API running."} @pytest.mark.asyncio async def test_get_api_status_unauthorized(): session = _mock_session(_mock_resp(status=401)) client = HAClient(HA_URL, session) with pytest.raises(Exception): await client.get_api_status() @pytest.mark.asyncio async def test_get_states_returns_list(): payload = [{"entity_id": "light.living_room", "state": "on"}] session = _mock_session(_mock_resp(payload)) client = HAClient(HA_URL, session) states = await client.get_states() assert isinstance(states, list) assert states[0]["entity_id"] == "light.living_room" @pytest.mark.asyncio async def test_get_config_returns_dict(): payload = {"version": "2024.1.0", "location_name": "Home"} session = _mock_session(_mock_resp(payload)) client = HAClient(HA_URL, session) config = await client.get_config() assert config["version"] == "2024.1.0" @pytest.mark.asyncio async def test_get_entity_registry_returns_list(): payload = [ {"entity_id": "light.hall", "platform": "zha", "area_id": "hallway"}, {"entity_id": "sensor.temp", "platform": "mqtt", "area_id": None}, ] session = _mock_session(_mock_resp(payload)) client = HAClient(HA_URL, session) registry = await client.get_entity_registry() assert len(registry) == 2 assert registry[0]["platform"] == "zha" @pytest.mark.asyncio async def test_make_session_sets_auth_header(): """make_session injects the Bearer token in all requests.""" async with make_session("my-secret-token") as session: assert session.headers.get("Authorization") == "Bearer my-secret-token" # --------------------------------------------------------------------------- # Entity registry TTL cache (Phase 3 Flag #3) # --------------------------------------------------------------------------- @pytest.mark.asyncio async def test_entity_registry_cached_on_second_call(): """Second call within TTL returns cache, making only one HTTP request.""" payload = [{"entity_id": "light.hall", "platform": "zha", "area_id": "hallway"}] session = _mock_session(_mock_resp(payload)) client = HAClient(HA_URL, session, entity_registry_cache_ttl=300.0) r1 = await client.get_entity_registry() r2 = await client.get_entity_registry() # from cache assert r1 == r2 session.get.assert_called_once() # only one HTTP request @pytest.mark.asyncio async def test_entity_registry_cache_bypassed_after_ttl(): """After TTL expiry (ttl=0), next call fetches fresh data.""" payload = [{"entity_id": "light.hall", "platform": "zha", "area_id": "hallway"}] session = _mock_session(_mock_resp(payload)) # TTL=0 means every call is stale → two fetches session.get.side_effect = [_mock_resp(payload), _mock_resp(payload)] client = HAClient(HA_URL, session, entity_registry_cache_ttl=0.0) await client.get_entity_registry() await client.get_entity_registry() assert session.get.call_count == 2 @pytest.mark.asyncio async def test_invalidate_registry_cache_forces_refetch(): """invalidate_registry_cache() makes the next call hit the network.""" payload = [{"entity_id": "light.hall", "platform": "zha", "area_id": ""}] session = _mock_session(_mock_resp(payload)) session.get.side_effect = [_mock_resp(payload), _mock_resp(payload)] client = HAClient(HA_URL, session, entity_registry_cache_ttl=300.0) await client.get_entity_registry() client.invalidate_registry_cache() await client.get_entity_registry() assert session.get.call_count == 2 @pytest.mark.asyncio async def test_entity_registry_cache_default_ttl_is_300(): session = _mock_session() client = HAClient(HA_URL, session) assert client._registry_cache_ttl == 300.0