/
/
/
1"""Test the ytmusicapi call wrapper's handling of a signed-out session."""
2
3from typing import Any
4from unittest.mock import MagicMock, patch
5
6import pytest
7import ytmusicapi
8from music_assistant_models.errors import LoginFailed
9
10from music_assistant.providers.ytmusic import helpers
11
12# A trimmed but realistic nav() KeyError: YouTube answered with its signed-out
13# page instead of an auth error, so the renderer lookup fails deep in that payload.
14SIGNED_OUT_PAYLOAD_ERROR = KeyError(
15 "Unable to find 'twoColumnBrowseResultsRenderer' using path "
16 "['contents', 'twoColumnBrowseResultsRenderer', 'tabs', 0] on "
17 "{'singleColumnBrowseResultsRenderer': {'tabs': [{'tabRenderer': {'content': "
18 "{'sectionListRenderer': {'contents': [{'itemSectionRenderer': {'contents': "
19 "[{'buttonRenderer': {'text': {'runs': [{'text': 'Sign in'}]}, "
20 "'navigationEndpoint': {'clickTrackingParams': '...', "
21 "'signInEndpoint': {'hack': True}}}}]}}]}}}}]}}, "
22 "exception: 'twoColumnBrowseResultsRenderer'"
23)
24
25
26def _patch_get_home(error: Exception) -> Any:
27 """Patch ytmusicapi.YTMusic so get_home() raises the given error."""
28 mock_ytm = MagicMock()
29 mock_ytm.get_home.side_effect = error
30 return patch.object(ytmusicapi, "YTMusic", return_value=mock_ytm)
31
32
33async def test_signed_out_payload_is_translated_to_login_failed() -> None:
34 """A KeyError carrying the signed-out page must surface as LoginFailed."""
35 with _patch_get_home(SIGNED_OUT_PAYLOAD_ERROR), pytest.raises(LoginFailed):
36 await helpers.get_home(headers={})
37
38
39async def test_unrelated_key_error_still_propagates() -> None:
40 """A KeyError unrelated to a signed-out session must propagate as-is."""
41 with _patch_get_home(KeyError("some_other_key")), pytest.raises(KeyError):
42 await helpers.get_home(headers={})
43
44
45async def test_get_artist_does_not_hide_signed_out_behind_its_fallback() -> None:
46 """get_artist's channel fallback must not turn a signed-out session into an artist."""
47 mock_ytm = MagicMock()
48 mock_ytm.get_artist.side_effect = SIGNED_OUT_PAYLOAD_ERROR
49 mock_ytm.get_user.side_effect = SIGNED_OUT_PAYLOAD_ERROR
50 with (
51 patch.object(ytmusicapi, "YTMusic", return_value=mock_ytm),
52 pytest.raises(LoginFailed),
53 ):
54 await helpers.get_artist(prov_artist_id="UC123", headers={})
55
56
57async def test_get_artist_fallback_still_returns_unknown() -> None:
58 """An artist that is neither a channel nor a user still falls back to Unknown."""
59 mock_ytm = MagicMock()
60 mock_ytm.get_artist.side_effect = KeyError("header")
61 mock_ytm.get_user.side_effect = KeyError("name")
62 with patch.object(ytmusicapi, "YTMusic", return_value=mock_ytm):
63 artist = await helpers.get_artist(prov_artist_id="UC123", headers={})
64 assert artist == {"channelId": "UC123", "name": "Unknown"}
65
66
67async def test_search_passes_auth_headers_and_user() -> None:
68 """search() must authenticate its YTMusic client so results respect account context."""
69 mock_ytm = MagicMock()
70 mock_ytm.search.return_value = []
71 headers = {"cookie": "abc"}
72 with patch.object(ytmusicapi, "YTMusic", return_value=mock_ytm) as mock_ytmusic:
73 await helpers.search(query="test", headers=headers, user="123")
74 mock_ytmusic.assert_called_once_with(auth=headers, language="en", user="123")
75