/
/
/
1"""Test Tidal Recommendation Manager."""
2
3from unittest.mock import AsyncMock, Mock, patch
4
5import pytest
6from music_assistant_models.enums import MediaType
7
8from music_assistant.providers.tidal.recommendations import TidalRecommendationManager
9
10
11@pytest.fixture
12def provider_mock() -> Mock:
13 """Return a mock provider."""
14 provider = Mock()
15 provider.domain = "tidal"
16 provider.instance_id = "tidal_instance"
17 provider.auth.user_id = "12345"
18 provider.auth.country_code = "US"
19 provider.api = AsyncMock()
20 provider.logger = Mock()
21
22 # Mock mass
23 provider.mass = Mock()
24 provider.mass.config.get_provider_configs = AsyncMock(return_value=[])
25 provider.mass.metadata.locale = "en_US"
26 provider.mass.cache.set = AsyncMock()
27
28 return provider
29
30
31@pytest.fixture
32def recommendation_manager(provider_mock: Mock) -> TidalRecommendationManager:
33 """Return a TidalRecommendationManager instance."""
34 return TidalRecommendationManager(provider_mock)
35
36
37@pytest.mark.usefixtures("provider_mock")
38async def test_get_recommendations(
39 recommendation_manager: TidalRecommendationManager,
40) -> None:
41 """Test get_recommendations."""
42 # Mock get_page_content to return a mock parser
43 mock_parser = Mock()
44 mock_parser._module_map = [{"title": "Test Module"}]
45 mock_parser.get_module_items.return_value = (
46 [Mock(item_id="rec_1", name="Recommendation 1")],
47 MediaType.PLAYLIST,
48 )
49
50 with patch.object(
51 recommendation_manager, "get_page_content", new_callable=AsyncMock
52 ) as mock_get_page:
53 mock_get_page.return_value = mock_parser
54
55 recommendations = await recommendation_manager.get_recommendations()
56
57 assert len(recommendations) == 1
58 assert recommendations[0].name == "Test Module"
59 assert len(recommendations[0].items) == 1
60
61 # Should fetch pages
62 assert mock_get_page.call_count >= 1
63
64
65async def test_get_recommendations_strips_at_symbol_when_multiple_instances(
66 recommendation_manager: TidalRecommendationManager, provider_mock: Mock
67) -> None:
68 """Test that username is included and '@' is stripped when multiple instances exist."""
69 provider_mock.auth.user = Mock(profile_name="[email protected]", user_name=None)
70
71 provider_mock.mass.config.get_provider_configs = AsyncMock(
72 return_value=[
73 Mock(domain="tidal", instance_id="tidal_instance_1"),
74 Mock(domain="tidal", instance_id="tidal_instance_2"),
75 Mock(domain="other", instance_id="other_instance"),
76 ]
77 )
78
79 parser_with_module = Mock()
80 parser_with_module._module_map = [{"title": "Test Module"}]
81 parser_with_module.get_module_items.return_value = (
82 [Mock(item_id="rec_1", name="Recommendation 1")],
83 MediaType.PLAYLIST,
84 )
85
86 parser_empty = Mock()
87 parser_empty._module_map = []
88 parser_empty.get_module_items = Mock()
89
90 with patch.object(
91 recommendation_manager, "get_page_content", new_callable=AsyncMock
92 ) as mock_get_page:
93 # Only first page returns the module, remaining pages return no modules
94 mock_get_page.side_effect = [parser_with_module] + [parser_empty] * 4
95
96 recommendations = await recommendation_manager.get_recommendations()
97
98 assert len(recommendations) == 1
99 assert recommendations[0].name == "Test Module (john)"
100 assert "@" not in recommendations[0].name
101 assert len(recommendations[0].items) == 1
102
103
104async def test_get_page_content(
105 recommendation_manager: TidalRecommendationManager, provider_mock: Mock
106) -> None:
107 """Test get_page_content."""
108 with patch(
109 "music_assistant.providers.tidal.recommendations.TidalPageParser"
110 ) as mock_parser_cls:
111 # Configure from_cache to be async and return None
112 mock_parser_cls.from_cache = AsyncMock(return_value=None)
113
114 # Configure parser instance
115 mock_parser_instance = mock_parser_cls.return_value
116 mock_parser_instance._module_map = []
117 mock_parser_instance._content_map = {}
118 mock_parser_instance._parsed_at = 1234567890
119 mock_parser_instance.parse_page_structure = Mock() # Ensure it's a synchronous mock
120
121 # Mock API response
122 provider_mock.api.get.return_value = ({"rows": []}, "etag")
123
124 parser = await recommendation_manager.get_page_content("pages/home")
125
126 assert parser == mock_parser_instance
127
128 # Should check cache
129 mock_parser_cls.from_cache.assert_called_with(provider_mock, "pages/home")
130
131 # Should fetch from API
132 provider_mock.api.get.assert_called()
133
134 # Should parse structure
135 mock_parser_instance.parse_page_structure.assert_called()
136
137 # Should cache result
138 provider_mock.mass.cache.set.assert_called()
139