From c1e3cdb7688af59ef875ec486f70fbdda578eca6 Mon Sep 17 00:00:00 2001 From: Matysh Date: Mon, 27 Jul 2026 11:23:39 +0300 Subject: [PATCH] test: HA-harness expectations follow the authenticated content URLs (audit B1) - upload/plan_set tests asserted the old public /houseplan_files/... paths and only run in CI, so the B1 change surfaced there - +test for the fail-closed admin check (audit B2/T4: the authorization boundary had zero coverage) --- tests_backend/test_ha_upload.py | 5 +++-- tests_backend/test_ha_websocket.py | 27 ++++++++++++++++++++++++++- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/tests_backend/test_ha_upload.py b/tests_backend/test_ha_upload.py index acee95e..41e4464 100644 --- a/tests_backend/test_ha_upload.py +++ b/tests_backend/test_ha_upload.py @@ -31,7 +31,8 @@ async def test_upload_ok(hass: HomeAssistant, hass_client: ClientSessionGenerato resp = await client.post("/api/houseplan/upload", data=fd) assert resp.status == 200 body = await resp.json() - assert body["ok"] and body["url"].startswith("/houseplan_files/files/m1/manual.pdf?v=") + # audit B1: uploads now return the AUTHENTICATED content URL + assert body["ok"] and body["url"].startswith("/api/houseplan/content/files/m1/manual.pdf?v=") async def test_upload_bad_ext(hass: HomeAssistant, hass_client: ClientSessionGenerator) -> None: @@ -57,4 +58,4 @@ async def test_upload_traversal_sanitized(hass: HomeAssistant, hass_client: Clie # no path segment may be exactly ".." (dots inside a name are harmless) path = body["url"].split("?", 1)[0] assert all(seg != ".." for seg in path.split("/")) - assert path.startswith("/houseplan_files/files/") + assert path.startswith("/api/houseplan/content/files/") diff --git a/tests_backend/test_ha_websocket.py b/tests_backend/test_ha_websocket.py index 917c77e..ed2b6f1 100644 --- a/tests_backend/test_ha_websocket.py +++ b/tests_backend/test_ha_websocket.py @@ -99,4 +99,29 @@ async def test_plan_set_validates(hass: HomeAssistant, hass_ws_client: WebSocket {"type": "houseplan/plan/set", "space_id": "s1", "ext": "png", "data": "aGVsbG8="} ) resp = await client.receive_json() - assert resp["success"] and resp["result"]["url"].startswith("/houseplan_files/plans/s1.png?v=") + assert resp["success"] and resp["result"]["url"].startswith("/api/houseplan/content/plans/_/s1.png?v=") + + +async def test_admin_check_fails_closed(hass, hass_ws_client): + """audit B2/T4: with no config entry the policy is unknown — deny writes. + + This used to allow them: plan uploads slipped through during a reload. + """ + from custom_components.houseplan import websocket_api as wsapi + + class _User: + is_admin = False + + class _Conn: + user = _User() + + # no entry at all → non-admin must be refused + assert wsapi._check_write(hass, _Conn()) is False + + class _Admin: + is_admin = True + + class _AdminConn: + user = _Admin() + + assert wsapi._check_write(hass, _AdminConn()) is True