152 lines
4.9 KiB
Python
152 lines
4.9 KiB
Python
|
|
"""Integration tests for SystemHealthCheck using aioresponses.
|
||
|
|
|
||
|
|
Uses real aiosqlite Storage + EventEmitter + mocked HTTP.
|
||
|
|
Marked 'integration' because it exercises the full stack end-to-end.
|
||
|
|
"""
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import json
|
||
|
|
from pathlib import Path
|
||
|
|
from typing import AsyncGenerator
|
||
|
|
|
||
|
|
import pytest
|
||
|
|
import pytest_asyncio
|
||
|
|
from aioresponses import aioresponses
|
||
|
|
|
||
|
|
from ha_diag.checks.system_health import SystemHealthCheck
|
||
|
|
from ha_diag.config import Settings
|
||
|
|
from ha_diag.event_emitter import EventEmitter
|
||
|
|
from ha_diag.ha_client import HAClient, make_session
|
||
|
|
from ha_diag.models import HAEventType
|
||
|
|
from ha_diag.storage import Storage
|
||
|
|
|
||
|
|
HA_URL = "http://ha-test-ken:8123"
|
||
|
|
|
||
|
|
|
||
|
|
def _settings(**overrides) -> Settings:
|
||
|
|
defaults: dict = {
|
||
|
|
"ha_url": HA_URL,
|
||
|
|
"ha_token": "test-token",
|
||
|
|
"node_name": "piha",
|
||
|
|
"location_tag": "ken",
|
||
|
|
"alert_cooldown_hours": 0.0,
|
||
|
|
"check_interval": 60,
|
||
|
|
"check_interval_unavailable": 3600,
|
||
|
|
}
|
||
|
|
defaults.update(overrides)
|
||
|
|
return Settings(**defaults)
|
||
|
|
|
||
|
|
|
||
|
|
@pytest_asyncio.fixture
|
||
|
|
async def storage(tmp_path: Path) -> AsyncGenerator[Storage, None]:
|
||
|
|
s = Storage(tmp_path / "integration_test.db")
|
||
|
|
await s.open()
|
||
|
|
yield s
|
||
|
|
await s.close()
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.fixture
|
||
|
|
def events_dir(tmp_path: Path) -> Path:
|
||
|
|
d = tmp_path / "events"
|
||
|
|
d.mkdir()
|
||
|
|
return d
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.integration
|
||
|
|
async def test_system_health_ok_components_no_event(
|
||
|
|
storage: Storage, events_dir: Path
|
||
|
|
):
|
||
|
|
"""All components healthy on first run → no events emitted."""
|
||
|
|
health = {
|
||
|
|
"homeassistant": {"type": "result", "data": {"version": "2025.5.0"}},
|
||
|
|
"recorder": {"type": "result", "data": {"backlog": 0}},
|
||
|
|
}
|
||
|
|
emitter = EventEmitter(events_dir, node_name="piha", location_tag="ken")
|
||
|
|
|
||
|
|
with aioresponses() as m:
|
||
|
|
m.get(f"{HA_URL}/api/system_health", payload=health)
|
||
|
|
async with make_session("test-token") as session:
|
||
|
|
client = HAClient(HA_URL, session)
|
||
|
|
check = SystemHealthCheck(client, storage, _settings())
|
||
|
|
results = await check.run()
|
||
|
|
|
||
|
|
assert results == []
|
||
|
|
assert not list(events_dir.glob("*.json"))
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.integration
|
||
|
|
async def test_system_health_degraded_emits_event_and_writes_file(
|
||
|
|
storage: Storage, events_dir: Path
|
||
|
|
):
|
||
|
|
"""Component degrades: event emitted + file written with correct structure."""
|
||
|
|
# First run: all ok
|
||
|
|
health_ok = {"cloud": {"type": "result", "data": {}}}
|
||
|
|
health_err = {"cloud": {"type": "error", "error": "Cloud connection lost"}}
|
||
|
|
emitter = EventEmitter(events_dir, node_name="piha", location_tag="ken")
|
||
|
|
|
||
|
|
with aioresponses() as m:
|
||
|
|
m.get(f"{HA_URL}/api/system_health", payload=health_ok)
|
||
|
|
async with make_session("test-token") as session:
|
||
|
|
client = HAClient(HA_URL, session)
|
||
|
|
await SystemHealthCheck(client, storage, _settings()).run()
|
||
|
|
|
||
|
|
# Second run: cloud errors
|
||
|
|
with aioresponses() as m:
|
||
|
|
m.get(f"{HA_URL}/api/system_health", payload=health_err)
|
||
|
|
async with make_session("test-token") as session:
|
||
|
|
client = HAClient(HA_URL, session)
|
||
|
|
check = SystemHealthCheck(client, storage, _settings())
|
||
|
|
results = await check.run()
|
||
|
|
|
||
|
|
assert len(results) == 1
|
||
|
|
assert results[0].event_type == HAEventType.ha_system_health_degraded
|
||
|
|
|
||
|
|
emitter.emit(
|
||
|
|
event_type=results[0].event_type,
|
||
|
|
severity=results[0].severity.value,
|
||
|
|
service="homeassistant",
|
||
|
|
message=results[0].message,
|
||
|
|
payload=results[0].payload,
|
||
|
|
)
|
||
|
|
|
||
|
|
files = list(events_dir.glob("*.json"))
|
||
|
|
assert len(files) == 1
|
||
|
|
data = json.loads(files[0].read_text())
|
||
|
|
assert data["type"] == "ha_system_health_degraded"
|
||
|
|
assert data["payload"]["component"] == "cloud"
|
||
|
|
assert data["payload"]["location_tag"] == "ken"
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.integration
|
||
|
|
async def test_system_health_recovery_and_re_degradation(storage: Storage):
|
||
|
|
"""Full ok→error→ok→error cycle: events fire on degradation, not on recovery."""
|
||
|
|
def _run(health):
|
||
|
|
with aioresponses() as m:
|
||
|
|
m.get(f"{HA_URL}/api/system_health", payload=health)
|
||
|
|
return make_session("test-token"), health
|
||
|
|
|
||
|
|
settings = _settings()
|
||
|
|
|
||
|
|
async def run_once(health):
|
||
|
|
with aioresponses() as m:
|
||
|
|
m.get(f"{HA_URL}/api/system_health", payload=health)
|
||
|
|
async with make_session("test-token") as session:
|
||
|
|
return await SystemHealthCheck(
|
||
|
|
HAClient(HA_URL, session), storage, settings
|
||
|
|
).run()
|
||
|
|
|
||
|
|
ok_h = {"cloud": {"type": "result", "data": {}}}
|
||
|
|
err_h = {"cloud": {"type": "error", "error": "timeout"}}
|
||
|
|
|
||
|
|
r1 = await run_once(ok_h) # baseline ok
|
||
|
|
r2 = await run_once(err_h) # first degradation
|
||
|
|
r3 = await run_once(err_h) # sustained error (no dup)
|
||
|
|
r4 = await run_once(ok_h) # recovery
|
||
|
|
r5 = await run_once(err_h) # second degradation
|
||
|
|
|
||
|
|
assert r1 == []
|
||
|
|
assert len(r2) == 1
|
||
|
|
assert r3 == []
|
||
|
|
assert r4 == []
|
||
|
|
assert len(r5) == 1
|