diff --git a/helpers/main.py b/helpers/main.py index 0b989a5..b879ea1 100644 --- a/helpers/main.py +++ b/helpers/main.py @@ -122,8 +122,24 @@ async def share_channel(channel, user, read_only=False): async def get_card_embeds(card, include_stats=False) -> list: + # WP-12: fetch evolution state and build tier badge prefix. + # Non-blocking — any failure falls back to no badge so card display is + # never broken by an unavailable or slow evolution API. + tier_badge = "" + try: + evo_state = await db_get(f"evolution/cards/{card['id']}", none_okay=True) + if evo_state and evo_state.get("current_tier", 0) > 0: + tier = evo_state["current_tier"] + tier_badge = f"[{'EVO' if tier >= 4 else f'T{tier}'}] " + except Exception: + logging.warning( + f"Could not fetch evolution state for card {card.get('id')}; " + "displaying without tier badge.", + exc_info=True, + ) + embed = discord.Embed( - title=f"{card['player']['p_name']}", + title=f"{tier_badge}{card['player']['p_name']}", color=int(card["player"]["rarity"]["color"], 16), ) # embed.description = card['team']['lname'] diff --git a/tests/test_card_embed_evolution.py b/tests/test_card_embed_evolution.py new file mode 100644 index 0000000..5a3b9e2 --- /dev/null +++ b/tests/test_card_embed_evolution.py @@ -0,0 +1,315 @@ +""" +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"