paper-dynasty-database/tests/test_refractor_image_url.py
Cal Corum be8bebe663 feat: include image_url in refractor cards API response
Adds image_url field to each card state entry in the GET
/api/v2/refractor/cards response. Resolved by looking up the variant
BattingCard/PitchingCard row. Returns null when no image has been
rendered yet.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 22:36:20 +00:00

312 lines
9.0 KiB
Python

"""Tests for image_url field in refractor cards API response.
What: Verifies that GET /api/v2/refractor/cards includes image_url in each card state
item, pulling the URL from the variant BattingCard or PitchingCard row.
Why: The refractor card art pipeline stores rendered card image URLs in the
BattingCard/PitchingCard rows. The Discord bot and website need image_url in
the /refractor/cards response so they can display variant art without a separate
lookup. These tests guard against regressions where image_url is accidentally
dropped from the response serialization.
Test cases:
test_cards_response_includes_image_url -- BattingCard with image_url set; verify
the value appears in the /cards response.
test_cards_response_image_url_null_when_not_set -- BattingCard with image_url=None;
verify null is returned (not omitted).
Uses the shared-memory SQLite TestClient pattern from test_refractor_state_api.py
so no PostgreSQL connection is required.
"""
import os
os.environ.setdefault("API_TOKEN", "test")
import pytest
from fastapi import FastAPI, Request
from fastapi.testclient import TestClient
from peewee import SqliteDatabase
from app.db_engine import (
BattingCard,
BattingSeasonStats,
Card,
Cardset,
Decision,
Event,
MlbPlayer,
Pack,
PackType,
PitchingCard,
PitchingSeasonStats,
Player,
ProcessedGame,
Rarity,
RefractorCardState,
RefractorCosmetic,
RefractorTierBoost,
RefractorTrack,
Roster,
RosterSlot,
ScoutClaim,
ScoutOpportunity,
StratGame,
StratPlay,
Team,
)
AUTH_HEADER = {"Authorization": "Bearer test"}
# ---------------------------------------------------------------------------
# SQLite database + model list
# ---------------------------------------------------------------------------
_img_url_db = SqliteDatabase(
"file:imgurlapitest?mode=memory&cache=shared",
uri=True,
pragmas={"foreign_keys": 1},
)
# Full model list matching the existing state API tests — needed so all FK
# constraints resolve in SQLite.
_IMG_URL_MODELS = [
Rarity,
Event,
Cardset,
MlbPlayer,
Player,
BattingCard,
PitchingCard,
Team,
PackType,
Pack,
Card,
Roster,
RosterSlot,
StratGame,
StratPlay,
Decision,
ScoutOpportunity,
ScoutClaim,
BattingSeasonStats,
PitchingSeasonStats,
ProcessedGame,
RefractorTrack,
RefractorCardState,
RefractorTierBoost,
RefractorCosmetic,
]
@pytest.fixture(autouse=False)
def setup_img_url_db():
"""Bind image-url test models to shared-memory SQLite and create tables.
What: Initialises the in-process SQLite database before each test and drops
all tables afterwards to ensure test isolation.
Why: SQLite shared-memory databases persist between tests in the same
process unless tables are dropped. Creating and dropping around each test
guarantees a clean state without requiring a real PostgreSQL instance.
"""
_img_url_db.bind(_IMG_URL_MODELS)
_img_url_db.connect(reuse_if_open=True)
_img_url_db.create_tables(_IMG_URL_MODELS)
yield _img_url_db
_img_url_db.drop_tables(list(reversed(_IMG_URL_MODELS)), safe=True)
def _build_image_url_app() -> FastAPI:
"""Minimal FastAPI app with refractor router for image_url tests."""
from app.routers_v2.refractor import router as refractor_router
app = FastAPI()
@app.middleware("http")
async def db_middleware(request: Request, call_next):
_img_url_db.connect(reuse_if_open=True)
return await call_next(request)
app.include_router(refractor_router)
return app
@pytest.fixture
def img_url_client(setup_img_url_db):
"""FastAPI TestClient backed by shared-memory SQLite for image_url tests."""
with TestClient(_build_image_url_app()) as c:
yield c
# ---------------------------------------------------------------------------
# Seed helpers
# ---------------------------------------------------------------------------
def _make_rarity():
r, _ = Rarity.get_or_create(
value=10, name="IU_Common", defaults={"color": "#ffffff"}
)
return r
def _make_cardset():
cs, _ = Cardset.get_or_create(
name="IU Test Set",
defaults={"description": "image url test cardset", "total_cards": 1},
)
return cs
def _make_player(name: str = "Test Player") -> Player:
return Player.create(
p_name=name,
rarity=_make_rarity(),
cardset=_make_cardset(),
set_num=1,
pos_1="CF",
image="https://example.com/img.png",
mlbclub="TST",
franchise="TST",
description="image url test",
)
def _make_team(suffix: str = "IU") -> Team:
return Team.create(
abbrev=suffix,
sname=suffix,
lname=f"Team {suffix}",
gmid=99900 + len(suffix),
gmname=f"gm_{suffix.lower()}",
gsheet="https://docs.google.com/iu_test",
wallet=500,
team_value=1000,
collection_value=1000,
season=11,
is_ai=False,
)
def _make_track(card_type: str = "batter") -> RefractorTrack:
track, _ = RefractorTrack.get_or_create(
name=f"IU {card_type} Track",
defaults=dict(
card_type=card_type,
formula="pa",
t1_threshold=100,
t2_threshold=300,
t3_threshold=700,
t4_threshold=1200,
),
)
return track
def _make_batting_card(player: Player, variant: int, image_url=None) -> BattingCard:
return BattingCard.create(
player=player,
variant=variant,
steal_low=1,
steal_high=3,
steal_auto=False,
steal_jump=1.0,
bunting="N",
hit_and_run="N",
running=5,
offense_col=1,
hand="R",
image_url=image_url,
)
def _make_card_state(
player: Player,
team: Team,
track: RefractorTrack,
variant: int,
current_tier: int = 1,
current_value: float = 150.0,
) -> RefractorCardState:
import datetime
return RefractorCardState.create(
player=player,
team=team,
track=track,
current_tier=current_tier,
current_value=current_value,
fully_evolved=False,
last_evaluated_at=datetime.datetime(2026, 4, 1, 12, 0, 0),
variant=variant,
)
# ---------------------------------------------------------------------------
# Tests
# ---------------------------------------------------------------------------
def test_cards_response_includes_image_url(setup_img_url_db, img_url_client):
"""GET /api/v2/refractor/cards includes image_url when the variant BattingCard has one.
What: Seeds a RefractorCardState at variant=1 and a matching BattingCard with
image_url set. Calls the /cards endpoint and asserts that image_url in the
response matches the seeded URL.
Why: This is the primary happy-path test for the image_url feature. If the
DB lookup in _build_card_state_response fails or the field is accidentally
omitted from the response dict, this test will catch it.
"""
player = _make_player("Homer Simpson")
team = _make_team("IU1")
track = _make_track("batter")
expected_url = (
"https://s3.example.com/cards/cardset-001/player-1/v1/battingcard.png"
)
_make_batting_card(player, variant=1, image_url=expected_url)
_make_card_state(player, team, track, variant=1)
resp = img_url_client.get(
f"/api/v2/refractor/cards?team_id={team.id}&evaluated_only=false",
headers=AUTH_HEADER,
)
assert resp.status_code == 200, resp.text
data = resp.json()
assert data["count"] == 1
item = data["items"][0]
assert "image_url" in item, "image_url key missing from response"
assert item["image_url"] == expected_url
def test_cards_response_image_url_null_when_not_set(setup_img_url_db, img_url_client):
"""GET /api/v2/refractor/cards returns image_url: null when BattingCard.image_url is None.
What: Seeds a BattingCard with image_url=None and a RefractorCardState at
variant=1. Verifies the response contains image_url with a null value.
Why: The image_url field must always be present in the response (even when
null) so API consumers can rely on its presence. Returning null rather than
omitting the key is the correct contract — omitting it would break consumers
that check for the key's presence to determine upload status.
"""
player = _make_player("Bart Simpson")
team = _make_team("IU2")
track = _make_track("batter")
_make_batting_card(player, variant=1, image_url=None)
_make_card_state(player, team, track, variant=1)
resp = img_url_client.get(
f"/api/v2/refractor/cards?team_id={team.id}&evaluated_only=false",
headers=AUTH_HEADER,
)
assert resp.status_code == 200, resp.text
data = resp.json()
assert data["count"] == 1
item = data["items"][0]
assert "image_url" in item, "image_url key missing from response"
assert item["image_url"] is None