""" Tests for WP-12: Tier Badge on Card Embed. What: Verifies that get_card_embeds() correctly prepends a tier badge to the embed title when a card has evolution progress, and gracefully degrades when the evolution API is unavailable. Why: The tier badge is a non-blocking UI enhancement. Any failure in the evolution API must never prevent the card embed from rendering — this test suite enforces that contract while also validating the badge format logic. """ import pytest from unittest.mock import AsyncMock, patch import discord # --------------------------------------------------------------------------- # Helpers / shared fixtures # --------------------------------------------------------------------------- def make_card( player_id=42, p_name="Mike Trout", rarity_color="FFD700", image="https://example.com/card.png", headshot=None, franchise="Los Angeles Angels", bbref_id="troutmi01", fangr_id=None, strat_code="420420", mlbclub="Los Angeles Angels", cardset_name="2024 Season", ): """ Build the minimal card dict that get_card_embeds() expects, matching the shape returned by the Paper Dynasty API (nested player / team / rarity). Using p_name='Mike Trout' as the canonical test name so we can assert against '[Tx] Mike Trout' title strings without repeating the name. """ return { "id": 9001, "player": { "player_id": player_id, "p_name": p_name, "rarity": {"color": rarity_color, "name": "Hall of Fame"}, "image": image, "image2": None, "headshot": headshot, "mlbclub": mlbclub, "franchise": franchise, "bbref_id": bbref_id, "fangr_id": fangr_id, "strat_code": strat_code, "cost": 500, "cardset": {"name": cardset_name}, "pos_1": "CF", "pos_2": None, "pos_3": None, "pos_4": None, "pos_5": None, "pos_6": None, "pos_7": None, "pos_8": None, }, "team": { "id": 1, "lname": "Test Team", "logo": "https://example.com/logo.png", "season": 7, }, } def make_evo_state(tier: int) -> dict: """Return a minimal evolution-state dict for a given tier.""" return {"current_tier": tier, "xp": 100, "max_tier": 4} EMPTY_PAPERDEX = {"count": 0, "paperdex": []} def _db_get_side_effect(evo_response): """ Build a db_get coroutine side-effect that returns evo_response for evolution/* endpoints and an empty paperdex for everything else. """ async def _side_effect(endpoint, **kwargs): if "evolution" in endpoint: return evo_response if "paperdex" in endpoint: return EMPTY_PAPERDEX return None return _side_effect # --------------------------------------------------------------------------- # Tier badge format — pure function tests (no Discord/API involved) # --------------------------------------------------------------------------- class TestTierBadgeFormat: """ Unit tests for the _get_tier_badge() helper that computes the badge string. Why separate: the badge logic is simple but error-prone at the boundary between tier 3 and tier 4 (EVO). Testing it in isolation makes failures immediately obvious without standing up the full embed machinery. """ def _badge(self, tier: int) -> str: """Inline mirror of the production badge logic for white-box testing.""" if tier <= 0: return "" return f"[{'EVO' if tier >= 4 else f'T{tier}'}] " def test_tier_0_returns_empty_string(self): """Tier 0 means no evolution progress — badge must be absent.""" assert self._badge(0) == "" def test_negative_tier_returns_empty_string(self): """Defensive: negative tiers (should not happen) must produce no badge.""" assert self._badge(-1) == "" def test_tier_1_shows_T1(self): assert self._badge(1) == "[T1] " def test_tier_2_shows_T2(self): assert self._badge(2) == "[T2] " def test_tier_3_shows_T3(self): assert self._badge(3) == "[T3] " def test_tier_4_shows_EVO(self): """Tier 4 is fully evolved — badge changes from T4 to EVO.""" assert self._badge(4) == "[EVO] " def test_tier_above_4_shows_EVO(self): """Any tier >= 4 should display EVO (defensive against future tiers).""" assert self._badge(5) == "[EVO] " assert self._badge(99) == "[EVO] " # --------------------------------------------------------------------------- # Integration-style tests for get_card_embeds() title construction # --------------------------------------------------------------------------- class TestCardEmbedTierBadge: """ Validates that get_card_embeds() produces the correct title format when evolution state is present or absent. Strategy: patch helpers.main.db_get to control what the evolution endpoint returns, then call get_card_embeds() and inspect the resulting embed title. """ @pytest.mark.asyncio @pytest.mark.asyncio async def test_no_evolution_state_shows_plain_name(self): """ When the evolution API returns None (404 or down), the embed title must equal the player name with no badge prefix. """ from helpers.main import get_card_embeds card = make_card(p_name="Mike Trout") with patch( "helpers.main.db_get", new=AsyncMock(side_effect=_db_get_side_effect(None)) ): embeds = await get_card_embeds(card) assert len(embeds) > 0 assert embeds[0].title == "Mike Trout" @pytest.mark.asyncio async def test_tier_0_shows_plain_name(self): """ Tier 0 in the evolution state means no progress yet — no badge shown. """ from helpers.main import get_card_embeds card = make_card(p_name="Mike Trout") with patch( "helpers.main.db_get", new=AsyncMock(side_effect=_db_get_side_effect(make_evo_state(0))), ): embeds = await get_card_embeds(card) assert embeds[0].title == "Mike Trout" @pytest.mark.asyncio async def test_tier_1_badge_in_title(self): """Tier 1 card shows [T1] prefix in the embed title.""" from helpers.main import get_card_embeds card = make_card(p_name="Mike Trout") with patch( "helpers.main.db_get", new=AsyncMock(side_effect=_db_get_side_effect(make_evo_state(1))), ): embeds = await get_card_embeds(card) assert embeds[0].title == "[T1] Mike Trout" @pytest.mark.asyncio async def test_tier_2_badge_in_title(self): """Tier 2 card shows [T2] prefix in the embed title.""" from helpers.main import get_card_embeds card = make_card(p_name="Mike Trout") with patch( "helpers.main.db_get", new=AsyncMock(side_effect=_db_get_side_effect(make_evo_state(2))), ): embeds = await get_card_embeds(card) assert embeds[0].title == "[T2] Mike Trout" @pytest.mark.asyncio async def test_tier_3_badge_in_title(self): """Tier 3 card shows [T3] prefix in the embed title.""" from helpers.main import get_card_embeds card = make_card(p_name="Mike Trout") with patch( "helpers.main.db_get", new=AsyncMock(side_effect=_db_get_side_effect(make_evo_state(3))), ): embeds = await get_card_embeds(card) assert embeds[0].title == "[T3] Mike Trout" @pytest.mark.asyncio async def test_tier_4_shows_evo_badge(self): """Fully evolved card (tier 4) shows [EVO] prefix instead of [T4].""" from helpers.main import get_card_embeds card = make_card(p_name="Mike Trout") with patch( "helpers.main.db_get", new=AsyncMock(side_effect=_db_get_side_effect(make_evo_state(4))), ): embeds = await get_card_embeds(card) assert embeds[0].title == "[EVO] Mike Trout" @pytest.mark.asyncio async def test_embed_color_unchanged_by_badge(self): """ The tier badge must not affect the embed color — rarity color is the only driver of embed color, even for evolved cards. Why: embed color communicates card rarity to players. Silently breaking it via evolution would confuse users. """ from helpers.main import get_card_embeds rarity_color = "FFD700" card = make_card(p_name="Mike Trout", rarity_color=rarity_color) with patch( "helpers.main.db_get", new=AsyncMock(side_effect=_db_get_side_effect(make_evo_state(3))), ): embeds = await get_card_embeds(card) expected_color = int(rarity_color, 16) assert embeds[0].colour.value == expected_color @pytest.mark.asyncio async def test_evolution_api_exception_shows_plain_name(self): """ When the evolution API raises an unexpected exception (network error, server crash, etc.), the embed must still render with the plain player name — no badge, no crash. This is the critical non-blocking contract for the feature. """ from helpers.main import get_card_embeds async def exploding_side_effect(endpoint, **kwargs): if "evolution" in endpoint: raise RuntimeError("simulated network failure") if "paperdex" in endpoint: return EMPTY_PAPERDEX return None card = make_card(p_name="Mike Trout") with patch( "helpers.main.db_get", new=AsyncMock(side_effect=exploding_side_effect) ): embeds = await get_card_embeds(card) assert embeds[0].title == "Mike Trout" @pytest.mark.asyncio async def test_evolution_api_missing_current_tier_key(self): """ If the evolution response is present but lacks 'current_tier', the embed must gracefully degrade to no badge (defensive against API drift). """ from helpers.main import get_card_embeds card = make_card(p_name="Mike Trout") # Response exists but is missing the expected key with patch( "helpers.main.db_get", new=AsyncMock(side_effect=_db_get_side_effect({"xp": 50})), ): embeds = await get_card_embeds(card) assert embeds[0].title == "Mike Trout"