music-assistant-server

4.7 KBPY
test_recommendations.py
4.7 KB139 lines • python
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