from __future__ import annotations import time from typing import Any import pytest import requests from apps.providers.adapters.balldontlie_provider import BalldontlieProviderAdapter from apps.providers.adapters.mvp_provider import MvpDemoProviderAdapter from apps.providers.clients.balldontlie import BalldontlieClient from apps.providers.exceptions import ProviderRateLimitError, ProviderTransientError from apps.providers.registry import get_default_provider_namespace, get_provider class _FakeResponse: def __init__(self, *, status_code: int, payload: dict[str, Any] | None = None, headers: dict[str, str] | None = None, text: str = ""): self.status_code = status_code self._payload = payload or {} self.headers = headers or {} self.text = text def json(self): return self._payload class _FakeSession: def __init__(self, responses: list[Any]): self._responses = responses def get(self, *args, **kwargs): item = self._responses.pop(0) if isinstance(item, Exception): raise item return item class _FakeBalldontlieClient: def get_json(self, path: str, *, params: dict[str, Any] | None = None) -> dict[str, Any]: if path == "teams": return { "data": [ { "id": 14, "full_name": "Los Angeles Lakers", "abbreviation": "LAL", } ] } return {"data": []} def list_paginated( self, path: str, *, params: dict[str, Any] | None = None, per_page: int = 100, page_limit: int = 1, ) -> list[dict[str, Any]]: if path == "players": return [ { "id": 237, "first_name": "LeBron", "last_name": "James", "position": "F", "team": {"id": 14}, } ] if path == "stats": return [ { "pts": 20, "reb": 8, "ast": 7, "stl": 1, "blk": 1, "turnover": 3, "fg_pct": 0.5, "fg3_pct": 0.4, "ft_pct": 0.9, "min": "35:12", "player": {"id": 237}, "team": {"id": 14}, "game": {"season": 2024}, }, { "pts": 30, "reb": 10, "ast": 9, "stl": 2, "blk": 0, "turnover": 4, "fg_pct": 0.6, "fg3_pct": 0.5, "ft_pct": 1.0, "min": "33:00", "player": {"id": 237}, "team": {"id": 14}, "game": {"season": 2024}, }, ] return [] @pytest.mark.django_db def test_provider_registry_backend_selection(settings): settings.PROVIDER_DEFAULT_NAMESPACE = "" settings.PROVIDER_BACKEND = "demo" assert get_default_provider_namespace() == "mvp_demo" assert isinstance(get_provider(), MvpDemoProviderAdapter) settings.PROVIDER_BACKEND = "balldontlie" assert get_default_provider_namespace() == "balldontlie" assert isinstance(get_provider(), BalldontlieProviderAdapter) settings.PROVIDER_DEFAULT_NAMESPACE = "mvp_demo" assert get_default_provider_namespace() == "mvp_demo" @pytest.mark.django_db def test_balldontlie_adapter_maps_payloads(settings): settings.PROVIDER_BALLDONTLIE_SEASONS = [2024] adapter = BalldontlieProviderAdapter(client=_FakeBalldontlieClient()) payload = adapter.sync_all() assert payload["competitions"][0]["external_id"] == "competition-nba" assert payload["teams"][0]["external_id"] == "team-14" assert payload["players"][0]["external_id"] == "player-237" assert payload["seasons"][0]["external_id"] == "season-2024" assert payload["player_stats"][0]["games_played"] == 2 assert payload["player_stats"][0]["points"] == 25.0 assert payload["player_stats"][0]["fg_pct"] == 55.0 @pytest.mark.django_db def test_balldontlie_client_retries_after_rate_limit(monkeypatch, settings): monkeypatch.setattr(time, "sleep", lambda _: None) settings.PROVIDER_REQUEST_RETRIES = 2 settings.PROVIDER_REQUEST_RETRY_SLEEP = 0 session = _FakeSession( responses=[ _FakeResponse(status_code=429, headers={"Retry-After": "0"}), _FakeResponse(status_code=200, payload={"data": []}), ] ) client = BalldontlieClient(session=session) payload = client.get_json("players") assert payload == {"data": []} @pytest.mark.django_db def test_balldontlie_client_timeout_retries_then_fails(monkeypatch, settings): monkeypatch.setattr(time, "sleep", lambda _: None) settings.PROVIDER_REQUEST_RETRIES = 2 settings.PROVIDER_REQUEST_RETRY_SLEEP = 0 session = _FakeSession(responses=[requests.Timeout("slow"), requests.Timeout("slow")]) client = BalldontlieClient(session=session) with pytest.raises(ProviderTransientError): client.get_json("players") @pytest.mark.django_db def test_balldontlie_client_raises_rate_limit_after_max_retries(monkeypatch, settings): monkeypatch.setattr(time, "sleep", lambda _: None) settings.PROVIDER_REQUEST_RETRIES = 2 settings.PROVIDER_REQUEST_RETRY_SLEEP = 0 session = _FakeSession( responses=[ _FakeResponse(status_code=429, headers={"Retry-After": "1"}), _FakeResponse(status_code=429, headers={"Retry-After": "1"}), ] ) client = BalldontlieClient(session=session) with pytest.raises(ProviderRateLimitError): client.get_json("players")