FUE-T06: trigger HA discovery republish after meter declare/update
frontend / frontend (push) Successful in 2m11s
pytest / test (push) Successful in 10m36s

This commit is contained in:
2026-06-25 20:54:42 +02:00
parent efbe36d7c0
commit c26160b10b
2 changed files with 121 additions and 0 deletions
+93
View File
@@ -72,6 +72,24 @@ def _declare_payload(**overrides) -> dict:
# ---------------------------------------------------------------------------
@pytest.fixture(autouse=True)
def mock_publish_discovery():
"""Auto-mock publish_discovery for all tests in this module.
The meters API now calls _trigger_discovery_republish (best-effort) after
every successful write. publish_discovery is lazy-imported inside that
helper, so we patch it at its canonical source path
(app.services.ha_discovery.publish_discovery). Tests that need to assert
the call receive this fixture explicitly; all others benefit from the
isolation it provides (no live MQTT broker required).
"""
with patch(
"app.services.ha_discovery.publish_discovery",
return_value=None,
) as mock:
yield mock
@pytest.fixture()
def meters_client(auth_database):
"""TestClient + SQLAlchemy engine for Meter API tests."""
@@ -660,3 +678,78 @@ def test_patch_started_at_earlier_updates_boundary(meters_client):
if ended is not None and ended.tzinfo is None:
ended = ended.replace(tzinfo=UTC)
assert ended == t1_earlier
# ---------------------------------------------------------------------------
# FUE-T06: HA discovery re-publish triggered after meter writes
# ---------------------------------------------------------------------------
def test_declare_meter_triggers_publish_discovery(meters_client, mock_publish_discovery):
"""POST /api/energy/meters triggers publish_discovery after successful commit."""
client, _ = meters_client
_login(client)
t0 = datetime(2025, 1, 1, 0, 0, 0, tzinfo=UTC)
with patch("app.api.routes.api.meters.recompute_range", return_value=0):
resp = client.post(
"/api/energy/meters",
json=_declare_payload(label="Discovery Meter", started_at=t0.isoformat()),
headers={"X-CSRF-Token": _CSRF},
)
assert resp.status_code == 201
# publish_discovery must have been called exactly once after the declare.
mock_publish_discovery.assert_called_once()
def test_patch_meter_triggers_publish_discovery(meters_client, mock_publish_discovery):
"""PATCH /api/energy/meters/{id} triggers publish_discovery after successful commit."""
client, _ = meters_client
_login(client)
t0 = datetime(2025, 1, 1, 0, 0, 0, tzinfo=UTC)
with patch("app.api.routes.api.meters.recompute_range", return_value=0):
resp = client.post(
"/api/energy/meters",
json=_declare_payload(label="Original Label", started_at=t0.isoformat()),
headers={"X-CSRF-Token": _CSRF},
)
meter_id = resp.json()["id"]
# Reset call count: the POST above also called publish_discovery.
mock_publish_discovery.reset_mock()
resp = client.patch(
f"/api/energy/meters/{meter_id}",
json={"label": "Renamed Label"},
headers={"X-CSRF-Token": _CSRF},
)
assert resp.status_code == 200
# publish_discovery must have been called exactly once after the PATCH.
mock_publish_discovery.assert_called_once()
def test_declare_meter_succeeds_when_publish_discovery_raises(meters_client):
"""publish_discovery raising an exception must NOT cause POST declare to return 500.
The _trigger_discovery_republish helper is best-effort: it swallows all
exceptions so that a broken MQTT / discovery layer never breaks the API.
"""
client, _ = meters_client
_login(client)
t0 = datetime(2025, 1, 1, 0, 0, 0, tzinfo=UTC)
with (
patch("app.api.routes.api.meters.recompute_range", return_value=0),
patch(
"app.services.ha_discovery.publish_discovery",
side_effect=RuntimeError("MQTT broker unreachable"),
),
):
resp = client.post(
"/api/energy/meters",
json=_declare_payload(label="Best Effort Meter", started_at=t0.isoformat()),
headers={"X-CSRF-Token": _CSRF},
)
# The meter must be created successfully despite the discovery failure.
assert resp.status_code == 201
assert resp.json()["label"] == "Best Effort Meter"