Skip to content

Commit 147ad5f

Browse files
committed
test: pin _restart_addon untested branches per Boy-Scout
Adds unit-test coverage for the two previously-untested branches in settings_ui.py:_restart_addon: - Missing SUPERVISOR_TOKEN (settings_ui.py:780-789) — non-addon installs hit this when the user clicks Restart against a Docker/pyinstaller setup; the structured 400 must surface rather than ever reaching the Supervisor URL. - Connection-drop-as-success (settings_ui.py:798-801) — the Supervisor kills our process mid-request during a restart, so a ReadError / RemoteProtocolError / ConnectError from the POST is the documented success signal. Mirrors the _capture_handler pattern from TestSaveToolsValidation. The fixture-level server.settings.verify_ssl = True is required by this PR's post-G1 access path (httpx accepts only bool/SSLContext for verify=). Boy-Scout fix while already touching _restart_addon for the verify_ssl-propagation refactor — closes the test-coverage gap I'd flagged in homeassistant-ai#960's approve-body but never followed up on at the time.
1 parent 732a98a commit 147ad5f

1 file changed

Lines changed: 81 additions & 0 deletions

File tree

tests/src/unit/test_settings_ui.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,3 +286,84 @@ async def test_drops_garbage_state_values(self, monkeypatch, tmp_path):
286286
assert resp.status_code == 200
287287
saved = json.loads(config_path.read_text())
288288
assert saved["tools"] == {"ha_good_tool": "disabled"}
289+
290+
291+
class TestRestartAddon:
292+
"""Tests for the `/api/settings/restart` handler — pins the two
293+
untested branches in `_restart_addon` (`settings_ui.py:780-789` no-token,
294+
`:798-801` connection-drop-as-success). Boy-Scout pin landed alongside
295+
the `verify_ssl` propagation in this PR; closes the test-coverage gap
296+
flagged in #960's approve-body."""
297+
298+
def _capture_handler(self, monkeypatch, *, with_token: bool = True) -> SaveHandler:
299+
"""Capture the `_restart_addon` closure from `register_settings_routes`.
300+
301+
Mirrors `TestSaveToolsValidation._capture_handler`. `with_token`
302+
toggles the env so the no-token branch and the happy-path branches
303+
can both be exercised from the same fixture.
304+
"""
305+
if with_token:
306+
monkeypatch.setenv("SUPERVISOR_TOKEN", "fake-supervisor-token")
307+
else:
308+
monkeypatch.delenv("SUPERVISOR_TOKEN", raising=False)
309+
310+
captured: dict[str, Any] = {}
311+
312+
def custom_route_factory(path: str, methods: list[str]):
313+
def decorator(fn: Any) -> Any:
314+
if path.endswith("/api/settings/restart") and "POST" in methods:
315+
captured["restart"] = fn
316+
return fn
317+
return decorator
318+
319+
mcp = MagicMock()
320+
mcp.custom_route = MagicMock(side_effect=custom_route_factory)
321+
server = MagicMock()
322+
# `_restart_addon` reads `server.settings.verify_ssl` in this PR's
323+
# post-G1 state — must resolve to a real bool, not a MagicMock,
324+
# because httpx accepts only bool/SSLContext for `verify=`.
325+
server.settings.verify_ssl = True
326+
register_settings_routes(mcp, server, secret_path="/x")
327+
return captured["restart"]
328+
329+
@pytest.mark.asyncio
330+
async def test_returns_400_without_supervisor_token(self, monkeypatch):
331+
"""`settings_ui.py:780-789` no-token branch: when SUPERVISOR_TOKEN is
332+
unset (non-addon install), the endpoint must surface a structured 400
333+
rather than ever reaching the Supervisor URL.
334+
"""
335+
restart = self._capture_handler(monkeypatch, with_token=False)
336+
request = MagicMock()
337+
338+
resp = await restart(request)
339+
340+
assert resp.status_code == 400
341+
body = json.loads(resp.body)
342+
assert body["success"] is False
343+
assert body["error"]["code"] == "CONFIG_VALIDATION_FAILED"
344+
345+
@pytest.mark.asyncio
346+
async def test_treats_connection_drop_as_success(self, monkeypatch):
347+
"""`settings_ui.py:798-801` drop-as-success branch: the Supervisor
348+
kills our process mid-request during a restart, so a `ReadError` /
349+
`RemoteProtocolError` / `ConnectError` from the POST is the
350+
documented success signal — not a failure to surface.
351+
"""
352+
restart = self._capture_handler(monkeypatch, with_token=True)
353+
request = MagicMock()
354+
355+
# Patch the AsyncClient at the module level so the restart's
356+
# `httpx.AsyncClient(...)` block resolves to a controllable mock.
357+
mock_client = MagicMock()
358+
mock_client.post = AsyncMock(side_effect=__import__("httpx").ReadError("kill"))
359+
cm = MagicMock()
360+
cm.__aenter__ = AsyncMock(return_value=mock_client)
361+
cm.__aexit__ = AsyncMock(return_value=None)
362+
363+
with patch("ha_mcp.settings_ui.httpx.AsyncClient", return_value=cm):
364+
resp = await restart(request)
365+
366+
assert resp.status_code == 200
367+
body = json.loads(resp.body)
368+
assert body["success"] is True
369+
assert "Restart initiated" in body["message"]

0 commit comments

Comments
 (0)