"""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)