/
/
/
1"""
2Repro test for support#5787.
3
4Some DLNA devices (e.g. Bose SoundTouch) return malformed DIDL-Lite XML in
5their track metadata. Accessing the metadata properties on the underlying
6DmrDevice then raises an XML ParseError, which must not propagate out of
7set_dynamic_attributes (it would otherwise kill the player update task).
8"""
9
10from __future__ import annotations
11
12from datetime import UTC, datetime
13from unittest.mock import AsyncMock, MagicMock, PropertyMock
14from xml.etree.ElementTree import ParseError
15
16import pytest
17from async_upnp_client.profiles.dlna import TransportState
18
19from music_assistant.providers.dlna.player import DLNAPlayer
20from tests.common import MockProvider
21
22
23@pytest.mark.asyncio
24async def test_set_dynamic_attributes_survives_malformed_device_metadata() -> None:
25 """Malformed DIDL metadata from the device must not crash the player update."""
26 provider = MockProvider("dlna", instance_id="dlna_test")
27 provider.mass.streams.base_url = "http://192.168.1.2:8097"
28
29 # subclass so the PropertyMocks stay local and don't leak into MagicMock itself
30 class MockDevice(MagicMock):
31 pass
32
33 device = MockDevice()
34 device.profile_device.available = True
35 device.name = "Bose SoundTouch"
36 device.volume_level = 0.5
37 device.is_volume_muted = False
38 device.transport_state = TransportState.PLAYING
39 device.current_track_uri = "http://192.168.1.10/stream.mp3"
40 device.media_position = None
41 # malformed XML in CurrentTrackMetaData raises on property access
42 for prop in ("media_title", "media_artist", "media_album_name", "media_image_url"):
43 setattr(
44 MockDevice,
45 prop,
46 PropertyMock(side_effect=ParseError("not well-formed (invalid token)")),
47 )
48
49 player = DLNAPlayer(
50 provider, # type: ignore[arg-type]
51 "uuid:bose-player",
52 "http://192.168.1.10/description.xml",
53 device=device,
54 )
55
56 await player.set_dynamic_attributes()
57
58 # metadata is dropped, but the update must complete and keep core attributes
59 assert player.available is True
60 assert player.current_media is not None
61 assert player.current_media.uri == "http://192.168.1.10/stream.mp3"
62 assert player.current_media.title is None
63
64
65@pytest.mark.asyncio
66async def test_poll_survives_malformed_device_metadata() -> None:
67 """A ParseError raised while polling the device must not crash the poll."""
68 provider = MockProvider("dlna", instance_id="dlna_test")
69 provider.mass.streams.base_url = "http://192.168.1.2:8097"
70
71 device = MagicMock()
72 device.profile_device.available = True
73 device.name = "Bose SoundTouch"
74 # async_update parses CurrentTrackMetaData eagerly and raises on bad XML
75 device.async_update = AsyncMock(side_effect=ParseError("not well-formed (invalid token)"))
76
77 player = DLNAPlayer(
78 provider, # type: ignore[arg-type]
79 "uuid:bose-player",
80 "http://192.168.1.10/description.xml",
81 device=device,
82 )
83 player.force_poll = True
84
85 await player.poll()
86
87 device.async_update.assert_awaited_once()
88
89
90@pytest.mark.asyncio
91async def test_set_dynamic_attributes_propagates_valid_device_metadata() -> None:
92 """Valid device metadata must be propagated into the player's current media."""
93 provider = MockProvider("dlna", instance_id="dlna_test")
94 provider.mass.streams.base_url = "http://192.168.1.2:8097"
95
96 device = MagicMock()
97 device.profile_device.available = True
98 device.name = "Bose SoundTouch"
99 device.volume_level = 0.5
100 device.is_volume_muted = False
101 device.transport_state = TransportState.PLAYING
102 device.current_track_uri = "http://192.168.1.10/stream.mp3"
103 device.media_position = 42
104 device.media_position_updated_at = datetime(2026, 7, 9, 12, 0, 0, tzinfo=UTC)
105 device.media_title = "Test Title"
106 device.media_artist = "Test Artist"
107 device.media_album_name = "Test Album"
108 device.media_image_url = "http://192.168.1.10/cover.jpg"
109 device.media_duration = 180
110
111 player = DLNAPlayer(
112 provider, # type: ignore[arg-type]
113 "uuid:bose-player",
114 "http://192.168.1.10/description.xml",
115 device=device,
116 )
117
118 await player.set_dynamic_attributes()
119
120 assert player.current_media is not None
121 assert player.current_media.uri == "http://192.168.1.10/stream.mp3"
122 assert player.current_media.title == "Test Title"
123 assert player.current_media.artist == "Test Artist"
124 assert player.current_media.album == "Test Album"
125 assert player.current_media.image_url == "http://192.168.1.10/cover.jpg"
126 assert player.current_media.duration == 180
127 assert player.elapsed_time == 42.0
128