feat: Feature 1.2 (NBA stats FastAPI service) + Feature 1.4 (database schema)
Feature 1.2: Python FastAPI microservice wrapping nba_api - GET /stats/season-avg, /stats/last-n, /stats/splits, /players/search - Redis caching (24hr/1hr/6hr/7day), 0.6s rate limiting, PRA derived stat - 27 Python tests passing Feature 1.4: Complete Supabase database schema - 6 tables: users, picks, scan_sessions, bets, outcomes, performance - RLS enabled on all tables with auth.uid() policies - 3 triggers: auto-create user, updated_at, scan count reset - 37 schema validation tests passing - Migration SQL ready, pending manual apply (WSL2 DNS blocker) Total: 92 tests (65 Node.js + 27 Python), all passing Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
import sys
|
||||
import os
|
||||
import pytest
|
||||
from unittest.mock import patch
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
||||
|
||||
from app.services.stats import (
|
||||
_map_stats,
|
||||
get_current_season,
|
||||
get_season_avg,
|
||||
get_last_n,
|
||||
get_splits,
|
||||
)
|
||||
|
||||
|
||||
class TestMapStats:
|
||||
def test_maps_all_nba_api_columns(self):
|
||||
row = {
|
||||
"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,
|
||||
}
|
||||
result = _map_stats(row)
|
||||
assert result["points"] == 26.3
|
||||
assert result["rebounds"] == 12.4
|
||||
assert result["assists"] == 9.1
|
||||
assert result["threes"] == 1.1
|
||||
assert result["blocks"] == 0.7
|
||||
assert result["steals"] == 1.4
|
||||
assert result["turnovers"] == 3.2
|
||||
assert result["minutes"] == 34.2
|
||||
assert result["games_played"] == 65.0
|
||||
|
||||
def test_computes_pra(self):
|
||||
row = {"PTS": 25.0, "REB": 10.0, "AST": 8.0}
|
||||
result = _map_stats(row)
|
||||
assert result["pra"] == 43.0
|
||||
|
||||
def test_handles_missing_stats(self):
|
||||
row = {}
|
||||
result = _map_stats(row)
|
||||
assert result["pra"] == 0
|
||||
assert "points" not in result
|
||||
|
||||
def test_rounds_to_one_decimal(self):
|
||||
row = {"PTS": 26.333333}
|
||||
result = _map_stats(row)
|
||||
assert result["points"] == 26.3
|
||||
|
||||
|
||||
class TestGetCurrentSeason:
|
||||
def test_current_season_format(self):
|
||||
season = get_current_season()
|
||||
assert len(season) == 7
|
||||
assert "-" in season
|
||||
year = int(season[:4])
|
||||
suffix = int(season[5:])
|
||||
assert suffix == (year + 1) % 100
|
||||
|
||||
|
||||
class TestGetSeasonAvg:
|
||||
def test_returns_season_averages(self, mock_nba_api):
|
||||
result = get_season_avg("Nikola Jokic", season="2025-26")
|
||||
assert result is not None
|
||||
assert result["player"] == "Nikola Jokic"
|
||||
assert result["team"] == "DEN"
|
||||
assert result["stats"]["points"] == 26.3
|
||||
assert result["stats"]["pra"] == 47.8
|
||||
assert result["source"] == "live"
|
||||
|
||||
def test_filters_by_stat_type(self, mock_nba_api):
|
||||
result = get_season_avg("Nikola Jokic", stat_type="points", season="2025-26")
|
||||
assert list(result["stats"].keys()) == ["points"]
|
||||
|
||||
def test_returns_cache_when_available(self):
|
||||
cached = {
|
||||
"player": "Nikola Jokic", "player_id": 203999, "team": "DEN",
|
||||
"season": "2025-26", "source": "live", "stats": {"points": 26.3},
|
||||
}
|
||||
with patch("app.services.stats.cache_get", return_value=cached), \
|
||||
patch("app.utils.player_map.cache_get", return_value=None), \
|
||||
patch("app.utils.player_map.cache_set"), \
|
||||
patch("app.utils.player_map.players.get_players", return_value=[
|
||||
{"id": 203999, "full_name": "Nikola Jokic", "is_active": True}
|
||||
]), \
|
||||
patch("app.services.stats.time.sleep"):
|
||||
result = get_season_avg("Nikola Jokic", season="2025-26")
|
||||
assert result["source"] == "cache"
|
||||
|
||||
|
||||
class TestGetLastN:
|
||||
def test_returns_last_n_averages(self, mock_nba_api):
|
||||
result = get_last_n("Nikola Jokic", n=5)
|
||||
assert result is not None
|
||||
assert result["last_n"] == 5
|
||||
assert result["stats"]["games_played"] == 5.0
|
||||
assert result["source"] == "live"
|
||||
assert result["stats"]["points"] == 26.0
|
||||
|
||||
def test_clamps_n_to_max_30(self, mock_nba_api):
|
||||
result = get_last_n("Nikola Jokic", n=50)
|
||||
assert result["last_n"] == 30
|
||||
|
||||
|
||||
class TestGetSplits:
|
||||
def test_home_away_split(self, mock_nba_api):
|
||||
result = get_splits("Nikola Jokic", "points", "home_away")
|
||||
assert result is not None
|
||||
assert "home" in result["splits"]
|
||||
assert "away" in result["splits"]
|
||||
assert result["splits"]["home"]["games"] == 3
|
||||
assert result["splits"]["away"]["games"] == 2
|
||||
|
||||
def test_rest_days_split(self, mock_nba_api):
|
||||
result = get_splits("Nikola Jokic", "points", "rest_days")
|
||||
assert result is not None
|
||||
assert "b2b" in result["splits"]
|
||||
assert "1_day_rest" in result["splits"]
|
||||
assert "2_plus_days_rest" in result["splits"]
|
||||
|
||||
def test_vs_team_split(self, mock_nba_api):
|
||||
result = get_splits("Nikola Jokic", "points", "vs_team", opponent="LAL")
|
||||
assert result is not None
|
||||
assert "vs_opponent" in result["splits"]
|
||||
assert "vs_all_others" in result["splits"]
|
||||
assert result["splits"]["vs_opponent"]["games"] == 2
|
||||
|
||||
def test_vs_team_requires_opponent(self, mock_nba_api):
|
||||
result = get_splits("Nikola Jokic", "points", "vs_team", opponent=None)
|
||||
assert result is None
|
||||
|
||||
|
||||
class TestPlayerNotFound:
|
||||
@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")
|
||||
def test_season_avg_returns_none(self, mock_set, mock_get, mock_players):
|
||||
result = get_season_avg("Nonexistent Player")
|
||||
assert result is None
|
||||
|
||||
@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")
|
||||
def test_last_n_returns_none(self, mock_set, mock_get, mock_players):
|
||||
result = get_last_n("Nonexistent Player")
|
||||
assert result is None
|
||||
Reference in New Issue
Block a user