diff --git a/tests_backend/test_decor_assets.py b/tests_backend/test_decor_assets.py
index d259daf4..c8067b55 100644
--- a/tests_backend/test_decor_assets.py
+++ b/tests_backend/test_decor_assets.py
@@ -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'',
b'',
@@ -77,6 +111,29 @@ def test_svg_canonicalizes_safe_geometry_and_requires_aspect_ratio() -> None:
validate_asset(b'', "bad.svg")
+def test_svg_dimension_and_text_boundaries_fail_closed() -> None:
+ with pytest.raises(DecorAssetError, match="viewBox"):
+ validate_asset(
+ b'',
+ "bad.svg",
+ )
+ with pytest.raises(DecorAssetError, match="dimensions"):
+ validate_asset(
+ b'',
+ "bad.svg",
+ )
+ with pytest.raises(DecorAssetError, match="safety limit"):
+ validate_asset(
+ b'',
+ "large.svg",
+ )
+ with pytest.raises(DecorAssetError, match="SVG text"):
+ validate_asset(
+ b'',
+ "text.svg",
+ )
+
+
def test_svg_preserves_safe_local_gradient_clip_mask_and_transparency() -> None:
asset = validate_asset(b'''