test: cover custom image raster paths

Issue: #51
User-Visible: no
This commit is contained in:
Sergey Matyunin
2026-09-02 21:56:04 +00:00
committed by claude[bot]
parent a265830e3b
commit 3a6ceeea54
+71 -1
View File
@@ -8,16 +8,28 @@ import pytest
from custom_components.houseplan.decor_assets import (
DecorAssetError,
asset_meta_path,
asset_refs,
public_asset,
read_catalog,
validate_asset,
)
PNG_1X1 = base64.b64decode(
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII="
)
JPEG_3X2 = base64.b64decode(
"/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAACAAMDASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwDx2iiiu04z/9k="
)
WEBP_VP8_3X2 = base64.b64decode(
"UklGRjAAAABXRUJQVlA4ICQAAABQAQCdASoDAAIAAUAmJQBOgC6gAP77LkvF3YjjJ4dVU9ffoAA="
)
WEBP_VP8L_3X2 = base64.b64decode(
"UklGRh4AAABXRUJQVlA4TBEAAAAvAkAAAAdQkTIUp/+BiOh/AAA="
)
WEBP_VP8X_3X2 = base64.b64decode(
"UklGRlIAAABXRUJQVlA4WAoAAAAQAAAAAgAAAQAAQUxQSAcAAAAAgICAgICAAFZQOCAkAAAAUAEAnQEqAwACAAFAJiUAToAuoAD++y5Lxd2I4yeHVVPX36AA"
)
def test_png_identity_and_dimensions_are_content_addressed() -> None:
@@ -33,6 +45,28 @@ def test_declared_mime_must_agree_with_extension_and_bytes() -> None:
assert validate_asset(PNG_1X1, "pixel.png", "image/png; charset=binary").width == 1
@pytest.mark.parametrize(("payload", "filename", "mime"), [
(JPEG_3X2, "photo.jpeg", "image/jpeg"),
(WEBP_VP8_3X2, "lossy.webp", "image/webp"),
(WEBP_VP8L_3X2, "lossless.webp", "image/webp"),
(WEBP_VP8X_3X2, "alpha.webp", "image/webp"),
])
def test_supported_raster_headers_and_full_decode(
payload: bytes, filename: str, mime: str,
) -> None:
asset = validate_asset(payload, filename, "application/octet-stream")
assert (asset.width, asset.height, asset.mime) == (3, 2, mime)
def test_upload_size_and_extension_guards() -> None:
with pytest.raises(DecorAssetError, match="empty"):
validate_asset(b"", "empty.png")
with pytest.raises(DecorAssetError, match="2 MiB"):
validate_asset(b"x" * (2 * 1024 * 1024 + 1), "large.png")
with pytest.raises(DecorAssetError, match="PNG, JPEG, WebP or SVG"):
validate_asset(PNG_1X1, "pixel.gif")
@pytest.mark.parametrize("payload", [
b'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 20"><script/></svg>',
b'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 20" onload="alert(1)"/>',
@@ -77,6 +111,29 @@ def test_svg_canonicalizes_safe_geometry_and_requires_aspect_ratio() -> None:
validate_asset(b'<svg xmlns="http://www.w3.org/2000/svg"><path d="M0 0"/></svg>', "bad.svg")
def test_svg_dimension_and_text_boundaries_fail_closed() -> None:
with pytest.raises(DecorAssetError, match="viewBox"):
validate_asset(
b'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10"><path/></svg>',
"bad.svg",
)
with pytest.raises(DecorAssetError, match="dimensions"):
validate_asset(
b'<svg xmlns="http://www.w3.org/2000/svg" width="bad" height="20"><path/></svg>',
"bad.svg",
)
with pytest.raises(DecorAssetError, match="safety limit"):
validate_asset(
b'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20000 1"><path/></svg>',
"large.svg",
)
with pytest.raises(DecorAssetError, match="SVG text"):
validate_asset(
b'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1 1"><path>text</path></svg>',
"text.svg",
)
def test_svg_preserves_safe_local_gradient_clip_mask_and_transparency() -> None:
asset = validate_asset(b'''<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 20">
<defs>
@@ -143,6 +200,13 @@ def test_catalog_ignores_missing_or_malformed_sidecars(tmp_path) -> None:
assert public_asset(row)["url"].endswith(f"/assets/_/{aid}.png")
def test_catalog_empty_directory_and_metadata_path(tmp_path) -> None:
missing = tmp_path / "missing"
aid = "c" * 64
assert read_catalog(missing) == []
assert asset_meta_path(tmp_path, aid) == tmp_path / f"{aid}.json"
def test_reference_scan_is_cross_space_and_image_only() -> None:
aid = "b" * 64
refs = asset_refs({"spaces": [
@@ -156,3 +220,9 @@ def test_reference_scan_is_cross_space_and_image_only() -> None:
{"space_id": "one", "decor_id": "a"},
{"space_id": "two", "decor_id": "b"},
]
def test_reference_scan_skips_malformed_spaces_and_asset_ids() -> None:
assert asset_refs({"spaces": [None, {"decor": [None, {
"kind": "image", "asset_id": "not-a-content-hash",
}]}]}) == {}