/
/
/
1"""Tests for the Spotify library albums listing."""
2
3from typing import Any
4from unittest.mock import AsyncMock, MagicMock
5
6from music_assistant.providers.spotify.provider import SpotifyProvider
7
8
9def _make_provider(instance_id: str = "spotify--test") -> SpotifyProvider:
10 """Return a Spotify provider with only what the library listing needs."""
11 provider = object.__new__(SpotifyProvider)
12 provider.config = MagicMock(instance_id=instance_id)
13 provider.manifest = MagicMock(domain="spotify")
14 provider.logger = MagicMock()
15 provider.mass = MagicMock()
16 return provider
17
18
19def _saved_album(album_id: str) -> dict[str, Any]:
20 """Return a saved album entry as returned by the me/albums endpoint."""
21 return {
22 "added_at": "2026-01-01T00:00:00Z",
23 "album": {
24 "id": album_id,
25 "name": album_id,
26 "album_type": "album",
27 "external_urls": {"spotify": f"https://open.spotify.com/album/{album_id}"},
28 "artists": [],
29 "images": [],
30 },
31 }
32
33
34async def test_library_albums_skips_null_entries() -> None:
35 """
36 A null entry in the me/albums response is skipped instead of aborting the sync.
37
38 Spotify returns such an entry for an album the account can no longer resolve, which
39 would otherwise crash the whole album sync.
40 """
41 provider = _make_provider()
42 # a valid album after the empty ones, so stopping at an empty entry fails this test
43 pages: list[dict[str, Any]] = [
44 {
45 "items": [
46 _saved_album("album1"),
47 None,
48 {"added_at": "2026-01-01T00:00:00Z", "album": None},
49 _saved_album("album2"),
50 ],
51 "total": 4,
52 }
53 ]
54 provider._get_cached_paginated_meta = AsyncMock(return_value={"etag": "etag", "total": 4}) # type: ignore[method-assign]
55 provider._get_data_with_caching = AsyncMock(side_effect=pages) # type: ignore[method-assign]
56
57 albums = [album async for album in provider.get_library_albums()]
58
59 assert [album.item_id for album in albums] == ["album1", "album2"]
60