/
/
/
1"""Integration tests for the Bandcamp provider."""
2
3from collections.abc import AsyncGenerator
4from typing import TYPE_CHECKING
5from unittest import mock
6
7import pytest
8from music_assistant_models.enums import MediaType, StreamType
9
10from music_assistant.mass import MusicAssistant
11from tests.common import wait_for_sync_completion
12
13if TYPE_CHECKING:
14 from music_assistant_models.config_entries import ProviderConfig
15
16
17@pytest.fixture
18async def bandcamp_provider( # noqa: PLR0915
19 mass: MusicAssistant,
20) -> AsyncGenerator[ProviderConfig]:
21 """Configure a Bandcamp test fixture, and add a provider to mass that uses it."""
22 # Mock the BandcampAPIClient to avoid real API calls
23 with mock.patch("music_assistant.providers.bandcamp.BandcampAPIClient") as mock_client_class:
24 mock_client = mock.AsyncMock()
25 mock_client_class.return_value = mock_client
26
27 # Configure mock client for collection access
28 mock_collection = mock.AsyncMock(has_more=False, last_token=None)
29 mock_collection.items = []
30
31 # Mock collection items for library tests
32 mock_item_artist = mock.AsyncMock()
33 mock_item_artist.item_type = "band"
34 mock_item_artist.item_id = 123
35 mock_item_artist.band_name = "Test Artist"
36 mock_item_artist.item_url = "https://test.bandcamp.com"
37
38 mock_item_album = mock.AsyncMock()
39 mock_item_album.item_type = "album"
40 mock_item_album.band_id = 123
41 mock_item_album.item_id = 456
42 mock_item_album.item_title = "Test Album"
43 mock_item_album.item_url = "https://test.bandcamp.com/album/test-album"
44
45 mock_collection.items = [mock_item_artist, mock_item_album]
46 mock_client.get_collection_items.return_value = mock_collection
47
48 # Mock artist and album data
49 mock_artist = mock.AsyncMock()
50 mock_artist.id = 123
51 mock_artist.name = "Test Artist"
52 mock_artist.url = "https://test.bandcamp.com"
53 mock_client.get_artist.return_value = mock_artist
54
55 mock_album = mock.AsyncMock()
56 mock_album.id = 456
57 mock_album.title = "Test Album"
58 mock_album.artist = mock_artist
59 mock_album.url = "https://test.bandcamp.com/album/test-album"
60 mock_album.art_url = "https://f4.bcbits.com/img/a1234567890_16.jpg"
61 mock_album.release_date = 1609459200
62 mock_album.about = "Test album description"
63 # Concrete performer credit string â the converter feeds it to
64 # slugify_performer which only accepts strings. None means
65 # "no separate performer; the album is by the band itself".
66 mock_album.tralbum_artist = None
67
68 mock_track = mock.AsyncMock()
69 mock_track.id = 789
70 mock_track.title = "Test Track"
71 mock_track.artist = mock_artist
72 mock_track.url = "https://test.bandcamp.com/track/test-track"
73 mock_track.duration = 300
74 mock_track.streaming_url = {"mp3-320": "https://example.com/track.mp3"}
75 mock_track.track_number = 1
76 mock_track.lyrics = "Test lyrics"
77 mock_track.tralbum_artist = None
78
79 # Configure the streaming_url to behave like a dictionary
80 mock_track.configure_mock(streaming_url={"mp3-320": "https://example.com/track.mp3"})
81
82 mock_album.tracks = [mock_track]
83 mock_client.get_album.return_value = mock_album
84 mock_client.get_track.return_value = mock_track
85
86 async with wait_for_sync_completion(mass):
87 config = await mass.config._create_provider_instance(
88 "bandcamp",
89 {"search_limit": 10, "top_tracks_limit": 50},
90 # the identity cookie is collected by the setup flow and lives in setup_data
91 setup_data=mass.config._encrypt_values({"identity": "mock_identity_token"}),
92 )
93 await mass.music.start_sync()
94
95 yield config
96
97
98@pytest.mark.usefixtures("bandcamp_provider")
99async def test_initial_sync(mass: MusicAssistant) -> None:
100 """Test that initial sync worked."""
101 # Test library access (requires identity token)
102 all_artists = await mass.music.artists.library_items()
103 artists = [artist for artist in all_artists if artist.provider == "bandcamp"]
104
105 assert len(artists) >= 0 # May be empty if no collection items
106
107 all_albums = await mass.music.albums.library_items()
108 albums = [album for album in all_albums if album.provider == "bandcamp"]
109
110 assert len(albums) >= 0 # May be empty if no collection items
111
112
113@pytest.mark.usefixtures("bandcamp_provider")
114async def test_search_functionality(mass: MusicAssistant) -> None:
115 """Test search functionality."""
116 # Mock search results
117 with mock.patch("music_assistant.providers.bandcamp.BandcampAPIClient") as mock_client_class:
118 mock_client = mock.AsyncMock()
119 mock_client_class.return_value = mock_client
120
121 # Mock search results
122 mock_search_result_track = mock.AsyncMock()
123 mock_search_result_track.__class__.__name__ = "SearchResultTrack"
124 mock_search_result_track.artist_id = 123
125 mock_search_result_track.album_id = 456
126 mock_search_result_track.id = 789
127 mock_search_result_track.name = "Search Test Track"
128 mock_search_result_track.artist_name = "Search Test Artist"
129 mock_search_result_track.album_name = "Search Test Album"
130 mock_search_result_track.url = "https://test.bandcamp.com/track/search-test"
131
132 mock_client.search.return_value = [mock_search_result_track]
133
134 # Perform search
135 results = await mass.music.search("test query", [MediaType.TRACK], limit=5)
136
137 # Filter for bandcamp results
138 bandcamp_tracks = [track for track in results.tracks if track.provider == "bandcamp"]
139 assert len(bandcamp_tracks) >= 0 # May be empty if search is mocked differently
140
141
142@pytest.mark.usefixtures("bandcamp_provider")
143async def test_get_artist_details(mass: MusicAssistant) -> None:
144 """Test getting artist details."""
145 # Get the bandcamp provider instance
146 bandcamp_provider = None
147 for provider in mass.music.providers:
148 if provider.domain == "bandcamp":
149 bandcamp_provider = provider
150 break
151
152 assert bandcamp_provider is not None
153
154 # Test artist retrieval
155 artist = await bandcamp_provider.get_artist("123")
156 assert artist is not None
157 assert artist.name == "Test Artist"
158 assert artist.provider == bandcamp_provider.instance_id
159
160
161@pytest.mark.usefixtures("bandcamp_provider")
162async def test_get_album_details(mass: MusicAssistant) -> None:
163 """Test getting album details."""
164 # Get the bandcamp provider instance
165 bandcamp_provider = None
166 for provider in mass.music.providers:
167 if provider.domain == "bandcamp":
168 bandcamp_provider = provider
169 break
170
171 assert bandcamp_provider is not None
172
173 # Test album retrieval
174 album = await bandcamp_provider.get_album("123-456")
175 assert album is not None
176 assert album.name == "Test Album"
177 assert album.provider == bandcamp_provider.instance_id
178
179
180@pytest.mark.usefixtures("bandcamp_provider")
181async def test_get_track_details(mass: MusicAssistant) -> None:
182 """Test getting track details."""
183 # Get the bandcamp provider instance
184 bandcamp_provider = None
185 for provider in mass.music.providers:
186 if provider.domain == "bandcamp":
187 bandcamp_provider = provider
188 break
189
190 assert bandcamp_provider is not None
191
192 # Test track retrieval
193 track = await bandcamp_provider.get_track("123-456-789")
194 assert track is not None
195 assert track.name == "Test Track"
196 assert track.provider == bandcamp_provider.instance_id
197
198
199@pytest.mark.usefixtures("bandcamp_provider")
200async def test_get_album_tracks(mass: MusicAssistant) -> None:
201 """Test getting album tracks."""
202 # Get the bandcamp provider instance
203 bandcamp_provider = None
204 for provider in mass.music.providers:
205 if provider.domain == "bandcamp":
206 bandcamp_provider = provider
207 break
208
209 assert bandcamp_provider is not None
210
211 # Test album tracks retrieval
212 tracks = await bandcamp_provider.get_album_tracks("123-456")
213 assert len(tracks) == 1
214 assert tracks[0].name == "Test Track"
215
216
217@pytest.mark.usefixtures("bandcamp_provider")
218async def test_stream_details(mass: MusicAssistant) -> None:
219 """Test stream details retrieval."""
220 # Get the bandcamp provider instance
221 bandcamp_provider = None
222 for provider in mass.music.providers:
223 if provider.domain == "bandcamp":
224 bandcamp_provider = provider
225 break
226
227 assert bandcamp_provider is not None
228
229 # Test stream details retrieval
230 stream_details = await bandcamp_provider.get_stream_details("123-456-789", MediaType.TRACK)
231 assert stream_details is not None
232 assert stream_details.stream_type == StreamType.HTTP
233 assert stream_details.path == "https://example.com/track.mp3"
234