Files
2026-09-09 02:53:36 +03:00

133 lines
5.1 KiB
Python

"""Backend wire and compatibility contract for the #437 summary panel."""
from pathlib import Path
import pytest
import voluptuous as vol
from pure_imports import load_pure
ROOT = Path(__file__).resolve().parents[1]
v = load_pure("hp_validation_summary", ROOT / "custom_components" / "houseplan" / "validation.py")
def panel(entity_id="sensor.live", space_id="f1"):
return {
"version": 1,
"title": "Summary",
"show_on_mobile": True,
"blocks": [{
"id": "b1", "title": "General", "visible": True,
"scope": {"type": "space", "space_id": space_id},
"values": [{
"id": "v1", "label": "Value",
"source": {"type": "entity", "entity_id": entity_id},
}],
}],
}
def config(summary=None):
value = {"spaces": [], "markers": [], "settings": {}}
if summary is not None:
value["settings"]["summary_panel"] = summary
return value
def test_summary_v1_schema_accepts_bounded_content_and_rejects_duplicate_ids():
valid = panel()
assert v.CONFIG_SCHEMA(config(valid))["settings"]["summary_panel"] == valid
duplicate = panel()
duplicate["blocks"][0]["values"].append(dict(duplicate["blocks"][0]["values"][0]))
with pytest.raises(vol.Invalid, match="value ids"):
v.CONFIG_SCHEMA(config(duplicate))
@pytest.mark.parametrize("mutation", [
lambda p: p.update(title=""),
lambda p: p.update(title=" "),
lambda p: p.update(blocks=p["blocks"] * 11),
lambda p: p["blocks"][0].update(scope={"type": "other"}),
lambda p: p["blocks"][0]["values"][0].update(source={"type": "system", "key": "other"}),
])
def test_summary_v1_schema_rejects_malformed_wire(mutation):
value = panel()
mutation(value)
with pytest.raises(vol.Invalid):
v.CONFIG_SCHEMA(config(value))
def test_old_client_omission_preserves_namespace_and_future_version_losslessly():
future = {"version": 2, "opaque": {"anything": [1, 2, 3]}}
previous = config(future)
candidate = config()
v.preserve_summary_panel_namespace(candidate, previous)
checked = v.CONFIG_SCHEMA(candidate)
assert checked["settings"]["summary_panel"] == future
def test_summary_user_text_is_trimmed_at_the_server_boundary():
value = panel(" sensor.live ")
value["title"] = " Summary "
value["blocks"][0]["title"] = " General "
value["blocks"][0]["values"][0]["label"] = " Value "
checked = v.CONFIG_SCHEMA(config(value))["settings"]["summary_panel"]
assert checked["title"] == "Summary"
assert checked["blocks"][0]["title"] == "General"
assert checked["blocks"][0]["values"][0]["label"] == "Value"
assert checked["blocks"][0]["values"][0]["source"]["entity_id"] == "sensor.live"
def test_reference_validation_allows_old_broken_ids_but_rejects_new_ones():
previous = config(panel("sensor.gone", "gone"))
unchanged = config(panel("sensor.gone", "gone"))
v.validate_summary_panel_references(unchanged, previous, set())
renamed = config(panel("sensor.gone", "gone"))
renamed["settings"]["summary_panel"]["title"] = "Renamed"
v.validate_summary_panel_references(renamed, previous, set())
replaced = config(panel("sensor.other", "gone"))
with pytest.raises(vol.Invalid, match="entity reference"):
v.validate_summary_panel_references(replaced, previous, set())
new_scope = config(panel("sensor.live", "other"))
with pytest.raises(vol.Invalid, match="space reference"):
v.validate_summary_panel_references(new_scope, previous, {"sensor.live"})
def test_ordinary_summary_contract_preserves_omission_but_not_explicit_empty():
previous = config(panel())
previous["settings"].update(known=True, future={"sentinel": 1})
omitted = {"spaces": [], "markers": [], "settings": {"known": True, "future": {"sentinel": 1}}}
checked = v.prepare_ordinary_summary_candidate(
omitted, previous, {"sensor.live"}, v.CONFIG_SCHEMA,
)
assert checked["settings"]["summary_panel"] == previous["settings"]["summary_panel"]
assert checked["settings"]["known"] is True
assert checked["settings"]["future"] == {"sentinel": 1}
empty = config({
"version": 1, "title": "Empty", "show_on_mobile": True, "blocks": [],
})
checked_empty = v.prepare_ordinary_summary_candidate(
empty, previous, set(), v.CONFIG_SCHEMA,
)
assert checked_empty["settings"]["summary_panel"]["blocks"] == []
def test_ordinary_summary_contract_validates_after_writer_normalization():
calls = []
previous = config(panel("sensor.old"))
candidate = config()
def normalize(value):
calls.append(value["settings"]["summary_panel"]["title"])
value["settings"]["summary_panel"]["title"] = "Normalized"
return v.CONFIG_SCHEMA(value)
checked = v.prepare_ordinary_summary_candidate(candidate, previous, set(), normalize)
assert calls == ["Summary"]
assert checked["settings"]["summary_panel"]["title"] == "Normalized"
replacement = config(panel("sensor.denied"))
with pytest.raises(vol.Invalid, match="entity reference"):
v.prepare_ordinary_summary_candidate(replacement, previous, set(), v.CONFIG_SCHEMA)