mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-06 06:38:57 +00:00
v1.60.0: harden background editing and device state
This commit is contained in:
@@ -59,6 +59,7 @@ async def test_deleted_marker_rejects_a_stale_layout_update(
|
||||
"markers": [
|
||||
{"id": "dev1", "binding": "device:dev1", "removed": True},
|
||||
{"id": "v_live", "binding": "virtual", "name": "Still here"},
|
||||
{"id": "v_real", "binding": "device:real-device"},
|
||||
],
|
||||
"settings": {},
|
||||
}
|
||||
@@ -84,8 +85,20 @@ async def test_deleted_marker_rejects_a_stale_layout_update(
|
||||
assert ignored_virtual["success"]
|
||||
assert ignored_virtual["result"]["ignored"] == "missing_virtual"
|
||||
|
||||
# `v_` is only the historical virtual naming convention. An explicit
|
||||
# non-virtual marker with that prefix must remain positionable.
|
||||
await client.send_json_auto_id({
|
||||
"type": "houseplan/layout/update",
|
||||
"device_id": "v_real",
|
||||
"pos": {"s": "f1", "x": 0.2, "y": 0.25},
|
||||
})
|
||||
real = await client.receive_json()
|
||||
assert real["success"] and "ignored" not in real["result"]
|
||||
|
||||
await client.send_json_auto_id({"type": "houseplan/layout/get"})
|
||||
assert (await client.receive_json())["result"]["layout"] == {}
|
||||
assert (await client.receive_json())["result"]["layout"] == {
|
||||
"v_real": {"s": "f1", "x": 0.2, "y": 0.25},
|
||||
}
|
||||
|
||||
# The compatibility wholesale endpoint applies the same filter.
|
||||
await client.send_json_auto_id({
|
||||
@@ -94,14 +107,18 @@ async def test_deleted_marker_rejects_a_stale_layout_update(
|
||||
"dev1": {"s": "f1", "x": 0.1, "y": 0.2},
|
||||
"v_deleted": {"s": "f1", "x": 0.3, "y": 0.4},
|
||||
"v_live": {"s": "f1", "x": 0.4, "y": 0.5},
|
||||
"v_real": {"s": "f1", "x": 0.45, "y": 0.55},
|
||||
"rl_r1": {"s": "f1", "x": 0.35, "y": 0.45},
|
||||
"ordinary_auto_device": {"s": "f1", "x": 0.5, "y": 0.6},
|
||||
},
|
||||
"expected_rev": 0,
|
||||
"expected_rev": 1,
|
||||
})
|
||||
assert (await client.receive_json())["success"]
|
||||
await client.send_json_auto_id({"type": "houseplan/layout/get"})
|
||||
assert (await client.receive_json())["result"]["layout"] == {
|
||||
"v_live": {"s": "f1", "x": 0.4, "y": 0.5},
|
||||
"v_real": {"s": "f1", "x": 0.45, "y": 0.55},
|
||||
"rl_r1": {"s": "f1", "x": 0.35, "y": 0.45},
|
||||
"ordinary_auto_device": {"s": "f1", "x": 0.5, "y": 0.6},
|
||||
}
|
||||
|
||||
@@ -119,6 +136,20 @@ async def test_trail_delete_clears_the_server_book(
|
||||
assert "m1" not in recorder.book.data
|
||||
|
||||
|
||||
async def test_trail_delete_rejects_non_admin_without_mutation(
|
||||
hass: HomeAssistant, hass_ws_client: WebSocketGenerator
|
||||
) -> None:
|
||||
await _setup(hass)
|
||||
recorder = hass.data[DOMAIN]["trail_recorder"]
|
||||
recorder.book.data["m1"] = {"current": {"points": [[1, 2]]}}
|
||||
user = await hass.auth.async_create_user("ordinary")
|
||||
client = await hass_ws_client(hass, user=user)
|
||||
await client.send_json_auto_id({"type": "houseplan/trail/delete", "marker_id": "m1"})
|
||||
response = await client.receive_json()
|
||||
assert not response["success"] and response["error"]["code"] == "unauthorized"
|
||||
assert "m1" in recorder.book.data
|
||||
|
||||
|
||||
async def test_config_rev_conflict(hass: HomeAssistant, hass_ws_client: WebSocketGenerator) -> None:
|
||||
await _setup(hass)
|
||||
client = await hass_ws_client(hass)
|
||||
|
||||
@@ -143,6 +143,36 @@ def test_trail_book_delete_forgets_current_and_previous_runs():
|
||||
assert book.delete("m1") is False
|
||||
|
||||
|
||||
def test_missing_trail_delete_does_not_mutate_live_tracking_pairs():
|
||||
rec, _hass, _states = _rec()
|
||||
before = {src: list(pairs) for src, pairs in rec.pairs.items()}
|
||||
unsubscribed = []
|
||||
rec._unsub_track = lambda: unsubscribed.append(True)
|
||||
assert _run_isolated(rec.async_delete("m1")) is False
|
||||
assert rec.pairs == before
|
||||
assert unsubscribed == []
|
||||
|
||||
|
||||
def test_trail_delete_prunes_pair_and_replaces_subscription():
|
||||
tracked = []
|
||||
old_track = trails.async_track_state_change_event
|
||||
trails.async_track_state_change_event = lambda hass, ents, cb: (
|
||||
tracked.append(list(ents)) or (lambda: None)
|
||||
)
|
||||
try:
|
||||
rec, _hass, _states = _rec()
|
||||
rec.pairs["other.source"] = [("m2", "vacuum.other")]
|
||||
rec.book.data["m1"] = {"current": {"points": [[1, 2]]}}
|
||||
unsubscribed = []
|
||||
rec._unsub_track = lambda: unsubscribed.append(True)
|
||||
assert _run_isolated(rec.async_delete("m1")) is True
|
||||
assert rec.pairs == {"other.source": [("m2", "vacuum.other")]}
|
||||
assert unsubscribed == [True]
|
||||
assert tracked == [["other.source", "vacuum.other"]]
|
||||
finally:
|
||||
trails.async_track_state_change_event = old_track
|
||||
|
||||
|
||||
def test_object_style_position_is_read():
|
||||
# Tasshack in-memory attributes hold a Point OBJECT, not a dict
|
||||
class Point:
|
||||
|
||||
@@ -136,6 +136,15 @@ def test_space_backdrop_transform_is_optional_and_bounded():
|
||||
v.SPACE_SCHEMA({**ok, **bad})
|
||||
|
||||
|
||||
def test_space_cell_scale_is_optional_finite_and_bounded():
|
||||
ok = {"id": "f1", "title": "1", "view_box": [0, 0, 1, 1], "rooms": []}
|
||||
assert "cell_cm" not in v.SPACE_SCHEMA(ok)
|
||||
assert v.SPACE_SCHEMA({**ok, "cell_cm": 20})["cell_cm"] == 20.0
|
||||
for bad in (0, -1, v.CELL_CM_MAX + 1, float("nan"), float("inf")):
|
||||
with pytest.raises(vol.Invalid):
|
||||
v.SPACE_SCHEMA({**ok, "cell_cm": bad})
|
||||
|
||||
|
||||
def test_marker_schema():
|
||||
v.MARKER_SCHEMA({"id": "m1", "binding": "device:abc"})
|
||||
v.MARKER_SCHEMA({"id": "m2", "binding": "virtual", "name": "X",
|
||||
|
||||
Reference in New Issue
Block a user