mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
133 lines
5.1 KiB
Python
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)
|