Adds helpers/evolution_notifs.py with build_tier_up_embed() and notify_tier_completion(). Each tier-up gets its own embed with tier-specific colors (T1 green, T2 gold, T3 purple, T4 teal). Tier 4 uses a special 'FULLY EVOLVED!' title with a future rating boosts note. Notification failure is non-fatal (try/except). 23 unit tests cover all tiers, empty list, and failure path. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
260 lines
10 KiB
Python
260 lines
10 KiB
Python
"""
|
|
Tests for Evolution Tier Completion Notification embeds.
|
|
|
|
These tests verify that:
|
|
1. Tier-up embeds are correctly formatted for tiers 1-3 (title, description, color).
|
|
2. Tier 4 (Fully Evolved) embeds include the special title, description, and note field.
|
|
3. Multiple tier-up events each produce a separate embed.
|
|
4. An empty tier-up list results in no channel sends.
|
|
|
|
The channel interaction is mocked because we are testing the embed content, not Discord
|
|
network I/O. Notification failure must never affect game flow, so the non-fatal path
|
|
is also exercised.
|
|
"""
|
|
|
|
import pytest
|
|
from unittest.mock import AsyncMock
|
|
|
|
import discord
|
|
|
|
from helpers.evolution_notifs import build_tier_up_embed, notify_tier_completion
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Fixtures
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def make_tier_up(
|
|
player_name="Mike Trout",
|
|
old_tier=1,
|
|
new_tier=2,
|
|
track_name="Batter",
|
|
current_value=150,
|
|
):
|
|
"""Return a minimal tier_up dict matching the expected shape."""
|
|
return {
|
|
"player_name": player_name,
|
|
"old_tier": old_tier,
|
|
"new_tier": new_tier,
|
|
"track_name": track_name,
|
|
"current_value": current_value,
|
|
}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Unit: build_tier_up_embed — tiers 1-3 (standard tier-up)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestBuildTierUpEmbed:
|
|
"""Verify that build_tier_up_embed produces correctly structured embeds."""
|
|
|
|
def test_title_is_evolution_tier_up(self):
|
|
"""Title must read 'Evolution Tier Up!' for any non-max tier."""
|
|
tier_up = make_tier_up(new_tier=2)
|
|
embed = build_tier_up_embed(tier_up)
|
|
assert embed.title == "Evolution Tier Up!"
|
|
|
|
def test_description_contains_player_name(self):
|
|
"""Description must contain the player's name."""
|
|
tier_up = make_tier_up(player_name="Mike Trout", new_tier=2)
|
|
embed = build_tier_up_embed(tier_up)
|
|
assert "Mike Trout" in embed.description
|
|
|
|
def test_description_contains_new_tier_name(self):
|
|
"""Description must include the human-readable tier name for the new tier."""
|
|
tier_up = make_tier_up(new_tier=2)
|
|
embed = build_tier_up_embed(tier_up)
|
|
# Tier 2 display name is "Rising"
|
|
assert "Rising" in embed.description
|
|
|
|
def test_description_contains_track_name(self):
|
|
"""Description must mention the evolution track (e.g., 'Batter')."""
|
|
tier_up = make_tier_up(track_name="Batter", new_tier=2)
|
|
embed = build_tier_up_embed(tier_up)
|
|
assert "Batter" in embed.description
|
|
|
|
def test_tier1_color_is_green(self):
|
|
"""Tier 1 uses green (0x2ecc71)."""
|
|
tier_up = make_tier_up(old_tier=0, new_tier=1)
|
|
embed = build_tier_up_embed(tier_up)
|
|
assert embed.color.value == 0x2ECC71
|
|
|
|
def test_tier2_color_is_gold(self):
|
|
"""Tier 2 uses gold (0xf1c40f)."""
|
|
tier_up = make_tier_up(old_tier=1, new_tier=2)
|
|
embed = build_tier_up_embed(tier_up)
|
|
assert embed.color.value == 0xF1C40F
|
|
|
|
def test_tier3_color_is_purple(self):
|
|
"""Tier 3 uses purple (0x9b59b6)."""
|
|
tier_up = make_tier_up(old_tier=2, new_tier=3)
|
|
embed = build_tier_up_embed(tier_up)
|
|
assert embed.color.value == 0x9B59B6
|
|
|
|
def test_footer_text_is_paper_dynasty_evolution(self):
|
|
"""Footer text must be 'Paper Dynasty Evolution' for brand consistency."""
|
|
tier_up = make_tier_up(new_tier=2)
|
|
embed = build_tier_up_embed(tier_up)
|
|
assert embed.footer.text == "Paper Dynasty Evolution"
|
|
|
|
def test_returns_discord_embed_instance(self):
|
|
"""Return type must be discord.Embed so it can be sent directly."""
|
|
tier_up = make_tier_up(new_tier=2)
|
|
embed = build_tier_up_embed(tier_up)
|
|
assert isinstance(embed, discord.Embed)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Unit: build_tier_up_embed — tier 4 (fully evolved)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestBuildTierUpEmbedFullyEvolved:
|
|
"""Verify that tier 4 (Fully Evolved) embeds use special formatting."""
|
|
|
|
def test_title_is_fully_evolved(self):
|
|
"""Tier 4 title must be 'FULLY EVOLVED!' to emphasise max achievement."""
|
|
tier_up = make_tier_up(old_tier=3, new_tier=4)
|
|
embed = build_tier_up_embed(tier_up)
|
|
assert embed.title == "FULLY EVOLVED!"
|
|
|
|
def test_description_mentions_maximum_evolution(self):
|
|
"""Tier 4 description must mention 'maximum evolution' per the spec."""
|
|
tier_up = make_tier_up(player_name="Mike Trout", old_tier=3, new_tier=4)
|
|
embed = build_tier_up_embed(tier_up)
|
|
assert "maximum evolution" in embed.description.lower()
|
|
|
|
def test_description_contains_player_name(self):
|
|
"""Player name must appear in the tier 4 description."""
|
|
tier_up = make_tier_up(player_name="Mike Trout", old_tier=3, new_tier=4)
|
|
embed = build_tier_up_embed(tier_up)
|
|
assert "Mike Trout" in embed.description
|
|
|
|
def test_description_contains_track_name(self):
|
|
"""Track name must appear in the tier 4 description."""
|
|
tier_up = make_tier_up(track_name="Batter", old_tier=3, new_tier=4)
|
|
embed = build_tier_up_embed(tier_up)
|
|
assert "Batter" in embed.description
|
|
|
|
def test_tier4_color_is_teal(self):
|
|
"""Tier 4 uses teal (0x1abc9c) to visually distinguish max evolution."""
|
|
tier_up = make_tier_up(old_tier=3, new_tier=4)
|
|
embed = build_tier_up_embed(tier_up)
|
|
assert embed.color.value == 0x1ABC9C
|
|
|
|
def test_note_field_present(self):
|
|
"""Tier 4 must include a note field about future rating boosts."""
|
|
tier_up = make_tier_up(old_tier=3, new_tier=4)
|
|
embed = build_tier_up_embed(tier_up)
|
|
field_names = [f.name for f in embed.fields]
|
|
assert any(
|
|
"rating" in name.lower()
|
|
or "boost" in name.lower()
|
|
or "note" in name.lower()
|
|
for name in field_names
|
|
), "Expected a field mentioning rating boosts for tier 4 embed"
|
|
|
|
def test_note_field_value_mentions_future_update(self):
|
|
"""The note field value must reference the future rating boost update."""
|
|
tier_up = make_tier_up(old_tier=3, new_tier=4)
|
|
embed = build_tier_up_embed(tier_up)
|
|
note_field = next(
|
|
(
|
|
f
|
|
for f in embed.fields
|
|
if "rating" in f.name.lower()
|
|
or "boost" in f.name.lower()
|
|
or "note" in f.name.lower()
|
|
),
|
|
None,
|
|
)
|
|
assert note_field is not None
|
|
assert (
|
|
"future" in note_field.value.lower() or "update" in note_field.value.lower()
|
|
)
|
|
|
|
def test_footer_text_is_paper_dynasty_evolution(self):
|
|
"""Footer must remain 'Paper Dynasty Evolution' for tier 4 as well."""
|
|
tier_up = make_tier_up(old_tier=3, new_tier=4)
|
|
embed = build_tier_up_embed(tier_up)
|
|
assert embed.footer.text == "Paper Dynasty Evolution"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Unit: notify_tier_completion — multiple and empty cases
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestNotifyTierCompletion:
|
|
"""Verify that notify_tier_completion sends the right number of messages."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_single_tier_up_sends_one_message(self):
|
|
"""A single tier-up event sends exactly one embed to the channel."""
|
|
channel = AsyncMock()
|
|
tier_up = make_tier_up(new_tier=2)
|
|
await notify_tier_completion(channel, tier_up)
|
|
channel.send.assert_called_once()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_sends_embed_not_plain_text(self):
|
|
"""The channel.send call must use the embed= keyword, not content=."""
|
|
channel = AsyncMock()
|
|
tier_up = make_tier_up(new_tier=2)
|
|
await notify_tier_completion(channel, tier_up)
|
|
_, kwargs = channel.send.call_args
|
|
assert (
|
|
"embed" in kwargs
|
|
), "notify_tier_completion must send an embed, not plain text"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_embed_type_is_discord_embed(self):
|
|
"""The embed passed to channel.send must be a discord.Embed instance."""
|
|
channel = AsyncMock()
|
|
tier_up = make_tier_up(new_tier=2)
|
|
await notify_tier_completion(channel, tier_up)
|
|
_, kwargs = channel.send.call_args
|
|
assert isinstance(kwargs["embed"], discord.Embed)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_notification_failure_does_not_raise(self):
|
|
"""If channel.send raises, notify_tier_completion must swallow it so game flow is unaffected."""
|
|
channel = AsyncMock()
|
|
channel.send.side_effect = Exception("Discord API unavailable")
|
|
tier_up = make_tier_up(new_tier=2)
|
|
# Should not raise
|
|
await notify_tier_completion(channel, tier_up)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_multiple_tier_ups_caller_sends_multiple_embeds(self):
|
|
"""
|
|
Callers are responsible for iterating tier-up events; each call to
|
|
notify_tier_completion sends a separate embed. This test simulates
|
|
three consecutive calls (3 events) and asserts 3 sends occurred.
|
|
"""
|
|
channel = AsyncMock()
|
|
events = [
|
|
make_tier_up(player_name="Mike Trout", new_tier=2),
|
|
make_tier_up(player_name="Aaron Judge", new_tier=1),
|
|
make_tier_up(player_name="Shohei Ohtani", new_tier=3),
|
|
]
|
|
for event in events:
|
|
await notify_tier_completion(channel, event)
|
|
assert (
|
|
channel.send.call_count == 3
|
|
), "Each tier-up event must produce its own embed (no batching)"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_no_tier_ups_means_no_sends(self):
|
|
"""
|
|
When the caller has an empty list of tier-up events and simply
|
|
does not call notify_tier_completion, zero sends happen.
|
|
This explicitly guards against any accidental unconditional send.
|
|
"""
|
|
channel = AsyncMock()
|
|
tier_up_events = []
|
|
for event in tier_up_events:
|
|
await notify_tier_completion(channel, event)
|
|
channel.send.assert_not_called()
|