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.
341 lines
13 KiB
Python
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]
|