diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e9ef7c0..330b79c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,7 +35,10 @@ jobs: - name: Poetry Config run: poetry config virtualenvs.in-project true - - uses: actions/cache@v2 + - name: Install system dependencies + run: sudo apt-get update && sudo apt-get install -y libjpeg-dev zlib1g-dev + + - uses: actions/cache@v4 with: path: .venv key: poetry-${{ hashFiles('**/poetry.lock') }} @@ -50,3 +53,6 @@ jobs: - name: Type-check (as warnings only) run: poetry run python -m mypy . || true + + - name: Run tests + run: poetry run pytest -q diff --git a/bot/bot_client.py b/bot/bot_client.py index 1976e05..ef21d54 100644 --- a/bot/bot_client.py +++ b/bot/bot_client.py @@ -251,6 +251,12 @@ async def load_all_extensions(self): self.disabled_commands() + try: + synced = await self.tree.sync() + print(f"- Synced {len(synced)} application command(s).") + except Exception as e: + print(f"Failed to sync application commands: {e}") + return errored async def reload_all_extensions(self): diff --git a/bot/cogs/amigosecreto.py b/bot/cogs/amigosecreto.py index c6aa423..b98c4ae 100644 --- a/bot/cogs/amigosecreto.py +++ b/bot/cogs/amigosecreto.py @@ -170,11 +170,17 @@ async def roll_amigo_secreto(self, ctx: Context): not_receiving = AmigoSecretoPerson.objects.filter(receiving=False).count() - if not_receiving > 0: - await ctx.author.send(f'Erro ao montar Amigo Secreto. Pessoas sem receber: {not_receiving}. Contate NRiver.') - return - - await ctx.author.send(f'Amigo Secreto montado com sucesso. Total de pessoas: {AmigoSecretoPerson.objects.count()}') + if not_receiving > 0: + await ctx.author.send( + f"Erro ao montar Amigo Secreto. Pessoas sem receber: {not_receiving}. " + "Contate NRiver." + ) + return + + await ctx.author.send( + "Amigo Secreto montado com sucesso. Total de pessoas: " + f"{AmigoSecretoPerson.objects.count()}" + ) @commands.check(is_pedim_or_nriver) @commands.command() @@ -205,9 +211,10 @@ async def activate_amigo_secreto(self, ctx: Context): start_date_brt = await ctx.prompt( "Digite a data de inicio do Amigo Secreto (exemplo '20/12/2025 20:00'):" ) - end_date_brt = await ctx.prompt( - "Digite a data de fim do Amigo Secreto (exemplo '30/12/2025 20:00'). A data do sorteio será 2 dias antes da data final informada:" - ) + end_date_brt = await ctx.prompt( + "Digite a data de fim do Amigo Secreto (exemplo '30/12/2025 20:00'). " + "A data do sorteio será 2 dias antes da data final informada:" + ) start_date_utc = datetime.strptime(start_date_brt, "%d/%m/%Y %H:%M") - timedelta(hours=3) end_date_utc = datetime.strptime(end_date_brt, "%d/%m/%Y %H:%M") - timedelta(hours=3) @@ -237,7 +244,10 @@ async def delete_amigo_secreto(self, ctx: Context): if not confirmation: return await ctx.send("Comando cancelado") - return await ctx.send(f"Todas as inscrições do Amigo Secreto do Atlantis foram deletadas. Total de pessoas inscritas: {total_count}") + return await ctx.send( + "Todas as inscrições do Amigo Secreto do Atlantis foram deletadas. " + f"Total de pessoas inscritas: {total_count}" + ) @commands.check(is_authenticated) @commands.command(aliases=["amigosecreto", "amigo"]) diff --git a/bot/cogs/chat.py b/bot/cogs/chat.py index dabd79f..4b7c4bc 100644 --- a/bot/cogs/chat.py +++ b/bot/cogs/chat.py @@ -314,12 +314,14 @@ async def github(self, ctx: Context): github_embed.set_thumbnail(url=github_icon) return await ctx.send(content=None, embed=github_embed) - # @commands.bot_has_permissions(embed_links=True) - @commands.command(aliases=["atlbot", "atlbotcommands"]) - async def atlcommands(self, ctx: Context): - runepixels_url = ( - f"https://runepixels.com/clans/{self.bot.setting.clan_name}/about" - ) + # @commands.bot_has_permissions(embed_links=True) + @commands.hybrid_command( + name="atlantisbot", aliases=["atlbot", "atlbotcommands", "atlcommands"] + ) + async def atlantisbot(self, ctx: Context): + runepixels_url = ( + f"https://runepixels.com/clans/{self.bot.setting.clan_name}/about" + ) clan_banner = f"http://services.runescape.com/m=avatar-rs/l=3/a=869/{self.bot.setting.clan_name}/clanmotif.png" embed_title = "RunePixels" diff --git a/poetry.lock b/poetry.lock index a784764..95a66ca 100644 --- a/poetry.lock +++ b/poetry.lock @@ -536,6 +536,15 @@ files = [ django = ">=2.2" pytz = "*" +[[package]] +name = "exceptiongroup" +version = "1.1.3" +description = "Backport of PEP 654 (exception groups)" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [] + [[package]] name = "feedparser" version = "6.0.10" @@ -684,6 +693,15 @@ files = [ {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, ] +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [] + [[package]] name = "kiwisolver" version = "1.4.4" @@ -1294,6 +1312,15 @@ files = [ docs = ["furo (>=2023.3.27)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"] test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.3.1)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"] +[[package]] +name = "pluggy" +version = "1.2.0" +description = "plugin and hook calling mechanisms for python" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [] + [[package]] name = "poetry-version" version = "0.1.5" @@ -1443,6 +1470,22 @@ future = "*" requests = "*" requests-oauthlib = "*" +[[package]] +name = "pytest" +version = "7.4.0" +description = "pytest: simple powerful testing with Python" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [] + +[package.dependencies] +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" +tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} + [[package]] name = "pytz" version = "2023.3" @@ -1788,4 +1831,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "c435dccc3470036d500ae157664ed6444401f1d77cd52734bd72671b27374750" +content-hash = "848379345137931d2d32cb2da98ad1e430d2c2d3b455d15a4af4b5242f7e6df8" diff --git a/pyproject.toml b/pyproject.toml index b40d47f..cebdeef 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,6 +31,7 @@ ruff = "^0.0.265" mypy = "^1.2.0" mypy_extensions = "^1.0.0" black = "^23.3.0" +pytest = "^7.4.0" [build-system] requires = ["poetry-core>=1.0.0"] diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..7ed37ac --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +"""Test package for AtlantisBot.""" diff --git a/tests/helpers.py b/tests/helpers.py new file mode 100644 index 0000000..71ca65d --- /dev/null +++ b/tests/helpers.py @@ -0,0 +1,105 @@ +from __future__ import annotations + +import sys +import types +from dataclasses import dataclass +from pathlib import Path +from types import SimpleNamespace +from typing import Any + + +ROOT = Path(__file__).resolve().parents[1] +if str(ROOT) not in sys.path: + sys.path.append(str(ROOT)) + + +def _stub_modules(): + stub_rs3clans = types.ModuleType("rs3clans") + stub_rs3clans.Clan = object + stub_rs3clans.Player = object + sys.modules["rs3clans"] = stub_rs3clans + + stub_bot_client = types.ModuleType("bot.bot_client") + + class Bot: + pass + + stub_bot_client.Bot = Bot + sys.modules["bot.bot_client"] = stub_bot_client + + stub_tools = types.ModuleType("bot.utils.tools") + stub_tools.right_arrow = "->" + + def has_any_role(*_args, **_kwargs): + return False + + stub_tools.has_any_role = has_any_role + sys.modules["bot.utils.tools"] = stub_tools + + stub_checks = types.ModuleType("bot.utils.checks") + + async def is_authenticated(ctx): + return True + + async def is_admin(ctx): + return True + + stub_checks.is_authenticated = is_authenticated + stub_checks.is_admin = is_admin + sys.modules["bot.utils.checks"] = stub_checks + + stub_context = types.ModuleType("bot.utils.context") + + class Context: + pass + + stub_context.Context = Context + sys.modules["bot.utils.context"] = stub_context + + stub_raids = types.ModuleType("bot.cogs.raids") + + async def time_till_raids(): + return 0, 0, 0 + + stub_raids.time_till_raids = time_till_raids + sys.modules["bot.cogs.raids"] = stub_raids + + +@dataclass +class FakeSettings: + clan_name: str = "Atlantis" + banner_image: str = "https://example.com/banner.png" + prefix: str = "!" + role: dict[str, int] | None = None + chat: dict[str, int] | None = None + + def __post_init__(self): + if self.role is None: + self.role = {"pvm_teacher": 123, "aod": 456, "aod_learner": 789} + if self.chat is None: + self.chat = {"aod": 321} + + +class FakeContext: + def __init__(self): + self.sent_embed = None + self.sent_content = None + self.author = SimpleNamespace(roles=[]) + + async def send(self, content=None, *, embed=None): + self.sent_embed = embed + self.sent_content = content + + +def make_chat(): + _stub_modules() + from bot.cogs.chat import Chat # noqa: E402 + + fake_bot = SimpleNamespace(setting=FakeSettings()) + return Chat(fake_bot) + + +def run_async(coro: Any): + import asyncio + + return asyncio.run(coro) diff --git a/tests/test_aplicar_aod_command.py b/tests/test_aplicar_aod_command.py new file mode 100644 index 0000000..aff9c91 --- /dev/null +++ b/tests/test_aplicar_aod_command.py @@ -0,0 +1,12 @@ +from tests.helpers import FakeContext, make_chat, run_async + + +def test_aplicar_aod_sends_text_message(): + chat = make_chat() + ctx = FakeContext() + + run_async(chat.aplicar_aod.callback(chat, ctx)) + + assert ctx.sent_embed is None + assert ctx.sent_content is not None + assert "Você aplicou para receber a tag de AoD" in ctx.sent_content diff --git a/tests/test_atlantisbot_command.py b/tests/test_atlantisbot_command.py new file mode 100644 index 0000000..54758fc --- /dev/null +++ b/tests/test_atlantisbot_command.py @@ -0,0 +1,20 @@ +from discord.ext import commands + +from tests.helpers import FakeContext, make_chat, run_async + + +def test_atlantisbot_is_hybrid_command(): + chat = make_chat() + ctx = FakeContext() + + command = chat.atlantisbot + assert isinstance(command, commands.HybridCommand) + assert command.name == "atlantisbot" + assert "atlbot" in command.aliases + + run_async(command.callback(chat, ctx)) + + assert ctx.sent_content is None + assert ctx.sent_embed is not None + assert ctx.sent_embed.title == "RunePixels" + assert any(field.name.startswith("!claninfo") for field in ctx.sent_embed.fields)