last.fm settings issue fix (#47)
* last.fm settings issue fix * format + add make format
This commit is contained in:
@@ -71,8 +71,9 @@ LASTFM_ERROR_MAP: dict[int, tuple[type[Exception], str]] = {
|
||||
9: (ConfigurationError, "Session key expired - please re-authorize with Last.fm"),
|
||||
10: (ConfigurationError, "Invalid API key - check your Last.fm API key"),
|
||||
11: (ExternalServiceError, "Last.fm service is temporarily offline"),
|
||||
26: (ConfigurationError, "API key has been suspended - contact Last.fm support"),
|
||||
14: (TokenNotAuthorizedError, "Token not yet authorized"),
|
||||
17: (ConfigurationError, "Authentication required - re-authorize Last.fm or make your listening history public"),
|
||||
26: (ConfigurationError, "API key has been suspended - contact Last.fm support"),
|
||||
29: (ExternalServiceError, "Rate limit exceeded"),
|
||||
}
|
||||
|
||||
@@ -107,6 +108,10 @@ class LastFmRepository:
|
||||
self._shared_secret = shared_secret
|
||||
self._session_key = session_key
|
||||
|
||||
@property
|
||||
def _can_sign(self) -> bool:
|
||||
return bool(self._shared_secret) and bool(self._session_key)
|
||||
|
||||
def configure(self, api_key: str, shared_secret: str, session_key: str = "") -> None:
|
||||
self._api_key = api_key
|
||||
self._shared_secret = shared_secret
|
||||
@@ -320,6 +325,7 @@ class LastFmRepository:
|
||||
data = await self._request(
|
||||
"user.getTopArtists",
|
||||
params={"user": username, "period": period, "limit": str(limit)},
|
||||
signed=self._can_sign,
|
||||
)
|
||||
artists = [
|
||||
parse_top_artist(item)
|
||||
@@ -340,6 +346,7 @@ class LastFmRepository:
|
||||
data = await self._request(
|
||||
"user.getTopAlbums",
|
||||
params={"user": username, "period": period, "limit": str(limit)},
|
||||
signed=self._can_sign,
|
||||
)
|
||||
albums = [
|
||||
parse_top_album(item)
|
||||
@@ -360,6 +367,7 @@ class LastFmRepository:
|
||||
data = await self._request(
|
||||
"user.getTopTracks",
|
||||
params={"user": username, "period": period, "limit": str(limit)},
|
||||
signed=self._can_sign,
|
||||
)
|
||||
tracks = [
|
||||
parse_top_track(item)
|
||||
@@ -378,6 +386,7 @@ class LastFmRepository:
|
||||
data = await self._request(
|
||||
"user.getRecentTracks",
|
||||
params={"user": username, "limit": str(limit), "extended": "0"},
|
||||
signed=self._can_sign,
|
||||
)
|
||||
tracks = [
|
||||
parse_recent_track(item)
|
||||
@@ -396,6 +405,7 @@ class LastFmRepository:
|
||||
data = await self._request(
|
||||
"user.getLovedTracks",
|
||||
params={"user": username, "limit": str(limit)},
|
||||
signed=self._can_sign,
|
||||
)
|
||||
tracks = [
|
||||
parse_loved_track(item)
|
||||
@@ -414,6 +424,7 @@ class LastFmRepository:
|
||||
data = await self._request(
|
||||
"user.getWeeklyArtistChart",
|
||||
params={"user": username},
|
||||
signed=self._can_sign,
|
||||
)
|
||||
artists = [
|
||||
parse_top_artist(item)
|
||||
@@ -432,6 +443,7 @@ class LastFmRepository:
|
||||
data = await self._request(
|
||||
"user.getWeeklyAlbumChart",
|
||||
params={"user": username},
|
||||
signed=self._can_sign,
|
||||
)
|
||||
albums = [
|
||||
parse_weekly_album_chart_item(item)
|
||||
|
||||
@@ -88,6 +88,14 @@ class TestHandleErrorResponse:
|
||||
with pytest.raises(ExternalServiceError, match="Last.fm error \\(999\\)"):
|
||||
repo._handle_error_response({"error": 999, "message": "weird"})
|
||||
|
||||
def test_error_17_raises_configuration_error(self):
|
||||
repo = _make_repo()
|
||||
with pytest.raises(ConfigurationError, match="re-authorize Last.fm"):
|
||||
repo._handle_error_response({
|
||||
"error": 17,
|
||||
"message": "Login: User required to be logged in",
|
||||
})
|
||||
|
||||
|
||||
class TestConfigureMethod:
|
||||
def test_configure_updates_credentials(self):
|
||||
@@ -107,6 +115,123 @@ class TestConstructorDefaults:
|
||||
assert repo._session_key == ""
|
||||
|
||||
|
||||
class TestCanSignProperty:
|
||||
def test_true_when_both_credentials_present(self):
|
||||
repo = _make_repo(shared_secret="sec", session_key="sk")
|
||||
assert repo._can_sign is True
|
||||
|
||||
def test_false_without_shared_secret(self):
|
||||
repo = _make_repo(shared_secret="", session_key="sk")
|
||||
assert repo._can_sign is False
|
||||
|
||||
def test_false_without_session_key(self):
|
||||
repo = _make_repo(shared_secret="sec", session_key="")
|
||||
assert repo._can_sign is False
|
||||
|
||||
def test_false_with_neither(self):
|
||||
repo = _make_repo(shared_secret="", session_key="")
|
||||
assert repo._can_sign is False
|
||||
|
||||
def test_configure_enables_can_sign(self):
|
||||
repo = _make_repo(shared_secret="", session_key="")
|
||||
assert repo._can_sign is False
|
||||
repo.configure(api_key="k", shared_secret="s", session_key="sk")
|
||||
assert repo._can_sign is True
|
||||
|
||||
|
||||
class TestSignedUserRequests:
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_recent_tracks_sends_signed_request(self):
|
||||
cache = _make_cache()
|
||||
http_client = AsyncMock(spec=httpx.AsyncClient)
|
||||
http_client.get = AsyncMock(
|
||||
return_value=MagicMock(
|
||||
status_code=200,
|
||||
json=lambda: {"recenttracks": {"track": []}},
|
||||
text="",
|
||||
)
|
||||
)
|
||||
repo = LastFmRepository(
|
||||
http_client=http_client,
|
||||
cache=cache,
|
||||
api_key="key",
|
||||
shared_secret="sec",
|
||||
session_key="sk-1",
|
||||
)
|
||||
await repo.get_user_recent_tracks("user1")
|
||||
call_params = http_client.get.call_args.kwargs.get("params", {})
|
||||
assert "api_sig" in call_params
|
||||
assert call_params["sk"] == "sk-1"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_recent_tracks_unsigned_without_session_key(self):
|
||||
cache = _make_cache()
|
||||
http_client = AsyncMock(spec=httpx.AsyncClient)
|
||||
http_client.get = AsyncMock(
|
||||
return_value=MagicMock(
|
||||
status_code=200,
|
||||
json=lambda: {"recenttracks": {"track": []}},
|
||||
text="",
|
||||
)
|
||||
)
|
||||
repo = LastFmRepository(
|
||||
http_client=http_client,
|
||||
cache=cache,
|
||||
api_key="key",
|
||||
)
|
||||
await repo.get_user_recent_tracks("user1")
|
||||
call_params = http_client.get.call_args.kwargs.get("params", {})
|
||||
assert "api_sig" not in call_params
|
||||
assert "sk" not in call_params
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_top_artists_sends_signed_request(self):
|
||||
cache = _make_cache()
|
||||
http_client = AsyncMock(spec=httpx.AsyncClient)
|
||||
http_client.get = AsyncMock(
|
||||
return_value=MagicMock(
|
||||
status_code=200,
|
||||
json=lambda: {"topartists": {"artist": []}},
|
||||
text="",
|
||||
)
|
||||
)
|
||||
repo = LastFmRepository(
|
||||
http_client=http_client,
|
||||
cache=cache,
|
||||
api_key="key",
|
||||
shared_secret="sec",
|
||||
session_key="sk-1",
|
||||
)
|
||||
await repo.get_user_top_artists("user1")
|
||||
call_params = http_client.get.call_args.kwargs.get("params", {})
|
||||
assert "api_sig" in call_params
|
||||
assert call_params["sk"] == "sk-1"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_error_17_through_signed_request_raises_configuration_error(self):
|
||||
cache = _make_cache()
|
||||
http_client = AsyncMock(spec=httpx.AsyncClient)
|
||||
http_client.get = AsyncMock(
|
||||
return_value=MagicMock(
|
||||
status_code=200,
|
||||
json=lambda: {
|
||||
"error": 17,
|
||||
"message": "Login: User required to be logged in",
|
||||
},
|
||||
text="",
|
||||
)
|
||||
)
|
||||
repo = LastFmRepository(
|
||||
http_client=http_client,
|
||||
cache=cache,
|
||||
api_key="key",
|
||||
shared_secret="sec",
|
||||
session_key="sk-1",
|
||||
)
|
||||
with pytest.raises(ConfigurationError, match="re-authorize Last.fm"):
|
||||
await repo.get_user_recent_tracks("user1")
|
||||
|
||||
|
||||
class TestUpdateNowPlaying:
|
||||
@pytest.mark.asyncio
|
||||
async def test_posts_with_required_params(self):
|
||||
|
||||
Reference in New Issue
Block a user