All checks were successful
Build Docker Image / build (pull_request) Successful in 2m32s
Enables career-total aggregation by real-world player identity (SbaPlayer) across all seasons. JOINs StratPlay → Player to access Player.sbaplayer FK, groups by that FK, and excludes players with null sbaplayer. Also refactors stratplay router from single file into package and adds integration tests. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
628 lines
21 KiB
Python
628 lines
21 KiB
Python
"""
|
|
Integration tests for stratplay router endpoints.
|
|
|
|
Hits the live dev API to verify all play routes return correct
|
|
status codes, response shapes, and data. Used as a before/after
|
|
regression suite when refactoring the stratplay router.
|
|
|
|
Usage:
|
|
# Capture baseline snapshots (run BEFORE refactor)
|
|
python -m pytest tests/integration/test_stratplay_routes.py --snapshot-update -v
|
|
|
|
# Verify against snapshots (run AFTER refactor)
|
|
python -m pytest tests/integration/test_stratplay_routes.py -v
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
import requests
|
|
|
|
BASE_URL = os.environ.get("TEST_API_URL", "http://10.10.0.42:814")
|
|
SNAPSHOT_DIR = Path(__file__).parent / "snapshots"
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def api():
|
|
"""Verify the API is reachable before running tests."""
|
|
try:
|
|
r = requests.get(f"{BASE_URL}/api/v3/current", timeout=5)
|
|
r.raise_for_status()
|
|
except requests.ConnectionError:
|
|
pytest.skip(f"API not reachable at {BASE_URL}")
|
|
return BASE_URL
|
|
|
|
|
|
def save_snapshot(name: str, data: dict):
|
|
"""Save response data as a JSON snapshot."""
|
|
SNAPSHOT_DIR.mkdir(parents=True, exist_ok=True)
|
|
path = SNAPSHOT_DIR / f"{name}.json"
|
|
path.write_text(json.dumps(data, sort_keys=True, indent=2))
|
|
|
|
|
|
def load_snapshot(name: str) -> dict | None:
|
|
"""Load a previously saved snapshot, or None if it doesn't exist."""
|
|
path = SNAPSHOT_DIR / f"{name}.json"
|
|
if path.exists():
|
|
return json.loads(path.read_text())
|
|
return None
|
|
|
|
|
|
def snapshot_update_mode() -> bool:
|
|
"""Check if we're in snapshot update mode (baseline capture)."""
|
|
return os.environ.get("SNAPSHOT_UPDATE", "").lower() in ("1", "true", "yes")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 1. Route registration (OpenAPI schema)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
# Only public (GET) routes are visible in OpenAPI by default.
|
|
# POST/PATCH/DELETE use include_in_schema=PRIVATE_IN_SCHEMA which hides them
|
|
# unless PRIVATE_IN_SCHEMA env var is set. We test what's always visible.
|
|
EXPECTED_PLAY_ROUTES = {
|
|
"/api/v3/plays/": ["get"],
|
|
"/api/v3/plays/batting": ["get"],
|
|
"/api/v3/plays/pitching": ["get"],
|
|
"/api/v3/plays/fielding": ["get"],
|
|
"/api/v3/plays/{play_id}": ["get"],
|
|
}
|
|
|
|
|
|
class TestRouteRegistration:
|
|
def test_all_play_routes_exist(self, api):
|
|
"""Verify every expected play route is registered in the OpenAPI schema."""
|
|
r = requests.get(f"{api}/api/openapi.json", timeout=10)
|
|
assert r.status_code == 200
|
|
paths = r.json()["paths"]
|
|
|
|
for route, methods in EXPECTED_PLAY_ROUTES.items():
|
|
assert route in paths, f"Route {route} missing from OpenAPI schema"
|
|
for method in methods:
|
|
assert (
|
|
method in paths[route]
|
|
), f"Method {method.upper()} missing for {route}"
|
|
|
|
def test_play_routes_have_plays_tag(self, api):
|
|
"""All play routes should be tagged with 'plays'."""
|
|
r = requests.get(f"{api}/api/openapi.json", timeout=10)
|
|
paths = r.json()["paths"]
|
|
|
|
for route in EXPECTED_PLAY_ROUTES:
|
|
if route not in paths:
|
|
continue
|
|
for method, spec in paths[route].items():
|
|
if method in ("get", "post", "patch", "delete"):
|
|
tags = spec.get("tags", [])
|
|
assert (
|
|
"plays" in tags
|
|
), f"{method.upper()} {route} missing 'plays' tag, has {tags}"
|
|
|
|
@pytest.mark.post_deploy
|
|
@pytest.mark.skip(
|
|
reason="@cache_result decorator uses *args/**kwargs wrapper which hides "
|
|
"typed params from FastAPI's OpenAPI schema generation. "
|
|
"The parameter works functionally (see sbaplayer_filter tests)."
|
|
)
|
|
def test_sbaplayer_id_parameter_exists(self, api):
|
|
"""Verify sbaplayer_id query parameter is present on stats endpoints.
|
|
|
|
NOTE: Currently skipped because the @cache_result decorator's wrapper
|
|
uses *args/**kwargs, which prevents FastAPI from discovering typed
|
|
parameters for OpenAPI schema generation. This is a pre-existing issue
|
|
affecting all cached endpoints, not specific to sbaplayer_id.
|
|
"""
|
|
r = requests.get(f"{api}/api/openapi.json", timeout=10)
|
|
paths = r.json()["paths"]
|
|
|
|
for route in [
|
|
"/api/v3/plays/batting",
|
|
"/api/v3/plays/pitching",
|
|
"/api/v3/plays/fielding",
|
|
]:
|
|
params = paths[route]["get"].get("parameters", [])
|
|
param_names = [p["name"] for p in params]
|
|
assert (
|
|
"sbaplayer_id" in param_names
|
|
), f"sbaplayer_id parameter missing from {route}"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 2. Generic plays query
|
|
# ---------------------------------------------------------------------------
|
|
|
|
# Peewee model_to_dict uses FK field names (game, pitcher) not _id suffixed
|
|
PLAY_REQUIRED_KEYS = {"id", "game", "play_num", "pitcher", "pa", "hit", "wpa"}
|
|
|
|
|
|
class TestGenericPlays:
|
|
def test_plays_basic(self, api):
|
|
"""GET /plays with season filter returns valid structure."""
|
|
r = requests.get(
|
|
f"{api}/api/v3/plays", params={"season": 12, "limit": 3}, timeout=10
|
|
)
|
|
assert r.status_code == 200
|
|
data = r.json()
|
|
assert "count" in data
|
|
assert "plays" in data
|
|
assert isinstance(data["count"], int)
|
|
assert isinstance(data["plays"], list)
|
|
|
|
def test_plays_have_expected_keys(self, api):
|
|
"""Each play object has the core required fields."""
|
|
r = requests.get(
|
|
f"{api}/api/v3/plays", params={"season": 12, "limit": 3}, timeout=10
|
|
)
|
|
data = r.json()
|
|
if data["count"] == 0:
|
|
pytest.skip("No play data for season 12")
|
|
for play in data["plays"]:
|
|
missing = PLAY_REQUIRED_KEYS - set(play.keys())
|
|
assert not missing, f"Play missing keys: {missing}"
|
|
|
|
def test_plays_empty_season(self, api):
|
|
"""Querying a nonexistent season returns 200 with empty results."""
|
|
r = requests.get(
|
|
f"{api}/api/v3/plays", params={"season": 99, "limit": 3}, timeout=10
|
|
)
|
|
assert r.status_code == 200
|
|
data = r.json()
|
|
assert data["count"] == 0
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 3. Batting stats
|
|
# ---------------------------------------------------------------------------
|
|
|
|
BATTING_REQUIRED_KEYS = {
|
|
"player",
|
|
"team",
|
|
"pa",
|
|
"ab",
|
|
"hit",
|
|
"hr",
|
|
"avg",
|
|
"obp",
|
|
"slg",
|
|
"ops",
|
|
"woba",
|
|
"wpa",
|
|
"re24_primary",
|
|
"run",
|
|
"rbi",
|
|
"bb",
|
|
"so",
|
|
}
|
|
|
|
|
|
class TestBattingStats:
|
|
def test_batting_basic(self, api):
|
|
"""GET /plays/batting returns valid structure."""
|
|
r = requests.get(
|
|
f"{api}/api/v3/plays/batting", params={"season": 12, "limit": 3}, timeout=10
|
|
)
|
|
assert r.status_code == 200
|
|
data = r.json()
|
|
assert "count" in data
|
|
assert "stats" in data
|
|
assert isinstance(data["stats"], list)
|
|
|
|
def test_batting_has_expected_keys(self, api):
|
|
"""Each batting stat has required computed fields."""
|
|
r = requests.get(
|
|
f"{api}/api/v3/plays/batting", params={"season": 12, "limit": 3}, timeout=10
|
|
)
|
|
data = r.json()
|
|
if not data["stats"]:
|
|
pytest.skip("No batting data")
|
|
for stat in data["stats"]:
|
|
missing = BATTING_REQUIRED_KEYS - set(stat.keys())
|
|
assert not missing, f"Batting stat missing keys: {missing}"
|
|
|
|
def test_batting_player_filter(self, api):
|
|
"""Filtering by player_id returns results for that player."""
|
|
r = requests.get(
|
|
f"{api}/api/v3/plays/batting",
|
|
params={"season": 12, "player_id": 1, "limit": 5},
|
|
timeout=10,
|
|
)
|
|
assert r.status_code == 200
|
|
|
|
@pytest.mark.post_deploy
|
|
def test_batting_sbaplayer_filter(self, api):
|
|
"""Filtering by sbaplayer_id returns 200. Requires feature branch deployed."""
|
|
r = requests.get(
|
|
f"{api}/api/v3/plays/batting",
|
|
params={"season": 12, "sbaplayer_id": 1, "limit": 5},
|
|
timeout=10,
|
|
)
|
|
assert r.status_code == 200
|
|
|
|
def test_batting_group_by_team(self, api):
|
|
"""Group by team sets player to 'TOT'."""
|
|
r = requests.get(
|
|
f"{api}/api/v3/plays/batting",
|
|
params={"season": 12, "group_by": "team", "limit": 3},
|
|
timeout=10,
|
|
)
|
|
assert r.status_code == 200
|
|
data = r.json()
|
|
if data["stats"]:
|
|
assert data["stats"][0]["player"] == "TOT"
|
|
|
|
def test_batting_short_output(self, api):
|
|
"""Short output returns player as int, not dict."""
|
|
r = requests.get(
|
|
f"{api}/api/v3/plays/batting",
|
|
params={"season": 12, "short_output": "true", "limit": 3},
|
|
timeout=10,
|
|
)
|
|
assert r.status_code == 200
|
|
data = r.json()
|
|
if data["stats"]:
|
|
assert isinstance(data["stats"][0]["player"], int)
|
|
|
|
def test_batting_snapshot(self, api):
|
|
"""Snapshot test: exact data match for a specific player+season query."""
|
|
params = {"season": 12, "player_id": 1}
|
|
r = requests.get(f"{api}/api/v3/plays/batting", params=params, timeout=10)
|
|
assert r.status_code == 200
|
|
data = r.json()
|
|
|
|
name = "batting_s12_p1"
|
|
if snapshot_update_mode():
|
|
save_snapshot(name, data)
|
|
return
|
|
|
|
expected = load_snapshot(name)
|
|
if expected is None:
|
|
pytest.skip("No snapshot saved yet; run with SNAPSHOT_UPDATE=1 first")
|
|
assert data == expected, f"Batting snapshot mismatch for {name}"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 4. Pitching stats
|
|
# ---------------------------------------------------------------------------
|
|
|
|
PITCHING_REQUIRED_KEYS = {
|
|
"player",
|
|
"team",
|
|
"tbf",
|
|
"outs",
|
|
"era",
|
|
"whip",
|
|
"win",
|
|
"loss",
|
|
"k/9",
|
|
"bb/9",
|
|
"wpa",
|
|
"re24_primary",
|
|
"hits",
|
|
"so",
|
|
"bb",
|
|
}
|
|
|
|
|
|
class TestPitchingStats:
|
|
def test_pitching_basic(self, api):
|
|
"""GET /plays/pitching returns valid structure."""
|
|
r = requests.get(
|
|
f"{api}/api/v3/plays/pitching",
|
|
params={"season": 12, "limit": 3},
|
|
timeout=10,
|
|
)
|
|
assert r.status_code == 200
|
|
data = r.json()
|
|
assert "count" in data
|
|
assert "stats" in data
|
|
|
|
def test_pitching_has_expected_keys(self, api):
|
|
"""Each pitching stat has required computed fields."""
|
|
r = requests.get(
|
|
f"{api}/api/v3/plays/pitching",
|
|
params={"season": 12, "limit": 3},
|
|
timeout=10,
|
|
)
|
|
data = r.json()
|
|
if not data["stats"]:
|
|
pytest.skip("No pitching data")
|
|
for stat in data["stats"]:
|
|
missing = PITCHING_REQUIRED_KEYS - set(stat.keys())
|
|
assert not missing, f"Pitching stat missing keys: {missing}"
|
|
|
|
def test_pitching_player_filter(self, api):
|
|
"""Filtering by player_id returns 200."""
|
|
r = requests.get(
|
|
f"{api}/api/v3/plays/pitching",
|
|
params={"season": 12, "player_id": 1, "limit": 5},
|
|
timeout=10,
|
|
)
|
|
assert r.status_code == 200
|
|
|
|
@pytest.mark.post_deploy
|
|
def test_pitching_sbaplayer_filter(self, api):
|
|
"""Filtering by sbaplayer_id returns 200. Requires feature branch deployed."""
|
|
r = requests.get(
|
|
f"{api}/api/v3/plays/pitching",
|
|
params={"season": 12, "sbaplayer_id": 1, "limit": 5},
|
|
timeout=10,
|
|
)
|
|
assert r.status_code == 200
|
|
|
|
def test_pitching_group_by_team(self, api):
|
|
"""Group by team returns results."""
|
|
r = requests.get(
|
|
f"{api}/api/v3/plays/pitching",
|
|
params={"season": 12, "group_by": "team", "limit": 3},
|
|
timeout=10,
|
|
)
|
|
assert r.status_code == 200
|
|
|
|
def test_pitching_snapshot(self, api):
|
|
"""Snapshot test: exact data match for a specific player+season query."""
|
|
params = {"season": 12, "player_id": 1}
|
|
r = requests.get(f"{api}/api/v3/plays/pitching", params=params, timeout=10)
|
|
assert r.status_code == 200
|
|
data = r.json()
|
|
|
|
name = "pitching_s12_p1"
|
|
if snapshot_update_mode():
|
|
save_snapshot(name, data)
|
|
return
|
|
|
|
expected = load_snapshot(name)
|
|
if expected is None:
|
|
pytest.skip("No snapshot saved yet; run with SNAPSHOT_UPDATE=1 first")
|
|
assert data == expected, f"Pitching snapshot mismatch for {name}"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 5. Fielding stats
|
|
# ---------------------------------------------------------------------------
|
|
|
|
FIELDING_REQUIRED_KEYS = {
|
|
"player",
|
|
"team",
|
|
"pos",
|
|
"x-ch",
|
|
"hit",
|
|
"error",
|
|
"sb",
|
|
"cs",
|
|
"pb",
|
|
"wpa",
|
|
"wf%",
|
|
}
|
|
|
|
|
|
class TestFieldingStats:
|
|
def test_fielding_basic(self, api):
|
|
"""GET /plays/fielding returns valid structure."""
|
|
r = requests.get(
|
|
f"{api}/api/v3/plays/fielding",
|
|
params={"season": 12, "limit": 3},
|
|
timeout=10,
|
|
)
|
|
assert r.status_code == 200
|
|
data = r.json()
|
|
assert "count" in data
|
|
assert "stats" in data
|
|
|
|
def test_fielding_has_expected_keys(self, api):
|
|
"""Each fielding stat has required fields."""
|
|
r = requests.get(
|
|
f"{api}/api/v3/plays/fielding",
|
|
params={"season": 12, "limit": 3},
|
|
timeout=10,
|
|
)
|
|
data = r.json()
|
|
if not data["stats"]:
|
|
pytest.skip("No fielding data")
|
|
for stat in data["stats"]:
|
|
missing = FIELDING_REQUIRED_KEYS - set(stat.keys())
|
|
assert not missing, f"Fielding stat missing keys: {missing}"
|
|
|
|
def test_fielding_player_filter(self, api):
|
|
"""Filtering by player_id returns 200."""
|
|
r = requests.get(
|
|
f"{api}/api/v3/plays/fielding",
|
|
params={"season": 12, "player_id": 1, "limit": 5},
|
|
timeout=10,
|
|
)
|
|
assert r.status_code == 200
|
|
|
|
@pytest.mark.post_deploy
|
|
def test_fielding_sbaplayer_filter(self, api):
|
|
"""Filtering by sbaplayer_id returns 200. Requires feature branch deployed."""
|
|
r = requests.get(
|
|
f"{api}/api/v3/plays/fielding",
|
|
params={"season": 12, "sbaplayer_id": 1, "limit": 5},
|
|
timeout=10,
|
|
)
|
|
assert r.status_code == 200
|
|
|
|
def test_fielding_position_filter(self, api):
|
|
"""Filtering by position returns 200."""
|
|
r = requests.get(
|
|
f"{api}/api/v3/plays/fielding",
|
|
params={"season": 12, "position": "C", "limit": 3},
|
|
timeout=10,
|
|
)
|
|
assert r.status_code == 200
|
|
|
|
def test_fielding_snapshot(self, api):
|
|
"""Snapshot test: exact data match for a specific player+season query."""
|
|
params = {"season": 12, "player_id": 1}
|
|
r = requests.get(f"{api}/api/v3/plays/fielding", params=params, timeout=10)
|
|
assert r.status_code == 200
|
|
data = r.json()
|
|
|
|
name = "fielding_s12_p1"
|
|
if snapshot_update_mode():
|
|
save_snapshot(name, data)
|
|
return
|
|
|
|
expected = load_snapshot(name)
|
|
if expected is None:
|
|
pytest.skip("No snapshot saved yet; run with SNAPSHOT_UPDATE=1 first")
|
|
assert data == expected, f"Fielding snapshot mismatch for {name}"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 6. Single play CRUD (read-only)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestPlayCrud:
|
|
def test_get_single_play(self, api):
|
|
"""GET /plays/{id} returns a play with matching id."""
|
|
# First find a valid play ID from the data
|
|
r = requests.get(
|
|
f"{api}/api/v3/plays", params={"season": 12, "limit": 1}, timeout=10
|
|
)
|
|
data = r.json()
|
|
if not data["plays"]:
|
|
pytest.skip("No play data available")
|
|
play_id = data["plays"][0]["id"]
|
|
|
|
r = requests.get(f"{api}/api/v3/plays/{play_id}", timeout=10)
|
|
assert r.status_code == 200
|
|
result = r.json()
|
|
assert result["id"] == play_id
|
|
|
|
def test_get_nonexistent_play(self, api):
|
|
"""GET /plays/999999999 returns an error (wrapped by handle_db_errors)."""
|
|
r = requests.get(f"{api}/api/v3/plays/999999999", timeout=10)
|
|
# handle_db_errors wraps HTTPException as 500 with detail message
|
|
assert r.status_code == 500
|
|
assert "not found" in r.json().get("detail", "").lower()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 7. Validation / error cases
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestValidation:
|
|
def test_week_and_stype_conflict(self, api):
|
|
"""Using both week and s_type should return an error."""
|
|
r = requests.get(
|
|
f"{api}/api/v3/plays/batting",
|
|
params={"season": 12, "week": 1, "s_type": "regular"},
|
|
timeout=10,
|
|
)
|
|
# handle_db_errors wraps the 400 HTTPException as 500
|
|
assert r.status_code in (400, 500)
|
|
assert "cannot be used" in r.json().get("detail", "").lower()
|
|
|
|
def test_week_and_week_start_conflict(self, api):
|
|
"""Using both week and week_start should return an error."""
|
|
r = requests.get(
|
|
f"{api}/api/v3/plays/batting",
|
|
params={"season": 12, "week": 1, "week_start": 1},
|
|
timeout=10,
|
|
)
|
|
assert r.status_code in (400, 500)
|
|
assert "cannot be used" in r.json().get("detail", "").lower()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 8. group_by=sbaplayer
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestGroupBySbaPlayer:
|
|
"""Tests for group_by=sbaplayer which aggregates across all Player records
|
|
sharing the same SbaPlayer identity (career totals by real-world player)."""
|
|
|
|
@pytest.mark.post_deploy
|
|
def test_batting_sbaplayer_group_by(self, api):
|
|
"""GET /plays/batting?group_by=sbaplayer&sbaplayer_id=1 returns exactly 1 row
|
|
with SbaPlayer object in 'player' field and team='TOT'."""
|
|
r = requests.get(
|
|
f"{api}/api/v3/plays/batting",
|
|
params={"group_by": "sbaplayer", "sbaplayer_id": 1},
|
|
timeout=15,
|
|
)
|
|
assert r.status_code == 200
|
|
data = r.json()
|
|
assert data["count"] == 1, f"Expected 1 career row, got {data['count']}"
|
|
row = data["stats"][0]
|
|
assert isinstance(row["player"], dict), "player should be SbaPlayer dict"
|
|
assert "first_name" in row["player"]
|
|
assert "last_name" in row["player"]
|
|
assert row["team"] == "TOT"
|
|
|
|
@pytest.mark.post_deploy
|
|
def test_batting_sbaplayer_career_totals(self, api):
|
|
"""Career PA via group_by=sbaplayer should be >= any single season's PA."""
|
|
# Get career total
|
|
r_career = requests.get(
|
|
f"{api}/api/v3/plays/batting",
|
|
params={"group_by": "sbaplayer", "sbaplayer_id": 1},
|
|
timeout=15,
|
|
)
|
|
assert r_career.status_code == 200
|
|
career_pa = r_career.json()["stats"][0]["pa"]
|
|
|
|
# Get per-season rows
|
|
r_seasons = requests.get(
|
|
f"{api}/api/v3/plays/batting",
|
|
params={"group_by": "player", "sbaplayer_id": 1, "limit": 999},
|
|
timeout=15,
|
|
)
|
|
assert r_seasons.status_code == 200
|
|
season_pas = [s["pa"] for s in r_seasons.json()["stats"]]
|
|
assert career_pa >= max(
|
|
season_pas
|
|
), f"Career PA ({career_pa}) should be >= max season PA ({max(season_pas)})"
|
|
|
|
@pytest.mark.post_deploy
|
|
def test_batting_sbaplayer_short_output(self, api):
|
|
"""short_output=true with group_by=sbaplayer returns integer player field."""
|
|
r = requests.get(
|
|
f"{api}/api/v3/plays/batting",
|
|
params={
|
|
"group_by": "sbaplayer",
|
|
"sbaplayer_id": 1,
|
|
"short_output": "true",
|
|
},
|
|
timeout=15,
|
|
)
|
|
assert r.status_code == 200
|
|
data = r.json()
|
|
assert data["count"] == 1
|
|
assert isinstance(data["stats"][0]["player"], int)
|
|
|
|
@pytest.mark.post_deploy
|
|
def test_pitching_sbaplayer_group_by(self, api):
|
|
"""GET /plays/pitching?group_by=sbaplayer returns 200 with valid structure."""
|
|
r = requests.get(
|
|
f"{api}/api/v3/plays/pitching",
|
|
params={"group_by": "sbaplayer", "sbaplayer_id": 1, "min_pa": 1},
|
|
timeout=15,
|
|
)
|
|
assert r.status_code == 200
|
|
data = r.json()
|
|
if data["stats"]:
|
|
row = data["stats"][0]
|
|
assert isinstance(row["player"], dict)
|
|
assert row["team"] == "TOT"
|
|
|
|
@pytest.mark.post_deploy
|
|
def test_fielding_sbaplayer_group_by(self, api):
|
|
"""GET /plays/fielding?group_by=sbaplayer returns 200 with valid structure."""
|
|
r = requests.get(
|
|
f"{api}/api/v3/plays/fielding",
|
|
params={"group_by": "sbaplayer", "sbaplayer_id": 1, "min_ch": 1},
|
|
timeout=15,
|
|
)
|
|
assert r.status_code == 200
|
|
data = r.json()
|
|
if data["stats"]:
|
|
row = data["stats"][0]
|
|
assert isinstance(row["player"], dict)
|
|
assert row["team"] == "TOT"
|