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