/
/
/
1"""Tests for library_get_album_tracks and library_get_artist_albums."""
2# mypy: disable-error-code="arg-type, no-untyped-def, type-arg, assignment, operator, misc, union-attr"
3
4from __future__ import annotations
5
6from typing import Any
7from unittest.mock import AsyncMock
8
9import pytest
10from fastmcp import Client, FastMCP
11from fastmcp.exceptions import ToolError
12from music_assistant_models.enums import MediaType
13
14from music_assistant.providers.fastmcp_server.tools._common import (
15 album_tracks_from_uri,
16 artist_albums_from_uri,
17)
18
19from .media_fakes import (
20 fake_album,
21 fake_artist,
22 fake_media_item,
23 fake_track,
24)
25
26
27class TestAlbumTracksFromUri:
28 """Unit tests for the shared album-tracks helper."""
29
30 async def test_returns_sorted_available_tracks(self, mock_mass: Any) -> None:
31 """Tracks are sorted by disc/track and unavailable entries are dropped."""
32 album = fake_album()
33 mock_mass.music.get_item_by_uri = AsyncMock(return_value=album)
34 mock_mass.music.albums.tracks = AsyncMock(
35 return_value=[
36 fake_track(uri="library://track/2", name="B", disc_number=1, track_number=2),
37 fake_track(uri="library://track/3", name="C", available=False),
38 fake_track(uri="library://track/1", name="A", disc_number=1, track_number=1),
39 ]
40 )
41 result = await album_tracks_from_uri(mock_mass, album.uri)
42 assert result.album.name == "Black Sands"
43 assert [t.uri for t in result.tracks] == [
44 "library://track/1",
45 "library://track/2",
46 ]
47 mock_mass.music.albums.tracks.assert_awaited_once_with("7", "library")
48
49 async def test_rejects_non_album_uri(self, mock_mass: Any) -> None:
50 """A track URI raises rather than returning an empty listing."""
51 mock_mass.music.get_item_by_uri = AsyncMock(
52 return_value=fake_media_item(MediaType.TRACK, uri="library://track/1")
53 )
54 with pytest.raises(ToolError, match="is not an album"):
55 await album_tracks_from_uri(mock_mass, "library://track/1")
56
57
58class TestGetAlbumTracksTool:
59 """Integration tests via the mounted library server."""
60
61 async def test_happy_path(self, library_server: FastMCP, mock_mass: Any) -> None:
62 """Tool returns album header and track briefs."""
63 album = fake_album(uri="spotify://album/abc")
64 mock_mass.music.get_item_by_uri = AsyncMock(return_value=album)
65 mock_mass.music.albums.tracks = AsyncMock(
66 return_value=[fake_track(uri="spotify://track/1", track_number=1)]
67 )
68 async with Client(library_server) as client:
69 result = await client.call_tool(
70 "library_get_album_tracks",
71 {"uri": "spotify://album/abc"},
72 )
73 text_blocks = [c.text for c in result.content if hasattr(c, "text")]
74 assert any("Black Sands" in t for t in text_blocks)
75 assert any("spotify://track/1" in t for t in text_blocks)
76
77
78class TestArtistAlbumsFromUri:
79 """Unit tests for the shared artist-albums helper."""
80
81 async def test_returns_albums_newest_first(self, mock_mass: Any) -> None:
82 """Albums are sorted by year descending and unavailable entries are dropped."""
83 artist = fake_artist()
84 mock_mass.music.get_item_by_uri = AsyncMock(return_value=artist)
85 mock_mass.music.artists.albums = AsyncMock(
86 return_value=[
87 fake_album(uri="library://album/1", name="Old", year=2010),
88 fake_album(uri="library://album/2", name="New", year=2020),
89 fake_album(uri="library://album/3", name="Hidden", year=2015, available=False),
90 ]
91 )
92
93 result = await artist_albums_from_uri(mock_mass, artist.uri)
94 assert result.artist.name == "deadmau5"
95 assert [a.uri for a in result.albums] == [
96 "library://album/2",
97 "library://album/1",
98 ]
99 mock_mass.music.artists.albums.assert_awaited_once_with("27", "library")
100
101 async def test_rejects_non_artist_uri(self, mock_mass: Any) -> None:
102 """An album URI raises rather than returning an empty listing."""
103 mock_mass.music.get_item_by_uri = AsyncMock(return_value=fake_album())
104 with pytest.raises(ToolError, match="is not an artist"):
105 await artist_albums_from_uri(mock_mass, "library://album/1")
106
107
108class TestGetArtistAlbumsTool:
109 """Integration tests via the mounted library server."""
110
111 async def test_happy_path(self, library_server: FastMCP, mock_mass: Any) -> None:
112 """Tool returns artist header and album briefs."""
113 artist = fake_artist(uri="spotify://artist/abc")
114 mock_mass.music.get_item_by_uri = AsyncMock(return_value=artist)
115 mock_mass.music.artists.albums = AsyncMock(
116 return_value=[fake_album(uri="spotify://album/1", name="Album One", year=2012)]
117 )
118 async with Client(library_server) as client:
119 result = await client.call_tool(
120 "library_get_artist_albums",
121 {"uri": "spotify://artist/abc"},
122 )
123 text_blocks = [c.text for c in result.content if hasattr(c, "text")]
124 assert any("deadmau5" in t for t in text_blocks)
125 assert any("spotify://album/1" in t for t in text_blocks)
126