fix(ha-diag-agent): structlog event kwarg collision + replace aioresponses
- main.py: rename event= to ha_event= in _log.warning() — structlog treats 'event' as a reserved positional arg; the old name caused TypeError when any check returned unhealthy results (events were still emitted, but the check was logged as check_error instead of check_unhealthy) - tests/test_ha_client.py: replace aioresponses with unittest.mock — aioresponses 0.7.8 is incompatible with aiohttp >=3.12 (missing stream_writer kwarg) - pyproject.toml: remove aioresponses from dev dependencies Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
6953815f41
commit
9ec43b6829
|
|
@ -22,7 +22,6 @@ dependencies = [
|
|||
dev = [
|
||||
"pytest>=8.1",
|
||||
"pytest-asyncio>=0.23",
|
||||
"aioresponses>=0.7",
|
||||
]
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ async def _run_check_and_emit(
|
|||
_log.warning(
|
||||
"check_unhealthy",
|
||||
check=check.name,
|
||||
event=result.event_type,
|
||||
ha_event=result.event_type,
|
||||
msg=result.message,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
"""Tests for HAClient using aioresponses to mock aiohttp."""
|
||||
"""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 aioresponses import aioresponses
|
||||
|
||||
from ha_diag.ha_client import HAClient, make_session
|
||||
|
||||
|
|
@ -10,11 +11,30 @@ 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():
|
||||
with aioresponses() as m:
|
||||
m.get(f"{HA_URL}/api/", payload={"message": "API running."})
|
||||
async with make_session(TOKEN) as session:
|
||||
session = _mock_session(_mock_resp({"message": "API running."}))
|
||||
client = HAClient(HA_URL, session)
|
||||
result = await client.get_api_status()
|
||||
assert result == {"message": "API running."}
|
||||
|
|
@ -22,9 +42,7 @@ async def test_get_api_status_ok():
|
|||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_api_status_unauthorized():
|
||||
with aioresponses() as m:
|
||||
m.get(f"{HA_URL}/api/", status=401)
|
||||
async with make_session(TOKEN) as session:
|
||||
session = _mock_session(_mock_resp(status=401))
|
||||
client = HAClient(HA_URL, session)
|
||||
with pytest.raises(Exception):
|
||||
await client.get_api_status()
|
||||
|
|
@ -33,9 +51,7 @@ async def test_get_api_status_unauthorized():
|
|||
@pytest.mark.asyncio
|
||||
async def test_get_states_returns_list():
|
||||
payload = [{"entity_id": "light.living_room", "state": "on"}]
|
||||
with aioresponses() as m:
|
||||
m.get(f"{HA_URL}/api/states", payload=payload)
|
||||
async with make_session(TOKEN) as session:
|
||||
session = _mock_session(_mock_resp(payload))
|
||||
client = HAClient(HA_URL, session)
|
||||
states = await client.get_states()
|
||||
assert isinstance(states, list)
|
||||
|
|
@ -45,9 +61,7 @@ async def test_get_states_returns_list():
|
|||
@pytest.mark.asyncio
|
||||
async def test_get_config_returns_dict():
|
||||
payload = {"version": "2024.1.0", "location_name": "Home"}
|
||||
with aioresponses() as m:
|
||||
m.get(f"{HA_URL}/api/config", payload=payload)
|
||||
async with make_session(TOKEN) as session:
|
||||
session = _mock_session(_mock_resp(payload))
|
||||
client = HAClient(HA_URL, session)
|
||||
config = await client.get_config()
|
||||
assert config["version"] == "2024.1.0"
|
||||
|
|
@ -59,9 +73,7 @@ async def test_get_entity_registry_returns_list():
|
|||
{"entity_id": "light.hall", "platform": "zha", "area_id": "hallway"},
|
||||
{"entity_id": "sensor.temp", "platform": "mqtt", "area_id": None},
|
||||
]
|
||||
with aioresponses() as m:
|
||||
m.get(f"{HA_URL}/api/config/entity_registry", payload=payload)
|
||||
async with make_session(TOKEN) as session:
|
||||
session = _mock_session(_mock_resp(payload))
|
||||
client = HAClient(HA_URL, session)
|
||||
registry = await client.get_entity_registry()
|
||||
assert len(registry) == 2
|
||||
|
|
@ -71,12 +83,7 @@ async def test_get_entity_registry_returns_list():
|
|||
@pytest.mark.asyncio
|
||||
async def test_make_session_sets_auth_header():
|
||||
"""make_session injects the Bearer token in all requests."""
|
||||
with aioresponses() as m:
|
||||
m.get(f"{HA_URL}/api/", payload={"message": "API running."})
|
||||
async with make_session("my-secret-token") as session:
|
||||
client = HAClient(HA_URL, session)
|
||||
await client.get_api_status()
|
||||
# Verify the Authorization header was sent
|
||||
assert session.headers.get("Authorization") == "Bearer my-secret-token"
|
||||
|
||||
|
||||
|
|
@ -89,47 +96,42 @@ async def test_make_session_sets_auth_header():
|
|||
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"}]
|
||||
with aioresponses() as m:
|
||||
m.get(f"{HA_URL}/api/config/entity_registry", payload=payload)
|
||||
async with make_session(TOKEN) as session:
|
||||
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 — no second HTTP call
|
||||
r2 = await client.get_entity_registry() # from cache
|
||||
assert r1 == r2
|
||||
# aioresponses would raise ConnectionError on the unmocked second request
|
||||
# if caching weren't working; reaching here means it used the cache.
|
||||
session.get.assert_called_once() # only one HTTP request
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_entity_registry_cache_bypassed_after_ttl(monkeypatch):
|
||||
"""After TTL expiry, next call fetches fresh data."""
|
||||
import time
|
||||
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"}]
|
||||
with aioresponses() as m:
|
||||
m.get(f"{HA_URL}/api/config/entity_registry", payload=payload)
|
||||
m.get(f"{HA_URL}/api/config/entity_registry", payload=payload)
|
||||
async with make_session(TOKEN) as session:
|
||||
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() # fetches
|
||||
await client.get_entity_registry() # TTL=0 → fetches again
|
||||
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": ""}]
|
||||
with aioresponses() as m:
|
||||
m.get(f"{HA_URL}/api/config/entity_registry", payload=payload)
|
||||
m.get(f"{HA_URL}/api/config/entity_registry", payload=payload)
|
||||
async with make_session(TOKEN) as session:
|
||||
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() # must hit network again
|
||||
await client.get_entity_registry()
|
||||
assert session.get.call_count == 2
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_entity_registry_cache_default_ttl_is_300():
|
||||
async with make_session(TOKEN) as session:
|
||||
session = _mock_session()
|
||||
client = HAClient(HA_URL, session)
|
||||
assert client._registry_cache_ttl == 300.0
|
||||
|
|
|
|||
Loading…
Reference in a new issue