From 3a6ceeea546815c5575f8050fe05cad570fe6919 Mon Sep 17 00:00:00 2001 From: Sergey Matyunin Date: Thu, 3 Sep 2026 00:35:27 +0300 Subject: [PATCH] test: cover custom image raster paths Issue: #51 User-Visible: no --- tests_backend/test_decor_assets.py | 72 +++++++++++++++++++++++++++++- 1 file changed, 71 insertions(+), 1 deletion(-) 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', + "text.svg", + ) + + def test_svg_preserves_safe_local_gradient_clip_mask_and_transparency() -> None: asset = validate_asset(b''' @@ -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", + }]}]}) == {}