/
/
/
1"""Tests for TuneIn library listing validation."""
2
3from __future__ import annotations
4
5from typing import Any
6from unittest.mock import AsyncMock, Mock
7
8import pytest
9from music_assistant_models.enums import MediaType
10from music_assistant_models.errors import InvalidDataError
11
12from music_assistant.providers.tunein import TuneInProvider
13
14
15@pytest.mark.parametrize("response", [None, {}, {"body": None}, {"body": {}}])
16async def test_malformed_preset_list_raises(
17 provider: TuneInProvider, response: dict[str, Any] | None
18) -> None:
19 """A malformed top-level preset response must abort the radio sync."""
20 provider._TuneInProvider__get_data = AsyncMock( # type: ignore[attr-defined]
21 return_value=response
22 )
23
24 with pytest.raises(InvalidDataError):
25 _ = [radio async for radio in provider.get_library_radios()]
26
27
28async def test_empty_preset_list_is_valid(provider: TuneInProvider) -> None:
29 """An explicit empty preset list remains a valid empty library."""
30 provider._TuneInProvider__get_data = AsyncMock( # type: ignore[attr-defined]
31 return_value={"body": []}
32 )
33
34 assert [radio async for radio in provider.get_library_radios()] == []
35
36
37async def test_malformed_preset_entry_is_reported(provider: TuneInProvider) -> None:
38 """An unidentifiable malformed preset must hold back radio deletions."""
39 provider._TuneInProvider__get_data = AsyncMock( # type: ignore[attr-defined]
40 return_value={"body": [None]}
41 )
42 provider.report_skipped_sync_item = Mock() # type: ignore[method-assign]
43
44 assert [radio async for radio in provider.get_library_radios()] == []
45
46 provider.report_skipped_sync_item.assert_called_once()
47 args = provider.report_skipped_sync_item.call_args.args
48 assert args[:2] == (MediaType.RADIO, None)
49 assert isinstance(args[2], InvalidDataError)
50
51
52@pytest.mark.parametrize("preset_id", [None, "", {}])
53async def test_audio_preset_without_id_is_reported(
54 provider: TuneInProvider, preset_id: Any
55) -> None:
56 """An audio preset without an id must hold back radio deletions."""
57 provider._TuneInProvider__get_data = AsyncMock( # type: ignore[attr-defined]
58 return_value={"body": [{"type": "audio", "preset_id": preset_id}]}
59 )
60 provider.report_skipped_sync_item = Mock() # type: ignore[method-assign]
61
62 assert [radio async for radio in provider.get_library_radios()] == []
63
64 provider.report_skipped_sync_item.assert_called_once()
65 args = provider.report_skipped_sync_item.call_args.args
66 assert args[:2] == (MediaType.RADIO, None)
67 assert isinstance(args[2], InvalidDataError)
68
69
70async def test_audio_preset_without_name_is_reported(provider: TuneInProvider) -> None:
71 """An audio preset without a name must be reported with its known id."""
72 provider._TuneInProvider__get_data = AsyncMock( # type: ignore[attr-defined]
73 return_value={"body": [{"type": "audio", "preset_id": "station-1"}]}
74 )
75 provider.report_skipped_sync_item = Mock() # type: ignore[method-assign]
76
77 assert [radio async for radio in provider.get_library_radios()] == []
78
79 provider.report_skipped_sync_item.assert_called_once()
80 args = provider.report_skipped_sync_item.call_args.args
81 assert args[:2] == (MediaType.RADIO, "station-1")
82 assert isinstance(args[2], InvalidDataError)
83
84
85async def test_audio_preset_without_streams_is_reported(provider: TuneInProvider) -> None:
86 """An audio preset without streams must be reported with its known id."""
87 provider._TuneInProvider__get_data = AsyncMock( # type: ignore[attr-defined]
88 return_value={"body": [{"type": "audio", "preset_id": "station-1", "text": "Station"}]}
89 )
90 provider._get_stream_info = AsyncMock(return_value=[]) # type: ignore[method-assign]
91 provider.report_skipped_sync_item = Mock() # type: ignore[method-assign]
92
93 assert [radio async for radio in provider.get_library_radios()] == []
94
95 provider.report_skipped_sync_item.assert_called_once()
96 args = provider.report_skipped_sync_item.call_args.args
97 assert args[:2] == (MediaType.RADIO, "station-1")
98 assert isinstance(args[2], InvalidDataError)
99
100
101@pytest.mark.parametrize(
102 "folder",
103 [
104 {"children": None, "text": "Folder"},
105 {"children": [], "text": None},
106 {"children": []},
107 ],
108)
109async def test_malformed_old_style_folder_is_reported(
110 provider: TuneInProvider, folder: dict[str, Any]
111) -> None:
112 """A malformed old-style folder must hold back radio deletions."""
113 provider._TuneInProvider__get_data = AsyncMock( # type: ignore[attr-defined]
114 return_value={"body": [folder]}
115 )
116 provider.report_skipped_sync_item = Mock() # type: ignore[method-assign]
117
118 assert [radio async for radio in provider.get_library_radios()] == []
119
120 provider.report_skipped_sync_item.assert_called_once()
121 args = provider.report_skipped_sync_item.call_args.args
122 assert args[:2] == (MediaType.RADIO, None)
123 assert isinstance(args[2], InvalidDataError)
124
125
126@pytest.mark.parametrize(
127 "item",
128 [
129 {"type": "link"},
130 {"type": "link", "URL": "https://example.com"},
131 {"type": "link", "item": "url", "text": "Custom station"},
132 ],
133)
134async def test_link_without_url_or_name_is_reported(
135 provider: TuneInProvider, item: dict[str, Any]
136) -> None:
137 """A link without its required fields must hold back radio deletions."""
138 provider._TuneInProvider__get_data = AsyncMock( # type: ignore[attr-defined]
139 return_value={"body": [item]}
140 )
141 provider.report_skipped_sync_item = Mock() # type: ignore[method-assign]
142
143 assert [radio async for radio in provider.get_library_radios()] == []
144
145 provider.report_skipped_sync_item.assert_called_once()
146 args = provider.report_skipped_sync_item.call_args.args
147 assert args[:2] == (MediaType.RADIO, None)
148 assert isinstance(args[2], InvalidDataError)
149