paper-dynasty-discord/tests/test_card_embed_refractor.py
cal 8c0c2eb21a test: refractor system comprehensive test coverage (#117)
test: add comprehensive refractor system test coverage (23 tests)

Covers TIER_NAMES/TIER_BADGES cross-module consistency, WP-14 tier_up dict
shape mismatch (latent KeyError documented), None channel handling, filter
combinations, progress bar boundaries, and malformed API response handling.
2026-03-24 21:06:13 +00:00

341 lines
13 KiB
Python

"""
Tests for WP-12: Tier Badge on Card Embed.
Verifies that get_card_embeds() prepends a tier badge to the card title when a
card has Refractor tier progression, and falls back gracefully when the Refractor
API is unavailable or returns no state.
"""
import pytest
from unittest.mock import AsyncMock, patch
import discord
# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------
def _make_card(card_id=1, player_name="Mike Trout", rarity_color="FFD700"):
"""Minimal card dict matching the API shape consumed by get_card_embeds."""
return {
"id": card_id,
"player": {
"player_id": 101,
"p_name": player_name,
"rarity": {"name": "MVP", "value": 5, "color": rarity_color},
"cost": 500,
"image": "https://example.com/card.png",
"image2": None,
"mlbclub": "Los Angeles Angels",
"franchise": "Los Angeles Angels",
"headshot": "https://example.com/headshot.jpg",
"cardset": {"name": "2023 Season"},
"pos_1": "CF",
"pos_2": None,
"pos_3": None,
"pos_4": None,
"pos_5": None,
"pos_6": None,
"pos_7": None,
"bbref_id": "troutmi01",
"strat_code": "420420",
"fangr_id": None,
"vanity_card": None,
},
"team": {
"id": 10,
"lname": "Paper Dynasty",
"logo": "https://example.com/logo.png",
"season": 7,
},
}
def _make_paperdex():
"""Minimal paperdex response."""
return {"count": 0, "paperdex": []}
# ---------------------------------------------------------------------------
# Helpers to patch the async dependencies of get_card_embeds
# ---------------------------------------------------------------------------
def _patch_db_get(evo_response=None, paperdex_response=None):
"""
Return a side_effect callable that routes db_get calls to the right mock
responses, so other get_card_embeds internals still behave.
"""
if paperdex_response is None:
paperdex_response = _make_paperdex()
async def _side_effect(endpoint, *args, **kwargs):
if str(endpoint).startswith("refractor/cards/"):
return evo_response
if endpoint == "paperdex":
return paperdex_response
# Fallback for any other endpoint (e.g. plays/batting, plays/pitching)
return None
return _side_effect
# ---------------------------------------------------------------------------
# Tests
# ---------------------------------------------------------------------------
class TestTierBadgeFormat:
"""Unit: tier badge string format for each tier level."""
@pytest.mark.asyncio
async def test_tier_zero_no_badge(self):
"""T0 evolution state (current_tier=0) should produce no badge in title."""
card = _make_card()
evo_state = {"current_tier": 0, "card_id": 1}
with patch("helpers.main.db_get", new_callable=AsyncMock) as mock_db:
mock_db.side_effect = _patch_db_get(evo_response=evo_state)
embeds = await _call_get_card_embeds(card)
assert embeds[0].title == "Mike Trout"
@pytest.mark.asyncio
async def test_tier_one_badge(self):
"""current_tier=1 should prefix title with [BC] (Base Chrome)."""
card = _make_card()
evo_state = {"current_tier": 1, "card_id": 1}
with patch("helpers.main.db_get", new_callable=AsyncMock) as mock_db:
mock_db.side_effect = _patch_db_get(evo_response=evo_state)
embeds = await _call_get_card_embeds(card)
assert embeds[0].title == "[BC] Mike Trout"
@pytest.mark.asyncio
async def test_tier_two_badge(self):
"""current_tier=2 should prefix title with [R] (Refractor)."""
card = _make_card()
evo_state = {"current_tier": 2, "card_id": 1}
with patch("helpers.main.db_get", new_callable=AsyncMock) as mock_db:
mock_db.side_effect = _patch_db_get(evo_response=evo_state)
embeds = await _call_get_card_embeds(card)
assert embeds[0].title == "[R] Mike Trout"
@pytest.mark.asyncio
async def test_tier_three_badge(self):
"""current_tier=3 should prefix title with [GR] (Gold Refractor)."""
card = _make_card()
evo_state = {"current_tier": 3, "card_id": 1}
with patch("helpers.main.db_get", new_callable=AsyncMock) as mock_db:
mock_db.side_effect = _patch_db_get(evo_response=evo_state)
embeds = await _call_get_card_embeds(card)
assert embeds[0].title == "[GR] Mike Trout"
@pytest.mark.asyncio
async def test_tier_four_superfractor_badge(self):
"""current_tier=4 (Superfractor) should prefix title with [SF]."""
card = _make_card()
evo_state = {"current_tier": 4, "card_id": 1}
with patch("helpers.main.db_get", new_callable=AsyncMock) as mock_db:
mock_db.side_effect = _patch_db_get(evo_response=evo_state)
embeds = await _call_get_card_embeds(card)
assert embeds[0].title == "[SF] Mike Trout"
class TestTierBadgeInTitle:
"""Unit: badge appears correctly in the embed title."""
@pytest.mark.asyncio
async def test_badge_prepended_to_player_name(self):
"""Badge should be prepended so title reads '[Tx] <player_name>'."""
card = _make_card(player_name="Juan Soto")
evo_state = {"current_tier": 2, "card_id": 1}
with patch("helpers.main.db_get", new_callable=AsyncMock) as mock_db:
mock_db.side_effect = _patch_db_get(evo_response=evo_state)
embeds = await _call_get_card_embeds(card)
assert embeds[0].title.startswith("[R] ")
assert "Juan Soto" in embeds[0].title
class TestFullyEvolvedBadge:
"""Unit: fully evolved card shows [SF] badge (Superfractor)."""
@pytest.mark.asyncio
async def test_fully_evolved_badge(self):
"""T4 card should show [SF] prefix, not [T4]."""
card = _make_card()
evo_state = {"current_tier": 4}
with patch("helpers.main.db_get", new_callable=AsyncMock) as mock_db:
mock_db.side_effect = _patch_db_get(evo_response=evo_state)
embeds = await _call_get_card_embeds(card)
assert embeds[0].title.startswith("[SF] ")
assert "[T4]" not in embeds[0].title
class TestNoBadgeGracefulFallback:
"""Unit: embed renders correctly when evolution state is absent or API fails."""
@pytest.mark.asyncio
async def test_no_evolution_state_no_badge(self):
"""When evolution API returns None (404), title has no badge."""
card = _make_card()
with patch("helpers.main.db_get", new_callable=AsyncMock) as mock_db:
mock_db.side_effect = _patch_db_get(evo_response=None)
embeds = await _call_get_card_embeds(card)
assert embeds[0].title == "Mike Trout"
@pytest.mark.asyncio
async def test_api_exception_no_badge(self):
"""When evolution API raises an exception, card display is unaffected."""
card = _make_card()
async def _failing_db_get(endpoint, *args, **kwargs):
if str(endpoint).startswith("refractor/cards/"):
raise ConnectionError("API unreachable")
if endpoint == "paperdex":
return _make_paperdex()
return None
with patch("helpers.main.db_get", new_callable=AsyncMock) as mock_db:
mock_db.side_effect = _failing_db_get
embeds = await _call_get_card_embeds(card)
assert embeds[0].title == "Mike Trout"
class TestEmbedColorUnchanged:
"""Unit: embed color comes from card rarity, not affected by evolution state."""
@pytest.mark.asyncio
async def test_embed_color_from_rarity_with_evolution(self):
"""Color is still derived from rarity even when a tier badge is present."""
rarity_color = "FF0000"
card = _make_card(rarity_color=rarity_color)
evo_state = {"current_tier": 2}
with patch("helpers.main.db_get", new_callable=AsyncMock) as mock_db:
mock_db.side_effect = _patch_db_get(evo_response=evo_state)
embeds = await _call_get_card_embeds(card)
assert embeds[0].color == discord.Color(int(rarity_color, 16))
@pytest.mark.asyncio
async def test_embed_color_from_rarity_without_evolution(self):
"""Color is derived from rarity when no evolution state exists."""
rarity_color = "00FF00"
card = _make_card(rarity_color=rarity_color)
with patch("helpers.main.db_get", new_callable=AsyncMock) as mock_db:
mock_db.side_effect = _patch_db_get(evo_response=None)
embeds = await _call_get_card_embeds(card)
assert embeds[0].color == discord.Color(int(rarity_color, 16))
# ---------------------------------------------------------------------------
# T1-7: TIER_BADGES format consistency check across modules
# ---------------------------------------------------------------------------
class TestTierBadgesFormatConsistency:
"""
T1-7: Assert that TIER_BADGES in cogs.refractor (format: "[BC]") and
helpers.main (format: "BC") are consistent — wrapping the helpers.main
value in brackets must produce the cogs.refractor value.
Why: The two modules intentionally use different formats for different
rendering contexts:
- helpers.main uses bare strings ("BC") because get_card_embeds
wraps them in brackets when building the embed title.
- cogs.refractor uses bracket strings ("[BC]") because
format_refractor_entry inlines them directly into the display string.
If either definition is updated without updating the other, embed titles
and /refractor status output will display inconsistent badges. This test
acts as an explicit contract check so any future change to either dict
is immediately surfaced here.
"""
def test_cogs_badge_equals_bracketed_helpers_badge_for_all_tiers(self):
"""
For every tier in cogs.refractor TIER_BADGES, wrapping the
helpers.main TIER_BADGES value in square brackets must produce
the cogs.refractor value.
i.e., f"[{helpers_badge}]" == cog_badge for all tiers.
"""
from cogs.refractor import TIER_BADGES as cog_badges
from helpers.main import TIER_BADGES as helpers_badges
assert set(cog_badges.keys()) == set(helpers_badges.keys()), (
"TIER_BADGES key sets differ between cogs.refractor and helpers.main. "
f"cogs keys: {set(cog_badges.keys())}, helpers keys: {set(helpers_badges.keys())}"
)
for tier, cog_badge in cog_badges.items():
helpers_badge = helpers_badges[tier]
expected = f"[{helpers_badge}]"
assert cog_badge == expected, (
f"Tier {tier} badge mismatch: "
f"cogs.refractor={cog_badge!r}, "
f"helpers.main={helpers_badge!r} "
f"(expected cog badge to equal '[{helpers_badge}]')"
)
def test_t1_badge_relationship(self):
"""T1: helpers.main 'BC' wrapped in brackets equals cogs.refractor '[BC]'."""
from cogs.refractor import TIER_BADGES as cog_badges
from helpers.main import TIER_BADGES as helpers_badges
assert f"[{helpers_badges[1]}]" == cog_badges[1]
def test_t2_badge_relationship(self):
"""T2: helpers.main 'R' wrapped in brackets equals cogs.refractor '[R]'."""
from cogs.refractor import TIER_BADGES as cog_badges
from helpers.main import TIER_BADGES as helpers_badges
assert f"[{helpers_badges[2]}]" == cog_badges[2]
def test_t3_badge_relationship(self):
"""T3: helpers.main 'GR' wrapped in brackets equals cogs.refractor '[GR]'."""
from cogs.refractor import TIER_BADGES as cog_badges
from helpers.main import TIER_BADGES as helpers_badges
assert f"[{helpers_badges[3]}]" == cog_badges[3]
def test_t4_badge_relationship(self):
"""T4: helpers.main 'SF' wrapped in brackets equals cogs.refractor '[SF]'."""
from cogs.refractor import TIER_BADGES as cog_badges
from helpers.main import TIER_BADGES as helpers_badges
assert f"[{helpers_badges[4]}]" == cog_badges[4]
# ---------------------------------------------------------------------------
# Helper: call get_card_embeds and return embed list
# ---------------------------------------------------------------------------
async def _call_get_card_embeds(card):
"""Import and call get_card_embeds, returning the list of embeds."""
from helpers.main import get_card_embeds
result = await get_card_embeds(card)
if isinstance(result, list):
return result
return [result]