"""#51 custom decor image security and lifecycle contracts."""
from __future__ import annotations
import base64
import json
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:
asset = validate_asset(PNG_1X1, "pixel.PNG")
assert (asset.mime, asset.ext, asset.width, asset.height) == ("image/png", ".png", 1, 1)
assert len(asset.asset_id) == 64
assert asset.asset_id == validate_asset(PNG_1X1, "other.png").asset_id
def test_declared_mime_must_agree_with_extension_and_bytes() -> None:
with pytest.raises(DecorAssetError, match="disagree"):
validate_asset(PNG_1X1, "pixel.png", "image/jpeg")
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'',
b'',
b']>',
b'',
b'',
b'',
b'',
b'',
b'',
b'',
b'',
b'',
])
def test_svg_rejects_the_whole_unsafe_document(payload: bytes) -> None:
with pytest.raises(DecorAssetError):
validate_asset(payload, "unsafe.svg")
def test_svg_utf16_doctype_is_rejected_before_entity_expansion() -> None:
payload = '''
]>
'''.encode("utf-16")
with pytest.raises(DecorAssetError, match="DTD, entities"):
validate_asset(payload, "encoded.svg")
def test_svg_canonicalizes_safe_geometry_and_requires_aspect_ratio() -> None:
asset = validate_asset(
b'',
"safe.svg",
)
assert (asset.width, asset.height, asset.mime) == (30, 20, "image/svg+xml")
with pytest.raises(DecorAssetError, match="dimensions"):
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'''''', "safe.svg", "image/svg+xml")
assert asset.data.startswith(b'