Files
houseplan-card/tests_backend/test_frontend_assets.py
T
2026-08-28 05:40:28 +00:00

69 lines
2.7 KiB
Python

"""Pure path/manifest guards for public frontend chunks (#337)."""
from __future__ import annotations
import json
import importlib.util
from pathlib import Path
_PATH = Path(__file__).parents[1] / "custom_components" / "houseplan" / "frontend_asset_manifest.py"
_SPEC = importlib.util.spec_from_file_location("hp_frontend_asset_manifest", _PATH)
assert _SPEC and _SPEC.loader
_MODULE = importlib.util.module_from_spec(_SPEC)
_SPEC.loader.exec_module(_MODULE)
resolve_frontend_asset = _MODULE.resolve_frontend_asset
def _root(tmp_path: Path) -> Path:
root = tmp_path / "frontend"
assets = root / "houseplan-assets"
assets.mkdir(parents=True)
(assets / "editor-abc.js").write_text("export {};", encoding="utf-8")
(assets / "stale.js").write_text("export {};", encoding="utf-8")
(root / "houseplan-assets.json").write_text(json.dumps({
"schema": 1,
"files": [{"path": "houseplan-assets/editor-abc.js"}],
}), encoding="utf-8")
return root
def test_frontend_asset_requires_current_manifest_entry(tmp_path: Path) -> None:
root = _root(tmp_path)
assert resolve_frontend_asset(root, "editor-abc.js") == (
root / "houseplan-assets" / "editor-abc.js"
).resolve()
assert resolve_frontend_asset(root, "stale.js") is None
assert resolve_frontend_asset(root, "missing.js") is None
assert resolve_frontend_asset(root, "houseplan-assets.json") is None
def test_frontend_asset_refuses_traversal_and_malformed_manifest(tmp_path: Path) -> None:
root = _root(tmp_path)
for value in (
"../editor-abc.js", "..\\editor-abc.js", "nested/editor-abc.js",
"..%2feditor-abc.js", "editor-abc.css", "..", "",
):
assert resolve_frontend_asset(root, value) is None
(root / "houseplan-assets.json").write_text("not json", encoding="utf-8")
assert resolve_frontend_asset(root, "editor-abc.js") is None
def test_frontend_asset_refuses_wrong_schema_and_symlink_escape(tmp_path: Path) -> None:
root = _root(tmp_path)
manifest = root / "houseplan-assets.json"
manifest.write_text(json.dumps({
"schema": 2, "files": [{"path": "houseplan-assets/editor-abc.js"}],
}), encoding="utf-8")
assert resolve_frontend_asset(root, "editor-abc.js") is None
outside = tmp_path / "outside.js"
outside.write_text("outside", encoding="utf-8")
link = root / "houseplan-assets" / "linked.js"
try:
link.symlink_to(outside)
except OSError:
return # Windows without Developer Mode; containment is covered elsewhere.
manifest.write_text(json.dumps({
"schema": 1, "files": [{"path": "houseplan-assets/linked.js"}],
}), encoding="utf-8")
assert resolve_frontend_asset(root, "linked.js") is None