import sys import os import pytest from unittest.mock import patch, MagicMock from httpx import AsyncClient, ASGITransport import pandas as pd sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) from app.main import app MOCK_CAREER_DF = pd.DataFrame([{ "SEASON_ID": "2025-26", "TEAM_ABBREVIATION": "DEN", "PTS": 26.3, "REB": 12.4, "AST": 9.1, "FG3M": 1.1, "BLK": 0.7, "STL": 1.4, "TOV": 3.2, "MIN": 34.2, "GP": 65, }]) MOCK_GAMELOG_DF = pd.DataFrame([ { "GAME_DATE": "MAR 21, 2026", "MATCHUP": "DEN vs. LAL", "TEAM_ABBREVIATION": "DEN", "PTS": 30, "REB": 15, "AST": 10, "FG3M": 2, "BLK": 1, "STL": 2, "TOV": 3, "MIN": 36, }, { "GAME_DATE": "MAR 20, 2026", "MATCHUP": "DEN @ PHX", "TEAM_ABBREVIATION": "DEN", "PTS": 22, "REB": 10, "AST": 8, "FG3M": 0, "BLK": 0, "STL": 1, "TOV": 4, "MIN": 32, }, ]) MOCK_PLAYERS = [{"id": 203999, "full_name": "Nikola Jokic", "is_active": True}] def mock_career(*args, **kwargs): m = MagicMock() m.get_data_frames.return_value = [MOCK_CAREER_DF] return m def mock_gamelog(*args, **kwargs): m = MagicMock() m.get_data_frames.return_value = [MOCK_GAMELOG_DF] return m @pytest.fixture def mock_nba_api(): with patch("app.services.stats.playercareerstats.PlayerCareerStats", side_effect=mock_career), \ patch("app.services.stats.playergamelog.PlayerGameLog", side_effect=mock_gamelog), \ patch("app.utils.player_map.players.get_players", return_value=MOCK_PLAYERS), \ patch("app.services.stats.cache_get", return_value=None), \ patch("app.services.stats.cache_set"), \ patch("app.utils.player_map.cache_get", return_value=None), \ patch("app.utils.player_map.cache_set"), \ patch("app.services.stats.time.sleep"): yield @pytest.mark.asyncio async def test_health(): with patch("app.main.cache_health", return_value=True): transport = ASGITransport(app=app) async with AsyncClient(transport=transport, base_url="http://test") as client: resp = await client.get("/health") assert resp.status_code == 200 assert resp.json()["status"] == "ok" @pytest.mark.asyncio async def test_player_search(mock_nba_api): transport = ASGITransport(app=app) async with AsyncClient(transport=transport, base_url="http://test") as client: resp = await client.get("/players/search?name=Jokic") assert resp.status_code == 200 assert len(resp.json()["results"]) > 0 assert resp.json()["results"][0]["full_name"] == "Nikola Jokic" @pytest.mark.asyncio async def test_player_search_not_found(): with patch("app.utils.player_map.players.get_players", return_value=[]), \ patch("app.utils.player_map.cache_get", return_value=None), \ patch("app.utils.player_map.cache_set"): transport = ASGITransport(app=app) async with AsyncClient(transport=transport, base_url="http://test") as client: resp = await client.get("/players/search?name=Zzzzzzz") assert resp.status_code == 404 @pytest.mark.asyncio async def test_season_avg(mock_nba_api): transport = ASGITransport(app=app) async with AsyncClient(transport=transport, base_url="http://test") as client: resp = await client.get("/stats/season-avg?player=Nikola Jokic&season=2025-26") assert resp.status_code == 200 data = resp.json() assert data["player"] == "Nikola Jokic" assert data["team"] == "DEN" assert "points" in data["stats"] assert "pra" in data["stats"] @pytest.mark.asyncio async def test_season_avg_invalid_stat_type(mock_nba_api): transport = ASGITransport(app=app) async with AsyncClient(transport=transport, base_url="http://test") as client: resp = await client.get("/stats/season-avg?player=Jokic&stat_type=invalid") assert resp.status_code == 400 @pytest.mark.asyncio async def test_last_n(mock_nba_api): transport = ASGITransport(app=app) async with AsyncClient(transport=transport, base_url="http://test") as client: resp = await client.get("/stats/last-n?player=Nikola Jokic&n=2") assert resp.status_code == 200 data = resp.json() assert data["last_n"] == 2 assert "points" in data["stats"] @pytest.mark.asyncio async def test_last_n_clamps_max(mock_nba_api): transport = ASGITransport(app=app) async with AsyncClient(transport=transport, base_url="http://test") as client: resp = await client.get("/stats/last-n?player=Nikola Jokic&n=50") # FastAPI validates n <= 30, returns 422 assert resp.status_code == 422 @pytest.mark.asyncio async def test_splits_home_away(mock_nba_api): transport = ASGITransport(app=app) async with AsyncClient(transport=transport, base_url="http://test") as client: resp = await client.get("/stats/splits?player=Nikola Jokic&stat_type=points&split_type=home_away") assert resp.status_code == 200 data = resp.json() assert "home" in data["splits"] assert "away" in data["splits"] @pytest.mark.asyncio async def test_splits_vs_team_requires_opponent(mock_nba_api): transport = ASGITransport(app=app) async with AsyncClient(transport=transport, base_url="http://test") as client: resp = await client.get("/stats/splits?player=Jokic&stat_type=points&split_type=vs_team") assert resp.status_code == 400 assert "opponent" in resp.json()["detail"] @pytest.mark.asyncio async def test_splits_invalid_split_type(mock_nba_api): transport = ASGITransport(app=app) async with AsyncClient(transport=transport, base_url="http://test") as client: resp = await client.get("/stats/splits?player=Jokic&stat_type=points&split_type=invalid") assert resp.status_code == 400 @pytest.mark.asyncio async def test_season_avg_player_not_found(): with patch("app.utils.player_map.players.get_players", return_value=[]), \ patch("app.utils.player_map.cache_get", return_value=None), \ patch("app.utils.player_map.cache_set"), \ patch("app.services.stats.cache_get", return_value=None), \ patch("app.services.stats.time.sleep"): transport = ASGITransport(app=app) async with AsyncClient(transport=transport, base_url="http://test") as client: resp = await client.get("/stats/season-avg?player=Nobody") assert resp.status_code == 404