"""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