/
/
/
1"""Tests for Sendspin artwork decoding resilience."""
2
3from __future__ import annotations
4
5import logging
6from io import BytesIO
7from types import SimpleNamespace
8from typing import cast
9
10from PIL import Image
11
12from music_assistant.providers.sendspin.player import SendspinPlayer
13
14
15def _fake_player() -> SendspinPlayer:
16 return cast("SendspinPlayer", SimpleNamespace(logger=logging.getLogger("test")))
17
18
19def _png_bytes(size: tuple[int, int] = (4, 4)) -> bytes:
20 buf = BytesIO()
21 Image.new("RGB", size, (1, 2, 3)).save(buf, "PNG")
22 return buf.getvalue()
23
24
25async def test_decode_artwork_valid_image() -> None:
26 """A valid raster image decodes to a Pillow image."""
27 img = await SendspinPlayer._decode_artwork(_fake_player(), _png_bytes())
28 assert img is not None
29 assert img.size == (4, 4)
30
31
32async def test_decode_artwork_returns_none_for_unidentifiable() -> None:
33 """SVG / arbitrary / empty bytes return None instead of raising."""
34 assert await SendspinPlayer._decode_artwork(_fake_player(), b"<svg></svg>") is None
35 assert await SendspinPlayer._decode_artwork(_fake_player(), b"") is None
36
37
38async def test_decode_artwork_returns_none_for_truncated_image() -> None:
39 """Truncated data opens lazily but fails on decode; it must be caught, not raised."""
40 full = _png_bytes((64, 64))
41 truncated = full[: len(full) // 2]
42 assert await SendspinPlayer._decode_artwork(_fake_player(), truncated) is None
43