184 lines
5.9 KiB
Python
184 lines
5.9 KiB
Python
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")
|