diff --git a/CHANGELOG.md b/CHANGELOG.md index 54ae5c35..16c1db0a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## [Unreleased] +### Added + +- More emojis in the messages that SpellBot sends. + ## [v17.10.2](https://github.com/lexicalunit/spellbot/releases/tag/v17.10.2) - 2026-02-04 ### Changed diff --git a/src/spellbot/actions/lfg_action.py b/src/spellbot/actions/lfg_action.py index 7f136044..5297d232 100644 --- a/src/spellbot/actions/lfg_action.py +++ b/src/spellbot/actions/lfg_action.py @@ -633,9 +633,13 @@ def mythic_track_link(player_xid: int) -> str: async def notify_player(player_xid: int) -> None: embed = base_embed.copy() if pin := player_pins[player_xid]: + mt_emoji = "" + emojis = self.bot.emojis_cache + if emoji := next((e for e in emojis if e.name == "mythic_track"), None): + mt_emoji = f"{emoji} " embed.description = ( f"{embed.description}\n\n" - f"Track your game on [Mythic Track]({mythic_track_link(player_xid)}) " + f"Track your game on {mt_emoji}[Mythic Track]({mythic_track_link(player_xid)}) " f"with PIN code: `{pin}`" ) if player := await safe_fetch_user(self.bot, player_xid): diff --git a/src/spellbot/assets/emoji/convoke.png b/src/spellbot/assets/emoji/convoke.png new file mode 100644 index 00000000..fae62478 Binary files /dev/null and b/src/spellbot/assets/emoji/convoke.png differ diff --git a/src/spellbot/assets/emoji/girudo.png b/src/spellbot/assets/emoji/girudo.png new file mode 100644 index 00000000..cb495b94 Binary files /dev/null and b/src/spellbot/assets/emoji/girudo.png differ diff --git a/src/spellbot/assets/emoji/mythic_track.png b/src/spellbot/assets/emoji/mythic_track.png new file mode 100644 index 00000000..5998aa91 Binary files /dev/null and b/src/spellbot/assets/emoji/mythic_track.png differ diff --git a/src/spellbot/assets/emoji/spellbot.png b/src/spellbot/assets/emoji/spellbot.png new file mode 100644 index 00000000..3f1b8220 Binary files /dev/null and b/src/spellbot/assets/emoji/spellbot.png differ diff --git a/src/spellbot/assets/emoji/spellbot_creator.png b/src/spellbot/assets/emoji/spellbot_creator.png new file mode 100644 index 00000000..ca7bcb65 Binary files /dev/null and b/src/spellbot/assets/emoji/spellbot_creator.png differ diff --git a/src/spellbot/assets/emoji/spellbot_supporter.png b/src/spellbot/assets/emoji/spellbot_supporter.png new file mode 100644 index 00000000..43642b1f Binary files /dev/null and b/src/spellbot/assets/emoji/spellbot_supporter.png differ diff --git a/src/spellbot/assets/emoji/spelltable.png b/src/spellbot/assets/emoji/spelltable.png new file mode 100644 index 00000000..0c792903 Binary files /dev/null and b/src/spellbot/assets/emoji/spelltable.png differ diff --git a/src/spellbot/assets/emoji/table_stream.png b/src/spellbot/assets/emoji/table_stream.png new file mode 100644 index 00000000..4d312baf Binary files /dev/null and b/src/spellbot/assets/emoji/table_stream.png differ diff --git a/src/spellbot/client.py b/src/spellbot/client.py index 773871a9..78d651a0 100644 --- a/src/spellbot/client.py +++ b/src/spellbot/client.py @@ -3,6 +3,7 @@ import asyncio import logging from contextlib import asynccontextmanager +from pathlib import Path from typing import TYPE_CHECKING from unittest.mock import AsyncMock from uuid import uuid4 @@ -23,6 +24,8 @@ from .settings import settings from .utils import user_can_moderate +ASSETS_DIR = Path(__file__).resolve().parent / "assets" + if TYPE_CHECKING: from collections.abc import AsyncGenerator @@ -96,13 +99,9 @@ async def setup_hook(self) -> None: # pragma: no cover async def _create_application_emoji( self, name: str, - image_url: str, + image_bytes: bytes, ) -> discord.Emoji | None: try: - async with httpx.AsyncClient(follow_redirects=True) as client: - response = await client.get(image_url) - response.raise_for_status() - image_bytes = response.content return await self.create_application_emoji(name=name, image=image_bytes) except Exception: logger.exception("warning: could not create application emoji %s", name) @@ -128,21 +127,23 @@ async def fetch() -> list[discord.PartialEmoji | discord.Emoji]: async def ensure( emojis: list[discord.PartialEmoji | discord.Emoji], name: str, - image_url: str, + image_bytes: bytes, ) -> discord.PartialEmoji | discord.Emoji | None: for emoji in emojis: if emoji.name == name: return emoji - return await self._create_application_emoji(name, image_url) + return await self._create_application_emoji(name, image_bytes) try: emojis = await fetch() - created = await ensure(emojis, "spellbot_creator", settings.EMOJI_SPELLBOT_CREATOR) - if created and created not in emojis: - emojis.append(created) - created = await ensure(emojis, "spellbot_supporter", settings.EMOJI_SPELLBOT_SUPPORTER) - if created and created not in emojis: - emojis.append(created) + emoji_dir = ASSETS_DIR / "emoji" + emoji_files = list(emoji_dir.glob("*.png")) + for image_path in emoji_files: + name = image_path.stem + image_bytes = image_path.read_bytes() + created = await ensure(emojis, name, image_bytes) + if created and created not in emojis: + emojis.append(created) self.emojis_cache = emojis logger.info("cached %d application emojis", len(self.emojis_cache)) except Exception: diff --git a/src/spellbot/enums.py b/src/spellbot/enums.py index 125d0c2b..0a313359 100644 --- a/src/spellbot/enums.py +++ b/src/spellbot/enums.py @@ -45,7 +45,10 @@ def __str__(self) -> str: NOT_ANY = "Not any", "_Please contact the players in your game to organize this game._", None, 8 SPELLTABLE = ( "SpellTable", - "_A SpellTable link will be created when all players have joined._", + ( + "_A {emoji}[SpellTable](https://spelltable.wizards.com/) link will " + "be created when all players have joined._" + ), "https://spelltable.wizards.com/", 4, ) @@ -56,19 +59,28 @@ def __str__(self) -> str: TTS = "TabletopSim", "_Please use TabletopSim for this game._", None, 10 TABLE_STREAM = ( "Table Stream", - "_A Table Stream link will be created when all players have joined._", + ( + "_A {emoji}[Table Stream](https://table-stream.com/) link will " + "be created when all players have joined._" + ), "https://table-stream.com/", 6, ) CONVOKE = ( "Convoke", - "_A Convoke link will be created when all players have joined._", + ( + "_A {emoji}[Convoke](https://www.convoke.games/) link will " + "be created when all players have joined._" + ), "https://www.convoke.games/", 8, ) GIRUDO = ( "Girudo", - "_A Girudo link will be created when all players have joined._", + ( + "_A {emoji}[Girudo](https://www.girudo.com/) link will " + "be created when all players have joined._" + ), "https://www.girudo.com/", 4, ) diff --git a/src/spellbot/models/game.py b/src/spellbot/models/game.py index a4d4b99b..d22435d1 100644 --- a/src/spellbot/models/game.py +++ b/src/spellbot/models/game.py @@ -283,8 +283,16 @@ def embed_description_link_info( effective_service: GameService, dm: bool, rematch: bool, + emojis: list[discord.Emoji | discord.PartialEmoji] | None = None, ) -> str: if self.status != GameStatus.STARTED.value: + if "{emoji}" in effective_service.pending_msg: + emoji_str = "" + if emojis: + emoji_name = effective_service.name.lower().replace("-", "_").replace(" ", "_") + if emoji := next((e for e in emojis if e.name == emoji_name), None): + emoji_str = f"{emoji} " + return effective_service.pending_msg.format(emoji=emoji_str) return effective_service.pending_msg if not self.show_links(dm): return "Please check your Direct Messages for your game details." @@ -347,6 +355,7 @@ def embed_description( dm: bool = False, suggested_vc: VoiceChannelSuggestion | None = None, rematch: bool = False, + emojis: list[discord.Emoji | discord.PartialEmoji] | None = None, ) -> str: if span := tracer.current_span(): # pragma: no cover span.set_tags( @@ -362,7 +371,7 @@ def embed_description( parts: list[str] = [] if self.guild.notice: parts.append(f"{self.guild.notice}") - parts.append(self.embed_description_link_info(effective_service, dm, rematch)) + parts.append(self.embed_description_link_info(effective_service, dm, rematch, emojis)) parts.extend(self.embed_description_extras(dm, suggested_vc)) parts.extend(self.embed_motd()) return "\n\n".join(parts) @@ -462,6 +471,7 @@ def to_embed( dm=dm, suggested_vc=suggested_vc, rematch=rematch, + emojis=emojis, ) if self.rules: embed.add_field(name="⚠️ Additional Rules:", value=self.rules, inline=False) diff --git a/src/spellbot/settings.py b/src/spellbot/settings.py index 2a030169..998a698e 100644 --- a/src/spellbot/settings.py +++ b/src/spellbot/settings.py @@ -29,8 +29,6 @@ class Settings: "DD_TRACE_ENABLED", "DEBUG_GUILD", "DONATE_LINK", - "EMOJI_SPELLBOT_CREATOR", - "EMOJI_SPELLBOT_SUPPORTER", "EMPTY_EMBED_COLOR", "EXPIRE_GAMES_LOOP_M", "EXPIRE_TIME_M", @@ -96,14 +94,6 @@ def __init__(self, guild_xid: int | None = None) -> None: # noqa: PLR0915 self.DEBUG_GUILD = getenv("DEBUG_GUILD") self.API_BASE_URL = getenv("API_BASE_URL", "https://bot.spellbot.io") self.OWNER_XID = getenv("OWNER_XID") - self.EMOJI_SPELLBOT_CREATOR = getenv( - "EMOJI_SPELLBOT_CREATOR", - "https://github.com/user-attachments/assets/beaabff5-dc19-40ee-a86e-40cd4b387fc4", - ) - self.EMOJI_SPELLBOT_SUPPORTER = getenv( - "EMOJI_SPELLBOT_SUPPORTER", - "https://github.com/user-attachments/assets/ec1045a4-4d81-412b-81fe-ea1f414a5e32", - ) # datadog self.DD_API_KEY = getenv("DD_API_KEY") diff --git a/tests/cogs/test_admin_cog.py b/tests/cogs/test_admin_cog.py index f76b9be5..3ba39d10 100644 --- a/tests/cogs/test_admin_cog.py +++ b/tests/cogs/test_admin_cog.py @@ -29,6 +29,11 @@ pytestmark = pytest.mark.use_db +SPELLTABLE_PENDING_MSG = ( + "_A [SpellTable](https://spelltable.wizards.com/) link will " + "be created when all players have joined._" +) + @pytest_asyncio.fixture async def cog(bot: SpellBot) -> AdminCog: @@ -355,8 +360,7 @@ async def test_happy_path( assert get_last_send_message(interaction, "embed") == { "color": settings.EMPTY_EMBED_COLOR, "description": ( - "_A SpellTable link will be created when all players have joined._\n\n" - f"{game.guild.motd}\n\n{game.channel.motd}" + f"{SPELLTABLE_PENDING_MSG}\n\n{game.guild.motd}\n\n{game.channel.motd}" ), "fields": [ {"inline": True, "name": "Format", "value": "Commander"}, diff --git a/tests/cogs/test_leave_cog.py b/tests/cogs/test_leave_cog.py index 60bffbbe..00a3bd9d 100644 --- a/tests/cogs/test_leave_cog.py +++ b/tests/cogs/test_leave_cog.py @@ -23,6 +23,11 @@ pytestmark = pytest.mark.use_db +SPELLTABLE_PENDING_MSG = ( + "_A [SpellTable](https://spelltable.wizards.com/) link will " + "be created when all players have joined._" +) + @pytest_asyncio.fixture async def cog(bot: SpellBot) -> LeaveGameCog: @@ -62,11 +67,7 @@ async def test_leave( safe_update_embed_call = leave_action.safe_update_embed.call_args_list[0] assert safe_update_embed_call.kwargs["embed"].to_dict() == { "color": settings.PENDING_EMBED_COLOR, - "description": ( - "_A SpellTable link will be created when all players have joined._\n" - "\n" - f"{guild.motd}\n\n{channel.motd}" - ), + "description": (f"{SPELLTABLE_PENDING_MSG}\n\n{guild.motd}\n\n{channel.motd}"), "fields": [ {"inline": False, "name": "Players", "value": f"• <@{p2.xid}> ({p2.name})"}, {"inline": True, "name": "Format", "value": "Commander"}, @@ -133,11 +134,7 @@ async def test_leave_all( safe_update_embed_call = leave_action.safe_update_embed.call_args_list[0] assert safe_update_embed_call.kwargs["embed"].to_dict() == { "color": settings.PENDING_EMBED_COLOR, - "description": ( - "_A SpellTable link will be created when all players have joined._\n" - "\n" - f"{guild.motd}\n\n{channel.motd}" - ), + "description": (f"{SPELLTABLE_PENDING_MSG}\n\n{guild.motd}\n\n{channel.motd}"), "fields": [ {"inline": False, "name": "Players", "value": f"• <@{p2.xid}> ({p2.name})"}, {"inline": True, "name": "Format", "value": "Commander"}, @@ -340,11 +337,7 @@ async def test_leave_button( safe_update_embed_origin_call = leave_action.safe_update_embed_origin.call_args_list[0] assert safe_update_embed_origin_call.kwargs["embed"].to_dict() == { "color": settings.PENDING_EMBED_COLOR, - "description": ( - "_A SpellTable link will be created when all players have joined._\n" - "\n" - f"{guild.motd}\n\n{channel.motd}" - ), + "description": (f"{SPELLTABLE_PENDING_MSG}\n\n{guild.motd}\n\n{channel.motd}"), "fields": [ {"inline": False, "name": "Players", "value": f"• <@{p2.xid}> ({p2.name})"}, {"inline": True, "name": "Format", "value": "Commander"}, @@ -406,11 +399,7 @@ async def test_leave_button_with_other_players( safe_update_embed_origin_call = leave_action.safe_update_embed_origin.call_args_list[0] assert safe_update_embed_origin_call.kwargs["embed"].to_dict() == { "color": settings.PENDING_EMBED_COLOR, - "description": ( - "_A SpellTable link will be created when all players have joined._\n" - "\n" - f"{guild.motd}\n\n{channel.motd}" - ), + "description": (f"{SPELLTABLE_PENDING_MSG}\n\n{guild.motd}\n\n{channel.motd}"), "fields": [ {"inline": False, "name": "Players", "value": f"• <@{p2.xid}> ({p2.name})"}, {"inline": True, "name": "Format", "value": "Commander"}, diff --git a/tests/cogs/test_lfg_cog.py b/tests/cogs/test_lfg_cog.py index ffec2881..303594f8 100644 --- a/tests/cogs/test_lfg_cog.py +++ b/tests/cogs/test_lfg_cog.py @@ -24,6 +24,11 @@ pytestmark = pytest.mark.use_db +SPELLTABLE_PENDING_MSG = ( + "_A [SpellTable](https://spelltable.wizards.com/) link will " + "be created when all players have joined._" +) + @pytest.fixture def cog(bot: SpellBot) -> LookingForGameCog: @@ -393,11 +398,7 @@ async def test_join( mock_call.assert_called_once() assert mock_call.call_args_list[0].kwargs["embed"].to_dict() == { "color": settings.PENDING_EMBED_COLOR, - "description": ( - "_A SpellTable link will be created when all players have joined._\n" - f"\n{guild.motd}\n" - f"\n{channel.motd}" - ), + "description": (f"{SPELLTABLE_PENDING_MSG}\n\n{guild.motd}\n\n{channel.motd}"), "fields": [ { "inline": False, diff --git a/tests/models/test_game.py b/tests/models/test_game.py index d44f1742..68b6013b 100644 --- a/tests/models/test_game.py +++ b/tests/models/test_game.py @@ -19,6 +19,11 @@ pytestmark = pytest.mark.use_db +SPELLTABLE_PENDING_MSG = ( + "_A [SpellTable](https://spelltable.wizards.com/) link will " + "be created when all players have joined._" +) + class TestModelGame: def test_game_to_dict(self, factories: Factories) -> None: @@ -67,7 +72,10 @@ def test_game_show_links(self, factories: Factories) -> None: [ pytest.param( GameService.SPELLTABLE, - "_A SpellTable link will be created when all players have joined._", + ( + "_A [SpellTable](https://spelltable.wizards.com/) link will " + "be created when all players have joined._" + ), id="spelltable", ), pytest.param( @@ -77,12 +85,18 @@ def test_game_show_links(self, factories: Factories) -> None: ), pytest.param( GameService.TABLE_STREAM, - "_A Table Stream link will be created when all players have joined._", + ( + "_A [Table Stream](https://table-stream.com/) link will " + "be created when all players have joined._" + ), id="table-stream", ), pytest.param( GameService.CONVOKE, - "_A Convoke link will be created when all players have joined._", + ( + "_A [Convoke](https://www.convoke.games/) link will " + "be created when all players have joined._" + ), id="convoke", ), pytest.param( @@ -138,7 +152,7 @@ def test_game_embed_pending(self, settings: Settings, factories: Factories) -> N assert game.to_embed().to_dict() == { "color": settings.PENDING_EMBED_COLOR, - "description": ("_A SpellTable link will be created when all players have joined._"), + "description": SPELLTABLE_PENDING_MSG, "fields": [ {"inline": False, "name": "Players", "value": f"• <@{player.xid}> ({player.name})"}, {"inline": True, "name": "Format", "value": "Commander"}, @@ -154,6 +168,44 @@ def test_game_embed_pending(self, settings: Settings, factories: Factories) -> N "flags": 0, } + def test_game_embed_pending_with_emoji( + self, + settings: Settings, + factories: Factories, + ) -> None: + guild = factories.guild.create(motd=None) + channel = factories.channel.create(guild=guild, motd=None) + game = factories.game.create(guild=guild, channel=channel) + factories.user.create(game=game) + + spelltable_emoji = MagicMock(spec=discord.Emoji) + spelltable_emoji.name = "spelltable" + emojis = [spelltable_emoji] + + embed = game.to_embed(emojis=emojis) + assert f"{spelltable_emoji}" in embed.description + assert "[SpellTable](https://spelltable.wizards.com/)" in embed.description + + def test_game_embed_pending_with_emoji_no_match( + self, + settings: Settings, + factories: Factories, + ) -> None: + guild = factories.guild.create(motd=None) + channel = factories.channel.create(guild=guild, motd=None) + game = factories.game.create(guild=guild, channel=channel) + factories.user.create(game=game) + + # Emoji with a name that doesn't match the service + other_emoji = MagicMock(spec=discord.Emoji) + other_emoji.name = "some_other_emoji" + emojis = [other_emoji] + + embed = game.to_embed(emojis=emojis) + # Should not contain the emoji since it doesn't match + assert f"{other_emoji}" not in embed.description + assert "[SpellTable](https://spelltable.wizards.com/)" in embed.description + def test_game_embed_pending_with_blind(self, settings: Settings, factories: Factories) -> None: guild = factories.guild.create(motd=None) channel = factories.channel.create(guild=guild, motd=None) @@ -162,7 +214,7 @@ def test_game_embed_pending_with_blind(self, settings: Settings, factories: Fact assert game.to_embed().to_dict() == { "color": settings.PENDING_EMBED_COLOR, - "description": ("_A SpellTable link will be created when all players have joined._"), + "description": SPELLTABLE_PENDING_MSG, "fields": [ {"inline": False, "name": "Players", "value": "**1 player name is hidden**"}, {"inline": True, "name": "Format", "value": "Commander"}, @@ -191,7 +243,7 @@ def test_game_embed_pending_with_blind_multiple_players( assert game.to_embed().to_dict() == { "color": settings.PENDING_EMBED_COLOR, - "description": ("_A SpellTable link will be created when all players have joined._"), + "description": SPELLTABLE_PENDING_MSG, "fields": [ {"inline": False, "name": "Players", "value": "**2 player names are hidden**"}, {"inline": True, "name": "Format", "value": "Commander"}, @@ -219,7 +271,7 @@ def test_game_embed_pending_with_blind_dm( assert game.to_embed(dm=True).to_dict() == { "color": settings.PENDING_EMBED_COLOR, - "description": ("_A SpellTable link will be created when all players have joined._"), + "description": SPELLTABLE_PENDING_MSG, "fields": [ {"inline": False, "name": "Players", "value": f"• <@{player.xid}> ({player.name})"}, {"inline": True, "name": "Format", "value": "Commander"}, @@ -243,7 +295,7 @@ def test_game_embed_with_rules(self, settings: Settings, factories: Factories) - assert game.to_embed().to_dict() == { "color": settings.PENDING_EMBED_COLOR, - "description": ("_A SpellTable link will be created when all players have joined._"), + "description": SPELLTABLE_PENDING_MSG, "fields": [ {"inline": False, "name": "⚠️ Additional Rules:", "value": "test rules"}, {"inline": False, "name": "Players", "value": f"• <@{player.xid}> ({player.name})"}, @@ -269,9 +321,7 @@ def test_game_embed_placeholders(self, settings: Settings, factories: Factories) assert game.to_embed().to_dict() == { "color": settings.PENDING_EMBED_COLOR, "description": ( - "_A SpellTable link will be created when all players have joined._\n\n" - f"player 1: {player.name}\n\n" - f"game id: {game.id}" + f"{SPELLTABLE_PENDING_MSG}\n\nplayer 1: {player.name}\n\ngame id: {game.id}" ), "fields": [ {"inline": False, "name": "Players", "value": f"• <@{player.xid}> ({player.name})"}, diff --git a/tests/test_bot.py b/tests/test_bot.py index 1f6eeb95..33096786 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -10,6 +10,7 @@ from discord.ext.commands import AutoShardedBot, CommandNotFound, Context, UserInputError from spellbot import SpellBot +from spellbot.client import ASSETS_DIR from spellbot.database import DatabaseSession from spellbot.enums import GameService from spellbot.errors import ( @@ -584,17 +585,6 @@ async def test_create_application_emoji_success( mocker: MockerFixture, ) -> None: """Test successful creation of application emoji.""" - mock_response = MagicMock() - mock_response.content = b"fake_image_bytes" - mock_response.raise_for_status = MagicMock() - - mock_client = AsyncMock() - mock_client.get = AsyncMock(return_value=mock_response) - mock_client.__aenter__ = AsyncMock(return_value=mock_client) - mock_client.__aexit__ = AsyncMock(return_value=None) - - mocker.patch("spellbot.client.httpx.AsyncClient", return_value=mock_client) - mock_emoji = MagicMock(spec=discord.Emoji) create_emoji_stub = mocker.patch.object( bot, @@ -602,7 +592,7 @@ async def test_create_application_emoji_success( AsyncMock(return_value=mock_emoji), ) - result = await bot._create_application_emoji("test_emoji", "https://example.com/emoji.png") + result = await bot._create_application_emoji("test_emoji", b"fake_image_bytes") assert result == mock_emoji create_emoji_stub.assert_called_once_with( @@ -616,14 +606,13 @@ async def test_create_application_emoji_exception( mocker: MockerFixture, ) -> None: """Test exception handling in create_application_emoji.""" - mock_client = AsyncMock() - mock_client.get = AsyncMock(side_effect=Exception("Network error")) - mock_client.__aenter__ = AsyncMock(return_value=mock_client) - mock_client.__aexit__ = AsyncMock(return_value=None) - - mocker.patch("spellbot.client.httpx.AsyncClient", return_value=mock_client) + mocker.patch.object( + bot, + "create_application_emoji", + AsyncMock(side_effect=Exception("Discord API error")), + ) - result = await bot._create_application_emoji("test_emoji", "https://example.com/emoji.png") + result = await bot._create_application_emoji("test_emoji", b"fake_image_bytes") assert result is None @@ -632,15 +621,16 @@ async def test_ensure_application_emojis_success( bot: SpellBot, mocker: MockerFixture, ) -> None: - """Test successful fetching and caching of application emojis.""" + """Test successful fetching and caching of application emojis when all emojis exist.""" + emoji_dir = ASSETS_DIR / "emoji" + emoji_files = list(emoji_dir.glob("*.png")) + emoji_names = [f.stem for f in emoji_files] + mock_response = MagicMock() mock_response.raise_for_status = MagicMock() mock_response.json = MagicMock( return_value={ - "items": [ - {"name": "spellbot_creator", "id": "123456"}, - {"name": "spellbot_supporter", "id": "789012"}, - ], + "items": [{"name": name, "id": str(i)} for i, name in enumerate(emoji_names)], }, ) @@ -653,9 +643,7 @@ async def test_ensure_application_emojis_success( await bot._ensure_application_emojis() - assert len(bot.emojis_cache) == 2 - assert bot.emojis_cache[0].name == "spellbot_creator" - assert bot.emojis_cache[1].name == "spellbot_supporter" + assert len(bot.emojis_cache) == len(emoji_names) async def test_ensure_application_emojis_creates_missing( self, @@ -663,6 +651,10 @@ async def test_ensure_application_emojis_creates_missing( mocker: MockerFixture, ) -> None: """Test that missing emojis are created.""" + emoji_dir = ASSETS_DIR / "emoji" + emoji_files = list(emoji_dir.glob("*.png")) + num_emojis = len(emoji_files) + mock_response = MagicMock() mock_response.raise_for_status = MagicMock() mock_response.json = MagicMock(return_value={"items": []}) # No existing emojis @@ -674,24 +666,25 @@ async def test_ensure_application_emojis_creates_missing( mocker.patch("spellbot.client.httpx.AsyncClient", return_value=mock_client) - # Create two different emoji objects so both append() lines are executed - mock_creator_emoji = MagicMock(spec=discord.Emoji) - mock_creator_emoji.name = "spellbot_creator" - mock_supporter_emoji = MagicMock(spec=discord.Emoji) - mock_supporter_emoji.name = "spellbot_supporter" + # Create mock emoji objects for each file + mock_emojis = [] + for f in emoji_files: + mock_emoji = MagicMock(spec=discord.Emoji) + mock_emoji.name = f.stem + mock_emojis.append(mock_emoji) create_stub = mocker.patch.object( bot, "_create_application_emoji", - AsyncMock(side_effect=[mock_creator_emoji, mock_supporter_emoji]), + AsyncMock(side_effect=mock_emojis), ) await bot._ensure_application_emojis() - # Should have called _create_application_emoji twice (for creator and supporter) - assert create_stub.call_count == 2 - # Both emojis should be in the cache - assert len(bot.emojis_cache) == 2 + # Should have called _create_application_emoji for each emoji file + assert create_stub.call_count == num_emojis + # All emojis should be in the cache + assert len(bot.emojis_cache) == num_emojis async def test_ensure_application_emojis_exception( self,