mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
committed by
claude[bot]
parent
a265830e3b
commit
3a6ceeea54
@@ -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",
|
||||
}]}]}) == {}
|
||||
|
||||
Reference in New Issue
Block a user