"""Tests for Spotify PKCE and token handling.""" import asyncio import base64 import hashlib import time import httpx2 import pytest from app.adapters.spotify.auth import ( TokenSet, derive_code_challenge, refresh_access_token, ) from app.adapters.spotify.session import PendingLogins def test_code_challenge_is_unpadded_base64url_sha256() -> None: verifier = "a-fixed-code-verifier" expected = base64.urlsafe_b64encode(hashlib.sha256(verifier.encode()).digest()) challenge = derive_code_challenge(verifier) assert challenge == expected.rstrip(b"=").decode() assert "=" not in challenge def test_refresh_keeps_existing_refresh_token_when_omitted() -> None: async def run() -> None: async def handler(request: httpx2.Request) -> httpx2.Response: assert request.url == "https://accounts.spotify.com/api/token" return httpx2.Response(200, json={"access_token": "new", "expires_in": 3600}) tokens = TokenSet("old", "keep-me", time.monotonic() + 3600) async with httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) as http: refreshed = await refresh_access_token(http, client_id="client", tokens=tokens) assert refreshed.access_token == "new" assert refreshed.refresh_token == "keep-me" asyncio.run(run()) def test_token_expiry_uses_sixty_second_skew() -> None: assert TokenSet("access", "refresh", time.monotonic() + 59).is_expired assert not TokenSet("access", "refresh", time.monotonic() + 61).is_expired def test_pending_login_add_sweeps_expired_entries(monkeypatch: pytest.MonkeyPatch) -> None: current_time = 0.0 monkeypatch.setattr( "app.adapters.spotify.session.time.monotonic", lambda: current_time, ) pending_logins = PendingLogins() pending_logins.add("expired", "old-verifier") current_time = 601.0 pending_logins.add("current", "new-verifier") assert set(pending_logins._entries) == {"current"}