218 lines
8.5 KiB
Python
218 lines
8.5 KiB
Python
import os
|
|
|
|
import pytest
|
|
|
|
from apps.competitions.models import Competition, Season
|
|
from apps.ingestion.models import IngestionError, IngestionRun
|
|
from apps.ingestion.services.sync import run_sync_job
|
|
from apps.players.models import Player
|
|
from apps.providers.exceptions import ProviderRateLimitError
|
|
from apps.providers.models import ExternalMapping
|
|
from apps.stats.models import PlayerSeason, PlayerSeasonStats
|
|
from apps.teams.models import Team
|
|
|
|
|
|
@pytest.mark.django_db
|
|
def test_run_full_sync_creates_domain_objects(settings):
|
|
settings.PROVIDER_DEFAULT_NAMESPACE = "mvp_demo"
|
|
|
|
run = run_sync_job(provider_namespace="mvp_demo", job_type=IngestionRun.JobType.FULL_SYNC)
|
|
|
|
assert run.status == IngestionRun.RunStatus.SUCCESS
|
|
assert Competition.objects.count() >= 1
|
|
assert Team.objects.count() >= 1
|
|
assert Season.objects.count() >= 1
|
|
assert Player.objects.count() >= 1
|
|
assert PlayerSeason.objects.count() >= 1
|
|
assert PlayerSeasonStats.objects.count() >= 1
|
|
assert Player.objects.filter(origin_competition__isnull=False).exists()
|
|
|
|
|
|
@pytest.mark.django_db
|
|
def test_full_sync_is_idempotent(settings):
|
|
settings.PROVIDER_DEFAULT_NAMESPACE = "mvp_demo"
|
|
|
|
run_sync_job(provider_namespace="mvp_demo", job_type=IngestionRun.JobType.FULL_SYNC)
|
|
counts_after_first = {
|
|
"competition": Competition.objects.count(),
|
|
"team": Team.objects.count(),
|
|
"season": Season.objects.count(),
|
|
"player": Player.objects.count(),
|
|
"player_season": PlayerSeason.objects.count(),
|
|
"player_stats": PlayerSeasonStats.objects.count(),
|
|
}
|
|
|
|
run_sync_job(provider_namespace="mvp_demo", job_type=IngestionRun.JobType.FULL_SYNC)
|
|
counts_after_second = {
|
|
"competition": Competition.objects.count(),
|
|
"team": Team.objects.count(),
|
|
"season": Season.objects.count(),
|
|
"player": Player.objects.count(),
|
|
"player_season": PlayerSeason.objects.count(),
|
|
"player_stats": PlayerSeasonStats.objects.count(),
|
|
}
|
|
|
|
assert counts_after_first == counts_after_second
|
|
|
|
|
|
@pytest.mark.django_db
|
|
def test_incremental_sync_runs_successfully(settings):
|
|
settings.PROVIDER_DEFAULT_NAMESPACE = "mvp_demo"
|
|
|
|
run = run_sync_job(
|
|
provider_namespace="mvp_demo",
|
|
job_type=IngestionRun.JobType.INCREMENTAL,
|
|
cursor="demo-cursor",
|
|
)
|
|
|
|
assert run.status == IngestionRun.RunStatus.SUCCESS
|
|
assert run.records_processed > 0
|
|
assert run.started_at is not None
|
|
assert run.finished_at is not None
|
|
assert run.finished_at >= run.started_at
|
|
assert run.error_summary == ""
|
|
|
|
|
|
@pytest.mark.django_db
|
|
def test_run_sync_handles_rate_limit(settings):
|
|
settings.PROVIDER_DEFAULT_NAMESPACE = "mvp_demo"
|
|
os.environ["PROVIDER_MVP_FORCE_RATE_LIMIT"] = "1"
|
|
|
|
with pytest.raises(ProviderRateLimitError):
|
|
run_sync_job(provider_namespace="mvp_demo", job_type=IngestionRun.JobType.FULL_SYNC)
|
|
|
|
run = IngestionRun.objects.order_by("-id").first()
|
|
assert run is not None
|
|
assert run.status == IngestionRun.RunStatus.FAILED
|
|
assert run.started_at is not None
|
|
assert run.finished_at is not None
|
|
assert "Rate limit" in run.error_summary
|
|
assert IngestionError.objects.filter(ingestion_run=run).exists()
|
|
|
|
os.environ.pop("PROVIDER_MVP_FORCE_RATE_LIMIT", None)
|
|
|
|
|
|
@pytest.mark.django_db
|
|
def test_balldontlie_sync_idempotency_with_stable_payload(monkeypatch):
|
|
class StableProvider:
|
|
def sync_all(self):
|
|
return {
|
|
"competitions": [
|
|
{
|
|
"external_id": "competition-nba",
|
|
"name": "NBA",
|
|
"slug": "nba",
|
|
"competition_type": "league",
|
|
"gender": "men",
|
|
"level": 1,
|
|
"country": {"name": "United States", "iso2_code": "US", "iso3_code": "USA"},
|
|
"is_active": True,
|
|
}
|
|
],
|
|
"teams": [
|
|
{
|
|
"external_id": "team-14",
|
|
"name": "Los Angeles Lakers",
|
|
"short_name": "LAL",
|
|
"slug": "los-angeles-lakers",
|
|
"country": {"name": "United States", "iso2_code": "US", "iso3_code": "USA"},
|
|
"is_national_team": False,
|
|
}
|
|
],
|
|
"seasons": [
|
|
{
|
|
"external_id": "season-2024",
|
|
"label": "2024-2025",
|
|
"start_date": "2024-10-01",
|
|
"end_date": "2025-06-30",
|
|
"is_current": False,
|
|
}
|
|
],
|
|
"players": [
|
|
{
|
|
"external_id": "player-237",
|
|
"first_name": "LeBron",
|
|
"last_name": "James",
|
|
"full_name": "LeBron James",
|
|
"birth_date": None,
|
|
"nationality": {"name": "United States", "iso2_code": "US", "iso3_code": "USA"},
|
|
"nominal_position": {"code": "SF", "name": "Small Forward"},
|
|
"inferred_role": {"code": "wing", "name": "Wing"},
|
|
"height_cm": None,
|
|
"weight_kg": None,
|
|
"dominant_hand": "unknown",
|
|
"is_active": True,
|
|
"aliases": [],
|
|
}
|
|
],
|
|
"player_stats": [
|
|
{
|
|
"external_id": "ps-2024-237-14",
|
|
"player_external_id": "player-237",
|
|
"team_external_id": "team-14",
|
|
"competition_external_id": "competition-nba",
|
|
"season_external_id": "season-2024",
|
|
"games_played": 2,
|
|
"games_started": 0,
|
|
"minutes_played": 68,
|
|
"points": 25,
|
|
"rebounds": 9,
|
|
"assists": 8,
|
|
"steals": 1.5,
|
|
"blocks": 0.5,
|
|
"turnovers": 3.5,
|
|
"fg_pct": 55.0,
|
|
"three_pct": 45.0,
|
|
"ft_pct": 95.0,
|
|
"usage_rate": None,
|
|
"true_shooting_pct": None,
|
|
"player_efficiency_rating": None,
|
|
}
|
|
],
|
|
"player_careers": [
|
|
{
|
|
"external_id": "career-2024-237-14",
|
|
"player_external_id": "player-237",
|
|
"team_external_id": "team-14",
|
|
"competition_external_id": "competition-nba",
|
|
"season_external_id": "season-2024",
|
|
"role_code": "",
|
|
"shirt_number": None,
|
|
"start_date": "2024-10-01",
|
|
"end_date": "2025-06-30",
|
|
"notes": "Imported from balldontlie aggregated box scores",
|
|
}
|
|
],
|
|
}
|
|
|
|
def sync_incremental(self, *, cursor: str | None = None):
|
|
payload = self.sync_all()
|
|
payload["cursor"] = cursor
|
|
return payload
|
|
|
|
monkeypatch.setattr("apps.ingestion.services.sync.get_provider", lambda namespace: StableProvider())
|
|
|
|
run_sync_job(provider_namespace="balldontlie", job_type=IngestionRun.JobType.FULL_SYNC)
|
|
counts_first = {
|
|
"competition": Competition.objects.count(),
|
|
"team": Team.objects.count(),
|
|
"season": Season.objects.count(),
|
|
"player": Player.objects.count(),
|
|
"player_season": PlayerSeason.objects.count(),
|
|
"player_stats": PlayerSeasonStats.objects.count(),
|
|
"mapping": ExternalMapping.objects.filter(provider_namespace="balldontlie").count(),
|
|
}
|
|
|
|
run_sync_job(provider_namespace="balldontlie", job_type=IngestionRun.JobType.FULL_SYNC)
|
|
counts_second = {
|
|
"competition": Competition.objects.count(),
|
|
"team": Team.objects.count(),
|
|
"season": Season.objects.count(),
|
|
"player": Player.objects.count(),
|
|
"player_season": PlayerSeason.objects.count(),
|
|
"player_stats": PlayerSeasonStats.objects.count(),
|
|
"mapping": ExternalMapping.objects.filter(provider_namespace="balldontlie").count(),
|
|
}
|
|
|
|
assert counts_first == counts_second
|