Files
houseplan-card/tests_backend/test_validation.py
T
Matysh 49b0cb4e05 perf/fix v1.43.1: external audit P1 — render cost, drag threshold, geometry, backend
L1: memoized space model + open pairs (structural fingerprint key, epoch
bumped synchronously at mutation time, not inside the debounce); hoisted
per-room geometry out of the render loop; smoke asserts zero recomputation
across state pushes.

L4: openings get the 3 px drag threshold used by every other pipeline and
only write when the geometry actually changed — taps open the dialog again.

G2: interiorPoint() replaces the vertex mean, so island rooms inside
concave (U/L) parents are accepted and their evenodd holes render; traced
duplicates still are not containment.

G3: segKey rounds before ordering — one shared wall, one key.

B2: _check_write fails closed when the entry is unavailable.
B3: layout/set honours expected_rev and returns the new rev.
B4: config/set without expected_rev over a non-empty store logs a warning.
B5: coordinates reject NaN/Infinity; spaces/rooms/markers/decor/layout capped.

+2 unit tests (118), +2 backend tests (14), smoke_render_perf; docs
same-commit
2026-07-27 10:58:18 +03:00

143 lines
5.1 KiB
Python

"""Unit tests for the pure House Plan validation (validation.py is loaded by path,
without importing the HA integration package)."""
import importlib.util
import os
import pytest
import voluptuous as vol
_PATH = os.path.join(
os.path.dirname(os.path.dirname(__file__)),
"custom_components", "houseplan", "validation.py",
)
_spec = importlib.util.spec_from_file_location("hp_validation", _PATH)
v = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(v)
def test_sanitize_marker_id():
assert v.sanitize_marker_id("../etc/passwd") == "_etc_passwd"
assert v.sanitize_marker_id("..") == "misc" # pure traversal → misc
assert v.sanitize_marker_id(".") == "misc"
assert v.sanitize_marker_id("") == "misc"
assert len(v.sanitize_marker_id("a" * 200)) == 64
def test_sanitize_filename_strips_path():
assert v.sanitize_filename("/a/b/c/manual.pdf") == "manual.pdf"
assert v.sanitize_filename("..\\..\\evil.pdf") == "evil.pdf" # backslashes = a path
assert v.sanitize_filename("...hidden.pdf") == "hidden.pdf" # leading dots stripped
def test_file_ext():
assert v.file_ext("manual.PDF") == "pdf"
assert v.file_ext("a/b/x.png") == "png"
assert v.file_ext("noext") == ""
def test_valid_space_id():
assert v.valid_space_id("f1")
assert v.valid_space_id("floor-2_a")
assert not v.valid_space_id("Floor 1")
assert not v.valid_space_id("a" * 65)
assert not v.valid_space_id("../x")
def test_room_schema_poly_or_rect():
v.ROOM_SCHEMA({"id": "r1", "name": "A", "poly": [[0, 0], [1, 0], [1, 1]]})
v.ROOM_SCHEMA({"id": "r2", "name": "B", "x": 0, "y": 0, "w": 1, "h": 1})
with pytest.raises(vol.Invalid):
v.ROOM_SCHEMA({"id": "r3", "name": "C"})
with pytest.raises(vol.Invalid):
v.ROOM_SCHEMA({"id": "r4", "name": "D", "poly": [[0, 0], [1, 1]]})
def test_space_schema_aspect_range():
ok = {"id": "f1", "title": "1", "aspect": 1.4, "view_box": [0, 0, 1, 1], "rooms": []}
v.SPACE_SCHEMA(ok)
with pytest.raises(vol.Invalid):
v.SPACE_SCHEMA({**ok, "aspect": 0})
with pytest.raises(vol.Invalid):
v.SPACE_SCHEMA({**ok, "view_box": [0, 0, 1]})
def test_marker_schema():
v.MARKER_SCHEMA({"id": "m1", "binding": "device:abc"})
v.MARKER_SCHEMA({"id": "m2", "binding": "virtual", "name": "X",
"pdfs": [{"name": "a.pdf", "url": "/u/a.pdf"}]})
with pytest.raises(vol.Invalid):
v.MARKER_SCHEMA({"binding": "virtual"})
def test_config_schema_defaults_and_extra():
out = v.CONFIG_SCHEMA({"spaces": []})
assert out["markers"] == [] and out["settings"] == {}
out2 = v.CONFIG_SCHEMA({"spaces": [], "virtual_devices": [], "device_overrides": {}})
assert "spaces" in out2
def test_config_schema_full_roundtrip():
cfg = {
"spaces": [{
"id": "f1", "title": "Floor 1", "plan_url": "/p/f1.svg",
"aspect": 0.8, "view_box": [0, 0, 1, 1],
"rooms": [{"id": "r1", "name": "Hall", "area": "hall",
"poly": [[0, 0], [0.5, 0], [0.5, 0.5], [0, 0.5]]}],
"segments": [[0, 0, 0.5, 0]],
}],
"markers": [{"id": "d1", "binding": "device:x", "model": "M", "link": "https://e.com"}],
"settings": {"group_lights": True},
}
out = v.CONFIG_SCHEMA(cfg)
assert out["spaces"][0]["rooms"][0]["area"] == "hall"
assert out["markers"][0]["binding"] == "device:x"
def test_layout_schema():
v.LAYOUT_SCHEMA({"dev1": {"x": 0.1, "y": 0.2, "s": "f1"}})
with pytest.raises(vol.Invalid):
v.LAYOUT_SCHEMA({"dev1": {"x": 0.1}})
def test_space_display_settings():
"""Per-space display settings validate; garbage color/mode is rejected."""
ok = {
"id": "f1", "title": "Floor 1", "aspect": 1.0, "view_box": [0, 0, 1, 1],
"rooms": [], "settings": {
"show_borders": True, "show_names": False,
"room_color": "#3ea6ff", "room_opacity": 0.5, "fill_mode": "lqi",
},
}
v.SPACE_SCHEMA(ok)
import pytest as _pytest
bad_color = dict(ok, settings={"room_color": "javascript:x"})
with _pytest.raises(Exception):
v.SPACE_SCHEMA(bad_color)
bad_mode = dict(ok, settings={"fill_mode": "rainbow"})
with _pytest.raises(Exception):
v.SPACE_SCHEMA(bad_mode)
def test_space_temp_bounds():
"""Temperature comfort bounds validate as floats; temp fill mode accepted."""
ok = {
"id": "f1", "title": "F", "aspect": 1.0, "view_box": [0, 0, 1, 1], "rooms": [],
"settings": {"fill_mode": "temp", "temp_min": 19.5, "temp_max": "24"},
}
v.SPACE_SCHEMA(ok)
def test_finite_coordinates_rejected():
"""audit B5: NaN/Infinity coordinates must not reach storage."""
for bad in ("NaN", "Infinity", "-Infinity", float("nan"), float("inf")):
with pytest.raises(vol.Invalid):
v.LAYOUT_SCHEMA({"dev1": {"x": bad, "y": 0.5}})
assert v.LAYOUT_SCHEMA({"dev1": {"x": 0.5, "y": 0.25}})
def test_collection_caps():
"""audit B5: unbounded collections are capped."""
big = {f"d{i}": {"x": 0.1, "y": 0.1} for i in range(v.MAX_LAYOUT + 1)}
with pytest.raises(vol.Invalid):
v.LAYOUT_SCHEMA(big)